Compact gear cards and add dual pet stats
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user