3 Commits

7 changed files with 112 additions and 3 deletions

View File

@@ -491,6 +491,14 @@ The screen is divided into two columns: a main column (75% width) containing the
- REQ-UI-BELT-CLEAR: When one or more belt, splitter, tunnel entry, or tunnel exit tiles are selected, the panel shows a "Clear" button that removes all items from the selected tiles. Clearing a tunnel entry or exit also discards all items currently in transit through that tunnel (REQ-BLD-TUNNEL-TRANSIT). This can be used to resolve stalled belts, splitters, and tunnels.
- REQ-UI-ENTITY-CLICK-SELECT: The player can click any ship (player or enemy) or any defence station (player or enemy) in the game world to select it. Clicking a ship or defence station clears any existing selection and establishes a single-entity selection containing only that entity. Ships and defence stations cannot participate in multi-select together with buildings. Clicking a scrap pile instead establishes a scrap selection (REQ-UI-SCRAP-CLICK-SELECT). Clicking empty world space (no building, ship, defence station, or scrap pile) clears the selection.
- REQ-UI-SHIP-STATS-PANEL: When a single ship is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **ship stats panel**. The panel structure mirrors REQ-MOD-UI-STATS-PANEL but reflects the ship's actual live state: stats are computed from its installed modules per REQ-MOD-STAT-CALC. The panel always shows all hull stats: HP (current / maximum), max linear speed, sensor range, main acceleration, maneuvering acceleration, angular acceleration, and max rotation speed. In addition, capability module summaries are shown conditioned on which module types are installed, using the same aggregation rules as REQ-MOD-UI-STATS-PANEL: weapons (combined DPS, maximum range), salvage (combined collection rate, maximum range), and repair (combined repair rate, maximum range), each section appearing only if at least one instance of that module type is installed. While debug draw mode is active (REQ-UI-DEBUG-DRAW), the panel additionally shows the ship's derived threat cost (REQ-MOD-THREAT).
- REQ-UI-SHIP-BEHAVIOR: The ship stats panel (REQ-UI-SHIP-STATS-PANEL) additionally displays the selected ship's **current behavior** — a single label naming the top-priority behavior currently governing the ship's navigation, as resolved by the fixed-priority behavior arbitration. Only the winning behavior is named; lower-priority behaviors that are suppressed are not shown, and neither are the salvage/repair cycles that run regardless of the active behavior (REQ-SHP-SALVAGE, REQ-SHP-REPAIR). The label updates live as the ship's behavior changes, and it is always shown (independent of debug draw mode, unlike the threat-cost line of REQ-UI-SHIP-STATS-PANEL). This applies to both player and enemy ships (REQ-UI-ENTITY-CLICK-SELECT); enemy ships only ever show **Engaging** or **Advancing**. The behavior labels (all wrapped in `tr()`) are:
- **Retreating** — the ship is retreating (REQ-SHP-RETREAT).
- **Engaging** — the ship is engaging a combat target (player: REQ-SHP-COMBAT; enemy: REQ-SHP-ENEMY-AI).
- **Salvaging** — the ship is executing salvage navigation: seeking scrap, collecting, or delivering to a Salvage Bay (REQ-SHP-SALVAGE).
- **Repairing** — the ship is navigating to a repair target (REQ-SHP-REPAIR).
- **Rallying** — the ship is moving to or orbiting the rally point (REQ-SHP-RALLY).
- **Standby** — the ship is holding with its fleet (REQ-SHP-STANDBY).
- **Advancing** — the ship is executing the baseline forward advance with no higher-priority behavior active (player: REQ-SHP-COMBAT advance toward the enemy; enemy: REQ-SHP-ENEMY-AI advance toward the asteroid).
- REQ-UI-STATION-STATS-PANEL: When a single defence station is selected (REQ-UI-ENTITY-CLICK-SELECT), the selected building panel shows a **station stats panel** displaying the station's stats computed at its current level: HP (current / maximum), damage, range, and fire rate.
- REQ-UI-SCRAP-CLICK-SELECT: The player can click any scrap pile (REQ-RES-SCRAP-DROP) in the game world to select it. Scrap forms its own selection category: clicking a scrap pile clears any existing selection and establishes a scrap selection containing only that pile. Scrap piles cannot participate in a selection together with buildings, ships, or defence stations. Hit-testing prioritizes actors over scrap: when a building, ship, or defence station lies under the cursor at the same point as a scrap pile, that object is selected in preference to the scrap; a scrap pile is selected only when no building, ship, or defence station is under the cursor. A selected scrap pile that despawns or is fully collected (REQ-RES-SCRAP-DROP) is removed from the selection; once the last selected pile is gone, the selection becomes empty (REQ-UI-EMPTY-SELECTION).
- REQ-UI-SCRAP-MULTI-SELECT: Multiple scrap piles can be selected by box-drag or by Ctrl+clicking individual piles to add or remove them from the selection, mirroring building multi-select (REQ-UI-MULTI-SELECT). A scrap selection contains only scrap piles. Because scrap cannot mix with other object types (REQ-UI-SCRAP-CLICK-SELECT), Ctrl+clicking a scrap pile while a building, ship, or defence station selection is active first clears that selection and begins a scrap selection; conversely, selecting a building, ship, or defence station while a scrap selection is active clears the scrap selection. Box-drag disambiguation between buildings and scrap follows REQ-UI-MULTI-SELECT (a box covering any building selects buildings; a box covering scrap but no buildings selects the scrap).

