Compare commits
3 Commits
9d28175b17
...
535d4f8f24
| Author | SHA1 | Date | |
|---|---|---|---|
| 535d4f8f24 | |||
| 8c4fb78fc9 | |||
| 698dd4d13d |
@@ -269,6 +269,8 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- **Right** (below the grid): The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD).
|
- **Right** (below the grid): The layout blueprint panel (see REQ-MOD-UI-BLUEPRINT-PANEL through REQ-MOD-UI-BLUEPRINT-FILE-LOAD).
|
||||||
- **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selected building panel are recalculated, and the ship layout preview is refreshed.
|
- **Bottom**: A "Confirm" button and a "Cancel" button. Cancel discards all changes made in this dialog session and closes the dialog. Confirm applies the changes: the shipyard's configured layout is updated, the required materials and cycle time displayed in the selected building panel are recalculated, and the ship layout preview is refreshed.
|
||||||
|
|
||||||
|
- REQ-MOD-UI-AUTO-DIALOG: When the player selects a schematic for a shipyard (operational building or construction site) through the schematic selection dialog (REQ-UI-SELECT-BUTTON), and the chosen schematic **differs** from the shipyard's current schematic, the layout configuration dialog (REQ-MOD-UI-DIALOG) opens automatically and immediately once the selection dialog closes — exactly as if the player had then clicked "Configure". Re-selecting the schematic already set does not reopen the dialog. This auto-open applies only to the manual schematic selection dialog; schematic changes applied via the copy-settings gesture (REQ-BLD-COPY-CONFIG) or blueprint placement (REQ-UI-BLUEPRINT-PLACE) do **not** auto-open the dialog. The player may still cancel the auto-opened dialog (REQ-MOD-UI-DIALOG), which leaves the newly selected schematic in place with its default empty layout; the "Configure" button (REQ-MOD-UI-PREVIEW) remains available to open the dialog again later.
|
||||||
|
|
||||||
- REQ-MOD-UI-MODULE-TOOLTIP: Each module selection button in the layout configuration dialog (REQ-MOD-UI-DIALOG) shows a hover tooltip with the descriptive text defined for that module type in `modules.toml` (the optional per-module tooltip field). If a module type defines no tooltip text, its button shows no tooltip. The "Remove" button is not a module type and has no config-defined tooltip.
|
- REQ-MOD-UI-MODULE-TOOLTIP: Each module selection button in the layout configuration dialog (REQ-MOD-UI-DIALOG) shows a hover tooltip with the descriptive text defined for that module type in `modules.toml` (the optional per-module tooltip field). If a module type defines no tooltip text, its button shows no tooltip. The "Remove" button is not a module type and has no config-defined tooltip.
|
||||||
|
|
||||||
- REQ-MOD-UI-STATS-PANEL: The **ship stats panel** in the layout configuration dialog shows the stats of the currently configured ship layout as they would be computed, incorporating all passive module modifiers per REQ-MOD-STAT-CALC. The panel updates in real time whenever modules are placed or removed in the layout grid.
|
- REQ-MOD-UI-STATS-PANEL: The **ship stats panel** in the layout configuration dialog shows the stats of the currently configured ship layout as they would be computed, incorporating all passive module modifiers per REQ-MOD-STAT-CALC. The panel updates in real time whenever modules are placed or removed in the layout grid.
|
||||||
@@ -457,10 +459,10 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
|
|
||||||
### Selected Building Panel
|
### Selected Building Panel
|
||||||
|
|
||||||
- REQ-UI-EMPTY-SELECTION: When no building is selected, the panel is empty.
|
- REQ-UI-EMPTY-SELECTION: When nothing is selected (no building, construction site, ship, defence station, or scrap pile), the panel is empty.
|
||||||
- REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows: building name, current recipe or schematic selection, input buffer contents, and output buffer contents. Buffer counts are displayed as `a/b` where `a` is the current item count and `b` is the per-cycle amount (items consumed per run for inputs; items produced per run for outputs). For a selected construction site, the recipe/schematic selection (and, for a shipyard, the layout preview and "Configure" button) are shown but the buffer rows are omitted (REQ-BLD-SITE-CONFIG).
|
- REQ-UI-SINGLE-SELECTION: When one building is selected, the panel shows: building name, current recipe or schematic selection, input buffer contents, and output buffer contents. Buffer counts are displayed as `a/b` where `a` is the current item count and `b` is the per-cycle amount (items consumed per run for inputs; items produced per run for outputs). For a selected construction site, the recipe/schematic selection (and, for a shipyard, the layout preview and "Configure" button) are shown but the buffer rows are omitted (REQ-BLD-SITE-CONFIG).
|
||||||
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the selected building panel also shows: (a) the cycle time of the currently selected recipe or schematic in seconds, and (b) the completion percentage of the active production cycle as an integer (e.g. `42%`), or the text `idle` when no production cycle is active. When no recipe or schematic is selected, neither the cycle time nor the progress indicator is shown.
|
- REQ-UI-PRODUCTION-PROGRESS: For buildings that produce items or ships (miner, smelter, assembler, reprocessing plant, shipyard), the selected building panel also shows: (a) the cycle time of the currently selected recipe or schematic in seconds, and (b) the completion percentage of the active production cycle as an integer (e.g. `42%`), or the text `idle` when no production cycle is active. When no recipe or schematic is selected, neither the cycle time nor the progress indicator is shown.
|
||||||
- REQ-UI-MULTI-SELECT: The player selects multiple buildings by box-drag or by Ctrl+clicking individual buildings to add or remove them from the selection.
|
- REQ-UI-MULTI-SELECT: The player selects multiple buildings by box-drag or by Ctrl+clicking individual buildings to add or remove them from the selection. A box-drag that covers at least one building selects buildings (any scrap piles within the box are ignored); a box-drag that covers no buildings but does cover scrap piles selects those scrap piles instead (REQ-UI-SCRAP-MULTI-SELECT).
|
||||||
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown. The panel additionally shows the **total building block cost** of the selection — the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button grid); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-BUTTON). Construction sites count at their building type's full placement cost regardless of construction progress.
|
- REQ-UI-MULTI-SELECTION: When multiple buildings are selected, the panel shows how many of each building type are selected. No per-building detail is shown. The panel additionally shows the **total building block cost** of the selection — the sum of each selected building's placement cost (`buildings.toml [[building]].cost`, per REQ-BLD-COST), counting only player-placeable buildings (buildings with a button in the build button grid); non-player-placeable buildings (the HQ and defence stations) are excluded from the total, consistent with the blueprint total (REQ-UI-BLUEPRINT-BUTTON). Construction sites count at their building type's full placement cost regardless of construction progress.
|
||||||
- REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel. Recipe selection (miner, assembler) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW).
|
- REQ-UI-CONFIG-INLINE: Recipe and schematic configuration for a selected building is shown within this panel. Recipe selection (miner, assembler) and schematic selection (shipyard) use the selection button and dialog (REQ-UI-SELECT-BUTTON) rather than an inline control. For shipyards, the panel additionally shows the ship layout preview and "Configure" button below the schematic selection button (REQ-MOD-UI-PREVIEW).
|
||||||
- REQ-UI-SELECT-BUTTON: **Recipe and schematic selection control.** Recipe selection (Miner ore type, Assembler recipe) and schematic selection (Shipyard) are each presented in the selected building panel as a single **selection button** whose caption is the name of the currently selected recipe or schematic, or a placeholder ("Select recipe" / "Select schematic") when none is selected. Clicking the button opens a modal **selection dialog** that pauses the game (speed set to 0×; on close, the speed is restored to what it was before the dialog was opened). The dialog contains a grid of option buttons, one per selectable option — only options that are currently unlocked are shown (REQ-LOCK-UI-RECIPE for recipes, REQ-LOCK-UI-SCHEMATIC for schematics). Hovering an option button shows the selection info tooltip (REQ-UI-SELECT-TOOLTIP). Clicking an option button selects that recipe/schematic, closes the dialog, and updates the selection button's caption in the selected building panel. The dialog can be dismissed without changing the current selection (e.g. closing it without clicking an option). Selecting a new recipe or schematic has the same effects as before (REQ-MAT-INPUT-BUFFER, REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
|
- REQ-UI-SELECT-BUTTON: **Recipe and schematic selection control.** Recipe selection (Miner ore type, Assembler recipe) and schematic selection (Shipyard) are each presented in the selected building panel as a single **selection button** whose caption is the name of the currently selected recipe or schematic, or a placeholder ("Select recipe" / "Select schematic") when none is selected. Clicking the button opens a modal **selection dialog** that pauses the game (speed set to 0×; on close, the speed is restored to what it was before the dialog was opened). The dialog contains a grid of option buttons, one per selectable option — only options that are currently unlocked are shown (REQ-LOCK-UI-RECIPE for recipes, REQ-LOCK-UI-SCHEMATIC for schematics). Hovering an option button shows the selection info tooltip (REQ-UI-SELECT-TOOLTIP). Clicking an option button selects that recipe/schematic, closes the dialog, and updates the selection button's caption in the selected building panel. The dialog can be dismissed without changing the current selection (e.g. closing it without clicking an option). Selecting a new recipe or schematic has the same effects as before (REQ-MAT-INPUT-BUFFER, REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
|
||||||
@@ -468,9 +470,12 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
- For a **recipe** (Miner or Assembler): the recipe name; the name and quantity of each input item (no inputs are listed for miner recipes, which consume nothing); the completion time (`duration_seconds`); and the name and quantity of the produced output item.
|
- For a **recipe** (Miner or Assembler): the recipe name; the name and quantity of each input item (no inputs are listed for miner recipes, which consume nothing); the completion time (`duration_seconds`); and the name and quantity of the produced output item.
|
||||||
- For a **ship schematic** (Shipyard): the ship's `display_name`; the name and quantity of each base required material (`[ship.schematic].materials`, excluding any module contributions); the base production time (`[ship.schematic].production_time_seconds`); and "Produces: 1 <ship display name>".
|
- For a **ship schematic** (Shipyard): the ship's `display_name`; the name and quantity of each base required material (`[ship.schematic].materials`, excluding any module contributions); the base production time (`[ship.schematic].production_time_seconds`); and "Produces: 1 <ship display name>".
|
||||||
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels.
|
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels.
|
||||||
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. Clicking a ship or defence station clears any existing selection and establishes a single-entity selection containing only that entity. Ships and defence stations cannot participate in multi-select together with buildings. Clicking empty world space (no building, ship, or defence station) clears the selection.
|
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. Clicking a ship or defence station clears any existing selection and establishes a single-entity selection containing only that entity. Ships and defence stations cannot participate in multi-select together with buildings. Clicking a scrap pile instead establishes a scrap selection (REQ-UI-SCRAP-CLICK-SELECT). Clicking empty world space (no building, ship, defence station, or scrap pile) clears the selection.
|
||||||
- REQ-UI-SHIP-STATS-PANEL: When a single ship is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **ship stats panel**. The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
|
- REQ-UI-SHIP-STATS-PANEL: When a single ship is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **ship stats panel**. The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
|
||||||
- REQ-UI-STATION-STATS-PANEL: When a single defence station is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **station stats panel** displaying the station's stats computed at its current level: HP (current / maximum), damage, range, and fire rate.
|
- REQ-UI-STATION-STATS-PANEL: When a single defence station is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **station stats panel** displaying the station's stats computed at its current level: HP (current / maximum), damage, range, and fire rate.
|
||||||
|
- REQ-UI-SCRAP-CLICK-SELECT: The player can click any scrap pile (REQ-RES-SCRAP-DROP) in the game world to select it. Scrap forms its own selection category: clicking a scrap pile clears any existing selection and establishes a scrap selection containing only that pile. Scrap piles cannot participate in a selection together with buildings, ships, or defence stations. Hit-testing prioritizes actors over scrap: when a building, ship, or defence station lies under the cursor at the same point as a scrap pile, that object is selected in preference to the scrap; a scrap pile is selected only when no building, ship, or defence station is under the cursor. A selected scrap pile that despawns or is fully collected (REQ-RES-SCRAP-DROP) is removed from the selection; once the last selected pile is gone, the selection becomes empty (REQ-UI-EMPTY-SELECTION).
|
||||||
|
- REQ-UI-SCRAP-MULTI-SELECT: Multiple scrap piles can be selected by box-drag or by Ctrl+clicking individual piles to add or remove them from the selection, mirroring building multi-select (REQ-UI-MULTI-SELECT). A scrap selection contains only scrap piles. Because scrap cannot mix with other object types (REQ-UI-SCRAP-CLICK-SELECT), Ctrl+clicking a scrap pile while a building, ship, or defence station selection is active first clears that selection and begins a scrap selection; conversely, selecting a building, ship, or defence station while a scrap selection is active clears the scrap selection. Box-drag disambiguation between buildings and scrap follows REQ-UI-MULTI-SELECT (a box covering any building selects buildings; a box covering scrap but no buildings selects the scrap).
|
||||||
|
- REQ-UI-SCRAP-PANEL: When one or more scrap piles are selected, the selected building panel shows the **total remaining scrap amount** across all selected piles — the sum of the piles' current remaining amounts (REQ-RES-SCRAP-DROP), e.g. "Scrap: 47". The same summed-amount display is used whether one pile or many are selected; no per-pile detail and no pile count are shown. The displayed total updates as selected piles are partially collected or despawn (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
|
||||||
### Build Button Grid
|
### Build Button Grid
|
||||||
|
|
||||||
@@ -484,11 +489,11 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
|
|
||||||
- REQ-UI-BLUEPRINT-PANEL: The blueprint panel is shown to the right of the build button grid. It contains, from top to bottom: a "Create Blueprint" button, and a list of blueprint entries (one per saved blueprint, in creation order). The panel has no Save or Load buttons; blueprints are persisted automatically (REQ-UI-BLUEPRINT-SAVE) and restored at startup (REQ-UI-BLUEPRINT-LOAD).
|
- REQ-UI-BLUEPRINT-PANEL: The blueprint panel is shown to the right of the build button grid. It contains, from top to bottom: a "Create Blueprint" button, and a list of blueprint entries (one per saved blueprint, in creation order). The panel has no Save or Load buttons; blueprints are persisted automatically (REQ-UI-BLUEPRINT-SAVE) and restored at startup (REQ-UI-BLUEPRINT-LOAD).
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button grid) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list.
|
- REQ-UI-BLUEPRINT-CREATE: The "Create Blueprint" button is enabled only when at least one player-placeable building (i.e. a building with a button in the build button grid) is currently selected; non-player-placeable buildings (HQ, defence stations) in the selection do not count toward this condition. A selected player-placeable building may be either an operational building or a construction site (a building placed but not yet fully built, REQ-BLD-SITE-CONFIG); both count toward this condition and are captured identically (REQ-UI-BLUEPRINT-STORAGE). When clicked, a modal dialog appears prompting the player to enter a name. The dialog has Confirm and Cancel buttons. Clicking Cancel closes the dialog with no effect. Clicking Confirm with a non-empty name creates a blueprint from the current selection, silently excluding any non-player-placeable buildings, and appends its button to the blueprint list.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-TEMP: Pressing the **T** key (REQ-UI-HOTKEYS) creates a **temporary blueprint** from the current selection and immediately enters blueprint placement mode for it, without opening the naming dialog. It has effect only when at least one player-placeable building is currently selected — the same condition as REQ-UI-BLUEPRINT-CREATE; pressing T with an empty selection, or a selection containing only non-player-placeable buildings (HQ, defence stations), does nothing. Entering this mode replaces any currently active build, blueprint placement, or demolish mode. The temporary blueprint is captured exactly as a saved blueprint (REQ-UI-BLUEPRINT-STORAGE), silently excluding any non-player-placeable buildings from the selection, but it is never named, never shown in the blueprint panel (REQ-UI-BLUEPRINT-PANEL), and never persisted to `blueprints.toml` (REQ-UI-BLUEPRINT-SAVE). Placement behaves identically to a saved blueprint's placement mode (REQ-UI-BLUEPRINT-MODE, REQ-UI-BLUEPRINT-PLACE): a ghost is rendered per building, R / Shift+R rotate the entire constellation, placement follows the same per-building validity and total-cost rules, and after a successful placement the mode stays active so the blueprint can be placed again. Right-clicking in the game world exits placement mode, at which point the temporary blueprint is discarded.
|
- REQ-UI-BLUEPRINT-TEMP: Pressing the **T** key (REQ-UI-HOTKEYS) creates a **temporary blueprint** from the current selection and immediately enters blueprint placement mode for it, without opening the naming dialog. It has effect only when at least one player-placeable building is currently selected — the same condition as REQ-UI-BLUEPRINT-CREATE; pressing T with an empty selection, or a selection containing only non-player-placeable buildings (HQ, defence stations), does nothing. Entering this mode replaces any currently active build, blueprint placement, or demolish mode. The temporary blueprint is captured exactly as a saved blueprint (REQ-UI-BLUEPRINT-STORAGE), silently excluding any non-player-placeable buildings from the selection, but it is never named, never shown in the blueprint panel (REQ-UI-BLUEPRINT-PANEL), and never persisted to `blueprints.toml` (REQ-UI-BLUEPRINT-SAVE). Placement behaves identically to a saved blueprint's placement mode (REQ-UI-BLUEPRINT-MODE, REQ-UI-BLUEPRINT-PLACE): a ghost is rendered per building, R / Shift+R rotate the entire constellation, placement follows the same per-building validity and total-cost rules, and after a successful placement the mode stays active so the blueprint can be placed again. Right-clicking in the game world exits placement mode, at which point the temporary blueprint is discarded.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-STORAGE: A blueprint stores its name and, for each building in the selection, the building type, its rotation, its tile offset (integer dx, dy) from the center of the bounding box of all selected buildings' footprints, and — where applicable — the selected recipe ID (miners and assemblers) or schematic ID (shipyards), and for splitters the two output filters (each a list of item types; an empty list means accept-all), at the time of capture. If no recipe or schematic was selected at capture time, none is stored; for a splitter with no filters set, no filter lists are stored. This structure maps directly to a TOML representation (e.g. one `[[building]]` array entry per constituent building, with the splitter filters as `filter_a`/`filter_b` arrays of item-type ids).
|
- REQ-UI-BLUEPRINT-STORAGE: A blueprint stores its name and, for each building in the selection, the building type, its rotation, its tile offset (integer dx, dy) from the center of the bounding box of all selected buildings' footprints, and — where applicable — the selected recipe ID (miners and assemblers) or schematic ID (shipyards), and for splitters the two output filters (each a list of item types; an empty list means accept-all), at the time of capture. A source building may be either an operational building or a construction site (REQ-BLD-SITE-CONFIG); a construction site is captured identically, storing whatever configuration it currently holds and never any buffer or construction-progress state. If no recipe or schematic was selected at capture time, none is stored; for a splitter with no filters set, no filter lists are stored. This structure maps directly to a TOML representation (e.g. one `[[building]]` array entry per constituent building, with the splitter filters as `filter_a`/`filter_b` arrays of item-type ids).
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-BUTTON: Each blueprint entry consists of a blueprint button and a dedicated delete icon ("×") placed to the right of the button. The blueprint button displays the blueprint name and, below it, the total building block cost of the blueprint (sum of the individual costs of all constituent buildings). A blueprint button is disabled when the player cannot afford the total cost. Clicking an enabled blueprint button enters blueprint placement mode for that blueprint. The delete icon is always enabled regardless of whether the player can afford the blueprint.
|
- REQ-UI-BLUEPRINT-BUTTON: Each blueprint entry consists of a blueprint button and a dedicated delete icon ("×") placed to the right of the button. The blueprint button displays the blueprint name and, below it, the total building block cost of the blueprint (sum of the individual costs of all constituent buildings). A blueprint button is disabled when the player cannot afford the total cost. Clicking an enabled blueprint button enters blueprint placement mode for that blueprint. The delete icon is always enabled regardless of whether the player can afford the blueprint.
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,9 @@ std::vector<ScrapInfo> ScrapSystem::allScrapInfo() const
|
|||||||
{
|
{
|
||||||
std::vector<ScrapInfo> result;
|
std::vector<ScrapInfo> result;
|
||||||
m_admin.forEach<ScrapDataComponent>(
|
m_admin.forEach<ScrapDataComponent>(
|
||||||
[&result, this](entt::entity e, const ScrapDataComponent& /*sd*/)
|
[&result, this](entt::entity e, const ScrapDataComponent& sd)
|
||||||
{
|
{
|
||||||
result.push_back(ScrapInfo{e, m_admin.get<PositionComponent>(e).value});
|
result.push_back(ScrapInfo{e, m_admin.get<PositionComponent>(e).value, sd.amount});
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct ScrapInfo
|
|||||||
{
|
{
|
||||||
entt::entity entity;
|
entt::entity entity;
|
||||||
QVector2D position;
|
QVector2D position;
|
||||||
|
int amount;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ScrapSystem
|
class ScrapSystem
|
||||||
|
|||||||
18
src/lib/eventsystem/event/ScrapSelectionChangedEvent.h
Normal file
18
src/lib/eventsystem/event/ScrapSelectionChangedEvent.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
// The set of currently selected scrap piles (REQ-UI-SCRAP-CLICK-SELECT,
|
||||||
|
// REQ-UI-SCRAP-MULTI-SELECT). An empty list means no scrap is selected. Scrap forms
|
||||||
|
// its own selection category, mutually exclusive with buildings and entities.
|
||||||
|
class ScrapSelectionChangedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ScrapSelectionChangedEvent(std::vector<entt::entity> scrap)
|
||||||
|
: scrap(std::move(scrap)) {}
|
||||||
|
const std::vector<entt::entity> scrap;
|
||||||
|
};
|
||||||
@@ -1,10 +1,55 @@
|
|||||||
#include "BuildingConfig.h"
|
#include "BuildingConfig.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <climits>
|
||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// The blueprint-relevant geometry shared by operational buildings and construction
|
||||||
|
// sites. Resolved from whichever of the two a selected id refers to.
|
||||||
|
struct SelectedBuilding
|
||||||
|
{
|
||||||
|
BuildingId id;
|
||||||
|
BuildingType type;
|
||||||
|
Rotation rotation;
|
||||||
|
QPoint anchor;
|
||||||
|
const std::vector<QPoint>* bodyCells;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolves a selected id to a player-placeable building or construction site, if it
|
||||||
|
// is one. Returns std::nullopt for an unknown id or a non-player-placeable building
|
||||||
|
// (the HQ and defence stations, per REQ-UI-BLUEPRINT-CREATE).
|
||||||
|
std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, BuildingId id)
|
||||||
|
{
|
||||||
|
const Building* building = sim.buildings().findBuilding(id);
|
||||||
|
const ConstructionSite* site = building ? nullptr : sim.buildings().findSite(id);
|
||||||
|
if (!building && !site)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BuildingType type = building ? building->type : site->type;
|
||||||
|
const BuildingDef* def = sim.config().buildings.findBuildingDef(type);
|
||||||
|
if (!def || !def->playerPlaceable)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedBuilding resolved;
|
||||||
|
resolved.id = id;
|
||||||
|
resolved.type = type;
|
||||||
|
resolved.rotation = building ? building->rotation : site->rotation;
|
||||||
|
resolved.anchor = building ? building->anchor : site->anchor;
|
||||||
|
resolved.bodyCells = building ? &building->bodyCells : &site->bodyCells;
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
|
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
|
||||||
{
|
{
|
||||||
const Building* building = sim.buildings().findBuilding(id);
|
const Building* building = sim.buildings().findBuilding(id);
|
||||||
@@ -49,3 +94,74 @@ std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, Building
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Blueprint captureBlueprintFromSelection(const Simulation& sim,
|
||||||
|
const std::vector<BuildingId>& selectedIds)
|
||||||
|
{
|
||||||
|
std::vector<SelectedBuilding> entries;
|
||||||
|
entries.reserve(selectedIds.size());
|
||||||
|
for (const BuildingId id : selectedIds)
|
||||||
|
{
|
||||||
|
const std::optional<SelectedBuilding> resolved = resolvePlaceable(sim, id);
|
||||||
|
if (resolved.has_value())
|
||||||
|
{
|
||||||
|
entries.push_back(*resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.empty())
|
||||||
|
{
|
||||||
|
return Blueprint{};
|
||||||
|
}
|
||||||
|
|
||||||
|
int minX = INT_MAX, maxX = INT_MIN;
|
||||||
|
int minY = INT_MAX, maxY = INT_MIN;
|
||||||
|
for (const SelectedBuilding& e : entries)
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : *e.bodyCells)
|
||||||
|
{
|
||||||
|
minX = std::min(minX, cell.x());
|
||||||
|
maxX = std::max(maxX, cell.x());
|
||||||
|
minY = std::min(minY, cell.y());
|
||||||
|
maxY = std::max(maxY, cell.y());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPoint center((minX + maxX) / 2, (minY + maxY) / 2);
|
||||||
|
|
||||||
|
Blueprint blueprint;
|
||||||
|
blueprint.buildings.reserve(entries.size());
|
||||||
|
for (const SelectedBuilding& e : entries)
|
||||||
|
{
|
||||||
|
BlueprintBuilding building;
|
||||||
|
building.type = e.type;
|
||||||
|
building.rotation = e.rotation;
|
||||||
|
building.offset = e.anchor - center;
|
||||||
|
// Recipe / schematic / layout / splitter-filter capture is shared with the
|
||||||
|
// copy-settings gesture (REQ-BLD-COPY-CONFIG) via readBuildingConfig, which
|
||||||
|
// handles operational buildings and construction sites alike.
|
||||||
|
const std::optional<BuildingConfig> config = readBuildingConfig(sim, e.id);
|
||||||
|
if (config.has_value())
|
||||||
|
{
|
||||||
|
building.recipeId = config->recipeId.value_or(std::string());
|
||||||
|
building.shipLayout = config->shipLayout;
|
||||||
|
building.splitterFilterA = config->splitterFilterA;
|
||||||
|
building.splitterFilterB = config->splitterFilterB;
|
||||||
|
}
|
||||||
|
blueprint.buildings.push_back(building);
|
||||||
|
}
|
||||||
|
return blueprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool selectionHasPlaceableBuilding(const Simulation& sim,
|
||||||
|
const std::vector<BuildingId>& selectedIds)
|
||||||
|
{
|
||||||
|
for (const BuildingId id : selectedIds)
|
||||||
|
{
|
||||||
|
if (resolvePlaceable(sim, id).has_value())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Blueprint.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "ItemType.h"
|
#include "ItemType.h"
|
||||||
@@ -38,3 +39,16 @@ struct BuildingConfig
|
|||||||
// by id, handling operational buildings and sites alike. Returns std::nullopt if
|
// by id, handling operational buildings and sites alike. Returns std::nullopt if
|
||||||
// no such building or site exists.
|
// no such building or site exists.
|
||||||
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id);
|
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id);
|
||||||
|
|
||||||
|
// Captures a blueprint from a selection of building / construction-site ids, keeping
|
||||||
|
// only player-placeable buildings and recording each one's type, rotation, offset
|
||||||
|
// from the selection's bounding-box center, and configuration. Operational buildings
|
||||||
|
// and construction sites are treated identically (REQ-UI-BLUEPRINT-CREATE,
|
||||||
|
// REQ-UI-BLUEPRINT-STORAGE). The returned blueprint is unnamed.
|
||||||
|
Blueprint captureBlueprintFromSelection(const Simulation& sim,
|
||||||
|
const std::vector<BuildingId>& selectedIds);
|
||||||
|
|
||||||
|
// True if any selected id refers to a player-placeable building or construction site
|
||||||
|
// (the enable condition for the Create Blueprint button, REQ-UI-BLUEPRINT-CREATE).
|
||||||
|
bool selectionHasPlaceableBuilding(const Simulation& sim,
|
||||||
|
const std::vector<BuildingId>& selectedIds);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
#include "EntityHitTest.h"
|
#include "EntityHitTest.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
|
#include "ScrapDataComponent.h"
|
||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
|
|
||||||
@@ -54,3 +56,48 @@ entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos)
|
|||||||
|
|
||||||
return bestShip;
|
return bestShip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entt::entity scrapAtWorldPos(EntityAdmin& admin, QVector2D worldPos)
|
||||||
|
{
|
||||||
|
// Slightly larger than the scrap's rendered radius (0.2 tiles) so small piles
|
||||||
|
// remain easy to click; tunable.
|
||||||
|
constexpr float kScrapHitRadiusSquared = 0.35f * 0.35f;
|
||||||
|
entt::entity bestScrap = entt::null;
|
||||||
|
float bestDistSquared = kScrapHitRadiusSquared;
|
||||||
|
|
||||||
|
admin.forEach<ScrapDataComponent, PositionComponent>(
|
||||||
|
[&](entt::entity entity, const ScrapDataComponent& /*sd*/, const PositionComponent& pos)
|
||||||
|
{
|
||||||
|
const float dx = pos.value.x() - worldPos.x();
|
||||||
|
const float dy = pos.value.y() - worldPos.y();
|
||||||
|
const float distSquared = dx * dx + dy * dy;
|
||||||
|
if (distSquared < bestDistSquared)
|
||||||
|
{
|
||||||
|
bestDistSquared = distSquared;
|
||||||
|
bestScrap = entity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return bestScrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<entt::entity> scrapInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB)
|
||||||
|
{
|
||||||
|
const int minX = std::min(tileA.x(), tileB.x());
|
||||||
|
const int maxX = std::max(tileA.x(), tileB.x());
|
||||||
|
const int minY = std::min(tileA.y(), tileB.y());
|
||||||
|
const int maxY = std::max(tileA.y(), tileB.y());
|
||||||
|
|
||||||
|
std::vector<entt::entity> result;
|
||||||
|
admin.forEach<ScrapDataComponent, PositionComponent>(
|
||||||
|
[&](entt::entity entity, const ScrapDataComponent& /*sd*/, const PositionComponent& pos)
|
||||||
|
{
|
||||||
|
const int tileX = static_cast<int>(std::floor(pos.value.x()));
|
||||||
|
const int tileY = static_cast<int>(std::floor(pos.value.y()));
|
||||||
|
if (tileX >= minX && tileX <= maxX && tileY >= minY && tileY <= maxY)
|
||||||
|
{
|
||||||
|
result.push_back(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
@@ -7,3 +10,12 @@
|
|||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
|
|
||||||
entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos);
|
entt::entity entityAtWorldPos(EntityAdmin& admin, QVector2D worldPos);
|
||||||
|
|
||||||
|
// Returns the nearest scrap pile whose center is within the scrap pick radius of
|
||||||
|
// worldPos, or entt::null if none (REQ-UI-SCRAP-CLICK-SELECT). Scrap is picked only
|
||||||
|
// after actors: entityAtWorldPos never returns scrap (scrap has no HealthComponent).
|
||||||
|
entt::entity scrapAtWorldPos(EntityAdmin& admin, QVector2D worldPos);
|
||||||
|
|
||||||
|
// Returns every scrap pile whose position falls within the inclusive tile rectangle
|
||||||
|
// spanned by tileA and tileB, in any corner order (REQ-UI-SCRAP-MULTI-SELECT).
|
||||||
|
std::vector<entt::entity> scrapInBox(EntityAdmin& admin, QPoint tileA, QPoint tileB);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include "Blueprint.h"
|
#include "Blueprint.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
|
#include "BuildingConfig.h"
|
||||||
#include "BuildingsConfig.h"
|
#include "BuildingsConfig.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
@@ -696,6 +697,92 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
|
|||||||
REQUIRE(b->recipeId == "mine_copper_ore");
|
REQUIRE(b->recipeId == "mine_copper_ore");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Blueprint capture from construction sites (REQ-UI-BLUEPRINT-CREATE,
|
||||||
|
// REQ-UI-BLUEPRINT-STORAGE): a still-under-construction building is a valid
|
||||||
|
// blueprint source, captured identically to an operational building.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig());
|
||||||
|
|
||||||
|
// Freshly placed → a ConstructionSite (not ticked to completion). A 1x1 belt keeps
|
||||||
|
// the body-cell bounding-box centered on the anchor, so a single site → zero offset.
|
||||||
|
const BuildingId id =
|
||||||
|
SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East);
|
||||||
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
|
REQUIRE(sim.buildings().findSite(id) != nullptr);
|
||||||
|
REQUIRE(sim.buildings().findBuilding(id) == nullptr);
|
||||||
|
|
||||||
|
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
|
||||||
|
|
||||||
|
REQUIRE(bp.buildings.size() == 1);
|
||||||
|
REQUIRE(bp.buildings[0].type == BuildingType::Belt);
|
||||||
|
REQUIRE(bp.buildings[0].offset == QPoint(0, 0)); // single 1x1 building → zero offset
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blueprint]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig());
|
||||||
|
|
||||||
|
const BuildingId id =
|
||||||
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
|
||||||
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
|
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
||||||
|
|
||||||
|
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
|
||||||
|
|
||||||
|
REQUIRE(bp.buildings.size() == 1);
|
||||||
|
REQUIRE(bp.buildings[0].recipeId == "mine_iron_ore");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Blueprint creation: mixed operational building and construction site are both captured",
|
||||||
|
"[blueprint]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig());
|
||||||
|
|
||||||
|
// Building A: place, configure, and tick to completion so it is operational.
|
||||||
|
const BuildingId idA =
|
||||||
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
|
||||||
|
REQUIRE(idA != kInvalidBuildingId);
|
||||||
|
SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore");
|
||||||
|
for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); }
|
||||||
|
REQUIRE(sim.buildings().findBuilding(idA) != nullptr);
|
||||||
|
|
||||||
|
// Building B: place and configure, but leave as a construction site.
|
||||||
|
const BuildingId idB =
|
||||||
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East);
|
||||||
|
REQUIRE(idB != kInvalidBuildingId);
|
||||||
|
SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore");
|
||||||
|
REQUIRE(sim.buildings().findSite(idB) != nullptr);
|
||||||
|
|
||||||
|
const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB });
|
||||||
|
|
||||||
|
REQUIRE(bp.buildings.size() == 2);
|
||||||
|
REQUIRE(bp.buildings[0].type == BuildingType::Miner);
|
||||||
|
REQUIRE(bp.buildings[1].type == BuildingType::Miner);
|
||||||
|
// Both the operational building's and the site's configs come through.
|
||||||
|
std::vector<std::string> recipes = { bp.buildings[0].recipeId, bp.buildings[1].recipeId };
|
||||||
|
std::sort(recipes.begin(), recipes.end());
|
||||||
|
REQUIRE(recipes == std::vector<std::string>{ "mine_copper_ore", "mine_iron_ore" });
|
||||||
|
// Distinct anchors → distinct offsets.
|
||||||
|
REQUIRE(bp.buildings[0].offset != bp.buildings[1].offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction site", "[blueprint]")
|
||||||
|
{
|
||||||
|
Simulation sim(loadConfig());
|
||||||
|
|
||||||
|
REQUIRE_FALSE(selectionHasPlaceableBuilding(sim, {}));
|
||||||
|
|
||||||
|
const BuildingId id =
|
||||||
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
|
||||||
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
|
REQUIRE(sim.buildings().findSite(id) != nullptr);
|
||||||
|
REQUIRE(selectionHasPlaceableBuilding(sim, { id }));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("Blueprint placement: interceptor schematic is unlocked at game start", "[blueprint]")
|
TEST_CASE("Blueprint placement: interceptor schematic is unlocked at game start", "[blueprint]")
|
||||||
{
|
{
|
||||||
// "interceptor" has unlock_at_station_level = -1 in the test config.
|
// "interceptor" has unlock_at_station_level = -1 in the test config.
|
||||||
|
|||||||
@@ -2,11 +2,22 @@
|
|||||||
|
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "DespawnAtComponent.h"
|
#include "DespawnAtComponent.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
|
#include "EntityHitTest.h"
|
||||||
#include "ScrapDataComponent.h"
|
#include "ScrapDataComponent.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool contains(const std::vector<entt::entity>& v, entt::entity e)
|
||||||
|
{
|
||||||
|
return std::find(v.begin(), v.end(), e) != v.end();
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Spawn
|
// Spawn
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -140,3 +151,78 @@ TEST_CASE("ScrapSystem: allScrapInfo returns all spawned scrap", "[scrap]")
|
|||||||
const std::vector<ScrapInfo> info = ss.allScrapInfo();
|
const std::vector<ScrapInfo> info = ss.allScrapInfo();
|
||||||
REQUIRE(info.size() == 2);
|
REQUIRE(info.size() == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("ScrapSystem: allScrapInfo reports each pile's remaining amount", "[scrap]")
|
||||||
|
{
|
||||||
|
EntityAdmin admin;
|
||||||
|
ScrapSystem ss(admin);
|
||||||
|
|
||||||
|
const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
|
||||||
|
const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
|
||||||
|
|
||||||
|
const std::vector<ScrapInfo> info = ss.allScrapInfo();
|
||||||
|
REQUIRE(info.size() == 2);
|
||||||
|
for (const ScrapInfo& i : info)
|
||||||
|
{
|
||||||
|
if (i.entity == a) { REQUIRE(i.amount == 3); }
|
||||||
|
else if (i.entity == b) { REQUIRE(i.amount == 6); }
|
||||||
|
else { FAIL("unexpected scrap entity"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Selection hit-testing (REQ-UI-SCRAP-CLICK-SELECT, REQ-UI-SCRAP-MULTI-SELECT)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("scrapAtWorldPos returns the pile near a point and null when far", "[scrap]")
|
||||||
|
{
|
||||||
|
EntityAdmin admin;
|
||||||
|
ScrapSystem ss(admin);
|
||||||
|
|
||||||
|
const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100);
|
||||||
|
|
||||||
|
// Extra parens keep Catch from decomposing the comparison, which is ambiguous
|
||||||
|
// between Catch's expression templates and entt's entity operator==.
|
||||||
|
REQUIRE((scrapAtWorldPos(admin, QVector2D(3.1f, 4.0f)) == e));
|
||||||
|
REQUIRE((scrapAtWorldPos(admin, QVector2D(10.0f, 10.0f)) == entt::null));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("scrapAtWorldPos returns the nearest of several piles", "[scrap]")
|
||||||
|
{
|
||||||
|
EntityAdmin admin;
|
||||||
|
ScrapSystem ss(admin);
|
||||||
|
|
||||||
|
const entt::entity near = ss.spawn(QVector2D(2.0f, 2.0f), 1, 100);
|
||||||
|
ss.spawn(QVector2D(2.4f, 2.0f), 1, 100);
|
||||||
|
|
||||||
|
REQUIRE((scrapAtWorldPos(admin, QVector2D(2.05f, 2.0f)) == near));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("entityAtWorldPos never returns a scrap pile", "[scrap]")
|
||||||
|
{
|
||||||
|
EntityAdmin admin;
|
||||||
|
ScrapSystem ss(admin);
|
||||||
|
|
||||||
|
ss.spawn(QVector2D(3.0f, 4.0f), 5, 100);
|
||||||
|
|
||||||
|
// Scrap has no HealthComponent, so the actor hit-test ignores it entirely.
|
||||||
|
REQUIRE((entityAtWorldPos(admin, QVector2D(3.0f, 4.0f)) == entt::null));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("scrapInBox returns exactly the piles inside the tile rectangle", "[scrap]")
|
||||||
|
{
|
||||||
|
EntityAdmin admin;
|
||||||
|
ScrapSystem ss(admin);
|
||||||
|
|
||||||
|
const entt::entity inA = ss.spawn(QVector2D(1.2f, 2.7f), 1, 100); // tile (1,2)
|
||||||
|
const entt::entity inB = ss.spawn(QVector2D(4.9f, 5.1f), 1, 100); // tile (4,5)
|
||||||
|
const entt::entity outX = ss.spawn(QVector2D(10.0f, 10.0f), 1, 100);
|
||||||
|
|
||||||
|
// Box given in reversed corner order to confirm normalization.
|
||||||
|
const std::vector<entt::entity> hit = scrapInBox(admin, QPoint(5, 5), QPoint(0, 0));
|
||||||
|
|
||||||
|
REQUIRE(hit.size() == 2);
|
||||||
|
REQUIRE(contains(hit, inA));
|
||||||
|
REQUIRE(contains(hit, inB));
|
||||||
|
REQUIRE_FALSE(contains(hit, outX));
|
||||||
|
}
|
||||||
|
|||||||
@@ -146,60 +146,10 @@ void BlueprintPanel::onBlueprintButtonClicked(int index)
|
|||||||
|
|
||||||
Blueprint BlueprintPanel::createBlueprintFromSelection() const
|
Blueprint BlueprintPanel::createBlueprintFromSelection() const
|
||||||
{
|
{
|
||||||
struct Entry
|
// Capture is shared, testable logic in lib/sim: it resolves each selected id as an
|
||||||
{
|
// operational building or a construction site alike (REQ-UI-BLUEPRINT-CREATE,
|
||||||
const Building* building;
|
// REQ-UI-BLUEPRINT-STORAGE).
|
||||||
};
|
return captureBlueprintFromSelection(*m_sim, m_selectedBuildingIds);
|
||||||
std::vector<Entry> entries;
|
|
||||||
entries.reserve(m_selectedBuildingIds.size());
|
|
||||||
|
|
||||||
for (const BuildingId id : m_selectedBuildingIds)
|
|
||||||
{
|
|
||||||
const Building* b = m_sim->buildings().findBuilding(id);
|
|
||||||
if (!b) { continue; }
|
|
||||||
const BuildingDef* def = m_config->buildings.findBuildingDef(b->type);
|
|
||||||
if (def && def->playerPlaceable) { entries.push_back({ b }); }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entries.empty()) { return Blueprint{}; }
|
|
||||||
|
|
||||||
int minX = INT_MAX, maxX = INT_MIN;
|
|
||||||
int minY = INT_MAX, maxY = INT_MIN;
|
|
||||||
for (const Entry& e : entries)
|
|
||||||
{
|
|
||||||
for (const QPoint& cell : e.building->bodyCells)
|
|
||||||
{
|
|
||||||
minX = std::min(minX, cell.x());
|
|
||||||
maxX = std::max(maxX, cell.x());
|
|
||||||
minY = std::min(minY, cell.y());
|
|
||||||
maxY = std::max(maxY, cell.y());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const QPoint center((minX + maxX) / 2, (minY + maxY) / 2);
|
|
||||||
|
|
||||||
Blueprint bp;
|
|
||||||
bp.buildings.reserve(entries.size());
|
|
||||||
for (const Entry& e : entries)
|
|
||||||
{
|
|
||||||
BlueprintBuilding bb;
|
|
||||||
bb.type = e.building->type;
|
|
||||||
bb.rotation = e.building->rotation;
|
|
||||||
bb.offset = e.building->anchor - center;
|
|
||||||
// Recipe / schematic / layout / splitter-filter capture is shared with the
|
|
||||||
// copy-settings gesture (REQ-BLD-COPY-CONFIG) via readBuildingConfig.
|
|
||||||
const std::optional<BuildingConfig> config =
|
|
||||||
readBuildingConfig(*m_sim, e.building->id);
|
|
||||||
if (config.has_value())
|
|
||||||
{
|
|
||||||
bb.recipeId = config->recipeId.value_or(std::string());
|
|
||||||
bb.shipLayout = config->shipLayout;
|
|
||||||
bb.splitterFilterA = config->splitterFilterA;
|
|
||||||
bb.splitterFilterB = config->splitterFilterB;
|
|
||||||
}
|
|
||||||
bp.buildings.push_back(bb);
|
|
||||||
}
|
|
||||||
return bp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int BlueprintPanel::computeBlueprintCost(const Blueprint& bp) const
|
int BlueprintPanel::computeBlueprintCost(const Blueprint& bp) const
|
||||||
@@ -295,19 +245,8 @@ void BlueprintPanel::loadFromDisk()
|
|||||||
|
|
||||||
void BlueprintPanel::refreshButtonStates()
|
void BlueprintPanel::refreshButtonStates()
|
||||||
{
|
{
|
||||||
const bool anyPlaceable = [&]() {
|
// A construction site counts the same as an operational building (REQ-UI-BLUEPRINT-CREATE).
|
||||||
for (const BuildingId id : m_selectedBuildingIds)
|
m_createBtn->setEnabled(selectionHasPlaceableBuilding(*m_sim, m_selectedBuildingIds));
|
||||||
{
|
|
||||||
const Building* b = m_sim->buildings().findBuilding(id);
|
|
||||||
if (!b) { continue; }
|
|
||||||
for (const BuildingDef& def : m_config->buildings.buildings)
|
|
||||||
{
|
|
||||||
if (def.type == b->type) { return def.playerPlaceable; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}();
|
|
||||||
m_createBtn->setEnabled(anyPlaceable);
|
|
||||||
|
|
||||||
for (int i = 0; i < static_cast<int>(m_blueprintButtons.size()); ++i)
|
for (int i = 0; i < static_cast<int>(m_blueprintButtons.size()); ++i)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "RepairBehavior.h"
|
#include "RepairBehavior.h"
|
||||||
#include "SalvageScrapBehavior.h"
|
#include "SalvageScrapBehavior.h"
|
||||||
|
#include "ScrapSelectionChangedEvent.h"
|
||||||
#include "ScrapSystem.h"
|
#include "ScrapSystem.h"
|
||||||
#include "SelectionChangedEvent.h"
|
#include "SelectionChangedEvent.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
@@ -271,6 +272,10 @@ void GameWorldView::onFrame()
|
|||||||
m_activeBeams = std::move(live);
|
m_activeBeams = std::move(live);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop selected scrap piles that were collected or despawned this frame, so the
|
||||||
|
// panel stops counting them and the selection empties out (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
pruneDespawnedScrap();
|
||||||
|
|
||||||
// Expire copy/paste flashes. Lifetime is wall-clock (the frame delta), so the
|
// Expire copy/paste flashes. Lifetime is wall-clock (the frame delta), so the
|
||||||
// flash plays for a fixed real duration regardless of game speed, including
|
// flash plays for a fixed real duration regardless of game speed, including
|
||||||
// while the game is paused (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
// while the game is paused (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||||
@@ -666,6 +671,36 @@ std::optional<QVector2D> GameWorldView::entityPosition(entt::entity entity) cons
|
|||||||
return m_sim->admin().get<PositionComponent>(entity).value;
|
return m_sim->admin().get<PositionComponent>(entity).value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameWorldView::clearScrapSelection()
|
||||||
|
{
|
||||||
|
if (m_selectedScrap.empty()) { return; }
|
||||||
|
m_selectedScrap.clear();
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ScrapSelectionChangedEvent>(m_selectedScrap));
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameWorldView::pruneDespawnedScrap()
|
||||||
|
{
|
||||||
|
if (m_selectedScrap.empty()) { return; }
|
||||||
|
|
||||||
|
std::vector<entt::entity> live;
|
||||||
|
for (const ScrapInfo& info : m_sim->scraps().allScrapInfo())
|
||||||
|
{
|
||||||
|
if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), info.entity)
|
||||||
|
!= m_selectedScrap.end())
|
||||||
|
{
|
||||||
|
live.push_back(info.entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (live.size() != m_selectedScrap.size())
|
||||||
|
{
|
||||||
|
m_selectedScrap = std::move(live);
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ScrapSelectionChangedEvent>(m_selectedScrap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameWorldView::stepSpeed(int delta)
|
void GameWorldView::stepSpeed(int delta)
|
||||||
{
|
{
|
||||||
const double kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
const double kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
||||||
@@ -1048,6 +1083,19 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter)
|
|||||||
// Outline sits 1px outside the footprint (into adjacent tiles).
|
// Outline sits 1px outside the footprint (into adjacent tiles).
|
||||||
painter.drawRect(rect->adjusted(-1, -1, 1, 1));
|
painter.drawRect(rect->adjusted(-1, -1, 1, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A ring around each selected scrap pile, sitting just outside the pile's
|
||||||
|
// rendered circle (radius tilePx()*0.2, matching drawScrap) (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
if (!m_selectedScrap.empty())
|
||||||
|
{
|
||||||
|
const qreal outlineRadius = static_cast<qreal>(tilePx() * 0.2f) + 3.0;
|
||||||
|
for (const ScrapInfo& scrap : m_sim->scraps().allScrapInfo())
|
||||||
|
{
|
||||||
|
if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), scrap.entity)
|
||||||
|
== m_selectedScrap.end()) { continue; }
|
||||||
|
painter.drawEllipse(worldToWidget(scrap.position), outlineRadius, outlineRadius);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
|
void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
|
||||||
@@ -1752,6 +1800,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
|
|
||||||
if (hitEntity != entt::null)
|
if (hitEntity != entt::null)
|
||||||
{
|
{
|
||||||
|
// Actors (ship/station) win over scrap and buildings (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
clearScrapSelection();
|
||||||
m_selectedBuildingIds.clear();
|
m_selectedBuildingIds.clear();
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
@@ -1775,6 +1825,8 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
}
|
}
|
||||||
if (id != kInvalidBuildingId)
|
if (id != kInvalidBuildingId)
|
||||||
{
|
{
|
||||||
|
// A building/construction site outranks scrap (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
clearScrapSelection();
|
||||||
if (event->modifiers() & Qt::ControlModifier)
|
if (event->modifiers() & Qt::ControlModifier)
|
||||||
{
|
{
|
||||||
bool found = false;
|
bool found = false;
|
||||||
@@ -1794,6 +1846,36 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
}
|
}
|
||||||
|
else if (const entt::entity scrapHit =
|
||||||
|
scrapAtWorldPos(m_sim->admin(), worldPos); scrapHit != entt::null)
|
||||||
|
{
|
||||||
|
// Scrap forms its own selection category; picking it clears any
|
||||||
|
// building selection (REQ-UI-SCRAP-CLICK-SELECT, REQ-UI-SCRAP-MULTI-SELECT).
|
||||||
|
if (!m_selectedBuildingIds.empty())
|
||||||
|
{
|
||||||
|
m_selectedBuildingIds.clear();
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
|
}
|
||||||
|
if (event->modifiers() & Qt::ControlModifier)
|
||||||
|
{
|
||||||
|
bool found = false;
|
||||||
|
std::vector<entt::entity> newSel;
|
||||||
|
for (entt::entity sel : m_selectedScrap)
|
||||||
|
{
|
||||||
|
if (sel == scrapHit) { found = true; }
|
||||||
|
else { newSel.push_back(sel); }
|
||||||
|
}
|
||||||
|
if (!found) { newSel.push_back(scrapHit); }
|
||||||
|
m_selectedScrap = newSel;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_selectedScrap = { scrapHit };
|
||||||
|
}
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ScrapSelectionChangedEvent>(m_selectedScrap));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!(event->modifiers() & Qt::ControlModifier))
|
if (!(event->modifiers() & Qt::ControlModifier))
|
||||||
@@ -1801,6 +1883,7 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
|||||||
m_selectedBuildingIds.clear();
|
m_selectedBuildingIds.clear();
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
|
clearScrapSelection();
|
||||||
}
|
}
|
||||||
m_boxSelecting = true;
|
m_boxSelecting = true;
|
||||||
m_boxStartTile = tile;
|
m_boxStartTile = tile;
|
||||||
@@ -1873,7 +1956,14 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(event->modifiers() & Qt::ControlModifier))
|
const bool ctrl = (event->modifiers() & Qt::ControlModifier) != 0;
|
||||||
|
|
||||||
|
if (!boxIds.empty())
|
||||||
|
{
|
||||||
|
// A box covering any building selects buildings; scrap in the box is
|
||||||
|
// ignored (REQ-UI-MULTI-SELECT, REQ-UI-SCRAP-MULTI-SELECT).
|
||||||
|
clearScrapSelection();
|
||||||
|
if (!ctrl)
|
||||||
{
|
{
|
||||||
m_selectedBuildingIds = boxIds;
|
m_selectedBuildingIds = boxIds;
|
||||||
}
|
}
|
||||||
@@ -1891,6 +1981,50 @@ void GameWorldView::mouseReleaseEvent(QMouseEvent* event)
|
|||||||
}
|
}
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No buildings in the box: a scrap-only box selects the scrap it covers
|
||||||
|
// (REQ-UI-SCRAP-MULTI-SELECT).
|
||||||
|
const std::vector<entt::entity> boxScrap =
|
||||||
|
scrapInBox(m_sim->admin(), m_boxStartTile, m_boxCurrentTile);
|
||||||
|
if (!boxScrap.empty())
|
||||||
|
{
|
||||||
|
if (!m_selectedBuildingIds.empty())
|
||||||
|
{
|
||||||
|
m_selectedBuildingIds.clear();
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
|
}
|
||||||
|
if (!ctrl)
|
||||||
|
{
|
||||||
|
m_selectedScrap = boxScrap;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (entt::entity e : boxScrap)
|
||||||
|
{
|
||||||
|
bool found = false;
|
||||||
|
for (entt::entity sel : m_selectedScrap)
|
||||||
|
{
|
||||||
|
if (sel == e) { found = true; break; }
|
||||||
|
}
|
||||||
|
if (!found) { m_selectedScrap.push_back(e); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<ScrapSelectionChangedEvent>(m_selectedScrap));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty box: a plain (non-additive) drag clears the current selection.
|
||||||
|
if (!ctrl)
|
||||||
|
{
|
||||||
|
m_selectedBuildingIds.clear();
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SelectionChangedEvent>(m_selectedBuildingIds));
|
||||||
|
clearScrapSelection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,13 @@ private:
|
|||||||
void placeBlueprintAtTile(QPoint center);
|
void placeBlueprintAtTile(QPoint center);
|
||||||
|
|
||||||
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
||||||
|
// Clears the scrap selection, emitting an empty ScrapSelectionChangedEvent when
|
||||||
|
// it was non-empty (REQ-UI-SCRAP-CLICK-SELECT). Used when another selection
|
||||||
|
// category takes over.
|
||||||
|
void clearScrapSelection();
|
||||||
|
// Drops despawned or fully-collected piles from the scrap selection and re-emits
|
||||||
|
// when it changed (REQ-UI-SCRAP-CLICK-SELECT). Called each frame from onFrame().
|
||||||
|
void pruneDespawnedScrap();
|
||||||
void stepSpeed(int delta);
|
void stepSpeed(int delta);
|
||||||
void placeAtTile(QPoint tile);
|
void placeAtTile(QPoint tile);
|
||||||
|
|
||||||
@@ -249,6 +256,7 @@ private:
|
|||||||
|
|
||||||
std::vector<BuildingId> m_selectedBuildingIds;
|
std::vector<BuildingId> m_selectedBuildingIds;
|
||||||
std::optional<entt::entity> m_selectedEntity;
|
std::optional<entt::entity> m_selectedEntity;
|
||||||
|
std::vector<entt::entity> m_selectedScrap;
|
||||||
bool m_boxSelecting;
|
bool m_boxSelecting;
|
||||||
QPoint m_boxStartTile;
|
QPoint m_boxStartTile;
|
||||||
QPoint m_boxCurrentTile;
|
QPoint m_boxCurrentTile;
|
||||||
|
|||||||
@@ -220,33 +220,13 @@ void MainWindow::handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> /*e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event)
|
void MainWindow::openShipLayoutDialog(BuildingId shipyardId,
|
||||||
|
const std::string& schematicId,
|
||||||
|
const ShipLayoutConfig& currentLayout)
|
||||||
{
|
{
|
||||||
const double prevSpeed = m_gameWorldView->gameSpeed();
|
const double prevSpeed = m_gameWorldView->gameSpeed();
|
||||||
m_gameWorldView->setGameSpeed(0.0);
|
m_gameWorldView->setGameSpeed(0.0);
|
||||||
|
|
||||||
// A construction site has no Building yet; fall back to its site record so
|
|
||||||
// the shipyard layout can be configured before it is built (REQ-BLD-SITE-CONFIG).
|
|
||||||
const Building* b = m_sim->buildings().findBuilding(event->shipyardId);
|
|
||||||
const ConstructionSite* s =
|
|
||||||
b ? nullptr : m_sim->buildings().findSite(event->shipyardId);
|
|
||||||
if (!b && !s)
|
|
||||||
{
|
|
||||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
|
||||||
m_gameWorldView->resetFrameTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& schematicId = b ? b->recipeId : s->recipeId;
|
|
||||||
const std::optional<ShipLayoutConfig>& layoutOpt =
|
|
||||||
b ? b->shipLayout : s->shipLayout;
|
|
||||||
|
|
||||||
ShipLayoutConfig currentLayout;
|
|
||||||
if (layoutOpt.has_value())
|
|
||||||
{
|
|
||||||
currentLayout = *layoutOpt;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<std::string> unlockedModuleIds;
|
std::set<std::string> unlockedModuleIds;
|
||||||
for (const ModuleDef& def : m_sim->config().modules.modules)
|
for (const ModuleDef& def : m_sim->config().modules.modules)
|
||||||
{
|
{
|
||||||
@@ -265,7 +245,7 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
|
|||||||
{
|
{
|
||||||
std::shared_ptr<SetShipLayoutCommand> command =
|
std::shared_ptr<SetShipLayoutCommand> command =
|
||||||
std::make_shared<SetShipLayoutCommand>();
|
std::make_shared<SetShipLayoutCommand>();
|
||||||
command->id = event->shipyardId;
|
command->id = shipyardId;
|
||||||
command->layout = *dialog.result();
|
command->layout = *dialog.result();
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<CommandRequestedEvent>(command));
|
std::make_shared<CommandRequestedEvent>(command));
|
||||||
@@ -275,6 +255,31 @@ void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> e
|
|||||||
m_gameWorldView->resetFrameTimer();
|
m_gameWorldView->resetFrameTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MainWindow::handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event)
|
||||||
|
{
|
||||||
|
// A construction site has no Building yet; fall back to its site record so
|
||||||
|
// the shipyard layout can be configured before it is built (REQ-BLD-SITE-CONFIG).
|
||||||
|
const Building* b = m_sim->buildings().findBuilding(event->shipyardId);
|
||||||
|
const ConstructionSite* s =
|
||||||
|
b ? nullptr : m_sim->buildings().findSite(event->shipyardId);
|
||||||
|
if (!b && !s)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& schematicId = b ? b->recipeId : s->recipeId;
|
||||||
|
const std::optional<ShipLayoutConfig>& layoutOpt =
|
||||||
|
b ? b->shipLayout : s->shipLayout;
|
||||||
|
|
||||||
|
ShipLayoutConfig currentLayout;
|
||||||
|
if (layoutOpt.has_value())
|
||||||
|
{
|
||||||
|
currentLayout = *layoutOpt;
|
||||||
|
}
|
||||||
|
|
||||||
|
openShipLayoutDialog(event->shipyardId, schematicId, currentLayout);
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event)
|
void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event)
|
||||||
{
|
{
|
||||||
const double prevSpeed = m_gameWorldView->gameSpeed();
|
const double prevSpeed = m_gameWorldView->gameSpeed();
|
||||||
@@ -293,12 +298,18 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BuildingType type = b ? b->type : s->type;
|
const BuildingType type = b ? b->type : s->type;
|
||||||
|
// Captured as a copy: a queued command may drain during the modal dialog's
|
||||||
|
// event loop and reallocate the building vectors, so b/s must not be
|
||||||
|
// dereferenced after dialog.exec() returns.
|
||||||
|
const std::string oldSchematic = b ? b->recipeId : s->recipeId;
|
||||||
const std::vector<RecipeSelectionOption> options =
|
const std::vector<RecipeSelectionOption> options =
|
||||||
buildRecipeSelectionOptions(type, *m_sim, m_sim->config());
|
buildRecipeSelectionOptions(type, *m_sim, m_sim->config());
|
||||||
const QString title = (type == BuildingType::Shipyard)
|
const QString title = (type == BuildingType::Shipyard)
|
||||||
? tr("Select Schematic")
|
? tr("Select Schematic")
|
||||||
: tr("Select Recipe");
|
: tr("Select Recipe");
|
||||||
|
|
||||||
|
bool autoOpenLayout = false;
|
||||||
|
std::string chosenSchematic;
|
||||||
RecipeSelectionDialog dialog(options, title, this);
|
RecipeSelectionDialog dialog(options, title, this);
|
||||||
if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
|
if (dialog.exec() == QDialog::Accepted && dialog.getChosenId().has_value())
|
||||||
{
|
{
|
||||||
@@ -307,10 +318,27 @@ void MainWindow::handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent
|
|||||||
command->recipeId = *dialog.getChosenId();
|
command->recipeId = *dialog.getChosenId();
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<CommandRequestedEvent>(command));
|
std::make_shared<CommandRequestedEvent>(command));
|
||||||
|
|
||||||
|
// REQ-MOD-UI-AUTO-DIALOG: picking a new schematic for a shipyard opens the
|
||||||
|
// layout configuration dialog immediately. Only on an actual change.
|
||||||
|
if (type == BuildingType::Shipyard && *dialog.getChosenId() != oldSchematic)
|
||||||
|
{
|
||||||
|
autoOpenLayout = true;
|
||||||
|
chosenSchematic = *dialog.getChosenId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_gameWorldView->setGameSpeed(prevSpeed);
|
m_gameWorldView->setGameSpeed(prevSpeed);
|
||||||
m_gameWorldView->resetFrameTimer();
|
m_gameWorldView->resetFrameTimer();
|
||||||
|
|
||||||
|
// The SetRecipeCommand above is queued (drains on a later frame) and clears
|
||||||
|
// the shipyard's layout, so open the dialog with the chosen schematic and an
|
||||||
|
// empty layout rather than reading the not-yet-updated building state. Speed
|
||||||
|
// is already restored so the helper snapshots the real speed to restore.
|
||||||
|
if (autoOpenLayout)
|
||||||
|
{
|
||||||
|
openShipLayoutDialog(event->buildingId, chosenSchematic, ShipLayoutConfig{});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
void MainWindow::handleEvent(std::shared_ptr<const GameOverEvent> /*event*/)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "WinEvent.h"
|
#include "WinEvent.h"
|
||||||
#include "RecipeSelectionRequestedEvent.h"
|
#include "RecipeSelectionRequestedEvent.h"
|
||||||
#include "SchematicChoicesAvailableEvent.h"
|
#include "SchematicChoicesAvailableEvent.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
#include "ShipLayoutBlueprint.h"
|
#include "ShipLayoutBlueprint.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
@@ -57,6 +58,12 @@ private:
|
|||||||
void handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const EscapeMenuRequestedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const LayoutDialogRequestedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) override;
|
||||||
|
|
||||||
|
// Opens the shipyard layout configuration dialog for the given schematic and
|
||||||
|
// current layout, applying the result via SetShipLayoutCommand (REQ-MOD-UI-DIALOG).
|
||||||
|
void openShipLayoutDialog(BuildingId shipyardId,
|
||||||
|
const std::string& schematicId,
|
||||||
|
const ShipLayoutConfig& currentLayout);
|
||||||
void layoutPanels();
|
void layoutPanels();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
#include "RecipeSelectionDialog.h"
|
#include "RecipeSelectionDialog.h"
|
||||||
#include "RecipeSelectionRequestedEvent.h"
|
#include "RecipeSelectionRequestedEvent.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "ScrapSystem.h"
|
||||||
#include "ShipLayoutPreview.h"
|
#include "ShipLayoutPreview.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "WeaponComponent.h"
|
#include "WeaponComponent.h"
|
||||||
@@ -190,6 +191,10 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
|
|||||||
m_layout->addWidget(m_stationStatsLabel);
|
m_layout->addWidget(m_stationStatsLabel);
|
||||||
m_stationStatsLabel->hide();
|
m_stationStatsLabel->hide();
|
||||||
|
|
||||||
|
m_scrapLabel = new QLabel(this);
|
||||||
|
m_layout->addWidget(m_scrapLabel);
|
||||||
|
m_scrapLabel->hide();
|
||||||
|
|
||||||
buildEmpty();
|
buildEmpty();
|
||||||
|
|
||||||
registerForEvents();
|
registerForEvents();
|
||||||
@@ -206,6 +211,9 @@ void SelectedBuildingPanel::onSelectionChanged(const std::vector<BuildingId>& id
|
|||||||
if (!ids.empty())
|
if (!ids.empty())
|
||||||
{
|
{
|
||||||
clearEntityDisplay();
|
clearEntityDisplay();
|
||||||
|
// A building selection supersedes any scrap selection (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
m_selectedScrap.clear();
|
||||||
|
m_scrapLabel->hide();
|
||||||
}
|
}
|
||||||
rebuild();
|
rebuild();
|
||||||
}
|
}
|
||||||
@@ -238,6 +246,7 @@ void SelectedBuildingPanel::hideAllWidgets()
|
|||||||
m_filterBLabel->hide();
|
m_filterBLabel->hide();
|
||||||
m_filterBList->hide();
|
m_filterBList->hide();
|
||||||
m_buffersLabel->hide();
|
m_buffersLabel->hide();
|
||||||
|
m_scrapLabel->hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectedBuildingPanel::clearContent()
|
void SelectedBuildingPanel::clearContent()
|
||||||
@@ -638,6 +647,13 @@ void SelectedBuildingPanel::handleEvent(
|
|||||||
|
|
||||||
void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
|
void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
|
||||||
{
|
{
|
||||||
|
if (!m_selectedScrap.empty())
|
||||||
|
{
|
||||||
|
// The total shrinks live as piles are collected or despawn (REQ-UI-SCRAP-PANEL).
|
||||||
|
refreshScrapTotal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_selectedEntity.has_value())
|
if (m_selectedEntity.has_value())
|
||||||
{
|
{
|
||||||
refreshEntityStats();
|
refreshEntityStats();
|
||||||
@@ -885,6 +901,9 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectedEven
|
|||||||
{
|
{
|
||||||
m_selectedEntity = event->entity;
|
m_selectedEntity = event->entity;
|
||||||
m_selectedBuildingIds.clear();
|
m_selectedBuildingIds.clear();
|
||||||
|
// An entity selection supersedes any scrap selection (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
m_selectedScrap.clear();
|
||||||
|
m_scrapLabel->hide();
|
||||||
clearContent();
|
clearContent();
|
||||||
|
|
||||||
EntityAdmin& admin = m_sim->admin();
|
EntityAdmin& admin = m_sim->admin();
|
||||||
@@ -1022,6 +1041,54 @@ void SelectedBuildingPanel::handleEvent(std::shared_ptr<const SelectionChangedEv
|
|||||||
onSelectionChanged(event->ids);
|
onSelectionChanged(event->ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::handleEvent(
|
||||||
|
std::shared_ptr<const ScrapSelectionChangedEvent> event)
|
||||||
|
{
|
||||||
|
m_selectedScrap = event->scrap;
|
||||||
|
if (!m_selectedScrap.empty())
|
||||||
|
{
|
||||||
|
// Scrap is its own selection category, mutually exclusive with buildings and
|
||||||
|
// entities (REQ-UI-SCRAP-CLICK-SELECT).
|
||||||
|
m_selectedBuildingIds.clear();
|
||||||
|
clearContent();
|
||||||
|
clearEntityDisplay();
|
||||||
|
buildScrap();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_scrapLabel->hide();
|
||||||
|
if (m_selectedBuildingIds.empty() && !m_selectedEntity.has_value())
|
||||||
|
{
|
||||||
|
buildEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::buildScrap()
|
||||||
|
{
|
||||||
|
clearContent();
|
||||||
|
m_entityTitleLabel->hide();
|
||||||
|
m_entityStatsPanel->hide();
|
||||||
|
m_stationStatsLabel->hide();
|
||||||
|
refreshScrapTotal();
|
||||||
|
m_scrapLabel->show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SelectedBuildingPanel::refreshScrapTotal()
|
||||||
|
{
|
||||||
|
// Sum the remaining amounts of the still-living selected piles (REQ-UI-SCRAP-PANEL).
|
||||||
|
int total = 0;
|
||||||
|
for (const ScrapInfo& info : m_sim->scraps().allScrapInfo())
|
||||||
|
{
|
||||||
|
if (std::find(m_selectedScrap.begin(), m_selectedScrap.end(), info.entity)
|
||||||
|
!= m_selectedScrap.end())
|
||||||
|
{
|
||||||
|
total += info.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_scrapLabel->setText(tr("Scrap: %1").arg(total));
|
||||||
|
}
|
||||||
|
|
||||||
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event)
|
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event)
|
||||||
{
|
{
|
||||||
m_debugDraw = event->active;
|
m_debugDraw = event->active;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "PlayerCommandsAppliedEvent.h"
|
#include "PlayerCommandsAppliedEvent.h"
|
||||||
#include "RecipesConfig.h"
|
#include "RecipesConfig.h"
|
||||||
|
#include "ScrapSelectionChangedEvent.h"
|
||||||
#include "SelectionChangedEvent.h"
|
#include "SelectionChangedEvent.h"
|
||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
@@ -37,6 +38,7 @@ class SelectedBuildingPanel : public QWidget,
|
|||||||
PlayerCommandsAppliedEvent,
|
PlayerCommandsAppliedEvent,
|
||||||
EntitySelectedEvent,
|
EntitySelectedEvent,
|
||||||
SelectionChangedEvent,
|
SelectionChangedEvent,
|
||||||
|
ScrapSelectionChangedEvent,
|
||||||
DebugDrawToggledEvent>
|
DebugDrawToggledEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -51,6 +53,7 @@ private:
|
|||||||
void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> event) override;
|
void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> 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;
|
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const ScrapSelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
@@ -77,6 +80,8 @@ private:
|
|||||||
void buildEmpty();
|
void buildEmpty();
|
||||||
void buildSingle(BuildingId id);
|
void buildSingle(BuildingId id);
|
||||||
void buildMulti(const std::vector<BuildingId>& ids);
|
void buildMulti(const std::vector<BuildingId>& ids);
|
||||||
|
void buildScrap();
|
||||||
|
void refreshScrapTotal();
|
||||||
void refreshBuffers(const Building* b);
|
void refreshBuffers(const Building* b);
|
||||||
void refreshSiteProgress(const ConstructionSite* s);
|
void refreshSiteProgress(const ConstructionSite* s);
|
||||||
void updateShipyardLayoutWidgets(BuildingType type,
|
void updateShipyardLayoutWidgets(BuildingType type,
|
||||||
@@ -115,6 +120,9 @@ private:
|
|||||||
QLabel* m_entityTitleLabel;
|
QLabel* m_entityTitleLabel;
|
||||||
QLabel* m_stationStatsLabel;
|
QLabel* m_stationStatsLabel;
|
||||||
|
|
||||||
|
std::vector<entt::entity> m_selectedScrap;
|
||||||
|
QLabel* m_scrapLabel;
|
||||||
|
|
||||||
void buildEntityShip(entt::entity entity);
|
void buildEntityShip(entt::entity entity);
|
||||||
void buildEntityStation(entt::entity entity);
|
void buildEntityStation(entt::entity entity);
|
||||||
void refreshEntityStats();
|
void refreshEntityStats();
|
||||||
|
|||||||
Reference in New Issue
Block a user