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
+142
View File
@@ -0,0 +1,142 @@
/* ============================================================
* Diablo2D — balance.js : tuning constants & progression curves
* ============================================================ */
'use strict';
window.D2 = window.D2 || {};
(function (D2) {
const BAL = {
/* --- progression --- */
maxLevel: 40,
statPointsPerLevel: 5,
skillPointsPerLevel: 1,
xpForLevel(l) { return Math.floor(58 * Math.pow(l, 1.72) + 24 * l); },
/* --- player base --- */
baseHp: 42, hpPerVit: 3.4, hpPerLevel: 7.5,
baseMana: 18, manaPerEne: 2.2, manaPerLevel: 3.5,
baseArmor: 3,
baseCrit: 0.05, baseCritDmg: 1.5,
baseSpeed: 3.4, // tiles / second
baseAttackCd: 0.62, // seconds (modified by weapon aps)
hpRegenPerSec: 0.35, manaRegenPerSec: 1.4,
/* primary stat -> derived */
meleeDmgPerStr: 0.011, // +1.1% weapon dmg / str
rangedDmgPerDex: 0.011,
castDmgPerEne: 0.012,
armorPerStr: 0.25,
critPerDex: 0.0018, // +0.18% / dex
dodgePerDex: 0.0022,
/* caps */
resistCap: 0.72,
armorDrCap: 0.78,
critCap: 0.75,
speedCap: 6.2,
/* --- monsters --- */
monsterLevel(act, floorIdx, torment) { return 1 + act * 4 + floorIdx + (torment || 0) * 9; },
mHp(mlvl) { return Math.floor(15 + 9.2 * Math.pow(mlvl, 1.36)); },
mDmg(mlvl) { return Math.floor(2.4 + 2.1 * Math.pow(mlvl, 1.28)); },
mXp(mlvl) { return Math.floor(7 + 5.2 * Math.pow(mlvl, 1.35)); },
mArmor(mlvl) { return Math.floor(2 + 1.6 * mlvl); },
eliteHpMult: 2.2, eliteDmgMult: 1.38, eliteXpMult: 2.4, eliteSizeMult: 1.22,
bossHpMult: 7.0, bossDmgMult: 1.55, bossXpMult: 12, bossSizeMult: 1.85,
/* --- combat --- */
armorK: 34 + 6.2, // DR = armor/(armor + K*mlvl) — K applied as armorK*mlvl
knockback: 0.16, // tiles
hitFlashTime: 0.12,
corpseTime: 14,
goldDropBase(mlvl) { return Math.floor(3 + 2.6 * mlvl * (0.8 + Math.random() * 0.5)); },
/* --- drops --- */
dropChances: {
gold: 0.42, potionHp: 0.11, potionMp: 0.075, gear: 0.145, gearEliteBonus: 0.35, gearBoss: 1.0,
},
rarityWeights: { common: 100, magic: 34, rare: 9.5, legendary: 1.35 },
magicFindRarityBoost(mf) { return 1 + mf / 100 * 0.85; }, // mf stat in percent points
potionStackMax: 12,
potionHealPct: 0.32,
potionManaPct: 0.38,
potionHealFlat(mlvl) { return 14 + 6 * mlvl; },
/* --- economy --- */
vendorStockSize: [8, 13],
vendorRerollCostMult: 0.0,
sellRatio: 0.28,
buyMarkup: 1.0,
healerCost(level) { return 12 + level * 6; },
stashRows: 7,
/* --- death --- */
deathGoldPenalty: 0.12,
/* --- acts --- */
/* ---- quest chain (Diablo-style: accept from NPCs, hunt, turn in) ---- */
questKillBase(act) { return 12 + act * 4; },
gambleCost(level) { return 80 + level * 45; },
tomeCost(level) { return 120 + level * 60; },
respecCost(level) { return 300 + level * 90; },
QUESTS: [
// Act I
{ id: 'a0_cull', giver: 'kashya', act: 0, type: 'kills', target: 15,
title: 'quest.cull', desc: 'quest.cull.desc', reward: { gold: 180, xp: 90 } },
{ id: 'a0_boss', giver: 'cain', act: 0, requires: 'a0_cull', type: 'boss',
title: 'quest.boss', desc: 'quest.boss.desc', reward: { gold: 450, xp: 320, itemLevelBonus: 2 } },
// Act II
{ id: 'a1_cull', giver: 'kashya', act: 1, type: 'kills', target: 18,
title: 'quest.cull', desc: 'quest.cull.desc', reward: { gold: 380, xp: 700 } },
{ id: 'a1_boss', giver: 'cain', act: 1, requires: 'a1_cull', type: 'boss',
title: 'quest.boss', desc: 'quest.boss.desc', reward: { gold: 800, xp: 1500, itemLevelBonus: 3 } },
// Act III
{ id: 'a2_cull', giver: 'kashya', act: 2, type: 'kills', target: 22,
title: 'quest.cull', desc: 'quest.cull.desc', reward: { gold: 900, xp: 4200 } },
{ id: 'a2_boss', giver: 'cain', act: 2, requires: 'a2_cull', type: 'boss',
title: 'quest.boss', desc: 'quest.boss.desc', reward: { gold: 1600, xp: 8000, itemLevelBonus: 4 } },
// Act IV
{ id: 'a3_cull', giver: 'kashya', act: 3, type: 'kills', target: 26,
title: 'quest.cull', desc: 'quest.cull.desc', reward: { gold: 2000, xp: 22000 } },
{ id: 'a3_boss', giver: 'cain', act: 3, requires: 'a3_cull', type: 'boss',
title: 'quest.boss', desc: 'quest.boss.desc', reward: { gold: 3500, xp: 40000, itemLevelBonus: 5 } },
// Post-victory repeatable hook
{ id: 'torment_1', giver: 'cain', act: 3, requiresVictory: true, type: 'boss', torment: true,
title: 'quest.torment', desc: 'quest.torment.desc', reward: { gold: 5000, xp: 60000, itemLevelBonus: 6 } },
],
questById(id) { return this.QUESTS.find(q => q.id === id); },
acts: [
{ name: 'act.names.0', floors: 4, boss: 'butcher', theme: 'cathedral', music: 'crypt' },
{ name: 'act.names.1', floors: 4, boss: 'boneking', theme: 'catacombs', music: 'crypt' },
{ name: 'act.names.2', floors: 4, boss: 'spiderqueen', theme: 'caves', music: 'cave' },
{ name: 'act.names.3', floors: 4, boss: 'terrorlord', theme: 'hell', music: 'hell' },
],
floorsPerAct: 4, // last floor of each act is the boss lair
maxTorment: 10,
/* --- shrine buffs --- */
shrineDuration: 45,
shrineBuffPower: { dmg: 0.35, speed: 0.3, armor: 0.5, xp: 0.5, regen: 4 },
/* --- skills --- */
skillMaxRank: 5,
tierUnlockLevels: [1, 4, 8, 13, 19],
manaCostRank(base) { return Math.round(base * (1 + 0.14 * 4)); }, // computed per rank elsewhere
cooldownReductionCap: 0.55,
/* --- misc --- */
interactRange: 1.35,
pickupRange: 1.15,
aggroRange: 7.5,
deaggroLeash: 14,
maxProjectiles: 260,
maxParticles: 700,
maxFloatingText: 90,
};
BAL.armorK = 40; // final constant
D2.BAL = BAL;
})(window.D2);