Compact gear cards and add dual pet stats

This commit is contained in:
Jan Bader
2026-06-16 15:19:30 +02:00
parent befcba3442
commit 01fbc347d3
3 changed files with 51 additions and 35 deletions
+2 -2
View File
@@ -18,7 +18,7 @@ Effects start at `1%` and can be upgraded up to their cap.
## Fixed item base stats ## 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 | | 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 7 | Boots | 1,850,000 health |
| Item 8 | Belt | 1,150,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 ## Supported effects
+42 -17
View File
@@ -83,6 +83,8 @@ function createDefaultPiece(slot, index) {
name: slot.fixed?.name || slot.title, name: slot.fixed?.name || slot.title,
baseType: slot.fixed?.baseType || (index % 3 === 1 ? "health" : "damage"), baseType: slot.fixed?.baseType || (index % 3 === 1 ? "health" : "damage"),
baseValue: slot.fixed?.baseValue || 0, 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), fixedBase: Boolean(slot.fixed),
effects: [{ type: "", value: 1 }, { type: "", value: 1 }], effects: [{ type: "", value: 1 }, { type: "", value: 1 }],
}; };
@@ -96,6 +98,8 @@ function enforceFixedPieceFields(piece, index) {
name: fixed.name, name: fixed.name,
baseType: fixed.baseType, baseType: fixed.baseType,
baseValue: fixed.baseValue, baseValue: fixed.baseValue,
baseDamage: fixed.baseType === "damage" ? fixed.baseValue : 0,
baseHealth: fixed.baseType === "health" ? fixed.baseValue : 0,
fixedBase: true, fixedBase: true,
}; };
} }
@@ -111,6 +115,8 @@ function normalizeState(candidate) {
pieces: fallback.pieces.map((piece, index) => enforceFixedPieceFields({ pieces: fallback.pieces.map((piece, index) => enforceFixedPieceFields({
...piece, ...piece,
...(candidate?.pieces?.[index] || {}), ...(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) => ({ effects: [0, 1].map((effectIndex) => ({
type: candidate?.pieces?.[index]?.effects?.[effectIndex]?.type || "", type: candidate?.pieces?.[index]?.effects?.[effectIndex]?.type || "",
value: clampEffectValue( value: clampEffectValue(
@@ -150,19 +156,34 @@ 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.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="name"]').value = piece.name;
node.querySelector('[data-field="baseType"]').value = piece.baseType; const baseFields = node.querySelector(".inline-fields");
node.querySelector('[data-field="baseValue"]').value = piece.baseValue;
if (piece.fixedBase) { if (piece.fixedBase) {
node.classList.add("fixed-piece"); node.classList.add("fixed-piece");
node.querySelector('[data-field="name"]').readOnly = true; node.querySelector('[data-field="name"]').readOnly = true;
node.querySelector('[data-field="baseType"]').disabled = true; baseFields.innerHTML = `
node.querySelector('[data-field="baseValue"]').readOnly = true; <label>Base stat
const badge = document.createElement("span"); <select data-field="baseType" disabled>
badge.className = "fixed-badge"; <option value="damage">Damage</option>
badge.textContent = "Fixed base"; <option value="health">Health</option>
node.querySelector(".gear-title-row").appendChild(badge); </select>
</label>
<label>Value
<input data-field="baseValue" type="number" min="0" step="1" readonly />
</label>`;
baseFields.querySelector('[data-field="baseType"]').value = piece.baseType;
baseFields.querySelector('[data-field="baseValue"]').value = piece.baseValue;
} else {
baseFields.innerHTML = `
<label>Damage base
<input data-field="baseDamage" type="number" min="0" step="1" />
</label>
<label>Health base
<input data-field="baseHealth" type="number" min="0" step="1" />
</label>`;
baseFields.querySelector('[data-field="baseDamage"]').value = piece.baseDamage || 0;
baseFields.querySelector('[data-field="baseHealth"]').value = piece.baseHealth || 0;
} }
const effects = node.querySelector(".effects"); const effects = node.querySelector(".effects");
@@ -216,9 +237,8 @@ function updateAnalysis() {
function calculateTotals() { function calculateTotals() {
const totals = { baseDamage: 0, baseHealth: 0, effects: Object.fromEntries(EFFECTS.map((effect) => [effect.id, 0])) }; const totals = { baseDamage: 0, baseHealth: 0, effects: Object.fromEntries(EFFECTS.map((effect) => [effect.id, 0])) };
for (const piece of state.pieces) { for (const piece of state.pieces) {
const baseValue = Number(piece.baseValue || 0); totals.baseDamage += Number(piece.baseDamage || 0);
if (piece.baseType === "health") totals.baseHealth += baseValue; totals.baseHealth += Number(piece.baseHealth || 0);
else totals.baseDamage += baseValue;
for (const effect of piece.effects) { for (const effect of piece.effects) {
if (effect.type) totals.effects[effect.type] += clampEffectValue(effect.type, Number(effect.value)); 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) { function scorePiece(piece, profile, totals) {
const baseWeight = piece.baseType === "health" ? profile.baseHealth : profile.baseDamage; const baseAverageDamage = Math.max(1, totals.baseDamage / 6);
const baseAverage = Math.max(1, piece.baseType === "health" ? totals.baseHealth / 6 : totals.baseDamage / 6); const baseAverageHealth = Math.max(1, totals.baseHealth / 6);
let score = (Number(piece.baseValue || 0) / baseAverage) * 35 * baseWeight; let score = (Number(piece.baseDamage || 0) / baseAverageDamage) * 35 * profile.baseDamage
+ (Number(piece.baseHealth || 0) / baseAverageHealth) * 35 * profile.baseHealth;
const reasons = []; const reasons = [];
for (const effect of piece.effects) { 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 (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)] }; return { piece, score, reasons: [...new Set(reasons)] };
} }
@@ -555,7 +576,11 @@ function handleGearInput(event) {
return; 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] = ["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) { if (event.target.dataset.effectType !== undefined) {
const index = Number(event.target.dataset.effectType); const index = Number(event.target.dataset.effectType);
+7 -16
View File
@@ -35,7 +35,7 @@ button, input, select, textarea {
border-radius: 0.75rem; border-radius: 0.75rem;
color: var(--text); color: var(--text);
background: rgba(255, 255, 255, 0.06); background: rgba(255, 255, 255, 0.06);
padding: 0.7rem 0.85rem; padding: 0.58rem 0.7rem;
} }
button { button {
@@ -177,23 +177,14 @@ h3 { margin-bottom: 0; }
.section-heading { margin: 1.5rem 0 1rem; } .section-heading { margin: 1.5rem 0 1rem; }
.section-heading p { color: var(--muted); } .section-heading p { color: var(--muted); }
.gear-grid { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); padding-bottom: 3rem; } .gear-grid { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); padding-bottom: 3rem; }
.gear-card { display: grid; gap: 0.85rem; } .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 { 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; } .piece-score { color: var(--accent); font-weight: 800; }
.fixed-badge { .inline-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
border: 1px solid rgba(125, 211, 252, 0.35); .effects { display: grid; gap: 0.5rem; }
border-radius: 999px; .effect-row { display: grid; grid-template-columns: minmax(0, 1fr) 88px; gap: 0.5rem; align-items: end; }
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; }
.cap-hint { color: var(--muted); font-size: 0.8rem; margin-top: 0.2rem; } .cap-hint { color: var(--muted); font-size: 0.8rem; margin-top: 0.2rem; }
@media (max-width: 900px) { @media (max-width: 900px) {