@@ -17,16 +18,7 @@ export function AbilityGrid({ abilities }: { abilities: string[][] }) {
| {ROW_LABELS[i]} |
{row.map((id, j) => (
-
+
|
))}
@@ -40,7 +32,7 @@ export function AbilityGrid({ abilities }: { abilities: string[][] }) {
* Click-to-toggle popover showing a player's ability grid; the trigger is
* the head-main ability icon. Closes when the pointer leaves it.
*/
-export function AbilityPopover({ abilities }: { abilities: string[][] }) {
+export function AbilityPopover({ abilities }: { abilities: CvAbility[][] }) {
const [open, setOpen] = useState(false);
const trigger = abilities[0]?.[0];
if (!trigger) return null;
@@ -53,10 +45,7 @@ export function AbilityPopover({ abilities }: { abilities: string[][] }) {
onClick={() => setOpen((o) => !o)}
title="Show abilities (from death events)"
>
-
+
{open && (
diff --git a/app/features/cv/components/DeathCard.tsx b/app/features/cv/components/DeathCard.tsx
index 66fa6fb3f..a0068ba09 100644
--- a/app/features/cv/components/DeathCard.tsx
+++ b/app/features/cv/components/DeathCard.tsx
@@ -1,4 +1,5 @@
-import { CV_ASSETS_URL } from "~/utils/urls";
+import { WeaponImage } from "~/components/Image";
+import type { MainWeaponId } from "~/modules/in-game-lists/types";
import type { DeathData } from "../core/detectors/death/index";
import { AbilityGrid } from "./AbilityGrid";
import { saveFixtureFromEvent } from "./fixture-export";
@@ -52,14 +53,14 @@ export function DeathCard(props: {
- {data.weaponId !== null && data.weaponType === "MAIN" && (
-

- )}
+ ) : null}
diff --git a/app/features/cv/components/MinimapCard.tsx b/app/features/cv/components/MinimapCard.tsx
index 0382c7c22..bd0dea154 100644
--- a/app/features/cv/components/MinimapCard.tsx
+++ b/app/features/cv/components/MinimapCard.tsx
@@ -1,26 +1,21 @@
-import { CV_ASSETS_URL } from "~/utils/urls";
+import { Ability } from "~/components/Ability";
+import { WeaponImage } from "~/components/Image";
import type {
MinimapData,
MinimapEnemy,
MinimapTeammate,
} from "../core/detectors/minimap/index";
+import type { CvAbility } from "../cv-types";
import { saveFixtureFromEvent } from "./fixture-export";
import { formatTime } from "./format";
-import { mainWeaponLabel, stageLabel } from "./labels";
+import { stageLabel } from "./labels";
-function AbilityRow({ abilities }: { abilities: (string | null)[] }) {
+function AbilityRow({ abilities }: { abilities: (CvAbility | null)[] }) {
return (
<>
{abilities.map((id, i) =>
id ? (
-
+
) : (
?
@@ -44,11 +39,11 @@ function PlayerRow({
{player.name ?? ""} |
{player.weaponId !== null ? (
-
) : (
"?"
diff --git a/app/features/cv/components/ScoreboardCard.tsx b/app/features/cv/components/ScoreboardCard.tsx
index fe54712e3..da168523d 100644
--- a/app/features/cv/components/ScoreboardCard.tsx
+++ b/app/features/cv/components/ScoreboardCard.tsx
@@ -1,4 +1,4 @@
-import { CV_ASSETS_URL } from "~/utils/urls";
+import { WeaponImage } from "~/components/Image";
import type { PlayerAbilityMap } from "../core/ability-harvest";
import type {
ScoreboardData,
@@ -27,17 +27,17 @@ function PlayerRows({
|
- {p.weaponId !== null && (
-
- )}
- {abilities?.has(offset + i) && (
+ ) : null}
+ {abilities?.has(offset + i) ? (
- )}
+ ) : null}
|
{p.name || "?"} |
diff --git a/app/features/cv/components/ScoreboardOwnCard.tsx b/app/features/cv/components/ScoreboardOwnCard.tsx
index 2eb875422..e380e2866 100644
--- a/app/features/cv/components/ScoreboardOwnCard.tsx
+++ b/app/features/cv/components/ScoreboardOwnCard.tsx
@@ -1,4 +1,4 @@
-import { CV_ASSETS_URL } from "~/utils/urls";
+import { WeaponImage } from "~/components/Image";
import type { ScoreboardOwnData } from "../core/detectors/scoreboard-own/index";
import { AbilityGrid } from "./AbilityGrid";
import { saveFixtureFromEvent } from "./fixture-export";
@@ -59,14 +59,14 @@ export function ScoreboardOwnCard(props: {
- {data.weaponId !== null && (
-

- )}
+ ) : null}
diff --git a/app/features/cv/core/ability-harvest.ts b/app/features/cv/core/ability-harvest.ts
index 4ac30e5f7..75c9b8a5e 100644
--- a/app/features/cv/core/ability-harvest.ts
+++ b/app/features/cv/core/ability-harvest.ts
@@ -7,6 +7,7 @@
* and weapon id.
*/
+import type { CvAbility } from "../cv-types";
import { DEATH_EVENT_TYPE, type DeathData } from "./detectors/death/index";
import { SCOREBOARD_EVENT_TYPES } from "./detectors/registry";
import type {
@@ -16,7 +17,7 @@ import type {
import type { DetectedEvent } from "./detectors/types";
/** player row index (0-7) → [head, clothes, shoes] ability-id rows */
-export type PlayerAbilityMap = Map;
+export type PlayerAbilityMap = Map;
/**
* Match a death's killer to a scoreboard player row. Both signals are OCR
diff --git a/app/features/cv/cv-types.ts b/app/features/cv/cv-types.ts
index 92ecfec9f..55a4a3861 100644
--- a/app/features/cv/cv-types.ts
+++ b/app/features/cv/cv-types.ts
@@ -17,7 +17,7 @@ export type CvLobby = (typeof CV_LOBBIES)[number];
* unrecognized marker (the UNKNOWN template in assets/cv/abilities —
* distinct from null, which means the badge was covered/absent).
*/
-export type CvAbility = Ability | "UNKNOWN";
+export type CvAbility = Ability | "UNKNOWN"; // xxx: why not AbilityWithUnknown
const MAIN_WEAPON_ID_SET: ReadonlySet = new Set(mainWeaponIds);
const ABILITY_SET: ReadonlySet = new Set(abilities.map((a) => a.name));
diff --git a/app/features/cv/routes/cv.tsx b/app/features/cv/routes/cv.tsx
index 5b027ec26..19fb7d883 100644
--- a/app/features/cv/routes/cv.tsx
+++ b/app/features/cv/routes/cv.tsx
@@ -3,6 +3,13 @@ import type { MetaFunction } from "react-router";
import { Placeholder } from "~/components/Placeholder";
import { useHydrated } from "~/hooks/useHydrated";
import { metaTags } from "~/utils/remix";
+import type { SendouRouteHandle } from "~/utils/remix.server";
+
+// `builds` powers the empty/UNKNOWN ability label in ; the weapon
+// and game-misc namespaces the CV cards rely on are always loaded.
+export const handle: SendouRouteHandle = {
+ i18n: ["builds"],
+};
export const meta: MetaFunction = (args) => {
return metaTags({