implement ships depart in waves

This commit is contained in:
2026-04-27 21:31:04 +02:00
parent 7b67093540
commit ed6b503767
10 changed files with 78 additions and 11 deletions

View File

@@ -1,28 +1,29 @@
[world] [world]
height_tiles = 60 height_tiles = 30
refund_percentage = 75 refund_percentage = 75
starting_building_blocks = 100 starting_building_blocks = 1000
scrap_despawn_seconds = 30 scrap_despawn_seconds = 30
belt_speed_tiles_per_second = 2 belt_speed_tiles_per_second = 2
tunnel_max_distance = 10 tunnel_max_distance = 10
departure_interval_seconds = 20
[regions] [regions]
asteroid_width = 40 asteroid_width = 40
player_buffer_width = 10 player_buffer_width = 20
contest_zone_width = 30 contest_zone_width = 60
enemy_buffer_width = 15 enemy_buffer_width = 20
[expansion] [expansion]
columns_per_expansion = 10 columns_per_expansion = 10
cost_building_blocks = 200 cost_building_blocks = 200
[push] [push]
push_expand_columns = 20 push_expand_columns = 10
scaling_factor = 1.2 scaling_factor = 1.2
[waves] [waves]
threat_rate_formula = "1*x - 30" threat_rate_formula = "0.1*x - 60"
ship_level_formula = "1 + x / 120" ship_level_formula = "1"
gap_min_seconds = 15 gap_min_seconds = 15
gap_max_seconds = 45 gap_max_seconds = 45
spawn_duration_seconds = 10 spawn_duration_seconds = 10

View File

@@ -225,6 +225,7 @@ WorldConfig ConfigLoader::loadWorld(const std::string& path)
cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds"); cfg.scrapDespawnSeconds = requireDouble(tbl["world"]["scrap_despawn_seconds"], file, "world.scrap_despawn_seconds");
cfg.beltSpeedTilesPerSecond = requireDouble(tbl["world"]["belt_speed_tiles_per_second"], file, "world.belt_speed_tiles_per_second"); cfg.beltSpeedTilesPerSecond = requireDouble(tbl["world"]["belt_speed_tiles_per_second"], file, "world.belt_speed_tiles_per_second");
cfg.tunnelMaxDistance = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance"], file, "world.tunnel_max_distance")); cfg.tunnelMaxDistance = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance"], file, "world.tunnel_max_distance"));
cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds");
cfg.regions.asteroidWidth = static_cast<int>(requireInt(tbl["regions"]["asteroid_width"], file, "regions.asteroid_width")); cfg.regions.asteroidWidth = static_cast<int>(requireInt(tbl["regions"]["asteroid_width"], file, "regions.asteroid_width"));
cfg.regions.playerBufferWidth = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width"], file, "regions.player_buffer_width")); cfg.regions.playerBufferWidth = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width"], file, "regions.player_buffer_width"));

View File

@@ -43,6 +43,7 @@ struct WorldConfig
double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP double scrapDespawnSeconds; // REQ-RES-SCRAP-DROP
double beltSpeedTilesPerSecond; // REQ-GW-BELT-SPEED double beltSpeedTilesPerSecond; // REQ-GW-BELT-SPEED
int tunnelMaxDistance; // REQ-BLD-TUNNEL-PAIR int tunnelMaxDistance; // REQ-BLD-TUNNEL-PAIR
double departureIntervalSeconds; // REQ-SHP-RALLY
WorldRegions regions; WorldRegions regions;
WorldExpansion expansion; WorldExpansion expansion;

View File

