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**
- Totals base damage and health.
- 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
@@ -102,14 +102,22 @@ Defensive effect totals use an estimated survivability value based on health, bl
## Strategy profile
The first version includes an adjustable strategy profile:
The app includes two adjustable strategy profiles:
- Balanced
- Damage focused
- Survivability focused
- Skill focused
- Lifesteal: prioritizes DPS and lifesteal-based healing.
- Regen: prioritizes health and health-regeneration based healing.
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
+32 -23
View File
@@ -15,29 +15,17 @@ const EFFECTS = [
];
const PROFILES = {
balanced: {
label: "Balanced",
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 },
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 },
lifesteal: {
label: "Lifesteal (DPS + Lifesteal)",
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.25,
baseHealth: 0.45,
},
survival: {
label: "Survivability focused",
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 },
baseDamage: 0.65,
baseHealth: 1.2,
},
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,
regen: {
label: "Regen (Health + Regen)",
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.45,
baseHealth: 1.35,
},
};
@@ -69,7 +57,7 @@ let state = loadDraft() || createDefaultState();
function createDefaultState() {
return {
profile: "balanced",
profile: "lifesteal",
combatStyle: "melee",
recommendationCount: 8,
pieces: slots.map((slot, index) => createDefaultPiece(slot, index)),
@@ -108,10 +96,11 @@ function enforceFixedPieceFields(piece, index) {
function normalizeState(candidate) {
const fallback = createDefaultState();
const profile = normalizeProfile(candidate?.profile);
return {
...fallback,
...candidate,
profile: PROFILES[candidate?.profile] ? candidate.profile : "balanced",
profile,
combatStyle: ["melee", "ranged", "skill"].includes(candidate?.combatStyle) ? candidate.combatStyle : "melee",
recommendationCount: Math.min(20, Math.max(3, Number(candidate?.recommendationCount || 8))),
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) {
if (!type) return 1;
const effect = effectById(type);
@@ -223,6 +219,7 @@ function updateAnalysis() {
const totals = calculateTotals();
const damageDetails = calculateDamageDetails(totals, state.combatStyle);
const dps = damageDetails.dps;
const regenDetails = calculateRegenDetails(totals, dps);
const survivability = calculateSurvivability(totals);
const pieceScores = state.pieces.map((piece) => scorePiece(piece, profile, totals));
@@ -230,7 +227,8 @@ function updateAnalysis() {
$("#totalHealth").textContent = fmt(totals.baseHealth);
$("#dpsValue").textContent = fmt(dps);
$("#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) => {
card.querySelector(".piece-score").textContent = `Score ${fmt(pieceScores[index].score)}`;
@@ -289,6 +287,17 @@ function calculateSurvivability(totals) {
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) {
if (combatStyle === "ranged") return effects.rangedDamage;
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 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>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 class="results">