Compare commits
4 Commits
4986c1bac8
...
0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 271855781c | |||
| 5f6ecbf6c8 | |||
| cd966daba9 | |||
| 88bc4f2170 |
@@ -4,10 +4,20 @@ message(STATUS "Using CMake ${CMAKE_VERSION}")
|
|||||||
|
|
||||||
include(cmake/add_files.cmake)
|
include(cmake/add_files.cmake)
|
||||||
include(cmake/create_source_groups.cmake)
|
include(cmake/create_source_groups.cmake)
|
||||||
|
include(cmake/version.cmake)
|
||||||
|
|
||||||
# Project ----------------------------------------------------------------------
|
# 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")
|
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)
|
selection_rect = "#00ff00" # box-drag selection rectangle (REQ-UI-MULTI-SELECT)
|
||||||
tile_highlight = "#ffffff22" # tile under cursor
|
tile_highlight = "#ffffff22" # tile under cursor
|
||||||
selected_outline = "#ffff00" # outline drawn around currently-selected building(s)
|
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)
|
# 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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
set(TARGET_BASE_NAME "DotaFactory")
|
set(TARGET_BASE_NAME "${PRODUCT_NAME}")
|
||||||
|
|
||||||
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
||||||
set(TARGET_LIB_NAME "${TARGET_BASE_NAME}_lib")
|
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})
|
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(APP_FILES)
|
||||||
unset(RELATIVE_HDRS)
|
unset(RELATIVE_HDRS)
|
||||||
unset(RELATIVE_SRCS)
|
unset(RELATIVE_SRCS)
|
||||||
|
|||||||
@@ -419,6 +419,12 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
|||||||
{
|
{
|
||||||
if (site.id == id)
|
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.recipeId = recipeId;
|
||||||
site.shipLayout = std::nullopt;
|
site.shipLayout = std::nullopt;
|
||||||
return;
|
return;
|
||||||
@@ -430,6 +436,12 @@ void BuildingSystem::setRecipe(BuildingId id, const std::string& recipeId)
|
|||||||
{
|
{
|
||||||
if (building.id == id)
|
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.recipeId = recipeId;
|
||||||
building.shipLayout = std::nullopt;
|
building.shipLayout = std::nullopt;
|
||||||
building.inputBuffer.counts.clear();
|
building.inputBuffer.counts.clear();
|
||||||
@@ -858,7 +870,16 @@ void BuildingSystem::tickShipyardProduction(Tick currentTick)
|
|||||||
{
|
{
|
||||||
const Port& p = building.outputPorts[0];
|
const Port& p = building.outputPorts[0];
|
||||||
const QVector2D spawnPos(p.tile.x() + 0.5f, p.tile.y() + 0.5f);
|
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;
|
building.production = std::nullopt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
#include "DynamicBodyComponent.h"
|
#include "DynamicBodyComponent.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
|
#include "FactionComponent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
#include "ItemType.h"
|
#include "ItemType.h"
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
#include "ShipStatsCalculator.h"
|
#include "ShipStatsCalculator.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
@@ -264,6 +266,50 @@ TEST_CASE("Shipyard: setShipLayout cancels in-progress production",
|
|||||||
CHECK_FALSE(b2->production.has_value());
|
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]")
|
TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 42);
|
Simulation sim(loadConfig(), 42);
|
||||||
@@ -292,6 +338,38 @@ TEST_CASE("Shipyard: setRecipe clears ship layout", "[modules][shipyard]")
|
|||||||
CHECK_FALSE(b2->shipLayout.has_value());
|
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
|
// Weapon modifier simulation tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -271,6 +271,20 @@ void GameWorldView::onFrame()
|
|||||||
m_activeBeams = std::move(live);
|
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
|
// Apply held scroll
|
||||||
{
|
{
|
||||||
// Pan speed depends on where the view is centered (REQ-UI-SCROLL-SPEED).
|
// Pan speed depends on where the view is centered (REQ-UI-SCROLL-SPEED).
|
||||||
@@ -384,6 +398,7 @@ void GameWorldView::paintGL()
|
|||||||
|
|
||||||
drawTiles(painter);
|
drawTiles(painter);
|
||||||
drawBuildings(painter);
|
drawBuildings(painter);
|
||||||
|
drawCopyConfigFeedback(painter);
|
||||||
drawStations(painter);
|
drawStations(painter);
|
||||||
drawBeltItems(painter);
|
drawBeltItems(painter);
|
||||||
drawScrap(painter);
|
drawScrap(painter);
|
||||||
@@ -998,6 +1013,29 @@ void GameWorldView::drawBuildings(QPainter& painter)
|
|||||||
drawSelectionHighlights(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)
|
void GameWorldView::drawSelectionHighlights(QPainter& painter)
|
||||||
{
|
{
|
||||||
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
|
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
|
||||||
@@ -1005,26 +1043,47 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter)
|
|||||||
|
|
||||||
for (BuildingId selId : m_selectedBuildingIds)
|
for (BuildingId selId : m_selectedBuildingIds)
|
||||||
{
|
{
|
||||||
std::optional<QPoint> anchor;
|
const std::optional<QRectF> rect = footprintWidgetRect(selId);
|
||||||
std::optional<QSize> footprint;
|
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))
|
void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
|
||||||
{
|
{
|
||||||
anchor = b->anchor;
|
const QColor color = m_visuals->overlays.copyConfig;
|
||||||
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; }
|
|
||||||
|
|
||||||
const QPointF tl = tileToWidget(*anchor);
|
// Eligible-target tint: while a configuration is cached, every same-type
|
||||||
const QRectF bboxRect(tl.x(), tl.y(),
|
// building and site (the source included) is a valid paste target and is
|
||||||
footprint->width() * static_cast<qreal>(tilePx()),
|
// washed in the copy-settings color (REQ-BLD-COPY-CONFIG-FEEDBACK).
|
||||||
footprint->height() * static_cast<qreal>(tilePx()));
|
if (m_copiedConfig.has_value())
|
||||||
painter.drawRect(bboxRect.adjusted(-1, -1, 1, 1));
|
{
|
||||||
|
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)); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1905,6 +1964,7 @@ void GameWorldView::copyConfigFrom(BuildingId id)
|
|||||||
if (!config->recipeId.has_value() && !config->isSplitter) { return; }
|
if (!config->recipeId.has_value() && !config->isSplitter) { return; }
|
||||||
|
|
||||||
m_copiedConfig = config;
|
m_copiedConfig = config;
|
||||||
|
m_copyConfigFlashes.push_back({ id, kCopyFlashDurationMs });
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameWorldView::pasteConfigTo(BuildingId id)
|
void GameWorldView::pasteConfigTo(BuildingId id)
|
||||||
@@ -1914,6 +1974,9 @@ void GameWorldView::pasteConfigTo(BuildingId id)
|
|||||||
const std::optional<BuildingConfig> target = readBuildingConfig(*m_sim, id);
|
const std::optional<BuildingConfig> target = readBuildingConfig(*m_sim, id);
|
||||||
if (!target.has_value() || target->type != m_copiedConfig->type) { return; }
|
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;
|
const BuildingConfig& source = *m_copiedConfig;
|
||||||
|
|
||||||
// The cached settings were valid on a same-type source building, so they are
|
// The cached settings were valid on a same-type source building, so they are
|
||||||
@@ -2041,6 +2104,7 @@ void GameWorldView::resetForNewGame()
|
|||||||
std::make_shared<DemolishModeChangedEvent>(false));
|
std::make_shared<DemolishModeChangedEvent>(false));
|
||||||
m_selectedBuildingIds.clear();
|
m_selectedBuildingIds.clear();
|
||||||
m_copiedConfig = std::nullopt;
|
m_copiedConfig = std::nullopt;
|
||||||
|
m_copyConfigFlashes.clear();
|
||||||
m_boxSelecting = false;
|
m_boxSelecting = false;
|
||||||
m_scrollXTiles = 0.0f;
|
m_scrollXTiles = 0.0f;
|
||||||
m_scrollLeft = false;
|
m_scrollLeft = false;
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ private:
|
|||||||
void drawTiles(QPainter& painter);
|
void drawTiles(QPainter& painter);
|
||||||
void drawBuildings(QPainter& painter);
|
void drawBuildings(QPainter& painter);
|
||||||
void drawSelectionHighlights(QPainter& painter);
|
void drawSelectionHighlights(QPainter& painter);
|
||||||
|
void drawCopyConfigFeedback(QPainter& painter);
|
||||||
void drawStations(QPainter& painter);
|
void drawStations(QPainter& painter);
|
||||||
void drawBeltItems(QPainter& painter);
|
void drawBeltItems(QPainter& painter);
|
||||||
void drawScrap(QPainter& painter);
|
void drawScrap(QPainter& painter);
|
||||||
@@ -138,6 +139,10 @@ private:
|
|||||||
QPoint widgetToTile(QPoint widgetPt) const;
|
QPoint widgetToTile(QPoint widgetPt) const;
|
||||||
QRectF tileRect(QPoint tile) const;
|
QRectF tileRect(QPoint tile) const;
|
||||||
QRect viewportRect() 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 asteroidLeftEdge() const;
|
||||||
float enemyStationRightEdge() const;
|
float enemyStationRightEdge() const;
|
||||||
@@ -226,6 +231,18 @@ private:
|
|||||||
// only while Shift is down and cleared on Shift release.
|
// only while Shift is down and cleared on Shift release.
|
||||||
std::optional<BuildingConfig> m_copiedConfig;
|
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;
|
bool m_demolishMode;
|
||||||
BuildingId m_demolishHoverBuildingId;
|
BuildingId m_demolishHoverBuildingId;
|
||||||
bool m_debugDraw;
|
bool m_debugDraw;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ struct OverlayVisuals
|
|||||||
QColor selectionRect;
|
QColor selectionRect;
|
||||||
QColor tileHighlight;
|
QColor tileHighlight;
|
||||||
QColor selectedOutline;
|
QColor selectedOutline;
|
||||||
|
QColor copyConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ToastVisuals
|
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.selectionRect = parseColor(requireString(ov, "selection_rect", "overlays"), "overlays.selection_rect");
|
||||||
cfg.overlays.tileHighlight = parseColor(requireString(ov, "tile_highlight", "overlays"), "overlays.tile_highlight");
|
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.selectedOutline = parseColor(requireString(ov, "selected_outline", "overlays"), "overlays.selected_outline");
|
||||||
|
cfg.overlays.copyConfig = parseColor(requireString(ov, "copy_config", "overlays"), "overlays.copy_config");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toast
|
// Toast
|
||||||
|
|||||||
Reference in New Issue
Block a user