Add maximum damage effect optimizer
This commit is contained in:
@@ -341,6 +341,7 @@ function updateAnalysis() {
|
|||||||
updateEffectImpactHints(totals, dps);
|
updateEffectImpactHints(totals, dps);
|
||||||
|
|
||||||
renderEffectTotals(totals, dps, survivability);
|
renderEffectTotals(totals, dps, survivability);
|
||||||
|
renderOptimalDamageSetup(totals, dps);
|
||||||
renderSacrifices(pieceScores);
|
renderSacrifices(pieceScores);
|
||||||
renderReplacementComparison(totals, dps, regenDetails.total, pieceScores);
|
renderReplacementComparison(totals, dps, regenDetails.total, pieceScores);
|
||||||
saveDraft();
|
saveDraft();
|
||||||
@@ -660,6 +661,74 @@ function renderEffectTotals(totals, currentDps, currentSurvivability) {
|
|||||||
`).join("");
|
`).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) {
|
function updateEffectImpactHints(totals, currentDps) {
|
||||||
document.querySelectorAll(".gear-card").forEach((card, pieceIndex) => {
|
document.querySelectorAll(".gear-card").forEach((card, pieceIndex) => {
|
||||||
const piece = state.pieces[pieceIndex];
|
const piece = state.pieces[pieceIndex];
|
||||||
|
|||||||
+13
@@ -89,6 +89,19 @@
|
|||||||
<div id="effectTotals" class="effect-totals"></div>
|
<div id="effectTotals" class="effect-totals"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="card optimum-card" aria-labelledby="optimum-heading">
|
||||||
|
<div class="gear-title-row">
|
||||||
|
<h2 id="optimum-heading">Maximum damage setup</h2>
|
||||||
|
<span id="optimumEffectCount" class="piece-score"></span>
|
||||||
|
</div>
|
||||||
|
<p class="muted-copy">Best max-roll damage effect mix using the same number of filled effect slots as your current build.</p>
|
||||||
|
<div class="optimum-results">
|
||||||
|
<div><span>Optimal DPS</span><strong id="optimumDps">0</strong></div>
|
||||||
|
<div><span>Difference vs current</span><strong id="optimumDpsDelta">0</strong></div>
|
||||||
|
</div>
|
||||||
|
<p id="optimumSuggestion" class="optimum-suggestion"></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card new-item-card" aria-labelledby="new-item-heading">
|
<section class="card new-item-card" aria-labelledby="new-item-heading">
|
||||||
<div class="gear-title-row">
|
<div class="gear-title-row">
|
||||||
<h2 id="new-item-heading">New item comparison</h2>
|
<h2 id="new-item-heading">New item comparison</h2>
|
||||||
|
|||||||
+30
-1
@@ -180,6 +180,7 @@ h3 { margin-bottom: 0; }
|
|||||||
.recommendation-list li { padding-left: 0.25rem; }
|
.recommendation-list li { padding-left: 0.25rem; }
|
||||||
.recommendation-list strong { color: var(--accent); }
|
.recommendation-list strong { color: var(--accent); }
|
||||||
.recommendation-list small { display: block; color: var(--muted); margin-top: 0.2rem; }
|
.recommendation-list small { display: block; color: var(--muted); margin-top: 0.2rem; }
|
||||||
|
.muted-copy { color: var(--muted); margin-bottom: 0.85rem; }
|
||||||
|
|
||||||
.effect-totals {
|
.effect-totals {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -255,6 +256,34 @@ h3 { margin-bottom: 0; }
|
|||||||
.delta-positive { color: #86efac; }
|
.delta-positive { color: #86efac; }
|
||||||
.delta-neutral { color: var(--muted); }
|
.delta-neutral { color: var(--muted); }
|
||||||
.delta-negative { color: #f87171; }
|
.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 {
|
.swap-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -270,6 +299,6 @@ h3 { margin-bottom: 0; }
|
|||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
main, .hero { width: min(100% - 1rem, 1440px); }
|
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; }
|
.save-grid { grid-template-columns: 1fr; }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user