Add optimization target toggle

This commit is contained in:
Jan Bader
2026-06-22 01:25:21 +02:00
parent 630d6d6fd6
commit 4b1b3a7379
3 changed files with 62 additions and 12 deletions
+38 -9
View File
@@ -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]) => `<option value="${id}">${profile.label}</option>`).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();
+12 -3
View File
@@ -91,12 +91,21 @@
<section class="card optimum-card" aria-labelledby="optimum-heading">
<div class="gear-title-row">
<h2 id="optimum-heading">Maximum damage setup</h2>
<h2 id="optimum-heading">Optimized effect 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-header-controls">
<p class="muted-copy">Best max-roll effect mix using the same number of filled effect slots as your current build.</p>
<label>
Optimize for
<select id="optimizationTarget">
<option value="damage">Raw damage</option>
<option value="lifestealRegen">Lifesteal regen</option>
</select>
</label>
</div>
<div class="optimum-results">
<div><span>Optimal DPS</span><strong id="optimumDps">0</strong></div>
<div><span id="optimumMetricLabel">Optimal DPS</span><strong id="optimumDps">0</strong></div>
<div><span>Difference vs current</span><strong id="optimumDpsDelta">0</strong></div>
</div>
<div class="effect-comparison-wrap">
+12
View File
@@ -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));