diff --git a/README.md b/README.md
index b6cde64..1e6f2d7 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/app.js b/app.js
index 0d21aa1..a443c24 100644
--- a/app.js
+++ b/app.js
@@ -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)}
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)}
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;
diff --git a/index.html b/index.html
index 41481bd..b1323af 100644
--- a/index.html
+++ b/index.html
@@ -74,7 +74,7 @@
Total damage base0
Total health base0
Calculated DPS0
- Defense score0
+ Regen0