implement waves

This commit is contained in:
2026-04-20 14:10:01 +02:00
parent 65de4ddc5c
commit 498b97db20
17 changed files with 1798 additions and 18 deletions

View File

@@ -1,8 +1,13 @@
#include "Simulation.h"
#include <cassert>
#include "BuildingSystem.h"
#include "CombatSystem.h"
#include "ScrapSystem.h"
#include "ShipSystem.h"
#include "SurfaceMask.h"
#include "WaveSystem.h"
Simulation::Simulation(const GameConfig& config, unsigned int seed)
: m_config(config)
@@ -10,8 +15,15 @@ Simulation::Simulation(const GameConfig& config, unsigned int seed)
, m_currentTick(0)
, m_nextId(1)
, m_buildingBlocksStock(config.world.startingBuildingBlocks)
, m_gameOver(false)
, m_hqId(kInvalidEntityId)
, m_playerStation1Id(kInvalidEntityId)
, m_playerStation2Id(kInvalidEntityId)
, m_beltSystem(config.world.beltSpeedTilesPerSecond)
{
m_currentEnemyStationIds[0] = kInvalidEntityId;
m_currentEnemyStationIds[1] = kInvalidEntityId;
m_buildingSystem = std::make_unique<BuildingSystem>(
config,
m_beltSystem,
@@ -20,35 +32,303 @@ Simulation::Simulation(const GameConfig& config, unsigned int seed)
m_rng);
m_shipSystem = std::make_unique<ShipSystem>(config, [this]() { return allocateId(); });
m_scrapSystem = std::make_unique<ScrapSystem>([this]() { return allocateId(); });
m_waveSystem = std::make_unique<WaveSystem>(config, m_rng);
m_combatSystem = std::make_unique<CombatSystem>(config);
// Initialize blueprint unlock state.
for (const ShipDef& def : config.ships.ships)
{
BlueprintState state;
state.unlocked = def.availableFromStart;
state.level = def.availableFromStart ? def.blueprint.playerProductionLevel : 0;
m_blueprintLevels[def.id] = state;
}
placeInitialStructures();
}
Simulation::~Simulation() = default;
// ---------------------------------------------------------------------------
// tick
// ---------------------------------------------------------------------------
void Simulation::tick()
{
m_buildingSystem->tickConstruction(m_currentTick);
m_buildingSystem->tickBeltPull(); // tick order step 3
m_buildingSystem->tickProduction(m_currentTick); // step 4
m_buildingSystem->tickBeltPush(); // step 5
m_beltSystem.tick(); // step 6
// Step 1: wave scheduler
m_waveSystem->tickWaveScheduler(m_currentTick, *m_shipSystem,
m_config.world.heightTiles);
// Step 7: ship behavior systems (movement arbitration via intent priority).
// Step 2: threat accumulation
m_waveSystem->tickThreatAccumulation(m_currentTick);
// Construction + production pipeline
m_buildingSystem->tickConstruction(m_currentTick);
m_buildingSystem->tickBeltPull(); // step 3
m_buildingSystem->tickProduction(m_currentTick); // step 4
m_buildingSystem->tickBeltPush(); // step 5
m_beltSystem.tick(); // step 6
// Step 7: ship behavior systems (movement arbitration via intent priority)
m_shipSystem->clearMovementIntents();
m_shipSystem->tickHomeReturn(); // priority 4
m_shipSystem->tickThreatResponse(*m_buildingSystem); // priority 3
m_shipSystem->tickRepairBehavior(*m_buildingSystem); // priority 2
m_shipSystem->tickScrapCollector(*m_scrapSystem, *m_buildingSystem); // priority 1
// Steps 8 & 9: combat resolution and deaths — added in implementation step 7.
// Step 8: combat resolution
m_combatSystem->tick(m_currentTick, *m_shipSystem,
*m_buildingSystem, m_fireEvents);
// Step 10: advance ship positions from winning intents.
// Step 9: deaths & loot
if (!m_gameOver)
{
tickDeathsAndLoot();
}
// Step 10: advance ship positions
m_shipSystem->tickMovement();
m_scrapSystem->tickDespawn(m_currentTick); // step 11
// Step 11: scrap despawn
m_scrapSystem->tickDespawn(m_currentTick);
++m_currentTick;
}
// ---------------------------------------------------------------------------
// Pre-placement
// ---------------------------------------------------------------------------
void Simulation::placeInitialStructures()
{
// HQ — right edge of asteroid (rightmost asteroid tile is x = -1).
const ParsedSurfaceMask hqParsed =
parseSurfaceMask(m_config.stations.hq.surfaceMask, Rotation::East);
const int hqAnchorX = -hqParsed.footprint.width();
const int hqAnchorY =
(m_config.world.heightTiles - hqParsed.footprint.height()) / 2;
const float hqHp =
static_cast<float>(m_config.stations.hq.hpFormula.evaluate(0.0));
m_hqId = m_buildingSystem->placeImmediate(
BuildingType::Hq,
m_config.stations.hq.surfaceMask,
QPoint(hqAnchorX, hqAnchorY),
Rotation::East, hqHp, hqHp);
// Player defence stations — right edge of player buffer zone.
const ParsedSurfaceMask psParsed =
parseSurfaceMask(m_config.stations.playerStation.surfaceMask, Rotation::East);
const int psAnchorX =
m_config.world.regions.playerBufferWidth - psParsed.footprint.width();
const double psLevel = static_cast<double>(m_config.stations.playerStation.level);
const float psHp = static_cast<float>(
m_config.stations.playerStation.hpFormula.evaluate(psLevel));
StationWeapon psWeapon;
psWeapon.damage = static_cast<float>(
m_config.stations.playerStation.damageFormula.evaluate(psLevel));
psWeapon.range = static_cast<float>(
m_config.stations.playerStation.rangeFormula.evaluate(psLevel));
psWeapon.fireRateHz = static_cast<float>(
m_config.stations.playerStation.fireRateFormula.evaluate(psLevel));
psWeapon.cooldownTicks = 0.0f;
const int ps1Y = m_config.world.heightTiles / 4;
const int ps2Y = 3 * m_config.world.heightTiles / 4;
m_playerStation1Id = m_buildingSystem->placeImmediate(
BuildingType::PlayerDefenceStation,
m_config.stations.playerStation.surfaceMask,
QPoint(psAnchorX, ps1Y), Rotation::East, psHp, psHp);
m_buildingSystem->initStationWeapon(m_playerStation1Id, psWeapon);
m_playerStation2Id = m_buildingSystem->placeImmediate(
BuildingType::PlayerDefenceStation,
m_config.stations.playerStation.surfaceMask,
QPoint(psAnchorX, ps2Y), Rotation::East, psHp, psHp);
m_buildingSystem->initStationWeapon(m_playerStation2Id, psWeapon);
// Enemy defence stations — generation 0 (initial set).
placeEnemyStationSet(0);
}
void Simulation::placeEnemyStationSet(int generation)
{
const ParsedSurfaceMask esParsed =
parseSurfaceMask(m_config.stations.enemyStation.surfaceMask, Rotation::East);
// Right edge of contest zone, shifted right by (generation * pushExpandColumns).
const int rightEdgeX = m_config.world.regions.playerBufferWidth
+ m_config.world.regions.contestZoneWidth
+ generation * m_config.world.push.pushExpandColumns;
const int anchorX = rightEdgeX - esParsed.footprint.width();
const double genD = static_cast<double>(generation);
const float esHp = static_cast<float>(
m_config.stations.enemyStation.hpFormula.evaluate(genD));
StationWeapon esWeapon;
esWeapon.damage = static_cast<float>(
m_config.stations.enemyStation.damageFormula.evaluate(genD));
esWeapon.range = static_cast<float>(
m_config.stations.enemyStation.rangeFormula.evaluate(genD));
esWeapon.fireRateHz = static_cast<float>(
m_config.stations.enemyStation.fireRateFormula.evaluate(genD));
esWeapon.cooldownTicks = 0.0f;
const int y1 = m_config.world.heightTiles / 4;
const int y2 = 3 * m_config.world.heightTiles / 4;
const EntityId id1 = m_buildingSystem->placeImmediate(
BuildingType::EnemyDefenceStation,
m_config.stations.enemyStation.surfaceMask,
QPoint(anchorX, y1), Rotation::East, esHp, esHp);
m_buildingSystem->initStationWeapon(id1, esWeapon);
const EntityId id2 = m_buildingSystem->placeImmediate(
BuildingType::EnemyDefenceStation,
m_config.stations.enemyStation.surfaceMask,
QPoint(anchorX, y2), Rotation::East, esHp, esHp);
m_buildingSystem->initStationWeapon(id2, esWeapon);
m_currentEnemyStationIds[0] = id1;
m_currentEnemyStationIds[1] = id2;
}
// ---------------------------------------------------------------------------
// Deaths & loot (tick step 9)
// ---------------------------------------------------------------------------
void Simulation::tickDeathsAndLoot()
{
// --- Dead ships ---
std::vector<EntityId> deadShipIds;
m_shipSystem->forEach([&deadShipIds](Ship& s)
{
if (s.hp <= 0.0f)
{
deadShipIds.push_back(s.id);
}
});
for (EntityId deadId : deadShipIds)
{
const Ship* s = m_shipSystem->findShip(deadId);
if (!s)
{
continue;
}
// Look up scrap drop amount from config.
for (const ShipDef& def : m_config.ships.ships)
{
if (def.id == s->blueprintId && def.loot.scrapDrop > 0)
{
const Tick despawnAt = m_currentTick
+ secondsToTicks(m_config.world.scrapDespawnSeconds);
m_scrapSystem->spawn(s->position, def.loot.scrapDrop, despawnAt);
break;
}
}
m_shipSystem->despawn(deadId);
}
// --- Dead buildings (HQ, player/enemy defence stations) ---
std::vector<EntityId> deadBuildingIds;
for (const Building& b : m_buildingSystem->allBuildings())
{
if (b.hp <= 0.0f &&
(b.type == BuildingType::Hq ||
b.type == BuildingType::PlayerDefenceStation ||
b.type == BuildingType::EnemyDefenceStation))
{
deadBuildingIds.push_back(b.id);
}
}
for (EntityId deadId : deadBuildingIds)
{
const Building* b = m_buildingSystem->findBuilding(deadId);
if (!b)
{
continue;
}
if (b->type == BuildingType::Hq)
{
m_gameOver = true;
}
else
{
const QVector2D center(
b->anchor.x() + b->footprint.width() / 2.0f,
b->anchor.y() + b->footprint.height() / 2.0f);
const Tick despawnAt = m_currentTick
+ secondsToTicks(m_config.world.scrapDespawnSeconds);
int scrap = 0;
if (b->type == BuildingType::PlayerDefenceStation)
{
const double lv = static_cast<double>(
m_config.stations.playerStation.level);
scrap = static_cast<int>(
m_config.stations.playerStation.scrapDropFormula.evaluate(lv));
}
else if (b->type == BuildingType::EnemyDefenceStation)
{
const double genD = static_cast<double>(m_waveSystem->generation());
scrap = static_cast<int>(
m_config.stations.enemyStation.scrapDropFormula.evaluate(genD));
}
if (scrap > 0)
{
m_scrapSystem->spawn(center, scrap, despawnAt);
}
}
m_buildingSystem->removeBuilding(deadId);
}
// --- Push check: if both current enemy stations are gone, trigger push ---
const bool es0Gone =
(m_buildingSystem->findBuilding(m_currentEnemyStationIds[0]) == nullptr);
const bool es1Gone =
(m_buildingSystem->findBuilding(m_currentEnemyStationIds[1]) == nullptr);
if (es0Gone && es1Gone &&
m_currentEnemyStationIds[0] != kInvalidEntityId)
{
m_waveSystem->applyPush();
placeEnemyStationSet(m_waveSystem->generation());
awardBlueprintDrop();
}
}
void Simulation::awardBlueprintDrop()
{
std::vector<std::string> ids;
ids.reserve(m_config.ships.ships.size());
for (const ShipDef& def : m_config.ships.ships)
{
ids.push_back(def.id);
}
std::uniform_int_distribution<int> dist(0, static_cast<int>(ids.size()) - 1);
const std::string chosen = ids[static_cast<std::size_t>(dist(m_rng))];
BlueprintState& state = m_blueprintLevels.at(chosen);
const bool wasNew = !state.unlocked;
state.unlocked = true;
state.level += 1;
BlueprintDropEvent evt;
evt.blueprintId = chosen;
evt.newLevel = state.level;
evt.wasNewUnlock = wasNew;
m_blueprintDropEvents.push_back(evt);
}
// ---------------------------------------------------------------------------
// Drains
// ---------------------------------------------------------------------------
std::vector<FireEvent> Simulation::drainFireEvents()
{
std::vector<FireEvent> result;
@@ -63,6 +343,10 @@ std::vector<BlueprintDropEvent> Simulation::drainBlueprintDropEvents()
return result;
}
// ---------------------------------------------------------------------------
// Accessors
// ---------------------------------------------------------------------------
Tick Simulation::currentTick() const
{
return m_currentTick;
@@ -73,6 +357,38 @@ int Simulation::buildingBlocksStock() const
return m_buildingBlocksStock;
}
bool Simulation::isGameOver() const
{
return m_gameOver;
}
double Simulation::threatLevel() const
{
return m_waveSystem->threatLevel();
}
int Simulation::blueprintLevel(const std::string& shipId) const
{
const std::map<std::string, BlueprintState>::const_iterator it =
m_blueprintLevels.find(shipId);
if (it == m_blueprintLevels.end())
{
return 0;
}
return it->second.level;
}
bool Simulation::isBlueprintUnlocked(const std::string& shipId) const
{
const std::map<std::string, BlueprintState>::const_iterator it =
m_blueprintLevels.find(shipId);
if (it == m_blueprintLevels.end())
{
return false;
}
return it->second.unlocked;
}
BuildingSystem& Simulation::buildings()
{
return *m_buildingSystem;