diff options
Diffstat (limited to 'source/game/StarSpawner.cpp')
-rw-r--r-- | source/game/StarSpawner.cpp | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/source/game/StarSpawner.cpp b/source/game/StarSpawner.cpp new file mode 100644 index 0000000..9fe2d00 --- /dev/null +++ b/source/game/StarSpawner.cpp @@ -0,0 +1,351 @@ +#include "StarSpawner.hpp" +#include "StarSpawnTypeDatabase.hpp" +#include "StarRandom.hpp" +#include "StarJsonExtra.hpp" +#include "StarPlayer.hpp" +#include "StarRoot.hpp" +#include "StarAssets.hpp" +#include "StarMonster.hpp" +#include "StarWeightedPool.hpp" +#include "StarLogging.hpp" + +namespace Star { + +Spawner::Spawner() { + auto assets = Root::singleton().assets(); + auto config = assets->json("/spawning.config"); + + m_spawnCellSize = config.getUInt("spawnCellSize"); + m_spawnCellMinimumEmptyTiles = config.getUInt("spawnCellMinimumEmptyTiles"); + m_spawnCellMinimumLiquidTiles = config.getUInt("spawnCellMinimumLiquidTiles"); + m_spawnCellMinimumNearSurfaceTiles = config.getUInt("spawnCellMinimumNearSurfaceTiles"); + m_spawnCellMinimumNearCeilingTiles = config.getUInt("spawnCellMinimumNearCeilingTiles"); + m_spawnCellMinimumAirTiles = config.getUInt("spawnCellMinimumAirTiles"); + m_spawnCellMinimumExposedTiles = config.getUInt("spawnCellMinimumExposedTiles"); + m_spawnCellNearSurfaceDistance = config.getUInt("spawnCellNearSurfaceDistance"); + m_spawnCellNearCeilingDistance = config.getUInt("spawnCellNearCeilingDistance"); + + m_minimumDayLevel = config.getFloat("minimumDayLevel"); + m_minimumLiquidLevel = config.getFloat("minimumLiquidLevel"); + m_spawnCheckResolution = config.getFloat("spawnCheckResolution"); + m_spawnSurfaceCheckDistance = config.getInt("spawnSurfaceCheckDistance"); + m_spawnCeilingCheckDistance = config.getInt("spawnCeilingCheckDistance"); + m_spawnProhibitedCheckPadding = config.getFloat("spawnProhibitedCheckPadding"); + + m_spawnCellLifetime = config.getFloat("spawnCellLifetime"); + m_windowActivationBorder = config.getUInt("windowActivationBorder"); + + m_active = config.getBool("defaultActive", true); + + m_debug = config.getBool("debug", false); +} + +void Spawner::init(SpawnerFacadePtr facade) { + m_facade = move(facade); +} + +void Spawner::uninit() { + for (auto entityId : m_spawnedEntities) + m_facade->despawnEntity(entityId); + m_facade.reset(); +} + +bool Spawner::active() const { + return m_active; +} + +void Spawner::setActive(bool active) { + m_active = active; +} + +void Spawner::activateRegion(RectF region) { + for (auto const& cell : cellIndexesForRange(region)) { + if (m_facade && m_facade->signalRegion(cellRegion(cell))) { + if (m_active && !m_activeSpawnCells.contains(cell)) + spawnInCell(cell); + m_activeSpawnCells[cell] = m_spawnCellLifetime; + } + } +} + +void Spawner::activateEmptyRegion(RectF region) { + for (auto const& cell : cellIndexesForRange(region)) + m_activeSpawnCells[cell] = m_spawnCellLifetime; +} + +void Spawner::update() { + if (!m_facade) + return; + + for (auto const& window : m_facade->clientWindows()) { + if (window != RectF()) + activateRegion(window.padded(m_windowActivationBorder)); + } + + eraseWhere(m_activeSpawnCells, [](auto& p) { + p.second -= WorldTimestep; + return p.second < 0.0f; + }); + + eraseWhere(m_spawnedEntities, [this](EntityId entityId) { + auto entity = m_facade->getEntity(entityId); + if (!entity) + return true; + + if (!m_activeSpawnCells.contains(cellIndexForPosition(entity->position()))) { + m_facade->despawnEntity(entity->entityId()); + return true; + } + + return false; + }); + + if (m_active && m_debug) + debugShowSpawnCells(); +} + +Vec2I Spawner::cellIndexForPosition(Vec2F const& position) const { + return Vec2I::floor(position / m_spawnCellSize); +} + +List<Vec2I> Spawner::cellIndexesForRange(RectF const& range) const { + List<Vec2I> cellIndexes; + for (auto srange : m_facade->geometry().splitRect(range)) { + auto indexes = RectI::integral(RectF(srange).scaled(1.0f / m_spawnCellSize)); + for (int x = indexes.xMin(); x < indexes.xMax(); ++x) { + for (int y = indexes.yMin(); y < indexes.yMax(); ++y) + cellIndexes.append({x, y}); + } + } + + return cellIndexes; +} + +RectF Spawner::cellRegion(Vec2I const& cellIndex) const { + return RectF::withSize(Vec2F(cellIndex) * m_spawnCellSize, Vec2F::filled(m_spawnCellSize)); +} + +Maybe<SpawnParameters> Spawner::spawnParametersForCell(Vec2I const& cellIndex) const { + unsigned emptyCount = 0; + unsigned nearSurfaceCount = 0; + unsigned nearCeilingCount = 0; + unsigned airCount = 0; + unsigned liquidCount = 0; + unsigned exposedCount = 0; + + auto region = RectI::withSize(cellIndex * m_spawnCellSize, Vec2I::filled(m_spawnCellSize)); + for (int x = region.xMin(); x < region.xMax(); ++x) { + for (int y = region.yMin(); y < region.yMax(); ++y) { + // Only empty blocks count towards spawn totals + if (m_facade->collision({x, y}) == CollisionKind::None) { + ++emptyCount; + + if (m_facade->liquidLevel({x, y}).level > m_minimumLiquidLevel) + ++liquidCount; + + if (m_facade->isBackgroundEmpty({x, y})) + ++exposedCount; + + // The empty block will will either count as an air block, a + // "near-surface" block, or a "near-ceiling" block. It will count as a + // near-surface block if it is within the NearSurfaceDistance of a + // CollsionKind::Block or CollisionKind::Platform block. If it is not a + // near-surface block, it will count as a near-ceiling block if it is + // within the NearCeilingDistance of a CollisionKind::Block. + bool nearSurface = false; + for (unsigned sd = 1; sd <= m_spawnCellNearSurfaceDistance; ++sd) { + auto collision = m_facade->collision({x, y - sd}); + if (BlockCollisionSet.contains(collision) || collision == CollisionKind::Platform) { + nearSurface = true; + break; + } + } + + bool nearCeiling = false; + if (!nearSurface) { + for (unsigned cd = 1; cd <= m_spawnCellNearCeilingDistance; ++cd) { + auto collision = m_facade->collision({x, y + cd}); + if (BlockCollisionSet.contains(collision)) { + nearCeiling = true; + break; + } + } + } + + if (nearSurface) + ++nearSurfaceCount; + else if (nearCeiling) + ++nearCeilingCount; + else + ++airCount; + } + } + } + + Set<SpawnParameters::Area> spawnAreas; + if (liquidCount > m_spawnCellMinimumLiquidTiles) + spawnAreas.add(SpawnParameters::Area::Liquid); + if (nearSurfaceCount > m_spawnCellMinimumNearSurfaceTiles) + spawnAreas.add(SpawnParameters::Area::Surface); + if (nearCeilingCount > m_spawnCellMinimumNearCeilingTiles) + spawnAreas.add(SpawnParameters::Area::Ceiling); + if (airCount > m_spawnCellMinimumAirTiles) + spawnAreas.add(SpawnParameters::Area::Air); + if (emptyCount < m_spawnCellMinimumEmptyTiles) + spawnAreas.add(SpawnParameters::Area::Solid); + + if (spawnAreas.empty()) + return {}; + + SpawnParameters::Region spawnRegion = SpawnParameters::Region::Enclosed; + if (exposedCount >= m_spawnCellMinimumExposedTiles) + spawnRegion = SpawnParameters::Region::Exposed; + + SpawnParameters::Time spawnTime = SpawnParameters::Time::Night; + if (m_facade->dayLevel() >= m_minimumDayLevel) + spawnTime = SpawnParameters::Time::Day; + + return SpawnParameters(spawnAreas, spawnRegion, spawnTime); +} + +Maybe<Vec2F> Spawner::adjustSpawnRegion(RectF const& spawnRegion, RectF const& boundBox, SpawnParameters const& spawnParameters) const { + auto checkPosition = [&](Vec2F const& position) -> bool { + RectF region = RectF(boundBox).translated(position); + + if (!m_facade->isFreeSpace(region)) + return spawnParameters.areas.contains(SpawnParameters::Area::Solid); + + if (m_facade->liquidLevel(Vec2I::floor(region.center())).level >= m_minimumLiquidLevel) + return spawnParameters.areas.contains(SpawnParameters::Area::Liquid); + + if (m_facade->spawningProhibited(region.padded(m_spawnProhibitedCheckPadding))) + return false; + + if (spawnParameters.areas.contains(SpawnParameters::Area::Air)) + return true; + + if (spawnParameters.areas.contains(SpawnParameters::Area::Surface)) { + Vec2F startCheck = {region.center()[0], region.yMin()}; + for (int sd = 0; sd <= m_spawnSurfaceCheckDistance; ++sd) { + auto collision = m_facade->collision(Vec2I::floor(startCheck - Vec2F(0, sd))); + if (BlockCollisionSet.contains(collision) || collision == CollisionKind::Platform) + return true; + } + + } else if (spawnParameters.areas.contains(SpawnParameters::Area::Ceiling)) { + Vec2F startCheck = {region.center()[0], region.yMax()}; + for (int cd = 0; cd <= m_spawnCeilingCheckDistance; ++cd) { + auto collision = m_facade->collision(Vec2I::floor(startCheck + Vec2F(0, cd))); + if (BlockCollisionSet.contains(collision)) + return true; + } + } + + return false; + }; + + List<Vec2F> tryPositions; + for (float x = spawnRegion.xMin(); x <= spawnRegion.xMax(); x += m_spawnCheckResolution) { + for (float y = spawnRegion.yMin(); y <= spawnRegion.yMax(); y += m_spawnCheckResolution) + tryPositions.append({x, y}); + } + + Random::shuffle(tryPositions); + for (auto const& p : tryPositions) { + if (checkPosition(p)) + return p; + } + + return {}; +} + +void Spawner::spawnInCell(Vec2I const& cell) { + auto cellSpawnParameters = spawnParametersForCell(cell); + if (!cellSpawnParameters) + return; + + if (m_debug) + m_debugSpawnInfo[cell] = SpawnCellDebugInfo{*cellSpawnParameters, 0, 0}; + + auto monsterDatabase = Root::singleton().monsterDatabase(); + auto spawnTypeDatabase = Root::singleton().spawnTypeDatabase(); + + RectF spawnRegion = cellRegion(cell); + auto spawnProfile = m_facade->spawnProfile(spawnRegion.center()); + + for (auto const& spawnTypeName : spawnProfile.spawnTypes) { + auto spawnType = spawnTypeDatabase->spawnType(spawnTypeName); + if (!spawnType.spawnParameters.compatible(*cellSpawnParameters)) + continue; + + if (Random::randf() < spawnType.spawnChance) { + uint64_t spawnSeed = staticRandomU64(spawnType.seedMix, m_facade->spawnSeed()); + int targetGroupSize = Random::randInt(spawnType.groupSize[0], spawnType.groupSize[1]); + for (int i = 0; i < targetGroupSize; ++i) { + String monsterType; + if (auto monsterPool = spawnType.monsterType.maybe<WeightedPool<String>>()) + monsterType = monsterPool->select(); + else + monsterType = spawnType.monsterType.get<String>(); + + auto monsterVariant = monsterDatabase->monsterVariant(monsterType, spawnSeed, spawnType.monsterParameters); + auto monsterBoundBox = monsterVariant.movementSettings.standingPoly->boundBox(); + + if (m_debug) + m_debugSpawnInfo[cell].spawnAttempts++; + + if (auto position = adjustSpawnRegion(spawnRegion, monsterBoundBox, spawnType.spawnParameters)) { + float level = m_facade->threatLevel(); + if (m_facade->dayLevel() >= m_minimumDayLevel) + level += Random::randf(spawnType.dayLevelAdjustment[0], spawnType.dayLevelAdjustment[1]); + else + level += Random::randf(spawnType.nightLevelAdjustment[0], spawnType.nightLevelAdjustment[1]); + + auto spawnProfile = m_facade->spawnProfile(*position); + + auto entity = monsterDatabase->createMonster(monsterVariant, level, spawnProfile.monsterParameters); + entity->setPosition(*position); + entity->setKeepAlive(true); + auto entityId = m_facade->spawnEntity(entity); + if (entityId != NullEntityId) + m_spawnedEntities.add(entityId); + + if (m_debug) + m_debugSpawnInfo[cell].spawns++; + } + } + } + } +} + +void Spawner::debugShowSpawnCells() { + eraseWhere(m_debugSpawnInfo, [this](auto& p) { + return !m_activeSpawnCells.contains(p.first); + }); + + auto regionVisibleToClient = [this](RectF const& region) { + for (auto const& window : m_facade->clientWindows()) { + if (m_facade->geometry().rectIntersectsRect(window, region)) + return true; + } + return false; + }; + + for (auto const& debugInfo : m_debugSpawnInfo) { + RectF spawnRegion = Spawner::cellRegion(debugInfo.first); + if (regionVisibleToClient(spawnRegion)) { + SpatialLogger::logPoly("world", PolyF(spawnRegion), {128, 0, 0, 255}); + StringList areaList; + for (auto area : debugInfo.second.spawnParameters.areas) + areaList.append(SpawnParameters::AreaNames.getRight(area).slice(0, 3)); + SpatialLogger::logText("world", strf("Areas: %s", areaList.join(", ")), spawnRegion.min() + Vec2F(0.5, 2.5), {255, 255, 255, 255}); + SpatialLogger::logText("world", strf("Region: %s", SpawnParameters::RegionNames.getRight(debugInfo.second.spawnParameters.region)), spawnRegion.min() + Vec2F(0.5, 1.5), {255, 255, 255, 255}); + SpatialLogger::logText("world", strf("Time: %s", SpawnParameters::TimeNames.getRight(debugInfo.second.spawnParameters.time)), spawnRegion.min() + Vec2F(0.5, 0.5), {255, 255, 255, 255}); + + if (debugInfo.second.spawnAttempts > 0) + SpatialLogger::logText("world", strf("Spawns: %s / %s", debugInfo.second.spawns, debugInfo.second.spawnAttempts), spawnRegion.min() + Vec2F(0.5, 3.5), (debugInfo.second.spawnAttempts > debugInfo.second.spawns) ? Color::Red.toRgba() : Color::Green.toRgba()); + } + } +} + +} |