From 196e6e363023983fcd124c9ea4cdcdef0c0e57c4 Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Sat, 20 Jun 2026 23:51:25 +0200 Subject: [PATCH] Add maximum damage effect optimizer --- app.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.html | 13 ++++++++++ styles.css | 31 +++++++++++++++++++++++- 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index e60e378..438977c 100644 --- a/app.js +++ b/app.js @@ -341,6 +341,7 @@ function updateAnalysis() { updateEffectImpactHints(totals, dps); renderEffectTotals(totals, dps, survivability); + renderOptimalDamageSetup(totals, dps); renderSacrifices(pieceScores); renderReplacementComparison(totals, dps, regenDetails.total, pieceScores); saveDraft(); @@ -660,6 +661,74 @@ function renderEffectTotals(totals, currentDps, currentSurvivability) { `).join(""); } +function getDamageOptimizationEffectIds(combatStyle) { + const ids = ["critChance", "critDamage", "doubleChance", "damage", "attackSpeed"]; + if (combatStyle === COMBAT_STYLES.RANGED) ids.push("rangedDamage"); + else if (combatStyle === COMBAT_STYLES.SKILL) ids.push("skillDamage", "skillCooldown"); + else ids.push("meleeDamage"); + return ids; +} + +function countActiveEffects() { + return state.pieces.reduce((count, piece) => count + (piece.effects || []).filter((effect) => effect.type).length, 0); +} + +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])) }; + const counts = Object.fromEntries(effectIds.map((id) => [id, 0])); + + function visit(effectIndex, remaining) { + const effectId = effectIds[effectIndex]; + if (effectIndex === effectIds.length - 1) { + 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 } }; + counts[effectId] = 0; + return; + } + + for (let count = 0; count <= remaining; count += 1) { + counts[effectId] = count; + visit(effectIndex + 1, remaining - count); + } + counts[effectId] = 0; + } + + visit(0, effectCount); + return best; +} + +function formatOptimalSuggestion(counts) { + return Object.entries(counts) + .filter(([, count]) => count > 0) + .map(([effectId, count]) => { + const effect = effectById(effectId); + return `${count}x ${effect.cap}% ${effect.label.toLowerCase()}`; + }) + .join(", "); +} + +function renderOptimalDamageSetup(totals, currentDps) { + const effectCount = countActiveEffects(); + const optimum = findOptimalDamageSetup(totals, effectCount, state.combatStyle); + const delta = percentChange(currentDps, optimum.dps); + + $("#optimumEffectCount").textContent = `${effectCount} effect${effectCount === 1 ? "" : "s"}`; + $("#optimumDps").textContent = fmt(optimum.dps); + $("#optimumDpsDelta").textContent = formatDelta(delta, "%"); + $("#optimumDpsDelta").className = deltaClass(delta); + $("#optimumSuggestion").textContent = effectCount + ? formatOptimalSuggestion(optimum.counts) + : "Add effects to your current build to see an optimized max-roll damage mix."; +} + 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 1c1fadb..49a1b25 100644 --- a/index.html +++ b/index.html @@ -89,6 +89,19 @@
+
+
+

Maximum damage setup

+ +
+

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

+
+
Optimal DPS0
+
Difference vs current0
+
+

+
+

New item comparison

diff --git a/styles.css b/styles.css index 4bcd6cc..3617017 100644 --- a/styles.css +++ b/styles.css @@ -180,6 +180,7 @@ h3 { margin-bottom: 0; } .recommendation-list li { padding-left: 0.25rem; } .recommendation-list strong { color: var(--accent); } .recommendation-list small { display: block; color: var(--muted); margin-top: 0.2rem; } +.muted-copy { color: var(--muted); margin-bottom: 0.85rem; } .effect-totals { display: grid; @@ -255,6 +256,34 @@ h3 { margin-bottom: 0; } .delta-positive { color: #86efac; } .delta-neutral { color: var(--muted); } .delta-negative { color: #f87171; } +.optimum-card { + display: grid; + gap: 0.75rem; + margin: 1rem 0; +} +.optimum-results { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.75rem; +} +.optimum-results > div { + border: 1px solid var(--border); + border-radius: 0.9rem; + background: rgba(255, 255, 255, 0.05); + padding: 0.75rem; +} +.optimum-results span { + display: block; + color: var(--muted); + margin-bottom: 0.25rem; +} +.optimum-results strong { font-size: 1.35rem; } +.optimum-suggestion { + margin: 0; + color: var(--accent); + font-weight: 800; + overflow-wrap: anywhere; +} .swap-actions { display: flex; flex-wrap: wrap; @@ -270,6 +299,6 @@ h3 { margin-bottom: 0; } @media (max-width: 560px) { main, .hero { width: min(100% - 1rem, 1440px); } - .summary-grid, .inline-fields, .effect-row { grid-template-columns: 1fr; } + .summary-grid, .inline-fields, .effect-row, .optimum-results { grid-template-columns: 1fr; } .save-grid { grid-template-columns: 1fr; } }