From 66cd109fe1b8472997b1040d45c51e48e929ed79 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 4 Sep 2026 21:43:49 +0200 Subject: [PATCH] feat(vf): add SpawnResult and DespawnPolicy to Spawning --- core/include/vf/Spawning.h | 141 +++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/core/include/vf/Spawning.h b/core/include/vf/Spawning.h index ffaf887..060044b 100644 --- a/core/include/vf/Spawning.h +++ b/core/include/vf/Spawning.h @@ -153,3 +153,144 @@ struct SpawnTrial { bool cellFound = false; }; +struct SpawnResult { + bool spawned = false; + SpawnDenial denial = SpawnDenial::NoRuleMatched; + Species species = Species::Zombie; + SpawnCategory category = SpawnCategory::Hostile; + BehaviorRole role = BehaviorRole::HostileHunter; + Vec3i origin; + std::vector positions; + + int packSize() const { return static_cast(positions.size()); } +}; + +// Deterministic 64-bit mix. Same seed, same cell, same salt, same number. +uint64_t spawnHash(uint64_t seed, int x, int y, int z, int salt); + +// Standable, has the headroom the rule wants and nothing solid in the way. +bool hasSpawnRoom(const World& world, const Vec3i& cell, int headroom); + +// The full per-cell verdict, checked in a fixed order so a caller can tell +// exactly which condition failed. +SpawnDenial checkSpawnRule(const SpawnRule& rule, const SpawnEnvironment& env, const Vec3i& cell, + const MobCap& cap); + +// Picks the highest standable cell in one column of the chunk, chosen from the +// seed rather than scanned in order. +bool pickSpawnCell(const SpawnEnvironment& env, const Vec3i& chunkColumn, uint64_t seed, + int attemptIndex, Vec3i& outCell); + +// Spreads a pack around an accepted origin. Members that land somewhere the +// rule rejects are simply left out; the origin is always member zero. +std::vector spreadPack(const SpawnRule& rule, const SpawnEnvironment& env, + const Vec3i& origin, int packSize, uint32_t& rng); + +enum class DespawnBand : uint8_t { + Protected = 0, + Random = 1, + Immediate = 2, +}; + +// Distance bands around the player. Beyond `instantDistance` a mob is gone the +// moment it is checked; between the two distances it rolls every tick; closer +// than `randomDistance` it never despawns on its own. +struct DespawnPolicy { + float instantDistance = 128.0f; + float randomDistance = 32.0f; + int randomChanceDenominator = 800; + bool despawnNamed = false; + bool despawnLeashed = false; +}; + +struct DespawnCandidate { + Vec3 position; + bool named = false; + bool leashed = false; + bool persistent = false; +}; + +DespawnBand despawnBandFor(const DespawnPolicy& policy, const Vec3& position, + const Vec3& playerPosition); +bool despawnCandidateIsProtected(const DespawnPolicy& policy, const DespawnCandidate& candidate); +bool shouldDespawn(const DespawnPolicy& policy, const DespawnCandidate& candidate, + const Vec3& playerPosition, uint32_t& rng); + +// A monster spawner block: counts down, refuses to fire without a player in +// range or with too many of its own kind already nearby, then picks a fresh +// delay from its range. +struct SpawnerBlockState { + Species species = Species::Zombie; + Vec3i position; + int spawnCount = 4; + int spawnRange = 4; + int nearbyLimit = 6; + int requiredHeadroom = 2; + float activationRange = 16.0f; + int minDelay = 200; + int maxDelay = 800; + int delay = 20; + int totalSpawned = 0; + int cyclesFired = 0; + uint32_t rng = 0x1f123bb5u; +}; + +struct SpawnerTickResult { + bool playerInRange = false; + bool fired = false; + SpawnDenial denial = SpawnDenial::None; + std::vector positions; + int delayAfter = 0; +}; + +SpawnerTickResult tickSpawnerBlock(SpawnerBlockState& spawner, const SpawnEnvironment& env, + int nearbyOfKind); + +// Runs spawn cycles over loaded chunk columns and keeps a per-reason tally of +// everything it refused, so a world that never spawns anything can be +// diagnosed instead of guessed at. +class Spawner { +public: + Spawner(); + explicit Spawner(std::vector ruleTable, const MobCap& caps = {}); + + const std::vector& rules() const { return ruleTable; } + const MobCap& caps() const { return capTable; } + + void setAttemptsPerColumn(int attempts) { attemptsPerColumnCount = attempts; } + int attemptsPerColumn() const { return attemptsPerColumnCount; } + + // Weighted pick among the rules of one category that fit the dimension. + const SpawnRule* pickRule(const SpawnEnvironment& env, SpawnCategory category, + uint64_t seed) const; + + // Draws a rule and a cell without touching the world state. + SpawnTrial planSpawn(const SpawnEnvironment& env, SpawnCategory category, + const Vec3i& chunkColumn, uint64_t seed, int attemptIndex) const; + + SpawnResult attemptSpawn(const SpawnEnvironment& env, const SpawnRule& rule, + const SpawnTrial& trial, uint64_t seed); + + SpawnResult runColumn(SpawnEnvironment& env, SpawnCategory category, const Vec3i& chunkColumn, + uint64_t seed, int attemptIndex); + + std::vector runCycle(SpawnEnvironment& env, SpawnCategory category, + const std::vector& chunkColumns, uint64_t seed); + + int denialCount(SpawnDenial denial) const; + int spawnedCount() const { return spawned; } + void resetStats(); + +private: + std::vector ruleTable; + MobCap capTable; + int attemptsPerColumnCount = 3; + int spawned = 0; + std::array denials{}; +}; + +// Hands a finished spawn straight to the behaviour layer: every member of the +// pack becomes an agent already carrying the tree role for its species. +std::vector agentsFromSpawn(const SpawnResult& result, uint32_t seed); + +} // namespace vf