feat: one-shot roulade on Space with auto-return to walk, drop reset button
Browse files- index.html +1 -2
- rl.js +41 -2
index.html
CHANGED
|
@@ -159,13 +159,12 @@
|
|
| 159 |
<button id="btn-walk" class="on">Run</button>
|
| 160 |
<button id="btn-sit">Sit</button>
|
| 161 |
<button id="btn-roulade">Roulade</button>
|
| 162 |
-
<button id="btn-reset">Reset</button>
|
| 163 |
</div>
|
| 164 |
<div class="row" id="swatches"></div>
|
| 165 |
</div>
|
| 166 |
<div class="keys">
|
| 167 |
<b>↑/↓</b> forward / back <b>←/→</b> turn <b>A/E</b> strafe<br />
|
| 168 |
-
<b>R</b> reset drag to orbit scroll to zoom
|
| 169 |
</div>
|
| 170 |
</div>
|
| 171 |
<div id="loading">Compiling physics…</div>
|
|
|
|
| 159 |
<button id="btn-walk" class="on">Run</button>
|
| 160 |
<button id="btn-sit">Sit</button>
|
| 161 |
<button id="btn-roulade">Roulade</button>
|
|
|
|
| 162 |
</div>
|
| 163 |
<div class="row" id="swatches"></div>
|
| 164 |
</div>
|
| 165 |
<div class="keys">
|
| 166 |
<b>↑/↓</b> forward / back <b>←/→</b> turn <b>A/E</b> strafe<br />
|
| 167 |
+
<b>Space</b> roulade <b>R</b> reset drag to orbit scroll to zoom
|
| 168 |
</div>
|
| 169 |
</div>
|
| 170 |
<div id="loading">Compiling physics…</div>
|
rl.js
CHANGED
|
@@ -195,6 +195,9 @@ const held = new Set();
|
|
| 195 |
|
| 196 |
let mode = "walk"; // "walk" | "sitstand" | "roulade"
|
| 197 |
let sitFlag = 0;
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
function resetSim() {
|
| 200 |
mujoco.mj_resetDataKeyframe(model, data, standKeyId);
|
|
@@ -268,6 +271,28 @@ async function controlStep() {
|
|
| 268 |
} else {
|
| 269 |
fallenSince = null;
|
| 270 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
}
|
| 272 |
|
| 273 |
let running = true;
|
|
@@ -464,6 +489,7 @@ const KEYMAP = {
|
|
| 464 |
window.addEventListener("keydown", (e) => {
|
| 465 |
if (e.repeat) return;
|
| 466 |
if (e.code === "KeyR") { resetSim(); return; }
|
|
|
|
| 467 |
const act = KEYMAP[e.code];
|
| 468 |
if (!act) return;
|
| 469 |
e.preventDefault();
|
|
@@ -481,10 +507,10 @@ window.addEventListener("blur", () => { held.clear(); refreshVelCmd(); });
|
|
| 481 |
const btnWalk = document.getElementById("btn-walk");
|
| 482 |
const btnSit = document.getElementById("btn-sit");
|
| 483 |
const btnRoulade = document.getElementById("btn-roulade");
|
| 484 |
-
document.getElementById("btn-reset").addEventListener("click", resetSim);
|
| 485 |
|
| 486 |
function setMode(next) {
|
| 487 |
quack();
|
|
|
|
| 488 |
if (next !== "sit") {
|
| 489 |
// Leaving a sit: let the sitstand policy stand the duck back up first.
|
| 490 |
if (mode === "sitstand" && sitFlag === 1) {
|
|
@@ -508,6 +534,19 @@ function setMode(next) {
|
|
| 508 |
}
|
| 509 |
syncButtons();
|
| 510 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
function syncButtons() {
|
| 512 |
const sitting = mode === "sitstand" && sitFlag === 1;
|
| 513 |
btnWalk.classList.toggle("on", mode === "walk" || (mode === "sitstand" && !sitting));
|
|
@@ -516,7 +555,7 @@ function syncButtons() {
|
|
| 516 |
}
|
| 517 |
btnWalk.addEventListener("click", () => setMode("walk"));
|
| 518 |
btnSit.addEventListener("click", () => setMode("sit"));
|
| 519 |
-
btnRoulade.addEventListener("click",
|
| 520 |
|
| 521 |
// ββ Colour swatches: re-skin the rig live, with a quack βββββββββββββββββ
|
| 522 |
// One representative colour per variant so the dots read at a glance.
|
|
|
|
| 195 |
|
| 196 |
let mode = "walk"; // "walk" | "sitstand" | "roulade"
|
| 197 |
let sitFlag = 0;
|
| 198 |
+
// One-shot roulade tracking: trigger time + whether the trunk actually
|
| 199 |
+
// tipped over yet. Set by triggerRoulade, cleared when we hand back to walk.
|
| 200 |
+
let rouladeRun = null;
|
| 201 |
|
| 202 |
function resetSim() {
|
| 203 |
mujoco.mj_resetDataKeyframe(model, data, standKeyId);
|
|
|
|
| 271 |
} else {
|
| 272 |
fallenSince = null;
|
| 273 |
}
|
| 274 |
+
|
| 275 |
+
// One-shot roulade, step-counted like the robot runtime (a single roll is
|
| 276 |
+
// ~1 s = 50 control steps there): hand back to walking once the trunk has
|
| 277 |
+
// tipped over and is upright again, or after a hard 2 s window if the roll
|
| 278 |
+
// never initiated. Counting steps instead of wall time keeps the logic
|
| 279 |
+
// correct when the sim is fast-forwarded or the tab is throttled.
|
| 280 |
+
if (mode === "roulade" && rouladeRun) {
|
| 281 |
+
rouladeRun.steps++;
|
| 282 |
+
if (obs[5] > -0.3) rouladeRun.tipped = true;
|
| 283 |
+
const upright = obs[5] < -0.85;
|
| 284 |
+
const done = rouladeRun.tipped && upright && rouladeRun.steps >= 40;
|
| 285 |
+
const expired = rouladeRun.steps >= 150; // 3 s, roll should long be over
|
| 286 |
+
if (done || expired) {
|
| 287 |
+
rouladeRun = null;
|
| 288 |
+
mode = "walk";
|
| 289 |
+
lastAction.fill(0);
|
| 290 |
+
// Timed out mid-roll: don't hand a tipped duck to the walking policy
|
| 291 |
+
// (it has no get-up skill), restart from the keyframe instead.
|
| 292 |
+
if (!upright) resetSim();
|
| 293 |
+
if (uiReady) syncButtons();
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
}
|
| 297 |
|
| 298 |
let running = true;
|
|
|
|
| 489 |
window.addEventListener("keydown", (e) => {
|
| 490 |
if (e.repeat) return;
|
| 491 |
if (e.code === "KeyR") { resetSim(); return; }
|
| 492 |
+
if (e.code === "Space") { e.preventDefault(); triggerRoulade(); return; }
|
| 493 |
const act = KEYMAP[e.code];
|
| 494 |
if (!act) return;
|
| 495 |
e.preventDefault();
|
|
|
|
| 507 |
const btnWalk = document.getElementById("btn-walk");
|
| 508 |
const btnSit = document.getElementById("btn-sit");
|
| 509 |
const btnRoulade = document.getElementById("btn-roulade");
|
|
|
|
| 510 |
|
| 511 |
function setMode(next) {
|
| 512 |
quack();
|
| 513 |
+
rouladeRun = null;
|
| 514 |
if (next !== "sit") {
|
| 515 |
// Leaving a sit: let the sitstand policy stand the duck back up first.
|
| 516 |
if (mode === "sitstand" && sitFlag === 1) {
|
|
|
|
| 534 |
}
|
| 535 |
syncButtons();
|
| 536 |
}
|
| 537 |
+
|
| 538 |
+
// One roulade, then straight back to running (Space key or the button).
|
| 539 |
+
// lastAction is deliberately NOT zeroed here: the robot runtime keeps one
|
| 540 |
+
// continuous action history across policy switches, and the roll initiates
|
| 541 |
+
// more reliably mid-gait with the true last actions in the obs.
|
| 542 |
+
function triggerRoulade() {
|
| 543 |
+
if (mode === "roulade") return;
|
| 544 |
+
quack();
|
| 545 |
+
mode = "roulade";
|
| 546 |
+
sitFlag = 0;
|
| 547 |
+
rouladeRun = { steps: 0, tipped: false };
|
| 548 |
+
syncButtons();
|
| 549 |
+
}
|
| 550 |
function syncButtons() {
|
| 551 |
const sitting = mode === "sitstand" && sitFlag === 1;
|
| 552 |
btnWalk.classList.toggle("on", mode === "walk" || (mode === "sitstand" && !sitting));
|
|
|
|
| 555 |
}
|
| 556 |
btnWalk.addEventListener("click", () => setMode("walk"));
|
| 557 |
btnSit.addEventListener("click", () => setMode("sit"));
|
| 558 |
+
btnRoulade.addEventListener("click", triggerRoulade);
|
| 559 |
|
| 560 |
// ββ Colour swatches: re-skin the rig live, with a quack βββββββββββββββββ
|
| 561 |
// One representative colour per variant so the dots read at a glance.
|