## 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)