Replace profiles with regen-focused modes
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user