tfrere HF Staff Cursor commited on
Commit
3ff9fc1
Β·
1 Parent(s): 0110fa1

feat: jukebox corner prop materializing with the ceremony

Browse files
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. assets/props/jukebox.glb +3 -0
  3. ceremony.js +65 -0
  4. index.html +1 -1
  5. rl.js +46 -0
.gitattributes CHANGED
@@ -37,3 +37,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
37
  *.wav filter=lfs diff=lfs merge=lfs -text
38
  *.png filter=lfs diff=lfs merge=lfs -text
39
  *.webp filter=lfs diff=lfs merge=lfs -text
 
 
37
  *.wav filter=lfs diff=lfs merge=lfs -text
38
  *.png filter=lfs diff=lfs merge=lfs -text
39
  *.webp filter=lfs diff=lfs merge=lfs -text
40
+ *.glb filter=lfs diff=lfs merge=lfs -text
assets/props/jukebox.glb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c57bd4676e7f3889692b1f9afe9e799fc4dfe2c97405e53cd1e5ed9b30d83059
3
+ size 1759760
ceremony.js CHANGED
@@ -38,6 +38,12 @@ export function createCeremony({
38
 
39
  let fxRig = null;
40
  let fxPrev = null;
 
 
 
 
 
 
41
  let entranceT0 = null;
42
  let entranceFxCued = false;
43
  let entranceDone = false;
@@ -60,6 +66,54 @@ export function createCeremony({
60
  bind();
61
  fx.start();
62
  fxPrev = performance.now();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  }
64
  function fxDrive() {
65
  if (fxPrev === null || fx.isDone()) return;
@@ -111,6 +165,7 @@ export function createCeremony({
111
  startCameraReset();
112
  syncRig();
113
  fx.setProgress(0);
 
114
  respawn = { t0: performance.now(), scanCued: false };
115
  }
116
 
@@ -131,6 +186,7 @@ export function createCeremony({
131
  function drive() {
132
  driveEntrance();
133
  driveRespawn();
 
134
  }
135
 
136
  function setReveal(floor, wall = floor) {
@@ -146,13 +202,21 @@ export function createCeremony({
146
  if (p >= 1) {
147
  fx.start();
148
  fxForceFinish();
 
149
  setLocked(false);
150
  entranceFinishedResolve();
151
  } else {
152
  fx.setProgress(p);
 
153
  }
154
  }
155
 
 
 
 
 
 
 
156
  // Boot hidden: clip parked below the feet from the first frame.
157
  bind();
158
 
@@ -164,6 +228,7 @@ export function createCeremony({
164
  bind,
165
  setReveal,
166
  setFx,
 
167
  entranceFinished,
168
  get entranceDone() { return entranceDone; },
169
  get respawnActive() { return respawn !== null; },
 
38
 
39
  let fxRig = null;
40
  let fxPrev = null;
41
+ // Decorative props (addPropFx): each owns a wireframe FX bound to its
42
+ // own root. They cue whenever the duck's scan cues (entrance and every
43
+ // respawn), offset by their stagger, and drive on their OWN clock so a
44
+ // late-finishing prop never extends the input lock or the ceremony's
45
+ // completion checks.
46
+ const props = []; // { fx, delayS, at, started, prev }
47
  let entranceT0 = null;
48
  let entranceFxCued = false;
49
  let entranceDone = false;
 
66
  bind();
67
  fx.start();
68
  fxPrev = performance.now();
69
+ propsCue();
70
+ }
71
+ function propsCue() {
72
+ const now = performance.now();
73
+ for (const p of props) {
74
+ p.at = now + p.delayS * 1000;
75
+ p.started = false;
76
+ p.prev = null;
77
+ }
78
+ }
79
+ function propsHide() {
80
+ for (const p of props) {
81
+ p.fx.setProgress(0);
82
+ p.at = null;
83
+ p.started = false;
84
+ }
85
+ }
86
+ function propsFinish() {
87
+ for (const p of props) {
88
+ if (!p.started) p.fx.start();
89
+ p.fx.update(1e3);
90
+ p.at = null;
91
+ }
92
+ }
93
+ // Deterministic screenshot/test hook (setFx): park every prop at the
94
+ // same scan progress as the duck, detached from the timed driver.
95
+ function propsSetProgress(v) {
96
+ for (const p of props) {
97
+ p.fx.setProgress(v);
98
+ p.at = null;
99
+ p.started = false;
100
+ }
101
+ }
102
+ function driveProps() {
103
+ const now = performance.now();
104
+ for (const p of props) {
105
+ if (p.at === null || now < p.at) continue;
106
+ if (!p.started) {
107
+ p.fx.start();
108
+ p.started = true;
109
+ p.prev = now;
110
+ continue;
111
+ }
112
+ if (p.fx.isDone()) continue;
113
+ const dt = Math.min((now - p.prev) / 1000, 0.25);
114
+ p.prev = now;
115
+ p.fx.update(dt);
116
+ }
117
  }
118
  function fxDrive() {
119
  if (fxPrev === null || fx.isDone()) return;
 
165
  startCameraReset();
166
  syncRig();
167
  fx.setProgress(0);
168
+ propsHide();
169
  respawn = { t0: performance.now(), scanCued: false };
170
  }
171
 
 
186
  function drive() {
187
  driveEntrance();
188
  driveRespawn();
189
+ driveProps();
190
  }
191
 
192
  function setReveal(floor, wall = floor) {
 
202
  if (p >= 1) {
203
  fx.start();
204
  fxForceFinish();
205
+ propsFinish();
206
  setLocked(false);
207
  entranceFinishedResolve();
208
  } else {
209
  fx.setProgress(p);
210
+ propsSetProgress(p);
211
  }
212
  }
213
 
214
+ // Register a decorative prop's wireframe FX (already init'd on its own
215
+ // root, hidden). It materializes delayS after each duck scan cue.
216
+ function addPropFx(propFx, delayS = 0) {
217
+ props.push({ fx: propFx, delayS, at: null, started: false, prev: null });
218
+ }
219
+
220
  // Boot hidden: clip parked below the feet from the first frame.
221
  bind();
222
 
 
228
  bind,
229
  setReveal,
230
  setFx,
231
+ addPropFx,
232
  entranceFinished,
233
  get entranceDone() { return entranceDone; },
234
  get respawnActive() { return respawn !== null; },
index.html CHANGED
@@ -1376,7 +1376,7 @@
1376
  appended: a plain src attribute can 401 when the browser blocks the
1377
  *.static.hf.space auth cookie inside the hub iframe. -->
1378
  <script type="module">
1379
- const V = "68"; // bump on rl.js changes to defeat heuristic caching
1380
  const sign = new URLSearchParams(location.search).get("__sign");
1381
  const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
1382
  import(`./rl.js?v=${V}${suffix}`);
 
1376
  appended: a plain src attribute can 401 when the browser blocks the
1377
  *.static.hf.space auth cookie inside the hub iframe. -->
1378
  <script type="module">
1379
+ const V = "70"; // bump on rl.js changes to defeat heuristic caching
1380
  const sign = new URLSearchParams(location.search).get("__sign");
1381
  const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
1382
  import(`./rl.js?v=${V}${suffix}`);
rl.js CHANGED
@@ -25,6 +25,7 @@
25
  import * as THREE from "three";
26
  import { OrbitControls } from "three/addons/controls/OrbitControls.js";
27
  import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
 
28
  import loadMujoco from "https://cdn.jsdelivr.net/npm/@mujoco/mujoco@3.11.0/mujoco.js";
29
  import * as ort from "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.27.0/dist/ort.min.mjs";
30
 
@@ -1359,6 +1360,50 @@ ball = createBallActor({
1359
  THREE, scene, camera, renderer, fxModule: fx, mesh: ballMesh, group: ballGroup,
1360
  });
1361
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1362
  // Comic sticker popups on game events (kick / quack / roll / ball spawn /
1363
  // ghost join). Fully self-contained DOM overlay: delete this import to
1364
  // remove the feature (the `stickers?.pop` hooks then no-op).
@@ -2129,6 +2174,7 @@ window.rl = {
2129
  get ballQposAdr() { return ballQposAdr; },
2130
  get chaseCam() { return chaseCam; },
2131
  set chaseCam(v) { chaseCam = !!v; },
 
2132
  get camResetActive() { return camResetT0 !== null; },
2133
  get respawnActive() { return ceremony?.respawnActive ?? false; },
2134
  get camPose() {
 
25
  import * as THREE from "three";
26
  import { OrbitControls } from "three/addons/controls/OrbitControls.js";
27
  import { RoomEnvironment } from "three/addons/environments/RoomEnvironment.js";
28
+ import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
29
  import loadMujoco from "https://cdn.jsdelivr.net/npm/@mujoco/mujoco@3.11.0/mujoco.js";
30
  import * as ort from "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.27.0/dist/ort.min.mjs";
31
 
 
1360
  THREE, scene, camera, renderer, fxModule: fx, mesh: ballMesh, group: ballGroup,
1361
  });
1362
 
1363
+ // ── Jukebox prop (decorative corner dressing, no physics) ────────────────
1364
+ // assets/props/jukebox.glb carries two meshes exported from Blender: the
1365
+ // ~39k-tri render mesh and a 500-tri "jukebox_wire" stand-in used only by
1366
+ // the hologram pass (same fxWireGeometry escape hatch as the ball - the
1367
+ // full mesh reads as a solid glowing blob). It materializes with the
1368
+ // duck's ceremony, staggered a beat behind, and replays on every respawn.
1369
+ const JUKEBOX_H = 0.42; // target height, m: playground-prop scale next to the 0.25 m duck
1370
+ const JUKEBOX_MARGIN = 0.24; // center distance from each wall inner face
1371
+ let jukeboxGroup = null; // exposed on window.rl for placement tweaks
1372
+ try {
1373
+ const gltf = await new GLTFLoader().loadAsync(
1374
+ signed(`./assets/props/jukebox.glb?v=${SELF_V}`),
1375
+ );
1376
+ const jukebox = gltf.scene;
1377
+ const jukeWire = jukebox.getObjectByName("jukebox_wire");
1378
+ jukeWire.removeFromParent();
1379
+ // Normalize whatever scale/centering the export carries: JUKEBOX_H tall,
1380
+ // resting on the floor. The wire proxy shares the render mesh's local
1381
+ // space, so it needs no transform of its own.
1382
+ const jukeBox3 = new THREE.Box3().setFromObject(jukebox);
1383
+ const jukeScale = JUKEBOX_H / (jukeBox3.max.y - jukeBox3.min.y);
1384
+ jukebox.scale.setScalar(jukeScale);
1385
+ jukebox.position.y = -jukeBox3.min.y * jukeScale;
1386
+ jukebox.traverse((o) => {
1387
+ if (o.isMesh) o.userData.fxWireGeometry = jukeWire.geometry;
1388
+ });
1389
+ const jukeGroup = new THREE.Group();
1390
+ jukeGroup.add(jukebox);
1391
+ // Back corner, behind the spawn (the duck faces +X), angled toward the
1392
+ // center of the arena so the front panel reads from the play area.
1393
+ jukeGroup.position.set(
1394
+ -(ARENA_HALF - JUKEBOX_MARGIN), 0, -(ARENA_HALF - JUKEBOX_MARGIN),
1395
+ );
1396
+ jukeGroup.rotation.y = Math.PI / 4;
1397
+ scene.add(jukeGroup);
1398
+ jukeboxGroup = jukeGroup;
1399
+ const jukeFx = fx.createWireframeFx();
1400
+ jukeFx.init({ THREE, scene, root: jukeGroup, camera, renderer, hidden: true });
1401
+ ceremony.addPropFx(jukeFx, 0.25);
1402
+ } catch (err) {
1403
+ // Decorative only: a missing/broken GLB must never halt the boot.
1404
+ console.warn("[rl] jukebox prop disabled:", err);
1405
+ }
1406
+
1407
  // Comic sticker popups on game events (kick / quack / roll / ball spawn /
1408
  // ghost join). Fully self-contained DOM overlay: delete this import to
1409
  // remove the feature (the `stickers?.pop` hooks then no-op).
 
2174
  get ballQposAdr() { return ballQposAdr; },
2175
  get chaseCam() { return chaseCam; },
2176
  set chaseCam(v) { chaseCam = !!v; },
2177
+ get jukebox() { return jukeboxGroup; },
2178
  get camResetActive() { return camResetT0 !== null; },
2179
  get respawnActive() { return ceremony?.respawnActive ?? false; },
2180
  get camPose() {