Files
deepseek fc1fa2d51e 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
2026-08-23 06:59:36 +00:00

135 lines
4.6 KiB
JavaScript

/* ============================================================
* Diablo2D — util.js : math, RNG, misc helpers
* ============================================================ */
'use strict';
window.D2 = window.D2 || {};
(function (D2) {
const TAU = Math.PI * 2;
const clamp = (v, a, b) => v < a ? a : (v > b ? b : v);
const lerp = (a, b, t) => a + (b - a) * t;
const smoothstep = (a, b, x) => { const t = clamp((x - a) / (b - a), 0, 1); return t * t * (3 - 2 * t); };
const dist2 = (ax, ay, bx, by) => { const dx = ax - bx, dy = ay - by; return dx * dx + dy * dy; };
const dist = (ax, ay, bx, by) => Math.sqrt(dist2(ax, ay, bx, by));
const angleTo = (ax, ay, bx, by) => Math.atan2(by - ay, bx - ax);
const wrapAngle = a => { while (a > Math.PI) a -= TAU; while (a < -Math.PI) a += TAU; return a; };
const approachAngle = (cur, target, maxStep) => {
let d = wrapAngle(target - cur);
if (Math.abs(d) <= maxStep) return target;
return cur + Math.sign(d) * maxStep;
};
const easeOutCubic = t => 1 - Math.pow(1 - t, 3);
const easeInQuad = t => t * t;
/* deterministic RNG */
function mulberry32(seed) {
let a = seed >>> 0;
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
class RNG {
constructor(seed) {
this.seedFn = mulberry32(seed);
this.seed = seed >>> 0;
}
next() { return this.seedFn(); }
range(a, b) { return a + this.seedFn() * (b - a); } // float [a,b)
int(a, b) { return Math.floor(this.range(a, b + 1)); } // int [a,b]
chance(p) { return this.seedFn() < p; }
pick(arr) { return arr[Math.floor(this.seedFn() * arr.length)]; }
weighted(entries) { // [{v,w}] or [[val,weight]]
let total = 0;
for (const e of entries) total += e.w !== undefined ? e.w : e[1];
let roll = this.seedFn() * total;
for (const e of entries) {
roll -= e.w !== undefined ? e.w : e[1];
if (roll <= 0) return e.v !== undefined ? e.v : e[0];
}
const last = entries[entries.length - 1];
return last.v !== undefined ? last.v : last[0];
}
shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(this.seedFn() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
}
let _uid = 1;
const uid = () => _uid++;
function fmtNum(n) {
n = Math.round(n);
if (Math.abs(n) >= 1000000) return (n / 1000000).toFixed(1) + 'M';
if (Math.abs(n) >= 10000) return (n / 1000).toFixed(1) + 'k';
return String(n);
}
function roman(n) {
if (n <= 0) return '0';
const map = [[1000,'M'],[900,'CM'],[500,'D'],[400,'CD'],[100,'C'],[90,'XC'],[50,'L'],[40,'XL'],[10,'X'],[9,'IX'],[5,'V'],[4,'IV'],[1,'I']];
let out = '';
for (const [v, s] of map) while (n >= v) { out += s; n -= v; }
return out;
}
function deepClone(o) { return JSON.parse(JSON.stringify(o)); }
/* color helpers: '#rrggbb' <-> [r,g,b] */
function hexToRgb(hex) {
const h = hex.replace('#', '');
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
}
function rgbToHex(r, g, b) {
const c = v => clamp(Math.round(v), 0, 255).toString(16).padStart(2, '0');
return '#' + c(r) + c(g) + c(b);
}
function lerpColor(c1, c2, t) {
const a = typeof c1 === 'string' ? hexToRgb(c1) : c1;
const b = typeof c2 === 'string' ? hexToRgb(c2) : c2;
return rgbToHex(lerp(a[0], b[0], t), lerp(a[1], b[1], t), lerp(a[2], b[2], t));
}
function shade(hex, f) { // f<1 darken, f>1 lighten
const [r, g, b] = hexToRgb(hex);
return rgbToHex(r * f, g * f, b * f);
}
function rgba(hex, alpha) {
const [r, g, b] = hexToRgb(hex);
return `rgba(${r},${g},${b},${alpha})`;
}
/* simple event bus */
class Bus {
constructor() { this._l = new Map(); }
on(evt, fn) {
if (!this._l.has(evt)) this._l.set(evt, []);
this._l.get(evt).push(fn);
}
off(evt, fn) {
const l = this._l.get(evt);
if (l) { const i = l.indexOf(fn); if (i >= 0) l.splice(i, 1); }
}
emit(evt, ...args) {
const l = this._l.get(evt);
if (l) for (const fn of [...l]) fn(...args);
}
}
D2.util = {
TAU, clamp, lerp, smoothstep, dist, dist2, angleTo, wrapAngle, approachAngle,
easeOutCubic, easeInQuad,
mulberry32, RNG, uid,
fmtNum, roman, deepClone,
hexToRgb, rgbToHex, lerpColor, shade, rgba,
Bus,
};
})(window.D2);