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,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);
|
||||
Reference in New Issue
Block a user