Compare commits
27 Commits
2c9433cea6
...
refactorin
| Author | SHA1 | Date | |
|---|---|---|---|
| 590bca458c | |||
| 59067a9c49 | |||
| 72d85d681c | |||
| a86ba3428a | |||
| 56b7248ac7 | |||
| 009f8c6d14 | |||
| a90218f5c0 | |||
| 7ce0751c60 | |||
| b3d6264ed3 | |||
| ade716edf2 | |||
| 3272431353 | |||
| d1b688f45e | |||
| df7f60c898 | |||
| 58e173ad5b | |||
| bb50f527d6 | |||
| 7540c21d5c | |||
| 2522a8c974 | |||
| e39b81eb22 | |||
| 71d0dad3f2 | |||
| fda88fe75c | |||
| d713257fb5 | |||
| 0029236135 | |||
| 622447c45b | |||
| a34d66f548 | |||
| cb5572ffdd | |||
| 10ba226af7 | |||
| 0a3288d1d1 |
@@ -31,7 +31,14 @@ keep the citation accurate.
|
|||||||
## Coding Guidelines
|
## Coding Guidelines
|
||||||
|
|
||||||
* avoid duplicate code
|
* avoid duplicate code
|
||||||
* do not use the "auto" keyword
|
* do not use the "auto" keyword, with two exceptions:
|
||||||
|
* **named local lambdas** — a lambda's type is unnameable, and `std::function`
|
||||||
|
is not an acceptable substitute in per-tick code because it adds a heap
|
||||||
|
allocation and an indirect call
|
||||||
|
* **iterator types** — `auto it = m_buildings.find(id)` is allowed where
|
||||||
|
spelling the iterator out adds length without adding information
|
||||||
|
* everywhere else the type is written out; in particular `auto` is not used
|
||||||
|
for plain values, return values, or range-for element types
|
||||||
* use Qt utility data types (like QPoint, QVector3D, QString, etc.)
|
* use Qt utility data types (like QPoint, QVector3D, QString, etc.)
|
||||||
* wrap strings that appear in the UI with Qt's "tr()"
|
* wrap strings that appear in the UI with Qt's "tr()"
|
||||||
* use the EventManager/EventHandler instead of defining own signals and slots
|
* use the EventManager/EventHandler instead of defining own signals and slots
|
||||||
|
|||||||
@@ -136,17 +136,43 @@ Belts and splitters are their own specialized subsystem. Belt items are **not**
|
|||||||
|
|
||||||
### Public Interface
|
### Public Interface
|
||||||
|
|
||||||
Narrow and representation-agnostic:
|
`BeltSystem.h` is authoritative. The surface is wider than the original design sketch — 15 public methods in five groups, not the 5-method port interface this section used to describe:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
class BeltSystem {
|
class BeltSystem {
|
||||||
public:
|
public:
|
||||||
bool tryPutItem(Port port, Item item);
|
// Placement — belts/splitters/tunnels are Buildings for cost and
|
||||||
std::optional<Item> tryTakeItem(Port port);
|
// construction, so BuildingSystem registers and unregisters their tiles.
|
||||||
|
void placeBelt(QPoint tile, Rotation direction);
|
||||||
|
void placeTunnelEntry(QPoint tile, Rotation direction, int maxDistance);
|
||||||
|
void placeTunnelExit(QPoint tile, Rotation direction);
|
||||||
|
void placeSplitter(QPoint tile, Rotation outputA, Rotation outputB);
|
||||||
|
void removeTile(QPoint tile);
|
||||||
|
|
||||||
|
// Splitter filter configuration (REQ-BLD-SPLITTER). A splitter's filters
|
||||||
|
// live here, not on Building, so callers that re-register a tile must
|
||||||
|
// carry them across (see BuildingSystem::reregisterBeltTile).
|
||||||
|
void setSplitterFilters(QPoint tile, const std::vector<ItemType>& filterA,
|
||||||
|
const std::vector<ItemType>& filterB);
|
||||||
|
std::optional<SplitterInfo> getSplitterInfo(QPoint tile) const;
|
||||||
|
|
||||||
|
// Port interface (buildings <-> belts)
|
||||||
|
bool tryPutItem(QPoint tile, Item item, Rotation fromDir = Rotation::West);
|
||||||
|
std::optional<Item> tryTakeItem(Port port);
|
||||||
|
std::optional<ItemType> peekItem(Port port) const;
|
||||||
|
double getProgressPerTick_tpt() const; // shared so building output items
|
||||||
|
// travel at belt speed (REQ-MAT-OUTPUT-EMERGE)
|
||||||
|
|
||||||
|
// Maintenance
|
||||||
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
|
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
|
||||||
void tick();
|
void tick();
|
||||||
|
|
||||||
|
// Rendering
|
||||||
void forEachVisualItem(QRect viewportTiles,
|
void forEachVisualItem(QRect viewportTiles,
|
||||||
std::function<void(VisualItem)> visit) const;
|
std::function<void(VisualItem)> visit) const;
|
||||||
|
|
||||||
|
// Determinism (docs/replay_design.md)
|
||||||
|
void appendChecksum(Hasher& hasher) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VisualItem {
|
struct VisualItem {
|
||||||
@@ -155,12 +181,12 @@ struct VisualItem {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Buildings interact with belts only through port-level push and pull. Rendering reads only through `forEachVisualItem`. No other system ever asks "what is on tile X".
|
Item *transport* is still reached only through push and pull: `tryPutItem` / `tryTakeItem` move items, `peekItem` reveals the leading item's type but never an identity, and rendering reads only through `forEachVisualItem`. The growth is in tile **topology** — placement, removal and splitter filters — which `BuildingSystem` drives because belts are `Building`s for cost, construction and deconstruction. That coupling is real and is not going away.
|
||||||
|
|
||||||
### Implementation Strategy
|
### Implementation Strategy
|
||||||
|
|
||||||
- v1: per-tile representation. Each belt tile stores up to 2 items with a progress value in `[0, 1]` along the tile's belt direction. Sufficient for the scale this game targets.
|
- v1: per-tile representation. Each belt tile stores up to 2 items with a progress value in `[0, 1]` along the tile's belt direction. Sufficient for the scale this game targets.
|
||||||
- v2 (optional, only if v1 profiles poorly): Factorio-style belt-segment compression. Because the public interface never exposes tile-level item identity, migration is internal to the subsystem.
|
- v2 (optional, only if v1 profiles poorly): Factorio-style belt-segment compression. The migration argument still holds for the item representation, since no method exposes tile-level item identity — but a v2 would have to keep the placement and splitter-filter methods working per tile, which is a stronger constraint than this section originally implied.
|
||||||
|
|
||||||
### Rendering Note
|
### Rendering Note
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
set(TARGET_BASE_NAME "${PRODUCT_NAME}")
|
set(TARGET_BASE_NAME "${PRODUCT_NAME}")
|
||||||
|
|
||||||
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
|||||||
, m_finished(false)
|
, m_finished(false)
|
||||||
, m_stopRequested(false)
|
, m_stopRequested(false)
|
||||||
{
|
{
|
||||||
|
m_factoryState = makeFactoryState(m_gameConfig);
|
||||||
|
|
||||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
m_buildingSystem = std::make_unique<BuildingSystem>(
|
||||||
m_gameConfig,
|
m_gameConfig,
|
||||||
m_beltSystem,
|
m_beltSystem,
|
||||||
@@ -162,7 +164,7 @@ void ArenaSimulation::placeStructures()
|
|||||||
hp, hp, false);
|
hp, hp, false);
|
||||||
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
||||||
m_admin.addComponent<HqProxyComponent>(m_team1HqEntity);
|
m_admin.addComponent<HqProxyComponent>(m_team1HqEntity);
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
|
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
|
||||||
@@ -183,7 +185,7 @@ void ArenaSimulation::placeStructures()
|
|||||||
hp, hp, true);
|
hp, hp, true);
|
||||||
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
||||||
m_admin.addComponent<HqProxyComponent>(m_team2HqEntity);
|
m_admin.addComponent<HqProxyComponent>(m_team2HqEntity);
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
|
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
|
||||||
@@ -237,7 +239,7 @@ void ArenaSimulation::placeStructures()
|
|||||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
ModuleOwnerComponent{stationEntity});
|
ModuleOwnerComponent{stationEntity});
|
||||||
}
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
|
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
|
||||||
@@ -322,13 +324,13 @@ void ArenaSimulation::tick()
|
|||||||
// Ship behavior systems (tick step 7): evaluate, select winner, execute.
|
// Ship behavior systems (tick step 7): evaluate, select winner, execute.
|
||||||
// Module + combat systems emit their tool beams into a shared buffer.
|
// Module + combat systems emit their tool beams into a shared buffer.
|
||||||
m_shipSystem->clearMovementIntents();
|
m_shipSystem->clearMovementIntents();
|
||||||
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_debrisSystem);
|
m_aiSystem->tick(m_admin, m_factoryState);
|
||||||
std::vector<BeamFiredEvent> beamFiredEvents;
|
std::vector<BeamFiredEvent> beamFiredEvents;
|
||||||
m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, *m_buildingSystem, beamFiredEvents);
|
m_salvagerSystem->tick(m_currentTick, m_factoryState, beamFiredEvents);
|
||||||
m_repairSystem->tick(m_currentTick, beamFiredEvents);
|
m_repairSystem->tick(m_currentTick, beamFiredEvents);
|
||||||
|
|
||||||
// Combat resolution (tick step 8).
|
// Combat resolution (tick step 8).
|
||||||
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, beamFiredEvents);
|
m_combatSystem->tick(m_currentTick, m_admin, beamFiredEvents);
|
||||||
m_beamFiredEvents.insert(m_beamFiredEvents.end(), beamFiredEvents.begin(), beamFiredEvents.end());
|
m_beamFiredEvents.insert(m_beamFiredEvents.end(), beamFiredEvents.begin(), beamFiredEvents.end());
|
||||||
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
||||||
|
|
||||||
@@ -392,7 +394,7 @@ void ArenaSimulation::tickDeaths()
|
|||||||
for (entt::entity deadEntity : deadStations)
|
for (entt::entity deadEntity : deadStations)
|
||||||
{
|
{
|
||||||
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
|
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
|
||||||
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
|
||||||
{
|
{
|
||||||
std::vector<entt::entity> stationChildren;
|
std::vector<entt::entity> stationChildren;
|
||||||
m_admin.forEach<ModuleOwnerComponent>(
|
m_admin.forEach<ModuleOwnerComponent>(
|
||||||
@@ -487,6 +489,11 @@ const ArenaConfig& ArenaSimulation::getArenaConfig() const
|
|||||||
return m_arenaConfig;
|
return m_arenaConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FactoryState& ArenaSimulation::getFactoryState() const
|
||||||
|
{
|
||||||
|
return m_factoryState;
|
||||||
|
}
|
||||||
|
|
||||||
const BuildingSystem& ArenaSimulation::getBuildings() const
|
const BuildingSystem& ArenaSimulation::getBuildings() const
|
||||||
{
|
{
|
||||||
return *m_buildingSystem;
|
return *m_buildingSystem;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "BalancingConfig.h"
|
#include "BalancingConfig.h"
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ public:
|
|||||||
|
|
||||||
const ArenaConfig& getArenaConfig() const;
|
const ArenaConfig& getArenaConfig() const;
|
||||||
const BuildingSystem& getBuildings() const;
|
const BuildingSystem& getBuildings() const;
|
||||||
|
const FactoryState& getFactoryState() const;
|
||||||
const ShipSystem& getShips() const;
|
const ShipSystem& getShips() const;
|
||||||
const DebrisSystem& getDebrisSystem() const;
|
const DebrisSystem& getDebrisSystem() const;
|
||||||
EntityAdmin& getAdmin();
|
EntityAdmin& getAdmin();
|
||||||
@@ -107,6 +109,7 @@ private:
|
|||||||
BuildingId m_nextBuildingId;
|
BuildingId m_nextBuildingId;
|
||||||
|
|
||||||
EntityAdmin m_admin;
|
EntityAdmin m_admin;
|
||||||
|
FactoryState m_factoryState;
|
||||||
BeltSystem m_beltSystem;
|
BeltSystem m_beltSystem;
|
||||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "ArenaView.h"
|
#include "ArenaView.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -306,7 +307,7 @@ void ArenaView::drawTiles(QPainter& painter)
|
|||||||
|
|
||||||
void ArenaView::drawBuildings(QPainter& painter)
|
void ArenaView::drawBuildings(QPainter& painter)
|
||||||
{
|
{
|
||||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
||||||
m_visuals->buildings.find(b.type);
|
m_visuals->buildings.find(b.type);
|
||||||
@@ -339,7 +340,7 @@ void ArenaView::drawBuildings(QPainter& painter)
|
|||||||
void ArenaView::drawDebris(QPainter& painter)
|
void ArenaView::drawDebris(QPainter& painter)
|
||||||
{
|
{
|
||||||
const float r = getTilePx() * 0.2f;
|
const float r = getTilePx() * 0.2f;
|
||||||
for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
|
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
|
||||||
{
|
{
|
||||||
const QPointF center = worldToWidget(debris.position);
|
const QPointF center = worldToWidget(debris.position);
|
||||||
painter.setBrush(QColor(128, 110, 90));
|
painter.setBrush(QColor(128, 110, 90));
|
||||||
|
|||||||
@@ -38,3 +38,32 @@ std::string buildingTypeId(BuildingType type)
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isAutoRecipeBuildingType(BuildingType type)
|
||||||
|
{
|
||||||
|
return type == BuildingType::Smelter
|
||||||
|
|| type == BuildingType::ReprocessingPlant;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isBeltSubsystemType(BuildingType type)
|
||||||
|
{
|
||||||
|
return type == BuildingType::Belt
|
||||||
|
|| type == BuildingType::Splitter
|
||||||
|
|| type == BuildingType::TunnelEntry
|
||||||
|
|| type == BuildingType::TunnelExit;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isProductionBuildingType(BuildingType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case BuildingType::Miner:
|
||||||
|
case BuildingType::Smelter:
|
||||||
|
case BuildingType::Assembler:
|
||||||
|
case BuildingType::ReprocessingPlant:
|
||||||
|
case BuildingType::Shipyard:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,3 +29,17 @@ std::optional<BuildingType> parseBuildingType(const std::string& id);
|
|||||||
|
|
||||||
// Canonical id string for a BuildingType. The inverse of parseBuildingType.
|
// Canonical id string for a BuildingType. The inverse of parseBuildingType.
|
||||||
std::string buildingTypeId(BuildingType type);
|
std::string buildingTypeId(BuildingType type);
|
||||||
|
|
||||||
|
// Smelter and Reprocessing Plant have no player-selected recipe
|
||||||
|
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). They auto-process whatever inputs
|
||||||
|
// they receive, matching against every recipe of their building type.
|
||||||
|
bool isAutoRecipeBuildingType(BuildingType type);
|
||||||
|
|
||||||
|
// Buildings that run a production cycle: Miner, Smelter, Assembler, Reprocessing
|
||||||
|
// Plant and Shipyard (REQ-UI-DEBUG-OVERLAY counts these).
|
||||||
|
bool isProductionBuildingType(BuildingType type);
|
||||||
|
|
||||||
|
// Belts, splitters, and tunnel ends keep their runtime data in the belt subsystem
|
||||||
|
// rather than in the Building instance, so placing/removing them must register or
|
||||||
|
// unregister a tile with BeltSystem.
|
||||||
|
bool isBeltSubsystemType(BuildingType type);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Item.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Item.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/PortGeometry.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
|
||||||
@@ -19,6 +20,7 @@ SET(HDRS
|
|||||||
SET(SRCS
|
SET(SRCS
|
||||||
${SRCS}
|
${SRCS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/PortGeometry.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
||||||
|
|||||||
57
src/lib/core/PortGeometry.cpp
Normal file
57
src/lib/core/PortGeometry.cpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#include "PortGeometry.h"
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
std::vector<Port> computeInputPorts(
|
||||||
|
const std::vector<QPoint>& bodyCells,
|
||||||
|
const std::vector<Port>& outputPorts)
|
||||||
|
{
|
||||||
|
// Build lookup sets for quick membership checks.
|
||||||
|
std::set<std::pair<int, int>> bodySet;
|
||||||
|
for (const QPoint& cell : bodyCells)
|
||||||
|
{
|
||||||
|
bodySet.insert({cell.x(), cell.y()});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::pair<int, int>> outputPortTiles;
|
||||||
|
for (const Port& port : outputPorts)
|
||||||
|
{
|
||||||
|
outputPortTiles.insert({port.tile.x(), port.tile.y()});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neighbour deltas and the corresponding "inward" belt direction.
|
||||||
|
const int dx[4] = {-1, 1, 0, 0};
|
||||||
|
const int dy[4] = { 0, 0, -1, 1};
|
||||||
|
const Rotation inward[4] = {
|
||||||
|
Rotation::East, // neighbour is to the West; belt flows East toward building
|
||||||
|
Rotation::West, // neighbour is to the East; belt flows West toward building
|
||||||
|
Rotation::South, // neighbour is above (row-1); belt flows South toward building
|
||||||
|
Rotation::North // neighbour is below (row+1); belt flows North toward building
|
||||||
|
};
|
||||||
|
|
||||||
|
std::set<std::pair<int, int>> seen;
|
||||||
|
std::vector<Port> inputPorts;
|
||||||
|
|
||||||
|
for (const QPoint& cell : bodyCells)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4; ++i)
|
||||||
|
{
|
||||||
|
const int nx = cell.x() + dx[i];
|
||||||
|
const int ny = cell.y() + dy[i];
|
||||||
|
const std::pair<int, int> neighbor = {nx, ny};
|
||||||
|
|
||||||
|
if (bodySet.count(neighbor)) { continue; }
|
||||||
|
if (outputPortTiles.count(neighbor)){ continue; }
|
||||||
|
if (seen.count(neighbor)) { continue; }
|
||||||
|
|
||||||
|
seen.insert(neighbor);
|
||||||
|
Port port;
|
||||||
|
port.tile = QPoint(nx, ny);
|
||||||
|
port.direction = inward[i];
|
||||||
|
inputPorts.push_back(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputPorts;
|
||||||
|
}
|
||||||
55
src/lib/core/PortGeometry.h
Normal file
55
src/lib/core/PortGeometry.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "Port.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
|
||||||
|
// Geometry of a building's input/output ports. A Port names the tile *outside* the
|
||||||
|
// building together with the direction items flow across it; these helpers give the
|
||||||
|
// building body tile on the other side of that edge, which is where the virtual
|
||||||
|
// input/output belt lives.
|
||||||
|
//
|
||||||
|
// Shared by the simulation (which moves items across the edge) and the renderer
|
||||||
|
// (which draws the virtual belt), so the two cannot disagree about which tile a
|
||||||
|
// port belongs to.
|
||||||
|
|
||||||
|
// The building body tile that owns an output port, given the port's outside tile
|
||||||
|
// (port.tile) and its facing direction. The virtual output belt occupies this tile
|
||||||
|
// and flows toward port.tile (REQ-MAT-OUTPUT-EMERGE).
|
||||||
|
inline QPoint outputBodyTile(QPoint portTile, Rotation direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case Rotation::East: return portTile + QPoint(-1, 0);
|
||||||
|
case Rotation::West: return portTile + QPoint( 1, 0);
|
||||||
|
case Rotation::North: return portTile + QPoint( 0, 1);
|
||||||
|
case Rotation::South: return portTile + QPoint( 0, -1);
|
||||||
|
}
|
||||||
|
return portTile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The building body tile an input port feeds into, given the port's outside belt
|
||||||
|
// tile (port.tile) and its inward flow direction. The virtual input belt occupies
|
||||||
|
// this tile and flows from the outer edge (progress 0.0) to the centre (0.5)
|
||||||
|
// (REQ-MAT-INPUT-INTAKE).
|
||||||
|
inline QPoint inputBodyTile(QPoint portTile, Rotation inwardDirection)
|
||||||
|
{
|
||||||
|
switch (inwardDirection)
|
||||||
|
{
|
||||||
|
case Rotation::East: return portTile + QPoint( 1, 0);
|
||||||
|
case Rotation::West: return portTile + QPoint(-1, 0);
|
||||||
|
case Rotation::North: return portTile + QPoint( 0, -1);
|
||||||
|
case Rotation::South: return portTile + QPoint( 0, 1);
|
||||||
|
}
|
||||||
|
return portTile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every belt-facing edge of a footprint that is not already an output port — the
|
||||||
|
// tiles a belt can feed the building from, with the direction items must flow to
|
||||||
|
// enter (REQ-MAT-INPUT-PORTS, REQ-BLD-BELT-DRAG). bodyCells and outputPorts are
|
||||||
|
// in absolute tile coordinates, and so is the result.
|
||||||
|
std::vector<Port> computeInputPorts(const std::vector<QPoint>& bodyCells,
|
||||||
|
const std::vector<Port>& outputPorts);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "AiSystem.h"
|
#include "AiSystem.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
@@ -42,8 +43,7 @@ AiSystem::AiSystem(const GameConfig& config)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
|
void AiSystem::tick(EntityAdmin& admin, const FactoryState& state)
|
||||||
const DebrisSystem& debris)
|
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
|
|
||||||
@@ -54,8 +54,8 @@ void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
|
|||||||
m_retreatEvaluator.evaluate(admin);
|
m_retreatEvaluator.evaluate(admin);
|
||||||
m_attackEvaluator.evaluate(admin);
|
m_attackEvaluator.evaluate(admin);
|
||||||
m_repairEvaluator.evaluate(admin);
|
m_repairEvaluator.evaluate(admin);
|
||||||
m_salvageScrapEvaluator.evaluate(admin, debris);
|
m_salvageScrapEvaluator.evaluate(admin);
|
||||||
m_deliverScrapEvaluator.evaluate(admin, buildings);
|
m_deliverScrapEvaluator.evaluate(admin, state);
|
||||||
|
|
||||||
// Phase 2: pick the highest-scoring behavior per ship.
|
// Phase 2: pick the highest-scoring behavior per ship.
|
||||||
selectWinningBehaviors(admin);
|
selectWinningBehaviors(admin);
|
||||||
@@ -68,7 +68,7 @@ void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
|
|||||||
m_attackExecutor.execute(admin);
|
m_attackExecutor.execute(admin);
|
||||||
m_repairExecutor.execute(admin);
|
m_repairExecutor.execute(admin);
|
||||||
m_salvageScrapExecutor.execute(admin);
|
m_salvageScrapExecutor.execute(admin);
|
||||||
m_deliverScrapExecutor.execute(admin, buildings);
|
m_deliverScrapExecutor.execute(admin, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AiSystem::selectWinningBehaviors(EntityAdmin& admin)
|
void AiSystem::selectWinningBehaviors(EntityAdmin& admin)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include "AdvanceEvaluator.h"
|
#include "AdvanceEvaluator.h"
|
||||||
#include "AdvanceExecutor.h"
|
#include "AdvanceExecutor.h"
|
||||||
#include "AttackEvaluator.h"
|
#include "AttackEvaluator.h"
|
||||||
@@ -17,9 +19,7 @@
|
|||||||
#include "StandbyEvaluator.h"
|
#include "StandbyEvaluator.h"
|
||||||
#include "StandbyExecutor.h"
|
#include "StandbyExecutor.h"
|
||||||
|
|
||||||
class BuildingSystem;
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
class DebrisSystem;
|
|
||||||
struct GameConfig;
|
struct GameConfig;
|
||||||
|
|
||||||
// Orchestrates ship-behavior decision-making in three batched phases:
|
// Orchestrates ship-behavior decision-making in three batched phases:
|
||||||
@@ -34,7 +34,7 @@ class AiSystem
|
|||||||
public:
|
public:
|
||||||
explicit AiSystem(const GameConfig& config);
|
explicit AiSystem(const GameConfig& config);
|
||||||
|
|
||||||
void tick(EntityAdmin& admin, const BuildingSystem& buildings, const DebrisSystem& debris);
|
void tick(EntityAdmin& admin, const FactoryState& state);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void selectWinningBehaviors(EntityAdmin& admin);
|
void selectWinningBehaviors(EntityAdmin& admin);
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ CombatSystem::CombatSystem(const GameConfig& config)
|
|||||||
|
|
||||||
void CombatSystem::tick(Tick currentTick,
|
void CombatSystem::tick(Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
BuildingSystem& /*buildings*/,
|
|
||||||
std::vector<BeamFiredEvent>& outBeamFiredEvents)
|
std::vector<BeamFiredEvent>& outBeamFiredEvents)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
class BuildingSystem;
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
|
|
||||||
class CombatSystem
|
class CombatSystem
|
||||||
@@ -25,7 +24,6 @@ public:
|
|||||||
|
|
||||||
void tick(Tick currentTick,
|
void tick(Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
BuildingSystem& buildings,
|
|
||||||
std::vector<BeamFiredEvent>& outBeamFiredEvents);
|
std::vector<BeamFiredEvent>& outBeamFiredEvents);
|
||||||
|
|
||||||
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
|
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ std::optional<int> DebrisSystem::consume(entt::entity entity)
|
|||||||
return amount;
|
return amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DebrisSystem::collectOne(entt::entity entity)
|
bool collectOne(EntityAdmin& admin, entt::entity entity)
|
||||||
{
|
{
|
||||||
if (!m_admin.isValid(entity) || !m_admin.hasAll<DebrisComponent>(entity))
|
if (!admin.isValid(entity) || !admin.hasAll<DebrisComponent>(entity))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
DebrisComponent& data = m_admin.get<DebrisComponent>(entity);
|
DebrisComponent& data = admin.get<DebrisComponent>(entity);
|
||||||
if (data.amount <= 0)
|
if (data.amount <= 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -60,18 +60,18 @@ bool DebrisSystem::collectOne(entt::entity entity)
|
|||||||
--data.amount;
|
--data.amount;
|
||||||
if (data.amount <= 0)
|
if (data.amount <= 0)
|
||||||
{
|
{
|
||||||
m_admin.destroy(entity);
|
admin.destroy(entity);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<DebrisInfo> DebrisSystem::getAllDebrisInfo() const
|
std::vector<DebrisInfo> getAllDebrisInfo(const EntityAdmin& admin)
|
||||||
{
|
{
|
||||||
std::vector<DebrisInfo> result;
|
std::vector<DebrisInfo> result;
|
||||||
m_admin.forEach<DebrisComponent>(
|
admin.forEach<DebrisComponent>(
|
||||||
[&result, this](entt::entity e, const DebrisComponent& sd)
|
[&result, &admin](entt::entity e, const DebrisComponent& sd)
|
||||||
{
|
{
|
||||||
result.push_back(DebrisInfo{e, m_admin.get<PositionComponent>(e).value, sd.amount});
|
result.push_back(DebrisInfo{e, admin.get<PositionComponent>(e).value, sd.amount});
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,17 @@ public:
|
|||||||
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
|
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
|
||||||
bool collectOne(entt::entity entity);
|
bool collectOne(entt::entity entity);
|
||||||
|
|
||||||
// Lightweight snapshot for callers that need to iterate all debris.
|
|
||||||
std::vector<DebrisInfo> getAllDebrisInfo() const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
EntityAdmin& m_admin;
|
EntityAdmin& m_admin;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Debris state read and changed straight off the registry — no system needed.
|
||||||
|
|
||||||
|
// Lightweight snapshot for callers that need to iterate all debris.
|
||||||
|
std::vector<DebrisInfo> getAllDebrisInfo(const EntityAdmin& admin);
|
||||||
|
|
||||||
|
// Collects a single scrap unit from the debris: decrements its amount by one,
|
||||||
|
// destroying the entity once depleted. Returns true if a scrap was collected,
|
||||||
|
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
|
||||||
|
bool collectOne(EntityAdmin& admin, entt::entity entity);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "SalvagerSystem.h"
|
#include "SalvagerSystem.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -23,14 +24,14 @@ SalvagerSystem::SalvagerSystem(EntityAdmin& admin)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void SalvagerSystem::tick(Tick currentTick, DebrisSystem& debris, BuildingSystem& buildings,
|
void SalvagerSystem::tick(Tick currentTick, FactoryState& state,
|
||||||
std::vector<BeamFiredEvent>& outBeamFiredEvents)
|
std::vector<BeamFiredEvent>& outBeamFiredEvents)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
// Apply collections whose mid-beam delay has elapsed (cycles started earlier).
|
// Apply collections whose mid-beam delay has elapsed (cycles started earlier).
|
||||||
applyPendingCollections(currentTick, debris);
|
applyPendingCollections(currentTick);
|
||||||
|
|
||||||
const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
|
const std::vector<DebrisInfo> allDebris = getAllDebrisInfo(m_admin);
|
||||||
|
|
||||||
// Tick down per-module collection cooldowns.
|
// Tick down per-module collection cooldowns.
|
||||||
m_admin.forEach<SalvagerComponent>(
|
m_admin.forEach<SalvagerComponent>(
|
||||||
@@ -89,7 +90,7 @@ void SalvagerSystem::tick(Tick currentTick, DebrisSystem& debris, BuildingSystem
|
|||||||
[&](entt::entity ship, const DeliverScrapBehavior& deliver, const PositionComponent& pos)
|
[&](entt::entity ship, const DeliverScrapBehavior& deliver, const PositionComponent& pos)
|
||||||
{
|
{
|
||||||
if (!deliver.deliveryBay.has_value()) { return; }
|
if (!deliver.deliveryBay.has_value()) { return; }
|
||||||
const Building* bay = buildings.findBuilding(*deliver.deliveryBay);
|
const Building* bay = findBuilding(state, *deliver.deliveryBay);
|
||||||
if (!bay) { return; }
|
if (!bay) { return; }
|
||||||
|
|
||||||
const QVector2D bayCenter(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
const QVector2D bayCenter(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
||||||
@@ -100,14 +101,14 @@ void SalvagerSystem::tick(Tick currentTick, DebrisSystem& debris, BuildingSystem
|
|||||||
if (!m_admin.hasAll<CargoComponent>(ship)) { return; }
|
if (!m_admin.hasAll<CargoComponent>(ship)) { return; }
|
||||||
CargoComponent& cargo = m_admin.get<CargoComponent>(ship);
|
CargoComponent& cargo = m_admin.get<CargoComponent>(ship);
|
||||||
if (cargo.current <= 0) { return; }
|
if (cargo.current <= 0) { return; }
|
||||||
if (buildings.deliverScrapToSalvageBay(*deliver.deliveryBay))
|
if (deliverScrapToSalvageBay(state, *deliver.deliveryBay))
|
||||||
{
|
{
|
||||||
--cargo.current;
|
--cargo.current;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void SalvagerSystem::applyPendingCollections(Tick currentTick, DebrisSystem& debris)
|
void SalvagerSystem::applyPendingCollections(Tick currentTick)
|
||||||
{
|
{
|
||||||
std::vector<PendingCollection>::iterator it = m_pendingCollections.begin();
|
std::vector<PendingCollection>::iterator it = m_pendingCollections.begin();
|
||||||
while (it != m_pendingCollections.end())
|
while (it != m_pendingCollections.end())
|
||||||
@@ -117,7 +118,7 @@ void SalvagerSystem::applyPendingCollections(Tick currentTick, DebrisSystem& deb
|
|||||||
if (m_admin.isValid(it->ship) && m_admin.hasAll<CargoComponent>(it->ship))
|
if (m_admin.isValid(it->ship) && m_admin.hasAll<CargoComponent>(it->ship))
|
||||||
{
|
{
|
||||||
CargoComponent& cargo = m_admin.get<CargoComponent>(it->ship);
|
CargoComponent& cargo = m_admin.get<CargoComponent>(it->ship);
|
||||||
if (cargo.current < cargo.maxCapacity && debris.collectOne(it->debris))
|
if (cargo.current < cargo.maxCapacity && collectOne(m_admin, it->debris))
|
||||||
{
|
{
|
||||||
++cargo.current;
|
++cargo.current;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "BeamFiredEvent.h"
|
#include "BeamFiredEvent.h"
|
||||||
@@ -7,9 +9,7 @@
|
|||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
class BuildingSystem;
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
class DebrisSystem;
|
|
||||||
|
|
||||||
// World-mutation system for salvage modules: each module runs a collection cycle
|
// World-mutation system for salvage modules: each module runs a collection cycle
|
||||||
// on its own cooldown. When a cycle starts it emits a salvage beam toward an
|
// on its own cooldown. When a cycle starts it emits a salvage beam toward an
|
||||||
@@ -21,7 +21,7 @@ class SalvagerSystem
|
|||||||
public:
|
public:
|
||||||
explicit SalvagerSystem(EntityAdmin& admin);
|
explicit SalvagerSystem(EntityAdmin& admin);
|
||||||
|
|
||||||
void tick(Tick currentTick, DebrisSystem& debris, BuildingSystem& buildings,
|
void tick(Tick currentTick, FactoryState& state,
|
||||||
std::vector<BeamFiredEvent>& outBeamFiredEvents);
|
std::vector<BeamFiredEvent>& outBeamFiredEvents);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -32,7 +32,7 @@ private:
|
|||||||
Tick appliesAt;
|
Tick appliesAt;
|
||||||
};
|
};
|
||||||
|
|
||||||
void applyPendingCollections(Tick currentTick, DebrisSystem& debris);
|
void applyPendingCollections(Tick currentTick);
|
||||||
|
|
||||||
EntityAdmin& m_admin;
|
EntityAdmin& m_admin;
|
||||||
std::vector<PendingCollection> m_pendingCollections;
|
std::vector<PendingCollection> m_pendingCollections;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "DeliverScrapEvaluator.h"
|
#include "DeliverScrapEvaluator.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const BuildingSystem& buildings)
|
void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const FactoryState& state)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
|
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
|
||||||
@@ -34,7 +35,7 @@ void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const BuildingSystem& b
|
|||||||
if (!deliver.deliveryBay.has_value())
|
if (!deliver.deliveryBay.has_value())
|
||||||
{
|
{
|
||||||
const Building* bay =
|
const Building* bay =
|
||||||
buildings.findNearestBuilding(pos.value, BuildingType::SalvageBay);
|
findNearestBuilding(state, pos.value, BuildingType::SalvageBay);
|
||||||
if (bay) { deliver.deliveryBay = bay->id; }
|
if (bay) { deliver.deliveryBay = bay->id; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
class BuildingSystem;
|
|
||||||
|
|
||||||
// Scores high only when the ship's cargo is full, and assigns the nearest
|
// Scores high only when the ship's cargo is full, and assigns the nearest
|
||||||
// SalvageBay as the delivery destination.
|
// SalvageBay as the delivery destination.
|
||||||
class DeliverScrapEvaluator
|
class DeliverScrapEvaluator
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void evaluate(EntityAdmin& admin, const BuildingSystem& buildings);
|
void evaluate(EntityAdmin& admin, const FactoryState& state);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "DeliverScrapExecutor.h"
|
#include "DeliverScrapExecutor.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
#include "SelectedBehaviorComponent.h"
|
#include "SelectedBehaviorComponent.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
void DeliverScrapExecutor::execute(EntityAdmin& admin, const BuildingSystem& buildings)
|
void DeliverScrapExecutor::execute(EntityAdmin& admin, const FactoryState& state)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
admin.forEach<DeliverScrapBehavior, SelectedBehaviorComponent, PositionComponent,
|
admin.forEach<DeliverScrapBehavior, SelectedBehaviorComponent, PositionComponent,
|
||||||
@@ -26,7 +27,7 @@ void DeliverScrapExecutor::execute(EntityAdmin& admin, const BuildingSystem& bui
|
|||||||
QVector2D dest = pos.value;
|
QVector2D dest = pos.value;
|
||||||
if (deliver.deliveryBay.has_value())
|
if (deliver.deliveryBay.has_value())
|
||||||
{
|
{
|
||||||
const Building* bay = buildings.findBuilding(*deliver.deliveryBay);
|
const Building* bay = findBuilding(state, *deliver.deliveryBay);
|
||||||
if (bay)
|
if (bay)
|
||||||
{
|
{
|
||||||
dest = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
dest = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
class BuildingSystem;
|
|
||||||
|
|
||||||
// Moves a ship toward its delivery bay when DeliverScrap is the winning
|
// Moves a ship toward its delivery bay when DeliverScrap is the winning
|
||||||
// behavior. Never decrements cargo — SalvagerSystem performs the delivery.
|
// behavior. Never decrements cargo — SalvagerSystem performs the delivery.
|
||||||
class DeliverScrapExecutor
|
class DeliverScrapExecutor
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void execute(EntityAdmin& admin, const BuildingSystem& buildings);
|
void execute(EntityAdmin& admin, const FactoryState& state);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,11 +15,11 @@
|
|||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const DebrisSystem& debris)
|
void SalvageScrapEvaluator::evaluate(EntityAdmin& admin)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
|
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
|
||||||
const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
|
const std::vector<DebrisInfo> allDebris = getAllDebrisInfo(admin);
|
||||||
|
|
||||||
admin.forEach<SalvageScrapBehavior, PositionComponent, SensorRangeComponent>(
|
admin.forEach<SalvageScrapBehavior, PositionComponent, SensorRangeComponent>(
|
||||||
[&](entt::entity e, SalvageScrapBehavior& salvage, const PositionComponent& pos,
|
[&](entt::entity e, SalvageScrapBehavior& salvage, const PositionComponent& pos,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
class DebrisSystem;
|
|
||||||
|
|
||||||
// When cargo is not full, finds the nearest debris within sensor range and sets
|
// When cargo is not full, finds the nearest debris within sensor range and sets
|
||||||
// it as the target, scoring high. Scores inactive when cargo is full or no debris
|
// it as the target, scoring high. Scores inactive when cargo is full or no debris
|
||||||
@@ -9,5 +8,5 @@ class DebrisSystem;
|
|||||||
class SalvageScrapEvaluator
|
class SalvageScrapEvaluator
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void evaluate(EntityAdmin& admin, const DebrisSystem& debris);
|
void evaluate(EntityAdmin& admin);
|
||||||
};
|
};
|
||||||
|
|||||||
173
src/lib/sim/BuildingBuffers.cpp
Normal file
173
src/lib/sim/BuildingBuffers.cpp
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#include "BuildingBuffers.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "ModulesConfig.h"
|
||||||
|
#include "ShipsConfig.h"
|
||||||
|
|
||||||
|
void initBuffers(Building& b, const RecipeDef& recipe)
|
||||||
|
{
|
||||||
|
b.inputBuffer.counts.clear();
|
||||||
|
b.inputBuffer.caps.clear();
|
||||||
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
|
{
|
||||||
|
const ItemType type{ing.item};
|
||||||
|
b.inputBuffer.counts[type] = 0;
|
||||||
|
b.inputBuffer.caps[type] = 2 * ing.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
b.outputBuffer.items.clear();
|
||||||
|
if (b.type == BuildingType::ReprocessingPlant)
|
||||||
|
{
|
||||||
|
// 1× max-per-roll (REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
|
||||||
|
int maxAmount = 0;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
if (out.amount > maxAmount)
|
||||||
|
{
|
||||||
|
maxAmount = out.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.outputBuffer.capacity = maxAmount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 2× per-cycle output.
|
||||||
|
int totalAmount = 0;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
totalAmount += out.amount;
|
||||||
|
}
|
||||||
|
b.outputBuffer.capacity = 2 * totalAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initAutoBuffers(const GameConfig& config, Building& b)
|
||||||
|
{
|
||||||
|
b.inputBuffer.counts.clear();
|
||||||
|
b.inputBuffer.caps.clear();
|
||||||
|
|
||||||
|
// Union the inputs of every recipe of this building type; the cap for each
|
||||||
|
// item is twice the largest per-cycle requirement across those recipes.
|
||||||
|
// Output capacity follows the same rules as initBuffers: the Reprocessing
|
||||||
|
// Plant holds one cycle's max output (REQ-MAT-OUTPUT-BUFFER-REPROCESSING),
|
||||||
|
// other auto buildings hold twice the largest per-cycle output.
|
||||||
|
int outputCapacity = 0;
|
||||||
|
for (const RecipeDef& recipe : config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (recipe.building != b.type)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
|
{
|
||||||
|
const ItemType type{ing.item};
|
||||||
|
b.inputBuffer.counts[type] = 0;
|
||||||
|
b.inputBuffer.caps[type] =
|
||||||
|
std::max(b.inputBuffer.caps[type], 2 * ing.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.type == BuildingType::ReprocessingPlant)
|
||||||
|
{
|
||||||
|
int maxAmount = 0;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
maxAmount = std::max(maxAmount, out.amount);
|
||||||
|
}
|
||||||
|
outputCapacity = std::max(outputCapacity, maxAmount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int totalAmount = 0;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
totalAmount += out.amount;
|
||||||
|
}
|
||||||
|
outputCapacity = std::max(outputCapacity, 2 * totalAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.outputBuffer.items.clear();
|
||||||
|
b.outputBuffer.capacity = outputCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
void initShipyardBuffers(const GameConfig& config, Building& b)
|
||||||
|
{
|
||||||
|
b.inputBuffer.counts.clear();
|
||||||
|
b.inputBuffer.caps.clear();
|
||||||
|
b.outputBuffer.items.clear();
|
||||||
|
b.outputBuffer.capacity = 0;
|
||||||
|
const ShipDef* def = config.ships.findShipDef(b.recipeId);
|
||||||
|
if (!def)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : def->schematic.materials)
|
||||||
|
{
|
||||||
|
const ItemType type{ing.item};
|
||||||
|
b.inputBuffer.counts[type] = 0;
|
||||||
|
b.inputBuffer.caps[type] = 2 * ing.amount;
|
||||||
|
}
|
||||||
|
if (b.shipLayout.has_value())
|
||||||
|
{
|
||||||
|
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
|
||||||
|
if (!modDef)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : modDef->materials)
|
||||||
|
{
|
||||||
|
const ItemType type{ing.item};
|
||||||
|
b.inputBuffer.counts.try_emplace(type, 0);
|
||||||
|
b.inputBuffer.caps[type] += 2 * ing.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initSalvageBayBuffer(const GameConfig& config, Building& b)
|
||||||
|
{
|
||||||
|
// Salvage Bay has no recipe-driven buffer; its output-buffer holding size for
|
||||||
|
// ship drop-off is config-defined (REQ-BLD-SALVAGE-BAY).
|
||||||
|
b.outputBuffer.items.clear();
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(BuildingType::SalvageBay);
|
||||||
|
b.outputBuffer.capacity =
|
||||||
|
(def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void reregisterBeltTile(BeltSystem& belts, const GameConfig& config,
|
||||||
|
const Building& building,
|
||||||
|
const std::vector<ItemType>& splitterFilterA,
|
||||||
|
const std::vector<ItemType>& splitterFilterB)
|
||||||
|
{
|
||||||
|
switch (building.type)
|
||||||
|
{
|
||||||
|
case BuildingType::Belt:
|
||||||
|
belts.placeBelt(building.anchor, building.rotation);
|
||||||
|
break;
|
||||||
|
case BuildingType::Splitter:
|
||||||
|
assert(building.outputPorts.size() >= 2);
|
||||||
|
belts.placeSplitter(building.anchor,
|
||||||
|
building.outputPorts[0].direction,
|
||||||
|
building.outputPorts[1].direction);
|
||||||
|
belts.setSplitterFilters(building.anchor, splitterFilterA, splitterFilterB);
|
||||||
|
break;
|
||||||
|
case BuildingType::TunnelEntry:
|
||||||
|
belts.placeTunnelEntry(building.anchor, building.rotation,
|
||||||
|
config.world.tunnelMaxDistance_tiles);
|
||||||
|
break;
|
||||||
|
case BuildingType::TunnelExit:
|
||||||
|
belts.placeTunnelExit(building.anchor, building.rotation);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
39
src/lib/sim/BuildingBuffers.h
Normal file
39
src/lib/sim/BuildingBuffers.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BeltSystem.h"
|
||||||
|
#include "Building.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "RecipesConfig.h"
|
||||||
|
|
||||||
|
// Setting a building up when it starts existing or is reconfigured: sizing its
|
||||||
|
// input/output buffers from what it will produce, and handing belt-like types back
|
||||||
|
// to BeltSystem. Free functions over the config and the building — they read no
|
||||||
|
// factory state, so both BuildingSystem and ConstructionSystem can use them.
|
||||||
|
|
||||||
|
// Buffers for a building running one known recipe: inputs capped at twice each
|
||||||
|
// ingredient's per-cycle amount, output at twice the per-cycle total (one cycle's
|
||||||
|
// max for a Reprocessing Plant, REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
|
||||||
|
void initBuffers(Building& b, const RecipeDef& recipe);
|
||||||
|
|
||||||
|
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant), unioned over
|
||||||
|
// every recipe of its type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||||
|
void initAutoBuffers(const GameConfig& config, Building& b);
|
||||||
|
|
||||||
|
// Buffers for a shipyard: its schematic's materials plus those of every placed
|
||||||
|
// module (REQ-BLD-SHIPYARD).
|
||||||
|
void initShipyardBuffers(const GameConfig& config, Building& b);
|
||||||
|
|
||||||
|
// The Salvage Bay holds no recipe inputs; its output capacity is config-defined
|
||||||
|
// (REQ-BLD-SALVAGE-BAY).
|
||||||
|
void initSalvageBayBuffer(const GameConfig& config, Building& b);
|
||||||
|
|
||||||
|
// Registers a belt, splitter or tunnel end with BeltSystem. A splitter's filters
|
||||||
|
// live in BeltSystem and are lost by removeTile, so they are passed back in
|
||||||
|
// (REQ-BLD-SPLITTER). No-op for every other building type.
|
||||||
|
void reregisterBeltTile(BeltSystem& belts, const GameConfig& config,
|
||||||
|
const Building& building,
|
||||||
|
const std::vector<ItemType>& splitterFilterA,
|
||||||
|
const std::vector<ItemType>& splitterFilterB);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "BuildingConfig.h"
|
#include "BuildingConfig.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
@@ -26,8 +27,8 @@ struct SelectedBuilding
|
|||||||
// (the HQ and defence stations, per REQ-UI-BLUEPRINT-CREATE).
|
// (the HQ and defence stations, per REQ-UI-BLUEPRINT-CREATE).
|
||||||
std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, BuildingId id)
|
std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, BuildingId id)
|
||||||
{
|
{
|
||||||
const Building* building = sim.getBuildings().findBuilding(id);
|
const Building* building = findBuilding(sim.getFactoryState(), id);
|
||||||
const ConstructionSite* site = building ? nullptr : sim.getBuildings().findSite(id);
|
const ConstructionSite* site = building ? nullptr : findSite(sim.getFactoryState(), id);
|
||||||
if (!building && !site)
|
if (!building && !site)
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -52,8 +53,8 @@ std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, Building
|
|||||||
|
|
||||||
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
|
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
|
||||||
{
|
{
|
||||||
const Building* building = sim.getBuildings().findBuilding(id);
|
const Building* building = findBuilding(sim.getFactoryState(), id);
|
||||||
const ConstructionSite* site = building ? nullptr : sim.getBuildings().findSite(id);
|
const ConstructionSite* site = building ? nullptr : findSite(sim.getFactoryState(), id);
|
||||||
if (!building && !site)
|
if (!building && !site)
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|||||||
52
src/lib/sim/BuildingGrid.cpp
Normal file
52
src/lib/sim/BuildingGrid.cpp
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#include "BuildingGrid.h"
|
||||||
|
|
||||||
|
#include "StateChecksum.h"
|
||||||
|
|
||||||
|
void BuildingGrid::occupy(QPoint cell, BuildingId id)
|
||||||
|
{
|
||||||
|
m_owners[{cell.x(), cell.y()}] = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildingGrid::occupy(const std::vector<QPoint>& cells, BuildingId id)
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : cells)
|
||||||
|
{
|
||||||
|
occupy(cell, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildingGrid::release(const std::vector<QPoint>& cells)
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : cells)
|
||||||
|
{
|
||||||
|
m_owners.erase({cell.x(), cell.y()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BuildingGrid::isOccupied(QPoint tile) const
|
||||||
|
{
|
||||||
|
return m_owners.count({tile.x(), tile.y()}) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BuildingId> BuildingGrid::findOwner(QPoint tile) const
|
||||||
|
{
|
||||||
|
const std::map<std::pair<int, int>, BuildingId>::const_iterator it =
|
||||||
|
m_owners.find({tile.x(), tile.y()});
|
||||||
|
if (it == m_owners.end())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildingGrid::appendChecksum(Hasher& hasher) const
|
||||||
|
{
|
||||||
|
// std::map iterates in sorted key order.
|
||||||
|
hasher.append(m_owners.size());
|
||||||
|
for (const std::pair<const std::pair<int, int>, BuildingId>& entry : m_owners)
|
||||||
|
{
|
||||||
|
hasher.append(entry.first.first);
|
||||||
|
hasher.append(entry.first.second);
|
||||||
|
hasher.append(entry.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/lib/sim/BuildingGrid.h
Normal file
46
src/lib/sim/BuildingGrid.h
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
|
||||||
|
class Hasher;
|
||||||
|
|
||||||
|
// The authority on which building owns which world tile.
|
||||||
|
//
|
||||||
|
// Every building and construction site claims its body cells here when it is placed
|
||||||
|
// and releases them when it is removed, so the map is the single place that knows
|
||||||
|
// whether a tile is free. It is a plain index owned by BuildingSystem, not a system:
|
||||||
|
// it has no per-tick behaviour and nothing outside BuildingSystem touches it.
|
||||||
|
//
|
||||||
|
// Keys are deliberately std::pair<int, int> rather than QPoint: the checksum folds the
|
||||||
|
// entries in map iteration order (docs/replay_design.md), so the comparator is part of
|
||||||
|
// the determinism contract and is not changed casually.
|
||||||
|
class BuildingGrid
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Records absolute body cells as owned by id. Re-occupying a cell overwrites its
|
||||||
|
// previous owner, matching the placement paths that reserve cells for a site and
|
||||||
|
// then hand them to the building it becomes.
|
||||||
|
void occupy(QPoint cell, BuildingId id);
|
||||||
|
void occupy(const std::vector<QPoint>& cells, BuildingId id);
|
||||||
|
|
||||||
|
// Releases absolute body cells. Cells that are not occupied are ignored.
|
||||||
|
void release(const std::vector<QPoint>& cells);
|
||||||
|
|
||||||
|
bool isOccupied(QPoint tile) const;
|
||||||
|
|
||||||
|
// The building owning the tile, or nullopt when the tile is free.
|
||||||
|
std::optional<BuildingId> findOwner(QPoint tile) const;
|
||||||
|
|
||||||
|
// Folds the occupancy into the hasher in deterministic order.
|
||||||
|
void appendChecksum(Hasher& hasher) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<std::pair<int, int>, BuildingId> m_owners;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,11 @@
|
|||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "BuildingBuffers.h"
|
||||||
|
#include "DeconstructionSystem.h"
|
||||||
|
#include "PlacementRules.h"
|
||||||
|
#include "ProductionRules.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
@@ -26,18 +31,6 @@
|
|||||||
|
|
||||||
class Hasher;
|
class Hasher;
|
||||||
|
|
||||||
// Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT).
|
|
||||||
// The simulation owns the classification so it stays in sync with the
|
|
||||||
// production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill
|
|
||||||
// color.
|
|
||||||
enum class ProductionStatus
|
|
||||||
{
|
|
||||||
Unconfigured, // no recipe/schematic selected (grey)
|
|
||||||
Producing, // a production cycle is active (green)
|
|
||||||
Starved, // idle: a required input is missing / Salvage Bay empty (red)
|
|
||||||
Blocked, // idle: output buffer full, inputs otherwise present (yellow)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Manages building placement, construction queuing, and the per-tick
|
// Manages building placement, construction queuing, and the per-tick
|
||||||
// production loop (belt→building pull, production, building→belt push).
|
// production loop (belt→building pull, production, building→belt push).
|
||||||
// All types including Belt and Splitter are stored as Building instances;
|
// All types including Belt and Splitter are stored as Building instances;
|
||||||
@@ -61,7 +54,7 @@ public:
|
|||||||
// queue. Terrain type (A vs S) is NOT checked here so that tests can stage
|
// queue. Terrain type (A vs S) is NOT checked here so that tests can stage
|
||||||
// arbitrary layouts; the player-facing entry point
|
// arbitrary layouts; the player-facing entry point
|
||||||
// (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid.
|
// (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid.
|
||||||
std::optional<BuildingId> place(BuildingType type, QPoint anchor, Rotation rotation,
|
std::optional<BuildingId> place(FactoryState& state, BuildingType type, QPoint anchor, Rotation rotation,
|
||||||
Tick currentTick);
|
Tick currentTick);
|
||||||
|
|
||||||
// Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and
|
// Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and
|
||||||
@@ -69,13 +62,12 @@ public:
|
|||||||
// other body (A) cell sits on the asteroid (x < 0 and x >= the left edge),
|
// other body (A) cell sits on the asteroid (x < 0 and x >= the left edge),
|
||||||
// and every cell has 0 <= y < world.height_tiles. There is no right-side
|
// and every cell has 0 <= y < world.height_tiles. There is no right-side
|
||||||
// bound — space extends rightward. Tile occupancy is NOT checked here.
|
// bound — space extends rightward. Tile occupancy is NOT checked here.
|
||||||
bool isPlacementValid(BuildingType type, QPoint anchor,
|
|
||||||
Rotation rotation) const;
|
|
||||||
|
|
||||||
// Sets the current buildable asteroid width in tiles. Grows the left
|
// Sets the current buildable asteroid width in tiles. Grows the left
|
||||||
// placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
|
// placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
|
||||||
// Defaults to world.regions.asteroid_width_tiles at construction.
|
// Defaults to world.regions.asteroid_width_tiles at construction.
|
||||||
void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; }
|
void setAsteroidWidth_tiles(FactoryState& state, int widthTiles) const
|
||||||
|
{ state.asteroidWidth_tiles = widthTiles; }
|
||||||
|
|
||||||
// Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT).
|
// Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT).
|
||||||
// A construction site is removed instantly and the full cost is returned.
|
// A construction site is removed instantly and the full cost is returned.
|
||||||
@@ -83,24 +75,23 @@ public:
|
|||||||
// (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is
|
// (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is
|
||||||
// credited later, on completion in tickDeconstruction, so this returns 0 for
|
// credited later, on completion in tickDeconstruction, so this returns 0 for
|
||||||
// it. Returns 0 for unknown ids and for a building already queued.
|
// it. Returns 0 for unknown ids and for a building already queued.
|
||||||
int deconstruct(BuildingId id, Tick currentTick);
|
int deconstruct(FactoryState& state, BuildingId id, Tick currentTick);
|
||||||
|
|
||||||
// Take a building back out of the deconstruction queue before it is removed
|
// Take a building back out of the deconstruction queue before it is removed
|
||||||
// (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation
|
// (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation
|
||||||
// (re-registering belt/tunnel/splitter tiles); discards deconstruction
|
// (re-registering belt/tunnel/splitter tiles); discards deconstruction
|
||||||
// progress and credits no refund. No-op if the id is not queued.
|
// progress and credits no refund. No-op if the id is not queued.
|
||||||
void cancelDeconstruction(BuildingId id);
|
void cancelDeconstruction(FactoryState& state, BuildingId id);
|
||||||
|
|
||||||
// True if the building is currently in the deconstruction queue.
|
// True if the building is currently in the deconstruction queue.
|
||||||
bool isQueuedForDeconstruction(BuildingId id) const;
|
|
||||||
|
|
||||||
// Set the recipe (or schematic id for shipyard) on a building or queued
|
// Set the recipe (or schematic id for shipyard) on a building or queued
|
||||||
// construction site. Clears both buffers on an operational building.
|
// construction site. Clears both buffers on an operational building.
|
||||||
void setRecipe(BuildingId id, const std::string& recipeId);
|
void setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId);
|
||||||
|
|
||||||
// Set the module layout for a shipyard. Cancels in-progress production
|
// Set the module layout for a shipyard. Cancels in-progress production
|
||||||
// (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD).
|
// (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD).
|
||||||
void setShipLayout(BuildingId id, const ShipLayoutConfig& layout);
|
void setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout);
|
||||||
|
|
||||||
// Splitter filter configuration for a queued/under-construction Splitter
|
// Splitter filter configuration for a queued/under-construction Splitter
|
||||||
// site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through
|
// site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through
|
||||||
@@ -109,130 +100,94 @@ public:
|
|||||||
// output directions (derived from its surface mask) and stored filters, or
|
// output directions (derived from its surface mask) and stored filters, or
|
||||||
// nullopt if the id is not a Splitter site. The stored filters are applied
|
// nullopt if the id is not a Splitter site. The stored filters are applied
|
||||||
// to BeltSystem when the splitter finishes building (tickConstruction).
|
// to BeltSystem when the splitter finishes building (tickConstruction).
|
||||||
std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(BuildingId id) const;
|
void setSiteSplitterFilters(FactoryState& state, BuildingId id,
|
||||||
void setSiteSplitterFilters(BuildingId id,
|
|
||||||
const std::vector<ItemType>& filterA,
|
const std::vector<ItemType>& filterA,
|
||||||
const std::vector<ItemType>& filterB);
|
const std::vector<ItemType>& filterB);
|
||||||
|
|
||||||
// -- Tick hooks (called from Simulation::tick in the documented order) ---
|
// -- Tick hooks (called from Simulation::tick in the documented order) ---
|
||||||
void tickConstruction(Tick currentTick);
|
|
||||||
// Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a
|
// Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a
|
||||||
// time, in parallel with tickConstruction. Removes the front building and
|
// time, in parallel with tickConstruction. Removes the front building and
|
||||||
// credits its refund when its timer elapses.
|
// credits its refund when its timer elapses.
|
||||||
void tickDeconstruction(Tick currentTick);
|
void tickBeltPull(FactoryState& state);
|
||||||
void tickBeltPull();
|
void tickProduction(FactoryState& state, Tick currentTick);
|
||||||
void tickProduction(Tick currentTick);
|
void tickShipyardProduction(FactoryState& state, Tick currentTick);
|
||||||
void tickShipyardProduction(Tick currentTick);
|
|
||||||
// Advances each building's virtual output belts, hands finished items off onto
|
// Advances each building's virtual output belts, hands finished items off onto
|
||||||
// the adjacent real belt, and feeds new buffered items into them
|
// the adjacent real belt, and feeds new buffered items into them
|
||||||
// (REQ-MAT-OUTPUT-EMERGE).
|
// (REQ-MAT-OUTPUT-EMERGE).
|
||||||
void tickOutputBelts();
|
void tickOutputBelts(FactoryState& state);
|
||||||
|
|
||||||
// -- Queries -------------------------------------------------------------
|
// -- Queries -------------------------------------------------------------
|
||||||
struct BeltTileInfo
|
|
||||||
{
|
|
||||||
BuildingId buildingId;
|
|
||||||
QPoint tile;
|
|
||||||
BuildingType type; // Belt or Splitter
|
|
||||||
Rotation directionA; // Belt: its direction; Splitter: first output
|
|
||||||
Rotation directionB; // Splitter: second output; Belt: same as directionA
|
|
||||||
};
|
|
||||||
|
|
||||||
const Building* findBuilding(BuildingId id) const;
|
|
||||||
const ConstructionSite* findSite(BuildingId id) const;
|
|
||||||
std::vector<Building> getAllBuildings() const;
|
|
||||||
std::vector<ConstructionSite> getAllSites() const;
|
|
||||||
|
|
||||||
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
|
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
|
||||||
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
|
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
|
||||||
int getProductionBuildingCount() const;
|
|
||||||
|
|
||||||
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
|
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
|
||||||
// that currently has an active production cycle.
|
// that currently has an active production cycle.
|
||||||
int getActiveProductionBuildingCount() const;
|
|
||||||
|
|
||||||
// Production state for the UI status light (REQ-UI-STATUS-LIGHT). Returns
|
// Production state for the UI status light (REQ-UI-STATUS-LIGHT). Returns
|
||||||
// nullopt for building types that show no light (belts, splitters, tunnels,
|
// nullopt for building types that show no light (belts, splitters, tunnels,
|
||||||
// HQ, defence stations). The Salvage Bay is a two-state special case:
|
// HQ, defence stations). The Salvage Bay is a two-state special case:
|
||||||
// Producing while its output buffer holds scrap, Starved when empty.
|
// Producing while its output buffer holds scrap, Starved when empty.
|
||||||
std::optional<ProductionStatus> getProductionStatus(const Building& building) const;
|
|
||||||
std::vector<BeltTileInfo> getAllBeltTiles() const;
|
|
||||||
bool isTileOccupied(QPoint tile) const;
|
|
||||||
|
|
||||||
// Visits every item currently emerging from a building output port on its
|
// Visits every item currently emerging from a building output port on its
|
||||||
// virtual output belt (REQ-MAT-OUTPUT-EMERGE), passing the item type and its
|
// virtual output belt (REQ-MAT-OUTPUT-EMERGE), passing the item type and its
|
||||||
// world-space centre (in tile units). Least-progressed first (drawn bottom) so
|
// world-space centre (in tile units). Least-progressed first (drawn bottom) so
|
||||||
// callers can paint in visit order (REQ-GW-TILE-SIZE ordering).
|
// callers can paint in visit order (REQ-GW-TILE-SIZE ordering).
|
||||||
void forEachEmergingItem(
|
void forEachEmergingItem(const FactoryState& state,
|
||||||
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
||||||
|
|
||||||
// Visits every item currently travelling inward on a building input port's
|
// Visits every item currently travelling inward on a building input port's
|
||||||
// virtual input belt (REQ-MAT-INPUT-INTAKE), passing the item type and its
|
// virtual input belt (REQ-MAT-INPUT-INTAKE), passing the item type and its
|
||||||
// world-space centre (in tile units). Least-progressed first (drawn bottom).
|
// world-space centre (in tile units). Least-progressed first (drawn bottom).
|
||||||
void forEachIncomingItem(
|
void forEachIncomingItem(const FactoryState& state,
|
||||||
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
||||||
|
|
||||||
// Returns the entity id of the building or construction site whose footprint
|
|
||||||
// exactly coincides with the ghost (type, anchor, rot) and is of the same
|
|
||||||
// building type. Returns nullopt otherwise.
|
|
||||||
std::optional<BuildingId> findRotateInPlaceTarget(BuildingType type,
|
|
||||||
QPoint anchor,
|
|
||||||
Rotation rot) const;
|
|
||||||
|
|
||||||
// Rotate an existing building or construction site to newRotation in place.
|
// Rotate an existing building or construction site to newRotation in place.
|
||||||
// For belt-type operational buildings, re-registers with BeltSystem (items
|
// For belt-type operational buildings, re-registers with BeltSystem (items
|
||||||
// currently on the tile are discarded by BeltSystem::removeTile).
|
// currently on the tile are discarded by BeltSystem::removeTile).
|
||||||
void rotateInPlace(BuildingId id, Rotation newRotation);
|
void rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation);
|
||||||
|
|
||||||
// Find nearest operational building of the given type; nullptr if none.
|
|
||||||
const Building* findNearestBuilding(QVector2D worldPos, BuildingType type) const;
|
|
||||||
|
|
||||||
// Input-capable adjacent tiles for a building or construction site
|
// Input-capable adjacent tiles for a building or construction site
|
||||||
// (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS): each returned Port.tile is the
|
// (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS): each returned Port.tile is the
|
||||||
// outside adjacent tile and Port.direction is the belt facing that points into
|
// outside adjacent tile and Port.direction is the belt facing that points into
|
||||||
// the target. Output-port edges are excluded. Empty for an unknown id.
|
// the target. Output-port edges are excluded. Empty for an unknown id.
|
||||||
std::vector<Port> getInputPorts(BuildingId id) const;
|
|
||||||
|
|
||||||
// Register / unregister tile occupancy for ECS station entities.
|
// Register / unregister tile occupancy for ECS station entities.
|
||||||
void registerTileOccupancy(const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
|
void registerTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
|
||||||
void unregisterTileOccupancy(const std::vector<QPoint>& cells);
|
void unregisterTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells);
|
||||||
|
|
||||||
// Place one "scrap" item into a SalvageBay's output buffer.
|
// Place one "scrap" item into a SalvageBay's output buffer.
|
||||||
// Returns false if bay not found, wrong type, or output buffer is full.
|
// Returns false if bay not found, wrong type, or output buffer is full.
|
||||||
bool deliverScrapToSalvageBay(BuildingId bayId);
|
|
||||||
|
|
||||||
// Bypass the construction queue and create a fully-operational Building
|
// Bypass the construction queue and create a fully-operational Building
|
||||||
// immediately. Used for pre-placed structures (HQ, defence stations).
|
// immediately. Used for pre-placed structures (HQ, defence stations).
|
||||||
// surfaceMask comes from the relevant config struct.
|
// surfaceMask comes from the relevant config struct.
|
||||||
BuildingId placeImmediate(BuildingType type,
|
BuildingId placeImmediate(FactoryState& state, BuildingType type,
|
||||||
const std::vector<std::string>& surfaceMask,
|
const std::vector<std::string>& surfaceMask,
|
||||||
QPoint anchor, Rotation rotation);
|
QPoint anchor, Rotation rotation);
|
||||||
|
|
||||||
// Remove an operational building by id without refund (used for deaths).
|
// Remove an operational building by id without refund (used for deaths).
|
||||||
// Returns true if found and removed.
|
// Returns true if found and removed.
|
||||||
bool removeBuilding(BuildingId id);
|
bool removeBuilding(FactoryState& state, BuildingId id);
|
||||||
|
|
||||||
// Mutable iteration over all operational buildings.
|
// Mutable iteration over all operational buildings.
|
||||||
void forEachBuilding(std::function<void(Building&)> fn);
|
void forEachBuilding(FactoryState& state, std::function<void(Building&)> fn);
|
||||||
|
|
||||||
// -- Determinism ---------------------------------------------------------
|
// -- Determinism ---------------------------------------------------------
|
||||||
// Folds all building, construction-site, and tile-occupancy state into the
|
// Folds all building, construction-site, and tile-occupancy state into the
|
||||||
// hasher in deterministic order (see docs/replay_design.md).
|
// hasher in deterministic order (see docs/replay_design.md).
|
||||||
void appendChecksum(Hasher& hasher) const;
|
void appendChecksum(const FactoryState& state, Hasher& hasher) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Starts the front deconstruction-queue entry's timer if not yet started
|
// Starts the front deconstruction-queue entry's timer if not yet started
|
||||||
// (mirrors how tickConstruction starts a queued construction site).
|
// (mirrors how tickConstruction starts a queued construction site).
|
||||||
void startFrontDeconstruction(Tick currentTick);
|
|
||||||
|
|
||||||
// Registers a belt/splitter/tunnel building's tile with the belt subsystem
|
// Registers a belt/splitter/tunnel building's tile with the belt subsystem
|
||||||
// (on construction completion, or when un-queuing a deconstruction). No-op for
|
// (on construction completion, or when un-queuing a deconstruction). No-op for
|
||||||
// non-belt-subsystem types. Splitter filters are (re)applied after placement.
|
// non-belt-subsystem types. Splitter filters are (re)applied after placement.
|
||||||
void reregisterBeltTile(const Building& building,
|
|
||||||
const std::vector<ItemType>& splitterFilterA,
|
|
||||||
const std::vector<ItemType>& splitterFilterB);
|
|
||||||
|
|
||||||
Building* findBuildingMutable(BuildingId id);
|
|
||||||
// True if the consumer would accept `type` at the given input port right now:
|
// True if the consumer would accept `type` at the given input port right now:
|
||||||
// it is a required input (or a building block for the HQ), the reservation-aware
|
// it is a required input (or a building block for the HQ), the reservation-aware
|
||||||
// buffer has room, and the input belt entry is free (REQ-MAT-INPUT-INTAKE).
|
// buffer has room, and the input belt entry is free (REQ-MAT-INPUT-INTAKE).
|
||||||
@@ -247,7 +202,7 @@ private:
|
|||||||
// Attempts to hand an emerging output item straight into a directly adjacent
|
// Attempts to hand an emerging output item straight into a directly adjacent
|
||||||
// building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE).
|
// building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE).
|
||||||
// Returns true if the item was accepted onto the consumer's input belt.
|
// Returns true if the item was accepted onto the consumer's input belt.
|
||||||
bool tryDirectCoupleDeposit(BuildingId producerId,
|
bool tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
|
||||||
const Port& outputPort,
|
const Port& outputPort,
|
||||||
const Item& item);
|
const Item& item);
|
||||||
|
|
||||||
@@ -255,35 +210,22 @@ private:
|
|||||||
// building (Smelter, Reprocessing Plant) offers every recipe of its type with
|
// building (Smelter, Reprocessing Plant) offers every recipe of its type with
|
||||||
// inputs; other buildings offer only their selected recipe. Shared by
|
// inputs; other buildings offer only their selected recipe. Shared by
|
||||||
// tickProduction and the status classifier (REQ-MAT-CYCLE, REQ-UI-STATUS-LIGHT).
|
// tickProduction and the status classifier (REQ-MAT-CYCLE, REQ-UI-STATUS-LIGHT).
|
||||||
std::vector<const RecipeDef*> gatherCandidateRecipes(const Building& b) const;
|
|
||||||
// True if every input of `recipe` is present in `b`'s input buffers in the
|
// True if every input of `recipe` is present in `b`'s input buffers in the
|
||||||
// required per-cycle amount (REQ-MAT-CYCLE input check).
|
// required per-cycle amount (REQ-MAT-CYCLE input check).
|
||||||
bool recipeInputsAvailable(const Building& b,
|
|
||||||
const RecipeDef& recipe) const;
|
|
||||||
// Combined base + module materials a shipyard needs per ship (REQ-BLD-SHIPYARD).
|
// Combined base + module materials a shipyard needs per ship (REQ-BLD-SHIPYARD).
|
||||||
std::map<std::string, int> computeShipyardRequiredMaterials(const Building& b) const;
|
|
||||||
// True if the building currently has all inputs/materials to start a cycle
|
// True if the building currently has all inputs/materials to start a cycle
|
||||||
// (ignoring output-buffer space); drives the Starved/Blocked distinction of
|
// (ignoring output-buffer space); drives the Starved/Blocked distinction of
|
||||||
// the status light (REQ-UI-STATUS-LIGHT).
|
// the status light (REQ-UI-STATUS-LIGHT).
|
||||||
bool hasInputsToStart(const Building& b) const;
|
|
||||||
|
|
||||||
void initBuffers(Building& b, const RecipeDef& recipe) const;
|
|
||||||
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input
|
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input
|
||||||
// caps span the union of every recipe of the building's type; no player
|
// caps span the union of every recipe of the building's type; no player
|
||||||
// recipe is selected (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
// recipe is selected (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||||
void initAutoBuffers(Building& b) const;
|
|
||||||
void initShipyardBuffers(Building& b) const;
|
|
||||||
void initSalvageBayBuffer(Building& b) const;
|
|
||||||
std::vector<Port> computeInputPorts(const Building& b) const;
|
|
||||||
// Core input-edge scan shared by operational buildings and construction sites.
|
// Core input-edge scan shared by operational buildings and construction sites.
|
||||||
std::vector<Port> computeInputPorts(const std::vector<QPoint>& bodyCells,
|
|
||||||
const std::vector<Port>& outputPorts) const;
|
|
||||||
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
|
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
|
||||||
bool bodyCellsWithinWorldBounds(
|
|
||||||
const std::vector<QPoint>& bodyCells,
|
|
||||||
QPoint anchor) const;
|
|
||||||
|
|
||||||
const GameConfig& m_config;
|
const GameConfig& m_config;
|
||||||
|
|
||||||
|
|
||||||
BeltSystem& m_belts;
|
BeltSystem& m_belts;
|
||||||
std::function<BuildingId()> m_allocateBuildingId;
|
std::function<BuildingId()> m_allocateBuildingId;
|
||||||
std::function<void(int)> m_addBuildingBlocks;
|
std::function<void(int)> m_addBuildingBlocks;
|
||||||
@@ -291,24 +233,4 @@ private:
|
|||||||
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
||||||
std::function<bool(const std::string&)> m_isItemUnlocked;
|
std::function<bool(const std::string&)> m_isItemUnlocked;
|
||||||
std::mt19937& m_rng;
|
std::mt19937& m_rng;
|
||||||
int m_asteroidWidth_tiles;
|
|
||||||
|
|
||||||
std::vector<Building> m_buildings;
|
|
||||||
std::deque<ConstructionSite> m_constructionQueue;
|
|
||||||
|
|
||||||
// One pending demolition of a fully-built building (REQ-BLD-DECON-QUEUE).
|
|
||||||
// completesAt == 0 means "queued but its timer has not started yet"
|
|
||||||
// (mirrors ConstructionSite). For a Splitter, the filters it had are captured
|
|
||||||
// here so cancelDeconstruction can restore them on re-registration.
|
|
||||||
struct DeconstructionEntry
|
|
||||||
{
|
|
||||||
BuildingId id = kInvalidBuildingId;
|
|
||||||
Tick completesAt = 0;
|
|
||||||
std::vector<ItemType> splitterFilterA;
|
|
||||||
std::vector<ItemType> splitterFilterB;
|
|
||||||
};
|
|
||||||
std::deque<DeconstructionEntry> m_deconstructionQueue;
|
|
||||||
|
|
||||||
// Maps every occupied body-cell coordinate to the entity that owns it.
|
|
||||||
std::map<std::pair<int, int>, BuildingId> m_tileOccupancy;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
||||||
@@ -19,6 +27,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.h
|
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/UnlockState.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
@@ -35,11 +44,19 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSlot.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltSlot.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/UnlockState.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
112
src/lib/sim/ConstructionSystem.cpp
Normal file
112
src/lib/sim/ConstructionSystem.cpp
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#include "ConstructionSystem.h"
|
||||||
|
|
||||||
|
#include "BuildingBuffers.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "PortGeometry.h"
|
||||||
|
#include "SurfaceMask.h"
|
||||||
|
#include "tracing.h"
|
||||||
|
|
||||||
|
void ConstructionSystem::tick(FactoryState& state, BeltSystem& belts, Tick currentTick)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
if (state.constructionQueue.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConstructionSite& front = state.constructionQueue.front();
|
||||||
|
|
||||||
|
// Guard: if somehow the front site was never started, start it now.
|
||||||
|
if (front.completesAt == 0)
|
||||||
|
{
|
||||||
|
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
|
||||||
|
if (def)
|
||||||
|
{
|
||||||
|
front.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTick < front.completesAt)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promote construction site to an operational Building.
|
||||||
|
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(
|
||||||
|
def ? def->surfaceMask : std::vector<std::string>{},
|
||||||
|
front.rotation);
|
||||||
|
|
||||||
|
Building building;
|
||||||
|
building.id = front.id;
|
||||||
|
building.anchor = front.anchor;
|
||||||
|
building.footprint = front.footprint;
|
||||||
|
building.rotation = front.rotation;
|
||||||
|
building.type = front.type;
|
||||||
|
building.recipeId = front.recipeId;
|
||||||
|
building.shipLayout = front.shipLayout;
|
||||||
|
|
||||||
|
for (const QPoint& cell : mask.bodyCells)
|
||||||
|
{
|
||||||
|
building.bodyCells.push_back(front.anchor + cell);
|
||||||
|
}
|
||||||
|
for (const Port& port : mask.outputPorts)
|
||||||
|
{
|
||||||
|
Port absPort;
|
||||||
|
absPort.tile = front.anchor + port.tile;
|
||||||
|
absPort.direction = port.direction;
|
||||||
|
building.outputPorts.push_back(absPort);
|
||||||
|
}
|
||||||
|
building.emergingItems.resize(building.outputPorts.size());
|
||||||
|
building.inputPorts = computeInputPorts(building.bodyCells, building.outputPorts);
|
||||||
|
building.incomingItems.assign(building.inputPorts.size(), {});
|
||||||
|
|
||||||
|
if (building.type == BuildingType::SalvageBay)
|
||||||
|
{
|
||||||
|
initSalvageBayBuffer(m_config, building);
|
||||||
|
}
|
||||||
|
else if (isAutoRecipeBuildingType(building.type))
|
||||||
|
{
|
||||||
|
// Smelter/Reprocessing Plant need no recipe selection; buffers are set
|
||||||
|
// up from all recipes of the type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||||
|
initAutoBuffers(m_config, building);
|
||||||
|
}
|
||||||
|
else if (!building.recipeId.empty())
|
||||||
|
{
|
||||||
|
if (building.type == BuildingType::Shipyard)
|
||||||
|
{
|
||||||
|
initShipyardBuffers(m_config, building);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const RecipeDef* recipe = m_config.recipes.findRecipeDef(building.recipeId, building.type);
|
||||||
|
if (recipe)
|
||||||
|
{
|
||||||
|
initBuffers(building, *recipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register with BeltSystem before the move (mask/building stays valid). Any
|
||||||
|
// filters configured while under construction carry over (REQ-BLD-SITE-CONFIG).
|
||||||
|
reregisterBeltTile(belts, m_config, building, front.splitterFilterA, front.splitterFilterB);
|
||||||
|
|
||||||
|
state.buildings.push_back(std::move(building));
|
||||||
|
|
||||||
|
state.constructionQueue.pop_front();
|
||||||
|
|
||||||
|
// Start next queued site if present.
|
||||||
|
if (!state.constructionQueue.empty() && state.constructionQueue.front().completesAt == 0)
|
||||||
|
{
|
||||||
|
const BuildingDef* nextDef =
|
||||||
|
m_config.buildings.findBuildingDef(state.constructionQueue.front().type);
|
||||||
|
if (nextDef)
|
||||||
|
{
|
||||||
|
state.constructionQueue.front().completesAt =
|
||||||
|
currentTick + secondsToTicks(nextDef->constructionTimeSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
30
src/lib/sim/ConstructionSystem.h
Normal file
30
src/lib/sim/ConstructionSystem.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BeltSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
// Advances the construction queue and turns a finished site into an operational
|
||||||
|
// building (REQ-BLD-CONSTRUCTION). One site is built at a time, in queue order:
|
||||||
|
// the front site's timer runs, and when it elapses the site becomes a Building —
|
||||||
|
// its ports and buffers are derived from its definition, its belt tile is handed
|
||||||
|
// back to BeltSystem, and the next queued site starts.
|
||||||
|
//
|
||||||
|
// It completes the building itself rather than handing the finished site back to
|
||||||
|
// BuildingSystem: everything materialisation needs is either in FactoryState, the
|
||||||
|
// config, or a free function (see BuildingBuffers.h, PortGeometry.h), so there is
|
||||||
|
// no intermediate value to pass and no ordering rule between two calls.
|
||||||
|
//
|
||||||
|
// Holds only the config; the world it works on arrives per tick, like the other
|
||||||
|
// systems in lib/ecs/system.
|
||||||
|
class ConstructionSystem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ConstructionSystem(const GameConfig& config) : m_config(config) {}
|
||||||
|
|
||||||
|
void tick(FactoryState& state, BeltSystem& belts, Tick currentTick);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const GameConfig& m_config;
|
||||||
|
};
|
||||||
67
src/lib/sim/DeconstructionSystem.cpp
Normal file
67
src/lib/sim/DeconstructionSystem.cpp
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#include "DeconstructionSystem.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "tracing.h"
|
||||||
|
|
||||||
|
void startFrontDeconstruction(FactoryState& state, const GameConfig& config,
|
||||||
|
Tick currentTick)
|
||||||
|
{
|
||||||
|
if (state.deconstructionQueue.empty()) { return; }
|
||||||
|
DeconstructionEntry& front = state.deconstructionQueue.front();
|
||||||
|
if (front.completesAt == 0)
|
||||||
|
{
|
||||||
|
front.completesAt =
|
||||||
|
currentTick + secondsToTicks(config.world.deconstructionTimeSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DeconstructionSystem::tick(FactoryState& state, Tick currentTick)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
if (state.deconstructionQueue.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeconstructionEntry& front = state.deconstructionQueue.front();
|
||||||
|
|
||||||
|
// Guard: if the front entry's timer was never started, start it now.
|
||||||
|
if (front.completesAt == 0)
|
||||||
|
{
|
||||||
|
startFrontDeconstruction(state, m_config, currentTick);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTick < front.completesAt)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the building from the world and credit its refund (REQ-BLD-DECONSTRUCT).
|
||||||
|
// Belt/tunnel/splitter tiles were already unregistered when the building was
|
||||||
|
// queued (see deconstruct), so only tile occupancy and the record remain.
|
||||||
|
for (std::vector<Building>::iterator it = state.buildings.begin();
|
||||||
|
it != state.buildings.end();
|
||||||
|
++it)
|
||||||
|
{
|
||||||
|
if (it->id != front.id) { continue; }
|
||||||
|
|
||||||
|
const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
|
||||||
|
state.grid.release(it->bodyCells);
|
||||||
|
state.buildings.erase(it);
|
||||||
|
if (def)
|
||||||
|
{
|
||||||
|
m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.deconstructionQueue.pop_front();
|
||||||
|
|
||||||
|
// Start the next queued deconstruction, if any.
|
||||||
|
startFrontDeconstruction(state, m_config, currentTick);
|
||||||
|
}
|
||||||
|
|
||||||
36
src/lib/sim/DeconstructionSystem.h
Normal file
36
src/lib/sim/DeconstructionSystem.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
// The queue timer for pending demolitions (REQ-BLD-DECON-QUEUE): one building at a
|
||||||
|
// time, in parallel with construction. When the front entry's timer elapses the
|
||||||
|
// building is removed from the world, its tiles are released, and its partial refund
|
||||||
|
// is credited.
|
||||||
|
//
|
||||||
|
// It needs no BeltSystem: a belt, splitter or tunnel end is unregistered the moment
|
||||||
|
// it is queued (see BuildingSystem::deconstruct), not when the timer completes.
|
||||||
|
//
|
||||||
|
// Holds the config and the refund sink; the world arrives per tick.
|
||||||
|
class DeconstructionSystem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeconstructionSystem(const GameConfig& config,
|
||||||
|
std::function<void(int)> addBuildingBlocks)
|
||||||
|
: m_config(config), m_addBuildingBlocks(std::move(addBuildingBlocks)) {}
|
||||||
|
|
||||||
|
void tick(FactoryState& state, Tick currentTick);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const GameConfig& m_config;
|
||||||
|
std::function<void(int)> m_addBuildingBlocks;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Starts the timer on the front entry of the deconstruction queue, if it has one and
|
||||||
|
// it has not started yet. Shared: BuildingSystem::deconstruct starts the timer when it
|
||||||
|
// queues the first entry, and DeconstructionSystem restarts it after each completion.
|
||||||
|
void startFrontDeconstruction(FactoryState& state, const GameConfig& config,
|
||||||
|
Tick currentTick);
|
||||||
181
src/lib/sim/FactoryQueries.cpp
Normal file
181
src/lib/sim/FactoryQueries.cpp
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include "PortGeometry.h"
|
||||||
|
#include "SurfaceMask.h"
|
||||||
|
|
||||||
|
#include "Item.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
|
||||||
|
const Building* findBuilding(const FactoryState& state, BuildingId id)
|
||||||
|
{
|
||||||
|
for (const Building& building : state.buildings)
|
||||||
|
{
|
||||||
|
if (building.id == id)
|
||||||
|
{
|
||||||
|
return &building;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Building* findBuilding(FactoryState& state, BuildingId id)
|
||||||
|
{
|
||||||
|
for (Building& building : state.buildings)
|
||||||
|
{
|
||||||
|
if (building.id == id)
|
||||||
|
{
|
||||||
|
return &building;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConstructionSite* findSite(const FactoryState& state, BuildingId id)
|
||||||
|
{
|
||||||
|
for (const ConstructionSite& site : state.constructionQueue)
|
||||||
|
{
|
||||||
|
if (site.id == id)
|
||||||
|
{
|
||||||
|
return &site;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Building> getAllBuildings(const FactoryState& state)
|
||||||
|
{
|
||||||
|
return state.buildings;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ConstructionSite> getAllSites(const FactoryState& state)
|
||||||
|
{
|
||||||
|
return std::vector<ConstructionSite>(state.constructionQueue.begin(),
|
||||||
|
state.constructionQueue.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
int getProductionBuildingCount(const FactoryState& state)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (const Building& b : state.buildings)
|
||||||
|
{
|
||||||
|
if (isProductionBuildingType(b.type)) { ++count; }
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getActiveProductionBuildingCount(const FactoryState& state)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (const Building& b : state.buildings)
|
||||||
|
{
|
||||||
|
if (isProductionBuildingType(b.type) && b.production.has_value()) { ++count; }
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isTileOccupied(const FactoryState& state, QPoint tile)
|
||||||
|
{
|
||||||
|
return state.grid.isOccupied(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isQueuedForDeconstruction(const FactoryState& state, BuildingId id)
|
||||||
|
{
|
||||||
|
const Building* building = findBuilding(state, id);
|
||||||
|
return building && building->queuedForDeconstruction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Building* findNearestBuilding(const FactoryState& state, QVector2D worldPos,
|
||||||
|
BuildingType type)
|
||||||
|
{
|
||||||
|
const Building* best = nullptr;
|
||||||
|
float bestDist = std::numeric_limits<float>::max();
|
||||||
|
for (const Building& b : state.buildings)
|
||||||
|
{
|
||||||
|
if (b.type != type)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QVector2D center(b.anchor.x() + b.footprint.width() / 2.0f,
|
||||||
|
b.anchor.y() + b.footprint.height() / 2.0f);
|
||||||
|
float dist = (center - worldPos).length();
|
||||||
|
if (dist < bestDist)
|
||||||
|
{
|
||||||
|
bestDist = dist;
|
||||||
|
best = &b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deliverScrapToSalvageBay(FactoryState& state, BuildingId bayId)
|
||||||
|
{
|
||||||
|
Building* bay = findBuilding(state, bayId);
|
||||||
|
if (!bay || bay->type != BuildingType::SalvageBay)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (bay->queuedForDeconstruction)
|
||||||
|
{
|
||||||
|
return false; // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
|
||||||
|
}
|
||||||
|
// Emerging scrap still counts against the bay's holding capacity
|
||||||
|
// (REQ-MAT-OUTPUT-EMERGE).
|
||||||
|
if (bay->getOutputItemCount() >= bay->outputBuffer.capacity)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bay->outputBuffer.items.push_back(Item{ItemType{"scrap"}});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Port> getInputPorts(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingId id)
|
||||||
|
{
|
||||||
|
if (const Building* building = findBuilding(state, id))
|
||||||
|
{
|
||||||
|
return building->inputPorts;
|
||||||
|
}
|
||||||
|
if (const ConstructionSite* site = findSite(state, id))
|
||||||
|
{
|
||||||
|
// A site stores no ports; derive its output ports from the mask (absolute)
|
||||||
|
// and run the same input-edge scan (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS).
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(site->type);
|
||||||
|
if (def == nullptr) { return {}; }
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, site->rotation);
|
||||||
|
std::vector<Port> outputPortsAbsolute;
|
||||||
|
outputPortsAbsolute.reserve(mask.outputPorts.size());
|
||||||
|
for (const Port& port : mask.outputPorts)
|
||||||
|
{
|
||||||
|
outputPortsAbsolute.push_back(Port{ site->anchor + port.tile, port.direction });
|
||||||
|
}
|
||||||
|
return computeInputPorts(site->bodyCells, outputPortsAbsolute);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<BeltSystem::SplitterInfo>
|
||||||
|
getSiteSplitterInfo(const FactoryState& state, const GameConfig& config, BuildingId id)
|
||||||
|
{
|
||||||
|
for (const ConstructionSite& site : state.constructionQueue)
|
||||||
|
{
|
||||||
|
if (site.id != id) { continue; }
|
||||||
|
if (site.type != BuildingType::Splitter) { return std::nullopt; }
|
||||||
|
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(site.type);
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(
|
||||||
|
def ? def->surfaceMask : std::vector<std::string>{}, site.rotation);
|
||||||
|
if (mask.outputPorts.size() < 2) { return std::nullopt; }
|
||||||
|
|
||||||
|
BeltSystem::SplitterInfo info;
|
||||||
|
info.outputA = mask.outputPorts[0].direction;
|
||||||
|
info.outputB = mask.outputPorts[1].direction;
|
||||||
|
info.filterA = site.splitterFilterA;
|
||||||
|
info.filterB = site.splitterFilterB;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
71
src/lib/sim/FactoryQueries.h
Normal file
71
src/lib/sim/FactoryQueries.h
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QVector2D>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "BeltSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Port.h"
|
||||||
|
|
||||||
|
// Queries and operations over the factory's world data that need nothing but that
|
||||||
|
// data — no config, no belts, no RNG. Free functions rather than BuildingSystem
|
||||||
|
// methods so that callers depend on the data they read instead of on the system
|
||||||
|
// that happens to tick it (see FactoryState.h).
|
||||||
|
//
|
||||||
|
// Most need nothing but the state. The two at the bottom also take the config,
|
||||||
|
// because answering them means reading a building definition — but still no belts,
|
||||||
|
// no RNG and no system.
|
||||||
|
|
||||||
|
// The building with the given id, or nullptr when no building has it. Construction
|
||||||
|
// sites are not buildings yet — use findSite for those.
|
||||||
|
const Building* findBuilding(const FactoryState& state, BuildingId id);
|
||||||
|
Building* findBuilding(FactoryState& state, BuildingId id);
|
||||||
|
|
||||||
|
// The queued construction site with the given id, or nullptr.
|
||||||
|
const ConstructionSite* findSite(const FactoryState& state, BuildingId id);
|
||||||
|
|
||||||
|
std::vector<Building> getAllBuildings(const FactoryState& state);
|
||||||
|
std::vector<ConstructionSite> getAllSites(const FactoryState& state);
|
||||||
|
|
||||||
|
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
|
||||||
|
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
|
||||||
|
int getProductionBuildingCount(const FactoryState& state);
|
||||||
|
|
||||||
|
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above that
|
||||||
|
// currently has an active production cycle.
|
||||||
|
int getActiveProductionBuildingCount(const FactoryState& state);
|
||||||
|
|
||||||
|
bool isTileOccupied(const FactoryState& state, QPoint tile);
|
||||||
|
|
||||||
|
// True while the building is in the deconstruction queue (REQ-BLD-DECON-QUEUE).
|
||||||
|
bool isQueuedForDeconstruction(const FactoryState& state, BuildingId id);
|
||||||
|
|
||||||
|
// The nearest building of the given type to a world position, or nullptr when
|
||||||
|
// none exists. Distance is measured to the building's footprint centre.
|
||||||
|
const Building* findNearestBuilding(const FactoryState& state, QVector2D worldPos,
|
||||||
|
BuildingType type);
|
||||||
|
|
||||||
|
// Hands one scrap to a Salvage Bay's output buffer (REQ-BLD-SALVAGE-BAY). Fails
|
||||||
|
// if the id is not a Salvage Bay, it is queued for deconstruction
|
||||||
|
// (REQ-BLD-DECON-QUEUE), or its holding capacity is already taken — emerging
|
||||||
|
// scrap counts against that capacity (REQ-MAT-OUTPUT-EMERGE).
|
||||||
|
bool deliverScrapToSalvageBay(FactoryState& state, BuildingId bayId);
|
||||||
|
|
||||||
|
// Every belt-facing edge of the building or site with this id (REQ-MAT-INPUT-PORTS,
|
||||||
|
// REQ-BLD-BELT-DRAG). Empty when the id is unknown. A site has no stored ports, so
|
||||||
|
// they are derived from its surface mask.
|
||||||
|
std::vector<Port> getInputPorts(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingId id);
|
||||||
|
|
||||||
|
// The two output directions and stored filters of a queued Splitter site
|
||||||
|
// (REQ-BLD-SITE-CONFIG), or nullopt if the id is not one. Operational splitters are
|
||||||
|
// configured through BeltSystem by tile instead.
|
||||||
|
std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(const FactoryState& state,
|
||||||
|
const GameConfig& config,
|
||||||
|
BuildingId id);
|
||||||
66
src/lib/sim/FactoryState.h
Normal file
66
src/lib/sim/FactoryState.h
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "BuildingGrid.h"
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
// One pending demolition of a fully-built building (REQ-BLD-DECON-QUEUE).
|
||||||
|
// completesAt == 0 means "queued but its timer has not started yet"
|
||||||
|
// (mirrors ConstructionSite). For a Splitter, the filters it had are captured
|
||||||
|
// here so cancelDeconstruction can restore them on re-registration.
|
||||||
|
struct DeconstructionEntry
|
||||||
|
{
|
||||||
|
BuildingId id = kInvalidBuildingId;
|
||||||
|
Tick completesAt = 0;
|
||||||
|
std::vector<ItemType> splitterFilterA;
|
||||||
|
std::vector<ItemType> splitterFilterB;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The factory's world data: every building, the work queued on them, and the
|
||||||
|
// tile ownership index. This is the buildings-side counterpart to EntityAdmin —
|
||||||
|
// data with no behaviour of its own beyond what BuildingGrid encapsulates.
|
||||||
|
//
|
||||||
|
// Buildings deliberately stay a plain vector rather than becoming EnTT entities
|
||||||
|
// (see docs/architecture.md). Separating this data from the systems that operate
|
||||||
|
// on it is not a step toward putting them in the entity model; it is the same
|
||||||
|
// data/behaviour split the ecs/system/ classes already follow, where world data
|
||||||
|
// arrives as a tick argument instead of being owned by the system.
|
||||||
|
//
|
||||||
|
// Owned by Simulation (and by ArenaSimulation in the balancing tool), not by the
|
||||||
|
// systems that operate on it. BuildingSystem holds a reference. The remaining step
|
||||||
|
// is to pass this into the tick methods instead, so the systems become stateless
|
||||||
|
// over it — that one is gated on the query surface, which today reaches the data
|
||||||
|
// through BuildingSystem's ~180 const call sites.
|
||||||
|
struct FactoryState
|
||||||
|
{
|
||||||
|
std::vector<Building> buildings;
|
||||||
|
std::deque<ConstructionSite> constructionQueue;
|
||||||
|
std::deque<DeconstructionEntry> deconstructionQueue;
|
||||||
|
|
||||||
|
// The authority on which building owns which tile; every placement and removal
|
||||||
|
// path claims and releases its body cells here.
|
||||||
|
BuildingGrid grid;
|
||||||
|
|
||||||
|
// Current buildable asteroid width, the left bound for placement. Grows as the
|
||||||
|
// player buys expansions (REQ-EXP-UNLOCK). Deliberately not checksummed: it is
|
||||||
|
// derived from config and Simulation's expansion count, which is folded already.
|
||||||
|
// Seeded from config by BuildingSystem's constructor.
|
||||||
|
int asteroidWidth_tiles = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A fresh factory for a new run: nothing built, and the asteroid bound seeded from
|
||||||
|
// config. Every owner of a FactoryState creates it this way — the bound has no
|
||||||
|
// sensible default without the config, so a default-constructed state would refuse
|
||||||
|
// every placement on the asteroid.
|
||||||
|
inline FactoryState makeFactoryState(const GameConfig& config)
|
||||||
|
{
|
||||||
|
FactoryState state;
|
||||||
|
state.asteroidWidth_tiles = config.world.regions.asteroidWidth_tiles;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
121
src/lib/sim/PlacementRules.cpp
Normal file
121
src/lib/sim/PlacementRules.cpp
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#include "PlacementRules.h"
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "SurfaceMask.h"
|
||||||
|
|
||||||
|
bool bodyCellsWithinWorldBounds(const FactoryState& state, const GameConfig& config,const std::vector<QPoint>& bodyCells,
|
||||||
|
QPoint anchor)
|
||||||
|
{
|
||||||
|
const int heightTiles = config.world.heightTiles;
|
||||||
|
const int leftEdgeX = -state.asteroidWidth_tiles;
|
||||||
|
for (const QPoint& cell : bodyCells)
|
||||||
|
{
|
||||||
|
const QPoint worldCell = anchor + cell;
|
||||||
|
if (worldCell.y() < 0 || worldCell.y() >= heightTiles)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (worldCell.x() < leftEdgeX)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPlacementValid(const FactoryState& state, const GameConfig& config,BuildingType type, QPoint anchor,
|
||||||
|
Rotation rotation)
|
||||||
|
{
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(type);
|
||||||
|
if (def == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
||||||
|
|
||||||
|
if (!bodyCellsWithinWorldBounds(state, config, mask.bodyCells, anchor))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terrain: ship-dock (S) cells must sit in space (x >= 0); all other body
|
||||||
|
// (A) cells must sit on the asteroid (x < 0). (REQ-BLD-PLACE-VALID)
|
||||||
|
for (const QPoint& cell : mask.bodyCells)
|
||||||
|
{
|
||||||
|
const QPoint worldCell = anchor + cell;
|
||||||
|
bool isShipDock = false;
|
||||||
|
for (const QPoint& dock : mask.shipDockCells)
|
||||||
|
{
|
||||||
|
if (dock == cell)
|
||||||
|
{
|
||||||
|
isShipDock = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isShipDock)
|
||||||
|
{
|
||||||
|
if (worldCell.x() < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (worldCell.x() >= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingType type, QPoint anchor, Rotation rot)
|
||||||
|
{
|
||||||
|
// Tunnel Entries and Tunnel Exits cannot be rotated in place; re-orienting a
|
||||||
|
// tunnel requires deconstructing and re-placing it (REQ-BLD-ROTATE-IN-PLACE).
|
||||||
|
if (type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(type);
|
||||||
|
if (!def) { return std::nullopt; }
|
||||||
|
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rot);
|
||||||
|
if (mask.bodyCells.empty()) { return std::nullopt; }
|
||||||
|
|
||||||
|
// All body cells must be occupied by the same entity.
|
||||||
|
const QPoint firstAbs = anchor + mask.bodyCells[0];
|
||||||
|
const std::optional<BuildingId> firstOwner = state.grid.findOwner(firstAbs);
|
||||||
|
if (!firstOwner.has_value()) { return std::nullopt; }
|
||||||
|
const BuildingId candidateId = *firstOwner;
|
||||||
|
|
||||||
|
for (const QPoint& rel : mask.bodyCells)
|
||||||
|
{
|
||||||
|
const std::optional<BuildingId> owner = state.grid.findOwner(anchor + rel);
|
||||||
|
if (!owner.has_value() || *owner != candidateId)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the candidate is the same building type with the same cell count.
|
||||||
|
for (const ConstructionSite& site : state.constructionQueue)
|
||||||
|
{
|
||||||
|
if (site.id != candidateId) { continue; }
|
||||||
|
if (site.type != type) { return std::nullopt; }
|
||||||
|
if (site.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
|
||||||
|
return candidateId;
|
||||||
|
}
|
||||||
|
for (const Building& b : state.buildings)
|
||||||
|
{
|
||||||
|
if (b.id != candidateId) { continue; }
|
||||||
|
if (b.type != type) { return std::nullopt; }
|
||||||
|
if (b.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
|
||||||
|
return candidateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
38
src/lib/sim/PlacementRules.h
Normal file
38
src/lib/sim/PlacementRules.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
|
||||||
|
// Where a building may be placed, and what is already sitting on those tiles.
|
||||||
|
// Free functions over the factory state and the config — they read no other
|
||||||
|
// system state, so they do not belong to BuildingSystem.
|
||||||
|
|
||||||
|
// True if every body cell lies inside the world: 0 <= y < world.height_tiles and
|
||||||
|
// x >= the current asteroid left edge (REQ-BLD-PLACE-VALID). Terrain type is not
|
||||||
|
// checked — see isPlacementValid for the full rule.
|
||||||
|
bool bodyCellsWithinWorldBounds(const FactoryState& state, const GameConfig& config,
|
||||||
|
const std::vector<QPoint>& bodyCells, QPoint anchor);
|
||||||
|
|
||||||
|
// True if the placement satisfies REQ-BLD-PLACE-VALID terrain and world-bounds
|
||||||
|
// rules: every ship-dock (S) cell sits in space (x >= 0), every other body (A)
|
||||||
|
// cell sits on the asteroid (x < 0 and x >= the left edge), and every cell has
|
||||||
|
// 0 <= y < world.height_tiles. There is no right-side bound — space extends
|
||||||
|
// rightward. Tile occupancy is NOT checked here.
|
||||||
|
bool isPlacementValid(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingType type, QPoint anchor, Rotation rotation);
|
||||||
|
|
||||||
|
// The building or site that a ghost of the given type/anchor/rotation would
|
||||||
|
// rotate in place rather than replace: same type, same body cells, one owner
|
||||||
|
// (REQ-BLD-ROTATE-IN-PLACE). Tunnels never qualify.
|
||||||
|
std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state,
|
||||||
|
const GameConfig& config,
|
||||||
|
BuildingType type, QPoint anchor,
|
||||||
|
Rotation rot);
|
||||||
142
src/lib/sim/ProductionRules.cpp
Normal file
142
src/lib/sim/ProductionRules.cpp
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
#include "ProductionRules.h"
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "ModulesConfig.h"
|
||||||
|
#include "ShipsConfig.h"
|
||||||
|
|
||||||
|
std::vector<const RecipeDef*>
|
||||||
|
gatherCandidateRecipes(const GameConfig& config, const Building& b)
|
||||||
|
{
|
||||||
|
std::vector<const RecipeDef*> candidates;
|
||||||
|
if (isAutoRecipeBuildingType(b.type))
|
||||||
|
{
|
||||||
|
for (const RecipeDef& r : config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (r.building == b.type && !r.inputs.empty())
|
||||||
|
{
|
||||||
|
candidates.push_back(&r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const RecipeDef* recipe = config.recipes.findRecipeDef(b.recipeId, b.type);
|
||||||
|
if (recipe)
|
||||||
|
{
|
||||||
|
candidates.push_back(recipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe)
|
||||||
|
{
|
||||||
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
|
{
|
||||||
|
const std::map<ItemType, int>::const_iterator it =
|
||||||
|
b.inputBuffer.counts.find(ItemType{ing.item});
|
||||||
|
const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
|
||||||
|
if (have < ing.amount)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::map<std::string, int>
|
||||||
|
computeShipyardRequiredMaterials(const GameConfig& config, const Building& b)
|
||||||
|
{
|
||||||
|
std::map<std::string, int> requiredMaterials;
|
||||||
|
const ShipDef* shipDef = config.ships.findShipDef(b.recipeId);
|
||||||
|
if (!shipDef)
|
||||||
|
{
|
||||||
|
return requiredMaterials;
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
||||||
|
{
|
||||||
|
requiredMaterials[ing.item] += ing.amount;
|
||||||
|
}
|
||||||
|
if (b.shipLayout.has_value())
|
||||||
|
{
|
||||||
|
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
|
||||||
|
if (!modDef)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : modDef->materials)
|
||||||
|
{
|
||||||
|
requiredMaterials[ing.item] += ing.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requiredMaterials;
|
||||||
|
}
|
||||||
|
bool hasInputsToStart(const GameConfig& config, const Building& b)
|
||||||
|
{
|
||||||
|
if (b.type == BuildingType::Shipyard)
|
||||||
|
{
|
||||||
|
const std::map<std::string, int> required =
|
||||||
|
computeShipyardRequiredMaterials(config, b);
|
||||||
|
for (const std::pair<const std::string, int>& req : required)
|
||||||
|
{
|
||||||
|
const std::map<ItemType, int>::const_iterator it =
|
||||||
|
b.inputBuffer.counts.find(ItemType{req.first});
|
||||||
|
const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
|
||||||
|
if (have < req.second)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recipe buildings: startable if any candidate recipe's inputs are satisfied.
|
||||||
|
// A Miner recipe has no inputs, so an idle Miner is always startable and its
|
||||||
|
// only idle reason is a full output buffer.
|
||||||
|
for (const RecipeDef* recipe : gatherCandidateRecipes(config, b))
|
||||||
|
{
|
||||||
|
if (recipeInputsAvailable(b, *recipe))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::optional<ProductionStatus>
|
||||||
|
getProductionStatus(const GameConfig& config, const Building& building)
|
||||||
|
{
|
||||||
|
// Salvage Bay has no recipe or production cycle (REQ-BLD-SALVAGE-BAY): it is
|
||||||
|
// "producing" while it holds scrap to push out, and starved when empty.
|
||||||
|
if (building.type == BuildingType::SalvageBay)
|
||||||
|
{
|
||||||
|
return building.getOutputItemCount() >= 1 ? ProductionStatus::Producing
|
||||||
|
: ProductionStatus::Starved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the five recipe/cycle production types show a status light besides the
|
||||||
|
// Salvage Bay; belts, splitters, tunnels, HQ, and stations show none.
|
||||||
|
if (!isProductionBuildingType(building.type))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grey only applies to player-configured types; auto-recipe buildings
|
||||||
|
// (Smelter, Reprocessing Plant) always run an implicit recipe.
|
||||||
|
if (!isAutoRecipeBuildingType(building.type) && building.recipeId.empty())
|
||||||
|
{
|
||||||
|
return ProductionStatus::Unconfigured;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (building.production.has_value())
|
||||||
|
{
|
||||||
|
return ProductionStatus::Producing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle: missing inputs (red) take precedence over a full output buffer
|
||||||
|
// (yellow). If inputs are present yet the building is idle, the only remaining
|
||||||
|
// reason it could not start a cycle is a full output buffer (REQ-MAT-CYCLE).
|
||||||
|
return hasInputsToStart(config, building) ? ProductionStatus::Blocked
|
||||||
|
: ProductionStatus::Starved;
|
||||||
|
}
|
||||||
47
src/lib/sim/ProductionRules.h
Normal file
47
src/lib/sim/ProductionRules.h
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "RecipesConfig.h"
|
||||||
|
|
||||||
|
// Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT).
|
||||||
|
// The simulation owns the classification so it stays in sync with the
|
||||||
|
// production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill
|
||||||
|
// color.
|
||||||
|
enum class ProductionStatus
|
||||||
|
{
|
||||||
|
Unconfigured, // no recipe/schematic selected (grey)
|
||||||
|
Producing, // a production cycle is active (green)
|
||||||
|
Starved, // idle: a required input is missing / Salvage Bay empty (red)
|
||||||
|
Blocked, // idle: output buffer full, inputs otherwise present (yellow)
|
||||||
|
};
|
||||||
|
|
||||||
|
// The rules deciding what a building can produce and whether it can start.
|
||||||
|
// Pure functions of the config and the building itself — they read no factory
|
||||||
|
// state, so they are free functions rather than BuildingSystem members.
|
||||||
|
|
||||||
|
// Recipes this building could run: every recipe of its type for an auto-recipe
|
||||||
|
// building (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), otherwise just its selected one.
|
||||||
|
std::vector<const RecipeDef*> gatherCandidateRecipes(const GameConfig& config,
|
||||||
|
const Building& b);
|
||||||
|
|
||||||
|
// True when the building's input buffer holds every ingredient the recipe needs.
|
||||||
|
bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe);
|
||||||
|
|
||||||
|
// Total materials a shipyard needs for its schematic plus its placed modules
|
||||||
|
// (REQ-BLD-SHIPYARD), keyed by item id.
|
||||||
|
std::map<std::string, int> computeShipyardRequiredMaterials(const GameConfig& config,
|
||||||
|
const Building& b);
|
||||||
|
|
||||||
|
// True when a production cycle could start right now, ignoring output-buffer space.
|
||||||
|
bool hasInputsToStart(const GameConfig& config, const Building& b);
|
||||||
|
|
||||||
|
// Status light for a building, or nullopt for types that show none — belts,
|
||||||
|
// splitters, tunnels, HQ and defence stations (REQ-UI-STATUS-LIGHT).
|
||||||
|
std::optional<ProductionStatus> getProductionStatus(const GameConfig& config,
|
||||||
|
const Building& building);
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "ConstructionSystem.h"
|
||||||
|
#include "DeconstructionSystem.h"
|
||||||
|
#include "PlacementRules.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include "AiSystem.h"
|
#include "AiSystem.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "DisplayName.h"
|
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "CombatSystem.h"
|
#include "CombatSystem.h"
|
||||||
#include "DynamicBodyComponent.h"
|
#include "DynamicBodyComponent.h"
|
||||||
@@ -43,14 +47,16 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
|||||||
, m_hqProxyEntity(entt::null)
|
, m_hqProxyEntity(entt::null)
|
||||||
, m_playerStation1Entity(entt::null)
|
, m_playerStation1Entity(entt::null)
|
||||||
, m_playerStation2Entity(entt::null)
|
, m_playerStation2Entity(entt::null)
|
||||||
|
, m_unlockState(m_config)
|
||||||
, m_beltSystem(m_config.world.beltSpeed_tps)
|
, m_beltSystem(m_config.world.beltSpeed_tps)
|
||||||
{
|
{
|
||||||
m_currentEnemyStationEntities[0] = entt::null;
|
m_currentEnemyStationEntities[0] = entt::null;
|
||||||
m_currentEnemyStationEntities[1] = entt::null;
|
m_currentEnemyStationEntities[1] = entt::null;
|
||||||
|
m_factoryState = makeFactoryState(m_config);
|
||||||
|
|
||||||
initializeSubsystems();
|
initializeSubsystems();
|
||||||
|
|
||||||
initializeUnlockState();
|
m_unlockState.initializeUnlockState();
|
||||||
placeInitialStructures();
|
placeInitialStructures();
|
||||||
registerForEvents();
|
registerForEvents();
|
||||||
}
|
}
|
||||||
@@ -94,10 +100,11 @@ void Simulation::reset(unsigned int seed)
|
|||||||
m_pendingSchematicChoices.clear();
|
m_pendingSchematicChoices.clear();
|
||||||
|
|
||||||
m_admin.clear();
|
m_admin.clear();
|
||||||
|
m_factoryState = makeFactoryState(m_config);
|
||||||
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
|
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
|
||||||
initializeSubsystems();
|
initializeSubsystems();
|
||||||
|
|
||||||
initializeUnlockState();
|
m_unlockState.initializeUnlockState();
|
||||||
placeInitialStructures();
|
placeInitialStructures();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,9 +117,7 @@ void Simulation::initializeSubsystems()
|
|||||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||||
[this](const std::string& id, QVector2D pos,
|
[this](const std::string& id, QVector2D pos,
|
||||||
const std::optional<ShipLayoutConfig>& layout) {
|
const std::optional<ShipLayoutConfig>& layout) {
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
if (!isSchematicUnlocked(id))
|
||||||
m_schematicLevels.find(id);
|
|
||||||
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -120,6 +125,9 @@ void Simulation::initializeSubsystems()
|
|||||||
},
|
},
|
||||||
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
||||||
m_rng);
|
m_rng);
|
||||||
|
m_constructionSystem = std::make_unique<ConstructionSystem>(m_config);
|
||||||
|
m_deconstructionSystem = std::make_unique<DeconstructionSystem>(
|
||||||
|
m_config, [this](int amount) { m_buildingBlocksStock += amount; });
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
||||||
m_aiSystem = std::make_unique<AiSystem>(m_config);
|
m_aiSystem = std::make_unique<AiSystem>(m_config);
|
||||||
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
||||||
@@ -131,55 +139,6 @@ void Simulation::initializeSubsystems()
|
|||||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Simulation::initializeUnlockState()
|
|
||||||
{
|
|
||||||
// Cache the ids granted by some unlock group (REQ-LOCK-EXPLICIT); an item
|
|
||||||
// starts locked iff it is granted by a group.
|
|
||||||
m_grantedShipIds.clear();
|
|
||||||
m_grantedModuleIds.clear();
|
|
||||||
m_grantedBuildingIds.clear();
|
|
||||||
m_grantedRecipeIds.clear();
|
|
||||||
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
|
||||||
{
|
|
||||||
m_grantedShipIds.insert(group.ships.begin(), group.ships.end());
|
|
||||||
m_grantedModuleIds.insert(group.modules.begin(), group.modules.end());
|
|
||||||
m_grantedBuildingIds.insert(group.buildings.begin(), group.buildings.end());
|
|
||||||
m_grantedRecipeIds.insert(group.recipes.begin(), group.recipes.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
m_awardedUnlockGroupIds.clear();
|
|
||||||
|
|
||||||
m_schematicLevels.clear();
|
|
||||||
for (const ShipDef& def : m_config.ships.ships)
|
|
||||||
{
|
|
||||||
SchematicState state;
|
|
||||||
state.unlocked = (m_grantedShipIds.count(def.id) == 0);
|
|
||||||
m_schematicLevels[def.id] = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_moduleSchematicLevels.clear();
|
|
||||||
for (const ModuleDef& def : m_config.modules.modules)
|
|
||||||
{
|
|
||||||
SchematicState state;
|
|
||||||
state.unlocked = (m_grantedModuleIds.count(def.id) == 0);
|
|
||||||
m_moduleSchematicLevels[def.id] = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_buildingLevels.clear();
|
|
||||||
for (const BuildingDef& def : m_config.buildings.buildings)
|
|
||||||
{
|
|
||||||
SchematicState state;
|
|
||||||
state.unlocked = (m_grantedBuildingIds.count(def.id) == 0);
|
|
||||||
m_buildingLevels[def.id] = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gated assembler recipes start locked; unlocked_at_start recipes are handled
|
|
||||||
// in the REQ-LOCK-IMPLICIT traversal, not tracked here.
|
|
||||||
m_unlockedRecipeSchematicIds.clear();
|
|
||||||
|
|
||||||
recomputeUnlocked();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// tick
|
// tick
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -199,15 +158,15 @@ void Simulation::apply(const Command& command)
|
|||||||
const BuildingId id = *placed;
|
const BuildingId id = *placed;
|
||||||
if (c.recipeId.has_value())
|
if (c.recipeId.has_value())
|
||||||
{
|
{
|
||||||
m_buildingSystem->setRecipe(id, *c.recipeId);
|
m_buildingSystem->setRecipe(m_factoryState, id, *c.recipeId);
|
||||||
}
|
}
|
||||||
if (c.shipLayout.has_value())
|
if (c.shipLayout.has_value())
|
||||||
{
|
{
|
||||||
m_buildingSystem->setShipLayout(id, *c.shipLayout);
|
m_buildingSystem->setShipLayout(m_factoryState, id, *c.shipLayout);
|
||||||
}
|
}
|
||||||
if (c.hasSplitterFilters)
|
if (c.hasSplitterFilters)
|
||||||
{
|
{
|
||||||
m_buildingSystem->setSiteSplitterFilters(id, c.splitterFilterA, c.splitterFilterB);
|
m_buildingSystem->setSiteSplitterFilters(m_factoryState, id, c.splitterFilterA, c.splitterFilterB);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -220,26 +179,26 @@ void Simulation::apply(const Command& command)
|
|||||||
case CommandKind::RotateInPlace:
|
case CommandKind::RotateInPlace:
|
||||||
{
|
{
|
||||||
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
||||||
m_buildingSystem->rotateInPlace(*c.id, c.newRotation);
|
m_buildingSystem->rotateInPlace(m_factoryState, *c.id, c.newRotation);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CommandKind::SetRecipe:
|
case CommandKind::SetRecipe:
|
||||||
{
|
{
|
||||||
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
|
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
|
||||||
m_buildingSystem->setRecipe(*c.id, c.recipeId);
|
m_buildingSystem->setRecipe(m_factoryState, *c.id, c.recipeId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CommandKind::SetShipLayout:
|
case CommandKind::SetShipLayout:
|
||||||
{
|
{
|
||||||
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
|
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
|
||||||
m_buildingSystem->setShipLayout(*c.id, c.layout);
|
m_buildingSystem->setShipLayout(m_factoryState, *c.id, c.layout);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CommandKind::SetSiteSplitterFilters:
|
case CommandKind::SetSiteSplitterFilters:
|
||||||
{
|
{
|
||||||
const SetSiteSplitterFiltersCommand& c =
|
const SetSiteSplitterFiltersCommand& c =
|
||||||
static_cast<const SetSiteSplitterFiltersCommand&>(command);
|
static_cast<const SetSiteSplitterFiltersCommand&>(command);
|
||||||
m_buildingSystem->setSiteSplitterFilters(*c.id, c.filterA, c.filterB);
|
m_buildingSystem->setSiteSplitterFilters(m_factoryState, *c.id, c.filterA, c.filterB);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CommandKind::SetSplitterFilters:
|
case CommandKind::SetSplitterFilters:
|
||||||
@@ -289,12 +248,12 @@ void Simulation::tick()
|
|||||||
m_waveSystem->tickThreatAccumulation();
|
m_waveSystem->tickThreatAccumulation();
|
||||||
|
|
||||||
// Construction + production pipeline
|
// Construction + production pipeline
|
||||||
m_buildingSystem->tickConstruction(m_currentTick);
|
m_constructionSystem->tick(m_factoryState, m_beltSystem, m_currentTick);
|
||||||
m_buildingSystem->tickDeconstruction(m_currentTick); // parallel to construction
|
m_deconstructionSystem->tick(m_factoryState, m_currentTick); // parallel to construction
|
||||||
m_buildingSystem->tickBeltPull(); // step 3
|
m_buildingSystem->tickBeltPull(m_factoryState); // step 3
|
||||||
m_buildingSystem->tickProduction(m_currentTick); // step 4
|
m_buildingSystem->tickProduction(m_factoryState, m_currentTick); // step 4
|
||||||
m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b
|
m_buildingSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b
|
||||||
m_buildingSystem->tickOutputBelts(); // step 5
|
m_buildingSystem->tickOutputBelts(m_factoryState); // step 5
|
||||||
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)
|
||||||
@@ -309,15 +268,14 @@ void Simulation::tick()
|
|||||||
m_shipSystem->clearMovementIntents();
|
m_shipSystem->clearMovementIntents();
|
||||||
// Score-based behavior selection: evaluate, select winner, execute (sets
|
// Score-based behavior selection: evaluate, select winner, execute (sets
|
||||||
// movement intent + preferred module targets only — no world mutation).
|
// movement intent + preferred module targets only — no world mutation).
|
||||||
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_debrisSystem);
|
m_aiSystem->tick(m_admin, m_factoryState);
|
||||||
// Module systems perform the world mutation (collection/delivery, healing).
|
// Module systems perform the world mutation (collection/delivery, healing).
|
||||||
// Each emits its tool beams and applies its own delayed (mid-beam) effects.
|
// Each emits its tool beams and applies its own delayed (mid-beam) effects.
|
||||||
m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, *m_buildingSystem, m_beamFiredEvents);
|
m_salvagerSystem->tick(m_currentTick, m_factoryState, m_beamFiredEvents);
|
||||||
m_repairSystem->tick(m_currentTick, m_beamFiredEvents);
|
m_repairSystem->tick(m_currentTick, m_beamFiredEvents);
|
||||||
|
|
||||||
// Step 8: combat resolution
|
// Step 8: combat resolution
|
||||||
m_combatSystem->tick(m_currentTick, m_admin,
|
m_combatSystem->tick(m_currentTick, m_admin, m_beamFiredEvents);
|
||||||
*m_buildingSystem, m_beamFiredEvents);
|
|
||||||
|
|
||||||
// Step 8b: deferred damage whose impact tick has arrived
|
// Step 8b: deferred damage whose impact tick has arrived
|
||||||
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
||||||
@@ -353,8 +311,7 @@ void Simulation::placeInitialStructures()
|
|||||||
(m_config.world.heightTiles - hqParsed.footprint.height()) / 2;
|
(m_config.world.heightTiles - hqParsed.footprint.height()) / 2;
|
||||||
const float hqHp =
|
const float hqHp =
|
||||||
static_cast<float>(m_config.stations.hq.hpFormula.evaluate(0.0));
|
static_cast<float>(m_config.stations.hq.hpFormula.evaluate(0.0));
|
||||||
m_hqBuildingId = m_buildingSystem->placeImmediate(
|
m_hqBuildingId = m_buildingSystem->placeImmediate(m_factoryState, BuildingType::Hq,
|
||||||
BuildingType::Hq,
|
|
||||||
m_config.stations.hq.surfaceMask,
|
m_config.stations.hq.surfaceMask,
|
||||||
QPoint(hqAnchorX, hqAnchorY),
|
QPoint(hqAnchorX, hqAnchorY),
|
||||||
Rotation::East);
|
Rotation::East);
|
||||||
@@ -403,7 +360,7 @@ void Simulation::placeInitialStructures()
|
|||||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
ModuleOwnerComponent{m_playerStation1Entity});
|
ModuleOwnerComponent{m_playerStation1Entity});
|
||||||
}
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const QPoint anchor(psAnchorX, ps2Y);
|
const QPoint anchor(psAnchorX, ps2Y);
|
||||||
@@ -420,7 +377,7 @@ void Simulation::placeInitialStructures()
|
|||||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
ModuleOwnerComponent{m_playerStation2Entity});
|
ModuleOwnerComponent{m_playerStation2Entity});
|
||||||
}
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rally point: center of the player defence stations' X column, world vertical midpoint.
|
// Rally point: center of the player defence stations' X column, world vertical midpoint.
|
||||||
@@ -475,7 +432,7 @@ void Simulation::placeEnemyStationSet(int generation)
|
|||||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
ModuleOwnerComponent{m_currentEnemyStationEntities[0]});
|
ModuleOwnerComponent{m_currentEnemyStationEntities[0]});
|
||||||
}
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const QPoint anchor(anchorX, y2);
|
const QPoint anchor(anchorX, y2);
|
||||||
@@ -492,7 +449,7 @@ void Simulation::placeEnemyStationSet(int generation)
|
|||||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
ModuleOwnerComponent{m_currentEnemyStationEntities[1]});
|
ModuleOwnerComponent{m_currentEnemyStationEntities[1]});
|
||||||
}
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,7 +523,7 @@ void Simulation::tickDeathsAndLoot()
|
|||||||
{
|
{
|
||||||
m_debrisSystem->spawn(pos.value, scrap, despawnAt);
|
m_debrisSystem->spawn(pos.value, scrap, despawnAt);
|
||||||
}
|
}
|
||||||
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
|
||||||
{
|
{
|
||||||
std::vector<entt::entity> stationChildren;
|
std::vector<entt::entity> stationChildren;
|
||||||
m_admin.forEach<ModuleOwnerComponent>(
|
m_admin.forEach<ModuleOwnerComponent>(
|
||||||
@@ -613,9 +570,9 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
std::vector<const UnlockGroupDef*> pool;
|
std::vector<const UnlockGroupDef*> pool;
|
||||||
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
||||||
{
|
{
|
||||||
if (m_awardedUnlockGroupIds.count(group.id) > 0) { continue; }
|
if (m_unlockState.isUnlockGroupAwarded(group.id)) { continue; }
|
||||||
if (group.stationLevel < 0 || group.stationLevel > destroyedStationLevel) { continue; }
|
if (group.stationLevel < 0 || group.stationLevel > destroyedStationLevel) { continue; }
|
||||||
if (!prerequisitesSatisfied(group.requiredGroupIds)) { continue; }
|
if (!m_unlockState.prerequisitesSatisfied(group.requiredGroupIds)) { continue; }
|
||||||
pool.push_back(&group);
|
pool.push_back(&group);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,7 +596,7 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
const std::size_t endIdx = pool.size() - 1 - static_cast<std::size_t>(i);
|
const std::size_t endIdx = pool.size() - 1 - static_cast<std::size_t>(i);
|
||||||
std::swap(pool[rollIdx], pool[endIdx]);
|
std::swap(pool[rollIdx], pool[endIdx]);
|
||||||
|
|
||||||
m_pendingSchematicChoices.push_back(makeUnlockOption(*pool[endIdx]));
|
m_pendingSchematicChoices.push_back(m_unlockState.makeUnlockOption(*pool[endIdx]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (artifactRolled)
|
if (artifactRolled)
|
||||||
@@ -651,48 +608,6 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SchematicChoiceOption Simulation::makeUnlockOption(const UnlockGroupDef& group) const
|
|
||||||
{
|
|
||||||
SchematicChoiceOption option;
|
|
||||||
option.isArtifact = false;
|
|
||||||
option.unlockGroupId = group.id;
|
|
||||||
option.displayName = toDisplayName(group.id);
|
|
||||||
|
|
||||||
for (const std::string& id : group.ships)
|
|
||||||
{
|
|
||||||
option.grantedItems.push_back({SchematicType::Ship, id, toDisplayName(id)});
|
|
||||||
}
|
|
||||||
for (const std::string& id : group.modules)
|
|
||||||
{
|
|
||||||
option.grantedItems.push_back({SchematicType::Module, id, toDisplayName(id)});
|
|
||||||
}
|
|
||||||
for (const std::string& id : group.buildings)
|
|
||||||
{
|
|
||||||
option.grantedItems.push_back({SchematicType::Building, id, toDisplayName(id)});
|
|
||||||
}
|
|
||||||
for (const std::string& id : group.recipes)
|
|
||||||
{
|
|
||||||
option.grantedItems.push_back({SchematicType::Recipe, id, toDisplayName(id)});
|
|
||||||
}
|
|
||||||
|
|
||||||
// REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by
|
|
||||||
// awarding this whole group. Seed the hypothetical explicit-unlock sets with
|
|
||||||
// every grant (ship + module materials via step 1a, recipe outputs via step
|
|
||||||
// 1b), then diff against the current implicit set.
|
|
||||||
std::set<std::string> hypotheticalShipIds = getUnlockedShipSchematicIds();
|
|
||||||
std::set<std::string> hypotheticalModuleIds = getUnlockedModuleSchematicIds();
|
|
||||||
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
|
|
||||||
for (const std::string& id : group.ships) { hypotheticalShipIds.insert(id); }
|
|
||||||
for (const std::string& id : group.modules) { hypotheticalModuleIds.insert(id); }
|
|
||||||
for (const std::string& id : group.recipes) { hypotheticalRecipeSchematicIds.insert(id); }
|
|
||||||
|
|
||||||
const UnlockedSets hypothetical = computeUnlockedSets(
|
|
||||||
hypotheticalShipIds, hypotheticalModuleIds, hypotheticalRecipeSchematicIds);
|
|
||||||
option.newlyUnlockedRecipeIds = computeNewlyUnlockedRecipeIds(hypothetical);
|
|
||||||
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Simulation::applySchematicChoice(int choiceIndex)
|
void Simulation::applySchematicChoice(int choiceIndex)
|
||||||
{
|
{
|
||||||
assert(choiceIndex >= 0 && choiceIndex < static_cast<int>(m_pendingSchematicChoices.size()));
|
assert(choiceIndex >= 0 && choiceIndex < static_cast<int>(m_pendingSchematicChoices.size()));
|
||||||
@@ -709,204 +624,24 @@ void Simulation::applySchematicChoice(int choiceIndex)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Award the whole unlock group (REQ-DEF-SCHEMATIC-DROP): unlock every granted
|
m_unlockState.awardUnlockGroup(chosen);
|
||||||
// ship, module, building, and assembler recipe at once.
|
|
||||||
m_awardedUnlockGroupIds.insert(chosen.unlockGroupId);
|
|
||||||
for (const GrantedSchematic& grant : chosen.grantedItems)
|
|
||||||
{
|
|
||||||
switch (grant.type)
|
|
||||||
{
|
|
||||||
case SchematicType::Ship: m_schematicLevels.at(grant.id).unlocked = true; break;
|
|
||||||
case SchematicType::Module: m_moduleSchematicLevels.at(grant.id).unlocked = true; break;
|
|
||||||
case SchematicType::Building: m_buildingLevels.at(grant.id).unlocked = true; break;
|
|
||||||
case SchematicType::Recipe: m_unlockedRecipeSchematicIds.insert(grant.id); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recomputeUnlocked();
|
|
||||||
m_pendingSchematicChoices.clear();
|
m_pendingSchematicChoices.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Implicit unlock computation (REQ-LOCK-IMPLICIT)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
void Simulation::recomputeUnlocked()
|
|
||||||
{
|
|
||||||
const UnlockedSets result = computeUnlockedSets(
|
|
||||||
getUnlockedShipSchematicIds(), getUnlockedModuleSchematicIds(), m_unlockedRecipeSchematicIds);
|
|
||||||
m_unlockedItemIds = result.itemIds;
|
|
||||||
m_unlockedRecipeIds = result.recipeIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<std::string> Simulation::getUnlockedShipSchematicIds() const
|
|
||||||
{
|
|
||||||
std::set<std::string> ids;
|
|
||||||
for (const auto& [id, state] : m_schematicLevels)
|
|
||||||
{
|
|
||||||
if (state.unlocked) { ids.insert(id); }
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<std::string> Simulation::getUnlockedModuleSchematicIds() const
|
|
||||||
{
|
|
||||||
std::set<std::string> ids;
|
|
||||||
for (const auto& [id, state] : m_moduleSchematicLevels)
|
|
||||||
{
|
|
||||||
if (state.unlocked) { ids.insert(id); }
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Simulation::prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const
|
|
||||||
{
|
|
||||||
// A prerequisite is satisfied only once the named unlock group has been
|
|
||||||
// awarded (REQ-LOCK-PREREQ).
|
|
||||||
for (const std::string& groupId : requiredGroupIds)
|
|
||||||
{
|
|
||||||
if (m_awardedUnlockGroupIds.count(groupId) == 0) { return false; }
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Simulation::UnlockedSets Simulation::computeUnlockedSets(
|
|
||||||
const std::set<std::string>& unlockedShipSchematicIds,
|
|
||||||
const std::set<std::string>& unlockedModuleSchematicIds,
|
|
||||||
const std::set<std::string>& unlockedRecipeSchematicIds) const
|
|
||||||
{
|
|
||||||
UnlockedSets result;
|
|
||||||
|
|
||||||
for (const ShipDef& def : m_config.ships.ships)
|
|
||||||
{
|
|
||||||
if (unlockedShipSchematicIds.count(def.id) == 0) { continue; }
|
|
||||||
for (const RecipeIngredient& mat : def.schematic.materials)
|
|
||||||
{
|
|
||||||
result.itemIds.insert(mat.item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const ModuleDef& def : m_config.modules.modules)
|
|
||||||
{
|
|
||||||
if (unlockedModuleSchematicIds.count(def.id) == 0) { continue; }
|
|
||||||
for (const RecipeIngredient& mat : def.materials)
|
|
||||||
{
|
|
||||||
result.itemIds.insert(mat.item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const RecipeDef& def : m_config.recipes.recipes)
|
|
||||||
{
|
|
||||||
// An assembler recipe seeds the base set when it is explicitly available:
|
|
||||||
// flagged unlocked_at_start (base recipes the graph can't reach), or a
|
|
||||||
// gated recipe whose unlock group has been awarded (REQ-LOCK-EXPLICIT).
|
|
||||||
if (def.building == BuildingType::Assembler
|
|
||||||
&& (def.unlockedAtStart || unlockedRecipeSchematicIds.count(def.id) > 0))
|
|
||||||
{
|
|
||||||
for (const RecipeOutput& out : def.outputs)
|
|
||||||
{
|
|
||||||
result.itemIds.insert(out.item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool changed = true;
|
|
||||||
while (changed)
|
|
||||||
{
|
|
||||||
changed = false;
|
|
||||||
for (const RecipeDef& recipe : m_config.recipes.recipes)
|
|
||||||
{
|
|
||||||
if (recipe.building != BuildingType::Miner
|
|
||||||
&& recipe.building != BuildingType::Smelter
|
|
||||||
&& recipe.building != BuildingType::Assembler)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Skip a gated assembler recipe (granted by an unlock group) whose
|
|
||||||
// group has not yet been awarded (REQ-LOCK-IMPLICIT step 2).
|
|
||||||
if (recipe.building == BuildingType::Assembler
|
|
||||||
&& m_grantedRecipeIds.count(recipe.id) > 0
|
|
||||||
&& unlockedRecipeSchematicIds.count(recipe.id) == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
bool producesUnlocked = false;
|
|
||||||
for (const RecipeOutput& out : recipe.outputs)
|
|
||||||
{
|
|
||||||
if (result.itemIds.count(out.item) > 0)
|
|
||||||
{
|
|
||||||
producesUnlocked = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!producesUnlocked) { continue; }
|
|
||||||
|
|
||||||
if (recipe.building == BuildingType::Miner
|
|
||||||
|| recipe.building == BuildingType::Assembler)
|
|
||||||
{
|
|
||||||
result.recipeIds.insert(recipe.id);
|
|
||||||
}
|
|
||||||
for (const RecipeIngredient& ing : recipe.inputs)
|
|
||||||
{
|
|
||||||
if (result.itemIds.insert(ing.item).second)
|
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> Simulation::computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const
|
|
||||||
{
|
|
||||||
std::vector<std::string> recipeIds;
|
|
||||||
for (const std::string& recipeId : hypothetical.recipeIds)
|
|
||||||
{
|
|
||||||
if (m_unlockedRecipeIds.count(recipeId) > 0) { continue; }
|
|
||||||
recipeIds.push_back(recipeId);
|
|
||||||
}
|
|
||||||
std::sort(recipeIds.begin(), recipeIds.end(),
|
|
||||||
[](const std::string& lhs, const std::string& rhs)
|
|
||||||
{
|
|
||||||
return toDisplayName(lhs) < toDisplayName(rhs);
|
|
||||||
});
|
|
||||||
return recipeIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Simulation::isRecipeUnlocked(const std::string& recipeId) const
|
bool Simulation::isRecipeUnlocked(const std::string& recipeId) const
|
||||||
{
|
{
|
||||||
return m_unlockedRecipeIds.count(recipeId) > 0;
|
return m_unlockState.isRecipeUnlocked(recipeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Simulation::isItemUnlocked(const std::string& itemId) const
|
bool Simulation::isItemUnlocked(const std::string& itemId) const
|
||||||
{
|
{
|
||||||
return m_unlockedItemIds.count(itemId) > 0;
|
return m_unlockState.isItemUnlocked(itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Determinism (see docs/replay_design.md)
|
// Determinism (see docs/replay_design.md)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
void Simulation::appendSchematicMap(Hasher& hasher,
|
|
||||||
const std::map<std::string, SchematicState>& levels)
|
|
||||||
{
|
|
||||||
hasher.append(levels.size());
|
|
||||||
for (const std::pair<const std::string, SchematicState>& entry : levels)
|
|
||||||
{
|
|
||||||
hasher.append(entry.first);
|
|
||||||
hasher.append(entry.second.unlocked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Simulation::appendStringSet(Hasher& hasher, const std::set<std::string>& ids)
|
|
||||||
{
|
|
||||||
hasher.append(ids.size());
|
|
||||||
for (const std::string& id : ids)
|
|
||||||
{
|
|
||||||
hasher.append(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long long Simulation::getRngFingerprint() const
|
unsigned long long Simulation::getRngFingerprint() const
|
||||||
{
|
{
|
||||||
return fingerprintRng(m_rng);
|
return fingerprintRng(m_rng);
|
||||||
@@ -937,16 +672,10 @@ unsigned long long Simulation::computeStateChecksum() const
|
|||||||
hasher.append(getNormalGapRemainingTicks());
|
hasher.append(getNormalGapRemainingTicks());
|
||||||
|
|
||||||
// Schematic / unlock state (std::map and std::set iterate in sorted order).
|
// Schematic / unlock state (std::map and std::set iterate in sorted order).
|
||||||
appendSchematicMap(hasher, m_schematicLevels);
|
m_unlockState.appendChecksum(hasher);
|
||||||
appendSchematicMap(hasher, m_moduleSchematicLevels);
|
|
||||||
appendSchematicMap(hasher, m_buildingLevels);
|
|
||||||
appendStringSet(hasher, m_awardedUnlockGroupIds);
|
|
||||||
appendStringSet(hasher, m_unlockedRecipeSchematicIds);
|
|
||||||
appendStringSet(hasher, m_unlockedRecipeIds);
|
|
||||||
appendStringSet(hasher, m_unlockedItemIds);
|
|
||||||
|
|
||||||
// Subsystems contribute their own state.
|
// Subsystems contribute their own state.
|
||||||
m_buildingSystem->appendChecksum(hasher);
|
m_buildingSystem->appendChecksum(m_factoryState, hasher);
|
||||||
m_beltSystem.appendChecksum(hasher);
|
m_beltSystem.appendChecksum(hasher);
|
||||||
|
|
||||||
// ECS component state. View iteration order is a pure function of the
|
// ECS component state. View iteration order is a pure function of the
|
||||||
@@ -1059,7 +788,7 @@ void Simulation::tryExpandAsteroid()
|
|||||||
}
|
}
|
||||||
m_buildingBlocksStock -= cost;
|
m_buildingBlocksStock -= cost;
|
||||||
++m_expansionsPurchased;
|
++m_expansionsPurchased;
|
||||||
m_buildingSystem->setAsteroidWidth_tiles(getCurrentAsteroidWidth_tiles());
|
m_buildingSystem->setAsteroidWidth_tiles(m_factoryState, getCurrentAsteroidWidth_tiles());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Simulation::isGameOver() const
|
bool Simulation::isGameOver() const
|
||||||
@@ -1089,12 +818,12 @@ double Simulation::getThreatAccumulationRate() const
|
|||||||
|
|
||||||
double Simulation::getMaxFactoryProductionThreatRate() const
|
double Simulation::getMaxFactoryProductionThreatRate() const
|
||||||
{
|
{
|
||||||
return static_cast<double>(m_buildingSystem->getProductionBuildingCount());
|
return static_cast<double>(getProductionBuildingCount(m_factoryState));
|
||||||
}
|
}
|
||||||
|
|
||||||
double Simulation::getCurrentFactoryProductionThreatRate() const
|
double Simulation::getCurrentFactoryProductionThreatRate() const
|
||||||
{
|
{
|
||||||
return static_cast<double>(m_buildingSystem->getActiveProductionBuildingCount());
|
return static_cast<double>(getActiveProductionBuildingCount(m_factoryState));
|
||||||
}
|
}
|
||||||
|
|
||||||
int Simulation::getBossWaveCounter() const
|
int Simulation::getBossWaveCounter() const
|
||||||
@@ -1114,37 +843,17 @@ Tick Simulation::getNormalGapRemainingTicks() const
|
|||||||
|
|
||||||
bool Simulation::isSchematicUnlocked(const std::string& shipId) const
|
bool Simulation::isSchematicUnlocked(const std::string& shipId) const
|
||||||
{
|
{
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
return m_unlockState.isSchematicUnlocked(shipId);
|
||||||
m_schematicLevels.find(shipId);
|
|
||||||
if (it == m_schematicLevels.end())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return it->second.unlocked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
|
bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
|
||||||
{
|
{
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
return m_unlockState.isModuleSchematicUnlocked(moduleId);
|
||||||
m_moduleSchematicLevels.find(moduleId);
|
|
||||||
if (it == m_moduleSchematicLevels.end())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return it->second.unlocked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Simulation::isBuildingUnlocked(BuildingType type) const
|
bool Simulation::isBuildingUnlocked(BuildingType type) const
|
||||||
{
|
{
|
||||||
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
return m_unlockState.isBuildingUnlocked(type);
|
||||||
if (def == nullptr)
|
|
||||||
{
|
|
||||||
// Types without a config entry (e.g. HQ, defence stations) are unrestricted.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
|
||||||
m_buildingLevels.find(def->id);
|
|
||||||
return it == m_buildingLevels.end() ? true : it->second.unlocked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
|
std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
|
||||||
@@ -1156,7 +865,7 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_buildingSystem->isPlacementValid(type, anchor, rotation))
|
if (!isPlacementValid(m_factoryState, m_config, type, anchor, rotation))
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
@@ -1175,17 +884,17 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
m_buildingBlocksStock -= cost;
|
m_buildingBlocksStock -= cost;
|
||||||
return m_buildingSystem->place(type, anchor, rotation, m_currentTick);
|
return m_buildingSystem->place(m_factoryState, type, anchor, rotation, m_currentTick);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Simulation::deconstruct(BuildingId id)
|
void Simulation::deconstruct(BuildingId id)
|
||||||
{
|
{
|
||||||
m_buildingBlocksStock += m_buildingSystem->deconstruct(id, m_currentTick);
|
m_buildingBlocksStock += m_buildingSystem->deconstruct(m_factoryState, id, m_currentTick);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Simulation::cancelDeconstruction(BuildingId id)
|
void Simulation::cancelDeconstruction(BuildingId id)
|
||||||
{
|
{
|
||||||
m_buildingSystem->cancelDeconstruction(id);
|
m_buildingSystem->cancelDeconstruction(m_factoryState, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildingSystem& Simulation::getBuildingsMutable()
|
BuildingSystem& Simulation::getBuildingsMutable()
|
||||||
@@ -1193,6 +902,11 @@ BuildingSystem& Simulation::getBuildingsMutable()
|
|||||||
return *m_buildingSystem;
|
return *m_buildingSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FactoryState& Simulation::getFactoryState() const
|
||||||
|
{
|
||||||
|
return m_factoryState;
|
||||||
|
}
|
||||||
|
|
||||||
const BuildingSystem& Simulation::getBuildings() const
|
const BuildingSystem& Simulation::getBuildings() const
|
||||||
{
|
{
|
||||||
return *m_buildingSystem;
|
return *m_buildingSystem;
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <set>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
#include "SchematicChoiceOption.h"
|
#include "SchematicChoiceOption.h"
|
||||||
@@ -22,9 +21,12 @@
|
|||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "TracePrintRequestedEvent.h"
|
#include "TracePrintRequestedEvent.h"
|
||||||
|
#include "UnlockState.h"
|
||||||
|
|
||||||
class AiSystem;
|
class AiSystem;
|
||||||
class BuildingSystem;
|
class BuildingSystem;
|
||||||
|
class ConstructionSystem;
|
||||||
|
class DeconstructionSystem;
|
||||||
struct Command;
|
struct Command;
|
||||||
class Hasher;
|
class Hasher;
|
||||||
class CombatSystem;
|
class CombatSystem;
|
||||||
@@ -119,6 +121,9 @@ public:
|
|||||||
// chokepoint) or, in tests, SimulationTestAccess — so production code cannot
|
// chokepoint) or, in tests, SimulationTestAccess — so production code cannot
|
||||||
// mutate the factory outside the recorded command path (docs/replay_design.md).
|
// mutate the factory outside the recorded command path (docs/replay_design.md).
|
||||||
const BuildingSystem& getBuildings() const;
|
const BuildingSystem& getBuildings() const;
|
||||||
|
|
||||||
|
// The factory's world data, for the free queries in FactoryQueries.h.
|
||||||
|
const FactoryState& getFactoryState() const;
|
||||||
const BeltSystem& getBelts() const;
|
const BeltSystem& getBelts() const;
|
||||||
ShipSystem& getShips();
|
ShipSystem& getShips();
|
||||||
const ShipSystem& getShips() const;
|
const ShipSystem& getShips() const;
|
||||||
@@ -203,73 +208,20 @@ private:
|
|||||||
entt::entity m_playerStation2Entity;
|
entt::entity m_playerStation2Entity;
|
||||||
entt::entity m_currentEnemyStationEntities[2];
|
entt::entity m_currentEnemyStationEntities[2];
|
||||||
|
|
||||||
// Schematic unlock state (REQ-DEF-SCHEMATIC-DROP).
|
// Schematic/unlock bookkeeping (REQ-DEF-SCHEMATIC-DROP, REQ-LOCK-EXPLICIT,
|
||||||
struct SchematicState
|
// REQ-LOCK-IMPLICIT, REQ-LOCK-BUILDING, REQ-LOCK-PREREQ). Constructed before
|
||||||
{
|
// initializeSubsystems() runs since BuildingSystem's spawn-gating lambda
|
||||||
bool unlocked;
|
// calls into it (see initializeSubsystems()).
|
||||||
};
|
UnlockState m_unlockState;
|
||||||
std::map<std::string, SchematicState> m_schematicLevels;
|
|
||||||
std::map<std::string, SchematicState> m_moduleSchematicLevels;
|
|
||||||
std::map<std::string, SchematicState> m_buildingLevels;
|
|
||||||
|
|
||||||
// Unlock groups awarded so far (REQ-LOCK-EXPLICIT). Group ids.
|
|
||||||
std::set<std::string> m_awardedUnlockGroupIds;
|
|
||||||
|
|
||||||
// Ids granted by some unlock group, per kind — cached from config at init.
|
|
||||||
// An item starts locked iff it appears in the corresponding set.
|
|
||||||
std::set<std::string> m_grantedShipIds;
|
|
||||||
std::set<std::string> m_grantedModuleIds;
|
|
||||||
std::set<std::string> m_grantedBuildingIds;
|
|
||||||
std::set<std::string> m_grantedRecipeIds;
|
|
||||||
|
|
||||||
// Builds the granted-id sets and initializes all per-item unlock maps from
|
|
||||||
// them (shared by the constructor and reset). Ends with recomputeUnlocked().
|
|
||||||
void initializeUnlockState();
|
|
||||||
|
|
||||||
// Builds a schematic choice option for one unlock group (REQ-DEF-SCHEMATIC-DROP).
|
|
||||||
SchematicChoiceOption makeUnlockOption(const UnlockGroupDef& group) const;
|
|
||||||
|
|
||||||
// Determinism helpers — fold sub-state into the hasher in deterministic order.
|
|
||||||
static void appendSchematicMap(Hasher& hasher,
|
|
||||||
const std::map<std::string, SchematicState>& levels);
|
|
||||||
static void appendStringSet(Hasher& hasher, const std::set<std::string>& ids);
|
|
||||||
|
|
||||||
// Explicitly unlocked assembler recipe schematics (REQ-LOCK-EXPLICIT).
|
|
||||||
std::set<std::string> m_unlockedRecipeSchematicIds;
|
|
||||||
|
|
||||||
// Implicit unlock sets derived from schematic state (REQ-LOCK-IMPLICIT).
|
|
||||||
std::set<std::string> m_unlockedRecipeIds;
|
|
||||||
std::set<std::string> m_unlockedItemIds;
|
|
||||||
|
|
||||||
// Recomputes m_unlockedRecipeIds and m_unlockedItemIds from current schematic state.
|
|
||||||
void recomputeUnlocked();
|
|
||||||
|
|
||||||
// Result of the REQ-LOCK-IMPLICIT traversal.
|
|
||||||
struct UnlockedSets
|
|
||||||
{
|
|
||||||
std::set<std::string> itemIds;
|
|
||||||
std::set<std::string> recipeIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pure REQ-LOCK-IMPLICIT traversal given hypothetical explicit-unlock sets.
|
|
||||||
UnlockedSets computeUnlockedSets(const std::set<std::string>& unlockedShipSchematicIds,
|
|
||||||
const std::set<std::string>& unlockedModuleSchematicIds,
|
|
||||||
const std::set<std::string>& unlockedRecipeSchematicIds) const;
|
|
||||||
|
|
||||||
// Current explicit-unlock id sets, derived from m_schematicLevels / m_moduleSchematicLevels.
|
|
||||||
std::set<std::string> getUnlockedShipSchematicIds() const;
|
|
||||||
std::set<std::string> getUnlockedModuleSchematicIds() const;
|
|
||||||
|
|
||||||
// True if every prerequisite unlock group has been awarded (REQ-LOCK-PREREQ).
|
|
||||||
bool prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const;
|
|
||||||
|
|
||||||
// Ids (sorted alphabetically by display name) of the recipes in
|
|
||||||
// hypothetical.recipeIds that are not yet in m_unlockedRecipeIds.
|
|
||||||
std::vector<std::string> computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const;
|
|
||||||
|
|
||||||
EntityAdmin m_admin;
|
EntityAdmin m_admin;
|
||||||
|
// The factory's world data. Owned here, not by BuildingSystem, so the systems
|
||||||
|
// that operate on it can be handed the same state (see FactoryState.h).
|
||||||
|
FactoryState m_factoryState;
|
||||||
BeltSystem m_beltSystem;
|
BeltSystem m_beltSystem;
|
||||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
|
std::unique_ptr<ConstructionSystem> m_constructionSystem;
|
||||||
|
std::unique_ptr<DeconstructionSystem> m_deconstructionSystem;
|
||||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||||
std::unique_ptr<AiSystem> m_aiSystem;
|
std::unique_ptr<AiSystem> m_aiSystem;
|
||||||
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
|
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
|
||||||
|
|||||||
352
src/lib/sim/UnlockState.cpp
Normal file
352
src/lib/sim/UnlockState.cpp
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
#include "UnlockState.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "DisplayName.h"
|
||||||
|
#include "StateChecksum.h"
|
||||||
|
|
||||||
|
UnlockState::UnlockState(const GameConfig& config)
|
||||||
|
: m_config(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::initializeUnlockState()
|
||||||
|
{
|
||||||
|
// Cache the ids granted by some unlock group (REQ-LOCK-EXPLICIT); an item
|
||||||
|
// starts locked iff it is granted by a group.
|
||||||
|
m_grantedShipIds.clear();
|
||||||
|
m_grantedModuleIds.clear();
|
||||||
|
m_grantedBuildingIds.clear();
|
||||||
|
m_grantedRecipeIds.clear();
|
||||||
|
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
||||||
|
{
|
||||||
|
m_grantedShipIds.insert(group.ships.begin(), group.ships.end());
|
||||||
|
m_grantedModuleIds.insert(group.modules.begin(), group.modules.end());
|
||||||
|
m_grantedBuildingIds.insert(group.buildings.begin(), group.buildings.end());
|
||||||
|
m_grantedRecipeIds.insert(group.recipes.begin(), group.recipes.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_awardedUnlockGroupIds.clear();
|
||||||
|
|
||||||
|
m_schematicLevels.clear();
|
||||||
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
|
{
|
||||||
|
SchematicState state;
|
||||||
|
state.unlocked = (m_grantedShipIds.count(def.id) == 0);
|
||||||
|
m_schematicLevels[def.id] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_moduleSchematicLevels.clear();
|
||||||
|
for (const ModuleDef& def : m_config.modules.modules)
|
||||||
|
{
|
||||||
|
SchematicState state;
|
||||||
|
state.unlocked = (m_grantedModuleIds.count(def.id) == 0);
|
||||||
|
m_moduleSchematicLevels[def.id] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_buildingLevels.clear();
|
||||||
|
for (const BuildingDef& def : m_config.buildings.buildings)
|
||||||
|
{
|
||||||
|
SchematicState state;
|
||||||
|
state.unlocked = (m_grantedBuildingIds.count(def.id) == 0);
|
||||||
|
m_buildingLevels[def.id] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gated assembler recipes start locked; unlocked_at_start recipes are handled
|
||||||
|
// in the REQ-LOCK-IMPLICIT traversal, not tracked here.
|
||||||
|
m_unlockedRecipeSchematicIds.clear();
|
||||||
|
|
||||||
|
recomputeUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isSchematicUnlocked(const std::string& shipId) const
|
||||||
|
{
|
||||||
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
|
m_schematicLevels.find(shipId);
|
||||||
|
if (it == m_schematicLevels.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return it->second.unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isModuleSchematicUnlocked(const std::string& moduleId) const
|
||||||
|
{
|
||||||
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
|
m_moduleSchematicLevels.find(moduleId);
|
||||||
|
if (it == m_moduleSchematicLevels.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return it->second.unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isRecipeUnlocked(const std::string& recipeId) const
|
||||||
|
{
|
||||||
|
return m_unlockedRecipeIds.count(recipeId) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isItemUnlocked(const std::string& itemId) const
|
||||||
|
{
|
||||||
|
return m_unlockedItemIds.count(itemId) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isBuildingUnlocked(BuildingType type) const
|
||||||
|
{
|
||||||
|
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
||||||
|
if (def == nullptr)
|
||||||
|
{
|
||||||
|
// Types without a config entry (e.g. HQ, defence stations) are unrestricted.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
|
m_buildingLevels.find(def->id);
|
||||||
|
return it == m_buildingLevels.end() ? true : it->second.unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isUnlockGroupAwarded(const std::string& groupId) const
|
||||||
|
{
|
||||||
|
return m_awardedUnlockGroupIds.count(groupId) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const
|
||||||
|
{
|
||||||
|
// A prerequisite is satisfied only once the named unlock group has been
|
||||||
|
// awarded (REQ-LOCK-PREREQ).
|
||||||
|
for (const std::string& groupId : requiredGroupIds)
|
||||||
|
{
|
||||||
|
if (m_awardedUnlockGroupIds.count(groupId) == 0) { return false; }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SchematicChoiceOption UnlockState::makeUnlockOption(const UnlockGroupDef& group) const
|
||||||
|
{
|
||||||
|
SchematicChoiceOption option;
|
||||||
|
option.isArtifact = false;
|
||||||
|
option.unlockGroupId = group.id;
|
||||||
|
option.displayName = toDisplayName(group.id);
|
||||||
|
|
||||||
|
for (const std::string& id : group.ships)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Ship, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
for (const std::string& id : group.modules)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Module, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
for (const std::string& id : group.buildings)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Building, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
for (const std::string& id : group.recipes)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Recipe, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
|
||||||
|
// REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by
|
||||||
|
// awarding this whole group. Seed the hypothetical explicit-unlock sets with
|
||||||
|
// every grant (ship + module materials via step 1a, recipe outputs via step
|
||||||
|
// 1b), then diff against the current implicit set.
|
||||||
|
std::set<std::string> hypotheticalShipIds = getUnlockedShipSchematicIds();
|
||||||
|
std::set<std::string> hypotheticalModuleIds = getUnlockedModuleSchematicIds();
|
||||||
|
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
|
||||||
|
for (const std::string& id : group.ships) { hypotheticalShipIds.insert(id); }
|
||||||
|
for (const std::string& id : group.modules) { hypotheticalModuleIds.insert(id); }
|
||||||
|
for (const std::string& id : group.recipes) { hypotheticalRecipeSchematicIds.insert(id); }
|
||||||
|
|
||||||
|
const UnlockedSets hypothetical = computeUnlockedSets(
|
||||||
|
hypotheticalShipIds, hypotheticalModuleIds, hypotheticalRecipeSchematicIds);
|
||||||
|
option.newlyUnlockedRecipeIds = computeNewlyUnlockedRecipeIds(hypothetical);
|
||||||
|
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::awardUnlockGroup(const SchematicChoiceOption& chosen)
|
||||||
|
{
|
||||||
|
// Award the whole unlock group (REQ-DEF-SCHEMATIC-DROP): unlock every granted
|
||||||
|
// ship, module, building, and assembler recipe at once.
|
||||||
|
m_awardedUnlockGroupIds.insert(chosen.unlockGroupId);
|
||||||
|
for (const GrantedSchematic& grant : chosen.grantedItems)
|
||||||
|
{
|
||||||
|
switch (grant.type)
|
||||||
|
{
|
||||||
|
case SchematicType::Ship: m_schematicLevels.at(grant.id).unlocked = true; break;
|
||||||
|
case SchematicType::Module: m_moduleSchematicLevels.at(grant.id).unlocked = true; break;
|
||||||
|
case SchematicType::Building: m_buildingLevels.at(grant.id).unlocked = true; break;
|
||||||
|
case SchematicType::Recipe: m_unlockedRecipeSchematicIds.insert(grant.id); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recomputeUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Implicit unlock computation (REQ-LOCK-IMPLICIT)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void UnlockState::recomputeUnlocked()
|
||||||
|
{
|
||||||
|
const UnlockedSets result = computeUnlockedSets(
|
||||||
|
getUnlockedShipSchematicIds(), getUnlockedModuleSchematicIds(), m_unlockedRecipeSchematicIds);
|
||||||
|
m_unlockedItemIds = result.itemIds;
|
||||||
|
m_unlockedRecipeIds = result.recipeIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> UnlockState::getUnlockedShipSchematicIds() const
|
||||||
|
{
|
||||||
|
std::set<std::string> ids;
|
||||||
|
for (const auto& [id, state] : m_schematicLevels)
|
||||||
|
{
|
||||||
|
if (state.unlocked) { ids.insert(id); }
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> UnlockState::getUnlockedModuleSchematicIds() const
|
||||||
|
{
|
||||||
|
std::set<std::string> ids;
|
||||||
|
for (const auto& [id, state] : m_moduleSchematicLevels)
|
||||||
|
{
|
||||||
|
if (state.unlocked) { ids.insert(id); }
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnlockState::UnlockedSets UnlockState::computeUnlockedSets(
|
||||||
|
const std::set<std::string>& unlockedShipSchematicIds,
|
||||||
|
const std::set<std::string>& unlockedModuleSchematicIds,
|
||||||
|
const std::set<std::string>& unlockedRecipeSchematicIds) const
|
||||||
|
{
|
||||||
|
UnlockedSets result;
|
||||||
|
|
||||||
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
|
{
|
||||||
|
if (unlockedShipSchematicIds.count(def.id) == 0) { continue; }
|
||||||
|
for (const RecipeIngredient& mat : def.schematic.materials)
|
||||||
|
{
|
||||||
|
result.itemIds.insert(mat.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const ModuleDef& def : m_config.modules.modules)
|
||||||
|
{
|
||||||
|
if (unlockedModuleSchematicIds.count(def.id) == 0) { continue; }
|
||||||
|
for (const RecipeIngredient& mat : def.materials)
|
||||||
|
{
|
||||||
|
result.itemIds.insert(mat.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||||
|
{
|
||||||
|
// An assembler recipe seeds the base set when it is explicitly available:
|
||||||
|
// flagged unlocked_at_start (base recipes the graph can't reach), or a
|
||||||
|
// gated recipe whose unlock group has been awarded (REQ-LOCK-EXPLICIT).
|
||||||
|
if (def.building == BuildingType::Assembler
|
||||||
|
&& (def.unlockedAtStart || unlockedRecipeSchematicIds.count(def.id) > 0))
|
||||||
|
{
|
||||||
|
for (const RecipeOutput& out : def.outputs)
|
||||||
|
{
|
||||||
|
result.itemIds.insert(out.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = true;
|
||||||
|
while (changed)
|
||||||
|
{
|
||||||
|
changed = false;
|
||||||
|
for (const RecipeDef& recipe : m_config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (recipe.building != BuildingType::Miner
|
||||||
|
&& recipe.building != BuildingType::Smelter
|
||||||
|
&& recipe.building != BuildingType::Assembler)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Skip a gated assembler recipe (granted by an unlock group) whose
|
||||||
|
// group has not yet been awarded (REQ-LOCK-IMPLICIT step 2).
|
||||||
|
if (recipe.building == BuildingType::Assembler
|
||||||
|
&& m_grantedRecipeIds.count(recipe.id) > 0
|
||||||
|
&& unlockedRecipeSchematicIds.count(recipe.id) == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool producesUnlocked = false;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
if (result.itemIds.count(out.item) > 0)
|
||||||
|
{
|
||||||
|
producesUnlocked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!producesUnlocked) { continue; }
|
||||||
|
|
||||||
|
if (recipe.building == BuildingType::Miner
|
||||||
|
|| recipe.building == BuildingType::Assembler)
|
||||||
|
{
|
||||||
|
result.recipeIds.insert(recipe.id);
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
|
{
|
||||||
|
if (result.itemIds.insert(ing.item).second)
|
||||||
|
{
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> UnlockState::computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const
|
||||||
|
{
|
||||||
|
std::vector<std::string> recipeIds;
|
||||||
|
for (const std::string& recipeId : hypothetical.recipeIds)
|
||||||
|
{
|
||||||
|
if (m_unlockedRecipeIds.count(recipeId) > 0) { continue; }
|
||||||
|
recipeIds.push_back(recipeId);
|
||||||
|
}
|
||||||
|
std::sort(recipeIds.begin(), recipeIds.end(),
|
||||||
|
[](const std::string& lhs, const std::string& rhs)
|
||||||
|
{
|
||||||
|
return toDisplayName(lhs) < toDisplayName(rhs);
|
||||||
|
});
|
||||||
|
return recipeIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Determinism (see docs/replay_design.md)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void UnlockState::appendSchematicMap(Hasher& hasher,
|
||||||
|
const std::map<std::string, SchematicState>& levels)
|
||||||
|
{
|
||||||
|
hasher.append(levels.size());
|
||||||
|
for (const std::pair<const std::string, SchematicState>& entry : levels)
|
||||||
|
{
|
||||||
|
hasher.append(entry.first);
|
||||||
|
hasher.append(entry.second.unlocked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::appendStringSet(Hasher& hasher, const std::set<std::string>& ids)
|
||||||
|
{
|
||||||
|
hasher.append(ids.size());
|
||||||
|
for (const std::string& id : ids)
|
||||||
|
{
|
||||||
|
hasher.append(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::appendChecksum(Hasher& hasher) const
|
||||||
|
{
|
||||||
|
appendSchematicMap(hasher, m_schematicLevels);
|
||||||
|
appendSchematicMap(hasher, m_moduleSchematicLevels);
|
||||||
|
appendSchematicMap(hasher, m_buildingLevels);
|
||||||
|
appendStringSet(hasher, m_awardedUnlockGroupIds);
|
||||||
|
appendStringSet(hasher, m_unlockedRecipeSchematicIds);
|
||||||
|
appendStringSet(hasher, m_unlockedRecipeIds);
|
||||||
|
appendStringSet(hasher, m_unlockedItemIds);
|
||||||
|
}
|
||||||
129
src/lib/sim/UnlockState.h
Normal file
129
src/lib/sim/UnlockState.h
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "SchematicChoiceOption.h"
|
||||||
|
|
||||||
|
class Hasher;
|
||||||
|
|
||||||
|
// Owns schematic/unlock bookkeeping for one run: which ship, module, and
|
||||||
|
// building schematics are unlocked (REQ-LOCK-EXPLICIT), which assembler recipe
|
||||||
|
// schematics have been explicitly granted, and the implicit recipe/item unlock
|
||||||
|
// sets derived from that state (REQ-LOCK-IMPLICIT). Reads config the same way
|
||||||
|
// BuildingSystem does (a bound const reference to Simulation::m_config, which is
|
||||||
|
// safe across restart because that member's storage address never changes —
|
||||||
|
// reset() move-assigns into it rather than replacing it).
|
||||||
|
//
|
||||||
|
// Simulation forwards its isXUnlocked-style public queries here and drives
|
||||||
|
// state changes (awarding an unlock group) here; the RNG-touching schematic
|
||||||
|
// choice generation itself stays in Simulation (call ordering of m_rng is the
|
||||||
|
// determinism backbone and must not move).
|
||||||
|
class UnlockState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit UnlockState(const GameConfig& config);
|
||||||
|
|
||||||
|
// Builds the granted-id sets and initializes all per-item unlock maps from
|
||||||
|
// them (shared by the constructor and Simulation::reset). Ends with
|
||||||
|
// recomputeUnlocked().
|
||||||
|
void initializeUnlockState();
|
||||||
|
|
||||||
|
// Ship schematic state query.
|
||||||
|
bool isSchematicUnlocked(const std::string& shipId) const;
|
||||||
|
|
||||||
|
// Module schematic state query.
|
||||||
|
bool isModuleSchematicUnlocked(const std::string& moduleId) const;
|
||||||
|
|
||||||
|
// Implicit recipe/item unlock queries (REQ-LOCK-IMPLICIT).
|
||||||
|
bool isRecipeUnlocked(const std::string& recipeId) const;
|
||||||
|
bool isItemUnlocked(const std::string& itemId) const;
|
||||||
|
|
||||||
|
// Building unlock query (REQ-LOCK-BUILDING). True if the building type is not
|
||||||
|
// gated by any unlock group, or its granting group has been awarded.
|
||||||
|
bool isBuildingUnlocked(BuildingType type) const;
|
||||||
|
|
||||||
|
// True if the unlock group has already been awarded to the player.
|
||||||
|
bool isUnlockGroupAwarded(const std::string& groupId) const;
|
||||||
|
|
||||||
|
// True if every prerequisite unlock group has been awarded (REQ-LOCK-PREREQ).
|
||||||
|
bool prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const;
|
||||||
|
|
||||||
|
// Builds a schematic choice option for one unlock group (REQ-DEF-SCHEMATIC-DROP).
|
||||||
|
SchematicChoiceOption makeUnlockOption(const UnlockGroupDef& group) const;
|
||||||
|
|
||||||
|
// Awards the unlock group backing `chosen` (REQ-DEF-SCHEMATIC-DROP): marks
|
||||||
|
// every granted ship/module/building schematic unlocked, records granted
|
||||||
|
// recipe schematics, marks the group as awarded, and recomputes the implicit
|
||||||
|
// unlock sets. Mirrors the non-artifact branch of the original
|
||||||
|
// Simulation::applySchematicChoice exactly; callers still special-case
|
||||||
|
// chosen.isArtifact themselves before calling this.
|
||||||
|
void awardUnlockGroup(const SchematicChoiceOption& chosen);
|
||||||
|
|
||||||
|
// Determinism helper (see Simulation::computeStateChecksum): folds unlock
|
||||||
|
// state into the hasher via the same seven calls, in the same order, that
|
||||||
|
// used to live at the Simulation::computeStateChecksum call site.
|
||||||
|
void appendChecksum(Hasher& hasher) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Schematic unlock state (REQ-DEF-SCHEMATIC-DROP).
|
||||||
|
struct SchematicState
|
||||||
|
{
|
||||||
|
bool unlocked;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recomputes m_unlockedRecipeIds and m_unlockedItemIds from current schematic state.
|
||||||
|
void recomputeUnlocked();
|
||||||
|
|
||||||
|
// Result of the REQ-LOCK-IMPLICIT traversal.
|
||||||
|
struct UnlockedSets
|
||||||
|
{
|
||||||
|
std::set<std::string> itemIds;
|
||||||
|
std::set<std::string> recipeIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pure REQ-LOCK-IMPLICIT traversal given hypothetical explicit-unlock sets.
|
||||||
|
UnlockedSets computeUnlockedSets(const std::set<std::string>& unlockedShipSchematicIds,
|
||||||
|
const std::set<std::string>& unlockedModuleSchematicIds,
|
||||||
|
const std::set<std::string>& unlockedRecipeSchematicIds) const;
|
||||||
|
|
||||||
|
// Current explicit-unlock id sets, derived from m_schematicLevels / m_moduleSchematicLevels.
|
||||||
|
std::set<std::string> getUnlockedShipSchematicIds() const;
|
||||||
|
std::set<std::string> getUnlockedModuleSchematicIds() const;
|
||||||
|
|
||||||
|
// Ids (sorted alphabetically by display name) of the recipes in
|
||||||
|
// hypothetical.recipeIds that are not yet in m_unlockedRecipeIds.
|
||||||
|
std::vector<std::string> computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const;
|
||||||
|
|
||||||
|
// Determinism helpers — fold sub-state into the hasher in deterministic order.
|
||||||
|
static void appendSchematicMap(Hasher& hasher,
|
||||||
|
const std::map<std::string, SchematicState>& levels);
|
||||||
|
static void appendStringSet(Hasher& hasher, const std::set<std::string>& ids);
|
||||||
|
|
||||||
|
const GameConfig& m_config;
|
||||||
|
|
||||||
|
std::map<std::string, SchematicState> m_schematicLevels;
|
||||||
|
std::map<std::string, SchematicState> m_moduleSchematicLevels;
|
||||||
|
std::map<std::string, SchematicState> m_buildingLevels;
|
||||||
|
|
||||||
|
// Unlock groups awarded so far (REQ-LOCK-EXPLICIT). Group ids.
|
||||||
|
std::set<std::string> m_awardedUnlockGroupIds;
|
||||||
|
|
||||||
|
// Ids granted by some unlock group, per kind — cached from config at init.
|
||||||
|
// An item starts locked iff it appears in the corresponding set.
|
||||||
|
std::set<std::string> m_grantedShipIds;
|
||||||
|
std::set<std::string> m_grantedModuleIds;
|
||||||
|
std::set<std::string> m_grantedBuildingIds;
|
||||||
|
std::set<std::string> m_grantedRecipeIds;
|
||||||
|
|
||||||
|
// Explicitly unlocked assembler recipe schematics (REQ-LOCK-EXPLICIT).
|
||||||
|
std::set<std::string> m_unlockedRecipeSchematicIds;
|
||||||
|
|
||||||
|
// Implicit unlock sets derived from schematic state (REQ-LOCK-IMPLICIT).
|
||||||
|
std::set<std::string> m_unlockedRecipeIds;
|
||||||
|
std::set<std::string> m_unlockedItemIds;
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <random>
|
#include <random>
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
|
#include "ConstructionSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
#include "DeliverScrapBehavior.h"
|
#include "DeliverScrapBehavior.h"
|
||||||
@@ -54,12 +57,14 @@
|
|||||||
struct Fixture
|
struct Fixture
|
||||||
{
|
{
|
||||||
GameConfig cfg;
|
GameConfig cfg;
|
||||||
|
FactoryState state = makeFactoryState(cfg);
|
||||||
BeltSystem belts;
|
BeltSystem belts;
|
||||||
BuildingId nextBuildingId;
|
BuildingId nextBuildingId;
|
||||||
int stock;
|
int stock;
|
||||||
std::mt19937 rng;
|
std::mt19937 rng;
|
||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
BuildingSystem buildings;
|
BuildingSystem buildings;
|
||||||
|
ConstructionSystem construction;
|
||||||
ShipSystem ships;
|
ShipSystem ships;
|
||||||
AiSystem ai;
|
AiSystem ai;
|
||||||
SalvagerSystem salvager;
|
SalvagerSystem salvager;
|
||||||
@@ -82,6 +87,7 @@ struct Fixture
|
|||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
[](const std::string&) -> bool { return true; },
|
[](const std::string&) -> bool { return true; },
|
||||||
rng)
|
rng)
|
||||||
|
, construction(cfg)
|
||||||
, ships(cfg, admin)
|
, ships(cfg, admin)
|
||||||
, ai(cfg)
|
, ai(cfg)
|
||||||
, salvager(admin)
|
, salvager(admin)
|
||||||
@@ -95,14 +101,14 @@ struct Fixture
|
|||||||
void decide()
|
void decide()
|
||||||
{
|
{
|
||||||
ships.clearMovementIntents();
|
ships.clearMovementIntents();
|
||||||
ai.tick(admin, buildings, scraps);
|
ai.tick(admin, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// World mutation: collection/delivery and healing.
|
// World mutation: collection/delivery and healing.
|
||||||
void runModules()
|
void runModules()
|
||||||
{
|
{
|
||||||
beamEvents.clear();
|
beamEvents.clear();
|
||||||
salvager.tick(tick, scraps, buildings, beamEvents);
|
salvager.tick(tick, state, beamEvents);
|
||||||
repair.tick(tick, beamEvents);
|
repair.tick(tick, beamEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +141,7 @@ struct Fixture
|
|||||||
void salvageTick()
|
void salvageTick()
|
||||||
{
|
{
|
||||||
beamEvents.clear();
|
beamEvents.clear();
|
||||||
salvager.tick(tick, scraps, buildings, beamEvents);
|
salvager.tick(tick, state, beamEvents);
|
||||||
++tick;
|
++tick;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -949,18 +955,18 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
|
|
||||||
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
|
const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay,
|
||||||
QPoint(-4, 0), Rotation::East, 0).value();
|
QPoint(-4, 0), Rotation::East, 0).value();
|
||||||
Tick t = 0;
|
Tick t = 0;
|
||||||
for (int i = 0; i < 500; ++i)
|
for (int i = 0; i < 500; ++i)
|
||||||
{
|
{
|
||||||
f.buildings.tickConstruction(t++);
|
f.construction.tick(f.state, f.belts, t++);
|
||||||
if (f.buildings.findBuilding(bayId) != nullptr)
|
if (findBuilding(f.state, bayId) != nullptr)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
REQUIRE(f.buildings.findBuilding(bayId) != nullptr);
|
REQUIRE(findBuilding(f.state, bayId) != nullptr);
|
||||||
|
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(5.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(5.0f, 0.0f),
|
||||||
@@ -984,15 +990,15 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo",
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
|
|
||||||
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
|
const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay,
|
||||||
QPoint(-4, 0), Rotation::East, 0).value();
|
QPoint(-4, 0), Rotation::East, 0).value();
|
||||||
Tick t = 0;
|
Tick t = 0;
|
||||||
for (int i = 0; i < 500; ++i)
|
for (int i = 0; i < 500; ++i)
|
||||||
{
|
{
|
||||||
f.buildings.tickConstruction(t++);
|
f.construction.tick(f.state, f.belts, t++);
|
||||||
if (f.buildings.findBuilding(bayId) != nullptr) { break; }
|
if (findBuilding(f.state, bayId) != nullptr) { break; }
|
||||||
}
|
}
|
||||||
const Building* bay = f.buildings.findBuilding(bayId);
|
const Building* bay = findBuilding(f.state, bayId);
|
||||||
REQUIRE(bay != nullptr);
|
REQUIRE(bay != nullptr);
|
||||||
// Config-driven output-buffer capacity is applied on placement (REQ-BLD-SALVAGE-BAY).
|
// Config-driven output-buffer capacity is applied on placement (REQ-BLD-SALVAGE-BAY).
|
||||||
REQUIRE(bay->outputBuffer.capacity == 20);
|
REQUIRE(bay->outputBuffer.capacity == 20);
|
||||||
@@ -1013,7 +1019,7 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo",
|
|||||||
|
|
||||||
// One unit handed over from cargo into the bay's output buffer.
|
// One unit handed over from cargo into the bay's output buffer.
|
||||||
REQUIRE(f.admin.get<CargoComponent>(ship).current == before - 1);
|
REQUIRE(f.admin.get<CargoComponent>(ship).current == before - 1);
|
||||||
const Building* bayAfter = f.buildings.findBuilding(bayId);
|
const Building* bayAfter = findBuilding(f.state, bayId);
|
||||||
REQUIRE(bayAfter != nullptr);
|
REQUIRE(bayAfter != nullptr);
|
||||||
REQUIRE(bayAfter->outputBuffer.items.size() == 1);
|
REQUIRE(bayAfter->outputBuffer.items.size() == 1);
|
||||||
REQUIRE(bayAfter->outputBuffer.items.front().type.id == "scrap");
|
REQUIRE(bayAfter->outputBuffer.items.front().type.id == "scrap");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
@@ -529,9 +530,9 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
|
|||||||
|
|
||||||
REQUIRE(idA != kInvalidBuildingId);
|
REQUIRE(idA != kInvalidBuildingId);
|
||||||
REQUIRE(idB != kInvalidBuildingId);
|
REQUIRE(idB != kInvalidBuildingId);
|
||||||
REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetA)); // (-6, 0)
|
REQUIRE(isTileOccupied(sim.getFactoryState(), cursor + offsetA)); // (-6, 0)
|
||||||
REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetB)); // (-4, 0)
|
REQUIRE(isTileOccupied(sim.getFactoryState(), cursor + offsetB)); // (-4, 0)
|
||||||
REQUIRE_FALSE(sim.getBuildings().isTileOccupied(cursor)); // center not occupied
|
REQUIRE_FALSE(isTileOccupied(sim.getFactoryState(), cursor)); // center not occupied
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]")
|
TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]")
|
||||||
@@ -600,7 +601,7 @@ TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and ch
|
|||||||
|
|
||||||
REQUIRE_FALSE(id.has_value());
|
REQUIRE_FALSE(id.has_value());
|
||||||
REQUIRE(sim.getBuildingBlocksStock() == startBlocks);
|
REQUIRE(sim.getBuildingBlocksStock() == startBlocks);
|
||||||
REQUIRE(sim.getBuildings().getAllSites().empty());
|
REQUIRE(getAllSites(sim.getFactoryState()).empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost",
|
TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost",
|
||||||
@@ -623,11 +624,11 @@ TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies
|
|||||||
|
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost);
|
REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost);
|
||||||
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 0)));
|
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-3, 0)));
|
||||||
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-2, 0)));
|
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-2, 0)));
|
||||||
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 1)));
|
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-3, 1)));
|
||||||
// The output-port tile (1,1)+anchor = (-2,1) is not a body cell.
|
// The output-port tile (1,1)+anchor = (-2,1) is not a body cell.
|
||||||
REQUIRE_FALSE(sim.getBuildings().isTileOccupied(QPoint(-2, 1)));
|
REQUIRE_FALSE(isTileOccupied(sim.getFactoryState(), QPoint(-2, 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -665,9 +666,9 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "
|
|||||||
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||||
|
|
||||||
const ConstructionSite* site = sim.getBuildings().findSite(id);
|
const ConstructionSite* site = findSite(sim.getFactoryState(), id);
|
||||||
REQUIRE(site != nullptr);
|
REQUIRE(site != nullptr);
|
||||||
REQUIRE(site->recipeId == "mine_iron_ore");
|
REQUIRE(site->recipeId == "mine_iron_ore");
|
||||||
}
|
}
|
||||||
@@ -679,7 +680,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
|
|||||||
|
|
||||||
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_copper_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_copper_ore");
|
||||||
|
|
||||||
// Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300.
|
// Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300.
|
||||||
// Run 301 ticks (0..300) to process the completion tick.
|
// Run 301 ticks (0..300) to process the completion tick.
|
||||||
@@ -688,7 +689,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
|
|||||||
sim.tick();
|
sim.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
const Building* b = sim.getBuildings().findBuilding(id);
|
const Building* b = findBuilding(sim.getFactoryState(), id);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
REQUIRE(b->recipeId == "mine_copper_ore");
|
REQUIRE(b->recipeId == "mine_copper_ore");
|
||||||
}
|
}
|
||||||
@@ -708,8 +709,8 @@ TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
|
|||||||
const BuildingId id =
|
const BuildingId id =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
|
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
|
||||||
REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
|
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
|
||||||
|
|
||||||
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
|
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
|
||||||
|
|
||||||
@@ -725,7 +726,7 @@ TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blue
|
|||||||
const BuildingId id =
|
const BuildingId id =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||||
|
|
||||||
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
|
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
|
||||||
|
|
||||||
@@ -742,16 +743,16 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
|
|||||||
const BuildingId idA =
|
const BuildingId idA =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(idA != kInvalidBuildingId);
|
REQUIRE(idA != kInvalidBuildingId);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idA, "mine_iron_ore");
|
||||||
for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); }
|
for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); }
|
||||||
REQUIRE(sim.getBuildings().findBuilding(idA) != nullptr);
|
REQUIRE(findBuilding(sim.getFactoryState(), idA) != nullptr);
|
||||||
|
|
||||||
// Building B: place and configure, but leave as a construction site.
|
// Building B: place and configure, but leave as a construction site.
|
||||||
const BuildingId idB =
|
const BuildingId idB =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East).value();
|
||||||
REQUIRE(idB != kInvalidBuildingId);
|
REQUIRE(idB != kInvalidBuildingId);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idB, "mine_copper_ore");
|
||||||
REQUIRE(sim.getBuildings().findSite(idB) != nullptr);
|
REQUIRE(findSite(sim.getFactoryState(), idB) != nullptr);
|
||||||
|
|
||||||
const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB });
|
const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB });
|
||||||
|
|
||||||
@@ -775,7 +776,7 @@ TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction
|
|||||||
const BuildingId id =
|
const BuildingId id =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
|
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
|
||||||
REQUIRE(selectionHasPlaceableBuilding(sim, { id }));
|
REQUIRE(selectionHasPlaceableBuilding(sim, { id }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,9 +854,9 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
|
|||||||
pm.rotation = Rotation::East;
|
pm.rotation = Rotation::East;
|
||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
|
||||||
|
|
||||||
const ConstructionSite* site = sim.getBuildings().findSite(id);
|
const ConstructionSite* site = findSite(sim.getFactoryState(), id);
|
||||||
REQUIRE(site != nullptr);
|
REQUIRE(site != nullptr);
|
||||||
REQUIRE(site->shipLayout.has_value());
|
REQUIRE(site->shipLayout.has_value());
|
||||||
REQUIRE(site->shipLayout->placedModules.size() == 1);
|
REQUIRE(site->shipLayout->placedModules.size() == 1);
|
||||||
@@ -877,7 +878,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
|
|||||||
pm.rotation = Rotation::North;
|
pm.rotation = Rotation::North;
|
||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
|
||||||
|
|
||||||
// Shipyard construction_time_seconds = 30 in the test config.
|
// Shipyard construction_time_seconds = 30 in the test config.
|
||||||
double constructionTime = 0.0;
|
double constructionTime = 0.0;
|
||||||
@@ -892,7 +893,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
|
|||||||
sim.tick();
|
sim.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
const Building* b = sim.getBuildings().findBuilding(id);
|
const Building* b = findBuilding(sim.getFactoryState(), id);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
REQUIRE(b->shipLayout.has_value());
|
REQUIRE(b->shipLayout.has_value());
|
||||||
REQUIRE(b->shipLayout->placedModules.size() == 1);
|
REQUIRE(b->shipLayout->placedModules.size() == 1);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@@ -36,8 +37,7 @@ BuildingId placeOperational(Simulation& sim, const GameConfig& cfg,
|
|||||||
{
|
{
|
||||||
const BuildingDef* def = findDef(cfg, type);
|
const BuildingDef* def = findDef(cfg, type);
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
return SimulationTestAccess::buildings(sim).placeImmediate(
|
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), type, def->surfaceMask, anchor, Rotation::East);
|
||||||
type, def->surfaceMask, anchor, Rotation::East);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShipDef* findAvailableSchematic(const GameConfig& cfg)
|
const ShipDef* findAvailableSchematic(const GameConfig& cfg)
|
||||||
@@ -66,7 +66,7 @@ TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]"
|
|||||||
Simulation sim(loadTestConfig(), 7);
|
Simulation sim(loadTestConfig(), 7);
|
||||||
|
|
||||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
|
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||||
|
|
||||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||||
REQUIRE(config.has_value());
|
REQUIRE(config.has_value());
|
||||||
@@ -102,8 +102,8 @@ TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
|
|||||||
REQUIRE(schematic != nullptr);
|
REQUIRE(schematic != nullptr);
|
||||||
|
|
||||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0));
|
const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0));
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, schematic->id);
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, schematic->id);
|
||||||
SimulationTestAccess::buildings(sim).setShipLayout(id, ShipLayoutConfig{});
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, ShipLayoutConfig{});
|
||||||
|
|
||||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||||
REQUIRE(config.has_value());
|
REQUIRE(config.has_value());
|
||||||
@@ -122,10 +122,10 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
|
|||||||
const BuildingId id =
|
const BuildingId id =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
|
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
|
||||||
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
|
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||||
|
|
||||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||||
REQUIRE(config.has_value());
|
REQUIRE(config.has_value());
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@
|
|||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "CombatSystem.h"
|
#include "CombatSystem.h"
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
@@ -51,6 +52,7 @@ static entt::entity findWeaponChild(EntityAdmin& admin, entt::entity ship)
|
|||||||
struct CombatFixture
|
struct CombatFixture
|
||||||
{
|
{
|
||||||
GameConfig cfg;
|
GameConfig cfg;
|
||||||
|
FactoryState state = makeFactoryState(cfg);
|
||||||
std::mt19937 rng;
|
std::mt19937 rng;
|
||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
BuildingId nextBuildingId;
|
BuildingId nextBuildingId;
|
||||||
@@ -110,7 +112,7 @@ TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[comb
|
|||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
f.combat.applyPendingDamage(5, f.admin);
|
f.combat.applyPendingDamage(5, f.admin);
|
||||||
|
|
||||||
REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
|
REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
|
||||||
@@ -143,13 +145,13 @@ TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]"
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
REQUIRE_FALSE(enemyFiredIn(events));
|
REQUIRE_FALSE(enemyFiredIn(events));
|
||||||
|
|
||||||
f.combat.tick(1, f.admin, f.buildings, events);
|
f.combat.tick(1, f.admin, events);
|
||||||
REQUIRE_FALSE(enemyFiredIn(events));
|
REQUIRE_FALSE(enemyFiredIn(events));
|
||||||
|
|
||||||
f.combat.tick(2, f.admin, f.buildings, events);
|
f.combat.tick(2, f.admin, events);
|
||||||
REQUIRE(enemyFiredIn(events));
|
REQUIRE(enemyFiredIn(events));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +166,7 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
|||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
REQUIRE(events.empty());
|
REQUIRE(events.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +309,7 @@ TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
|
|||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
|
|
||||||
for (Tick t = 1; t < 5; ++t)
|
for (Tick t = 1; t < 5; ++t)
|
||||||
{
|
{
|
||||||
@@ -329,7 +331,7 @@ TEST_CASE("CombatSystem: damage applied exactly at impact tick", "[combat]")
|
|||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
f.combat.applyPendingDamage(5, f.admin);
|
f.combat.applyPendingDamage(5, f.admin);
|
||||||
|
|
||||||
REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
|
REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
|
||||||
@@ -346,7 +348,7 @@ TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[comb
|
|||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
|
|
||||||
f.ships.despawn(player);
|
f.ships.despawn(player);
|
||||||
|
|
||||||
@@ -369,7 +371,7 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
|
|||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
|
|
||||||
f.ships.despawn(enemy);
|
f.ships.despawn(enemy);
|
||||||
|
|
||||||
@@ -413,7 +415,7 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
|||||||
|
|
||||||
sim.tick();
|
sim.tick();
|
||||||
|
|
||||||
const std::vector<DebrisInfo> scraps = sim.getDebrisSystem().getAllDebrisInfo();
|
const std::vector<DebrisInfo> scraps = getAllDebrisInfo(sim.getAdmin());
|
||||||
REQUIRE(scraps.size() == 1);
|
REQUIRE(scraps.size() == 1);
|
||||||
CHECK(sim.getAdmin().get<DebrisComponent>(scraps[0].entity).amount == 59);
|
CHECK(sim.getAdmin().get<DebrisComponent>(scraps[0].entity).amount == 59);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe"
|
|||||||
|
|
||||||
const BuildingId id =
|
const BuildingId id =
|
||||||
SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
SimulationTestAccess::buildings(viaDirect).setRecipe(id, "mine_iron_ore");
|
SimulationTestAccess::buildings(viaDirect).setRecipe(SimulationTestAccess::state(viaDirect), id, "mine_iron_ore");
|
||||||
|
|
||||||
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
|
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,16 +116,16 @@ TEST_CASE("DebrisSystem: collectOne depletes one scrap and keeps the debris unti
|
|||||||
|
|
||||||
const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 3, 100);
|
const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 3, 100);
|
||||||
|
|
||||||
REQUIRE(ss.collectOne(e));
|
REQUIRE(collectOne(admin, e));
|
||||||
REQUIRE(admin.isValid(e));
|
REQUIRE(admin.isValid(e));
|
||||||
REQUIRE(admin.get<DebrisComponent>(e).amount == 2);
|
REQUIRE(admin.get<DebrisComponent>(e).amount == 2);
|
||||||
|
|
||||||
REQUIRE(ss.collectOne(e));
|
REQUIRE(collectOne(admin, e));
|
||||||
REQUIRE(admin.isValid(e));
|
REQUIRE(admin.isValid(e));
|
||||||
REQUIRE(admin.get<DebrisComponent>(e).amount == 1);
|
REQUIRE(admin.get<DebrisComponent>(e).amount == 1);
|
||||||
|
|
||||||
// Final unit collected: the debris is removed once depleted.
|
// Final unit collected: the debris is removed once depleted.
|
||||||
REQUIRE(ss.collectOne(e));
|
REQUIRE(collectOne(admin, e));
|
||||||
REQUIRE_FALSE(admin.isValid(e));
|
REQUIRE_FALSE(admin.isValid(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ TEST_CASE("DebrisSystem: collectOne returns false for an invalid entity", "[debr
|
|||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
DebrisSystem ss(admin);
|
DebrisSystem ss(admin);
|
||||||
|
|
||||||
REQUIRE_FALSE(ss.collectOne(entt::null));
|
REQUIRE_FALSE(collectOne(admin, entt::null));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -149,7 +149,7 @@ TEST_CASE("DebrisSystem: getAllDebrisInfo returns all spawned debris", "[debris]
|
|||||||
ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
|
ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
|
||||||
ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
|
ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
|
||||||
|
|
||||||
const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
|
const std::vector<DebrisInfo> info = getAllDebrisInfo(admin);
|
||||||
REQUIRE(info.size() == 2);
|
REQUIRE(info.size() == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ TEST_CASE("DebrisSystem: getAllDebrisInfo reports each debris entry.s remaining
|
|||||||
const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
|
const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
|
||||||
const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
|
const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
|
||||||
|
|
||||||
const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
|
const std::vector<DebrisInfo> info = getAllDebrisInfo(admin);
|
||||||
REQUIRE(info.size() == 2);
|
REQUIRE(info.size() == 2);
|
||||||
for (const DebrisInfo& i : info)
|
for (const DebrisInfo& i : info)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,11 +5,15 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
|
#include "FactionComponent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "HealthComponent.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "SchematicChoiceOption.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "SimulationTestAccess.h"
|
#include "SimulationTestAccess.h"
|
||||||
#include "StateChecksum.h"
|
#include "StateChecksum.h"
|
||||||
|
#include "StationBodyComponent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "TestConfig.h"
|
#include "TestConfig.h"
|
||||||
|
|
||||||
@@ -17,9 +21,33 @@ namespace
|
|||||||
{
|
{
|
||||||
constexpr int kScriptTicks = 2000;
|
constexpr int kScriptTicks = 2000;
|
||||||
|
|
||||||
|
// Ticks at which the scripted session destroys the enemy stations, and the ticks
|
||||||
|
// on which the resulting schematic choice is taken. A station dying triggers the
|
||||||
|
// choice generation (REQ-DEF-SCHEMATIC-DROP), which lands during that same tick,
|
||||||
|
// so the choice is applied on the tick after.
|
||||||
|
constexpr int kFirstStationKillTick = 800;
|
||||||
|
constexpr int kFirstChoiceTick = kFirstStationKillTick + 1;
|
||||||
|
constexpr int kSecondStationKillTick = 1400;
|
||||||
|
constexpr int kSecondChoiceTick = kSecondStationKillTick + 1;
|
||||||
|
|
||||||
|
// Zeroes the HP of every enemy station, so the next tick processes their death.
|
||||||
|
void killEnemyStations(Simulation& sim)
|
||||||
|
{
|
||||||
|
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||||
|
[](entt::entity, StationBodyComponent&, FactionComponent& faction,
|
||||||
|
HealthComponent& health)
|
||||||
|
{
|
||||||
|
if (faction.isEnemy) { health.hp = 0.0f; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Runs a fixed scripted session and returns the full-state checksum after every
|
// Runs a fixed scripted session and returns the full-state checksum after every
|
||||||
// tick. The script places a small factory, deconstructs part of it mid-run, and
|
// tick. The script places a small factory, deconstructs part of it mid-run, and
|
||||||
// otherwise lets waves/combat run so the RNG stream and ECS state are exercised.
|
// otherwise lets waves/combat run so the RNG stream and ECS state are exercised.
|
||||||
|
// It also destroys the enemy stations twice and takes the offered schematic
|
||||||
|
// choice, so that unlock state (awarded groups, per-schematic levels, and the
|
||||||
|
// implicit recipe/item sets derived from them) is exercised as well and reaches
|
||||||
|
// the checksum via UnlockState::appendChecksum.
|
||||||
std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
|
std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
|
||||||
{
|
{
|
||||||
Simulation sim(loadTestConfig(), seed);
|
Simulation sim(loadTestConfig(), seed);
|
||||||
@@ -40,6 +68,26 @@ std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
|
|||||||
SimulationTestAccess::place(sim, BuildingType::Smelter, QPoint(-3, 3), Rotation::East);
|
SimulationTestAccess::place(sim, BuildingType::Smelter, QPoint(-3, 3), Rotation::East);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (t == kFirstStationKillTick || t == kSecondStationKillTick)
|
||||||
|
{
|
||||||
|
killEnemyStations(sim);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t == kFirstChoiceTick)
|
||||||
|
{
|
||||||
|
// Guarded rather than assumed: if the test config ever stops offering
|
||||||
|
// a group here, this script would silently stop covering unlock state.
|
||||||
|
REQUIRE(sim.hasSchematicChoicesPending());
|
||||||
|
SimulationTestAccess::applySchematicChoice(sim, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The second award is opportunistic — whether a group is still eligible
|
||||||
|
// depends on what the first one granted and on the prerequisite gating.
|
||||||
|
if (t == kSecondChoiceTick && sim.hasSchematicChoicesPending())
|
||||||
|
{
|
||||||
|
SimulationTestAccess::applySchematicChoice(sim, 0);
|
||||||
|
}
|
||||||
|
|
||||||
sim.tick();
|
sim.tick();
|
||||||
checksums.push_back(sim.computeStateChecksum());
|
checksums.push_back(sim.computeStateChecksum());
|
||||||
}
|
}
|
||||||
@@ -152,3 +200,44 @@ TEST_CASE("Simulation: different seeds diverge in state checksum", "[determinism
|
|||||||
// the RNG-driven divergence; a constant checksum would be a broken hash).
|
// the RNG-driven divergence; a constant checksum would be a broken hash).
|
||||||
REQUIRE(a != b);
|
REQUIRE(a != b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Unlock state coverage
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Simulation: unlock state contributes to the state checksum",
|
||||||
|
"[determinism][unlock]")
|
||||||
|
{
|
||||||
|
// Two sessions with identical history up to the schematic choice; only one
|
||||||
|
// takes the choice. This pins down that awarding an unlock group actually
|
||||||
|
// reaches the checksum, which the scripted-session tests above rely on but
|
||||||
|
// cannot show on their own: they would still pass if UnlockState were left
|
||||||
|
// out of the fold entirely.
|
||||||
|
Simulation taken(loadTestConfig(), 12345u);
|
||||||
|
Simulation skipped(loadTestConfig(), 12345u);
|
||||||
|
|
||||||
|
for (int t = 0; t < kFirstStationKillTick; ++t)
|
||||||
|
{
|
||||||
|
taken.tick();
|
||||||
|
skipped.tick();
|
||||||
|
}
|
||||||
|
killEnemyStations(taken);
|
||||||
|
killEnemyStations(skipped);
|
||||||
|
taken.tick();
|
||||||
|
skipped.tick();
|
||||||
|
|
||||||
|
// In lockstep before the choice, so the divergence below has one cause.
|
||||||
|
REQUIRE(taken.computeStateChecksum() == skipped.computeStateChecksum());
|
||||||
|
|
||||||
|
REQUIRE(taken.hasSchematicChoicesPending());
|
||||||
|
|
||||||
|
// An artifact choice bumps m_artifactCount, which is folded separately; the
|
||||||
|
// divergence would then not be attributable to unlock state.
|
||||||
|
REQUIRE_FALSE(taken.getPendingSchematicChoices()[0].isArtifact);
|
||||||
|
|
||||||
|
SimulationTestAccess::applySchematicChoice(taken, 0);
|
||||||
|
|
||||||
|
// The pending-choice list is not itself folded into the checksum, so the
|
||||||
|
// only state that changed is the unlock bookkeeping.
|
||||||
|
REQUIRE(taken.computeStateChecksum() != skipped.computeStateChecksum());
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
@@ -61,8 +62,7 @@ static const BuildingDef* findShipyardDef(const GameConfig& cfg)
|
|||||||
|
|
||||||
static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
|
static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
|
||||||
{
|
{
|
||||||
return SimulationTestAccess::buildings(sim).placeImmediate(
|
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), BuildingType::Shipyard,
|
||||||
BuildingType::Shipyard,
|
|
||||||
yardDef.surfaceMask,
|
yardDef.surfaceMask,
|
||||||
QPoint(0, 0),
|
QPoint(0, 0),
|
||||||
Rotation::East);
|
Rotation::East);
|
||||||
@@ -72,7 +72,7 @@ static void fillMaterials(Simulation& sim, BuildingId yardId,
|
|||||||
const ShipDef& def,
|
const ShipDef& def,
|
||||||
const ShipLayoutConfig& layout)
|
const ShipLayoutConfig& layout)
|
||||||
{
|
{
|
||||||
SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b) {
|
SimulationTestAccess::buildings(sim).forEachBuilding(SimulationTestAccess::state(sim), [&](Building& b) {
|
||||||
if (b.id != yardId)
|
if (b.id != yardId)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -207,7 +207,7 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
|
|||||||
REQUIRE(yardDef != nullptr);
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
|
||||||
|
|
||||||
ShipLayoutConfig layout;
|
ShipLayoutConfig layout;
|
||||||
PlacedModule pm;
|
PlacedModule pm;
|
||||||
@@ -216,9 +216,9 @@ TEST_CASE("Shipyard: setShipLayout reinitializes buffers with module materials",
|
|||||||
pm.rotation = Rotation::East;
|
pm.rotation = Rotation::East;
|
||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
|
||||||
|
|
||||||
const Building* b = sim.getBuildings().findBuilding(yardId);
|
const Building* b = findBuilding(sim.getFactoryState(), yardId);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
// armor_plate needs 2 iron_ingot; interceptor needs 3 iron_ingot + 1 circuit_board
|
// armor_plate needs 2 iron_ingot; interceptor needs 3 iron_ingot + 1 circuit_board
|
||||||
// Total iron_ingot = 5, buffer cap = 2 * 5 = 10
|
// Total iron_ingot = 5, buffer cap = 2 * 5 = 10
|
||||||
@@ -236,14 +236,14 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
|
|||||||
REQUIRE(yardDef != nullptr);
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
|
||||||
|
|
||||||
// Fill materials and tick to start production.
|
// Fill materials and tick to start production.
|
||||||
ShipLayoutConfig emptyLayout;
|
ShipLayoutConfig emptyLayout;
|
||||||
fillMaterials(sim, yardId, *def, emptyLayout);
|
fillMaterials(sim, yardId, *def, emptyLayout);
|
||||||
sim.tick();
|
sim.tick();
|
||||||
|
|
||||||
const Building* b1 = sim.getBuildings().findBuilding(yardId);
|
const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
|
||||||
REQUIRE(b1 != nullptr);
|
REQUIRE(b1 != nullptr);
|
||||||
REQUIRE(b1->production.has_value());
|
REQUIRE(b1->production.has_value());
|
||||||
|
|
||||||
@@ -255,9 +255,9 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
|
|||||||
pm.rotation = Rotation::East;
|
pm.rotation = Rotation::East;
|
||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
|
||||||
|
|
||||||
const Building* b2 = sim.getBuildings().findBuilding(yardId);
|
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
|
||||||
REQUIRE(b2 != nullptr);
|
REQUIRE(b2 != nullptr);
|
||||||
CHECK_FALSE(b2->production.has_value());
|
CHECK_FALSE(b2->production.has_value());
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
|
|||||||
REQUIRE(yardDef != nullptr);
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(yardId, "interceptor");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, "interceptor");
|
||||||
// Deliberately no setShipLayout: recipe set, layout left unconfigured.
|
// Deliberately no setShipLayout: recipe set, layout left unconfigured.
|
||||||
|
|
||||||
// Charge only the base-hull materials (an empty layout adds none).
|
// Charge only the base-hull materials (an empty layout adds none).
|
||||||
@@ -313,7 +313,7 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
|||||||
REQUIRE(yardDef != nullptr);
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
|
||||||
|
|
||||||
ShipLayoutConfig layout;
|
ShipLayoutConfig layout;
|
||||||
PlacedModule pm;
|
PlacedModule pm;
|
||||||
@@ -321,15 +321,15 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
|||||||
pm.position = QPoint(0, 0);
|
pm.position = QPoint(0, 0);
|
||||||
pm.rotation = Rotation::East;
|
pm.rotation = Rotation::East;
|
||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
|
||||||
|
|
||||||
const Building* b1 = sim.getBuildings().findBuilding(yardId);
|
const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
|
||||||
REQUIRE(b1 != nullptr);
|
REQUIRE(b1 != nullptr);
|
||||||
REQUIRE(b1->shipLayout.has_value());
|
REQUIRE(b1->shipLayout.has_value());
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"destroyer");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"destroyer");
|
||||||
|
|
||||||
const Building* b2 = sim.getBuildings().findBuilding(yardId);
|
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
|
||||||
REQUIRE(b2 != nullptr);
|
REQUIRE(b2 != nullptr);
|
||||||
CHECK_FALSE(b2->shipLayout.has_value());
|
CHECK_FALSE(b2->shipLayout.has_value());
|
||||||
}
|
}
|
||||||
@@ -342,7 +342,7 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
|
|||||||
REQUIRE(yardDef != nullptr);
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
|
||||||
|
|
||||||
ShipLayoutConfig layout;
|
ShipLayoutConfig layout;
|
||||||
PlacedModule pm;
|
PlacedModule pm;
|
||||||
@@ -350,16 +350,16 @@ TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
|
|||||||
pm.position = QPoint(0, 0);
|
pm.position = QPoint(0, 0);
|
||||||
pm.rotation = Rotation::East;
|
pm.rotation = Rotation::East;
|
||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), yardId, layout);
|
||||||
|
|
||||||
const Building* b1 = sim.getBuildings().findBuilding(yardId);
|
const Building* b1 = findBuilding(sim.getFactoryState(), yardId);
|
||||||
REQUIRE(b1 != nullptr);
|
REQUIRE(b1 != nullptr);
|
||||||
REQUIRE(b1->shipLayout.has_value());
|
REQUIRE(b1->shipLayout.has_value());
|
||||||
|
|
||||||
// Re-selecting the same recipe must be a no-op and preserve the layout.
|
// Re-selecting the same recipe must be a no-op and preserve the layout.
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId,"interceptor");
|
||||||
|
|
||||||
const Building* b2 = sim.getBuildings().findBuilding(yardId);
|
const Building* b2 = findBuilding(sim.getFactoryState(), yardId);
|
||||||
REQUIRE(b2 != nullptr);
|
REQUIRE(b2 != nullptr);
|
||||||
REQUIRE(b2->shipLayout.has_value());
|
REQUIRE(b2->shipLayout.has_value());
|
||||||
REQUIRE(b2->shipLayout->placedModules.size() == 1);
|
REQUIRE(b2->shipLayout->placedModules.size() == 1);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@@ -57,8 +58,7 @@ static const BuildingDef* findShipyardDef(const GameConfig& cfg)
|
|||||||
|
|
||||||
static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
|
static BuildingId placeShipyard(Simulation& sim, const BuildingDef& yardDef)
|
||||||
{
|
{
|
||||||
return SimulationTestAccess::buildings(sim).placeImmediate(
|
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), BuildingType::Shipyard,
|
||||||
BuildingType::Shipyard,
|
|
||||||
yardDef.surfaceMask,
|
yardDef.surfaceMask,
|
||||||
QPoint(0, 0),
|
QPoint(0, 0),
|
||||||
Rotation::East);
|
Rotation::East);
|
||||||
@@ -74,7 +74,7 @@ static int countShips(Simulation& sim)
|
|||||||
|
|
||||||
static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def)
|
static void fillMaterials(Simulation& sim, BuildingId yardId, const ShipDef& def)
|
||||||
{
|
{
|
||||||
SimulationTestAccess::buildings(sim).forEachBuilding([&](Building& b)
|
SimulationTestAccess::buildings(sim).forEachBuilding(SimulationTestAccess::state(sim), [&](Building& b)
|
||||||
{
|
{
|
||||||
if (b.id != yardId)
|
if (b.id != yardId)
|
||||||
{
|
{
|
||||||
@@ -106,7 +106,7 @@ TEST_CASE("Shipyard: spawns a player ship after production cycle completes",
|
|||||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||||
REQUIRE(yardId != kInvalidBuildingId);
|
REQUIRE(yardId != kInvalidBuildingId);
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
|
||||||
fillMaterials(sim, yardId, *def);
|
fillMaterials(sim, yardId, *def);
|
||||||
|
|
||||||
// First tick: materials consumed, production cycle starts — no ship yet.
|
// First tick: materials consumed, production cycle starts — no ship yet.
|
||||||
@@ -165,7 +165,7 @@ TEST_CASE("Shipyard: does not spawn with insufficient materials", "[shipyard]")
|
|||||||
const int shipsBefore = countShips(sim);
|
const int shipsBefore = countShips(sim);
|
||||||
|
|
||||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
|
||||||
// Materials remain at zero (default after setRecipe); no cycle starts.
|
// Materials remain at zero (default after setRecipe); no cycle starts.
|
||||||
|
|
||||||
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
|
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
|
||||||
@@ -187,7 +187,7 @@ TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipya
|
|||||||
REQUIRE(yardDef != nullptr);
|
REQUIRE(yardDef != nullptr);
|
||||||
|
|
||||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(yardId, def->id);
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), yardId, def->id);
|
||||||
|
|
||||||
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
|
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ TEST_CASE("Shipyard: spawns a second ship after materials replenished", "[shipya
|
|||||||
// Verify the shipyard production field cleared (i.e. the cycle completed
|
// Verify the shipyard production field cleared (i.e. the cycle completed
|
||||||
// and is not still running).
|
// and is not still running).
|
||||||
bool productionCleared = false;
|
bool productionCleared = false;
|
||||||
for (const Building& b : sim.getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(sim.getFactoryState()))
|
||||||
{
|
{
|
||||||
if (b.id == yardId)
|
if (b.id == yardId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
class BeltSystem;
|
class BeltSystem;
|
||||||
@@ -25,6 +26,7 @@ class BuildingSystem;
|
|||||||
struct SimulationTestAccess
|
struct SimulationTestAccess
|
||||||
{
|
{
|
||||||
static BuildingSystem& buildings(Simulation& sim) { return sim.getBuildingsMutable(); }
|
static BuildingSystem& buildings(Simulation& sim) { return sim.getBuildingsMutable(); }
|
||||||
|
static FactoryState& state(Simulation& sim) { return sim.m_factoryState; }
|
||||||
static BeltSystem& belts(Simulation& sim) { return sim.getBeltsMutable(); }
|
static BeltSystem& belts(Simulation& sim) { return sim.getBeltsMutable(); }
|
||||||
|
|
||||||
static std::optional<BuildingId> place(Simulation& sim, BuildingType type,
|
static std::optional<BuildingId> place(Simulation& sim, BuildingType type,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ TEST_CASE("WaveSystem: Simulation pre-places HQ + 2 player + 2 enemy stations",
|
|||||||
|
|
||||||
// HQ is still a Building (for belt integration).
|
// HQ is still a Building (for belt integration).
|
||||||
int hqCount = 0;
|
int hqCount = 0;
|
||||||
for (const Building& b : sim.getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(sim.getFactoryState()))
|
||||||
{
|
{
|
||||||
if (b.type == BuildingType::Hq) { ++hqCount; }
|
if (b.type == BuildingType::Hq) { ++hqCount; }
|
||||||
}
|
}
|
||||||
@@ -143,7 +144,7 @@ TEST_CASE("WaveSystem: HQ anchor is at asteroid right edge", "[wave]")
|
|||||||
{
|
{
|
||||||
const Simulation sim(loadTestConfig(), 42);
|
const Simulation sim(loadTestConfig(), 42);
|
||||||
|
|
||||||
for (const Building& b : sim.getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(sim.getFactoryState()))
|
||||||
{
|
{
|
||||||
if (b.type != BuildingType::Hq) { continue; }
|
if (b.type != BuildingType::Hq) { continue; }
|
||||||
// Rightmost body cell must be at x = -1 (asteroid right edge).
|
// Rightmost body cell must be at x = -1 (asteroid right edge).
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.h
|
||||||
@@ -30,6 +31,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPanel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.cpp
|
||||||
|
|||||||
403
src/ui/FieldSelectionPanel.cpp
Normal file
403
src/ui/FieldSelectionPanel.cpp
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
#include "FieldSelectionPanel.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <QFont>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "DebrisSystem.h"
|
||||||
|
#include "DisplayName.h"
|
||||||
|
#include "EntityAdmin.h"
|
||||||
|
#include "FactionComponent.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "HealthComponent.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
|
#include "SelectedBehaviorComponent.h"
|
||||||
|
#include "ShipIdentityComponent.h"
|
||||||
|
#include "ShipStatsCalculator.h"
|
||||||
|
#include "ShipStatsPanel.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "StationBodyComponent.h"
|
||||||
|
#include "ThreatCostCalculator.h"
|
||||||
|
#include "WeaponComponent.h"
|
||||||
|
|
||||||
|
|
||||||
|
FieldSelectionPanel::FieldSelectionPanel(Simulation* sim,
|
||||||
|
const GameConfig* config,
|
||||||
|
QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, m_sim(sim)
|
||||||
|
, m_config(config)
|
||||||
|
{
|
||||||
|
// Zero margins and the same spacing as the enclosing SelectedBuildingPanel layout, so
|
||||||
|
// nesting the field widgets in this panel leaves their geometry unchanged.
|
||||||
|
m_layout = new QVBoxLayout(this);
|
||||||
|
m_layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
m_layout->setSpacing(4);
|
||||||
|
m_layout->setAlignment(Qt::AlignTop);
|
||||||
|
|
||||||
|
m_entityTitleLabel = new QLabel(this);
|
||||||
|
QFont titleFont = m_entityTitleLabel->font();
|
||||||
|
titleFont.setBold(true);
|
||||||
|
m_entityTitleLabel->setFont(titleFont);
|
||||||
|
m_layout->addWidget(m_entityTitleLabel);
|
||||||
|
m_entityTitleLabel->hide();
|
||||||
|
|
||||||
|
m_entityStatsPanel = new ShipStatsPanel(config, this);
|
||||||
|
m_layout->addWidget(m_entityStatsPanel);
|
||||||
|
m_entityStatsPanel->hide();
|
||||||
|
|
||||||
|
m_stationStatsLabel = new QLabel(this);
|
||||||
|
m_stationStatsLabel->setWordWrap(true);
|
||||||
|
m_layout->addWidget(m_stationStatsLabel);
|
||||||
|
m_stationStatsLabel->hide();
|
||||||
|
|
||||||
|
m_entitySummaryLabel = new QLabel(this);
|
||||||
|
m_entitySummaryLabel->setWordWrap(true);
|
||||||
|
m_layout->addWidget(m_entitySummaryLabel);
|
||||||
|
m_entitySummaryLabel->hide();
|
||||||
|
|
||||||
|
m_scrapLabel = new QLabel(this);
|
||||||
|
m_layout->addWidget(m_scrapLabel);
|
||||||
|
m_scrapLabel->hide();
|
||||||
|
|
||||||
|
hide();
|
||||||
|
|
||||||
|
registerForEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldSelectionPanel::~FieldSelectionPanel()
|
||||||
|
{
|
||||||
|
unregisterForEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::setSelectedEntities(const std::vector<entt::entity>& entities)
|
||||||
|
{
|
||||||
|
m_selectedEntities = entities;
|
||||||
|
rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::setSelectedDebris(const std::vector<entt::entity>& debris)
|
||||||
|
{
|
||||||
|
m_selectedDebris = debris;
|
||||||
|
rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::clearSelection()
|
||||||
|
{
|
||||||
|
m_selectedEntities.clear();
|
||||||
|
m_selectedDebris.clear();
|
||||||
|
rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FieldSelectionPanel::hasSelection() const
|
||||||
|
{
|
||||||
|
return !m_selectedEntities.empty() || !m_selectedDebris.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::hideAllWidgets()
|
||||||
|
{
|
||||||
|
m_entityTitleLabel->hide();
|
||||||
|
m_entityStatsPanel->hide();
|
||||||
|
m_stationStatsLabel->hide();
|
||||||
|
m_entitySummaryLabel->hide();
|
||||||
|
m_scrapLabel->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::rebuild()
|
||||||
|
{
|
||||||
|
if (!hasSelection())
|
||||||
|
{
|
||||||
|
// Nothing in the field category: take no space, leaving the panel to whatever
|
||||||
|
// the building category shows (REQ-UI-SELECTION-CATEGORIES).
|
||||||
|
hideAllWidgets();
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
show();
|
||||||
|
|
||||||
|
EntityAdmin& admin = m_sim->getAdmin();
|
||||||
|
|
||||||
|
// A full single-object stats panel is shown only for a lone field object: one actor
|
||||||
|
// with no debris, or one piece of debris with no actors. As soon as the selection holds
|
||||||
|
// more than one object (multiple actors, multiple debris, or actors plus debris), the
|
||||||
|
// panel switches to the compact count summary (REQ-UI-FIELD-MULTI-SELECTION).
|
||||||
|
if (m_selectedEntities.size() == 1 && m_selectedDebris.empty())
|
||||||
|
{
|
||||||
|
m_entitySummaryLabel->hide();
|
||||||
|
m_scrapLabel->hide();
|
||||||
|
const entt::entity entity = m_selectedEntities.front();
|
||||||
|
if (admin.isValid(entity) && admin.hasAll<ShipIdentityComponent>(entity))
|
||||||
|
{
|
||||||
|
buildEntityShip(entity);
|
||||||
|
}
|
||||||
|
else if (admin.isValid(entity) && admin.hasAll<StationBodyComponent>(entity))
|
||||||
|
{
|
||||||
|
buildEntityStation(entity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_entityTitleLabel->hide();
|
||||||
|
m_entityStatsPanel->hide();
|
||||||
|
m_stationStatsLabel->hide();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_selectedEntities.empty() && m_selectedDebris.size() == 1)
|
||||||
|
{
|
||||||
|
// Single piece of debris: a "Debris" heading plus a "Scrap" stat row, styled like
|
||||||
|
// the ship/station stats panels (REQ-UI-DEBRIS-PANEL).
|
||||||
|
m_entitySummaryLabel->hide();
|
||||||
|
m_entityStatsPanel->hide();
|
||||||
|
m_stationStatsLabel->hide();
|
||||||
|
buildDebrisSingle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// More than one field object: a compact count summary. buildEntitySummary() appends the
|
||||||
|
// "Debris x N" and "Scrap x N" lines when debris is part of the selection.
|
||||||
|
m_entityTitleLabel->hide();
|
||||||
|
m_entityStatsPanel->hide();
|
||||||
|
m_stationStatsLabel->hide();
|
||||||
|
m_scrapLabel->hide();
|
||||||
|
buildEntitySummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::refreshDisplay()
|
||||||
|
{
|
||||||
|
if (!hasSelection()) { return; }
|
||||||
|
|
||||||
|
// Keep the live values current: the single-actor stats panel, the single-debris stats
|
||||||
|
// panel (whose Scrap row shrinks as it is collected), or the count summary (whose Scrap
|
||||||
|
// line shrinks likewise) — matching the layout chosen by rebuild()
|
||||||
|
// (REQ-UI-SHIP-STATS-PANEL, REQ-UI-DEBRIS-PANEL).
|
||||||
|
if (m_selectedEntities.size() == 1 && m_selectedDebris.empty())
|
||||||
|
{
|
||||||
|
refreshEntityStats();
|
||||||
|
}
|
||||||
|
else if (m_selectedEntities.empty() && m_selectedDebris.size() == 1)
|
||||||
|
{
|
||||||
|
buildDebrisSingle();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
buildEntitySummary();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::buildDebrisSingle()
|
||||||
|
{
|
||||||
|
// "Debris" heading + a single "Scrap" stat row for the piece's remaining amount,
|
||||||
|
// mirroring the single-actor stats panels (REQ-UI-DEBRIS-PANEL).
|
||||||
|
m_entityTitleLabel->setText(tr("Debris"));
|
||||||
|
m_entityTitleLabel->show();
|
||||||
|
m_scrapLabel->setText(tr("Scrap: %1").arg(selectedDebrisScrapTotal()));
|
||||||
|
m_scrapLabel->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::buildEntitySummary()
|
||||||
|
{
|
||||||
|
EntityAdmin& admin = m_sim->getAdmin();
|
||||||
|
|
||||||
|
// Group actors by faction + kind + ship schematic, preserving first-seen order
|
||||||
|
// (REQ-UI-FIELD-MULTI-SELECTION).
|
||||||
|
std::vector<QString> keys;
|
||||||
|
std::map<QString, int> counts;
|
||||||
|
std::map<QString, QString> labels;
|
||||||
|
|
||||||
|
for (entt::entity entity : m_selectedEntities)
|
||||||
|
{
|
||||||
|
if (!admin.isValid(entity)) { continue; }
|
||||||
|
const bool isEnemy = admin.hasAll<FactionComponent>(entity)
|
||||||
|
&& admin.get<FactionComponent>(entity).isEnemy;
|
||||||
|
|
||||||
|
QString key;
|
||||||
|
QString label;
|
||||||
|
if (admin.hasAll<ShipIdentityComponent>(entity))
|
||||||
|
{
|
||||||
|
const std::string& id = admin.get<ShipIdentityComponent>(entity).schematicId;
|
||||||
|
const QString name = QString::fromStdString(toDisplayName(id));
|
||||||
|
key = (isEnemy ? QStringLiteral("ship:enemy:") : QStringLiteral("ship:player:"))
|
||||||
|
+ QString::fromStdString(id);
|
||||||
|
label = isEnemy ? tr("Enemy %1").arg(name) : name;
|
||||||
|
}
|
||||||
|
else if (admin.hasAll<StationBodyComponent>(entity))
|
||||||
|
{
|
||||||
|
key = isEnemy ? QStringLiteral("station:enemy") : QStringLiteral("station:player");
|
||||||
|
label = isEnemy ? tr("Enemy Defence Station") : tr("Player Defence Station");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (counts.find(key) == counts.end())
|
||||||
|
{
|
||||||
|
keys.push_back(key);
|
||||||
|
labels[key] = label;
|
||||||
|
}
|
||||||
|
counts[key] += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One "<type> x <count>" line per group (matching the recipe tooltip and the building
|
||||||
|
// multi-selection). No total-count header, consistent with the building panel. When
|
||||||
|
// debris is part of the selection, a "Debris x <count>" line followed by a
|
||||||
|
// "Scrap x <total>" line are appended into the same label so the line spacing is
|
||||||
|
// uniform (REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-DEBRIS-PANEL).
|
||||||
|
QStringList lines;
|
||||||
|
for (const QString& key : keys)
|
||||||
|
{
|
||||||
|
lines << tr("%1 x %2").arg(labels[key]).arg(counts[key]);
|
||||||
|
}
|
||||||
|
if (!m_selectedDebris.empty())
|
||||||
|
{
|
||||||
|
lines << tr("Debris x %1").arg(static_cast<int>(m_selectedDebris.size()));
|
||||||
|
lines << scrapTotalText();
|
||||||
|
}
|
||||||
|
m_entitySummaryLabel->setText(lines.join('\n'));
|
||||||
|
m_entitySummaryLabel->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::buildEntityShip(entt::entity entity)
|
||||||
|
{
|
||||||
|
EntityAdmin& admin = m_sim->getAdmin();
|
||||||
|
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
|
||||||
|
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
||||||
|
|
||||||
|
m_entityTitleLabel->setText(tr("Ship: %1")
|
||||||
|
.arg(QString::fromStdString(identity.schematicId)));
|
||||||
|
m_entityTitleLabel->show();
|
||||||
|
|
||||||
|
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
||||||
|
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
||||||
|
m_entityStatsPanel->setBehavior(
|
||||||
|
admin.get<SelectedBehaviorComponent>(entity).winner);
|
||||||
|
m_entityStatsPanel->setDebugDrawEnabled(m_debugDraw);
|
||||||
|
|
||||||
|
const ShipDef* schematicDef =
|
||||||
|
m_config->ships.findShipDef(identity.schematicId);
|
||||||
|
if (schematicDef)
|
||||||
|
{
|
||||||
|
const double threat = calculateShipThreatCost(
|
||||||
|
m_config->threatCosts, *m_config, schematicDef->id,
|
||||||
|
schematicDef->defaultModules);
|
||||||
|
m_entityStatsPanel->setThreatCost(threat);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_entityStatsPanel->show();
|
||||||
|
|
||||||
|
m_stationStatsLabel->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::buildEntityStation(entt::entity entity)
|
||||||
|
{
|
||||||
|
EntityAdmin& admin = m_sim->getAdmin();
|
||||||
|
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
||||||
|
|
||||||
|
const bool isEnemy = admin.hasAll<FactionComponent>(entity)
|
||||||
|
&& admin.get<FactionComponent>(entity).isEnemy;
|
||||||
|
m_entityTitleLabel->setText(isEnemy
|
||||||
|
? tr("Enemy Defence Station")
|
||||||
|
: tr("Player Defence Station"));
|
||||||
|
m_entityTitleLabel->show();
|
||||||
|
|
||||||
|
float totalDps = 0.0f;
|
||||||
|
float maxRange = 0.0f;
|
||||||
|
bool hasWeapons = false;
|
||||||
|
|
||||||
|
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
|
||||||
|
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const WeaponComponent& w)
|
||||||
|
{
|
||||||
|
if (owner.owner != entity) { return; }
|
||||||
|
hasWeapons = true;
|
||||||
|
totalDps += w.damage * w.fireRateHz;
|
||||||
|
if (w.range_tiles > maxRange) { maxRange = w.range_tiles; }
|
||||||
|
});
|
||||||
|
|
||||||
|
QString statsText = tr("HP: %1 / %2")
|
||||||
|
.arg(static_cast<int>(health.hp + 0.5f))
|
||||||
|
.arg(static_cast<int>(health.maxHp + 0.5f));
|
||||||
|
|
||||||
|
if (hasWeapons)
|
||||||
|
{
|
||||||
|
statsText += tr("\nDPS: %1").arg(QString::number(static_cast<double>(totalDps), 'f', 1));
|
||||||
|
statsText += tr("\nRange: %1 tiles").arg(QString::number(static_cast<double>(maxRange), 'f', 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_stationStatsLabel->setText(statsText);
|
||||||
|
m_stationStatsLabel->show();
|
||||||
|
|
||||||
|
m_entityStatsPanel->hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::refreshEntityStats()
|
||||||
|
{
|
||||||
|
// Only the single-actor stats panel needs a live refresh; the multi-actor summary is
|
||||||
|
// static counts, and GameWorldView prunes dead/despawned actors and re-emits the
|
||||||
|
// selection (REQ-UI-ENTITY-CLICK-SELECT), so the panel does not mutate it here.
|
||||||
|
if (m_selectedEntities.size() != 1) { return; }
|
||||||
|
|
||||||
|
EntityAdmin& admin = m_sim->getAdmin();
|
||||||
|
const entt::entity entity = m_selectedEntities.front();
|
||||||
|
|
||||||
|
if (!admin.isValid(entity) || !admin.hasAll<HealthComponent>(entity)) { return; }
|
||||||
|
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
||||||
|
if (health.hp <= 0.0f) { return; }
|
||||||
|
|
||||||
|
if (admin.hasAll<ShipIdentityComponent>(entity))
|
||||||
|
{
|
||||||
|
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
||||||
|
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
||||||
|
m_entityStatsPanel->setBehavior(
|
||||||
|
admin.get<SelectedBehaviorComponent>(entity).winner);
|
||||||
|
}
|
||||||
|
else if (admin.hasAll<StationBodyComponent>(entity))
|
||||||
|
{
|
||||||
|
buildEntityStation(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int FieldSelectionPanel::selectedDebrisScrapTotal() const
|
||||||
|
{
|
||||||
|
// Sum the remaining scrap across the still-living selected debris (REQ-UI-DEBRIS-PANEL).
|
||||||
|
int total = 0;
|
||||||
|
for (const DebrisInfo& info : getAllDebrisInfo(m_sim->getAdmin()))
|
||||||
|
{
|
||||||
|
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
|
||||||
|
!= m_selectedDebris.end())
|
||||||
|
{
|
||||||
|
total += info.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FieldSelectionPanel::scrapTotalText() const
|
||||||
|
{
|
||||||
|
return tr("Scrap x %1").arg(selectedDebrisScrapTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::handleEvent(std::shared_ptr<const TickAdvancedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
refreshDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::handleEvent(
|
||||||
|
std::shared_ptr<const PlayerCommandsAppliedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
// Player commands are applied by a queued drain, not synchronously. When the game is
|
||||||
|
// paused no tick advances, so TickAdvancedEvent never fires; refresh here too.
|
||||||
|
refreshDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FieldSelectionPanel::handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event)
|
||||||
|
{
|
||||||
|
m_debugDraw = event->active;
|
||||||
|
m_entityStatsPanel->setDebugDrawEnabled(event->active);
|
||||||
|
}
|
||||||
91
src/ui/FieldSelectionPanel.h
Normal file
91
src/ui/FieldSelectionPanel.h
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
#include "DebugDrawToggledEvent.h"
|
||||||
|
#include "EventHandler.h"
|
||||||
|
#include "PlayerCommandsAppliedEvent.h"
|
||||||
|
#include "TickAdvancedEvent.h"
|
||||||
|
|
||||||
|
struct GameConfig;
|
||||||
|
class Simulation;
|
||||||
|
class ShipStatsPanel;
|
||||||
|
class QLabel;
|
||||||
|
class QVBoxLayout;
|
||||||
|
|
||||||
|
// Renders the "field" selection category — ships, defence stations and debris — as either
|
||||||
|
// a single-object stats panel (ship, station, or debris) or a compact multi-object count
|
||||||
|
// summary (REQ-UI-SELECTION-CATEGORIES, REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-DEBRIS-PANEL).
|
||||||
|
//
|
||||||
|
// The panel owns its own selection state and its own widgets, and nothing else. Which of
|
||||||
|
// the two selection categories owns the side panel is arbitrated by the parent
|
||||||
|
// SelectedBuildingPanel: it feeds this panel through setSelectedEntities() /
|
||||||
|
// setSelectedDebris() / clearSelection() and asks it via hasSelection(). This panel hides
|
||||||
|
// itself whenever its selection is empty, so an inactive field category takes no space.
|
||||||
|
class FieldSelectionPanel : public QWidget,
|
||||||
|
public CombinedEventHandler<TickAdvancedEvent,
|
||||||
|
PlayerCommandsAppliedEvent,
|
||||||
|
DebugDrawToggledEvent>
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
FieldSelectionPanel(Simulation* sim, const GameConfig* config,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
~FieldSelectionPanel() override;
|
||||||
|
|
||||||
|
// Replaces the selected actors (ships and defence stations); debris is left alone,
|
||||||
|
// the two coexist within the field category (REQ-UI-SELECTION-CATEGORIES).
|
||||||
|
void setSelectedEntities(const std::vector<entt::entity>& entities);
|
||||||
|
// Replaces the selected debris; the selected actors are left alone.
|
||||||
|
void setSelectedDebris(const std::vector<entt::entity>& debris);
|
||||||
|
// Drops the whole field selection — used when the building category takes over.
|
||||||
|
void clearSelection();
|
||||||
|
// True while the field category has anything selected, i.e. while this panel owns
|
||||||
|
// the side panel's content.
|
||||||
|
bool hasSelection() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
||||||
|
|
||||||
|
// Picks the layout for the current selection and shows/hides this panel accordingly.
|
||||||
|
void rebuild();
|
||||||
|
// Keeps the live values of the layout chosen by rebuild() current.
|
||||||
|
void refreshDisplay();
|
||||||
|
void buildEntityShip(entt::entity entity);
|
||||||
|
void buildEntityStation(entt::entity entity);
|
||||||
|
void buildEntitySummary();
|
||||||
|
void buildDebrisSingle();
|
||||||
|
void refreshEntityStats();
|
||||||
|
void hideAllWidgets();
|
||||||
|
// Summed remaining scrap across the selected debris (REQ-UI-DEBRIS-PANEL).
|
||||||
|
int selectedDebrisScrapTotal() const;
|
||||||
|
// "Scrap x N" line for the multi-object summary (REQ-UI-FIELD-MULTI-SELECTION).
|
||||||
|
QString scrapTotalText() const;
|
||||||
|
|
||||||
|
Simulation* m_sim;
|
||||||
|
const GameConfig* m_config;
|
||||||
|
|
||||||
|
bool m_debugDraw = false;
|
||||||
|
|
||||||
|
// The selected ships/defence stations. Shares the "field" selection category with
|
||||||
|
// debris (m_selectedDebris): both can be non-empty at once (REQ-UI-SELECTION-CATEGORIES).
|
||||||
|
std::vector<entt::entity> m_selectedEntities;
|
||||||
|
std::vector<entt::entity> m_selectedDebris;
|
||||||
|
|
||||||
|
QVBoxLayout* m_layout;
|
||||||
|
QLabel* m_entityTitleLabel;
|
||||||
|
ShipStatsPanel* m_entityStatsPanel;
|
||||||
|
QLabel* m_stationStatsLabel;
|
||||||
|
QLabel* m_entitySummaryLabel;
|
||||||
|
// Shows the debris "Scrap" stat row (single selection) — the scrap total for the
|
||||||
|
// multi-object summary lives in m_entitySummaryLabel instead.
|
||||||
|
QLabel* m_scrapLabel;
|
||||||
|
};
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
#include "GameWorldView.h"
|
#include "GameWorldView.h"
|
||||||
|
#include "PlacementRules.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "ProductionRules.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -50,6 +53,7 @@
|
|||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "HqProxyComponent.h"
|
#include "HqProxyComponent.h"
|
||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
|
#include "PortGeometry.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "RepairBehavior.h"
|
#include "RepairBehavior.h"
|
||||||
#include "SalvageScrapBehavior.h"
|
#include "SalvageScrapBehavior.h"
|
||||||
@@ -166,18 +170,6 @@ Rotation rotateCounterClockwise(Rotation r)
|
|||||||
return Rotation::East;
|
return Rotation::East;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPoint portBodyTile(QPoint portTile, Rotation direction)
|
|
||||||
{
|
|
||||||
switch (direction)
|
|
||||||
{
|
|
||||||
case Rotation::East: return portTile + QPoint(-1, 0);
|
|
||||||
case Rotation::West: return portTile + QPoint( 1, 0);
|
|
||||||
case Rotation::North: return portTile + QPoint( 0, 1);
|
|
||||||
case Rotation::South: return portTile + QPoint( 0, -1);
|
|
||||||
}
|
|
||||||
return portTile;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill color for a building's status light per its production state
|
// Fill color for a building's status light per its production state
|
||||||
// (REQ-UI-STATUS-LIGHT).
|
// (REQ-UI-STATUS-LIGHT).
|
||||||
QColor statusLightFill(ProductionStatus status, const StatusLightVisuals& sl)
|
QColor statusLightFill(ProductionStatus status, const StatusLightVisuals& sl)
|
||||||
@@ -588,7 +580,7 @@ QRect GameWorldView::getViewportRect() const
|
|||||||
float GameWorldView::getAsteroidLeftEdge() const
|
float GameWorldView::getAsteroidLeftEdge() const
|
||||||
{
|
{
|
||||||
float leftX = -static_cast<float>(m_sim->getCurrentAsteroidWidth_tiles());
|
float leftX = -static_cast<float>(m_sim->getCurrentAsteroidWidth_tiles());
|
||||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
for (const QPoint& cell : b.bodyCells)
|
for (const QPoint& cell : b.bodyCells)
|
||||||
{
|
{
|
||||||
@@ -671,7 +663,7 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
|
|||||||
// Terrain and world-bounds validity are owned by the simulation
|
// Terrain and world-bounds validity are owned by the simulation
|
||||||
// (REQ-BLD-PLACE-VALID); the presentation layer only adds the occupancy /
|
// (REQ-BLD-PLACE-VALID); the presentation layer only adds the occupancy /
|
||||||
// rotate-in-place check.
|
// rotate-in-place check.
|
||||||
if (!m_sim->getBuildings().isPlacementValid(type, anchor, rot))
|
if (!isPlacementValid(m_sim->getFactoryState(), m_sim->getConfig(), type, anchor, rot))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -683,7 +675,7 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
|
|||||||
bool anyOccupied = false;
|
bool anyOccupied = false;
|
||||||
for (const QPoint& relCell : parsed.bodyCells)
|
for (const QPoint& relCell : parsed.bodyCells)
|
||||||
{
|
{
|
||||||
if (m_sim->getBuildings().isTileOccupied(anchor + relCell))
|
if (isTileOccupied(m_sim->getFactoryState(), anchor + relCell))
|
||||||
{
|
{
|
||||||
anyOccupied = true;
|
anyOccupied = true;
|
||||||
break;
|
break;
|
||||||
@@ -692,14 +684,14 @@ bool GameWorldView::isValidPlacement(BuildingType type, QPoint anchor,
|
|||||||
|
|
||||||
if (anyOccupied)
|
if (anyOccupied)
|
||||||
{
|
{
|
||||||
return m_sim->getBuildings().findRotateInPlaceTarget(type, anchor, rot).has_value();
|
return findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), type, anchor, rot).has_value();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<BuildingId> GameWorldView::buildingAtTile(QPoint tile) const
|
std::optional<BuildingId> GameWorldView::buildingAtTile(QPoint tile) const
|
||||||
{
|
{
|
||||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
for (const QPoint& cell : b.bodyCells)
|
for (const QPoint& cell : b.bodyCells)
|
||||||
{
|
{
|
||||||
@@ -714,7 +706,7 @@ std::optional<BuildingId> GameWorldView::buildingAtTile(QPoint tile) const
|
|||||||
|
|
||||||
std::optional<BuildingId> GameWorldView::siteAtTile(QPoint tile) const
|
std::optional<BuildingId> GameWorldView::siteAtTile(QPoint tile) const
|
||||||
{
|
{
|
||||||
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
|
for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
for (const QPoint& cell : s.bodyCells)
|
for (const QPoint& cell : s.bodyCells)
|
||||||
{
|
{
|
||||||
@@ -736,7 +728,7 @@ std::vector<BuildingId> GameWorldView::buildingsInBox(QPoint cornerA, QPoint cor
|
|||||||
const int y1 = std::max(cornerA.y(), cornerB.y());
|
const int y1 = std::max(cornerA.y(), cornerB.y());
|
||||||
|
|
||||||
std::vector<BuildingId> ids;
|
std::vector<BuildingId> ids;
|
||||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
for (const QPoint& cell : b.bodyCells)
|
for (const QPoint& cell : b.bodyCells)
|
||||||
{
|
{
|
||||||
@@ -748,7 +740,7 @@ std::vector<BuildingId> GameWorldView::buildingsInBox(QPoint cornerA, QPoint cor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
|
for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
for (const QPoint& cell : s.bodyCells)
|
for (const QPoint& cell : s.bodyCells)
|
||||||
{
|
{
|
||||||
@@ -785,7 +777,7 @@ void GameWorldView::pruneDespawnedDebris()
|
|||||||
if (m_selectedDebris.empty()) { return; }
|
if (m_selectedDebris.empty()) { return; }
|
||||||
|
|
||||||
std::vector<entt::entity> live;
|
std::vector<entt::entity> live;
|
||||||
for (const DebrisInfo& info : m_sim->getDebrisSystem().getAllDebrisInfo())
|
for (const DebrisInfo& info : getAllDebrisInfo(m_sim->getAdmin()))
|
||||||
{
|
{
|
||||||
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
|
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
|
||||||
!= m_selectedDebris.end())
|
!= m_selectedDebris.end())
|
||||||
@@ -874,8 +866,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
|||||||
for (const BlueprintBuilding& bb : bp.buildings)
|
for (const BlueprintBuilding& bb : bp.buildings)
|
||||||
{
|
{
|
||||||
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
||||||
if (m_sim->getBuildings().findRotateInPlaceTarget(
|
if (findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), bb.type, center + bb.offset, bb.rotation).has_value())
|
||||||
bb.type, center + bb.offset, bb.rotation).has_value())
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -889,7 +880,7 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
|||||||
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
if (!m_sim->isBuildingUnlocked(bb.type)) { continue; }
|
||||||
const QPoint anchor = center + bb.offset;
|
const QPoint anchor = center + bb.offset;
|
||||||
const std::optional<BuildingId> rotateTarget =
|
const std::optional<BuildingId> rotateTarget =
|
||||||
m_sim->getBuildings().findRotateInPlaceTarget(bb.type, anchor, bb.rotation);
|
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), bb.type, anchor, bb.rotation);
|
||||||
if (rotateTarget.has_value())
|
if (rotateTarget.has_value())
|
||||||
{
|
{
|
||||||
std::shared_ptr<RotateInPlaceCommand> rotateCommand =
|
std::shared_ptr<RotateInPlaceCommand> rotateCommand =
|
||||||
@@ -962,14 +953,14 @@ TunnelTileMap GameWorldView::collectTunnelTiles() const
|
|||||||
// single-cell tile, so a just-placed tunnel (not yet constructed) is matchable
|
// single-cell tile, so a just-placed tunnel (not yet constructed) is matchable
|
||||||
// (REQ-BLD-TUNNEL-MODE, REQ-BLD-TUNNEL-SELECT-HIGHLIGHT).
|
// (REQ-BLD-TUNNEL-MODE, REQ-BLD-TUNNEL-SELECT-HIGHLIGHT).
|
||||||
TunnelTileMap tunnels;
|
TunnelTileMap tunnels;
|
||||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
if (b.type == BuildingType::TunnelEntry || b.type == BuildingType::TunnelExit)
|
if (b.type == BuildingType::TunnelEntry || b.type == BuildingType::TunnelExit)
|
||||||
{
|
{
|
||||||
tunnels[b.anchor] = TunnelTileInfo{b.type, b.rotation};
|
tunnels[b.anchor] = TunnelTileInfo{b.type, b.rotation};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
|
for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
if (s.type == BuildingType::TunnelEntry || s.type == BuildingType::TunnelExit)
|
if (s.type == BuildingType::TunnelEntry || s.type == BuildingType::TunnelExit)
|
||||||
{
|
{
|
||||||
@@ -1025,7 +1016,7 @@ void GameWorldView::placeAtTile(QPoint tile)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::optional<BuildingId> rotateTarget =
|
const std::optional<BuildingId> rotateTarget =
|
||||||
m_sim->getBuildings().findRotateInPlaceTarget(type, tile, m_ghostRotation);
|
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), type, tile, m_ghostRotation);
|
||||||
if (rotateTarget.has_value())
|
if (rotateTarget.has_value())
|
||||||
{
|
{
|
||||||
std::shared_ptr<RotateInPlaceCommand> command =
|
std::shared_ptr<RotateInPlaceCommand> command =
|
||||||
@@ -1045,7 +1036,7 @@ void GameWorldView::placeAtTile(QPoint tile)
|
|||||||
|| type == BuildingType::TunnelEntry
|
|| type == BuildingType::TunnelEntry
|
||||||
|| type == BuildingType::TunnelExit)
|
|| type == BuildingType::TunnelExit)
|
||||||
{
|
{
|
||||||
if (!m_sim->getBuildings().isTileOccupied(tile) && canAfford(type))
|
if (!isTileOccupied(m_sim->getFactoryState(), tile) && canAfford(type))
|
||||||
{
|
{
|
||||||
enqueuePlaceBuilding(type, tile, m_ghostRotation);
|
enqueuePlaceBuilding(type, tile, m_ghostRotation);
|
||||||
}
|
}
|
||||||
@@ -1072,7 +1063,7 @@ void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
|
|||||||
std::optional<BuildingType> targetType;
|
std::optional<BuildingType> targetType;
|
||||||
if (targetId.has_value())
|
if (targetId.has_value())
|
||||||
{
|
{
|
||||||
if (const Building* building = m_sim->getBuildings().findBuilding(*targetId))
|
if (const Building* building = findBuilding(m_sim->getFactoryState(), *targetId))
|
||||||
{
|
{
|
||||||
targetType = building->type;
|
targetType = building->type;
|
||||||
}
|
}
|
||||||
@@ -1080,7 +1071,7 @@ void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
|
|||||||
else if (std::optional<BuildingId> siteId = siteAtTile(cursorTile); siteId.has_value())
|
else if (std::optional<BuildingId> siteId = siteAtTile(cursorTile); siteId.has_value())
|
||||||
{
|
{
|
||||||
targetId = siteId;
|
targetId = siteId;
|
||||||
if (const ConstructionSite* site = m_sim->getBuildings().findSite(*siteId))
|
if (const ConstructionSite* site = findSite(m_sim->getFactoryState(), *siteId))
|
||||||
{
|
{
|
||||||
targetType = site->type;
|
targetType = site->type;
|
||||||
}
|
}
|
||||||
@@ -1089,7 +1080,7 @@ void GameWorldView::recomputeBeltDragPath(QPoint cursorTile)
|
|||||||
if (targetId.has_value() && targetType.has_value()
|
if (targetId.has_value() && targetType.has_value()
|
||||||
&& *targetType != BuildingType::Belt)
|
&& *targetType != BuildingType::Belt)
|
||||||
{
|
{
|
||||||
const std::vector<Port> inputPorts = m_sim->getBuildings().getInputPorts(*targetId);
|
const std::vector<Port> inputPorts = getInputPorts(m_sim->getFactoryState(), m_sim->getConfig(), *targetId);
|
||||||
std::optional<Port> best;
|
std::optional<Port> best;
|
||||||
float bestDistanceSq = 0.0f;
|
float bestDistanceSq = 0.0f;
|
||||||
for (const Port& port : inputPorts)
|
for (const Port& port : inputPorts)
|
||||||
@@ -1133,8 +1124,7 @@ std::vector<GameWorldView::BeltDragResolved> GameWorldView::resolveBeltDragPath(
|
|||||||
{
|
{
|
||||||
BeltDragResolved item;
|
BeltDragResolved item;
|
||||||
const std::optional<BuildingId> rotateTarget =
|
const std::optional<BuildingId> rotateTarget =
|
||||||
m_sim->getBuildings().findRotateInPlaceTarget(
|
findRotateInPlaceTarget(m_sim->getFactoryState(), m_sim->getConfig(), BuildingType::Belt, entry.tile, entry.rotation);
|
||||||
BuildingType::Belt, entry.tile, entry.rotation);
|
|
||||||
if (rotateTarget.has_value())
|
if (rotateTarget.has_value())
|
||||||
{
|
{
|
||||||
// A tile holding only a belt (or belt site) is re-oriented, no cost.
|
// A tile holding only a belt (or belt site) is re-oriented, no cost.
|
||||||
@@ -1309,7 +1299,7 @@ bool GameWorldView::drawBuildingIcon(QPainter& painter, BuildingType type,
|
|||||||
|
|
||||||
void GameWorldView::drawBuildings(QPainter& painter)
|
void GameWorldView::drawBuildings(QPainter& painter)
|
||||||
{
|
{
|
||||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
||||||
m_visuals->buildings.find(b.type);
|
m_visuals->buildings.find(b.type);
|
||||||
@@ -1341,7 +1331,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
|||||||
|
|
||||||
for (const Port& port : b.outputPorts)
|
for (const Port& port : b.outputPorts)
|
||||||
{
|
{
|
||||||
drawPortGlyph(painter, portBodyTile(port.tile, port.direction),
|
drawPortGlyph(painter, outputBodyTile(port.tile, port.direction),
|
||||||
port.direction, bv.outline, /*centered*/ false);
|
port.direction, bv.outline, /*centered*/ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1351,7 +1341,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
|||||||
// building footprints are rectangular, so a corner of the axis-aligned
|
// building footprints are rectangular, so a corner of the axis-aligned
|
||||||
// bounding box is the true corner.
|
// bounding box is the true corner.
|
||||||
if (const std::optional<ProductionStatus> status =
|
if (const std::optional<ProductionStatus> status =
|
||||||
m_sim->getBuildings().getProductionStatus(b))
|
getProductionStatus(m_sim->getConfig(), b))
|
||||||
{
|
{
|
||||||
const float px = getTilePx();
|
const float px = getTilePx();
|
||||||
const float r = px * 0.18f;
|
const float r = px * 0.18f;
|
||||||
@@ -1376,7 +1366,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
|||||||
}
|
}
|
||||||
|
|
||||||
painter.setOpacity(0.5);
|
painter.setOpacity(0.5);
|
||||||
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
|
for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
||||||
m_visuals->buildings.find(s.type);
|
m_visuals->buildings.find(s.type);
|
||||||
@@ -1441,7 +1431,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
|||||||
for (const Port& port : siteMask.outputPorts)
|
for (const Port& port : siteMask.outputPorts)
|
||||||
{
|
{
|
||||||
const QPoint absBody = s.anchor
|
const QPoint absBody = s.anchor
|
||||||
+ portBodyTile(port.tile, port.direction);
|
+ outputBodyTile(port.tile, port.direction);
|
||||||
drawPortGlyph(painter, absBody, port.direction, bv.outline,
|
drawPortGlyph(painter, absBody, port.direction, bv.outline,
|
||||||
/*centered*/ false);
|
/*centered*/ false);
|
||||||
}
|
}
|
||||||
@@ -1453,7 +1443,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
|||||||
// after every building and construction site fill so a belt (or other tile)
|
// after every building and construction site fill so a belt (or other tile)
|
||||||
// placed directly below the HQ cannot overpaint the bar (REQ-UI-STATUS-LIGHT
|
// placed directly below the HQ cannot overpaint the bar (REQ-UI-STATUS-LIGHT
|
||||||
// neighbours case, same rationale as the selection highlights below).
|
// neighbours case, same rationale as the selection highlights below).
|
||||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
if (b.type != BuildingType::Hq) { continue; }
|
if (b.type != BuildingType::Hq) { continue; }
|
||||||
const QPointF tl = tileToWidget(b.anchor);
|
const QPointF tl = tileToWidget(b.anchor);
|
||||||
@@ -1484,12 +1474,12 @@ std::optional<QRectF> GameWorldView::footprintWidgetRect(BuildingId id) const
|
|||||||
std::optional<QPoint> anchor;
|
std::optional<QPoint> anchor;
|
||||||
std::optional<QSize> footprint;
|
std::optional<QSize> footprint;
|
||||||
|
|
||||||
if (const Building* b = m_sim->getBuildings().findBuilding(id))
|
if (const Building* b = findBuilding(m_sim->getFactoryState(), id))
|
||||||
{
|
{
|
||||||
anchor = b->anchor;
|
anchor = b->anchor;
|
||||||
footprint = b->footprint;
|
footprint = b->footprint;
|
||||||
}
|
}
|
||||||
else if (const ConstructionSite* s = m_sim->getBuildings().findSite(id))
|
else if (const ConstructionSite* s = findSite(m_sim->getFactoryState(), id))
|
||||||
{
|
{
|
||||||
anchor = s->anchor;
|
anchor = s->anchor;
|
||||||
footprint = s->footprint;
|
footprint = s->footprint;
|
||||||
@@ -1520,7 +1510,7 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter)
|
|||||||
if (!m_selectedDebris.empty())
|
if (!m_selectedDebris.empty())
|
||||||
{
|
{
|
||||||
const qreal outlineRadius = static_cast<qreal>(getTilePx() * 0.2f) + 3.0;
|
const qreal outlineRadius = static_cast<qreal>(getTilePx() * 0.2f) + 3.0;
|
||||||
for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
|
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
|
||||||
{
|
{
|
||||||
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), debris.entity)
|
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), debris.entity)
|
||||||
== m_selectedDebris.end()) { continue; }
|
== m_selectedDebris.end()) { continue; }
|
||||||
@@ -1541,13 +1531,13 @@ void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
|
|||||||
painter.setPen(Qt::NoPen);
|
painter.setPen(Qt::NoPen);
|
||||||
painter.setBrush(color);
|
painter.setBrush(color);
|
||||||
const BuildingType type = m_copiedConfig->type;
|
const BuildingType type = m_copiedConfig->type;
|
||||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
if (b.type != type) { continue; }
|
if (b.type != type) { continue; }
|
||||||
const std::optional<QRectF> rect = footprintWidgetRect(b.id);
|
const std::optional<QRectF> rect = footprintWidgetRect(b.id);
|
||||||
if (rect.has_value()) { painter.drawRect(*rect); }
|
if (rect.has_value()) { painter.drawRect(*rect); }
|
||||||
}
|
}
|
||||||
for (const ConstructionSite& s : m_sim->getBuildings().getAllSites())
|
for (const ConstructionSite& s : getAllSites(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
if (s.type != type) { continue; }
|
if (s.type != type) { continue; }
|
||||||
const std::optional<QRectF> rect = footprintWidgetRect(s.id);
|
const std::optional<QRectF> rect = footprintWidgetRect(s.id);
|
||||||
@@ -1582,7 +1572,7 @@ void GameWorldView::drawPortItems(QPainter& painter)
|
|||||||
const double margin = kPortMarginTiles * static_cast<double>(getTilePx());
|
const double margin = kPortMarginTiles * static_cast<double>(getTilePx());
|
||||||
|
|
||||||
QRegion clip(rect());
|
QRegion clip(rect());
|
||||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
if (b.type == BuildingType::Belt || b.type == BuildingType::Splitter
|
if (b.type == BuildingType::Belt || b.type == BuildingType::Splitter
|
||||||
|| b.type == BuildingType::TunnelEntry || b.type == BuildingType::TunnelExit)
|
|| b.type == BuildingType::TunnelEntry || b.type == BuildingType::TunnelExit)
|
||||||
@@ -1615,8 +1605,8 @@ void GameWorldView::drawPortItems(QPainter& painter)
|
|||||||
|
|
||||||
painter.save();
|
painter.save();
|
||||||
painter.setClipRegion(clip);
|
painter.setClipRegion(clip);
|
||||||
m_sim->getBuildings().forEachEmergingItem(drawItem);
|
m_sim->getBuildings().forEachEmergingItem(m_sim->getFactoryState(), drawItem);
|
||||||
m_sim->getBuildings().forEachIncomingItem(drawItem);
|
m_sim->getBuildings().forEachIncomingItem(m_sim->getFactoryState(), drawItem);
|
||||||
painter.restore();
|
painter.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1664,7 +1654,7 @@ void GameWorldView::drawBeltItems(QPainter& painter)
|
|||||||
void GameWorldView::drawDebris(QPainter& painter)
|
void GameWorldView::drawDebris(QPainter& painter)
|
||||||
{
|
{
|
||||||
const float r = getTilePx() * 0.2f;
|
const float r = getTilePx() * 0.2f;
|
||||||
for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
|
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
|
||||||
{
|
{
|
||||||
const QPointF center = worldToWidget(debris.position);
|
const QPointF center = worldToWidget(debris.position);
|
||||||
painter.setBrush(QColor(128, 110, 90));
|
painter.setBrush(QColor(128, 110, 90));
|
||||||
@@ -1993,11 +1983,11 @@ void GameWorldView::drawSelectedTunnelConnections(QPainter& painter)
|
|||||||
std::optional<QPoint> anchor;
|
std::optional<QPoint> anchor;
|
||||||
std::optional<BuildingType> type;
|
std::optional<BuildingType> type;
|
||||||
Rotation rotation = Rotation::East;
|
Rotation rotation = Rotation::East;
|
||||||
if (const Building* b = m_sim->getBuildings().findBuilding(id))
|
if (const Building* b = findBuilding(m_sim->getFactoryState(), id))
|
||||||
{
|
{
|
||||||
anchor = b->anchor; type = b->type; rotation = b->rotation;
|
anchor = b->anchor; type = b->type; rotation = b->rotation;
|
||||||
}
|
}
|
||||||
else if (const ConstructionSite* s = m_sim->getBuildings().findSite(id))
|
else if (const ConstructionSite* s = findSite(m_sim->getFactoryState(), id))
|
||||||
{
|
{
|
||||||
anchor = s->anchor; type = s->type; rotation = s->rotation;
|
anchor = s->anchor; type = s->type; rotation = s->rotation;
|
||||||
}
|
}
|
||||||
@@ -2102,7 +2092,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
|||||||
|
|
||||||
// Queued for deconstruction: tint every building currently in the
|
// Queued for deconstruction: tint every building currently in the
|
||||||
// deconstruction queue, regardless of mode (REQ-BLD-DECON-QUEUE).
|
// deconstruction queue, regardless of mode (REQ-BLD-DECON-QUEUE).
|
||||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
if (!b.queuedForDeconstruction) { continue; }
|
if (!b.queuedForDeconstruction) { continue; }
|
||||||
for (const QPoint& cell : b.bodyCells)
|
for (const QPoint& cell : b.bodyCells)
|
||||||
@@ -2117,12 +2107,12 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
|||||||
{
|
{
|
||||||
for (BuildingId id : buildingsInBox(m_boxStartTile, m_boxCurrentTile))
|
for (BuildingId id : buildingsInBox(m_boxStartTile, m_boxCurrentTile))
|
||||||
{
|
{
|
||||||
const Building* b = m_sim->getBuildings().findBuilding(id);
|
const Building* b = findBuilding(m_sim->getFactoryState(), id);
|
||||||
if (b && b->type == BuildingType::Hq) { continue; }
|
if (b && b->type == BuildingType::Hq) { continue; }
|
||||||
const std::vector<QPoint>* cells = nullptr;
|
const std::vector<QPoint>* cells = nullptr;
|
||||||
const ConstructionSite* s = nullptr;
|
const ConstructionSite* s = nullptr;
|
||||||
if (b) { cells = &b->bodyCells; }
|
if (b) { cells = &b->bodyCells; }
|
||||||
else if ((s = m_sim->getBuildings().findSite(id))) { cells = &s->bodyCells; }
|
else if ((s = findSite(m_sim->getFactoryState(), id))) { cells = &s->bodyCells; }
|
||||||
if (cells)
|
if (cells)
|
||||||
{
|
{
|
||||||
for (const QPoint& cell : *cells)
|
for (const QPoint& cell : *cells)
|
||||||
@@ -2134,7 +2124,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
|||||||
}
|
}
|
||||||
else if (m_deconstructMode && m_deconstructHoverBuildingId.has_value())
|
else if (m_deconstructMode && m_deconstructHoverBuildingId.has_value())
|
||||||
{
|
{
|
||||||
const Building* b = m_sim->getBuildings().findBuilding(*m_deconstructHoverBuildingId);
|
const Building* b = findBuilding(m_sim->getFactoryState(), *m_deconstructHoverBuildingId);
|
||||||
if (b)
|
if (b)
|
||||||
{
|
{
|
||||||
for (const QPoint& cell : b->bodyCells)
|
for (const QPoint& cell : b->bodyCells)
|
||||||
@@ -2215,7 +2205,7 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
|
|||||||
|
|
||||||
for (const Port& port : parsed.outputPorts)
|
for (const Port& port : parsed.outputPorts)
|
||||||
{
|
{
|
||||||
drawPortGlyph(painter, anchorTile + portBodyTile(port.tile, port.direction),
|
drawPortGlyph(painter, anchorTile + outputBodyTile(port.tile, port.direction),
|
||||||
port.direction, lineColor, /*centered*/ false);
|
port.direction, lineColor, /*centered*/ false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2759,7 +2749,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
|
|
||||||
if (m_deconstructMode)
|
if (m_deconstructMode)
|
||||||
{
|
{
|
||||||
const BuildingSystem& buildings = m_sim->getBuildings();
|
const FactoryState& factory = m_sim->getFactoryState();
|
||||||
|
|
||||||
// Split covered ids into construction sites (removed instantly) and
|
// Split covered ids into construction sites (removed instantly) and
|
||||||
// operational deconstructible buildings (the HQ is protected; player
|
// operational deconstructible buildings (the HQ is protected; player
|
||||||
@@ -2768,12 +2758,12 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
std::vector<BuildingId> operational;
|
std::vector<BuildingId> operational;
|
||||||
for (BuildingId id : boxIds)
|
for (BuildingId id : boxIds)
|
||||||
{
|
{
|
||||||
if (const Building* b = buildings.findBuilding(id))
|
if (const Building* b = findBuilding(factory, id))
|
||||||
{
|
{
|
||||||
if (b->type == BuildingType::Hq) { continue; }
|
if (b->type == BuildingType::Hq) { continue; }
|
||||||
operational.push_back(id);
|
operational.push_back(id);
|
||||||
}
|
}
|
||||||
else if (buildings.findSite(id))
|
else if (findSite(factory, id))
|
||||||
{
|
{
|
||||||
sites.push_back(id);
|
sites.push_back(id);
|
||||||
}
|
}
|
||||||
@@ -2795,7 +2785,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
bool allQueued = !operational.empty();
|
bool allQueued = !operational.empty();
|
||||||
for (BuildingId id : operational)
|
for (BuildingId id : operational)
|
||||||
{
|
{
|
||||||
if (!buildings.isQueuedForDeconstruction(id)) { allQueued = false; break; }
|
if (!isQueuedForDeconstruction(factory, id)) { allQueued = false; break; }
|
||||||
}
|
}
|
||||||
for (BuildingId id : operational)
|
for (BuildingId id : operational)
|
||||||
{
|
{
|
||||||
@@ -2806,7 +2796,7 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
command->id = id;
|
command->id = id;
|
||||||
enqueueCommand(command);
|
enqueueCommand(command);
|
||||||
}
|
}
|
||||||
else if (!buildings.isQueuedForDeconstruction(id))
|
else if (!isQueuedForDeconstruction(factory, id))
|
||||||
{
|
{
|
||||||
std::shared_ptr<DeconstructCommand> command =
|
std::shared_ptr<DeconstructCommand> command =
|
||||||
std::make_shared<DeconstructCommand>();
|
std::make_shared<DeconstructCommand>();
|
||||||
@@ -3008,7 +2998,7 @@ void GameWorldView::pasteConfigTo(BuildingId id)
|
|||||||
{
|
{
|
||||||
// Operational splitters are configured by tile; sites by BuildingId
|
// Operational splitters are configured by tile; sites by BuildingId
|
||||||
// (mirrors SelectedBuildingPanel::onSplitterFilterChanged).
|
// (mirrors SelectedBuildingPanel::onSplitterFilterChanged).
|
||||||
if (const Building* building = m_sim->getBuildings().findBuilding(id))
|
if (const Building* building = findBuilding(m_sim->getFactoryState(), id))
|
||||||
{
|
{
|
||||||
std::shared_ptr<SetSplitterFiltersCommand> command =
|
std::shared_ptr<SetSplitterFiltersCommand> command =
|
||||||
std::make_shared<SetSplitterFiltersCommand>();
|
std::make_shared<SetSplitterFiltersCommand>();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "MainWindow.h"
|
#include "MainWindow.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <random>
|
#include <random>
|
||||||
@@ -277,9 +278,9 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
|
|||||||
{
|
{
|
||||||
// A construction site has no Building yet; fall back to its site record so
|
// A construction site has no Building yet; fall back to its site record so
|
||||||
// the shipyard layout can be configured before it is built (REQ-BLD-SITE-CONFIG).
|
// the shipyard layout can be configured before it is built (REQ-BLD-SITE-CONFIG).
|
||||||
const Building* b = m_sim->getBuildings().findBuilding(event->shipyardId);
|
const Building* b = findBuilding(m_sim->getFactoryState(), event->shipyardId);
|
||||||
const ConstructionSite* s =
|
const ConstructionSite* s =
|
||||||
b ? nullptr : m_sim->getBuildings().findSite(event->shipyardId);
|
b ? nullptr : findSite(m_sim->getFactoryState(), event->shipyardId);
|
||||||
if (!b && !s)
|
if (!b && !s)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -304,9 +305,9 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
|||||||
|
|
||||||
// A construction site has no Building yet; fall back to its site record so
|
// A construction site has no Building yet; fall back to its site record so
|
||||||
// the recipe/schematic can be chosen before it is built (REQ-BLD-SITE-CONFIG).
|
// the recipe/schematic can be chosen before it is built (REQ-BLD-SITE-CONFIG).
|
||||||
const Building* b = m_sim->getBuildings().findBuilding(event->buildingId);
|
const Building* b = findBuilding(m_sim->getFactoryState(), event->buildingId);
|
||||||
const ConstructionSite* s =
|
const ConstructionSite* s =
|
||||||
b ? nullptr : m_sim->getBuildings().findSite(event->buildingId);
|
b ? nullptr : findSite(m_sim->getFactoryState(), event->buildingId);
|
||||||
if (!b && !s)
|
if (!b && !s)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "SelectedBuildingPanel.h"
|
#include "SelectedBuildingPanel.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
@@ -9,26 +10,14 @@
|
|||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QListWidget>
|
#include <QListWidget>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QStringList>
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "CommandRequestedEvent.h"
|
#include "CommandRequestedEvent.h"
|
||||||
#include "DisplayName.h"
|
|
||||||
#include "DynamicBodyComponent.h"
|
|
||||||
#include "EntityAdmin.h"
|
|
||||||
#include "EntitySelectionChangedEvent.h"
|
#include "EntitySelectionChangedEvent.h"
|
||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
#include "FactionComponent.h"
|
#include "FieldSelectionPanel.h"
|
||||||
#include "HealthComponent.h"
|
|
||||||
#include "ModuleOwnerComponent.h"
|
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "ShipIdentityComponent.h"
|
|
||||||
#include "ShipStatsCalculator.h"
|
|
||||||
#include "ShipStatsPanel.h"
|
|
||||||
#include "ThreatCostCalculator.h"
|
|
||||||
#include "StationBodyComponent.h"
|
|
||||||
#include "TickAdvancedEvent.h"
|
#include "TickAdvancedEvent.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
@@ -40,10 +29,8 @@
|
|||||||
#include "RecipeSelectionDialog.h"
|
#include "RecipeSelectionDialog.h"
|
||||||
#include "RecipeSelectionRequestedEvent.h"
|
#include "RecipeSelectionRequestedEvent.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "DebrisSystem.h"
|
|
||||||
#include "ShipLayoutPreview.h"
|
#include "ShipLayoutPreview.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "WeaponComponent.h"
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -98,20 +85,6 @@ bool hasRecipeSelection(BuildingType type)
|
|||||||
|| type == BuildingType::Shipyard;
|
|| type == BuildingType::Shipyard;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-recipe buildings have no selected recipe; their production is driven by
|
|
||||||
// whatever inputs they receive.
|
|
||||||
bool isAutoRecipeBuilding(BuildingType type)
|
|
||||||
{
|
|
||||||
return type == BuildingType::Smelter
|
|
||||||
|| type == BuildingType::ReprocessingPlant;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isBeltLike(BuildingType type)
|
|
||||||
{
|
|
||||||
return type == BuildingType::Belt || type == BuildingType::Splitter
|
|
||||||
|| type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString rotationLabel(Rotation r)
|
QString rotationLabel(Rotation r)
|
||||||
{
|
{
|
||||||
switch (r)
|
switch (r)
|
||||||
@@ -182,30 +155,10 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
|||||||
connect(m_filterBList, &QListWidget::itemChanged,
|
connect(m_filterBList, &QListWidget::itemChanged,
|
||||||
this, &SelectedBuildingPanel::onSplitterFilterChanged);
|
this, &SelectedBuildingPanel::onSplitterFilterChanged);
|
||||||
|
|
||||||
m_entityTitleLabel = new QLabel(this);
|
// The field selection renders below the building content and hides itself while
|
||||||
QFont titleFont = m_entityTitleLabel->font();
|
// nothing field-side is selected, so it costs no space then.
|
||||||
titleFont.setBold(true);
|
m_fieldSelectionPanel = new FieldSelectionPanel(sim, config, this);
|
||||||
m_entityTitleLabel->setFont(titleFont);
|
m_layout->addWidget(m_fieldSelectionPanel);
|
||||||
m_layout->addWidget(m_entityTitleLabel);
|
|
||||||
m_entityTitleLabel->hide();
|
|
||||||
|
|
||||||
m_entityStatsPanel = new ShipStatsPanel(config, this);
|
|
||||||
m_layout->addWidget(m_entityStatsPanel);
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
|
|
||||||
m_stationStatsLabel = new QLabel(this);
|
|
||||||
m_stationStatsLabel->setWordWrap(true);
|
|
||||||
m_layout->addWidget(m_stationStatsLabel);
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
|
|
||||||
m_entitySummaryLabel = new QLabel(this);
|
|
||||||
m_entitySummaryLabel->setWordWrap(true);
|
|
||||||
m_layout->addWidget(m_entitySummaryLabel);
|
|
||||||
m_entitySummaryLabel->hide();
|
|
||||||
|
|
||||||
m_scrapLabel = new QLabel(this);
|
|
||||||
m_layout->addWidget(m_scrapLabel);
|
|
||||||
m_scrapLabel->hide();
|
|
||||||
|
|
||||||
buildEmpty();
|
buildEmpty();
|
||||||
|
|
||||||
@@ -224,13 +177,21 @@ void SelectedBuildingPanel::onSelectionChanged(const std::vector<BuildingId>& id
|
|||||||
{
|
{
|
||||||
// A building selection is exclusive: it supersedes any field selection —
|
// A building selection is exclusive: it supersedes any field selection —
|
||||||
// actors and scrap (REQ-UI-SELECTION-CATEGORIES).
|
// actors and scrap (REQ-UI-SELECTION-CATEGORIES).
|
||||||
clearEntityDisplay();
|
m_fieldSelectionPanel->clearSelection();
|
||||||
m_selectedDebris.clear();
|
|
||||||
m_scrapLabel->hide();
|
|
||||||
}
|
}
|
||||||
rebuild();
|
rebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::yieldToFieldSelection()
|
||||||
|
{
|
||||||
|
// The mirror image of onSelectionChanged(): a field selection — actors, debris, or
|
||||||
|
// both — supersedes any building selection (REQ-UI-SELECTION-CATEGORIES). An empty
|
||||||
|
// field selection changes nothing here: the building content, if any, keeps the panel.
|
||||||
|
if (!m_fieldSelectionPanel->hasSelection()) { return; }
|
||||||
|
m_selectedBuildingIds.clear();
|
||||||
|
buildEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
void SelectedBuildingPanel::rebuild()
|
void SelectedBuildingPanel::rebuild()
|
||||||
{
|
{
|
||||||
if (m_selectedBuildingIds.empty())
|
if (m_selectedBuildingIds.empty())
|
||||||
@@ -259,22 +220,14 @@ void SelectedBuildingPanel::hideAllWidgets()
|
|||||||
m_filterBLabel->hide();
|
m_filterBLabel->hide();
|
||||||
m_filterBList->hide();
|
m_filterBList->hide();
|
||||||
m_buffersLabel->hide();
|
m_buffersLabel->hide();
|
||||||
m_scrapLabel->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SelectedBuildingPanel::clearContent()
|
|
||||||
{
|
|
||||||
m_singleBuildingId = std::nullopt;
|
|
||||||
hideAllWidgets();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectedBuildingPanel::buildEmpty()
|
void SelectedBuildingPanel::buildEmpty()
|
||||||
{
|
{
|
||||||
clearContent();
|
// Shows nothing for the building category — either because nothing is selected or
|
||||||
m_entityTitleLabel->hide();
|
// because the field category has taken the panel over.
|
||||||
m_entityStatsPanel->hide();
|
m_singleBuildingId = std::nullopt;
|
||||||
m_stationStatsLabel->hide();
|
hideAllWidgets();
|
||||||
m_entitySummaryLabel->hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectedBuildingPanel::buildSingle(BuildingId id)
|
void SelectedBuildingPanel::buildSingle(BuildingId id)
|
||||||
@@ -282,8 +235,8 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
|
|||||||
m_singleBuildingId = id;
|
m_singleBuildingId = id;
|
||||||
hideAllWidgets();
|
hideAllWidgets();
|
||||||
|
|
||||||
const Building* b = m_sim->getBuildings().findBuilding(id);
|
const Building* b = findBuilding(m_sim->getFactoryState(), id);
|
||||||
const ConstructionSite* s = b ? nullptr : m_sim->getBuildings().findSite(id);
|
const ConstructionSite* s = b ? nullptr : findSite(m_sim->getFactoryState(), id);
|
||||||
if (!b && !s)
|
if (!b && !s)
|
||||||
{
|
{
|
||||||
buildEmpty();
|
buildEmpty();
|
||||||
@@ -347,7 +300,7 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
|
|||||||
|
|
||||||
// Belt "Clear" removes items from a live belt tile; a construction site has
|
// Belt "Clear" removes items from a live belt tile; a construction site has
|
||||||
// none and is not registered with BeltSystem yet, so hide it for sites.
|
// none and is not registered with BeltSystem yet, so hide it for sites.
|
||||||
if (isBeltLike(type) && !m_singleIsSite)
|
if (isBeltSubsystemType(type) && !m_singleIsSite)
|
||||||
{
|
{
|
||||||
m_clearBeltBtn->show();
|
m_clearBeltBtn->show();
|
||||||
}
|
}
|
||||||
@@ -361,7 +314,7 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
|
|||||||
std::optional<BeltSystem::SplitterInfo> info;
|
std::optional<BeltSystem::SplitterInfo> info;
|
||||||
if (m_singleIsSite)
|
if (m_singleIsSite)
|
||||||
{
|
{
|
||||||
info = m_sim->getBuildings().getSiteSplitterInfo(id);
|
info = getSiteSplitterInfo(m_sim->getFactoryState(), m_sim->getConfig(), id);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -428,7 +381,7 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
|||||||
// Auto-recipe buildings (Smelter, Reprocessing Plant) have no selected
|
// Auto-recipe buildings (Smelter, Reprocessing Plant) have no selected
|
||||||
// recipe; while a cycle runs, resolve the recipe actually in production so
|
// recipe; while a cycle runs, resolve the recipe actually in production so
|
||||||
// the cycle time and progress can be shown (REQ-UI-PRODUCTION-PROGRESS).
|
// the cycle time and progress can be shown (REQ-UI-PRODUCTION-PROGRESS).
|
||||||
if (!recipe && isAutoRecipeBuilding(b->type) && b->production.has_value())
|
if (!recipe && isAutoRecipeBuildingType(b->type) && b->production.has_value())
|
||||||
{
|
{
|
||||||
recipe = m_config->recipes.findRecipeDef(b->production->recipeId, b->type);
|
recipe = m_config->recipes.findRecipeDef(b->production->recipeId, b->type);
|
||||||
}
|
}
|
||||||
@@ -522,7 +475,7 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isProductionBuilding(b->type)
|
if (isProductionBuilding(b->type)
|
||||||
&& (recipe || shipDef || isAutoRecipeBuilding(b->type)))
|
&& (recipe || shipDef || isAutoRecipeBuildingType(b->type)))
|
||||||
{
|
{
|
||||||
if (recipe || shipDef)
|
if (recipe || shipDef)
|
||||||
{
|
{
|
||||||
@@ -646,29 +599,11 @@ void SelectedBuildingPanel::handleEvent(
|
|||||||
|
|
||||||
void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
|
void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
|
||||||
{
|
{
|
||||||
if (!m_selectedEntities.empty() || !m_selectedDebris.empty())
|
// Only a single selected building has live content to refresh. While the field
|
||||||
{
|
// category owns the panel there is none: yieldToFieldSelection() has cleared it, so
|
||||||
// Field selection. Keep the live values current: the single-actor stats panel,
|
// this returns immediately and the field panel refreshes itself off the same events.
|
||||||
// the single-debris stats panel (whose Scrap row shrinks as it is collected), or
|
|
||||||
// the count summary (whose Scrap line shrinks likewise) — matching the layout
|
|
||||||
// chosen by buildFieldSelection() (REQ-UI-SHIP-STATS-PANEL, REQ-UI-DEBRIS-PANEL).
|
|
||||||
if (m_selectedEntities.size() == 1 && m_selectedDebris.empty())
|
|
||||||
{
|
|
||||||
refreshEntityStats();
|
|
||||||
}
|
|
||||||
else if (m_selectedEntities.empty() && m_selectedDebris.size() == 1)
|
|
||||||
{
|
|
||||||
buildDebrisSingle();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
buildEntitySummary();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_singleBuildingId.has_value()) { return; }
|
if (!m_singleBuildingId.has_value()) { return; }
|
||||||
const Building* b = m_sim->getBuildings().findBuilding(*m_singleBuildingId);
|
const Building* b = findBuilding(m_sim->getFactoryState(), *m_singleBuildingId);
|
||||||
if (b)
|
if (b)
|
||||||
{
|
{
|
||||||
if (m_titleLabel->text().startsWith(tr("(Building) ")))
|
if (m_titleLabel->text().startsWith(tr("(Building) ")))
|
||||||
@@ -681,7 +616,7 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ConstructionSite* s = m_sim->getBuildings().findSite(*m_singleBuildingId);
|
const ConstructionSite* s = findSite(m_sim->getFactoryState(), *m_singleBuildingId);
|
||||||
if (s)
|
if (s)
|
||||||
{
|
{
|
||||||
// A periodic tick only advances construction progress, so update just the
|
// A periodic tick only advances construction progress, so update just the
|
||||||
@@ -715,13 +650,13 @@ void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
|
|||||||
std::map<BuildingType, int> counts;
|
std::map<BuildingType, int> counts;
|
||||||
for (BuildingId id : ids)
|
for (BuildingId id : ids)
|
||||||
{
|
{
|
||||||
const Building* b = m_sim->getBuildings().findBuilding(id);
|
const Building* b = findBuilding(m_sim->getFactoryState(), id);
|
||||||
if (b)
|
if (b)
|
||||||
{
|
{
|
||||||
counts[b->type]++;
|
counts[b->type]++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const ConstructionSite* s = m_sim->getBuildings().findSite(id);
|
const ConstructionSite* s = findSite(m_sim->getFactoryState(), id);
|
||||||
if (s)
|
if (s)
|
||||||
{
|
{
|
||||||
counts[s->type]++;
|
counts[s->type]++;
|
||||||
@@ -735,7 +670,7 @@ void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
|
|||||||
{
|
{
|
||||||
text += buildingTypeName(entry.first) + " x "
|
text += buildingTypeName(entry.first) + " x "
|
||||||
+ QString::number(entry.second) + "\n";
|
+ QString::number(entry.second) + "\n";
|
||||||
if (isBeltLike(entry.first))
|
if (isBeltSubsystemType(entry.first))
|
||||||
{
|
{
|
||||||
hasBelt = true;
|
hasBelt = true;
|
||||||
}
|
}
|
||||||
@@ -883,8 +818,8 @@ void SelectedBuildingPanel::onClearBelt()
|
|||||||
std::vector<QPoint> tiles;
|
std::vector<QPoint> tiles;
|
||||||
for (BuildingId id : m_selectedBuildingIds)
|
for (BuildingId id : m_selectedBuildingIds)
|
||||||
{
|
{
|
||||||
const Building* b = m_sim->getBuildings().findBuilding(id);
|
const Building* b = findBuilding(m_sim->getFactoryState(), id);
|
||||||
if (b && isBeltLike(b->type))
|
if (b && isBeltSubsystemType(b->type))
|
||||||
{
|
{
|
||||||
for (const QPoint& cell : b->bodyCells)
|
for (const QPoint& cell : b->bodyCells)
|
||||||
{
|
{
|
||||||
@@ -904,259 +839,8 @@ void SelectedBuildingPanel::onClearBelt()
|
|||||||
|
|
||||||
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event)
|
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event)
|
||||||
{
|
{
|
||||||
m_selectedEntities = event->entities;
|
m_fieldSelectionPanel->setSelectedEntities(event->entities);
|
||||||
if (!m_selectedEntities.empty())
|
yieldToFieldSelection();
|
||||||
{
|
|
||||||
// A field selection supersedes any building selection (REQ-UI-SELECTION-CATEGORIES).
|
|
||||||
m_selectedBuildingIds.clear();
|
|
||||||
}
|
|
||||||
buildFieldSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SelectedBuildingPanel::buildFieldSelection()
|
|
||||||
{
|
|
||||||
if (m_selectedEntities.empty() && m_selectedDebris.empty())
|
|
||||||
{
|
|
||||||
// Nothing in the field category. Fall back to empty unless buildings own the panel.
|
|
||||||
clearEntityDisplay();
|
|
||||||
m_scrapLabel->hide();
|
|
||||||
if (m_selectedBuildingIds.empty())
|
|
||||||
{
|
|
||||||
buildEmpty();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A field selection owns the panel: drop any building content.
|
|
||||||
clearContent();
|
|
||||||
|
|
||||||
EntityAdmin& admin = m_sim->getAdmin();
|
|
||||||
|
|
||||||
// A full single-object stats panel is shown only for a lone field object: one actor
|
|
||||||
// with no debris, or one piece of debris with no actors. As soon as the selection holds
|
|
||||||
// more than one object (multiple actors, multiple debris, or actors plus debris), the
|
|
||||||
// panel switches to the compact count summary (REQ-UI-FIELD-MULTI-SELECTION).
|
|
||||||
if (m_selectedEntities.size() == 1 && m_selectedDebris.empty())
|
|
||||||
{
|
|
||||||
m_entitySummaryLabel->hide();
|
|
||||||
m_scrapLabel->hide();
|
|
||||||
const entt::entity entity = m_selectedEntities.front();
|
|
||||||
if (admin.isValid(entity) && admin.hasAll<ShipIdentityComponent>(entity))
|
|
||||||
{
|
|
||||||
buildEntityShip(entity);
|
|
||||||
}
|
|
||||||
else if (admin.isValid(entity) && admin.hasAll<StationBodyComponent>(entity))
|
|
||||||
{
|
|
||||||
buildEntityStation(entity);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_entityTitleLabel->hide();
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_selectedEntities.empty() && m_selectedDebris.size() == 1)
|
|
||||||
{
|
|
||||||
// Single piece of debris: a "Debris" heading plus a "Scrap" stat row, styled like
|
|
||||||
// the ship/station stats panels (REQ-UI-DEBRIS-PANEL).
|
|
||||||
m_entitySummaryLabel->hide();
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
buildDebrisSingle();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// More than one field object: a compact count summary. buildEntitySummary() appends the
|
|
||||||
// "Debris x N" and "Scrap x N" lines when debris is part of the selection.
|
|
||||||
m_entityTitleLabel->hide();
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
m_scrapLabel->hide();
|
|
||||||
buildEntitySummary();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SelectedBuildingPanel::buildDebrisSingle()
|
|
||||||
{
|
|
||||||
// "Debris" heading + a single "Scrap" stat row for the piece's remaining amount,
|
|
||||||
// mirroring the single-actor stats panels (REQ-UI-DEBRIS-PANEL).
|
|
||||||
m_entityTitleLabel->setText(tr("Debris"));
|
|
||||||
m_entityTitleLabel->show();
|
|
||||||
m_scrapLabel->setText(tr("Scrap: %1").arg(selectedDebrisScrapTotal()));
|
|
||||||
m_scrapLabel->show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SelectedBuildingPanel::buildEntitySummary()
|
|
||||||
{
|
|
||||||
EntityAdmin& admin = m_sim->getAdmin();
|
|
||||||
|
|
||||||
// Group actors by faction + kind + ship schematic, preserving first-seen order
|
|
||||||
// (REQ-UI-FIELD-MULTI-SELECTION).
|
|
||||||
std::vector<QString> keys;
|
|
||||||
std::map<QString, int> counts;
|
|
||||||
std::map<QString, QString> labels;
|
|
||||||
|
|
||||||
for (entt::entity entity : m_selectedEntities)
|
|
||||||
{
|
|
||||||
if (!admin.isValid(entity)) { continue; }
|
|
||||||
const bool isEnemy = admin.hasAll<FactionComponent>(entity)
|
|
||||||
&& admin.get<FactionComponent>(entity).isEnemy;
|
|
||||||
|
|
||||||
QString key;
|
|
||||||
QString label;
|
|
||||||
if (admin.hasAll<ShipIdentityComponent>(entity))
|
|
||||||
{
|
|
||||||
const std::string& id = admin.get<ShipIdentityComponent>(entity).schematicId;
|
|
||||||
const QString name = QString::fromStdString(toDisplayName(id));
|
|
||||||
key = (isEnemy ? QStringLiteral("ship:enemy:") : QStringLiteral("ship:player:"))
|
|
||||||
+ QString::fromStdString(id);
|
|
||||||
label = isEnemy ? tr("Enemy %1").arg(name) : name;
|
|
||||||
}
|
|
||||||
else if (admin.hasAll<StationBodyComponent>(entity))
|
|
||||||
{
|
|
||||||
key = isEnemy ? QStringLiteral("station:enemy") : QStringLiteral("station:player");
|
|
||||||
label = isEnemy ? tr("Enemy Defence Station") : tr("Player Defence Station");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (counts.find(key) == counts.end())
|
|
||||||
{
|
|
||||||
keys.push_back(key);
|
|
||||||
labels[key] = label;
|
|
||||||
}
|
|
||||||
counts[key] += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// One "<type> x <count>" line per group (matching the recipe tooltip and the building
|
|
||||||
// multi-selection). No total-count header, consistent with the building panel. When
|
|
||||||
// debris is part of the selection, a "Debris x <count>" line followed by a
|
|
||||||
// "Scrap x <total>" line are appended into the same label so the line spacing is
|
|
||||||
// uniform (REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-DEBRIS-PANEL).
|
|
||||||
QStringList lines;
|
|
||||||
for (const QString& key : keys)
|
|
||||||
{
|
|
||||||
lines << tr("%1 x %2").arg(labels[key]).arg(counts[key]);
|
|
||||||
}
|
|
||||||
if (!m_selectedDebris.empty())
|
|
||||||
{
|
|
||||||
lines << tr("Debris x %1").arg(static_cast<int>(m_selectedDebris.size()));
|
|
||||||
lines << scrapTotalText();
|
|
||||||
}
|
|
||||||
m_entitySummaryLabel->setText(lines.join('\n'));
|
|
||||||
m_entitySummaryLabel->show();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SelectedBuildingPanel::buildEntityShip(entt::entity entity)
|
|
||||||
{
|
|
||||||
EntityAdmin& admin = m_sim->getAdmin();
|
|
||||||
const ShipIdentityComponent& identity = admin.get<ShipIdentityComponent>(entity);
|
|
||||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
|
||||||
|
|
||||||
m_entityTitleLabel->setText(tr("Ship: %1")
|
|
||||||
.arg(QString::fromStdString(identity.schematicId)));
|
|
||||||
m_entityTitleLabel->show();
|
|
||||||
|
|
||||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
|
||||||
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
|
||||||
m_entityStatsPanel->setBehavior(
|
|
||||||
admin.get<SelectedBehaviorComponent>(entity).winner);
|
|
||||||
m_entityStatsPanel->setDebugDrawEnabled(m_debugDraw);
|
|
||||||
|
|
||||||
const ShipDef* schematicDef =
|
|
||||||
m_config->ships.findShipDef(identity.schematicId);
|
|
||||||
if (schematicDef)
|
|
||||||
{
|
|
||||||
const double threat = calculateShipThreatCost(
|
|
||||||
m_config->threatCosts, *m_config, schematicDef->id,
|
|
||||||
schematicDef->defaultModules);
|
|
||||||
m_entityStatsPanel->setThreatCost(threat);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_entityStatsPanel->show();
|
|
||||||
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SelectedBuildingPanel::buildEntityStation(entt::entity entity)
|
|
||||||
{
|
|
||||||
EntityAdmin& admin = m_sim->getAdmin();
|
|
||||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
|
||||||
|
|
||||||
const bool isEnemy = admin.hasAll<FactionComponent>(entity)
|
|
||||||
&& admin.get<FactionComponent>(entity).isEnemy;
|
|
||||||
m_entityTitleLabel->setText(isEnemy
|
|
||||||
? tr("Enemy Defence Station")
|
|
||||||
: tr("Player Defence Station"));
|
|
||||||
m_entityTitleLabel->show();
|
|
||||||
|
|
||||||
float totalDps = 0.0f;
|
|
||||||
float maxRange = 0.0f;
|
|
||||||
bool hasWeapons = false;
|
|
||||||
|
|
||||||
admin.forEach<ModuleOwnerComponent, WeaponComponent>(
|
|
||||||
[&](entt::entity /*child*/, const ModuleOwnerComponent& owner, const WeaponComponent& w)
|
|
||||||
{
|
|
||||||
if (owner.owner != entity) { return; }
|
|
||||||
hasWeapons = true;
|
|
||||||
totalDps += w.damage * w.fireRateHz;
|
|
||||||
if (w.range_tiles > maxRange) { maxRange = w.range_tiles; }
|
|
||||||
});
|
|
||||||
|
|
||||||
QString statsText = tr("HP: %1 / %2")
|
|
||||||
.arg(static_cast<int>(health.hp + 0.5f))
|
|
||||||
.arg(static_cast<int>(health.maxHp + 0.5f));
|
|
||||||
|
|
||||||
if (hasWeapons)
|
|
||||||
{
|
|
||||||
statsText += tr("\nDPS: %1").arg(QString::number(static_cast<double>(totalDps), 'f', 1));
|
|
||||||
statsText += tr("\nRange: %1 tiles").arg(QString::number(static_cast<double>(maxRange), 'f', 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
m_stationStatsLabel->setText(statsText);
|
|
||||||
m_stationStatsLabel->show();
|
|
||||||
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
void SelectedBuildingPanel::refreshEntityStats()
|
|
||||||
{
|
|
||||||
// Only the single-actor stats panel needs a live refresh; the multi-actor summary is
|
|
||||||
// static counts, and GameWorldView prunes dead/despawned actors and re-emits the
|
|
||||||
// selection (REQ-UI-ENTITY-CLICK-SELECT), so the panel does not mutate it here.
|
|
||||||
if (m_selectedEntities.size() != 1) { return; }
|
|
||||||
|
|
||||||
EntityAdmin& admin = m_sim->getAdmin();
|
|
||||||
const entt::entity entity = m_selectedEntities.front();
|
|
||||||
|
|
||||||
if (!admin.isValid(entity) || !admin.hasAll<HealthComponent>(entity)) { return; }
|
|
||||||
const HealthComponent& health = admin.get<HealthComponent>(entity);
|
|
||||||
if (health.hp <= 0.0f) { return; }
|
|
||||||
|
|
||||||
if (admin.hasAll<ShipIdentityComponent>(entity))
|
|
||||||
{
|
|
||||||
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
|
|
||||||
m_entityStatsPanel->refreshFromLive(stats, health.hp);
|
|
||||||
m_entityStatsPanel->setBehavior(
|
|
||||||
admin.get<SelectedBehaviorComponent>(entity).winner);
|
|
||||||
}
|
|
||||||
else if (admin.hasAll<StationBodyComponent>(entity))
|
|
||||||
{
|
|
||||||
buildEntityStation(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SelectedBuildingPanel::clearEntityDisplay()
|
|
||||||
{
|
|
||||||
m_selectedEntities.clear();
|
|
||||||
m_entityTitleLabel->hide();
|
|
||||||
m_entityStatsPanel->hide();
|
|
||||||
m_stationStatsLabel->hide();
|
|
||||||
m_entitySummaryLabel->hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||||
@@ -1167,38 +851,8 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const SelectionChangedEv
|
|||||||
void SelectedBuildingPanel::handleEvent(
|
void SelectedBuildingPanel::handleEvent(
|
||||||
std::shared_ptr<const DebrisSelectionChangedEvent> event)
|
std::shared_ptr<const DebrisSelectionChangedEvent> event)
|
||||||
{
|
{
|
||||||
m_selectedDebris = event->debris;
|
// Debris is a field object: it supersedes any building selection but coexists
|
||||||
if (!m_selectedDebris.empty())
|
// with actors (REQ-UI-SELECTION-CATEGORIES).
|
||||||
{
|
m_fieldSelectionPanel->setSelectedDebris(event->debris);
|
||||||
// Debris is a field object: it supersedes any building selection but coexists
|
yieldToFieldSelection();
|
||||||
// with actors (REQ-UI-SELECTION-CATEGORIES).
|
|
||||||
m_selectedBuildingIds.clear();
|
|
||||||
}
|
|
||||||
buildFieldSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
int SelectedBuildingPanel::selectedDebrisScrapTotal() const
|
|
||||||
{
|
|
||||||
// Sum the remaining scrap across the still-living selected debris (REQ-UI-DEBRIS-PANEL).
|
|
||||||
int total = 0;
|
|
||||||
for (const DebrisInfo& info : m_sim->getDebrisSystem().getAllDebrisInfo())
|
|
||||||
{
|
|
||||||
if (std::find(m_selectedDebris.begin(), m_selectedDebris.end(), info.entity)
|
|
||||||
!= m_selectedDebris.end())
|
|
||||||
{
|
|
||||||
total += info.amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString SelectedBuildingPanel::scrapTotalText() const
|
|
||||||
{
|
|
||||||
return tr("Scrap x %1").arg(selectedDebrisScrapTotal());
|
|
||||||
}
|
|
||||||
|
|
||||||
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event)
|
|
||||||
{
|
|
||||||
m_debugDraw = event->active;
|
|
||||||
m_entityStatsPanel->setDebugDrawEnabled(event->active);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,9 @@
|
|||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
|
||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "DebugDrawToggledEvent.h"
|
|
||||||
#include "EntitySelectionChangedEvent.h"
|
#include "EntitySelectionChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
@@ -26,20 +23,27 @@
|
|||||||
#include "TickAdvancedEvent.h"
|
#include "TickAdvancedEvent.h"
|
||||||
|
|
||||||
class Simulation;
|
class Simulation;
|
||||||
|
class FieldSelectionPanel;
|
||||||
class ShipLayoutPreview;
|
class ShipLayoutPreview;
|
||||||
class ShipStatsPanel;
|
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QListWidget;
|
class QListWidget;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
|
|
||||||
|
// Shows the current selection. The building category (buildings and construction sites)
|
||||||
|
// is rendered by this panel itself; the field category (ships, defence stations, debris)
|
||||||
|
// is rendered by the embedded FieldSelectionPanel.
|
||||||
|
//
|
||||||
|
// The two categories are mutually exclusive (REQ-UI-SELECTION-CATEGORIES) and this panel
|
||||||
|
// is the sole arbiter of which one owns the content: it listens to all three selection
|
||||||
|
// events, forwards the field ones to the child panel, and drops the losing category's
|
||||||
|
// content. Neither panel touches the other's widgets.
|
||||||
class SelectedBuildingPanel : public QWidget,
|
class SelectedBuildingPanel : public QWidget,
|
||||||
public CombinedEventHandler<TickAdvancedEvent,
|
public CombinedEventHandler<TickAdvancedEvent,
|
||||||
PlayerCommandsAppliedEvent,
|
PlayerCommandsAppliedEvent,
|
||||||
EntitySelectionChangedEvent,
|
EntitySelectionChangedEvent,
|
||||||
SelectionChangedEvent,
|
SelectionChangedEvent,
|
||||||
DebrisSelectionChangedEvent,
|
DebrisSelectionChangedEvent>
|
||||||
DebugDrawToggledEvent>
|
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -54,7 +58,6 @@ private:
|
|||||||
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onSelectRecipeClicked();
|
void onSelectRecipeClicked();
|
||||||
@@ -73,17 +76,14 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
void onSelectionChanged(const std::vector<BuildingId>& ids);
|
void onSelectionChanged(const std::vector<BuildingId>& ids);
|
||||||
|
// Gives the panel to the field category once it has anything selected.
|
||||||
|
void yieldToFieldSelection();
|
||||||
void refreshSelectionDisplay(RefreshReason reason);
|
void refreshSelectionDisplay(RefreshReason reason);
|
||||||
void rebuild();
|
void rebuild();
|
||||||
void hideAllWidgets();
|
void hideAllWidgets();
|
||||||
void clearContent();
|
|
||||||
void buildEmpty();
|
void buildEmpty();
|
||||||
void buildSingle(BuildingId id);
|
void buildSingle(BuildingId id);
|
||||||
void buildMulti(const std::vector<BuildingId>& ids);
|
void buildMulti(const std::vector<BuildingId>& ids);
|
||||||
// Summed remaining scrap across the selected debris (REQ-UI-DEBRIS-PANEL).
|
|
||||||
int selectedDebrisScrapTotal() const;
|
|
||||||
// "Scrap x N" line for the multi-object summary (REQ-UI-FIELD-MULTI-SELECTION).
|
|
||||||
QString scrapTotalText() const;
|
|
||||||
void refreshBuffers(const Building* b);
|
void refreshBuffers(const Building* b);
|
||||||
void refreshSiteProgress(const ConstructionSite* s);
|
void refreshSiteProgress(const ConstructionSite* s);
|
||||||
void updateShipyardLayoutWidgets(BuildingType type,
|
void updateShipyardLayoutWidgets(BuildingType type,
|
||||||
@@ -116,28 +116,7 @@ private:
|
|||||||
QPoint m_splitterTile;
|
QPoint m_splitterTile;
|
||||||
std::string m_currentRecipeId;
|
std::string m_currentRecipeId;
|
||||||
|
|
||||||
bool m_debugDraw = false;
|
// Renders the field selection (actors + debris) below the building content
|
||||||
// The selected ships/defence stations. Shares the "field" selection category with
|
// (REQ-UI-FIELD-MULTI-SELECTION). Hides itself while nothing field-side is selected.
|
||||||
// debris (m_selectedDebris): both can be non-empty at once (REQ-UI-SELECTION-CATEGORIES).
|
FieldSelectionPanel* m_fieldSelectionPanel;
|
||||||
std::vector<entt::entity> m_selectedEntities;
|
|
||||||
ShipStatsPanel* m_entityStatsPanel;
|
|
||||||
QLabel* m_entityTitleLabel;
|
|
||||||
QLabel* m_stationStatsLabel;
|
|
||||||
QLabel* m_entitySummaryLabel;
|
|
||||||
|
|
||||||
std::vector<entt::entity> m_selectedDebris;
|
|
||||||
// Shows the debris "Scrap" stat row (single selection) — the scrap total for the
|
|
||||||
// multi-object summary lives in m_entitySummaryLabel instead.
|
|
||||||
QLabel* m_scrapLabel;
|
|
||||||
|
|
||||||
// Renders the combined field selection (actors + debris): a single-object stats panel
|
|
||||||
// (ship, station, or debris) or a multi-object count summary that appends the debris
|
|
||||||
// count and scrap total when debris is also selected (REQ-UI-FIELD-MULTI-SELECTION).
|
|
||||||
void buildFieldSelection();
|
|
||||||
void buildEntityShip(entt::entity entity);
|
|
||||||
void buildEntityStation(entt::entity entity);
|
|
||||||
void buildEntitySummary();
|
|
||||||
void buildDebrisSingle();
|
|
||||||
void refreshEntityStats();
|
|
||||||
void clearEntityDisplay();
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user