#include "vf/Mob.h" #include #include #include #include namespace vf { namespace { uint32_t xorshift(uint32_t& state) { state ^= state << 13; state ^= state >> 17; state ^= state << 5; return state; } float unitFloat(uint32_t& rng) { return static_cast(xorshift(rng)) / 4294967295.0f; } 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); } Vec3i floorCell(const Vec3& position) { return {static_cast(std::floor(position.x)), static_cast(std::floor(position.y)), static_cast(std::floor(position.z))}; } Vec3 randomHeading(uint32_t& rng) { float angle = unitFloat(rng) * 6.2831853f; return {std::cos(angle), 0.0f, std::sin(angle)}; } // Follows an A* path toward `goalPos` for one step, returning the heading to // the next path cell. Falls back to `fallbackDir` when no multi-step path // exists (adjacent goal or unreachable target). Vec3 pathTowardWishDir(const World& world, const Mob& body, const Vec3& goalPos, const PathConfig& pathConfig, const Vec3& fallbackDir) { Vec3i start = mobFeetCell(body); Vec3i goal = floorCell(goalPos); std::vector path = findPath(world, start, goal, pathConfig); if (path.size() >= 2) { Vec3 center{static_cast(path[1].x) + 0.5f, body.position.y, static_cast(path[1].z) + 0.5f}; Vec3 toNext{center.x - body.position.x, 0.0f, center.z - body.position.z}; return normalize(toNext); } if (path.size() == 1) return {0.0f, 0.0f, 0.0f}; return fallbackDir; } SpeciesStats makeStats(float maxHealth, float width, float height, float walkSpeed, float sprintMultiplier, float attackDamage, float attackRange, float attackCooldown, float alertRadius, float disengageRadius, int spawnLightMax, int spawnLightMin) { SpeciesStats stats; stats.maxHealth = maxHealth; stats.width = width; stats.height = height; stats.walkSpeed = walkSpeed; stats.sprintMultiplier = sprintMultiplier; stats.attackDamage = attackDamage; stats.attackRange = attackRange; stats.attackCooldown = attackCooldown; stats.alertRadius = alertRadius; stats.disengageRadius = disengageRadius; stats.spawnLightMax = spawnLightMax; stats.spawnLightMin = spawnLightMin; return stats; } std::array(Species::Count)> buildSpeciesTable() { std::array(Species::Count)> table{}; table[static_cast(Species::Sheep)] = makeStats(10.0f, 0.9f, 1.3f, 2.0f, 1.6f, 0.0f, 0.0f, 0.0f, 8.0f, 12.0f, 15, 9); table[static_cast(Species::Cow)] = makeStats(10.0f, 0.9f, 1.4f, 2.0f, 1.6f, 0.0f, 0.0f, 0.0f, 8.0f, 12.0f, 15, 9); table[static_cast(Species::Pig)] = makeStats(10.0f, 0.9f, 0.9f, 2.0f, 1.6f, 0.0f, 0.0f, 0.0f, 8.0f, 12.0f, 15, 9); table[static_cast(Species::Zombie)] = makeStats(20.0f, 0.6f, 1.95f, 2.3f, 1.4f, 3.0f, 1.4f, 1.0f, 16.0f, 20.0f, 7, 0); table[static_cast(Species::Skeleton)] = makeStats(20.0f, 0.6f, 1.95f, 2.3f, 1.4f, 2.0f, 8.0f, 1.4f, 16.0f, 20.0f, 7, 0); table[static_cast(Species::Creeper)] = makeStats(20.0f, 0.6f, 1.7f, 2.5f, 1.3f, 6.0f, 1.6f, 1.5f, 16.0f, 20.0f, 7, 0); return table; } constexpr int fullSkyLight = 15; } // namespace bool isHostile(Species species) { switch (species) { case Species::Zombie: case Species::Skeleton: case Species::Creeper: return true; default: return false; } } const SpeciesStats& speciesStats(Species species) { static const std::array(Species::Count)> table = buildSpeciesTable(); return table[static_cast(species)]; } Creature spawnCreature(Species species, const Vec3& position, uint32_t seed) { const SpeciesStats& stats = speciesStats(species); Creature creature; creature.species = species; creature.body.position = position; creature.body.width = stats.width; creature.body.height = stats.height; creature.health = stats.maxHealth; creature.rng = seed != 0 ? seed : 0x9e3779b9u; creature.state = CreatureState::Idle; return creature; } void updateCreature(const World& world, Creature& creature, const Vec3& threatPos, bool hasThreat, const CreatureConfig& config, float dt) { if (!creature.alive) return; if (creature.attackCooldownRemaining > 0.0f) creature.attackCooldownRemaining = std::max(0.0f, creature.attackCooldownRemaining - dt); const SpeciesStats& stats = speciesStats(creature.species); float distToThreat = hasThreat ? horizontalDistance(creature.body.position, threatPos) : 0.0f; bool wasEngaged = creature.state == CreatureState::Chase || creature.state == CreatureState::Attack || creature.state == CreatureState::Flee; float triggerRadius = wasEngaged ? stats.disengageRadius : stats.alertRadius; bool engaged = hasThreat && distToThreat <= triggerRadius; if (isHostile(creature.species)) { creature.state = !engaged ? CreatureState::Wander : (distToThreat <= stats.attackRange ? CreatureState::Attack : CreatureState::Chase); } else { creature.state = engaged ? CreatureState::Flee : CreatureState::Wander; } Vec3 wishDir{0.0f, 0.0f, 0.0f}; float speed = 0.0f; switch (creature.state) { case CreatureState::Idle: break; case CreatureState::Wander: { creature.stateTimer -= dt; if (creature.stateTimer <= 0.0f || length(creature.body.wanderDir) < 0.1f) { creature.body.wanderDir = randomHeading(creature.rng); creature.stateTimer = 1.5f + unitFloat(creature.rng) * 2.5f; } wishDir = creature.body.wanderDir; speed = stats.walkSpeed; break; } case CreatureState::Chase: { wishDir = pathTowardWishDir(world, creature.body, threatPos, config.physics.path, creature.body.wanderDir); creature.body.wanderDir = wishDir; speed = stats.walkSpeed * stats.sprintMultiplier; break; } case CreatureState::Flee: { Vec3 away{creature.body.position.x - threatPos.x, 0.0f, creature.body.position.z - threatPos.z}; away = normalize(away); if (length(away) < 0.1f) away = randomHeading(creature.rng); Vec3 fleeTarget = creature.body.position + away * 8.0f; wishDir = pathTowardWishDir(world, creature.body, fleeTarget, config.physics.path, away); creature.body.wanderDir = wishDir; speed = stats.walkSpeed * stats.sprintMultiplier; break; } case CreatureState::Attack: wishDir = {0.0f, 0.0f, 0.0f}; speed = 0.0f; break; } MobConfig physics = config.physics; physics.moveSpeed = speed; stepMob(world, creature.body, wishDir, physics, dt); } bool damageCreature(Creature& creature, float amount) { if (!creature.alive) return false; creature.health -= amount; if (creature.health <= 0.0f) { creature.health = 0.0f; creature.alive = false; return true; } return false; } bool creatureCanAttack(const Creature& creature, const Vec3& targetPos) { if (!creature.alive || creature.attackCooldownRemaining > 0.0f) return false; const SpeciesStats& stats = speciesStats(creature.species); return horizontalDistance(creature.body.position, targetPos) <= stats.attackRange; } void creatureAttack(Creature& creature) { creature.attackCooldownRemaining = speciesStats(creature.species).attackCooldown; } std::vector dropsFor(Species species) { switch (species) { case Species::Sheep: return {{ItemId::RawMutton, 1, 2, 1.0f}}; case Species::Cow: return {{ItemId::RawBeef, 1, 3, 1.0f}}; case Species::Pig: return {{ItemId::RawPorkchop, 1, 3, 1.0f}}; case Species::Zombie: return {{ItemId::RottenFlesh, 0, 2, 0.8f}}; case Species::Skeleton: return {{ItemId::Stick, 0, 2, 0.7f}}; case Species::Creeper: return {{ItemId::Gunpowder, 0, 2, 1.0f}}; default: return {}; } } std::vector rollDrops(Species species, uint32_t& rng) { std::vector drops; for (const DropEntry& entry : dropsFor(species)) { if (unitFloat(rng) > entry.chance) continue; int spread = entry.maxCount > entry.minCount ? static_cast(xorshift(rng) % static_cast(entry.maxCount - entry.minCount + 1)) : 0; int count = entry.minCount + spread; if (count > 0) drops.push_back(ItemSlot{entry.item, count, 0}); } return drops; } int mobSpawnLightLevel(const World& world, const Vec3i& cell, int searchRadius) { bool skyOpen = true; for (int y = cell.y + 1; y <= cell.y + 64; ++y) { if (world.solidAt(cell.x, y, cell.z)) { skyOpen = false; break; } } int sky = skyOpen ? fullSkyLight : 0; int emitterBest = 0; for (int dx = -searchRadius; dx <= searchRadius; ++dx) { for (int dy = -searchRadius; dy <= searchRadius; ++dy) { for (int dz = -searchRadius; dz <= searchRadius; ++dz) { int manhattan = std::abs(dx) + std::abs(dy) + std::abs(dz); if (manhattan > searchRadius) continue; Block block = world.blockAt(cell.x + dx, cell.y + dy, cell.z + dz); int emission = blockLightEmission(block); if (emission <= 0) continue; int level = emission - manhattan; if (level > emitterBest) emitterBest = level; } } } return std::max(sky, emitterBest); } bool speciesAllowsBiome(Species species, Biome biome) { switch (species) { case Species::Sheep: case Species::Cow: case Species::Pig: return biome == Biome::Plains || biome == Biome::Forest; case Species::Zombie: case Species::Skeleton: return biome != Biome::Ocean; case Species::Creeper: return biome != Biome::Ocean && biome != Biome::Beach; default: return false; } } bool canSpawnAt(const World& world, const Vec3i& cell, Species species, Biome biome, const Vec3& playerPos, int currentPopulationInArea, const SpawnConfig& config) { if (currentPopulationInArea >= config.maxPerArea) return false; if (!speciesAllowsBiome(species, biome)) return false; if (!isStandable(world, cell)) return false; float dist = horizontalDistance( {static_cast(cell.x) + 0.5f, 0.0f, static_cast(cell.z) + 0.5f}, playerPos); if (dist < config.minPlayerDistance || dist > config.maxPlayerDistance) return false; int light = mobSpawnLightLevel(world, cell); const SpeciesStats& stats = speciesStats(species); if (isHostile(species)) { if (light > stats.spawnLightMax) return false; } else { if (light < stats.spawnLightMin) return false; } return true; } SpawnAttempt trySpawn(const World& world, const Vec3i& cell, Biome biome, const Vec3& playerPos, int currentPopulationInArea, uint32_t& rng, const SpawnConfig& config) { std::array(Species::Count)> candidates{}; int candidateCount = 0; for (int i = 0; i < static_cast(Species::Count); ++i) { Species species = static_cast(i); if (canSpawnAt(world, cell, species, biome, playerPos, currentPopulationInArea, config)) candidates[static_cast(candidateCount++)] = species; } if (candidateCount == 0) return {}; uint32_t pick = xorshift(rng) % static_cast(candidateCount); SpawnAttempt attempt; attempt.spawned = true; attempt.species = candidates[pick]; attempt.position = {static_cast(cell.x) + 0.5f, static_cast(cell.y), static_cast(cell.z) + 0.5f}; return attempt; } bool advanceSpawnCycle(SpawnCycle& cycle, float dt) { cycle.timer += dt; if (cycle.timer < cycle.interval) return false; cycle.timer -= cycle.interval; return true; } } // namespace vf