Files
voxelforge/core/src/Spawning.cpp
T
2026-09-09 18:58:04 +02:00

499 lines
19 KiB
C++

#include "vf/Spawning.h"
#include <algorithm>
#include <cmath>
#include "vf/Effect.h"
namespace vf {
namespace {
constexpr int nightSkyLightPenalty = 11;
float straightDistance(const Vec3& a, const Vec3& b) {
Vec3 delta = a - b;
return length(delta);
}
bool groundAndRoomFit(const SpawnRule& rule, const SpawnEnvironment& env, const Vec3i& cell) {
Block ground = env.world->blockAt(cell.x, cell.y - 1, cell.z);
if (!isSolid(ground)) return false;
if (!rule.allowsGround(ground)) return false;
return hasSpawnRoom(*env.world, cell, rule.requiredHeadroom);
}
bool lightFits(const SpawnRule& rule, const SpawnEnvironment& env, const Vec3i& cell) {
int blockLight = env.blockLightAt(cell);
int skyLight = env.skyLightAt(cell);
if (blockLight > rule.maxBlockLight) return false;
if (skyLight > rule.maxSkyLight) return false;
if (blockLight < rule.minBlockLight) return false;
if (skyLight < rule.minSkyLight) return false;
return true;
}
SpawnRule makeHostileRule(Species species, int packMin, int packMax) {
SpawnRule rule;
rule.species = species;
rule.category = SpawnCategory::Hostile;
rule.dimension = DimensionId::Overworld;
rule.maxBlockLight = 7;
rule.maxSkyLight = 7;
rule.minY = 1;
rule.maxY = 127;
rule.requiredHeadroom = 2;
rule.dayAllowed = false;
rule.nightAllowed = true;
rule.packMin = packMin;
rule.packMax = packMax;
rule.packSpread = 3;
rule.weight = 10;
rule.minPlayerDistance = 24.0f;
rule.maxPlayerDistance = 128.0f;
return rule;
}
SpawnRule makePassiveRule(Species species, int packMin, int packMax,
std::vector<BiomeKind> biomeList) {
SpawnRule rule;
rule.species = species;
rule.category = SpawnCategory::Passive;
rule.dimension = DimensionId::Overworld;
rule.minBlockLight = 0;
rule.minSkyLight = 9;
rule.maxBlockLight = 15;
rule.maxSkyLight = 15;
rule.minY = 48;
rule.maxY = 127;
rule.groundBlocks = {Block::Grass};
rule.requiredHeadroom = 2;
rule.biomes = std::move(biomeList);
rule.dayAllowed = true;
rule.nightAllowed = true;
rule.packMin = packMin;
rule.packMax = packMax;
rule.packSpread = 4;
rule.weight = 12;
rule.minPlayerDistance = 12.0f;
rule.maxPlayerDistance = 96.0f;
return rule;
}
} // namespace
const char* spawnCategoryName(SpawnCategory category) {
switch (category) {
case SpawnCategory::Hostile: return "hostile";
case SpawnCategory::Passive: return "passive";
case SpawnCategory::Water: return "water";
case SpawnCategory::Ambient: return "ambient";
case SpawnCategory::Count: break;
}
return "unknown";
}
SpawnCategory categoryForSpecies(Species species) {
return isHostile(species) ? SpawnCategory::Hostile : SpawnCategory::Passive;
}
const char* spawnDenialName(SpawnDenial denial) {
switch (denial) {
case SpawnDenial::None: return "none";
case SpawnDenial::CategoryCapReached: return "categoryCapReached";
case SpawnDenial::WrongDimension: return "wrongDimension";
case SpawnDenial::WrongBiome: return "wrongBiome";
case SpawnDenial::WrongTimeOfDay: return "wrongTimeOfDay";
case SpawnDenial::HeightOutOfRange: return "heightOutOfRange";
case SpawnDenial::UnsuitableGround: return "unsuitableGround";
case SpawnDenial::NoHeadroom: return "noHeadroom";
case SpawnDenial::BlockLightTooHigh: return "blockLightTooHigh";
case SpawnDenial::SkyLightTooHigh: return "skyLightTooHigh";
case SpawnDenial::LightTooLow: return "lightTooLow";
case SpawnDenial::TooCloseToPlayer: return "tooCloseToPlayer";
case SpawnDenial::TooFarFromPlayer: return "tooFarFromPlayer";
case SpawnDenial::NoStandableCell: return "noStandableCell";
case SpawnDenial::NoRuleMatched: return "noRuleMatched";
case SpawnDenial::Count: break;
}
return "unknown";
}
bool SpawnRule::allowsBiome(BiomeKind biome) const {
if (biomes.empty()) return true;
return std::find(biomes.begin(), biomes.end(), biome) != biomes.end();
}
bool SpawnRule::allowsGround(Block block) const {
if (groundBlocks.empty()) return isSolid(block);
return std::find(groundBlocks.begin(), groundBlocks.end(), block) != groundBlocks.end();
}
std::vector<SpawnRule> defaultSpawnRules() {
std::vector<SpawnRule> rules;
rules.push_back(makeHostileRule(Species::Zombie, 2, 4));
rules.push_back(makeHostileRule(Species::Skeleton, 1, 3));
SpawnRule creeper = makeHostileRule(Species::Creeper, 1, 2);
creeper.biomes = {BiomeKind::Plains, BiomeKind::Forest, BiomeKind::Taiga,
BiomeKind::Tundra, BiomeKind::Swamp, BiomeKind::Jungle,
BiomeKind::Savanna, BiomeKind::Mountains, BiomeKind::Desert};
rules.push_back(creeper);
rules.push_back(makePassiveRule(
Species::Sheep, 2, 4,
{BiomeKind::Plains, BiomeKind::Forest, BiomeKind::Savanna, BiomeKind::Taiga}));
rules.push_back(makePassiveRule(
Species::Cow, 2, 4,
{BiomeKind::Plains, BiomeKind::Forest, BiomeKind::Savanna, BiomeKind::Taiga}));
rules.push_back(makePassiveRule(
Species::Pig, 1, 3, {BiomeKind::Plains, BiomeKind::Forest, BiomeKind::Swamp}));
return rules;
}
const SpawnRule* findSpawnRule(const std::vector<SpawnRule>& rules, Species species) {
for (const SpawnRule& rule : rules) {
if (rule.species == species) return &rule;
}
return nullptr;
}
int MobCap::baseCapFor(SpawnCategory category) const {
return baseCap[static_cast<std::size_t>(category)];
}
int MobCap::capFor(SpawnCategory category, int loadedChunks) const {
if (loadedChunks <= 0) return 0;
long long scaled = static_cast<long long>(baseCapFor(category)) * loadedChunks;
return static_cast<int>(scaled / chunksPerCapUnit);
}
int SpawnEnvironment::populationOf(SpawnCategory category) const {
return population[static_cast<std::size_t>(category)];
}
void SpawnEnvironment::addPopulation(SpawnCategory category, int amount) {
population[static_cast<std::size_t>(category)] += amount;
}
BiomeKind SpawnEnvironment::biomeAt(int gx, int gz) const {
return biomes ? biomes->biomeAt(gx, gz) : fallbackBiome;
}
int SpawnEnvironment::blockLightAt(const Vec3i& cell) const {
return static_cast<int>(light->blockAtWorld(cell.x, cell.y, cell.z));
}
int SpawnEnvironment::skyLightAt(const Vec3i& cell) const {
int raw = static_cast<int>(light->sunAtWorld(cell.x, cell.y, cell.z));
if (daytime) return raw;
return std::max(0, raw - nightSkyLightPenalty);
}
uint64_t spawnHash(uint64_t seed, int x, int y, int z, int salt) {
uint64_t mixed = seed;
mixed ^= static_cast<uint64_t>(static_cast<uint32_t>(x)) * 0x9e3779b97f4a7c15ull;
mixed ^= static_cast<uint64_t>(static_cast<uint32_t>(y)) * 0xc2b2ae3d27d4eb4full;
mixed ^= static_cast<uint64_t>(static_cast<uint32_t>(z)) * 0x165667b19e3779f9ull;
mixed ^= static_cast<uint64_t>(static_cast<uint32_t>(salt)) * 0x27d4eb2f165667c5ull;
mixed ^= mixed >> 33;
mixed *= 0xff51afd7ed558ccdull;
mixed ^= mixed >> 33;
mixed *= 0xc4ceb9fe1a85ec53ull;
mixed ^= mixed >> 33;
return mixed;
}
bool hasSpawnRoom(const World& world, const Vec3i& cell, int headroom) {
for (int step = 0; step < headroom; ++step) {
if (world.solidAt(cell.x, cell.y + step, cell.z)) return false;
}
return true;
}
SpawnDenial checkSpawnRule(const SpawnRule& rule, const SpawnEnvironment& env, const Vec3i& cell,
const MobCap& cap) {
if (env.populationOf(rule.category) >= cap.capFor(rule.category, env.loadedChunks))
return SpawnDenial::CategoryCapReached;
if (rule.dimension != env.dimension) return SpawnDenial::WrongDimension;
if (cell.y < rule.minY || cell.y > rule.maxY) return SpawnDenial::HeightOutOfRange;
if (!rule.allowsBiome(env.biomeAt(cell.x, cell.z))) return SpawnDenial::WrongBiome;
if (env.daytime && !rule.dayAllowed) return SpawnDenial::WrongTimeOfDay;
if (!env.daytime && !rule.nightAllowed) return SpawnDenial::WrongTimeOfDay;
Block ground = env.world->blockAt(cell.x, cell.y - 1, cell.z);
if (!isSolid(ground) || !rule.allowsGround(ground)) return SpawnDenial::UnsuitableGround;
if (!hasSpawnRoom(*env.world, cell, rule.requiredHeadroom)) return SpawnDenial::NoHeadroom;
int blockLight = env.blockLightAt(cell);
int skyLight = env.skyLightAt(cell);
if (blockLight > rule.maxBlockLight) return SpawnDenial::BlockLightTooHigh;
if (skyLight > rule.maxSkyLight) return SpawnDenial::SkyLightTooHigh;
if (blockLight < rule.minBlockLight) return SpawnDenial::LightTooLow;
if (skyLight < rule.minSkyLight) return SpawnDenial::LightTooLow;
if (env.playerPresent) {
Vec3 spot = behaviorCellCenter(cell);
float distance = straightDistance(spot, env.playerPosition);
if (distance < rule.minPlayerDistance) return SpawnDenial::TooCloseToPlayer;
if (distance > rule.maxPlayerDistance) return SpawnDenial::TooFarFromPlayer;
}
return SpawnDenial::None;
}
bool pickSpawnCell(const SpawnEnvironment& env, const Vec3i& chunkColumn, uint64_t seed,
int attemptIndex, Vec3i& outCell) {
uint64_t roll = spawnHash(seed, chunkColumn.x, attemptIndex, chunkColumn.z, 0x5a);
int localX = static_cast<int>(roll % static_cast<uint64_t>(chunkSize));
int localZ = static_cast<int>((roll >> 8) % static_cast<uint64_t>(chunkSize));
int gx = chunkColumn.x * chunkSize + localX;
int gz = chunkColumn.z * chunkSize + localZ;
for (int y = env.topY; y >= env.bottomY; --y) {
Vec3i probe{gx, y, gz};
if (!isStandable(*env.world, probe, env.path)) continue;
outCell = probe;
return true;
}
return false;
}
std::vector<Vec3> spreadPack(const SpawnRule& rule, const SpawnEnvironment& env,
const Vec3i& origin, int packSize, uint32_t& rng) {
std::vector<Vec3> members;
members.push_back(behaviorCellCenter(origin));
for (int index = 1; index < packSize; ++index) {
int dx = behaviorRandomRange(rng, -rule.packSpread, rule.packSpread);
int dz = behaviorRandomRange(rng, -rule.packSpread, rule.packSpread);
if (dx == 0 && dz == 0) continue;
bool placed = false;
for (int dy = 0; dy <= 2 && !placed; ++dy) {
for (int sign = 1; sign >= -1 && !placed; sign -= 2) {
Vec3i probe{origin.x + dx, origin.y + dy * sign, origin.z + dz};
if (!groundAndRoomFit(rule, env, probe)) continue;
if (!lightFits(rule, env, probe)) continue;
members.push_back(behaviorCellCenter(probe));
placed = true;
}
}
}
return members;
}
DespawnBand despawnBandFor(const DespawnPolicy& policy, const Vec3& position,
const Vec3& playerPosition) {
float distance = straightDistance(position, playerPosition);
if (distance > policy.instantDistance) return DespawnBand::Immediate;
if (distance > policy.randomDistance) return DespawnBand::Random;
return DespawnBand::Protected;
}
bool despawnCandidateIsProtected(const DespawnPolicy& policy, const DespawnCandidate& candidate) {
if (candidate.persistent) return true;
if (candidate.named && !policy.despawnNamed) return true;
if (candidate.leashed && !policy.despawnLeashed) return true;
return false;
}
bool shouldDespawn(const DespawnPolicy& policy, const DespawnCandidate& candidate,
const Vec3& playerPosition, uint32_t& rng) {
if (despawnCandidateIsProtected(policy, candidate)) return false;
DespawnBand band = despawnBandFor(policy, candidate.position, playerPosition);
if (band == DespawnBand::Immediate) return true;
if (band == DespawnBand::Protected) return false;
uint32_t denominator = static_cast<uint32_t>(std::max(1, policy.randomChanceDenominator));
return behaviorNextRandom(rng) % denominator == 0u;
}
SpawnerTickResult tickSpawnerBlock(SpawnerBlockState& spawner, const SpawnEnvironment& env,
int nearbyOfKind) {
SpawnerTickResult outcome;
outcome.delayAfter = spawner.delay;
Vec3 blockCenter = behaviorCellCenter(spawner.position);
outcome.playerInRange =
env.playerPresent && straightDistance(blockCenter, env.playerPosition) <=
spawner.activationRange;
if (!outcome.playerInRange) {
outcome.denial = SpawnDenial::TooFarFromPlayer;
return outcome;
}
if (spawner.delay > 0) {
--spawner.delay;
outcome.delayAfter = spawner.delay;
return outcome;
}
if (nearbyOfKind >= spawner.nearbyLimit) {
outcome.denial = SpawnDenial::CategoryCapReached;
spawner.delay = behaviorRandomRange(spawner.rng, spawner.minDelay, spawner.maxDelay);
outcome.delayAfter = spawner.delay;
return outcome;
}
for (int index = 0; index < spawner.spawnCount; ++index) {
int dx = behaviorRandomRange(spawner.rng, -spawner.spawnRange, spawner.spawnRange);
int dy = behaviorRandomRange(spawner.rng, -1, 1);
int dz = behaviorRandomRange(spawner.rng, -spawner.spawnRange, spawner.spawnRange);
Vec3i probe{spawner.position.x + dx, spawner.position.y + dy, spawner.position.z + dz};
if (!env.world->solidAt(probe.x, probe.y - 1, probe.z)) continue;
if (!hasSpawnRoom(*env.world, probe, spawner.requiredHeadroom)) continue;
outcome.positions.push_back(behaviorCellCenter(probe));
}
if (outcome.positions.empty()) {
outcome.denial = SpawnDenial::NoHeadroom;
} else {
outcome.fired = true;
spawner.totalSpawned += static_cast<int>(outcome.positions.size());
++spawner.cyclesFired;
}
spawner.delay = behaviorRandomRange(spawner.rng, spawner.minDelay, spawner.maxDelay);
outcome.delayAfter = spawner.delay;
return outcome;
}
Spawner::Spawner() : ruleTable(defaultSpawnRules()) {}
Spawner::Spawner(std::vector<SpawnRule> rules, const MobCap& caps)
: ruleTable(std::move(rules)), capTable(caps) {}
const SpawnRule* Spawner::pickRule(const SpawnEnvironment& env, SpawnCategory category,
uint64_t seed) const {
int totalWeight = 0;
for (const SpawnRule& rule : ruleTable) {
if (rule.category != category) continue;
if (rule.dimension != env.dimension) continue;
if (rule.weight <= 0) continue;
totalWeight += rule.weight;
}
if (totalWeight == 0) return nullptr;
int pick = static_cast<int>(seed % static_cast<uint64_t>(totalWeight));
for (const SpawnRule& rule : ruleTable) {
if (rule.category != category) continue;
if (rule.dimension != env.dimension) continue;
if (rule.weight <= 0) continue;
pick -= rule.weight;
if (pick < 0) return &rule;
}
return nullptr;
}
SpawnTrial Spawner::planSpawn(const SpawnEnvironment& env, SpawnCategory category,
const Vec3i& chunkColumn, uint64_t seed, int attemptIndex) const {
SpawnTrial trial;
trial.category = category;
uint64_t ruleSeed = spawnHash(seed, chunkColumn.x, attemptIndex, chunkColumn.z, 0x11);
const SpawnRule* rule = pickRule(env, category, ruleSeed);
if (rule == nullptr) return trial;
trial.ruleFound = true;
trial.species = rule->species;
trial.category = rule->category;
if (!pickSpawnCell(env, chunkColumn, seed, attemptIndex, trial.cell)) return trial;
trial.cellFound = true;
uint32_t packRng =
static_cast<uint32_t>(spawnHash(seed, trial.cell.x, trial.cell.y, trial.cell.z, 0x9c));
trial.packSize = behaviorRandomRange(packRng, rule->packMin, rule->packMax);
return trial;
}
SpawnResult Spawner::attemptSpawn(const SpawnEnvironment& env, const SpawnRule& rule,
const SpawnTrial& trial, uint64_t seed) {
SpawnResult result;
result.species = rule.species;
result.category = rule.category;
result.role = roleForSpecies(rule.species);
result.origin = trial.cell;
result.denial = checkSpawnRule(rule, env, trial.cell, capTable);
if (result.denial != SpawnDenial::None) {
++denials[static_cast<std::size_t>(result.denial)];
return result;
}
uint32_t rng = static_cast<uint32_t>(
spawnHash(seed, trial.cell.x, trial.cell.y, trial.cell.z, 0x9c));
result.positions = spreadPack(rule, env, trial.cell, trial.packSize, rng);
result.spawned = true;
++spawned;
return result;
}
SpawnResult Spawner::runColumn(SpawnEnvironment& env, SpawnCategory category,
const Vec3i& chunkColumn, uint64_t seed, int attemptIndex) {
SpawnTrial trial = planSpawn(env, category, chunkColumn, seed, attemptIndex);
SpawnResult result;
result.category = category;
result.species = trial.species;
if (!trial.ruleFound) {
result.denial = SpawnDenial::NoRuleMatched;
++denials[static_cast<std::size_t>(result.denial)];
return result;
}
if (!trial.cellFound) {
result.denial = SpawnDenial::NoStandableCell;
++denials[static_cast<std::size_t>(result.denial)];
return result;
}
uint64_t ruleSeed = spawnHash(seed, chunkColumn.x, attemptIndex, chunkColumn.z, 0x11);
result = attemptSpawn(env, *findSpawnRule(ruleTable, trial.species), trial, ruleSeed);
if (result.spawned) env.addPopulation(category, result.packSize());
return result;
}
std::vector<SpawnResult> Spawner::runCycle(SpawnEnvironment& env, SpawnCategory category,
const std::vector<Vec3i>& chunkColumns, uint64_t seed) {
std::vector<SpawnResult> results;
results.reserve(chunkColumns.size() * static_cast<std::size_t>(attemptsPerColumnCount));
for (const Vec3i& column : chunkColumns) {
for (int attempt = 0; attempt < attemptsPerColumnCount; ++attempt) {
results.push_back(runColumn(env, category, column, seed, attempt));
}
}
return results;
}
int Spawner::denialCount(SpawnDenial denial) const {
return denials[static_cast<std::size_t>(denial)];
}
void Spawner::resetStats() {
denials.fill(0);
spawned = 0;
}
std::vector<BehaviorAgent> agentsFromSpawn(const SpawnResult& result, uint32_t seed) {
std::vector<BehaviorAgent> agents;
if (!result.spawned) return agents;
const SpeciesStats& stats = speciesStats(result.species);
agents.reserve(result.positions.size());
uint32_t stream = seed != 0u ? seed : 0x9e3779b9u;
for (const Vec3& spot : result.positions) {
BehaviorAgent agent;
agent.species = result.species;
agent.position = spot;
agent.homePosition = spot;
agent.health = stats.maxHealth;
agent.maxHealth = stats.maxHealth;
agent.moveStep = stats.walkSpeed / static_cast<float>(ticksPerSecond);
agent.rng = behaviorNextRandom(stream);
agents.push_back(agent);
}
return agents;
}
} // namespace vf