feat: one-shot kick moves from the runtime's ball-kick policies
Browse filesball_kick_left/right.onnx (same 61-dim obs interface) run for a fixed
0.5 s window with zeroed commands, then hand back to walking - same
mechanics as the robot runtime. F on the keyboard alternates feet,
RB / LB on the gamepad pick explicitly. Kicks only launch from a
standing walk and are mutually locked with the roll.
Co-authored-by: Cursor <cursoragent@cursor.com>
- README.md +3 -0
- index.html +4 -1
- policies/ball_kick_left.onnx +3 -0
- policies/ball_kick_right.onnx +3 -0
- rl.js +84 -20
README.md
CHANGED
|
@@ -20,6 +20,7 @@ runs the policy network at 50 Hz. No server, no backend.
|
|
| 20 |
| Run | `BEST_alpha_walking.onnx` | Velocity-tracking locomotion (arrows / WASD to steer) |
|
| 21 |
| Sit | `BEST_alpha_sitstand.onnx` | Sits down on its hull, stands back up |
|
| 22 |
| Roll | `roulade.onnx` | Rolls over and recovers |
|
|
|
|
| 23 |
|
| 24 |
Policies and MJCF model from
|
| 25 |
[apirrone/microduck_runtime](https://github.com/apirrone/microduck_runtime)
|
|
@@ -31,6 +32,7 @@ and [apirrone/mjlab_microduck](https://github.com/apirrone/mjlab_microduck).
|
|
| 31 |
- Arrow left / right: turn
|
| 32 |
- A / E: strafe
|
| 33 |
- Space: roll (one barrel roll, then back to running)
|
|
|
|
| 34 |
- R: reset
|
| 35 |
- Drag to orbit, scroll to zoom
|
| 36 |
- Colour dots: repaint the duck (it quacks)
|
|
@@ -42,6 +44,7 @@ Plug in a controller and the same mapping as the real robot runtime applies:
|
|
| 42 |
- Left stick: forward / back + strafe
|
| 43 |
- Right stick: turn
|
| 44 |
- X: roll
|
|
|
|
| 45 |
- D-pad down: sit / stand toggle
|
| 46 |
- Right trigger: mouth (analog) + quack
|
| 47 |
|
|
|
|
| 20 |
| Run | `BEST_alpha_walking.onnx` | Velocity-tracking locomotion (arrows / WASD to steer) |
|
| 21 |
| Sit | `BEST_alpha_sitstand.onnx` | Sits down on its hull, stands back up |
|
| 22 |
| Roll | `roulade.onnx` | Rolls over and recovers |
|
| 23 |
+
| Kick | `ball_kick_left.onnx` / `ball_kick_right.onnx` | Blind one-shot kick (0.5 s window, zeroed commands), left or right leg |
|
| 24 |
|
| 25 |
Policies and MJCF model from
|
| 26 |
[apirrone/microduck_runtime](https://github.com/apirrone/microduck_runtime)
|
|
|
|
| 32 |
- Arrow left / right: turn
|
| 33 |
- A / E: strafe
|
| 34 |
- Space: roll (one barrel roll, then back to running)
|
| 35 |
+
- F: kick, alternating feet
|
| 36 |
- R: reset
|
| 37 |
- Drag to orbit, scroll to zoom
|
| 38 |
- Colour dots: repaint the duck (it quacks)
|
|
|
|
| 44 |
- Left stick: forward / back + strafe
|
| 45 |
- Right stick: turn
|
| 46 |
- X: roll
|
| 47 |
+
- RB / LB: right / left kick
|
| 48 |
- D-pad down: sit / stand toggle
|
| 49 |
- Right trigger: mouth (analog) + quack
|
| 50 |
|
index.html
CHANGED
|
@@ -392,10 +392,12 @@
|
|
| 392 |
<span class="hint"><span class="key" id="key-turnl">←</span><span class="key" id="key-turnr">→</span> turn</span>
|
| 393 |
<span class="hint"><span class="key" id="key-left">A</span><span class="key" id="key-right">E</span> strafe</span><br />
|
| 394 |
<span class="hint"><span class="key" id="key-roll">Space</span> roll</span>
|
|
|
|
| 395 |
<span class="hint"><span class="key" id="key-reset">R</span> reset</span>
|
| 396 |
drag to orbit · scroll to zoom<br />
|
| 397 |
<span id="pad-hint">gamepad:
|
| 398 |
<span class="hint"><span class="key" id="key-pad-x">X</span> roll</span>
|
|
|
|
| 399 |
<span class="hint"><span class="key" id="key-pad-sit">⇓</span> sit</span>
|
| 400 |
<span class="hint"><span class="key" id="key-pad-run">⇑</span> run</span>
|
| 401 |
<span class="hint"><span class="key" id="key-pad-rt">RT</span> quack</span>
|
|
@@ -419,6 +421,7 @@
|
|
| 419 |
<ul>
|
| 420 |
<li><b>Move</b><span>Arrows or ZQSD to run and turn, A / E to strafe, drag to orbit.</span></li>
|
| 421 |
<li><b>Roll</b><span>Space triggers a barrel roll, then it gets back up.</span></li>
|
|
|
|
| 422 |
<li><b>Gamepad</b><span>Plug one in - same mapping as the real robot.</span></li>
|
| 423 |
<li><b>Ghosts</b><span>Other visitors right now appear as translucent ducks, live.</span></li>
|
| 424 |
</ul>
|
|
@@ -468,7 +471,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 = "
|
| 472 |
const sign = new URLSearchParams(location.search).get("__sign");
|
| 473 |
const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
|
| 474 |
import(`./rl.js?v=${V}${suffix}`);
|
|
|
|
| 392 |
<span class="hint"><span class="key" id="key-turnl">←</span><span class="key" id="key-turnr">→</span> turn</span>
|
| 393 |
<span class="hint"><span class="key" id="key-left">A</span><span class="key" id="key-right">E</span> strafe</span><br />
|
| 394 |
<span class="hint"><span class="key" id="key-roll">Space</span> roll</span>
|
| 395 |
+
<span class="hint"><span class="key" id="key-kick">F</span> kick</span>
|
| 396 |
<span class="hint"><span class="key" id="key-reset">R</span> reset</span>
|
| 397 |
drag to orbit · scroll to zoom<br />
|
| 398 |
<span id="pad-hint">gamepad:
|
| 399 |
<span class="hint"><span class="key" id="key-pad-x">X</span> roll</span>
|
| 400 |
+
<span class="hint"><span class="key" id="key-pad-rb">RB</span><span class="key" id="key-pad-lb">LB</span> kick</span>
|
| 401 |
<span class="hint"><span class="key" id="key-pad-sit">⇓</span> sit</span>
|
| 402 |
<span class="hint"><span class="key" id="key-pad-run">⇑</span> run</span>
|
| 403 |
<span class="hint"><span class="key" id="key-pad-rt">RT</span> quack</span>
|
|
|
|
| 421 |
<ul>
|
| 422 |
<li><b>Move</b><span>Arrows or ZQSD to run and turn, A / E to strafe, drag to orbit.</span></li>
|
| 423 |
<li><b>Roll</b><span>Space triggers a barrel roll, then it gets back up.</span></li>
|
| 424 |
+
<li><b>Kick</b><span>F throws a kick, alternating feet (RB / LB on the pad).</span></li>
|
| 425 |
<li><b>Gamepad</b><span>Plug one in - same mapping as the real robot.</span></li>
|
| 426 |
<li><b>Ghosts</b><span>Other visitors right now appear as translucent ducks, live.</span></li>
|
| 427 |
</ul>
|
|
|
|
| 471 |
appended: a plain src attribute can 401 when the browser blocks the
|
| 472 |
*.static.hf.space auth cookie inside the hub iframe. -->
|
| 473 |
<script type="module">
|
| 474 |
+
const V = "25"; // bump on rl.js changes to defeat heuristic caching
|
| 475 |
const sign = new URLSearchParams(location.search).get("__sign");
|
| 476 |
const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
|
| 477 |
import(`./rl.js?v=${V}${suffix}`);
|
policies/ball_kick_left.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d6928284dccd3dd61e08bf2f760effa74309fbefd97b2b31afb2a60f526d196a
|
| 3 |
+
size 793685
|
policies/ball_kick_right.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:147a32c388c6b19111b3ac3b550a9a6dc8b8bf267118af4d8c3712522eedb5af
|
| 3 |
+
size 793685
|
rl.js
CHANGED
|
@@ -49,6 +49,10 @@ const POLICIES = {
|
|
| 49 |
walk: `${POLICY_DIR}/BEST_alpha_walking.onnx`,
|
| 50 |
sitstand: `${POLICY_DIR}/BEST_alpha_sitstand.onnx`,
|
| 51 |
roll: `${POLICY_DIR}/roulade.onnx`,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
};
|
| 53 |
|
| 54 |
// From the ONNX metadata (identical for all alpha policies) and the STAND
|
|
@@ -169,11 +173,14 @@ const rigPromise = (async () => {
|
|
| 169 |
return buildRig(k, { materialForMesh: materialHookFor(VARIANTS[currentVariant]) });
|
| 170 |
})();
|
| 171 |
const sessionOpts = { executionProviders: ["wasm"] };
|
| 172 |
-
[sessions.walk, sessions.sitstand, sessions.roll
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
setLoading("Compiling physics\u2026");
|
| 179 |
const model = mujoco.MjModel.from_xml_string(xml, vfs);
|
|
@@ -197,13 +204,14 @@ 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, 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();
|
| 204 |
|
| 205 |
-
let mode = "walk"; // "walk" | "sitstand" | "roll"
|
| 206 |
let sitFlag = 0;
|
|
|
|
| 207 |
|
| 208 |
// The twist the policy actually receives: live gamepad sticks win over the
|
| 209 |
// held keys. No input means zero command - the duck stands in place.
|
|
@@ -212,13 +220,19 @@ let sitFlag = 0;
|
|
| 212 |
// hands back to walk on its own - steering would only knock the roll over.
|
| 213 |
const ZERO_CMD = new Float32Array(3);
|
| 214 |
function effectiveCmd() {
|
| 215 |
-
if (mode === "roll") return ZERO_CMD;
|
| 216 |
return padActive ? padCmd : velCmd;
|
| 217 |
}
|
| 218 |
// One-shot roll tracking: trigger time + whether the trunk actually
|
| 219 |
// tipped over yet. Set by triggerRoll, cleared when we hand back to walk.
|
| 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
|
|
@@ -237,6 +251,7 @@ function resetSim() {
|
|
| 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);
|
|
@@ -267,11 +282,13 @@ function buildObs() {
|
|
| 267 |
// command: walking/roll use the twist; sitstand uses cmd[0] as the
|
| 268 |
// posture flag.
|
| 269 |
cmd.fill(0, 0, 3);
|
| 270 |
-
if (mode === "
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
const c = effectiveCmd();
|
| 272 |
cmd[0] = c[0]; cmd[1] = c[1]; cmd[2] = c[2];
|
| 273 |
-
} else {
|
| 274 |
-
cmd[0] = sitFlag;
|
| 275 |
}
|
| 276 |
for (let c = 0; c < CMD_SIZE; c++) obs[i++] = cmd[c];
|
| 277 |
return obs;
|
|
@@ -312,6 +329,18 @@ async function controlStep() {
|
|
| 312 |
// tipped over and is upright again, or after a hard 2 s window if the roll
|
| 313 |
// never initiated. Counting steps instead of wall time keeps the logic
|
| 314 |
// correct when the sim is fast-forwarded or the tab is throttled.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
if (mode === "roll" && rollRun) {
|
| 316 |
rollRun.steps++;
|
| 317 |
if (obs[5] > -0.3) rollRun.tipped = true;
|
|
@@ -526,10 +555,11 @@ const keyEls = {
|
|
| 526 |
fwd: el("key-fwd"), back: el("key-back"),
|
| 527 |
turnl: el("key-turnl"), turnr: el("key-turnr"),
|
| 528 |
left: el("key-left"), right: el("key-right"),
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
|
|
|
| 533 |
const STICK_R = 15; // px, max dot travel inside the 46px stick circle
|
| 534 |
let resetFlashAt = -Infinity;
|
| 535 |
|
|
@@ -558,8 +588,11 @@ function renderStats() {
|
|
| 558 |
keyEls[k].classList.toggle("lit", held.has(k));
|
| 559 |
}
|
| 560 |
keyEls.roll.classList.toggle("lit", mode === "roll" && rollSource === "kb");
|
|
|
|
| 561 |
keyEls.reset.classList.toggle("lit", performance.now() - resetFlashAt < 400);
|
| 562 |
keyEls.padX.classList.toggle("lit", mode === "roll" && rollSource === "pad");
|
|
|
|
|
|
|
| 563 |
keyEls.padSit.classList.toggle("lit", sitting);
|
| 564 |
keyEls.padRun.classList.toggle("lit", padPrev.dpadUp);
|
| 565 |
keyEls.padRt.classList.toggle("lit", padJaw > 0.3);
|
|
@@ -602,10 +635,17 @@ const KEYMAP = {
|
|
| 602 |
KeyQ: "left", KeyE: "right",
|
| 603 |
};
|
| 604 |
|
|
|
|
|
|
|
| 605 |
window.addEventListener("keydown", (e) => {
|
| 606 |
if (e.repeat) return;
|
| 607 |
if (e.code === "KeyR") { resetSim(); resetFlashAt = performance.now(); return; }
|
| 608 |
if (e.code === "Space") { e.preventDefault(); triggerRoll(); return; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
const act = KEYMAP[e.code];
|
| 610 |
if (!act) return;
|
| 611 |
e.preventDefault();
|
|
@@ -652,11 +692,19 @@ pollPad = function pollGamepad() {
|
|
| 652 |
padCmd.fill(0);
|
| 653 |
}
|
| 654 |
|
| 655 |
-
// Standard mapping indices: X=2, DpadDown=13, RT=7 (analog
|
| 656 |
const x = !!gp.buttons[2]?.pressed;
|
| 657 |
if (x && !padPrev.x) triggerRoll("pad");
|
| 658 |
padPrev.x = x;
|
| 659 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
const dpadDown = !!gp.buttons[13]?.pressed;
|
| 661 |
if (dpadDown && !padPrev.dpadDown) {
|
| 662 |
const sitting = mode === "sitstand" && sitFlag === 1;
|
|
@@ -687,9 +735,9 @@ pollPad = function pollGamepad() {
|
|
| 687 |
const modeLabel = document.getElementById("mode-label");
|
| 688 |
|
| 689 |
function setMode(next) {
|
| 690 |
-
// No policy switching mid-roll
|
| 691 |
-
//
|
| 692 |
-
if (mode === "roll" && rollRun) return;
|
| 693 |
clearModeTimers();
|
| 694 |
quack();
|
| 695 |
rollRun = null;
|
|
@@ -739,6 +787,22 @@ function triggerRoll(source = "kb") {
|
|
| 739 |
rollRun = { steps: 0, tipped: false };
|
| 740 |
syncButtons();
|
| 741 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
// Slot-machine roll: the old text slides up and out while the new one
|
| 743 |
// rises in from below the (overflow-hidden) label box.
|
| 744 |
function setModeLabel(text) {
|
|
@@ -758,7 +822,7 @@ function setModeLabel(text) {
|
|
| 758 |
|
| 759 |
function syncButtons() {
|
| 760 |
const sitting = mode === "sitstand" && sitFlag === 1;
|
| 761 |
-
setModeLabel(mode === "roll" ? "Roll" : sitting ? "Sit" : "Run");
|
| 762 |
}
|
| 763 |
|
| 764 |
// ββ Colour swatches: re-skin the rig live, with a quack βββββββββββββββββ
|
|
|
|
| 49 |
walk: `${POLICY_DIR}/BEST_alpha_walking.onnx`,
|
| 50 |
sitstand: `${POLICY_DIR}/BEST_alpha_sitstand.onnx`,
|
| 51 |
roll: `${POLICY_DIR}/roulade.onnx`,
|
| 52 |
+
// Blind one-shot kicks (the operator aims the robot, no ball in obs):
|
| 53 |
+
// the runtime swaps these in for a 0.5 s window, commands zeroed.
|
| 54 |
+
kickL: `${POLICY_DIR}/ball_kick_left.onnx`,
|
| 55 |
+
kickR: `${POLICY_DIR}/ball_kick_right.onnx`,
|
| 56 |
};
|
| 57 |
|
| 58 |
// From the ONNX metadata (identical for all alpha policies) and the STAND
|
|
|
|
| 173 |
return buildRig(k, { materialForMesh: materialHookFor(VARIANTS[currentVariant]) });
|
| 174 |
})();
|
| 175 |
const sessionOpts = { executionProviders: ["wasm"] };
|
| 176 |
+
[sessions.walk, sessions.sitstand, sessions.roll, sessions.kickL, sessions.kickR] =
|
| 177 |
+
await Promise.all([
|
| 178 |
+
ort.InferenceSession.create(signed(POLICIES.walk), sessionOpts),
|
| 179 |
+
ort.InferenceSession.create(signed(POLICIES.sitstand), sessionOpts),
|
| 180 |
+
ort.InferenceSession.create(signed(POLICIES.roll), sessionOpts),
|
| 181 |
+
ort.InferenceSession.create(signed(POLICIES.kickL), sessionOpts),
|
| 182 |
+
ort.InferenceSession.create(signed(POLICIES.kickR), sessionOpts),
|
| 183 |
+
]);
|
| 184 |
|
| 185 |
setLoading("Compiling physics\u2026");
|
| 186 |
const model = mujoco.MjModel.from_xml_string(xml, vfs);
|
|
|
|
| 204 |
// the control loop reads it before the input section below has evaluated.
|
| 205 |
const padCmd = new Float32Array(3);
|
| 206 |
let padActive = false;
|
| 207 |
+
const padPrev = { x: false, rb: false, lb: false, dpadDown: false, dpadUp: false, rtArmed: true };
|
| 208 |
// Declared before the control loop starts: buildObs reads it via
|
| 209 |
// effectiveCmd on the very first control step.
|
| 210 |
const held = new Set();
|
| 211 |
|
| 212 |
+
let mode = "walk"; // "walk" | "sitstand" | "roll" | "kickL" | "kickR"
|
| 213 |
let sitFlag = 0;
|
| 214 |
+
const isKick = () => mode === "kickL" || mode === "kickR";
|
| 215 |
|
| 216 |
// The twist the policy actually receives: live gamepad sticks win over the
|
| 217 |
// held keys. No input means zero command - the duck stands in place.
|
|
|
|
| 220 |
// hands back to walk on its own - steering would only knock the roll over.
|
| 221 |
const ZERO_CMD = new Float32Array(3);
|
| 222 |
function effectiveCmd() {
|
| 223 |
+
if (mode === "roll" || isKick()) return ZERO_CMD;
|
| 224 |
return padActive ? padCmd : velCmd;
|
| 225 |
}
|
| 226 |
// One-shot roll tracking: trigger time + whether the trunk actually
|
| 227 |
// tipped over yet. Set by triggerRoll, cleared when we hand back to walk.
|
| 228 |
let rollRun = null;
|
| 229 |
let rollSource = "kb"; // which hint lights up: keyboard Space or pad X
|
| 230 |
+
// One-shot kick tracking: the runtime swaps the kick policy in for a fixed
|
| 231 |
+
// 0.5 s window (25 control steps at 50 Hz) with zeroed commands, then hands
|
| 232 |
+
// straight back to walking. lastAction stays continuous across both swaps.
|
| 233 |
+
let kickRun = null;
|
| 234 |
+
let kickSource = "kb";
|
| 235 |
+
const KICK_STEPS = 25;
|
| 236 |
|
| 237 |
// Pending mode-transition timers (sit hand-over, stand-up hand-back).
|
| 238 |
// Every transition entry point clears them: a stale timer firing after
|
|
|
|
| 251 |
// freshly reset duck.
|
| 252 |
clearModeTimers();
|
| 253 |
rollRun = null;
|
| 254 |
+
kickRun = null;
|
| 255 |
mode = "walk";
|
| 256 |
mujoco.mj_resetDataKeyframe(model, data, standKeyId);
|
| 257 |
mujoco.mj_forward(model, data);
|
|
|
|
| 282 |
// command: walking/roll use the twist; sitstand uses cmd[0] as the
|
| 283 |
// posture flag.
|
| 284 |
cmd.fill(0, 0, 3);
|
| 285 |
+
if (mode === "sitstand") {
|
| 286 |
+
cmd[0] = sitFlag;
|
| 287 |
+
} else {
|
| 288 |
+
// walk uses the live twist; roll and kick see all-zero commands
|
| 289 |
+
// (via effectiveCmd), matching how they were trained.
|
| 290 |
const c = effectiveCmd();
|
| 291 |
cmd[0] = c[0]; cmd[1] = c[1]; cmd[2] = c[2];
|
|
|
|
|
|
|
| 292 |
}
|
| 293 |
for (let c = 0; c < CMD_SIZE; c++) obs[i++] = cmd[c];
|
| 294 |
return obs;
|
|
|
|
| 329 |
// tipped over and is upright again, or after a hard 2 s window if the roll
|
| 330 |
// never initiated. Counting steps instead of wall time keeps the logic
|
| 331 |
// correct when the sim is fast-forwarded or the tab is throttled.
|
| 332 |
+
// One-shot kick: fixed 0.5 s window like the robot runtime, then straight
|
| 333 |
+
// back to walking. lastAction is NOT zeroed on either swap - the runtime
|
| 334 |
+
// keeps one continuous action history across policy switches.
|
| 335 |
+
if (isKick() && kickRun) {
|
| 336 |
+
kickRun.steps++;
|
| 337 |
+
if (kickRun.steps >= KICK_STEPS) {
|
| 338 |
+
kickRun = null;
|
| 339 |
+
mode = "walk";
|
| 340 |
+
if (uiReady) syncButtons();
|
| 341 |
+
}
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
if (mode === "roll" && rollRun) {
|
| 345 |
rollRun.steps++;
|
| 346 |
if (obs[5] > -0.3) rollRun.tipped = true;
|
|
|
|
| 555 |
fwd: el("key-fwd"), back: el("key-back"),
|
| 556 |
turnl: el("key-turnl"), turnr: el("key-turnr"),
|
| 557 |
left: el("key-left"), right: el("key-right"),
|
| 558 |
+
roll: el("key-roll"), reset: el("key-reset"), kick: el("key-kick"),
|
| 559 |
+
padX: el("key-pad-x"), padSit: el("key-pad-sit"),
|
| 560 |
+
padRun: el("key-pad-run"), padRt: el("key-pad-rt"),
|
| 561 |
+
padRb: el("key-pad-rb"), padLb: el("key-pad-lb"),
|
| 562 |
+
};
|
| 563 |
const STICK_R = 15; // px, max dot travel inside the 46px stick circle
|
| 564 |
let resetFlashAt = -Infinity;
|
| 565 |
|
|
|
|
| 588 |
keyEls[k].classList.toggle("lit", held.has(k));
|
| 589 |
}
|
| 590 |
keyEls.roll.classList.toggle("lit", mode === "roll" && rollSource === "kb");
|
| 591 |
+
keyEls.kick.classList.toggle("lit", isKick() && kickSource === "kb");
|
| 592 |
keyEls.reset.classList.toggle("lit", performance.now() - resetFlashAt < 400);
|
| 593 |
keyEls.padX.classList.toggle("lit", mode === "roll" && rollSource === "pad");
|
| 594 |
+
keyEls.padRb.classList.toggle("lit", mode === "kickR" && kickSource === "pad");
|
| 595 |
+
keyEls.padLb.classList.toggle("lit", mode === "kickL" && kickSource === "pad");
|
| 596 |
keyEls.padSit.classList.toggle("lit", sitting);
|
| 597 |
keyEls.padRun.classList.toggle("lit", padPrev.dpadUp);
|
| 598 |
keyEls.padRt.classList.toggle("lit", padJaw > 0.3);
|
|
|
|
| 635 |
KeyQ: "left", KeyE: "right",
|
| 636 |
};
|
| 637 |
|
| 638 |
+
// Keyboard kicks alternate feet (the pad picks explicitly via RB/LB).
|
| 639 |
+
let kickNextLeft = false;
|
| 640 |
window.addEventListener("keydown", (e) => {
|
| 641 |
if (e.repeat) return;
|
| 642 |
if (e.code === "KeyR") { resetSim(); resetFlashAt = performance.now(); return; }
|
| 643 |
if (e.code === "Space") { e.preventDefault(); triggerRoll(); return; }
|
| 644 |
+
if (e.code === "KeyF") {
|
| 645 |
+
e.preventDefault();
|
| 646 |
+
if (triggerKick(kickNextLeft ? "left" : "right")) kickNextLeft = !kickNextLeft;
|
| 647 |
+
return;
|
| 648 |
+
}
|
| 649 |
const act = KEYMAP[e.code];
|
| 650 |
if (!act) return;
|
| 651 |
e.preventDefault();
|
|
|
|
| 692 |
padCmd.fill(0);
|
| 693 |
}
|
| 694 |
|
| 695 |
+
// Standard mapping indices: X=2, LB=4, RB=5, DpadDown=13, RT=7 (analog).
|
| 696 |
const x = !!gp.buttons[2]?.pressed;
|
| 697 |
if (x && !padPrev.x) triggerRoll("pad");
|
| 698 |
padPrev.x = x;
|
| 699 |
|
| 700 |
+
// RB / LB: right / left kick, same buttons as the robot runtime.
|
| 701 |
+
const rb = !!gp.buttons[5]?.pressed;
|
| 702 |
+
if (rb && !padPrev.rb) triggerKick("right", "pad");
|
| 703 |
+
padPrev.rb = rb;
|
| 704 |
+
const lb = !!gp.buttons[4]?.pressed;
|
| 705 |
+
if (lb && !padPrev.lb) triggerKick("left", "pad");
|
| 706 |
+
padPrev.lb = lb;
|
| 707 |
+
|
| 708 |
const dpadDown = !!gp.buttons[13]?.pressed;
|
| 709 |
if (dpadDown && !padPrev.dpadDown) {
|
| 710 |
const sitting = mode === "sitstand" && sitFlag === 1;
|
|
|
|
| 735 |
const modeLabel = document.getElementById("mode-label");
|
| 736 |
|
| 737 |
function setMode(next) {
|
| 738 |
+
// No policy switching mid-roll or mid-kick: both end on their own and
|
| 739 |
+
// return to walk - switching now would floor the duck.
|
| 740 |
+
if ((mode === "roll" && rollRun) || (isKick() && kickRun)) return;
|
| 741 |
clearModeTimers();
|
| 742 |
quack();
|
| 743 |
rollRun = null;
|
|
|
|
| 787 |
rollRun = { steps: 0, tipped: false };
|
| 788 |
syncButtons();
|
| 789 |
}
|
| 790 |
+
|
| 791 |
+
// One blind kick (the duck can't see any ball - it's a scripted boot),
|
| 792 |
+
// left or right leg. Same launch constraints as the roll. Returns whether
|
| 793 |
+
// the kick actually launched so the keyboard's foot alternation only
|
| 794 |
+
// advances on real kicks.
|
| 795 |
+
function triggerKick(foot, source = "kb") {
|
| 796 |
+
if (mode !== "walk" || standTimer) return false;
|
| 797 |
+
clearModeTimers();
|
| 798 |
+
kickSource = source;
|
| 799 |
+
quack();
|
| 800 |
+
mode = foot === "left" ? "kickL" : "kickR";
|
| 801 |
+
sitFlag = 0;
|
| 802 |
+
kickRun = { steps: 0 };
|
| 803 |
+
syncButtons();
|
| 804 |
+
return true;
|
| 805 |
+
}
|
| 806 |
// Slot-machine roll: the old text slides up and out while the new one
|
| 807 |
// rises in from below the (overflow-hidden) label box.
|
| 808 |
function setModeLabel(text) {
|
|
|
|
| 822 |
|
| 823 |
function syncButtons() {
|
| 824 |
const sitting = mode === "sitstand" && sitFlag === 1;
|
| 825 |
+
setModeLabel(mode === "roll" ? "Roll" : isKick() ? "Kick" : sitting ? "Sit" : "Run");
|
| 826 |
}
|
| 827 |
|
| 828 |
// ββ Colour swatches: re-skin the rig live, with a quack βββββββββββββββββ
|