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)通过。
This commit is contained in:
@@ -133,11 +133,13 @@ export function VideoSurface({
|
||||
dirty.current = true;
|
||||
}
|
||||
|
||||
// 命中动画期间必须持续重绘,否则抖动只画了一帧就停住。
|
||||
// 命中动画期间必须持续重绘,否则闪红/伤害数字只画了一帧就停住。
|
||||
//
|
||||
// **抖动没有了**(见 `HitFeedbackConfig::shake` 的文档):不再有"整幅图跟着晃"的
|
||||
// 那一路,所以这里也不再需要因为抖动而复绘。
|
||||
const pulseAge = current.hit ? timestamp - current.hit.at : Number.POSITIVE_INFINITY;
|
||||
const animating =
|
||||
pulseAge < 700 &&
|
||||
(current.hitFeedback.shake || current.hitFeedback.marker || current.hitFeedback.flash);
|
||||
pulseAge < 700 && (current.hitFeedback.marker || current.hitFeedback.flash);
|
||||
|
||||
if (!dirty.current && !animating) return;
|
||||
dirty.current = false;
|
||||
@@ -300,11 +302,8 @@ function draw(
|
||||
? layout(box, frame.width, frame.height, current.view)
|
||||
: fallback;
|
||||
|
||||
const shake = current.hit ? shakeOffset(current.hit, timestamp, current.hitFeedback) : { x: 0, y: 0 };
|
||||
|
||||
if (frame) {
|
||||
ctx.save();
|
||||
ctx.translate(shake.x, shake.y);
|
||||
|
||||
if (current.mirrored) {
|
||||
ctx.translate(width, 0);
|
||||
@@ -317,7 +316,7 @@ function draw(
|
||||
ctx.restore();
|
||||
|
||||
if (current.grid?.visible) {
|
||||
drawGrid(ctx, geometry, current.grid, shake);
|
||||
drawGrid(ctx, geometry, current.grid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +332,7 @@ function draw(
|
||||
}
|
||||
|
||||
if (crosshair?.visible) {
|
||||
drawCrosshair(ctx, aimX + shake.x, aimY + shake.y, Math.min(width, height), crosshair);
|
||||
drawCrosshair(ctx, aimX, aimY, Math.min(width, height), crosshair);
|
||||
}
|
||||
|
||||
// 伤害数字:画在屏幕坐标里,不跟着缩放走。
|
||||
@@ -348,39 +347,20 @@ function draw(
|
||||
ctx.font = `600 ${Math.round(Math.max(14, Math.min(width, height) * 0.05))}px ui-sans-serif, system-ui, sans-serif`;
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(
|
||||
`-${current.hit.damage}`,
|
||||
aimX + shake.x + 24,
|
||||
aimY + shake.y - 26 - progress * 26,
|
||||
);
|
||||
ctx.fillText(`-${current.hit.damage}`, aimX + 24, aimY - 26 - progress * 26);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shakeOffset(hit: HitPulse, timestamp: number, feedback: HitFeedbackConfig) {
|
||||
if (!feedback.shake) return { x: 0, y: 0 };
|
||||
|
||||
const age = timestamp - hit.at;
|
||||
if (age < 0 || age > 400) return { x: 0, y: 0 };
|
||||
|
||||
const amplitude = 7 * feedback.intensity * (1 - age / 400);
|
||||
return {
|
||||
x: Math.sin(age * 0.09) * amplitude,
|
||||
y: Math.cos(age * 0.11) * amplitude,
|
||||
};
|
||||
}
|
||||
|
||||
function drawGrid(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
geometry: { left: number; top: number; drawWidth: number; drawHeight: number },
|
||||
grid: GridConfig,
|
||||
shake: { x: number; y: number },
|
||||
) {
|
||||
if (geometry.drawWidth <= 0 || geometry.drawHeight <= 0) return;
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(shake.x, shake.y);
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${grid.opacity})`;
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user