View File

@@ -14,6 +14,7 @@
#include "HealthComponent.h"
#include "InspectWindowClosedEvent.h"
#include "ModuleOwnerComponent.h"
#include "SelectedBehaviorComponent.h"
#include "ShipIdentityComponent.h"
#include "ShipStatsCalculator.h"
#include "ShipStatsPanel.h"
@@ -278,6 +279,8 @@ void InspectWindow::handleEvent(std::shared_ptr<const EntitySelectedEvent> event
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
m_entityStatsPanel->refreshFromLive(stats, health.hp);
m_entityStatsPanel->setBehavior(
admin.get<SelectedBehaviorComponent>(entity).winner);
m_entityStatsPanel->show();
m_stationStatsLabel->hide();
}
@@ -355,6 +358,8 @@ void InspectWindow::refreshEntityStats()
{
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
m_entityStatsPanel->refreshFromLive(stats, health.hp);
m_entityStatsPanel->setBehavior(
admin.get<SelectedBehaviorComponent>(entity).winner);
}
else if (admin.hasAll<StationBodyComponent>(entity))
{

View File

@@ -15,11 +15,13 @@
#include <QDir>
#include <QFont>
#include <QKeyEvent>
#include <QLinearGradient>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QPen>
#include <QPolygonF>
#include <QRadialGradient>
#include <QRegion>
#include <QStringList>
#include <QTimer>
@@ -1487,6 +1489,9 @@ void GameWorldView::drawDebugOverlay(QPainter& painter)
void GameWorldView::drawBeams(QPainter& painter)
{
const QPainter::RenderHints savedHints = painter.renderHints();
painter.setRenderHint(QPainter::Antialiasing, true);
for (const ActiveBeam& beam : m_activeBeams)
{
const std::optional<QVector2D> shooterPos = entityPosition(beam.event.shooter);
@@ -1500,10 +1505,55 @@ void GameWorldView::drawBeams(QPainter& painter)
case BeamKind::Repair: color = m_visuals->beams.repairColor; break;
case BeamKind::Salvage: color = m_visuals->beams.salvageColor; break;
}
painter.setPen(QPen(color, m_visuals->beams.widthPx));
painter.drawLine(worldToWidget(*shooterPos),
worldToWidget(*targetPos + beam.targetOffset));
const QPointF s = worldToWidget(*shooterPos);
const QPointF t = worldToWidget(*targetPos + beam.targetOffset);
// Unit direction/perpendicular of the beam in widget space. A degenerate
// zero-length beam (shooter and target coincide) has no direction to
// taper along, so skip it.
const QVector2D delta(static_cast<float>(t.x() - s.x()),
static_cast<float>(t.y() - s.y()));
const float lengthPx = delta.length();
if (lengthPx < 0.001f) { continue; }
const QVector2D dir = delta / lengthPx;
const QVector2D perp(-dir.y(), dir.x());
// Directional taper: draw the beam as a quad that is wide at the shooter
// and narrows to a faint tip at the target, so it reads as an arrow
// pointing away from whoever fired it. Without this, a beam strung
// between two nearby ships is symmetric and gives no cue which end is the
// source (the readability problem this addresses).
const float widthPx = std::max(1.0f, static_cast<float>(m_visuals->beams.widthPx));
const float baseHalf = widthPx * 1.f;
const float tipHalf = widthPx * 0.35f;
QPolygonF quad;
quad << QPointF(s.x() + static_cast<qreal>(perp.x() * baseHalf),
s.y() + static_cast<qreal>(perp.y() * baseHalf))
<< QPointF(s.x() - static_cast<qreal>(perp.x() * baseHalf),
s.y() - static_cast<qreal>(perp.y() * baseHalf))
<< QPointF(t.x() - static_cast<qreal>(perp.x() * tipHalf),
t.y() - static_cast<qreal>(perp.y() * tipHalf))
<< QPointF(t.x() + static_cast<qreal>(perp.x() * tipHalf),
t.y() + static_cast<qreal>(perp.y() * tipHalf));
QColor bright = color;
bright.setAlpha(255);
QColor faint = color;
faint.setAlpha(90);
QLinearGradient bodyGrad(s, t);
bodyGrad.setColorAt(0.0, bright);
bodyGrad.setColorAt(1.0, faint);
painter.setPen(Qt::NoPen);
painter.setBrush(bodyGrad);
painter.drawPolygon(quad);
}
painter.setBrush(Qt::NoBrush);
painter.setRenderHints(savedHints);
}
void GameWorldView::drawOverlays(QPainter& painter)

