From 01fbc347d370ca141fb3f13077574d55d6af793f Mon Sep 17 00:00:00 2001 From: Jan Bader Date: Tue, 16 Jun 2026 15:19:30 +0200 Subject: [PATCH] Compact gear cards and add dual pet stats --- README.md | 4 ++-- app.js | 59 ++++++++++++++++++++++++++++++++++++++---------------- styles.css | 23 +++++++-------------- 3 files changed, 51 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 4e5da23..45e19ad 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ 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: +The 8 core item slots are fixed and cannot be renamed or have their base stat edited in the app. The UI shows the item name directly instead of generic item numbers: | Slot | Item | Base stat | | --- | --- | ---: | @@ -31,7 +31,7 @@ The 8 core item slots are fixed and cannot be renamed or have their base stat ed | Item 7 | Boots | 1,850,000 health | | Item 8 | Belt | 1,150,000 health | -Mount and pet base stats remain editable. +Mount and pet base stats remain editable, and each has separate damage and health base values because they can provide both stats at once. ## Supported effects diff --git a/app.js b/app.js index 85c7a34..4a8f29d 100644 --- a/app.js +++ b/app.js @@ -83,6 +83,8 @@ function createDefaultPiece(slot, index) { name: slot.fixed?.name || slot.title, baseType: slot.fixed?.baseType || (index % 3 === 1 ? "health" : "damage"), baseValue: slot.fixed?.baseValue || 0, + baseDamage: slot.fixed?.baseType === "damage" ? slot.fixed.baseValue : 0, + baseHealth: slot.fixed?.baseType === "health" ? slot.fixed.baseValue : 0, fixedBase: Boolean(slot.fixed), effects: [{ type: "", value: 1 }, { type: "", value: 1 }], }; @@ -96,6 +98,8 @@ function enforceFixedPieceFields(piece, index) { name: fixed.name, baseType: fixed.baseType, baseValue: fixed.baseValue, + baseDamage: fixed.baseType === "damage" ? fixed.baseValue : 0, + baseHealth: fixed.baseType === "health" ? fixed.baseValue : 0, fixedBase: true, }; } @@ -111,6 +115,8 @@ function normalizeState(candidate) { pieces: fallback.pieces.map((piece, index) => enforceFixedPieceFields({ ...piece, ...(candidate?.pieces?.[index] || {}), + baseDamage: Number(candidate?.pieces?.[index]?.baseDamage ?? (candidate?.pieces?.[index]?.baseType === "damage" ? candidate?.pieces?.[index]?.baseValue : piece.baseDamage) ?? 0), + baseHealth: Number(candidate?.pieces?.[index]?.baseHealth ?? (candidate?.pieces?.[index]?.baseType === "health" ? candidate?.pieces?.[index]?.baseValue : piece.baseHealth) ?? 0), effects: [0, 1].map((effectIndex) => ({ type: candidate?.pieces?.[index]?.effects?.[effectIndex]?.type || "", value: clampEffectValue( @@ -150,19 +156,34 @@ function renderGear() { state.pieces.forEach((piece, pieceIndex) => { const node = template.content.firstElementChild.cloneNode(true); node.dataset.index = pieceIndex; - node.querySelector("h3").textContent = piece.fixedBase ? `${piece.title} ยท ${piece.name}` : piece.title; + node.querySelector("h3").textContent = piece.name; node.querySelector('[data-field="name"]').value = piece.name; - node.querySelector('[data-field="baseType"]').value = piece.baseType; - node.querySelector('[data-field="baseValue"]').value = piece.baseValue; + const baseFields = node.querySelector(".inline-fields"); 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); + baseFields.innerHTML = ` + + `; + baseFields.querySelector('[data-field="baseType"]').value = piece.baseType; + baseFields.querySelector('[data-field="baseValue"]').value = piece.baseValue; + } else { + baseFields.innerHTML = ` + + `; + baseFields.querySelector('[data-field="baseDamage"]').value = piece.baseDamage || 0; + baseFields.querySelector('[data-field="baseHealth"]').value = piece.baseHealth || 0; } const effects = node.querySelector(".effects"); @@ -216,9 +237,8 @@ function updateAnalysis() { function calculateTotals() { const totals = { baseDamage: 0, baseHealth: 0, effects: Object.fromEntries(EFFECTS.map((effect) => [effect.id, 0])) }; for (const piece of state.pieces) { - const baseValue = Number(piece.baseValue || 0); - if (piece.baseType === "health") totals.baseHealth += baseValue; - else totals.baseDamage += baseValue; + totals.baseDamage += Number(piece.baseDamage || 0); + totals.baseHealth += Number(piece.baseHealth || 0); for (const effect of piece.effects) { if (effect.type) totals.effects[effect.type] += clampEffectValue(effect.type, Number(effect.value)); } @@ -290,9 +310,10 @@ function calculateGroupScore(totals, profile, groups, baseKind) { } function scorePiece(piece, profile, totals) { - const baseWeight = piece.baseType === "health" ? profile.baseHealth : profile.baseDamage; - const baseAverage = Math.max(1, piece.baseType === "health" ? totals.baseHealth / 6 : totals.baseDamage / 6); - let score = (Number(piece.baseValue || 0) / baseAverage) * 35 * baseWeight; + const baseAverageDamage = Math.max(1, totals.baseDamage / 6); + const baseAverageHealth = Math.max(1, totals.baseHealth / 6); + let score = (Number(piece.baseDamage || 0) / baseAverageDamage) * 35 * profile.baseDamage + + (Number(piece.baseHealth || 0) / baseAverageHealth) * 35 * profile.baseHealth; const reasons = []; for (const effect of piece.effects) { @@ -307,7 +328,7 @@ function scorePiece(piece, profile, totals) { if (profile.weights[effect.type] <= 3) reasons.push(`${def.label} is low priority for this profile`); } - if (!Number(piece.baseValue || 0)) reasons.push("no base stat entered"); + if (!Number(piece.baseDamage || 0) && !Number(piece.baseHealth || 0)) reasons.push("no base stat entered"); return { piece, score, reasons: [...new Set(reasons)] }; } @@ -555,7 +576,11 @@ function handleGearInput(event) { return; } const field = event.target.dataset.field; - piece[field] = field === "baseValue" ? Math.max(0, Number(event.target.value || 0)) : event.target.value; + piece[field] = ["baseValue", "baseDamage", "baseHealth"].includes(field) ? Math.max(0, Number(event.target.value || 0)) : event.target.value; + if (field === "baseDamage" || field === "baseHealth") { + piece.baseType = Number(piece.baseHealth || 0) > Number(piece.baseDamage || 0) ? "health" : "damage"; + piece.baseValue = Math.max(Number(piece.baseDamage || 0), Number(piece.baseHealth || 0)); + } } if (event.target.dataset.effectType !== undefined) { const index = Number(event.target.dataset.effectType); diff --git a/styles.css b/styles.css index 302a3db..1e5dfef 100644 --- a/styles.css +++ b/styles.css @@ -35,7 +35,7 @@ button, input, select, textarea { border-radius: 0.75rem; color: var(--text); background: rgba(255, 255, 255, 0.06); - padding: 0.7rem 0.85rem; + padding: 0.58rem 0.7rem; } button { @@ -177,23 +177,14 @@ h3 { margin-bottom: 0; } .section-heading { margin: 1.5rem 0 1rem; } .section-heading p { color: var(--muted); } -.gear-grid { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); padding-bottom: 3rem; } -.gear-card { display: grid; gap: 0.85rem; } +.gear-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); padding-bottom: 3rem; } +.gear-card { display: grid; gap: 0.6rem; padding: 0.8rem; } .gear-title-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } +.gear-title-row h3 { font-size: 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; } +.inline-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; } +.effects { display: grid; gap: 0.5rem; } +.effect-row { display: grid; grid-template-columns: minmax(0, 1fr) 88px; gap: 0.5rem; align-items: end; } .cap-hint { color: var(--muted); font-size: 0.8rem; margin-top: 0.2rem; } @media (max-width: 900px) {