/* ============================================================ * Diablo2D — monsters.js : bestiary, elite affixes, bosses * ============================================================ */ 'use strict'; window.D2 = window.D2 || {}; (function (D2) { /* ai archetypes: melee | ranged | caster | charger | swarm | flyer | summoner */ const SPECIES = [ /* ---- ACT I : The Ravaged Cathedral ---- */ { id: 'skeleton', name: { en: 'Skeleton', vi: 'Xương Khô' }, acts: [0], ai: 'melee', hpMult: 0.9, dmgMult: 1.0, speed: 2.5, radius: 0.32, range: 0.95, cd: 1.15, xpMult: 1.0, palette: { body: '#cfc7ae', accent: '#8a8272' }, shape: 'skeleton' }, { id: 'skel_archer', name: { en: 'Skeleton Archer', vi: 'Cung Thủ Xương' }, acts: [0], ai: 'ranged', hpMult: 0.75, dmgMult: 0.9, speed: 2.2, radius: 0.3, range: 6.5, cd: 1.7, xpMult: 1.15, proj: { kind: 'arrow', speed: 8.5, color: '#d8cba8' }, palette: { body: '#cfc7ae', accent: '#6e5b34' }, shape: 'skeleton_bow' }, { id: 'fallen', name: { en: 'Fallen One', vi: 'Kẻ Sa Đọa' }, acts: [0], ai: 'swarm', hpMult: 0.55, dmgMult: 0.75, speed: 3.6, radius: 0.26, range: 0.85, cd: 0.85, xpMult: 0.85, palette: { body: '#b0522f', accent: '#e8c26a' }, shape: 'imp' }, { id: 'zombie', name: { en: 'Zombie', vi: 'Thi Sống' }, acts: [0, 1], ai: 'melee', hpMult: 1.6, dmgMult: 1.1, speed: 1.35, radius: 0.36, range: 1.0, cd: 1.5, xpMult: 1.2, palette: { body: '#7a8a62', accent: '#4c5840' }, shape: 'zombie' }, { id: 'bat', name: { en: 'Cave Bat', vi: 'Dơi Hang' }, acts: [0, 2], ai: 'flyer', hpMult: 0.45, dmgMult: 0.7, speed: 4.3, radius: 0.24, range: 0.8, cd: 0.8, xpMult: 0.8, palette: { body: '#4a3a52', accent: '#8a6aa2' }, shape: 'bat' }, { id: 'cultist', name: { en: 'Dark Cultist', vi: 'Giáo Đồ Bóng Tối' }, acts: [0], ai: 'caster', hpMult: 0.8, dmgMult: 1.15, speed: 2.1, radius: 0.32, range: 6.0, cd: 2.1, xpMult: 1.4, proj: { kind: 'shadowbolt', speed: 6.5, color: '#a06ad8' }, special: 'blinkAway', palette: { body: '#5a2a4a', accent: '#c23a3a' }, shape: 'cultist' }, /* ---- ACT II : The Weeping Catacombs ---- */ { id: 'bone_warrior', name: { en: 'Bone Warrior', vi: 'Chiến Binh Xương' }, acts: [1], ai: 'melee', hpMult: 1.25, dmgMult: 1.2, speed: 2.8, radius: 0.34, range: 1.0, cd: 1.05, xpMult: 1.25, palette: { body: '#d8d0bc', accent: '#7a3040' }, shape: 'skeleton' }, { id: 'tomb_spider', name: { en: 'Tomb Spider', vi: 'Nhện Mộ' }, acts: [1, 2], ai: 'ranged', hpMult: 0.9, dmgMult: 0.95, speed: 2.6, radius: 0.33, range: 5.0, cd: 1.6, xpMult: 1.3, proj: { kind: 'spit', speed: 7.0, color: '#8adf5a', elem: 'pois', slow: 0.35, slowDur: 1.6 }, palette: { body: '#3a4a2e', accent: '#8adf5a' }, shape: 'spider' }, { id: 'wraith', name: { en: 'Wraith', vi: 'Bóng Ma' }, acts: [1], ai: 'flyer', hpMult: 0.85, dmgMult: 1.1, speed: 3.4, radius: 0.3, range: 0.9, cd: 1.1, xpMult: 1.5, elem: 'cold', chillOnHit: true, palette: { body: '#6a86b8', accent: '#cfe4ff' }, shape: 'wraith' }, { id: 'cursed_priest', name: { en: 'Cursed Priest', vi: 'Tu Sĩ Nguyền Rủa' }, acts: [1], ai: 'summoner', hpMult: 1.0, dmgMult: 0.8, speed: 1.9, radius: 0.33, range: 6.5, cd: 3.4, xpMult: 1.8, summon: { species: 'skeleton', count: 2, maxAlive: 5 }, proj: { kind: 'shadowbolt', speed: 6.0, color: '#b08ae8' }, special: 'blinkAway', palette: { body: '#443a5e', accent: '#e8e0b0' }, shape: 'cultist' }, { id: 'hellhound', name: { en: 'Hellhound', vi: 'Hầu Địa Ngục' }, acts: [1, 3], ai: 'charger', hpMult: 1.1, dmgMult: 1.3, speed: 3.0, radius: 0.34, range: 1.0, cd: 1.2, xpMult: 1.45, chargeRange: 6, elem: 'fire', palette: { body: '#702a20', accent: '#ff9a3a' }, shape: 'hound' }, /* ---- ACT III : The Fungal Depths ---- */ { id: 'sporeling', name: { en: 'Sporeling', vi: 'Bào Tử Linh' }, acts: [2], ai: 'swarm', hpMult: 0.6, dmgMult: 0.8, speed: 3.2, radius: 0.27, range: 0.85, cd: 0.9, xpMult: 0.9, corpsePoison: true, palette: { body: '#5a8a3a', accent: '#c8e88a' }, shape: 'shroom' }, { id: 'cave_troll', name: { en: 'Cave Troll', vi: 'Troll Hang' }, acts: [2], ai: 'charger', hpMult: 2.2, dmgMult: 1.5, speed: 1.9, radius: 0.46, range: 1.2, cd: 1.6, xpMult: 2.2, chargeRange: 7, palette: { body: '#5e6a56', accent: '#a8b89a' }, shape: 'troll' }, { id: 'bog_witch', name: { en: 'Bog Witch', vi: 'Phù Thủy Đầm Lầy' }, acts: [2], ai: 'caster', hpMult: 0.95, dmgMult: 1.2, speed: 2.0, radius: 0.32, range: 6.0, cd: 2.0, xpMult: 1.7, proj: { kind: 'venomorb', speed: 6.0, color: '#9adf3a', elem: 'pois', pool: true }, special: 'blinkAway', palette: { body: '#3e5a2e', accent: '#b8e86a' }, shape: 'cultist' }, { id: 'acid_spitter', name: { en: 'Acid Spitter', vi: 'Phun Axit' }, acts: [2], ai: 'ranged', hpMult: 1.0, dmgMult: 1.05, speed: 2.3, radius: 0.35, range: 5.5, cd: 1.5, xpMult: 1.35, proj: { kind: 'acid', speed: 7.5, color: '#d8f04a', elem: 'pois' }, palette: { body: '#6a7a2a', accent: '#e8f56a' }, shape: 'zombie' }, { id: 'mushroom_cap', name: { en: 'Spore Cap', vi: 'Nấm Bào Tử' }, acts: [2], ai: 'melee', hpMult: 1.4, dmgMult: 0.9, speed: 1.6, radius: 0.38, range: 1.0, cd: 1.4, xpMult: 1.4, explodeOnDeath: { elem: 'pois', radius: 2.2, dmgMult: 1.4 }, palette: { body: '#8a5a3a', accent: '#c88a5a' }, shape: 'shroom_big' }, /* ---- ACT IV : Hell's Threshold ---- */ { id: 'demon_imp', name: { en: 'Demon Imp', vi: 'Yêu Tinh' }, acts: [3], ai: 'swarm', hpMult: 0.7, dmgMult: 0.95, speed: 3.8, radius: 0.27, range: 0.9, cd: 0.8, xpMult: 1.1, elem: 'fire', palette: { body: '#a83a2a', accent: '#ffb03a' }, shape: 'imp' }, { id: 'hell_knight', name: { en: 'Hell Knight', vi: 'Kỵ Sĩ Địa Ngục' }, acts: [3], ai: 'melee', hpMult: 1.9, dmgMult: 1.35, speed: 2.5, radius: 0.38, range: 1.05, cd: 1.1, xpMult: 1.9, palette: { body: '#3a3038', accent: '#c22a2a' }, shape: 'knight' }, { id: 'flame_archer', name: { en: 'Flame Archer', vi: 'Cung Thủ Lửa' }, acts: [3], ai: 'ranged', hpMult: 0.9, dmgMult: 1.2, speed: 2.6, radius: 0.31, range: 7.0, cd: 1.5, xpMult: 1.5, proj: { kind: 'firearrow', speed: 9.0, color: '#ff7a3a', elem: 'fire' }, palette: { body: '#5e2a20', accent: '#ff9a3a' }, shape: 'skeleton_bow' }, { id: 'succubus', name: { en: 'Succubus', vi: 'Ác Nữ' }, acts: [3], ai: 'caster', hpMult: 1.0, dmgMult: 1.35, speed: 2.4, radius: 0.32, range: 6.5, cd: 1.9, xpMult: 1.9, proj: { kind: 'fireball', speed: 7.0, color: '#ff5a2a', elem: 'fire', aoe: 1.2 }, special: 'blinkAway', palette: { body: '#7a2a4e', accent: '#ffb0c8' }, shape: 'cultist' }, { id: 'pit_lord', name: { en: 'Pit Lord', vi: 'Chúa Vực' }, acts: [3], ai: 'charger', hpMult: 2.6, dmgMult: 1.65, speed: 1.8, radius: 0.5, range: 1.25, cd: 1.5, xpMult: 2.6, chargeRange: 8, elem: 'fire', palette: { body: '#5a1e18', accent: '#ff6a2a' }, shape: 'troll' }, { id: 'void_walker', name: { en: 'Void Walker', vi: 'Bước Qua Hư Không' }, acts: [3], ai: 'melee', hpMult: 1.2, dmgMult: 1.4, speed: 2.9, radius: 0.33, range: 1.0, cd: 1.0, xpMult: 1.8, special: 'blinkTo', palette: { body: '#2a2440', accent: '#8a6aff' }, shape: 'wraith' }, ]; const SPECIES_MAP = {}; for (const s of SPECIES) SPECIES_MAP[s.id] = s; /* ---------------- elite (champion) affixes ---------------- */ const ELITE_AFFIXES = [ { id: 'extrafast', name: { en: 'Extra Fast', vi: 'Siêu Nhanh' }, color: '#ffd24a', apply: m => { m.speed *= 1.4; } }, { id: 'extrastrong', name: { en: 'Extra Strong', vi: 'Siêu Mạnh' }, color: '#ff5a4a', apply: m => { m.dmg *= 1.42; } }, { id: 'tough', name: { en: 'Ironback', vi: 'Lưng Thép' }, color: '#9ab0c8', apply: m => { m.maxHp *= 1.6; m.hp = m.maxHp; } }, { id: 'fireench', name: { en: 'Fire Enchanted', vi: 'Phép Hỏa' }, color: '#ff8a3a', apply: m => { m.elem = 'fire'; m.deathNova = { elem: 'fire', radius: 2.4 }; } }, { id: 'coldench', name: { en: 'Cold Enchanted', vi: 'Phép Băng' }, color: '#6ab8ff', apply: m => { m.elem = 'cold'; m.chillOnHit = true; m.deathNova = { elem: 'cold', radius: 2.4, chill: true }; } }, { id: 'litench', name: { en: 'Lightning Enchanted', vi: 'Phép Lôi' }, color: '#ffe86a', apply: m => { m.chainOnHitChance = 0.35; } }, { id: 'vampiric', name: { en: 'Vampiric', vi: 'Hút Máu' }, color: '#c83a6a', apply: m => { m.lifestealPct = 0.12; } }, { id: 'regen', name: { en: 'Regenerator', vi: 'Tái Tạo' }, color: '#6ade8a', apply: m => { m.regenPct = 0.02; } }, { id: 'teleport', name: { en: 'Teleporter', vi: 'Dịch Chuyển' }, color: '#b08aff', apply: m => { m.special = 'blinkTo'; } }, { id: 'fanatic', name: { en: 'Fanatic', vi: 'Cuồng Tín' }, color: '#ffa04a', apply: m => { m.cdMult = 0.6; } }, ]; function rollEliteAffixes() { const pool = [...ELITE_AFFIXES]; const out = []; const n = Math.random() < 0.42 ? 2 : 1; for (let i = 0; i < n && pool.length; i++) { out.push(pool.splice((Math.random() * pool.length) | 0, 1)[0]); } return out; } /* ---------------- bosses ---------------- */ const BOSSES = [ { id: 'butcher', name: { en: 'The Flesh Butcher', vi: 'Đồ Tể Xác Thịt' }, act: 0, base: 'zombie', size: 1.9, hpMult: 1.0, dmgMult: 1.0, speed: 2.3, radius: 0.62, palette: { body: '#8a4a3a', accent: '#d84a2a' }, shape: 'butcher', attacks: ['cleaveArc', 'chargeAt', 'summonFallen', 'frenzy'], intro: { en: 'Fresh meat!', vi: 'Thịt tươi đây!' } }, { id: 'boneking', name: { en: 'The Bone King', vi: 'Vua Xương Khô' }, act: 1, base: 'skeleton', size: 1.8, hpMult: 1.05, dmgMult: 1.0, speed: 2.2, radius: 0.58, palette: { body: '#e0d8c0', accent: '#5a8ac8' }, shape: 'boneking', attacks: ['boneSpearVolley', 'summonSkeletons', 'curseRing', 'blinkAway'], intro: { en: 'My legions are endless...', vi: 'Quân đội của ta là vô tận...' } }, { id: 'spiderqueen', name: { en: 'Aranea, Spider Queen', vi: 'Aranea, Nữ Hoàng Nhện' }, act: 2, base: 'tomb_spider', size: 2.0, hpMult: 1.1, dmgMult: 1.05, speed: 2.4, radius: 0.64, palette: { body: '#2e3a24', accent: '#a8e84a' }, shape: 'queenspider', attacks: ['poisonPools', 'webVolley', 'spawnSpiderlings', 'lunge'], intro: { en: 'You will make a fine broodmother.', vi: 'Bạn sẽ làm mẫu vật nuôi tốt đấy.' } }, { id: 'terrorlord', name: { en: "Malgor, Lord of Terror", vi: 'Malgor, Chúa Tể Kinh Hoàng' }, act: 3, base: 'pit_lord', size: 2.3, hpMult: 1.35, dmgMult: 1.1, speed: 2.2, radius: 0.72, palette: { body: '#6e1a10', accent: '#ffb02a' }, shape: 'terrorlord', attacks: ['fireNovaRing', 'lightningSpiral', 'meteorRain', 'chargeAt', 'enrage'], intro: { en: 'I AM THE SIN OF THIS WORLD!', vi: 'TA LÀ TỘI LỖI CỦA THẾ GIAN NÀY!' } }, ]; const BOSS_MAP = {}; for (const b of BOSSES) BOSS_MAP[b.id] = b; /* ---------------- stat rolling ---------------- */ /** * Build concrete combat stats for a monster instance. * Returns plain object used by entities.Monster. */ function buildMonster(speciesId, mlvl, opts = {}) { const sp = typeof speciesId === 'string' ? SPECIES_MAP[speciesId] : speciesId; if (!sp) throw new Error('unknown species ' + speciesId); const BAL = D2.BAL; let eliteAffixes = []; if (opts.elite) eliteAffixes = rollEliteAffixes(); const boss = !!opts.bossDef; const bd = opts.bossDef || null; const m = { speciesId: sp.id, def: sp, level: mlvl, isElite: eliteAffixes.length > 0, isBoss: boss, bossDef: bd, name: boss ? (bd.name[lang()] || bd.name.en) : (sp.name[lang()] || sp.name.en), shape: bd ? bd.shape : sp.shape, palette: bd ? bd.palette : sp.palette, maxHp: Math.round(BAL.mHp(mlvl) * sp.hpMult * (bd ? bd.hpMult : 1)), dmg: BAL.mDmg(mlvl) * sp.dmgMult, speed: sp.speed, radius: sp.radius * (bd ? bd.size : 1), attackRange: sp.range, attackCd: sp.cd, cdMult: 1, xpReward: Math.round(BAL.mXp(mlvl) * sp.xpMult), elem: sp.elem || null, proj: sp.proj || null, summon: sp.summon || null, special: sp.special || null, chargeRange: sp.chargeRange || 0, explodeOnDeath: sp.explodeOnDeath || null, corpsePoison: !!sp.corpsePoison, chillOnHit: !!sp.chillOnHit, resists: { fire: 0, cold: 0, lit: 0, pois: 0 }, }; if (bd && bd.attacks) m.attacks = bd.attacks.slice(); if (m.isElite) { m.maxHp = Math.round(m.maxHp * BAL.eliteHpMult); m.dmg *= BAL.eliteDmgMult; m.xpReward = Math.round(m.xpReward * BAL.eliteXpMult); m.radius *= BAL.eliteSizeMult; m.eliteColor = eliteAffixes[0].color; m.eliteName = eliteAffixes.map(a => a.name[lang()] || a.name.en).join(' '); for (const af of eliteAffixes) af.apply(m); } if (boss) { m.maxHp = Math.round(m.maxHp * BAL.bossHpMult); m.dmg *= BAL.bossDmgMult; m.xpReward = Math.round(m.xpReward * BAL.bossXpMult); m.radius = sp.radius * bd.size; m.speed = bd.speed || m.speed; m.cdMult = 1.15; } m.hp = m.maxHp; return m; } /** weighted random species valid for act */ function rollSpeciesForAct(actIdx) { const pool = SPECIES.filter(s => s.acts.includes(actIdx)); if (!pool.length) return SPECIES[0]; return weighted(pool.map(s => ({ v: s, w: s.ai === 'melee' ? 2.2 : 1 }))); } function weighted(entries) { let total = 0; for (const e of entries) total += e.w; let r = Math.random() * total; for (const e of entries) { r -= e.w; if (r <= 0) return e.v; } return entries[entries.length - 1].v; } function lang() { return (D2.i18n && D2.i18n.getLang()) || 'en'; } D2.Monsters = { SPECIES, SPECIES_MAP, ELITE_AFFIXES, BOSSES, BOSS_MAP, buildMonster, rollSpeciesForAct, rollEliteAffixes, }; })(window.D2);