Support compact numeric input suffixes
This commit is contained in:
@@ -61,7 +61,29 @@ const slots = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const $ = (selector) => document.querySelector(selector);
|
const $ = (selector) => document.querySelector(selector);
|
||||||
const fmt = (value) => Number(value || 0).toLocaleString(undefined, { maximumFractionDigits: 1 });
|
const NUMBER_SUFFIXES = Object.freeze({ k: 1_000, m: 1_000_000, b: 1_000_000_000 });
|
||||||
|
|
||||||
|
function parseNumberInput(value, fallback = 0) {
|
||||||
|
if (typeof value === "number") return Number.isFinite(value) ? value : fallback;
|
||||||
|
const text = String(value ?? "").trim().replace(/,/g, "").toLowerCase();
|
||||||
|
if (!text) return fallback;
|
||||||
|
const match = /^([+-]?(?:\d+(?:\.\d*)?|\.\d+))\s*([kmb])?$/.exec(text);
|
||||||
|
if (!match) return fallback;
|
||||||
|
const parsed = Number(match[1]) * (NUMBER_SUFFIXES[match[2]] || 1);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCompactNumber(value) {
|
||||||
|
const number = Number(value || 0);
|
||||||
|
if (!Number.isFinite(number)) return "0";
|
||||||
|
const abs = Math.abs(number);
|
||||||
|
const suffix = abs >= NUMBER_SUFFIXES.b ? "b" : abs >= NUMBER_SUFFIXES.m ? "m" : abs >= NUMBER_SUFFIXES.k ? "k" : "";
|
||||||
|
const divisor = suffix ? NUMBER_SUFFIXES[suffix] : 1;
|
||||||
|
const scaled = number / divisor;
|
||||||
|
return `${Number(scaled.toPrecision(3))}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmt = formatCompactNumber;
|
||||||
const effectById = (id) => EFFECTS.find((effect) => effect.id === id);
|
const effectById = (id) => EFFECTS.find((effect) => effect.id === id);
|
||||||
|
|
||||||
let state = loadDraft() || createDefaultState();
|
let state = loadDraft() || createDefaultState();
|
||||||
@@ -212,7 +234,7 @@ function renderControls() {
|
|||||||
$("#profileSelect").value = state.profile;
|
$("#profileSelect").value = state.profile;
|
||||||
$("#combatStyle").value = state.combatStyle;
|
$("#combatStyle").value = state.combatStyle;
|
||||||
$("#optimizationTarget").value = state.optimizationTarget;
|
$("#optimizationTarget").value = state.optimizationTarget;
|
||||||
$("#recommendationCount").value = state.recommendationCount;
|
$("#recommendationCount").value = fmt(state.recommendationCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEffectRow(effect, effectIndex, typeAttribute, valueAttribute, showInitialHint = true) {
|
function createEffectRow(effect, effectIndex, typeAttribute, valueAttribute, showInitialHint = true) {
|
||||||
@@ -226,7 +248,7 @@ function createEffectRow(effect, effectIndex, typeAttribute, valueAttribute, sho
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Percent
|
<label>Percent
|
||||||
<input ${valueAttribute}="${effectIndex}" type="number" min="1" step="0.1" value="${effect.value}" />
|
<input ${valueAttribute}="${effectIndex}" type="text" inputmode="decimal" value="${fmt(effect.value)}" />
|
||||||
<span class="cap-hint"></span>
|
<span class="cap-hint"></span>
|
||||||
</label>`;
|
</label>`;
|
||||||
row.querySelector("select").value = effect.type;
|
row.querySelector("select").value = effect.type;
|
||||||
@@ -258,20 +280,20 @@ function renderGear() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Value
|
<label>Value
|
||||||
<input data-field="baseValue" type="number" min="0" step="1" readonly />
|
<input data-field="baseValue" type="text" inputmode="decimal" />
|
||||||
</label>`;
|
</label>`;
|
||||||
baseFields.querySelector('[data-field="baseType"]').value = piece.baseType;
|
baseFields.querySelector('[data-field="baseType"]').value = piece.baseType;
|
||||||
baseFields.querySelector('[data-field="baseValue"]').value = piece.baseValue;
|
baseFields.querySelector('[data-field="baseValue"]').value = fmt(piece.baseValue);
|
||||||
} else {
|
} else {
|
||||||
baseFields.innerHTML = `
|
baseFields.innerHTML = `
|
||||||
<label>Damage base
|
<label>Damage base
|
||||||
<input data-field="baseDamage" type="number" min="0" step="1" />
|
<input data-field="baseDamage" type="text" inputmode="decimal" />
|
||||||
</label>
|
</label>
|
||||||
<label>Health base
|
<label>Health base
|
||||||
<input data-field="baseHealth" type="number" min="0" step="1" />
|
<input data-field="baseHealth" type="text" inputmode="decimal" />
|
||||||
</label>`;
|
</label>`;
|
||||||
baseFields.querySelector('[data-field="baseDamage"]').value = piece.baseDamage || 0;
|
baseFields.querySelector('[data-field="baseDamage"]').value = fmt(piece.baseDamage || 0);
|
||||||
baseFields.querySelector('[data-field="baseHealth"]').value = piece.baseHealth || 0;
|
baseFields.querySelector('[data-field="baseHealth"]').value = fmt(piece.baseHealth || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const effects = node.querySelector(".effects");
|
const effects = node.querySelector(".effects");
|
||||||
@@ -304,15 +326,15 @@ function renderNewItemComparison() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Value
|
<label>Value
|
||||||
<input data-new-item-field="${field}" type="number" min="0" step="1" value="${value}" />
|
<input data-new-item-field="${field}" type="text" inputmode="decimal" value="${fmt(value)}" />
|
||||||
</label>`;
|
</label>`;
|
||||||
} else {
|
} else {
|
||||||
baseFields.innerHTML = `
|
baseFields.innerHTML = `
|
||||||
<label>Damage base
|
<label>Damage base
|
||||||
<input data-new-item-field="baseDamage" type="number" min="0" step="1" value="${state.newItem.baseDamage || 0}" />
|
<input data-new-item-field="baseDamage" type="text" inputmode="decimal" value="${fmt(state.newItem.baseDamage || 0)}" />
|
||||||
</label>
|
</label>
|
||||||
<label>Health base
|
<label>Health base
|
||||||
<input data-new-item-field="baseHealth" type="number" min="0" step="1" value="${state.newItem.baseHealth || 0}" />
|
<input data-new-item-field="baseHealth" type="text" inputmode="decimal" value="${fmt(state.newItem.baseHealth || 0)}" />
|
||||||
</label>`;
|
</label>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -970,7 +992,13 @@ function bindEvents() {
|
|||||||
updateAnalysis();
|
updateAnalysis();
|
||||||
});
|
});
|
||||||
$("#recommendationCount").addEventListener("input", (event) => {
|
$("#recommendationCount").addEventListener("input", (event) => {
|
||||||
state.recommendationCount = Math.min(20, Math.max(3, Number(event.target.value || 8)));
|
state.recommendationCount = Math.min(20, Math.max(3, Math.round(parseNumberInput(event.target.value, 8))));
|
||||||
|
formatChangedInput(event, state.recommendationCount);
|
||||||
|
updateAnalysis();
|
||||||
|
});
|
||||||
|
$("#recommendationCount").addEventListener("change", (event) => {
|
||||||
|
state.recommendationCount = Math.min(20, Math.max(3, Math.round(parseNumberInput(event.target.value, 8))));
|
||||||
|
formatChangedInput(event, state.recommendationCount);
|
||||||
updateAnalysis();
|
updateAnalysis();
|
||||||
});
|
});
|
||||||
$("#resetBuild").addEventListener("click", () => {
|
$("#resetBuild").addEventListener("click", () => {
|
||||||
@@ -1001,7 +1029,8 @@ function bindEvents() {
|
|||||||
function handleNewItemInput(event) {
|
function handleNewItemInput(event) {
|
||||||
if (event.target.id === "newItemTarget") return;
|
if (event.target.id === "newItemTarget") return;
|
||||||
if (event.target.dataset.newItemField) {
|
if (event.target.dataset.newItemField) {
|
||||||
state.newItem[event.target.dataset.newItemField] = Math.max(0, Number(event.target.value || 0));
|
state.newItem[event.target.dataset.newItemField] = Math.max(0, parseNumberInput(event.target.value, 0));
|
||||||
|
formatChangedInput(event, state.newItem[event.target.dataset.newItemField]);
|
||||||
}
|
}
|
||||||
if (event.target.dataset.newItemEffectType !== undefined) {
|
if (event.target.dataset.newItemEffectType !== undefined) {
|
||||||
const index = Number(event.target.dataset.newItemEffectType);
|
const index = Number(event.target.dataset.newItemEffectType);
|
||||||
@@ -1011,7 +1040,8 @@ function handleNewItemInput(event) {
|
|||||||
}
|
}
|
||||||
if (event.target.dataset.newItemEffectValue !== undefined) {
|
if (event.target.dataset.newItemEffectValue !== undefined) {
|
||||||
const index = Number(event.target.dataset.newItemEffectValue);
|
const index = Number(event.target.dataset.newItemEffectValue);
|
||||||
state.newItem.effects[index].value = clampEffectValue(state.newItem.effects[index].type, Number(event.target.value || 1));
|
state.newItem.effects[index].value = clampEffectValue(state.newItem.effects[index].type, parseNumberInput(event.target.value, 1));
|
||||||
|
formatChangedInput(event, state.newItem.effects[index].value);
|
||||||
}
|
}
|
||||||
updateAnalysis();
|
updateAnalysis();
|
||||||
}
|
}
|
||||||
@@ -1022,16 +1052,21 @@ function handleGearInput(event) {
|
|||||||
const piece = state.pieces[Number(card.dataset.index)];
|
const piece = state.pieces[Number(card.dataset.index)];
|
||||||
|
|
||||||
if (event.target.dataset.field) {
|
if (event.target.dataset.field) {
|
||||||
if (piece.fixedBase && ["name", "baseType", "baseValue"].includes(event.target.dataset.field)) {
|
if (piece.fixedBase && ["name", "baseType"].includes(event.target.dataset.field)) {
|
||||||
event.target.value = piece[event.target.dataset.field];
|
event.target.value = piece[event.target.dataset.field];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const field = event.target.dataset.field;
|
const field = event.target.dataset.field;
|
||||||
piece[field] = ["baseValue", "baseDamage", "baseHealth"].includes(field) ? Math.max(0, Number(event.target.value || 0)) : event.target.value;
|
piece[field] = ["baseValue", "baseDamage", "baseHealth"].includes(field) ? Math.max(0, parseNumberInput(event.target.value, 0)) : event.target.value;
|
||||||
|
if (field === "baseValue") {
|
||||||
|
if (piece.baseType === "health") piece.baseHealth = piece.baseValue;
|
||||||
|
else piece.baseDamage = piece.baseValue;
|
||||||
|
}
|
||||||
if (field === "baseDamage" || field === "baseHealth") {
|
if (field === "baseDamage" || field === "baseHealth") {
|
||||||
piece.baseType = Number(piece.baseHealth || 0) > Number(piece.baseDamage || 0) ? "health" : "damage";
|
piece.baseType = Number(piece.baseHealth || 0) > Number(piece.baseDamage || 0) ? "health" : "damage";
|
||||||
piece.baseValue = Math.max(Number(piece.baseDamage || 0), Number(piece.baseHealth || 0));
|
piece.baseValue = Math.max(Number(piece.baseDamage || 0), Number(piece.baseHealth || 0));
|
||||||
}
|
}
|
||||||
|
if (["baseValue", "baseDamage", "baseHealth"].includes(field)) formatChangedInput(event, piece[field]);
|
||||||
}
|
}
|
||||||
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);
|
||||||
@@ -1041,11 +1076,16 @@ function handleGearInput(event) {
|
|||||||
}
|
}
|
||||||
if (event.target.dataset.effectValue !== undefined) {
|
if (event.target.dataset.effectValue !== undefined) {
|
||||||
const index = Number(event.target.dataset.effectValue);
|
const index = Number(event.target.dataset.effectValue);
|
||||||
piece.effects[index].value = clampEffectValue(piece.effects[index].type, Number(event.target.value || 1));
|
piece.effects[index].value = clampEffectValue(piece.effects[index].type, parseNumberInput(event.target.value, 1));
|
||||||
|
formatChangedInput(event, piece.effects[index].value);
|
||||||
}
|
}
|
||||||
updateAnalysis();
|
updateAnalysis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatChangedInput(event, value) {
|
||||||
|
if (event.type === "change") event.target.value = fmt(value);
|
||||||
|
}
|
||||||
|
|
||||||
function saveNamedSlot() {
|
function saveNamedSlot() {
|
||||||
const name = $("#slotName").value.trim();
|
const name = $("#slotName").value.trim();
|
||||||
if (!name) return setStatus("Enter a slot name first.", true);
|
if (!name) return setStatus("Enter a slot name first.", true);
|
||||||
|
|||||||
+2
-2
@@ -42,7 +42,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Recommendation count
|
Recommendation count
|
||||||
<input id="recommendationCount" type="number" min="3" max="20" step="1" value="8" />
|
<input id="recommendationCount" type="text" inputmode="numeric" value="8" />
|
||||||
</label>
|
</label>
|
||||||
<button id="resetBuild" type="button" class="secondary">Reset current build</button>
|
<button id="resetBuild" type="button" class="secondary">Reset current build</button>
|
||||||
</section>
|
</section>
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Value
|
Value
|
||||||
<input data-field="baseValue" type="number" min="0" step="1" />
|
<input data-field="baseValue" type="text" inputmode="decimal" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="effects"></div>
|
<div class="effects"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user