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>
215 lines
7.1 KiB
GDScript
215 lines
7.1 KiB
GDScript
## 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.0–20.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)
|