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:
2026-08-23 06:59:36 +00:00
commit fc1fa2d51e
42 changed files with 11784 additions and 0 deletions
+124
View File
@@ -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);