osb/source/game/StarSpawner.cpp

351 lines
13 KiB
C++
Raw Permalink Normal View History

2023-06-20 04:33:09 +00:00
#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 = std::move(facade);
2023-06-20 04:33:09 +00:00
}
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(float dt) {
2023-06-20 04:33:09 +00:00
if (!m_facade)
return;
for (auto const& window : m_facade->clientWindows()) {
if (window != RectF())
activateRegion(window.padded(m_windowActivationBorder));
}
eraseWhere(m_activeSpawnCells, [dt](auto& p) {
return (p.second -= dt) < 0.0f;
});
2023-06-20 04:33:09 +00:00
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));
2023-06-27 10:23:44 +00:00
SpatialLogger::logText("world", strf("Areas: {}", areaList.join(", ")), spawnRegion.min() + Vec2F(0.5, 2.5), {255, 255, 255, 255});
SpatialLogger::logText("world", strf("Region: {}", SpawnParameters::RegionNames.getRight(debugInfo.second.spawnParameters.region)), spawnRegion.min() + Vec2F(0.5, 1.5), {255, 255, 255, 255});
SpatialLogger::logText("world", strf("Time: {}", SpawnParameters::TimeNames.getRight(debugInfo.second.spawnParameters.time)), spawnRegion.min() + Vec2F(0.5, 0.5), {255, 255, 255, 255});
2023-06-20 04:33:09 +00:00
if (debugInfo.second.spawnAttempts > 0)
2023-06-27 10:23:44 +00:00
SpatialLogger::logText("world", strf("Spawns: {} / {}", 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());
2023-06-20 04:33:09 +00:00
}
}
}
}