implement waves
This commit is contained in:
191
src/lib/sim/WaveSystem.cpp
Normal file
191
src/lib/sim/WaveSystem.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
#include "WaveSystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "ShipSystem.h"
|
||||
|
||||
WaveSystem::WaveSystem(const GameConfig& config, std::mt19937& rng)
|
||||
: m_config(config)
|
||||
, m_rng(rng)
|
||||
{
|
||||
// Draw the initial inter-wave gap (REQ-WAV-GAP, REQ-WAV-GRACE-PERIOD).
|
||||
m_nextEventTick = drawGapTicks();
|
||||
}
|
||||
|
||||
void WaveSystem::tickWaveScheduler(Tick currentTick, ShipSystem& ships,
|
||||
int worldHeightTiles)
|
||||
{
|
||||
if (!m_waveActive)
|
||||
{
|
||||
if (currentTick < m_nextEventTick)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Gap expired: compose the next wave.
|
||||
m_pendingSpawns = composeWave(currentTick, worldHeightTiles);
|
||||
m_waveActive = true;
|
||||
}
|
||||
|
||||
// Spawn any ships whose scheduled tick has arrived.
|
||||
std::vector<SpawnEntry> remaining;
|
||||
remaining.reserve(m_pendingSpawns.size());
|
||||
for (const SpawnEntry& entry : m_pendingSpawns)
|
||||
{
|
||||
if (currentTick >= entry.spawnAt)
|
||||
{
|
||||
ships.spawn(entry.blueprintId, entry.level, entry.position,
|
||||
/*isEnemy=*/true);
|
||||
}
|
||||
else
|
||||
{
|
||||
remaining.push_back(entry);
|
||||
}
|
||||
}
|
||||
m_pendingSpawns = std::move(remaining);
|
||||
|
||||
if (m_pendingSpawns.empty())
|
||||
{
|
||||
m_waveActive = false;
|
||||
m_nextEventTick = currentTick + drawGapTicks();
|
||||
}
|
||||
}
|
||||
|
||||
void WaveSystem::tickThreatAccumulation(Tick currentTick)
|
||||
{
|
||||
const double elapsedSeconds = static_cast<double>(currentTick) * kTickDurationSeconds;
|
||||
const double rate = m_config.world.waves.threatRateFormula.evaluate(elapsedSeconds);
|
||||
if (rate > 0.0)
|
||||
{
|
||||
m_threatLevel += rate * m_pushScalingMultiplier * kTickDurationSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
void WaveSystem::applyPush()
|
||||
{
|
||||
m_pushScalingMultiplier *= m_config.world.push.scalingFactor;
|
||||
++m_generation;
|
||||
}
|
||||
|
||||
double WaveSystem::threatLevel() const
|
||||
{
|
||||
return m_threatLevel;
|
||||
}
|
||||
|
||||
int WaveSystem::generation() const
|
||||
{
|
||||
return m_generation;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
std::vector<WaveSystem::SpawnEntry> WaveSystem::composeWave(Tick currentTick,
|
||||
int worldHeightTiles)
|
||||
{
|
||||
const double elapsedSeconds = static_cast<double>(currentTick) * kTickDurationSeconds;
|
||||
const int shipLevel = std::max(1, static_cast<int>(
|
||||
m_config.world.waves.shipLevelFormula.evaluate(elapsedSeconds)));
|
||||
|
||||
// Build eligible ship list with their costs at the current level.
|
||||
struct EligibleShip
|
||||
{
|
||||
std::string blueprintId;
|
||||
double cost;
|
||||
};
|
||||
std::vector<EligibleShip> eligible;
|
||||
for (const ShipDef& def : m_config.ships.ships)
|
||||
{
|
||||
const double cost = def.threat.costFormula.evaluate(static_cast<double>(shipLevel));
|
||||
if (cost > 0.0)
|
||||
{
|
||||
EligibleShip es;
|
||||
es.blueprintId = def.id;
|
||||
es.cost = cost;
|
||||
eligible.push_back(es);
|
||||
}
|
||||
}
|
||||
|
||||
if (eligible.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
// Take the current threat level as the wave budget and reset it.
|
||||
// Unspent budget is re-added after composition (carry-over).
|
||||
double budget = m_threatLevel;
|
||||
m_threatLevel = 0.0;
|
||||
|
||||
// Enemy spawn buffer X range for the current generation.
|
||||
const float leftX = static_cast<float>(
|
||||
m_config.world.regions.playerBufferWidth
|
||||
+ m_config.world.regions.contestZoneWidth
|
||||
+ m_generation * m_config.world.push.pushExpandColumns);
|
||||
const float rightX = leftX + static_cast<float>(m_config.world.regions.enemyBufferWidth) - 1.0f;
|
||||
|
||||
std::uniform_real_distribution<float> xDist(leftX, rightX);
|
||||
std::uniform_int_distribution<int> yDist(0, worldHeightTiles - 1);
|
||||
|
||||
std::vector<SpawnEntry> picked;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Collect indices of ships whose cost fits the remaining budget.
|
||||
std::vector<std::size_t> fitting;
|
||||
for (std::size_t i = 0; i < eligible.size(); ++i)
|
||||
{
|
||||
if (eligible[i].cost <= budget)
|
||||
{
|
||||
fitting.push_back(i);
|
||||
}
|
||||
}
|
||||
if (fitting.empty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
std::uniform_int_distribution<int> pick(0, static_cast<int>(fitting.size()) - 1);
|
||||
const std::size_t chosenIdx = fitting[static_cast<std::size_t>(pick(m_rng))];
|
||||
const EligibleShip& chosen = eligible[chosenIdx];
|
||||
|
||||
budget -= chosen.cost;
|
||||
|
||||
SpawnEntry entry;
|
||||
entry.blueprintId = chosen.blueprintId;
|
||||
entry.level = shipLevel;
|
||||
entry.spawnAt = 0; // set below after all picks are done
|
||||
entry.position = QVector2D(xDist(m_rng),
|
||||
static_cast<float>(yDist(m_rng)) + 0.5f);
|
||||
picked.push_back(entry);
|
||||
}
|
||||
|
||||
// Carry leftover budget forward to the next wave.
|
||||
m_threatLevel += budget;
|
||||
|
||||
// Spread spawn times evenly across spawnDurationSeconds.
|
||||
const int count = static_cast<int>(picked.size());
|
||||
if (count == 1)
|
||||
{
|
||||
picked[0].spawnAt = currentTick;
|
||||
}
|
||||
else if (count > 1)
|
||||
{
|
||||
const Tick spawnDurationTicks =
|
||||
secondsToTicks(m_config.world.waves.spawnDurationSeconds);
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
picked[static_cast<std::size_t>(i)].spawnAt =
|
||||
currentTick + static_cast<Tick>(i) * spawnDurationTicks / (count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return picked;
|
||||
}
|
||||
|
||||
Tick WaveSystem::drawGapTicks()
|
||||
{
|
||||
const Tick minTicks = secondsToTicks(m_config.world.waves.gapMinSeconds);
|
||||
const Tick maxTicks = secondsToTicks(m_config.world.waves.gapMaxSeconds);
|
||||
std::uniform_int_distribution<Tick> dist(minTicks, maxTicks);
|
||||
return dist(m_rng);
|
||||
}
|
||||
Reference in New Issue
Block a user