// Cutscene layer: page-load entrance and every later respawn. // // Physics (resetSim) snaps the duck to the stand keyframe; this module // owns what the player sees around that snap. Two mutually exclusive // timelines share the duck's wireframe FX: // // entrance (once) grid + walls draw in, then the duck scans up // respawn (every kill / Space / loco switch) // 0.00 duck clipped away, camera glides home from the kill view // 0.80 camera almost settled -> duck scan-up cues // 0.90 camera lands // 1.70 scan done, inputs unlock // // rl.js injects the world (rig, grid, camera reset) and the lock/flash // callbacks; this file never touches MuJoCo. export const CAM_RESET_S = 0.9; export const RESPAWN_SCAN_AT = 0.8; // cue scan-up at 80% of the camera glide const ENTRANCE_GRID_S = 1.1; const ENTRANCE_WALL_DELAY_S = 0.35; const ENTRANCE_WALL_S = 0.9; const ENTRANCE_FX_START_S = 0.6; const clamp01 = (x) => Math.min(Math.max(x, 0), 1); export function createCeremony({ THREE, scene, camera, renderer, fx, getRig, grid, wallMats, syncRig, startCameraReset, setLocked, flashReset, }) { const ENTRANCE_TOTAL_S = Math.max( ENTRANCE_GRID_S, ENTRANCE_WALL_DELAY_S + ENTRANCE_WALL_S, ENTRANCE_FX_START_S + fx.TOTAL_S, ); let fxRig = null; let fxPrev = null; let entranceT0 = null; let entranceFxCued = false; let entranceDone = false; let respawn = null; // { t0, scanCued } | null let entranceFinishedResolve; const entranceFinished = new Promise((r) => { entranceFinishedResolve = r; }); function bind() { const rig = getRig(); if (fxRig === rig) return; fx.dispose(); fx.init({ THREE, scene, rig, camera, renderer }); rig.placer.traverse((o) => { if (o.isMesh && !o.userData.meshName) o.userData.fxOverlay = true; }); fxRig = rig; } function fxStart() { bind(); fx.start(); fxPrev = performance.now(); } function fxDrive() { if (fxPrev === null || fx.isDone()) return; const now = performance.now(); const dt = Math.min((now - fxPrev) / 1000, 0.25); fxPrev = now; fx.update(dt); } function fxForceFinish() { if (!fx.isDone()) fx.update(1e3); } function startEntrance() { if (entranceT0 !== null || entranceDone) return; entranceT0 = performance.now(); entranceFxCued = false; grid.material.uniforms.uReveal.value = 0; for (const m of wallMats) m.uniforms.uReveal.value = 0; } function driveEntrance() { if (entranceT0 === null) return; const t = (performance.now() - entranceT0) / 1000; grid.material.uniforms.uReveal.value = clamp01(t / ENTRANCE_GRID_S); const wallR = clamp01((t - ENTRANCE_WALL_DELAY_S) / ENTRANCE_WALL_S); for (const m of wallMats) m.uniforms.uReveal.value = wallR; if (t >= ENTRANCE_FX_START_S && !entranceFxCued) { entranceFxCued = true; fxStart(); } fxDrive(); if (t >= ENTRANCE_TOTAL_S) { if (!fx.isDone() && t < ENTRANCE_TOTAL_S + 0.5) return; fxForceFinish(); entranceT0 = null; entranceDone = true; grid.material.uniforms.uReveal.value = 1; for (const m of wallMats) m.uniforms.uReveal.value = 1; setLocked(false); entranceFinishedResolve(); } } function playRespawn() { if (!entranceDone) return; setLocked(true); flashReset(); bind(); startCameraReset(); syncRig(); fx.setProgress(0); respawn = { t0: performance.now(), scanCued: false }; } function driveRespawn() { if (!respawn) return; const u = (performance.now() - respawn.t0) / 1000 / CAM_RESET_S; if (!respawn.scanCued && u >= RESPAWN_SCAN_AT) { respawn.scanCued = true; fxStart(); } if (respawn.scanCued) fxDrive(); if (respawn.scanCued && fx.isDone()) { respawn = null; setLocked(false); } } function drive() { driveEntrance(); driveRespawn(); } function setReveal(floor, wall = floor) { entranceT0 = null; grid.material.uniforms.uReveal.value = floor; for (const m of wallMats) m.uniforms.uReveal.value = wall; } function setFx(p) { entranceT0 = null; respawn = null; bind(); if (p >= 1) { fx.start(); fxForceFinish(); setLocked(false); entranceFinishedResolve(); } else { fx.setProgress(p); } } // Boot hidden: clip parked below the feet from the first frame. bind(); return { CAM_RESET_S, startEntrance, playRespawn, drive, bind, setReveal, setFx, entranceFinished, get entranceDone() { return entranceDone; }, get respawnActive() { return respawn !== null; }, }; }