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>
This commit is contained in:
knsoandClaude Opus 5 committed 2026-09-15 21:27:03 +02:00
1 parent 780e246fe5
commit fb38652e7f
153 files changed
+42561 -4

No files matched your search

@@ -0,0 +1,173 @@
// 按键文字 -> 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();