diff --git a/App/sims/kofferdetektiv/engine.js b/App/sims/kofferdetektiv/engine.js index c5329c3..af2c3b7 100644 --- a/App/sims/kofferdetektiv/engine.js +++ b/App/sims/kofferdetektiv/engine.js @@ -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'] diff --git a/App/sims/kofferdetektiv/game-3d.html b/App/sims/kofferdetektiv/game-3d.html index 392f118..f19054d 100644 --- a/App/sims/kofferdetektiv/game-3d.html +++ b/App/sims/kofferdetektiv/game-3d.html @@ -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%}