Compare commits
4 Commits
1641189b75
...
3716c2b734
| Author | SHA1 | Date | |
|---|---|---|---|
| 3716c2b734 | |||
| 5317f35198 | |||
| ed17664ef1 | |||
| 49f7129bd5 |
@@ -119,7 +119,7 @@ multiplied_attack_rate_hz_formula = "0.8"
|
|||||||
id = "laser_cannon_xs"
|
id = "laser_cannon_xs"
|
||||||
unlock_at_station_level = -1
|
unlock_at_station_level = -1
|
||||||
surface_mask = ["O"]
|
surface_mask = ["O"]
|
||||||
materials = [{item = "laser_cannon_xs_module", amount = 1}]
|
materials = [{item = "iron_ore", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 0.5
|
production_time_seconds = 0.5
|
||||||
threat_cost = 5.0
|
threat_cost = 5.0
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ layout = ["O"]
|
|||||||
default_modules = [{type = "laser_cannon_xs", x = 0, y = 0, rotation = "east"}]
|
default_modules = [{type = "laser_cannon_xs", x = 0, y = 0, rotation = "east"}]
|
||||||
|
|
||||||
[ship.schematic]
|
[ship.schematic]
|
||||||
materials = [{item = "drone_hull", amount = 1}]
|
materials = [{item = "iron_ore", amount = 1}]
|
||||||
player_production_level = 1
|
player_production_level = 1
|
||||||
production_time_seconds = 5
|
production_time_seconds = 5
|
||||||
|
|
||||||
|
|||||||
@@ -59,23 +59,43 @@ Simulation types shared across subsystems:
|
|||||||
- `Item` — `struct Item { ItemType type; }`. Items on belts have no persistent identity across ticks.
|
- `Item` — `struct Item { ItemType type; }`. Items on belts have no persistent identity across ticks.
|
||||||
- `Port` — `struct Port { QPoint tile; Rotation direction; }`. Identifies a belt-adjacent cell and the direction of flow across that cell.
|
- `Port` — `struct Port { QPoint tile; Rotation direction; }`. Identifies a belt-adjacent cell and the direction of flow across that cell.
|
||||||
- `MovementIntent` — `struct MovementIntent { int priority; QVector2D target; }`. Priority follows the order declared under Movement Arbitration. Cleared at the start of each tick; the highest-priority write wins; `tickMovement` reads the winner.
|
- `MovementIntent` — `struct MovementIntent { int priority; QVector2D target; }`. Priority follows the order declared under Movement Arbitration. Cleared at the start of each tick; the highest-priority write wins; `tickMovement` reads the winner.
|
||||||
- `FireEvent` — `struct FireEvent { EntityId shooter; EntityId target; Tick emittedAt; }`. Transient record emitted each time a weapon fires (REQ-SHP-FIRING, REQ-SHP-FIRING-BEAM). Buffered in a sim-owned queue and drained by the renderer; see Sim → UI Events.
|
- `WeaponFiredEvent` — `struct WeaponFiredEvent : public Event { entt::entity shooter; entt::entity target; Tick emittedAt; }`. Transient record emitted each time a weapon fires (REQ-SHP-FIRING, REQ-SHP-FIRING-BEAM). Buffered in a sim-owned vector during the tick, then drained and re-emitted via EventManager by the UI frame handler; see Sim → UI Events.
|
||||||
- `SchematicDropEvent` — `struct SchematicDropEvent { ShipSchematicId schematic; int newLevel; bool wasNewUnlock; }`. Emitted when a destroyed enemy-defence-station set awards a schematic (REQ-DEF-SCHEMATIC-DROP). The UI renders a toast (REQ-UI-SCHEMATIC-TOAST); `wasNewUnlock` chooses between the "unlocked" and "level → N" wording.
|
- `SchematicChoiceOption` — `struct SchematicChoiceOption { string schematicId; SchematicType type; string displayName; bool isNewUnlock; int targetLevel; }`. Describes one option in the schematic choice dialog (REQ-DEF-SCHEMATIC-DROP). Up to three are generated when an enemy station set is destroyed. `SchematicType` is `Ship`, `Module`, or `Recipe`.
|
||||||
|
- `SchematicChoicesAvailableEvent` — EventManager event carrying a `vector<SchematicChoiceOption>`. Sent by the UI each frame when pending choices are detected; handled by `MainWindow` which opens the schematic choice dialog.
|
||||||
|
|
||||||
## Sim → UI Events
|
## Event System
|
||||||
|
|
||||||
The sim owns a small set of per-frame event queues that the UI drains on each render. These carry one-shot signals that are not derivable from persistent state — currently weapon fires (REQ-SHP-FIRING-BEAM) and schematic drops (REQ-UI-SCHEMATIC-TOAST). Additional event types can be added here later (e.g., building-complete, unit-death flashes) without changing the pattern.
|
All inter-component communication — both sim→UI and UI→UI — uses a unified `EventManager`/`EventHandler` system. No custom Qt signals/slots are used for inter-widget communication.
|
||||||
|
|
||||||
Implementation: a plain `std::vector<FireEvent>` owned by `Simulation`, one vector per event type. Combat resolution (tick-order step 8) appends to it. The UI calls `simulation.drainFireEvents()` once per rendered frame, which returns the accumulated vector by move and clears the internal one. Beams are tracked by the renderer for 0.3 s of wall time (9 ticks at 30 Hz) using the events' `emittedAt` tick, then discarded. If either the shooter or target entity is gone when the renderer looks them up, the beam is dropped early.
|
### EventManager
|
||||||
|
|
||||||
We deliberately do **not** use `QObject` signals/slots or `QEvent`:
|
`EventManager` is a singleton (`EventManager::getInstance()`) that routes events to registered handlers.
|
||||||
|
|
||||||
- **Determinism.** A plain ordered vector preserves tick-order exactly; the queue is part of per-tick state, inspectable in tests.
|
- `sendEventImmediately(shared_ptr<Event>)` — synchronous dispatch to all handlers of the event's type.
|
||||||
- **Sim/UI seam.** The sim exposes pull-style access only; the UI never subscribes into the sim, keeping the simulation/presentation split clean.
|
- `addEvent(shared_ptr<Event>)` — queues the event for later batch processing.
|
||||||
- **Headless testability.** Catch2 tests read the queue directly after `tick()`; no event loop, no `QApplication`.
|
- `processEvents()` — drains the queue, dispatching each event to its handlers.
|
||||||
- **Zero overhead.** Sim types remain plain structs — no `QObject`, no moc, no signal dispatch machinery.
|
|
||||||
|
|
||||||
If the number of event types grows past a handful, we can wrap them in a small `EventQueue<T>` template, still owned by the sim. Signals/slots would only be warranted if we needed multiple independent subscribers or cross-thread dispatch, and we need neither.
|
The EventManager is thread-safe (mutex-guarded).
|
||||||
|
|
||||||
|
### EventHandler
|
||||||
|
|
||||||
|
`EventHandler<T>` is a CRTP-style template that a class inherits to receive events of type `T`. It provides `registerForEvent()` / `unregisterForEvent()` and requires an override of `handleEvent(shared_ptr<const T>)`.
|
||||||
|
|
||||||
|
`CombinedEventHandler<Ts...>` is a variadic template for classes that handle multiple event types. It provides `registerForEvents()` / `unregisterForEvents()` and requires one `handleEvent` override per type.
|
||||||
|
|
||||||
|
### Sim → UI Events
|
||||||
|
|
||||||
|
The simulation layer stays free of EventManager — it uses a plain `std::vector<WeaponFiredEvent>` internally (owned by `CombatSystem`). This preserves determinism, tick-order fidelity, and headless testability (Catch2 tests read the queue directly via `drainWeaponFiredEvents()` after `tick()`).
|
||||||
|
|
||||||
|
The UI frame handler (`GameWorldView::onFrame` / `ArenaView::onFrame`) bridges the gap: each frame it calls `simulation.drainWeaponFiredEvents()`, then re-emits each `WeaponFiredEvent` via `EventManager::sendEventImmediately()`. Subscribers (the same view's `handleEvent(WeaponFiredEvent)`) create `ActiveBeam` records tracked for 0.3 s of wall time, then discarded. If either the shooter or target entity is gone when the renderer looks them up, the beam is dropped early.
|
||||||
|
|
||||||
|
Schematic drops: when an enemy station set is destroyed, the simulation generates up to 3 `SchematicChoiceOption` entries and stores them as pending state. The UI polls `hasSchematicChoicesPending()` each frame and, when true, sends a `SchematicChoicesAvailableEvent` via EventManager. `MainWindow` handles this event by pausing the game and opening a modal `SchematicChoiceDialog`. The player's selection is fed back via `applySchematicChoice(index)`.
|
||||||
|
|
||||||
|
### UI Events
|
||||||
|
|
||||||
|
All UI interactions — building selection, builder/blueprint mode transitions, speed changes, demolish mode, escape menu, layout dialog requests — are communicated via EventManager events rather than Qt signals/slots. Each event is a small struct inheriting `Event` (e.g., `SelectionChangedEvent`, `BuildingTypeSelectedEvent`, `SpeedChangeRequestedEvent`). Widgets register as `CombinedEventHandler` for the events they care about and emit events via `EventManager::sendEventImmediately()`.
|
||||||
|
|
||||||
|
Bidirectional interactions use separate request/notification event types to avoid infinite recursion (e.g., `ExitBuilderModeRequestedEvent` from `BuildButtonGrid` → `GameWorldView`, vs. `BuilderModeExitedEvent` from `GameWorldView` → `BuildButtonGrid`).
|
||||||
|
|
||||||
## Tick Order
|
## Tick Order
|
||||||
|
|
||||||
@@ -88,8 +108,8 @@ Within a single simulation tick, subsystems run in this fixed order. The order i
|
|||||||
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT).
|
5. **Building → belt push** — buildings push items from output buffer onto the belt tile at their output port (REQ-MAT-OUTPUT-PORT).
|
||||||
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
|
6. **Belt tick** — advance items along belt tiles; apply splitter routing (REQ-BLD-SPLITTER).
|
||||||
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then run `tickThreatResponse`, `tickScrapCollector`, `tickRepairBehavior`, `tickHomeReturn` in any order (arbitration is via intent priority).
|
7. **Ship behavior systems** — clear `MovementIntent` on each ship, then run `tickThreatResponse`, `tickScrapCollector`, `tickRepairBehavior`, `tickHomeReturn` in any order (arbitration is via intent priority).
|
||||||
8. **Combat resolution** — ships and defence stations acquire targets, fire, apply damage; queue deaths. Each fire appends a `FireEvent` to the sim's fire-event queue (REQ-SHP-FIRING-BEAM).
|
8. **Combat resolution** — ships and defence stations acquire targets, fire, apply damage; queue deaths. Each fire appends a `WeaponFiredEvent` to the sim's weapon-fired-event queue (REQ-SHP-FIRING-BEAM).
|
||||||
9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP); if a full enemy-defence-station set was destroyed this tick, award one schematic (REQ-DEF-SCHEMATIC-DROP) and append a `SchematicDropEvent`; remove entities.
|
9. **Deaths & loot** — process queued deaths: drop scrap (REQ-RES-SCRAP-DROP); if a full enemy-defence-station set was destroyed this tick, generate up to 3 schematic choice options (REQ-DEF-SCHEMATIC-DROP) stored as pending state for the UI to present; remove entities.
|
||||||
10. **`tickMovement`** — advance ship positions based on final `MovementIntent`.
|
10. **`tickMovement`** — advance ship positions based on final `MovementIntent`.
|
||||||
11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP).
|
11. **Scrap despawn** — decrement scrap timers; remove expired scrap (REQ-RES-SCRAP-DROP).
|
||||||
|
|
||||||
@@ -280,7 +300,7 @@ The game world is rendered by a single `GameWorldView` widget that inherits `QOp
|
|||||||
|
|
||||||
### Threading
|
### Threading
|
||||||
|
|
||||||
Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly without locks. If profiling later justifies moving the sim to a worker thread, the pull-style `drainFireEvents()` / `drainSchematicDropEvents()` / `forEachVisualItem()` APIs already support a clean snapshot-and-render split; a single mutex at the sim boundary would suffice.
|
Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly without locks. If profiling later justifies moving the sim to a worker thread, the pull-style `drainWeaponFiredEvents()` / `getPendingSchematicChoices()` / `applySchematicChoice()` / `forEachVisualItem()` APIs already support a clean snapshot-and-render split; a single mutex at the sim boundary would suffice. The `ArenaSimulation` used by the balancing tool runs headlessly on a worker thread; fire events accumulate in its internal vector and are only drained when `ArenaView` drives `tickOnce()` on the main thread during interactive inspection.
|
||||||
|
|
||||||
### Layer Order (back to front)
|
### Layer Order (back to front)
|
||||||
|
|
||||||
@@ -289,9 +309,9 @@ Sim and UI run on the same thread for v1. `paintEvent` reads sim state directly
|
|||||||
3. **Belt items** — 10×10 colored squares emitted by `BeltSystem::forEachVisualItem`.
|
3. **Belt items** — 10×10 colored squares emitted by `BeltSystem::forEachVisualItem`.
|
||||||
4. **Scrap** — glyphs at world positions.
|
4. **Scrap** — glyphs at world positions.
|
||||||
5. **Ships** — colored arrows oriented by velocity; color keyed to role (player combat / salvage / repair / enemy).
|
5. **Ships** — colored arrows oriented by velocity; color keyed to role (player combat / salvage / repair / enemy).
|
||||||
6. **Laser beams** — lines derived from live `FireEvent`s kept by the renderer for 0.3 s (REQ-SHP-FIRING-BEAM).
|
6. **Laser beams** — lines derived from live `WeaponFiredEvent`s kept by the renderer for 0.3 s (REQ-SHP-FIRING-BEAM).
|
||||||
7. **Build overlays** — ghost in builder mode (REQ-BLD-GHOST), demolish-mode tint, tile highlight under cursor, box-drag selection rectangle.
|
7. **Build overlays** — ghost in builder mode (REQ-BLD-GHOST), demolish-mode tint, tile highlight under cursor, box-drag selection rectangle.
|
||||||
8. **Screen-space UI** — schematic toasts (REQ-UI-SCHEMATIC-TOAST) and any other screen-anchored elements, drawn after resetting the world-space transform.
|
8. **Screen-space UI** — screen-anchored elements, drawn after resetting the world-space transform.
|
||||||
|
|
||||||
### Coordinates and Scrolling
|
### Coordinates and Scrolling
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Config files use the TOML format. The following config files drive game paramete
|
|||||||
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, enemy ship level formula, belt speed, starting building blocks, departure interval.
|
- **world.toml** — world dimensions, region widths, expansion amounts, building refund percentage, wave timing, boss wave timing, enemy ship level formula, belt speed, starting building blocks, departure interval.
|
||||||
- **buildings.toml** — building block cost and construction time per building type.
|
- **buildings.toml** — building block cost and construction time per building type.
|
||||||
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlock_at_station_level` (integer): -1 means the recipe is explicitly unlocked at game start; a value ≥ 0 means the recipe starts locked and a schematic for it can be awarded via defence station destruction (see REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP).
|
- **recipes.toml** — crafting recipes: inputs, outputs, quantities, durations, and reprocessing plant probabilities. Assembler recipe entries may optionally define `unlock_at_station_level` (integer): -1 means the recipe is explicitly unlocked at game start; a value ≥ 0 means the recipe starts locked and a schematic for it can be awarded via defence station destruction (see REQ-LOCK-EXPLICIT, REQ-DEF-SCHEMATIC-DROP).
|
||||||
- **ships.toml** — per schematic: a human-readable display name (used in toasts and UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as formulas of ship level, required build materials, threat cost formula, player production level, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
|
- **ships.toml** — per schematic: a human-readable display name (used in the UI), hull stats (HP, max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, max rotation speed) as formulas of ship level, required build materials, threat cost formula, player production level, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the schematic already unlocked), a layout grid defining the ship's module slots, and a `default_modules` list used for enemy wave ships (see REQ-WAV-DEFAULT-MODULES).
|
||||||
- **modules.toml** — per module type: id, surface mask, materials list, initial player production level, production time, threat cost, fill color, glyph, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the module schematic already unlocked), and an optional capability section and/or stat modifier formulas. A module with a capability section (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas is a **capability module** that grants the ship a weapon, salvage bay, or repair tool per instance (see REQ-MOD-CONFIG for the full list of formulas per capability type). A module with only `added_*`/`multiplied_*` formulas is a **passive module** that modifies stats on the ship or on capability module instances (see REQ-MOD-STAT-CALC).
|
- **modules.toml** — per module type: id, surface mask, materials list, initial player production level, production time, threat cost, fill color, glyph, the station level at which the schematic becomes available for unlock (`unlock_at_station_level`; -1 means the player starts with the module schematic already unlocked), and an optional capability section and/or stat modifier formulas. A module with a capability section (`[module.weapon]`, `[module.salvage]`, or `[module.repair]`) containing base stat formulas is a **capability module** that grants the ship a weapon, salvage bay, or repair tool per instance (see REQ-MOD-CONFIG for the full list of formulas per capability type). A module with only `added_*`/`multiplied_*` formulas is a **passive module** that modifies stats on the ship or on capability module instances (see REQ-MOD-STAT-CALC).
|
||||||
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
- **stations.toml** — HP, damage, range, fire rate, and scrap drop for player and enemy defence stations, defined as formulas of station level.
|
||||||
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship schematic, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
- **visuals.toml** — rendering-only config (not game parameters): fill and outline colors and glyphs for every building type, item type, ship schematic, and station type; beam color and width; overlay and toast colors. Loaded by the UI at startup; the simulation does not read it.
|
||||||
@@ -169,7 +169,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
|
|
||||||
Each repair module instance operates independently: it has its own repair rate (`repair_rate`) and repair range (`repair_range`). On each tick, a module first attempts to heal the ship's current behavior-level navigation target if that target is within the module's `repair_range` and is damaged (HP above zero and below maximum HP). If those conditions are not met — because the target is out of the module's `repair_range`, already at full health, or destroyed — the module independently searches for the nearest damaged friendly (player ship or player defence station) within its own `repair_range` and heals that instead. If no valid target is found within range, the module idles. A ship with multiple repair modules can therefore heal different targets simultaneously. Navigation is driven solely by the behavior-level target; individual module fallback targets do not affect which direction the ship moves.
|
Each repair module instance operates independently: it has its own repair rate (`repair_rate`) and repair range (`repair_range`). On each tick, a module first attempts to heal the ship's current behavior-level navigation target if that target is within the module's `repair_range` and is damaged (HP above zero and below maximum HP). If those conditions are not met — because the target is out of the module's `repair_range`, already at full health, or destroyed — the module independently searches for the nearest damaged friendly (player ship or player defence station) within its own `repair_range` and heals that instead. If no valid target is found within range, the module idles. A ship with multiple repair modules can therefore heal different targets simultaneously. Navigation is driven solely by the behavior-level target; individual module fallback targets do not affect which direction the ship moves.
|
||||||
- REQ-SHP-ENEMY-AI: **Enemy ships** — engage the closest valid target (player defence station, HQ, or player ship) within their sensor range. If no target is in sensor range, they move toward the asteroid (leftward in world coordinates).
|
- REQ-SHP-ENEMY-AI: **Enemy ships** — engage the closest valid target (player defence station, HQ, or player ship) within their sensor range. If no target is in sensor range, they move toward the asteroid (leftward in world coordinates).
|
||||||
- REQ-SHP-SCHEMATICS: The player selects a schematic per shipyard by clicking it. New schematics are unlocked automatically when an enemy defence station set is destroyed (REQ-DEF-SCHEMATIC-DROP) — there is no physical loot to collect.
|
- REQ-SHP-SCHEMATICS: The player selects a schematic per shipyard by clicking it. New schematics are unlocked by destroying enemy defence station sets (REQ-DEF-SCHEMATIC-DROP) — there is no physical loot to collect.
|
||||||
|
|
||||||
## Ship Modules
|
## Ship Modules
|
||||||
|
|
||||||
@@ -270,13 +270,24 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- REQ-DEF-ENEMY-FIRE: Enemy defence stations automatically fire at player ships within range.
|
- REQ-DEF-ENEMY-FIRE: Enemy defence stations automatically fire at player ships within range.
|
||||||
- REQ-DEF-NO-CROSSFIRE: Enemy and player defence stations are never in each other's firing range.
|
- REQ-DEF-NO-CROSSFIRE: Enemy and player defence stations are never in each other's firing range.
|
||||||
- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the boss countdown is advanced (REQ-WAV-BOSS-ADVANCE), the scrollable area is extended (REQ-GW-PUSH-EXPAND), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP).
|
- REQ-DEF-PUSH: When both enemy defence stations in a set are destroyed, the boss countdown is advanced (REQ-WAV-BOSS-ADVANCE), the scrollable area is extended (REQ-GW-PUSH-EXPAND), a new set of enemy defence stations is placed at the new boundary, and exactly one schematic drop is awarded for the destroyed set (REQ-DEF-SCHEMATIC-DROP).
|
||||||
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop is automatic — no physical item to collect. A schematic is chosen uniformly at random from the eligible drop pool, which contains:
|
- REQ-DEF-SCHEMATIC-DROP: Each destroyed set of enemy defence stations awards exactly one schematic drop (not one per station). The drop opens a **schematic choice dialog** — a modal dialog that pauses the game (speed set to 0×; on close, speed is restored to what it was before the dialog opened). Up to three schematic options are drawn uniformly at random **without replacement** from the eligible drop pool. If the pool contains fewer than three entries, only that many options are shown. The eligible drop pool contains:
|
||||||
- All **ship schematics** and **module schematics** whose `unlock_at_station_level` is -1 or is ≤ the level of the destroyed station set.
|
- All **ship schematics** and **module schematics** whose `unlock_at_station_level` is -1 or is ≤ the level of the destroyed station set.
|
||||||
- All **assembler recipe schematics** whose `unlock_at_station_level` is ≥ 0 and ≤ the level of the destroyed station set, whose output item is currently implicitly unlocked (REQ-LOCK-IMPLICIT), and which have not yet been awarded.
|
- All **assembler recipe schematics** whose `unlock_at_station_level` is ≥ 0 and ≤ the level of the destroyed station set, whose output item is currently implicitly unlocked (REQ-LOCK-IMPLICIT), and which have not yet been awarded.
|
||||||
|
|
||||||
For a **ship or module schematic** drop: if the player does not yet have the schematic, it is unlocked (ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG)). If the player already has it, the schematic's `player_production_level` is incremented by 1 — for ship schematics, subsequent ships of that type are produced at a higher level; for module schematics, all instances of that module type use the higher level in their stat formulas. The player is notified via a toast (REQ-UI-SCHEMATIC-TOAST).
|
Each option in the dialog displays: the schematic name (ship `display_name` from `ships.toml`, module `id` from `modules.toml`, or the output item type for assembler recipes), the schematic type (ship, module, or assembler recipe), and whether selecting it would be a **new unlock** or a **level-up** (showing the target level for level-ups). Assembler recipe schematics are always new unlocks since they are removed from the pool once awarded.
|
||||||
|
|
||||||
For an **assembler recipe schematic** drop: the recipe is explicitly unlocked and becomes available in the assembler recipe-selection dropdown (subject to REQ-LOCK-UI-RECIPE). The schematic is removed from the drop pool permanently (REQ-LOCK-EXPLICIT). The implicit unlock set is recomputed (REQ-LOCK-IMPLICIT). No toast is shown.
|
Each option additionally displays a vertical list of item names labeled "Unlocks recipes for:", showing which recipes would newly become implicitly unlocked (REQ-LOCK-IMPLICIT) if this option were selected — specifically, the output items of miner recipes and assembler recipes (without `unlock_at_station_level`) that are not currently implicitly unlocked but would become so after applying this option's effect:
|
||||||
|
- For a ship or module schematic that would be a **new unlock**, its `materials` are added to the base set per REQ-LOCK-IMPLICIT step 1a before recomputation.
|
||||||
|
- For a ship or module schematic **level-up**, the implicit unlock set is unchanged, so the list is always empty.
|
||||||
|
- For an assembler recipe schematic, its output item is added to the base set per REQ-LOCK-IMPLICIT step 1b before recomputation.
|
||||||
|
|
||||||
|
Item names are deduplicated and sorted alphabetically. If no recipes would be newly unlocked, the list shows "None".
|
||||||
|
|
||||||
|
The player selects one option by clicking it. The selected schematic is applied and the dialog closes:
|
||||||
|
|
||||||
|
For a **ship or module schematic**: if the player does not yet have the schematic, it is unlocked (ship schematics unlock the corresponding shipyard selection; module schematics unlock the module type for placement in the layout configuration dialog (REQ-MOD-UI-DIALOG)). If the player already has it, the schematic's `player_production_level` is incremented by 1 — for ship schematics, subsequent ships of that type are produced at a higher level; for module schematics, all instances of that module type use the higher level in their stat formulas.
|
||||||
|
|
||||||
|
For an **assembler recipe schematic**: the recipe is explicitly unlocked and becomes available in the assembler recipe-selection dropdown (subject to REQ-LOCK-UI-RECIPE). The schematic is removed from the drop pool permanently (REQ-LOCK-EXPLICIT). The implicit unlock set is recomputed (REQ-LOCK-IMPLICIT).
|
||||||
|
|
||||||
## Progression & Locking
|
## Progression & Locking
|
||||||
|
|
||||||
@@ -354,13 +365,6 @@ The screen is divided into three vertical sections:
|
|||||||
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
|
- REQ-UI-PORT-GLYPH: Every output port of every building is indicated by a directional glyph drawn on the port's tile. The glyph is a `>` rotated to face the port's exit direction (`>` for East, `^` for North, `<` for West, `v` for South). It is drawn at the midpoint between the tile center and the tile edge that the port exits through (i.e. halfway from center toward the exit edge). The indicator is rendered for all building states: operational buildings, construction sites, and the builder-mode ghost. Buildings with multiple output ports (e.g. splitters) show one indicator per port.
|
||||||
- REQ-UI-HP-BARS: All entities with HP — the HQ, player and enemy defence stations, and player and enemy ships — render an HP bar below them. The bar is always visible regardless of current HP. The bar's filled portion represents the fraction of current HP to maximum HP.
|
- REQ-UI-HP-BARS: All entities with HP — the HQ, player and enemy defence stations, and player and enemy ships — render an HP bar below them. The bar is always visible regardless of current HP. The bar's filled portion represents the fraction of current HP to maximum HP.
|
||||||
- REQ-UI-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out.
|
- REQ-UI-NO-ZOOM: The view has a fixed zoom level; the player cannot zoom in or out.
|
||||||
- REQ-UI-SCHEMATIC-TOAST: When a schematic is unlocked or leveled up (REQ-DEF-SCHEMATIC-DROP), a transient notification toast appears in the top-right corner of the game world view for 4 seconds and then fades out. Toast text:
|
|
||||||
- **Ship schematic — new unlock**: `Schematic unlocked: <Ship Name>` (where `<Ship Name>` is `ships.toml [ship.schematic].display_name`).
|
|
||||||
- **Ship schematic — level-up (duplicate drop)**: `<Ship Name> production level → N` (where N is the new level).
|
|
||||||
- **Module schematic — new unlock**: `Module unlocked: <Module Id>` (where `<Module Id>` is the module's `id` from `modules.toml`).
|
|
||||||
- **Module schematic — level-up (duplicate drop)**: `<Module Id> production level → N` (where N is the new level).
|
|
||||||
|
|
||||||
If multiple toasts arrive in close succession, they stack vertically in a queue (most recent at the top) and each fades out independently after its own 4-second lifetime.
|
|
||||||
- REQ-UI-HOTKEYS: Global keyboard shortcuts:
|
- REQ-UI-HOTKEYS: Global keyboard shortcuts:
|
||||||
- **Space** — toggles pause. Pressing Space pauses (sets speed to 0×) and stores the previously selected non-zero speed; pressing Space again restores that speed.
|
- **Space** — toggles pause. Pressing Space pauses (sets speed to 0×) and stores the previously selected non-zero speed; pressing Space again restores that speed.
|
||||||
- **W** — increases game speed by one step in the sequence 0×, 0.5×, 1×, 2×, 4× (no wrap-around past 4×).
|
- **W** — increases game speed by one step in the sequence 0×, 0.5×, 1×, 2×, 4× (no wrap-around past 4×).
|
||||||
|
|||||||
@@ -259,9 +259,9 @@ void ArenaSimulation::tick()
|
|||||||
m_aiSystem->tickSalvageBehavior(m_admin, *m_scrapSystem, *m_buildingSystem);
|
m_aiSystem->tickSalvageBehavior(m_admin, *m_scrapSystem, *m_buildingSystem);
|
||||||
|
|
||||||
// Combat resolution (tick step 8).
|
// Combat resolution (tick step 8).
|
||||||
std::vector<FireEvent> fireEvents;
|
std::vector<WeaponFiredEvent> weaponFiredEvents;
|
||||||
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, fireEvents);
|
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, weaponFiredEvents);
|
||||||
m_fireEvents.insert(m_fireEvents.end(), fireEvents.begin(), fireEvents.end());
|
m_weaponFiredEvents.insert(m_weaponFiredEvents.end(), weaponFiredEvents.begin(), weaponFiredEvents.end());
|
||||||
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
||||||
|
|
||||||
// Deaths (tick step 9, simplified).
|
// Deaths (tick step 9, simplified).
|
||||||
@@ -393,10 +393,10 @@ void ArenaSimulation::tickOnce()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<FireEvent> ArenaSimulation::drainFireEvents()
|
std::vector<WeaponFiredEvent> ArenaSimulation::drainWeaponFiredEvents()
|
||||||
{
|
{
|
||||||
std::vector<FireEvent> result;
|
std::vector<WeaponFiredEvent> result;
|
||||||
result.swap(m_fireEvents);
|
result.swap(m_weaponFiredEvents);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
#include "FireEvent.h"
|
#include "WeaponFiredEvent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ public:
|
|||||||
void requestStop();
|
void requestStop();
|
||||||
|
|
||||||
void tickOnce();
|
void tickOnce();
|
||||||
std::vector<FireEvent> drainFireEvents();
|
std::vector<WeaponFiredEvent> drainWeaponFiredEvents();
|
||||||
|
|
||||||
ArenaStatus status() const;
|
ArenaStatus status() const;
|
||||||
bool isFinished() const;
|
bool isFinished() const;
|
||||||
@@ -104,7 +104,7 @@ private:
|
|||||||
int m_winnerTeam;
|
int m_winnerTeam;
|
||||||
std::atomic<bool> m_stopRequested;
|
std::atomic<bool> m_stopRequested;
|
||||||
|
|
||||||
std::vector<FireEvent> m_fireEvents;
|
std::vector<WeaponFiredEvent> m_weaponFiredEvents;
|
||||||
|
|
||||||
mutable std::mutex m_statusMutex;
|
mutable std::mutex m_statusMutex;
|
||||||
ArenaStatus m_status;
|
ArenaStatus m_status;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
#include "FacingComponent.h"
|
#include "FacingComponent.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
|
#include "GameSpeedChangedEvent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
@@ -45,6 +46,13 @@ ArenaView::ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals,
|
|||||||
connect(m_renderTimer, &QTimer::timeout, this, &ArenaView::onFrame);
|
connect(m_renderTimer, &QTimer::timeout, this, &ArenaView::onFrame);
|
||||||
m_renderTimer->start();
|
m_renderTimer->start();
|
||||||
m_frameTimer.start();
|
m_frameTimer.start();
|
||||||
|
|
||||||
|
registerForEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
ArenaView::~ArenaView()
|
||||||
|
{
|
||||||
|
unregisterForEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArenaView::setGameSpeed(double multiplier)
|
void ArenaView::setGameSpeed(double multiplier)
|
||||||
@@ -54,7 +62,8 @@ void ArenaView::setGameSpeed(double multiplier)
|
|||||||
m_prevNonZeroSpeed = multiplier;
|
m_prevNonZeroSpeed = multiplier;
|
||||||
}
|
}
|
||||||
m_gameSpeedMultiplier = multiplier;
|
m_gameSpeedMultiplier = multiplier;
|
||||||
emit speedChanged(multiplier);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<GameSpeedChangedEvent>(multiplier));
|
||||||
}
|
}
|
||||||
|
|
||||||
double ArenaView::gameSpeed() const
|
double ArenaView::gameSpeed() const
|
||||||
@@ -93,34 +102,17 @@ void ArenaView::onFrame()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit fire events via EventManager
|
||||||
{
|
{
|
||||||
const std::vector<FireEvent> fires = m_sim->drainFireEvents();
|
const std::vector<WeaponFiredEvent> fires = m_sim->drainWeaponFiredEvents();
|
||||||
for (const FireEvent& fe : fires)
|
for (const WeaponFiredEvent& fe : fires)
|
||||||
{
|
{
|
||||||
float maxRadius = 0.125f;
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
if (m_sim->admin().isValid(fe.target)
|
std::make_shared<WeaponFiredEvent>(fe));
|
||||||
&& m_sim->admin().hasAll<StationBodyComponent>(fe.target))
|
|
||||||
{
|
|
||||||
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(fe.target);
|
|
||||||
const int shorter = std::min(sb.footprint.width(),
|
|
||||||
sb.footprint.height());
|
|
||||||
maxRadius = shorter / 2.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
|
|
||||||
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
|
|
||||||
const float angle = angleDist(m_rng);
|
|
||||||
const float radius = radiusDist(m_rng);
|
|
||||||
|
|
||||||
ActiveBeam beam;
|
|
||||||
beam.event = fe;
|
|
||||||
beam.emittedWallMs = m_wallMs;
|
|
||||||
beam.targetOffset = QVector2D(radius * std::cos(angle),
|
|
||||||
radius * std::sin(angle));
|
|
||||||
m_activeBeams.push_back(beam);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expire old beams
|
||||||
{
|
{
|
||||||
std::vector<ActiveBeam> live;
|
std::vector<ActiveBeam> live;
|
||||||
for (const ActiveBeam& b : m_activeBeams)
|
for (const ActiveBeam& b : m_activeBeams)
|
||||||
@@ -136,12 +128,36 @@ void ArenaView::onFrame()
|
|||||||
if (m_sim->isFinished() && !m_finishedEmitted)
|
if (m_sim->isFinished() && !m_finishedEmitted)
|
||||||
{
|
{
|
||||||
m_finishedEmitted = true;
|
m_finishedEmitted = true;
|
||||||
emit finished();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ArenaView::handleEvent(std::shared_ptr<const WeaponFiredEvent> event)
|
||||||
|
{
|
||||||
|
float maxRadius = 0.125f;
|
||||||
|
if (m_sim->admin().isValid(event->target)
|
||||||
|
&& m_sim->admin().hasAll<StationBodyComponent>(event->target))
|
||||||
|
{
|
||||||
|
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(event->target);
|
||||||
|
const int shorter = std::min(sb.footprint.width(),
|
||||||
|
sb.footprint.height());
|
||||||
|
maxRadius = shorter / 2.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
|
||||||
|
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
|
||||||
|
const float angle = angleDist(m_rng);
|
||||||
|
const float radius = radiusDist(m_rng);
|
||||||
|
|
||||||
|
ActiveBeam beam;
|
||||||
|
beam.event = *event;
|
||||||
|
beam.emittedWallMs = m_wallMs;
|
||||||
|
beam.targetOffset = QVector2D(radius * std::cos(angle),
|
||||||
|
radius * std::sin(angle));
|
||||||
|
m_activeBeams.push_back(beam);
|
||||||
|
}
|
||||||
|
|
||||||
void ArenaView::paintGL()
|
void ArenaView::paintGL()
|
||||||
{
|
{
|
||||||
QPainter painter(this);
|
QPainter painter(this);
|
||||||
@@ -414,4 +430,3 @@ void ArenaView::drawBeams(QPainter& painter)
|
|||||||
worldToWidget(*targetPos + beam.targetOffset));
|
worldToWidget(*targetPos + beam.targetOffset));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "FireEvent.h"
|
#include "EventHandler.h"
|
||||||
|
#include "WeaponFiredEvent.h"
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
#include "EntitySelectedEvent.h"
|
#include "EntitySelectedEvent.h"
|
||||||
@@ -20,23 +21,21 @@
|
|||||||
class ArenaSimulation;
|
class ArenaSimulation;
|
||||||
class QPainter;
|
class QPainter;
|
||||||
|
|
||||||
class ArenaView : public QOpenGLWidget
|
class ArenaView : public QOpenGLWidget,
|
||||||
|
public EventHandler<WeaponFiredEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals,
|
ArenaView(ArenaSimulation* sim, const VisualsConfig* visuals,
|
||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
|
~ArenaView() override;
|
||||||
|
|
||||||
void setGameSpeed(double multiplier);
|
void setGameSpeed(double multiplier);
|
||||||
double gameSpeed() const;
|
double gameSpeed() const;
|
||||||
void togglePause();
|
void togglePause();
|
||||||
void stopRendering();
|
void stopRendering();
|
||||||
|
|
||||||
signals:
|
|
||||||
void speedChanged(double multiplier);
|
|
||||||
void finished();
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void paintGL() override;
|
void paintGL() override;
|
||||||
void mousePressEvent(QMouseEvent* event) override;
|
void mousePressEvent(QMouseEvent* event) override;
|
||||||
@@ -45,6 +44,8 @@ private slots:
|
|||||||
void onFrame();
|
void onFrame();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void handleEvent(std::shared_ptr<const WeaponFiredEvent> event) override;
|
||||||
|
|
||||||
void drawTiles(QPainter& painter);
|
void drawTiles(QPainter& painter);
|
||||||
void drawBuildings(QPainter& painter);
|
void drawBuildings(QPainter& painter);
|
||||||
void drawStations(QPainter& painter);
|
void drawStations(QPainter& painter);
|
||||||
@@ -62,7 +63,7 @@ private:
|
|||||||
|
|
||||||
struct ActiveBeam
|
struct ActiveBeam
|
||||||
{
|
{
|
||||||
FireEvent event;
|
WeaponFiredEvent event;
|
||||||
qint64 emittedWallMs;
|
qint64 emittedWallMs;
|
||||||
QVector2D targetOffset;
|
QVector2D targetOffset;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
ArenaWidget::ArenaWidget(const std::string& arenaName, QWidget* parent)
|
#include "ArenaInspectRequestedEvent.h"
|
||||||
|
#include "ArenaStartRequestedEvent.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
|
||||||
|
ArenaWidget::ArenaWidget(int arenaIndex, const std::string& arenaName, QWidget* parent)
|
||||||
: QFrame(parent)
|
: QFrame(parent)
|
||||||
|
, m_arenaIndex(arenaIndex)
|
||||||
, m_running(false)
|
, m_running(false)
|
||||||
, m_wasFinished(false)
|
, m_wasFinished(false)
|
||||||
{
|
{
|
||||||
@@ -31,11 +36,17 @@ void ArenaWidget::buildLayout(const std::string& arenaName)
|
|||||||
titleRow->addStretch();
|
titleRow->addStretch();
|
||||||
|
|
||||||
m_inspectButton = new QPushButton(tr("Inspect"), this);
|
m_inspectButton = new QPushButton(tr("Inspect"), this);
|
||||||
connect(m_inspectButton, &QPushButton::clicked, this, &ArenaWidget::inspectRequested);
|
connect(m_inspectButton, &QPushButton::clicked, this, [this]() {
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ArenaInspectRequestedEvent>(m_arenaIndex));
|
||||||
|
});
|
||||||
titleRow->addWidget(m_inspectButton);
|
titleRow->addWidget(m_inspectButton);
|
||||||
|
|
||||||
m_startButton = new QPushButton(tr("Start"), this);
|
m_startButton = new QPushButton(tr("Start"), this);
|
||||||
connect(m_startButton, &QPushButton::clicked, this, &ArenaWidget::startRequested);
|
connect(m_startButton, &QPushButton::clicked, this, [this]() {
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ArenaStartRequestedEvent>(m_arenaIndex));
|
||||||
|
});
|
||||||
titleRow->addWidget(m_startButton);
|
titleRow->addWidget(m_startButton);
|
||||||
|
|
||||||
outerLayout->addLayout(titleRow);
|
outerLayout->addLayout(titleRow);
|
||||||
|
|||||||
@@ -14,19 +14,16 @@ class ArenaWidget : public QFrame
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ArenaWidget(const std::string& arenaName, QWidget* parent = nullptr);
|
ArenaWidget(int arenaIndex, const std::string& arenaName, QWidget* parent = nullptr);
|
||||||
|
|
||||||
void updateStatus(const ArenaStatus& status);
|
void updateStatus(const ArenaStatus& status);
|
||||||
void startSimulation();
|
void startSimulation();
|
||||||
void resetToGrey();
|
void resetToGrey();
|
||||||
|
|
||||||
signals:
|
|
||||||
void startRequested();
|
|
||||||
void inspectRequested();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void buildLayout(const std::string& arenaName);
|
void buildLayout(const std::string& arenaName);
|
||||||
|
|
||||||
|
int m_arenaIndex;
|
||||||
QLabel* m_titleLabel;
|
QLabel* m_titleLabel;
|
||||||
QLabel* m_team1Header;
|
QLabel* m_team1Header;
|
||||||
QLabel* m_team2Header;
|
QLabel* m_team2Header;
|
||||||
|
|||||||
@@ -48,14 +48,17 @@ BalancingWindow::BalancingWindow(const BalancingConfig& balancingConfig,
|
|||||||
m_pollTimer = new QTimer(this);
|
m_pollTimer = new QTimer(this);
|
||||||
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
|
connect(m_pollTimer, &QTimer::timeout, this, &BalancingWindow::pollStatuses);
|
||||||
m_pollTimer->start(100);
|
m_pollTimer->start(100);
|
||||||
|
|
||||||
|
registerForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
BalancingWindow::~BalancingWindow()
|
BalancingWindow::~BalancingWindow()
|
||||||
{
|
{
|
||||||
|
unregisterForEvents();
|
||||||
|
|
||||||
m_pollTimer->stop();
|
m_pollTimer->stop();
|
||||||
if (m_inspectWindow)
|
if (m_inspectWindow)
|
||||||
{
|
{
|
||||||
m_inspectWindow->disconnect(this);
|
|
||||||
delete m_inspectWindow;
|
delete m_inspectWindow;
|
||||||
m_inspectWindow = nullptr;
|
m_inspectWindow = nullptr;
|
||||||
}
|
}
|
||||||
@@ -81,16 +84,11 @@ void BalancingWindow::populateArenas(const BalancingConfig& balancingConfig)
|
|||||||
entry.config = arenaConfig;
|
entry.config = arenaConfig;
|
||||||
entry.simulation = std::make_unique<ArenaSimulation>(
|
entry.simulation = std::make_unique<ArenaSimulation>(
|
||||||
m_gameConfig, arenaConfig, m_nextSeed++);
|
m_gameConfig, arenaConfig, m_nextSeed++);
|
||||||
entry.widget = new ArenaWidget(arenaConfig.name, scrollContent);
|
entry.widget = new ArenaWidget(index, arenaConfig.name, scrollContent);
|
||||||
contentLayout->addWidget(entry.widget);
|
contentLayout->addWidget(entry.widget);
|
||||||
|
|
||||||
entry.widget->updateStatus(entry.simulation->status());
|
entry.widget->updateStatus(entry.simulation->status());
|
||||||
|
|
||||||
connect(entry.widget, &ArenaWidget::startRequested,
|
|
||||||
this, [this, index]() { startArena(index); });
|
|
||||||
connect(entry.widget, &ArenaWidget::inspectRequested,
|
|
||||||
this, [this, index]() { inspectArena(index); });
|
|
||||||
|
|
||||||
m_arenas.push_back(std::move(entry));
|
m_arenas.push_back(std::move(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +156,21 @@ void BalancingWindow::startAll()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BalancingWindow::handleEvent(std::shared_ptr<const ArenaStartRequestedEvent> event)
|
||||||
|
{
|
||||||
|
startArena(event->arenaIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BalancingWindow::handleEvent(std::shared_ptr<const ArenaInspectRequestedEvent> event)
|
||||||
|
{
|
||||||
|
inspectArena(event->arenaIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BalancingWindow::handleEvent(std::shared_ptr<const InspectWindowClosedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
closeInspectWindow();
|
||||||
|
}
|
||||||
|
|
||||||
void BalancingWindow::startArena(int index)
|
void BalancingWindow::startArena(int index)
|
||||||
{
|
{
|
||||||
ArenaEntry& entry = m_arenas[index];
|
ArenaEntry& entry = m_arenas[index];
|
||||||
@@ -179,7 +192,6 @@ void BalancingWindow::inspectArena(int index)
|
|||||||
{
|
{
|
||||||
if (m_inspectWindow)
|
if (m_inspectWindow)
|
||||||
{
|
{
|
||||||
m_inspectWindow->disconnect(this);
|
|
||||||
delete m_inspectWindow;
|
delete m_inspectWindow;
|
||||||
m_inspectWindow = nullptr;
|
m_inspectWindow = nullptr;
|
||||||
|
|
||||||
@@ -210,8 +222,6 @@ void BalancingWindow::inspectArena(int index)
|
|||||||
|
|
||||||
m_inspectWindow = new InspectWindow(
|
m_inspectWindow = new InspectWindow(
|
||||||
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
|
m_inspectedSim.get(), &m_gameConfig, &m_visuals, entry.config.name, nullptr);
|
||||||
connect(m_inspectWindow, &InspectWindow::closed,
|
|
||||||
this, &BalancingWindow::closeInspectWindow);
|
|
||||||
|
|
||||||
setMainControlsEnabled(false);
|
setMainControlsEnabled(false);
|
||||||
m_inspectWindow->show();
|
m_inspectWindow->show();
|
||||||
@@ -224,7 +234,6 @@ void BalancingWindow::closeInspectWindow()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_inspectWindow->disconnect(this);
|
|
||||||
m_inspectWindow->deleteLater();
|
m_inspectWindow->deleteLater();
|
||||||
m_inspectWindow = nullptr;
|
m_inspectWindow = nullptr;
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,22 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "ArenaInspectRequestedEvent.h"
|
||||||
|
#include "ArenaStartRequestedEvent.h"
|
||||||
#include "ArenaWidget.h"
|
#include "ArenaWidget.h"
|
||||||
#include "ArenaSimulation.h"
|
#include "ArenaSimulation.h"
|
||||||
#include "BalancingConfig.h"
|
#include "BalancingConfig.h"
|
||||||
|
#include "EventHandler.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "InspectWindowClosedEvent.h"
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
|
|
||||||
class InspectWindow;
|
class InspectWindow;
|
||||||
|
|
||||||
class BalancingWindow : public QWidget
|
class BalancingWindow : public QWidget,
|
||||||
|
public CombinedEventHandler<ArenaStartRequestedEvent,
|
||||||
|
ArenaInspectRequestedEvent,
|
||||||
|
InspectWindowClosedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -30,15 +37,20 @@ public:
|
|||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
~BalancingWindow() override;
|
~BalancingWindow() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handleEvent(std::shared_ptr<const ArenaStartRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const ArenaInspectRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const InspectWindowClosedEvent> event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void pollStatuses();
|
void pollStatuses();
|
||||||
void reloadConfig();
|
void reloadConfig();
|
||||||
void startAll();
|
void startAll();
|
||||||
|
|
||||||
|
private:
|
||||||
void startArena(int index);
|
void startArena(int index);
|
||||||
void inspectArena(int index);
|
void inspectArena(int index);
|
||||||
void closeInspectWindow();
|
void closeInspectWindow();
|
||||||
|
|
||||||
private:
|
|
||||||
void populateArenas(const BalancingConfig& balancingConfig);
|
void populateArenas(const BalancingConfig& balancingConfig);
|
||||||
void stopAllArenas();
|
void stopAllArenas();
|
||||||
void updateButtons();
|
void updateButtons();
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
|
|
||||||
#include "ArenaView.h"
|
#include "ArenaView.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
|
#include "EventManager.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
|
#include "InspectWindowClosedEvent.h"
|
||||||
#include "ModuleOwnerComponent.h"
|
#include "ModuleOwnerComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipStatsCalculator.h"
|
#include "ShipStatsCalculator.h"
|
||||||
@@ -76,9 +78,6 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const GameConfig* config,
|
|||||||
m_arenaView = new ArenaView(sim, visuals, this);
|
m_arenaView = new ArenaView(sim, visuals, this);
|
||||||
mainLayout->addWidget(m_arenaView, 1);
|
mainLayout->addWidget(m_arenaView, 1);
|
||||||
|
|
||||||
connect(m_arenaView, &ArenaView::speedChanged,
|
|
||||||
this, &InspectWindow::onSpeedChanged);
|
|
||||||
|
|
||||||
// Info panel (bottom)
|
// Info panel (bottom)
|
||||||
{
|
{
|
||||||
QWidget* infoPanel = new QWidget(this);
|
QWidget* infoPanel = new QWidget(this);
|
||||||
@@ -140,19 +139,20 @@ InspectWindow::InspectWindow(ArenaSimulation* sim, const GameConfig* config,
|
|||||||
|
|
||||||
setFocusPolicy(Qt::StrongFocus);
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
|
|
||||||
registerForEvent();
|
registerForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
InspectWindow::~InspectWindow()
|
InspectWindow::~InspectWindow()
|
||||||
{
|
{
|
||||||
unregisterForEvent();
|
unregisterForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InspectWindow::closeEvent(QCloseEvent* event)
|
void InspectWindow::closeEvent(QCloseEvent* event)
|
||||||
{
|
{
|
||||||
m_arenaView->stopRendering();
|
m_arenaView->stopRendering();
|
||||||
m_pollTimer->stop();
|
m_pollTimer->stop();
|
||||||
emit closed();
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<InspectWindowClosedEvent>());
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,11 +176,11 @@ void InspectWindow::onSpeedButton(int index)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InspectWindow::onSpeedChanged(double multiplier)
|
void InspectWindow::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < kSpeedCount; ++i)
|
for (int i = 0; i < kSpeedCount; ++i)
|
||||||
{
|
{
|
||||||
const bool active = (std::abs(kSpeeds[i] - multiplier) < 0.001);
|
const bool active = (std::abs(kSpeeds[i] - event->speed) < 0.001);
|
||||||
m_speedButtons[static_cast<std::size_t>(i)]->setChecked(active);
|
m_speedButtons[static_cast<std::size_t>(i)]->setChecked(active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,13 +15,15 @@
|
|||||||
#include "EntitySelectedEvent.h"
|
#include "EntitySelectedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "GameSpeedChangedEvent.h"
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
|
|
||||||
class ArenaView;
|
class ArenaView;
|
||||||
class ShipStatsPanel;
|
class ShipStatsPanel;
|
||||||
|
|
||||||
class InspectWindow : public QWidget,
|
class InspectWindow : public QWidget,
|
||||||
public EventHandler<EntitySelectedEvent>
|
public CombinedEventHandler<EntitySelectedEvent,
|
||||||
|
GameSpeedChangedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -31,19 +33,16 @@ public:
|
|||||||
const std::string& arenaName, QWidget* parent = nullptr);
|
const std::string& arenaName, QWidget* parent = nullptr);
|
||||||
~InspectWindow() override;
|
~InspectWindow() override;
|
||||||
|
|
||||||
signals:
|
|
||||||
void closed();
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void closeEvent(QCloseEvent* event) override;
|
void closeEvent(QCloseEvent* event) override;
|
||||||
void keyPressEvent(QKeyEvent* event) override;
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onSpeedButton(int index);
|
void onSpeedButton(int index);
|
||||||
void onSpeedChanged(double multiplier);
|
|
||||||
void pollStatus();
|
void pollStatus();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Blueprint.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingId.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingId.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/FireEvent.h
|
|
||||||
${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}/SchematicDropEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ SET(SRCS
|
|||||||
${SRCS}
|
${SRCS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
27
src/lib/core/DisplayName.cpp
Normal file
27
src/lib/core/DisplayName.cpp
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#include "DisplayName.h"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
std::string toDisplayName(const std::string& id)
|
||||||
|
{
|
||||||
|
std::string result;
|
||||||
|
bool nextUpper = true;
|
||||||
|
for (char c : id)
|
||||||
|
{
|
||||||
|
if (c == '_')
|
||||||
|
{
|
||||||
|
result += ' ';
|
||||||
|
nextUpper = true;
|
||||||
|
}
|
||||||
|
else if (nextUpper)
|
||||||
|
{
|
||||||
|
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
|
nextUpper = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
5
src/lib/core/DisplayName.h
Normal file
5
src/lib/core/DisplayName.h
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
std::string toDisplayName(const std::string& id);
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Tick.h"
|
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
|
||||||
|
|
||||||
// Transient record emitted each time a weapon fires (REQ-SHP-FIRING,
|
|
||||||
// REQ-SHP-FIRING-BEAM). Buffered in a sim-owned queue and drained by the
|
|
||||||
// renderer each frame to draw the 0.3-second laser beam.
|
|
||||||
struct FireEvent
|
|
||||||
{
|
|
||||||
entt::entity shooter;
|
|
||||||
entt::entity target;
|
|
||||||
Tick emittedAt;
|
|
||||||
};
|
|
||||||
28
src/lib/core/SchematicChoiceOption.h
Normal file
28
src/lib/core/SchematicChoiceOption.h
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
enum class SchematicType
|
||||||
|
{
|
||||||
|
Ship,
|
||||||
|
Module,
|
||||||
|
Recipe
|
||||||
|
};
|
||||||
|
|
||||||
|
// One option presented to the player in the schematic choice dialog
|
||||||
|
// (REQ-DEF-SCHEMATIC-DROP). Built by the simulation when enemy stations are
|
||||||
|
// destroyed; the UI reads these to populate the dialog.
|
||||||
|
struct SchematicChoiceOption
|
||||||
|
{
|
||||||
|
std::string schematicId;
|
||||||
|
SchematicType type;
|
||||||
|
std::string displayName;
|
||||||
|
bool isNewUnlock;
|
||||||
|
int targetLevel;
|
||||||
|
|
||||||
|
// Display names of items produced by recipes that would newly become
|
||||||
|
// implicitly unlocked (REQ-LOCK-IMPLICIT) if this option is selected.
|
||||||
|
// Deduplicated and sorted alphabetically; empty if none.
|
||||||
|
std::vector<std::string> newlyUnlockedItemNames;
|
||||||
|
};
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
// Emitted in tick step 9 (Deaths & loot) when a destroyed enemy-defence-station
|
|
||||||
// set awards a schematic (REQ-DEF-SCHEMATIC-DROP). The UI renders a toast
|
|
||||||
// (REQ-UI-SCHEMATIC-TOAST); wasNewUnlock chooses between the "unlocked" and
|
|
||||||
// "level -> N" wording. isModuleSchematic selects ship vs. module toast text.
|
|
||||||
struct SchematicDropEvent
|
|
||||||
{
|
|
||||||
std::string schematicId; // matches ShipDef::id or ModuleDef::id in the config.
|
|
||||||
int newLevel;
|
|
||||||
bool wasNewUnlock;
|
|
||||||
bool isModuleSchematic;
|
|
||||||
};
|
|
||||||
@@ -21,7 +21,7 @@ CombatSystem::CombatSystem(const GameConfig& config)
|
|||||||
void CombatSystem::tick(Tick currentTick,
|
void CombatSystem::tick(Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
BuildingSystem& /*buildings*/,
|
BuildingSystem& /*buildings*/,
|
||||||
std::vector<FireEvent>& outFireEvents)
|
std::vector<WeaponFiredEvent>& outWeaponFiredEvents)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
// All weapons (ships and stations) are child entities linked via ModuleOwnerComponent.
|
// All weapons (ships and stations) are child entities linked via ModuleOwnerComponent.
|
||||||
@@ -35,7 +35,7 @@ void CombatSystem::tick(Tick currentTick,
|
|||||||
}
|
}
|
||||||
const PositionComponent& pos = admin.get<PositionComponent>(owner.owner);
|
const PositionComponent& pos = admin.get<PositionComponent>(owner.owner);
|
||||||
const FactionComponent& faction = admin.get<FactionComponent>(owner.owner);
|
const FactionComponent& faction = admin.get<FactionComponent>(owner.owner);
|
||||||
resolveWeapon(owner.owner, weapon, pos, faction, currentTick, admin, outFireEvents);
|
resolveWeapon(owner.owner, weapon, pos, faction, currentTick, admin, outWeaponFiredEvents);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ void CombatSystem::resolveWeapon(
|
|||||||
const FactionComponent& ownFaction,
|
const FactionComponent& ownFaction,
|
||||||
Tick currentTick,
|
Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
std::vector<FireEvent>& out)
|
std::vector<WeaponFiredEvent>& out)
|
||||||
{
|
{
|
||||||
if (weapon.cooldownTicks > 0.0f)
|
if (weapon.cooldownTicks > 0.0f)
|
||||||
{
|
{
|
||||||
@@ -115,7 +115,7 @@ void CombatSystem::resolveWeapon(
|
|||||||
m_pendingDamage.push_back({targetEntity, weapon.damage,
|
m_pendingDamage.push_back({targetEntity, weapon.damage,
|
||||||
currentTick + kWeaponImpactDelayTicks});
|
currentTick + kWeaponImpactDelayTicks});
|
||||||
|
|
||||||
FireEvent evt;
|
WeaponFiredEvent evt;
|
||||||
evt.shooter = shipEntity;
|
evt.shooter = shipEntity;
|
||||||
evt.target = targetEntity;
|
evt.target = targetEntity;
|
||||||
evt.emittedAt = currentTick;
|
evt.emittedAt = currentTick;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "FireEvent.h"
|
#include "WeaponFiredEvent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
@@ -26,7 +26,7 @@ public:
|
|||||||
void tick(Tick currentTick,
|
void tick(Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
BuildingSystem& buildings,
|
BuildingSystem& buildings,
|
||||||
std::vector<FireEvent>& outFireEvents);
|
std::vector<WeaponFiredEvent>& outWeaponFiredEvents);
|
||||||
|
|
||||||
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
|
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ private:
|
|||||||
const FactionComponent& ownFaction,
|
const FactionComponent& ownFaction,
|
||||||
Tick currentTick,
|
Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
std::vector<FireEvent>& out);
|
std::vector<WeaponFiredEvent>& out);
|
||||||
|
|
||||||
const GameConfig& m_config;
|
const GameConfig& m_config;
|
||||||
};
|
};
|
||||||
|
|||||||
10
src/lib/eventsystem/event/ArenaInspectRequestedEvent.h
Normal file
10
src/lib/eventsystem/event/ArenaInspectRequestedEvent.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class ArenaInspectRequestedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ArenaInspectRequestedEvent(int arenaIndex) : arenaIndex(arenaIndex) {}
|
||||||
|
const int arenaIndex;
|
||||||
|
};
|
||||||
10
src/lib/eventsystem/event/ArenaStartRequestedEvent.h
Normal file
10
src/lib/eventsystem/event/ArenaStartRequestedEvent.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class ArenaStartRequestedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ArenaStartRequestedEvent(int arenaIndex) : arenaIndex(arenaIndex) {}
|
||||||
|
const int arenaIndex;
|
||||||
|
};
|
||||||
7
src/lib/eventsystem/event/BlueprintModeExitedEvent.h
Normal file
7
src/lib/eventsystem/event/BlueprintModeExitedEvent.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class BlueprintModeExitedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
12
src/lib/eventsystem/event/BlueprintPlacementRequestedEvent.h
Normal file
12
src/lib/eventsystem/event/BlueprintPlacementRequestedEvent.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Blueprint.h"
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class BlueprintPlacementRequestedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit BlueprintPlacementRequestedEvent(Blueprint blueprint)
|
||||||
|
: blueprint(std::move(blueprint)) {}
|
||||||
|
const Blueprint blueprint;
|
||||||
|
};
|
||||||
7
src/lib/eventsystem/event/BuilderModeExitedEvent.h
Normal file
7
src/lib/eventsystem/event/BuilderModeExitedEvent.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class BuilderModeExitedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
11
src/lib/eventsystem/event/BuildingTypeSelectedEvent.h
Normal file
11
src/lib/eventsystem/event/BuildingTypeSelectedEvent.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class BuildingTypeSelectedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit BuildingTypeSelectedEvent(BuildingType type) : type(type) {}
|
||||||
|
const BuildingType type;
|
||||||
|
};
|
||||||
@@ -6,6 +6,24 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EntitySelectedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameSpeedChangedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuilderModeExitedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintModeExitedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeChangedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTypeSelectedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ExitBuilderModeRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeToggleRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ExitBlueprintModeRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SpeedChangeRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/LayoutDialogRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindowClosedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ArenaStartRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ArenaInspectRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/WeaponFiredEvent.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
10
src/lib/eventsystem/event/DemolishModeChangedEvent.h
Normal file
10
src/lib/eventsystem/event/DemolishModeChangedEvent.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class DemolishModeChangedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit DemolishModeChangedEvent(bool active) : active(active) {}
|
||||||
|
const bool active;
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class DemolishModeToggleRequestedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
7
src/lib/eventsystem/event/EscapeMenuRequestedEvent.h
Normal file
7
src/lib/eventsystem/event/EscapeMenuRequestedEvent.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class EscapeMenuRequestedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class ExitBlueprintModeRequestedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class ExitBuilderModeRequestedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
7
src/lib/eventsystem/event/GameOverEvent.h
Normal file
7
src/lib/eventsystem/event/GameOverEvent.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class GameOverEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
7
src/lib/eventsystem/event/InspectWindowClosedEvent.h
Normal file
7
src/lib/eventsystem/event/InspectWindowClosedEvent.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class InspectWindowClosedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
12
src/lib/eventsystem/event/LayoutDialogRequestedEvent.h
Normal file
12
src/lib/eventsystem/event/LayoutDialogRequestedEvent.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class LayoutDialogRequestedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit LayoutDialogRequestedEvent(BuildingId shipyardId)
|
||||||
|
: shipyardId(shipyardId) {}
|
||||||
|
const BuildingId shipyardId;
|
||||||
|
};
|
||||||
14
src/lib/eventsystem/event/SchematicChoicesAvailableEvent.h
Normal file
14
src/lib/eventsystem/event/SchematicChoicesAvailableEvent.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
#include "SchematicChoiceOption.h"
|
||||||
|
|
||||||
|
class SchematicChoicesAvailableEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SchematicChoicesAvailableEvent(std::vector<SchematicChoiceOption> choices)
|
||||||
|
: choices(std::move(choices)) {}
|
||||||
|
const std::vector<SchematicChoiceOption> choices;
|
||||||
|
};
|
||||||
14
src/lib/eventsystem/event/SelectionChangedEvent.h
Normal file
14
src/lib/eventsystem/event/SelectionChangedEvent.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class SelectionChangedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SelectionChangedEvent(std::vector<BuildingId> ids)
|
||||||
|
: ids(std::move(ids)) {}
|
||||||
|
const std::vector<BuildingId> ids;
|
||||||
|
};
|
||||||
10
src/lib/eventsystem/event/SpeedChangeRequestedEvent.h
Normal file
10
src/lib/eventsystem/event/SpeedChangeRequestedEvent.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class SpeedChangeRequestedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SpeedChangeRequestedEvent(double multiplier) : multiplier(multiplier) {}
|
||||||
|
const double multiplier;
|
||||||
|
};
|
||||||
17
src/lib/eventsystem/event/WeaponFiredEvent.h
Normal file
17
src/lib/eventsystem/event/WeaponFiredEvent.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
struct WeaponFiredEvent : public Event
|
||||||
|
{
|
||||||
|
WeaponFiredEvent() = default;
|
||||||
|
WeaponFiredEvent(entt::entity shooter, entt::entity target, Tick emittedAt)
|
||||||
|
: shooter(shooter), target(target), emittedAt(emittedAt) {}
|
||||||
|
|
||||||
|
entt::entity shooter = entt::null;
|
||||||
|
entt::entity target = entt::null;
|
||||||
|
Tick emittedAt = 0;
|
||||||
|
};
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
#include "AiSystem.h"
|
#include "AiSystem.h"
|
||||||
|
#include "DisplayName.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "CombatSystem.h"
|
#include "CombatSystem.h"
|
||||||
#include "DynamicBodySystem.h"
|
#include "DynamicBodySystem.h"
|
||||||
@@ -134,8 +136,8 @@ void Simulation::reset(unsigned int seed)
|
|||||||
m_playerStation2Entity = entt::null;
|
m_playerStation2Entity = entt::null;
|
||||||
m_currentEnemyStationEntities[0] = entt::null;
|
m_currentEnemyStationEntities[0] = entt::null;
|
||||||
m_currentEnemyStationEntities[1] = entt::null;
|
m_currentEnemyStationEntities[1] = entt::null;
|
||||||
m_fireEvents.clear();
|
m_weaponFiredEvents.clear();
|
||||||
m_schematicDropEvents.clear();
|
m_pendingSchematicChoices.clear();
|
||||||
|
|
||||||
m_admin.clear();
|
m_admin.clear();
|
||||||
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
|
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
|
||||||
@@ -244,7 +246,7 @@ void Simulation::tick()
|
|||||||
|
|
||||||
// Step 8: combat resolution
|
// Step 8: combat resolution
|
||||||
m_combatSystem->tick(m_currentTick, m_admin,
|
m_combatSystem->tick(m_currentTick, m_admin,
|
||||||
*m_buildingSystem, m_fireEvents);
|
*m_buildingSystem, m_weaponFiredEvents);
|
||||||
|
|
||||||
// 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);
|
||||||
@@ -532,11 +534,11 @@ void Simulation::tickDeathsAndLoot()
|
|||||||
const int destroyedLevel = m_waveSystem->generation();
|
const int destroyedLevel = m_waveSystem->generation();
|
||||||
m_waveSystem->onEnemyStationsDestroyed();
|
m_waveSystem->onEnemyStationsDestroyed();
|
||||||
placeEnemyStationSet(m_waveSystem->generation());
|
placeEnemyStationSet(m_waveSystem->generation());
|
||||||
awardSchematicDrop(destroyedLevel);
|
generateSchematicChoices(destroyedLevel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Simulation::awardSchematicDrop(int destroyedStationLevel)
|
void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
||||||
{
|
{
|
||||||
enum class DropType { Ship, Module, Recipe };
|
enum class DropType { Ship, Module, Recipe };
|
||||||
struct PoolEntry { std::string id; DropType type; };
|
struct PoolEntry { std::string id; DropType type; };
|
||||||
@@ -572,32 +574,103 @@ void Simulation::awardSchematicDrop(int destroyedStationLevel)
|
|||||||
pool.push_back({def.id, DropType::Recipe});
|
pool.push_back({def.id, DropType::Recipe});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::uniform_int_distribution<int> dist(0, static_cast<int>(pool.size()) - 1);
|
if (pool.empty()) { return; }
|
||||||
const PoolEntry& chosen = pool[static_cast<std::size_t>(dist(m_rng))];
|
|
||||||
|
|
||||||
if (chosen.type == DropType::Recipe)
|
const int numChoices = std::min(static_cast<int>(pool.size()), 3);
|
||||||
|
m_pendingSchematicChoices.clear();
|
||||||
|
|
||||||
|
const std::set<std::string> currentShipIds = getUnlockedShipSchematicIds();
|
||||||
|
const std::set<std::string> currentModuleIds = getUnlockedModuleSchematicIds();
|
||||||
|
|
||||||
|
for (int i = 0; i < numChoices; ++i)
|
||||||
{
|
{
|
||||||
m_unlockedRecipeSchematicIds.insert(chosen.id);
|
std::uniform_int_distribution<int> dist(0, static_cast<int>(pool.size()) - 1 - i);
|
||||||
recomputeUnlocked();
|
const int roll = dist(m_rng);
|
||||||
|
const std::size_t rollIdx = static_cast<std::size_t>(roll);
|
||||||
|
const std::size_t endIdx = pool.size() - 1 - static_cast<std::size_t>(i);
|
||||||
|
std::swap(pool[rollIdx], pool[endIdx]);
|
||||||
|
const PoolEntry& entry = pool[endIdx];
|
||||||
|
|
||||||
|
SchematicChoiceOption option;
|
||||||
|
option.schematicId = entry.id;
|
||||||
|
|
||||||
|
if (entry.type == DropType::Ship)
|
||||||
|
{
|
||||||
|
option.type = SchematicType::Ship;
|
||||||
|
option.displayName = toDisplayName(entry.id);
|
||||||
|
const SchematicState& state = m_schematicLevels.at(entry.id);
|
||||||
|
option.isNewUnlock = !state.unlocked;
|
||||||
|
option.targetLevel = state.level + 1;
|
||||||
|
}
|
||||||
|
else if (entry.type == DropType::Module)
|
||||||
|
{
|
||||||
|
option.type = SchematicType::Module;
|
||||||
|
option.displayName = toDisplayName(entry.id);
|
||||||
|
const SchematicState& state = m_moduleSchematicLevels.at(entry.id);
|
||||||
|
option.isNewUnlock = !state.unlocked;
|
||||||
|
option.targetLevel = state.level + 1;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
SchematicState& state = (chosen.type == DropType::Module)
|
option.type = SchematicType::Recipe;
|
||||||
? m_moduleSchematicLevels.at(chosen.id)
|
option.isNewUnlock = true;
|
||||||
: m_schematicLevels.at(chosen.id);
|
option.targetLevel = 0;
|
||||||
const bool wasNew = !state.unlocked;
|
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (def.id == entry.id && !def.outputs.empty())
|
||||||
|
{
|
||||||
|
option.displayName = toDisplayName(def.outputs[0].item);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by this option.
|
||||||
|
std::set<std::string> hypotheticalShipIds = currentShipIds;
|
||||||
|
std::set<std::string> hypotheticalModuleIds = currentModuleIds;
|
||||||
|
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
|
||||||
|
|
||||||
|
if (entry.type == DropType::Ship && option.isNewUnlock)
|
||||||
|
{
|
||||||
|
hypotheticalShipIds.insert(entry.id);
|
||||||
|
}
|
||||||
|
else if (entry.type == DropType::Module && option.isNewUnlock)
|
||||||
|
{
|
||||||
|
hypotheticalModuleIds.insert(entry.id);
|
||||||
|
}
|
||||||
|
else if (entry.type == DropType::Recipe)
|
||||||
|
{
|
||||||
|
hypotheticalRecipeSchematicIds.insert(entry.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const UnlockedSets hypothetical = computeUnlockedSets(
|
||||||
|
hypotheticalShipIds, hypotheticalModuleIds, hypotheticalRecipeSchematicIds);
|
||||||
|
option.newlyUnlockedItemNames = computeNewlyUnlockedItemNames(hypothetical);
|
||||||
|
|
||||||
|
m_pendingSchematicChoices.push_back(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Simulation::applySchematicChoice(int choiceIndex)
|
||||||
|
{
|
||||||
|
assert(choiceIndex >= 0 && choiceIndex < static_cast<int>(m_pendingSchematicChoices.size()));
|
||||||
|
const SchematicChoiceOption& chosen = m_pendingSchematicChoices[static_cast<std::size_t>(choiceIndex)];
|
||||||
|
|
||||||
|
if (chosen.type == SchematicType::Recipe)
|
||||||
|
{
|
||||||
|
m_unlockedRecipeSchematicIds.insert(chosen.schematicId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SchematicState& state = (chosen.type == SchematicType::Module)
|
||||||
|
? m_moduleSchematicLevels.at(chosen.schematicId)
|
||||||
|
: m_schematicLevels.at(chosen.schematicId);
|
||||||
state.unlocked = true;
|
state.unlocked = true;
|
||||||
state.level += 1;
|
state.level += 1;
|
||||||
|
}
|
||||||
SchematicDropEvent evt;
|
|
||||||
evt.schematicId = chosen.id;
|
|
||||||
evt.newLevel = state.level;
|
|
||||||
evt.wasNewUnlock = wasNew;
|
|
||||||
evt.isModuleSchematic = (chosen.type == DropType::Module);
|
|
||||||
m_schematicDropEvents.push_back(evt);
|
|
||||||
|
|
||||||
recomputeUnlocked();
|
recomputeUnlocked();
|
||||||
}
|
m_pendingSchematicChoices.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -606,34 +679,64 @@ void Simulation::awardSchematicDrop(int destroyedStationLevel)
|
|||||||
|
|
||||||
void Simulation::recomputeUnlocked()
|
void Simulation::recomputeUnlocked()
|
||||||
{
|
{
|
||||||
m_unlockedItemIds.clear();
|
const UnlockedSets result = computeUnlockedSets(
|
||||||
m_unlockedRecipeIds.clear();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
{
|
{
|
||||||
if (!isSchematicUnlocked(def.id)) { continue; }
|
if (unlockedShipSchematicIds.count(def.id) == 0) { continue; }
|
||||||
for (const RecipeIngredient& mat : def.schematic.materials)
|
for (const RecipeIngredient& mat : def.schematic.materials)
|
||||||
{
|
{
|
||||||
m_unlockedItemIds.insert(mat.item);
|
result.itemIds.insert(mat.item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const ModuleDef& def : m_config.modules.modules)
|
for (const ModuleDef& def : m_config.modules.modules)
|
||||||
{
|
{
|
||||||
if (!isModuleSchematicUnlocked(def.id)) { continue; }
|
if (unlockedModuleSchematicIds.count(def.id) == 0) { continue; }
|
||||||
for (const RecipeIngredient& mat : def.materials)
|
for (const RecipeIngredient& mat : def.materials)
|
||||||
{
|
{
|
||||||
m_unlockedItemIds.insert(mat.item);
|
result.itemIds.insert(mat.item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const RecipeDef& def : m_config.recipes.recipes)
|
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||||
{
|
{
|
||||||
if (def.building == BuildingType::Assembler
|
if (def.building == BuildingType::Assembler
|
||||||
&& def.unlockAtStationLevel.has_value()
|
&& def.unlockAtStationLevel.has_value()
|
||||||
&& m_unlockedRecipeSchematicIds.count(def.id) > 0)
|
&& unlockedRecipeSchematicIds.count(def.id) > 0)
|
||||||
{
|
{
|
||||||
for (const RecipeOutput& out : def.outputs)
|
for (const RecipeOutput& out : def.outputs)
|
||||||
{
|
{
|
||||||
m_unlockedItemIds.insert(out.item);
|
result.itemIds.insert(out.item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -652,14 +755,14 @@ void Simulation::recomputeUnlocked()
|
|||||||
}
|
}
|
||||||
if (recipe.building == BuildingType::Assembler
|
if (recipe.building == BuildingType::Assembler
|
||||||
&& recipe.unlockAtStationLevel.has_value()
|
&& recipe.unlockAtStationLevel.has_value()
|
||||||
&& m_unlockedRecipeSchematicIds.count(recipe.id) == 0)
|
&& unlockedRecipeSchematicIds.count(recipe.id) == 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
bool producesUnlocked = false;
|
bool producesUnlocked = false;
|
||||||
for (const RecipeOutput& out : recipe.outputs)
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
{
|
{
|
||||||
if (m_unlockedItemIds.count(out.item) > 0)
|
if (result.itemIds.count(out.item) > 0)
|
||||||
{
|
{
|
||||||
producesUnlocked = true;
|
producesUnlocked = true;
|
||||||
break;
|
break;
|
||||||
@@ -670,17 +773,38 @@ void Simulation::recomputeUnlocked()
|
|||||||
if (recipe.building == BuildingType::Miner
|
if (recipe.building == BuildingType::Miner
|
||||||
|| recipe.building == BuildingType::Assembler)
|
|| recipe.building == BuildingType::Assembler)
|
||||||
{
|
{
|
||||||
m_unlockedRecipeIds.insert(recipe.id);
|
result.recipeIds.insert(recipe.id);
|
||||||
}
|
}
|
||||||
for (const RecipeIngredient& ing : recipe.inputs)
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
{
|
{
|
||||||
if (m_unlockedItemIds.insert(ing.item).second)
|
if (result.itemIds.insert(ing.item).second)
|
||||||
{
|
{
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> Simulation::computeNewlyUnlockedItemNames(const UnlockedSets& hypothetical) const
|
||||||
|
{
|
||||||
|
std::set<std::string> itemNames;
|
||||||
|
for (const std::string& recipeId : hypothetical.recipeIds)
|
||||||
|
{
|
||||||
|
if (m_unlockedRecipeIds.count(recipeId) > 0) { continue; }
|
||||||
|
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (def.id != recipeId) { continue; }
|
||||||
|
for (const RecipeOutput& out : def.outputs)
|
||||||
|
{
|
||||||
|
itemNames.insert(toDisplayName(out.item));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::vector<std::string>(itemNames.begin(), itemNames.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Simulation::isRecipeUnlocked(const std::string& recipeId) const
|
bool Simulation::isRecipeUnlocked(const std::string& recipeId) const
|
||||||
@@ -697,20 +821,24 @@ bool Simulation::isItemUnlocked(const std::string& itemId) const
|
|||||||
// Drains
|
// Drains
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
std::vector<FireEvent> Simulation::drainFireEvents()
|
std::vector<WeaponFiredEvent> Simulation::drainWeaponFiredEvents()
|
||||||
{
|
{
|
||||||
std::vector<FireEvent> result;
|
std::vector<WeaponFiredEvent> result;
|
||||||
result.swap(m_fireEvents);
|
result.swap(m_weaponFiredEvents);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<SchematicDropEvent> Simulation::drainSchematicDropEvents()
|
const std::vector<SchematicChoiceOption>& Simulation::getPendingSchematicChoices() const
|
||||||
{
|
{
|
||||||
std::vector<SchematicDropEvent> result;
|
return m_pendingSchematicChoices;
|
||||||
result.swap(m_schematicDropEvents);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Simulation::hasSchematicChoicesPending() const
|
||||||
|
{
|
||||||
|
return !m_pendingSchematicChoices.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Accessors
|
// Accessors
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
#include "SchematicDropEvent.h"
|
#include "SchematicChoiceOption.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "FireEvent.h"
|
#include "WeaponFiredEvent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
@@ -50,10 +50,18 @@ public:
|
|||||||
|
|
||||||
// Returns all fire events accumulated since the last drain, clearing the
|
// Returns all fire events accumulated since the last drain, clearing the
|
||||||
// internal queue. Call once per rendered frame (REQ-SHP-FIRING-BEAM).
|
// internal queue. Call once per rendered frame (REQ-SHP-FIRING-BEAM).
|
||||||
std::vector<FireEvent> drainFireEvents();
|
std::vector<WeaponFiredEvent> drainWeaponFiredEvents();
|
||||||
|
|
||||||
// Returns all schematic drop events since the last drain.
|
// Returns the pending schematic choices (empty if no drop is pending).
|
||||||
std::vector<SchematicDropEvent> drainSchematicDropEvents();
|
const std::vector<SchematicChoiceOption>& getPendingSchematicChoices() const;
|
||||||
|
|
||||||
|
// Returns true if there are pending schematic choices waiting for player input.
|
||||||
|
bool hasSchematicChoicesPending() const;
|
||||||
|
|
||||||
|
// Applies the player's chosen schematic from the pending choices.
|
||||||
|
// choiceIndex must be in [0, pendingChoices.size()).
|
||||||
|
// Clears the pending choices after application.
|
||||||
|
void applySchematicChoice(int choiceIndex);
|
||||||
|
|
||||||
Tick currentTick() const;
|
Tick currentTick() const;
|
||||||
int buildingBlocksStock() const;
|
int buildingBlocksStock() const;
|
||||||
@@ -108,8 +116,8 @@ private:
|
|||||||
// Tick step 9: remove dead ships and buildings, drop scrap, handle push.
|
// Tick step 9: remove dead ships and buildings, drop scrap, handle push.
|
||||||
void tickDeathsAndLoot();
|
void tickDeathsAndLoot();
|
||||||
|
|
||||||
// Award a random schematic drop (REQ-DEF-SCHEMATIC-DROP) and emit the event.
|
// Generate up to 3 schematic choices (REQ-DEF-SCHEMATIC-DROP) for the player.
|
||||||
void awardSchematicDrop(int destroyedStationLevel);
|
void generateSchematicChoices(int destroyedStationLevel);
|
||||||
|
|
||||||
GameConfig m_config;
|
GameConfig m_config;
|
||||||
std::mt19937 m_rng;
|
std::mt19937 m_rng;
|
||||||
@@ -146,6 +154,26 @@ private:
|
|||||||
// Recomputes m_unlockedRecipeIds and m_unlockedItemIds from current schematic state.
|
// Recomputes m_unlockedRecipeIds and m_unlockedItemIds from current schematic state.
|
||||||
void recomputeUnlocked();
|
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;
|
||||||
|
|
||||||
|
// Display names (deduplicated, alphabetical) of output items of recipes in
|
||||||
|
// hypothetical.recipeIds that are not yet in m_unlockedRecipeIds.
|
||||||
|
std::vector<std::string> computeNewlyUnlockedItemNames(const UnlockedSets& hypothetical) const;
|
||||||
|
|
||||||
EntityAdmin m_admin;
|
EntityAdmin m_admin;
|
||||||
BeltSystem m_beltSystem;
|
BeltSystem m_beltSystem;
|
||||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
@@ -157,6 +185,6 @@ private:
|
|||||||
std::unique_ptr<WaveSystem> m_waveSystem;
|
std::unique_ptr<WaveSystem> m_waveSystem;
|
||||||
std::unique_ptr<CombatSystem> m_combatSystem;
|
std::unique_ptr<CombatSystem> m_combatSystem;
|
||||||
|
|
||||||
std::vector<FireEvent> m_fireEvents;
|
std::vector<WeaponFiredEvent> m_weaponFiredEvents;
|
||||||
std::vector<SchematicDropEvent> m_schematicDropEvents;
|
std::vector<SchematicChoiceOption> m_pendingSchematicChoices;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "FireEvent.h"
|
#include "WeaponFiredEvent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "HqProxyComponent.h"
|
#include "HqProxyComponent.h"
|
||||||
#include "ModuleOwnerComponent.h"
|
#include "ModuleOwnerComponent.h"
|
||||||
@@ -111,7 +111,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<FireEvent> events;
|
std::vector<WeaponFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, f.buildings, events);
|
||||||
f.combat.applyPendingDamage(5, f.admin);
|
f.combat.applyPendingDamage(5, f.admin);
|
||||||
|
|
||||||
@@ -135,16 +135,16 @@ TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]"
|
|||||||
f.admin.get<WeaponComponent>(wc).cooldownTicks = 3.0f; // override to 3
|
f.admin.get<WeaponComponent>(wc).cooldownTicks = 3.0f; // override to 3
|
||||||
}
|
}
|
||||||
|
|
||||||
auto enemyFiredIn = [&enemy](const std::vector<FireEvent>& evts)
|
auto enemyFiredIn = [&enemy](const std::vector<WeaponFiredEvent>& evts)
|
||||||
{
|
{
|
||||||
for (const FireEvent& evt : evts)
|
for (const WeaponFiredEvent& evt : evts)
|
||||||
{
|
{
|
||||||
if (evt.shooter == enemy) { return true; }
|
if (evt.shooter == enemy) { return true; }
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<FireEvent> events;
|
std::vector<WeaponFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, f.buildings, events);
|
||||||
REQUIRE_FALSE(enemyFiredIn(events));
|
REQUIRE_FALSE(enemyFiredIn(events));
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
|||||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(500.0f, 0.0f), false);
|
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(500.0f, 0.0f), false);
|
||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
std::vector<FireEvent> events;
|
std::vector<WeaponFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, f.buildings, events);
|
||||||
REQUIRE(events.empty());
|
REQUIRE(events.empty());
|
||||||
}
|
}
|
||||||
@@ -204,9 +204,9 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
|
|||||||
|
|
||||||
sim.tick();
|
sim.tick();
|
||||||
|
|
||||||
const std::vector<FireEvent> events = sim.drainFireEvents();
|
const std::vector<WeaponFiredEvent> events = sim.drainWeaponFiredEvents();
|
||||||
bool stationFired = false;
|
bool stationFired = false;
|
||||||
for (const FireEvent& evt : events)
|
for (const WeaponFiredEvent& evt : events)
|
||||||
{
|
{
|
||||||
if (evt.shooter == stationEntity) { stationFired = true; }
|
if (evt.shooter == stationEntity) { stationFired = true; }
|
||||||
}
|
}
|
||||||
@@ -242,9 +242,9 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
|
|||||||
|
|
||||||
sim.tick();
|
sim.tick();
|
||||||
|
|
||||||
const std::vector<FireEvent> events = sim.drainFireEvents();
|
const std::vector<WeaponFiredEvent> events = sim.drainWeaponFiredEvents();
|
||||||
bool stationFired = false;
|
bool stationFired = false;
|
||||||
for (const FireEvent& evt : events)
|
for (const WeaponFiredEvent& evt : events)
|
||||||
{
|
{
|
||||||
if (evt.shooter == stationEntity) { stationFired = true; }
|
if (evt.shooter == stationEntity) { stationFired = true; }
|
||||||
}
|
}
|
||||||
@@ -280,9 +280,9 @@ TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]
|
|||||||
|
|
||||||
sim.tick();
|
sim.tick();
|
||||||
|
|
||||||
const std::vector<FireEvent> events = sim.drainFireEvents();
|
const std::vector<WeaponFiredEvent> events = sim.drainWeaponFiredEvents();
|
||||||
bool playerFiredAtStation = false;
|
bool playerFiredAtStation = false;
|
||||||
for (const FireEvent& evt : events)
|
for (const WeaponFiredEvent& evt : events)
|
||||||
{
|
{
|
||||||
if (evt.shooter == playerShip && evt.target == stationEntity)
|
if (evt.shooter == playerShip && evt.target == stationEntity)
|
||||||
{
|
{
|
||||||
@@ -308,7 +308,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<FireEvent> events;
|
std::vector<WeaponFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, f.buildings, events);
|
||||||
|
|
||||||
for (Tick t = 1; t < 5; ++t)
|
for (Tick t = 1; t < 5; ++t)
|
||||||
@@ -330,7 +330,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<FireEvent> events;
|
std::vector<WeaponFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, f.buildings, events);
|
||||||
f.combat.applyPendingDamage(5, f.admin);
|
f.combat.applyPendingDamage(5, f.admin);
|
||||||
|
|
||||||
@@ -347,7 +347,7 @@ TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[comb
|
|||||||
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
const entt::entity player = f.ships.spawn(combatDef->id, 1, QVector2D(4.0f, 5.0f), false);
|
||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
std::vector<FireEvent> events;
|
std::vector<WeaponFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, f.buildings, events);
|
||||||
|
|
||||||
f.ships.despawn(player);
|
f.ships.despawn(player);
|
||||||
@@ -370,7 +370,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<FireEvent> events;
|
std::vector<WeaponFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, f.buildings, events);
|
||||||
|
|
||||||
f.ships.despawn(enemy);
|
f.ships.despawn(enemy);
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
|
#include "DisplayName.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "RecipesConfig.h"
|
#include "RecipesConfig.h"
|
||||||
|
#include "SchematicChoiceOption.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
|
|
||||||
@@ -16,7 +18,7 @@ static GameConfig loadConfig()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Zeros the HP of both enemy defence stations and advances one tick so that
|
// Zeros the HP of both enemy defence stations and advances one tick so that
|
||||||
// tickDeathsAndLoot fires, triggering the push and schematic drop.
|
// tickDeathsAndLoot fires, triggering the push and schematic choices.
|
||||||
static void killEnemyStations(Simulation& sim)
|
static void killEnemyStations(Simulation& sim)
|
||||||
{
|
{
|
||||||
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||||
@@ -30,15 +32,25 @@ static void killEnemyStations(Simulation& sim)
|
|||||||
sim.tick();
|
sim.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kills enemy stations and applies the first schematic choice (index 0).
|
||||||
|
static void killEnemyStationsAndApply(Simulation& sim)
|
||||||
|
{
|
||||||
|
killEnemyStations(sim);
|
||||||
|
if (sim.hasSchematicChoicesPending())
|
||||||
|
{
|
||||||
|
sim.applySchematicChoice(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Destroys station sets until recipeId is unlocked or maxDestructions is reached.
|
// Destroys station sets until recipeId is unlocked or maxDestructions is reached.
|
||||||
// Returns true if the recipe is unlocked on exit.
|
// Applies schematic choice 0 after each destruction. Returns true if unlocked.
|
||||||
static bool awaitRecipeUnlock(Simulation& sim, const std::string& recipeId,
|
static bool awaitRecipeUnlock(Simulation& sim, const std::string& recipeId,
|
||||||
int maxDestructions = 150)
|
int maxDestructions = 150)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < maxDestructions; ++i)
|
for (int i = 0; i < maxDestructions; ++i)
|
||||||
{
|
{
|
||||||
if (sim.isRecipeUnlocked(recipeId)) { return true; }
|
if (sim.isRecipeUnlocked(recipeId)) { return true; }
|
||||||
killEnemyStations(sim);
|
killEnemyStationsAndApply(sim);
|
||||||
}
|
}
|
||||||
return sim.isRecipeUnlocked(recipeId);
|
return sim.isRecipeUnlocked(recipeId);
|
||||||
}
|
}
|
||||||
@@ -175,7 +187,7 @@ TEST_CASE("RecipeSchematic: recipe whose output is not implicitly unlocked is ne
|
|||||||
Simulation sim(loadConfig());
|
Simulation sim(loadConfig());
|
||||||
for (int i = 0; i < 50; ++i)
|
for (int i = 0; i < 50; ++i)
|
||||||
{
|
{
|
||||||
killEnemyStations(sim);
|
killEnemyStationsAndApply(sim);
|
||||||
}
|
}
|
||||||
REQUIRE_FALSE(sim.isRecipeUnlocked("exotic_alloy"));
|
REQUIRE_FALSE(sim.isRecipeUnlocked("exotic_alloy"));
|
||||||
}
|
}
|
||||||
@@ -186,7 +198,7 @@ TEST_CASE("RecipeSchematic: recipe with level > destroyed station level is not a
|
|||||||
// advanced_circuit has unlock_at_station_level = 1. Destroying a single
|
// advanced_circuit has unlock_at_station_level = 1. Destroying a single
|
||||||
// level-0 station set must not award it regardless of the RNG outcome.
|
// level-0 station set must not award it regardless of the RNG outcome.
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadConfig());
|
||||||
killEnemyStations(sim);
|
killEnemyStationsAndApply(sim);
|
||||||
REQUIRE_FALSE(sim.isRecipeUnlocked("advanced_circuit"));
|
REQUIRE_FALSE(sim.isRecipeUnlocked("advanced_circuit"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,25 +221,35 @@ TEST_CASE("RecipeSchematic: awarded recipe schematic stays unlocked and is not a
|
|||||||
// Destroy 30 more station sets; the recipe is no longer in the pool.
|
// Destroy 30 more station sets; the recipe is no longer in the pool.
|
||||||
for (int i = 0; i < 30; ++i)
|
for (int i = 0; i < 30; ++i)
|
||||||
{
|
{
|
||||||
killEnemyStations(sim);
|
killEnemyStationsAndApply(sim);
|
||||||
}
|
}
|
||||||
|
|
||||||
REQUIRE(sim.isRecipeUnlocked("quick_circuit"));
|
REQUIRE(sim.isRecipeUnlocked("quick_circuit"));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("RecipeSchematic: no SchematicDropEvent is emitted for a recipe schematic drop",
|
TEST_CASE("RecipeSchematic: recipe schematic can appear in pending choices",
|
||||||
"[recipe_schematic]")
|
"[recipe_schematic]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadConfig());
|
||||||
sim.drainSchematicDropEvents(); // clear any startup events
|
|
||||||
|
|
||||||
awaitRecipeUnlock(sim, "quick_circuit");
|
bool foundRecipeChoice = false;
|
||||||
|
for (int i = 0; i < 150 && !foundRecipeChoice; ++i)
|
||||||
const std::vector<SchematicDropEvent> events = sim.drainSchematicDropEvents();
|
|
||||||
for (const SchematicDropEvent& ev : events)
|
|
||||||
{
|
{
|
||||||
CHECK(ev.schematicId != "quick_circuit");
|
killEnemyStations(sim);
|
||||||
|
if (sim.hasSchematicChoicesPending())
|
||||||
|
{
|
||||||
|
for (const SchematicChoiceOption& opt : sim.getPendingSchematicChoices())
|
||||||
|
{
|
||||||
|
if (opt.type == SchematicType::Recipe)
|
||||||
|
{
|
||||||
|
foundRecipeChoice = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
sim.applySchematicChoice(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CHECK(foundRecipeChoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -256,3 +278,85 @@ TEST_CASE("RecipeSchematic: reset keeps -1 recipes unlocked and their seed items
|
|||||||
REQUIRE(sim.isItemUnlocked("premium_circuit"));
|
REQUIRE(sim.isItemUnlocked("premium_circuit"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Unlock dialog: newly-unlocked recipe preview (REQ-DEF-SCHEMATIC-DROP)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("RecipeSchematic: newlyUnlockedItemNames is sorted, deduplicated, and empty for level-ups",
|
||||||
|
"[recipe_schematic]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig());
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
{
|
||||||
|
killEnemyStations(sim);
|
||||||
|
if (!sim.hasSchematicChoicesPending()) { continue; }
|
||||||
|
|
||||||
|
for (const SchematicChoiceOption& opt : sim.getPendingSchematicChoices())
|
||||||
|
{
|
||||||
|
// Strictly ascending implies sorted and deduplicated.
|
||||||
|
for (std::size_t j = 1; j < opt.newlyUnlockedItemNames.size(); ++j)
|
||||||
|
{
|
||||||
|
CHECK(opt.newlyUnlockedItemNames[j - 1] < opt.newlyUnlockedItemNames[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A level-up doesn't change the explicit unlock state, so the
|
||||||
|
// implicit unlock set - and thus this preview - must be empty.
|
||||||
|
if (!opt.isNewUnlock)
|
||||||
|
{
|
||||||
|
CHECK(opt.newlyUnlockedItemNames.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sim.applySchematicChoice(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("RecipeSchematic: newlyUnlockedItemNames matches recipes that actually become unlocked",
|
||||||
|
"[recipe_schematic]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig());
|
||||||
|
const GameConfig cfg = loadConfig();
|
||||||
|
|
||||||
|
auto unlockedTrackedRecipeIds = [&]()
|
||||||
|
{
|
||||||
|
std::set<std::string> ids;
|
||||||
|
for (const RecipeDef& def : cfg.recipes.recipes)
|
||||||
|
{
|
||||||
|
if ((def.building == BuildingType::Miner || def.building == BuildingType::Assembler)
|
||||||
|
&& sim.isRecipeUnlocked(def.id))
|
||||||
|
{
|
||||||
|
ids.insert(def.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
{
|
||||||
|
killEnemyStations(sim);
|
||||||
|
if (!sim.hasSchematicChoicesPending()) { continue; }
|
||||||
|
|
||||||
|
const std::set<std::string> unlockedBefore = unlockedTrackedRecipeIds();
|
||||||
|
const SchematicChoiceOption choice = sim.getPendingSchematicChoices()[0];
|
||||||
|
|
||||||
|
sim.applySchematicChoice(0);
|
||||||
|
|
||||||
|
std::set<std::string> expectedNames;
|
||||||
|
for (const RecipeDef& def : cfg.recipes.recipes)
|
||||||
|
{
|
||||||
|
if ((def.building == BuildingType::Miner || def.building == BuildingType::Assembler)
|
||||||
|
&& sim.isRecipeUnlocked(def.id) && unlockedBefore.count(def.id) == 0)
|
||||||
|
{
|
||||||
|
for (const RecipeOutput& out : def.outputs)
|
||||||
|
{
|
||||||
|
expectedNames.insert(toDisplayName(out.item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const std::vector<std::string> expected(expectedNames.begin(), expectedNames.end());
|
||||||
|
|
||||||
|
REQUIRE(choice.newlyUnlockedItemNames == expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,29 +43,29 @@ TEST_CASE("Simulation::tick 10 times yields currentTick == 10", "[simulation]")
|
|||||||
REQUIRE(sim.currentTick() == 10);
|
REQUIRE(sim.currentTick() == 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Simulation::drainFireEvents returns empty initially", "[simulation]")
|
TEST_CASE("Simulation::drainWeaponFiredEvents returns empty initially", "[simulation]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadConfig());
|
||||||
|
|
||||||
REQUIRE(sim.drainFireEvents().empty());
|
REQUIRE(sim.drainWeaponFiredEvents().empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Simulation::drainFireEvents clears queue on drain", "[simulation]")
|
TEST_CASE("Simulation::drainWeaponFiredEvents clears queue on drain", "[simulation]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadConfig());
|
||||||
|
|
||||||
// First drain: empty.
|
// First drain: empty.
|
||||||
sim.drainFireEvents();
|
sim.drainWeaponFiredEvents();
|
||||||
|
|
||||||
// Second drain must also be empty (not a double-return).
|
// Second drain must also be empty (not a double-return).
|
||||||
REQUIRE(sim.drainFireEvents().empty());
|
REQUIRE(sim.drainWeaponFiredEvents().empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Simulation::drainSchematicDropEvents returns empty initially", "[simulation]")
|
TEST_CASE("Simulation::hasSchematicChoicesPending returns false initially", "[simulation]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadConfig());
|
||||||
|
|
||||||
REQUIRE(sim.drainSchematicDropEvents().empty());
|
REQUIRE_FALSE(sim.hasSchematicChoicesPending());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
#include "WeaponComponent.h"
|
#include "WeaponComponent.h"
|
||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
|
#include "RecipesConfig.h"
|
||||||
|
#include "SchematicChoiceOption.h"
|
||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
@@ -247,7 +249,7 @@ TEST_CASE("WaveSystem: destroying both enemy stations triggers a push", "[wave]"
|
|||||||
REQUIRE(enemyCount == 2);
|
REQUIRE(enemyCount == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("WaveSystem: push emits exactly one SchematicDropEvent", "[wave]")
|
TEST_CASE("WaveSystem: push generates pending schematic choices", "[wave]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 42);
|
Simulation sim(loadConfig(), 42);
|
||||||
|
|
||||||
@@ -259,11 +261,13 @@ TEST_CASE("WaveSystem: push emits exactly one SchematicDropEvent", "[wave]")
|
|||||||
|
|
||||||
sim.tick();
|
sim.tick();
|
||||||
|
|
||||||
const std::vector<SchematicDropEvent> events = sim.drainSchematicDropEvents();
|
REQUIRE(sim.hasSchematicChoicesPending());
|
||||||
REQUIRE(events.size() == 1);
|
const std::vector<SchematicChoiceOption>& choices = sim.getPendingSchematicChoices();
|
||||||
|
REQUIRE(choices.size() >= 1);
|
||||||
|
REQUIRE(choices.size() <= 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("WaveSystem: push schematic drop awards a known ship id", "[wave]")
|
TEST_CASE("WaveSystem: push schematic choices have valid ids", "[wave]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 42);
|
Simulation sim(loadConfig(), 42);
|
||||||
|
|
||||||
@@ -274,23 +278,68 @@ TEST_CASE("WaveSystem: push schematic drop awards a known ship id", "[wave]")
|
|||||||
});
|
});
|
||||||
|
|
||||||
sim.tick();
|
sim.tick();
|
||||||
const std::vector<SchematicDropEvent> events = sim.drainSchematicDropEvents();
|
const std::vector<SchematicChoiceOption>& choices = sim.getPendingSchematicChoices();
|
||||||
REQUIRE(events.size() == 1);
|
REQUIRE_FALSE(choices.empty());
|
||||||
|
|
||||||
|
for (const SchematicChoiceOption& opt : choices)
|
||||||
|
{
|
||||||
bool validId = false;
|
bool validId = false;
|
||||||
for (const ShipDef& def : sim.config().ships.ships)
|
for (const ShipDef& def : sim.config().ships.ships)
|
||||||
{
|
{
|
||||||
if (def.id == events[0].schematicId) { validId = true; break; }
|
if (def.id == opt.schematicId) { validId = true; break; }
|
||||||
}
|
}
|
||||||
if (!validId)
|
if (!validId)
|
||||||
{
|
{
|
||||||
for (const ModuleDef& def : sim.config().modules.modules)
|
for (const ModuleDef& def : sim.config().modules.modules)
|
||||||
{
|
{
|
||||||
if (def.id == events[0].schematicId) { validId = true; break; }
|
if (def.id == opt.schematicId) { validId = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!validId)
|
||||||
|
{
|
||||||
|
for (const RecipeDef& def : sim.config().recipes.recipes)
|
||||||
|
{
|
||||||
|
if (def.id == opt.schematicId) { validId = true; break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
REQUIRE(validId);
|
REQUIRE(validId);
|
||||||
REQUIRE(events[0].newLevel >= 1);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("WaveSystem: schematic choices have no duplicates", "[wave]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
|
||||||
|
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||||
|
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
|
||||||
|
{
|
||||||
|
if (f.isEnemy) { h.hp = -1.0f; }
|
||||||
|
});
|
||||||
|
|
||||||
|
sim.tick();
|
||||||
|
const std::vector<SchematicChoiceOption>& choices = sim.getPendingSchematicChoices();
|
||||||
|
std::set<std::string> ids;
|
||||||
|
for (const SchematicChoiceOption& opt : choices)
|
||||||
|
{
|
||||||
|
ids.insert(opt.schematicId);
|
||||||
|
}
|
||||||
|
REQUIRE(ids.size() == choices.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("WaveSystem: applySchematicChoice clears pending and applies", "[wave]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig(), 42);
|
||||||
|
|
||||||
|
sim.admin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||||
|
[](entt::entity /*e*/, const StationBodyComponent& /*sb*/, const FactionComponent& f, HealthComponent& h)
|
||||||
|
{
|
||||||
|
if (f.isEnemy) { h.hp = -1.0f; }
|
||||||
|
});
|
||||||
|
|
||||||
|
sim.tick();
|
||||||
|
REQUIRE(sim.hasSchematicChoicesPending());
|
||||||
|
sim.applySchematicChoice(0);
|
||||||
|
REQUIRE_FALSE(sim.hasSchematicChoicesPending());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
|
TEST_CASE("WaveSystem: push places new enemy stations further right", "[wave]")
|
||||||
|
|||||||
@@ -12,8 +12,11 @@
|
|||||||
#include <QScrollArea>
|
#include <QScrollArea>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "BlueprintPlacementRequestedEvent.h"
|
||||||
#include "BlueprintSerializer.h"
|
#include "BlueprintSerializer.h"
|
||||||
#include "BuildingBlocksChangedEvent.h"
|
#include "BuildingBlocksChangedEvent.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "ExitBlueprintModeRequestedEvent.h"
|
||||||
|
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
@@ -62,12 +65,12 @@ BlueprintPanel::BlueprintPanel(Simulation* sim, const GameConfig* config, QWidge
|
|||||||
connect(m_saveBtn, &QPushButton::clicked, this, &BlueprintPanel::onSaveClicked);
|
connect(m_saveBtn, &QPushButton::clicked, this, &BlueprintPanel::onSaveClicked);
|
||||||
connect(m_loadBtn, &QPushButton::clicked, this, &BlueprintPanel::onLoadClicked);
|
connect(m_loadBtn, &QPushButton::clicked, this, &BlueprintPanel::onLoadClicked);
|
||||||
|
|
||||||
registerForEvent();
|
registerForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
BlueprintPanel::~BlueprintPanel()
|
BlueprintPanel::~BlueprintPanel()
|
||||||
{
|
{
|
||||||
unregisterForEvent();
|
unregisterForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BlueprintPanel::onSelectionChanged(const std::vector<BuildingId>& ids)
|
void BlueprintPanel::onSelectionChanged(const std::vector<BuildingId>& ids)
|
||||||
@@ -114,7 +117,8 @@ void BlueprintPanel::onDeleteBlueprintClicked(int index)
|
|||||||
if (m_activeIndex == index)
|
if (m_activeIndex == index)
|
||||||
{
|
{
|
||||||
m_activeIndex = -1;
|
m_activeIndex = -1;
|
||||||
emit exitBlueprintModeRequested();
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ExitBlueprintModeRequestedEvent>());
|
||||||
}
|
}
|
||||||
else if (m_activeIndex > index)
|
else if (m_activeIndex > index)
|
||||||
{
|
{
|
||||||
@@ -131,7 +135,8 @@ void BlueprintPanel::onBlueprintButtonClicked(int index)
|
|||||||
if (m_activeIndex == index)
|
if (m_activeIndex == index)
|
||||||
{
|
{
|
||||||
clearActiveBlueprintButton();
|
clearActiveBlueprintButton();
|
||||||
emit exitBlueprintModeRequested();
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ExitBlueprintModeRequestedEvent>());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +147,8 @@ void BlueprintPanel::onBlueprintButtonClicked(int index)
|
|||||||
|
|
||||||
m_activeIndex = index;
|
m_activeIndex = index;
|
||||||
m_blueprintButtons[static_cast<std::size_t>(index)]->setChecked(true);
|
m_blueprintButtons[static_cast<std::size_t>(index)]->setChecked(true);
|
||||||
emit blueprintPlacementRequested(m_blueprints[static_cast<std::size_t>(index)]);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<BlueprintPlacementRequestedEvent>(m_blueprints[static_cast<std::size_t>(index)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
Blueprint BlueprintPanel::createBlueprintFromSelection() const
|
Blueprint BlueprintPanel::createBlueprintFromSelection() const
|
||||||
@@ -312,7 +318,8 @@ void BlueprintPanel::onLoadClicked()
|
|||||||
|
|
||||||
if (m_activeIndex >= 0)
|
if (m_activeIndex >= 0)
|
||||||
{
|
{
|
||||||
emit exitBlueprintModeRequested();
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ExitBlueprintModeRequestedEvent>());
|
||||||
m_activeIndex = -1;
|
m_activeIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,3 +357,13 @@ void BlueprintPanel::refreshButtonStates()
|
|||||||
canAfford || m_activeIndex == i);
|
canAfford || m_activeIndex == i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BlueprintPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||||
|
{
|
||||||
|
onSelectionChanged(event->ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BlueprintPanel::handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
clearActiveBlueprintButton();
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "Blueprint.h"
|
#include "Blueprint.h"
|
||||||
|
#include "BlueprintModeExitedEvent.h"
|
||||||
#include "BuildingBlocksChangedEvent.h"
|
#include "BuildingBlocksChangedEvent.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "SelectionChangedEvent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
class Simulation;
|
class Simulation;
|
||||||
@@ -17,7 +19,9 @@ class QScrollArea;
|
|||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
|
|
||||||
class BlueprintPanel : public QWidget,
|
class BlueprintPanel : public QWidget,
|
||||||
public EventHandler<BuildingBlocksChangedEvent>
|
public CombinedEventHandler<BuildingBlocksChangedEvent,
|
||||||
|
SelectionChangedEvent,
|
||||||
|
BlueprintModeExitedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -25,16 +29,10 @@ public:
|
|||||||
BlueprintPanel(Simulation* sim, const GameConfig* config, QWidget* parent = nullptr);
|
BlueprintPanel(Simulation* sim, const GameConfig* config, QWidget* parent = nullptr);
|
||||||
~BlueprintPanel() override;
|
~BlueprintPanel() override;
|
||||||
|
|
||||||
public slots:
|
|
||||||
void onSelectionChanged(const std::vector<BuildingId>& ids);
|
|
||||||
void clearActiveBlueprintButton();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void blueprintPlacementRequested(Blueprint blueprint);
|
|
||||||
void exitBlueprintModeRequested();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onCreateClicked();
|
void onCreateClicked();
|
||||||
@@ -44,6 +42,8 @@ private slots:
|
|||||||
void onLoadClicked();
|
void onLoadClicked();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void onSelectionChanged(const std::vector<BuildingId>& ids);
|
||||||
|
void clearActiveBlueprintButton();
|
||||||
Blueprint createBlueprintFromSelection() const;
|
Blueprint createBlueprintFromSelection() const;
|
||||||
int computeBlueprintCost(const Blueprint& bp) const;
|
int computeBlueprintCost(const Blueprint& bp) const;
|
||||||
void rebuildButtons();
|
void rebuildButtons();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#include "BuildButtonGrid.h"
|
#include "BuildButtonGrid.h"
|
||||||
|
|
||||||
#include <cctype>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
@@ -8,35 +7,11 @@
|
|||||||
#include <QSignalMapper>
|
#include <QSignalMapper>
|
||||||
|
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
|
#include "BuildingTypeSelectedEvent.h"
|
||||||
namespace
|
#include "DemolishModeToggleRequestedEvent.h"
|
||||||
{
|
#include "DisplayName.h"
|
||||||
|
#include "EventManager.h"
|
||||||
QString displayName(const std::string& id)
|
#include "ExitBuilderModeRequestedEvent.h"
|
||||||
{
|
|
||||||
QString result;
|
|
||||||
bool nextUpper = true;
|
|
||||||
for (char c : id)
|
|
||||||
{
|
|
||||||
if (c == '_')
|
|
||||||
{
|
|
||||||
result += ' ';
|
|
||||||
nextUpper = true;
|
|
||||||
}
|
|
||||||
else if (nextUpper)
|
|
||||||
{
|
|
||||||
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
|
||||||
nextUpper = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
|
|
||||||
BuildButtonGrid::BuildButtonGrid(const GameConfig* config, QWidget* parent)
|
BuildButtonGrid::BuildButtonGrid(const GameConfig* config, QWidget* parent)
|
||||||
@@ -62,7 +37,7 @@ BuildButtonGrid::BuildButtonGrid(const GameConfig* config, QWidget* parent)
|
|||||||
m_types.push_back(def.type);
|
m_types.push_back(def.type);
|
||||||
m_costs[def.type] = def.cost;
|
m_costs[def.type] = def.cost;
|
||||||
|
|
||||||
const QString label = displayName(def.id)
|
const QString label = QString::fromStdString(toDisplayName(def.id))
|
||||||
+ "\n" + tr("%1 Blocks").arg(def.cost);
|
+ "\n" + tr("%1 Blocks").arg(def.cost);
|
||||||
QPushButton* btn = new QPushButton(label, this);
|
QPushButton* btn = new QPushButton(label, this);
|
||||||
btn->setCheckable(true);
|
btn->setCheckable(true);
|
||||||
@@ -87,7 +62,17 @@ BuildButtonGrid::BuildButtonGrid(const GameConfig* config, QWidget* parent)
|
|||||||
m_demolishButton->setCheckable(true);
|
m_demolishButton->setCheckable(true);
|
||||||
m_demolishButton->setFixedHeight(48);
|
m_demolishButton->setFixedHeight(48);
|
||||||
layout->addWidget(m_demolishButton, row, col);
|
layout->addWidget(m_demolishButton, row, col);
|
||||||
connect(m_demolishButton, SIGNAL(clicked()), this, SIGNAL(demolishModeToggleRequested()));
|
connect(m_demolishButton, &QPushButton::clicked, this, [this]() {
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<DemolishModeToggleRequestedEvent>());
|
||||||
|
});
|
||||||
|
|
||||||
|
registerForEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildButtonGrid::~BuildButtonGrid()
|
||||||
|
{
|
||||||
|
unregisterForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildButtonGrid::updateAffordability(int buildingBlocks)
|
void BuildButtonGrid::updateAffordability(int buildingBlocks)
|
||||||
@@ -101,11 +86,6 @@ void BuildButtonGrid::updateAffordability(int buildingBlocks)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildButtonGrid::setDemolishModeActive(bool active)
|
|
||||||
{
|
|
||||||
m_demolishButton->setChecked(active);
|
|
||||||
}
|
|
||||||
|
|
||||||
void BuildButtonGrid::clearActiveButton()
|
void BuildButtonGrid::clearActiveButton()
|
||||||
{
|
{
|
||||||
if (m_activeIndex >= 0 && m_activeIndex < static_cast<int>(m_buttons.size()))
|
if (m_activeIndex >= 0 && m_activeIndex < static_cast<int>(m_buttons.size()))
|
||||||
@@ -125,7 +105,8 @@ void BuildButtonGrid::onBuildButton(int index)
|
|||||||
if (m_activeIndex == index)
|
if (m_activeIndex == index)
|
||||||
{
|
{
|
||||||
clearActiveButton();
|
clearActiveButton();
|
||||||
emit builderModeExited();
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ExitBuilderModeRequestedEvent>());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,5 +117,16 @@ void BuildButtonGrid::onBuildButton(int index)
|
|||||||
|
|
||||||
m_activeIndex = index;
|
m_activeIndex = index;
|
||||||
m_buttons[static_cast<std::size_t>(index)]->setChecked(true);
|
m_buttons[static_cast<std::size_t>(index)]->setChecked(true);
|
||||||
emit buildingTypeSelected(m_types[static_cast<std::size_t>(index)]);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<BuildingTypeSelectedEvent>(m_types[static_cast<std::size_t>(index)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonGrid::handleEvent(std::shared_ptr<const BuilderModeExitedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
clearActiveButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildButtonGrid::handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event)
|
||||||
|
{
|
||||||
|
m_demolishButton->setChecked(event->active);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,28 +5,30 @@
|
|||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include "BuilderModeExitedEvent.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
|
#include "DemolishModeChangedEvent.h"
|
||||||
|
#include "EventHandler.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
|
|
||||||
class BuildButtonGrid : public QWidget
|
class BuildButtonGrid : public QWidget,
|
||||||
|
public CombinedEventHandler<BuilderModeExitedEvent,
|
||||||
|
DemolishModeChangedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
BuildButtonGrid(const GameConfig* config, QWidget* parent = nullptr);
|
BuildButtonGrid(const GameConfig* config, QWidget* parent = nullptr);
|
||||||
|
~BuildButtonGrid() override;
|
||||||
|
|
||||||
void updateAffordability(int buildingBlocks);
|
void updateAffordability(int buildingBlocks);
|
||||||
void clearActiveButton();
|
void clearActiveButton();
|
||||||
|
|
||||||
signals:
|
private:
|
||||||
void buildingTypeSelected(BuildingType type);
|
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
|
||||||
void builderModeExited();
|
void handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event) override;
|
||||||
void demolishModeToggleRequested();
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void setDemolishModeActive(bool active);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onBuildButton(int index);
|
void onBuildButton(int index);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,5 +27,6 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutPreview.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsPanel.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceDialog.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,14 +20,17 @@
|
|||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
|
#include "DemolishModeChangedEvent.h"
|
||||||
#include "EntityHitTest.h"
|
#include "EntityHitTest.h"
|
||||||
#include "EntitySelectedEvent.h"
|
#include "EntitySelectedEvent.h"
|
||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
#include "FacingComponent.h"
|
#include "FacingComponent.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
|
#include "GameOverEvent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
|
#include "SelectionChangedEvent.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
@@ -35,10 +38,14 @@
|
|||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
#include "SurfaceMask.h"
|
#include "SurfaceMask.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "EscapeMenuRequestedEvent.h"
|
||||||
#include "TracePrintRequestedEvent.h"
|
#include "TracePrintRequestedEvent.h"
|
||||||
#include "BossWaveUpdatedEvent.h"
|
#include "BossWaveUpdatedEvent.h"
|
||||||
|
#include "BuilderModeExitedEvent.h"
|
||||||
|
#include "BlueprintModeExitedEvent.h"
|
||||||
#include "BuildingBlocksChangedEvent.h"
|
#include "BuildingBlocksChangedEvent.h"
|
||||||
#include "GameSpeedChangedEvent.h"
|
#include "GameSpeedChangedEvent.h"
|
||||||
|
#include "SchematicChoicesAvailableEvent.h"
|
||||||
#include "TickAdvancedEvent.h"
|
#include "TickAdvancedEvent.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -68,31 +75,6 @@ Rotation rotateCounterClockwise(Rotation r)
|
|||||||
return Rotation::East;
|
return Rotation::East;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
QString toDisplayName(const std::string& id)
|
|
||||||
{
|
|
||||||
QString result;
|
|
||||||
bool nextUpper = true;
|
|
||||||
for (char c : id)
|
|
||||||
{
|
|
||||||
if (c == '_')
|
|
||||||
{
|
|
||||||
result += ' ';
|
|
||||||
nextUpper = true;
|
|
||||||
}
|
|
||||||
else if (nextUpper)
|
|
||||||
{
|
|
||||||
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
|
||||||
nextUpper = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPoint portBodyTile(QPoint portTile, Rotation direction)
|
QPoint portBodyTile(QPoint portTile, Rotation direction)
|
||||||
{
|
{
|
||||||
switch (direction)
|
switch (direction)
|
||||||
@@ -129,6 +111,7 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
|
|||||||
, m_scrollLeft(false)
|
, m_scrollLeft(false)
|
||||||
, m_scrollRight(false)
|
, m_scrollRight(false)
|
||||||
, m_gameOverShown(false)
|
, m_gameOverShown(false)
|
||||||
|
, m_schematicChoiceShown(false)
|
||||||
{
|
{
|
||||||
setFocusPolicy(Qt::StrongFocus);
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
setMouseTracking(true);
|
setMouseTracking(true);
|
||||||
@@ -138,6 +121,13 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
|
|||||||
connect(m_renderTimer, &QTimer::timeout, this, &GameWorldView::onFrame);
|
connect(m_renderTimer, &QTimer::timeout, this, &GameWorldView::onFrame);
|
||||||
m_renderTimer->start();
|
m_renderTimer->start();
|
||||||
m_frameTimer.start();
|
m_frameTimer.start();
|
||||||
|
|
||||||
|
registerForEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
GameWorldView::~GameWorldView()
|
||||||
|
{
|
||||||
|
unregisterForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::initializeGL()
|
void GameWorldView::initializeGL()
|
||||||
@@ -160,56 +150,13 @@ void GameWorldView::onFrame()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain fire events → active beams
|
// Emit fire events via EventManager
|
||||||
{
|
{
|
||||||
const std::vector<FireEvent> fires = m_sim->drainFireEvents();
|
const std::vector<WeaponFiredEvent> fires = m_sim->drainWeaponFiredEvents();
|
||||||
for (const FireEvent& fe : fires)
|
for (const WeaponFiredEvent& fe : fires)
|
||||||
{
|
{
|
||||||
float maxRadius = 0.125f;
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
if (m_sim->admin().isValid(fe.target)
|
std::make_shared<WeaponFiredEvent>(fe));
|
||||||
&& m_sim->admin().hasAll<StationBodyComponent>(fe.target))
|
|
||||||
{
|
|
||||||
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(fe.target);
|
|
||||||
const int shorter = std::min(sb.footprint.width(), sb.footprint.height());
|
|
||||||
maxRadius = shorter / 2.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
|
|
||||||
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
|
|
||||||
const float angle = angleDist(m_rng);
|
|
||||||
const float radius = radiusDist(m_rng);
|
|
||||||
|
|
||||||
ActiveBeam beam;
|
|
||||||
beam.event = fe;
|
|
||||||
beam.emittedWallMs = m_wallMs;
|
|
||||||
beam.targetOffset = QVector2D(radius * std::cos(angle),
|
|
||||||
radius * std::sin(angle));
|
|
||||||
m_activeBeams.push_back(beam);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drain schematic drop events → toasts
|
|
||||||
{
|
|
||||||
const std::vector<SchematicDropEvent> drops =
|
|
||||||
m_sim->drainSchematicDropEvents();
|
|
||||||
for (const SchematicDropEvent& ev : drops)
|
|
||||||
{
|
|
||||||
const QString name = toDisplayName(ev.schematicId);
|
|
||||||
ToastEntry toast;
|
|
||||||
if (ev.isModuleSchematic)
|
|
||||||
{
|
|
||||||
toast.text = ev.wasNewUnlock
|
|
||||||
? tr("Module unlocked: ") + name
|
|
||||||
: name + tr(" production level -> ") + QString::number(ev.newLevel);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
toast.text = ev.wasNewUnlock
|
|
||||||
? tr("Schematic unlocked: ") + name
|
|
||||||
: name + tr(" production level -> ") + QString::number(ev.newLevel);
|
|
||||||
}
|
|
||||||
toast.createdWallMs = m_wallMs;
|
|
||||||
m_toasts.push_back(toast);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,19 +173,6 @@ void GameWorldView::onFrame()
|
|||||||
m_activeBeams = std::move(live);
|
m_activeBeams = std::move(live);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expire old toasts
|
|
||||||
{
|
|
||||||
std::vector<ToastEntry> live;
|
|
||||||
for (const ToastEntry& t : m_toasts)
|
|
||||||
{
|
|
||||||
if (m_wallMs - t.createdWallMs < kToastLifetimeMs)
|
|
||||||
{
|
|
||||||
live.push_back(t);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m_toasts = std::move(live);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply held scroll
|
// Apply held scroll
|
||||||
{
|
{
|
||||||
const float delta = kScrollSpeedTilesPerSec
|
const float delta = kScrollSpeedTilesPerSec
|
||||||
@@ -276,12 +210,25 @@ void GameWorldView::onFrame()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schematic choice available
|
||||||
|
if (m_sim->hasSchematicChoicesPending() && !m_schematicChoiceShown)
|
||||||
|
{
|
||||||
|
m_schematicChoiceShown = true;
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SchematicChoicesAvailableEvent>(m_sim->getPendingSchematicChoices()));
|
||||||
|
}
|
||||||
|
if (!m_sim->hasSchematicChoicesPending())
|
||||||
|
{
|
||||||
|
m_schematicChoiceShown = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Game over check
|
// Game over check
|
||||||
if (m_sim->isGameOver() && !m_gameOverShown)
|
if (m_sim->isGameOver() && !m_gameOverShown)
|
||||||
{
|
{
|
||||||
m_gameOverShown = true;
|
m_gameOverShown = true;
|
||||||
m_gameSpeedMultiplier = 0.0;
|
m_gameSpeedMultiplier = 0.0;
|
||||||
emit gameOver();
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<GameOverEvent>());
|
||||||
}
|
}
|
||||||
|
|
||||||
update();
|
update();
|
||||||
@@ -1097,41 +1044,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::drawScreenSpace(QPainter& painter)
|
void GameWorldView::drawScreenSpace(QPainter& /*painter*/)
|
||||||
{
|
{
|
||||||
painter.resetTransform();
|
|
||||||
|
|
||||||
const int margin = 8;
|
|
||||||
const int toastW = 320;
|
|
||||||
const int toastH = 36;
|
|
||||||
const int spacing = 4;
|
|
||||||
|
|
||||||
QFont toastFont = painter.font();
|
|
||||||
toastFont.setPointSize(m_visuals->toast.fontSize);
|
|
||||||
painter.setFont(toastFont);
|
|
||||||
|
|
||||||
int y = margin;
|
|
||||||
for (const ToastEntry& toast : m_toasts)
|
|
||||||
{
|
|
||||||
const qint64 age = m_wallMs - toast.createdWallMs;
|
|
||||||
double opacity = 1.0;
|
|
||||||
if (age > kToastFadeStartMs)
|
|
||||||
{
|
|
||||||
opacity = 1.0 - static_cast<double>(age - kToastFadeStartMs)
|
|
||||||
/ static_cast<double>(kToastLifetimeMs - kToastFadeStartMs);
|
|
||||||
opacity = std::max(0.0, opacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
painter.setOpacity(opacity);
|
|
||||||
const int x = width() - toastW - margin;
|
|
||||||
const QRect toastRect(x, y, toastW, toastH);
|
|
||||||
painter.fillRect(toastRect, m_visuals->toast.bg);
|
|
||||||
painter.setPen(m_visuals->toast.fg);
|
|
||||||
painter.drawText(toastRect.adjusted(8, 0, -8, 0),
|
|
||||||
Qt::AlignVCenter | Qt::AlignLeft, toast.text);
|
|
||||||
painter.setOpacity(1.0);
|
|
||||||
y += toastH + spacing;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1222,7 +1136,8 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Qt::Key_Escape:
|
case Qt::Key_Escape:
|
||||||
emit escapeMenuRequested();
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<EscapeMenuRequestedEvent>());
|
||||||
break;
|
break;
|
||||||
case Qt::Key_Backspace:
|
case Qt::Key_Backspace:
|
||||||
toggleDemolishMode();
|
toggleDemolishMode();
|
||||||
@@ -1309,7 +1224,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
if (hitEntity != entt::null)
|
if (hitEntity != entt::null)
|
||||||
{
|
{
|
||||||
m_selectedBuildingIds.clear();
|
m_selectedBuildingIds.clear();
|
||||||
emit selectionChanged(m_selectedBuildingIds);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
m_selectedEntity = hitEntity;
|
m_selectedEntity = hitEntity;
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<EntitySelectedEvent>(hitEntity));
|
std::make_shared<EntitySelectedEvent>(hitEntity));
|
||||||
@@ -1346,14 +1262,16 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
{
|
{
|
||||||
m_selectedBuildingIds = { id };
|
m_selectedBuildingIds = { id };
|
||||||
}
|
}
|
||||||
emit selectionChanged(m_selectedBuildingIds);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!(event->modifiers() & Qt::ControlModifier))
|
if (!(event->modifiers() & Qt::ControlModifier))
|
||||||
{
|
{
|
||||||
m_selectedBuildingIds.clear();
|
m_selectedBuildingIds.clear();
|
||||||
emit selectionChanged(m_selectedBuildingIds);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
}
|
}
|
||||||
m_boxSelecting = true;
|
m_boxSelecting = true;
|
||||||
m_boxStartTile = tile;
|
m_boxStartTile = tile;
|
||||||
@@ -1452,12 +1370,13 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
if (!found) { m_selectedBuildingIds.push_back(id); }
|
if (!found) { m_selectedBuildingIds.push_back(id); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emit selectionChanged(m_selectedBuildingIds);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Slots
|
// Methods (formerly slots)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
void GameWorldView::toggleDemolishMode()
|
void GameWorldView::toggleDemolishMode()
|
||||||
@@ -1473,7 +1392,8 @@ void GameWorldView::toggleDemolishMode()
|
|||||||
if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
|
if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
|
||||||
m_demolishMode = true;
|
m_demolishMode = true;
|
||||||
}
|
}
|
||||||
emit demolishModeChanged(m_demolishMode);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<DemolishModeChangedEvent>(m_demolishMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::enterBuilderMode(BuildingType type)
|
void GameWorldView::enterBuilderMode(BuildingType type)
|
||||||
@@ -1483,14 +1403,16 @@ void GameWorldView::enterBuilderMode(BuildingType type)
|
|||||||
m_ghostValid = false;
|
m_ghostValid = false;
|
||||||
m_demolishMode = false;
|
m_demolishMode = false;
|
||||||
m_blueprintMode.reset();
|
m_blueprintMode.reset();
|
||||||
emit demolishModeChanged(false);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<DemolishModeChangedEvent>(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::enterBlueprintMode(Blueprint blueprint)
|
void GameWorldView::enterBlueprintMode(Blueprint blueprint)
|
||||||
{
|
{
|
||||||
if (m_builderType.has_value()) { exitBuilderMode(); }
|
if (m_builderType.has_value()) { exitBuilderMode(); }
|
||||||
m_demolishMode = false;
|
m_demolishMode = false;
|
||||||
emit demolishModeChanged(false);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<DemolishModeChangedEvent>(false));
|
||||||
m_blueprintGhostTile = m_ghostTile;
|
m_blueprintGhostTile = m_ghostTile;
|
||||||
m_blueprintMode = std::move(blueprint);
|
m_blueprintMode = std::move(blueprint);
|
||||||
}
|
}
|
||||||
@@ -1498,7 +1420,8 @@ void GameWorldView::enterBlueprintMode(Blueprint blueprint)
|
|||||||
void GameWorldView::exitBlueprintMode()
|
void GameWorldView::exitBlueprintMode()
|
||||||
{
|
{
|
||||||
m_blueprintMode.reset();
|
m_blueprintMode.reset();
|
||||||
emit blueprintModeExited();
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<BlueprintModeExitedEvent>());
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::exitBuilderMode()
|
void GameWorldView::exitBuilderMode()
|
||||||
@@ -1506,7 +1429,8 @@ void GameWorldView::exitBuilderMode()
|
|||||||
m_builderType.reset();
|
m_builderType.reset();
|
||||||
m_beltDragTiles.clear();
|
m_beltDragTiles.clear();
|
||||||
m_dragging = false;
|
m_dragging = false;
|
||||||
emit builderModeExited();
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<BuilderModeExitedEvent>());
|
||||||
}
|
}
|
||||||
|
|
||||||
double GameWorldView::gameSpeed() const
|
double GameWorldView::gameSpeed() const
|
||||||
@@ -1514,6 +1438,11 @@ double GameWorldView::gameSpeed() const
|
|||||||
return m_gameSpeedMultiplier;
|
return m_gameSpeedMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameWorldView::resetFrameTimer()
|
||||||
|
{
|
||||||
|
m_frameTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
void GameWorldView::setGameSpeed(double multiplier)
|
void GameWorldView::setGameSpeed(double multiplier)
|
||||||
{
|
{
|
||||||
m_gameSpeedMultiplier = multiplier;
|
m_gameSpeedMultiplier = multiplier;
|
||||||
@@ -1526,12 +1455,13 @@ void GameWorldView::resetForNewGame()
|
|||||||
exitBuilderMode();
|
exitBuilderMode();
|
||||||
exitBlueprintMode();
|
exitBlueprintMode();
|
||||||
m_activeBeams.clear();
|
m_activeBeams.clear();
|
||||||
m_toasts.clear();
|
m_schematicChoiceShown = false;
|
||||||
m_ghostRotation = Rotation::East;
|
m_ghostRotation = Rotation::East;
|
||||||
m_ghostValid = false;
|
m_ghostValid = false;
|
||||||
m_demolishMode = false;
|
m_demolishMode = false;
|
||||||
m_demolishHoverBuildingId = kInvalidBuildingId;
|
m_demolishHoverBuildingId = kInvalidBuildingId;
|
||||||
emit demolishModeChanged(false);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<DemolishModeChangedEvent>(false));
|
||||||
m_selectedBuildingIds.clear();
|
m_selectedBuildingIds.clear();
|
||||||
m_boxSelecting = false;
|
m_boxSelecting = false;
|
||||||
m_scrollXTiles = 0.0f;
|
m_scrollXTiles = 0.0f;
|
||||||
@@ -1543,7 +1473,66 @@ void GameWorldView::resetForNewGame()
|
|||||||
m_lastBlocks = -1;
|
m_lastBlocks = -1;
|
||||||
m_lastBossCounter = -1;
|
m_lastBossCounter = -1;
|
||||||
m_lastBossCountdown = Tick(-1);
|
m_lastBossCountdown = Tick(-1);
|
||||||
emit selectionChanged({});
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SelectionChangedEvent>(std::vector<BuildingId>{}));
|
||||||
setGameSpeed(1.0);
|
setGameSpeed(1.0);
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Event handlers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void GameWorldView::handleEvent(std::shared_ptr<const WeaponFiredEvent> event)
|
||||||
|
{
|
||||||
|
float maxRadius = 0.125f;
|
||||||
|
if (m_sim->admin().isValid(event->target)
|
||||||
|
&& m_sim->admin().hasAll<StationBodyComponent>(event->target))
|
||||||
|
{
|
||||||
|
const StationBodyComponent& sb = m_sim->admin().get<StationBodyComponent>(event->target);
|
||||||
|
const int shorter = std::min(sb.footprint.width(), sb.footprint.height());
|
||||||
|
maxRadius = shorter / 2.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uniform_real_distribution<float> angleDist(0.0f, 6.28318530f);
|
||||||
|
std::uniform_real_distribution<float> radiusDist(0.0f, maxRadius);
|
||||||
|
const float angle = angleDist(m_rng);
|
||||||
|
const float radius = radiusDist(m_rng);
|
||||||
|
|
||||||
|
ActiveBeam beam;
|
||||||
|
beam.event = *event;
|
||||||
|
beam.emittedWallMs = m_wallMs;
|
||||||
|
beam.targetOffset = QVector2D(radius * std::cos(angle),
|
||||||
|
radius * std::sin(angle));
|
||||||
|
m_activeBeams.push_back(beam);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::handleEvent(std::shared_ptr<const BuildingTypeSelectedEvent> event)
|
||||||
|
{
|
||||||
|
enterBuilderMode(event->type);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::handleEvent(std::shared_ptr<const ExitBuilderModeRequestedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
exitBuilderMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::handleEvent(std::shared_ptr<const DemolishModeToggleRequestedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
toggleDemolishMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::handleEvent(std::shared_ptr<const BlueprintPlacementRequestedEvent> event)
|
||||||
|
{
|
||||||
|
enterBlueprintMode(event->blueprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::handleEvent(std::shared_ptr<const ExitBlueprintModeRequestedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
exitBlueprintMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::handleEvent(std::shared_ptr<const SpeedChangeRequestedEvent> event)
|
||||||
|
{
|
||||||
|
setGameSpeed(event->multiplier);
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,10 +13,20 @@
|
|||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "Blueprint.h"
|
#include "Blueprint.h"
|
||||||
#include "SchematicDropEvent.h"
|
#include "BlueprintModeExitedEvent.h"
|
||||||
|
#include "BlueprintPlacementRequestedEvent.h"
|
||||||
|
#include "BuilderModeExitedEvent.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
|
#include "BuildingTypeSelectedEvent.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "FireEvent.h"
|
#include "DemolishModeChangedEvent.h"
|
||||||
|
#include "DemolishModeToggleRequestedEvent.h"
|
||||||
|
#include "EventHandler.h"
|
||||||
|
#include "ExitBlueprintModeRequestedEvent.h"
|
||||||
|
#include "ExitBuilderModeRequestedEvent.h"
|
||||||
|
#include "WeaponFiredEvent.h"
|
||||||
|
#include "SchematicChoiceOption.h"
|
||||||
|
#include "SpeedChangeRequestedEvent.h"
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
#include "EntitySelectedEvent.h"
|
#include "EntitySelectedEvent.h"
|
||||||
@@ -38,31 +48,24 @@ struct QPointCompare
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class GameWorldView : public QOpenGLWidget
|
class GameWorldView : public QOpenGLWidget,
|
||||||
|
public CombinedEventHandler<WeaponFiredEvent,
|
||||||
|
BuildingTypeSelectedEvent,
|
||||||
|
ExitBuilderModeRequestedEvent,
|
||||||
|
DemolishModeToggleRequestedEvent,
|
||||||
|
BlueprintPlacementRequestedEvent,
|
||||||
|
ExitBlueprintModeRequestedEvent,
|
||||||
|
SpeedChangeRequestedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
GameWorldView(Simulation* sim, const GameConfig* config,
|
GameWorldView(Simulation* sim, const GameConfig* config,
|
||||||
const VisualsConfig* visuals, QWidget* parent = nullptr);
|
const VisualsConfig* visuals, QWidget* parent = nullptr);
|
||||||
|
~GameWorldView() override;
|
||||||
|
|
||||||
signals:
|
|
||||||
void selectionChanged(const std::vector<BuildingId>& ids);
|
|
||||||
void gameOver();
|
|
||||||
void builderModeExited();
|
|
||||||
void blueprintModeExited();
|
|
||||||
void escapeMenuRequested();
|
|
||||||
void demolishModeChanged(bool active);
|
|
||||||
|
|
||||||
public:
|
|
||||||
double gameSpeed() const;
|
double gameSpeed() const;
|
||||||
|
void resetFrameTimer();
|
||||||
public slots:
|
|
||||||
void enterBuilderMode(BuildingType type);
|
|
||||||
void exitBuilderMode();
|
|
||||||
void enterBlueprintMode(Blueprint blueprint);
|
|
||||||
void exitBlueprintMode();
|
|
||||||
void toggleDemolishMode();
|
|
||||||
void setGameSpeed(double multiplier);
|
void setGameSpeed(double multiplier);
|
||||||
void resetForNewGame();
|
void resetForNewGame();
|
||||||
|
|
||||||
@@ -79,6 +82,14 @@ private slots:
|
|||||||
void onFrame();
|
void onFrame();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void handleEvent(std::shared_ptr<const WeaponFiredEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const BuildingTypeSelectedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const ExitBuilderModeRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const DemolishModeToggleRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const BlueprintPlacementRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const ExitBlueprintModeRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const SpeedChangeRequestedEvent> event) override;
|
||||||
|
|
||||||
void drawTiles(QPainter& painter);
|
void drawTiles(QPainter& painter);
|
||||||
void drawBuildings(QPainter& painter);
|
void drawBuildings(QPainter& painter);
|
||||||
void drawStations(QPainter& painter);
|
void drawStations(QPainter& painter);
|
||||||
@@ -118,22 +129,20 @@ private:
|
|||||||
void stepSpeed(int delta);
|
void stepSpeed(int delta);
|
||||||
void placeAtTile(QPoint tile);
|
void placeAtTile(QPoint tile);
|
||||||
|
|
||||||
|
void enterBuilderMode(BuildingType type);
|
||||||
|
void exitBuilderMode();
|
||||||
|
void enterBlueprintMode(Blueprint blueprint);
|
||||||
|
void exitBlueprintMode();
|
||||||
|
void toggleDemolishMode();
|
||||||
|
|
||||||
struct ActiveBeam
|
struct ActiveBeam
|
||||||
{
|
{
|
||||||
FireEvent event;
|
WeaponFiredEvent event;
|
||||||
qint64 emittedWallMs;
|
qint64 emittedWallMs;
|
||||||
QVector2D targetOffset;
|
QVector2D targetOffset;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ToastEntry
|
|
||||||
{
|
|
||||||
QString text;
|
|
||||||
qint64 createdWallMs;
|
|
||||||
};
|
|
||||||
|
|
||||||
static constexpr qint64 kBeamLifetimeMs = 300;
|
static constexpr qint64 kBeamLifetimeMs = 300;
|
||||||
static constexpr qint64 kToastLifetimeMs = 4000;
|
|
||||||
static constexpr qint64 kToastFadeStartMs = 3500;
|
|
||||||
static constexpr float kScrollSpeedTilesPerSec = 10.0f;
|
static constexpr float kScrollSpeedTilesPerSec = 10.0f;
|
||||||
|
|
||||||
Simulation* m_sim;
|
Simulation* m_sim;
|
||||||
@@ -151,7 +160,6 @@ private:
|
|||||||
QTimer* m_renderTimer;
|
QTimer* m_renderTimer;
|
||||||
|
|
||||||
std::vector<ActiveBeam> m_activeBeams;
|
std::vector<ActiveBeam> m_activeBeams;
|
||||||
std::vector<ToastEntry> m_toasts;
|
|
||||||
|
|
||||||
std::optional<BuildingType> m_builderType;
|
std::optional<BuildingType> m_builderType;
|
||||||
Rotation m_ghostRotation;
|
Rotation m_ghostRotation;
|
||||||
@@ -176,6 +184,7 @@ private:
|
|||||||
bool m_scrollLeft;
|
bool m_scrollLeft;
|
||||||
bool m_scrollRight;
|
bool m_scrollRight;
|
||||||
bool m_gameOverShown;
|
bool m_gameOverShown;
|
||||||
|
bool m_schematicChoiceShown;
|
||||||
|
|
||||||
Tick m_lastTick = Tick(-1);
|
Tick m_lastTick = Tick(-1);
|
||||||
int m_lastBlocks = -1;
|
int m_lastBlocks = -1;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QSignalMapper>
|
#include <QSignalMapper>
|
||||||
|
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "SpeedChangeRequestedEvent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
||||||
@@ -90,6 +92,7 @@ void HeaderBar::onSpeedButton(int index)
|
|||||||
{
|
{
|
||||||
if (index >= 0 && index < kSpeedCount)
|
if (index >= 0 && index < kSpeedCount)
|
||||||
{
|
{
|
||||||
emit speedChanged(kSpeeds[index]);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SpeedChangeRequestedEvent>(kSpeeds[index]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ public:
|
|||||||
explicit HeaderBar(QWidget* parent = nullptr);
|
explicit HeaderBar(QWidget* parent = nullptr);
|
||||||
~HeaderBar() override;
|
~HeaderBar() override;
|
||||||
|
|
||||||
signals:
|
|
||||||
void speedChanged(double multiplier);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onSpeedButton(int index);
|
void onSpeedButton(int index);
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
#include "GameWorldView.h"
|
#include "GameWorldView.h"
|
||||||
|
#include "SchematicChoiceDialog.h"
|
||||||
#include "HeaderBar.h"
|
#include "HeaderBar.h"
|
||||||
#include "SelectedBuildingPanel.h"
|
#include "SelectedBuildingPanel.h"
|
||||||
#include "ShipLayoutBlueprintSerializer.h"
|
#include "ShipLayoutBlueprintSerializer.h"
|
||||||
@@ -52,52 +53,6 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* p
|
|||||||
bottomLayout->addWidget(m_buildButtonGrid, 1);
|
bottomLayout->addWidget(m_buildButtonGrid, 1);
|
||||||
bottomLayout->addWidget(m_blueprintPanel, 1);
|
bottomLayout->addWidget(m_blueprintPanel, 1);
|
||||||
|
|
||||||
// Signals: game world → other panels
|
|
||||||
connect(m_gameWorldView, &GameWorldView::selectionChanged,
|
|
||||||
m_selectedBuildingPanel, &SelectedBuildingPanel::onSelectionChanged);
|
|
||||||
|
|
||||||
connect(m_gameWorldView, &GameWorldView::gameOver,
|
|
||||||
this, &MainWindow::onGameOver);
|
|
||||||
|
|
||||||
connect(m_gameWorldView, &GameWorldView::escapeMenuRequested,
|
|
||||||
this, &MainWindow::onEscapeMenuRequested);
|
|
||||||
|
|
||||||
connect(m_selectedBuildingPanel, &SelectedBuildingPanel::layoutDialogRequested,
|
|
||||||
this, &MainWindow::onLayoutDialogRequested);
|
|
||||||
|
|
||||||
// Signals: build grid → game world
|
|
||||||
connect(m_buildButtonGrid, &BuildButtonGrid::buildingTypeSelected,
|
|
||||||
m_gameWorldView, &GameWorldView::enterBuilderMode);
|
|
||||||
|
|
||||||
connect(m_buildButtonGrid, &BuildButtonGrid::builderModeExited,
|
|
||||||
m_gameWorldView, &GameWorldView::exitBuilderMode);
|
|
||||||
|
|
||||||
connect(m_gameWorldView, &GameWorldView::builderModeExited,
|
|
||||||
m_buildButtonGrid, &BuildButtonGrid::clearActiveButton);
|
|
||||||
|
|
||||||
connect(m_buildButtonGrid, &BuildButtonGrid::demolishModeToggleRequested,
|
|
||||||
m_gameWorldView, &GameWorldView::toggleDemolishMode);
|
|
||||||
|
|
||||||
connect(m_gameWorldView, &GameWorldView::demolishModeChanged,
|
|
||||||
m_buildButtonGrid, &BuildButtonGrid::setDemolishModeActive);
|
|
||||||
|
|
||||||
// Signals: blueprint panel ↔ game world
|
|
||||||
connect(m_gameWorldView, &GameWorldView::selectionChanged,
|
|
||||||
m_blueprintPanel, &BlueprintPanel::onSelectionChanged);
|
|
||||||
|
|
||||||
connect(m_blueprintPanel, &BlueprintPanel::blueprintPlacementRequested,
|
|
||||||
m_gameWorldView, &GameWorldView::enterBlueprintMode);
|
|
||||||
|
|
||||||
connect(m_blueprintPanel, &BlueprintPanel::exitBlueprintModeRequested,
|
|
||||||
m_gameWorldView, &GameWorldView::exitBlueprintMode);
|
|
||||||
|
|
||||||
connect(m_gameWorldView, &GameWorldView::blueprintModeExited,
|
|
||||||
m_blueprintPanel, &BlueprintPanel::clearActiveBlueprintButton);
|
|
||||||
|
|
||||||
// Signals: header bar → game world
|
|
||||||
connect(m_headerBar, &HeaderBar::speedChanged,
|
|
||||||
m_gameWorldView, &GameWorldView::setGameSpeed);
|
|
||||||
|
|
||||||
m_gameWorldView->setFocus();
|
m_gameWorldView->setFocus();
|
||||||
|
|
||||||
connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) {
|
connect(qApp, &QApplication::focusChanged, this, [this](QWidget*, QWidget* newWidget) {
|
||||||
@@ -125,12 +80,12 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir, QWidget* p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerForEvent();
|
registerForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::~MainWindow()
|
MainWindow::~MainWindow()
|
||||||
{
|
{
|
||||||
unregisterForEvent();
|
unregisterForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::resizeEvent(QResizeEvent* event)
|
void MainWindow::resizeEvent(QResizeEvent* event)
|
||||||
@@ -176,7 +131,21 @@ void MainWindow::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> e
|
|||||||
m_buildButtonGrid->updateAffordability(event->blocks);
|
m_buildButtonGrid->updateAffordability(event->blocks);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onEscapeMenuRequested()
|
void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event)
|
||||||
|
{
|
||||||
|
const double prevSpeed = m_gameWorldView->gameSpeed();
|
||||||
|
m_gameWorldView->setGameSpeed(0.0);
|
||||||
|
|
||||||
|
SchematicChoiceDialog dialog(event->choices, this);
|
||||||
|
dialog.exec();
|
||||||
|
|
||||||
|
m_sim->applySchematicChoice(dialog.getChosenIndex());
|
||||||
|
|
||||||
|
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||||
|
m_gameWorldView->resetFrameTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*event*/)
|
||||||
{
|
{
|
||||||
const double prevSpeed = m_gameWorldView->gameSpeed();
|
const double prevSpeed = m_gameWorldView->gameSpeed();
|
||||||
m_gameWorldView->setGameSpeed(0.0);
|
m_gameWorldView->setGameSpeed(0.0);
|
||||||
@@ -204,6 +173,7 @@ void MainWindow::onEscapeMenuRequested()
|
|||||||
QMessageBox::critical(this, tr("Config Error"),
|
QMessageBox::critical(this, tr("Config Error"),
|
||||||
tr("Failed to reload config:\n%1").arg(e.what()));
|
tr("Failed to reload config:\n%1").arg(e.what()));
|
||||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||||
|
m_gameWorldView->resetFrameTimer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
m_gameWorldView->resetForNewGame();
|
m_gameWorldView->resetForNewGame();
|
||||||
@@ -215,18 +185,20 @@ void MainWindow::onEscapeMenuRequested()
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||||
|
m_gameWorldView->resetFrameTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onLayoutDialogRequested(BuildingId shipyardId)
|
void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event)
|
||||||
{
|
{
|
||||||
const double prevSpeed = m_gameWorldView->gameSpeed();
|
const double prevSpeed = m_gameWorldView->gameSpeed();
|
||||||
m_gameWorldView->setGameSpeed(0.0);
|
m_gameWorldView->setGameSpeed(0.0);
|
||||||
|
|
||||||
const Building* b = m_sim->buildings().findBuilding(shipyardId);
|
const Building* b = m_sim->buildings().findBuilding(event->shipyardId);
|
||||||
if (!b)
|
if (!b)
|
||||||
{
|
{
|
||||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||||
|
m_gameWorldView->resetFrameTimer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,13 +226,14 @@ void MainWindow::onLayoutDialogRequested(BuildingId shipyardId)
|
|||||||
this);
|
this);
|
||||||
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
|
if (dialog.exec() == QDialog::Accepted && dialog.result().has_value())
|
||||||
{
|
{
|
||||||
m_sim->buildings().setShipLayout(shipyardId, *dialog.result());
|
m_sim->buildings().setShipLayout(event->shipyardId, *dialog.result());
|
||||||
}
|
}
|
||||||
|
|
||||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||||
|
m_gameWorldView->resetFrameTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onGameOver()
|
void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
||||||
{
|
{
|
||||||
const Tick tick = m_sim->currentTick();
|
const Tick tick = m_sim->currentTick();
|
||||||
const int totalSeconds = static_cast<int>(ticksToSeconds(tick));
|
const int totalSeconds = static_cast<int>(ticksToSeconds(tick));
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
|
|
||||||
#include "BuildingBlocksChangedEvent.h"
|
#include "BuildingBlocksChangedEvent.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
|
#include "EscapeMenuRequestedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
|
#include "GameOverEvent.h"
|
||||||
|
#include "LayoutDialogRequestedEvent.h"
|
||||||
|
#include "SchematicChoicesAvailableEvent.h"
|
||||||
#include "ShipLayoutBlueprint.h"
|
#include "ShipLayoutBlueprint.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
@@ -22,7 +26,11 @@ class QCloseEvent;
|
|||||||
class QResizeEvent;
|
class QResizeEvent;
|
||||||
|
|
||||||
class MainWindow : public QWidget,
|
class MainWindow : public QWidget,
|
||||||
public EventHandler<BuildingBlocksChangedEvent>
|
public CombinedEventHandler<BuildingBlocksChangedEvent,
|
||||||
|
SchematicChoicesAvailableEvent,
|
||||||
|
GameOverEvent,
|
||||||
|
EscapeMenuRequestedEvent,
|
||||||
|
LayoutDialogRequestedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -36,13 +44,12 @@ protected:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const GameOverEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event) override;
|
||||||
void layoutPanels();
|
void layoutPanels();
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onGameOver();
|
|
||||||
void onEscapeMenuRequested();
|
|
||||||
void onLayoutDialogRequested(BuildingId shipyardId);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_configDir;
|
std::string m_configDir;
|
||||||
VisualsConfig m_visuals;
|
VisualsConfig m_visuals;
|
||||||
|
|||||||
125
src/ui/SchematicChoiceDialog.cpp
Normal file
125
src/ui/SchematicChoiceDialog.cpp
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#include "SchematicChoiceDialog.h"
|
||||||
|
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
SchematicChoiceDialog::SchematicChoiceDialog(
|
||||||
|
const std::vector<SchematicChoiceOption>& options,
|
||||||
|
QWidget* parent)
|
||||||
|
: QDialog(parent)
|
||||||
|
, m_chosenIndex(0)
|
||||||
|
{
|
||||||
|
setWindowTitle(tr("Schematic Drop"));
|
||||||
|
setWindowFlags(windowFlags() & ~Qt::WindowCloseButtonHint);
|
||||||
|
setModal(true);
|
||||||
|
|
||||||
|
QVBoxLayout* mainLayout = new QVBoxLayout(this);
|
||||||
|
|
||||||
|
QLabel* titleLabel = new QLabel(tr("Choose a schematic to unlock:"), this);
|
||||||
|
QFont titleFont = titleLabel->font();
|
||||||
|
titleFont.setPointSize(titleFont.pointSize() + 2);
|
||||||
|
titleFont.setBold(true);
|
||||||
|
titleLabel->setFont(titleFont);
|
||||||
|
titleLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
mainLayout->addWidget(titleLabel);
|
||||||
|
|
||||||
|
QHBoxLayout* optionsLayout = new QHBoxLayout();
|
||||||
|
mainLayout->addLayout(optionsLayout);
|
||||||
|
|
||||||
|
for (int i = 0; i < static_cast<int>(options.size()); ++i)
|
||||||
|
{
|
||||||
|
const SchematicChoiceOption& option = options[static_cast<std::size_t>(i)];
|
||||||
|
|
||||||
|
QWidget* card = new QWidget(this);
|
||||||
|
QVBoxLayout* cardLayout = new QVBoxLayout(card);
|
||||||
|
card->setStyleSheet("QWidget { border: 1px solid gray; padding: 8px; }");
|
||||||
|
|
||||||
|
QLabel* nameLabel = new QLabel(QString::fromStdString(option.displayName), card);
|
||||||
|
QFont nameFont = nameLabel->font();
|
||||||
|
nameFont.setPointSize(nameFont.pointSize() + 1);
|
||||||
|
nameFont.setBold(true);
|
||||||
|
nameLabel->setFont(nameFont);
|
||||||
|
nameLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
cardLayout->addWidget(nameLabel);
|
||||||
|
|
||||||
|
QString typeText;
|
||||||
|
if (option.type == SchematicType::Ship)
|
||||||
|
{
|
||||||
|
typeText = tr("Ship");
|
||||||
|
}
|
||||||
|
else if (option.type == SchematicType::Module)
|
||||||
|
{
|
||||||
|
typeText = tr("Module");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
typeText = tr("Recipe");
|
||||||
|
}
|
||||||
|
QLabel* typeLabel = new QLabel(typeText, card);
|
||||||
|
typeLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
cardLayout->addWidget(typeLabel);
|
||||||
|
|
||||||
|
QString statusText;
|
||||||
|
if (option.isNewUnlock)
|
||||||
|
{
|
||||||
|
statusText = tr("New unlock");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
statusText = tr("Level up -> %1").arg(option.targetLevel);
|
||||||
|
}
|
||||||
|
QLabel* statusLabel = new QLabel(statusText, card);
|
||||||
|
statusLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
cardLayout->addWidget(statusLabel);
|
||||||
|
|
||||||
|
QLabel* unlocksHeaderLabel = new QLabel(tr("Unlocks recipes for:"), card);
|
||||||
|
QFont unlocksHeaderFont = unlocksHeaderLabel->font();
|
||||||
|
unlocksHeaderFont.setBold(true);
|
||||||
|
unlocksHeaderLabel->setFont(unlocksHeaderFont);
|
||||||
|
unlocksHeaderLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
cardLayout->addWidget(unlocksHeaderLabel);
|
||||||
|
|
||||||
|
QString unlocksText;
|
||||||
|
if (option.newlyUnlockedItemNames.empty())
|
||||||
|
{
|
||||||
|
unlocksText = tr("None");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QStringList itemLines;
|
||||||
|
for (const std::string& itemName : option.newlyUnlockedItemNames)
|
||||||
|
{
|
||||||
|
itemLines << QString::fromStdString(itemName);
|
||||||
|
}
|
||||||
|
unlocksText = itemLines.join("\n");
|
||||||
|
}
|
||||||
|
QLabel* unlocksLabel = new QLabel(unlocksText, card);
|
||||||
|
unlocksLabel->setAlignment(Qt::AlignCenter);
|
||||||
|
cardLayout->addWidget(unlocksLabel);
|
||||||
|
|
||||||
|
QPushButton* selectButton = new QPushButton(tr("Select"), card);
|
||||||
|
cardLayout->addWidget(selectButton);
|
||||||
|
|
||||||
|
const int index = i;
|
||||||
|
connect(selectButton, &QPushButton::clicked, this, [this, index]()
|
||||||
|
{
|
||||||
|
onOptionClicked(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
optionsLayout->addWidget(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int SchematicChoiceDialog::getChosenIndex() const
|
||||||
|
{
|
||||||
|
return m_chosenIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SchematicChoiceDialog::onOptionClicked(int index)
|
||||||
|
{
|
||||||
|
m_chosenIndex = index;
|
||||||
|
accept();
|
||||||
|
}
|
||||||
23
src/ui/SchematicChoiceDialog.h
Normal file
23
src/ui/SchematicChoiceDialog.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
|
||||||
|
#include "SchematicChoiceOption.h"
|
||||||
|
|
||||||
|
class SchematicChoiceDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
SchematicChoiceDialog(const std::vector<SchematicChoiceOption>& options,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
int getChosenIndex() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void onOptionClicked(int index);
|
||||||
|
|
||||||
|
int m_chosenIndex;
|
||||||
|
};
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "DynamicBodyComponent.h"
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "EntitySelectedEvent.h"
|
#include "EntitySelectedEvent.h"
|
||||||
|
#include "EventManager.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "ModuleOwnerComponent.h"
|
#include "ModuleOwnerComponent.h"
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "ItemType.h"
|
#include "ItemType.h"
|
||||||
|
#include "LayoutDialogRequestedEvent.h"
|
||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "ShipLayoutPreview.h"
|
#include "ShipLayoutPreview.h"
|
||||||
@@ -139,7 +141,8 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
|||||||
connect(m_configureLayoutBtn, &QPushButton::clicked, this, [this]() {
|
connect(m_configureLayoutBtn, &QPushButton::clicked, this, [this]() {
|
||||||
if (m_singleBuildingId != kInvalidBuildingId)
|
if (m_singleBuildingId != kInvalidBuildingId)
|
||||||
{
|
{
|
||||||
emit layoutDialogRequested(m_singleBuildingId);
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<LayoutDialogRequestedEvent>(m_singleBuildingId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
connect(m_filterAList, &QListWidget::itemChanged,
|
connect(m_filterAList, &QListWidget::itemChanged,
|
||||||
@@ -861,3 +864,8 @@ void SelectedBuildingPanel::clearEntityDisplay()
|
|||||||
m_entityStatsPanel->hide();
|
m_entityStatsPanel->hide();
|
||||||
m_stationStatsLabel->hide();
|
m_stationStatsLabel->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||||
|
{
|
||||||
|
onSelectionChanged(event->ids);
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "RecipesConfig.h"
|
#include "RecipesConfig.h"
|
||||||
|
#include "SelectionChangedEvent.h"
|
||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
@@ -30,7 +31,9 @@ class QPushButton;
|
|||||||
class QVBoxLayout;
|
class QVBoxLayout;
|
||||||
|
|
||||||
class SelectedBuildingPanel : public QWidget,
|
class SelectedBuildingPanel : public QWidget,
|
||||||
public CombinedEventHandler<TickAdvancedEvent, EntitySelectedEvent>
|
public CombinedEventHandler<TickAdvancedEvent,
|
||||||
|
EntitySelectedEvent,
|
||||||
|
SelectionChangedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -39,15 +42,10 @@ public:
|
|||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
~SelectedBuildingPanel() override;
|
~SelectedBuildingPanel() override;
|
||||||
|
|
||||||
signals:
|
|
||||||
void layoutDialogRequested(BuildingId shipyardId);
|
|
||||||
|
|
||||||
public slots:
|
|
||||||
void onSelectionChanged(const std::vector<BuildingId>& ids);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
void handleEvent(std::shared_ptr<const EntitySelectedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onRecipeChanged(int comboIndex);
|
void onRecipeChanged(int comboIndex);
|
||||||
@@ -55,6 +53,7 @@ private slots:
|
|||||||
void onSplitterFilterChanged();
|
void onSplitterFilterChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void onSelectionChanged(const std::vector<BuildingId>& ids);
|
||||||
void rebuild();
|
void rebuild();
|
||||||
void clearContent();
|
void clearContent();
|
||||||
void buildEmpty();
|
void buildEmpty();
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
#include "ShipLayoutDialog.h"
|
#include "ShipLayoutDialog.h"
|
||||||
#include "ShipStatsPanel.h"
|
#include "ShipStatsPanel.h"
|
||||||
|
|
||||||
#include <cctype>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
|
#include "DisplayName.h"
|
||||||
|
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QInputDialog>
|
#include <QInputDialog>
|
||||||
@@ -20,30 +21,6 @@ namespace
|
|||||||
|
|
||||||
const int kCellSize = 32;
|
const int kCellSize = 32;
|
||||||
|
|
||||||
QString displayName(const std::string& id)
|
|
||||||
{
|
|
||||||
QString result;
|
|
||||||
bool nextUpper = true;
|
|
||||||
for (char c : id)
|
|
||||||
{
|
|
||||||
if (c == '_')
|
|
||||||
{
|
|
||||||
result += ' ';
|
|
||||||
nextUpper = true;
|
|
||||||
}
|
|
||||||
else if (nextUpper)
|
|
||||||
{
|
|
||||||
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
|
||||||
nextUpper = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> rotateMaskCW(const std::vector<std::string>& grid)
|
std::vector<std::string> rotateMaskCW(const std::vector<std::string>& grid)
|
||||||
{
|
{
|
||||||
if (grid.empty())
|
if (grid.empty())
|
||||||
@@ -478,7 +455,7 @@ ShipLayoutDialog::ShipLayoutDialog(const GameConfig* config,
|
|||||||
m_moduleButtons.push_back(nullptr);
|
m_moduleButtons.push_back(nullptr);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const QString label = displayName(def.id)
|
const QString label = QString::fromStdString(toDisplayName(def.id))
|
||||||
+ "\n" + QString::fromStdString(def.glyph);
|
+ "\n" + QString::fromStdString(def.glyph);
|
||||||
QPushButton* btn = new QPushButton(label, this);
|
QPushButton* btn = new QPushButton(label, this);
|
||||||
btn->setCheckable(true);
|
btn->setCheckable(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user