Diablo2D — Shadows of Tristram: complete browser ARPG
- 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
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
/* ============================================================
|
||||
* 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);
|
||||
Reference in New Issue
Block a user