grechka-game/srcv3/input/joystick3d.js

193 lines
7.9 KiB
JavaScript
Raw 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.

// ==================== ДЖОЙСТИК 3D — GrechkaCraft v3 ====================
// Аналоговый джойстик: движение во все стороны по горизонтали (X и Z),
// камера вращается вторым пальцем по экрану. Кнопки прыжка и копки рядом.
import { state } from '../core/state.js';
let joyEl = null, knobEl = null, jumpEl = null, mineEl = null;
let joyId = null; // активный pointer на джойстике
let jcx = 0, jcy = 0, jrad = 56; // центр джойстика и рабочий радиус в px
let touchSeated = false; // джойстик сейчас показан
let hideT = 0; // таймер скрытия
function setKnob(dx, dy) {
if (!knobEl) return;
knobEl.style.transform = `translate(${dx}px, ${dy}px)`;
}
function resetAxes() {
state.inp.ax = 0;
state.inp.ay = 0;
setKnob(0, 0);
if (joyEl) joyEl.classList.remove('act');
}
export function initJoystick() {
joyEl = document.getElementById('v2joy');
knobEl = joyEl ? joyEl.querySelector('.knob') : null;
jumpEl = document.getElementById('v2jump');
mineEl = document.getElementById('v2mine');
if (!joyEl) return;
const rectCenter = () => {
const r = joyEl.getBoundingClientRect();
jcx = r.left + r.width / 2;
jcy = r.top + r.height / 2;
jrad = r.width / 2 - 12;
};
joyEl.addEventListener('pointerdown', (e) => {
e.preventDefault();
rectCenter();
joyId = e.pointerId;
joyEl.setPointerCapture(joyId);
joyEl.classList.add('act');
update(e);
});
joyEl.addEventListener('pointermove', (e) => {
if (e.pointerId !== joyId) return;
e.preventDefault();
update(e);
});
const end = (e) => {
if (e.pointerId !== joyId) return;
joyId = null;
resetAxes();
};
joyEl.addEventListener('pointerup', end);
joyEl.addEventListener('pointercancel', end);
function update(e) {
let dx = e.clientX - jcx;
let dy = e.clientY - jcy;
const len = Math.hypot(dx, dy);
if (len > jrad) { // не даём уйти за круг
dx = (dx / len) * jrad;
dy = (dy / len) * jrad;
}
setKnob(dx, dy);
// мёртвая зона — чтобы лёгкое касание не дёргало персонажа
const nx = dx / jrad, ny = dy / jrad;
const mag = Math.hypot(nx, ny);
const DEAD = 0.18;
if (mag < DEAD) {
state.inp.ax = 0;
state.inp.ay = 0;
} else {
// нормализуем так, чтобы край джойстика давал полную скорость
const k = (mag - DEAD) / (1 - DEAD) / mag;
state.inp.ax = nx * k;
state.inp.ay = ny * k;
}
}
// прыжок
if (jumpEl) {
jumpEl.addEventListener('pointerdown', (e) => {
e.preventDefault();
state.inp.j = true;
});
const up = (e) => { e.preventDefault(); state.inp.j = false; };
jumpEl.addEventListener('pointerup', up);
jumpEl.addEventListener('pointercancel', up);
jumpEl.addEventListener('pointerleave', up);
}
// копка: добыча/установка — удержание
if (mineEl) {
mineEl.addEventListener('pointerdown', (e) => {
e.preventDefault();
state.inp.mineHeld = true;
window.dispatchEvent(new CustomEvent('v2-mine', { detail: { down: true } }));
});
const up = (e) => {
e.preventDefault();
state.inp.mineHeld = false;
window.dispatchEvent(new CustomEvent('v2-mine', { detail: { down: false } }));
};
mineEl.addEventListener('pointerup', up);
mineEl.addEventListener('pointercancel', up);
mineEl.addEventListener('pointerleave', up);
}
// держим джойстик в нуле, если палец ушёл за экран
window.addEventListener('pointerup', (e) => {
if (joyId !== null && e.pointerId === joyId) { joyId = null; resetAxes(); }
});
window.addEventListener('blur', resetAxes);
// ---------- джойстик: только если палец в левой нижней четверти экрана ----------
// Показываем при касании, прячем через 0.6 с после отпускания — чтобы
// не перекрывать хотбар с избранными блоками.
const ZONE = () => ({ w: window.innerWidth * 0.5, h: window.innerHeight * 0.5 });
const inZone = (x, y) => {
const z = ZONE();
return x < z.w && y > window.innerHeight - z.h;
};
const wake = () => {
clearTimeout(hideT);
// Кнопки прыжка и копки видны ВСЕГДА на тач-устройствах: без прыжка
// и добычи играть невозможно, и требование контекстного показа
// касалось только джойстика.
[jumpEl, mineEl].forEach(e => e && e.classList.add('on'));
if (touchSeated) return;
touchSeated = true;
if (joyEl) joyEl.classList.add('on');
};
const sleepSoon = () => {
clearTimeout(hideT);
hideT = setTimeout(() => {
if (joyId === null) {
touchSeated = false;
// Прячем ТОЛЬКО джойстик. Раньше класс 'on' снимался со всех
// элементов или не снимался вовсе: в первом случае исчезали
// кнопки прыжка и копки, во втором джойстик висел поверх
// хотбара всё время.
if (joyEl) joyEl.classList.remove('on');
[jumpEl, mineEl].forEach(e => e && e.classList.add('on'));
}
}, 600);
};
// Первый контакт с зоной — сажаем джойстик под палец
window.addEventListener('pointerdown', (e) => {
if (e.pointerType === 'mouse') return;
if (!inZone(e.clientX, e.clientY)) return;
wake();
if (joyId !== null) return;
// если палец попал в кнопки — не мешаем им
// contains() бросает исключение, если e.target — не Node (например,
// window при синтетическом событии). Проверяем instanceof.
const isNode = e.target && typeof e.target === 'object' && 'nodeType' in e.target;
if (isNode && ((jumpEl && jumpEl.contains(e.target)) || (mineEl && mineEl.contains(e.target)))) return;
// ищем ближайшую точку джойстика к пальцу
const r = joyEl.getBoundingClientRect();
const cxr = r.left + r.width / 2, cyr = r.top + r.height / 2;
const far = Math.hypot(e.clientX - cxr, e.clientY - cyr) > r.width * 0.75;
if (far) { jcx = e.clientX; jcy = e.clientY; }
// Запоминаем палец СРАЗУ: без этого joyId оставался null, update()
// не считал смещение, и inp.ax/inp.ay всегда были 0 — джойстик
// показывался, но игрок не двигался. Обработчик самого джойстика
// тоже ставит joyEl.classList.add('act').
if (joyId === null) {
joyId = e.pointerId;
joyEl.classList.add('act');
update(e);
}
}, { passive: true, capture: true });
window.addEventListener('pointerup', sleepSoon, { passive: true });
window.addEventListener('pointercancel', sleepSoon, { passive: true });
window.addEventListener('blur', () => { touchSeated = false; });
// десктоп: всё скрыто, клавиатура
if (window.matchMedia('(hover: hover) and (pointer: fine)').matches) {
[joyEl, jumpEl, mineEl].forEach(e => e && e.classList.remove('on'));
}
// Автопоказ на старте УБРАН: джойстик должен появляться только по
// касанию левой нижней зоны, а не висеть поверх хотбара сразу.
sleepSoon();
}