View File

@@ -44,6 +44,7 @@ QString shipTooltip(const ShipDef& def)
{
lines << itemLine(material.item, material.amount);
}
lines << QObject::tr(" + installed modules");
}
lines << QObject::tr("Completion time: %1 s")

View File

@@ -21,6 +21,7 @@
#include "FactionComponent.h"
#include "HealthComponent.h"
#include "ModuleOwnerComponent.h"
#include "SelectedBehaviorComponent.h"
#include "ShipIdentityComponent.h"
#include "ShipStatsCalculator.h"
#include "ShipStatsPanel.h"
@@ -953,6 +954,8 @@ void SelectedBuildingPanel::buildEntityShip(entt::entity entity)
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
m_entityStatsPanel->refreshFromLive(stats, health.hp);
m_entityStatsPanel->setBehavior(
admin.get<SelectedBehaviorComponent>(entity).winner);
m_entityStatsPanel->setDebugDrawEnabled(m_debugDraw);
for (const ShipDef& def : m_config->ships.ships)
@@ -1036,6 +1039,8 @@ void SelectedBuildingPanel::refreshEntityStats()
{
const ShipStats stats = buildShipStatsFromEntity(admin, entity);
m_entityStatsPanel->refreshFromLive(stats, health.hp);
m_entityStatsPanel->setBehavior(
admin.get<SelectedBehaviorComponent>(entity).winner);
}
else if (admin.hasAll<StationBodyComponent>(entity))
{

View File

@@ -107,6 +107,12 @@ ShipStatsPanel::ShipStatsPanel(const GameConfig* config, QWidget* parent)
m_repairSection->setVisible(false);
layout->addWidget(m_repairSection);
// Current behavior — live entities only; hidden in the static design
// preview (REQ-UI-SHIP-BEHAVIOR).
m_behaviorLabel = makeSectionHeader(QString(), this);
m_behaviorLabel->setVisible(false);
layout->addWidget(m_behaviorLabel);
// Threat cost — debug-only, initially hidden.
m_threatCostLabel = makeStatLabel(this);
m_threatCostLabel->setVisible(false);
@@ -125,6 +131,9 @@ void ShipStatsPanel::refresh(const std::string& shipId,
const double threat = calculateShipThreatCost(m_config->threatCosts, *m_config,
shipId, modules);
setThreatCost(threat);
// The static design preview has no live behavior to show.
m_behaviorLabel->setVisible(false);
}
void ShipStatsPanel::refreshFromLive(const ShipStats& stats, float currentHp)
@@ -204,6 +213,32 @@ void ShipStatsPanel::applyStats(const ShipStats& stats, const QString& hpText)
}
}
void ShipStatsPanel::setBehavior(BehaviorKind kind)
{
QString label;
switch (kind)
{
case BehaviorKind::Retreat: label = tr("Retreating"); break;
case BehaviorKind::Attack: label = tr("Engaging"); break;
case BehaviorKind::SalvageScrap:
case BehaviorKind::DeliverScrap: label = tr("Salvaging"); break;
case BehaviorKind::Repair: label = tr("Repairing"); break;
case BehaviorKind::Rally: label = tr("Rallying"); break;
case BehaviorKind::Standby: label = tr("Standby"); break;
case BehaviorKind::Advance: label = tr("Advancing"); break;
case BehaviorKind::None: break;
}
if (label.isEmpty())
{
m_behaviorLabel->setVisible(false);
return;
}
m_behaviorLabel->setText(tr("Behavior: %1").arg(label));
m_behaviorLabel->setVisible(true);
}
void ShipStatsPanel::setThreatCost(double cost)
{
m_threatCostLabel->setText(tr("Threat Cost: %1").arg(cost, 0, 'f', 1));

View File

@@ -6,6 +6,7 @@
#include <QWidget>
#include "BehaviorKind.h"
#include "ShipLayout.h"
#include "ShipStatsCalculator.h"
@@ -24,6 +25,9 @@ public:
void refreshFromLive(const ShipStats& stats, float currentHp);
// Displays the ship's current top-priority behavior (REQ-UI-SHIP-BEHAVIOR).
void setBehavior(BehaviorKind kind);
void setThreatCost(double cost);
void setDebugDrawEnabled(bool enabled);
@@ -33,6 +37,7 @@ private:
const GameConfig* m_config;
bool m_debugDraw = false;
QLabel* m_behaviorLabel;
QLabel* m_hpLabel;
QLabel* m_speedLabel;
QLabel* m_sensorRangeLabel;