/** * 画面角上的分段血条(平行四边形,一条分 5 格)。 * * # 为什么改到角上、改成斜的 * * 原来它在画面**下面**一条窄带里。改成画面左上角/右上角之后: * * - 画面不再是"被一条横带压住的一块",整块视频区域给到取景; * - 两边各占一角,**同时**看到自己与对手的血量,视线不用离开画面; * - 斜切的形状不会在第一时间被误认成字幕条或进度条,扫一眼就知道是血量。 * * 左上角给自己、右上角给对手(见 `TeamPage`)。裁判端两格各自一个角,与用户端同一套 * 视觉语言。 * * # 一条分 5 格 = 一格 20 点 * * 整条是一条,里面分成 **5 格**:格子少而大,隔着几米扫一眼就能数出"还剩三格", * 比读一条连续的长度或一个百分比快。血量归零就是 0 格,那时图传中央会给出胜负。 * * 血少了,亮着的格子从**中心那一侧**开始灭(左条从右往左灭,右条从左往右灭),所以两条 * 血一起减少时,视觉上是往两侧退——不会看成"条变长了"。 * * # 未接入时不显示 0 * * `available` 为 false 时显示占位说明而不是 0:0 是一个看起来像真值的假值,现场看到 0 * 会以为对方已经没血了,然后开始找裁判理论。这一条在旧客户端上是踩过坑的。 */ import { useEffect, useState } from "react"; import { teamColor, withAlpha } from "../lib/format"; import type { HealthConfig, TeamHealthDto } from "../lib/types"; interface Props { health: TeamHealthDto; config: HealthConfig; /** 挂在画面哪一角。`right` 会把条整个镜像过来(含缺血的退向)。 */ align: "left" | "right"; /** 这一格是谁:用户端用「我方 / 对手」把两条同色的血区分开。 */ tag?: string; } /** 整条分成几格。 */ const CELLS = 5; /** 一格多少血。整车一条血 100 点。 */ const CELL_HP = 100 / CELLS; /** 脉动的周期。与 CSS 动画保持一致。 */ const PULSE_MS = 900; interface Cell { /** 这一格是不是亮着(这一格的血还在)。 */ on: boolean; /** 落在低血阈值以内:这是"危险区",即使还亮着也提前给个警示色。 */ danger: boolean; } export function HealthBar({ health, config, align, tag }: Props) { const [pulseOn, setPulseOn] = useState(true); // 低血脉动:只有真的低血时才起定时器,否则一个空转的 setInterval 会让整个界面 // 每秒重渲染一次。 const pulsing = config.pulse && health.available && health.critical; useEffect(() => { if (!pulsing) { setPulseOn(true); return; } const timer = window.setInterval(() => setPulseOn((on) => !on), PULSE_MS / 2); return () => window.clearInterval(timer); }, [pulsing]); const color = health.critical ? "#ff5f5f" : teamColor(health.team); const hp = health.available ? Math.max(0, Math.min(100, health.hp ?? 0)) : 0; // 向上取整:还剩 1 点血的时候必须还剩一格亮着。向下取整会让"还有 10 点血"显示成空条, // 而那正是最需要它准确的时候。 const lit = Math.ceil(hp / CELL_HP); const dangerCells = config.lowThreshold > 0 && config.lowThreshold < 100 ? Math.ceil(config.lowThreshold / CELL_HP) : 0; // 逻辑顺序:0 号格是"满血时最靠外的那一格"。 // // **不要在这里按 `align` 反转数组**:右角那条的翻转让 CSS 的 `scaleX(-1)` 做 // (`segbars--right .segbars__track`),两边都做等于翻两次、又翻回来了——表现是右条 // 从外侧开始灭,与左条相反。 const cells: Cell[] = Array.from({ length: CELLS }, (_, index) => ({ on: index < lit, danger: index >= CELLS - dangerCells, })); // 宽度按配置收窄。它挂在画面角上,太宽会盖住画面本身。 const width = `${Math.round(config.width * 100)}%`; return (
{tag && {tag}} {config.showTeamName && ( {health.name} )} {health.available ? ( config.showNumbers && ( {hp} ) ) : ( 装甲板未接入 )}
); } /** * 这一格是谁。 * * 用户端两条血分别属于两边,而形状是对称的——没有这个标签,"左边这条是我的"要靠位置猜。 */ export function healthTag(health: TeamHealthDto, myTeam: number): string { return health.team === myTeam ? "我方" : "对手"; }