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**
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user