界面改动都在浏览器预览里逐条核对过(DOM 尺寸 + 视觉),截图留在 desktop/screenshots/ (该目录不进仓库)。 1. 客户端显示对手的血量 快照里两端都给两支队伍(顺序固定为队伍号 0/1),界面按 `team` 取那一格,不再靠 数组下标。顺带修掉一个真 bug:用户端"对手那一格"以前会走 `armor_id`——那是我自己 绑定的主板,于是右上角显示的是自己的血,比不显示更糟。现在只有"自己那一块"才用 `armor_id`;同理 `assigned_armor` 里的裁判端槽位指定对用户端不再生效。 2. 血条改到画面两个上角,平行四边形,一条分 5 格 左上角自己、右上角对手(裁判端两格各贴自己的角,读法一致)。整条 skewX(-15deg), 右条再 scaleX(-1) 镜像,两条斜边朝外对称、**都从靠中心的一侧开始灭**。一格 20 点血。 条挂在绝对定位的一层上并 pointer-events:none(那一层盖着画面,而画面要能滚轮缩放、 拖动),倍率与复位徽标因此从上沿挪到下沿。未接入显示"装甲板未接入"而不是 0。 注意:不要在 JS 里再反转一次数组——那会和 CSS 的 scaleX(-1) 互相抵消,表现是右条 从外侧开始灭(代码里写了注释钉住这件事)。 3. 受击画面不抖动 删掉整幅图(含瞄准器)的位移与 shakeOffset。`HitFeedbackConfig::shake` 字段保留但 **不再生效**:配置文件是 deny_unknown_fields 的,删字段会让所有写过 shake = true 的 配置解析失败,而配置解析失败等于程序起不来。默认值改 false,设置面板去掉开关, 并有单测钉住"旧配置读得进来、默认不抖"。 4. 血量归零算击败,不显示"损毁" 血条上不再出现"已击毁/损毁",归零的结论放到图传中央:胜利 / 击败 / 平局。结论在 Rust 侧算(snapshot::resolve_outcomes,4 条单测),规则有意不对称——**输只看自己** (自己归零就是输,哪怕对手主板掉线),**赢要看两边**(对手归零且自己还有血), 两边同时归零是平局。它一直显示到血量被重置,不做几秒淡出,否则截图与回放里就再也 没有"这局结束了"的痕迹。 5. 裁判端可以改队伍名 两格画面各自下面一个输入框:回车/失焦生效、Esc 取消、超 12 字截断、留空回落到 「队伍一 / 队伍二」。命令是 `set_team_name(team, name)`,**一次只改一边**——传整个 两条的数组就成了"读两条、改一条、写两条",而读来的那两条来自最多 200ms 前的快照, 紧接着改另一边会把上一次的改动覆盖回去。规范化统一在 `config::sanitize_team_name` (合并空白、按字符截断、留空回落),并同样过密码闸门(裁判端本来就没有密码)。 名字只有一个来源(快照的 `teamNames` / DTO 的 `name`):装甲板表、槽位标签、设置 面板与"等待图传"的占位文字都跟着改,不再有第二处自己拼"队伍一/队伍二"。 测试:core 80(69→80)、src-tauri 30、protocol 15;`pnpm build`(tsc + vite)通过。
89 lines
3.1 KiB
TypeScript
89 lines
3.1 KiB
TypeScript
/** 纯格式化函数。界面里所有"给人看的数字"都从这里过一遍。 */
|
|
|
|
/** `0x3D4D8A9A`。与 Rust 侧的 `snapshot::id_text` 输出一致。 */
|
|
export function idText(id: number | null | undefined): string {
|
|
if (id === null || id === undefined) return "—";
|
|
return `0x${id.toString(16).toUpperCase().padStart(8, "0")}`;
|
|
}
|
|
|
|
/** 22000 → `21.5 kB`。 */
|
|
export function bytes(value: number): string {
|
|
if (value <= 0) return "—";
|
|
if (value < 1024) return `${value} B`;
|
|
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} kB`;
|
|
return `${(value / 1024 / 1024).toFixed(2)} MB`;
|
|
}
|
|
|
|
/** 毫秒 → 人话。 */
|
|
export function elapsed(ms: number | null | undefined): string {
|
|
if (ms === null || ms === undefined) return "—";
|
|
|
|
if (ms < 1000) return `${Math.round(ms)} ms`;
|
|
if (ms < 60_000) return `${(ms / 1000).toFixed(1)} s`;
|
|
|
|
const totalSeconds = Math.round(ms / 1000);
|
|
const minutes = Math.floor(totalSeconds / 60);
|
|
const seconds = totalSeconds % 60;
|
|
|
|
if (minutes < 60) return `${minutes} 分 ${seconds} 秒`;
|
|
|
|
const hours = Math.floor(minutes / 60);
|
|
return `${hours} 小时 ${minutes % 60} 分`;
|
|
}
|
|
|
|
/** 录制时长:从开始到现在的墙钟。 */
|
|
export function since(ms: number | null | undefined, now = Date.now()): string {
|
|
if (!ms) return "—";
|
|
return elapsed(now - ms);
|
|
}
|
|
|
|
/** Unix 毫秒 → `14:03:22`。 */
|
|
export function clockTime(ms: number | null | undefined): string {
|
|
if (!ms) return "—";
|
|
|
|
const date = new Date(ms);
|
|
const pad = (value: number) => value.toString().padStart(2, "0");
|
|
return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
}
|
|
|
|
/** 小数位固定的帧率。0 表示"还没读数",显示成 `—` 而不是 `0.0`。 */
|
|
export function fps(value: number): string {
|
|
if (!Number.isFinite(value) || value <= 0) return "—";
|
|
return value.toFixed(1);
|
|
}
|
|
|
|
/** 队伍名的兜底值。与 Rust 侧的 `config::DEFAULT_TEAM_NAMES` 一致。 */
|
|
export const DEFAULT_TEAM_NAMES = ["队伍一", "队伍二"];
|
|
|
|
/**
|
|
* 队伍名。
|
|
*
|
|
* 名字是**配置里可以改的**(裁判端能直接编辑),所以显示的地方一律从这里取:散在各处的
|
|
* `team === 0 ? "队伍一" : "队伍二"` 会在改名之后留下一半旧名字,而且不会报错。
|
|
*
|
|
* `names` 传配置里的两条;缺省或留空时回落到默认名。
|
|
*/
|
|
export function teamName(team: number, names?: readonly string[] | null): string {
|
|
const custom = names?.[team]?.trim();
|
|
if (custom) return custom;
|
|
return DEFAULT_TEAM_NAMES[team] ?? "队伍";
|
|
}
|
|
|
|
/** 队伍配色:与装甲板灯色一致(队伍一红、队伍二蓝)。 */
|
|
export function teamColor(team: number): string {
|
|
return team === 0 ? "#ff5f5f" : "#5aa9ff";
|
|
}
|
|
|
|
/** 把 `#RRGGBB` 变成 `rgba(...)`。瞄准器颜色要带透明度时用。 */
|
|
export function withAlpha(hex: string, alpha: number): string {
|
|
const match = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
if (!match) return hex;
|
|
|
|
const value = parseInt(match[1], 16);
|
|
const r = (value >> 16) & 0xff;
|
|
const g = (value >> 8) & 0xff;
|
|
const b = value & 0xff;
|
|
|
|
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
}
|