Files
voxelforge/tests/testSpawning.cpp
T
2026-09-13 17:20:43 +02:00

317 lines
11 KiB
C++

#include "testHarness.h"
#include "vf/Spawning.h"
#include <string>
#include <vector>
using namespace vf;
namespace {
constexpr int floorY = 64;
constexpr int spawnY = 65;
constexpr int fieldSpan = 48;
// One flat plate of `surface` with the sky wide open above it, big enough for
// the light box below to see nothing but that plate.
World plateWorld(Block surface) {
World world;
for (int x = -fieldSpan; x <= fieldSpan; ++x) {
for (int z = -fieldSpan; z <= fieldSpan; ++z) world.setBlock(x, floorY, z, surface);
}
return world;
}
LightField bakeLight(const World& world) {
return computeLight(world, Vec3i{-fieldSpan, floorY - 2, -fieldSpan},
Vec3i{fieldSpan * 2 + 1, 10, fieldSpan * 2 + 1});
}
SpawnEnvironment nightEnvironment(const World& world, const LightField& light) {
SpawnEnvironment env;
env.world = &world;
env.light = &light;
env.dimension = DimensionId::Overworld;
env.fallbackBiome = BiomeKind::Plains;
env.daytime = false;
env.playerPresent = true;
env.playerPosition = {40.5f, static_cast<float>(spawnY), 0.5f};
env.loadedChunks = 289;
env.topY = 70;
env.bottomY = 61;
return env;
}
SpawnRule zombieRule() {
std::vector<SpawnRule> rules = defaultSpawnRules();
return *findSpawnRule(rules, Species::Zombie);
}
SpawnRule sheepRule() {
std::vector<SpawnRule> rules = defaultSpawnRules();
return *findSpawnRule(rules, Species::Sheep);
}
Vec3i originCell() { return Vec3i{0, spawnY, 0}; }
} // namespace
VF_TEST(theStockRuleTableCoversEverySpeciesWithACoherentCategory) {
std::vector<SpawnRule> rules = defaultSpawnRules();
CHECK_EQ(rules.size(), 6u);
for (int index = 0; index < static_cast<int>(Species::Count); ++index) {
Species species = static_cast<Species>(index);
const SpawnRule* rule = findSpawnRule(rules, species);
CHECK(rule != nullptr);
if (rule == nullptr) continue;
CHECK(rule->category == categoryForSpecies(species));
CHECK(rule->packMin >= 1);
CHECK(rule->packMax >= rule->packMin);
CHECK(rule->weight > 0);
}
CHECK(findSpawnRule(rules, Species::Zombie)->dayAllowed == false);
CHECK(findSpawnRule(rules, Species::Sheep)->dayAllowed);
CHECK(std::string(spawnCategoryName(SpawnCategory::Water)) == std::string("water"));
CHECK(std::string(spawnCategoryName(SpawnCategory::Ambient)) == std::string("ambient"));
for (int index = 0; index < spawnDenialCount; ++index) {
SpawnDenial denial = static_cast<SpawnDenial>(index);
CHECK(std::string(spawnDenialName(denial)) != std::string("unknown"));
}
}
VF_TEST(aCellThatSatisfiesEveryConditionIsAccepted) {
World world = plateWorld(Block::Grass);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
MobCap cap;
CHECK_EQ(env.skyLightAt(originCell()), 4);
CHECK_EQ(env.blockLightAt(originCell()), 0);
CHECK(checkSpawnRule(zombieRule(), env, originCell(), cap) == SpawnDenial::None);
}
VF_TEST(aFullCategoryCapRefusesTheSpawnBeforeAnythingElseIsChecked) {
World world = plateWorld(Block::Grass);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
MobCap cap;
env.population[static_cast<std::size_t>(SpawnCategory::Hostile)] =
cap.capFor(SpawnCategory::Hostile, env.loadedChunks);
CHECK(checkSpawnRule(zombieRule(), env, originCell(), cap) ==
SpawnDenial::CategoryCapReached);
env.population[static_cast<std::size_t>(SpawnCategory::Hostile)] -= 1;
CHECK(checkSpawnRule(zombieRule(), env, originCell(), cap) == SpawnDenial::None);
}
VF_TEST(aRuleFromAnotherDimensionIsRefused) {
World world = plateWorld(Block::Grass);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
env.dimension = DimensionId::Nether;
CHECK(checkSpawnRule(zombieRule(), env, originCell(), MobCap{}) ==
SpawnDenial::WrongDimension);
}
VF_TEST(aCellOutsideTheRulesHeightBandIsRefused) {
World world = plateWorld(Block::Grass);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
SpawnRule rule = zombieRule();
rule.minY = spawnY + 4;
CHECK(checkSpawnRule(rule, env, originCell(), MobCap{}) == SpawnDenial::HeightOutOfRange);
rule.minY = 1;
rule.maxY = spawnY - 1;
CHECK(checkSpawnRule(rule, env, originCell(), MobCap{}) == SpawnDenial::HeightOutOfRange);
}
VF_TEST(aBiomeOutsideTheRulesListIsRefused) {
World world = plateWorld(Block::Grass);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
std::vector<SpawnRule> rules = defaultSpawnRules();
SpawnRule creeper = *findSpawnRule(rules, Species::Creeper);
CHECK(!creeper.biomes.empty());
env.fallbackBiome = BiomeKind::Ocean;
CHECK(checkSpawnRule(creeper, env, originCell(), MobCap{}) == SpawnDenial::WrongBiome);
env.fallbackBiome = BiomeKind::Forest;
CHECK(checkSpawnRule(creeper, env, originCell(), MobCap{}) == SpawnDenial::None);
}
VF_TEST(aNocturnalRuleIsRefusedInBroadDaylight) {
World world = plateWorld(Block::Grass);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
env.daytime = true;
CHECK(checkSpawnRule(zombieRule(), env, originCell(), MobCap{}) ==
SpawnDenial::WrongTimeOfDay);
}
VF_TEST(theWrongBlockUnderfootIsRefused) {
World world = plateWorld(Block::Grass);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
SpawnRule rule = zombieRule();
rule.groundBlocks = {Block::Sand};
CHECK(checkSpawnRule(rule, env, originCell(), MobCap{}) == SpawnDenial::UnsuitableGround);
rule.groundBlocks = {Block::Sand, Block::Grass};
CHECK(checkSpawnRule(rule, env, originCell(), MobCap{}) == SpawnDenial::None);
}
VF_TEST(aCeilingPressedOntoTheSpawnCellIsRefusedForNoHeadroom) {
World world = plateWorld(Block::Grass);
world.setBlock(0, spawnY + 1, 0, Block::Stone);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
CHECK(checkSpawnRule(zombieRule(), env, originCell(), MobCap{}) == SpawnDenial::NoHeadroom);
SpawnRule crawler = zombieRule();
crawler.requiredHeadroom = 1;
CHECK(checkSpawnRule(crawler, env, originCell(), MobCap{}) == SpawnDenial::None);
}
VF_TEST(aTorchLitCellIsRefusedForTooMuchBlockLight) {
World world = plateWorld(Block::Grass);
world.setBlock(1, spawnY, 1, Block::Glowstone);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
CHECK(env.blockLightAt(originCell()) > zombieRule().maxBlockLight);
CHECK(checkSpawnRule(zombieRule(), env, originCell(), MobCap{}) ==
SpawnDenial::BlockLightTooHigh);
// The same cell far from the emitter is dark enough again.
Vec3i faraway{30, spawnY, 30};
CHECK_EQ(env.blockLightAt(faraway), 0);
CHECK(checkSpawnRule(zombieRule(), env, faraway, MobCap{}) == SpawnDenial::None);
}
VF_TEST(anOpenSkyAtNoonIsRefusedForTooMuchSkyLight) {
World world = plateWorld(Block::Grass);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
env.daytime = true;
SpawnRule daywalker = zombieRule();
daywalker.dayAllowed = true;
CHECK_EQ(env.skyLightAt(originCell()), 15);
CHECK(checkSpawnRule(daywalker, env, originCell(), MobCap{}) ==
SpawnDenial::SkyLightTooHigh);
}
VF_TEST(aSunlessCellIsRefusedForTooLittleLight) {
World world = plateWorld(Block::Grass);
for (int x = -fieldSpan; x <= fieldSpan; ++x) {
for (int z = -fieldSpan; z <= fieldSpan; ++z) world.setBlock(x, floorY + 4, z, Block::Stone);
}
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
env.daytime = true;
env.playerPosition = {20.5f, static_cast<float>(spawnY), 0.5f};
CHECK_EQ(env.skyLightAt(originCell()), 0);
CHECK(checkSpawnRule(sheepRule(), env, originCell(), MobCap{}) == SpawnDenial::LightTooLow);
}
VF_TEST(theSpawnRingAroundThePlayerIsRefusedOnBothSides) {
World world = plateWorld(Block::Grass);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
env.playerPosition = {0.5f, static_cast<float>(spawnY), 0.5f};
CHECK(checkSpawnRule(zombieRule(), env, originCell(), MobCap{}) ==
SpawnDenial::TooCloseToPlayer);
env.playerPosition = {400.5f, static_cast<float>(spawnY), 0.5f};
CHECK(checkSpawnRule(zombieRule(), env, originCell(), MobCap{}) ==
SpawnDenial::TooFarFromPlayer);
}
VF_TEST(aColumnWithNoFloorReportsThatNoCellWasStandable) {
World world;
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
Vec3i cell;
CHECK(!pickSpawnCell(env, Vec3i{0, 0, 0}, 99u, 0, cell));
Spawner spawner;
SpawnResult result = spawner.runColumn(env, SpawnCategory::Hostile, Vec3i{0, 0, 0}, 99u, 0);
CHECK(result.denial == SpawnDenial::NoStandableCell);
CHECK(!result.spawned);
CHECK_EQ(spawner.denialCount(SpawnDenial::NoStandableCell), 1);
}
VF_TEST(aCategoryWithNoRulesReportsThatNoRuleMatched) {
World world = plateWorld(Block::Grass);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
Spawner spawner;
SpawnResult result = spawner.runColumn(env, SpawnCategory::Water, Vec3i{0, 0, 0}, 7u, 0);
CHECK(result.denial == SpawnDenial::NoRuleMatched);
CHECK_EQ(spawner.denialCount(SpawnDenial::NoRuleMatched), 1);
// Adding one water rule is all it takes for the category to come alive.
std::vector<SpawnRule> rules = defaultSpawnRules();
SpawnRule diver = sheepRule();
diver.category = SpawnCategory::Water;
rules.push_back(diver);
Spawner wet(rules);
CHECK(wet.pickRule(env, SpawnCategory::Water, 3u) != nullptr);
}
VF_TEST(theMobCapGrowsWithTheNumberOfLoadedChunks) {
MobCap cap;
CHECK_EQ(cap.capFor(SpawnCategory::Hostile, 289), cap.baseCapFor(SpawnCategory::Hostile));
CHECK_EQ(cap.capFor(SpawnCategory::Hostile, 578), cap.baseCapFor(SpawnCategory::Hostile) * 2);
CHECK_EQ(cap.capFor(SpawnCategory::Hostile, 0), 0);
int previous = -1;
for (int chunks = 0; chunks <= 1156; chunks += 17) {
int current = cap.capFor(SpawnCategory::Hostile, chunks);
CHECK(current >= previous);
previous = current;
}
CHECK(cap.capFor(SpawnCategory::Hostile, 1156) > cap.capFor(SpawnCategory::Hostile, 289));
CHECK(cap.capFor(SpawnCategory::Passive, 289) < cap.capFor(SpawnCategory::Hostile, 289));
// A quarter of the world loaded holds roughly a quarter of the mobs.
CHECK_EQ(cap.capFor(SpawnCategory::Hostile, 72), 17);
}
VF_TEST(aRunningCycleStopsSpawningOnceTheCapIsReached) {
World world = plateWorld(Block::Grass);
LightField light = bakeLight(world);
SpawnEnvironment env = nightEnvironment(world, light);
env.loadedChunks = 17;
env.playerPosition = {0.5f, static_cast<float>(spawnY), 0.5f};
MobCap cap;
int limit = cap.capFor(SpawnCategory::Hostile, env.loadedChunks);
CHECK(limit > 0);
std::vector<Vec3i> columns;
for (int cx = -2; cx <= 2; ++cx) {
for (int cz = -2; cz <= 2; ++cz) columns.push_back(Vec3i{cx, 0, cz});
}
Spawner spawner;
spawner.setAttemptsPerColumn(6);
for (int cycle = 0; cycle < 12; ++cycle) {
spawner.runCycle(env, SpawnCategory::Hostile, columns, 1234u + static_cast<uint64_t>(cycle));
}