mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-20 18:29:48 -05:00
201 lines
5.2 KiB
TypeScript
201 lines
5.2 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { useLoaderData } from "react-router";
|
|
import { ModeImage, StageImage } from "~/components/Image";
|
|
import type { CustomPickBanStep } from "~/db/tables";
|
|
import { useTournament } from "~/features/tournament/routes/to.$id";
|
|
import * as PickBan from "~/features/tournament-bracket/core/PickBan";
|
|
import type { ModeShort, StageId } from "~/modules/in-game-lists/types";
|
|
import type { TournamentMatchLoaderData } from "../loaders/to.$id.matches.$mid.server";
|
|
import styles from "./MatchMapInfo.module.css";
|
|
|
|
export function MatchMapInfo({ teams }: { teams: [number, number] }) {
|
|
const data = useLoaderData<TournamentMatchLoaderData>();
|
|
const tournament = useTournament();
|
|
|
|
const teamOne = tournament.teamById(teams[0]);
|
|
const teamTwo = tournament.teamById(teams[1]);
|
|
|
|
const customFlow = data.match.roundMaps?.customFlow;
|
|
if (!customFlow) return null;
|
|
|
|
const pickBanTeams: [PickBan.PickBanTeam, PickBan.PickBanTeam] = [
|
|
{ id: teams[0], seed: teamOne?.seed ?? 0 },
|
|
{ id: teams[1], seed: teamTwo?.seed ?? 0 },
|
|
];
|
|
|
|
const teamOneBans: BanEvent[] = [];
|
|
const teamTwoBans: BanEvent[] = [];
|
|
|
|
for (let i = 0; i < data.pickBanEvents.length; i++) {
|
|
const event = data.pickBanEvents[i]!;
|
|
if (event.type !== "BAN" && event.type !== "MODE_BAN") continue;
|
|
|
|
const teamId = resolveTeamForEvent({
|
|
eventIndex: i,
|
|
preSet: customFlow.preSet,
|
|
postGame: customFlow.postGame,
|
|
teams: pickBanTeams,
|
|
results: data.results,
|
|
});
|
|
|
|
if (teamId === teams[0]) {
|
|
teamOneBans.push(event);
|
|
} else if (teamId === teams[1]) {
|
|
teamTwoBans.push(event);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<div className="stack md">
|
|
<BanSection teamName={teamOne?.name ?? "???"} bans={teamOneBans} />
|
|
<BanSection teamName={teamTwo?.name ?? "???"} bans={teamTwoBans} />
|
|
<PlayedSection results={data.results} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function resolveTeamForEvent({
|
|
eventIndex,
|
|
preSet,
|
|
postGame,
|
|
teams,
|
|
results,
|
|
}: {
|
|
eventIndex: number;
|
|
preSet: CustomPickBanStep[];
|
|
postGame: CustomPickBanStep[];
|
|
teams: [PickBan.PickBanTeam, PickBan.PickBanTeam];
|
|
results: Array<{ winnerTeamId: number }>;
|
|
}): number | null {
|
|
const step =
|
|
eventIndex < preSet.length
|
|
? preSet[eventIndex]
|
|
: postGame[(eventIndex - preSet.length) % postGame.length];
|
|
|
|
if (!step?.side) return null;
|
|
|
|
// PickBan.resolveTeamFromSide uses the last element of results for WINNER/LOSER,
|
|
// but here we iterate over all historical events so we need to slice
|
|
// results to the correct post-game cycle
|
|
if (step.side === "WINNER" || step.side === "LOSER") {
|
|
const cycleIndex = Math.floor(
|
|
(eventIndex - preSet.length) / postGame.length,
|
|
);
|
|
if (!results[cycleIndex]) return null;
|
|
|
|
return PickBan.resolveTeamFromSide({
|
|
side: step.side,
|
|
teams,
|
|
results: results.slice(0, cycleIndex + 1),
|
|
});
|
|
}
|
|
|
|
return PickBan.resolveTeamFromSide({
|
|
side: step.side,
|
|
teams,
|
|
results,
|
|
});
|
|
}
|
|
|
|
interface BanEvent {
|
|
stageId: StageId | null;
|
|
mode: ModeShort | null;
|
|
type: string;
|
|
}
|
|
|
|
function BanSection({
|
|
teamName,
|
|
bans,
|
|
}: {
|
|
teamName: string;
|
|
bans: BanEvent[];
|
|
}) {
|
|
const { t } = useTranslation(["game-misc", "tournament"]);
|
|
const mapBans = bans.filter(
|
|
(b): b is BanEvent & { stageId: StageId; mode: ModeShort } =>
|
|
b.type === "BAN" && b.stageId !== null && b.mode !== null,
|
|
);
|
|
const modeBans = bans.filter(
|
|
(b): b is BanEvent & { mode: ModeShort } =>
|
|
b.type === "MODE_BAN" && b.mode !== null,
|
|
);
|
|
|
|
return (
|
|
<div className={styles.section}>
|
|
<h2 className={styles.heading}>
|
|
{t("tournament:match.mapInfo.bans", { teamName })}
|
|
</h2>
|
|
{mapBans.length === 0 && modeBans.length === 0 ? (
|
|
<div className={styles.emptyText}>
|
|
{t("tournament:match.mapInfo.noBans")}
|
|
</div>
|
|
) : null}
|
|
{mapBans.length > 0 ? (
|
|
<div className={styles.maps}>
|
|
{mapBans.map((ban, i) => (
|
|
<MapEntry key={i} stageId={ban.stageId} />
|
|
))}
|
|
</div>
|
|
) : null}
|
|
{modeBans.length > 0 ? (
|
|
<div className={styles.maps}>
|
|
{modeBans.map((ban, i) => (
|
|
<div key={i} className={styles.modeEntry}>
|
|
<ModeImage mode={ban.mode} size={24} />
|
|
{t(`game-misc:MODE_LONG_${ban.mode}`)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function PlayedSection({
|
|
results,
|
|
}: {
|
|
results: Array<{ stageId: StageId; mode: ModeShort }>;
|
|
}) {
|
|
const { t } = useTranslation(["game-misc", "tournament"]);
|
|
|
|
return (
|
|
<div className={styles.section}>
|
|
<h2 className={styles.heading}>
|
|
{t("tournament:match.mapInfo.playedStages")}
|
|
</h2>
|
|
{results.length === 0 ? (
|
|
<div className={styles.emptyText}>
|
|
{t("tournament:match.mapInfo.noPlayedStages")}
|
|
</div>
|
|
) : (
|
|
<div className={styles.maps}>
|
|
{results.map((result, i) => (
|
|
<MapEntry key={i} stageId={result.stageId} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MapEntry({ stageId, mode }: { stageId: StageId; mode?: ModeShort }) {
|
|
const { t } = useTranslation(["game-misc"]);
|
|
|
|
return (
|
|
<div className={styles.mapEntry}>
|
|
<StageImage
|
|
stageId={stageId}
|
|
height={50}
|
|
width={90}
|
|
className={styles.stageImage}
|
|
/>
|
|
<div className={styles.mapLabel}>
|
|
{mode ? `${t(`game-misc:MODE_SHORT_${mode}`)} ` : null}
|
|
{t(`game-misc:STAGE_${stageId}`).split(" ")[0]}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|