From ad742d34237decc9aabecdaabedabb479a7ece0b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 21:40:59 +0200 Subject: [PATCH] feat(vf): add SpawnRule and MobCap --- core/include/vf/Spawning.h | 155 +++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 core/include/vf/Spawning.h diff --git a/core/include/vf/Spawning.h b/core/include/vf/Spawning.h new file mode 100644 index 0000000..ffaf887 --- /dev/null +++ b/core/include/vf/Spawning.h @@ -0,0 +1,155 @@ +#pragma once + +#include +#include +#include + +#include "Behavior.h" +#include "BiomeGen.h" +#include "Block.h" +#include "Dimension.h" +#include "Lighting.h" +#include "Math.h" +#include "Mob.h" +#include "Pathfind.h" +#include "World.h" + +namespace vf { + +// Natural spawning: which mob may appear where, how many of them the loaded +// world can hold, how they arrive in packs and when they vanish again. Every +// decision is a pure function of the world, the rule table and a seed - the +// same inputs always produce the same spawn. +enum class SpawnCategory : uint8_t { + Hostile = 0, + Passive = 1, + Water = 2, + Ambient = 3, + Count, +}; + +constexpr int spawnCategoryCount = static_cast(SpawnCategory::Count); + +const char* spawnCategoryName(SpawnCategory category); +SpawnCategory categoryForSpecies(Species species); + +// Why a spawn did not happen. A refusal always names its reason instead of +// collapsing into a bare false. +enum class SpawnDenial : uint8_t { + None = 0, + CategoryCapReached, + WrongDimension, + WrongBiome, + WrongTimeOfDay, + HeightOutOfRange, + UnsuitableGround, + NoHeadroom, + BlockLightTooHigh, + SkyLightTooHigh, + LightTooLow, + TooCloseToPlayer, + TooFarFromPlayer, + NoStandableCell, + NoRuleMatched, + Count, +}; + +constexpr int spawnDenialCount = static_cast(SpawnDenial::Count); + +const char* spawnDenialName(SpawnDenial denial); + +// One species' conditions. An empty biome or ground list means "anything"; +// each light bound is checked on its own channel, so a rule can demand open +// sky and still refuse a torch-lit cell. +struct SpawnRule { + Species species = Species::Zombie; + SpawnCategory category = SpawnCategory::Hostile; + DimensionId dimension = DimensionId::Overworld; + + int minBlockLight = 0; + int maxBlockLight = 15; + int minSkyLight = 0; + int maxSkyLight = 15; + int minY = 0; + int maxY = 127; + + std::vector groundBlocks; + int requiredHeadroom = 2; + std::vector biomes; + + bool dayAllowed = true; + bool nightAllowed = true; + + int packMin = 1; + int packMax = 4; + int packSpread = 3; + int weight = 10; + + float minPlayerDistance = 8.0f; + float maxPlayerDistance = 96.0f; + + bool allowsBiome(BiomeKind biome) const; + bool allowsGround(Block block) const; +}; + +// The stock table covers every species Mob.h defines: three hostile and three +// passive. Water and Ambient exist as first-class categories with their own +// caps and are filled by whatever rules a caller adds. +std::vector defaultSpawnRules(); +const SpawnRule* findSpawnRule(const std::vector& rules, Species species); + +// Population ceiling per category, scaled by how much world is actually +// loaded: the same base numbers give a small cap for a handful of chunks and +// the full cap for a fully loaded view distance. +struct MobCap { + std::array baseCap{{70, 10, 5, 15}}; + int chunksPerCapUnit = 289; + + int capFor(SpawnCategory category, int loadedChunks) const; + int baseCapFor(SpawnCategory category) const; +}; + +// Everything a spawn check reads besides the rule itself. `light` is the baked +// field from Lighting.h, so block light and sky light come from the same +// source the renderer uses. +struct SpawnEnvironment { + const World* world = nullptr; + const LightField* light = nullptr; + const BiomeGen* biomes = nullptr; + + DimensionId dimension = DimensionId::Overworld; + BiomeKind fallbackBiome = BiomeKind::Plains; + bool daytime = true; + + Vec3 playerPosition; + bool playerPresent = true; + + int loadedChunks = 289; + int topY = 96; + int bottomY = 1; + PathConfig path; + + std::array population{{0, 0, 0, 0}}; + + int populationOf(SpawnCategory category) const; + void addPopulation(SpawnCategory category, int amount); + BiomeKind biomeAt(int gx, int gz) const; + int blockLightAt(const Vec3i& cell) const; + // Sky light as the spawn rules see it: the baked value during the day, + // dimmed by the night penalty after dusk, which is what lets hostiles + // appear under an open sky only once it is dark. + int skyLightAt(const Vec3i& cell) const; +}; + +// The plan for one spawn attempt: which rule won the weighted draw, which cell +// the seed picked and how large the pack would be. Nothing here has been +// validated yet - that is what attemptSpawn does with it. +struct SpawnTrial { + Vec3i cell; + Species species = Species::Zombie; + SpawnCategory category = SpawnCategory::Hostile; + int packSize = 0; + bool ruleFound = false; + bool cellFound = false; +}; +