| import { useState } from "react"; |
| import { gameApi, useGame } from "../store.js"; |
| import { Quickbar, SoundToggle } from "./Hud.jsx"; |
| import SimulatorPanel from "./SimulatorPanel.jsx"; |
|
|
| export default function MicroduckControls() { |
| const loco = useGame((s) => s.loco); |
| const mode = useGame((s) => s.modeLabel); |
| const locked = useGame((s) => s.emotionBusy || s.locoSwitching || s.scriptPlaying); |
| const recording = useGame((s) => s.scriptRecording); |
| const [head, setHead] = useState([0, 0, 0, 0]); |
| const release = () => gameApi.setDrive?.(0, 0); |
| const drive = (e, forward, turn) => { |
| e.currentTarget.setPointerCapture(e.pointerId); |
| if (recording) gameApi.script?.add(`${forward > 0 ? "Forward" : forward < 0 ? "Backwards" : turn > 0 ? "Turn left" : "Turn right"}`, async () => { gameApi.setDrive?.(forward, turn); await gameApi.script.wait(1); gameApi.setDrive?.(0, 0); }); |
| else gameApi.setDrive?.(forward, turn); |
| }; |
| const action = (label, fn) => recording ? gameApi.script?.add(label, async () => { await fn(); await gameApi.script.wait(0.4); }) : fn(); |
| return <SimulatorPanel side="right" title="Microduck"> |
| <p role="status">{mode} 路 {loco}</p> |
| <fieldset disabled={locked}> |
| <legend>Move 路 hold to drive</legend> |
| <div className="sim-buttons">{[["Forward", 1, 0], ["Backwards", -1, 0], ["Turn left", 0, 1], ["Turn right", 0, -1]].map(([label, forward, turn]) => <button key={label} onPointerDown={(e) => drive(e, forward, turn)} onPointerUp={release} onPointerCancel={release} onLostPointerCapture={release} onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") { e.preventDefault(); gameApi.setDrive?.(forward, turn); } }} onKeyUp={release} onBlur={release}>{label}</button>)}</div> |
| <legend>Actions</legend> |
| <div className="sim-buttons"> |
| <button disabled={loco !== "legs"} onClick={() => action("Kick left", () => gameApi.triggerKick?.("left"))}>Kick left</button> |
| <button disabled={loco !== "legs"} onClick={() => action("Kick right", () => gameApi.triggerKick?.("right"))}>Kick right</button> |
| <button disabled={loco !== "legs"} onClick={() => action("Roll", () => gameApi.triggerRoll?.())}>Roll</button> |
| <button disabled={loco !== "legs"} onClick={() => action("Sit", () => gameApi.setMode?.("sit"))}>Sit</button> |
| <button onClick={() => action("Stand / Walk", () => gameApi.setMode?.("walk"))}>Stand / Walk</button> |
| <button disabled={loco !== "rollers"} onClick={() => action("Crouch", () => gameApi.triggerCrouch?.())}>Crouch</button> |
| <button disabled={loco !== "legs"} onClick={() => action("Ground pick", () => gameApi.triggerGroundPick?.())}>Ground pick</button> |
| <button onClick={() => action("Microduck quack", () => gameApi.quack?.())}>Quack</button> |
| <button onClick={() => action("Ball", () => gameApi.spawnBall?.())}>Ball</button> |
| <button onClick={() => action("Reset Microduck", () => gameApi.resetSim?.())}>Reset</button> |
| </div> |
| <legend>Emotions</legend> |
| <div className="sim-buttons">{[["joy", "Happy"], ["sadness", "Sad"], ["anger", "Angry"]].map(([id, label]) => <button key={id} onClick={() => action(`Microduck ${label}`, () => gameApi.playMicroduckEmotion?.(id))}>{label}</button>)}</div> |
| <legend>Head</legend> |
| <button onClick={() => action("Toggle head control", () => gameApi.toggleHeadMode?.())}>Toggle gamepad head control</button> |
| {["Neck pitch", "Head pitch", "Head yaw", "Head roll"].map((label, i) => <label className="sim-field" key={label}><span>{label}</span><input type="range" min="-0.6" max="0.6" step="0.02" value={head[i]} onChange={(e) => { const next = [...head]; next[i] = Number(e.target.value); setHead(next); gameApi.setHead?.(next); }} /></label>)} |
| <button onClick={() => action("Neutral head", () => { setHead([0, 0, 0, 0]); gameApi.setHead?.([0, 0, 0, 0]); })}>Neutral head</button> |
| <legend>Appearance & locomotion</legend> |
| <Quickbar /> |
| <legend>Camera</legend> |
| <div className="sim-buttons"><button onClick={() => action("Chase camera", () => gameApi.toggleCamera?.())}>Chase camera</button><button onClick={() => action("Both robots camera", () => gameApi.sharedCamera?.())}>Both robots</button></div> |
| </fieldset> |
| <SoundToggle /> |
| <details className="sim-help"><summary>Keyboard & gamepad</summary><p>Arrows / WASD / ZQSD: walk and turn. Q/E: kicks. F: alternate kicks. R: sit/stand or crouch. G: ground pick. M: legs/rollers. Space: reset. C: chase camera. Drag: orbit. Scroll: zoom.</p><p>Gamepad: left stick moves; right stick orbits; Y controls head; LB/RB kick; X or D-pad down sits; A picks; hold D-pad up switches mode; RT quacks; LT wheee; R3 chase; Menu pauses.</p></details> |
| </SimulatorPanel>; |
| } |
|
|