share the world shapes both views (game and balancing) draw identically

This commit is contained in:
2026-08-05 20:47:18 +02:00
parent 26d7448492
commit 3a1951559d
8 changed files with 198 additions and 102 deletions

View File

@@ -30,6 +30,7 @@
#include "ShipIdentityComponent.h"
#include "StationBodyComponent.h"
#include "DebrisComponent.h"
#include "WorldPrimitives.h"
namespace
{
@@ -312,14 +313,10 @@ void ArenaView::drawBuildings(QPainter& painter, const WorldCoordinates& coordin
void ArenaView::drawDebris(QPainter& painter, const WorldCoordinates& coordinates)
{
const float r = coordinates.getTilePx() * 0.2f;
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
{
const QPointF center = coordinates.worldToWidget(debris.position);
painter.setBrush(QColor(128, 110, 90));
painter.setPen(QPen(QColor(50, 40, 30), 1));
painter.drawEllipse(center,
static_cast<qreal>(r), static_cast<qreal>(r));
drawDebrisMarker(painter, coordinates,
coordinates.worldToWidget(debris.position));
}
}
@@ -353,14 +350,9 @@ void ArenaView::drawStations(QPainter& painter, const WorldCoordinates& coordina
if (h.maxHp > 0.0f)
{
const float fraction = std::max(0.0f, h.hp / h.maxHp);
const qreal barH = static_cast<qreal>(coordinates.getTilePx()) * 0.12;
const qreal barY = bboxRect.bottom() + 1.0;
const qreal barW = bboxRect.width();
painter.fillRect(QRectF(bboxRect.left(), barY, barW, barH),
QColor(60, 60, 60));
painter.fillRect(QRectF(bboxRect.left(), barY, barW * static_cast<qreal>(fraction), barH),
f.isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60));
drawHealthBar(painter, coordinates, bboxRect.left(),
bboxRect.bottom() + 1.0, bboxRect.width(),
h.hp / h.maxHp, f.isEnemy);
}
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
@@ -374,6 +366,8 @@ void ArenaView::drawStations(QPainter& painter, const WorldCoordinates& coordina
void ArenaView::drawShips(QPainter& painter, const WorldCoordinates& coordinates)
{
const float forward = getShipForwardExtentPx(coordinates);
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, HealthComponent>(
[&](entt::entity e, const ShipIdentityComponent& si,
@@ -385,34 +379,16 @@ void ArenaView::drawShips(QPainter& painter, const WorldCoordinates& coordinates
if (it == m_visuals->ships.end()) { return; }
const QPointF center = coordinates.worldToWidget(pos.value);
const QVector2D dir(std::cos(facing.radians), std::sin(facing.radians));
const QVector2D perp(-dir.y(), dir.x());
const float fwd = coordinates.getTilePx() * 0.45f;
const float side = coordinates.getTilePx() * 0.25f;
QPolygonF tri;
tri << QPointF(center.x() + static_cast<qreal>(dir.x() * fwd),
center.y() + static_cast<qreal>(dir.y() * fwd))
<< QPointF(center.x() + static_cast<qreal>(perp.x() * side - dir.x() * side),
center.y() + static_cast<qreal>(perp.y() * side - dir.y() * side))
<< QPointF(center.x() + static_cast<qreal>(-perp.x() * side - dir.x() * side),
center.y() + static_cast<qreal>(-perp.y() * side - dir.y() * side));
painter.setPen(QPen(it->second.outline, 1));
painter.setBrush(it->second.fill);
painter.drawPolygon(tri);
drawShipBody(painter, coordinates, center, facing.radians,
it->second.fill, it->second.outline);
if (h.maxHp > 0.0f)
{
const float fraction = std::max(0.0f, h.hp / h.maxHp);
const qreal barW = static_cast<qreal>(fwd) * 2.0;
const qreal barH = static_cast<qreal>(coordinates.getTilePx()) * 0.12;
const qreal barX = center.x() - static_cast<qreal>(fwd);
const qreal barY = center.y() + static_cast<qreal>(fwd) + 1.0;
painter.fillRect(QRectF(barX, barY, barW, barH), QColor(60, 60, 60));
painter.fillRect(QRectF(barX, barY, barW * static_cast<qreal>(fraction), barH),
fac.isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60));
const qreal barW = static_cast<qreal>(forward) * 2.0;
const qreal barX = center.x() - static_cast<qreal>(forward);
const qreal barY = center.y() + static_cast<qreal>(forward) + 1.0;
drawHealthBar(painter, coordinates, barX, barY, barW,
h.hp / h.maxHp, fac.isEnemy);
}
if (m_selectedEntity.has_value() && *m_selectedEntity == e)
@@ -428,7 +404,6 @@ void ArenaView::drawShips(QPainter& painter, const WorldCoordinates& coordinates
void ArenaView::drawDebugSensorRanges(QPainter& painter,
const WorldCoordinates& coordinates)
{
painter.setBrush(Qt::NoBrush);
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, SensorRangeComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
const PositionComponent& pos, const SensorRangeComponent& sensor)
@@ -437,13 +412,9 @@ void ArenaView::drawDebugSensorRanges(QPainter& painter,
m_visuals->ships.find(si.schematicId);
if (it == m_visuals->ships.end()) { return; }
const QPointF center = coordinates.worldToWidget(pos.value);
const qreal radiusPx = static_cast<qreal>(sensor.value_tiles)
* static_cast<qreal>(coordinates.getTilePx());
QColor circleColor = it->second.outline;
circleColor.setAlpha(77);
painter.setPen(QPen(circleColor, 1));
painter.drawEllipse(center, radiusPx, radiusPx);
drawSensorRange(painter, coordinates,
coordinates.worldToWidget(pos.value),
sensor.value_tiles, it->second.outline);
});
}

View File

@@ -9,6 +9,10 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.h
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsConfig.h
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.h
# Shared world-space shapes so the arena keeps looking like the game
# (see WorldPrimitives.h). The balancing target does not link the ui library,
# so the few ui files it needs are compiled into it, as above.
${CMAKE_CURRENT_SOURCE_DIR}/../ui/WorldPrimitives.h
PARENT_SCOPE
)
@@ -23,5 +27,6 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/InspectWindow.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../ui/ShipStatsPanel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../ui/WorldPrimitives.cpp
PARENT_SCOPE
)

View File

@@ -7,6 +7,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/ModalPauseScope.h
${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.h
${CMAKE_CURRENT_SOURCE_DIR}/InputMapper.h
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.h
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.h
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
@@ -30,6 +31,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/ModalDimOverlay.cpp
${CMAKE_CURRENT_SOURCE_DIR}/GameWorldView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/InputMapper.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.cpp
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonGrid.cpp
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp

View File

@@ -66,6 +66,7 @@
#include "StationBodyComponent.h"
#include "DebrisComponent.h"
#include "SurfaceMask.h"
#include "WorldPrimitives.h"
#include "Tick.h"
#include "TunnelCompletion.h"
#include "EscapeMenuRequestedEvent.h"
@@ -1331,7 +1332,7 @@ void GameWorldView::drawBuildings(QPainter& painter, const WorldCoordinates& coo
{
if (h.maxHp > 0.0f)
{
drawHpBar(painter, coordinates,
drawHealthBar(painter, coordinates,
bboxRect.left(), bboxRect.bottom() + 1.0,
bboxRect.width(), h.hp / h.maxHp, f.isEnemy);
}
@@ -1384,11 +1385,11 @@ void GameWorldView::drawSelectionHighlights(QPainter& painter,
}
// A ring around each selected piece of debris, sitting just outside the debris's
// rendered circle (radius getTilePx()*0.2, matching drawDebris) (REQ-UI-DEBRIS-CLICK-SELECT).
// own rendered circle (REQ-UI-DEBRIS-CLICK-SELECT).
if (!m_selection.getSelectedDebris().empty())
{
const qreal outlineRadius =
static_cast<qreal>(coordinates.getTilePx() * 0.2f) + 3.0;
static_cast<qreal>(getDebrisRadiusPx(coordinates)) + 3.0;
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
{
if (!m_selection.isDebrisSelected(debris.entity)) { continue; }
@@ -1534,14 +1535,10 @@ void GameWorldView::drawBeltItems(QPainter& painter, const WorldCoordinates& coo
void GameWorldView::drawDebris(QPainter& painter, const WorldCoordinates& coordinates)
{
const float r = coordinates.getTilePx() * 0.2f;
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
{
const QPointF center = coordinates.worldToWidget(debris.position);
painter.setBrush(QColor(128, 110, 90));
painter.setPen(QPen(QColor(50, 40, 30), 1));
painter.drawEllipse(center,
static_cast<qreal>(r), static_cast<qreal>(r));
drawDebrisMarker(painter, coordinates,
coordinates.worldToWidget(debris.position));
}
}
@@ -1589,7 +1586,7 @@ void GameWorldView::drawStations(QPainter& painter, const WorldCoordinates& coor
// HP bar below footprint.
if (h.maxHp > 0.0f)
{
drawHpBar(painter, coordinates,
drawHealthBar(painter, coordinates,
bboxRect.left(), bboxRect.bottom() + 1.0,
bboxRect.width(), h.hp / h.maxHp, f.isEnemy);
}
@@ -1598,6 +1595,8 @@ void GameWorldView::drawStations(QPainter& painter, const WorldCoordinates& coor
void GameWorldView::drawShips(QPainter& painter, const WorldCoordinates& coordinates)
{
const float forward = getShipForwardExtentPx(coordinates);
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, HealthComponent>(
[&](entt::entity e, const ShipIdentityComponent& si,
@@ -1609,58 +1608,31 @@ void GameWorldView::drawShips(QPainter& painter, const WorldCoordinates& coordin
if (it == m_visuals->ships.end()) { return; }
const QPointF center = coordinates.worldToWidget(pos.value);
const QVector2D dir(std::cos(facing.radians), std::sin(facing.radians));
const QVector2D perp(-dir.y(), dir.x());
const float fwd = coordinates.getTilePx() * 0.45f;
const float side = coordinates.getTilePx() * 0.25f;
QPolygonF tri;
tri << QPointF(center.x() + static_cast<qreal>(dir.x() * fwd),
center.y() + static_cast<qreal>(dir.y() * fwd))
<< QPointF(center.x() + static_cast<qreal>(perp.x() * side - dir.x() * side),
center.y() + static_cast<qreal>(perp.y() * side - dir.y() * side))
<< QPointF(center.x() + static_cast<qreal>(-perp.x() * side - dir.x() * side),
center.y() + static_cast<qreal>(-perp.y() * side - dir.y() * side));
painter.setPen(QPen(it->second.outline, 1));
painter.setBrush(it->second.fill);
painter.drawPolygon(tri);
drawShipBody(painter, coordinates, center, facing.radians,
it->second.fill, it->second.outline);
if (m_selection.isActorSelected(e))
{
painter.setPen(QPen(m_visuals->overlays.selectedOutline, 2));
painter.setBrush(Qt::NoBrush);
const qreal r = static_cast<qreal>(fwd) + 2.0;
const qreal r = static_cast<qreal>(forward) + 2.0;
painter.drawEllipse(center, r, r);
}
if (h.maxHp > 0.0f)
{
const qreal barW = static_cast<qreal>(fwd) * 2.0;
const qreal barX = center.x() - static_cast<qreal>(fwd);
const qreal barY = center.y() + static_cast<qreal>(fwd) + 1.0;
drawHpBar(painter, coordinates, barX, barY, barW,
h.hp / h.maxHp, fac.isEnemy);
const qreal barW = static_cast<qreal>(forward) * 2.0;
const qreal barX = center.x() - static_cast<qreal>(forward);
const qreal barY = center.y() + static_cast<qreal>(forward) + 1.0;
drawHealthBar(painter, coordinates, barX, barY, barW,
h.hp / h.maxHp, fac.isEnemy);
}
});
}
void GameWorldView::drawHpBar(QPainter& painter, const WorldCoordinates& coordinates,
qreal left, qreal top, qreal width,
float fraction, bool isEnemy)
{
const qreal barH = static_cast<qreal>(coordinates.getTilePx()) * 0.12;
const float clamped = std::max(0.0f, fraction);
painter.fillRect(QRectF(left, top, width, barH), QColor(60, 60, 60));
painter.fillRect(QRectF(left, top, width * static_cast<qreal>(clamped), barH),
isEnemy ? QColor(200, 60, 60) : QColor(60, 200, 60));
}
void GameWorldView::drawDebugSensorRanges(QPainter& painter,
const WorldCoordinates& coordinates)
{
painter.setBrush(Qt::NoBrush);
m_sim->getAdmin().forEach<ShipIdentityComponent, PositionComponent, FacingComponent,
FactionComponent, SensorRangeComponent>(
[&](entt::entity /*e*/, const ShipIdentityComponent& si,
@@ -1671,13 +1643,9 @@ void GameWorldView::drawDebugSensorRanges(QPainter& painter,
m_visuals->ships.find(si.schematicId);
if (it == m_visuals->ships.end()) { return; }
const QPointF center = coordinates.worldToWidget(pos.value);
const qreal radiusPx = static_cast<qreal>(sensor.value_tiles)
* static_cast<qreal>(coordinates.getTilePx());
QColor circleColor = it->second.outline;
circleColor.setAlpha(77);
painter.setPen(QPen(circleColor, 1));
painter.drawEllipse(center, radiusPx, radiusPx);
drawSensorRange(painter, coordinates,
coordinates.worldToWidget(pos.value),
sensor.value_tiles, it->second.outline);
});
}

View File

@@ -176,9 +176,6 @@ private:
QPointF center, float halfPx);
void drawDebris(QPainter& painter, const WorldCoordinates& coordinates);
void drawShips(QPainter& painter, const WorldCoordinates& coordinates);
void drawHpBar(QPainter& painter, const WorldCoordinates& coordinates,
qreal left, qreal top, qreal width,
float fraction, bool isEnemy);
void drawDebugSensorRanges(QPainter& painter, const WorldCoordinates& coordinates);
void drawDebugTargetLines(QPainter& painter, const WorldCoordinates& coordinates);
void drawDebugOverlay(QPainter& painter);

101
src/ui/WorldPrimitives.cpp Normal file
View File

@@ -0,0 +1,101 @@
#include "WorldPrimitives.h"
#include <algorithm>
#include <cmath>
#include <QPainter>
#include <QPen>
#include <QPolygonF>
#include <QRectF>
#include <QVector2D>
namespace
{
// Ship triangle proportions, as fractions of a tile: the nose reaches further
// than the tail corners spread, so the facing direction reads at a glance.
const float kShipForwardTileFactor = 0.45f;
const float kShipSideTileFactor = 0.25f;
const float kDebrisRadiusTileFactor = 0.2f;
// Health bar height as a fraction of a tile.
const qreal kHealthBarHeightTileFactor = 0.12;
const QColor kHealthBarTrack (60, 60, 60);
const QColor kHealthBarEnemy (200, 60, 60);
const QColor kHealthBarPlayer (60, 200, 60);
const QColor kDebrisFill (128, 110, 90);
const QColor kDebrisOutline(50, 40, 30);
// Sensor circles are drawn faint so a crowded field stays readable.
const int kSensorRangeAlpha = 77;
}
float getShipForwardExtentPx(const WorldCoordinates& coordinates)
{
return coordinates.getTilePx() * kShipForwardTileFactor;
}
float getDebrisRadiusPx(const WorldCoordinates& coordinates)
{
return coordinates.getTilePx() * kDebrisRadiusTileFactor;
}
void drawShipBody(QPainter& painter, const WorldCoordinates& coordinates,
QPointF center, float facing_radians,
const QColor& fill, const QColor& outline)
{
const QVector2D direction(std::cos(facing_radians), std::sin(facing_radians));
const QVector2D perpendicular(-direction.y(), direction.x());
const float forward = getShipForwardExtentPx(coordinates);
const float side = coordinates.getTilePx() * kShipSideTileFactor;
QPolygonF triangle;
triangle
<< QPointF(center.x() + static_cast<qreal>(direction.x() * forward),
center.y() + static_cast<qreal>(direction.y() * forward))
<< QPointF(center.x() + static_cast<qreal>(perpendicular.x() * side - direction.x() * side),
center.y() + static_cast<qreal>(perpendicular.y() * side - direction.y() * side))
<< QPointF(center.x() + static_cast<qreal>(-perpendicular.x() * side - direction.x() * side),
center.y() + static_cast<qreal>(-perpendicular.y() * side - direction.y() * side));
painter.setPen(QPen(outline, 1));
painter.setBrush(fill);
painter.drawPolygon(triangle);
}
void drawHealthBar(QPainter& painter, const WorldCoordinates& coordinates,
qreal left, qreal top, qreal width, float fraction, bool isEnemy)
{
const qreal height = static_cast<qreal>(coordinates.getTilePx())
* kHealthBarHeightTileFactor;
const float clamped = std::max(0.0f, fraction);
painter.fillRect(QRectF(left, top, width, height), kHealthBarTrack);
painter.fillRect(QRectF(left, top, width * static_cast<qreal>(clamped), height),
isEnemy ? kHealthBarEnemy : kHealthBarPlayer);
}
void drawDebrisMarker(QPainter& painter, const WorldCoordinates& coordinates,
QPointF center)
{
const qreal radius = static_cast<qreal>(getDebrisRadiusPx(coordinates));
painter.setBrush(kDebrisFill);
painter.setPen(QPen(kDebrisOutline, 1));
painter.drawEllipse(center, radius, radius);
}
void drawSensorRange(QPainter& painter, const WorldCoordinates& coordinates,
QPointF center, float range_tiles, const QColor& shipOutline)
{
const qreal radius = static_cast<qreal>(range_tiles)
* static_cast<qreal>(coordinates.getTilePx());
QColor circleColor = shipOutline;
circleColor.setAlpha(kSensorRangeAlpha);
painter.setPen(QPen(circleColor, 1));
painter.setBrush(Qt::NoBrush);
painter.drawEllipse(center, radius, radius);
}

50
src/ui/WorldPrimitives.h Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
#include <QColor>
#include <QPointF>
#include "WorldCoordinates.h"
class QPainter;
// The handful of world-space shapes the game view and the balancing tool's arena
// view draw identically: a ship, its health bar, a piece of debris, a sensor range.
//
// Shared because the arena exists to eyeball combat, which only works while a ship
// there looks like a ship in the game — if these drift, the balancing tool quietly
// stops representing what it is measuring. Deliberately kept to shapes that are
// genuinely the same in both: the two views differ on selection highlights, beams
// and target lines, and those stay with each view.
//
// Free functions taking explicit values, with no state and no simulation: each
// view keeps its own iteration and layer order. Sizes derive from the tile size so
// everything scales with the view (REQ-GW-TILE-SIZE).
// Distance from a ship's centre to its nose, in pixels. The selection ring and the
// health bar are positioned from this, so callers need it whether or not they are
// the ones drawing the body.
float getShipForwardExtentPx(const WorldCoordinates& coordinates);
// Radius of a piece of debris, in pixels. Shared so the selection ring around
// debris cannot drift from the debris it is meant to circle.
float getDebrisRadiusPx(const WorldCoordinates& coordinates);
// Draws a ship as a triangle at `center` (widget space), pointing along
// `facing_radians`.
void drawShipBody(QPainter& painter, const WorldCoordinates& coordinates,
QPointF center, float facing_radians,
const QColor& fill, const QColor& outline);
// Draws a health bar: a dark track with `fraction` of it filled, red for an enemy
// and green for the player. A negative fraction is clamped to empty.
void drawHealthBar(QPainter& painter, const WorldCoordinates& coordinates,
qreal left, qreal top, qreal width, float fraction, bool isEnemy);
// Draws a piece of debris as a small brown circle at `center` (widget space).
void drawDebrisMarker(QPainter& painter, const WorldCoordinates& coordinates,
QPointF center);
// Draws a ship's sensor range as a faint circle in the ship's own outline colour,
// so overlapping ranges stay distinguishable.
void drawSensorRange(QPainter& painter, const WorldCoordinates& coordinates,
QPointF center, float range_tiles, const QColor& shipOutline);