extract the scroll position into WorldCamera

This commit is contained in:
2026-08-05 19:48:06 +02:00
parent fa9dbd62ad
commit f6df95abb2
8 changed files with 423 additions and 58 deletions

View File

@@ -199,7 +199,7 @@ GameWorldView::GameWorldView(Simulation* sim, const GameConfig* config,
, m_commandManager(*sim)
, m_gameSpeedMultiplier(1.0)
, m_prevNonZeroSpeed(1.0)
, m_scrollXTiles(0.0f)
, m_camera(config->world.scroll, config->world.regions)
, m_ghostRotation(Rotation::East)
, m_ghostValid(false)
, m_dragging(false)
@@ -366,18 +366,19 @@ void GameWorldView::onFrame()
// Apply held scroll
{
// Pan speed depends on where the view is centered (REQ-UI-SCROLL-SPEED).
const float viewCenterX = m_scrollXTiles;
const float delta = panSpeedTilesPerSecondAt(viewCenterX)
* static_cast<float>(elapsed) / 1000.0f;
const float scrollBefore = m_scrollXTiles;
if (m_scrollLeft) { m_scrollXTiles -= delta; }
if (m_scrollRight) { m_scrollXTiles += delta; }
clampScroll();
// Holding both keys cancels out, as it did when each key moved the view
// independently.
PanDirection direction = PanDirection::None;
if (m_scrollLeft != m_scrollRight)
{
direction = m_scrollLeft ? PanDirection::Left : PanDirection::Right;
}
const bool viewMoved = m_camera.advance(direction, elapsed, getScrollBounds());
// While the view scrolls, the tile under a stationary cursor changes,
// so refresh the box-select rectangle even though no mouse move fires.
if (m_boxSelecting && m_scrollXTiles != scrollBefore)
if (m_boxSelecting && viewMoved)
{
m_boxCurrentTile =
getCoordinates().widgetToTile(mapFromGlobal(QCursor::pos()));
@@ -526,7 +527,7 @@ void GameWorldView::paintGL()
WorldCoordinates GameWorldView::getCoordinates() const
{
return WorldCoordinates::scrolling(size(), m_config->world.heightTiles,
m_scrollXTiles);
m_camera.getViewCenterXTiles());
}
float GameWorldView::getAsteroidLeftEdge() const
@@ -562,47 +563,13 @@ float GameWorldView::getEnemyStationRightEdge() const
return rightX;
}
namespace
ScrollBounds GameWorldView::getScrollBounds() const
{
// Linearly blend from valueAt0 (for x <= x0) to valueAt1 (for x >= x1), clamped
// outside [x0, x1]. A zero- or negative-width band collapses to a hard step at x1.
float lerpClamped(float valueAt0, float valueAt1, float x0, float x1, float x)
{
if (x1 <= x0) { return x < x1 ? valueAt0 : valueAt1; }
const float t = std::max(0.0f, std::min(1.0f, (x - x0) / (x1 - x0)));
return valueAt0 + (valueAt1 - valueAt0) * t;
}
}
float GameWorldView::panSpeedTilesPerSecondAt(float viewCenterXTiles) const
{
// Slow near the asteroid/player buffer, fast across the contest zone, with a
// linear ramp straddling each contest-zone boundary (REQ-UI-SCROLL-SPEED). The
// contest zone spans from the player buffer's right edge to the enemy stations,
// the latter tracked live so the ramp follows the front line as it is pushed.
const float slow = static_cast<float>(m_config->world.scroll.panSpeedSlow_tps);
const float fast = static_cast<float>(m_config->world.scroll.panSpeedFast_tps);
const float half = static_cast<float>(m_config->world.scroll.panRampBandWidth_tiles) / 2.0f;
const float leftEdge = static_cast<float>(m_config->world.regions.playerBufferWidth_tiles);
const float rightEdge = getEnemyStationRightEdge();
// Rising ramp at the left boundary (slow -> fast) and falling ramp at the right
// boundary (fast -> slow); their minimum yields flat-slow outside, flat-fast in
// the middle, and — if the bands overlap in a narrow contest zone — a single peak
// below the fast speed where the two ramps cross.
const float leftRamp = lerpClamped(slow, fast, leftEdge - half, leftEdge + half, viewCenterXTiles);
const float rightRamp = lerpClamped(fast, slow, rightEdge - half, rightEdge + half, viewCenterXTiles);
return std::min(leftRamp, rightRamp);
}
void GameWorldView::clampScroll()
{
// m_scrollXTiles is the view center, so the pan limits are the edges themselves:
// the view can pan left until the buildable/asteroid edge is centered, and right
// until the enemy stations are centered (revealing a little space beyond them).
const float leftBound = getAsteroidLeftEdge();
const float rightBound = getEnemyStationRightEdge();
m_scrollXTiles = std::max(leftBound, std::min(m_scrollXTiles, rightBound));
// The camera clamps its view center to these, so the pan limits are the edges
// themselves: the view can pan left until the buildable/asteroid edge is
// centered, and right until the enemy stations are centered (revealing a little
// space beyond them).
return ScrollBounds{getAsteroidLeftEdge(), getEnemyStationRightEdge()};
}
// ---------------------------------------------------------------------------
@@ -3102,7 +3069,7 @@ void GameWorldView::resetForNewGame()
m_copiedConfig = std::nullopt;
m_copyConfigFlashes.clear();
m_boxSelecting = false;
m_scrollXTiles = 0.0f;
m_camera.reset();
m_scrollLeft = false;
m_scrollRight = false;
m_gameOverShown = false;

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;