Commit Graph

274 Commits

Author SHA1 Message Date
d713257fb5 move the field selection out of SelectedBuildingPanel
The two selection categories used to arbitrate ownership of the panel by
poking each other's widgets: buildFieldSelection() called clearContent()
and buildEmpty(), buildEmpty() hid the four entity widgets, and
hideAllWidgets() hid the scrap label. Splitting the halves apart without
naming an arbiter would only have spread that across a class boundary.

SelectedBuildingPanel is now the sole arbiter. It still receives all
three selection events, forwards the two field ones to the embedded
FieldSelectionPanel, and drops its own selection and content as soon as
the field panel reports a selection (yieldToFieldSelection), mirroring
what onSelectionChanged() already did in the other direction. The field
panel decides only what to render and whether it is visible at all.

Dropping the field branch of refreshSelectionDisplay() is behaviour
preserving: whenever the field category owns the panel, m_singleBuildingId
is null, so the building refresh returns immediately anyway.

clearContent() and buildEmpty() became identical once the cross-half
hiding was gone, so only buildEmpty() remains.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-04 15:47:22 +02:00
0029236135 add FieldSelectionPanel for the ships/stations/debris selection
SelectedBuildingPanel has grown to 1200 lines by carrying two unrelated
selection categories. Introduce the field half as its own widget first,
so the cut-over is a separate, reviewable step.

The panel owns only its own selection state and widgets: it renders the
single-object stats panel (ship, station, debris) or the multi-object
count summary, subscribes to the tick/commands-applied refresh signals
and the debug-draw toggle, and hides itself while it has no selection.
Which category owns the side panel is not its decision - the parent
feeds it through setSelectedEntities/setSelectedDebris/clearSelection.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-04 15:42:25 +02:00
cb5572ffdd cover unlock state in the determinism tests
The scripted session only placed buildings, so every unlock container stayed at
its initial value for the whole run and the checksum never saw them change. It
now destroys the enemy stations twice and takes the offered schematic choice,
so awarded groups, per-schematic levels and the derived recipe/item sets are
exercised too.

Adds a test that pins down that unlock state actually reaches the checksum: two
sessions in lockstep, one takes the choice, checksums must diverge. The
scripted-session tests cannot show this themselves — they compare runs against
each other, so they pass whether or not UnlockState is in the fold. Verified by
temporarily removing the fold: the new test fails, the old two do not.

The first choice is asserted rather than assumed, so a config change that stops
offering a group fails loudly instead of silently dropping the coverage.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-04 15:08:04 +02:00
10ba226af7 wire Simulation to forward schematic/unlock queries to UnlockState
Simulation now owns an UnlockState member (constructed before
initializeSubsystems(), since BuildingSystem's spawn-gating lambda
calls into it via isSchematicUnlocked instead of poking the old
m_schematicLevels map directly). The public isXUnlocked accessors
become one-line forwards, applySchematicChoice's group-awarding block
becomes a single awardUnlockGroup() call, and generateSchematicChoices
(which still owns m_rng and must not change call ordering) now reads
group state and builds options through UnlockState.

The checksum fold at computeStateChecksum's schematic/unlock section
had to move together with the containers it reads; UnlockState::
appendChecksum makes the identical seven appendSchematicMap/
appendStringSet calls in the identical order, so the fold is
unaffected. Verified via a temporary golden-checksum test case
(added, checked, then removed) that tick1/100/999/1999 checksums for
seed 12345 are byte-identical to pre-refactor.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-04 14:10:52 +02:00
0a3288d1d1 add UnlockState class for schematic/unlock bookkeeping
Simulation.h/.cpp had grown a large block of schematic/unlock state
(containers, the implicit-unlock traversal, checksum folding) that has
nothing to do with tick orchestration. Split it into its own class so
Simulation stays legible as "tick orchestration + subsystem handles".

