Files
settled-reach/client/tests/test_smooth_camera_sprint15.gd
T
jpmschweitzerandClaude Opus 4.6 4eb54a6f7a fix(client): resolve all gdlint warnings — zero warnings policy
Fix 354 gdlint warnings across 65 files: 194 class-definitions-order
(reorder declarations), 138 max-line-length (split long lines),
22 code issues (unused args, no-else-return, naming). Update .gdlintrc
to exclude addons/ and raise max-public-methods for test files.
No logic changes — declaration order, whitespace, and naming only.

Ticket: #783

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 09:59:56 +02:00

215 lines
7.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Sprint 15 — Smooth camera movement tests (#117)
## Validates exponential lerp, teleport snap, and configurable smoothing.
## Spec: D-015 (camera locked, fixed-north), #117 (interpolated tracking).
class_name TestSmoothCameraSprint15
extends GdUnitTestSuite
const MAIN_SCENE = preload("res://scenes/main.tscn")
var _instance: Node = null
func before_test() -> void:
SimBridge.reset_test_state()
GameState.current_tick = 0
GameState.player_position = Vector2.ZERO
GameState.visible_entities = []
GameState.visible_tiles = []
GameState.visible_positions = {}
GameState.current_monologue = null
GameState.current_dialogue = null
func after_test() -> void:
if _instance and is_instance_valid(_instance):
_instance.queue_free()
_instance = null
# --- Configurable smoothing constant ---
func test_camera_smoothing_speed_constant_defined() -> void:
# #117: CAMERA_SMOOTHING_SPEED must be declared in Constants (configurable).
assert_that(Constants.CAMERA_SMOOTHING_SPEED > 0.0).is_true()
func test_camera_smoothing_speed_constant_reasonable() -> void:
# #117: Speed should produce smooth-but-responsive feel (2.020.0 range).
assert_that(
Constants.CAMERA_SMOOTHING_SPEED >= 2.0 and Constants.CAMERA_SMOOTHING_SPEED <= 20.0
).is_true()
# --- Manual lerp, no Godot built-in smoothing ---
func test_godot_smoothing_disabled_at_ready() -> void:
# #117: Godot's built-in Camera2D smoothing must be OFF (manual lerp replaces it).
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
var camera: Camera2D = _instance.get_node("Camera2D")
assert_that(camera.position_smoothing_enabled).is_false()
func test_godot_smoothing_stays_off_after_frames() -> void:
# #117: Smoothing must NOT be re-enabled at any point — manual lerp only.
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
for i in range(5):
_instance._process(0.016)
var camera: Camera2D = _instance.get_node("Camera2D")
assert_that(camera.position_smoothing_enabled).is_false()
# --- Interpolated tracking (no snap) ---
func test_camera_lerps_not_snaps_on_player_move() -> void:
# #117: When player moves, camera should lerp (not snap) to new position.
# After 1 frame at ~60fps, camera should be partway there — not at target.
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
var camera: Camera2D = _instance.get_node("Camera2D")
var start_y := camera.global_position.y # anchored at player (10,10) → 320px
SimBridge._test_input_queue.append("MoveNorth")
_instance._process(0.016)
# Player moved to (10,9) → target_y = 288. Camera should be between 288 and 320.
var target_y: float = GameState.player_position.y * Constants.TILE_SIZE
assert_that(camera.global_position.y < start_y).is_true()
assert_that(camera.global_position.y > target_y).is_true()
func test_camera_converges_to_player_over_multiple_frames() -> void:
# #117: After enough frames the camera should be within 1px of target.
# At LERP_SPEED=8: ~95% convergence in 0.25s, >99% in 0.5s.
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
SimBridge._test_input_queue.append("MoveNorth")
_instance._process(0.016) # trigger the move, get new player position
var target := GameState.player_position * Constants.TILE_SIZE
# Run 120 frames (~2s at 60fps) — converges within 1px for any speed ≥ 2.0
for i in range(120):
_instance._process(0.016)
var camera: Camera2D = _instance.get_node("Camera2D")
var dist := camera.global_position.distance_to(target)
assert_that(dist < 1.0).is_true()
func test_camera_stationary_player_no_drift() -> void:
# #117: When player is stationary, camera should not drift (lerp to same point).
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
var camera: Camera2D = _instance.get_node("Camera2D")
var initial_pos := camera.global_position
# Run several frames with no movement
for i in range(10):
_instance._process(0.016)
# Camera should still be at anchored position (target = same point).
# Use distance check — lerp toward same point may introduce float rounding.
assert_that(camera.global_position.distance_to(initial_pos) < 0.01).is_true()
# --- Teleport snap ---
func test_teleport_snaps_camera_immediately() -> void:
# #117: _teleport_in_progress causes camera to snap (not lerp) in the same frame.
# Manually displace camera, set the flag, call _process — camera should snap to target.
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
var camera: Camera2D = _instance.get_node("Camera2D")
# Displace camera from its anchored position
camera.global_position = Vector2(0, 0)
# Set teleport flag — next _process() should snap to player target
_instance._camera_anchored = true
_instance._teleport_in_progress = true
_instance._process(0.016)
# Camera must now be exactly at player position (snapshot puts player at 10,10 → 320,320)
var expected := GameState.player_position * Constants.TILE_SIZE
assert_that(camera.global_position).is_equal(expected)
func test_teleport_flag_cleared_after_snap() -> void:
# #117: _teleport_in_progress must be false after the snap frame.
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
_instance._camera_anchored = true
_instance._teleport_in_progress = true
_instance._process(0.016)
assert_that(_instance._teleport_in_progress).is_false()
func test_camera_resumes_lerp_after_teleport() -> void:
# #117: Frame after teleport snap must resume lerp (not continue snapping).
# After teleport flag clears, any position delta produces lerp movement.
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
# Frame 1: teleport snap — camera displaced, flag set, expect snap
var camera: Camera2D = _instance.get_node("Camera2D")
camera.global_position = Vector2(0, 0)
_instance._camera_anchored = true
_instance._teleport_in_progress = true
_instance._process(0.016)
# After snap: camera at player position (10,10) = (320, 320)
var post_snap_y := camera.global_position.y
# Frame 2: player moves north — camera should lerp, not snap
SimBridge._test_input_queue.append("MoveNorth")
_instance._process(0.016)
var new_target_y: float = GameState.player_position.y * Constants.TILE_SIZE
# Camera must be between snap position and new target (lerping, not snapping)
assert_that(camera.global_position.y < post_snap_y).is_true()
assert_that(camera.global_position.y > new_target_y).is_true()
# Teleport flag must not be re-set by normal movement
assert_that(_instance._teleport_in_progress).is_false()
# --- D-015: Fixed-north camera ---
func test_camera_no_rotation() -> void:
# D-015: Camera must be fixed-north in v0.1 — no rotation regardless of facing.
var scene := MAIN_SCENE
_instance = scene.instantiate()
auto_free(_instance)
add_child(_instance)
var camera: Camera2D = _instance.get_node("Camera2D")
assert_that(camera.rotation).is_equal(0.0)
_instance._process(0.016)
assert_that(camera.rotation).is_equal(0.0)