# Issue #38 render interpolation pseudocode

## Problem

Phase L.5 correctly restored retail's `CPhysicsObj::update_object`
physics gate: the body only integrates when accumulated frame time reaches
`PhysicsBody.MinQuantum` (1/30 second). That keeps collision behavior aligned
with retail, but the renderer currently reads `_body.Position` directly.

At 60+ FPS that means several rendered frames can show the same physics
position, then jump to the next 30 Hz position. Chase camera makes the stepping
obvious because both the player mesh and camera target follow that discrete
physics sample.

## Evidence

- Named retail: `CPhysicsObj::update_object` at `0x00515d10`
  (`docs/research/named-retail/acclient_2013_pseudo_c.txt:283950`) skips or
  consumes time outside the valid quantum window and calls
  `CPhysicsObj::UpdateObjectInternal` only for accepted physics quanta.
- ACE mirrors the same constants in `PhysicsGlobals`: `MinQuantum = 1/30`,
  `MaxQuantum = 0.1`, `HugeQuantum = 2.0`.
- ACE's `InterpolationManager` / `PositionManager` prove client-style
  smoothing is a known AC-family concept, but their queue chases network target
  positions. Issue #38 is narrower: local render-only interpolation between the
  last two authoritative local physics ticks.
- Glenn Fiedler's "Fix Your Timestep!" describes the canonical fixed-timestep
  accumulator plus render interpolation pattern:
  https://gafferongames.com/post/fix_your_timestep/

## Intentional divergence

Retail did not need a separate high-FPS render interpolation layer because the
2013 client effectively presented near the same cadence as its physics gate.
acdream renders at modern display rates, so we preserve retail's 30 Hz physics
truth but draw a blended visual position between the last two physics truths.

## Pseudocode

State stored by `PlayerMovementController`:

```text
physicsAccum: leftover render time not yet consumed by physics
prevPhysicsPos: body position at the start of the most recently completed tick
currPhysicsPos: body position at the end of the most recently completed tick
```

On spawn, teleport, or any authoritative SetPosition:

```text
body.Position = newPosition
prevPhysicsPos = newPosition
currPhysicsPos = newPosition
physicsAccum = 0
```

Each Update(dt):

```text
physicsAccum += dt

if physicsAccum > HugeQuantum:
    physicsAccum = 0
    prevPhysicsPos = body.Position
    currPhysicsPos = body.Position
    return/render body.Position

if physicsAccum >= MinQuantum:
    oldTickEnd = currPhysicsPos
    preIntegratePos = body.Position

    tickDt = min(physicsAccum, MaxQuantum)
    calc_acceleration()
    UpdatePhysicsInternal(tickDt)

    postIntegratePos = body.Position
    resolve collision from preIntegratePos to postIntegratePos
    body.Position = resolved physics position
    update contact/cell/velocity exactly as before

    prevPhysicsPos = oldTickEnd
    currPhysicsPos = body.Position
    physicsAccum -= tickDt

alpha = clamp(physicsAccum / MinQuantum, 0, 1)
renderPosition = lerp(prevPhysicsPos, currPhysicsPos, alpha)
```

Important constraints:

- Never write `renderPosition` back to `_body.Position`.
- `MovementResult.Position` remains the authoritative physics/collision/network
  position.
- Add `MovementResult.RenderPosition` for mesh and camera drawing only.
- Outbound `MoveToState` / `AutonomousPosition` keep using
  `MovementResult.Position`.
- If no physics tick has completed yet, `prevPhysicsPos == currPhysicsPos`, so
  interpolation is a no-op.
