Replace profiles with regen-focused modes

This commit is contained in:
Jan Bader
2026-06-16 20:35:41 +02:00
parent e12a58ba1f
commit b8b1346f3f
3 changed files with 48 additions and 31 deletions
+15 -7
View File
@@ -70,7 +70,7 @@ The app lets you enter the current state of all equipment and then recommends:
3. **Overall build summary** 3. **Overall build summary**
- Totals base damage and health. - Totals base damage and health.
- Totals effect percentages across all items, mount, and pets. - Totals effect percentages across all items, mount, and pets.
- Shows calculated DPS and a defensive score to compare save slots. - Shows calculated DPS and estimated regen to compare save slots.
## DPS model ## DPS model
@@ -102,14 +102,22 @@ Defensive effect totals use an estimated survivability value based on health, bl
## Strategy profile ## Strategy profile
The first version includes an adjustable strategy profile: The app includes two adjustable strategy profiles:
- Balanced - Lifesteal: prioritizes DPS and lifesteal-based healing.
- Damage focused - Regen: prioritizes health and health-regeneration based healing.
- Survivability focused
- Skill focused
Each profile changes effect weights used for recommendations. These weights are intentionally transparent in the code so they can be tuned as better Forge Master formulas become known. Each profile changes effect weights used for sacrifice scoring and tie-breaking. DPS-based upgrade recommendations still use the current build math directly.
## Regen metric
The Regen card estimates healing from both sources:
```text
regen = max health * health regen% + DPS * lifesteal%
```
The displayed breakdown separates health regen from lifesteal healing.
## Save slots ## Save slots
+32 -23
View File
@@ -15,29 +15,17 @@ const EFFECTS = [
]; ];
const PROFILES = { const PROFILES = {
balanced: { lifesteal: {
label: "Balanced", label: "Lifesteal (DPS + Lifesteal)",
weights: { critChance: 8, critDamage: 4, blockChance: 7, healthRegen: 7, lifesteal: 7, doubleChance: 8, damage: 9, meleeDamage: 3, rangedDamage: 5, attackSpeed: 6, skillDamage: 5, skillCooldown: 8, health: 7 }, weights: { critChance: 10, critDamage: 6, blockChance: 2, healthRegen: 2, lifesteal: 10, doubleChance: 10, damage: 10, meleeDamage: 6, rangedDamage: 6, attackSpeed: 9, skillDamage: 7, skillCooldown: 7, health: 3 },
baseDamage: 1,
baseHealth: 0.75,
},
damage: {
label: "Damage focused",
weights: { critChance: 10, critDamage: 6, blockChance: 3, healthRegen: 3, lifesteal: 6, doubleChance: 10, damage: 10, meleeDamage: 5, rangedDamage: 7, attackSpeed: 8, skillDamage: 7, skillCooldown: 7, health: 3 },
baseDamage: 1.25, baseDamage: 1.25,
baseHealth: 0.45, baseHealth: 0.45,
}, },
survival: { regen: {
label: "Survivability focused", label: "Regen (Health + Regen)",
weights: { critChance: 5, critDamage: 2, blockChance: 10, healthRegen: 10, lifesteal: 9, doubleChance: 5, damage: 5, meleeDamage: 2, rangedDamage: 3, attackSpeed: 4, skillDamage: 3, skillCooldown: 5, health: 10 }, weights: { critChance: 4, critDamage: 2, blockChance: 7, healthRegen: 12, lifesteal: 5, doubleChance: 4, damage: 4, meleeDamage: 2, rangedDamage: 2, attackSpeed: 3, skillDamage: 3, skillCooldown: 3, health: 12 },
baseDamage: 0.65, baseDamage: 0.45,
baseHealth: 1.2, baseHealth: 1.35,
},
skill: {
label: "Skill focused",
weights: { critChance: 7, critDamage: 4, blockChance: 5, healthRegen: 5, lifesteal: 6, doubleChance: 7, damage: 7, meleeDamage: 2, rangedDamage: 4, attackSpeed: 5, skillDamage: 10, skillCooldown: 11, health: 5 },
baseDamage: 0.9,
baseHealth: 0.65,
}, },
}; };
@@ -69,7 +57,7 @@ let state = loadDraft() || createDefaultState();
function createDefaultState() { function createDefaultState() {
return { return {
profile: "balanced", profile: "lifesteal",
combatStyle: "melee", combatStyle: "melee",
recommendationCount: 8, recommendationCount: 8,
pieces: slots.map((slot, index) => createDefaultPiece(slot, index)), pieces: slots.map((slot, index) => createDefaultPiece(slot, index)),
@@ -108,10 +96,11 @@ function enforceFixedPieceFields(piece, index) {
function normalizeState(candidate) { function normalizeState(candidate) {
const fallback = createDefaultState(); const fallback = createDefaultState();
const profile = normalizeProfile(candidate?.profile);
return { return {
...fallback, ...fallback,
...candidate, ...candidate,
profile: PROFILES[candidate?.profile] ? candidate.profile : "balanced", profile,
combatStyle: ["melee", "ranged", "skill"].includes(candidate?.combatStyle) ? candidate.combatStyle : "melee", combatStyle: ["melee", "ranged", "skill"].includes(candidate?.combatStyle) ? candidate.combatStyle : "melee",
recommendationCount: Math.min(20, Math.max(3, Number(candidate?.recommendationCount || 8))), recommendationCount: Math.min(20, Math.max(3, Number(candidate?.recommendationCount || 8))),
pieces: fallback.pieces.map((piece, index) => enforceFixedPieceFields({ pieces: fallback.pieces.map((piece, index) => enforceFixedPieceFields({
@@ -131,6 +120,13 @@ function normalizeState(candidate) {
}; };
} }
function normalizeProfile(profile) {
if (PROFILES[profile]) return profile;
if (["damage", "skill", "balanced"].includes(profile)) return "lifesteal";
if (profile === "survival") return "regen";
return "lifesteal";
}
function clampEffectValue(type, value) { function clampEffectValue(type, value) {
if (!type) return 1; if (!type) return 1;
const effect = effectById(type); const effect = effectById(type);
@@ -223,6 +219,7 @@ function updateAnalysis() {
const totals = calculateTotals(); const totals = calculateTotals();
const damageDetails = calculateDamageDetails(totals, state.combatStyle); const damageDetails = calculateDamageDetails(totals, state.combatStyle);
const dps = damageDetails.dps; const dps = damageDetails.dps;
const regenDetails = calculateRegenDetails(totals, dps);
const survivability = calculateSurvivability(totals); const survivability = calculateSurvivability(totals);
const pieceScores = state.pieces.map((piece) => scorePiece(piece, profile, totals)); const pieceScores = state.pieces.map((piece) => scorePiece(piece, profile, totals));
@@ -230,7 +227,8 @@ function updateAnalysis() {
$("#totalHealth").textContent = fmt(totals.baseHealth); $("#totalHealth").textContent = fmt(totals.baseHealth);
$("#dpsValue").textContent = fmt(dps); $("#dpsValue").textContent = fmt(dps);
$("#hitDamageDetails").innerHTML = `Hit ${fmt(damageDetails.singleHit)}<br>Crit ${fmt(damageDetails.critHit)}`; $("#hitDamageDetails").innerHTML = `Hit ${fmt(damageDetails.singleHit)}<br>Crit ${fmt(damageDetails.critHit)}`;
$("#defenseScore").textContent = fmt(calculateGroupScore(totals, profile, ["defense", "hybrid"], "health")); $("#regenValue").textContent = fmt(regenDetails.total);
$("#regenDetails").innerHTML = `Health regen ${fmt(regenDetails.healthRegen)}<br>Lifesteal ${fmt(regenDetails.lifesteal)}`;
document.querySelectorAll(".gear-card").forEach((card, index) => { document.querySelectorAll(".gear-card").forEach((card, index) => {
card.querySelector(".piece-score").textContent = `Score ${fmt(pieceScores[index].score)}`; card.querySelector(".piece-score").textContent = `Score ${fmt(pieceScores[index].score)}`;
@@ -289,6 +287,17 @@ function calculateSurvivability(totals) {
return health * blockMultiplier * regenMultiplier * lifestealMultiplier; return health * blockMultiplier * regenMultiplier * lifestealMultiplier;
} }
function calculateRegenDetails(totals, dps) {
const maxHealth = Math.max(0, totals.baseHealth) * (1 + totals.effects.health / 100);
const healthRegen = maxHealth * (totals.effects.healthRegen / 100);
const lifesteal = Math.max(0, dps) * (totals.effects.lifesteal / 100);
return {
healthRegen,
lifesteal,
total: healthRegen + lifesteal,
};
}
function getStyleDamageBonus(effects, combatStyle) { function getStyleDamageBonus(effects, combatStyle) {
if (combatStyle === "ranged") return effects.rangedDamage; if (combatStyle === "ranged") return effects.rangedDamage;
if (combatStyle === "skill") return effects.skillDamage; if (combatStyle === "skill") return effects.skillDamage;
+1 -1
View File
@@ -74,7 +74,7 @@
<article class="metric"><span>Total damage base</span><strong id="totalDamage">0</strong></article> <article class="metric"><span>Total damage base</span><strong id="totalDamage">0</strong></article>
<article class="metric"><span>Total health base</span><strong id="totalHealth">0</strong></article> <article class="metric"><span>Total health base</span><strong id="totalHealth">0</strong></article>
<article class="metric"><span>Calculated DPS</span><strong id="dpsValue">0</strong><small id="hitDamageDetails"></small></article> <article class="metric"><span>Calculated DPS</span><strong id="dpsValue">0</strong><small id="hitDamageDetails"></small></article>
<article class="metric"><span>Defense score</span><strong id="defenseScore">0</strong></article> <article class="metric"><span>Regen</span><strong id="regenValue">0</strong><small id="regenDetails"></small></article>
</section> </section>
<section class="results"> <section class="results">