The development player now renders remote travelers using the original Darkwind rig. The environment and saved game stay compatible with the existing world content. This change is on the development branch; it is not a deployment to the beta.
#Art and ownership
games/darkwind/traveler.ts owns the model choice, four coat colors, clip selection, stride length and placement. Engine packages import no game art. The playable traveler-base variant omits the decorative hand lantern, so a character does not visually claim the quest item before acquiring it. The temporary model has 11 material draws, 28,216 triangles, 14 joints and 154 palette matrices per actor. The furnished diagnostic uses the corresponding lantern-bearing variant. Character art records the mesh changes, review command and previous resource counts.
The model faces +Z; the game profile rotates it to the motor's -Z forward convention. Its foot origin is placed 0.76 metres below the motor center, matching the 0.48 capsule half-height plus 0.28 radius. Procedural floor dressing and controller margins can still leave small visual discrepancies; foot placement and refined gait remain open.
WebGPURenderer.configureActors() independently admits and owns a rig, verifies every referenced mesh against the installed catalog and reserves its per-actor palette cost before publication. The existing 25-draw actor allowance remains conservative. The shared 4,096-matrix limit includes ordinary scene skins, automatic model draws and all actors. Catalog replacement cannot remove geometry still referenced by the configured rig, even when no peers are visible. Graphics reconstruction installs the same catalog and rig before requesting the current scene baseline. Actor samples and gait caches are removed with departing actors and disposed with the renderer.
#Room protocol 3
Client poses contain place, position, yaw and optional locomotion: {speed, grounded}. Speed is bounded to 0–8 m/s; grounding is boolean. The production worker reports horizontal character-motor velocity, excluding platform carry, and suppresses gait speed while input is paused. Clients cannot supply motion stamps or overwrite peer identity. The relay normalizes facing to [-π, π] before publishing it, preventing large accumulated angles from escaping presentation bounds.
The server adds a motion stamp to each peer:
sample: { stream: string; sequence: string; time: number }The stream is a new UUID for each admitted connection. Sequence is an exact decimal uint64 incremented only by valid pose packets. Time is monotonic relay milliseconds, never a client clock. Presence broadcasts and heartbeats preserve the stamp. Reconnection and server restart create a new stream even when the saved session identity remains the same. Receiving clients validate peer shape, count, identity, name, color, place, pose and stamp before exposing the list to presentation.
This is a deliberate wire-version change: protocol-1/2 hellos are rejected with code 1008. A future rollout must serve matching protocol-3 client and relay builds together; reloading fetches the new client. Room disk format 1, world fingerprints and local save format 2 are unchanged. This does not add trusted server simulation or full generated-packet validation.
#Displayed motion and animation
Presentation format 6 carries locomotion with its required motion stamp on actor baseline, delta and motion records. Formats 2–5 retain their prior schemas and fixtures; missing locomotion uses the earlier displacement-derived gait. A legacy motion update clears a previously explicit locomotion value. The production worker forwards the server-owned stamp; the renderer has a separate bounded cache for each visible actor.
The cache retains at most eight samples and sixteen clock-offset observations. It interpolates against the relay timeline with an adaptive 100–250 ms delay, uses the shortest yaw arc, and never rewinds displayed time when the delay changes. Duplicate/older sequence numbers do not refresh sample age. New streams, gaps over one second and displacements over three metres reset the cache. Extrapolation lasts at most 150 ms with an 8 m/s velocity cap, then freezes position and reports zero gait speed. Repeated presence broadcasts cannot keep a disconnected movement stream running forever.
Displayed horizontal speed drives a two-clip idle/walk blend. Local translations/scales blend linearly and local quaternions use normalized shortest-path interpolation before hierarchical composition and skin palette construction. The display cache eases its gait weight over 80 ms and advances walk phase from speed and the game-owned stride length. It samples actual palettes and recomputes conservative bounds for both color and shadow culling. Nameplates use the same displayed position.
This is replicated cooperative locomotion, not trusted server-validated gameplay animation. Grounding selects a held airborne clip; a bounded two-layer blend combines idle/walk with that pose over 60 ms. A rider standing on a moving platform remains idle. Discrete grounding changes follow the same delayed relay timeline as position, rather than anticipating the next sample. Stalled motion settles to grounded idle after the existing extrapolation budget; fresh samples resume the current state. Clip phase and transition weights remain reconstructible renderer caches. Local saves retain player position and physics re-establishes grounding after load; full checkpoints retain the existing motor-state contract. Remote locomotion is ephemeral in RemoteActor@2, with an explicit identity migration from version 1 and no disconnected actors restored from saves.
The airborne clip adds no geometry or material changes. Both existing clips, meshes, joints, colors and resource census are unchanged; the user's requested character redesign remains deferred to final polish. General blend graphs, root motion, foot IK, inventory-driven attachments, timestamped action cues and reference-device performance acceptance remain open. After a render gap of at least half a second, the blend selects the current motion target and advances at most 100 ms of stride. The previous behavior reset the blend to idle and froze the stride on every slow frame, making sustained walking impossible below two rendered frames per second. The bounded step preserves a moving pose without replaying a long suspension; it does not promise real-time animation cadence on an overloaded GPU.
#Verification
Headless cases cover frame-count independence, yaw wrap, stale/duplicate rejection, capped extrapolation and storage, adaptive delay, stream resets, owned inputs, versioned presentation, palette-budget rejection, peer validation, clip endpoints, normalized blended rotations, idle settling and reproducible base art. Real socket tests also cover protocol-2 rejection, stamp ownership, large-angle normalization and new streams after durable room restart.
The multiplayer browser test uses a real local WebSocket proxy, redirects only the test worker's room URL, and drops pose packets while forwarding heartbeats and other room messages. It checks visible rig admission, walking, stalled idle, resumed motion and cache cleanup after departure. Page-level WebSocket routing did not intercept the worker socket in the initial attempt; that ineffective test harness is replaced rather than counted as a successful network fault injection. Existing shared interactions and device-loss recovery run against the same actor-enabled client.
The F2 inspector and exported report expose rigged/walking/stalled actor counts, retained samples and palette cost. These counters aid diagnosis; they are not GPU timing or a multi-user capacity benchmark.
Recorded acceptance distinguishes the full browser run (26 passed, one obsolete allocation assertion) from the two passing final-source follow-ups. Hardware performance and the earlier CI capture/presentation delays remain unverified.
#Isolated development playtest
With Node 22.23.4 and dependencies installed, run the development branch on its own ports and room folder:
DW_HTTP_PORT=5175 DW_RELAY_PORT=8789 \
DW_DATA_DIR=.data/traveler-preview \
DW_ALLOWED_ORIGINS=http://localhost:5175 \
npm run devOpen http://localhost:5175 in two separate browser profiles or independent private sessions. Enter the house in both, then walk, turn and stop in one while watching from the other. Check that nameplates follow the displayed character, that idle resumes when movement stops, and that leaving removes the character. F2 should show one rigged traveler in each client; after the other leaves, the actor and sample counts should return to zero. Shared doors, chat and the lantern pickup retain their existing behavior. This development command uses neither the running beta's port 8787 nor its room folder.
The slow-render regression withholds GPU completion notifications for 650 ms while a second real client walks back and forth inside the room. It checks a walking state under the delayed cadence and a return to idle after keyboard movement stops. A separate headless regression fails against the old gait logic and checks changing skin palettes, bounded stride advancement, long-gap idle and invalid cached gait state.
Slow-frame acceptance retains the failing pre-fix test, passing regression, full 298-case headless run and three browser scenarios.