#include "WaveSystem.h" #include #include "ShipSystem.h" WaveSystem::WaveSystem(const GameConfig& config, std::mt19937& rng) : m_config(config) , m_rng(rng) { m_bossCountdownTicks = secondsToTicks(config.world.waves.bossCountdownSeconds); m_normalGapRemainingTicks = drawGapTicks(); } void WaveSystem::tickWaveScheduler(Tick currentTick, ShipSystem& ships, int worldHeightTiles) { // 1. Advance boss countdown. --m_bossCountdownTicks; // 2. Trigger boss wave when countdown expires. if (m_bossCountdownTicks <= 0) { triggerBossWave(currentTick, worldHeightTiles); } // 3. Advance post-boss quiet window. if (m_postBossQuietRemainingTicks > 0) { --m_postBossQuietRemainingTicks; } // 4. Normal wave gap: only advances outside quiet windows and between waves. if (!isInQuietWindow() && !m_normalWaveActive) { if (m_normalGapRemainingTicks > 0) { --m_normalGapRemainingTicks; } if (m_normalGapRemainingTicks == 0) { triggerNormalWave(currentTick, worldHeightTiles); } } // 5. Spawn any ships (from either queue) whose scheduled tick has arrived. auto spawnDue = [&](std::vector& queue) { std::vector remaining; remaining.reserve(queue.size()); for (const SpawnEntry& entry : queue) { if (currentTick >= entry.spawnAt) { ships.spawn(entry.schematicId, entry.level, entry.position, /*isEnemy=*/true, entry.layout); } else { remaining.push_back(entry); } } queue = std::move(remaining); }; spawnDue(m_normalPendingSpawns); spawnDue(m_bossPendingSpawns); // 6. When all normal wave ships have spawned, draw the next gap. if (m_normalWaveActive && m_normalPendingSpawns.empty()) { m_normalWaveActive = false; m_normalGapRemainingTicks = drawGapTicks(); } } void WaveSystem::tickThreatAccumulation() { const double x = static_cast(m_bossWaveCounter); const double rate = m_config.world.waves.threatRateFormula.evaluate(x); if (rate > 0.0) { m_threatLevel += rate * kTickDurationSeconds; } } void WaveSystem::onEnemyStationsDestroyed() { const Tick advance = secondsToTicks(m_config.world.push.bossAdvanceSeconds); m_bossCountdownTicks = std::max(Tick{0}, m_bossCountdownTicks - advance); ++m_generation; } double WaveSystem::threatLevel() const { return m_threatLevel; } int WaveSystem::generation() const { return m_generation; } int WaveSystem::bossWaveCounter() const { return m_bossWaveCounter; } Tick WaveSystem::bossCountdownTicks() const { return m_bossCountdownTicks; } // --------------------------------------------------------------------------- // Private helpers // --------------------------------------------------------------------------- bool WaveSystem::isInQuietWindow() const { if (m_postBossQuietRemainingTicks > 0) { return true; } const Tick quietBeforeTicks = secondsToTicks(m_config.world.waves.bossQuietBeforeSeconds); return m_bossCountdownTicks <= quietBeforeTicks; } void WaveSystem::triggerNormalWave(Tick currentTick, int worldHeightTiles) { double budget = m_threatLevel; m_threatLevel = 0.0; m_normalPendingSpawns = selectWaveShips(budget, currentTick, worldHeightTiles); m_threatLevel += budget; // carry leftover forward m_normalWaveActive = true; } void WaveSystem::triggerBossWave(Tick currentTick, int worldHeightTiles) { const double x = static_cast(m_bossWaveCounter); const double rate = std::max(0.0, m_config.world.waves.threatRateFormula.evaluate(x)); double budget = rate * m_config.world.waves.bossThreatDurationSeconds + m_threatLevel; m_threatLevel = 0.0; m_bossPendingSpawns = selectWaveShips(budget, currentTick, worldHeightTiles); m_threatLevel += budget; // carry leftover to first normal wave of new cycle m_bossCountdownTicks = secondsToTicks(m_config.world.waves.bossCountdownSeconds); m_postBossQuietRemainingTicks = secondsToTicks(m_config.world.waves.bossQuietAfterSeconds); ++m_bossWaveCounter; } std::vector WaveSystem::selectWaveShips(double& budget, Tick currentTick, int worldHeightTiles) { const int shipLevel = std::max(1, static_cast( m_config.world.waves.shipLevelFormula.evaluate( static_cast(m_bossWaveCounter)))); // Build eligible ship list with their costs at the current level. struct EligibleShip { std::string schematicId; double cost; std::vector defaultModules; }; std::vector eligible; for (const ShipDef& def : m_config.ships.ships) { const double cost = def.threat.costFormula.evaluate(static_cast(shipLevel)); if (cost > 0.0) { EligibleShip es; es.schematicId = def.id; es.cost = cost; es.defaultModules = def.defaultModules; eligible.push_back(es); } } if (eligible.empty()) { return {}; } // Enemy spawn buffer X range for the current generation. const float leftX = static_cast( m_config.world.regions.playerBufferWidth + m_config.world.regions.contestZoneWidth + m_generation * m_config.world.push.pushExpandColumns); const float rightX = leftX + static_cast(m_config.world.regions.enemyBufferWidth) - 1.0f; std::uniform_real_distribution xDist(leftX, rightX); std::uniform_int_distribution yDist(0, worldHeightTiles - 1); std::vector picked; while (true) { // Collect indices of ships whose cost fits the remaining budget. std::vector 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 pick(0, static_cast(fitting.size()) - 1); const std::size_t chosenIdx = fitting[static_cast(pick(m_rng))]; const EligibleShip& chosen = eligible[chosenIdx]; budget -= chosen.cost; SpawnEntry entry; entry.schematicId = chosen.schematicId; entry.level = shipLevel; entry.spawnAt = 0; // set below after all picks are done entry.position = QVector2D(xDist(m_rng), static_cast(yDist(m_rng)) + 0.5f); entry.layout.placedModules = chosen.defaultModules; picked.push_back(entry); } // Spread spawn times evenly across spawnDurationSeconds. const int count = static_cast(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(i)].spawnAt = currentTick + static_cast(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 dist(minTicks, maxTicks); return dist(m_rng); }