Commit Graph

23 Commits

Author SHA1 Message Date
3eec462b4a move the debug stats panel out of the renderer
It was the one thing in WorldRenderer positioned in pixels rather than tiles,
kept there only to preserve its draw order. With the order agreed not to matter,
the world-space / screen-space split becomes exact.

The real payoff is translation. drawDebugOverlay was the only caller of tr() in
the renderer, so Q_DECLARE_TR_FUNCTIONS and the QCoreApplication include go with
it. Nothing left in the renderer draws translatable text - its text is
config-driven glyphs, ASCII port arrows and numbers - so it no longer needs a
tie to the meta-object system at all. That is the argument I should have weighed
originally instead of anchoring on draw order.

The panel now paints on top of ships and beams rather than under them, and the
widget gates it on its own m_debugDraw directly. The frame still carries
isDebugDrawEnabled, because the sensor ranges and target lines it also gates are
genuinely world-space.

Dropped the painter.resetTransform() the function opened with: it was vestigial
from the older design that drew the world through painter.translate, and has
been a no-op since every conversion became explicit arithmetic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-05 21:23:38 +02:00
9aed6d844e extract WorldRenderer
The last seam of the decomposition, and the one the earlier ones were groundwork
for: every draw method already took a WorldCoordinates, read mode state through
BuildModeController and selection through SelectionController, and used the
shared shapes in WorldPrimitives, so the move needed almost no rewriting.

paintGL is now a call sequence. The split is the world-space / screen-space line
already drawn by the WorldCoordinates work: the renderer draws everything
positioned in tiles, while the pause and deconstruct vignettes and the replay
overlay - which never took a WorldCoordinates because they are anchored to the
viewport - stay with the widget.

WorldRenderFrame is what keeps the renderer independent of the widget. It reads
the simulation directly, but the rest of what it draws is interaction state the
widget owns: the selection, the active build mode, live beams, the copy-settings
feedback, the box-select rectangle. Those are gathered per frame and passed by
reference, so the renderer holds no copy a later click could invalidate, and it
knows nothing about input.

Three more queries had to stop belonging to the view first, because the renderer
and the click path both need them: buildingsInBox and collectTunnelTiles move to
FactoryQueries, and makeTunnelLookup with the TunnelTileMap and QPointCompare it
needs move to TunnelCompletion, which already owned that concept.

Two small things fell out of leaving QWidget. The port-item clip region used
QWidget::rect() and now takes the painter's own viewport. drawDebugOverlay puts
translated text on screen, so the renderer declares tr() via
Q_DECLARE_TR_FUNCTIONS rather than becoming a QObject. drawDebugOverlay stays in
the renderer despite being screen-anchored: it is drawn mid-sequence, so moving
it out would put it on top of the ships instead of under them.

The simulation reference is non-const only because EntityAdmin's component
accessors are; the renderer never writes it.

Draw order is unchanged, and so is behaviour. GameWorldView.cpp is 1431 lines,
from 3265 when this branch started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-05 19:09:40 +02:00
286116d2b1 share the world shapes both views draw identically
Groundwork for the WorldRenderer extraction: lift the shapes GameWorldView and
ArenaView already draw the same way, before writing a renderer around them.

Four of them turned out to be identical down to the constants - the ship
triangle, the health bar, the debris marker and the sensor-range circle. The
health bar was written three times: once in GameWorldView::drawHpBar and inlined
twice in ArenaView, for stations and for ships.

The argument for sharing is not de-duplication - it is about fifty lines. It is
that the arena exists to eyeball combat, so it only does its job while a ship
there looks like a ship in the game. Retuning the ship shape and having the
balancing tool silently keep the old one is a quiet way to make the tool lie.

Deliberately narrow. The two views differ on selection highlights, beams, target
lines, and all of the factory rendering, and those stay where they are; the
arena has already diverged on some of that and nobody minded, which is a reason
to keep the shared set to shapes that are genuinely the same rather than to
aspire to more. Free functions over explicit values, no state and no simulation,
so each view keeps its own iteration and layer order.

The balancing target does not link the ui library, so it compiles
WorldPrimitives into itself - the mechanism already used for VisualsLoader and
ShipStatsPanel.

Two shared getters come with it: the ship's forward extent, which the selection
ring and health bar are positioned from, and the debris radius, which the debris
selection ring previously tracked via a comment saying "matching drawDebris".

Behaviour is unchanged. WorldRenderer is next and will be built on these.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-05 17:56:50 +02:00
b0fffdb00f extract the scroll position into WorldCamera
Second seam of the GameWorldView decomposition: the view no longer owns a scroll
position, only the pan intent and the bounds.

WorldCamera works purely in world units — tiles and tiles per second, never
pixels. That is what keeps it independent of WorldCoordinates: the two meet only
where GameWorldView feeds getViewCenterXTiles() into the transform, and neither
knows the other exists.

Two things are passed in rather than reached for, both so the camera stays a
plain value with no simulation dependency:

