implement waves
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user