Implement REQ-BLD-COPY-CONFIG-FEEDBACK: copy-settings visual feedback
Add on-screen feedback for the Shift copy-settings gesture, driven by a single configurable color (visuals.toml [overlays].copy_config): - eligible-target tint over every same-type building/site while a config is cached (source included), shown only while a paste is possible; - a brief selection-style outline flash on copy (source) and paste (target). Flashes use a wall-clock countdown (frame delta), so they play for a fixed 0.3 s regardless of game speed and while paused, and fade independently per building. Extract the footprint-rect resolution shared by the selection highlight and the new feedback into footprintWidgetRect(). Pure rendering; no simulation or selection state touched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNNLeFWhVzvxkK9qVXP2K
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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).
|
||||||
if (const Building* b = m_sim->buildings().findBuilding(selId))
|
painter.drawRect(rect->adjusted(-1, -1, 1, 1));
|
||||||
{
|
|
||||||
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; }
|
|
||||||
|
|
||||||
const QPointF tl = tileToWidget(*anchor);
|
void GameWorldView::drawCopyConfigFeedback(QPainter& painter)
|
||||||
const QRectF bboxRect(tl.x(), tl.y(),
|
{
|
||||||
footprint->width() * static_cast<qreal>(tilePx()),
|
const QColor color = m_visuals->overlays.copyConfig;
|
||||||
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; }
|
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