This commit only adds the new UnlockState.h/.cpp (registered in
CMakeLists.txt) with the containers, types, and logic moved in
verbatim; nothing references it yet, so this is a no-op for behavior.
Simulation is wired to use it in the next commit.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-04 14:09:28 +02:00
2c9433cea6 move the shared TOML helpers into the utility namespace
The names are generic (makeError, requireInt, parseFile), and at global scope
with external linkage they would form an overload set with the same-named
anonymous-namespace helpers in VisualsLoader.cpp and BalancingConfig.cpp the
moment either file includes TomlHelpers.h — silently, since the signatures
differ. Namespacing keeps that door shut.

Call sites are qualified explicitly rather than pulled in with a using
directive, matching how utility::getRandomInt and friends are already called.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-04 09:27:43 +02:00
21eb6ad096 extract loadUnlocks into ConfigLoaderUnlocks.cpp
Finishes the ConfigLoader.cpp domain split. ConfigLoader.cpp now holds
only the cross-domain validateUnlocks pass and loadFromDirectory
orchestrator, as intended — everything else lives in its own
ConfigLoader<Domain>.cpp. Pure move; no logic change.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 22:38:10 +02:00
bf579bb76e extract loadModules into ConfigLoaderModules.cpp
Continues the ConfigLoader.cpp domain split. StatEntry/kKnownStats are
only used by loadModules, so they move along as a domain-local
anonymous-namespace table rather than into TomlHelpers. Pure move; no
logic change.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 22:36:08 +02:00
6bde69dc33 extract loadStations into ConfigLoaderStations.cpp
Continues the ConfigLoader.cpp domain split. Pure move of loadStations;
no domain-specific helpers to relocate here.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 22:33:40 +02:00
bf99cd0694 extract loadShips into ConfigLoaderShips.cpp
Continues the ConfigLoader.cpp domain split. parseRotationString and
parsePlacedModules are only used by loadShips, so they move along as
domain-local anonymous-namespace helpers rather than into TomlHelpers.
Pure move; no logic change.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 22:31:39 +02:00
6f107e3479 extract loadRecipes into ConfigLoaderRecipes.cpp
Continues the ConfigLoader.cpp domain split. parseRecipeOutputs is only
used by loadRecipes, so it moves along as a domain-local anonymous-
namespace helper rather than into TomlHelpers. Pure move; no logic
change.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 22:29:17 +02:00
8f42519911 extract loadBuildings into ConfigLoaderBuildings.cpp
Continues the ConfigLoader.cpp domain split. Pure move of loadBuildings;
no domain-specific helpers to relocate here.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 22:27:05 +02:00
bb7b90f9ea extract loadWorld into ConfigLoaderWorld.cpp
Continues the ConfigLoader.cpp domain split: world.toml parsing has no
domain-specific helpers of its own, so this is a straight move of
loadWorld with no logic change.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 22:25:14 +02:00
e702c01005 extract shared TOML helpers into TomlHelpers.h/.cpp
ConfigLoader.cpp had grown to 877 lines by mixing generic TOML-parsing
helpers (used by every per-file loader) with per-domain parsing logic.
Splitting the file per config domain first requires pulling out the
helpers shared by two or more domains, so each domain .cpp can include
them without duplication. Pure move: no logic, message, or ordering
changes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 22:22:48 +02:00
b1720dc2b3 drop the now-dead payloads from the sim-backed state-change events
TickAdvanced, BuildingBlocksChanged, ExpansionCostChanged, BossWaveUpdated and
ArtifactCountChanged all duplicated state the Simulation already owns. With
every subscriber re-reading from the sim, the fields had no readers left, so
the five events become payload-free refresh signals and the emitters keep the
values only as locals for change detection.

