draw ghost in semi-transparent building color

This commit is contained in:
2026-06-23 21:17:16 +02:00
parent 577927ef70
commit f818c90af0
3 changed files with 71 additions and 38 deletions

View File

@@ -96,10 +96,10 @@ Modules in `modules.toml` define a `surface_mask` — a list of strings that des
- REQ-BLD-QUEUE: Placed buildings enter a construction queue and are built one at a time. Each building takes a duration defined in `buildings.toml [[building]].construction_time_seconds` to construct. - REQ-BLD-QUEUE: Placed buildings enter a construction queue and are built one at a time. Each building takes a duration defined in `buildings.toml [[building]].construction_time_seconds` to construct.
- REQ-BLD-ASTEROID-ONLY: Buildings can only be placed on asteroid tiles (per surface_mask; tiles marked `S` may extend into space). - REQ-BLD-ASTEROID-ONLY: Buildings can only be placed on asteroid tiles (per surface_mask; tiles marked `S` may extend into space).
- REQ-BLD-BUILDER-MODE: Clicking a build button activates builder mode for that building type. Builder mode is exited by right-clicking in the game world or clicking the same build button again. - REQ-BLD-BUILDER-MODE: Clicking a build button activates builder mode for that building type. Builder mode is exited by right-clicking in the game world or clicking the same build button again.
- REQ-BLD-GHOST: While in builder mode, a ghost of the building is rendered at the tile under the cursor, showing where it would be placed. - REQ-BLD-GHOST: While in builder mode, a ghost of the building is rendered at the tile under the cursor, showing where it would be placed. The ghost is drawn semi-transparently in the building type's own visuals — its `fill` and `outline` colors and `glyph` from `visuals.toml` — so that different building types are visually distinguishable in builder mode rather than all looking alike. When the current cursor position is invalid, the ghost instead uses the distinct "invalid" color (REQ-BLD-PLACE-VALID), which overrides the per-building coloring.
- REQ-BLD-ROTATE: While in builder mode, pressing E rotates the ghost 90° clockwise and Q rotates it 90° counter-clockwise. Rotation affects the direction of the output port. - REQ-BLD-ROTATE: While in builder mode, pressing E rotates the ghost 90° clockwise and Q rotates it 90° counter-clockwise. Rotation affects the direction of the output port.
- REQ-BLD-PLACE: Clicking a valid tile in builder mode places a construction site and adds it to the build queue, consuming building blocks from the global stock. - REQ-BLD-PLACE: Clicking a valid tile in builder mode places a construction site and adds it to the build queue, consuming building blocks from the global stock.
- REQ-BLD-PLACE-VALID: A placement position is valid only if (a) every footprint cell in the rotated `surface_mask` is satisfied by the underlying terrain — `A` cells coincide with asteroid tiles, `S` cells coincide with space tiles — (b) no footprint cell overlaps an existing placed building or construction site, except as allowed by REQ-BLD-ROTATE-IN-PLACE, and (c) the player has enough building blocks to afford the building. The ghost (REQ-BLD-GHOST) is rendered in a distinct "invalid" color when the current cursor position fails any of these conditions. - REQ-BLD-PLACE-VALID: A placement position is valid only if (a) every footprint cell in the rotated `surface_mask` is satisfied by the underlying terrain — `A` cells coincide with asteroid tiles, `S` cells coincide with space tiles — (b) no footprint cell overlaps an existing placed building or construction site, except as allowed by REQ-BLD-ROTATE-IN-PLACE, and (c) the player has enough building blocks to afford the building. The ghost (REQ-BLD-GHOST) is rendered in a distinct "invalid" color — overriding its per-building coloring (REQ-BLD-GHOST) — when the current cursor position fails any of these conditions.
- REQ-BLD-ROTATE-IN-PLACE: If the ghost's footprint exactly coincides with the footprint of an existing placed building or construction site of the same building type, clicking places no new construction site and consumes no building blocks. Instead, the existing building or site is rotated to match the ghost's rotation. If the target is a construction site, its construction progress is preserved. This applies in both normal builder mode and blueprint placement mode; in blueprint placement mode it is evaluated per building in the blueprint independently — buildings in the blueprint whose footprint coincides with an existing same-type building or site are rotated in place, while the remaining buildings in the blueprint are placed as normal construction sites (subject to the usual validity checks and total cost). - REQ-BLD-ROTATE-IN-PLACE: If the ghost's footprint exactly coincides with the footprint of an existing placed building or construction site of the same building type, clicking places no new construction site and consumes no building blocks. Instead, the existing building or site is rotated to match the ghost's rotation. If the target is a construction site, its construction progress is preserved. This applies in both normal builder mode and blueprint placement mode; in blueprint placement mode it is evaluated per building in the blueprint independently — buildings in the blueprint whose footprint coincides with an existing same-type building or site are rotated in place, while the remaining buildings in the blueprint are placed as normal construction sites (subject to the usual validity checks and total cost).
- REQ-BLD-BELT-DRAG: For belts, the player can click and drag across multiple tiles to place a construction site on each tile in one gesture. - REQ-BLD-BELT-DRAG: For belts, the player can click and drag across multiple tiles to place a construction site on each tile in one gesture.
- REQ-BLD-TUNNEL-AUTO-SWITCH: After the player successfully places a Tunnel Entry construction site, builder mode automatically switches to Tunnel Exit (and vice versa), preserving the current ghost rotation. This makes it easy to immediately place the paired end without manually selecting the complementary type. - REQ-BLD-TUNNEL-AUTO-SWITCH: After the player successfully places a Tunnel Entry construction site, builder mode automatically switches to Tunnel Exit (and vice versa), preserving the current ghost rotation. This makes it easy to immediately place the paired end without manually selecting the complementary type.
@@ -459,7 +459,7 @@ The screen is divided into two columns: a main column (75% width) containing the
- REQ-UI-BLUEPRINT-BUTTON: Each blueprint entry consists of a blueprint button and a dedicated delete icon ("×") placed to the right of the button. The blueprint button displays the blueprint name and, below it, the total building block cost of the blueprint (sum of the individual costs of all constituent buildings). A blueprint button is disabled when the player cannot afford the total cost. Clicking an enabled blueprint button enters blueprint placement mode for that blueprint. The delete icon is always enabled regardless of whether the player can afford the blueprint. - REQ-UI-BLUEPRINT-BUTTON: Each blueprint entry consists of a blueprint button and a dedicated delete icon ("×") placed to the right of the button. The blueprint button displays the blueprint name and, below it, the total building block cost of the blueprint (sum of the individual costs of all constituent buildings). A blueprint button is disabled when the player cannot afford the total cost. Clicking an enabled blueprint button enters blueprint placement mode for that blueprint. The delete icon is always enabled regardless of whether the player can afford the blueprint.
- REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). Pressing Q/E rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint. - REQ-UI-BLUEPRINT-MODE: In blueprint placement mode a ghost is rendered for every building in the blueprint at the position determined by its stored tile offset from the bounding-box center, which is anchored to the tile under the cursor. Each ghost is rendered individually as valid or invalid, applying REQ-BLD-PLACE-VALID conditions (a) and (b) per building (the other ghosts in the same blueprint do not count as existing buildings for the overlap check). A valid ghost uses its building type's semi-transparent per-building coloring (REQ-BLD-GHOST); an invalid ghost uses the distinct "invalid" color, as in single-building builder mode. Pressing Q/E rotates the entire constellation 90° counter-clockwise / clockwise: each building's tile offset is rotated around the bounding-box center and each building's own rotation is updated, consistent with REQ-BLD-ROTATE. Blueprint placement mode is exited by right-clicking in the game world. Clicking a different blueprint button exits the current mode and enters blueprint placement mode for the newly clicked blueprint.
- REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. If splitter output filters are stored, they are applied to the construction site immediately and carry over when it finishes building (REQ-BLD-SITE-CONFIG). Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately. - REQ-UI-BLUEPRINT-PLACE: Left-clicking in blueprint placement mode places the blueprint if (a) every building in the constellation satisfies REQ-BLD-PLACE-VALID conditions (a) and (b) at its resolved tile, and (b) the player has enough building blocks to afford the total cost. If both conditions are met, a construction site is added to the build queue for each building in the blueprint and the full total cost is deducted from the global building blocks stock in one transaction. If a recipe ID is stored for a building, it is applied to the construction site immediately. If a schematic ID is stored, it is applied only if that schematic is currently unlocked; if it is not unlocked, the shipyard's schematic is left unset. If splitter output filters are stored, they are applied to the construction site immediately and carry over when it finishes building (REQ-BLD-SITE-CONFIG). Locked recipe IDs and splitter filter entries for locked item types are handled on placement per REQ-LOCK-UI-BLUEPRINT. After a successful placement the game remains in blueprint placement mode, allowing the player to place the same blueprint again immediately.

