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:
2026-08-23 06:59:36 +00:00
commit fc1fa2d51e
42 changed files with 11784 additions and 0 deletions
+303
View File
@@ -0,0 +1,303 @@
/* ============================================================
* Diablo2D — i18n.js : English / Vietnamese localization
* ============================================================ */
'use strict';
window.D2 = window.D2 || {};
(function (D2) {
const dict = {
en: {
/* generic */
ok: 'OK', cancel: 'Cancel', close: 'Close', yes: 'Yes', no: 'No',
back: 'Back', confirm: 'Confirm', none: 'None', level_short: 'Lv',
/* title */
'title.newgame': 'New Game', 'title.continue': 'Continue', 'title.settings': 'Settings',
'title.help': 'How to Play', 'title.credits': 'Credits',
'title.tagline': 'Shadows of Tristram',
'title.subtitle': 'A dark action-RPG for your browser',
'title.no_save': 'No saved hero found.',
'title.confirm_overwrite': 'Starting a new game will overwrite your saved hero. Continue?',
'title.saved_hero': 'Saved Hero',
'title.delete_save': 'Delete Save',
'title.confirm_delete': 'Permanently delete the saved hero?',
/* classes */
'class.crusader.name': 'Crusader',
'class.crusader.desc': 'A holy warrior clad in heavy armor. Wades into melee with sweeping strikes and unbreakable defenses.',
'class.ranger.name': 'Shadow Huntress',
'class.ranger.desc': 'A swift rogue who strikes from the shadows with bow and blade, bleeding her prey from afar.',
'class.sorceress.name': 'Sorceress',
'class.sorceress.desc': 'Master of fire, frost and storm. Fragile in body, devastating in arcane power.',
'class.select': 'Choose Your Fate',
'class.play': 'Begin Descent',
/* stats */
'stat.strength': 'Strength', 'stat.dexterity': 'Dexterity', 'stat.vitality': 'Vitality', 'stat.energy': 'Energy',
'stat.damage': 'Damage', 'stat.armor': 'Armor', 'stat.crit': 'Crit Chance', 'stat.critdmg': 'Crit Damage',
'stat.speed': 'Move Speed', 'stat.magicfind': 'Magic Find', 'stat.lifelench': 'Life per Kill',
'stat.fire': 'Fire Resist', 'stat.cold': 'Cold Resist', 'stat.lightning': 'Lightning Resist', 'stat.poison': 'Poison Resist',
'stat.points': 'Attribute Points', 'stat.skillpoints': 'Skill Points',
/* ui panels */
'ui.inventory': 'Inventory', 'ui.character': 'Character', 'ui.skills': 'Skills',
'ui.stash': 'Stash', 'ui.vendor': 'Blacksmith', 'ui.healer': 'Healer',
'ui.questlog': 'Quest Log', 'ui.map': 'World Map', 'ui.help': 'How to Play',
'ui.equip.weapon': 'Weapon', 'ui.equip.offhand': 'Off-hand', 'ui.equip.head': 'Head',
'ui.equip.chest': 'Body', 'ui.equip.hands': 'Hands', 'ui.equip.feet': 'Feet',
'ui.equip.amulet': 'Amulet', 'ui.equip.ring1': 'Ring', 'ui.equip.ring2': 'Ring',
'ui.gold': 'Gold', 'ui.sell': 'Sell', 'ui.buy': 'Buy', 'ui.repair_none': '',
'ui.heal_full': 'Full Restore', 'ui.price': 'Price',
'ui.empty_slot': 'Empty', 'ui.drop_confirm': 'This item will be destroyed. Drop it?',
/* hotbar */
'hb.health_potion': 'Health Potion', 'hb.mana_potion': 'Mana Potion',
'hb.inventory': 'Inventory (I)', 'hb.character': 'Character (C)', 'hb.skills': 'Skills (T)',
'hb.quests': 'Quest Log (J)', 'hb.map': 'Map (M)', 'hb.help': 'Help (F1)',
/* messages */
'msg.levelup': 'Level Up! Press C to assign points.',
'msg.skillpoint': 'New skill learned! Press T to view skills.',
'msg.not_enough_mana': 'Not enough mana!',
'msg.skill_cooldown': 'Skill is not ready.',
'msg.inventory_full': 'Inventory is full!',
'msg.potion_none': 'No potions left!',
'msg.boss_spawn': 'A great evil awakens...',
'msg.boss_slain': 'The guardian has fallen!',
'msg.act_clear': 'Act complete! The waypoint stirs...',
'msg.saved': 'Game saved.',
'msg.hardcore_death': 'Your hardcore hero has perished. The record burns.',
'msg.town_portal': 'Returned to town.',
'msg.elite_slain': 'Champion slain!',
'msg.shrine': 'You touch the shrine...',
/* quests */
'quest.main_prefix': 'Act {0}: ',
'quest.descend': 'Descend to depth {0}',
'quest.kill_boss': 'Slay {0}',
'quest.done': 'Complete',
/* death / victory */
'death.title': 'YOU HAVE DIED',
'death.subtitle': 'Even heroes fall... but darkness always offers another chance.',
'death.respawn': 'Awaken in Town', 'death.title_screen': 'Return to Title',
'victory.title': 'THE LORD OF TERROR IS VANQUISHED',
'victory.subtitle': 'Tristram breathes again — yet the depths always call louder.',
'victory.continue': 'Continue in Torment {0}', 'victory.title_screen': 'Return to Title',
'victory.stats': 'Run Statistics',
/* settings */
'set.master': 'Master Volume', 'set.music': 'Music', 'set.sfx': 'Sound Effects',
'set.lang': 'Language', 'set.shake': 'Screen Shake', 'set.dmgnum': 'Damage Numbers',
'set.labels': 'Item Labels', 'set.hardcore': 'Hardcore Mode (new game)',
'set.export': 'Export Save Code', 'set.import': 'Import Save Code', 'set.wipe': 'Erase All Data',
'set.copy_ok': 'Copied!', 'set.import_bad': 'Invalid save code.', 'set.import_ok': 'Save imported!',
'set.confirm_wipe': 'Erase ALL saved data? This cannot be undone.',
/* help */
'help.move': 'Left click — move / attack. Hold to keep moving toward cursor.',
'help.combat': 'Right click & keys 14 — cast skills. Q / E — potions.',
'help.items': 'Hold Alt to reveal ground item labels. Click an item to pick up.',
'help.panels': 'I inventory · C character · T skills · J quests · M map · Esc pause.',
'help.tip1': 'Champions glow with an aura — they hit harder but drop better loot.',
'help.tip2': 'Shrines grant powerful temporary blessings.',
'help.tip3': 'Resistances matter more than armor against elemental bosses.',
'help.keys_title': 'Controls',
/* town npcs */
'npc.charsi.name': 'Charsi',
'npc.akara.name': 'Akara',
'npc.kashya.name': 'Kashya',
'npc.cain.name': 'Deckard Cain',
'npc.gheed.name': 'Gheed',
'npc.stash.name': 'Camp Storage',
'msg.quest_accepted': 'Quest accepted: {0}',
'msg.quest_reward': 'Quest complete! +{0} gold',
'quest.seek_hint': 'Visit camp — new tasks await.',
'ui.rare_item': 'a Rare item',
'npc.charsi.name': 'Charsi',
'npc.akara.name': 'Akara',
'npc.kashya.name': 'Kashya',
'npc.cain.name': 'Deckard Cain',
'npc.gheed.name': 'Gheed',
'npc.stash.name': 'Kho trại',
'msg.quest_accepted': 'Đã nhận nhiệm vụ: {0}',
'msg.quest_reward': 'Nhiệm vụ hoàn thành! +{0} vàng',
'quest.seek_hint': 'Hãy ghé trại — có việc mới đang chờ.',
'ui.rare_item': 'một món Hiếm',
'npc.charsi.greet': 'Steel keeps you alive down there. Browse my wares.',
'npc.akara.greet': 'The Light mend you. What do you need, child?',
'npc.kashya.greet': 'The camp needs a blade like yours. We have work.',
'npc.cain.greet': 'Stay awhile and listen…',
'npc.gheed.greet': 'Psst. Finest goods in the camp — provenance uncertain.',
'npc.stash.greet': 'Your vault awaits.',
'npc.waypoint.greet': 'Where does fate pull you, hero?',
'ui.accept': 'Accept quest',
'ui.turn_in': 'Turn in',
'ui.reward': 'Reward',
'ui.later': 'Not now',
'ui.lore': 'Ask for lore',
'ui.buy_tome': 'Buy Skill Tome (+1 skill pt)',
'ui.respec': 'Respec all points',
'ui.gamble': 'Gamble for goods',
'ui.gamble_hint': 'Rarity unknown until bought…',
'ui.quest_done_return': 'Return to {0}',
'ui.tome_bought': 'Knowledge burns behind your eyes… (+1 skill point)',
'ui.respec_done': 'Your training is undone. Points refunded.',
'ui.gamble_win': 'Fortune favors you… this time.',
'quest.cull': 'Cull the Fallen',
'quest.cull.desc': 'Slay {0} denizens of the Cathedral depths. Kashya counts every kill.',
'quest.boss': 'The {0}',
'quest.boss.desc': 'Descend to the lair and destroy {0}. The camp will not rest until it is done.',
'quest.torment': 'Echoes of Terror',
'quest.torment.desc': 'Malgor is dead, yet his echo festers. Open torment {0} and cull it anew.',
'town.enter': 'Tristram Outpost — Sanctuary',
/* misc */
'floor.depth': 'Depth {0}',
'act.names.0': 'The Ravaged Cathedral',
'act.names.1': 'The Weeping Catacombs',
'act.names.2': 'The Fungal Depths',
'act.names.3': "Hell's Threshold",
torment: 'Torment {0}',
'loading.tips.0': 'Elites drop enchanted loot. Hunt the glowing ones.',
'loading.tips.1': 'Potions refill each time you visit town.',
'loading.tips.2': 'Fire melts flesh; cold freezes bone. Bring both.',
'loading.tips.3': 'Barrels and urns hide coins... and sometimes worse.',
'loading.tips.4': 'Magic Find increases rare and legendary drops.',
'credits.line1': 'Design, code, art & audio — procedurally generated, no external assets.',
'credits.line2': 'Inspired by the classics that defined the genre.',
},
vi: {
ok: 'Đồng ý', cancel: 'Hủy', close: 'Đóng', yes: 'Có', no: 'Không',
back: 'Quay lại', confirm: 'Xác nhận', none: 'Không', level_short: 'Cấp',
'title.newgame': 'Chơi mới', 'title.continue': 'Tiếp tục', 'title.settings': 'Cài đặt',
'title.help': 'Hướng dẫn chơi', 'title.credits': 'Thông tin',
'title.tagline': 'Bóng Đêm Tristram',
'title.subtitle': 'Action-RPG tối giản chạy ngay trên trình duyệt',
'title.no_save': 'Không tìm thấy bản lưu nào.',
'title.confirm_overwrite': 'Bắt đầu game mới sẽ xóa hero đã lưu. Tiếp tục?',
'title.saved_hero': 'Hero đã lưu',
'title.delete_save': 'Xóa bản lưu',
'title.confirm_delete': 'Xóa vĩnh viễn hero đã lưu?',
'class.crusader.name': 'Thập Tự Quân',
'class.crusader.desc': 'Chiến binh thánh thiện trong bộ giáp nặng, quét sạch kẻ thù bằng chém rộng và phòng thủ thép.',
'class.ranger.name': 'Nữ Thợ Săn Bóng Đêm',
'class.ranger.desc': 'Nữ sát thủ nhanh nhẹn, hạ gục con mồi từ xa bằng cung tên độc và bóng tối.',
'class.sorceress.name': 'Pháp Sư',
'class.sorceress.desc': 'Bậc thầy hỏa, băng và lôi. Thân yếu nhưng sức mạnh huyền thuật tàn khốc.',
'class.select': 'Chọn Số Phận Của Bạn',
'class.play': 'Bắt Đầu Hạ Sơn',
'stat.strength': 'Sức mạnh', 'stat.dexterity': 'Nhuệ', 'stat.vitality': 'Sinh lực', 'stat.energy': 'Năng lượng',
'stat.damage': 'Sát thương', 'stat.armor': 'Giáp', 'stat.crit': 'Tỷ lệ chí mạng', 'stat.critdmg': 'ST chí mạng',
'stat.speed': 'Tốc độ chạy', 'stat.magicfind': 'May mắn tìm đồ', 'stat.lifelench': 'Hồi máu mỗi lần hạ',
'stat.fire': 'Kháng Hỏa', 'stat.cold': 'Kháng Băng', 'stat.lightning': 'Kháng Lôi', 'stat.poison': 'Kháng Độc',
'stat.points': 'Điểm thuộc tính', 'stat.skillpoints': 'Điểm kỹ năng',
'ui.inventory': 'Túi đồ', 'ui.character': 'Nhân vật', 'ui.skills': 'Kỹ năng',
'ui.stash': 'Kho đồ', 'ui.vendor': 'Thợ Rèn', 'ui.healer': 'Pháp Sư Trị Liệu',
'ui.questlog': 'Nhật nhiệm vụ', 'ui.map': 'Bản đồ thế giới', 'ui.help': 'Hướng dẫn',
'ui.equip.weapon': 'Vũ khí', 'ui.equip.offhand': 'Tay trái', 'ui.equip.head': 'Đầu',
'ui.equip.chest': 'Thân', 'ui.equip.hands': 'Tay', 'ui.equip.feet': 'Chân',
'ui.equip.amulet': 'Mặt dây', 'ui.equip.ring1': 'Nhẫn', 'ui.equip.ring2': 'Nhẫn',
'ui.gold': 'Vàng', 'ui.sell': 'Bán', 'ui.buy': 'Mua',
'ui.heal_full': 'Hồi phục hoàn toàn', 'ui.price': 'Giá',
'ui.empty_slot': 'Trống', 'ui.drop_confirm': 'Vật phẩm sẽ bị phá hủy. Vứt bỏ?',
'hb.health_potion': 'Bình máu', 'hb.mana_potion': 'Bình mana',
'hb.inventory': 'Túi đồ (I)', 'hb.character': 'Nhân vật (C)', 'hb.skills': 'Kỹ năng (T)',
'hb.quests': 'Nhiệm vụ (J)', 'hb.map': 'Bản đồ (M)', 'hb.help': 'Trợ giúp (F1)',
'msg.levelup': 'Lên cấp! Nhấn C để cộng điểm.',
'msg.skillpoint': 'Kỹ năng mới! Nhấn T để xem kỹ năng.',
'msg.not_enough_mana': 'Không đủ mana!',
'msg.skill_cooldown': 'Kỹ năng chưa hồi xong.',
'msg.inventory_full': 'Túi đồ đầy!',
'msg.potion_none': 'Hết bình hồi máu!',
'msg.boss_spawn': 'Một ma lực khổng lồ thức dậy...',
'msg.boss_slain': 'Ác thần đã bị hạ!',
'msg.act_clear': 'Hoàn thành chương! Điểm dịch chuyển rung lên...',
'msg.saved': 'Đã lưu game.',
'msg.hardcore_death': 'Hero Hardcore của bạn đã chết. Bản lưu bị thiêu rụi.',
'msg.town_portal': 'Đã về trấn.',
'msg.elite_slain': 'Đã hạ tinh anh!',
'msg.shrine': 'Bạn chạm vào đền thờ...',
'quest.main_prefix': 'Chương {0}: ',
'quest.descend': 'Hạ xuống độ sâu {0}',
'quest.kill_boss': 'Hạ gục {0}',
'quest.done': 'Hoàn thành',
'death.title': 'BẠN ĐÃ CHẾT',
'death.subtitle': 'Anh hùng cũng có thể ngã... nhưng bóng tối luôn cho thêm một cơ hội.',
'death.respawn': 'Thức dậy ở trấn', 'death.title_screen': 'Về màn hình chính',
'victory.title': 'CHÚA TỂ KINH HOÀNG ĐÃ BỊ TIÊU DIỆT',
'victory.subtitle': 'Tristram lại được thở — nhưng vực sâu luôn gọi to hơn.',
'victory.continue': 'Tiếp tục ở Torment {0}', 'victory.title_screen': 'Về màn hình chính',
'victory.stats': 'Thống kê chuyến chiến',
'set.master': 'Âm lượng tổng', 'set.music': 'Nhạc nền', 'set.sfx': 'Hiệu ứng âm thanh',
'set.lang': 'Ngôn ngữ', 'set.shake': 'Rung màn hình', 'set.dmgnum': 'Số sát thương',
'set.labels': 'Nhãn đồ trên đất', 'set.hardcore': 'Chế độ Hardcore (chơi mới)',
'set.export': 'Xuất mã lưu', 'set.import': 'Nhập mã lưu', 'set.wipe': 'Xóa toàn bộ dữ liệu',
'set.copy_ok': 'Đã sao chép!', 'set.import_bad': 'Mã lưu không hợp lệ.', 'set.import_ok': 'Đã nhập bản lưu!',
'set.confirm_wipe': 'Xóa TOÀN BỘ dữ liệu đã lưu? Không thể hoàn tác.',
'help.move': 'Chuột trái — di chuyển / tấn công. Giữ để liên tục đi theo con trỏ.',
'help.combat': 'Chuột phải & phím 14 — dùng kỹ năng. Q / E — bình máu, bình mana.',
'help.items': 'Giữ Alt để hiện nhãn đồ trên đất. Nhấp vào món để nhặt.',
'help.panels': 'I túi đồ · C nhân vật · T kỹ năng · J nhiệm vụ · M bản đồ · Esc tạm dừng.',
'help.tip1': 'Tinh anh phát sáng quanh thân — mạnh hơn nhưng rớt đồ tốt hơn.',
'help.tip2': 'Đền thờ ban phước tạm thời rất mạnh.',
'help.tip3': 'Kháng nguyên tố quan trọng hơn giáp khi đấu boss hệ elemental.',
'help.keys_title': 'Điều khiển',
'npc.charsi.greet': 'Thép giữ bạn sống dưới đó. Xem hàng của ta đi.',
'npc.akara.greet': 'Ánh Sáng chữa lành ngươi. Con cần gì nào?',
'npc.kashya.greet': 'Trại cần một lưỡi kiếm như ngươi. Ta có việc đây.',
'npc.cain.greet': 'Ở lại nghe ta kể chuyện này…',
'npc.gheed.greet': 'Này. Hàng tốt nhất trại — nguồn gốc thì… khỏi hỏi.',
'npc.stash.greet': 'Kho của bạn đang chờ.',
'npc.waypoint.greet': 'Định mệnh đang kéo bạn đến đâu, anh hùng?',
'ui.accept': 'Nhận nhiệm vụ',
'ui.turn_in': 'Trả nhiệm vụ',
'ui.reward': 'Thưởng',
'ui.later': 'Để sau',
'ui.lore': 'Hỏi chuyện xưa',
'ui.buy_tome': 'Mua Sách Kỹ năng (+1 điểm)',
'ui.respec': 'Phân bổ lại toàn bộ điểm',
'ui.gamble': 'Đánh bạc đổi hàng',
'ui.gamble_hint': 'Đến khi mua mới biết hiếm hay thường…',
'ui.quest_done_return': 'Quay về gặp {0}',
'ui.tome_bought': 'Tri thức bừng cháy trong đầu con… (+1 điểm kỹ năng)',
'ui.respec_done': 'Toàn bộ điểm đã hoàn trả.',
'ui.gamble_win': 'Thần may mắn đứng về phía ngươi… lần này.',
'quest.cull': 'Thanh tẩy Kẻ ngã ngã',
'quest.cull.desc': 'Hạ {0} sinh vật trong hang động. Kashya đếm từng mạng.',
'quest.boss': '{0}',
'quest.boss.desc': 'Xuống tới hang ổ và tiêu diệt {0}. Trại sẽ không yên khi nó còn sống.',
'quest.torment': 'Ám ảnh Khủng khiếp',
'quest.torment.desc': 'Malgor chết rồi nhưng bóng ma nó còn. Mở torment {0} và thanh tẩy lại.',
'town.enter': 'Trấn Tristram — Nơi Tôn Nghiêm',
'floor.depth': 'Độ sâu {0}',
'act.names.0': 'Nhà thờ hoang tàn',
'act.names.1': 'Hầm mộ than khóc',
'act.names.2': 'Vực nấm sâu thẳm',
'act.names.3': 'Ngưỡng cửa Địa Ngục',
torment: 'Torment {0}',
'loading.tips.0': 'Tinh anh rớt đồ phù phép. Săn những con phát sáng.',
'loading.tips.1': 'Bình hồi máu được nạp lại mỗi khi về trấn.',
'loading.tips.2': 'Lửa thiêu thịt; băng đóng băng xương. Mang cả hai.',
'loading.tips.3': 'Thùng rượu và lư đồng giấu vàng... đôi khi còn giấu cả thứ khác.',
'loading.tips.4': 'May mắn tìm đồ tăng tỷ lệ rớt đồ hiếm và huyền thoại.',
'credits.line1': 'Thiết kế, lập trình, hình ảnh & âm nhạc — sinh ra thủ tục, không dùng tài nguyên ngoài.',
'credits.line2': 'Lấy cảm hứng từ những tựa game kinh điển định hình thể loại.',
},
};
let lang = 'en';
function t(key, ...vars) {
let s = (dict[lang] && dict[lang][key]) ?? dict.en[key] ?? key;
vars.forEach((v, i) => { s = s.replaceAll('{' + i + '}', String(v)); });
return s;
}
function setLang(l) { lang = (dict[l] ? l : 'en'); if (typeof document !== 'undefined') document.documentElement.lang = lang; }
function getLang() { return lang; }
function languages() { return Object.keys(dict); }
/* apply data-i18n attributes inside a root element */
function applyDom(root = document) {
root.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.getAttribute('data-i18n'));
});
root.querySelectorAll('[data-i18n-title]').forEach(el => {
el.title = t(el.getAttribute('data-i18n-title'));
});
}
D2.i18n = { t, setLang, getLang, languages, applyDom, dict };
})(window.D2);