diff --git a/README.md b/README.md index e958acd..4e5da23 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,23 @@ Each equipped piece has: 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 | Effect | Maximum | diff --git a/app.js b/app.js index 3652cfe..85c7a34 100644 --- a/app.js +++ b/app.js @@ -44,8 +44,18 @@ const PROFILES = { const STORAGE_KEY = "forgeMasterOptimizer.v1"; const DRAFT_KEY = "forgeMasterOptimizer.draft.v1"; 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 = [ - ...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" }, ...Array.from({ length: 3 }, (_, i) => ({ type: "pet", title: `Pet ${i + 1}` })), ]; @@ -61,15 +71,32 @@ function createDefaultState() { profile: "balanced", combatStyle: "melee", recommendationCount: 8, - pieces: slots.map((slot, index) => ({ - id: `${slot.type}-${index}`, - type: slot.type, - title: slot.title, - name: slot.title, - baseType: index % 3 === 1 ? "health" : "damage", - baseValue: 0, - effects: [{ type: "", value: 1 }, { type: "", value: 1 }], - })), + pieces: slots.map((slot, index) => createDefaultPiece(slot, index)), + }; +} + +function createDefaultPiece(slot, index) { + return { + id: `${slot.type}-${index}`, + 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", 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) => ({ + pieces: fallback.pieces.map((piece, index) => enforceFixedPieceFields({ ...piece, ...(candidate?.pieces?.[index] || {}), effects: [0, 1].map((effectIndex) => ({ @@ -91,7 +118,7 @@ function normalizeState(candidate) { Number(candidate?.pieces?.[index]?.effects?.[effectIndex]?.value || 1), ), })), - })), + }, index)), }; } @@ -123,10 +150,20 @@ function renderGear() { state.pieces.forEach((piece, pieceIndex) => { const node = template.content.firstElementChild.cloneNode(true); 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="baseType"]').value = piece.baseType; 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"); piece.effects.forEach((effect, effectIndex) => { @@ -513,6 +550,10 @@ function handleGearInput(event) { const piece = state.pieces[Number(card.dataset.index)]; 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; piece[field] = field === "baseValue" ? Math.max(0, Number(event.target.value || 0)) : event.target.value; } diff --git a/styles.css b/styles.css index c6b5917..302a3db 100644 --- a/styles.css +++ b/styles.css @@ -49,6 +49,11 @@ button:hover { filter: brightness(1.1); } button.secondary { background: var(--panel-2); } 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; } +input[readonly], select:disabled { + cursor: not-allowed; + color: var(--muted); + background: rgba(255, 255, 255, 0.035); +} label { display: grid; @@ -176,6 +181,16 @@ h3 { margin-bottom: 0; } .gear-card { display: grid; gap: 0.85rem; } .gear-title-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } .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; } .effects { display: grid; gap: 0.75rem; } .effect-row { display: grid; grid-template-columns: 1fr 110px; gap: 0.75rem; align-items: end; }