This makes the convention uniform: a state-change event backed by the
simulation carries nothing. Events whose state lives in the view (selection,
game speed, deconstruct and debug-draw modes) keep their payloads, since there
is no sim getter behind them.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 21:56:47 +02:00
099f0b55fc make HeaderBar read the tick, artifacts and boss wave from the simulation
The last three payloads HeaderBar still consumed as truth. All are backed by
Simulation getters (getCurrentTick, getArtifactCount, getBossWaveCounter,
getBossCountdownTicks) and the win count by world.artifacts.artifactWinCount,
so the handlers now re-read rather than trust the event.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 21:54:02 +02:00
b133a21914 make BlueprintPanel read the block stock from the simulation
BlueprintPanel was the second panel caching BuildingBlocksChangedEvent's
payload as truth; it already held a Simulation*, so refreshButtonStates()
now re-reads getBuildingBlocksStock() at the point of use, per the "events
are refresh signals" rule.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 21:53:19 +02:00
69848eb9f8 remove the last duplicate findModuleDef from ShipLayoutPreview
ShipLayoutPreview was the one widget the findFooDef sweep missed: it held a
bare const std::vector<ModuleDef>* rather than a config, so the shared
ModulesConfig::findModuleDef was not a drop-in and the file-local copy
survived. Hold the ModulesConfig instead and call the shared finder.

The sole caller already had the ModulesConfig one dereference away.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
2026-08-03 21:28:53 +02:00
993325d97c remove duplicate findBuildingDef from BuildingSystem
BuildingSystem::findBuildingDef was a byte-equivalent re-implementation of
BuildingsConfig::findBuildingDef. Same cleanup as the GameWorldView copy;
this one sits in the sim layer, so it was missed by both earlier passes.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 21:34:24 +02:00
75f306f650 route the win-path restart through ResetCommand
The Restart button on the Win dialog called Simulation::reset() directly,
while the escape-menu and Game Over restarts enqueue a ResetCommand. That
bypassed the command chokepoint every sim mutation is supposed to flow
through (docs/replay_design.md), so a post-win restart was never recorded
into the replay stream, and it had UI code calling a sim mutator directly.

Mirror the Game Over path instead. The manual resetForNewGame() call goes
away with it: GameWorldView::onFrame already resets the view when it drains
a Reset command.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 21:31:27 +02:00
0eb9c97e5d dedupe AttackExecutor and RepairExecutor via executeOrbitAndAssign
The two executors were line-for-line duplicates: orbit the behavior target,
then hand it to the owner's in-range modules. Both now call a templated
executeOrbitAndAssign<Behavior, ModuleComponent>; the view types, iteration
order, and sequence of component writes are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 21:16:15 +02:00
28d0416458 add ModalPauseScope for the pause-around-modal idiom
Four MainWindow sites hand-rolled snapshot speed / setGameSpeed(0) / modal /
restore + resetFrameTimer, with the restore duplicated on early-return paths.
ModalPauseScope does it via RAII, with restore()/release() for the two sites
that must restore early or not at all.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 21:12:53 +02:00
59fde8dbbc extract MainWindow::reloadConfig
The three restart paths each repeated the same config + visuals reload with
its own try/catch and error dialog. Only that shared part is extracted; each
site keeps its own follow-up (ResetCommand vs. direct Simulation::reset) and
its own error-path cleanup.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 21:06:41 +02:00
5355f9f77d drop EntityAdmin::add in favour of addComponent
The private add<T> template was identical to the public addComponent<T>;
the spawn factory methods now use the public one.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 21:04:03 +02:00
f678dab387 share a single ItemIconCache across the UI
Four separate caches rasterized the same item SVGs, one of them rebuilt on
every recipe-dialog open. MainWindow now owns one cache and hands a
non-owning pointer to HeaderBar, BuildButtonGrid, GameWorldView and
RecipeSelectionDialog.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 21:00:19 +02:00
ebee62166d lift the shared Centroid helper into ai/Centroid.h
AdvanceExecutor and StandbyExecutor each carried a verbatim copy of the
struct in an anonymous namespace; it now sits next to OrbitMath.h.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 20:55:29 +02:00
3c549a160c share one loadTestConfig() helper across the tests
19 test translation units each defined an identical local loadConfig().
They now include src/test/TestConfig.h, which lives off the lib/ui/app
include path like SimulationTestAccess.h.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 20:53:21 +02:00
dc58f6ea32 make HeaderBar read block stock and expansion cost from the simulation
HeaderBar was the only panel caching event payloads as truth. It now holds
a const Simulation* and re-reads getBuildingBlocksStock() /
getCurrentExpansionCost() in the handlers, per the "events are refresh
signals" rule.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 20:49:38 +02:00
5bd804601c dedupe tunnel lookup and key tunnel tiles by QPoint
The identical 7-line TunnelLookup lambda existed in updateTunnelGhost and
drawSelectedTunnelConnections; it is now GameWorldView::makeTunnelLookup.
The tile key moved from std::pair<int, int> to QPoint with the existing
QPointCompare comparator, dropping the manual packing at every site.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 20:47:28 +02:00
92a4f02cef extract Simulation::initializeSubsystems
The constructor and reset() held a character-for-character identical
26-line subsystem construction block, including three capturing lambdas.
Both run before the first tick, so the closures can be shared. Order is
unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 20:44:43 +02:00
84d32b6c16 add findShipDef/findModuleDef/findRecipeDef to config structs
Ships/Modules/RecipesConfig now carry lookup helpers mirroring
BuildingsConfig::findBuildingDef. The hand-rolled linear scans in
BuildingSystem, ShipSystem, ShipStatsCalculator, ThreatCostCalculator,
ShipLayoutDialog, SelectedBuildingPanel and SchematicChoiceDialog now
call them instead.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 20:42:24 +02:00
d31ff68ab7 remove duplicate findBuildingDef from GameWorldView
GameWorldView::findBuildingDef was a byte-equivalent re-implementation of
BuildingsConfig::findBuildingDef. All call sites now use the config helper,
matching SelectedBuildingPanel and BlueprintPanel.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 20:34:56 +02:00
b722955f7e fix splitter filters being lost when rotating in place
rotateInPlace re-implemented the belt-tile re-registration switch inline
instead of calling reregisterBeltTile, and its splitter branch omitted the
setSplitterFilters call the canonical version has. Since an operational
splitter keeps its filters only in BeltSystem, removeTile discarded them and
rotating a configured splitter silently reset it to "accept all".

