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
This commit is contained in:
2026-08-05 13:15:49 +02:00
parent 8ec413e1b9
commit b0fffdb00f
8 changed files with 423 additions and 58 deletions

View File

@@ -49,6 +49,7 @@
#include "TickDriver.h"
#include "TunnelCompletion.h"
#include "VisualsConfig.h"
#include "WorldCamera.h"
#include "WorldCoordinates.h"
struct Command;
@@ -184,9 +185,9 @@ private:
float getAsteroidLeftEdge() const;
float getEnemyStationRightEdge() const;
// Horizontal pan speed at a given view-center X, in tiles/s (REQ-UI-SCROLL-SPEED).
float panSpeedTilesPerSecondAt(float viewCenterXTiles) const;
void clampScroll();
// The camera's current pan limits, read fresh from the simulation each frame:
// both edges move with asteroid expansion and with pushes (REQ-GW-SCROLL-LIMIT).
ScrollBounds getScrollBounds() const;
bool isValidPlacement(BuildingType type, QPoint anchor, Rotation rot) const;
std::optional<BuildingId> buildingAtTile(QPoint tile) const;
@@ -333,8 +334,9 @@ private:
std::mt19937 m_rng;
double m_gameSpeedMultiplier;
double m_prevNonZeroSpeed;
// World-X (tiles) at the center of the viewport (see getViewLeftTiles()).
float m_scrollXTiles;
// Horizontal view position (REQ-UI-SCROLL). Owns the scroll position itself;
// this widget only supplies the pan intent and the simulation-derived bounds.
WorldCamera m_camera;
QTimer* m_renderTimer;
@@ -390,6 +392,11 @@ private:
QPoint m_boxStartTile;
QPoint m_boxCurrentTile;
// Held-key state for the A / D pan controls (REQ-UI-SCROLL, REQ-UI-HOTKEYS),
// collapsed into a PanDirection for the camera each frame. Kept here rather
// than on the camera because it is key state, not view state: once controls
// are rebindable this becomes an input mapper publishing the direction, and
// the camera's interface does not change.
bool m_scrollLeft;
bool m_scrollRight;
bool m_gameOverShown;