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

411 lines
26 KiB
JavaScript

/* ============================================================
* Diablo2D — items.js : item bases, affixes, legendaries, rolling
* ============================================================ */
'use strict';
window.D2 = window.D2 || {};
(function (D2) {
const RARITIES = {
common: { color: '#c8c8c8', mult: 1.00, affixes: [0, 0], weight: 100 },
magic: { color: '#7c9cf5', mult: 1.08, affixes: [1, 2], weight: 34 },
rare: { color: '#f2e34c', mult: 1.16, affixes: [3, 4], weight: 9.5 },
legendary: { color: '#ef8f34', mult: 1.28, affixes: [0, 1], weight: 1.35 },
};
const SLOTS = ['weapon', 'offhand', 'head', 'chest', 'hands', 'feet', 'amulet', 'ring'];
/* ---------------- base items ---------------- */
/* dmg:[min,max] at minIlvl; armor base; aps = attacks per second */
const BASES = [
/* weapons */
{ id: 'dagger', name: { en: 'Dagger', vi: 'Dao Găm' }, slot: 'weapon', kind: 'melee', minIlvl: 1, tier: 1, dmg: [2, 5], aps: 1.62 },
{ id: 'shortsword', name: { en: 'Short Sword', vi: 'Kiếm Ngắn' }, slot: 'weapon', kind: 'melee', minIlvl: 1, tier: 1, dmg: [3, 7], aps: 1.38 },
{ id: 'club', name: { en: 'Club', vi: 'Gậy Thô' }, slot: 'weapon', kind: 'melee', minIlvl: 1, tier: 1, dmg: [4, 8], aps: 1.18 },
{ id: 'longsword', name: { en: 'Long Sword', vi: 'Kiếm Dài' }, slot: 'weapon', kind: 'melee', minIlvl: 7, tier: 2, dmg: [6, 12], aps: 1.32 },
{ id: 'battleaxe', name: { en: 'Battle Axe', vi: 'Rìu Chiến' }, slot: 'weapon', kind: 'melee', minIlvl: 10, tier: 2, dmg: [9, 16], aps: 1.08 },
{ id: 'mace', name: { en: 'Mace', vi: 'Chùy' }, slot: 'weapon', kind: 'melee', minIlvl: 9, tier: 2, dmg: [8, 14], aps: 1.15 },
{ id: 'warhammer', name: { en: 'War Hammer', vi: 'Búa Chiến' }, slot: 'weapon', kind: 'melee', minIlvl: 17, tier: 3, dmg: [13, 21], aps: 0.95 },
{ id: 'greatsword', name: { en: 'Great Sword', vi: 'Đại Kiếm' }, slot: 'weapon', kind: 'melee', minIlvl: 19, tier: 3, dmg: [15, 24], aps: 1.0 },
{ id: 'reaver', name: { en: 'Reaver Axe', vi: 'Rìu Hủy Diệt' }, slot: 'weapon', kind: 'melee', minIlvl: 26, tier: 4, dmg: [20, 33], aps: 1.02 },
{ id: 'shortbow', name: { en: 'Short Bow', vi: 'Cung Ngắn' }, slot: 'weapon', kind: 'ranged', minIlvl: 1, tier: 1, dmg: [3, 6], aps: 1.45 },
{ id: 'huntingbow', name: { en: 'Hunting Bow', vi: 'Cung Săn Bắn' }, slot: 'weapon', kind: 'ranged', minIlvl: 8, tier: 2, dmg: [7, 12], aps: 1.32 },
{ id: 'longbow', name: { en: 'Long Bow', vi: 'Cung Dài' }, slot: 'weapon', kind: 'ranged', minIlvl: 16, tier: 3, dmg: [11, 19], aps: 1.18 },
{ id: 'shadowbow', name: { en: 'Shadow Bow', vi: 'Cung Bóng Đêm' }, slot: 'weapon', kind: 'ranged', minIlvl: 25, tier: 4, dmg: [17, 28], aps: 1.24 },
{ id: 'gnarledstaff', name: { en: 'Gnarled Staff', vi: 'Trượng Gồ Ghề' }, slot: 'weapon', kind: 'cast', minIlvl: 1, tier: 1, dmg: [3, 6], aps: 1.28, implicit: { manaFlat: 4 } },
{ id: 'oakstaff', name: { en: 'Oak Staff', vi: 'Trượng Sồi' }, slot: 'weapon', kind: 'cast', minIlvl: 9, tier: 2, dmg: [6, 11], aps: 1.22, implicit: { manaFlat: 8 } },
{ id: 'runestaff', name: { en: 'Rune Staff', vi: 'Trượng Phù Văn' }, slot: 'weapon', kind: 'cast', minIlvl: 17, tier: 3, dmg: [10, 18], aps: 1.15, implicit: { manaFlat: 14 } },
{ id: 'archspire', name: { en: 'Arch Spire', vi: 'Tháp Pháp Thuật' }, slot: 'weapon', kind: 'cast', minIlvl: 26, tier: 4, dmg: [15, 27], aps: 1.1, implicit: { manaFlat: 22 } },
/* offhand */
{ id: 'buckler', name: { en: 'Buckler', vi: 'Khiên Nhỏ' }, slot: 'offhand', kind: 'shield', minIlvl: 1, tier: 1, armor: 4, implicit: { blockChance: 8 } },
{ id: 'kiteshield', name: { en: 'Kite Shield', vi: 'Khiên Diềm' }, slot: 'offhand', kind: 'shield', minIlvl: 10, tier: 2, armor: 10, implicit: { blockChance: 12 } },
{ id: 'towerward', name: { en: 'Tower Ward', vi: 'Lá Chắn Tháp' }, slot: 'offhand', kind: 'shield', minIlvl: 20, tier: 3, armor: 19, implicit: { blockChance: 16 } },
{ id: 'quiver', name: { en: 'Quiver', vi: 'Túi Đao' }, slot: 'offhand', kind: 'focus', minIlvl: 1, tier: 1, armor: 1, implicit: { attackSpeedPct: 5 } },
{ id: 'focusorb', name: { en: 'Focus Orb', vi: 'Cầu Tập Trung' }, slot: 'offhand', kind: 'focus', minIlvl: 1, tier: 1, armor: 1, implicit: { manaRegen: 0.5 } },
/* head */
{ id: 'cap', name: { en: 'Cap', vi: 'Mũ Vải' }, slot: 'head', kind: 'armor', minIlvl: 1, tier: 1, armor: 3 },
{ id: 'helm', name: { en: 'Helm', vi: 'Mũ Giáp' }, slot: 'head', kind: 'armor', minIlvl: 8, tier: 2, armor: 8 },
{ id: 'great Helm', name: { en: 'Great Helm', vi: 'Mũ Giáp Lớn' }, slot: 'head', kind: 'armor', minIlvl: 18, tier: 3, armor: 15 },
{ id: 'crownplate', name: { en: 'Crown Plate', vi: 'Vương Miện Thép' }, slot: 'head', kind: 'armor', minIlvl: 27, tier: 4, armor: 23 },
/* chest */
{ id: 'leatherarmor', name: { en: 'Leather Armor', vi: 'Giáp Da' }, slot: 'chest', kind: 'armor', minIlvl: 1, tier: 1, armor: 6 },
{ id: 'chainmail', name: { en: 'Chain Mail', vi: 'Giáp Xích' }, slot: 'chest', kind: 'armor', minIlvl: 9, tier: 2, armor: 14 },
{ id: 'platearmor', name: { en: 'Plate Armor', vi: 'Giáp Tấm' }, slot: 'chest', kind: 'armor', minIlvl: 19, tier: 3, armor: 26 },
{ id: 'doomplate', name: { en: 'War Plate', vi: 'Giáp Chiến Thần' }, slot: 'chest', kind: 'armor', minIlvl: 28, tier: 4, armor: 40 },
/* hands */
{ id: 'gloves', name: { en: 'Gloves', vi: 'Găng Vải' }, slot: 'hands', kind: 'armor', minIlvl: 1, tier: 1, armor: 2 },
{ id: 'gauntlets', name: { en: 'Gauntlets', vi: 'Găng Sắt' }, slot: 'hands', kind: 'armor', minIlvl: 10, tier: 2, armor: 6 },
{ id: 'warmitts', name: { en: 'War Mitts', vi: 'Găng Chiến' }, slot: 'hands', kind: 'armor', minIlvl: 21, tier: 3, armor: 11 },
/* feet */
{ id: 'boots', name: { en: 'Boots', vi: 'Ủng' }, slot: 'feet', kind: 'armor', minIlvl: 1, tier: 1, armor: 2, implicit: { moveSpeedPct: 4 } },
{ id: 'greaves', name: { en: 'Greaves', vi: 'Giáp Chân' }, slot: 'feet', kind: 'armor', minIlvl: 11, tier: 2, armor: 6, implicit: { moveSpeedPct: 6 } },
{ id: 'warboots', name: { en: 'War Treads', vi: 'Ủng Chiến Binh' }, slot: 'feet', kind: 'armor', minIlvl: 22, tier: 3, armor: 11, implicit: { moveSpeedPct: 8 } },
/* jewelry */
{ id: 'amulet', name: { en: 'Amulet', vi: 'Mặt Dây' }, slot: 'amulet', kind: 'jewelry', minIlvl: 1, tier: 1 },
{ id: 'ring', name: { en: 'Ring', vi: 'Nhẫn' }, slot: 'ring', kind: 'jewelry', minIlvl: 1, tier: 1 },
];
// fix accidental key with space
BASES.find(b => b.id === 'great Helm').id = 'greathelm';
/* ---------------- affixes ---------------- */
/* tiers: choose highest tier with t.ilvl <= ilvl; value uniform in [min,max] */
const AFFIXES = [
{ id: 'str', pre: { en: 'Mighty', vi: 'Uy Lực' }, suf: { en: 'of Strength', vi: 'của Sức Mạnh' }, slots: 'any', stat: 'str',
tiers: [{ ilvl: 1, min: 1, max: 3 }, { ilvl: 9, min: 2, max: 5 }, { ilvl: 18, min: 4, max: 8 }, { ilvl: 27, min: 7, max: 13 }] },
{ id: 'dex', pre: { en: 'Deft', vi: 'Thoăn' }, suf: { en: 'of Dexterity', vi: 'của Nhuệ' }, slots: 'any', stat: 'dex',
tiers: [{ ilvl: 1, min: 1, max: 3 }, { ilvl: 9, min: 2, max: 5 }, { ilvl: 18, min: 4, max: 8 }, { ilvl: 27, min: 7, max: 13 }] },
{ id: 'vit', pre: { en: 'Stout', vi: 'Vững' }, suf: { en: 'of Vitality', vi: 'của Sinh Lực' }, slots: 'any', stat: 'vit',
tiers: [{ ilvl: 1, min: 1, max: 3 }, { ilvl: 9, min: 2, max: 5 }, { ilvl: 18, min: 4, max: 8 }, { ilvl: 27, min: 7, max: 13 }] },
{ id: 'ene', pre: { en: 'Arcane', vi: 'Huyền Bí' }, suf: { en: 'of Energy', vi: 'của Năng Lượng' }, slots: 'any', stat: 'ene',
tiers: [{ ilvl: 1, min: 1, max: 3 }, { ilvl: 9, min: 2, max: 5 }, { ilvl: 18, min: 4, max: 8 }, { ilvl: 27, min: 7, max: 13 }] },
{ id: 'dmgpct', pre: { en: 'Brutal', vi: 'Man Dại' }, suf: null, slots: ['weapon', 'amulet', 'ring'], stat: 'dmgPct',
tiers: [{ ilvl: 1, min: 6, max: 12 }, { ilvl: 12, min: 10, max: 20 }, { ilvl: 24, min: 18, max: 32 }] },
{ id: 'flatdmg', pre: { en: 'Cruel', vi: 'Tàn Nhẫn' }, suf: { en: 'of Wounds', vi: 'của Vết Thương' }, slots: ['weapon', 'ring', 'amulet'], stat: '_flatDmg',
tiers: [{ ilvl: 1, min: 1, max: 3 }, { ilvl: 12, min: 3, max: 7 }, { ilvl: 24, min: 6, max: 13 }] },
{ id: 'crit', pre: { en: 'Keen', vi: 'Sắc Bén' }, suf: { en: 'of Precision', vi: 'của Chuẩn Xác' }, slots: 'any', stat: 'critChance',
tiers: [{ ilvl: 1, min: 2, max: 4 }, { ilvl: 12, min: 3, max: 6 }, { ilvl: 24, min: 5, max: 9 }] },
{ id: 'critdmg', pre: { en: 'Savage', vi: 'Hung Tợn' }, suf: null, slots: ['weapon', 'hands', 'ring', 'amulet'], stat: 'critDmgPct',
tiers: [{ ilvl: 4, min: 10, max: 22 }, { ilvl: 16, min: 18, max: 36 }, { ilvl: 26, min: 30, max: 55 }] },
{ id: 'aspd', pre: { en: 'Swift', vi: 'Nhanh Nhẹn' }, suf: null, slots: ['weapon', 'hands', 'ring'], stat: 'attackSpeedPct',
tiers: [{ ilvl: 1, min: 4, max: 8 }, { ilvl: 14, min: 7, max: 13 }, { ilvl: 25, min: 11, max: 19 }] },
{ id: 'mspd', pre: { en: 'Fleet', vi: 'Thanh Nhẹ' }, suf: null, slots: ['feet', 'ring', 'amulet'], stat: 'moveSpeedPct',
tiers: [{ ilvl: 1, min: 4, max: 8 }, { ilvl: 14, min: 7, max: 12 }] },
{ id: 'armor', pre: { en: 'Stalwart', vi: 'Kiên Cố' }, suf: { en: 'of Warding', vi: 'của Che Chở' }, slots: ['head', 'chest', 'hands', 'feet', 'offhand'], stat: 'armor',
tiers: [{ ilvl: 1, min: 3, max: 7 }, { ilvl: 12, min: 8, max: 16 }, { ilvl: 24, min: 16, max: 32 }] },
{ id: 'hp', pre: { en: 'Robust', vi: 'Trâu Bò' }, suf: { en: 'of Life', vi: 'của Máu' }, slots: 'any', stat: 'hpFlat',
tiers: [{ ilvl: 1, min: 6, max: 12 }, { ilvl: 10, min: 12, max: 24 }, { ilvl: 20, min: 24, max: 46 }, { ilvl: 29, min: 42, max: 80 }] },
{ id: 'mana', pre: { en: 'Clear', vi: 'Trong Trời' }, suf: { en: 'of Mana', vi: 'của Mana' }, slots: 'any', stat: 'manaFlat',
tiers: [{ ilvl: 1, min: 4, max: 9 }, { ilvl: 12, min: 9, max: 18 }, { ilvl: 24, min: 16, max: 32 }] },
{ id: 'hpreg', pre: { en: 'Renewing', vi: 'Hồi Phục' }, suf: { en: 'of Regeneration', vi: 'của Tái Tạo' }, slots: ['chest', 'amulet', 'ring'], stat: 'hpRegen',
tiers: [{ ilvl: 1, min: 1, max: 3 }, { ilvl: 15, min: 3, max: 7 }] },
{ id: 'mpreg', pre: { en: 'Flowing', vi: 'Chảy Chảy' }, suf: { en: 'of the Well', vi: 'của Giếng Mana' }, slots: ['head', 'hands', 'amulet', 'ring', 'offhand'], stat: 'manaRegen',
tiers: [{ ilvl: 1, min: .4, max: 1 }, { ilvl: 15, min: 1, max: 2.2 }] },
{ id: 'lok', pre: null, suf: { en: 'of Feasting', vi: 'của Thịnh Soạn' }, slots: ['weapon', 'ring', 'amulet'], stat: 'lifeOnKill',
tiers: [{ ilvl: 1, min: 1, max: 3 }, { ilvl: 14, min: 3, max: 7 }, { ilvl: 26, min: 6, max: 13 }] },
{ id: 'mf', pre: { en: 'Lucky', vi: 'May Mắn' }, suf: { en: 'of Fortune', vi: 'của Vận May' }, slots: 'any', stat: 'magicFind',
tiers: [{ ilvl: 1, min: 5, max: 12 }, { ilvl: 15, min: 10, max: 22 }, { ilvl: 26, min: 18, max: 35 }] },
{ id: 'gf', pre: { en: 'Golden', vi: 'Vàng Óng' }, suf: { en: 'of Greed', vi: 'của Tham Lam' }, slots: 'any', stat: 'goldFind',
tiers: [{ ilvl: 1, min: 8, max: 18 }, { ilvl: 15, min: 15, max: 32 }] },
{ id: 'fire', pre: { en: 'Flaming', vi: 'Rực Lửa' }, suf: { en: 'of Embers', vi: 'của Than Hồng' }, slots: ['weapon', 'ring', 'amulet'], stat: 'fireDmg',
tiers: [{ ilvl: 1, min: 2, max: 5 }, { ilvl: 12, min: 5, max: 11 }, { ilvl: 24, min: 10, max: 21 }] },
{ id: 'cold', pre: { en: 'Frozen', vi: 'Băng Giá' }, suf: { en: 'of Frost', vi: 'của Sương Muối' }, slots: ['weapon', 'ring', 'amulet'], stat: 'coldDmg',
tiers: [{ ilvl: 1, min: 2, max: 4 }, { ilvl: 12, min: 4, max: 9 }, { ilvl: 24, min: 8, max: 17 }] },
{ id: 'lit', pre: { en: 'Crackling', vi: 'Rét Rít' }, suf: { en: 'of Storms', vi: 'của Bão Tố' }, slots: ['weapon', 'ring', 'amulet'], stat: 'litDmg',
tiers: [{ ilvl: 1, min: 1, max: 6 }, { ilvl: 12, min: 4, max: 13 }, { ilvl: 24, min: 9, max: 24 }] },
{ id: 'pois', pre: { en: 'Venomous', vi: 'Nhiễm Độc' }, suf: { en: 'of the Serpent', vi: 'của Con Rắn' }, slots: ['weapon', 'ring', 'amulet'], stat: 'poisDmg',
tiers: [{ ilvl: 1, min: 3, max: 7 }, { ilvl: 12, min: 6, max: 14 }, { ilvl: 24, min: 12, max: 26 }] },
{ id: 'resfire', pre: null, suf: { en: 'of Flame Ward', vi: 'chống Hỏa' }, slots: 'any', stat: 'resFire',
tiers: [{ ilvl: 1, min: 5, max: 12 }, { ilvl: 16, min: 10, max: 22 }, { ilvl: 27, min: 16, max: 34 }] },
{ id: 'rescold', pre: null, suf: { en: 'of Ice Ward', vi: 'chống Băng' }, slots: 'any', stat: 'resCold',
tiers: [{ ilvl: 1, min: 5, max: 12 }, { ilvl: 16, min: 10, max: 22 }, { ilvl: 27, min: 16, max: 34 }] },
{ id: 'reslit', pre: null, suf: { en: 'of Storm Ward', vi: 'chống Lôi' }, slots: 'any', stat: 'resLit',
tiers: [{ ilvl: 1, min: 5, max: 12 }, { ilvl: 16, min: 10, max: 22 }, { ilvl: 27, min: 16, max: 34 }] },
{ id: 'respois', pre: null, suf: { en: 'of Venom Ward', vi: 'chống Độc' }, slots: 'any', stat: 'resPois',
tiers: [{ ilvl: 1, min: 5, max: 12 }, { ilvl: 16, min: 10, max: 22 }, { ilvl: 27, min: 16, max: 34 }] },
{ id: 'cdr', pre: { en: 'Temporal', vi: 'Thời Không' }, suf: null, slots: ['head', 'hands', 'amulet', 'ring'], stat: 'cdr',
tiers: [{ ilvl: 6, min: 4, max: 8 }, { ilvl: 20, min: 7, max: 13 }] },
{ id: 'thorns', pre: { en: 'Spiked', vi: 'Gai Góc' }, suf: { en: 'of Retribution', vi: 'của Trừng Phạt' }, slots: ['head', 'chest', 'offhand', 'amulet'], stat: 'thorns',
tiers: [{ ilvl: 1, min: 2, max: 5 }, { ilvl: 14, min: 5, max: 12 }, { ilvl: 25, min: 10, max: 22 }] },
];
/* ---------------- legendaries ---------------- */
const LEGENDARIES = [
{ id: 'butchers_cleaver', base: 'battleaxe', name: { en: "Butcher's Cleaver", vi: 'Cán Dao Của Đồ Tể' },
special: 'cleaveEcho', stats: { dmgPct: 20, str: 8, hpFlat: 25 },
flavor: { en: 'Fresh meat!', vi: 'Thịt tươi đây!' } },
{ id: 'windstring', base: 'longbow', name: { en: 'Windstring', vi: 'Dây Gió' },
special: 'doubleShot', stats: { attackSpeedPct: 14, dex: 10 },
flavor: { en: 'Twice the arrows, twice the silence.', vi: 'Gấp đôi mũi tên, gấp đôi im lặng.' } },
{ id: 'emberheart', base: 'runestaff', name: { en: 'Emberheart', vi: 'Tim Than Hồng' },
special: 'fireNovaOnKill', stats: { fireDmg: 18, ene: 12 },
flavor: { en: 'It still beats.', vi: 'Nó vẫn còn đập.' } },
{ id: 'frostweave', base: 'platearmor', name: { en: 'Frostweave', vi: 'Vải Băng' },
special: 'chillAttacker', stats: { armor: 20, resCold: 25 },
flavor: { en: 'Cold embraces those who dare.', vi: 'Băng giá ôm lấy kẻ dám lại gần.' } },
{ id: 'abyss_ring', base: 'ring', name: { en: 'Ring of the Abyss', vi: 'Nhẫn Vực Sâu' },
special: 'allSkills', stats: { manaFlat: 20, ene: 8 },
flavor: { en: 'Stare long enough and it stares back.', vi: 'Nhìn đủ lâu, nó sẽ nhìn lại.' } },
{ id: 'gravebind', base: 'amulet', name: { en: 'Gravebind', vi: 'Dây Mộ' },
special: 'execHeal', stats: { lifeOnKill: 8, vit: 10 },
flavor: { en: 'Every death feeds the root.', vi: 'Mỗi cái chết nuôi cái rễ.' } },
{ id: 'stormrunners', base: 'warboots', name: { en: 'Stormrunners', vi: 'Dạo Bước Bão' },
special: 'stormDash', stats: { moveSpeedPct: 16, cdr: 10 },
flavor: { en: 'The ground remembers nothing.', vi: 'Mặt đất chẳng nhớ gì cả.' } },
{ id: 'aegis_ash', base: 'greathelm', name: { en: 'Aegis of Ash', vi: 'Hộ Khiên Tro Tàn' },
special: null, stats: { thorns: 18, resFire: 30, armor: 22, str: 8 },
flavor: { en: 'What burned once cannot burn again.', vi: 'Đã cháy một lần thì không cháy lần nữa.' } },
{ id: 'bloodletter', base: 'dagger', name: { en: 'Bloodletter', vi: 'Xả Máu' },
special: 'lifesteal', stats: { critChance: 8, aspd: 10 },
flavor: { en: 'It drinks before you feast.', vi: 'Nó uống trước khi bạn ăn mừng.' } },
{ id: 'crown_torment', base: 'crownplate', name: { en: 'Crown of Torment', vi: 'Vương Miện Đày Ái' },
special: 'eliteSlayer', stats: { dmgPct: 12, hpFlat: 30 },
flavor: { en: 'Kneel, champions. Your king is here.', vi: 'Quỳ xuống, các tinh anh. Vua của chúng mày tới rồi.' } },
{ id: 'vortex_gauntlets', base: 'gauntlets', name: { en: 'Vortex Gauntlets', vi: 'Găng Xoáy Ốc' },
special: 'chainOnHit', stats: { litDmg: 12, attackSpeedPct: 8 },
flavor: { en: 'One strike becomes a storm.', vi: 'Một đòn hóa thành cơn bão.' } },
{ id: 'doomplate_leg', base: 'doomplate', name: { en: 'Doomplate of Ruin', vi: 'Giáp Tấm Hủy Diệt' },
special: 'corpseBoom', stats: { armor: 34, vit: 12, thorns: 12 },
flavor: { en: 'They die standing closer than they think.', vi: 'Chúng chết ở khoảng cách gần hơn chúng tưởng.' } },
{ id: 'serpent_coil', base: 'ring', name: { en: 'Serpent Coil', vi: 'Vòng Xuyên Rắn' },
special: 'venomStrike', stats: { poisDmg: 14, dex: 8 },
flavor: { en: 'The bite you never saw.', vi: 'Cú cắn bạn không bao giờ thấy.' } },
{ id: 'voidwalker_treads', base: 'greaves', name: { en: 'Voidwalker Treads', vi: 'Dạo Bước Hư Không' },
special: 'voidstep', stats: { moveSpeedPct: 10, cdr: 12 },
flavor: { en: 'Distance is a rumor.', vi: 'Khoảng cách chỉ là tin đồn.' } },
];
/* ---------------- name generation ---------------- */
const RARE_PRE = ['Doom', 'Grim', 'Blood', 'Storm', 'Night', 'Ash', 'Bone', 'Vile', 'Wraith', 'Storm', 'Sun', 'Shadow', 'Hell', 'Raven'];
const RARE_SUF = ['bane', 'song', 'weaver', 'fang', 'howl', 'brand', 'shroud', 'mark', 'veil', 'claw', 'render', 'wail'];
const RARE_OF = {
en: ['the Lost', 'the Fallen', 'the Deep', 'the Hunt', 'the Void', 'Sorrow', 'Ruin', 'the Pact', 'the Pyre', 'the Plague'],
vi: ['Người Lạc', 'Kẻ Sa Đọa', 'Vực Sâu', 'Cuộc Săn', 'Hư Không', 'Nỗi Đau', 'Sự Hủy Diệt', 'Khế Ước', 'Lửa Thiêu', 'Cơn Dịch'],
};
function lang() { return (D2.i18n && D2.i18n.getLang()) || 'en'; }
function pick(arr) { return arr[(Math.random() * arr.length) | 0]; }
function ri(a, b) { return a + Math.floor(Math.random() * (b - a + 1)); }
function baseName(base) {
const n = base.name[lang()];
return n || base.name.en;
}
function makeName(base, rarity, rolled) {
if (rarity === 'common') return baseName(base);
if (rarity === 'magic') {
const withPre = rolled.find(a => AFFIX_MAP[a.id].pre);
const withSuf = rolled.find(a => AFFIX_MAP[a.id].suf);
let n = baseName(base);
if (withPre && (!withSuf || Math.random() < 0.5)) n = AFFIX_MAP[withPre.id].pre[lang()] + ' ' + n;
else if (withSuf) n += ' ' + AFFIX_MAP[withSuf.id].suf[lang()];
else if (withPre) n = AFFIX_MAP[withPre.id].pre[lang()] + ' ' + n;
return n;
}
if (rarity === 'rare') {
let n = pick(RARE_PRE) + pick(RARE_SUF);
if (Math.random() < 0.4) n += ' ' + pick(RARE_OF[lang()] || RARE_OF.en);
return n;
}
return baseName(base);
}
const AFFIX_MAP = {};
for (const a of AFFIXES) AFFIX_MAP[a.id] = a;
/* ---------------- rolling ---------------- */
function tierValue(affix, ilvl) {
let best = affix.tiers[0];
for (const t of affix.tiers) if (t.ilvl <= ilvl) best = t;
const v = ri(best.min * 10, best.max * 10) / 10;
return Math.round(v * 10) / 10;
}
function eligibleAffixes(slot) {
return AFFIXES.filter(a => a.slots === 'any' || (Array.isArray(a.slots) && a.slots.includes(slot)));
}
function scaleBase(base, ilvl, rarityMult) {
const grow = 1 + 0.105 * Math.max(0, ilvl - base.minIlvl);
const out = {};
if (base.dmg) {
out.dmgMin = Math.max(1, Math.round(base.dmg[0] * grow * rarityMult));
out.dmgMax = Math.max(out.dmgMin + 1, Math.round(base.dmg[1] * grow * rarityMult));
out.aps = base.aps;
}
if (base.armor) out.armor = Math.max(1, Math.round(base.armor * grow * rarityMult));
if (base.implicit) out.stats = Object.assign({}, base.implicit);
else out.stats = {};
return out;
}
/** main entry: roll a randomized item */
function rollItem(ilvl, opts = {}) {
ilvl = Math.max(1, Math.round(ilvl));
const slot = opts.slot || pick(SLOTS);
/* pick base among eligible, favoring higher tiers */
const cands = BASES.filter(b => b.slot === slot && b.minIlvl <= ilvl);
if (!cands.length) return null;
const weighted = cands.map(b => ({ v: b, w: Math.pow(b.tier, 1.6) }));
const base = weightedPick(weighted);
/* rarity */
let rarity = opts.rarity;
if (!rarity) {
const mf = opts.magicFind || 0;
const boost = D2.BAL.magicFindRarityBoost(mf);
const wts = [
{ v: 'legendary', w: RARITIES.legendary.weight * boost * (opts.boss ? 7 : 1) * (opts.elite ? 2.6 : 1) },
{ v: 'rare', w: RARITIES.rare.weight * boost * (opts.elite ? 1.7 : 1) },
{ v: 'magic', w: RARITIES.magic.weight * (opts.elite ? 1.3 : 1) },
{ v: 'common', w: RARITIES.common.weight },
];
rarity = weightedPick(wts);
}
const rar = RARITIES[rarity];
const item = {
uid: D2.util.uid(),
slot, baseId: base.id, kind: base.kind,
rarity, ilvl,
reqLevel: Math.max(1, ilvl - 1),
};
Object.assign(item, scaleBase(base, ilvl, rar.mult));
/* legendary: fixed identity */
if (rarity === 'legendary') {
const pool = opts.legendaryPool || LEGENDARIES;
let leg = pick(pool);
if (opts.slot) {
const matching = pool.filter(l => {
const lb = BASES.find(b => b.id === l.base);
return lb && lb.slot === slot;
});
if (matching.length) leg = pick(matching);
}
item.baseId = leg.base;
const lb = BASES.find(b => b.id === leg.base);
Object.assign(item, scaleBase(lb, ilvl, RARITIES.legendary.mult));
item.legendaryId = leg.id;
item.special = leg.special;
item.flavor = leg.flavor ? (leg.flavor[lang()] || leg.flavor.en) : null;
item.name = leg.name[lang()] || leg.name.en;
for (const [k, vBase] of Object.entries(leg.stats)) {
const grow = 1 + 0.055 * Math.max(0, ilvl - lb.minIlvl);
item.stats[k] = (item.stats[k] || 0) + Math.round(vBase * grow);
}
if (Math.random() < 0.65) {
const extra = rollAffixes(eligibleAffixes(slot), 1, ilvl, item.stats);
for (const a of extra) item.stats[a.stat === '_flatDmg' ? 'flatDmg' : a.stat] =
(item.stats[a.stat === '_flatDmg' ? 'flatDmg' : a.stat] || 0) + a.val;
}
item.value = price(item);
return item;
}
/* affix count */
const count = ri(rar.affixes[0], rar.affixes[1]);
const rolled = rollAffixes(eligibleAffixes(slot), count, ilvl, item.stats);
for (const a of rolled) {
if (a.stat === '_flatDmg') {
// flat damage adds both sides
const f = Math.round(a.val);
item.flatDmg = f;
} else {
item.stats[a.stat] = (item.stats[a.stat] || 0) + a.val;
}
}
item.name = makeName(base, rarity, rolled);
item.value = price(item);
return item;
}
function rollAffixes(pool, count, ilvl, existingStats) {
const out = [];
const usedIds = new Set();
const avail = [...pool];
while (out.length < count && avail.length) {
const a = weightedPick(avail.map(x => ({ v: x, w: 1 })));
avail.splice(avail.indexOf(a), 1);
if (usedIds.has(a.id)) continue;
usedIds.add(a.id);
out.push({ id: a.id, stat: a.stat, val: tierValue(a, ilvl) });
}
return out;
}
function weightedPick(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 price(item) {
let core = 0;
if (item.dmgMax) core = (item.dmgMin + item.dmgMax) / 2 * item.aps * 6;
else if (item.armor) core = item.armor * 4.2;
else core = 10;
let statScore = 0;
for (const v of Object.values(item.stats || {})) statScore += typeof v === 'number' ? Math.abs(v) : 0;
if (item.flatDmg) statScore += item.flatDmg * 2;
return Math.max(5, Math.round((core + statScore * 2.2) * (1 + item.ilvl * 0.06) *
({ common: 1, magic: 1.6, rare: 2.6, legendary: 6 }[item.rarity])));
}
/* starter gear per class */
function startingWeapon(cls) {
const baseId = cls === 'crusader' ? 'shortsword' : cls === 'ranger' ? 'shortbow' : 'gnarledstaff';
const base = BASES.find(b => b.id === baseId);
return {
uid: D2.util.uid(), slot: 'weapon', baseId, kind: base.kind, rarity: 'common',
ilvl: 1, reqLevel: 1, name: baseName(base),
dmgMin: base.dmg[0], dmgMax: base.dmg[1], aps: base.aps,
stats: base.implicit ? { ...base.implicit } : {},
value: 10,
};
}
function startingArmor() {
const base = BASES.find(b => b.id === 'leatherarmor');
return {
uid: D2.util.uid(), slot: 'chest', baseId: 'leatherarmor', kind: 'armor', rarity: 'common',
ilvl: 1, reqLevel: 1, name: baseName(base),
armor: base.armor, stats: {}, value: 8,
};
}
function startingPotion() {
const base = BASES.find(b => b.id === 'cap');
void base;
return null;
}
/* vendor stock */
function vendorStock(mlvl, count) {
const out = [];
for (let i = 0; i < count; i++) {
const it = rollItem(Math.max(1, mlvl + ri(-1, 2)), { magicFind: 0 });
if (it) out.push(it);
}
return out;
}
D2.Items = {
RARITIES, SLOTS, BASES, AFFIXES, LEGENDARIES, AFFIX_MAP,
rollItem, price, baseName, startingWeapon, startingArmor, vendorStock,
baseById: id => BASES.find(b => b.id === id),
legendaryById: id => LEGENDARIES.find(l => l.id === id),
isWeapon: it => !!it.dmgMax,
};
})(window.D2);