Files
ESP32S3_vt/desktop/src/components/MatchBanner.tsx
T
Horldsence a93ae065cf desktop: 双方血条(角上平行四边形)、归零即击败、去掉受击抖动、裁判端可改队名
界面改动都在浏览器预览里逐条核对过(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)通过。
2026-10-14 20:48:12 +08:00

57 lines
2.0 KiB
TypeScript

/**
* 图传中央的胜负横幅:一边归零之后,画面正中显示「胜利」或「击败」。
*
* # 为什么这次放在正中
*
* 命中反馈那一套刻意避开正中(伤害数字放在准星旁),因为被打中的那一下恰恰最需要看着
* 准星。**胜负不是那种信息**:它是一局的结束,此刻不再需要瞄准;反过来说,它是这一屏
* 上唯一还重要的事,放在角上会让人错过整场比赛的结论。
*
* # 它一直显示到血量被重置
*
* 不做 3 秒淡出:淡出之后屏幕上就再没有"这局结束了"的痕迹,截图、回放、现场的争议都
* 无从对照。血量被重置(长按 BOOT / 下一局开始)时它自己消失——那个动作本来就是
* "这局翻篇了"。
*
* 用词是两边各说各的:赢的一边看到「胜利」,输的一边看到「击败」。同时归零显示「平局」,
* 因为那时候任何一边显示"胜利"都是错的。
*/
import type { MatchOutcome } from "../lib/types";
interface Props {
/** 从这一边看的结论。两边都活着时是 null,什么都不显示。 */
outcome: MatchOutcome | null;
/** 我这一边的名字。 */
mine: string;
/** 对手的名字。 */
theirs: string;
}
const WORD: Record<MatchOutcome, string> = {
victory: "胜利",
defeat: "击败",
draw: "平局",
};
export function MatchBanner({ outcome, mine, theirs }: Props) {
if (!outcome) return null;
const caption =
outcome === "victory"
? `击败 ${theirs}`
: outcome === "defeat"
? `被 ${theirs} 击败`
: "双方装甲板同时归零";
return (
<div className={`match-banner match-banner--${outcome}`} role="status">
<div className="match-banner__card">
<span className="match-banner__team">{mine}</span>
<span className="match-banner__word">{WORD[outcome]}</span>
<span className="match-banner__caption">{caption}</span>
</div>
</div>
);
}