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 @@
+Best max-roll damage effect mix using the same number of filled effect slots as your current build.
+