Add optimization target toggle
This commit is contained in:
@@ -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 COMBAT_STYLES = Object.freeze({ MELEE: "melee", RANGED: "ranged", SKILL: "skill" });
|
||||||
const PIECE_TYPES = Object.freeze({ ITEM: "item", MOUNT: "mount", PET: "pet", 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 REPLACEMENT_TARGETS = Object.freeze({ MOUNT: "mount", PET: "pet" });
|
||||||
|
const OPTIMIZATION_TARGETS = Object.freeze({ DAMAGE: "damage", LIFESTEAL_REGEN: "lifestealRegen" });
|
||||||
|
|
||||||
const PROFILES = {
|
const PROFILES = {
|
||||||
[PROFILE_IDS.LIFESTEAL]: {
|
[PROFILE_IDS.LIFESTEAL]: {
|
||||||
@@ -73,6 +74,7 @@ function createDefaultState() {
|
|||||||
return {
|
return {
|
||||||
profile: PROFILE_IDS.LIFESTEAL,
|
profile: PROFILE_IDS.LIFESTEAL,
|
||||||
combatStyle: COMBAT_STYLES.MELEE,
|
combatStyle: COMBAT_STYLES.MELEE,
|
||||||
|
optimizationTarget: OPTIMIZATION_TARGETS.DAMAGE,
|
||||||
recommendationCount: 8,
|
recommendationCount: 8,
|
||||||
pieces: slots.map((slot, index) => createDefaultPiece(slot, index)),
|
pieces: slots.map((slot, index) => createDefaultPiece(slot, index)),
|
||||||
newItem: createDefaultNewItem(),
|
newItem: createDefaultNewItem(),
|
||||||
@@ -128,6 +130,7 @@ function normalizeState(candidate) {
|
|||||||
...candidate,
|
...candidate,
|
||||||
profile,
|
profile,
|
||||||
combatStyle: Object.values(COMBAT_STYLES).includes(candidate?.combatStyle) ? candidate.combatStyle : COMBAT_STYLES.MELEE,
|
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))),
|
recommendationCount: Math.min(20, Math.max(3, Number(candidate?.recommendationCount || 8))),
|
||||||
newItem: normalizeNewItem(candidate?.newItem),
|
newItem: normalizeNewItem(candidate?.newItem),
|
||||||
pieces: fallback.pieces.map((piece, index) => normalizePiece(piece, candidate?.pieces?.[index], index)),
|
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").innerHTML = Object.entries(PROFILES).map(([id, profile]) => `<option value="${id}">${profile.label}</option>`).join("");
|
||||||
$("#profileSelect").value = state.profile;
|
$("#profileSelect").value = state.profile;
|
||||||
$("#combatStyle").value = state.combatStyle;
|
$("#combatStyle").value = state.combatStyle;
|
||||||
|
$("#optimizationTarget").value = state.optimizationTarget;
|
||||||
$("#recommendationCount").value = state.recommendationCount;
|
$("#recommendationCount").value = state.recommendationCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,8 +665,9 @@ function renderEffectTotals(totals, currentDps, currentSurvivability) {
|
|||||||
`).join("");
|
`).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDamageOptimizationEffectIds(combatStyle) {
|
function getDamageOptimizationEffectIds(combatStyle, optimizationTarget = OPTIMIZATION_TARGETS.DAMAGE) {
|
||||||
const ids = ["critChance", "critDamage", "doubleChance", "damage", "attackSpeed"];
|
const ids = ["critChance", "critDamage", "doubleChance", "damage", "attackSpeed"];
|
||||||
|
if (optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN) ids.push("lifesteal");
|
||||||
if (combatStyle === COMBAT_STYLES.RANGED) ids.push("rangedDamage");
|
if (combatStyle === COMBAT_STYLES.RANGED) ids.push("rangedDamage");
|
||||||
else if (combatStyle === COMBAT_STYLES.SKILL) ids.push("skillDamage", "skillCooldown");
|
else if (combatStyle === COMBAT_STYLES.SKILL) ids.push("skillDamage", "skillCooldown");
|
||||||
else ids.push("meleeDamage");
|
else ids.push("meleeDamage");
|
||||||
@@ -689,9 +694,24 @@ function createEmptyEffectTotals() {
|
|||||||
return Object.fromEntries(EFFECTS.map((effect) => [effect.id, 0]));
|
return Object.fromEntries(EFFECTS.map((effect) => [effect.id, 0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
function findOptimalDamageSetup(totals, effectCount, combatStyle) {
|
function calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget) {
|
||||||
const effectIds = getDamageOptimizationEffectIds(combatStyle);
|
const dps = calculateDps(candidateTotals, combatStyle);
|
||||||
let best = { dps: calculateDps({ ...totals, effects: createEmptyEffectTotals() }, combatStyle), counts: Object.fromEntries(effectIds.map((id) => [id, 0])) };
|
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]));
|
const counts = Object.fromEntries(effectIds.map((id) => [id, 0]));
|
||||||
|
|
||||||
function visit(effectIndex, remaining) {
|
function visit(effectIndex, remaining) {
|
||||||
@@ -700,8 +720,9 @@ function findOptimalDamageSetup(totals, effectCount, combatStyle) {
|
|||||||
counts[effectId] = remaining;
|
counts[effectId] = remaining;
|
||||||
const effects = createEmptyEffectTotals();
|
const effects = createEmptyEffectTotals();
|
||||||
for (const id of effectIds) effects[id] = counts[id] * effectById(id).cap;
|
for (const id of effectIds) effects[id] = counts[id] * effectById(id).cap;
|
||||||
const dps = calculateDps({ baseDamage: totals.baseDamage, baseHealth: totals.baseHealth, effects }, combatStyle);
|
const candidateTotals = { baseDamage: totals.baseDamage, baseHealth: totals.baseHealth, effects };
|
||||||
if (dps > best.dps) best = { dps, counts: { ...counts } };
|
const metric = calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget);
|
||||||
|
if (metric > best.metric) best = { metric, counts: { ...counts } };
|
||||||
counts[effectId] = 0;
|
counts[effectId] = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -736,12 +757,16 @@ function renderEffectComparisonRows(optimumCounts, currentSummary) {
|
|||||||
|
|
||||||
function renderOptimalDamageSetup(totals, currentDps) {
|
function renderOptimalDamageSetup(totals, currentDps) {
|
||||||
const effectCount = countActiveEffects();
|
const effectCount = countActiveEffects();
|
||||||
const optimum = findOptimalDamageSetup(totals, effectCount, state.combatStyle);
|
const optimum = findOptimalDamageSetup(totals, effectCount, state.combatStyle, state.optimizationTarget);
|
||||||
const delta = percentChange(currentDps, optimum.dps);
|
const currentMetric = state.optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN
|
||||||
|
? calculateRegenDetails(totals, currentDps).lifesteal
|
||||||
|
: currentDps;
|
||||||
|
const delta = percentChange(currentMetric, optimum.metric);
|
||||||
const currentEffectSummary = summarizeCurrentEffects();
|
const currentEffectSummary = summarizeCurrentEffects();
|
||||||
|
|
||||||
$("#optimumEffectCount").textContent = `${effectCount} effect${effectCount === 1 ? "" : "s"}`;
|
$("#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").textContent = formatDelta(delta, "%");
|
||||||
$("#optimumDpsDelta").className = deltaClass(delta);
|
$("#optimumDpsDelta").className = deltaClass(delta);
|
||||||
renderEffectComparisonRows(effectCount ? optimum.counts : {}, currentEffectSummary);
|
renderEffectComparisonRows(effectCount ? optimum.counts : {}, currentEffectSummary);
|
||||||
@@ -927,6 +952,10 @@ function bindEvents() {
|
|||||||
state.combatStyle = event.target.value;
|
state.combatStyle = event.target.value;
|
||||||
updateAnalysis();
|
updateAnalysis();
|
||||||
});
|
});
|
||||||
|
$("#optimizationTarget").addEventListener("change", (event) => {
|
||||||
|
state.optimizationTarget = event.target.value;
|
||||||
|
updateAnalysis();
|
||||||
|
});
|
||||||
$("#recommendationCount").addEventListener("input", (event) => {
|
$("#recommendationCount").addEventListener("input", (event) => {
|
||||||
state.recommendationCount = Math.min(20, Math.max(3, Number(event.target.value || 8)));
|
state.recommendationCount = Math.min(20, Math.max(3, Number(event.target.value || 8)));
|
||||||
updateAnalysis();
|
updateAnalysis();
|
||||||
|
|||||||
+12
-3
@@ -91,12 +91,21 @@
|
|||||||
|
|
||||||
<section class="card optimum-card" aria-labelledby="optimum-heading">
|
<section class="card optimum-card" aria-labelledby="optimum-heading">
|
||||||
<div class="gear-title-row">
|
<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>
|
<span id="optimumEffectCount" class="piece-score"></span>
|
||||||
</div>
|
</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 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><span>Difference vs current</span><strong id="optimumDpsDelta">0</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="effect-comparison-wrap">
|
<div class="effect-comparison-wrap">
|
||||||
|
|||||||
+12
@@ -261,6 +261,18 @@ h3 { margin-bottom: 0; }
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
margin: 1rem 0;
|
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 {
|
.optimum-results {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user