View File

@@ -1086,25 +1086,8 @@ void GameWorldView::drawOverlays(QPainter& painter)
// Builder-mode ghost // Builder-mode ghost
if (m_builderType.has_value()) if (m_builderType.has_value())
{ {
const BuildingDef* def = findBuildingDef(*m_builderType); drawBuildingGhost(painter, *m_builderType, m_ghostTile,
if (def) m_ghostRotation, m_ghostValid);
{
const ParsedSurfaceMask parsed =
parseSurfaceMask(def->surfaceMask, m_ghostRotation);
const QColor& ghostColor = m_ghostValid
? m_visuals->overlays.ghostValid
: m_visuals->overlays.ghostInvalid;
for (const QPoint& cell : parsed.bodyCells)
{
painter.fillRect(tileRect(m_ghostTile + cell), ghostColor);
}
for (const Port& port : parsed.outputPorts)
{
const QPoint absBody = m_ghostTile
+ portBodyTile(port.tile, port.direction);
drawPortGlyph(painter, absBody, port.direction, Qt::white);
}
}
} }
// Blueprint placement ghost // Blueprint placement ghost
@@ -1114,22 +1097,7 @@ void GameWorldView::drawOverlays(QPainter& painter)
{ {
const QPoint anchor = m_blueprintGhostTile + bb.offset; const QPoint anchor = m_blueprintGhostTile + bb.offset;
const bool valid = isValidPlacement(bb.type, anchor, bb.rotation); const bool valid = isValidPlacement(bb.type, anchor, bb.rotation);
const QColor& ghostColor = valid drawBuildingGhost(painter, bb.type, anchor, bb.rotation, valid);
? m_visuals->overlays.ghostValid
: m_visuals->overlays.ghostInvalid;
const BuildingDef* def = findBuildingDef(bb.type);
if (!def) { continue; }
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, bb.rotation);
for (const QPoint& cell : parsed.bodyCells)
{
painter.fillRect(tileRect(anchor + cell), ghostColor);
}
for (const Port& port : parsed.outputPorts)
{
drawPortGlyph(painter,
anchor + portBodyTile(port.tile, port.direction),
port.direction, Qt::white);
}
} }
} }
@@ -1160,6 +1128,68 @@ void GameWorldView::drawOverlays(QPainter& painter)
} }
} }
void GameWorldView::drawBuildingGhost(QPainter& painter, BuildingType type,
QPoint anchorTile, Rotation rotation,
bool valid)
{
const BuildingDef* def = findBuildingDef(type);
if (!def) { return; }
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
m_visuals->buildings.find(type);
if (it == m_visuals->buildings.end()) { return; }
const BuildingVisuals& bv = it->second;
// Valid ghosts show the building type's own colors; invalid ghosts override
// with the distinct invalid color (REQ-BLD-GHOST, REQ-BLD-PLACE-VALID). The
// invalid color's RGB is taken at full opacity so it does not double-dim
// against the setOpacity below (the configured color carries its own alpha).
const QColor invalidColor(m_visuals->overlays.ghostInvalid.red(),
m_visuals->overlays.ghostInvalid.green(),
m_visuals->overlays.ghostInvalid.blue());
const QColor fillColor = valid ? bv.fill : invalidColor;
const QColor lineColor = valid ? bv.outline : invalidColor;
const ParsedSurfaceMask parsed = parseSurfaceMask(def->surfaceMask, rotation);
if (parsed.bodyCells.empty()) { return; }
painter.setOpacity(0.5);
QPoint minCell = parsed.bodyCells.front();
QPoint maxCell = parsed.bodyCells.front();
for (const QPoint& cell : parsed.bodyCells)
{
painter.fillRect(tileRect(anchorTile + cell), fillColor);
minCell.setX(std::min(minCell.x(), cell.x()));
minCell.setY(std::min(minCell.y(), cell.y()));
maxCell.setX(std::max(maxCell.x(), cell.x()));
maxCell.setY(std::max(maxCell.y(), cell.y()));
}
const QPointF tl = tileToWidget(anchorTile + minCell);
const QRectF bboxRect(tl.x(), tl.y(),
(maxCell.x() - minCell.x() + 1) * static_cast<qreal>(tilePx()),
(maxCell.y() - minCell.y() + 1) * static_cast<qreal>(tilePx()));
painter.setPen(QPen(lineColor, 1));
painter.setBrush(Qt::NoBrush);
painter.drawRect(bboxRect);
if (!bv.glyph.isEmpty())
{
painter.setPen(lineColor);
painter.drawText(bboxRect, Qt::AlignCenter, bv.glyph);
}
for (const Port& port : parsed.outputPorts)
{
drawPortGlyph(painter, anchorTile + portBodyTile(port.tile, port.direction),
port.direction, lineColor);
}
painter.setOpacity(1.0);
}
void GameWorldView::drawScreenSpace(QPainter& /*painter*/) void GameWorldView::drawScreenSpace(QPainter& /*painter*/)
{ {
} }

View File

@@ -126,6 +126,9 @@ private:
void drawPortGlyph(QPainter& painter, QPoint bodyTile, void drawPortGlyph(QPainter& painter, QPoint bodyTile,
Rotation direction, const QColor& color); Rotation direction, const QColor& color);
void drawBuildingGhost(QPainter& painter, BuildingType type,
QPoint anchorTile, Rotation rotation, bool valid);
void placeBlueprintAtTile(QPoint center); void placeBlueprintAtTile(QPoint center);
std::optional<QVector2D> entityPosition(entt::entity entity) const; std::optional<QVector2D> entityPosition(entt::entity entity) const;