@@ -62,6 +62,11 @@ struct HomeReturn
QVector2D homePos; QVector2D homePos;
}; };
struct RallyBehavior
{
QVector2D rallyPoint;
};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Ship // Ship
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -86,6 +91,7 @@ struct Ship
std::optional<ScrapCollector> scrapCollector; std::optional<ScrapCollector> scrapCollector;
std::optional<RepairBehavior> repairBehavior; std::optional<RepairBehavior> repairBehavior;
std::optional<HomeReturn> homeReturn; std::optional<HomeReturn> homeReturn;
std::optional<RallyBehavior> rallyBehavior;
// Cleared at the start of the behavior step each tick; the highest-priority // Cleared at the start of the behavior step each tick; the highest-priority
// write from behavior systems wins (architecture.md §Movement Arbitration). // write from behavior systems wins (architecture.md §Movement Arbitration).

View File

@@ -64,6 +64,11 @@ EntityId ShipSystem::spawn(const std::string& schematicId, int level, QVector2D
ThreatResponse tr; ThreatResponse tr;
tr.engagementRange = w.range; tr.engagementRange = w.range;
ship.threatResponse = tr; ship.threatResponse = tr;
if (!isEnemy)
{
ship.rallyBehavior = RallyBehavior{m_rallyPoint};
}
} }
if (def->salvage) if (def->salvage)
@@ -271,11 +276,18 @@ void ShipSystem::tickThreatResponse(const BuildingSystem& buildings)
} }
else else
{ {
// No target: patrol rightward (aggressive). // No target: gather at rally point or patrol rightward once departed.
if (3 > s.intent.priority) if (3 > s.intent.priority)
{ {
s.intent = MovementIntent{3, QVector2D(s.position.x() + 1000.0f, if (s.rallyBehavior)
s.position.y())}; {
s.intent = MovementIntent{3, s.rallyBehavior->rallyPoint};
}
else
{
s.intent = MovementIntent{3, QVector2D(s.position.x() + 1000.0f,
s.position.y())};
}
} }
} }
} }
@@ -636,6 +648,26 @@ void ShipSystem::tickScrapCollector(ScrapSystem& scraps, const BuildingSystem& b
} }
} }
// ---------------------------------------------------------------------------
// Rally point management (REQ-SHP-RALLY)
// ---------------------------------------------------------------------------
void ShipSystem::setRallyPoint(QVector2D point)
{
m_rallyPoint = point;
}
void ShipSystem::triggerRallyDeparture()
{
for (Ship& s : m_ships)
{
if (!s.isEnemy)
{
s.rallyBehavior = std::nullopt;
}
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// tickMovement (tick-order step 10) // tickMovement (tick-order step 10)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -47,6 +47,12 @@ public:
// -- Movement (tick-order step 10) --------------------------------------- // -- Movement (tick-order step 10) ---------------------------------------
void tickMovement(); void tickMovement();
// Set the rally point that newly spawned player combat ships will loiter at.
void setRallyPoint(QVector2D point);
// Release all gathered player combat ships to advance toward the enemy.
void triggerRallyDeparture();
// Reduce ship HP by amount. Does not remove the ship; step 9 handles death. // Reduce ship HP by amount. Does not remove the ship; step 9 handles death.
// Returns false if ship not found. // Returns false if ship not found.
bool damageShip(EntityId id, float amount); bool damageShip(EntityId id, float amount);
@@ -66,4 +72,5 @@ private:
const GameConfig& m_config; const GameConfig& m_config;
std::function<EntityId()> m_allocateId; std::function<EntityId()> m_allocateId;
std::vector<Ship> m_ships; std::vector<Ship> m_ships;
QVector2D m_rallyPoint;
}; };

View File

