Compare commits

...

6 Commits

18 changed files with 242 additions and 21 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

@@ -4,7 +4,7 @@
Config files use the TOML format. The following config files drive game parameters: Config files use the TOML format. The following config files drive game parameters:
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, enemy ship level formula, belt speed, starting building blocks. - **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, enemy ship level formula, belt speed, starting building blocks, departure interval.
- **buildings.toml** — building block cost and construction time per building type. - **buildings.toml** — building block cost and construction time per building type.
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. - **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities.
- **ships.toml** — per schematic: a human-readable display name (used in toasts and UI), ship stats (HP, speed, damage, attack range, attack rate, sensor range) as formulas of ship level, required build materials, threat cost formula, player production level, and whether the schematic is available from game start. - **ships.toml** — per schematic: a human-readable display name (used in toasts and UI), ship stats (HP, speed, damage, attack range, attack rate, sensor range) as formulas of ship level, required build materials, threat cost formula, player production level, and whether the schematic is available from game start.
@@ -119,6 +119,7 @@ Output port indicators are not building tiles themselves. A building may have mo
- REQ-SHP-COMBAT: **Combat ships** (player) — engage enemy ships within sensor range. The player can configure the following per shipyard (applied to all ships produced by that shipyard): - REQ-SHP-COMBAT: **Combat ships** (player) — engage enemy ships within sensor range. The player can configure the following per shipyard (applied to all ships produced by that shipyard):
- Stance: aggressive (advance toward enemies) / defensive (hold position near asteroid). - Stance: aggressive (advance toward enemies) / defensive (hold position near asteroid).
- Target priority: closest / highest HP / structures first. - Target priority: closest / highest HP / structures first.
- REQ-SHP-RALLY: After spawning, aggressive-stance combat ships move to and loiter at the **rally point** — the midpoint between the two player defence stations (center of their Y-span, at the player defence stations' X position). While at the rally point, ships still engage any enemy that enters sensor range. Every `world.toml [world].departure_interval_seconds` seconds (default 20), all combat ships currently at the rally point depart simultaneously and begin their normal aggressive advance toward the enemy. The departure timer is global and shared across all shipyards; it is not reset by individual ship arrivals at the rally point.
- REQ-SHP-SALVAGE: **Salvage ships** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If scrap enters sensor range, move to it, collect, and deliver it to a Salvage Bay on the asteroid; after delivery, resume patrol. If an enemy ship enters sensor range while not currently targeting or carrying scrap, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. Salvage ships are vulnerable to enemy ships while operating. - REQ-SHP-SALVAGE: **Salvage ships** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If scrap enters sensor range, move to it, collect, and deliver it to a Salvage Bay on the asteroid; after delivery, resume patrol. If an enemy ship enters sensor range while not currently targeting or carrying scrap, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. Salvage ships are vulnerable to enemy ships while operating.
- REQ-SHP-REPAIR: **Repair ships** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If a damaged player defence station or player ship enters sensor range, move to it and repair. If an enemy ship enters sensor range while not currently repairing, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. The player can configure the target priority per shipyard: - REQ-SHP-REPAIR: **Repair ships** (player) — patrol by moving forward (rightward, away from the asteroid) while searching sensor range. If a damaged player defence station or player ship enters sensor range, move to it and repair. If an enemy ship enters sensor range while not currently repairing, turn back (move toward the asteroid) until the enemy is no longer in sensor range, then resume patrol. The player can configure the target priority per shipyard:
- Defence stations first / ships first / nearest target. - Defence stations first / ships first / nearest target.
@@ -197,6 +198,11 @@ The screen is divided into three vertical sections:
- **Backspace** — activates demolish mode; Backspace again exits it. (See also REQ-UI-DEMOLISH-BUTTON for the equivalent button.) - **Backspace** — activates demolish mode; Backspace again exits it. (See also REQ-UI-DEMOLISH-BUTTON for the equivalent button.)
- **Q / E** — in builder mode, rotate the ghost counter-clockwise / clockwise (REQ-BLD-ROTATE). - **Q / E** — in builder mode, rotate the ghost counter-clockwise / clockwise (REQ-BLD-ROTATE).
- **Escape** — opens the escape menu (REQ-UI-GAME-MENU). - **Escape** — opens the escape menu (REQ-UI-GAME-MENU).
- **M** — toggles debug draw mode (REQ-UI-DEBUG-DRAW).
### Debug Draw
- REQ-UI-DEBUG-DRAW: A debug draw mode can be toggled on and off with the **M** key (REQ-UI-HOTKEYS). It is inactive by default. While active, the sensor range of every ship — both player and enemy — is drawn as a circle centered on the ship, using that ship role's outline color from `visuals.toml`.
### Escape Menu ### Escape Menu

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

@@ -199,7 +199,7 @@ void BeltSystem::reevaluateTunnelPairing()
// Port interface // Port interface
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
bool BeltSystem::tryPutItem(QPoint tile, Item item) bool BeltSystem::tryPutItem(QPoint tile, Item item, Rotation fromDir)
{ {
const std::map<std::pair<int, int>, BeltTile>::iterator bIt = m_belts.find(key(tile)); const std::map<std::pair<int, int>, BeltTile>::iterator bIt = m_belts.find(key(tile));
if (bIt != m_belts.end()) if (bIt != m_belts.end())
@@ -207,6 +207,19 @@ bool BeltSystem::tryPutItem(QPoint tile, Item item)
return tryPlaceOnBelt(tile, item); return tryPlaceOnBelt(tile, item);
} }
const std::map<std::pair<int, int>, SplitterTile>::iterator splIt =
m_splitters.find(key(tile));
if (splIt != m_splitters.end())
{
if (!splIt->second.back)
{
splIt->second.back = BeltItemSlot{item, 0.0};
splIt->second.backDir = fromDir;
return true;
}
return false;
}
const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt = const std::map<std::pair<int, int>, TunnelEntryTile>::iterator teIt =
m_tunnelEntries.find(key(tile)); m_tunnelEntries.find(key(tile));
if (teIt != m_tunnelEntries.end()) if (teIt != m_tunnelEntries.end())

View File

@@ -72,8 +72,9 @@ public:
// port.direction = direction items flow on that tile // port.direction = direction items flow on that tile
// //
// tryPutItem: place item onto tile. // tryPutItem: place item onto tile.
// Returns false if the tile is not a belt, or tile full. // Returns false if the tile is not a belt/splitter, or tile full.
bool tryPutItem(QPoint tile, Item item); // fromDir: travel direction of the item (used for splitter animation).
bool tryPutItem(QPoint tile, Item item, Rotation fromDir = Rotation::West);
// tryTakeItem: remove and return the leading item from port.tile. // tryTakeItem: remove and return the leading item from port.tile.
// Returns nullopt if tile is not a belt, direction mismatches, or tile empty. // Returns nullopt if tile is not a belt, direction mismatches, or tile empty.

View File

@@ -713,7 +713,7 @@ void BuildingSystem::tickBeltPush()
break; break;
} }
const Item item = building.outputBuffer.items.front(); const Item item = building.outputBuffer.items.front();
if (m_belts.tryPutItem(outputPort.tile, item)) if (m_belts.tryPutItem(outputPort.tile, item, outputPort.direction))
{ {
building.outputBuffer.items.erase(building.outputBuffer.items.begin()); building.outputBuffer.items.erase(building.outputBuffer.items.begin());
} }

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

