diff --git a/client/scripts/constants.gd b/client/scripts/constants.gd index a4aaf0908..f286ad59b 100644 --- a/client/scripts/constants.gd +++ b/client/scripts/constants.gd @@ -99,6 +99,11 @@ const DIALOGUE_MAX_WIDTH: int = 1200 # Default camera zoom — used as fallback when get_camera_2d() returns null const CAMERA_DEFAULT_ZOOM: Vector2 = Vector2(2.0, 2.0) +# #117: Camera smoothing speed — exponential interpolation via manual lerp in main.gd. +# Same pattern as EntityRenderer.LERP_SPEED. At 8.0: ~55% convergence after 0.1s. +# Slightly softer than entity movement (12.0) for a touch of cinematic camera lag. +const CAMERA_SMOOTHING_SPEED: float = 8.0 + # #517: Implant UI font color grading — avoid pure white, project through a lens const IMPLANT_TEXT_COLOR: Color = Color("#E0F7FA") # Cyan-white — primary text const IMPLANT_TEXT_DIM: Color = Color("#9EBFC4") # Dimmed variant — secondary text diff --git a/client/scripts/main.gd b/client/scripts/main.gd index 9421f0c8b..cd7d6febc 100644 --- a/client/scripts/main.gd +++ b/client/scripts/main.gd @@ -35,13 +35,10 @@ const LISTENING_FOCUS_TICKS: int = 30 # D-071: stationary ticks before Listenin func _ready() -> void: print("The Settled Reach — client initialized") - # Disable camera smoothing during init. Camera2D's position_smoothing - # lerps an internal smoothed_camera_pos toward global_position each frame. - # That smoothed position initializes at (0,0) — the Camera2D's default in - # the .tscn. Even after we set global_position to the player coords, - # smoothing causes the viewport to still show (0,0) on the first rendered - # frame because the lerp hasn't converged. With smoothing OFF, the viewport - # uses global_position directly. Re-enabled in _process() after anchor. + # #117: Manual lerp approach — disable Godot's built-in Camera2D smoothing. + # We lerp camera.global_position directly in _process() using CAMERA_SMOOTHING_SPEED, + # matching entity_renderer.gd's exponential smoothing pattern. Built-in smoothing + # would conflict because we'd be setting global_position to the target every frame. camera.position_smoothing_enabled = false # Connect to simulation (test mode sets CONNECTED immediately) @@ -70,7 +67,7 @@ func _ready() -> void: SimBridge.connection_state_changed.connect(_on_connection_state_changed) -func _process(_delta: float) -> void: +func _process(delta: float) -> void: # Main game loop: poll snapshot, apply state, flush input var snapshot: Variant = SimBridge.poll_snapshot() if snapshot != null: @@ -159,23 +156,18 @@ func _process(_delta: float) -> void: _consume_conversation_ended() _consume_dialogue_response() - # Track camera to player position every frame (D-015: locked, no panning) + # Track camera to player (D-015: locked, fixed-north). + # #117: Manual exponential smoothing — same pattern as EntityRenderer.LERP_SPEED. + # Teleport (flag set by _teleport_transition): snap immediately, resume lerp next frame. + # Init: camera already snapped in _ready() or late-anchor path above. if _camera_anchored: - camera.global_position = GameState.player_position * Constants.TILE_SIZE - - # Re-enable smoothing after the first anchored frame. The frame that just - # rendered used smoothing=OFF (correct viewport from frame one). Now we - # turn smoothing back on and sync its internal state so subsequent frames - # get smooth camera tracking during gameplay. - # #501: Skip re-enable during teleport — _teleport_transition() disables - # smoothing for a clean camera snap. Defer by one frame to avoid the - # re-enable block in the same _process() call undoing the snap. - if _camera_anchored and not camera.position_smoothing_enabled: + var target := GameState.player_position * Constants.TILE_SIZE if _teleport_in_progress: + camera.global_position = target _teleport_in_progress = false else: - camera.position_smoothing_enabled = true - camera.reset_smoothing() + var weight := 1.0 - exp(-Constants.CAMERA_SMOOTHING_SPEED * delta) + camera.global_position = camera.global_position.lerp(target, weight) # Send queued input to simulation # #507: Server-bound inputs are accumulated into _pending_record_inputs across frames. @@ -433,10 +425,9 @@ func _detect_teleport(old_pos: Vector2, new_pos: Vector2) -> bool: # Clears dialogue/monologue/interaction state (server clears its side too). # Scoped to Gauntlet testing only — production fast-travel uses diegetic gates. func _teleport_transition() -> void: - # Snap camera: disable smoothing, force re-anchor. - # _teleport_in_progress defers smoothing re-enable by one frame so the - # re-enable block at the bottom of _process() doesn't undo the snap. - camera.position_smoothing_enabled = false + # Snap camera immediately to new position. _teleport_in_progress causes + # the lerp block in _process() to snap again on the same frame (in case + # player_position updates after this call) and skip lerp next frame. camera.global_position = GameState.player_position * Constants.TILE_SIZE _camera_anchored = true _teleport_in_progress = true