Implement REQ-UI-PORT-TARGET-GLYPH builder-ghost port preview

While in builder mode, draw an additional directional glyph centered in
each output port's target cell (the cell just outside the footprint the
port pushes into), previewing where output will flow before placement.

- drawPortGlyph gains a `centered` flag so it can render either offset
  toward the port body's exit edge (existing on-tile glyph) or centered
  in a given tile (the new target cell).
- drawBuildingGhost gains `showPortTargetGlyphs`, passed true only for the
  single-building builder ghost and false for the blueprint-placement
  ghost, so the preview is builder-mode only.
- The Tunnel Entry is excluded — it receives items from any non-mouth edge
  rather than emitting into one adjacent cell.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
2026-07-19 22:09:50 +02:00
parent ae3ce1b40c
commit 9f390db80c
2 changed files with 42 additions and 17 deletions

View File

@@ -895,25 +895,31 @@ void GameWorldView::placeAtTile(QPoint tile)
// Port glyph helper
// ---------------------------------------------------------------------------
void GameWorldView::drawPortGlyph(QPainter& painter, QPoint bodyTile,
Rotation direction, const QColor& color)
void GameWorldView::drawPortGlyph(QPainter& painter, QPoint tile,
Rotation direction, const QColor& color,
bool centered)
{
const float px = getTilePx();
const QRectF tr = tileRect(bodyTile);
const QRectF tr = tileRect(tile);
const QPointF center(tr.x() + static_cast<qreal>(px) * 0.5,
tr.y() + static_cast<qreal>(px) * 0.5);
QPointF offset;
QPointF edgeOffset;
const char* ch;
switch (direction)
{
case Rotation::East: offset = QPointF(px * 0.25f, 0); ch = ">"; break;
case Rotation::West: offset = QPointF(-px * 0.25f, 0); ch = "<"; break;
case Rotation::North: offset = QPointF(0, -px * 0.25f); ch = "^"; break;
case Rotation::South: offset = QPointF(0, px * 0.25f); ch = "v"; break;
case Rotation::East: edgeOffset = QPointF(px * 0.25f, 0); ch = ">"; break;
case Rotation::West: edgeOffset = QPointF(-px * 0.25f, 0); ch = "<"; break;
case Rotation::North: edgeOffset = QPointF(0, -px * 0.25f); ch = "^"; break;
case Rotation::South: edgeOffset = QPointF(0, px * 0.25f); ch = "v"; break;
default: return;
}
// Centered glyphs sit in the middle of the tile (used for a port's target
// cell, REQ-UI-PORT-TARGET-GLYPH); otherwise offset toward the exit edge of
// the port's body tile (REQ-UI-PORT-GLYPH).
const QPointF offset = centered ? QPointF(0.0, 0.0) : edgeOffset;
const qreal half = static_cast<qreal>(px) * 0.3;
const QPointF pos = center + offset;
const QRectF textRect(pos.x() - half, pos.y() - half, half * 2.0, half * 2.0);
@@ -991,7 +997,7 @@ void GameWorldView::drawBuildings(QPainter& painter)
for (const Port& port : b.outputPorts)
{
drawPortGlyph(painter, portBodyTile(port.tile, port.direction),
port.direction, bv.outline);
port.direction, bv.outline, /*centered*/ false);
}
// HP bar below the HQ footprint; the HQ's HP lives on its proxy entity.
@@ -1070,7 +1076,8 @@ void GameWorldView::drawBuildings(QPainter& painter)
{
const QPoint absBody = s.anchor
+ portBodyTile(port.tile, port.direction);
drawPortGlyph(painter, absBody, port.direction, bv.outline);
drawPortGlyph(painter, absBody, port.direction, bv.outline,
/*centered*/ false);
}
}
}
@@ -1571,7 +1578,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
if (m_builderType.has_value())
{
drawBuildingGhost(painter, *m_builderType, m_ghostTile,
m_ghostRotation, m_ghostValid);
m_ghostRotation, m_ghostValid,
/*showPortTargetGlyphs*/ true);
}
// Blueprint placement ghost
@@ -1581,7 +1589,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
{
const QPoint anchor = m_blueprintGhostTile + bb.offset;
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
drawBuildingGhost(painter, bb.type, anchor, bb.rotation, valid);
drawBuildingGhost(painter, bb.type, anchor, bb.rotation, valid,
/*showPortTargetGlyphs*/ false);
}
}
@@ -1634,7 +1643,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
QPoint anchorTile, Rotation rotation,
bool valid)
bool valid, bool showPortTargetGlyphs)
{
const BuildingDef* def = findBuildingDef(type);
if (!def) { return; }
@@ -1688,7 +1697,21 @@ void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
for (const Port& port : parsed.outputPorts)
{
drawPortGlyph(painter, anchorTile + portBodyTile(port.tile, port.direction),
port.direction, lineColor);
port.direction, lineColor, /*centered*/ false);
}
// REQ-UI-PORT-TARGET-GLYPH: while in builder mode, additionally mark each
// output port's target cell (the cell just outside the footprint the port
// pushes into) with a directional glyph, previewing where output will flow.
// The Tunnel Entry is excluded — it receives items from any of its non-mouth
// edges rather than emitting into a single adjacent cell.
if (showPortTargetGlyphs && type != BuildingType::TunnelEntry)
{
for (const Port& port : parsed.outputPorts)
{
drawPortGlyph(painter, anchorTile + port.tile, port.direction,
lineColor, /*centered*/ true);
}
}
painter.setOpacity(1.0);

View File

@@ -175,11 +175,13 @@ private:
std::vector<BuildingId> buildingsInBox(QPoint cornerA, QPoint cornerB) const;
QVector2D widgetToWorld(QPoint widgetPt) const;
void drawPortGlyph(QPainter& painter, QPoint bodyTile,
Rotation direction, const QColor& color);
void drawPortGlyph(QPainter& painter, QPoint tile,
Rotation direction, const QColor& color,
bool centered);
void drawBuildingGhost(QPainter& painter, BuildingType type,
QPoint anchorTile, Rotation rotation, bool valid);
QPoint anchorTile, Rotation rotation, bool valid,
bool showPortTargetGlyphs);
void placeBlueprintAtTile(QPoint center);