- ScrollBounds, because the pan limits move with asteroid expansion and with
  pushes. The camera clamps on every advance(), not only when panning, so the
  view follows the bounds inward when they shrink.
- PanDirection, because pan intent is not the camera's business. Today
  GameWorldView collapses its two held-key flags into it; if controls become
  rebindable the camera's interface does not change.

The config structs are referenced, not copied: they live inside the Simulation's
GameConfig, which is assigned in place on restart (REQ-CFG-RELOAD), so reloaded
scroll tuning takes effect without rebuilding the camera. A test pins that.

The pan-speed curve (REQ-UI-SCROLL-SPEED) had no coverage at all and is the
least obvious code in the file — two ramps combined by min, with a peak below
the fast speed where the bands overlap in a narrow contest zone, and a hard step
when the band width is zero. All of that is now tested.

Behaviour is unchanged, including the cases worth naming: holding both keys
still cancels out, and the moved/not-moved result that drives the box-select
refresh still counts movement caused purely by the bounds changing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-05 13:15:49 +02:00
8ec413e1b9 use WorldCoordinates in ArenaView too
ArenaView carried its own copy of the same five transforms. It differs from the
game view in exactly two respects: the arena is a fixed world shown whole, so
the tile size is the tighter of the two axis fits rather than the height fit,
and there is no scrolling, so the left edge is always 0.

That is small enough to absorb into WorldCoordinates as a second named factory.
The raw constructor becomes private and both call sites go through
scrolling(...) or fitToWorld(...), which also reads better than three
positional ints at the call site.

ArenaView is threaded the same way GameWorldView was: paintGL builds one
snapshot, every draw takes a const WorldCoordinates&, and mousePressEvent takes
its own.

ArenaView::widgetToWorld guarded against a near-zero tile size; that guard now
lives in WorldCoordinates as a tilePx fallback to 1.0, alongside the existing
degenerate-world-size fallback, so both factories are total and no conversion
can divide by zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-05 10:18:52 +02:00
4f6de79352 extract the world<->widget transform into WorldCoordinates
The nine coordinate helpers on GameWorldView were the first seam of the
planned decomposition: stateless math that every draw method, the placement
validation, and the belt-drag code reached into private state to get at.

WorldCoordinates is an immutable value built from the viewport size, the world
height, and the scroll center. It lives in lib/core rather than ui, following
BeltDragPath and TunnelCompletion — UI-only helpers kept there so Catch2 can
reach them. That is what lets the transform math be unit-tested at all, since
the test target links lib only.

Rather than leave delegating one-liners behind, the snapshot is threaded
through the call graph: paintGL builds one per frame, every world-space draw
takes a const WorldCoordinates&, and the screen-space draws (vignettes, replay
overlay, debug text) take none. Those methods no longer touch m_scrollXTiles,
width(), or height(), which is most of the groundwork for extracting
WorldRenderer later. The mouse handlers each take their own snapshot.

The snapshot is deliberately not cached in a member: a resize or a scroll
would silently invalidate it, and a stale transform surfaces as misaligned
hit-testing rather than as a visible failure.

widgetToTile and widgetToWorld had the same arithmetic inlined separately;
the former now goes through the latter.

The architecture doc's coordinate section was already stale — it described a
painter.translate approach and a hardcoded tilePx = 20, neither of which was
true — so it is rewritten to match the code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
2026-08-05 10:04:14 +02:00
64c344c3a3 correct the belt subsystem interface description in architecture.md 2026-08-04 18:12:02 +02:00
8b71fe1a03 Rename ship/station scrap drop entities to "debris" 2026-07-23 20:51:05 +02:00
e20a0bba67 Rename Demolish to Deconstruct 2026-07-22 21:40:42 +02:00
c21af63e84 deselect build tool when it becomes unaffordable and fix stale enabled button 2026-07-19 21:20:59 +02:00
9573b9789a change repair_tool application and add beams for salvager and repair_tool 2026-06-19 21:15:47 +02:00
e8dd73bcb0 refactor AI system 2026-06-15 09:16:56 +02:00
5317f35198 switch to using own event system 2026-06-13 17:52:22 +02:00
49f7129bd5 schematic selection dialog 2026-06-13 14:19:51 +02:00
fb83db98ab rename blueprint to schematic 2026-04-26 21:00:55 +02:00
8b84297b41 update architecture.md so that we can use QVector2D in the lib 2026-04-20 07:34:32 +02:00
e3e188a24c requirements cleanup 2026-04-18 23:14:49 +02:00
f29e2ba235 requirements clarifications 2026-04-18 23:07:22 +02:00
8d4fece87d rendering requirements 2026-04-18 22:52:57 +02:00
13f4800191 requirements for blueprint drops 2026-04-18 22:37:41 +02:00
b5e2cbbb4f laser requirements 2026-04-18 22:28:40 +02:00
1796d3771c requirements iteration 2026-04-18 22:21:31 +02:00
467a87e278 add architecture description file 2026-04-17 22:08:50 +02:00