#include "WaveSystem.h" #include #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 remaining; remaining.reserve(m_pendingSpawns.size()); for (const SpawnEntry& entry : m_pendingSpawns) { if (currentTick >= entry.spawnAt) { ships.spawn(entry.schematicId, 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(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::composeWave(Tick currentTick, int worldHeightTiles) { const double elapsedSeconds = static_cast(currentTick) * kTickDurationSeconds; const int shipLevel = std::max(1, static_cast( m_config.world.waves.shipLevelFormula.evaluate(elapsedSeconds))); // Build eligible ship list with their costs at the current level. struct EligibleShip { std::string schematicId; double cost; }; 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; 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( 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); 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(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); }