Compare commits
10 Commits
44c3c83080
...
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 271855781c | |||
| 5f6ecbf6c8 | |||
| cd966daba9 | |||
| 88bc4f2170 | |||
| 4986c1bac8 | |||
| e110e7e413 | |||
| 6274b5ce60 | |||
| 16ed6a5695 | |||
| 13dacab274 | |||
| 2ddf13238c |
@@ -4,10 +4,20 @@ message(STATUS "Using CMake ${CMAKE_VERSION}")
|
||||
|
||||
include(cmake/add_files.cmake)
|
||||
include(cmake/create_source_groups.cmake)
|
||||
include(cmake/version.cmake)
|
||||
|
||||
# Project ----------------------------------------------------------------------
|
||||
|
||||
project(DotaFactory)
|
||||
# Product identity — anything that depends on the product/project name is defined
|
||||
# here so it lives in a single place and can change in the future. These values
|
||||
# feed the build targets in src/CMakeLists.txt and the Windows version resource
|
||||
# (see cmake/version.rc.in).
|
||||
set(PRODUCT_NAME "DotaFactory") # internal name and executable base name
|
||||
set(PRODUCT_DISPLAY_NAME "Dota Factory") # human-readable product / file description
|
||||
set(PRODUCT_COMPANY "TODO: company") # placeholder
|
||||
set(PRODUCT_COPYRIGHT "TODO: copyright") # placeholder
|
||||
|
||||
project(${PRODUCT_NAME})
|
||||
|
||||
set(CMAKE_BUILD_TYPE_INIT "Release")
|
||||
|
||||
|
||||
@@ -348,6 +348,7 @@ demolish_tint = "#ff000033" # demolish-mode hover tint
|
||||
selection_rect = "#00ff00" # box-drag selection rectangle (REQ-UI-MULTI-SELECT)
|
||||
tile_highlight = "#ffffff22" # tile under cursor
|
||||
selected_outline = "#ffff00" # outline drawn around currently-selected building(s)
|
||||
copy_config = "#33ccff66" # copy-settings eligible-target tint + copy/paste flash (REQ-BLD-COPY-CONFIG-FEEDBACK)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Schematic-drop toasts (REQ-UI-SCHEMATIC-TOAST)
|
||||
|
||||
60
cmake/version.cmake
Normal file
60
cmake/version.cmake
Normal file
@@ -0,0 +1,60 @@
|
||||
# simple check for a git repo
|
||||
if(EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||
find_package(Git)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_BRANCH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} log -1 --format=%h
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} log -1 --format=%ci
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_COMMIT_TIME
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} describe --long --match "[0-9]*" HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_VERSION_NUMBER
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
string(REGEX REPLACE "^([0-9]+)\\..*" "\\1" VERSION_MAJOR "${GIT_VERSION_NUMBER}")
|
||||
string(REGEX REPLACE "^[0-9]+\\.([0-9]+).*" "\\1" VERSION_MINOR "${GIT_VERSION_NUMBER}")
|
||||
string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" VERSION_PATCH "${GIT_VERSION_NUMBER}")
|
||||
string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+).*" "\\1" VERSION_COMMIT "${GIT_VERSION_NUMBER}")
|
||||
|
||||
else(EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||
set(GIT_BRANCH "")
|
||||
set(GIT_COMMIT_HASH "")
|
||||
set(GIT_VERSION_NUMBER "")
|
||||
set(VERSION_MAJOR "0")
|
||||
set(VERSION_MINOR "0")
|
||||
set(VERSION_PATCH "0")
|
||||
set(VERSION_COMMIT "0")
|
||||
set(BUILD_TYPE "")
|
||||
|
||||
endif(EXISTS "${CMAKE_SOURCE_DIR}/.git")
|
||||
|
||||
set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}.${VERSION_COMMIT}")
|
||||
|
||||
message(STATUS "Version: ${VERSION_STRING}")
|
||||
|
||||
# message(STATUS "Git current branch: ${GIT_BRANCH}")
|
||||
# message(STATUS "Git version number: " ${GIT_VERSION_NUMBER} )
|
||||
# message(STATUS "Git commit hash: ${GIT_COMMIT_HASH}")
|
||||
# message(STATUS "Git commit time: ${GIT_COMMIT_TIME}")
|
||||
# message(STATUS "Version major: ${VERSION_MAJOR}")
|
||||
# message(STATUS "Version minor: ${VERSION_MINOR}")
|
||||
# message(STATUS "Version patch: ${VERSION_PATCH}")
|
||||
# message(STATUS "Version commit: ${VERSION_COMMIT}")
|
||||
35
cmake/version.rc.in
Normal file
35
cmake/version.rc.in
Normal file
@@ -0,0 +1,35 @@
|
||||
// Windows version resource. Generated by CMake via configure_file() from this
|
||||
// template; @VAR@ placeholders are filled from cmake/version.cmake (version
|
||||
// numbers) and the product identity variables in the top-level CMakeLists.txt.
|
||||
// Shows up on the executable's Details tab (right-click -> Properties).
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION @VERSION_MAJOR@,@VERSION_MINOR@,@VERSION_PATCH@,@VERSION_COMMIT@
|
||||
PRODUCTVERSION @VERSION_MAJOR@,@VERSION_MINOR@,@VERSION_PATCH@,@VERSION_COMMIT@
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0" // US English (0x0409), Unicode (0x04b0)
|
||||
BEGIN
|
||||
VALUE "CompanyName", "@PRODUCT_COMPANY@"
|
||||
VALUE "FileDescription", "@PRODUCT_DISPLAY_NAME@"
|
||||
VALUE "FileVersion", "@VERSION_STRING@"
|
||||
VALUE "InternalName", "@PRODUCT_NAME@"
|
||||
VALUE "OriginalFilename", "@PRODUCT_NAME@.exe"
|
||||
VALUE "ProductName", "@PRODUCT_DISPLAY_NAME@"
|
||||
VALUE "ProductVersion", "@VERSION_STRING@"
|
||||
VALUE "LegalCopyright", "@PRODUCT_COPYRIGHT@"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200 // 0x409 = en-US, 1200 = Unicode code page
|
||||
END
|
||||
END
|
||||
@@ -112,6 +112,12 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
|
||||
- REQ-BLD-DEMOLISH-CLICK: While in demolish mode (REQ-UI-HOTKEYS, REQ-UI-DEMOLISH-BUTTON), left-clicking a placed factory building or construction site in the game world demolishes it, following the refund rules of REQ-BLD-DEMOLISH — the partial refund for built buildings and the full refund for still-queued construction sites. Clicking a building that cannot be demolished (the HQ or a player defence station, per REQ-BLD-DEMOLISH), or clicking empty world space, has no effect. Demolish mode stays active after a demolition so the player can demolish further buildings without re-entering the mode; it is exited via the Q toggle (REQ-UI-HOTKEYS) or the Demolish button (REQ-UI-DEMOLISH-BUTTON).
|
||||
- REQ-BLD-DEMOLISH-BOX: While in demolish mode (REQ-UI-HOTKEYS, REQ-UI-DEMOLISH-BUTTON), the player can click and drag a selection box in the game world. A selection rectangle is drawn while dragging, using the same box-drag gesture and coverage semantics as the multi-select box (REQ-UI-MULTI-SELECT). On mouse up, every placed factory building and construction site covered by the box is demolished, each following the refund rules of REQ-BLD-DEMOLISH — the partial refund for built buildings and the full refund for still-queued construction sites. Buildings that cannot be demolished (the HQ and player defence stations, per REQ-BLD-DEMOLISH) are excluded from the box demolition; ships and defence stations are never affected.
|
||||
- REQ-BLD-SITE-CONFIG: A construction site — a building that has been placed but is still queued or under construction (REQ-BLD-QUEUE) — can be selected and configured exactly like the equivalent operational building, before it finishes building. Whatever configuration the building type supports is available on the site: the recipe for a Miner or Assembler (REQ-UI-SELECT-BUTTON), the produced-ship schematic and its module layout for a Shipyard (REQ-UI-SELECT-BUTTON, REQ-MOD-UI-PREVIEW, REQ-MOD-UI-DIALOG), and the output filters for a Splitter (REQ-BLD-SPLITTER) — all set through the same Selected Building Panel controls (REQ-UI-CONFIG-INLINE). Only currently unlocked recipes and schematics are offered, exactly as for operational buildings (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SCHEMATIC, REQ-LOCK-UI-SPLITTER). The configuration is stored on the construction site and carries over unchanged when construction completes, so the building becomes operational already configured. A construction site has no input/output buffers and runs no production cycle, so the buffer and production-progress portions of the panel (REQ-UI-SINGLE-SELECTION, REQ-UI-PRODUCTION-PROGRESS) are not shown for it; only its construction progress (REQ-UI-CONSTRUCTION-PROGRESS) and its configuration controls appear. (Blueprint placement already applies a stored recipe or schematic to a construction site on placement per REQ-UI-BLUEPRINT-PLACE; this requirement additionally lets the player set or change that configuration directly on an existing site.)
|
||||
- REQ-BLD-COPY-CONFIG: **Copy building settings (hold Shift).** While the Shift key is held, the player can copy one building's settings onto other buildings of the same type, so several identical machines can be set up without opening each one's panel. This gesture is available only in the default selection mode; while a builder, blueprint placement, or demolish mode is active it is disabled, so it never clashes with placement or demolition clicks.
|
||||
- **Shift + right-click** a building copies its current settings into a temporary cache, along with the building's type. The settings copied are whatever that building type supports: the selected recipe (Miner, Assembler), the selected schematic together with its module layout (Shipyard), or the two output filters (Splitter, REQ-BLD-SPLITTER). Copying succeeds only when there is something to copy — a Miner or Assembler with a recipe selected, a Shipyard with a schematic selected, or any Splitter (whose output filters, even when empty/accept-all, always constitute valid settings). Shift + right-clicking a configurable building with nothing yet selected, a building type that has no settings at all (Smelter, Reprocessing Plant, Salvage Bay, belt/tunnel tiles, the HQ), or empty world space, has no effect and leaves any existing cache unchanged.
|
||||
- **Shift + left-click** a building of the **same type** as the cached one applies the cached settings to it, exactly as if the player had made that selection through the selected building panel — with the same effects as a normal selection change (buffer clearing per REQ-MAT-INPUT-BUFFER and REQ-MAT-OUTPUT-BUFFER, and, for a Shipyard, in-progress cycle cancellation per REQ-BLD-SHIPYARD). This can be repeated on any number of same-type buildings while Shift stays held. Shift + left-clicking a building of a different type than the cached one, any building while the cache is empty, or empty world space, has no effect.
|
||||
- Both operational buildings and construction sites take part as source and target (REQ-BLD-SITE-CONFIG); settings applied to a construction site carry over unchanged when it finishes building.
|
||||
- **Releasing Shift clears the temporary cache.** It is never persisted and does not survive Shift being released; the next copy starts fresh.
|
||||
- Because the cached settings were already valid on a same-type source building, they remain valid and available on the target (a selected recipe/schematic stays unlocked per REQ-LOCK-UI-RECIPE and REQ-LOCK-UI-SCHEMATIC; splitter filter item types stay unlocked per REQ-LOCK-UI-SPLITTER).
|
||||
|
||||
## Building Types
|
||||
|
||||
@@ -421,6 +427,7 @@ The screen is divided into two columns: a main column (75% width) containing the
|
||||
- **A / D** — scroll the view left / right (REQ-UI-SCROLL).
|
||||
- **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).
|
||||
- **T** — create a temporary blueprint from the current selection and enter its placement mode (REQ-UI-BLUEPRINT-TEMP).
|
||||
- **Escape** — opens the escape menu (REQ-UI-GAME-MENU).
|
||||
- **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.
|
||||
@@ -475,6 +482,8 @@ 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-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-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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
set(TARGET_BASE_NAME "DotaFactory")
|
||||
set(TARGET_BASE_NAME "${PRODUCT_NAME}")
|
||||
|
||||
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
||||
set(TARGET_LIB_NAME "${TARGET_BASE_NAME}_lib")
|
||||
@@ -183,6 +183,19 @@ target_compile_definitions(${TARGET_APP_NAME} PRIVATE
|
||||
)
|
||||
target_link_libraries(${TARGET_APP_NAME} ${TARGET_UI_NAME})
|
||||
|
||||
# Embed the Windows version resource so the version shows on the executable's
|
||||
# Details tab (right-click -> Properties). Values come from cmake/version.cmake
|
||||
# (version numbers) and the product identity variables in the top-level
|
||||
# CMakeLists.txt. MSVC compiles the .rc automatically once it is a target source.
|
||||
if (WIN32)
|
||||
configure_file(
|
||||
"${CMAKE_SOURCE_DIR}/cmake/version.rc.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/version.rc"
|
||||
@ONLY
|
||||
)
|
||||
target_sources(${TARGET_APP_NAME} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/version.rc")
|
||||
endif ()
|
||||
|
||||
unset(APP_FILES)
|
||||
unset(RELATIVE_HDRS)
|
||||
unset(RELATIVE_SRCS)
|
||||
|
||||
@@ -22,6 +22,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DemolishModeToggleRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintPlacementRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ExitBlueprintModeRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TemporaryBlueprintRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SpeedChangeRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/LayoutDialogRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionRequestedEvent.h
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "Event.h"
|
||||
|
||||
// Emitted when the player presses the temporary-blueprint hotkey (REQ-UI-BLUEPRINT-TEMP).
|
||||
// Carries no payload: the blueprint is built from the current selection by the receiver.
|
||||
class TemporaryBlueprintRequestedEvent : public Event
|
||||
{
|
||||
};
|
||||
51
src/lib/sim/BuildingConfig.cpp
Normal file
51
src/lib/sim/BuildingConfig.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "BuildingConfig.h"
|
||||
|
||||
#include "BeltSystem.h"
|
||||
#include "Building.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
std::optional<BuildingConfig> readBuildingConfig(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;
|
||||
}
|
||||
|
||||
BuildingConfig config;
|
||||
config.type = building ? building->type : site->type;
|
||||
|
||||
const std::string& recipeId = building ? building->recipeId : site->recipeId;
|
||||
if (!recipeId.empty())
|
||||
{
|
||||
config.recipeId = recipeId;
|
||||
}
|
||||
|
||||
config.shipLayout = building ? building->shipLayout : site->shipLayout;
|
||||
|
||||
if (config.type == BuildingType::Splitter)
|
||||
{
|
||||
config.isSplitter = true;
|
||||
if (building)
|
||||
{
|
||||
// Operational splitter filters live in the BeltSystem, keyed by tile.
|
||||
const std::optional<BeltSystem::SplitterInfo> info =
|
||||
sim.belts().getSplitterInfo(building->anchor);
|
||||
if (info.has_value())
|
||||
{
|
||||
config.splitterFilterA = info->filterA;
|
||||
config.splitterFilterB = info->filterB;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// A site keeps its pre-completion filters on the ConstructionSite.
|
||||
config.splitterFilterA = site->splitterFilterA;
|
||||
config.splitterFilterB = site->splitterFilterB;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
40
src/lib/sim/BuildingConfig.h
Normal file
40
src/lib/sim/BuildingConfig.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "BuildingId.h"
|
||||
#include "BuildingType.h"
|
||||
#include "ItemType.h"
|
||||
#include "ShipLayout.h"
|
||||
|
||||
class Simulation;
|
||||
|
||||
// The user-configurable settings of a single building or construction site: the
|
||||
// selected recipe / ship schematic, the shipyard module layout, and (for
|
||||
// splitters) the two output filters. Shared by the copy-settings gesture
|
||||
// (REQ-BLD-COPY-CONFIG) and blueprint capture (REQ-UI-BLUEPRINT-STORAGE).
|
||||
struct BuildingConfig
|
||||
{
|
||||
BuildingType type = BuildingType::Miner;
|
||||
|
||||
// Selected recipe (Miner / Assembler) or ship schematic id (Shipyard); unset
|
||||
// when nothing is selected.
|
||||
std::optional<std::string> recipeId;
|
||||
|
||||
// Shipyard module layout (REQ-MOD-LAYOUT).
|
||||
std::optional<ShipLayoutConfig> shipLayout;
|
||||
|
||||
// Splitter output filters (empty = accept all). isSplitter distinguishes an
|
||||
// empty-filter splitter (a valid accept-all configuration) from a building
|
||||
// type that has no splitter filters at all.
|
||||
bool isSplitter = false;
|
||||
std::vector<ItemType> splitterFilterA;
|
||||
std::vector<ItemType> splitterFilterB;
|
||||
};
|
||||
|
||||
// Reads the current configuration of the building or construction site identified
|
||||
// by id, handling operational buildings and sites alike. Returns std::nullopt if
|
||||
// no such building or site exists.
|
||||
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id);
|
||||
@@ -419,6 +419,12 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
||||
{
|
||||
if (site.id == id)
|
||||
{
|
||||
// No-op if the recipe is unchanged, so a redundant selection does
|
||||
// not wipe an already-configured ship layout.
|
||||
if (site.recipeId == recipeId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
site.recipeId = recipeId;
|
||||
site.shipLayout = std::nullopt;
|
||||
return;
|
||||
@@ -430,6 +436,12 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
||||
{
|
||||
if (building.id == id)
|
||||
{
|
||||
// No-op if the recipe is unchanged, so a redundant selection does
|
||||
// not wipe an already-configured ship layout or reset buffers.
|
||||
if (building.recipeId == recipeId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
building.recipeId = recipeId;
|
||||
building.shipLayout = std::nullopt;
|
||||
building.inputBuffer.counts.clear();
|
||||
@@ -858,7 +870,16 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
||||
{
|
||||
const Port& p = building.outputPorts[0];
|
||||
const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
|
||||
m_spawnShip(building.recipeId, spawnPos, building.shipLayout);
|
||||
// A shipyard builds exactly what the player configured and
|
||||
// paid for. When no layout is set it produces a bare hull, so
|
||||
// pass an explicit empty layout rather than nullopt: the latter
|
||||
// would make ShipSystem fall back to the schematic's
|
||||
// defaultModules (a wave-only loadout) and yield free weapons.
|
||||
const std::optional<ShipLayoutConfig> layout =
|
||||
building.shipLayout.has_value()
|
||||
? building.shipLayout
|
||||
: std::make_optional<ShipLayoutConfig>();
|
||||
m_spawnShip(building.recipeId, spawnPos, layout);
|
||||
}
|
||||
building.production = std::nullopt;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
||||
@@ -31,6 +32,7 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ReplayPlayer.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TickDriver.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
|
||||
|
||||
133
src/test/BuildingConfigTest.cpp
Normal file
133
src/test/BuildingConfigTest.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
#include "catch.hpp"
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingConfig.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "BuildingType.h"
|
||||
#include "ConfigLoader.h"
|
||||
#include "GameConfig.h"
|
||||
#include "Rotation.h"
|
||||
#include "ShipLayout.h"
|
||||
#include "ShipsConfig.h"
|
||||
#include "Simulation.h"
|
||||
#include "SimulationTestAccess.h"
|
||||
|
||||
// readBuildingConfig underpins the copy-settings gesture (REQ-BLD-COPY-CONFIG):
|
||||
// it extracts a building's recipe / schematic / layout / splitter filters so they
|
||||
// can be stamped onto a same-type building. It reads operational buildings and
|
||||
// construction sites alike.
|
||||
|
||||
namespace
|
||||
{
|
||||
GameConfig loadConfig()
|
||||
{
|
||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
||||
}
|
||||
|
||||
const BuildingDef* findDef(const GameConfig& cfg, BuildingType type)
|
||||
{
|
||||
for (const BuildingDef& def : cfg.buildings.buildings)
|
||||
{
|
||||
if (def.type == type) { return &def; }
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BuildingId placeOperational(Simulation& sim, const GameConfig& cfg,
|
||||
BuildingType type, QPoint anchor)
|
||||
{
|
||||
const BuildingDef* def = findDef(cfg, type);
|
||||
REQUIRE(def != nullptr);
|
||||
return SimulationTestAccess::buildings(sim).placeImmediate(
|
||||
type, def->surfaceMask, anchor, Rotation::East);
|
||||
}
|
||||
|
||||
const ShipDef* findAvailableSchematic(const GameConfig& cfg)
|
||||
{
|
||||
for (const ShipDef& def : cfg.ships.ships)
|
||||
{
|
||||
if (def.unlockAtStationLevel == -1) { return &def; }
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(loadConfig(), 7);
|
||||
|
||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
|
||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
||||
|
||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||
REQUIRE(config.has_value());
|
||||
CHECK(config->type == BuildingType::Miner);
|
||||
REQUIRE(config->recipeId.has_value());
|
||||
CHECK(*config->recipeId == "mine_iron_ore");
|
||||
CHECK_FALSE(config->isSplitter);
|
||||
CHECK_FALSE(config->shipLayout.has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
|
||||
"[copyconfig]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(loadConfig(), 7);
|
||||
|
||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Assembler, QPoint(0, 0));
|
||||
|
||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||
REQUIRE(config.has_value());
|
||||
CHECK(config->type == BuildingType::Assembler);
|
||||
CHECK_FALSE(config->recipeId.has_value()); // nothing to copy
|
||||
CHECK_FALSE(config->isSplitter);
|
||||
}
|
||||
|
||||
TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
|
||||
"[copyconfig]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(loadConfig(), 7);
|
||||
|
||||
const ShipDef* schematic = findAvailableSchematic(cfg);
|
||||
REQUIRE(schematic != nullptr);
|
||||
|
||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0));
|
||||
SimulationTestAccess::buildings(sim).setRecipe(id, schematic->id);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(id, ShipLayoutConfig{});
|
||||
|
||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||
REQUIRE(config.has_value());
|
||||
CHECK(config->type == BuildingType::Shipyard);
|
||||
REQUIRE(config->recipeId.has_value());
|
||||
CHECK(*config->recipeId == schematic->id);
|
||||
CHECK(config->shipLayout.has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
|
||||
{
|
||||
const GameConfig cfg = loadConfig();
|
||||
Simulation sim(loadConfig(), 7);
|
||||
|
||||
// A placed miner enters the construction queue as a site (not yet operational).
|
||||
const BuildingId id =
|
||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East);
|
||||
REQUIRE(id != kInvalidBuildingId);
|
||||
REQUIRE(sim.buildings().findBuilding(id) == nullptr);
|
||||
REQUIRE(sim.buildings().findSite(id) != nullptr);
|
||||
|
||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
||||
|
||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||
REQUIRE(config.has_value());
|
||||
CHECK(config->type == BuildingType::Miner);
|
||||
REQUIRE(config->recipeId.has_value());
|
||||
CHECK(*config->recipeId == "mine_iron_ore");
|
||||
}
|
||||
|
||||
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[copyconfig]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 7);
|
||||
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
|
||||
}
|
||||
@@ -9,6 +9,7 @@ add_files(
|
||||
BeltSystemTest.cpp
|
||||
SurfaceMaskTest.cpp
|
||||
BuildingTest.cpp
|
||||
BuildingConfigTest.cpp
|
||||
ShipTest.cpp
|
||||
ScrapTest.cpp
|
||||
BehaviorSystemTest.cpp
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "ConfigLoader.h"
|
||||
#include "DynamicBodyComponent.h"
|
||||
#include "EntityAdmin.h"
|
||||
#include "FactionComponent.h"
|
||||
#include "GameConfig.h"
|
||||
#include "HealthComponent.h"
|
||||
#include "ItemType.h"
|
||||
@@ -13,6 +14,7 @@
|
||||
#include "ModulesConfig.h"
|
||||
#include "Rotation.h"
|
||||
#include "SensorRangeComponent.h"
|
||||
#include "ShipIdentityComponent.h"
|
||||
#include "ShipLayout.h"
|
||||
#include "ShipStatsCalculator.h"
|
||||
#include "ShipSystem.h"
|
||||
@@ -264,6 +266,50 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
|
||||
CHECK_FALSE(b2->production.has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("Shipyard: builds a bare hull when no layout is configured",
|
||||
"[modules][shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
const ShipDef* def = findSchematic(sim.config(), "interceptor");
|
||||
REQUIRE(def != nullptr);
|
||||
// The schematic carries a weapon in its (wave-only) default loadout. This
|
||||
// test pins that a player shipyard with no configured layout does NOT hand
|
||||
// that weapon out for free: it builds an unarmed bare hull, matching the
|
||||
// base-hull materials it was charged.
|
||||
REQUIRE_FALSE(def->defaultModules.empty());
|
||||
|
||||
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId, "interceptor");
|
||||
// Deliberately no setShipLayout: recipe set, layout left unconfigured.
|
||||
|
||||
// Charge only the base-hull materials (an empty layout adds none).
|
||||
fillMaterials(sim, yardId, *def, ShipLayoutConfig{});
|
||||
|
||||
// Tick through one full production cycle so the ship spawns.
|
||||
const Tick cycleTicks = secondsToTicks(def->schematic.productionTimeSeconds);
|
||||
for (Tick i = 0; i <= cycleTicks; ++i)
|
||||
{
|
||||
sim.tick();
|
||||
}
|
||||
|
||||
// Locate the freshly built player ship.
|
||||
entt::entity built = entt::null;
|
||||
sim.admin().forEach<ShipIdentityComponent, FactionComponent>(
|
||||
[&](entt::entity e, const ShipIdentityComponent& si, const FactionComponent& fac)
|
||||
{
|
||||
if (!fac.isEnemy && si.schematicId == "interceptor") { built = e; }
|
||||
});
|
||||
REQUIRE(sim.admin().isValid(built));
|
||||
|
||||
// Bare hull: the schematic's default weapon must NOT have been installed.
|
||||
const bool hasWeapon =
|
||||
findFirstWeaponChild(sim.admin(), built) != entt::null;
|
||||
CHECK_FALSE(hasWeapon);
|
||||
}
|
||||
|
||||
TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
@@ -292,6 +338,38 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
||||
CHECK_FALSE(b2->shipLayout.has_value());
|
||||
}
|
||||
|
||||
TEST_CASE("Shipyard: setRecipe with unchanged recipe keeps ship layout",
|
||||
"[modules][shipyard]")
|
||||
{
|
||||
Simulation sim(loadConfig(), 42);
|
||||
const BuildingDef* yardDef = findShipyardDef(sim.config());
|
||||
REQUIRE(yardDef != nullptr);
|
||||
|
||||
const BuildingId yardId = placeShipyard(sim, *yardDef);
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
||||
|
||||
ShipLayoutConfig layout;
|
||||
PlacedModule pm;
|
||||
pm.moduleId = "armor_plate";
|
||||
pm.position = QPoint(0, 0);
|
||||
pm.rotation = Rotation::East;
|
||||
layout.placedModules.push_back(pm);
|
||||
SimulationTestAccess::buildings(sim).setShipLayout(yardId, layout);
|
||||
|
||||
const Building* b1 = sim.buildings().findBuilding(yardId);
|
||||
REQUIRE(b1 != nullptr);
|
||||
REQUIRE(b1->shipLayout.has_value());
|
||||
|
||||
// Re-selecting the same recipe must be a no-op and preserve the layout.
|
||||
SimulationTestAccess::buildings(sim).setRecipe(yardId,"interceptor");
|
||||
|
||||
const Building* b2 = sim.buildings().findBuilding(yardId);
|
||||
REQUIRE(b2 != nullptr);
|
||||
REQUIRE(b2->shipLayout.has_value());
|
||||
REQUIRE(b2->shipLayout->placedModules.size() == 1);
|
||||
CHECK(b2->shipLayout->placedModules[0].moduleId == "armor_plate");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Weapon modifier simulation tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "ExitBlueprintModeRequestedEvent.h"
|
||||
|
||||
#include "Building.h"
|
||||
#include "BuildingConfig.h"
|
||||
#include "BuildingSystem.h"
|
||||
#include "Simulation.h"
|
||||
|
||||
@@ -188,20 +189,19 @@ Blueprint BlueprintPanel::createBlueprintFromSelection() const
|
||||
for (const Entry& e : entries)
|
||||
{
|
||||
BlueprintBuilding bb;
|
||||
bb.type = e.building->type;
|
||||
bb.rotation = e.building->rotation;
|
||||
bb.offset = e.building->anchor - center;
|
||||
bb.recipeId = e.building->recipeId;
|
||||
bb.shipLayout = e.building->shipLayout;
|
||||
if (e.building->type == BuildingType::Splitter)
|
||||
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())
|
||||
{
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -339,3 +339,17 @@ void BlueprintPanel::handleEvent(std::shared_ptr<const BlueprintModeExitedEvent>
|
||||
{
|
||||
clearActiveBlueprintButton();
|
||||
}
|
||||
|
||||
void BlueprintPanel::handleEvent(std::shared_ptr<const TemporaryBlueprintRequestedEvent> /*event*/)
|
||||
{
|
||||
// Temporary blueprint (REQ-UI-BLUEPRINT-TEMP): build from the current selection and
|
||||
// enter placement mode without adding it to the list or persisting it. If nothing
|
||||
// player-placeable is selected, do nothing.
|
||||
Blueprint bp = createBlueprintFromSelection();
|
||||
if (bp.buildings.empty()) { return; }
|
||||
|
||||
// No saved blueprint is active while a temporary one is being placed.
|
||||
clearActiveBlueprintButton();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<BlueprintPlacementRequestedEvent>(std::move(bp)));
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "EventHandler.h"
|
||||
#include "GameConfig.h"
|
||||
#include "SelectionChangedEvent.h"
|
||||
#include "TemporaryBlueprintRequestedEvent.h"
|
||||
#include "Tick.h"
|
||||
|
||||
class Simulation;
|
||||
@@ -21,7 +22,8 @@ class QVBoxLayout;
|
||||
class BlueprintPanel : public QWidget,
|
||||
public CombinedEventHandler<BuildingBlocksChangedEvent,
|
||||
SelectionChangedEvent,
|
||||
BlueprintModeExitedEvent>
|
||||
BlueprintModeExitedEvent,
|
||||
TemporaryBlueprintRequestedEvent>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -33,6 +35,7 @@ private:
|
||||
void handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const BlueprintModeExitedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const TemporaryBlueprintRequestedEvent> event) override;
|
||||
|
||||
private slots:
|
||||
void onCreateClicked();
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
#include "EscapeMenuRequestedEvent.h"
|
||||
#include "TracePrintRequestedEvent.h"
|
||||
#include "BuildHotkeyPressedEvent.h"
|
||||
#include "TemporaryBlueprintRequestedEvent.h"
|
||||
#include "BossWaveUpdatedEvent.h"
|
||||
#include "BuilderModeExitedEvent.h"
|
||||
#include "BlueprintModeExitedEvent.h"
|
||||
@@ -270,6 +271,20 @@ void GameWorldView::onFrame()
|
||||
m_activeBeams = std::move(live);
|
||||
}
|
||||
|
||||
// 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
|
||||
// while the game is paused (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||
if (!m_copyConfigFlashes.empty())
|
||||
{
|
||||
std::vector<CopyConfigFlash> live;
|
||||
for (CopyConfigFlash flash : m_copyConfigFlashes)
|
||||
{
|
||||
flash.remainingMs -= elapsed;
|
||||
if (flash.remainingMs > 0) { live.push_back(flash); }
|
||||
}
|
||||
m_copyConfigFlashes = std::move(live);
|
||||
}
|
||||
|
||||
// Apply held scroll
|
||||
{
|
||||
// Pan speed depends on where the view is centered (REQ-UI-SCROLL-SPEED).
|
||||
@@ -383,6 +398,7 @@ void GameWorldView::paintGL()
|
||||
|
||||
drawTiles(painter);
|
||||
drawBuildings(painter);
|
||||
drawCopyConfigFeedback(painter);
|
||||
drawStations(painter);
|
||||
drawBeltItems(painter);
|
||||
drawScrap(painter);
|
||||
@@ -997,6 +1013,29 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
||||
drawSelectionHighlights(painter);
|
||||
}
|
||||
|
||||
std::optional<QRectF> GameWorldView::footprintWidgetRect(BuildingId id) const
|
||||
{
|
||||
std::optional<QPoint> anchor;
|
||||
std::optional<QSize> footprint;
|
||||
|
||||
if (const Building* b = m_sim->buildings().findBuilding(id))
|
||||
{
|
||||
anchor = b->anchor;
|
||||
footprint = b->footprint;
|
||||
}
|
||||
else if (const ConstructionSite* s = m_sim->buildings().findSite(id))
|
||||
{
|
||||
anchor = s->anchor;
|
||||
footprint = s->footprint;
|
||||
}
|
||||
if (!anchor.has_value() || !footprint.has_value()) { return std::nullopt; }
|
||||
|
||||
const QPointF tl = tileToWidget(*anchor);
|
||||
return QRectF(tl.x(), tl.y(),
|
||||
footprint->width() * static_cast<qreal>(tilePx()),
|
||||
footprint->height() * static_cast<qreal>(tilePx()));
|
||||
}
|
||||
|
||||
void GameWorldView::drawSelectionHighlights(QPainter& painter)
|
||||
{
|
||||
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
|
||||
@@ -1004,26 +1043,47 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter)
|
||||
|
||||
for (BuildingId selId : m_selectedBuildingIds)
|
||||
{
|
||||
std::optional<QPoint> anchor;
|
||||
std::optional<QSize> footprint;
|
||||
const std::optional<QRectF> rect = footprintWidgetRect(selId);
|
||||
if (!rect.has_value()) { continue; }
|
||||
// Outline sits 1px outside the footprint (into adjacent tiles).
|
||||
painter.drawRect(rect->adjusted(-1, -1, 1, 1));
|
||||
}
|
||||
}
|
||||
|
||||
if (const Building* b = m_sim->buildings().findBuilding(selId))
|
||||
{
|
||||
anchor = b->anchor;
|
||||
footprint = b->footprint;
|
||||
}
|
||||
else if (const ConstructionSite* s = m_sim->buildings().findSite(selId))
|
||||
{
|
||||
anchor = s->anchor;
|
||||
footprint = s->footprint;
|
||||
}
|
||||
if (!anchor.has_value() || !footprint.has_value()) { continue; }
|
||||
void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
|
||||
{
|
||||
const QColor color = m_visuals->overlays.copyConfig;
|
||||
|
||||
const QPointF tl = tileToWidget(*anchor);
|
||||
const QRectF bboxRect(tl.x(), tl.y(),
|
||||
footprint->width() * static_cast<qreal>(tilePx()),
|
||||
footprint->height() * static_cast<qreal>(tilePx()));
|
||||
painter.drawRect(bboxRect.adjusted(-1, -1, 1, 1));
|
||||
// Eligible-target tint: while a configuration is cached, every same-type
|
||||
// building and site (the source included) is a valid paste target and is
|
||||
// washed in the copy-settings color (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||
if (m_copiedConfig.has_value())
|
||||
{
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.setBrush(color);
|
||||
const BuildingType type = m_copiedConfig->type;
|
||||
for (const Building& b : m_sim->buildings().allBuildings())
|
||||
{
|
||||
if (b.type != type) { continue; }
|
||||
const std::optional<QRectF> rect = footprintWidgetRect(b.id);
|
||||
if (rect.has_value()) { painter.drawRect(*rect); }
|
||||
}
|
||||
for (const ConstructionSite& s : m_sim->buildings().allSites())
|
||||
{
|
||||
if (s.type != type) { continue; }
|
||||
const std::optional<QRectF> rect = footprintWidgetRect(s.id);
|
||||
if (rect.has_value()) { painter.drawRect(*rect); }
|
||||
}
|
||||
}
|
||||
|
||||
// Copy / paste flashes: a brief outline in the same color, drawn like the
|
||||
// selection outline (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||
painter.setPen(QPen(color, 2));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
for (const CopyConfigFlash& flash : m_copyConfigFlashes)
|
||||
{
|
||||
const std::optional<QRectF> rect = footprintWidgetRect(flash.id);
|
||||
if (rect.has_value()) { painter.drawRect(rect->adjusted(-1, -1, 1, 1)); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1582,6 +1642,13 @@ void GameWorldView::keyPressEvent(QKeyEvent* event)
|
||||
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
|
||||
else { toggleDemolishMode(); }
|
||||
break;
|
||||
case Qt::Key_T:
|
||||
// Request a temporary blueprint from the current selection (REQ-UI-BLUEPRINT-TEMP).
|
||||
// The BlueprintPanel owns the selection and blueprint-capture logic; it decides
|
||||
// whether anything placeable is selected and drives placement mode from there.
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<TemporaryBlueprintRequestedEvent>());
|
||||
break;
|
||||
case Qt::Key_Escape:
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<EscapeMenuRequestedEvent>());
|
||||
@@ -1609,6 +1676,8 @@ void GameWorldView::keyReleaseEvent(QKeyEvent* event)
|
||||
}
|
||||
if (event->key() == Qt::Key_A) { m_scrollLeft = false; }
|
||||
if (event->key() == Qt::Key_D) { m_scrollRight = false; }
|
||||
// Releasing Shift discards the copied building settings (REQ-BLD-COPY-CONFIG).
|
||||
if (event->key() == Qt::Key_Shift) { m_copiedConfig.reset(); }
|
||||
QOpenGLWidget::keyReleaseEvent(event);
|
||||
}
|
||||
|
||||
@@ -1621,6 +1690,15 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
if (m_builderType.has_value()) { exitBuilderMode(); }
|
||||
else if (m_blueprintMode.has_value()) { exitBlueprintMode(); }
|
||||
else if (m_demolishMode) { toggleDemolishMode(); }
|
||||
else if (event->modifiers() & Qt::ShiftModifier)
|
||||
{
|
||||
// Shift + right-click copies a building's settings, but only in the
|
||||
// default selection mode (REQ-BLD-COPY-CONFIG).
|
||||
const QPoint tile = widgetToTile(event->pos());
|
||||
BuildingId id = buildingAtTile(tile);
|
||||
if (id == kInvalidBuildingId) { id = siteAtTile(tile); }
|
||||
if (id != kInvalidBuildingId) { copyConfigFrom(id); }
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1655,6 +1733,20 @@ void GameWorldView::mousePressEvent(QMouseEvent* event)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Shift + left-click applies the copied settings to a same-type building
|
||||
// (REQ-BLD-COPY-CONFIG). Consumes the click so it does not change the
|
||||
// selection. Only active in the default selection mode.
|
||||
if ((event->modifiers() & Qt::ShiftModifier) && m_copiedConfig.has_value())
|
||||
{
|
||||
BuildingId id = buildingAtTile(tile);
|
||||
if (id == kInvalidBuildingId) { id = siteAtTile(tile); }
|
||||
if (id != kInvalidBuildingId)
|
||||
{
|
||||
pasteConfigTo(id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const QVector2D worldPos = widgetToWorld(event->pos());
|
||||
const entt::entity hitEntity = entityAtWorldPos(m_sim->admin(), worldPos);
|
||||
|
||||
@@ -1860,6 +1952,85 @@ void GameWorldView::rotateGhost(bool clockwise)
|
||||
}
|
||||
}
|
||||
|
||||
void GameWorldView::copyConfigFrom(BuildingId id)
|
||||
{
|
||||
const std::optional<BuildingConfig> config = readBuildingConfig(*m_sim, id);
|
||||
if (!config.has_value()) { return; }
|
||||
|
||||
// Only cache when there is something to copy: a selected recipe / schematic
|
||||
// (Miner, Assembler, Shipyard) or any Splitter (empty filters = accept-all).
|
||||
// Building types with no settings (Smelter, Reprocessing Plant, Salvage Bay,
|
||||
// belts / tunnels, HQ) leave any existing cache untouched (REQ-BLD-COPY-CONFIG).
|
||||
if (!config->recipeId.has_value() && !config->isSplitter) { return; }
|
||||
|
||||
m_copiedConfig = config;
|
||||
m_copyConfigFlashes.push_back({ id, kCopyFlashDurationMs });
|
||||
}
|
||||
|
||||
void GameWorldView::pasteConfigTo(BuildingId id)
|
||||
{
|
||||
if (!m_copiedConfig.has_value()) { return; }
|
||||
|
||||
const std::optional<BuildingConfig> target = readBuildingConfig(*m_sim, id);
|
||||
if (!target.has_value() || target->type != m_copiedConfig->type) { return; }
|
||||
|
||||
// The paste applies below; flash the target to confirm (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||
m_copyConfigFlashes.push_back({ id, kCopyFlashDurationMs });
|
||||
|
||||
const BuildingConfig& source = *m_copiedConfig;
|
||||
|
||||
// The cached settings were valid on a same-type source building, so they are
|
||||
// valid and available on the target: unlock state is global, so a selected
|
||||
// recipe / schematic (REQ-LOCK-UI-RECIPE, REQ-LOCK-UI-SCHEMATIC) and splitter
|
||||
// filter item types (REQ-LOCK-UI-SPLITTER) remain unlocked. Applying reuses the
|
||||
// same configuration commands as the selected building panel, inheriting their
|
||||
// buffer-clearing and mid-cycle-cancel semantics (REQ-MAT-INPUT-BUFFER,
|
||||
// REQ-MAT-OUTPUT-BUFFER, REQ-BLD-SHIPYARD).
|
||||
if (source.isSplitter)
|
||||
{
|
||||
// Operational splitters are configured by tile; sites by BuildingId
|
||||
// (mirrors SelectedBuildingPanel::onSplitterFilterChanged).
|
||||
if (const Building* building = m_sim->buildings().findBuilding(id))
|
||||
{
|
||||
std::shared_ptr<SetSplitterFiltersCommand> command =
|
||||
std::make_shared<SetSplitterFiltersCommand>();
|
||||
command->tile = building->anchor;
|
||||
command->filterA = source.splitterFilterA;
|
||||
command->filterB = source.splitterFilterB;
|
||||
enqueueCommand(command);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::shared_ptr<SetSiteSplitterFiltersCommand> command =
|
||||
std::make_shared<SetSiteSplitterFiltersCommand>();
|
||||
command->id = id;
|
||||
command->filterA = source.splitterFilterA;
|
||||
command->filterB = source.splitterFilterB;
|
||||
enqueueCommand(command);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.recipeId.has_value())
|
||||
{
|
||||
std::shared_ptr<SetRecipeCommand> command = std::make_shared<SetRecipeCommand>();
|
||||
command->id = id;
|
||||
command->recipeId = *source.recipeId;
|
||||
enqueueCommand(command);
|
||||
}
|
||||
|
||||
// For a shipyard the schematic (above) must be applied before its module
|
||||
// layout, so both commands drain in order at the next tick boundary.
|
||||
if (source.type == BuildingType::Shipyard && source.shipLayout.has_value())
|
||||
{
|
||||
std::shared_ptr<SetShipLayoutCommand> command =
|
||||
std::make_shared<SetShipLayoutCommand>();
|
||||
command->id = id;
|
||||
command->layout = *source.shipLayout;
|
||||
enqueueCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
void GameWorldView::enterBuilderMode(BuildingType type)
|
||||
{
|
||||
m_builderType = type;
|
||||
@@ -1932,6 +2103,8 @@ void GameWorldView::resetForNewGame()
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<DemolishModeChangedEvent>(false));
|
||||
m_selectedBuildingIds.clear();
|
||||
m_copiedConfig = std::nullopt;
|
||||
m_copyConfigFlashes.clear();
|
||||
m_boxSelecting = false;
|
||||
m_scrollXTiles = 0.0f;
|
||||
m_scrollLeft = false;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <QVector2D>
|
||||
|
||||
#include "Blueprint.h"
|
||||
#include "BuildingConfig.h"
|
||||
#include "BlueprintModeExitedEvent.h"
|
||||
#include "BlueprintPlacementRequestedEvent.h"
|
||||
#include "BuilderModeExitedEvent.h"
|
||||
@@ -116,6 +117,7 @@ private:
|
||||
void drawTiles(QPainter& painter);
|
||||
void drawBuildings(QPainter& painter);
|
||||
void drawSelectionHighlights(QPainter& painter);
|
||||
void drawCopyConfigFeedback(QPainter& painter);
|
||||
void drawStations(QPainter& painter);
|
||||
void drawBeltItems(QPainter& painter);
|
||||
void drawScrap(QPainter& painter);
|
||||
@@ -137,6 +139,10 @@ private:
|
||||
QPoint widgetToTile(QPoint widgetPt) const;
|
||||
QRectF tileRect(QPoint tile) const;
|
||||
QRect viewportRect() const;
|
||||
// Widget-space rectangle covering a building or construction site's footprint,
|
||||
// or nullopt if the id resolves to neither. Shared by the selection highlight
|
||||
// and the copy-settings feedback (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||
std::optional<QRectF> footprintWidgetRect(BuildingId id) const;
|
||||
|
||||
float asteroidLeftEdge() const;
|
||||
float enemyStationRightEdge() const;
|
||||
@@ -165,6 +171,12 @@ private:
|
||||
void stepSpeed(int delta);
|
||||
void placeAtTile(QPoint tile);
|
||||
|
||||
// Copy-settings gesture (REQ-BLD-COPY-CONFIG): Shift+right-click copies a
|
||||
// building's configuration into m_copiedConfig; Shift+left-click applies it to
|
||||
// another building of the same type via the existing configuration commands.
|
||||
void copyConfigFrom(BuildingId id);
|
||||
void pasteConfigTo(BuildingId id);
|
||||
|
||||
void enterBuilderMode(BuildingType type);
|
||||
void exitBuilderMode();
|
||||
void enterBlueprintMode(Blueprint blueprint);
|
||||
@@ -215,6 +227,22 @@ private:
|
||||
std::optional<Blueprint> m_blueprintMode;
|
||||
QPoint m_blueprintGhostTile;
|
||||
|
||||
// Temporary cache for the copy-settings gesture (REQ-BLD-COPY-CONFIG); held
|
||||
// only while Shift is down and cleared on Shift release.
|
||||
std::optional<BuildingConfig> m_copiedConfig;
|
||||
|
||||
// Brief outline flash shown on a building when settings are copied from it or
|
||||
// pasted onto it (REQ-BLD-COPY-CONFIG-FEEDBACK). remainingMs counts down in
|
||||
// wall-clock time so the flash plays at a fixed length regardless of game speed
|
||||
// (and while paused).
|
||||
struct CopyConfigFlash
|
||||
{
|
||||
BuildingId id;
|
||||
qint64 remainingMs;
|
||||
};
|
||||
std::vector<CopyConfigFlash> m_copyConfigFlashes;
|
||||
static constexpr qint64 kCopyFlashDurationMs = 300;
|
||||
|
||||
bool m_demolishMode;
|
||||
BuildingId m_demolishHoverBuildingId;
|
||||
bool m_debugDraw;
|
||||
|
||||
@@ -48,6 +48,7 @@ struct OverlayVisuals
|
||||
QColor selectionRect;
|
||||
QColor tileHighlight;
|
||||
QColor selectedOutline;
|
||||
QColor copyConfig;
|
||||
};
|
||||
|
||||
struct ToastVisuals
|
||||
|
||||
@@ -224,6 +224,7 @@ VisualsConfig VisualsLoader::load(const std::string& path)
|
||||
cfg.overlays.selectionRect = parseColor(requireString(ov, "selection_rect", "overlays"), "overlays.selection_rect");
|
||||
cfg.overlays.tileHighlight = parseColor(requireString(ov, "tile_highlight", "overlays"), "overlays.tile_highlight");
|
||||
cfg.overlays.selectedOutline = parseColor(requireString(ov, "selected_outline", "overlays"), "overlays.selected_outline");
|
||||
cfg.overlays.copyConfig = parseColor(requireString(ov, "copy_config", "overlays"), "overlays.copy_config");
|
||||
}
|
||||
|
||||
// Toast
|
||||
|
||||
Reference in New Issue
Block a user