feedback5 R5: split the in-match UI logic into modules

public/js/ui/gameLogic.js becomes a re-export barrel. The bodies move verbatim into public/js/ui/gameLogic/*.js (shared, phases, camera, watch, bonds, shop, placement, draft, enemies, format, shortcuts, settings, result, loadout, panel). The barrel re-exports the same 96 names in the same order. isObj and int gain export on shared.js so sibling modules can import them; they stay off the barrel. No importer changes.
This commit is contained in:
sganggs committed 2026-10-05 17:06:45 +08:00
1 parent 8ab6d021da
commit eb04f5dfa4
16 files changed
+1864 -1778

No files matched your search

+98
View File
@@ -0,0 +1,98 @@
// 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';
// ---- players, statuses, fields ------------------------------------------------------------------------
/** Status → glyph + text (research 06 §11.1). */
export const STATUS_META = Object.freeze({
acting: { glyph: 'dots', text: '行动中', tone: 'lo' },
ready: { glyph: 'check', text: '已就绪', tone: 'mint' },
deciding: { glyph: 'hourglass', text: '决策中', tone: 'gold' },
combat: { glyph: 'sword', text: '作战中', tone: 'orange' },
done: { glyph: 'check', text: '作战结束', tone: 'mint' },
helping: { glyph: 'shield', text: '联防中', tone: 'orange' },
left: { glyph: 'exit', text: '已离开', tone: 'red' },
dead: { glyph: 'close', text: '已淘汰', 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万'. */
export function fmtNum(v) {
const n = Number(v);
if (!Number.isFinite(n)) return '—';
if (Math.abs(n) >= 1e8) return `${(n / 1e8).toFixed(n >= 1e9 ? 0 : 1)}亿`;
if (Math.abs(n) >= 1e5) return `${(n / 1e4).toFixed(n >= 1e6 ? 0 : 1)}万`;
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] };
}