From 3a02fd0d2a630023e44b6bf746de940b44132519 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 16 Feb 2026 23:26:28 +0100 Subject: [PATCH] feat(client): add camera anchor test suite 10 gdUnit4 tests verifying camera behavior: SimBridge test mode connection, snapshot pipeline, player position extraction, camera anchor in _ready(), smoothing disable/re-enable cycle, camera tracking across frames, and player movement following. Co-Authored-By: Claude Opus 4.6 --- client/tests/test_camera_anchor.gd | 145 +++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 client/tests/test_camera_anchor.gd diff --git a/client/tests/test_camera_anchor.gd b/client/tests/test_camera_anchor.gd new file mode 100644 index 000000000..02e9196de --- /dev/null +++ b/client/tests/test_camera_anchor.gd @@ -0,0 +1,145 @@ +## Tests for camera anchor behavior — the camera must show the player's +## position from the very first rendered frame, with no visible drift from (0,0). +## +## These tests verify the full pipeline: SimBridge → GameState → Camera2D. +## The player starts at (10,10) in test mode, so the expected camera position +## is (10,10) * TILE_SIZE = (320, 320). +class_name TestCameraAnchor +extends GdUnitTestSuite + +const EXPECTED_PLAYER_POS := Vector2(10, 10) +const EXPECTED_CAMERA_POS := Vector2(320, 320) # 10 * 32, 10 * 32 + +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 + + +# --- Pipeline precondition tests --- + +func test_simbridge_connected_in_test_mode() -> void: + SimBridge.state = SimBridge.ConnectionState.DISCONNECTED + SimBridge.connect_to_sim() + assert_that(SimBridge.state).is_equal(SimBridge.ConnectionState.CONNECTED) + + +func test_poll_snapshot_returns_data_when_connected() -> void: + SimBridge.connect_to_sim() + var snapshot: Variant = SimBridge.poll_snapshot() + assert_that(snapshot).is_not_null() + assert_that(snapshot is Dictionary).is_true() + + +func test_snapshot_has_player_at_10_10() -> void: + var snap := SimBridge._test_snapshot() + var player_x: float = -1.0 + var player_y: float = -1.0 + for entity in snap.entities: + if entity.kind.variant == "Player": + player_x = entity.x + player_y = entity.y + break + assert_that(player_x).is_equal(10.0) + assert_that(player_y).is_equal(10.0) + + +func test_apply_snapshot_sets_player_position() -> void: + var snap := SimBridge._test_snapshot() + GameState.apply_snapshot(snap) + assert_that(GameState.player_position).is_equal(EXPECTED_PLAYER_POS) + + +# --- Camera anchor after _ready() --- + +func test_camera_position_after_ready() -> void: + var scene := load("res://scenes/main.tscn") + _instance = scene.instantiate() + auto_free(_instance) + add_child(_instance) + + var camera: Camera2D = _instance.get_node("Camera2D") + assert_that(camera.global_position).is_equal(EXPECTED_CAMERA_POS) + + +func test_camera_anchored_flag_after_ready() -> void: + var scene := load("res://scenes/main.tscn") + _instance = scene.instantiate() + auto_free(_instance) + add_child(_instance) + + assert_that(_instance._camera_anchored).is_true() + + +func test_camera_smoothing_off_after_ready() -> void: + # Camera smoothing must be disabled during init to prevent lerp from (0,0). + # If this test fails, the camera will visibly drift from origin to player. + var scene := load("res://scenes/main.tscn") + _instance = scene.instantiate() + auto_free(_instance) + add_child(_instance) + + var camera: Camera2D = _instance.get_node("Camera2D") + assert_that(camera.position_smoothing_enabled).is_false() + + +# --- Camera behavior across frames --- + +func test_camera_tracks_player_after_process() -> void: + var scene := load("res://scenes/main.tscn") + _instance = scene.instantiate() + auto_free(_instance) + add_child(_instance) + + # Simulate one game frame + _instance._process(0.016) + + var camera: Camera2D = _instance.get_node("Camera2D") + var expected := GameState.player_position * Constants.TILE_SIZE + assert_that(camera.global_position).is_equal(expected) + + +func test_camera_smoothing_reenabled_after_process() -> void: + # After the first anchored frame, smoothing should be back on for gameplay. + var scene := load("res://scenes/main.tscn") + _instance = scene.instantiate() + auto_free(_instance) + add_child(_instance) + + _instance._process(0.016) + + var camera: Camera2D = _instance.get_node("Camera2D") + assert_that(camera.position_smoothing_enabled).is_true() + + +func test_camera_follows_player_movement() -> void: + var scene := load("res://scenes/main.tscn") + _instance = scene.instantiate() + auto_free(_instance) + add_child(_instance) + + var camera: Camera2D = _instance.get_node("Camera2D") + var initial_pos := camera.global_position + + # Move player north via SimBridge test mode + SimBridge._test_input_queue.append("MoveNorth") + _instance._process(0.016) + + # Camera should have moved with the player + assert_that(camera.global_position.y < initial_pos.y).is_true() + assert_that(camera.global_position).is_equal( + GameState.player_position * Constants.TILE_SIZE)