From b2559daa94ca315e1108afff26ea834e3cbaf81c Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 24 Aug 2026 17:53:26 +0200 Subject: [PATCH] feat(core): add base entity transform and physics integration --- core/include/vf/Entity.h | 48 ++++++++++++++++ core/src/Entity.cpp | 116 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 core/include/vf/Entity.h create mode 100644 core/src/Entity.cpp diff --git a/core/include/vf/Entity.h b/core/include/vf/Entity.h new file mode 100644 index 0000000..236adb9 --- /dev/null +++ b/core/include/vf/Entity.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include "World.h" +#include "Math.h" +#include "Physics.h" +#include "Pathfind.h" + +namespace vf { + +enum class MobState : uint8_t { + Wander = 0, + Chase = 1, +}; + +struct Mob { + Vec3 position; // feet centre (bottom of the collision box) + Vec3 velocity; + float width = 0.6f; + float height = 1.8f; + bool onGround = false; + MobState state = MobState::Wander; + uint32_t rng = 0x2545f491u; // deterministic wander stream + Vec3 wanderDir; +}; + +struct MobConfig { + float moveSpeed = 3.2f; + float gravity = 26.0f; + float maxFallSpeed = 55.0f; + float chaseRadius = 12.0f; // start chasing inside this range + float loseRadius = 16.0f; // give up chasing beyond this (hysteresis) + PathConfig path; +}; + +Aabb mobBox(const Mob& mob); +Vec3i mobFeetCell(const Mob& mob); + +// One physics tick: applies the horizontal wish direction and gravity, then +// resolves AABB collisions against solid voxels axis by axis (move and slide). +void stepMob(const World& world, Mob& mob, const Vec3& wishDir, const MobConfig& config, float dt); + +// State machine driving the mob. Chases when the target is within range and a +// path exists, otherwise wanders. Updates mob.state and returns the horizontal +// wish direction to hand to stepMob. +Vec3 updateMobBrain(const World& world, Mob& mob, const Vec3& targetPos, const MobConfig& config); + +} // namespace vf diff --git a/core/src/Entity.cpp b/core/src/Entity.cpp new file mode 100644 index 0000000..7ecceb9 --- /dev/null +++ b/core/src/Entity.cpp @@ -0,0 +1,116 @@ +#include "vf/Entity.h" + +#include + +namespace vf { + +namespace { + +constexpr float epsilon = 1e-4f; + +float& axis(Vec3& v, int i) { return (&v.x)[i]; } + +Aabb boxAt(const Mob& mob, const Vec3& position) { + float half = mob.width * 0.5f; + return Aabb{ + {position.x - half, position.y, position.z - half}, + {position.x + half, position.y + mob.height, position.z + half}, + }; +} + +bool moveAxis(const World& world, Mob& mob, int a, float disp) { + axis(mob.position, a) += disp; + Aabb box = boxAt(mob, mob.position); + if (!boxOverlapsSolid(world, box)) return false; + + if (disp > 0.0f) { + float snapped = std::floor(axis(box.max, a)); + axis(mob.position, a) -= (axis(box.max, a) - snapped) + epsilon; + } else if (disp < 0.0f) { + float snapped = std::ceil(axis(box.min, a)); + axis(mob.position, a) += (snapped - axis(box.min, a)) + epsilon; + } + return true; +} + +float horizontalDistance(const Vec3& a, const Vec3& b) { + float dx = a.x - b.x, dz = a.z - b.z; + return std::sqrt(dx * dx + dz * dz); +} + +Vec3 pickWander(uint32_t& rng) { + rng ^= rng << 13; + rng ^= rng >> 17; + rng ^= rng << 5; + float angle = (static_cast(rng) / 4294967295.0f) * 6.2831853f; + return {std::cos(angle), 0.0f, std::sin(angle)}; +} + +Vec3 wanderDirection(Mob& mob) { + if (length(mob.wanderDir) < 0.1f) { + mob.wanderDir = pickWander(mob.rng); + } else { + uint32_t r = mob.rng; + r ^= r << 13; + r ^= r >> 17; + r ^= r << 5; + mob.rng = r; + if ((r & 31u) == 0) mob.wanderDir = pickWander(mob.rng); + } + return mob.wanderDir; +} + +Vec3i feetCellOf(const Vec3& position) { + return {static_cast(std::floor(position.x)), static_cast(std::floor(position.y)), + static_cast(std::floor(position.z))}; +} + +} // namespace + +Aabb mobBox(const Mob& mob) { return boxAt(mob, mob.position); } + +Vec3i mobFeetCell(const Mob& mob) { return feetCellOf(mob.position); } + +void stepMob(const World& world, Mob& mob, const Vec3& wishDir, const MobConfig& config, float dt) { + mob.velocity.x = wishDir.x * config.moveSpeed; + mob.velocity.z = wishDir.z * config.moveSpeed; + mob.velocity.y -= config.gravity * dt; + if (mob.velocity.y < -config.maxFallSpeed) mob.velocity.y = -config.maxFallSpeed; + + mob.onGround = false; + moveAxis(world, mob, 0, mob.velocity.x * dt); + moveAxis(world, mob, 2, mob.velocity.z * dt); + bool collidedY = moveAxis(world, mob, 1, mob.velocity.y * dt); + if (collidedY) { + if (mob.velocity.y < 0.0f) mob.onGround = true; + mob.velocity.y = 0.0f; + } +} + +Vec3 updateMobBrain(const World& world, Mob& mob, const Vec3& targetPos, const MobConfig& config) { + float dist = horizontalDistance(mob.position, targetPos); + bool wantChase = mob.state == MobState::Chase ? dist <= config.loseRadius + : dist <= config.chaseRadius; + + if (wantChase) { + Vec3i start = mobFeetCell(mob); + Vec3i goal = feetCellOf(targetPos); + std::vector path = findPath(world, start, goal, config.path); + if (path.size() >= 2) { + mob.state = MobState::Chase; + Vec3 center{static_cast(path[1].x) + 0.5f, mob.position.y, + static_cast(path[1].z) + 0.5f}; + Vec3 toNext{center.x - mob.position.x, 0.0f, center.z - mob.position.z}; + return normalize(toNext); + } + if (path.size() == 1) { + mob.state = MobState::Chase; + return {0.0f, 0.0f, 0.0f}; + } + } + + mob.state = MobState::Wander; + return wanderDirection(mob); +} + +} // namespace vf