Fix core item base stats

This commit is contained in:
Jan Bader
2026-06-16 15:17:16 +02:00
parent 9432b6a4ce
commit befcba3442
3 changed files with 86 additions and 13 deletions
+17
View File
@@ -16,6 +16,23 @@ Each equipped piece has:
Effects start at `1%` and can be upgraded up to their cap. Effects start at `1%` and can be upgraded up to their cap.
## Fixed item base stats
The 8 core item slots are fixed and cannot be renamed or have their base stat edited in the app:
| Slot | Item | Base stat |
| --- | --- | ---: |
| Item 1 | Helmet | 1,930,000 health |
| Item 2 | Armor | 1,890,000 health |
| Item 3 | Gloves | 237,000 damage |
| Item 4 | Collar | 237,000 damage |
| Item 5 | Ring | 244,000 damage |
| Item 6 | Weapon | 398,000 damage |
| Item 7 | Boots | 1,850,000 health |
| Item 8 | Belt | 1,150,000 health |
Mount and pet base stats remain editable.
## Supported effects ## Supported effects
| Effect | Maximum | | Effect | Maximum |
+54 -13
View File
@@ -44,8 +44,18 @@ const PROFILES = {
const STORAGE_KEY = "forgeMasterOptimizer.v1"; const STORAGE_KEY = "forgeMasterOptimizer.v1";
const DRAFT_KEY = "forgeMasterOptimizer.draft.v1"; const DRAFT_KEY = "forgeMasterOptimizer.draft.v1";
const EXPORT_VERSION = 1; const EXPORT_VERSION = 1;
const FIXED_ITEMS = [
{ name: "Helmet", baseType: "health", baseValue: 1930000 },
{ name: "Armor", baseType: "health", baseValue: 1890000 },
{ name: "Gloves", baseType: "damage", baseValue: 237000 },
{ name: "Collar", baseType: "damage", baseValue: 237000 },
{ name: "Ring", baseType: "damage", baseValue: 244000 },
{ name: "Weapon", baseType: "damage", baseValue: 398000 },
{ name: "Boots", baseType: "health", baseValue: 1850000 },
{ name: "Belt", baseType: "health", baseValue: 1150000 },
];
const slots = [ const slots = [
...Array.from({ length: 8 }, (_, i) => ({ type: "item", title: `Item ${i + 1}` })), ...FIXED_ITEMS.map((item, i) => ({ type: "item", title: `Item ${i + 1}`, fixed: item })),
{ type: "mount", title: "Mount" }, { type: "mount", title: "Mount" },
...Array.from({ length: 3 }, (_, i) => ({ type: "pet", title: `Pet ${i + 1}` })), ...Array.from({ length: 3 }, (_, i) => ({ type: "pet", title: `Pet ${i + 1}` })),
]; ];
@@ -61,15 +71,32 @@ function createDefaultState() {
profile: "balanced", profile: "balanced",
combatStyle: "melee", combatStyle: "melee",
recommendationCount: 8, recommendationCount: 8,
pieces: slots.map((slot, index) => ({ pieces: slots.map((slot, index) => createDefaultPiece(slot, index)),
id: `${slot.type}-${index}`, };
type: slot.type, }
title: slot.title,
name: slot.title, function createDefaultPiece(slot, index) {
baseType: index % 3 === 1 ? "health" : "damage", return {
baseValue: 0, id: `${slot.type}-${index}`,
effects: [{ type: "", value: 1 }, { type: "", value: 1 }], type: slot.type,
})), title: slot.title,
name: slot.fixed?.name || slot.title,
baseType: slot.fixed?.baseType || (index % 3 === 1 ? "health" : "damage"),
baseValue: slot.fixed?.baseValue || 0,
fixedBase: Boolean(slot.fixed),
effects: [{ type: "", value: 1 }, { type: "", value: 1 }],
};
}
function enforceFixedPieceFields(piece, index) {
const fixed = FIXED_ITEMS[index];
if (!fixed || piece.type !== "item") return piece;
return {
...piece,
name: fixed.name,
baseType: fixed.baseType,
baseValue: fixed.baseValue,
fixedBase: true,
}; };
} }
@@ -81,7 +108,7 @@ function normalizeState(candidate) {
profile: PROFILES[candidate?.profile] ? candidate.profile : "balanced", profile: PROFILES[candidate?.profile] ? candidate.profile : "balanced",
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) => ({ pieces: fallback.pieces.map((piece, index) => enforceFixedPieceFields({
...piece, ...piece,
...(candidate?.pieces?.[index] || {}), ...(candidate?.pieces?.[index] || {}),
effects: [0, 1].map((effectIndex) => ({ effects: [0, 1].map((effectIndex) => ({
@@ -91,7 +118,7 @@ function normalizeState(candidate) {
Number(candidate?.pieces?.[index]?.effects?.[effectIndex]?.value || 1), Number(candidate?.pieces?.[index]?.effects?.[effectIndex]?.value || 1),
), ),
})), })),
})), }, index)),
}; };
} }
@@ -123,10 +150,20 @@ function renderGear() {
state.pieces.forEach((piece, pieceIndex) => { state.pieces.forEach((piece, pieceIndex) => {
const node = template.content.firstElementChild.cloneNode(true); const node = template.content.firstElementChild.cloneNode(true);
node.dataset.index = pieceIndex; node.dataset.index = pieceIndex;
node.querySelector("h3").textContent = piece.title; node.querySelector("h3").textContent = piece.fixedBase ? `${piece.title} · ${piece.name}` : piece.title;
node.querySelector('[data-field="name"]').value = piece.name; node.querySelector('[data-field="name"]').value = piece.name;
node.querySelector('[data-field="baseType"]').value = piece.baseType; node.querySelector('[data-field="baseType"]').value = piece.baseType;
node.querySelector('[data-field="baseValue"]').value = piece.baseValue; node.querySelector('[data-field="baseValue"]').value = piece.baseValue;
if (piece.fixedBase) {
node.classList.add("fixed-piece");
node.querySelector('[data-field="name"]').readOnly = true;
node.querySelector('[data-field="baseType"]').disabled = true;
node.querySelector('[data-field="baseValue"]').readOnly = true;
const badge = document.createElement("span");
badge.className = "fixed-badge";
badge.textContent = "Fixed base";
node.querySelector(".gear-title-row").appendChild(badge);
}
const effects = node.querySelector(".effects"); const effects = node.querySelector(".effects");
piece.effects.forEach((effect, effectIndex) => { piece.effects.forEach((effect, effectIndex) => {
@@ -513,6 +550,10 @@ function handleGearInput(event) {
const piece = state.pieces[Number(card.dataset.index)]; const piece = state.pieces[Number(card.dataset.index)];
if (event.target.dataset.field) { if (event.target.dataset.field) {
if (piece.fixedBase && ["name", "baseType", "baseValue"].includes(event.target.dataset.field)) {
event.target.value = piece[event.target.dataset.field];
return;
}
const field = event.target.dataset.field; const field = event.target.dataset.field;
piece[field] = field === "baseValue" ? Math.max(0, Number(event.target.value || 0)) : event.target.value; piece[field] = field === "baseValue" ? Math.max(0, Number(event.target.value || 0)) : event.target.value;
} }
+15
View File
@@ -49,6 +49,11 @@ button:hover { filter: brightness(1.1); }
button.secondary { background: var(--panel-2); } button.secondary { background: var(--panel-2); }
button.danger { border-color: rgba(251, 113, 133, 0.5); background: rgba(251, 113, 133, 0.16); } button.danger { border-color: rgba(251, 113, 133, 0.5); background: rgba(251, 113, 133, 0.16); }
textarea { width: 100%; min-height: 9rem; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } textarea { width: 100%; min-height: 9rem; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
input[readonly], select:disabled {
cursor: not-allowed;
color: var(--muted);
background: rgba(255, 255, 255, 0.035);
}
label { label {
display: grid; display: grid;
@@ -176,6 +181,16 @@ h3 { margin-bottom: 0; }
.gear-card { display: grid; gap: 0.85rem; } .gear-card { display: grid; gap: 0.85rem; }
.gear-title-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } .gear-title-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
.piece-score { color: var(--accent); font-weight: 800; } .piece-score { color: var(--accent); font-weight: 800; }
.fixed-badge {
border: 1px solid rgba(125, 211, 252, 0.35);
border-radius: 999px;
color: var(--accent);
background: rgba(125, 211, 252, 0.08);
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 800;
white-space: nowrap;
}
.inline-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; } .inline-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
.effects { display: grid; gap: 0.75rem; } .effects { display: grid; gap: 0.75rem; }
.effect-row { display: grid; grid-template-columns: 1fr 110px; gap: 0.75rem; align-items: end; } .effect-row { display: grid; grid-template-columns: 1fr 110px; gap: 0.75rem; align-items: end; }