feat: right-stick camera orbit with inertia, subtle Tron glow on grid
Browse files- Gamepad right stick orbits the camera around the duck through a
smoothed angular velocity (exponential ramp in, ~0.3 s coast out), so
motion eases instead of stopping dead. Vertical is flight-style
inverted. Any deflection detaches the chase cam; R3 toggles it back,
mirroring the C key. New RS/R3 HUD hints, README gamepad section
updated.
- Grid lines (floor and arena walls) get a thicker antialiased core and
a faint wide halo for a restrained Tron look, plus slightly brighter
line color and opacity.
Co-authored-by: Cursor <cursoragent@cursor.com>
- README.md +2 -0
- index.html +3 -1
- rl.js +83 -13
README.md
CHANGED
|
@@ -46,6 +46,8 @@ area so neither the ball nor the duck can wander off.
|
|
| 46 |
Plug in a controller and the same mapping as the real robot runtime applies:
|
| 47 |
|
| 48 |
- Left stick: forward / back + turn
|
|
|
|
|
|
|
| 49 |
- X: roll
|
| 50 |
- Y: pop / respawn the ball
|
| 51 |
- RB / LB: right / left kick
|
|
|
|
| 46 |
Plug in a controller and the same mapping as the real robot runtime applies:
|
| 47 |
|
| 48 |
- Left stick: forward / back + turn
|
| 49 |
+
- Right stick: orbit the camera (detaches the chase cam)
|
| 50 |
+
- R3 (right stick click): toggle the chase cam back on
|
| 51 |
- X: roll
|
| 52 |
- Y: pop / respawn the ball
|
| 53 |
- RB / LB: right / left kick
|
index.html
CHANGED
|
@@ -552,6 +552,8 @@
|
|
| 552 |
<span class="hint"><span class="key" id="key-pad-x">X</span> roll</span>
|
| 553 |
<span class="hint"><span class="key" id="key-pad-y">Y</span> ball</span>
|
| 554 |
<span class="hint"><span class="key" id="key-pad-rb">RB</span><span class="key" id="key-pad-lb">LB</span> kick</span>
|
|
|
|
|
|
|
| 555 |
<span class="hint"><span class="key" id="key-pad-sit">⇓</span> sit</span>
|
| 556 |
<span class="hint"><span class="key" id="key-pad-run">⇑</span> run</span>
|
| 557 |
<span class="hint"><span class="key" id="key-pad-rt">RT</span> quack</span>
|
|
@@ -671,7 +673,7 @@
|
|
| 671 |
appended: a plain src attribute can 401 when the browser blocks the
|
| 672 |
*.static.hf.space auth cookie inside the hub iframe. -->
|
| 673 |
<script type="module">
|
| 674 |
-
const V = "
|
| 675 |
const sign = new URLSearchParams(location.search).get("__sign");
|
| 676 |
const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
|
| 677 |
import(`./rl.js?v=${V}${suffix}`);
|
|
|
|
| 552 |
<span class="hint"><span class="key" id="key-pad-x">X</span> roll</span>
|
| 553 |
<span class="hint"><span class="key" id="key-pad-y">Y</span> ball</span>
|
| 554 |
<span class="hint"><span class="key" id="key-pad-rb">RB</span><span class="key" id="key-pad-lb">LB</span> kick</span>
|
| 555 |
+
<span class="hint"><span class="key" id="key-pad-rs">RS</span> camera</span>
|
| 556 |
+
<span class="hint"><span class="key" id="key-pad-r3">R3</span> chase</span>
|
| 557 |
<span class="hint"><span class="key" id="key-pad-sit">⇓</span> sit</span>
|
| 558 |
<span class="hint"><span class="key" id="key-pad-run">⇑</span> run</span>
|
| 559 |
<span class="hint"><span class="key" id="key-pad-rt">RT</span> quack</span>
|
|
|
|
| 673 |
appended: a plain src attribute can 401 when the browser blocks the
|
| 674 |
*.static.hf.space auth cookie inside the hub iframe. -->
|
| 675 |
<script type="module">
|
| 676 |
+
const V = "30"; // bump on rl.js changes to defeat heuristic caching
|
| 677 |
const sign = new URLSearchParams(location.search).get("__sign");
|
| 678 |
const suffix = sign ? `&__sign=${encodeURIComponent(sign)}` : "";
|
| 679 |
import(`./rl.js?v=${V}${suffix}`);
|
rl.js
CHANGED
|
@@ -256,7 +256,10 @@ const velCmd = new Float32Array(3); // twist command, driven by held keys
|
|
| 256 |
// the control loop reads it before the input section below has evaluated.
|
| 257 |
const padCmd = new Float32Array(3);
|
| 258 |
let padActive = false;
|
| 259 |
-
const padPrev = { x: false, y: false, rb: false, lb: false, dpadDown: false, dpadUp: false, rtArmed: true };
|
|
|
|
|
|
|
|
|
|
| 260 |
// Declared before the control loop starts: buildObs reads it via
|
| 261 |
// effectiveCmd on the very first control step.
|
| 262 |
const held = new Set();
|
|
@@ -527,7 +530,7 @@ function makeInfiniteGrid() {
|
|
| 527 |
uniforms: {
|
| 528 |
uCell: { value: 0.1 },
|
| 529 |
uSection: { value: 0.5 },
|
| 530 |
-
uCellColor: { value: new THREE.Color(
|
| 531 |
uSectionColor: { value: new THREE.Color(0xffb366) },
|
| 532 |
uFadeDist: { value: 3.0 },
|
| 533 |
uFocus: { value: new THREE.Vector3() },
|
|
@@ -544,18 +547,24 @@ function makeInfiniteGrid() {
|
|
| 544 |
varying vec3 vWorld;
|
| 545 |
uniform float uCell, uSection, uFadeDist;
|
| 546 |
uniform vec3 uCellColor, uSectionColor, uFocus;
|
|
|
|
|
|
|
|
|
|
| 547 |
float gridLine(vec2 p, float size) {
|
| 548 |
vec2 r = p / size;
|
| 549 |
vec2 g = abs(fract(r - 0.5) - 0.5) / fwidth(r);
|
| 550 |
-
|
|
|
|
|
|
|
|
|
|
| 551 |
}
|
| 552 |
void main() {
|
| 553 |
float cell = gridLine(vWorld.xz, uCell);
|
| 554 |
float section = gridLine(vWorld.xz, uSection);
|
| 555 |
float d = distance(vWorld.xz, uFocus.xz);
|
| 556 |
float fade = pow(clamp(1.0 - d / uFadeDist, 0.0, 1.0), 1.6);
|
| 557 |
-
vec3 col = mix(uCellColor, uSectionColor, section);
|
| 558 |
-
float alpha = max(section * 0.
|
| 559 |
if (alpha < 0.004) discard;
|
| 560 |
gl_FragColor = vec4(col, alpha);
|
| 561 |
}
|
|
@@ -580,7 +589,7 @@ function makeWallGridMaterial(alongX) {
|
|
| 580 |
uniforms: {
|
| 581 |
uCell: { value: 0.1 },
|
| 582 |
uSection: { value: 0.5 },
|
| 583 |
-
uCellColor: { value: new THREE.Color(
|
| 584 |
uSectionColor: { value: new THREE.Color(0xffb366) },
|
| 585 |
// Gentler radial fade than the floor: the walls sit 1.5+ m from the
|
| 586 |
// duck by construction and would vanish with the floor's 3 m fade.
|
|
@@ -601,10 +610,14 @@ function makeWallGridMaterial(alongX) {
|
|
| 601 |
varying vec3 vWorld;
|
| 602 |
uniform float uCell, uSection, uFadeDist, uWallH, uAlongX;
|
| 603 |
uniform vec3 uCellColor, uSectionColor, uFocus;
|
|
|
|
| 604 |
float gridLine(vec2 p, float size) {
|
| 605 |
vec2 r = p / size;
|
| 606 |
vec2 g = abs(fract(r - 0.5) - 0.5) / fwidth(r);
|
| 607 |
-
|
|
|
|
|
|
|
|
|
|
| 608 |
}
|
| 609 |
void main() {
|
| 610 |
// Wall surface coords: the in-plane horizontal world axis + height.
|
|
@@ -615,8 +628,8 @@ function makeWallGridMaterial(alongX) {
|
|
| 615 |
float d = distance(vWorld.xz, uFocus.xz);
|
| 616 |
float fade = pow(clamp(1.0 - d / uFadeDist, 0.0, 1.0), 1.6);
|
| 617 |
float vert = 1.0 - clamp(vWorld.y / uWallH, 0.0, 1.0);
|
| 618 |
-
vec3 col = mix(uCellColor, uSectionColor, section);
|
| 619 |
-
float alpha = max(section * 0.
|
| 620 |
if (alpha < 0.004) discard;
|
| 621 |
gl_FragColor = vec4(col, alpha);
|
| 622 |
}
|
|
@@ -916,7 +929,8 @@ const keyEls = {
|
|
| 916 |
padX: el("key-pad-x"), padSit: el("key-pad-sit"),
|
| 917 |
padRun: el("key-pad-run"), padRt: el("key-pad-rt"),
|
| 918 |
padRb: el("key-pad-rb"), padLb: el("key-pad-lb"),
|
| 919 |
-
padY: el("key-pad-y"),
|
|
|
|
| 920 |
};
|
| 921 |
const STICK_R = 15; // px, max dot travel inside the 46px stick circle
|
| 922 |
let resetFlashAt = -Infinity;
|
|
@@ -961,6 +975,8 @@ function renderStats() {
|
|
| 961 |
keyEls.padSit.classList.toggle("lit", sitting);
|
| 962 |
keyEls.padRun.classList.toggle("lit", padPrev.dpadUp);
|
| 963 |
keyEls.padRt.classList.toggle("lit", padJaw > 0.3);
|
|
|
|
|
|
|
| 964 |
}
|
| 965 |
|
| 966 |
// Real implementation assigned in the gamepad section below; the render
|
|
@@ -1024,18 +1040,62 @@ window.addEventListener("keyup", (e) => {
|
|
| 1024 |
window.addEventListener("blur", () => { held.clear(); refreshVelCmd(); });
|
| 1025 |
|
| 1026 |
// ββ Gamepad: same mapping as the robot runtime (microduck_runtime) ββββββ
|
| 1027 |
-
//
|
| 1028 |
-
//
|
| 1029 |
-
//
|
|
|
|
| 1030 |
const PAD_DEADZONE = 0.15;
|
| 1031 |
const PAD_ALPHA = 0.12;
|
| 1032 |
const dz = (v) => (Math.abs(v) < PAD_DEADZONE ? 0 : v);
|
| 1033 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1034 |
pollPad = function pollGamepad() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1035 |
const gp = [...(navigator.getGamepads?.() ?? [])].find((p) => p && p.connected);
|
| 1036 |
document.body.classList.toggle("pad-connected", !!gp);
|
| 1037 |
if (!gp) {
|
| 1038 |
if (padActive) { padActive = false; padCmd.fill(0); padJaw = 0; }
|
|
|
|
|
|
|
|
|
|
| 1039 |
return;
|
| 1040 |
}
|
| 1041 |
// Left stick only: vertical = forward/back, horizontal = turn.
|
|
@@ -1057,6 +1117,15 @@ pollPad = function pollGamepad() {
|
|
| 1057 |
padCmd.fill(0);
|
| 1058 |
}
|
| 1059 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
// Standard mapping indices: X=2, Y=3, LB=4, RB=5, DpadDown=13, RT=7 (analog).
|
| 1061 |
const x = !!gp.buttons[2]?.pressed;
|
| 1062 |
if (x && !padPrev.x) triggerRoll("pad");
|
|
@@ -1239,6 +1308,7 @@ window.rl = {
|
|
| 1239 |
get ballQposAdr() { return ballQposAdr; },
|
| 1240 |
get chaseCam() { return chaseCam; },
|
| 1241 |
set chaseCam(v) { chaseCam = !!v; },
|
|
|
|
| 1242 |
step: async (n = 1) => { for (let i = 0; i < n; i++) await controlStep(); },
|
| 1243 |
render: () => { syncRig(); renderer.render(scene, camera); },
|
| 1244 |
// One full render-loop iteration, for tests driving frames manually.
|
|
|
|
| 256 |
// the control loop reads it before the input section below has evaluated.
|
| 257 |
const padCmd = new Float32Array(3);
|
| 258 |
let padActive = false;
|
| 259 |
+
const padPrev = { x: false, y: false, rb: false, lb: false, r3: false, dpadDown: false, dpadUp: false, rtArmed: true };
|
| 260 |
+
// Right-stick camera state, read by renderStats before the gamepad
|
| 261 |
+
// section below has evaluated (same reason as padCmd above).
|
| 262 |
+
let padOrbitLive = false; // HUD: RS keycap lit while deflected
|
| 263 |
// Declared before the control loop starts: buildObs reads it via
|
| 264 |
// effectiveCmd on the very first control step.
|
| 265 |
const held = new Set();
|
|
|
|
| 530 |
uniforms: {
|
| 531 |
uCell: { value: 0.1 },
|
| 532 |
uSection: { value: 0.5 },
|
| 533 |
+
uCellColor: { value: new THREE.Color(0x8e8371) },
|
| 534 |
uSectionColor: { value: new THREE.Color(0xffb366) },
|
| 535 |
uFadeDist: { value: 3.0 },
|
| 536 |
uFocus: { value: new THREE.Vector3() },
|
|
|
|
| 547 |
varying vec3 vWorld;
|
| 548 |
uniform float uCell, uSection, uFadeDist;
|
| 549 |
uniform vec3 uCellColor, uSectionColor, uFocus;
|
| 550 |
+
// Tron-style line: a thicker antialiased core plus a faint, much
|
| 551 |
+
// wider halo added on top (squared falloff keeps it a whisper of a
|
| 552 |
+
// glow rather than a bloom wash).
|
| 553 |
float gridLine(vec2 p, float size) {
|
| 554 |
vec2 r = p / size;
|
| 555 |
vec2 g = abs(fract(r - 0.5) - 0.5) / fwidth(r);
|
| 556 |
+
float d = min(g.x, g.y);
|
| 557 |
+
float core = 1.0 - smoothstep(0.0, 1.8, d);
|
| 558 |
+
float halo = 1.0 - smoothstep(0.0, 7.0, d);
|
| 559 |
+
return core + halo * halo * 0.22;
|
| 560 |
}
|
| 561 |
void main() {
|
| 562 |
float cell = gridLine(vWorld.xz, uCell);
|
| 563 |
float section = gridLine(vWorld.xz, uSection);
|
| 564 |
float d = distance(vWorld.xz, uFocus.xz);
|
| 565 |
float fade = pow(clamp(1.0 - d / uFadeDist, 0.0, 1.0), 1.6);
|
| 566 |
+
vec3 col = mix(uCellColor, uSectionColor, clamp(section, 0.0, 1.0));
|
| 567 |
+
float alpha = min(max(section * 0.6, cell * 0.4) * fade, 1.0);
|
| 568 |
if (alpha < 0.004) discard;
|
| 569 |
gl_FragColor = vec4(col, alpha);
|
| 570 |
}
|
|
|
|
| 589 |
uniforms: {
|
| 590 |
uCell: { value: 0.1 },
|
| 591 |
uSection: { value: 0.5 },
|
| 592 |
+
uCellColor: { value: new THREE.Color(0x8e8371) },
|
| 593 |
uSectionColor: { value: new THREE.Color(0xffb366) },
|
| 594 |
// Gentler radial fade than the floor: the walls sit 1.5+ m from the
|
| 595 |
// duck by construction and would vanish with the floor's 3 m fade.
|
|
|
|
| 610 |
varying vec3 vWorld;
|
| 611 |
uniform float uCell, uSection, uFadeDist, uWallH, uAlongX;
|
| 612 |
uniform vec3 uCellColor, uSectionColor, uFocus;
|
| 613 |
+
// Same Tron-style core + faint halo as the floor grid.
|
| 614 |
float gridLine(vec2 p, float size) {
|
| 615 |
vec2 r = p / size;
|
| 616 |
vec2 g = abs(fract(r - 0.5) - 0.5) / fwidth(r);
|
| 617 |
+
float d = min(g.x, g.y);
|
| 618 |
+
float core = 1.0 - smoothstep(0.0, 1.8, d);
|
| 619 |
+
float halo = 1.0 - smoothstep(0.0, 7.0, d);
|
| 620 |
+
return core + halo * halo * 0.22;
|
| 621 |
}
|
| 622 |
void main() {
|
| 623 |
// Wall surface coords: the in-plane horizontal world axis + height.
|
|
|
|
| 628 |
float d = distance(vWorld.xz, uFocus.xz);
|
| 629 |
float fade = pow(clamp(1.0 - d / uFadeDist, 0.0, 1.0), 1.6);
|
| 630 |
float vert = 1.0 - clamp(vWorld.y / uWallH, 0.0, 1.0);
|
| 631 |
+
vec3 col = mix(uCellColor, uSectionColor, clamp(section, 0.0, 1.0));
|
| 632 |
+
float alpha = min(max(section * 0.9, cell * 0.6) * fade * (0.3 + 0.7 * vert), 1.0);
|
| 633 |
if (alpha < 0.004) discard;
|
| 634 |
gl_FragColor = vec4(col, alpha);
|
| 635 |
}
|
|
|
|
| 929 |
padX: el("key-pad-x"), padSit: el("key-pad-sit"),
|
| 930 |
padRun: el("key-pad-run"), padRt: el("key-pad-rt"),
|
| 931 |
padRb: el("key-pad-rb"), padLb: el("key-pad-lb"),
|
| 932 |
+
padY: el("key-pad-y"), padRs: el("key-pad-rs"),
|
| 933 |
+
padR3: el("key-pad-r3"),
|
| 934 |
};
|
| 935 |
const STICK_R = 15; // px, max dot travel inside the 46px stick circle
|
| 936 |
let resetFlashAt = -Infinity;
|
|
|
|
| 975 |
keyEls.padSit.classList.toggle("lit", sitting);
|
| 976 |
keyEls.padRun.classList.toggle("lit", padPrev.dpadUp);
|
| 977 |
keyEls.padRt.classList.toggle("lit", padJaw > 0.3);
|
| 978 |
+
keyEls.padRs.classList.toggle("lit", padOrbitLive); // while deflected
|
| 979 |
+
keyEls.padR3.classList.toggle("lit", chaseCam); // steady while chasing
|
| 980 |
}
|
| 981 |
|
| 982 |
// Real implementation assigned in the gamepad section below; the render
|
|
|
|
| 1040 |
window.addEventListener("blur", () => { held.clear(); refreshVelCmd(); });
|
| 1041 |
|
| 1042 |
// ββ Gamepad: same mapping as the robot runtime (microduck_runtime) ββββββ
|
| 1043 |
+
// Left stick: vertical = vx (asymmetric fwd/back), horizontal = turn,
|
| 1044 |
+
// EMA-smoothed like the runtime's cmd_alpha. Right stick orbits the
|
| 1045 |
+
// camera (detaches the chase cam, R3 re-toggles it). X = roll, Y = ball,
|
| 1046 |
+
// RB/LB = kicks, DpadDown = sit/stand, DpadUp = back to run, RT = mouth.
|
| 1047 |
const PAD_DEADZONE = 0.15;
|
| 1048 |
const PAD_ALPHA = 0.12;
|
| 1049 |
const dz = (v) => (Math.abs(v) < PAD_DEADZONE ? 0 : v);
|
| 1050 |
|
| 1051 |
+
// Right-stick camera orbit, in OrbitControls-compatible terms: rebuild
|
| 1052 |
+
// the camera-target offset as a spherical, nudge azimuth/elevation, and
|
| 1053 |
+
// re-place the camera. controls.update() then runs on the result, so the
|
| 1054 |
+
// damping bookkeeping never fights it (same trick as updateChaseCam).
|
| 1055 |
+
// The stick does not move the camera directly: it steers an angular
|
| 1056 |
+
// VELOCITY that eases toward the stick's target rate (frame-rate
|
| 1057 |
+
// independent exponential), so pushing ramps up gently and releasing
|
| 1058 |
+
// coasts to a stop over ~0.3 s instead of freezing on the spot.
|
| 1059 |
+
// Vertical is flight-style inverted by request: stick up orbits the
|
| 1060 |
+
// camera downward, stick down orbits it upward.
|
| 1061 |
+
const PAD_ORBIT_SPEED = 2.4; // rad/s at full deflection
|
| 1062 |
+
const PAD_ORBIT_SMOOTH = 8; // 1/s response rate (~95% in 0.37 s)
|
| 1063 |
+
const padOrbitVel = { az: 0, el: 0 }; // smoothed angular velocity, rad/s
|
| 1064 |
+
const _padSph = new THREE.Spherical();
|
| 1065 |
+
const _padOff = new THREE.Vector3();
|
| 1066 |
+
function padOrbitStep(rx, ry, dt) {
|
| 1067 |
+
padOrbitLive = rx !== 0 || ry !== 0;
|
| 1068 |
+
if (padOrbitLive) chaseCam = false; // detach, same as a mouse grab
|
| 1069 |
+
const k = 1 - Math.exp(-PAD_ORBIT_SMOOTH * dt);
|
| 1070 |
+
padOrbitVel.az += (rx * PAD_ORBIT_SPEED - padOrbitVel.az) * k;
|
| 1071 |
+
// Inverted Y; elevation runs a touch slower, full-rate pitch is twitchy.
|
| 1072 |
+
padOrbitVel.el += (-ry * PAD_ORBIT_SPEED * 0.75 - padOrbitVel.el) * k;
|
| 1073 |
+
if (chaseCam) { padOrbitVel.az = 0; padOrbitVel.el = 0; return; }
|
| 1074 |
+
if (Math.abs(padOrbitVel.az) < 1e-3 && Math.abs(padOrbitVel.el) < 1e-3) return;
|
| 1075 |
+
_padOff.copy(camera.position).sub(controls.target);
|
| 1076 |
+
_padSph.setFromVector3(_padOff);
|
| 1077 |
+
// Stick right sweeps the camera right around the duck.
|
| 1078 |
+
_padSph.theta -= padOrbitVel.az * dt;
|
| 1079 |
+
_padSph.phi += padOrbitVel.el * dt;
|
| 1080 |
+
_padSph.phi = Math.min(controls.maxPolarAngle, Math.max(0.08, _padSph.phi));
|
| 1081 |
+
_padSph.makeSafe();
|
| 1082 |
+
camera.position.setFromSpherical(_padSph).add(controls.target);
|
| 1083 |
+
camera.lookAt(controls.target);
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
let padPollT = performance.now();
|
| 1087 |
pollPad = function pollGamepad() {
|
| 1088 |
+
const now = performance.now();
|
| 1089 |
+
// Clamped so a background-tab stall can't slingshot the camera.
|
| 1090 |
+
const dt = Math.min((now - padPollT) / 1000, 0.05);
|
| 1091 |
+
padPollT = now;
|
| 1092 |
const gp = [...(navigator.getGamepads?.() ?? [])].find((p) => p && p.connected);
|
| 1093 |
document.body.classList.toggle("pad-connected", !!gp);
|
| 1094 |
if (!gp) {
|
| 1095 |
if (padActive) { padActive = false; padCmd.fill(0); padJaw = 0; }
|
| 1096 |
+
padOrbitLive = false;
|
| 1097 |
+
padOrbitVel.az = 0;
|
| 1098 |
+
padOrbitVel.el = 0;
|
| 1099 |
return;
|
| 1100 |
}
|
| 1101 |
// Left stick only: vertical = forward/back, horizontal = turn.
|
|
|
|
| 1117 |
padCmd.fill(0);
|
| 1118 |
}
|
| 1119 |
|
| 1120 |
+
// Right stick: camera orbit with velocity smoothing. Runs every frame
|
| 1121 |
+
// (not just while deflected) so a released stick coasts to a stop.
|
| 1122 |
+
padOrbitStep(dz(gp.axes[2] ?? 0), dz(gp.axes[3] ?? 0), dt);
|
| 1123 |
+
|
| 1124 |
+
// R3 (right stick click): toggle the chase cam, gamepad twin of KeyC.
|
| 1125 |
+
const r3Btn = !!gp.buttons[11]?.pressed;
|
| 1126 |
+
if (r3Btn && !padPrev.r3) chaseCam = !chaseCam;
|
| 1127 |
+
padPrev.r3 = r3Btn;
|
| 1128 |
+
|
| 1129 |
// Standard mapping indices: X=2, Y=3, LB=4, RB=5, DpadDown=13, RT=7 (analog).
|
| 1130 |
const x = !!gp.buttons[2]?.pressed;
|
| 1131 |
if (x && !padPrev.x) triggerRoll("pad");
|
|
|
|
| 1308 |
get ballQposAdr() { return ballQposAdr; },
|
| 1309 |
get chaseCam() { return chaseCam; },
|
| 1310 |
set chaseCam(v) { chaseCam = !!v; },
|
| 1311 |
+
padOrbitStep,
|
| 1312 |
step: async (n = 1) => { for (let i = 0; i < n; i++) await controlStep(); },
|
| 1313 |
render: () => { syncRig(); renderer.render(scene, camera); },
|
| 1314 |
// One full render-loop iteration, for tests driving frames manually.
|