Files
ark-stronghold-protocol/public/js/ui/gameLogic/format.js
T
xcdoge b4bc89ad4e feedback7 UIBACK: the top-bar capsule counts like the official — resolved = killed + leaked among the enemies the round scheduled (opening 0/3, a leak 1/3, a kill 2/3, the splitter 3/3); a split child, a summon and a boss summon are in neither number but still cost LP and break 完美作战 (PR #308 by @xcdoge, the capsule half of PR #157 — authored by them: the sim / protocol / HUD hunks applied as reviewed): inTotal / killedInTotal / leakedInTotal / resolved, b.snap / BattleResult / b.progress (optional field) / m.public progress carry resolved, killed may exceed total (validation bounds it by the content-spawn allowance); dropped the PR's stray design-doc line — it had replaced the S→C b.end bullet of docs/design/network.md, which is back; the 联防 live-counter change mark follows the enemy list now that a spawn no longer moves total (uniteLeftMark + test); golden: only total / players.*.total / snaps move, outcomes bit-identical — roster 23 (roster-002 003 007 008 009 010 011 012 013 015 017 018 020 023 024 025 026 027 037 045 047 048 049), bonds 2 (bond-maniShip-low, bond-soloShip-high), fields 11 (boss-boss_1 3 5 pair+solo, boss-boss_6-solo, hidden-boss_8 10 pair+solo), diy 11 (diy-036 040 056 062 068 073 083 091 095 114 115), matches 12 (solo-FUNNY-1 NORMAL-1 HARD-1 ABYSS-1 ABYSS-2, coop2-NORMAL-3 coop2-HARD-4 coop3-ABYSS-5 coop4-ABYSS-7 coop2-NORMAL-8-serverrun solo-HARD-9-boosted coop2-NORMAL-70-diy: the total column of 36 round rows), standins unchanged
2026-10-08 19:30:36 +08:00

114 lines
5.1 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, resolved, total, dp, boss } (boss: { hp, max } when present).
* `resolved` is the capsule's numerator — knocked out + leaked among the round's own enemies (Battle.leakedInTotal);
* an older snapshot without it falls back to the kill count.
* @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) };
const killed = n(snap.killed);
return { killed, resolved: n(snap.resolved) ?? 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] };
}