- Isometric canvas renderer (depth-sorted, FOV/fog, additive lighting) - 3 classes x 20 skills, 4 acts x 4 floors + boss lairs, torment I-X - Diablo-style loot: rarities, affix tiers, 14 legendaries, vendor, stash - Rogue camp with 6 NPCs: Charsi/Akara/Kashya/Cain/Gheed/storage - NPC quest chain (accept -> hunt -> turn in) with rewards & gating - Procedural WebAudio SFX + generative music, EN/VI localization - Saves, settings, waypoints, hardcore mode, PWA manifest - 93-assertion headless suite + browser E2E via CDP
125 lines
4.2 KiB
JavaScript
125 lines
4.2 KiB
JavaScript
/* ============================================================
|
|
* Diablo2D — input.js : unified mouse/keyboard state
|
|
* Canvas-only listeners; DOM UI handles its own events.
|
|
* ============================================================ */
|
|
'use strict';
|
|
|
|
window.D2 = window.D2 || {};
|
|
(function (D2) {
|
|
|
|
const state = {
|
|
mx: 0, my: 0, // canvas-space cursor
|
|
left: false, right: false, mid: false,
|
|
leftPressed: false, // true only on frame of press
|
|
rightPressed: false,
|
|
wheelDelta: 0,
|
|
keys: new Set(), // currently held (KeyboardEvent.code)
|
|
pressedKeys: new Set(), // pressed this frame
|
|
releasedKeys: new Set(),
|
|
anyKeyPress: false,
|
|
lastMouseWorld: null,
|
|
};
|
|
|
|
let canvas = null;
|
|
const _pressedQueueKeys = new Set();
|
|
const _releasedQueueKeys = new Set();
|
|
|
|
function init(targetCanvas) {
|
|
canvas = targetCanvas;
|
|
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.repeat) return;
|
|
state.keys.add(e.code);
|
|
_pressedQueueKeys.add(e.code);
|
|
state.anyKeyPress = true;
|
|
// block browser scroll/space navigation during gameplay
|
|
if (['Space', 'ArrowUp', 'ArrowDown', 'Tab'].includes(e.code)) e.preventDefault();
|
|
}, { passive: false });
|
|
|
|
window.addEventListener('keyup', (e) => {
|
|
state.keys.delete(e.code);
|
|
_releasedQueueKeys.add(e.code);
|
|
});
|
|
|
|
window.addEventListener('blur', () => {
|
|
state.keys.clear();
|
|
state.left = state.right = false;
|
|
});
|
|
|
|
canvas.addEventListener('mousemove', (e) => {
|
|
const r = canvas.getBoundingClientRect();
|
|
state.mx = e.clientX - r.left;
|
|
state.my = e.clientY - r.top;
|
|
/* authoritative resync: if the browser says buttons are up, believe it.
|
|
Heals a missed mouseup (released outside the window, alt-tab, etc.) */
|
|
if (typeof e.buttons === 'number') {
|
|
if (!(e.buttons & 1)) state.left = false;
|
|
if (!(e.buttons & 2)) state.right = false;
|
|
if (!(e.buttons & 4)) state.mid = false;
|
|
}
|
|
});
|
|
|
|
canvas.addEventListener('mousedown', (e) => {
|
|
updateMousePos(e);
|
|
if (e.button === 0) { state.left = true; state.leftPressed = true; }
|
|
if (e.button === 2) { state.right = true; state.rightPressed = true; }
|
|
if (e.button === 1) { state.mid = true; e.preventDefault(); }
|
|
});
|
|
|
|
const releaseAll = () => { state.left = false; state.right = false; state.mid = false; };
|
|
window.addEventListener('mouseup', (e) => {
|
|
if (e.button === 0) state.left = false;
|
|
if (e.button === 2) state.right = false;
|
|
if (e.button === 1) state.mid = false;
|
|
});
|
|
/* cursor left the page / OS took the pointer / touch cancelled */
|
|
document.documentElement.addEventListener('mouseleave', releaseAll);
|
|
window.addEventListener('pointercancel', releaseAll);
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) releaseAll();
|
|
});
|
|
|
|
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
|
|
|
|
canvas.addEventListener('wheel', (e) => {
|
|
state.wheelDelta += Math.sign(e.deltaY);
|
|
e.preventDefault();
|
|
}, { passive: false });
|
|
|
|
window.addEventListener('mousedown', (e) => { // audio unlock gesture
|
|
D2.audio && D2.audio.unlock();
|
|
}, { once: false, capture: true });
|
|
}
|
|
|
|
function updateMousePos(e) {
|
|
const r = canvas.getBoundingClientRect();
|
|
state.mx = e.clientX - r.left;
|
|
state.my = e.clientY - r.top;
|
|
}
|
|
|
|
/** call at end of each simulation frame */
|
|
function endFrame() {
|
|
state.leftPressed = false;
|
|
state.rightPressed = false;
|
|
state.wheelDelta = 0;
|
|
state.pressedKeys.clear();
|
|
state.releasedKeys.clear();
|
|
for (const k of _pressedQueueKeys) state.pressedKeys.add(k);
|
|
for (const k of _releasedQueueKeys) state.releasedKeys.add(k);
|
|
_pressedQueueKeys.clear();
|
|
_releasedQueueKeys.clear();
|
|
state.anyKeyPress = false;
|
|
}
|
|
|
|
const isDown = code => state.keys.has(code);
|
|
const wasPressed = code => state.pressedKeys.has(code);
|
|
const wasReleased = code => state.releasedKeys.has(code);
|
|
|
|
/* virtual press used by UI hotbar buttons */
|
|
function injectPress(code) {
|
|
_pressedQueueKeys.add(code);
|
|
}
|
|
|
|
D2.input = { state, init, endFrame, isDown, wasPressed, wasReleased, injectPress };
|
|
})(window.D2);
|