diff --git a/core/src/Spawning.cpp b/core/src/Spawning.cpp index 2e7c7d1..6fce226 100644 --- a/core/src/Spawning.cpp +++ b/core/src/Spawning.cpp @@ -312,3 +312,187 @@ SpawnerTickResult tickSpawnerBlock(SpawnerBlockState& spawner, const SpawnEnviro 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(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 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(seed % static_cast(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(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(result.denial)]; + return result; + } + + uint32_t rng = static_cast( + 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(result.denial)]; + return result; + } + if (!trial.cellFound) { + result.denial = SpawnDenial::NoStandableCell; + ++denials[static_cast(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 Spawner::runCycle(SpawnEnvironment& env, SpawnCategory category, + const std::vector& chunkColumns, uint64_t seed) { + std::vector results; + results.reserve(chunkColumns.size() * static_cast(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(denial)]; +} + +void Spawner::resetStats() { + denials.fill(0); + spawned = 0; +} + +std::vector agentsFromSpawn(const SpawnResult& result, uint32_t seed) { + std::vector 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(ticksPerSecond); + agent.rng = behaviorNextRandom(stream); + agents.push_back(agent); + } + return agents; +} + +} // namespace vf