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
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
/* ============================================================
|
||||
* 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);
|
||||
Reference in New Issue
Block a user