Normalize mixed damage lifesteal weighting

This commit is contained in:
Jan Bader
2026-06-22 01:32:12 +02:00
parent 135e19b798
commit 34e827f41a
2 changed files with 22 additions and 13 deletions
+21 -12
View File
@@ -694,24 +694,28 @@ function createEmptyEffectTotals() {
return Object.fromEntries(EFFECTS.map((effect) => [effect.id, 0])); return Object.fromEntries(EFFECTS.map((effect) => [effect.id, 0]));
} }
function calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget) { function calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget, metricContext = {}) {
const dps = calculateDps(candidateTotals, combatStyle); const dps = calculateDps(candidateTotals, combatStyle);
const lifestealRegen = calculateRegenDetails(candidateTotals, dps).lifesteal; const lifestealRegen = calculateRegenDetails(candidateTotals, dps).lifesteal;
if (optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN) return lifestealRegen; if (optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN) return lifestealRegen;
if (optimizationTarget === OPTIMIZATION_TARGETS.DAMAGE_LIFESTEAL_MIX) {
const damageScore = metricContext.bestDamageDps > 0 ? dps / metricContext.bestDamageDps : 0;
const lifestealScore = metricContext.bestLifestealRegen > 0 ? lifestealRegen / metricContext.bestLifestealRegen : 0;
return (2 * damageScore) + lifestealScore;
}
return dps; return dps;
} }
function getOptimizationMetricLabel(optimizationTarget) { function getOptimizationMetricLabel(optimizationTarget) {
if (optimizationTarget === OPTIMIZATION_TARGETS.DAMAGE_LIFESTEAL_MIX) return "Optimal DPS with 2:1 mix"; if (optimizationTarget === OPTIMIZATION_TARGETS.DAMAGE_LIFESTEAL_MIX) return "Optimal weighted score";
return optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN ? "Optimal lifesteal regen" : "Optimal DPS"; return optimizationTarget === OPTIMIZATION_TARGETS.LIFESTEAL_REGEN ? "Optimal lifesteal regen" : "Optimal DPS";
} }
function findOptimalDamageSetup(totals, effectCount, combatStyle, optimizationTarget) { function findOptimalDamageSetup(totals, effectCount, combatStyle, optimizationTarget, metricContext = {}) {
const effectIds = getDamageOptimizationEffectIds(combatStyle, optimizationTarget); const effectIds = getDamageOptimizationEffectIds(combatStyle, optimizationTarget);
const emptyTotals = { ...totals, effects: createEmptyEffectTotals() }; const emptyTotals = { ...totals, effects: createEmptyEffectTotals() };
const requiredLifestealCount = optimizationTarget === OPTIMIZATION_TARGETS.DAMAGE_LIFESTEAL_MIX ? Math.round(effectCount / 3) : null;
let best = { let best = {
metric: requiredLifestealCount ? -Infinity : calculateOptimizationMetric(emptyTotals, combatStyle, optimizationTarget), metric: calculateOptimizationMetric(emptyTotals, combatStyle, optimizationTarget, metricContext),
counts: Object.fromEntries(effectIds.map((id) => [id, 0])), 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]));
@@ -720,14 +724,10 @@ function findOptimalDamageSetup(totals, effectCount, combatStyle, optimizationTa
const effectId = effectIds[effectIndex]; const effectId = effectIds[effectIndex];
if (effectIndex === effectIds.length - 1) { if (effectIndex === effectIds.length - 1) {
counts[effectId] = remaining; counts[effectId] = remaining;
if (requiredLifestealCount !== null && counts.lifesteal !== requiredLifestealCount) {
counts[effectId] = 0;
return;
}
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 candidateTotals = { baseDamage: totals.baseDamage, baseHealth: totals.baseHealth, effects }; const candidateTotals = { baseDamage: totals.baseDamage, baseHealth: totals.baseHealth, effects };
const metric = calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget); const metric = calculateOptimizationMetric(candidateTotals, combatStyle, optimizationTarget, metricContext);
if (metric > best.metric) best = { metric, counts: { ...counts } }; if (metric > best.metric) best = { metric, counts: { ...counts } };
counts[effectId] = 0; counts[effectId] = 0;
return; return;
@@ -763,8 +763,9 @@ 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, state.optimizationTarget); const metricContext = getOptimizationMetricContext(totals, effectCount, state.combatStyle, state.optimizationTarget);
const currentMetric = calculateOptimizationMetric(totals, state.combatStyle, state.optimizationTarget); const optimum = findOptimalDamageSetup(totals, effectCount, state.combatStyle, state.optimizationTarget, metricContext);
const currentMetric = calculateOptimizationMetric(totals, state.combatStyle, state.optimizationTarget, metricContext);
const delta = percentChange(currentMetric, optimum.metric); const delta = percentChange(currentMetric, optimum.metric);
const currentEffectSummary = summarizeCurrentEffects(); const currentEffectSummary = summarizeCurrentEffects();
@@ -776,6 +777,14 @@ function renderOptimalDamageSetup(totals, currentDps) {
renderEffectComparisonRows(effectCount ? optimum.counts : {}, currentEffectSummary); renderEffectComparisonRows(effectCount ? optimum.counts : {}, currentEffectSummary);
} }
function getOptimizationMetricContext(totals, effectCount, combatStyle, optimizationTarget) {
if (optimizationTarget !== OPTIMIZATION_TARGETS.DAMAGE_LIFESTEAL_MIX || effectCount <= 0) return {};
return {
bestDamageDps: findOptimalDamageSetup(totals, effectCount, combatStyle, OPTIMIZATION_TARGETS.DAMAGE).metric,
bestLifestealRegen: findOptimalDamageSetup(totals, effectCount, combatStyle, OPTIMIZATION_TARGETS.LIFESTEAL_REGEN).metric,
};
}
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];
+1 -1
View File
@@ -101,7 +101,7 @@
<select id="optimizationTarget"> <select id="optimizationTarget">
<option value="damage">Raw damage</option> <option value="damage">Raw damage</option>
<option value="lifestealRegen">Lifesteal regen</option> <option value="lifestealRegen">Lifesteal regen</option>
<option value="damageLifestealMix">2x damage + 1x lifesteal</option> <option value="damageLifestealMix">2:1 weighted damage + lifesteal</option>
</select> </select>
</label> </label>
</div> </div>