界面改动都在浏览器预览里逐条核对过(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)通过。
96 lines
3.3 KiB
TypeScript
96 lines
3.3 KiB
TypeScript
/**
|
||
* 裁判端就地改队伍名。
|
||
*
|
||
* # 为什么是输入框而不是设置面板里的两项
|
||
*
|
||
* 现场改名字的时机是"比赛开始前,两队报上名来"。那时候裁判盯着的是两格画面,让他在
|
||
* 设置面板的某一节里找到对应项、改完再关掉,是一次没有必要的往返。名字就放在两格画面
|
||
* 各自下面,改完直接看到血条上的名字变了。
|
||
*
|
||
* # 回车/失焦才提交
|
||
*
|
||
* 逐字提交会在每次按键后写一次配置文件(还带一次 Argon2 无关的原子替换),而且中间态
|
||
* ("红"、"红队")会被后端截断/回落规则改写一次,光标跟着跳。失焦提交是唯一不会跟
|
||
* 打字打架的时机。
|
||
*
|
||
* 超过上限的字数由后端统一截断(`TEAM_NAME_MAX_CHARS`),这里也限一下长度——输入框
|
||
* 里就能看到"打不进去了",比提交后被悄悄截掉诚实。
|
||
*/
|
||
|
||
import { useEffect, useRef, useState } from "react";
|
||
|
||
import { DEFAULT_TEAM_NAMES } from "../lib/format";
|
||
|
||
/** 与 Rust 侧的 `config::TEAM_NAME_MAX_CHARS` 一致。 */
|
||
const MAX_CHARS = 12;
|
||
|
||
interface Props {
|
||
team: number;
|
||
/** 当前生效的名字(来自快照,可能已经被后端规范化过)。 */
|
||
name: string;
|
||
/** 提交。后端会规范化,成功之后快照会带新名字回来。 */
|
||
onSave: (name: string) => void;
|
||
disabled?: boolean;
|
||
}
|
||
|
||
export function TeamNameEditor({ team, name, onSave, disabled }: Props) {
|
||
const [draft, setDraft] = useState(name);
|
||
const focused = useRef(false);
|
||
/** Esc 已经改回去了,紧接着的那次失焦不该再提交。 */
|
||
const reverted = useRef(false);
|
||
|
||
// 焦点不在输入框里时跟着快照走:改完之后后端返回的规范名要能显示出来,而正在打字时
|
||
// 不能被一份恰好到达的快照顶掉。
|
||
useEffect(() => {
|
||
if (!focused.current) setDraft(name);
|
||
}, [name]);
|
||
|
||
const commit = () => {
|
||
focused.current = false;
|
||
|
||
if (reverted.current) {
|
||
reverted.current = false;
|
||
return;
|
||
}
|
||
|
||
if (draft === name) return;
|
||
|
||
const trimmed = draft.trim();
|
||
setDraft(trimmed);
|
||
onSave(trimmed);
|
||
};
|
||
|
||
return (
|
||
<label className="team-name">
|
||
<span className="team-name__label" style={{ color: team === 0 ? "#ff5f5f" : "#5aa9ff" }}>
|
||
队名
|
||
</span>
|
||
<input
|
||
className="input team-name__input"
|
||
value={draft}
|
||
maxLength={MAX_CHARS}
|
||
disabled={disabled}
|
||
placeholder={DEFAULT_TEAM_NAMES[team] ?? "队伍"}
|
||
title="改完按回车或点到别处生效;留空回落到默认名"
|
||
onChange={(event) => setDraft(event.target.value)}
|
||
onFocus={() => {
|
||
focused.current = true;
|
||
}}
|
||
onBlur={commit}
|
||
onKeyDown={(event) => {
|
||
if (event.key === "Enter") {
|
||
// 只走失焦这一条提交路径,避免回车与失焦各提交一次。
|
||
event.currentTarget.blur();
|
||
} else if (event.key === "Escape") {
|
||
// 注意:不能只 `setDraft(name)` 再 blur——blur 是同一次事件里同步发生的,
|
||
// `commit` 读到的还是这一帧的旧 draft,于是"取消"反而把改动存了下去。
|
||
reverted.current = true;
|
||
setDraft(name);
|
||
event.currentTarget.blur();
|
||
}
|
||
}}
|
||
/>
|
||
</label>
|
||
);
|
||
}
|