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

@@ -15,6 +15,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
PARENT_SCOPE
)
@@ -27,6 +28,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
PARENT_SCOPE
)

View File

@@ -0,0 +1,72 @@
#include "WorldCamera.h"
#include <algorithm>
namespace
{
// 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;
}
}
WorldCamera::WorldCamera(const WorldScroll& scroll, const WorldRegions& regions)
: m_scroll(&scroll)
, m_regions(&regions)
, m_viewCenterXTiles(0.0f)
{
}
bool WorldCamera::advance(PanDirection direction, qint64 elapsedMs,
ScrollBounds bounds)
{
const float before = m_viewCenterXTiles;
if (direction != PanDirection::None)
{
const float distance =
getPanSpeedTilesPerSecondAt(m_viewCenterXTiles, bounds.rightTiles)
* static_cast<float>(elapsedMs) / 1000.0f;
m_viewCenterXTiles += (direction == PanDirection::Left) ? -distance : distance;
}
m_viewCenterXTiles = std::max(bounds.leftTiles,
std::min(m_viewCenterXTiles, bounds.rightTiles));
return m_viewCenterXTiles != before;
}
float WorldCamera::getViewCenterXTiles() const
{
return m_viewCenterXTiles;
}
void WorldCamera::reset()
{
m_viewCenterXTiles = 0.0f;
}
float WorldCamera::getPanSpeedTilesPerSecondAt(float viewCenterXTiles,
float contestZoneRightEdgeTiles) 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_scroll->panSpeedSlow_tps);
const float fast = static_cast<float>(m_scroll->panSpeedFast_tps);
const float half = static_cast<float>(m_scroll->panRampBandWidth_tiles) / 2.0f;
const float leftEdge = static_cast<float>(m_regions->playerBufferWidth_tiles);
const float rightEdge = contestZoneRightEdgeTiles;
// 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);
}

View File

@@ -0,0 +1,72 @@
#pragma once
#include <QtGlobal>
#include "WorldConfig.h"
// Which way the player is currently panning the view (REQ-UI-SCROLL). A plain
// direction rather than key state: the camera is deliberately agnostic about how
// the intent was expressed, so rebindable controls would change nothing here.
enum class PanDirection
{
None,
Left,
Right
};
// The horizontal limits of the view center, in world tiles (REQ-GW-SCROLL-LIMIT):
// the view can pan left until the asteroid's left edge is centered and right until
// the enemy stations are. Both move as the game progresses — the left edge with
// asteroid expansion (REQ-GW-ASTEROID-EXPAND), the right edge as stations are
// pushed back (REQ-GW-PUSH-EXPAND) — so they are supplied per frame by the caller
// that can see the simulation, rather than queried here. That keeps the camera a
// value with no simulation dependency.
struct ScrollBounds
{
float leftTiles;
float rightTiles;
};
// Horizontal view position for the game world (REQ-UI-SCROLL, REQ-UI-SCROLL-SPEED).
// Works purely in world units — tiles and tiles per second, never pixels. Turning
// the resulting position into a widget transform is WorldCoordinates' job; the two
// meet only where the view feeds getViewCenterXTiles() into that transform.
class WorldCamera
{
public:
// Both config structs are referenced rather than copied: they live inside the
// Simulation's GameConfig, which is assigned in place on restart
// (REQ-CFG-RELOAD), so a camera built once still picks up reloaded tuning.
WorldCamera(const WorldScroll& scroll, const WorldRegions& regions);
// Pans by `direction` for `elapsedMs` of wall-clock time, then clamps into
// `bounds`. Wall clock rather than ticks because panning is presentation only
// (REQ-UI-NO-ZOOM's sibling concern) and keeps working while the simulation is
// paused. Clamping happens on every call, not just when panning, so the view
// follows the bounds inward when they shrink.
//
// Returns true when the view center actually moved — including when it moved
// only because the bounds did. Callers use that to refresh anything anchored to
// the world under a stationary cursor, such as the box-select rectangle.
bool advance(PanDirection direction, qint64 elapsedMs, ScrollBounds bounds);
// World X (tiles) at the center of the viewport.
float getViewCenterXTiles() const;
// Returns the view to the start-of-run position. Deliberately does not clamp:
// a new run's bounds are not known here, and the next advance() clamps anyway.
void reset();
// Pan speed at a given view center, in tiles/s (REQ-UI-SCROLL-SPEED). Public
// because the ramp shape is the subtle part of this class and is worth testing
// directly; advance() uses it internally. `contestZoneRightEdgeTiles` is the
// live right-hand boundary — the same value as ScrollBounds::rightTiles — so
// the ramp follows the front line as it is pushed.
float getPanSpeedTilesPerSecondAt(float viewCenterXTiles,
float contestZoneRightEdgeTiles) const;
private:
const WorldScroll* m_scroll;
const WorldRegions* m_regions;
float m_viewCenterXTiles;
};