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/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
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -271,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).
|
||||
@@ -384,6 +398,7 @@ void GameWorldView::paintGL()
|
||||
|
||||
drawTiles(painter);
|
||||
drawBuildings(painter);
|
||||
drawCopyConfigFeedback(painter);
|
||||
drawStations(painter);
|
||||
drawBeltItems(painter);
|
||||
drawScrap(painter);
|
||||
@@ -998,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));
|
||||
@@ -1005,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)); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1905,6 +1964,7 @@ void GameWorldView::copyConfigFrom(BuildingId id)
|
||||
if (!config->recipeId.has_value() && !config->isSplitter) { return; }
|
||||
|
||||
m_copiedConfig = config;
|
||||
m_copyConfigFlashes.push_back({ id, kCopyFlashDurationMs });
|
||||
}
|
||||
|
||||
void GameWorldView::pasteConfigTo(BuildingId id)
|
||||
@@ -1914,6 +1974,9 @@ void GameWorldView::pasteConfigTo(BuildingId id)
|
||||
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
|
||||
@@ -2041,6 +2104,7 @@ void GameWorldView::resetForNewGame()
|
||||
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;
|
||||
|
||||
@@ -117,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);
|
||||
@@ -138,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;
|
||||
@@ -226,6 +231,18 @@ private:
|
||||
// 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