The owned Rapier adapter now sets the current integration duration before computing the character sweep. Rapier 0.21.0's computeColliderMovement reads IntegrationParameters.dt when resolving moving obstacles. Previously the adapter set that value only after the sweep, so the rider used the previous duration while the world used the new one.
The elevator regression settles a capsule on a four-metre-wide platform, then starts an upward velocity of 0.6 metres/second. Across all sixteen transitions between 120, 60, 30 and 15 Hz, the first step must preserve rider-to-platform clearance within 20 micrometres. Before the correction, switching from 120 to 30 Hz moved the rider 5 mm while the platform moved 20 mm. The corrected step moves both 20 mm. This fixture tests the adapter's duration parameter; it does not change the application's fixed simulation clock or establish a variable-frame-rate gameplay contract.
A second fixture restores the native snapshot immediately before that transition, checks the corrected displacement, then compares every native checkpoint across ninety further steps with varying durations. That is same-runtime replay evidence. It does not establish cross-browser or cross-platform determinism.
#Reproduction
Run with the repository's supported Node version:
node --import tsx --test tests/physics-motor.test.ts
node --import tsx tools/cli/probe-platforms.tsThe probe prints measurements, not a pass/fail declaration. It compares raw native position-driven and velocity-driven bodies with owned platforms moving along X, Y and Z, with fixed and varying durations. It reports travel, rider drift, minimum capsule clearance and airborne ticks. These adapter fixtures are separate from the canonical integration below.
The corrected implementation still exhibits horizontal slip. In the four-metre-wide position-driven fixture, a platform travels 1.8 m over three seconds at 30 Hz while the rider loses about 0.239 m along X and 0.105 m along Z. An upward position-driven platform retains a roughly 20 mm lag. Native carry alone is therefore insufficient for the planned relative-transform support contract. Adding the entire platform displacement on top of native carry would also risk double movement.
#Owned platform carry
PhysicsScene.createPlatform, setPlatformTarget, removePlatform, platformStates and supportedPlatform now provide an owned adapter path for horizontal box platforms. A target contains a position and yaw; the player stays upright. Targets are copied and bounded to 0.5 metres and 22.5 degrees per tick, with at most 32 registered platforms. State reads return owned values, including the actual native pose, pending target and blocked state.
The adapter uses velocity-based kinematic bodies. Position-based bodies ignore direct velocity clearing, as confirmed by the pinned runtime experiment; using those bodies would leave native carry active and apply movement twice. Each tick clears owned platform velocity before the locomotion sweep, identifies support at the resulting position, carries the rider through that support's relative transform, and then drives the native platform toward its target. Only registered platforms use this path; arbitrary bodies added through the native world retain Rapier's behavior.
Carry is swept against other colliders. Rotation uses short chords inflated by their maximum arc deviation, with a 128-sweep work budget. An obstruction or exhausted carry budget stops that platform for the tick and leaves its target pending. A 1 mm reserve above the controller offset prevents float32 contact rounding from repeatedly stopping tangential walking. Jumping releases carry immediately without adding platform momentum; walking off, removal and teleport clear support. This is the current motor policy, not the full world-configurable movement system.
Seven focused cases cover starts/reversals and changing tick durations on all axes, an off-centre full turn, walking speed and departure, jump departure, a ceiling obstruction, an intermediate rotation obstacle, repeated mid-motion restore, removal, and invalid command/binding rejection. Translational and rotational relative-position assertions use a 2 mm tolerance. The straight-line probe measures less than 0.01 mm final axial drift in its six owned cases; that measurement applies only to this fixture and runtime.
Owned-platform scenes emit physics checkpoint format 2 with profile [email protected]/character-platforms.1. It includes stable IDs, opaque native body/collider bindings, pending poses, blocked flags and support identity. Restore validates native body kind, box shape, local pose, unique bindings, bounded target and actual contact for the claimed support. Y-up character control is required. Scenes that never create an owned platform still emit the unchanged format-1/profile-1 checkpoint; old replay traces are not regenerated. Removing all platforms retains format 2 so a scene does not silently change profile.
#Canonical runtime integration
Simulation.createPlatform, setPlatformTarget and removePlatform now own the bridge between native platforms and canonical entities. Creation takes an already allocated spawned: UUID, position, yaw, size and a material ID from the compiled catalog. Allocate the ID once at trusted ingress with allocateSpawnedEntityId, then retain the complete command for replay. The simulation does not generate entropy while executing a command.
The entity uses Transform, Collider, RigidBody, KinematicPlatform, PlaceMembership, RenderMesh, MaterialRef and PersistentIdentity. Collision uses the explicit physics:platform-box:1 binding; visual geometry uses the separate compiled box mesh. Actual native pose/size and velocity are published with the player's pose and CharacterMotor.platform in the same ECS transaction. The support value is the persistent platform entity ID. The ordinary ECS presentation path supplies rendered matrices; there is no separate platform renderer channel.
KinematicPlatform retains target position/yaw and the blocked flag. Active collider, pose, motor-support and component dependencies must agree with native state. Direct conflicting ECS edits, parented/animated platform roots, missing bindings and mismatched restores are rejected. Child transform attachments can follow a platform; removal rejects a dangling attachment. The 32-platform limit applies per place. Native restore also checks the fixed collision friction, restitution and groups.
Creation/removal stage an independent native scene before committing canonical changes. A rejected commit disposes the candidate and leaves the existing physics snapshot, entity handles and journal unchanged. Target changes restore the previous native target if their ECS commit is rejected. These command guarantees do not extend to general rollback of a failed simulation tick: the existing scheduler treats a system failure as an unhealthy boundary and prevents subsequent valid checkpoints.
Departure parks the old place's platform velocities and clears its transient blocked status while retaining pose and pending target. Destination collision is built from its canonical rows before publishing the place transition. Returning rebuilds the platform at its retained pose and resumes the pending target on the next active tick. Pausing does not advance platforms. Runtime commands address spawned platforms in the active place; authored routes are described below. Multiplayer command authority remains open.
The eighteenth saved built-in component requires simulation checkpoint profile 6. Profiles 1–5 migrate explicitly; existing component descriptors, entity values and native bytes remain unchanged during catalog migration. A genuine profile-5 checkpoint and its next boundary are retained as migration fixtures. The replay test also converts every new platform-free checkpoint envelope back to profile 5 and verifies all 426 historical hashes, including the 26 restore boundaries. Historical fixtures remain intact; new replay, hierarchy, authoring and prefab references have separate versions. Lightweight local player saves remain format 2 and do not persist runtime-created platforms; use the full simulation checkpoint for this capability.
Seven canonical cases cover native/presentation alignment, support, pending-target restore/replay, forged bindings, rejected commands and direct edits, attachment-aware removal, place departure/return, jump release, blocked carry, pause and actual legacy migration. A browser-worker renderer fixture checks visible creation, moved pixels, exact restored pixels, removed geometry and released resource ownership. These fixtures are development diagnostics, not authored additions to Darkwind.
#Remaining implementation
Application input/worker commands for runtime platform creation, lightweight persistence for runtime-created platforms, shared authority/replication and a playable moving-platform journey remain open. Authored route phases now have the lightweight save path described below. The shipped world has no moving platforms. The existing player tests verify unchanged gameplay; the dedicated Observatory fixture exercises rendered platforms and authored routes.
Pitch/roll, platform-versus-world and platform-versus-platform collision policy, non-rider crushing, inherited jump momentum, and safe creation/streaming under a character remain open. The carry sweep protects the supported rider; it is not a general kinematic body solver. Fixtures create their platforms before settling the character, and do not establish runtime floor-loading acceptance. Browser/native parity and reference-hardware acceptance are also unproven. Milestone 3 remains open.
The timestep correction does not add checkpoint fields or change the pinned native profile. Existing fixed-step replay fixtures must remain byte-identical; the historical traces are not regenerated to accept this fix. Moving-body behavior after a duration transition is deliberately corrected, so the old erroneous displacement is not a compatibility guarantee.
Canonical integration acceptance records 315 passing headless checks, eight final native follow-ups, type/import validation and four browser scenarios. The browser capability report includes the rendered platform fixture and the complete Node/Chromium profile-6 replay comparison.
#Authored platform routes
World format 4 and compiler route-world.4 accept a platform property on a visible box with collider: true. The first route point must equal the object's initial position and yaw. Roots remain upright; size is 0.05–100 metres per axis, with at most 32 platforms per place. A route has 2–32 absolute place-local poses, secondsPerLeg (0.1–3,600), pauseSeconds (0–3,600), and mode loop, ping-pong or once. Each adjacent leg must move or turn and stays within 6 metres/second and π radians/second. Loop validation includes the closing leg. Yaw follows the shortest arc using bounded arithmetic, preserving authored endpoints without cross-runtime trigonometric rounding in stored targets.
"platform": {
"mode": "ping-pong",
"secondsPerLeg": 2,
"pauseSeconds": 1,
"points": [
{"position": [0, 0.8, 0], "yaw": 0},
{"position": [2, 1.2, 0], "yaw": 0.4}
]
}Each waypoint pauses before departure. A one-way route holds its final pose. A ping-pong route reverses through the intermediate points without teleporting. Motion advances by simulation time, with at most one leg per motor tick and the adapter's translation/rotation budget. Unused time at a corner or motion cap is discarded; it is not accumulated for a later catch-up jump. Consequently coarse time steps can lengthen a journey. Paused simulations and inactive places do not advance route time.
The compiler emits the persistent authored identity, separate box collision/visual bindings, and a PlatformRoute time component. Authored-route transforms project the native float32 quaternion directly; reconstructing it through JavaScript trigonometry produced a one-ulp Node/Chromium mismatch in the initial experiment. Manual platforms retain their existing profile-6 representation for exact migration. The physics adapter omits the object's fixed collider; the simulation builds its owned kinematic platform instead. Route targets are prepared before the motor sweep. Actual pose, rider support, native body state and successful route time are published in one ECS transaction. A blocked rider leaves both platform pose and route time at the previous boundary; jumping releases support and permits the route to resume. This retains the existing rider-only obstruction policy, not general platform-versus-world collision.
Route definitions belong to immutable compiled content. Native/canonical agreement and route phase/pose agreement are checked during commits and restore, allowing a bounded float32 position tolerance (at least 20 micrometres, scaled to world coordinate magnitude) and 20 microradians for yaw. Conflicting root animation, parent, door/item/material conditions, hidden geometry or mismatched initial poses reject. Direct setPlatformTarget rejects route-controlled entities. The authoring API/CLI can create a routed box or set its platform property, upgrading to format 4 without replacing its identity; setting that property to null removes the route from the authored source. Route edits require rebuilding the world; live activation and prefab route expansion remain open.
Simulation profile 7 adds the nineteenth saved component and explicitly migrates profiles 1–6. The genuine profile-6 fixture retains a supported rider and pending manual target; migration preserves its native/gameplay payload and next tick. Full checkpoints retain route time, including blocked and inactive-place boundaries. The original lightweight format-2 save did not retain route phases; the format-3 path below supersedes that limitation for authored routes. Runtime-created platforms still require a full checkpoint, and the multiplayer protocol does not replicate platform authority. This feature is therefore an engine fixture, not a new shared-beta journey.
The platform-routes.1 Engine Zoo world exercises translation, elevation, yaw, waits and return travel. Its native replay pins 420 tick boundaries and seven restores over a complete ping-pong cycle. The Observatory additionally renders changed route pixels and exact restored pixels, then compares the whole replay result with Node. Historical platform-free traces remain pinned separately; no old reference is overwritten.
Authored-route acceptance and the capability report record the 324-check headless suite, CLI round-trip, and exact Node/Chromium replay plus rendered restoration. This is scoped parity on the recorded engines, not universal cross-device determinism.
#Lightweight authored-route saves
Simulation.save() emits player-save format 3 for worlds containing authored routes. It includes the normal player progress, compiled world hash and a complete, sorted list of persistent route IDs and accepted times across every place. The list is bounded to 1,024 entries (32 routes × 32 places). Validation rejects duplicate, missing, unknown and malformed entries, invalid phases and mismatched content. A same-release content edit cannot silently relocate a saved platform. Worlds without authored routes continue to emit format 2. Formats 1/2 retain their migration behavior and explicitly begin authored routes at time zero.
Restore rebuilds authored transforms and native boxes from each saved route phase. Active support geometry is installed before the initial native physics/query update, so the first resumed controller sweep sees the floor. Inactive route clocks remain parked. The player resumes paused; support and obstruction are recomputed on the first motor step. Blocked routes save their last accepted phase, so reconstructing the floor cannot advance past the obstruction during startup. Lightweight saves reset transient motor velocity and do not claim exact native replay; full simulation profile 7 and its historical fixtures remain unchanged.
The production simulation worker attaches route saves to the corresponding accepted UI frame and ready message. The page writes that matched envelope to darkwind:lantern-house:save:v3, preferring it on reload while preserving older v1/v2 keys. Worlds without routes retain the v2 path. Reset removes all three local save generations. Scene-buffer pressure and GPU readiness remain independent from this bounded state/save delivery. Shared platform authority, runtime-created platform persistence and moving-platform content in Darkwind remain separate requirements.
Route-save acceptance records 339 passing headless checks and seven final browser cases. Worker evidence validates accepted state/save boundaries through JSON storage and a new production simulation worker. Renderer evidence records identical pixels after lightweight restoration, rider support on resume and the unchanged full native replay. Legacy formats, invalid/duplicate/missing phases, stale content, inactive places and blocked riders have focused coverage.