neon-sprawl/docs/decomposition/modules/E1_M2_IsometricCameraContro...

12 KiB
Raw Permalink Blame History

E1.M2 — IsometricCameraController

Summary

Field Value
Module ID E1.M2
Epic Epic 1 — Core Player Runtime
Stage target Prototype
Status Ready (prototype slice) — follow + CameraState + discrete zoom bands + occlusion + pick-through (NEO-15NEO-17, NEO-20); contracts + hardening (NEO-18); see dependency register
Linear Project E1.M2 — IsometricCameraController; prototype backlog issues under Epic 1 — Core Player RuntimeLinear backlog

Purpose

Authority: Client vs server — camera is client-local for prototype; server does not use camera pose for gameplay checks.

Delivers an isometric follow camera that keeps the player readable during motion: follow behavior, discrete zoom bands, occlusion handling, and yaw held at a fixed default for players in early prototypes so combat telegraphs and UI stay easy to reason about. After release, the shipped camera model is settled (fixed presentation vs product setting that allows yaw)—we do not plan to revisit that post-launch. Before release, we may still decide to expose yaw orbit for comfort; the implementation follows the mid-project rotation policy so that decision stays mid-project scope, not a rewrite.

Responsibilities

  • Locked isometric follow for player-facing controls in prototype: yaw = 0 (no orbit bound in UI) while the rig and CameraState still model yaw explicitly so optional orbit can be enabled later via config and input binding.
  • Zoom bands and configuration-driven limits.
  • Occlusion policy so critical gameplay elements remain visible where possible.
  • Pitch / roll remain fixed for the isometric presentation; only yaw is the optional future degree of freedom.

Mid-project rotation policy (practical compromise)

Intent: Ship prototypes and early milestones as if the camera were fully fixed from the players perspective, without closing the door to yaw orbit before release.

Topic Direction
When this applies Mid-project only. We may enable or keep disabled yaw orbit while still in development. Post-release, treat the chosen shipped behavior as final—no planned pivot between “fixed” and “orbit” for live players.
Implementation Single seam: e.g. allow_yaw, max_yaw_deg, or equivalent; default keeps yaw at 0 and no rotate input until product + combat readability agree. CameraState carries yaw (default 0) so consumers do not assume “camera yaw does not exist.”
Gameplay semantics Prefer world-anchored movement, targeting, and server checks—not camera-relative aim—so turning yaw on later is mostly presentation + UI, not a combat authority redesign.
Telegraphs & VFX Design now for a world where yaw might exist: ground shapes remain world-accurate; plan screen-space or height readability affordances where foreshortening under yaw would otherwise lie (outlines, rings, bars—exact art TBD).
Migration risk If we add yaw mid-project, expect a telegraph + minimap / compass pass. Fixed → yaw is usually lower risk than yaw → fixed (content and players may already assume spin for sightlines).

Key contracts

Contract Role
CameraState Per-frame snapshot: follow path, effective distance, zoom band index, focus, yaw (orbit delta; prototype 0). Policy / presentation exports are not duplicated in this type for the prototype — read the rig when needed (Consumer contract).
ZoomBandConfig Data-driven min/max or discrete zoom steps.
OcclusionPolicy Rules for fading, dithering, or offset when geometry blocks the view.

Consumer contract (NEO-18)

Single place for dependents (e.g. E6.M2): what is on CameraState vs what stays on the rig. Authority: E1.M2 — client vs server.

CameraState fields (client/scripts/camera_state.gd)

Refreshed every _physics_process tick on IsometricFollowCamera after the follow target has moved (process_physics_priority after Player). Same object each tick (rig-owned).

Field Meaning
yaw Orbit delta (rad) around the follow target relative to presentation_yaw_degnot full compass. 0 while allow_yaw is false (prototype UX).
follow_target_path NodePath to the follow anchor (e.g. player).
distance Effective follow distance this tick (active band or fallback).
zoom_band_index Active discrete band index, or 0 when zoom config is invalid / unused.
focus_world World look-at focus last tick.

Rig-only reads (client/scripts/isometric_follow_camera.gd, World/IsometricFollowCamera in main.tscn)

Export / node Meaning
allow_yaw, max_yaw_deg Orbit seam (no rotate input bound in prototype).
presentation_yaw_deg, pitch_elevation_deg, follow_distance, focus_vertical_offset Fixed isometric presentation (pitch/roll not exposed as free axes).
zoom_band_config, occlusion_policy Resources driving bands and occlusion.
Child Camera3D Active render camera (ground_pick uses viewport / fallback_camera from main.gd).

Telemetry: Throttled camera_zoom_changed and occlusion/perf hooks are TODO(E9.M1) until an event schema exists (zoom_band_changed signal on the rig today).

Module dependencies

  • E1.M1 — InputAndMovementRuntime: camera follows the player anchor derived from movement/position.

