feat(client): sprint 38 — free camera viewer, archetype strip, test fixes

- Add free camera mode (F4 toggle): WASD pan, scroll zoom, decoupled
  from player position (#898)
- Strip archetype-driven code: remove character_archetype, lattice_profile,
  and lattice color palettes from client (#882)
- Fix confrontation_monologue signal not firing in headless test mode (#867)
- Revive fog state behavioral tests: EXP_EXPLORED persistence, grow-only
  bounds, texture-resize copy, BoundaryWall handling (#879)
- Triage pre-existing test failures: fix examine_display dismiss timing,
  fog test position fragility, rendering snapshot assertions,
  time_display format (#871)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 10:19:11 +02:00
co-authored by Claude Opus 4.6
parent f9fdfb7712
commit 3f5b4258ba
18 changed files with 504 additions and 235 deletions
+41 -1
View File
@@ -1,6 +1,11 @@
extends Node2D
const TELEPORT_DISTANCE_THRESHOLD: float = 5.0
# #898: Free camera pan speed in pixels/second (unzoomed) and zoom step per scroll tick.
const FREE_CAMERA_PAN_SPEED: float = 400.0
const FREE_CAMERA_ZOOM_STEP: float = 0.1
const FREE_CAMERA_ZOOM_MIN: float = 0.5
const FREE_CAMERA_ZOOM_MAX: float = 8.0
var economics_app = null # EconomicsApp — populated in _ready() via ImplantRegistry
var atlas_app = null # AtlasApp — populated in _ready() via ImplantRegistry
@@ -184,10 +189,31 @@ func _ready() -> void:
atlas_app.economics_link_requested.connect(_on_atlas_economics_link)
func _unhandled_input(event: InputEvent) -> void:
# #898: Scroll wheel zoom in free camera mode.
if GameState.free_camera_mode and event is InputEventMouseButton:
var mb := event as InputEventMouseButton
if mb.pressed:
var zoom := camera.zoom
if mb.button_index == MOUSE_BUTTON_WHEEL_UP:
zoom += Vector2(FREE_CAMERA_ZOOM_STEP, FREE_CAMERA_ZOOM_STEP)
elif mb.button_index == MOUSE_BUTTON_WHEEL_DOWN:
zoom -= Vector2(FREE_CAMERA_ZOOM_STEP, FREE_CAMERA_ZOOM_STEP)
camera.zoom = zoom.clamp(
Vector2(FREE_CAMERA_ZOOM_MIN, FREE_CAMERA_ZOOM_MIN),
Vector2(FREE_CAMERA_ZOOM_MAX, FREE_CAMERA_ZOOM_MAX)
)
get_viewport().set_input_as_handled()
func _unhandled_key_input(event: InputEvent) -> void:
if not (event is InputEventKey) or not event.is_pressed() or event.is_echo():
return
var key_event := event as InputEventKey
# #898: F4 toggles free camera mode.
if key_event.physical_keycode == KEY_F4:
GameState.free_camera_mode = not GameState.free_camera_mode
return
# Registry-driven toggle: each manifest declares its own default_key.
for manifest: ImplantAppManifest in ImplantRegistry.get_manifests():
if manifest.app_path.is_empty():
@@ -239,9 +265,23 @@ func _process(delta: float) -> void:
# #559: Dispatch snapshot to registered handlers (router pattern).
_router.dispatch(snapshot)
# #898: Free camera WASD pan — runs in place of player tracking.
if GameState.free_camera_mode:
var pan := Vector2.ZERO
if Input.is_action_pressed("move_north"):
pan.y -= 1.0
if Input.is_action_pressed("move_south"):
pan.y += 1.0
if Input.is_action_pressed("move_east"):
pan.x += 1.0
if Input.is_action_pressed("move_west"):
pan.x -= 1.0
if pan != Vector2.ZERO:
var speed := FREE_CAMERA_PAN_SPEED / camera.zoom.x
camera.global_position += pan.normalized() * speed * delta
# Track camera to player (D-015: locked, fixed-north).
# #117: Manual exponential smoothing.
if _camera_anchored:
elif _camera_anchored:
var target := GameState.player_position * Constants.TILE_SIZE
if _teleport_in_progress:
camera.global_position = target