tfrere HF Staff Cursor commited on
Commit
f402bd3
·
1 Parent(s): 53d6d58

fix: harden mode state machine + per-duck randomized chirps

Browse files

State machine:
- sit/stand hand-over timers are now cancellable and cleared by every
transition entry point (setMode, triggerRoll, resetSim); a stale 2 s
stand-up timer could previously flip a sitting or rolling duck to the
walking policy mid-motion
- resetSim cancels any roll in flight and pending timers, always lands
on walk (R mid-roll no longer replays the roll on the reset duck)
- rolls only launch from a standing walk, not from a sit
- RT quack uses a Schmitt trigger (fire at 0.35, re-arm below 0.2):
analog jitter around a single threshold caused multiple quacks

Sound:
- ship the runtime's four chirp voice banks (12 takes each); each
colourway has its own duck voice and every quack picks a random take

Co-authored-by: Cursor <cursoragent@cursor.com>

Files changed (50) hide show
  1. assets/{quack.wav → voices/duck1/chirp_a.wav} +2 -2
  2. assets/voices/duck1/chirp_b.wav +3 -0
  3. assets/voices/duck1/chirp_c.wav +3 -0
  4. assets/voices/duck1/chirp_d.wav +3 -0
  5. assets/voices/duck1/chirp_e.wav +3 -0
  6. assets/voices/duck1/chirp_f.wav +3 -0
  7. assets/voices/duck1/chirp_g.wav +3 -0
  8. assets/voices/duck1/chirp_h.wav +3 -0
  9. assets/voices/duck1/chirp_i.wav +3 -0
  10. assets/voices/duck1/chirp_j.wav +3 -0
  11. assets/voices/duck1/chirp_k.wav +3 -0
  12. assets/voices/duck1/chirp_l.wav +3 -0
  13. assets/voices/duck2/chirp_a.wav +3 -0
  14. assets/voices/duck2/chirp_b.wav +3 -0
  15. assets/voices/duck2/chirp_c.wav +3 -0
  16. assets/voices/duck2/chirp_d.wav +3 -0
  17. assets/voices/duck2/chirp_e.wav +3 -0
  18. assets/voices/duck2/chirp_f.wav +3 -0
  19. assets/voices/duck2/chirp_g.wav +3 -0
  20. assets/voices/duck2/chirp_h.wav +3 -0
  21. assets/voices/duck2/chirp_i.wav +3 -0
  22. assets/voices/duck2/chirp_j.wav +3 -0
  23. assets/voices/duck2/chirp_k.wav +3 -0
  24. assets/voices/duck2/chirp_l.wav +3 -0
  25. assets/voices/duck3/chirp_a.wav +3 -0
  26. assets/voices/duck3/chirp_b.wav +3 -0
  27. assets/voices/duck3/chirp_c.wav +3 -0
  28. assets/voices/duck3/chirp_d.wav +3 -0
  29. assets/voices/duck3/chirp_e.wav +3 -0
  30. assets/voices/duck3/chirp_f.wav +3 -0
  31. assets/voices/duck3/chirp_g.wav +3 -0
  32. assets/voices/duck3/chirp_h.wav +3 -0
  33. assets/voices/duck3/chirp_i.wav +3 -0
  34. assets/voices/duck3/chirp_j.wav +3 -0
  35. assets/voices/duck3/chirp_k.wav +3 -0
  36. assets/voices/duck3/chirp_l.wav +3 -0
  37. assets/voices/duck4/chirp_a.wav +3 -0
  38. assets/voices/duck4/chirp_b.wav +3 -0
  39. assets/voices/duck4/chirp_c.wav +3 -0
  40. assets/voices/duck4/chirp_d.wav +3 -0
  41. assets/voices/duck4/chirp_e.wav +3 -0
  42. assets/voices/duck4/chirp_f.wav +3 -0
  43. assets/voices/duck4/chirp_g.wav +3 -0
  44. assets/voices/duck4/chirp_h.wav +3 -0
  45. assets/voices/duck4/chirp_i.wav +3 -0
  46. assets/voices/duck4/chirp_j.wav +3 -0
  47. assets/voices/duck4/chirp_k.wav +3 -0
  48. assets/voices/duck4/chirp_l.wav +3 -0
  49. index.html +1 -1
  50. rl.js +57 -14
