feat(vf): add SpawnRule and MobCap

This commit is contained in:
dualfroz committed 2026-09-04 21:40:59 +02:00
1 parent dcdca6519a
commit ad742d3423
1 file changed
+155
+155
View File
@@ -0,0 +1,155 @@
#pragma once
#include <array>
#include <cstdint>
#include <vector>
#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<int>(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<int>(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<Block> groundBlocks;
int requiredHeadroom = 2;
std::vector<BiomeKind> 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<SpawnRule> defaultSpawnRules();
const SpawnRule* findSpawnRule(const std::vector<SpawnRule>& 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<int, spawnCategoryCount> 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<int, spawnCategoryCount> 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;
};