From 34e827f41a6d9761a00ab78d6722af9463e44099 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Mon, 22 Jun 2026 01:32:12 +0200 Subject: [PATCH] Normalize mixed damage lifesteal weighting --- app.js | 33 +++++++++++++++++++++------------ index.html | 2 +- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/app.js b/app.js index d99fc77..38330b0 100644 --- a/app.js +++ b/app.js @@ -694,24 +694,28 @@ function createEmptyEffectTotals() { return Object.fromEntries(EFFECTS.map((effect) => [effect.id, 0])); } -function calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget) { +function calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget, metricContext = {}) { const dps = calculateDps(candidateTotals, combatStyle); const lifestealRegen = calculateRegenDetails(candidateTotals, dps).lifesteal; if (optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN) return lifestealRegen; + if (optimizationTarget === OPTIMIZATION_TARGETS.DAMAGE_LIFESTEAL_MIX) { + const damageScore = metricContext.bestDamageDps > 0 ? dps / metricContext.bestDamageDps : 0; + const lifestealScore = metricContext.bestLifestealRegen > 0 ? lifestealRegen / metricContext.bestLifestealRegen : 0; + return (2 * damageScore) + lifestealScore; + } return dps; } function getOptimizationMetricLabel(optimizationTarget) { - if (optimizationTarget === OPTIMIZATION_TARGETS.DAMAGE_LIFESTEAL_MIX) return "Optimal DPS with 2:1 mix"; + if (optimizationTarget === OPTIMIZATION_TARGETS.DAMAGE_LIFESTEAL_MIX) return "Optimal weighted score"; return optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN ? "Optimal lifesteal regen" : "Optimal DPS"; } -function findOptimalDamageSetup(totals, effectCount, combatStyle, optimizationTarget) { +function findOptimalDamageSetup(totals, effectCount, combatStyle, optimizationTarget, metricContext = {}) { const effectIds = getDamageOptimizationEffectIds(combatStyle, optimizationTarget); const emptyTotals = { ...totals, effects: createEmptyEffectTotals() }; - const requiredLifestealCount = optimizationTarget === OPTIMIZATION_TARGETS.DAMAGE_LIFESTEAL_MIX ? Math.round(effectCount / 3) : null; let best = { - metric: requiredLifestealCount ? -Infinity : calculateOptimizationMetric(emptyTotals, combatStyle, optimizationTarget), + metric: calculateOptimizationMetric(emptyTotals, combatStyle, optimizationTarget, metricContext), counts: Object.fromEntries(effectIds.map((id) => [id, 0])), }; const counts = Object.fromEntries(effectIds.map((id) => [id, 0])); @@ -720,14 +724,10 @@ function findOptimalDamageSetup(totals, effectCount, combatStyle, optimizationTa const effectId = effectIds[effectIndex]; if (effectIndex === effectIds.length - 1) { counts[effectId] = remaining; - if (requiredLifestealCount !== null && counts.lifesteal !== requiredLifestealCount) { - counts[effectId] = 0; - return; - } const effects = createEmptyEffectTotals(); for (const id of effectIds) effects[id] = counts[id] * effectById(id).cap; const candidateTotals = { baseDamage: totals.baseDamage, baseHealth: totals.baseHealth, effects }; - const metric = calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget); + const metric = calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget, metricContext); if (metric > best.metric) best = { metric, counts: { ...counts } }; counts[effectId] = 0; return; @@ -763,8 +763,9 @@ function renderEffectComparisonRows(optimumCounts, currentSummary) { function renderOptimalDamageSetup(totals, currentDps) { const effectCount = countActiveEffects(); - const optimum = findOptimalDamageSetup(totals, effectCount, state.combatStyle, state.optimizationTarget); - const currentMetric = calculateOptimizationMetric(totals, state.combatStyle, state.optimizationTarget); + const metricContext = getOptimizationMetricContext(totals, effectCount, state.combatStyle, state.optimizationTarget); + const optimum = findOptimalDamageSetup(totals, effectCount, state.combatStyle, state.optimizationTarget, metricContext); + const currentMetric = calculateOptimizationMetric(totals, state.combatStyle, state.optimizationTarget, metricContext); const delta = percentChange(currentMetric, optimum.metric); const currentEffectSummary = summarizeCurrentEffects(); @@ -776,6 +777,14 @@ function renderOptimalDamageSetup(totals, currentDps) { renderEffectComparisonRows(effectCount ? optimum.counts : {}, currentEffectSummary); } +function getOptimizationMetricContext(totals, effectCount, combatStyle, optimizationTarget) { + if (optimizationTarget !== OPTIMIZATION_TARGETS.DAMAGE_LIFESTEAL_MIX || effectCount <= 0) return {}; + return { + bestDamageDps: findOptimalDamageSetup(totals, effectCount, combatStyle, OPTIMIZATION_TARGETS.DAMAGE).metric, + bestLifestealRegen: findOptimalDamageSetup(totals, effectCount, combatStyle, OPTIMIZATION_TARGETS.LIFESTEAL_REGEN).metric, + }; +} + function updateEffectImpactHints(totals, currentDps) { document.querySelectorAll(".gear-card").forEach((card, pieceIndex) => { const piece = state.pieces[pieceIndex]; diff --git a/index.html b/index.html index 0c31308..aaadd9b 100644 --- a/index.html +++ b/index.html @@ -101,7 +101,7 @@