96 lines
9.7 KiB
Markdown
96 lines
9.7 KiB
Markdown
# NEO-14 — Implementation plan
|
|
|
|
## Story reference
|
|
|
|
| Field | Value |
|
|
|--------|--------|
|
|
| **Key** | NEO-14 |
|
|
| **Title** | Godot client: eliminate CharacterBody3D idle / rest vibration |
|
|
| **Linear** | [NEO-14](https://linear.app/neon-sprawl/issue/NEO-14) |
|
|
| **Parent** | [Tech Debt](https://linear.app/neon-sprawl/project/tech-debt-d2148715e875) |
|
|
| **Related** | [NEO-11](https://linear.app/neon-sprawl/issue/NEO-11) (path-follow baseline; idle jitter explicitly deferred here) |
|
|
|
|
## Goal, scope, and out-of-scope
|
|
|
|
**Goal:** Re-open **NEO-14** to restore a **stable rest pose** after later camera work made the prototype jitter worse: no perceptible idle vibration on flat walkable ground, and no continuous **x/z** drift while the avatar has no active walk goal.
|
|
|
|
**In scope**
|
|
|
|
- Reproduce the current regression in the prototype scene and separate **true player transform drift** from **camera presentation amplification**.
|
|
- Diagnose and fix idle jitter in the prototype **`CharacterBody3D`** player driven by **`player.gd`** and/or the follow rig in **`isometric_follow_camera.gd`** when evidence shows the camera is making tiny corrections read as larger visible jitter.
|
|
- Preserve **NEO-11** behavior: walking, arrival at goal, **`snap_to_server`** on boot, descend bypass tradeoff, and **NEO-10** validation UX on the server side (client must not regress those flows).
|
|
- Add or update focused **GdUnit** coverage for any new pure helper or guard logic introduced by the fix.
|
|
- Document non-obvious fixes in **`client/README.md`** and the living plan if the final solution changes the anti-jitter pattern or camera integration expectations.
|
|
|
|
**Out of scope**
|
|
|
|
- Full prediction/reconciliation, server-side nav, or redesign of **MoveCommand** / **PositionAuthorityClient** contracts.
|
|
- Replacing the locomotion stack or redesigning the isometric follow camera beyond the smallest anti-jitter hardening needed for this regression.
|
|
- Product decisions about enabling camera yaw/orbit; this story only hardens the fixed-yaw prototype behavior.
|
|
|
|
## Acceptance criteria checklist
|
|
|
|
- [ ] At rest (no click-to-move goal, feet on floor), **no visible vibration** in the default prototype scene for **several seconds** of observation on flat ground, and no measurable-looking **x/z** drift at idle.
|
|
- [ ] **Walking**, **arrival**, and **`snap_to_server`** still behave correctly; **no regression** on NEO-10 bumps or the **NEO-11** obstacle / descend-bypass tradeoff documented in **`client/README.md`**.
|
|
- [ ] If camera-follow hardening is required, it does **not** introduce new visible lag, framing drift, or occlusion regressions in the current E1.M2 stack.
|
|
- [ ] If a **scene** or **project** setting change is required, it is **checked in** and called out for designers (README or short comment in scene notes).
|
|
|
|
## Resolution
|
|
|
|
**Re-opened 2026-04-09 after NEO-15/26/27/30 follow-camera work:** the earlier NEO-14 mitigation shipped, but testing now reports worse visible idle jitter even on a flat surface. The current branch treats that as **real idle correction noise first**, not purely a camera problem.
|
|
|
|
**Implemented on this branch:** when the capsule already has **stable flat support** (floor normal effectively upright and no post-arrival loose-floor window), **`player.gd`** now skips the corrective idle **`move_and_slide()`** / rim-escape loop entirely and keeps the body parked. The flat-idle classification was broadened so incidental extra contacts on an otherwise flat support do not re-enable the old correction loop. Stable grounded idle also keeps an explicit **x/z anchor**, clamping away any residual horizontal creep until movement resumes or support is no longer classified as stable. Goal arrival and the descend-bypass check now both measure against the capsule **feet height** instead of the body origin, which stops same-height floor clicks near obstacles from dropping into the old straight-line wall push. **`_snap_capsule_upright()`** also avoids rewriting **`global_transform`** when the basis is already identity, reducing needless transform churn during idle.
|
|
|
|
## Technical approach
|
|
|
|
1. **Reproduce and isolate** in Godot 4.x editor: Remote **Transform** on the player and camera rig, **Visible Collision Shapes**, and short idle observation on flat ground first, then random bumps, to determine whether the player root is really moving in **x/z**.
|
|
2. **CharacterBody3D / physics** — inspect the current idle path in **`player.gd`**: idle **`move_and_slide()`**, rim settle, bump proximity escape, and upright snap. Prevent continuous idle correction on stable flat contacts while preserving the original bump / rim escape behavior that NEO-14 added for rougher geometry.
|
|
3. **Camera / parenting** — if the player still only moves by tiny amounts, harden **`isometric_follow_camera.gd`** so the rig does not magnify sub-threshold target motion. Preferred direction: smooth or deadzone the **focus** used by **`desired_eye_world()`**, **`look_at()`**, and occlusion instead of smoothing only the eye position.
|
|
4. **Navigation agent** — confirm **`NavigationAgent3D`** rest targets (`set_target_position(global_position)` on clear/arrive) are not reintroducing path churn that feeds the idle correction loop.
|
|
5. **Ship** the smallest change that clears the AC, then update **`client/README.md`** and related docs with the final anti-jitter pattern and verification notes.
|
|
|
|
## Decisions
|
|
|
|
| Topic | Choice | Rationale |
|
|
|------|---------|-----------|
|
|
| **Regression framing** | Treat this as a **re-opened NEO-14** fix, not a brand new camera story. | Linear already tracks idle vibration (NEO-14), and the new symptom is the same bug surface with later camera work as a likely amplifier. |
|
|
| **Debug order** | Prove whether **player transform drift** exists before shipping a camera-only mitigation. | A camera-only band-aid could hide a real **`CharacterBody3D`** rest-state bug. |
|
|
| **Camera hardening scope** | Only harden follow behavior if player-side cleanup is insufficient or if raw focus tracking is clearly amplifying tiny motion. | Keeps the fix minimal and avoids reopening broader E1.M2 follow-camera design work. |
|
|
| **Flat idle strategy** | Detect **stable flat support** and skip corrective idle movement in that state. | The regression report is specifically about persistent idle **x/z** drift on a flat surface, where the old rim/bump correction loop should not be necessary. |
|
|
| **Stable support classification** | Let a strongly upright floor normal win even when physics reports incidental extra contacts. | Flat supports were still falling back into the correction path when side contacts were reported even though the capsule should have been considered settled. |
|
|
| **Residual drift containment** | Keep a stable-grounded idle **x/z anchor** and restore it each frame while idle remains stable. | Even if the exact remaining source of horizontal creep is hard to reproduce in headless runs, this guarantees flat idle does not visibly wander once the player has settled. |
|
|
| **Descend bypass framing** | Treat “below the player” using the capsule **feet**, not the body origin. | Floor picks on the same slab always sit below the capsule origin, which was incorrectly forcing direct steering into obstacle walls and reintroducing jitter after contact. |
|
|
| **Upright snap writes** | Skip the upright rewrite when the basis is already **`Basis.IDENTITY`**. | Avoids touching **`global_transform`** every idle frame when there is nothing to correct. |
|
|
|
|
## Files to add
|
|
|
|
| Path | Purpose |
|
|
|------|---------|
|
|
| None. | This follow-up should fit in existing movement, camera, test, and documentation files. |
|
|
|
|
## Files to modify
|
|
|
|
| Path | Rationale |
|
|
|------|-----------|
|
|
| `client/scripts/player.gd` | Primary idle-motion diagnosis and fix: idle **`move_and_slide`** policy, rim/bump escape guards, and any extracted helper logic used to distinguish stable floor contact from real corrective motion. |
|
|
| `client/test/player_test.gd` | Add regression coverage for any new idle guard/helper logic added to **`player.gd`**. |
|
|
| `client/README.md` | Update the documented anti-jitter pattern and manual verification steps once the final fix is known. |
|
|
|
|
## Tests
|
|
|
|
| Action | What to cover |
|
|
|--------|----------------|
|
|
| **Manual (required)** | Run main scene with server as in **NEO-7/NEO-11** README steps: stand idle **10+ s** at spawn on flat ground, stand idle again after a move, idle on the random green bumps, click-move including **stepped bumps** (NEO-10 props), cold boot **`snap_to_server`**, far/vertical rejects, and NEO-20 occluder click-through sanity. |
|
|
| **Automated** | Updated **`client/test/player_test.gd`** with stable-idle plus capsule-feet helper coverage. Headless GdUnit run: **52 tests, 0 errors, 0 failures** via Godot **4.6** and **`res://addons/gdUnit4/bin/GdUnitCmdTool.gd`**. One-off headless idle probe against **`main.tscn`** observed **`x_range=0`** and **`z_range=0`** across a **240-physics-frame** idle sample after settle. |
|
|
|
|
## Open questions / risks
|
|
|
|
- The current regression report says jitter is now worse **even on flat ground**, so the earlier assumption that the problem was mainly bump/rim contact may no longer be sufficient.
|
|
- A player-only fix may still leave visible follow jitter if the camera continues to aim at raw focus each render frame.
|
|
- A camera-only fix may hide real **`CharacterBody3D`** drift and make future movement regressions harder to see.
|
|
- **`_snap_capsule_upright()`** still uses full **identity basis** — when **yaw** facing is added, preserve **Y** rotation only (see **`TODO`** in **`player.gd`**). Moving platforms / uneven nav mesh not validated.
|
|
|
|
## PR / review
|
|
|
|
Cross-check [E1.M1 — InputAndMovementRuntime](../decomposition/modules/E1_M1_InputAndMovementRuntime.md), **`docs/plans/NEO-11-implementation-plan.md`** (residual polish pointer), and **`client/README.md`** after changes.
|