assets/{quack.wav → voices/duck1/chirp_a.wav} RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:71c1a42876e55ade7e0dc72a5b2d68244bb192a3bf24caf18cc8645eece6bdee
3
- size 46840
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:450d807c1e6f535f30a1e50491df9b57567b8b8be357148de731de8b7582c8c2
3
+ size 5336
assets/voices/duck1/chirp_b.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:660fa601e43eb7f05e67903446ccb11b9205bec31fdf4695a824c885e1da621e
3
+ size 5510
assets/voices/duck1/chirp_c.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2451a1286f28eaa5f54875c54486e03dd234c425c70c62ecc35c37f1bc745fc8
3
+ size 8658
assets/voices/duck1/chirp_d.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:070253cd8fd2323226677935a6a7f6bb664e556a2292493e9269c8bc50cda188
3
+ size 11002
assets/voices/duck1/chirp_e.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f0225bf39edae00420250f26610b923ca369eacaae97d775ca6e0d68d0789f2
3
+ size 7214
assets/voices/duck1/chirp_f.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:41bb6b9105669e0d297ab4e396491ff4798d0092c9f67911858cc970bb6e2fee
3
+ size 6460
assets/voices/duck1/chirp_g.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c33fe26fc9491564c12a2ab4d49da7d5502d02e097067e1ca7b5e48078a5f2c8
3
+ size 11068
assets/voices/duck1/chirp_h.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:47a7a3b9d6e06b601daaa47c3426c5dab963c9a05bd9878ca39155fbe7a55f8b
3
+ size 10512
assets/voices/duck1/chirp_i.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0114c825dec129ae02fbd2aacf0595497d919108865e25e57b6610dd2f41e270
3
+ size 6578
assets/voices/duck1/chirp_j.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3a7a18a47c5a18d9fd21a78ea25dcb657feb7460680171357c5f123fc05ec454
3
+ size 7278
assets/voices/duck1/chirp_k.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bd64c51c26fe8f311e41ef9e949ca5a47718b529ce973d32d9490b2a350c89db
3
+ size 9542
assets/voices/duck1/chirp_l.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cecbff6accd2f0d525d302db9c6582d695e31e907bcee14bb82ede4db1d204e0
3
+ size 8632
assets/voices/duck2/chirp_a.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f7aa1decf578eab8c05b1fc2ed5205439d19541bf270d26762440d0a8ca0aa9
3
+ size 8702
assets/voices/duck2/chirp_b.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2fa61541951c240fa9e94cfb3582bf61f6a57254b1233e6c7342b852474bd33e
3
+ size 7032
assets/voices/duck2/chirp_c.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4ae92893fe916f96629c26ded93e71855fd0cee490c1690027ce0a19b3a29284
3
+ size 16544
assets/voices/duck2/chirp_d.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:971282d4fa0ab372713bc42bc510d03d6f033e8760e7592658cfec8c4ceace71
3
+ size 10252
assets/voices/duck2/chirp_e.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a1d662e8b23203d5622984e698e9ed67a749133416fee6ddc59a3552675259b9
3
+ size 8972
assets/voices/duck2/chirp_f.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d340696b480ae7cc2cc5b85de00741e86bd29ce766ad8eaa5f305726a079ae35
3
+ size 8576
assets/voices/duck2/chirp_g.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d79c3369e113a4323ec8afe3a4ca22b7a47e3867266af233974ff423d8fc134a
3
+ size 13824
assets/voices/duck2/chirp_h.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b04e48823c5b41f61c60633644bb3ffbf20611de9dcdc9bcdaff3c02ed5b2b5b
3
+ size 13046
assets/voices/duck2/chirp_i.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d34507b77bbab2a1ca9a467c19089e4605c6836194bc4a56b2dbd1890ad0f487
3
+ size 5716
assets/voices/duck2/chirp_j.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e7c217684a7fb0f27acb3a06052c784f3a66f775122bb368c2be4c27a50ac3f2
3
+ size 9098
assets/voices/duck2/chirp_k.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:00d2a8938313ea05addc67743d36dd21ba466601817c7ad891bd0ea15dfc1ec2
3
+ size 11676
assets/voices/duck2/chirp_l.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a1fc77cdb7c00e5ed7ebe618452c11c517b7f3f647dfaa70566896ec2bfae084
3
+ size 13124
assets/voices/duck3/chirp_a.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4ed6496a3208db4057f1547e2cc5c18a078d4200abdd1b564d7a502640448854
3
+ size 5896
assets/voices/duck3/chirp_b.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b701381ae6aaa98a46114ad96847fcd4b087a4b3f1f02b5f1f2bbda7219dd183
3
+ size 8448
assets/voices/duck3/chirp_c.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f4e678fe44d371931ce323495a2e9bf9957fda5a382526acc58c1a82cc3e9eef
3
+ size 13430
assets/voices/duck3/chirp_d.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6f52cab48cb6eb9566e40da17023d8365064848746be2b444786431e8d8bd092
3
+ size 12864
assets/voices/duck3/chirp_e.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:95f839d2c444acfa5bf823d9b8c65e84774f82650f4d8e3b5c7aa80cd4f242a0
3
+ size 5496
assets/voices/duck3/chirp_f.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f6e581b10f2b4e81f13ffa186f9761285e581dda161614008a922ed59f46079a
3
+ size 6528
assets/voices/duck3/chirp_g.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f78916cbdc2065d914f20c509ef0157500d14d386c1d8e2be00c018a99a8e942
3
+ size 17304
assets/voices/duck3/chirp_h.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c9a4c5a0b36cc2e684b7e00e95f9e6ec02f6dc2799337a2cddb1e791f84278e2
3
+ size 12524
assets/voices/duck3/chirp_i.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4dc2ef2ce703297da12fbe5f9722236df4a5faa907142cfc00e56970c75bc445
3
+ size 8418
assets/voices/duck3/chirp_j.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:97931d92be70e1448d6bed56515b8ffb1a439a6e44ae6c35b8c5e3ae7b400c00
3
+ size 5182
assets/voices/duck3/chirp_k.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5d68b91b7b6ae44a24b693183587dd27f1c8d8e9c6fd94e0f5c7c0931d9f98f0
3
+ size 17800
assets/voices/duck3/chirp_l.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0f874aacabad93a0f706695874260dc421b3624e716ba4bfbc3e83e5ba840af6
3
+ size 13884
assets/voices/duck4/chirp_a.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:356abe1da47758ab6df17954975123c53be79a9bcc919b5adf06d7ac8a602fb2
3
+ size 7350
assets/voices/duck4/chirp_b.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:63c6dba54db0a001e7ed9376cdccb9bd47a9e255fa7fd909e2505dc9fd332193
3
+ size 9116
assets/voices/duck4/chirp_c.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:22804c3f59ff7eccef58aacbfd855fe9808f35cbe21ff4cd72264c2e30fd156a
3
+ size 13554
assets/voices/duck4/chirp_d.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8158e8e9d118037bd03bed4d564dd9e70b07ade5acf8cb56b7d52cbe77dc53cd
3
+ size 11580
assets/voices/duck4/chirp_e.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:836664d06509dbe43daddef6bd3415372774158145d950139b0e30f4057be41a
3
+ size 8552
assets/voices/duck4/chirp_f.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2839e0865a5e42aff830b2d1ccf113fba60c28e403c42886ba1b1e5bd470ce52
3
+ size 7752
assets/voices/duck4/chirp_g.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3ba26ccde31aafa4d43df34058da9a7b53aacdeaa847b2a9f12660b31a022a1e
3
+ size 12176
assets/voices/duck4/chirp_h.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:28231a6ec54e9fabf53c6add53987b8b946ded9b384e7d0af5a9bca5fe0c0f57
3
+ size 13742
assets/voices/duck4/chirp_i.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d028418fb90dcf2b91efb8bc034ac6a934a302e50243271cc9cd272f956d3810
3
+ size 6604
assets/voices/duck4/chirp_j.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e768b952f37f22c2852e06ae1c88ef84d662721b7b518204b79292fbd794b543
3
+ size 6472
assets/voices/duck4/chirp_k.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:80ccda91a3632145bfa6a750f8f1230ab59e94e541b82fb81c74810a56802c24
3
+ size 17340
assets/voices/duck4/chirp_l.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5a683cdb89ec94c655e900275a5afbd0a618e7523888d6bc681de4bbffe30f3d
3
+ size 11280
index.html CHANGED
@@ -468,7 +468,7 @@
468
  appended: a plain src attribute can 401 when the browser blocks the
