- Result.post() now returns { ok, reason } instead of void — it used to
silently no-op (unconfigured channel, or zero rows built) while
/tg-admin result post always reported success regardless
- Fixed a crash when a TGScore's class is missing and the character it
names is gone from CharacterRegistry (orphaned history) — Emoji.class()
and Emoji.nation() now degrade to a placeholder instead of throwing
- Fixed tg-history files being written with only a scores array, missing
date/confirmed/nationKD — Score.submit's loadHistory now fills in the
full TGResult shape on every read; scripts/migrate-history-shape.py
backfills existing files (50 fixed in dev, run on prod too — this
script is gitignored like the other migration scripts, copy manually)
- ATK/DEF/Heal shorthand parser: accepts comma as a decimal separator
(3,4M == 3.4M), and rejects implausibly large values (150.0M, or an
already-full number with a redundant M) via a configurable ceiling
(Config.tg.maxStatValue, default 50,000,000, /tg-config tg
set-max-stat-value) instead of silently producing multi-billion junk
- data/updates v0.10.1 (result posting fixes) and v0.10.2 (parser
refinements) — sequenced after the already-deployed v0.10 rather than
backfilling v0.9.x, since posting order in #updates is what matters
- new announcement (002) explaining the shorthand input change to players
- doc updates: REFERENCE.md and MERGE_CHECKLIST.md cover all of the above
60 lines
2.5 KiB
TypeScript
60 lines
2.5 KiB
TypeScript
import { Config } from "@systems/config";
|
|
|
|
/**
|
|
* Parses a combat-stat value (ATK/DEF/Heal) that may use K/M shorthand,
|
|
* case-insensitive (e.g. "500", "500k", "1.4M", "2.7m"). Comma is accepted
|
|
* as a decimal separator too ("3,4M" parses the same as "3.4M") since
|
|
* players naturally try it.
|
|
*
|
|
* A bare number with no suffix is inferred: >= 100 is assumed to already
|
|
* be in thousands ("500" -> 500K), below 100 is assumed to be shorthand
|
|
* millions ("1.4" -> 1.4M) — combat stats are never realistically in the
|
|
* single-to-double-digit range.
|
|
*
|
|
* Also enforces a configurable plausibility ceiling (`Config.tg.maxStatValue`,
|
|
* default 50M) — real recorded ATK/DEF values top out around 10-13M, but a
|
|
* stray extra digit or a redundant M on an already-large number
|
|
* ("150.0M", "124000M") silently produces a nonsense multi-billion value
|
|
* with no upstream validation catching it otherwise. The cap is officer-
|
|
* adjustable (`/tg-config tg set-max-stat-value`) rather than hardcoded,
|
|
* since what counts as "impossible" can shift with game balance changes —
|
|
* this is a sanity check, not a hard game-rule.
|
|
*
|
|
* Shared by the Submit Score modal, `/tg score set`, and
|
|
* `/tg-admin score-inject` so all three entry points behave identically.
|
|
*/
|
|
export interface StatValueResult {
|
|
value?: number;
|
|
error?: string;
|
|
}
|
|
|
|
export function parseStatValue(raw: string | null | undefined, label: string): StatValueResult {
|
|
if (!raw) return {};
|
|
|
|
const normalized = raw.trim().replace(",", ".");
|
|
const match = normalized.match(/^(\d+(?:\.\d+)?)\s*([kKmM])?$/);
|
|
if (!match) {
|
|
return { error: `${label} must be ${STAT_VALUE_HINT}.` };
|
|
}
|
|
|
|
const parsed = parseFloat(match[1]);
|
|
const suffix = match[2]?.toLowerCase();
|
|
|
|
let value: number;
|
|
if (suffix === "k") value = Math.round(parsed * 1_000);
|
|
else if (suffix === "m") value = Math.round(parsed * 1_000_000);
|
|
else value = Math.round(parsed >= 100 ? parsed * 1_000 : parsed * 1_000_000);
|
|
|
|
const cap = Config.get({ section: "tg", key: "maxStatValue" });
|
|
if (value > cap) {
|
|
return {
|
|
error: `${label} of ${value.toLocaleString()} is above the ${cap.toLocaleString()} sanity limit — ` +
|
|
`check for a typo (e.g. an extra digit or a redundant M/K). If this is genuinely correct, ` +
|
|
`an officer can raise the limit with \`/tg-config tg set-max-stat-value\`.`,
|
|
};
|
|
}
|
|
|
|
return { value };
|
|
}
|
|
|
|
export const STAT_VALUE_HINT = "a number, optionally with a K or M suffix (e.g. `500K`, `1.4M`)";
|