@@ -13,6 +13,7 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
: m_config(std::move(config)) : m_config(std::move(config))
, m_rng(seed) , m_rng(seed)
, m_currentTick(0) , m_currentTick(0)
, m_nextDepartureTick(secondsToTicks(m_config.world.departureIntervalSeconds))
, m_nextId(1) , m_nextId(1)
, m_buildingBlocksStock(m_config.world.startingBuildingBlocks) , m_buildingBlocksStock(m_config.world.startingBuildingBlocks)
, m_gameOver(false) , m_gameOver(false)
@@ -73,6 +74,7 @@ void Simulation::reset(unsigned int seed)
{ {
m_rng.seed(seed); m_rng.seed(seed);
m_currentTick = 0; m_currentTick = 0;
m_nextDepartureTick = secondsToTicks(m_config.world.departureIntervalSeconds);
m_nextId = 1; m_nextId = 1;
m_buildingBlocksStock = m_config.world.startingBuildingBlocks; m_buildingBlocksStock = m_config.world.startingBuildingBlocks;
m_gameOver = false; m_gameOver = false;
@@ -139,6 +141,14 @@ void Simulation::tick()
m_beltSystem.tick(); // step 6 m_beltSystem.tick(); // step 6
// Step 7: ship behavior systems (movement arbitration via intent priority) // Step 7: ship behavior systems (movement arbitration via intent priority)
// Departure timer: release gathered combat ships on a fixed interval (REQ-SHP-RALLY).
if (m_currentTick >= m_nextDepartureTick)
{
m_shipSystem->triggerRallyDeparture();
m_nextDepartureTick += secondsToTicks(m_config.world.departureIntervalSeconds);
}
m_shipSystem->clearMovementIntents(); m_shipSystem->clearMovementIntents();
m_shipSystem->tickHomeReturn(); // priority 4 m_shipSystem->tickHomeReturn(); // priority 4
m_shipSystem->tickThreatResponse(*m_buildingSystem); // priority 3 m_shipSystem->tickThreatResponse(*m_buildingSystem); // priority 3
@@ -217,6 +227,11 @@ void Simulation::placeInitialStructures()
QPoint(psAnchorX, ps2Y), Rotation::East, psHp, psHp); QPoint(psAnchorX, ps2Y), Rotation::East, psHp, psHp);
m_buildingSystem->initStationWeapon(m_playerStation2Id, psWeapon); m_buildingSystem->initStationWeapon(m_playerStation2Id, psWeapon);
// Rally point: center of the player defence stations' X column, world vertical midpoint.
const float rallyX = static_cast<float>(psAnchorX) + psParsed.footprint.width() / 2.0f;
const float rallyY = static_cast<float>(m_config.world.heightTiles) / 2.0f;
m_shipSystem->setRallyPoint(QVector2D(rallyX, rallyY));
// Enemy defence stations — generation 0 (initial set). // Enemy defence stations — generation 0 (initial set).
placeEnemyStationSet(0); placeEnemyStationSet(0);
} }

View File

@@ -92,6 +92,7 @@ private:
std::mt19937 m_rng; std::mt19937 m_rng;
Tick m_currentTick; Tick m_currentTick;
Tick m_nextDepartureTick;
EntityId m_nextId; EntityId m_nextId;
int m_buildingBlocksStock; int m_buildingBlocksStock;
bool m_gameOver = false; bool m_gameOver = false;

View File

@@ -165,6 +165,7 @@ scrap_despawn_seconds = 30
belt_speed_tiles_per_second = 2 belt_speed_tiles_per_second = 2
starting_building_blocks = 100 starting_building_blocks = 100
tunnel_max_distance = 10 tunnel_max_distance = 10
departure_interval_seconds = 20
[regions] [regions]
asteroid_width = 40 asteroid_width = 40
@@ -211,6 +212,7 @@ scrap_despawn_seconds = 30
belt_speed_tiles_per_second = 2 belt_speed_tiles_per_second = 2
starting_building_blocks = 100 starting_building_blocks = 100
tunnel_max_distance = 10 tunnel_max_distance = 10
departure_interval_seconds = 20
[regions] [regions]
asteroid_width = 40 asteroid_width = 40

View File

@@ -5,6 +5,7 @@ starting_building_blocks = 100
scrap_despawn_seconds = 30 scrap_despawn_seconds = 30
belt_speed_tiles_per_second = 2 belt_speed_tiles_per_second = 2
tunnel_max_distance = 10 tunnel_max_distance = 10
departure_interval_seconds = 20
[regions] [regions]
asteroid_width = 40 asteroid_width = 40