/** * Per-player splatted / special-held bands over a game's scanned icon-strip reads, rendered above * the ObjectiveTimeline on the same `t` axis (pass `domain` to share the range). Reads re-confirm * an unchanged state every few seconds; a longer gap means the HUD was not observed, so bands never * bridge across one. */ import { useTranslation } from "react-i18next"; import type { MainWeaponId } from "~/modules/in-game-lists/types"; import { abilityImageUrl } from "~/utils/urls"; import { Image, WeaponImage } from "./Image"; import { formatElapsed, TIMELINE_PLOT_GUTTER_PX, } from "./objective-timeline-utils"; import styles from "./PlayerStatusTimeline.module.css"; /** Consecutive reads further apart than this leave an unknown gap. */ const MAX_BRIDGE_SECONDS = 15; /** Trailing open band drawn this long past its last confirming read. */ export const PLAYER_STATUS_TAIL_SECONDS = 1; type PlayerFlags = readonly [boolean, boolean, boolean, boolean]; /** One icon-strip read, sides in `[alpha, bravo]` order. */ export interface PlayerStatusTimelineSample { /** seconds into the source (video, stream or game) the read was made at */ t: number; special: readonly [PlayerFlags, PlayerFlags]; dead: readonly [PlayerFlags, PlayerFlags]; } export interface PlayerStatusTimelineTeam { label: string; /** weapon per slot in row order; null/absent slots render a placeholder */ weapons: (MainWeaponId | null)[]; } export function PlayerStatusTimeline({ samples, teams, domain, }: { samples: readonly PlayerStatusTimelineSample[]; teams: readonly [PlayerStatusTimelineTeam, PlayerStatusTimelineTeam]; /** x-axis range override, to share the objective chart's axis */ domain?: [number, number]; }) { const { t } = useTranslation(["common"]); const sorted = samples.toSorted((a, b) => a.t - b.t); if (sorted.length === 0) return null; const min = Math.min(domain?.[0] ?? Number.POSITIVE_INFINITY, sorted[0]!.t); const max = Math.max( domain?.[1] ?? 0, sorted[sorted.length - 1]!.t + PLAYER_STATUS_TAIL_SECONDS, ); const range = Math.max(1, max - min); const leftOf = (span: StatusSpan) => `${((span.start - min) / range) * 100}%`; const widthOf = (span: StatusSpan) => `${((span.end - span.start) / range) * 100}%`; const titleOf = (label: string, span: StatusSpan) => `${label} · ${formatElapsed(span.start)}–${formatElapsed(span.end)}`; return (