@@ -6922,6 +6922,9 @@ const joystickEl = document.getElementById('joystick');
69226922const stickEl = document.getElementById('stick');
69236923let joyVec = { x: 0, y: 0 };
69246924let joyActive = false, joyOrigin = { x: 0, y: 0 }, joyTouchId = null;
6925+ // Pinching the world opens the map (see the pinch watcher after the camera's touch handlers).
6926+ const PINCH_RATIO_IN = 0.62, PINCH_RATIO_OUT = 1.45;
6927+ let pinch = null; // { a, b, d0 } while two fingers that came down together are on the world
69256928
69266929const JOY_BASE = MOBILE_UI ? 128 : 110; // ring diameter (px)
69276930const JOY_KNOB = MOBILE_UI ? 56 : 46;
@@ -6959,6 +6962,7 @@ if (joyZone && MOBILE_UI) {
69596962 e.preventDefault();
69606963 }, { passive: false });
69616964 joyZone.addEventListener('touchmove', (e) => {
6965+ if (pinch) return; // two fingers came down together: a pinch, not a walk
69626966 for (const t of e.changedTouches) {
69636967 if (t.identifier !== joyTouchId) continue;
69646968 let dx = t.clientX - joyOrigin.x, dy = t.clientY - joyOrigin.y;
@@ -7581,6 +7585,7 @@ canvas.addEventListener('touchstart', (e) => {
75817585 for (const t of e.changedTouches) {
75827586 touchLooks.set(t.identifier, { startX: t.clientX, startY: t.clientY, lastX: t.clientX, lastY: t.clientY, moved: 0 });
75837587 }
7588+
75847589 camGlide.velYaw = 0; camGlide.velPitch = 0; // finger down = direct control again
75857590}, { passive: true });
75867591canvas.addEventListener('touchmove', (e) => {
@@ -7590,6 +7595,7 @@ canvas.addEventListener('touchmove', (e) => {
75907595 const dx = t.clientX - tl.lastX, dy = t.clientY - tl.lastY;
75917596 tl.moved += Math.abs(dx) + Math.abs(dy);
75927597 tl.lastX = t.clientX; tl.lastY = t.clientY;
7598+ if (pinch) continue; // two fingers on the world: a pinch, not a turn of the camera (judged below)
75937599 if (MOBILE_UI && tl.moved >= CLICK_DRAG_THRESHOLD) {
75947600 // Faster than the first pass (+~50%) but fed through a short
75957601 // smoothing buffer (see updateCameraGlide) so speed reads as
@@ -7625,6 +7631,38 @@ const touchLookEnd = (e) => {
76257631 }
76267632};
76277633canvas.addEventListener('touchend', touchLookEnd, { passive: true });
7634+ // The pinch watcher. Two fingers that come down on the world (the scene, or
7635+ // the joystick's half of it) within a quarter of a second of each other and
7636+ // are drawn together open the map; it reads as zooming out. A thumb already
7637+ // holding the joystick when the camera finger lands is play, not a pinch.
7638+ if (MOBILE_UI) {
7639+ const PINCH_TOGETHER_MS = 250;
7640+ const downs = new Map(); // id -> { x, y, t }
7641+ const onWorld = (el) => el === canvas || (el && el.closest && !!el.closest('#joyZone'));
7642+ const span = () => { const a = downs.get(pinch.a), b = downs.get(pinch.b); return a && b ? Math.hypot(a.x - b.x, a.y - b.y) : null; };
7643+ window.addEventListener('touchstart', (e) => {
7644+ for (const t of e.changedTouches) if (onWorld(t.target)) downs.set(t.identifier, { x: t.clientX, y: t.clientY, t: performance.now() });
7645+ pinch = null;
7646+ if (downs.size === 2 && !Modals.isOpen('mapModalOpen')) {
7647+ const [[a, A], [b, B]] = [...downs.entries()];
7648+ if (Math.abs(A.t - B.t) <= PINCH_TOGETHER_MS) { pinch = { a, b, d0: 0 }; pinch.d0 = span() || 0; }
7649+ }
7650+ }, { capture: true, passive: true });
7651+ window.addEventListener('touchmove', (e) => {
7652+ for (const t of e.changedTouches) { const d = downs.get(t.identifier); if (d) { d.x = t.clientX; d.y = t.clientY; } }
7653+ if (!pinch || pinch.d0 < 60) return;
7654+ const d = span();
7655+ if (d != null && d < pinch.d0 * PINCH_RATIO_IN) {
7656+ pinch = null;
7657+ for (const tl of touchLooks.values()) tl.moved = CLICK_DRAG_THRESHOLD; // neither finger is a tap on anything now
7658+ joyTouchId = null; joyActive = false; joyVec = { x: 0, y: 0 }; restJoystick(); // and she is not walking
7659+ if (typeof window.__openTownMap === 'function') window.__openTownMap();
7660+ }
7661+ }, { capture: true, passive: true });
7662+ const up = (e) => { for (const t of e.changedTouches) { downs.delete(t.identifier); if (pinch && (t.identifier === pinch.a || t.identifier === pinch.b)) pinch = null; } };
7663+ window.addEventListener('touchend', up, { capture: true, passive: true });
7664+ window.addEventListener('touchcancel', up, { capture: true, passive: true });
7665+ }
76287666canvas.addEventListener('touchcancel', (e) => { for (const t of e.changedTouches) touchLooks.delete(t.identifier); }, { passive: true });
76297667
76307668// ── Touch camera feel ───────────────────────────────────────────────────────
@@ -17756,7 +17794,37 @@ const { openNotifModal } = _notif;
1775617794 function toggle() { Modals.isOpen('mapModalOpen') ? close() : open(); }
1775717795 window.__toggleTownMap = toggle;
1775817796 window.__closeTownMap = close;
17759- const mi = document.getElementById('menuMap'); if (mi) mi.addEventListener('click', open);
17797+ window.__openTownMap = open;
17798+ // Three ways to it on a phone, none of which puts anything new on the
17799+ // screen: tap the health-and-level pill at the top left; pinch the world
17800+ // (fingers together) — and spread them on the map to put it away; or Map,
17801+ // first in the ☰ menu. Someone who takes the long way through the menu is
17802+ // told the short ways, once.
17803+ const tellShortWays = () => {
17804+ if (!MOBILE_UI) return;
17805+ try { if (localStorage.getItem('tc_map_ways_told')) return; localStorage.setItem('tc_map_ways_told', '1'); } catch (_) { return; }
17806+ setTimeout(() => setUnlockToast('🗺️ Next time: tap your health and level at the top left, or pinch the world, to open the map.', { always: true, level: 'important' }), 400);
17807+ };
17808+ const mi = document.getElementById('menuMap'); if (mi) mi.addEventListener('click', () => { open(); tellShortWays(); });
17809+ const pill = document.getElementById('mobileVitals');
17810+ if (pill) {
17811+ // touchstart, like the action buttons: a second finger's tap gets no click while the thumb holds the joystick
17812+ pill.addEventListener('touchstart', (e) => { if (e.cancelable) e.preventDefault(); toggle(); }, { passive: false });
17813+ pill.addEventListener('click', (e) => { e.preventDefault(); toggle(); });
17814+ }
17815+ if (MOBILE_UI) { const h = document.getElementById('mapHint'); if (h) h.textContent = 'spread two fingers, or tap Close, to put it away'; }
17816+ // Spread two fingers on the open map to put it away.
17817+ const mm = document.getElementById('mapModal');
17818+ if (mm) {
17819+ let spread = null;
17820+ mm.addEventListener('touchstart', (e) => { spread = e.touches.length === 2 ? Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY) : null; }, { passive: true });
17821+ mm.addEventListener('touchmove', (e) => {
17822+ if (spread == null || e.touches.length !== 2) return;
17823+ const d = Math.hypot(e.touches[0].clientX - e.touches[1].clientX, e.touches[0].clientY - e.touches[1].clientY);
17824+ if (d > spread * PINCH_RATIO_OUT) { spread = null; close(); }
17825+ }, { passive: true });
17826+ mm.addEventListener('touchend', () => { spread = null; }, { passive: true });
17827+ }
1776017828 const cb = document.getElementById('mapCloseBtn'); if (cb) cb.addEventListener('click', close);
1776117829 window.addEventListener('resize', () => { if (Modals.isOpen('mapModalOpen')) render(); });
1776217830})();
0 commit comments