469
  *.static.hf.space auth cookie inside the hub iframe. -->
470
  <script type="module">
471
- const V = "23"; // bump on rl.js changes to defeat heuristic caching
472
  const sign = new URLSearchParams(location.search).get("__sign");
473
  const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
474
  import(`./rl.js?v=${V}${suffix}`);
 
468
  appended: a plain src attribute can 401 when the browser blocks the
469
  *.static.hf.space auth cookie inside the hub iframe. -->
470
  <script type="module">
471
+ const V = "24"; // bump on rl.js changes to defeat heuristic caching
472
  const sign = new URLSearchParams(location.search).get("__sign");
473
  const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
474
  import(`./rl.js?v=${V}${suffix}`);
rl.js CHANGED
@@ -197,7 +197,7 @@ const velCmd = new Float32Array(3); // twist command, driven by held keys
197
  // the control loop reads it before the input section below has evaluated.
198
  const padCmd = new Float32Array(3);
199
  let padActive = false;
200
- const padPrev = { x: false, dpadDown: false, dpadUp: false, rt: 0 };
201
  // Declared before the control loop starts: buildObs reads it via
202
  // effectiveCmd on the very first control step.
203
  const held = new Set();
@@ -220,7 +220,24 @@ function effectiveCmd() {
220
  let rollRun = null;
221
  let rollSource = "kb"; // which hint lights up: keyboard Space or pad X
222
 
 
 
 
 
 
 
 
 
 
 
 
223
  function resetSim() {
 
 
 
 
 
 
224
  mujoco.mj_resetDataKeyframe(model, data, standKeyId);
225
  mujoco.mj_forward(model, data);
226
  lastAction.fill(0);
@@ -461,14 +478,28 @@ function syncRig() {
461
  const QUACK_MS = 480;
462
  let quackAt = -Infinity;
463
  let padJaw = 0;
464
- // Same sound as the robot: the runtime plays its voice-bank chirp on every
465
- // mouth-open edge and falls back to the stock quack.wav — the only audio
466
- // asset in the repo, so that's what we ship. Restarting playback on each
467
- // trigger matches the runtime's "cut off any still-playing sound".
468
  // Browsers block audio until the first user gesture; the rejected play()
469
  // is swallowed and sound simply starts working after the first click/key.
470
- const quackSound = new Audio(signed("./assets/quack.wav"));
471
- quackSound.volume = 0.7;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  // Silent jaw flap for mode/colour changes; the actual quack sound only
473
  // plays on the gamepad right trigger (anything more gets noisy fast).
474
  const quack = () => {
@@ -476,8 +507,7 @@ const quack = () => {
476
  };
477
  const quackLoud = () => {
478
  quack();
479
- quackSound.currentTime = 0;
480
- quackSound.play().catch(() => {});
481
  };
482
  function jawOpenNow() {
483
  const t = (performance.now() - quackAt) / QUACK_MS;
@@ -642,11 +672,14 @@ pollPad = function pollGamepad() {
642
 
643
  // Triggers drive the mouth like the runtime (max of both), and the right
644
  // one quacks on its rising edge — same as the robot's chirp.
 
 
 
645
  const rt = gp.buttons[7]?.value ?? 0;
646
  const lt = gp.buttons[6]?.value ?? 0;
647
  padJaw = Math.max(rt, lt);
648
- if (padPrev.rt < 0.3 && rt >= 0.3) quackLoud();
649
- padPrev.rt = rt;
650
  };
651
 
652
  // Read-only state label (bottom-left): reflects the active policy,
@@ -657,13 +690,19 @@ function setMode(next) {
657
  // No policy switching mid-roll: the roll ends on its own (upright again
658
  // or expired) and returns to walk - switching now would floor the duck.
659
  if (mode === "roll" && rollRun) return;
 
660
  quack();
661
  rollRun = null;
662
  if (next !== "sit") {
663
  // Leaving a sit: let the sitstand policy stand the duck back up first.
664
  if (mode === "sitstand" && sitFlag === 1) {
665
  sitFlag = 0;
666
- setTimeout(() => { mode = next; lastAction.fill(0); syncButtons(); }, 2000);
 
 
 
 
 
667
  syncButtons();
668
  return;
669
  }
@@ -676,7 +715,8 @@ function setMode(next) {
676
  mode = "sitstand";
677
  sitFlag = 0;
678
  lastAction.fill(0);
679
- setTimeout(() => {
 
680
  if (mode === "sitstand") { sitFlag = 1; syncButtons(); }
681
  }, 800);
682
  }
@@ -688,7 +728,10 @@ function setMode(next) {
688
  // continuous action history across policy switches, and the roll initiates
689
  // more reliably mid-gait with the true last actions in the obs.
690
  function triggerRoll(source = "kb") {
691
- if (mode === "roll") return;
 
 
 
692
  rollSource = source;
693
  quack();
694
  mode = "roll";
 
197
  // the control loop reads it before the input section below has evaluated.
198
  const padCmd = new Float32Array(3);
199
  let padActive = false;
200
+ const padPrev = { x: false, dpadDown: false, dpadUp: false, rtArmed: true };
201
  // Declared before the control loop starts: buildObs reads it via
202
  // effectiveCmd on the very first control step.
203
  const held = new Set();
 
220
  let rollRun = null;
221
  let rollSource = "kb"; // which hint lights up: keyboard Space or pad X
222
 
223
+ // Pending mode-transition timers (sit hand-over, stand-up hand-back).
224
+ // Every transition entry point clears them: a stale timer firing after
225
+ // the state has moved on is exactly how a sitting or rolling duck ends
226
+ // up handed to the walking policy mid-motion.
227
+ let sitTimer = null;
228
+ let standTimer = null;
229
+ function clearModeTimers() {
230
+ clearTimeout(sitTimer); sitTimer = null;
231
+ clearTimeout(standTimer); standTimer = null;
232
+ }
233
+
234
  function resetSim() {
235
+ // A reset is a full transition to the walking stand: cancel any roll in
236
+ // flight and any pending sit/stand hand-over, or they'd replay on the
237
+ // freshly reset duck.
238
+ clearModeTimers();
239
+ rollRun = null;
240
+ mode = "walk";
241
  mujoco.mj_resetDataKeyframe(model, data, standKeyId);
242
  mujoco.mj_forward(model, data);
243
  lastAction.fill(0);
 
478
  const QUACK_MS = 480;
479
  let quackAt = -Infinity;
480
  let padJaw = 0;
481
+ // Voice banks from the robot runtime: each printed duck ships a different
482
+ // voice, so each colourway gets its own bank and every quack draws a
483
+ // random chirp take from it - same as the real ducks all sounding
484
+ // slightly different. Audio elements are created lazily and cached.
485
  // Browsers block audio until the first user gesture; the rejected play()
486
  // is swallowed and sound simply starts working after the first click/key.
487
+ const CHIRP_TAKES = "abcdefghijkl";
488
+ const VOICE_BANK = { classic: "duck1", charcoal: "duck2", purple: "duck3", blue: "duck4" };
489
+ const chirpCache = new Map();
490
+ function playChirp() {
491
+ const bank = VOICE_BANK[currentVariant] ?? "duck1";
492
+ const take = CHIRP_TAKES[(Math.random() * CHIRP_TAKES.length) | 0];
493
+ const url = signed(`./assets/voices/${bank}/chirp_${take}.wav`);
494
+ let a = chirpCache.get(url);
495
+ if (!a) {
496
+ a = new Audio(url);
497
+ a.volume = 0.7;
498
+ chirpCache.set(url, a);
499
+ }
500
+ a.currentTime = 0;
501
+ a.play().catch(() => {});
502
+ }
503
  // Silent jaw flap for mode/colour changes; the actual quack sound only
504
  // plays on the gamepad right trigger (anything more gets noisy fast).
505
  const quack = () => {
 
507
  };
508
  const quackLoud = () => {
509
  quack();
510
+ playChirp();
 
511
  };
512
  function jawOpenNow() {
513
  const t = (performance.now() - quackAt) / QUACK_MS;
 
672
 
673
  // Triggers drive the mouth like the runtime (max of both), and the right
674
  // one quacks on its rising edge — same as the robot's chirp.
675
+ // Schmitt trigger on the analog RT value: fire at 0.35, re-arm only once
676
+ // it drops below 0.2. A single threshold re-fires on every jitter around
677
+ // it, which is how one squeeze used to quack several times.
678
  const rt = gp.buttons[7]?.value ?? 0;
679
  const lt = gp.buttons[6]?.value ?? 0;
680
  padJaw = Math.max(rt, lt);
681
+ if (padPrev.rtArmed && rt >= 0.35) { quackLoud(); padPrev.rtArmed = false; }
682
+ else if (!padPrev.rtArmed && rt < 0.2) padPrev.rtArmed = true;
683
  };
684
 
685
  // Read-only state label (bottom-left): reflects the active policy,
 
690
  // No policy switching mid-roll: the roll ends on its own (upright again
691
  // or expired) and returns to walk - switching now would floor the duck.
692
  if (mode === "roll" && rollRun) return;
693
+ clearModeTimers();
694
  quack();
695
  rollRun = null;
696
  if (next !== "sit") {
697
  // Leaving a sit: let the sitstand policy stand the duck back up first.
698
  if (mode === "sitstand" && sitFlag === 1) {
699
  sitFlag = 0;
700
+ standTimer = setTimeout(() => {
701
+ standTimer = null;
702
+ mode = next;
703
+ lastAction.fill(0);
704
+ syncButtons();
705
+ }, 2000);
706
  syncButtons();
707
  return;
708
  }
 
715
  mode = "sitstand";
716
  sitFlag = 0;
717
  lastAction.fill(0);
718
+ sitTimer = setTimeout(() => {
719
+ sitTimer = null;
720
  if (mode === "sitstand") { sitFlag = 1; syncButtons(); }
721
  }, 800);
722
  }
 
728
  // continuous action history across policy switches, and the roll initiates
729
  // more reliably mid-gait with the true last actions in the obs.
730
  function triggerRoll(source = "kb") {
731
+ // Rolls only launch from a standing walk: from a sit (or mid sit/stand
732
+ // hand-over) the roll policy just faceplants the duck.
733
+ if (mode !== "walk" || standTimer) return;
734
+ clearModeTimers();
735
  rollSource = source;
736
  quack();
737
  mode = "roll";