- 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
143 lines
5.7 KiB
JavaScript
143 lines
5.7 KiB
JavaScript
/* ============================================================
|
|
* 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);
|