Compare commits
3 Commits
d271d65678
...
31a8915b0f
| Author | SHA1 | Date | |
|---|---|---|---|
| 31a8915b0f | |||
| f818c90af0 | |||
| 577927ef70 |
@@ -96,10 +96,10 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- REQ-BLD-QUEUE: Placed buildings enter a construction queue and are built one at a time. Each building takes a duration defined in `buildings.toml [[building]].construction_time_seconds` to construct.
|
- REQ-BLD-QUEUE: Placed buildings enter a construction queue and are built one at a time. Each building takes a duration defined in `buildings.toml [[building]].construction_time_seconds` to construct.
|
||||||
- REQ-BLD-ASTEROID-ONLY: Buildings can only be placed on asteroid tiles (per surface_mask; tiles marked `S` may extend into space).
|
- REQ-BLD-ASTEROID-ONLY: Buildings can only be placed on asteroid tiles (per surface_mask; tiles marked `S` may extend into space).
|
||||||
- REQ-BLD-BUILDER-MODE: Clicking a build button activates builder mode for that building type. Builder mode is exited by right-clicking in the game world or clicking the same build button again.
|
- REQ-BLD-BUILDER-MODE: Clicking a build button activates builder mode for that building type. Builder mode is exited by right-clicking in the game world or clicking the same build button again.
|
||||||
- REQ-BLD-GHOST: While in builder mode, a ghost of the building is rendered at the tile under the cursor, showing where it would be placed.
|
- REQ-BLD-GHOST: While in builder mode, a ghost of the building is rendered at the tile under the cursor, showing where it would be placed. The ghost is drawn semi-transparently in the building type's own visuals — its `fill` and `outline` colors and `glyph` from `visuals.toml` — so that different building types are visually distinguishable in builder mode rather than all looking alike. When the current cursor position is invalid, the ghost instead uses the distinct "invalid" color (REQ-BLD-PLACE-VALID), which overrides the per-building coloring.
|
||||||
- REQ-BLD-ROTATE: While in builder mode, pressing E rotates the ghost 90° clockwise and Q rotates it 90° counter-clockwise. Rotation affects the direction of the output port.
|
- REQ-BLD-ROTATE: While in builder mode, pressing Shift+R rotates the ghost 90° clockwise and R rotates it 90° counter-clockwise. Rotation affects the direction of the output port.
|
||||||
- REQ-BLD-PLACE: Clicking a valid tile in builder mode places a construction site and adds it to the build queue, consuming building blocks from the global stock.
|
- REQ-BLD-PLACE: Clicking a valid tile in builder mode places a construction site and adds it to the build queue, consuming building blocks from the global stock.
|
||||||
- REQ-BLD-PLACE-VALID: A placement position is valid only if (a) every footprint cell in the rotated `surface_mask` is satisfied by the underlying terrain — `A` cells coincide with asteroid tiles, `S` cells coincide with space tiles — (b) no footprint cell overlaps an existing placed building or construction site, except as allowed by REQ-BLD-ROTATE-IN-PLACE, and (c) the player has enough building blocks to afford the building. The ghost (REQ-BLD-GHOST) is rendered in a distinct "invalid" color when the current cursor position fails any of these conditions.
|
- REQ-BLD-PLACE-VALID: A placement position is valid only if (a) every footprint cell in the rotated `surface_mask` is satisfied by the underlying terrain — `A` cells coincide with asteroid tiles, `S` cells coincide with space tiles — (b) no footprint cell overlaps an existing placed building or construction site, except as allowed by REQ-BLD-ROTATE-IN-PLACE, and (c) the player has enough building blocks to afford the building. The ghost (REQ-BLD-GHOST) is rendered in a distinct "invalid" color — overriding its per-building coloring (REQ-BLD-GHOST) — when the current cursor position fails any of these conditions.
|
||||||
- REQ-BLD-ROTATE-IN-PLACE: If the ghost's footprint exactly coincides with the footprint of an existing placed building or construction site of the same building type, clicking places no new construction site and consumes no building blocks. Instead, the existing building or site is rotated to match the ghost's rotation. If the target is a construction site, its construction progress is preserved. This applies in both normal builder mode and blueprint placement mode; in blueprint placement mode it is evaluated per building in the blueprint independently — buildings in the blueprint whose footprint coincides with an existing same-type building or site are rotated in place, while the remaining buildings in the blueprint are placed as normal construction sites (subject to the usual validity checks and total cost).
|
- REQ-BLD-ROTATE-IN-PLACE: If the ghost's footprint exactly coincides with the footprint of an existing placed building or construction site of the same building type, clicking places no new construction site and consumes no building blocks. Instead, the existing building or site is rotated to match the ghost's rotation. If the target is a construction site, its construction progress is preserved. This applies in both normal builder mode and blueprint placement mode; in blueprint placement mode it is evaluated per building in the blueprint independently — buildings in the blueprint whose footprint coincides with an existing same-type building or site are rotated in place, while the remaining buildings in the blueprint are placed as normal construction sites (subject to the usual validity checks and total cost).
|
||||||
- REQ-BLD-BELT-DRAG: For belts, the player can click and drag across multiple tiles to place a construction site on each tile in one gesture.
|
- REQ-BLD-BELT-DRAG: For belts, the player can click and drag across multiple tiles to place a construction site on each tile in one gesture.
|
||||||
- REQ-BLD-TUNNEL-AUTO-SWITCH: After the player successfully places a Tunnel Entry construction site, builder mode automatically switches to Tunnel Exit (and vice versa), preserving the current ghost rotation. This makes it easy to immediately place the paired end without manually selecting the complementary type.
|
- REQ-BLD-TUNNEL-AUTO-SWITCH: After the player successfully places a Tunnel Entry construction site, builder mode automatically switches to Tunnel Exit (and vice versa), preserving the current ghost rotation. This makes it easy to immediately place the paired end without manually selecting the complementary type.
|
||||||
@@ -121,8 +121,8 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
- An item matching neither output's filter is routed to the unfiltered output. If both outputs have a filter and the item matches neither, the splitter stalls and moves no items until the situation is resolved.
|
- An item matching neither output's filter is routed to the unfiltered output. If both outputs have a filter and the item matches neither, the splitter stalls and moves no items until the situation is resolved.
|
||||||
- If neither output has a filter, items are distributed by strict alternation.
|
- If neither output has a filter, items are distributed by strict alternation.
|
||||||
- In all alternation cases, if one output is blocked the item goes to the other output until it unblocks.
|
- In all alternation cases, if one output is blocked the item goes to the other output until it unblocks.
|
||||||
- REQ-BLD-TUNNEL-ENTRY: **Tunnel Entry** (1×1): The sending end of a tunnel pair. The player sets a direction (N, S, E, W) at placement, rotatable with Q/E. Items arriving from an adjacent belt tile whose direction points into the entry are forwarded through the tunnel to the paired Tunnel Exit (see REQ-BLD-TUNNEL-PAIR, REQ-BLD-TUNNEL-TRANSIT). If the entry is unpaired, or if the paired exit's output is blocked, the entry blocks like a full belt tile.
|
- REQ-BLD-TUNNEL-ENTRY: **Tunnel Entry** (1×1): The sending end of a tunnel pair. The player sets a direction (N, S, E, W) at placement, rotatable with R/Shift+R. Items arriving from an adjacent belt tile whose direction points into the entry are forwarded through the tunnel to the paired Tunnel Exit (see REQ-BLD-TUNNEL-PAIR, REQ-BLD-TUNNEL-TRANSIT). If the entry is unpaired, or if the paired exit's output is blocked, the entry blocks like a full belt tile.
|
||||||
- REQ-BLD-TUNNEL-EXIT: **Tunnel Exit** (1×1): The receiving end of a tunnel pair. The player sets a direction at placement, rotatable with Q/E. Items received from the paired Tunnel Entry emerge from the output side of the exit tile — the tile adjacent in the exit's facing direction — continuing in that direction. If the exit is unpaired or its output is blocked, it holds received items until they can advance.
|
- REQ-BLD-TUNNEL-EXIT: **Tunnel Exit** (1×1): The receiving end of a tunnel pair. The player sets a direction at placement, rotatable with R/Shift+R. Items received from the paired Tunnel Entry emerge from the output side of the exit tile — the tile adjacent in the exit's facing direction — continuing in that direction. If the exit is unpaired or its output is blocked, it holds received items until they can advance.
|
||||||
- REQ-BLD-TUNNEL-PAIR: **Tunnel pairing rules.** Pairing is re-evaluated for all Tunnel Entries whenever any Tunnel Entry or Tunnel Exit is placed or demolished.
|
- REQ-BLD-TUNNEL-PAIR: **Tunnel pairing rules.** Pairing is re-evaluated for all Tunnel Entries whenever any Tunnel Entry or Tunnel Exit is placed or demolished.
|
||||||
- A Tunnel Entry searches tile-by-tile in its facing direction for a partner. Any tunnel building (entry or exit) that faces a *different* direction is ignored and skipped. The search stops at the first tunnel building that faces the *same* direction as the searching entry.
|
- A Tunnel Entry searches tile-by-tile in its facing direction for a partner. Any tunnel building (entry or exit) that faces a *different* direction is ignored and skipped. The search stops at the first tunnel building that faces the *same* direction as the searching entry.
|
||||||
- If that first same-direction tunnel building is a Tunnel Exit, is within `tunnel_max_distance` tiles of the entry, and is not already paired with a closer entry, the two form a pair.
|
- If that first same-direction tunnel building is a Tunnel Exit, is within `tunnel_max_distance` tiles of the entry, and is not already paired with a closer entry, the two form a pair.
|
||||||
@@ -207,7 +207,7 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
|||||||
### Module Placement
|
### Module Placement
|
||||||
|
|
||||||
- REQ-MOD-PLACEMENT: In the layout configuration dialog (REQ-MOD-UI-DIALOG), the player places modules onto the ship's layout grid. Clicking a module button in the module selection grid enters module placement mode for that module type. While in placement mode, a ghost of the module's surface mask is rendered at the cell under the cursor. Clicking a valid position places one instance of the module. A position is valid if every `O` cell in the module's (rotated) surface mask coincides with an unoccupied buildable cell of the ship's layout. The player may place unlimited instances of the same module type.
|
- REQ-MOD-PLACEMENT: In the layout configuration dialog (REQ-MOD-UI-DIALOG), the player places modules onto the ship's layout grid. Clicking a module button in the module selection grid enters module placement mode for that module type. While in placement mode, a ghost of the module's surface mask is rendered at the cell under the cursor. Clicking a valid position places one instance of the module. A position is valid if every `O` cell in the module's (rotated) surface mask coincides with an unoccupied buildable cell of the ship's layout. The player may place unlimited instances of the same module type.
|
||||||
- REQ-MOD-ROTATION: While in module placement mode, pressing Q rotates the module ghost 90° counter-clockwise and E rotates it 90° clockwise. Rotation transforms the surface mask grid identically to building rotation (REQ-BLD-ROTATE).
|
- REQ-MOD-ROTATION: While in module placement mode, pressing R rotates the module ghost 90° counter-clockwise and Shift+R rotates it 90° clockwise. Rotation transforms the surface mask grid identically to building rotation (REQ-BLD-ROTATE).
|
||||||
- REQ-MOD-REMOVE: The module selection grid includes a "Remove" button. Clicking it enters remove mode. In remove mode, clicking on a cell occupied by a placed module removes that entire module instance from the layout. Remove mode is exited by clicking the Remove button again or by selecting a module for placement.
|
- REQ-MOD-REMOVE: The module selection grid includes a "Remove" button. Clicking it enters remove mode. In remove mode, clicking on a cell occupied by a placed module removes that entire module instance from the layout. Remove mode is exited by clicking the Remove button again or by selecting a module for placement.
|
||||||
|
|
||||||
### Module Effects
|
### Module Effects
|
||||||
@@ -401,14 +401,17 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
- **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×).
|
||||||
- **S** — decreases game speed by one step in the same sequence (no wrap-around past 0×).
|
- **S** — decreases game speed by one step in the same sequence (no wrap-around past 0×).
|
||||||
- **Backspace** — activates demolish mode; Backspace again exits it. (See also REQ-UI-DEMOLISH-BUTTON for the equivalent button.)
|
- **A / D** — scroll the view left / right (REQ-UI-SCROLL).
|
||||||
- **Q / E** — in builder mode, rotate the ghost counter-clockwise / clockwise (REQ-BLD-ROTATE).
|
- **Q** — context-sensitive. If a build mode is active (builder mode or blueprint placement mode), pressing Q exits it. Otherwise, pressing Q toggles demolish mode: it enters demolish mode if inactive, or exits demolish mode if already active. (See also REQ-UI-DEMOLISH-BUTTON for the equivalent button.)
|
||||||
|
- **R / Shift+R** — in builder mode, rotate the ghost counter-clockwise / clockwise (REQ-BLD-ROTATE).
|
||||||
- **Escape** — opens the escape menu (REQ-UI-GAME-MENU).
|
- **Escape** — opens the escape menu (REQ-UI-GAME-MENU).
|
||||||
- **M** — toggles debug draw mode (REQ-UI-DEBUG-DRAW).
|
- **Build mode selection** — pressing a build hotkey activates builder mode for the corresponding building type, equivalent to clicking its build button (REQ-BLD-BUILDER-MODE):
|
||||||
|
- **1** — Belt, **2** — Splitter, **3** — Tunnel Entry, **4** — Tunnel Exit.
|
||||||
|
- **Shift+1** — Miner, **Shift+2** — Smelter, **Shift+3** — Assembler, **Shift+4** — Shipyard, **Shift+5** — Salvage Bay, **Shift+6** — Reprocessing Plant.
|
||||||
|
|
||||||
### Debug Draw
|
### Debug Draw
|
||||||
|
|
||||||
- REQ-UI-DEBUG-DRAW: A debug draw mode can be toggled on and off with the **M** key (REQ-UI-HOTKEYS). It is inactive by default. While active, the sensor range of every ship — both player and enemy — is drawn as a circle centered on the ship, using that ship schematic's outline color from `visuals.toml`.
|
- REQ-UI-DEBUG-DRAW: A debug draw mode can be toggled on and off with the **F3** key. It is inactive by default. While active, the sensor range of every ship — both player and enemy — is drawn as a circle centered on the ship, using that ship schematic's outline color from `visuals.toml`.
|
||||||
|
|
||||||
- REQ-UI-DEBUG-OVERLAY: While debug draw mode is active (REQ-UI-DEBUG-DRAW), a text overlay is drawn in the upper left corner of the game world view. The overlay has a semi-transparent black background sized to fit its content. It displays the following lines of text:
|
- REQ-UI-DEBUG-OVERLAY: While debug draw mode is active (REQ-UI-DEBUG-DRAW), a text overlay is drawn in the upper left corner of the game world view. The overlay has a semi-transparent black background sized to fit its content. It displays the following lines of text:
|
||||||
- `Accumulated Threat Level: <level>` — where `<level>` is the current accumulated threat level (see REQ-WAV-THREAT-RATE).
|
- `Accumulated Threat Level: <level>` — where `<level>` is the current accumulated threat level (see REQ-WAV-THREAT-RATE).
|
||||||
@@ -447,7 +450,7 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
- REQ-UI-BUILD-GRID: All placeable building types are shown as a flat grid of buttons with no grouping.
|
- REQ-UI-BUILD-GRID: All placeable building types are shown as a flat grid of buttons with no grouping.
|
||||||
- REQ-UI-BUILD-COST: Each button caption shows the building name and its building block cost, e.g. "Belt: 2 Blocks".
|
- REQ-UI-BUILD-COST: Each button caption shows the building name and its building block cost, e.g. "Belt: 2 Blocks".
|
||||||
- REQ-UI-BUILD-DISABLED: Buttons for buildings the player cannot currently afford are shown as disabled.
|
- REQ-UI-BUILD-DISABLED: Buttons for buildings the player cannot currently afford are shown as disabled.
|
||||||
- REQ-UI-DEMOLISH-BUTTON: A dedicated **Demolish** button is shown in the build button grid. Clicking it toggles demolish mode on and off, equivalent to pressing Backspace (REQ-UI-HOTKEYS). The button is shown in a visually active/pressed state while demolish mode is active.
|
- REQ-UI-DEMOLISH-BUTTON: A dedicated **Demolish** button is shown in the build button grid. Clicking it toggles demolish mode on and off, equivalent to the Q demolish toggle (REQ-UI-HOTKEYS). The button is shown in a visually active/pressed state while demolish mode is active.
|
||||||
|
|
||||||
### Blueprint Panel
|
### Blueprint Panel
|
||||||
|
|
||||||
@@ -455,13 +458,13 @@ The screen is divided into two columns: a main column (75% width) containing the
|
|||||||
|
|
||||||
- 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. 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-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) at the time of capture. If no recipe or schematic was selected at capture time, none is stored. This structure maps directly to a TOML representation (e.g. one `[[building]]` array entry per constituent building).
|
- 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-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.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). Pressing Q/E rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint.
|
- REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). A valid ghost uses its building type's semi-transparent per-building coloring (REQ-BLD-GHOST); an invalid ghost uses the distinct "invalid" color, as in single-building builder mode. Pressing R / Shift+R rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately.
|
- REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. If splitter output filters are stored, they are applied to the construction site immediately and carry over when it finishes building (REQ-BLD-SITE-CONFIG). Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately.
|
||||||
|
|
||||||
- REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited.
|
- REQ-UI-BLUEPRINT-DELETE: Clicking the delete icon ("×") on a blueprint entry immediately removes that blueprint from the list. If the deleted blueprint was active in blueprint placement mode, that mode is exited.
|
||||||
|
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ void ArenaView::mousePressEvent(QMouseEvent* event)
|
|||||||
|
|
||||||
void ArenaView::keyPressEvent(QKeyEvent* event)
|
void ArenaView::keyPressEvent(QKeyEvent* event)
|
||||||
{
|
{
|
||||||
if (event->key() == Qt::Key_M)
|
if (event->key() == Qt::Key_F3)
|
||||||
{
|
{
|
||||||
m_debugDraw = !m_debugDraw;
|
m_debugDraw = !m_debugDraw;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -51,6 +51,24 @@ std::string serialize(const std::vector<Blueprint>& blueprints)
|
|||||||
bldTbl.insert("offset_x", static_cast<int64_t>(b.offset.x()));
|
bldTbl.insert("offset_x", static_cast<int64_t>(b.offset.x()));
|
||||||
bldTbl.insert("offset_y", static_cast<int64_t>(b.offset.y()));
|
bldTbl.insert("offset_y", static_cast<int64_t>(b.offset.y()));
|
||||||
bldTbl.insert("recipe_id", b.recipeId);
|
bldTbl.insert("recipe_id", b.recipeId);
|
||||||
|
if (!b.splitterFilterA.empty())
|
||||||
|
{
|
||||||
|
toml::array filterArr;
|
||||||
|
for (const ItemType& item : b.splitterFilterA)
|
||||||
|
{
|
||||||
|
filterArr.push_back(item.id);
|
||||||
|
}
|
||||||
|
bldTbl.insert("filter_a", std::move(filterArr));
|
||||||
|
}
|
||||||
|
if (!b.splitterFilterB.empty())
|
||||||
|
{
|
||||||
|
toml::array filterArr;
|
||||||
|
for (const ItemType& item : b.splitterFilterB)
|
||||||
|
{
|
||||||
|
filterArr.push_back(item.id);
|
||||||
|
}
|
||||||
|
bldTbl.insert("filter_b", std::move(filterArr));
|
||||||
|
}
|
||||||
if (b.shipLayout.has_value())
|
if (b.shipLayout.has_value())
|
||||||
{
|
{
|
||||||
toml::array modArr;
|
toml::array modArr;
|
||||||
@@ -138,6 +156,28 @@ std::vector<Blueprint> deserialize(const std::string& tomlContent)
|
|||||||
bb.offset.setX(static_cast<int>((*bldTbl)["offset_x"].value_or(int64_t{0})));
|
bb.offset.setX(static_cast<int>((*bldTbl)["offset_x"].value_or(int64_t{0})));
|
||||||
bb.offset.setY(static_cast<int>((*bldTbl)["offset_y"].value_or(int64_t{0})));
|
bb.offset.setY(static_cast<int>((*bldTbl)["offset_y"].value_or(int64_t{0})));
|
||||||
bb.recipeId = (*bldTbl)["recipe_id"].value_or(std::string{});
|
bb.recipeId = (*bldTbl)["recipe_id"].value_or(std::string{});
|
||||||
|
|
||||||
|
const toml::array* filterAArr = (*bldTbl)["filter_a"].as_array();
|
||||||
|
if (filterAArr)
|
||||||
|
{
|
||||||
|
for (std::size_t k = 0; k < filterAArr->size(); ++k)
|
||||||
|
{
|
||||||
|
const std::optional<std::string> itemId =
|
||||||
|
(*filterAArr)[k].value<std::string>();
|
||||||
|
if (itemId) { bb.splitterFilterA.push_back(ItemType{*itemId}); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toml::array* filterBArr = (*bldTbl)["filter_b"].as_array();
|
||||||
|
if (filterBArr)
|
||||||
|
{
|
||||||
|
for (std::size_t k = 0; k < filterBArr->size(); ++k)
|
||||||
|
{
|
||||||
|
const std::optional<std::string> itemId =
|
||||||
|
(*filterBArr)[k].value<std::string>();
|
||||||
|
if (itemId) { bb.splitterFilterB.push_back(ItemType{*itemId}); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toml::array* modArr = (*bldTbl)["modules"].as_array();
|
const toml::array* modArr = (*bldTbl)["modules"].as_array();
|
||||||
if (modArr)
|
if (modArr)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
|
#include "ItemType.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
@@ -18,6 +19,11 @@ struct BlueprintBuilding
|
|||||||
QPoint offset; // tile offset from bounding-box center (floor for even sizes)
|
QPoint offset; // tile offset from bounding-box center (floor for even sizes)
|
||||||
std::string recipeId; // empty = none selected
|
std::string recipeId; // empty = none selected
|
||||||
std::optional<ShipLayoutConfig> shipLayout;
|
std::optional<ShipLayoutConfig> shipLayout;
|
||||||
|
|
||||||
|
// Splitter output filters captured at blueprint creation (REQ-UI-BLUEPRINT-STORAGE).
|
||||||
|
// Empty = accept all. Re-applied to the placed splitter site (REQ-UI-BLUEPRINT-PLACE).
|
||||||
|
std::vector<ItemType> splitterFilterA;
|
||||||
|
std::vector<ItemType> splitterFilterB;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Blueprint
|
struct Blueprint
|
||||||
|
|||||||
11
src/lib/eventsystem/event/BuildHotkeyPressedEvent.h
Normal file
11
src/lib/eventsystem/event/BuildHotkeyPressedEvent.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
class BuildHotkeyPressedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit BuildHotkeyPressedEvent(BuildingType type) : type(type) {}
|
||||||
|
const BuildingType type;
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EscapeMenuRequestedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeChangedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeChangedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTypeSelectedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingTypeSelectedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildHotkeyPressedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ExitBuilderModeRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ExitBuilderModeRequestedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeToggleRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeToggleRequestedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "Blueprint.h"
|
#include "Blueprint.h"
|
||||||
#include "BlueprintSerializer.h"
|
#include "BlueprintSerializer.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
|
#include "ItemType.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
|
|
||||||
@@ -263,3 +264,42 @@ TEST_CASE("BlueprintSerializer: shipyard with empty modules list round-trips", "
|
|||||||
REQUIRE(loaded[0].buildings[0].shipLayout.has_value());
|
REQUIRE(loaded[0].buildings[0].shipLayout.has_value());
|
||||||
REQUIRE(loaded[0].buildings[0].shipLayout->placedModules.empty());
|
REQUIRE(loaded[0].buildings[0].shipLayout->placedModules.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Splitter output filters round-trip through TOML serialization
|
||||||
|
// (REQ-UI-BLUEPRINT-STORAGE).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: splitter output filters round-trip", "[serializer]")
|
||||||
|
{
|
||||||
|
BlueprintBuilding splitter =
|
||||||
|
makeBuilding(BuildingType::Splitter, Rotation::East, QPoint(0, 0));
|
||||||
|
splitter.splitterFilterA = { ItemType{"iron_ore"}, ItemType{"copper_ore"} };
|
||||||
|
splitter.splitterFilterB = { ItemType{"scrap"} };
|
||||||
|
|
||||||
|
const Blueprint original = makeBlueprintWith({ splitter }, "Sorter");
|
||||||
|
const std::string toml = BlueprintSerializer::serialize({ original });
|
||||||
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
||||||
|
|
||||||
|
REQUIRE(loaded.size() == 1);
|
||||||
|
REQUIRE(loaded[0].buildings.size() == 1);
|
||||||
|
|
||||||
|
const BlueprintBuilding& b = loaded[0].buildings[0];
|
||||||
|
REQUIRE(b.splitterFilterA
|
||||||
|
== std::vector<ItemType>{ ItemType{"iron_ore"}, ItemType{"copper_ore"} });
|
||||||
|
REQUIRE(b.splitterFilterB == std::vector<ItemType>{ ItemType{"scrap"} });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("BlueprintSerializer: splitter with no filters round-trips empty", "[serializer]")
|
||||||
|
{
|
||||||
|
const Blueprint original = makeBlueprintWith(
|
||||||
|
{ makeBuilding(BuildingType::Splitter, Rotation::East, QPoint(0, 0)) }, "Open");
|
||||||
|
|
||||||
|
const std::string toml = BlueprintSerializer::serialize({ original });
|
||||||
|
const std::vector<Blueprint> loaded = BlueprintSerializer::deserialize(toml);
|
||||||
|
|
||||||
|
REQUIRE(loaded.size() == 1);
|
||||||
|
REQUIRE(loaded[0].buildings.size() == 1);
|
||||||
|
REQUIRE(loaded[0].buildings[0].splitterFilterA.empty());
|
||||||
|
REQUIRE(loaded[0].buildings[0].splitterFilterB.empty());
|
||||||
|
}
|
||||||
|
|||||||
@@ -201,6 +201,16 @@ Blueprint BlueprintPanel::createBlueprintFromSelection() const
|
|||||||
bb.offset = e.building->anchor - center;
|
bb.offset = e.building->anchor - center;
|
||||||
bb.recipeId = e.building->recipeId;
|
bb.recipeId = e.building->recipeId;
|
||||||
bb.shipLayout = e.building->shipLayout;
|
bb.shipLayout = e.building->shipLayout;
|
||||||
|
if (e.building->type == BuildingType::Splitter)
|
||||||
|
{
|
||||||
|
const std::optional<BeltSystem::SplitterInfo> info =
|
||||||
|
m_sim->belts().getSplitterInfo(e.building->anchor);
|
||||||
|
if (info.has_value())
|
||||||
|
{
|
||||||
|
bb.splitterFilterA = info->filterA;
|
||||||
|
bb.splitterFilterB = info->filterB;
|
||||||
|
}
|
||||||
|
}
|
||||||
bp.buildings.push_back(bb);
|
bp.buildings.push_back(bb);
|
||||||
}
|
}
|
||||||
return bp;
|
return bp;
|
||||||
|
|||||||
@@ -130,3 +130,20 @@ void BuildButtonGrid::handleEvent(std::shared_ptr<const DemolishModeChangedEvent
|
|||||||
{
|
{
|
||||||
m_demolishButton->setChecked(event->active);
|
m_demolishButton->setChecked(event->active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BuildButtonGrid::handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event)
|
||||||
|
{
|
||||||
|
for (std::size_t i = 0; i < m_types.size(); ++i)
|
||||||
|
{
|
||||||
|
if (m_types[i] == event->type)
|
||||||
|
{
|
||||||
|
// Equivalent to clicking the build button: a disabled (unaffordable)
|
||||||
|
// button cannot be clicked, so the hotkey is likewise inert.
|
||||||
|
if (m_buttons[i]->isEnabled())
|
||||||
|
{
|
||||||
|
onBuildButton(static_cast<int>(i));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "BuilderModeExitedEvent.h"
|
#include "BuilderModeExitedEvent.h"
|
||||||
|
#include "BuildHotkeyPressedEvent.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "DemolishModeChangedEvent.h"
|
#include "DemolishModeChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
@@ -15,7 +16,8 @@ class QPushButton;
|
|||||||
|
|
||||||
class BuildButtonGrid : public QWidget,
|
class BuildButtonGrid : public QWidget,
|
||||||
public CombinedEventHandler<BuilderModeExitedEvent,
|
public CombinedEventHandler<BuilderModeExitedEvent,
|
||||||
DemolishModeChangedEvent>
|
DemolishModeChangedEvent,
|
||||||
|
BuildHotkeyPressedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -29,6 +31,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const DemolishModeChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onBuildButton(int index);
|
void onBuildButton(int index);
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "EscapeMenuRequestedEvent.h"
|
#include "EscapeMenuRequestedEvent.h"
|
||||||
#include "TracePrintRequestedEvent.h"
|
#include "TracePrintRequestedEvent.h"
|
||||||
|
#include "BuildHotkeyPressedEvent.h"
|
||||||
#include "BossWaveUpdatedEvent.h"
|
#include "BossWaveUpdatedEvent.h"
|
||||||
#include "BuilderModeExitedEvent.h"
|
#include "BuilderModeExitedEvent.h"
|
||||||
#include "BlueprintModeExitedEvent.h"
|
#include "BlueprintModeExitedEvent.h"
|
||||||
@@ -58,6 +59,19 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// Keep only the filter entries whose item type is currently unlocked
|
||||||
|
// (REQ-LOCK-UI-BLUEPRINT). An empty result means "accept all".
|
||||||
|
std::vector<ItemType> filterUnlockedItems(const std::vector<ItemType>& filter,
|
||||||
|
const Simulation& sim)
|
||||||
|
{
|
||||||
|
std::vector<ItemType> result;
|
||||||
|
for (const ItemType& item : filter)
|
||||||
|
{
|
||||||
|
if (sim.isItemUnlocked(item.id)) { result.push_back(item); }
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
Rotation rotateClockwise(Rotation r)
|
Rotation rotateClockwise(Rotation r)
|
||||||
{
|
{
|
||||||
switch (r)
|
switch (r)
|
||||||
@@ -532,6 +546,18 @@ void GameWorldView::placeBlueprintAtTile(QPoint center)
|
|||||||
{
|
{
|
||||||
m_sim->buildings().setShipLayout(id, *bb.shipLayout);
|
m_sim->buildings().setShipLayout(id, *bb.shipLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bb.type == BuildingType::Splitter
|
||||||
|
&& (!bb.splitterFilterA.empty() || !bb.splitterFilterB.empty()))
|
||||||
|
{
|
||||||
|
// The splitter is still a construction site, so the filters carry
|
||||||
|
// over when it finishes building (REQ-UI-BLUEPRINT-PLACE). Locked
|
||||||
|
// item types are dropped per REQ-LOCK-UI-BLUEPRINT.
|
||||||
|
m_sim->buildings().setSiteSplitterFilters(
|
||||||
|
id,
|
||||||
|
filterUnlockedItems(bb.splitterFilterA, *m_sim),
|
||||||
|
filterUnlockedItems(bb.splitterFilterB, *m_sim));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1061,25 +1087,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
|||||||
// Builder-mode ghost
|
// Builder-mode ghost
|
||||||
if (m_builderType.has_value())
|
if (m_builderType.has_value())
|
||||||
{
|
{
|
||||||
const BuildingDef* def = findBuildingDef(*m_builderType);
|
drawBuildingGhost(painter, *m_builderType, m_ghostTile,
|
||||||
if (def)
|
m_ghostRotation, m_ghostValid);
|
||||||
{
|
|
||||||
const ParsedSurfaceMask parsed =
|
|
||||||
parseSurfaceMask(def->surfaceMask, m_ghostRotation);
|
|
||||||
const QColor& ghostColor = m_ghostValid
|
|
||||||
? m_visuals->overlays.ghostValid
|
|
||||||
: m_visuals->overlays.ghostInvalid;
|
|
||||||
for (const QPoint& cell : parsed.bodyCells)
|
|
||||||
{
|
|
||||||
painter.fillRect(tileRect(m_ghostTile + cell), ghostColor);
|
|
||||||
}
|
|
||||||
for (const Port& port : parsed.outputPorts)
|
|
||||||
{
|
|
||||||
const QPoint absBody = m_ghostTile
|
|
||||||
+ portBodyTile(port.tile, port.direction);
|
|
||||||
drawPortGlyph(painter, absBody, port.direction, Qt::white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blueprint placement ghost
|
// Blueprint placement ghost
|
||||||
@@ -1089,22 +1098,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
|||||||
{
|
{
|
||||||
const QPoint anchor = m_blueprintGhostTile + bb.offset;
|
const QPoint anchor = m_blueprintGhostTile + bb.offset;
|
||||||
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
|
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
|
||||||
const QColor& ghostColor = valid
|
drawBuildingGhost(painter, bb.type, anchor, bb.rotation, valid);
|
||||||
? m_visuals->overlays.ghostValid
|
|
||||||
: m_visuals->overlays.ghostInvalid;
|
|
||||||
const BuildingDef* def = findBuildingDef(bb.type);
|
|
||||||
if (!def) { continue; }
|
|
||||||
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, bb.rotation);
|
|
||||||
for (const QPoint& cell : parsed.bodyCells)
|
|
||||||
{
|
|
||||||
painter.fillRect(tileRect(anchor + cell), ghostColor);
|
|
||||||
}
|
|
||||||
for (const Port& port : parsed.outputPorts)
|
|
||||||
{
|
|
||||||
drawPortGlyph(painter,
|
|
||||||
anchor + portBodyTile(port.tile, port.direction),
|
|
||||||
port.direction, Qt::white);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1135,6 +1129,68 @@ void GameWorldView::drawOverlays(QPainter& painter)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
|
||||||
|
QPoint anchorTile, Rotation rotation,
|
||||||
|
bool valid)
|
||||||
|
{
|
||||||
|
const BuildingDef* def = findBuildingDef(type);
|
||||||
|
if (!def) { return; }
|
||||||
|
|
||||||
|
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
||||||
|
m_visuals->buildings.find(type);
|
||||||
|
if (it == m_visuals->buildings.end()) { return; }
|
||||||
|
const BuildingVisuals& bv = it->second;
|
||||||
|
|
||||||
|
// Valid ghosts show the building type's own colors; invalid ghosts override
|
||||||
|
// with the distinct invalid color (REQ-BLD-GHOST, REQ-BLD-PLACE-VALID). The
|
||||||
|
// invalid color's RGB is taken at full opacity so it does not double-dim
|
||||||
|
// against the setOpacity below (the configured color carries its own alpha).
|
||||||
|
const QColor invalidColor(m_visuals->overlays.ghostInvalid.red(),
|
||||||
|
m_visuals->overlays.ghostInvalid.green(),
|
||||||
|
m_visuals->overlays.ghostInvalid.blue());
|
||||||
|
const QColor fillColor = valid ? bv.fill : invalidColor;
|
||||||
|
const QColor lineColor = valid ? bv.outline : invalidColor;
|
||||||
|
|
||||||
|
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rotation);
|
||||||
|
if (parsed.bodyCells.empty()) { return; }
|
||||||
|
|
||||||
|
painter.setOpacity(0.5);
|
||||||
|
|
||||||
|
QPoint minCell = parsed.bodyCells.front();
|
||||||
|
QPoint maxCell = parsed.bodyCells.front();
|
||||||
|
for (const QPoint& cell : parsed.bodyCells)
|
||||||
|
{
|
||||||
|
painter.fillRect(tileRect(anchorTile + cell), fillColor);
|
||||||
|
minCell.setX(std::min(minCell.x(), cell.x()));
|
||||||
|
minCell.setY(std::min(minCell.y(), cell.y()));
|
||||||
|
maxCell.setX(std::max(maxCell.x(), cell.x()));
|
||||||
|
maxCell.setY(std::max(maxCell.y(), cell.y()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPointF tl = tileToWidget(anchorTile + minCell);
|
||||||
|
const QRectF bboxRect(tl.x(), tl.y(),
|
||||||
|
(maxCell.x() - minCell.x() + 1) * static_cast<qreal>(tilePx()),
|
||||||
|
(maxCell.y() - minCell.y() + 1) * static_cast<qreal>(tilePx()));
|
||||||
|
|
||||||
|
painter.setPen(QPen(lineColor, 1));
|
||||||
|
painter.setBrush(Qt::NoBrush);
|
||||||
|
painter.drawRect(bboxRect);
|
||||||
|
|
||||||
|
if (!bv.glyph.isEmpty())
|
||||||
|
{
|
||||||
|
painter.setPen(lineColor);
|
||||||
|
painter.drawText(bboxRect, Qt::AlignCenter, bv.glyph);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Port& port : parsed.outputPorts)
|
||||||
|
{
|
||||||
|
drawPortGlyph(painter, anchorTile + portBodyTile(port.tile, port.direction),
|
||||||
|
port.direction, lineColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.setOpacity(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
void GameWorldView::drawScreenSpace(QPainter& /*painter*/)
|
void GameWorldView::drawScreenSpace(QPainter& /*painter*/)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -1151,6 +1207,45 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Number-key build-mode hotkeys (REQ-UI-HOTKEYS). nativeVirtualKey gives the
|
||||||
|
// physical digit independent of keyboard layout and Shift (with Shift held, key()
|
||||||
|
// for the number row can arrive as Key_Exclam etc.). VK_1..VK_9 = 0x31..0x39.
|
||||||
|
const quint32 virtualKey = event->nativeVirtualKey();
|
||||||
|
if (virtualKey >= 0x31 && virtualKey <= 0x39)
|
||||||
|
{
|
||||||
|
const int digit = static_cast<int>(virtualKey - 0x30);
|
||||||
|
const bool shift = (event->modifiers() & Qt::ShiftModifier) != 0;
|
||||||
|
std::optional<BuildingType> type;
|
||||||
|
if (!shift)
|
||||||
|
{
|
||||||
|
switch (digit)
|
||||||
|
{
|
||||||
|
case 1: type = BuildingType::Belt; break;
|
||||||
|
case 2: type = BuildingType::Splitter; break;
|
||||||
|
case 3: type = BuildingType::TunnelEntry; break;
|
||||||
|
case 4: type = BuildingType::TunnelExit; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (digit)
|
||||||
|
{
|
||||||
|
case 1: type = BuildingType::Miner; break;
|
||||||
|
case 2: type = BuildingType::Smelter; break;
|
||||||
|
case 3: type = BuildingType::Assembler; break;
|
||||||
|
case 4: type = BuildingType::Shipyard; break;
|
||||||
|
case 5: type = BuildingType::SalvageBay; break;
|
||||||
|
case 6: type = BuildingType::ReprocessingPlant; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type.has_value())
|
||||||
|
{
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<BuildHotkeyPressedEvent>(*type));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (event->key())
|
switch (event->key())
|
||||||
{
|
{
|
||||||
case Qt::Key_A:
|
case Qt::Key_A:
|
||||||
@@ -1176,69 +1271,24 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
|
|||||||
case Qt::Key_S:
|
case Qt::Key_S:
|
||||||
stepSpeed(-1);
|
stepSpeed(-1);
|
||||||
break;
|
break;
|
||||||
case Qt::Key_E:
|
case Qt::Key_R:
|
||||||
if (m_builderType.has_value())
|
rotateGhost(event->modifiers() & Qt::ShiftModifier);
|
||||||
{
|
|
||||||
m_ghostRotation = rotateClockwise(m_ghostRotation);
|
|
||||||
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation);
|
|
||||||
}
|
|
||||||
else if (m_blueprintMode.has_value())
|
|
||||||
{
|
|
||||||
for (BlueprintBuilding& bb : m_blueprintMode->buildings)
|
|
||||||
{
|
|
||||||
const BuildingDef* def = findBuildingDef(bb.type);
|
|
||||||
if (!def) { continue; }
|
|
||||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation);
|
|
||||||
int minX = INT_MAX, minY = INT_MAX;
|
|
||||||
for (const QPoint& cell : mask.bodyCells)
|
|
||||||
{
|
|
||||||
const QPoint abs = bb.offset + cell;
|
|
||||||
minX = std::min(minX, -abs.y());
|
|
||||||
minY = std::min(minY, abs.x());
|
|
||||||
}
|
|
||||||
bb.offset = QPoint(minX, minY);
|
|
||||||
bb.rotation = rotateClockwise(bb.rotation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case Qt::Key_Q:
|
case Qt::Key_Q:
|
||||||
if (m_builderType.has_value())
|
if (m_builderType.has_value()) { exitBuilderMode(); }
|
||||||
{
|
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
|
||||||
m_ghostRotation = rotateCounterClockwise(m_ghostRotation);
|
else { toggleDemolishMode(); }
|
||||||
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation);
|
|
||||||
}
|
|
||||||
else if (m_blueprintMode.has_value())
|
|
||||||
{
|
|
||||||
for (BlueprintBuilding& bb : m_blueprintMode->buildings)
|
|
||||||
{
|
|
||||||
const BuildingDef* def = findBuildingDef(bb.type);
|
|
||||||
if (!def) { continue; }
|
|
||||||
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation);
|
|
||||||
int minX = INT_MAX, minY = INT_MAX;
|
|
||||||
for (const QPoint& cell : mask.bodyCells)
|
|
||||||
{
|
|
||||||
const QPoint abs = bb.offset + cell;
|
|
||||||
minX = std::min(minX, abs.y());
|
|
||||||
minY = std::min(minY, -abs.x());
|
|
||||||
}
|
|
||||||
bb.offset = QPoint(minX, minY);
|
|
||||||
bb.rotation = rotateCounterClockwise(bb.rotation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case Qt::Key_Escape:
|
case Qt::Key_Escape:
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<EscapeMenuRequestedEvent>());
|
std::make_shared<EscapeMenuRequestedEvent>());
|
||||||
break;
|
break;
|
||||||
case Qt::Key_Backspace:
|
case Qt::Key_F3:
|
||||||
toggleDemolishMode();
|
|
||||||
break;
|
|
||||||
case Qt::Key_M:
|
|
||||||
m_debugDraw = !m_debugDraw;
|
m_debugDraw = !m_debugDraw;
|
||||||
EventManager::getInstance()->sendEventImmediately(
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
std::make_shared<DebugDrawToggledEvent>(m_debugDraw));
|
std::make_shared<DebugDrawToggledEvent>(m_debugDraw));
|
||||||
break;
|
break;
|
||||||
case Qt::Key_L:
|
case Qt::Key_F4:
|
||||||
EventManager::getInstance()->addEvent(std::make_shared<TracePrintRequestedEvent>());
|
EventManager::getInstance()->addEvent(std::make_shared<TracePrintRequestedEvent>());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -1490,6 +1540,43 @@ void GameWorldView::toggleDemolishMode()
|
|||||||
std::make_shared<DemolishModeChangedEvent>(m_demolishMode));
|
std::make_shared<DemolishModeChangedEvent>(m_demolishMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameWorldView::rotateGhost(bool clockwise)
|
||||||
|
{
|
||||||
|
if (m_builderType.has_value())
|
||||||
|
{
|
||||||
|
m_ghostRotation = clockwise ? rotateClockwise(m_ghostRotation)
|
||||||
|
: rotateCounterClockwise(m_ghostRotation);
|
||||||
|
m_ghostValid = isValidPlacement(*m_builderType, m_ghostTile, m_ghostRotation);
|
||||||
|
}
|
||||||
|
else if (m_blueprintMode.has_value())
|
||||||
|
{
|
||||||
|
for (BlueprintBuilding& bb : m_blueprintMode->buildings)
|
||||||
|
{
|
||||||
|
const BuildingDef* def = findBuildingDef(bb.type);
|
||||||
|
if (!def) { continue; }
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, bb.rotation);
|
||||||
|
int minX = INT_MAX, minY = INT_MAX;
|
||||||
|
for (const QPoint& cell : mask.bodyCells)
|
||||||
|
{
|
||||||
|
const QPoint abs = bb.offset + cell;
|
||||||
|
if (clockwise)
|
||||||
|
{
|
||||||
|
minX = std::min(minX, -abs.y());
|
||||||
|
minY = std::min(minY, abs.x());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
minX = std::min(minX, abs.y());
|
||||||
|
minY = std::min(minY, -abs.x());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bb.offset = QPoint(minX, minY);
|
||||||
|
bb.rotation = clockwise ? rotateClockwise(bb.rotation)
|
||||||
|
: rotateCounterClockwise(bb.rotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void GameWorldView::enterBuilderMode(BuildingType type)
|
void GameWorldView::enterBuilderMode(BuildingType type)
|
||||||
{
|
{
|
||||||
m_builderType = type;
|
m_builderType = type;
|
||||||
|
|||||||
@@ -126,6 +126,9 @@ private:
|
|||||||
void drawPortGlyph(QPainter& painter, QPoint bodyTile,
|
void drawPortGlyph(QPainter& painter, QPoint bodyTile,
|
||||||
Rotation direction, const QColor& color);
|
Rotation direction, const QColor& color);
|
||||||
|
|
||||||
|
void drawBuildingGhost(QPainter& painter, BuildingType type,
|
||||||
|
QPoint anchorTile, Rotation rotation, bool valid);
|
||||||
|
|
||||||
void placeBlueprintAtTile(QPoint center);
|
void placeBlueprintAtTile(QPoint center);
|
||||||
|
|
||||||
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
||||||
@@ -137,6 +140,7 @@ private:
|
|||||||
void enterBlueprintMode(Blueprint blueprint);
|
void enterBlueprintMode(Blueprint blueprint);
|
||||||
void exitBlueprintMode();
|
void exitBlueprintMode();
|
||||||
void toggleDemolishMode();
|
void toggleDemolishMode();
|
||||||
|
void rotateGhost(bool clockwise);
|
||||||
|
|
||||||
struct ActiveBeam
|
struct ActiveBeam
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -583,9 +583,22 @@ std::optional<ShipLayoutConfig> ShipLayoutDialog::result() const
|
|||||||
|
|
||||||
void ShipLayoutDialog::keyPressEvent(QKeyEvent* event)
|
void ShipLayoutDialog::keyPressEvent(QKeyEvent* event)
|
||||||
{
|
{
|
||||||
if (event->key() == Qt::Key_Q)
|
if (event->key() == Qt::Key_R)
|
||||||
{
|
{
|
||||||
// Rotate CCW = 3 CW steps.
|
if (event->modifiers() & Qt::ShiftModifier)
|
||||||
|
{
|
||||||
|
// Shift+R: rotate CW.
|
||||||
|
switch (m_currentRotation)
|
||||||
|
{
|
||||||
|
case Rotation::East: m_currentRotation = Rotation::South; break;
|
||||||
|
case Rotation::South: m_currentRotation = Rotation::West; break;
|
||||||
|
case Rotation::West: m_currentRotation = Rotation::North; break;
|
||||||
|
case Rotation::North: m_currentRotation = Rotation::East; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// R: rotate CCW.
|
||||||
switch (m_currentRotation)
|
switch (m_currentRotation)
|
||||||
{
|
{
|
||||||
case Rotation::East: m_currentRotation = Rotation::North; break;
|
case Rotation::East: m_currentRotation = Rotation::North; break;
|
||||||
@@ -593,17 +606,6 @@ void ShipLayoutDialog::keyPressEvent(QKeyEvent* event)
|
|||||||
case Rotation::West: m_currentRotation = Rotation::South; break;
|
case Rotation::West: m_currentRotation = Rotation::South; break;
|
||||||
case Rotation::South: m_currentRotation = Rotation::East; break;
|
case Rotation::South: m_currentRotation = Rotation::East; break;
|
||||||
}
|
}
|
||||||
updateGridWidget();
|
|
||||||
}
|
|
||||||
else if (event->key() == Qt::Key_E)
|
|
||||||
{
|
|
||||||
// Rotate CW.
|
|
||||||
switch (m_currentRotation)
|
|
||||||
{
|
|
||||||
case Rotation::East: m_currentRotation = Rotation::South; break;
|
|
||||||
case Rotation::South: m_currentRotation = Rotation::West; break;
|
|
||||||
case Rotation::West: m_currentRotation = Rotation::North; break;
|
|
||||||
case Rotation::North: m_currentRotation = Rotation::East; break;
|
|
||||||
}
|
}
|
||||||
updateGridWidget();
|
updateGridWidget();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user