Files
ark-stronghold-protocol/public/js/ui/gameLogic/format.js
T

111 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ui/gameLogic/format.js — HUD numbers, status glyphs, range boxes, unit flags. Re-exported from ../gameLogic.js.
import { UF } from '../../../../shared/constants.js';
import { clamp, isObj, tileKey } from './shared.js';
import { N_, langInfo } from '../../../../shared/i18n.js';
// ---- players, statuses, fields ------------------------------------------------------------------------
/** Status → glyph + text (research 06 §11.1). */
export const STATUS_META = Object.freeze({
acting: { glyph: 'dots', text: N_('行动中'), tone: 'lo' },
ready: { glyph: 'check', text: N_('已就绪'), tone: 'mint' },
deciding: { glyph: 'hourglass', text: N_('决策中'), tone: 'gold' },
combat: { glyph: 'sword', text: N_('作战中'), tone: 'orange' },
done: { glyph: 'check', text: N_('作战结束'), tone: 'mint' },
helping: { glyph: 'shield', text: N_('联防中'), tone: 'orange' },
left: { glyph: 'exit', text: N_('已离开'), tone: 'red' },
dead: { glyph: 'close', text: N_('已淘汰'), tone: 'red' },
});
// ---- snapshots / battle HUD ---------------------------------------------------------------------------------
/**
* HUD numbers from a b.snap: { killed, total, dp, boss } (boss: { hp, max } when present).
* @param {any} snap
*/
export function snapHud(snap) {
if (!isObj(snap)) return null;
const n = (v) => (Number.isFinite(v) ? v : null);
let boss = null;
if (isObj(snap.boss) && Number.isFinite(snap.boss.hp)) boss = { hp: snap.boss.hp, max: n(snap.boss.max) ?? n(snap.boss.maxHp) };
return { killed: n(snap.killed), total: n(snap.total), dp: n(snap.dp), boss };
}
/** Boss HP fraction 0..1 (null when unknown). */
export function bossFrac(bossHp) {
if (!isObj(bossHp)) return null;
const hp = Number(bossHp.hp);
const max = Number(bossHp.max ?? bossHp.maxHp);
if (!Number.isFinite(hp) || !Number.isFinite(max) || max <= 0) return null;
return clamp(hp / max, 0, 1);
}
/**
* The boss bar's percentage text for a fraction (bossFrac): whole percents from 10 %, one decimal below, and never
* "0.0%" while the leader still has HP — a sliver reads "<0.1%" (user playtest #6 item 5: a bar at 0.0 % with the
* leader still fighting read as a leader that could not die). null for an unknown fraction.
* @param {number|null} frac
*/
export function bossPctText(frac) {
if (frac == null || !Number.isFinite(frac)) return null;
const pct = clamp(frac, 0, 1) * 100;
if (pct >= 10) return `${pct.toFixed(0)}%`;
if (pct > 0 && pct < 0.05) return '<0.1%';
return `${pct.toFixed(1)}%`;
}
/** Whether a snapshot unit tuple has a flag. */
export const hasFlag = (flags, bit) => (Number(flags) & bit) !== 0;
export { UF };
// ---- stats & range --------------------------------------------------------------------------------------------
/** Attack interval in seconds (bat × 100 / aspd). */
export function attackInterval(bat, aspd = 100) {
const b = Number(bat);
const a = Number(aspd) > 0 ? Number(aspd) : 100;
if (!Number.isFinite(b) || b <= 0) return null;
return b * 100 / a;
}
/**
* Compact number: 12345 → '12,345'; 1.5e6 → '150万' in Chinese, whose units count in 10⁴ / 10⁸ steps (万 / 亿; a language
* pack names its own pair in `_meta.numberUnits`, shared/i18nPacks.js — no template can move the decimal point); a
* language without them uses the thousands-based units: 1.5e6 → '1.5M', 2.5e5 → '250K', 3e9 → '3B' (English).
*/
export function fmtNum(v) {
const n = Number(v);
if (!Number.isFinite(n)) return '—';
const units = langInfo()?.numberUnits;
if (!units) {
if (Math.abs(n) >= 1e9) return `${(n / 1e9).toFixed(Math.abs(n) >= 1e10 ? 0 : 1)}B`;
if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(Math.abs(n) >= 1e7 ? 0 : 1)}M`;
if (Math.abs(n) >= 1e5) return `${Math.round(n / 1e3)}K`;
return Math.round(n).toLocaleString('en-US');
}
if (Math.abs(n) >= 1e8) return `${(n / 1e8).toFixed(n >= 1e9 ? 0 : 1)}${units[1]}`;
if (Math.abs(n) >= 1e5) return `${(n / 1e4).toFixed(n >= 1e6 ? 0 : 1)}${units[0]}`;
return Math.round(n).toLocaleString('en-US');
}
/**
* Bounding box + cell set of a range grid (always including the own tile [0,0]); `mirror` flips columns.
* @param {Array<[number, number]>} grid [[dRow, dCol], …]
* @param {boolean} [mirror]
* @returns {{ rows: number, cols: number, r0: number, c0: number, cells: Set<string>, self: [number, number] }}
*/
export function rangeGridBox(grid, mirror = false) {
const cells = new Set();
let minR = 0; let maxR = 0; let minC = 0; let maxC = 0;
for (const g of Array.isArray(grid) ? grid : []) {
if (!Array.isArray(g) || !Number.isFinite(g[0]) || !Number.isFinite(g[1])) continue;
const r = g[0];
const c = mirror ? -g[1] : g[1];
cells.add(tileKey(r, c));
minR = Math.min(minR, r); maxR = Math.max(maxR, r); minC = Math.min(minC, c); maxC = Math.max(maxC, c);
}
return { rows: maxR - minR + 1, cols: maxC - minC + 1, r0: maxR, c0: minC, cells, self: [0, 0] };
}