diff --git a/README.md b/README.md index b6cde64..1e6f2d7 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ The app lets you enter the current state of all equipment and then recommends: 3. **Overall build summary** - Totals base damage and health. - Totals effect percentages across all items, mount, and pets. - - Shows calculated DPS and a defensive score to compare save slots. + - Shows calculated DPS and estimated regen to compare save slots. ## DPS model @@ -102,14 +102,22 @@ Defensive effect totals use an estimated survivability value based on health, bl ## Strategy profile -The first version includes an adjustable strategy profile: +The app includes two adjustable strategy profiles: -- Balanced -- Damage focused -- Survivability focused -- Skill focused +- Lifesteal: prioritizes DPS and lifesteal-based healing. +- Regen: prioritizes health and health-regeneration based healing. -Each profile changes effect weights used for recommendations. These weights are intentionally transparent in the code so they can be tuned as better Forge Master formulas become known. +Each profile changes effect weights used for sacrifice scoring and tie-breaking. DPS-based upgrade recommendations still use the current build math directly. + +## Regen metric + +The Regen card estimates healing from both sources: + +```text +regen = max health * health regen% + DPS * lifesteal% +``` + +The displayed breakdown separates health regen from lifesteal healing. ## Save slots diff --git a/app.js b/app.js index 0d21aa1..a443c24 100644 --- a/app.js +++ b/app.js @@ -15,29 +15,17 @@ const EFFECTS = [ ]; const PROFILES = { - balanced: { - label: "Balanced", - weights: { critChance: 8, critDamage: 4, blockChance: 7, healthRegen: 7, lifesteal: 7, doubleChance: 8, damage: 9, meleeDamage: 3, rangedDamage: 5, attackSpeed: 6, skillDamage: 5, skillCooldown: 8, health: 7 }, - baseDamage: 1, - baseHealth: 0.75, - }, - damage: { - label: "Damage focused", - weights: { critChance: 10, critDamage: 6, blockChance: 3, healthRegen: 3, lifesteal: 6, doubleChance: 10, damage: 10, meleeDamage: 5, rangedDamage: 7, attackSpeed: 8, skillDamage: 7, skillCooldown: 7, health: 3 }, + lifesteal: { + label: "Lifesteal (DPS + Lifesteal)", + weights: { critChance: 10, critDamage: 6, blockChance: 2, healthRegen: 2, lifesteal: 10, doubleChance: 10, damage: 10, meleeDamage: 6, rangedDamage: 6, attackSpeed: 9, skillDamage: 7, skillCooldown: 7, health: 3 }, baseDamage: 1.25, baseHealth: 0.45, }, - survival: { - label: "Survivability focused", - weights: { critChance: 5, critDamage: 2, blockChance: 10, healthRegen: 10, lifesteal: 9, doubleChance: 5, damage: 5, meleeDamage: 2, rangedDamage: 3, attackSpeed: 4, skillDamage: 3, skillCooldown: 5, health: 10 }, - baseDamage: 0.65, - baseHealth: 1.2, - }, - skill: { - label: "Skill focused", - weights: { critChance: 7, critDamage: 4, blockChance: 5, healthRegen: 5, lifesteal: 6, doubleChance: 7, damage: 7, meleeDamage: 2, rangedDamage: 4, attackSpeed: 5, skillDamage: 10, skillCooldown: 11, health: 5 }, - baseDamage: 0.9, - baseHealth: 0.65, + regen: { + label: "Regen (Health + Regen)", + weights: { critChance: 4, critDamage: 2, blockChance: 7, healthRegen: 12, lifesteal: 5, doubleChance: 4, damage: 4, meleeDamage: 2, rangedDamage: 2, attackSpeed: 3, skillDamage: 3, skillCooldown: 3, health: 12 }, + baseDamage: 0.45, + baseHealth: 1.35, }, }; @@ -69,7 +57,7 @@ let state = loadDraft() || createDefaultState(); function createDefaultState() { return { - profile: "balanced", + profile: "lifesteal", combatStyle: "melee", recommendationCount: 8, pieces: slots.map((slot, index) => createDefaultPiece(slot, index)), @@ -108,10 +96,11 @@ function enforceFixedPieceFields(piece, index) { function normalizeState(candidate) { const fallback = createDefaultState(); + const profile = normalizeProfile(candidate?.profile); return { ...fallback, ...candidate, - profile: PROFILES[candidate?.profile] ? candidate.profile : "balanced", + profile, combatStyle: ["melee", "ranged", "skill"].includes(candidate?.combatStyle) ? candidate.combatStyle : "melee", recommendationCount: Math.min(20, Math.max(3, Number(candidate?.recommendationCount || 8))), pieces: fallback.pieces.map((piece, index) => enforceFixedPieceFields({ @@ -131,6 +120,13 @@ function normalizeState(candidate) { }; } +function normalizeProfile(profile) { + if (PROFILES[profile]) return profile; + if (["damage", "skill", "balanced"].includes(profile)) return "lifesteal"; + if (profile === "survival") return "regen"; + return "lifesteal"; +} + function clampEffectValue(type, value) { if (!type) return 1; const effect = effectById(type); @@ -223,6 +219,7 @@ function updateAnalysis() { const totals = calculateTotals(); const damageDetails = calculateDamageDetails(totals, state.combatStyle); const dps = damageDetails.dps; + const regenDetails = calculateRegenDetails(totals, dps); const survivability = calculateSurvivability(totals); const pieceScores = state.pieces.map((piece) => scorePiece(piece, profile, totals)); @@ -230,7 +227,8 @@ function updateAnalysis() { $("#totalHealth").textContent = fmt(totals.baseHealth); $("#dpsValue").textContent = fmt(dps); $("#hitDamageDetails").innerHTML = `Hit ${fmt(damageDetails.singleHit)}
Crit ${fmt(damageDetails.critHit)}`; - $("#defenseScore").textContent = fmt(calculateGroupScore(totals, profile, ["defense", "hybrid"], "health")); + $("#regenValue").textContent = fmt(regenDetails.total); + $("#regenDetails").innerHTML = `Health regen ${fmt(regenDetails.healthRegen)}
Lifesteal ${fmt(regenDetails.lifesteal)}`; document.querySelectorAll(".gear-card").forEach((card, index) => { card.querySelector(".piece-score").textContent = `Score ${fmt(pieceScores[index].score)}`; @@ -289,6 +287,17 @@ function calculateSurvivability(totals) { return health * blockMultiplier * regenMultiplier * lifestealMultiplier; } +function calculateRegenDetails(totals, dps) { + const maxHealth = Math.max(0, totals.baseHealth) * (1 + totals.effects.health / 100); + const healthRegen = maxHealth * (totals.effects.healthRegen / 100); + const lifesteal = Math.max(0, dps) * (totals.effects.lifesteal / 100); + return { + healthRegen, + lifesteal, + total: healthRegen + lifesteal, + }; +} + function getStyleDamageBonus(effects, combatStyle) { if (combatStyle === "ranged") return effects.rangedDamage; if (combatStyle === "skill") return effects.skillDamage; diff --git a/index.html b/index.html index 41481bd..b1323af 100644 --- a/index.html +++ b/index.html @@ -74,7 +74,7 @@
Total damage base0
Total health base0
Calculated DPS0
-
Defense score0
+
Regen0