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
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
+42 -17
View File
@@ -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 = `
<label>Base stat
<select data-field="baseType" disabled>
<option value="damage">Damage</option>
<option value="health">Health</option>
</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");
@@ -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);
+7 -16
View File
@@ -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) {