implement visual feedback for copy building settings with shift + click gesture
This commit is contained in:
@@ -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