Replace the duplicated switch with a call to reregisterBeltTile, capturing
the filters beforehand via getSplitterInfo — the same idiom deconstruct
already uses. This removes the second copy of the switch that allowed the
two to drift apart in the first place.

Add a regression test; the existing [rotate-in-place] cases covered belt
tiles only, which is why this went unnoticed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GH8ZMRY3vhxxXcaUxBqxkk
2026-08-02 20:22:53 +02:00
370a3036c1 fix issue where HP bar is drawn below belts 2026-08-02 15:57:44 +02:00
d1051607b2 add spacing between items in header bar 2026-07-23 21:20:56 +02:00
79b79ab7c3 Show building_block icon in header stock, expand button and build costs 2026-07-23 21:13:37 +02:00
f766ae4a86 Allow to draw produced-item icons in recipe dialog and game world 2026-07-23 20:54:04 +02:00
8b71fe1a03 Rename ship/station scrap drop entities to "debris" 2026-07-23 20:51:05 +02:00
11daa61714 Add refund-percentage tooltip to Deconstruct button 2026-07-23 20:46:39 +02:00
60d6767d93 draw building icons in the game world 2026-07-23 20:45:50 +02:00
7c1455b8a0 add icons for build buttons 2026-07-22 21:44:20 +02:00
e20a0bba67 Rename Demolish to Deconstruct 2026-07-22 21:40:42 +02:00
b2ce20e6ad Add deconstruction queue 2026-07-22 21:37:56 +02:00
a9082c57f3 Implement config-driven unlock groups so that multiple things can be unlocked at once (including buildings) 2026-07-22 21:34:59 +02:00
a8a6a04f1e Highlight tunnel connections in green when selected 2026-07-21 21:16:21 +02:00
a75222f111 Never rotate tunnels in place 2026-07-21 21:14:27 +02:00
9b63af6ccb Unify tunnel build mode into a single Tunnel button 2026-07-21 21:12:06 +02:00
4ca5b332cd Snap belt-drag end tile to a building's input edge 2026-07-21 21:09:07 +02:00
0c4eb480be Update belt drag L-path immediately on rotate 2026-07-21 21:06:54 +02:00
b2c1ea34fd Implement deferred L-shaped belt drag placement 2026-07-21 21:05:31 +02:00