Fix broken images

This commit is contained in:
Kalle
2026-07-19 17:23:47 +03:00
parent e0c2f88250
commit 725d9e2802
8 changed files with 50 additions and 57 deletions

View File

@@ -4,11 +4,12 @@
*/
import { useState } from "react";
import { CV_ASSETS_URL } from "~/utils/urls";
import { Ability } from "~/components/Ability";
import type { CvAbility } from "../cv-types";
const ROW_LABELS = ["head", "clothes", "shoes"] as const;
export function AbilityGrid({ abilities }: { abilities: string[][] }) {
export function AbilityGrid({ abilities }: { abilities: CvAbility[][] }) {
return (
<table className="players">
<tbody>
@@ -17,16 +18,7 @@ export function AbilityGrid({ abilities }: { abilities: string[][] }) {
<td>{ROW_LABELS[i]}</td>
{row.map((id, j) => (
<td key={j}>
<img
className="weapon-icon"
src={`${CV_ASSETS_URL}/abilities/${id}.png`}
alt={id}
title={id}
style={{
width: j === 0 ? 28 : 20,
height: j === 0 ? 28 : 20,
}}
/>
<Ability ability={id} size={j === 0 ? "SUBTINY" : "TINY"} />
</td>
))}
</tr>
@@ -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)"
>
<img
src={`${CV_ASSETS_URL}/abilities/${trigger}.png`}
alt="abilities"
/>
<Ability ability={trigger} size="TINY" />
</button>
{open && (
<div className="popover">

View File

@@ -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: {
</div>
<div className="teams">
<div className="team">
{data.weaponId !== null && data.weaponType === "MAIN" && (
<img
{data.weaponId !== null && data.weaponType === "MAIN" ? (
<WeaponImage
weaponSplId={data.weaponId as MainWeaponId}
variant="build"
size={28}
className="weapon-icon"
src={`${CV_ASSETS_URL}/main-weapons/${data.weaponId}.png`}
alt={weaponName ?? String(data.weaponId)}
title={`weapon ${data.weaponId}`}
/>
)}
) : null}
<AbilityGrid abilities={data.abilities} />
</div>
</div>

View File

@@ -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 ? (
<img
key={i}
className="weapon-icon"
src={`${CV_ASSETS_URL}/abilities/${id}.png`}
alt={id}
title={id}
style={{ width: 22, height: 22 }}
/>
<Ability key={i} ability={id} size="TINY" />
) : (
<span key={i} title="unreadable badge">
?
@@ -44,11 +39,11 @@ function PlayerRow({
<td>{player.name ?? ""}</td>
<td>
{player.weaponId !== null ? (
<img
<WeaponImage
weaponSplId={player.weaponId}
variant="build"
size={28}
className="weapon-icon"
src={`${CV_ASSETS_URL}/main-weapons/${player.weaponId}.png`}
alt={mainWeaponLabel(player.weaponId) ?? String(player.weaponId)}
title={mainWeaponLabel(player.weaponId) ?? String(player.weaponId)}
/>
) : (
"?"

View File

@@ -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({
<tr key={i}>
<td>
<span className="weapon-cell">
{p.weaponId !== null && (
<img
{p.weaponId !== null ? (
<WeaponImage
weaponSplId={p.weaponId}
variant="build"
size={28}
className="weapon-icon"
src={`${CV_ASSETS_URL}/main-weapons/${p.weaponId}.png`}
alt={String(p.weaponId)}
title={`weapon ${p.weaponId}`}
/>
)}
{abilities?.has(offset + i) && (
) : null}
{abilities?.has(offset + i) ? (
<AbilityPopover abilities={abilities.get(offset + i)!} />
)}
) : null}
</span>
</td>
<td>{p.name || "?"}</td>

View File

@@ -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: {
</div>
<div className="teams">
<div className="team">
{data.weaponId !== null && (
<img
{data.weaponId !== null ? (
<WeaponImage
weaponSplId={data.weaponId}
variant="build"
size={28}
className="weapon-icon"
src={`${CV_ASSETS_URL}/main-weapons/${data.weaponId}.png`}
alt={mainWeaponLabel(data.weaponId) ?? String(data.weaponId)}
title={`weapon ${data.weaponId}`}
/>
)}
) : null}
<AbilityGrid abilities={data.abilities} />
</div>
</div>

View File

@@ -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<number, string[][]>;
export type PlayerAbilityMap = Map<number, CvAbility[][]>;
/**
* Match a death's killer to a scoreboard player row. Both signals are OCR

View File

@@ -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<number> = new Set(mainWeaponIds);
const ABILITY_SET: ReadonlySet<string> = new Set(abilities.map((a) => a.name));

View File

@@ -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 <Ability />; 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({