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

179 lines
5.2 KiB
JavaScript

/* ============================================================
* Diablo2D — entities.js : entity classes & spatial index
* ============================================================ */
'use strict';
window.D2 = window.D2 || {};
(function (D2) {
let nextUid = 1;
/* ---------------- Player ---------------- */
class Player {
constructor(classId) {
this.uid = nextUid++;
this.kind = 'player';
this.classId = classId;
this.x = 0; this.y = 0;
this.radius = 0.34;
this.dead = false;
/* progression */
this.level = 1;
this.xp = 0;
this.gold = 60;
this.statPoints = 0;
this.skillPoints = 0;
this.attributes = { str: 0, dex: 0, vit: 0, ene: 0 }; // allocated points
this.skills = {}; // skillId -> rank
this.hotbar = ['basic', null, null, null, null]; // slot0=basic(LMB), 1=RMB, 2..4 keys
this.potions = { hp: 4, mp: 3 };
this.inventory = []; // items
this.equip = { weapon: null, offhand: null, head: null, chest: null, hands: null, feet: null, amulet: null, ring1: null, ring2: null };
/* derived (recomputed) */
this.stats = {};
this.maxHp = 50; this.hp = 50;
this.maxMana = 20; this.mana = 20;
/* movement / anim */
this.moving = false;
this.walkPhase = 0;
this.attackAnim = 0;
this.hurtFlash = 0;
this.attackCd = 0;
this.channel = null; // whirlwind/dash state
this.guaranteedCritT = 0;
this.smokeveilT = 0;
this.buffs = []; // {id,name,icon,t,dur,mods}
this.dots = [];
/* run meta */
this.kills = 0;
this.eliteKills = 0;
this.bossKills = 0;
this.deaths = 0;
}
}
/* ---------------- Monster ---------------- */
class Monster {
constructor(statBlock, x, y) {
Object.assign(this, statBlock);
this.uid = nextUid++;
this.kind = 'monster';
this.x = x; this.y = y;
this.homeX = x; this.homeY = y;
this.dead = false;
this.dying = false;
/* ai state */
this.state = 'idle'; // idle | chase | windup | recover | charging | fleeing
this.aggro = false;
this.stateT = 0;
this.attackT = 0; // windup progress
this.attackAnim = 0;
this.hurtFlash = 0;
this.cdT = 0; // attack cooldown
this.specialCd = 0;
this.chargeVX = 0; this.chargeVY = 0;
this.path = null; this.pathIdx = 0; this.pathT = 0;
this.moving = false; this.speedFactor = 1;
/* statuses */
this.frozen = 0;
this.chillT = 0; this.chillSlow = 0;
this.dots = [];
this.regenAcc = 0;
}
}
/* ---------------- Projectile ---------------- */
const projectiles = [];
class Projectile {
constructor(o) {
Object.assign(this, {
x: 0, y: 0, vx: 0, vy: 0,
speed: 10, dmg: 5, elem: null,
fromPlayer: true,
pierce: false,
maxHits: 1,
hits: new Set(),
life: 2.2,
visual: 'bolt',
color: '#fff',
size: 1,
slow: 0, slowDur: 0,
bleed: false,
aoe: 0,
pool: false,
freeze: 0,
knockback: 0,
guaranteedCrit: false,
ownerSkillId: null,
z: 0,
}, o);
this.uid = nextUid++;
this.dead = false;
}
}
/* ---------------- Pickup ---------------- */
class Pickup {
constructor(kind, x, y, extra = {}) {
this.uid = nextUid++;
this.kind = kind; // gold | potion | item
this.x = x; this.y = y;
this.amount = extra.amount || 0;
this.item = extra.item || null;
this.potionType = extra.potionType || null;
this.t = 0; // age (for pop animation)
this.magnet = false;
}
}
/* ---------------- Spatial hash for monsters ---------------- */
class Grid {
constructor(cell = 2.5) { this.cell = cell; this.map = new Map(); }
key(x, y) { return ((x / this.cell) | 0) + ',' + ((y / this.cell) | 0); }
rebuild(entities) {
this.map.clear();
for (const e of entities) {
if (e.dead) continue;
const k = this.key(e.x, e.y);
let arr = this.map.get(k);
if (!arr) { arr = []; this.map.set(k, arr); }
arr.push(e);
}
}
query(x, y, r, out = []) {
out.length = 0;
const c = this.cell;
const x0 = ((x - r) / c) | 0, x1 = ((x + r) / c) | 0;
const y0 = ((y - r) / c) | 0, y1 = ((y + r) / c) | 0;
for (let gy = y0; gy <= y1; gy++) {
for (let gx = x0; gx <= x1; gx++) {
const arr = this.map.get(gx + ',' + gy);
if (arr) for (const e of arr) {
const dx = e.x - x, dy = e.y - y;
if (dx * dx + dy * dy <= r * r) out.push(e);
}
}
}
return out;
}
nearest(x, y, maxR, filter) {
let best = null, bd = maxR * maxR;
const cand = this.query(x, y, maxR, []);
for (const e of cand) {
if (filter && !filter(e)) continue;
const d = (e.x - x) ** 2 + (e.y - y) ** 2;
if (d < bd) { bd = d; best = e; }
}
return best;
}
}
D2.entities = { Player, Monster, Projectile, Pickup, Grid };
})(window.D2);