Files
gta6/three-player-controller/example/control-hints.js
knsoandClaude Opus 5 fb38652e7f chore(controller): three-player-controller als Ordner statt Submodul
Das Submodul zeigte auf GitHub, der eingetragene Stand 028c652 lag aber nur
lokal vor: GitHub-Master ac39682 plus Showcase mit Figuren und launch.json.
Frische Klone konnten den Controller deshalb nicht laden. Jetzt liegt dieser
Stand als normaler Ordner im Repo, ohne Verweis auf GitHub.

dist/ und node_modules/ im Controller bleiben ignoriert und werden wie bisher
lokal gebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:27:03 +02:00

174 lines
5.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 按键文字 -> KeyboardEvent.code(可被 data-code 覆盖)
const LABEL_CODES = {
W: ["KeyW", "ArrowUp"],
A: ["KeyA", "ArrowLeft"],
S: ["KeyS", "ArrowDown"],
D: ["KeyD", "ArrowRight"],
Shift: ["ShiftLeft", "ShiftRight"],
Space: ["Space"],
RMB: ["Mouse2"],
LMB: ["Mouse0"],
1: ["Digit1"],
4: ["Digit4"],
};
// 在 GUI / 输入框里操作时不点亮 HUD
function shouldIgnore(el) {
if (!(el instanceof Element)) return false;
return Boolean(el.closest("input, textarea, select, [contenteditable='true'], .lil-gui"));
}
// 解析 kbd 对应的键码:优先 data-code,否则按显示文字查表
function codesFor(kbd) {
const explicit = kbd.getAttribute("data-code");
if (explicit) return explicit.trim().split(/\s+/).filter(Boolean);
const label = kbd.textContent.replace(/\s+/g, " ").trim();
if (LABEL_CODES[label]) return LABEL_CODES[label];
if (/^[A-Za-z]$/.test(label)) return [`Key${label.toUpperCase()}`];
return [];
}
const LOOK_SCALE = 0.28; // 鼠标位移换算到指示点的系数
const LOOK_MAX = 5; // 指示点最大偏移(px)
const LOOK_DECAY = 0.84; // 每帧回弹衰减
const LOOK_REST = 0.04; // 小于此值视为静止
// 确保每个移动提示块都有鼠标图标(HTML 未写时补一个)
function ensureLookMice(hud) {
const mice = [];
for (const move of hud.querySelectorAll(".hint-move")) {
let mouse = move.querySelector(".hint-mouse");
if (!mouse) {
const cluster = document.createElement("div");
cluster.className = "hint-cluster";
cluster.innerHTML = '<div class="hint-mouse" aria-hidden="true"><div class="hint-mouse-body"><span class="hint-mouse-wheel"></span><span class="hint-mouse-nub"></span></div></div><span class="hint-text">Look</span>';
move.append(cluster);
mouse = cluster.querySelector(".hint-mouse");
}
mice.push(mouse);
}
return mice;
}
// 绑定 HUD 按键高亮与鼠标 Look 指示
export function bindControlHints(hud = document.querySelector(".hud")) {
if (!hud) return;
const keys = [...hud.querySelectorAll("kbd")];
const mice = ensureLookMice(hud);
if (!keys.length && !mice.length) return;
const pressed = new Set(); // 当前按下的 KeyboardEvent.code / MouseN
let lookX = 0;
let lookY = 0;
let lookRaf = 0;
// 同步 kbd.is-down
const syncKeys = () => {
for (const kbd of keys) {
kbd.classList.toggle("is-down", codesFor(kbd).some((code) => pressed.has(code)));
}
};
// 把 Look 偏移写到鼠标图标
const applyLook = () => {
const x = `${lookX}px`;
const y = `${lookY}px`;
const active = Math.hypot(lookX, lookY) > LOOK_REST;
for (const mouse of mice) {
mouse.style.setProperty("--look-x", x);
mouse.style.setProperty("--look-y", y);
mouse.classList.toggle("is-active", active);
}
};
// 指示点回弹;静止后停掉 rAF
const tickLook = () => {
lookX *= LOOK_DECAY;
lookY *= LOOK_DECAY;
if (Math.abs(lookX) < LOOK_REST && Math.abs(lookY) < LOOK_REST) {
lookX = 0;
lookY = 0;
lookRaf = 0;
applyLook();
return;
}
applyLook();
lookRaf = requestAnimationFrame(tickLook);
};
// 键盘按下 / 抬起
const onKey = (event, down) => {
if (down && shouldIgnore(event.target)) return;
if (down) pressed.add(event.code);
else pressed.delete(event.code);
syncKeys();
};
// 鼠标按键(LMB / RMB 等)
const onMouse = (event, down) => {
if (down && shouldIgnore(event.target)) return;
const code = `Mouse${event.button}`;
if (down) pressed.add(code);
else pressed.delete(code);
syncKeys();
};
// 指针锁定时始终跟踪;否则仅在按住拖拽转视角时跟踪
const onMouseMove = (event) => {
if (!document.pointerLockElement && event.buttons === 0) return;
if (!document.pointerLockElement && shouldIgnore(event.target)) return;
lookX = Math.max(-LOOK_MAX, Math.min(LOOK_MAX, lookX + event.movementX * LOOK_SCALE));
lookY = Math.max(-LOOK_MAX, Math.min(LOOK_MAX, lookY + event.movementY * LOOK_SCALE));
applyLook();
if (!lookRaf) lookRaf = requestAnimationFrame(tickLook);
};
// 失焦时清掉按下态和 Look 偏移
const clear = () => {
pressed.clear();
syncKeys();
lookX = 0;
lookY = 0;
if (lookRaf) {
cancelAnimationFrame(lookRaf);
lookRaf = 0;
}
applyLook();
};
window.addEventListener("keydown", (event) => onKey(event, true));
window.addEventListener("keyup", (event) => onKey(event, false));
window.addEventListener("mousedown", (event) => onMouse(event, true));
window.addEventListener("mouseup", (event) => onMouse(event, false));
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("blur", clear);
document.addEventListener("visibilitychange", () => {
if (document.hidden) clear();
});
}
// 按上下车切换走路 / 载具两套提示,并转调原有回调
export function bindVehicleHintMode(player, hud = document.querySelector(".hud")) {
if (!player || !hud) return;
const setMode = (mode) => {
hud.dataset.hintMode = mode;
};
setMode(player.getControllerMode?.() === 1 ? "vehicle" : "on-foot");
const prevEnter = player.onVehicleEnter;
const prevExit = player.onVehicleExit;
player.onVehicleEnter = (vehicle) => {
setMode("vehicle");
prevEnter?.(vehicle);
};
player.onVehicleExit = (vehicle) => {
setMode("on-foot");
prevExit?.(vehicle);
};
}
bindControlHints();