#include "testHarness.h" #include "vf/Mob.h" #include using namespace vf; namespace { World openField(int x0 = -60, int z0 = -60, int x1 = 60, int z1 = 60) { World world; for (int x = x0; x <= x1; ++x) for (int z = z0; z <= z1; ++z) world.setBlock(x, 0, z, Block::Stone); return world; } World sealedRoom() { World world; for (int x = 0; x <= 4; ++x) { for (int z = 0; z <= 4; ++z) { for (int y = 0; y <= 4; ++y) { bool shell = x == 0 || x == 4 || z == 0 || z == 4 || y == 0 || y == 4; if (shell) world.setBlock(x, y, z, Block::Stone); } } } return world; } 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); } } // namespace VF_TEST(isHostileClassifiesEachSpecies) { CHECK(!isHostile(Species::Sheep)); CHECK(!isHostile(Species::Cow)); CHECK(!isHostile(Species::Pig)); CHECK(isHostile(Species::Zombie)); CHECK(isHostile(Species::Skeleton)); CHECK(isHostile(Species::Creeper)); } VF_TEST(spawnCreatureAdoptsSpeciesStats) { Creature creature = spawnCreature(Species::Cow, {3.0f, 1.0f, 3.0f}, 42); const SpeciesStats& stats = speciesStats(Species::Cow); CHECK_NEAR(creature.health, stats.maxHealth, 1e-6f); CHECK_NEAR(creature.body.width, stats.width, 1e-6f); CHECK_NEAR(creature.body.height, stats.height, 1e-6f); CHECK(creature.alive); CHECK(creature.state == CreatureState::Idle); } VF_TEST(passiveWandersWhenNoThreatIsNearby) { World world = openField(); Creature creature = spawnCreature(Species::Sheep, {5.0f, 1.0f, 5.0f}, 7); CreatureConfig config; Vec3 farThreat{200.0f, 1.0f, 200.0f}; updateCreature(world, creature, farThreat, false, config, 1.0f / 20.0f); CHECK(creature.state == CreatureState::Wander); } VF_TEST(passiveFleesAndGainsDistanceFromNearbyThreat) { World world = openField(); Creature creature = spawnCreature(Species::Sheep, {5.0f, 1.0f, 5.0f}, 11); CreatureConfig config; Vec3 threat{7.0f, 1.0f, 5.0f}; // 2 blocks away, inside the 8-block alert radius float startDist = horizontalDistance(creature.body.position, threat); for (int i = 0; i < 120; ++i) updateCreature(world, creature, threat, true, config, 1.0f / 20.0f); float endDist = horizontalDistance(creature.body.position, threat); CHECK(creature.state == CreatureState::Flee); CHECK(endDist > startDist); } VF_TEST(hostileWandersWhenThreatIsFar) { World world = openField(); Creature creature = spawnCreature(Species::Zombie, {5.0f, 1.0f, 5.0f}, 3); CreatureConfig config; Vec3 farThreat{500.0f, 1.0f, 500.0f}; updateCreature(world, creature, farThreat, true, config, 1.0f / 20.0f); CHECK(creature.state == CreatureState::Wander); } VF_TEST(hostileChasesAndClosesDistanceOnReachableThreat) { World world = openField(); Creature creature = spawnCreature(Species::Zombie, {5.0f, 1.0f, 5.0f}, 5); CreatureConfig config; Vec3 threat{11.0f, 1.0f, 5.0f}; // within the 16-block alert radius, open ground float startDist = horizontalDistance(creature.body.position, threat); bool sawChase = false; for (int i = 0; i < 150; ++i) { updateCreature(world, creature, threat, true, config, 1.0f / 20.0f); if (creature.state == CreatureState::Chase) sawChase = true; } float endDist = horizontalDistance(creature.body.position, threat); CHECK(sawChase); CHECK(endDist < startDist); } VF_TEST(hostileEntersAttackStateWithinRange) { World world = openField(); Creature creature = spawnCreature(Species::Zombie, {5.0f, 1.0f, 5.0f}, 9); CreatureConfig config; Vec3 threat{5.5f, 1.0f, 5.0f}; // 0.5 blocks away, inside the 1.4 attack range updateCreature(world, creature, threat, true, config, 1.0f / 20.0f); CHECK(creature.state == CreatureState::Attack); } VF_TEST(hysteresisKeepsChasingInsideDisengageBandThenGivesUp) { World world = openField(); Creature creature = spawnCreature(Species::Zombie, {5.0f, 1.0f, 5.0f}, 13); creature.state = CreatureState::Chase; CreatureConfig config; Vec3 inBand{5.0f + 18.0f, 1.0f, 5.0f}; // between alert (16) and disengage (20) updateCreature(world, creature, inBand, true, config, 1.0f / 20.0f); CHECK(creature.state == CreatureState::Chase); creature.state = CreatureState::Chase; Vec3 farOut{5.0f + 30.0f, 1.0f, 5.0f}; // beyond disengage radius updateCreature(world, creature, farOut, true, config, 1.0f / 20.0f); CHECK(creature.state == CreatureState::Wander); } VF_TEST(damageCreatureTracksHealthAndDeath) { Creature creature = spawnCreature(Species::Pig, {0.0f, 1.0f, 0.0f}, 21); float maxHealth = creature.health; bool died = damageCreature(creature, maxHealth * 0.5f); CHECK(!died); CHECK(creature.alive); CHECK_NEAR(creature.health, maxHealth * 0.5f, 1e-3f); died = damageCreature(creature, maxHealth); CHECK(died); CHECK(!creature.alive); CHECK_EQ(creature.health, 0.0f); } VF_TEST(creatureAttackEntersCooldownThenRecovers) { World world = openField(); Creature creature = spawnCreature(Species::Zombie, {5.0f, 1.0f, 5.0f}, 17); Vec3 threat{5.5f, 1.0f, 5.0f}; CreatureConfig config; updateCreature(world, creature, threat, true, config, 1.0f / 20.0f); CHECK(creature.state == CreatureState::Attack); CHECK(creatureCanAttack(creature, threat)); creatureAttack(creature); CHECK(!creatureCanAttack(creature, threat)); float cooldown = speciesStats(Species::Zombie).attackCooldown; for (float t = 0.0f; t < cooldown + 0.5f; t += 1.0f / 20.0f) updateCreature(world, creature, threat, true, config, 1.0f / 20.0f); CHECK(creatureCanAttack(creature, threat)); } VF_TEST(dropsForMatchesSpeciesTheme) { auto sheepDrops = dropsFor(Species::Sheep); CHECK_EQ(sheepDrops.size(), static_cast(1)); CHECK(sheepDrops[0].item == ItemId::RawMutton); auto creeperDrops = dropsFor(Species::Creeper); CHECK(creeperDrops[0].item == ItemId::Gunpowder); } VF_TEST(rollDropsIsDeterministicForTheSameSeed) { uint32_t seedA = 999u; uint32_t seedB = 999u; std::vector a = rollDrops(Species::Cow, seedA); std::vector b = rollDrops(Species::Cow, seedB); CHECK(a.size() == b.size()); for (std::size_t i = 0; i < a.size(); ++i) { CHECK(a[i].item == b[i].item); CHECK_EQ(a[i].count, b[i].count); } CHECK_EQ(seedA, seedB); // rng advanced identically } VF_TEST(mobSpawnLightLevelIsHighOutsideAndDarkInsideASealedRoom) { World open = openField(); int openLight = mobSpawnLightLevel(open, Vec3i{5, 1, 5}); CHECK_EQ(openLight, 15); World room = sealedRoom(); int roomLight = mobSpawnLightLevel(room, Vec3i{2, 1, 2}); CHECK_EQ(roomLight, 0); } VF_TEST(canSpawnAtRespectsPopulationCap) { World world = openField(); Vec3i cell{5, 1, 5}; Vec3 player{5.0f, 1.0f, 25.0f}; // 20 blocks away, inside default range SpawnConfig config; config.maxPerArea = 3; CHECK(canSpawnAt(world, cell, Species::Sheep, Biome::Plains, player, 2, config)); CHECK(!canSpawnAt(world, cell, Species::Sheep, Biome::Plains, player, 3, config)); } VF_TEST(canSpawnAtRespectsBiomeRestrictions) { World world = openField(); Vec3i cell{5, 1, 5}; Vec3 player{5.0f, 1.0f, 25.0f}; CHECK(canSpawnAt(world, cell, Species::Sheep, Biome::Plains, player, 0)); CHECK(!canSpawnAt(world, cell, Species::Sheep, Biome::Ocean, player, 0)); CHECK(speciesAllowsBiome(Species::Creeper, Biome::Forest)); CHECK(!speciesAllowsBiome(Species::Creeper, Biome::Ocean)); CHECK(!speciesAllowsBiome(Species::Creeper, Biome::Beach)); CHECK(speciesAllowsBiome(Species::Zombie, Biome::Desert)); CHECK(!speciesAllowsBiome(Species::Zombie, Biome::Ocean)); } VF_TEST(canSpawnAtRespectsLightThresholds) { World room = sealedRoom(); Vec3i darkCell{2, 1, 2}; Vec3 farPlayer{2.0f, 1.0f, 22.0f}; // horizontal distance ~20, inside range CHECK(canSpawnAt(room, darkCell, Species::Zombie, Biome::Forest, farPlayer, 0)); World lit = openField(); Vec3i brightCell{5, 1, 5}; CHECK(!canSpawnAt(lit, brightCell, Species::Zombie, Biome::Forest, farPlayer, 0)); } VF_TEST(canSpawnAtRespectsPlayerDistanceWindow) { World world = openField(); Vec3i cell{5, 1, 5}; SpawnConfig config; Vec3 tooClose{5.0f, 1.0f, 6.0f}; CHECK(!canSpawnAt(world, cell, Species::Sheep, Biome::Plains, tooClose, 0, config)); Vec3 tooFar{5.0f, 1.0f, 200.0f}; CHECK(!canSpawnAt(world, cell, Species::Sheep, Biome::Plains, tooFar, 0, config)); Vec3 justRight{5.0f, 1.0f, 25.0f}; CHECK(canSpawnAt(world, cell, Species::Sheep, Biome::Plains, justRight, 0, config)); } VF_TEST(trySpawnPicksOnlyAmongValidCandidatesDeterministically) { World world = openField(); Vec3i cell{5, 1, 5}; Vec3 player{5.0f, 1.0f, 25.0f}; // daylight rules out hostiles (light 15 > their max of 7) uint32_t rngA = 2026u; uint32_t rngB = 2026u; SpawnAttempt a = trySpawn(world, cell, Biome::Plains, player, 0, rngA); SpawnAttempt b = trySpawn(world, cell, Biome::Plains, player, 0, rngB); CHECK(a.spawned); CHECK(a.species == b.species); CHECK(!isHostile(a.species)); } VF_TEST(trySpawnFindsNoCandidateWhenAreaIsFull) { World world = openField(); Vec3i cell{5, 1, 5}; Vec3 player{5.0f, 1.0f, 25.0f}; SpawnConfig config; config.maxPerArea = 0; uint32_t rng = 1u; SpawnAttempt attempt = trySpawn(world, cell, Biome::Plains, player, 0, rng, config); CHECK(!attempt.spawned); } VF_TEST(advanceSpawnCycleFiresOncePerIntervalAndCatchesUpOnce) { SpawnCycle cycle; cycle.interval = 1.0f; int fires = 0; for (int i = 0; i < 25; ++i) if (advanceSpawnCycle(cycle, 0.1f)) ++fires; CHECK_EQ(fires, 2); // fires at t=1.0 and t=2.0 across 2.5s of ticking SpawnCycle burst; burst.interval = 1.0f; CHECK(advanceSpawnCycle(burst, 10.0f)); // huge dt: only one attempt fires this call CHECK_NEAR(burst.timer, 9.0f, 1e-4f); // the rest of the backlog is kept, not dropped int extraFires = 0; for (int i = 0; i < 15; ++i) if (advanceSpawnCycle(burst, 0.0f)) ++extraFires; CHECK_EQ(extraFires, 9); // backlog drains at one interval per subsequent call }