/* ============================================================ * 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);