Dependents (by design)

  • E6.M2 — Consent and risk UX may use camera-adjacent presentation; epic lists E1.M2 as a dependency for in-zone risk signaling and readability.

See Epic 1 Slice 2 — Locked isometric camera: follow-center, zoom bands, occlusion policy, yaw fixed for players in prototype (implementation still models yaw per mid-project rotation policy); optional telemetry such as throttled camera_zoom_changed and perf stress markers for occluders.

Implementation snapshot (NEO-15 + NEO-16 + NEO-17 + NEO-20 + NEO-18, 2026-04-08 / 2026-04-10)

  • Godot: client/scripts/isometric_follow_camera.gd on World/IsometricFollowCamera; child Camera3D (current). scripts/camera_state.gdRefCounted tick snapshot (yaw = orbit component, 0 while allow_yaw is false; distance = effective follow distance; zoom_band_index; focus_world, follow_target_path).

  • Seam: allow_yaw / max_yaw_deg on the rig; orbit would adjust _orbit_yaw_rad (no input bound yet). Presentation compass uses presentation_yaw_deg separately from orbit yaw in state.

  • Zoom: ZoomBandConfig resource (client/scripts/zoom_band_config.gd); default client/resources/isometric_zoom_bands.tres on main rig. Bands are distance-only (ascending near→far); pitch / FOV unchanged vs NEO-15. Input: camera_zoom_in / camera_zoom_out in client/project.godot (mouse wheel + = / - + keypad + / ). follow_distance remains fallback when config is null or has no bands; state zoom_band_index is 0 in that case. zoom_band_changed signal on rig; TODO(E9.M1) for throttled camera_zoom_changed telemetry.

  • Occlusion: OcclusionPolicy resource (client/scripts/occlusion_policy.gd); default client/resources/isometric_occlusion_policy.tres on main rig. Technique: each _physics_process tick, iterative intersect_ray from _smoothed_eye to player focus (up to max_occluder_cast_depth calls). Bodies tagged "occluder" that intersect the ray have their MeshInstance3D surfaces overridden with a transparent StandardMaterial3D copy (fade_alpha = 0.25). Materials restored instantly when the body clears the ray. Null-material surfaces (no mesh or override material set) receive a plain transparent StandardMaterial3D; other non-StandardMaterial3D surfaces are skipped with push_warning. Prototype district: Obstacle node in main.tscn tagged "occluder". Perf marker: occluder_count_log_threshold export (default 0 = disabled) emits push_warning for stress-test reference. See risks and telemetry for readability gate.

    Prototype demo readability gate: before shipping the prototype demo, the PR for NEO-17 must include a before/after screenshot or clip demonstrating that the player is no longer fully hidden by the Obstacle. This serves as the first data point for the occlusion-hiding-telegraphs risk documented below.

  • Click-through input (NEO-20): scripts/ground_pick.gd unconditionally skips bodies in the "occluder" group during click-to-move ground-pick raycasts. When a cast hits an occluder the ray origin advances OCCLUDER_PICK_THROUGH meters past the hit point and continues, independent of whether OcclusionPolicy is currently fading that body. This keeps the "occluder" group as the sole tagging convention shared between the camera occlusion system and ground-pick input — no dedicated collision layer is added for this case.

  • Integration hardening (NEO-18): Consumer contract + camera_state.gd header; E6.M2 adjacency notes (E6_M2_ConsentAndRiskUxSignals.md); invalid / freed occluder bodies are dropped from _occluder_overrides before each occlusion ray pass and before full restore (occluder_override_key_is_valid, tests in isometric_follow_camera_test.gd) — no material restore on that path (freed subtree would error); live bodies still restore via _restore_occluder when they leave the ray. Follow / state / occlusion run in _physics_process with process_physics_priority = 1 so the rig updates after Player (default 0) and tracks move_and_slide without display-vs-physics jitter.

Linear backlog

Parent epic: Epic 1 — Core Player Runtime.

Key Type Summary
E1.M2 Project E1.M2 — IsometricCameraController (module umbrella)
NEO-15 Story Isometric follow camera (fixed yaw prototype; CameraState seam)
NEO-19 Story Expand prototype client district for camera/nav stress QA
NEO-16 Story ZoomBandConfig + discrete zoom input (clamped)
NEO-17 Story OcclusionPolicy — keep player readable through geometry
NEO-18 Story Camera integration hardening + dependent contract notes

Suggested order: NEO-15 → NEO-16 → NEO-17 → NEO-18. NEO-19 may run in parallel with or after NEO-15 (scene/nav geography; does not require zoom/occlusion).

Risks and telemetry

  • Occlusion hiding telegraphs: tune policy early and include readability checklist in prototype gates.
  • Enabling yaw orbit mid-project without a telegraph/UI/minimap pass can undermine readability; design ground effects for world accuracy plus screen-space or height cues early (see mid-project rotation policy).

Source anchors