From 4b1b3a7379e33ba7cb425ffbe4d462a250e5651b Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Mon, 22 Jun 2026 01:25:21 +0200 Subject: [PATCH] Add optimization target toggle --- app.js | 47 ++++++++++++++++++++++++++++++++++++++--------- index.html | 15 ++++++++++++--- styles.css | 12 ++++++++++++ 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/app.js b/app.js index 67b21d7..b3d95d2 100644 --- a/app.js +++ b/app.js @@ -22,6 +22,7 @@ const PROFILE_IDS = Object.freeze({ LIFESTEAL: "lifesteal", REGEN: "regen" }); const COMBAT_STYLES = Object.freeze({ MELEE: "melee", RANGED: "ranged", SKILL: "skill" }); const PIECE_TYPES = Object.freeze({ ITEM: "item", MOUNT: "mount", PET: "pet", SKILL: "skill" }); const REPLACEMENT_TARGETS = Object.freeze({ MOUNT: "mount", PET: "pet" }); +const OPTIMIZATION_TARGETS = Object.freeze({ DAMAGE: "damage", LIFESTEAL_REGEN: "lifestealRegen" }); const PROFILES = { [PROFILE_IDS.LIFESTEAL]: { @@ -73,6 +74,7 @@ function createDefaultState() { return { profile: PROFILE_IDS.LIFESTEAL, combatStyle: COMBAT_STYLES.MELEE, + optimizationTarget: OPTIMIZATION_TARGETS.DAMAGE, recommendationCount: 8, pieces: slots.map((slot, index) => createDefaultPiece(slot, index)), newItem: createDefaultNewItem(), @@ -128,6 +130,7 @@ function normalizeState(candidate) { ...candidate, profile, combatStyle: Object.values(COMBAT_STYLES).includes(candidate?.combatStyle) ? candidate.combatStyle : COMBAT_STYLES.MELEE, + optimizationTarget: Object.values(OPTIMIZATION_TARGETS).includes(candidate?.optimizationTarget) ? candidate.optimizationTarget : OPTIMIZATION_TARGETS.DAMAGE, recommendationCount: Math.min(20, Math.max(3, Number(candidate?.recommendationCount || 8))), newItem: normalizeNewItem(candidate?.newItem), pieces: fallback.pieces.map((piece, index) => normalizePiece(piece, candidate?.pieces?.[index], index)), @@ -208,6 +211,7 @@ function renderControls() { $("#profileSelect").innerHTML = Object.entries(PROFILES).map(([id, profile]) => ``).join(""); $("#profileSelect").value = state.profile; $("#combatStyle").value = state.combatStyle; + $("#optimizationTarget").value = state.optimizationTarget; $("#recommendationCount").value = state.recommendationCount; } @@ -661,8 +665,9 @@ function renderEffectTotals(totals, currentDps, currentSurvivability) { `).join(""); } -function getDamageOptimizationEffectIds(combatStyle) { +function getDamageOptimizationEffectIds(combatStyle, optimizationTarget = OPTIMIZATION_TARGETS.DAMAGE) { const ids = ["critChance", "critDamage", "doubleChance", "damage", "attackSpeed"]; + if (optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN) ids.push("lifesteal"); if (combatStyle === COMBAT_STYLES.RANGED) ids.push("rangedDamage"); else if (combatStyle === COMBAT_STYLES.SKILL) ids.push("skillDamage", "skillCooldown"); else ids.push("meleeDamage"); @@ -689,9 +694,24 @@ function createEmptyEffectTotals() { return Object.fromEntries(EFFECTS.map((effect) => [effect.id, 0])); } -function findOptimalDamageSetup(totals, effectCount, combatStyle) { - const effectIds = getDamageOptimizationEffectIds(combatStyle); - let best = { dps: calculateDps({ ...totals, effects: createEmptyEffectTotals() }, combatStyle), counts: Object.fromEntries(effectIds.map((id) => [id, 0])) }; +function calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget) { + const dps = calculateDps(candidateTotals, combatStyle); + return optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN + ? calculateRegenDetails(candidateTotals, dps).lifesteal + : dps; +} + +function getOptimizationMetricLabel(optimizationTarget) { + return optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN ? "Optimal lifesteal regen" : "Optimal DPS"; +} + +function findOptimalDamageSetup(totals, effectCount, combatStyle, optimizationTarget) { + const effectIds = getDamageOptimizationEffectIds(combatStyle, optimizationTarget); + const emptyTotals = { ...totals, effects: createEmptyEffectTotals() }; + let best = { + metric: calculateOptimizationMetric(emptyTotals, combatStyle, optimizationTarget), + counts: Object.fromEntries(effectIds.map((id) => [id, 0])), + }; const counts = Object.fromEntries(effectIds.map((id) => [id, 0])); function visit(effectIndex, remaining) { @@ -700,8 +720,9 @@ function findOptimalDamageSetup(totals, effectCount, combatStyle) { counts[effectId] = remaining; const effects = createEmptyEffectTotals(); for (const id of effectIds) effects[id] = counts[id] * effectById(id).cap; - const dps = calculateDps({ baseDamage: totals.baseDamage, baseHealth: totals.baseHealth, effects }, combatStyle); - if (dps > best.dps) best = { dps, counts: { ...counts } }; + const candidateTotals = { baseDamage: totals.baseDamage, baseHealth: totals.baseHealth, effects }; + const metric = calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget); + if (metric > best.metric) best = { metric, counts: { ...counts } }; counts[effectId] = 0; return; } @@ -736,12 +757,16 @@ function renderEffectComparisonRows(optimumCounts, currentSummary) { function renderOptimalDamageSetup(totals, currentDps) { const effectCount = countActiveEffects(); - const optimum = findOptimalDamageSetup(totals, effectCount, state.combatStyle); - const delta = percentChange(currentDps, optimum.dps); + const optimum = findOptimalDamageSetup(totals, effectCount, state.combatStyle, state.optimizationTarget); + const currentMetric = state.optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN + ? calculateRegenDetails(totals, currentDps).lifesteal + : currentDps; + const delta = percentChange(currentMetric, optimum.metric); const currentEffectSummary = summarizeCurrentEffects(); $("#optimumEffectCount").textContent = `${effectCount} effect${effectCount === 1 ? "" : "s"}`; - $("#optimumDps").textContent = fmt(optimum.dps); + $("#optimumMetricLabel").textContent = getOptimizationMetricLabel(state.optimizationTarget); + $("#optimumDps").textContent = fmt(optimum.metric); $("#optimumDpsDelta").textContent = formatDelta(delta, "%"); $("#optimumDpsDelta").className = deltaClass(delta); renderEffectComparisonRows(effectCount ? optimum.counts : {}, currentEffectSummary); @@ -927,6 +952,10 @@ function bindEvents() { state.combatStyle = event.target.value; updateAnalysis(); }); + $("#optimizationTarget").addEventListener("change", (event) => { + state.optimizationTarget = event.target.value; + updateAnalysis(); + }); $("#recommendationCount").addEventListener("input", (event) => { state.recommendationCount = Math.min(20, Math.max(3, Number(event.target.value || 8))); updateAnalysis(); diff --git a/index.html b/index.html index a938217..1e28468 100644 --- a/index.html +++ b/index.html @@ -91,12 +91,21 @@
-

Maximum damage setup

+

Optimized effect setup

-

Best max-roll damage effect mix using the same number of filled effect slots as your current build.

+
+

Best max-roll effect mix using the same number of filled effect slots as your current build.

+ +
-
Optimal DPS0
+
Optimal DPS0
Difference vs current0
diff --git a/styles.css b/styles.css index 482d102..ce9e0cb 100644 --- a/styles.css +++ b/styles.css @@ -261,6 +261,18 @@ h3 { margin-bottom: 0; } gap: 0.75rem; margin: 1rem 0; } +.optimum-header-controls { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: end; + justify-content: space-between; +} +.optimum-header-controls .muted-copy { + flex: 1 1 28rem; + margin-bottom: 0; +} +.optimum-header-controls label { min-width: 220px; } .optimum-results { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));