@@ -629,6 +629,24 @@ TEST_CASE("BeltSystem: splitter accepts new items after building pulls from fron
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value()); REQUIRE(bs.peekItem(Port{tileSpl, Rotation::South}).has_value());
} }
TEST_CASE("BeltSystem: tryPutItem succeeds directly on a splitter tile", "[belt]")
{
// Regression: buildings outputting onto a splitter tile were silently dropped
// because tryPutItem had no splitter case and returned false.
BeltSystem bs(kFastBeltSpeed);
const QPoint tileSpl(0, 0);
bs.placeSplitter(tileSpl, Rotation::North, Rotation::South);
REQUIRE(bs.tryPutItem(tileSpl, makeItem("iron_ore"), Rotation::East));
// Item should arrive at one of the output fronts after the splitter ticks through.
bs.tick(); // back advances to 0.5, routes to frontA
bs.tick(); // frontA reaches 1.0
REQUIRE(bs.peekItem(Port{tileSpl, Rotation::North}).has_value());
}
TEST_CASE("BeltSystem: splitter alternates between two unregistered outputs (building inputs)", "[belt]") TEST_CASE("BeltSystem: splitter alternates between two unregistered outputs (building inputs)", "[belt]")
{ {
BeltSystem bs(kFastBeltSpeed); BeltSystem bs(kFastBeltSpeed);

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

View File

@@ -117,6 +117,7 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
, m_dragging(false) , m_dragging(false)
, m_demolishMode(false) , m_demolishMode(false)
, m_demolishHoverId(kInvalidEntityId) , m_demolishHoverId(kInvalidEntityId)
, m_debugDraw(false)
, m_boxSelecting(false) , m_boxSelecting(false)
, m_scrollLeft(false) , m_scrollLeft(false)
, m_scrollRight(false) , m_scrollRight(false)
@@ -246,6 +247,7 @@ void GameWorldView::paintGL()
drawBuildings(painter); drawBuildings(painter);
drawBeltItems(painter); drawBeltItems(painter);
drawScrap(painter); drawScrap(painter);
if (m_debugDraw) { drawDebugSensorRanges(painter); }
drawShips(painter); drawShips(painter);
drawBeams(painter); drawBeams(painter);
drawOverlays(painter); drawOverlays(painter);
@@ -775,6 +777,42 @@ void GameWorldView::drawShips(QPainter& painter)
} }
} }
void GameWorldView::drawDebugSensorRanges(QPainter& painter)
{
painter.setBrush(Qt::NoBrush);
for (const Ship& ship : m_sim->ships().allShips())
{
const ShipRole role = shipRole(ship);
const std::map<ShipRole, ShipVisuals>::const_iterator it =
m_visuals->ships.find(role);
if (it == m_visuals->ships.end()) { continue; }
float range = 0.0f;
if (ship.weapon.has_value())
{
range = ship.weapon->range;
}
else if (ship.repairTool.has_value())
{
range = ship.repairTool->range;
}
else if (ship.cargo.has_value())
{
range = ship.cargo->collectionRange * 5.0f;
}
else
{
continue;
}
const QPointF center = worldToWidget(ship.position);
const qreal radiusPx = static_cast<qreal>(range)
* static_cast<qreal>(tilePx());
painter.setPen(QPen(it->second.outline, 1));
painter.drawEllipse(center, radiusPx, radiusPx);
}
}
void GameWorldView::drawBeams(QPainter& painter) void GameWorldView::drawBeams(QPainter& painter)
{ {
painter.setPen(QPen(m_visuals->beams.color, m_visuals->beams.widthPx)); painter.setPen(QPen(m_visuals->beams.color, m_visuals->beams.widthPx));
@@ -996,6 +1034,9 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
case Qt::Key_Backspace: case Qt::Key_Backspace:
toggleDemolishMode(); toggleDemolishMode();
break; break;
case Qt::Key_M:
m_debugDraw = !m_debugDraw;
break;
default: default:
QOpenGLWidget::keyPressEvent(event); QOpenGLWidget::keyPressEvent(event);
break; break;
@@ -1067,7 +1108,11 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
} }
else else
{ {
const EntityId id = buildingAtTile(tile); EntityId id = buildingAtTile(tile);
if (id == kInvalidEntityId)
{
id = siteAtTile(tile);
}
if (id != kInvalidEntityId) if (id != kInvalidEntityId)
{ {
if (event->modifiers() & Qt::ControlModifier) if (event->modifiers() & Qt::ControlModifier)
@@ -1162,6 +1207,18 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
} }
} }
} }
for (const ConstructionSite& s : m_sim->buildings().allSites())
{
for (const QPoint& cell : s.bodyCells)
{
if (cell.x() >= x0 && cell.x() <= x1
&& cell.y() >= y0 && cell.y() <= y1)
{
boxSel.push_back(s.id);
break;
}
}
}
if (!(event->modifiers() & Qt::ControlModifier)) if (!(event->modifiers() & Qt::ControlModifier))
{ {

View File

@@ -81,6 +81,7 @@ private:
void drawBeltItems(QPainter& painter); void drawBeltItems(QPainter& painter);
void drawScrap(QPainter& painter); void drawScrap(QPainter& painter);
void drawShips(QPainter& painter); void drawShips(QPainter& painter);
void drawDebugSensorRanges(QPainter& painter);
void drawBeams(QPainter& painter); void drawBeams(QPainter& painter);
void drawOverlays(QPainter& painter); void drawOverlays(QPainter& painter);
void drawScreenSpace(QPainter& painter); void drawScreenSpace(QPainter& painter);
@@ -156,6 +157,7 @@ private:
bool m_demolishMode; bool m_demolishMode;
EntityId m_demolishHoverId; EntityId m_demolishHoverId;
bool m_debugDraw;
std::vector<EntityId> m_selectedIds; std::vector<EntityId> m_selectedIds;
bool m_boxSelecting; bool m_boxSelecting;

View File

@@ -168,7 +168,43 @@ void SelectedBuildingPanel::buildSingle(EntityId id)
const Building* b = m_sim->buildings().findBuilding(id); const Building* b = m_sim->buildings().findBuilding(id);
if (!b) if (!b)
{ {
buildEmpty(); const ConstructionSite* s = m_sim->buildings().findSite(id);
if (!s)
{
buildEmpty();
return;
}
QString progress;
if (s->completesAt == 0)
{
progress = "Queued";
}
else
{
const BuildingDef* def = nullptr;
for (const BuildingDef& d : m_config->buildings.buildings)
{
if (d.type == s->type) { def = &d; break; }
}
if (def && def->constructionTimeSeconds > 0)
{
const Tick duration = secondsToTicks(def->constructionTimeSeconds);
const Tick elapsed = m_sim->currentTick() - (s->completesAt - duration);
const int pct = static_cast<int>(
std::max(Tick(0), std::min(duration, elapsed)) * 100 / duration);
progress = QString::number(pct) + "% complete";
}
else
{
progress = "Building...";
}
}
m_titleLabel->setText("(Building) " + buildingTypeName(s->type));
m_titleLabel->show();
m_buffersLabel->setText(progress);
m_buffersLabel->show();
return; return;
} }
@@ -367,12 +403,27 @@ void SelectedBuildingPanel::onStateUpdated(Tick /*tick*/, int /*blocks*/, double
{ {
if (m_singleId == kInvalidEntityId) { return; } if (m_singleId == kInvalidEntityId) { return; }
const Building* b = m_sim->buildings().findBuilding(m_singleId); const Building* b = m_sim->buildings().findBuilding(m_singleId);
if (!b) if (b)
{ {
buildEmpty(); // If the panel was last showing this id as a construction site, the
// full building UI (recipe combo, ports, etc.) hasn't been built yet.
if (m_titleLabel->text().startsWith("(Building) "))
{
rebuild();
}
else
{
refreshBuffers(b);
}
return; return;
} }
refreshBuffers(b); const ConstructionSite* s = m_sim->buildings().findSite(m_singleId);
if (s)
{
rebuild();
return;
}
buildEmpty();
} }
void SelectedBuildingPanel::buildMulti(const std::vector<EntityId>& ids) void SelectedBuildingPanel::buildMulti(const std::vector<EntityId>& ids)
@@ -393,6 +444,12 @@ void SelectedBuildingPanel::buildMulti(const std::vector<EntityId>& ids)
if (b) if (b)
{ {
counts[b->type]++; counts[b->type]++;
continue;
}
const ConstructionSite* s = m_sim->buildings().findSite(id);
if (s)
{
counts[s->type]++;
} }
} }