Kofferdetektiv iPad: zwei feste Joysticks (immer 50% sichtbar)

Statt schwebendem Links-Joystick + Rechts-Wisch jetzt zwei feste,
halbtransparente Joysticks:
- links = Umsehen (Auge 👁), ratenbasiertes Drehen/Neigen in update()
- rechts = Bewegen (Fuß 🦶)
Touch wird dem naeheren Joystick im Fangradius zugewiesen (assign/stickVec).
Interaktions-Button (🧳) nach unten-mittig verschoben (kollidierte sonst
mit dem rechten Bewegungs-Joystick). Joysticks nur auf Touch sichtbar
(body.kd-touch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 13:57:20 +02:00
parent 8ec005f889
commit 11c48817de
2 changed files with 56 additions and 33 deletions
+41 -25
View File
@@ -393,7 +393,8 @@ const World = {
fundMeshes: {}, // Fundsachen am Boden (Badge-Sammeln), je Spawn-Index
player: { x: 0, z: 24, yaw: 0, speed: 4.2 },
keys: {}, look: { lx: 0, ly: 0 }, pitch: 0,
joy: { active: false, dx: 0, dy: 0 },
joy: { active: false, dx: 0, dy: 0 }, // rechter Joystick: Bewegen (Fuß)
lookJoy: { dx: 0, dy: 0 }, // linker Joystick: Umsehen (Auge)
moving: false, locked: false,
jumpY: 0, vy: 0,
animated: [], // {update(dt,t)} — Bänder, Roboter, Blinklichter
@@ -2757,41 +2758,46 @@ const World = {
}
});
// Touch: linke Hälfte Joystick, rechte Hälfte Umschauen
const joyEl = $('joystick'), knob = $('joy-knob');
const joyPos = t => {
const r = joyEl.getBoundingClientRect();
const dx = (t.clientX - (r.left + r.width / 2)) / (r.width / 2);
const dy = (t.clientY - (r.top + r.height / 2)) / (r.height / 2);
// Touch: zwei feste, immer sichtbare Joysticks.
// links = Umsehen (Auge 👁) → this.lookJoy (ratenbasiertes Drehen in update())
// rechts = Bewegen (Fuß 🦶) → this.joy
if (IS_TOUCH) document.body.classList.add('kd-touch');
const moveEl = $('joystick'), moveKnob = $('joy-knob');
const lookEl = $('lookstick'), lookKnob = $('look-knob');
const KNOB_TRAVEL = 34; // px, wie weit der Knopf maximal auswandert
const CATCH_R = 160; // px Fangradius um den Joystick-Mittelpunkt
const centerOf = el => { const r = el.getBoundingClientRect(); return { cx: r.left + r.width / 2, cy: r.top + r.height / 2, hw: r.width / 2, hh: r.height / 2 }; };
// Normalisierter Vektor (1..1) eines Touches relativ zum festen Joystick-Zentrum.
const stickVec = (el, t) => {
const c = centerOf(el);
const dx = (t.clientX - c.cx) / c.hw, dy = (t.clientY - c.cy) / c.hh;
const len = Math.hypot(dx, dy) || 1, cl = Math.min(1, len);
return { dx: dx / len * cl, dy: dy / len * cl };
};
// Weist einen neuen Touch dem näheren freien Joystick zu (nur im Fangradius).
const assign = t => {
const dist = el => { const c = centerOf(el); return Math.hypot(t.clientX - c.cx, t.clientY - c.cy); };
const dM = dist(moveEl), dL = dist(lookEl);
if (dM <= dL) return dM < CATCH_R ? 'move' : null;
return dL < CATCH_R ? 'look' : null;
};
let joyTouch = null, lookTouch = null;
const applyMove = t => { const v = stickVec(moveEl, t); this.joy.dx = v.dx; this.joy.dy = v.dy; moveKnob.style.transform = 'translate(' + v.dx * KNOB_TRAVEL + 'px,' + v.dy * KNOB_TRAVEL + 'px)'; };
const applyLook = t => { const v = stickVec(lookEl, t); this.lookJoy.dx = v.dx; this.lookJoy.dy = v.dy; lookKnob.style.transform = 'translate(' + v.dx * KNOB_TRAVEL + 'px,' + v.dy * KNOB_TRAVEL + 'px)'; };
canvas.parentElement.addEventListener('touchstart', e => {
if (UI.overlayOpen()) return;
if (e.target.closest && e.target.closest('#btn-touch-act, #btn-map, #minimap-wrap')) return;
for (const t of e.changedTouches) {
if (t.clientX < innerWidth / 2 && joyTouch === null) {
joyTouch = t.identifier; this.joy.active = true;
joyEl.style.left = (t.clientX - 60) + 'px'; joyEl.style.top = (t.clientY - 60) + 'px';
joyEl.classList.add('visible');
const p = joyPos(t); this.joy.dx = p.dx; this.joy.dy = p.dy;
} else if (lookTouch === null) {
lookTouch = t.identifier; this.look.lx = t.clientX; this.look.ly = t.clientY;
}
const which = assign(t);
if (which === 'move' && joyTouch === null) { joyTouch = t.identifier; this.joy.active = true; applyMove(t); }
else if (which === 'look' && lookTouch === null) { lookTouch = t.identifier; applyLook(t); }
}
e.preventDefault();
}, { passive: false });
canvas.parentElement.addEventListener('touchmove', e => {
for (const t of e.changedTouches) {
if (t.identifier === joyTouch) {
const p = joyPos(t); this.joy.dx = p.dx; this.joy.dy = p.dy;
knob.style.transform = 'translate(' + p.dx * 34 + 'px,' + p.dy * 34 + 'px)';
} else if (t.identifier === lookTouch) {
this.player.yaw -= (t.clientX - this.look.lx) * 0.005;
this.pitch = Math.max(-0.9, Math.min(0.9, this.pitch - (t.clientY - this.look.ly) * 0.004));
this.look.lx = t.clientX; this.look.ly = t.clientY;
}
if (t.identifier === joyTouch) applyMove(t);
else if (t.identifier === lookTouch) applyLook(t);
}
e.preventDefault();
}, { passive: false });
@@ -2799,9 +2805,12 @@ const World = {
for (const t of e.changedTouches) {
if (t.identifier === joyTouch) {
joyTouch = null; this.joy.active = false; this.joy.dx = this.joy.dy = 0;
joyEl.classList.remove('visible'); knob.style.transform = '';
moveKnob.style.transform = '';
}
if (t.identifier === lookTouch) {
lookTouch = null; this.lookJoy.dx = this.lookJoy.dy = 0;
lookKnob.style.transform = '';
}
if (t.identifier === lookTouch) lookTouch = null;
}
};
canvas.parentElement.addEventListener('touchend', endTouch);
@@ -2878,6 +2887,13 @@ const World = {
if (!this.blocked(p.x + vx, p.z)) p.x += vx;
if (!this.blocked(p.x, p.z + vz)) p.z += vz;
}
// Linker Joystick: ratenbasiertes Umsehen (Auslenkung = Dreh-/Neige-Tempo).
if (this.lookJoy.dx || this.lookJoy.dy) {
const LOOK_RATE = 2.4; // rad/s bei Vollausschlag
p.yaw -= this.lookJoy.dx * LOOK_RATE * dt;
this.pitch = Math.max(-0.9, Math.min(0.9, this.pitch - this.lookJoy.dy * LOOK_RATE * dt));
}
if (this.mode === 'glb') this.checkPortals();
Sound.setWalking(this.moving && this.jumpY <= 0 && !UI.overlayOpen(),
this.keys['ShiftLeft'] || this.keys['ShiftRight']
+15 -8
View File
@@ -88,15 +88,21 @@ img{max-width:100%}
background:rgba(232,197,71,.94);color:#1f2733;font-weight:700;padding:10px 18px;border-radius:10px;
font-size:15px;opacity:0;transition:opacity .2s}
#prompt.visible{opacity:1}
#btn-touch-act{position:absolute;right:26px;bottom:90px;width:84px;height:84px;border-radius:50%;
background:rgba(232,197,71,.92);border:none;font-size:34px;display:none;pointer-events:auto;
#btn-touch-act{position:absolute;left:50%;bottom:44px;transform:translateX(-50%);width:84px;height:84px;border-radius:50%;
background:rgba(232,197,71,.92);border:none;font-size:34px;display:none;pointer-events:auto;z-index:45;
box-shadow:0 4px 14px rgba(0,0,0,.4)}
#btn-touch-act.visible{display:block}
#joystick{position:absolute;width:120px;height:120px;border-radius:50%;border:2px solid rgba(159,216,239,.4);
background:rgba(13,21,32,.35);display:none;pointer-events:none}
#joystick.visible{display:block}
#joy-knob{position:absolute;left:50%;top:50%;width:48px;height:48px;margin:-24px 0 0 -24px;border-radius:50%;
background:rgba(159,216,239,.55)}
/* Zwei feste Joysticks (nur Touch), IMMER halbtransparent sichtbar:
links Umsehen (Auge), rechts Bewegen (Fuß). */
#joystick,#lookstick{position:absolute;bottom:30px;width:132px;height:132px;border-radius:50%;
border:2px solid rgba(159,216,239,.45);background:rgba(13,21,32,.30);
display:none;opacity:.5;pointer-events:none;touch-action:none;z-index:40}
body.kd-touch #joystick,body.kd-touch #lookstick{display:block}
#lookstick{left:24px}
#joystick{right:24px}
#joy-knob,#look-knob{position:absolute;left:50%;top:50%;width:58px;height:58px;margin:-29px 0 0 -29px;border-radius:50%;
background:rgba(159,216,239,.55);display:flex;align-items:center;justify-content:center;
font-size:28px;line-height:1;transition:transform .04s linear}
#toast{position:absolute;top:70px;left:50%;transform:translateX(-50%);max-width:min(640px,90vw);
background:rgba(13,21,32,.92);color:#e8eef4;border:1px solid #2c3a4a;border-radius:12px;
padding:12px 20px;font-size:15px;opacity:0;transition:opacity .3s;pointer-events:none;z-index:50;text-align:center}
@@ -437,7 +443,8 @@ img{max-width:100%}
<div id="mode-badge"></div>
<div id="prompt">E — Gepäckstück untersuchen</div>
<button id="btn-touch-act" aria-label="Gepäckstück untersuchen">🧳</button>
<div id="joystick"><div id="joy-knob"></div></div>
<div id="lookstick"><div id="look-knob">👁️</div></div>
<div id="joystick"><div id="joy-knob">🦶</div></div>
<div id="crosshair"></div>
<div id="lock-hint">🖱️ Klicken, um dich umzusehen — Esc gibt die Maus frei</div>
<div id="toast"></div>