All checks were successful
SteamWarCI Build successful
- Introduced a new events collection in config.ts with schema validation. - Created a new event markdown file for the WarGear event. - Updated German translations to include new event-related strings. - Modified PageLayout to support a wide layout option. - Enhanced announcements page to improve tag filtering and post rendering. - Implemented dynamic event pages with detailed event information and fight plans. - Added an index page for events to list all upcoming events.
166 lines
7.0 KiB
Svelte
166 lines
7.0 KiB
Svelte
<script lang="ts">
|
|
import type { ExtendedEvent, EventFight, ResponseGroups, ResponseRelation } from "@type/event.ts";
|
|
import type { DoubleEleminationViewConfig } from "./types";
|
|
import EventCard from "./EventCard.svelte";
|
|
import EventFightChip from "./EventFightChip.svelte";
|
|
import { onMount, onDestroy, tick } from "svelte";
|
|
import { fightConnector } from "./connections.svelte.ts";
|
|
|
|
const { event, config }: { event: ExtendedEvent; config: DoubleEleminationViewConfig } = $props();
|
|
|
|
const defaultGroup: ResponseGroups = {
|
|
id: -1,
|
|
name: "Double Elimination",
|
|
pointsPerWin: 0,
|
|
pointsPerLoss: 0,
|
|
pointsPerDraw: 0,
|
|
type: "ELIMINATION_STAGE",
|
|
points: null,
|
|
};
|
|
|
|
function indexRelations(ev: ExtendedEvent): Map<number, ResponseRelation[]> {
|
|
const map = new Map<number, ResponseRelation[]>();
|
|
for (const rel of ev.relations) {
|
|
const list = map.get(rel.fight) ?? [];
|
|
list.push(rel);
|
|
map.set(rel.fight, list);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
const relationsByFight = indexRelations(event);
|
|
const fightMap = new Map<number, EventFight>(event.fights.map((f) => [f.id, f]));
|
|
|
|
function collectBracket(startFinalId: number): EventFight[][] {
|
|
const finalFight = fightMap.get(startFinalId);
|
|
if (!finalFight) return [];
|
|
const bracketGroupId = finalFight.group?.id ?? null;
|
|
const stages: EventFight[][] = [];
|
|
let layer: EventFight[] = [finalFight];
|
|
const visited = new Set<number>([finalFight.id]);
|
|
while (layer.length) {
|
|
stages.push(layer);
|
|
const next: EventFight[] = [];
|
|
for (const fight of layer) {
|
|
const rels = relationsByFight.get(fight.id) ?? [];
|
|
for (const rel of rels) {
|
|
if (rel.type === "FIGHT" && rel.fromFight) {
|
|
const src = fightMap.get(rel.fromFight.id) ?? rel.fromFight;
|
|
if (!src) continue;
|
|
// Only traverse within the same bracket (group) to avoid cross-bracket pollution
|
|
if (bracketGroupId !== null && src.group?.id !== bracketGroupId) continue;
|
|
if (!visited.has(src.id)) {
|
|
visited.add(src.id);
|
|
next.push(src);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
layer = next;
|
|
}
|
|
stages.reverse();
|
|
return stages;
|
|
}
|
|
|
|
const winnersStages = $derived(collectBracket(config.winnersFinalFight));
|
|
const losersStages = $derived(collectBracket(config.losersFinalFight));
|
|
const grandFinal = fightMap.get(config.grandFinalFight);
|
|
|
|
function stageName(count: number, isWinners: boolean): string {
|
|
switch (count) {
|
|
case 1:
|
|
return isWinners ? "Finale (W)" : "Finale (L)";
|
|
case 2:
|
|
return isWinners ? "Halbfinale (W)" : "Halbfinale (L)";
|
|
case 4:
|
|
return isWinners ? "Viertelfinale (W)" : "Viertelfinale (L)";
|
|
case 8:
|
|
return isWinners ? "Achtelfinale (W)" : "Achtelfinale (L)";
|
|
default:
|
|
return `Runde (${count}) ${isWinners ? "W" : "L"}`;
|
|
}
|
|
}
|
|
|
|
let connector: any;
|
|
const unsubscribe = fightConnector.subscribe((v) => (connector = v));
|
|
onDestroy(() => {
|
|
connector.clearAllConnections();
|
|
unsubscribe();
|
|
});
|
|
|
|
function buildConnections() {
|
|
if (!connector) return;
|
|
connector.clearAllConnections();
|
|
// Track offsets per source fight and team to stagger multiple outgoing lines for visual clarity
|
|
const fightTeamOffsetMap = new Map<string, number>();
|
|
const step = 8; // px separation between parallel lines
|
|
for (const rel of event.relations) {
|
|
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
|
|
const fromId = rel.fromFight.id;
|
|
const fromEl = document.getElementById(`fight-${fromId}`) as HTMLElement | null;
|
|
const toEl = document.getElementById(`fight-${rel.fight}-team-${rel.team.toLowerCase()}`) as HTMLElement | null;
|
|
if (!fromEl || !toEl) continue;
|
|
// Use team-signed offsets so BLUE goes left (negative), RED goes right (positive)
|
|
const key = `${fromId}:${rel.team}`;
|
|
const index = fightTeamOffsetMap.get(key) ?? 0;
|
|
const sign = rel.team === "BLUE" ? -1 : 1;
|
|
const offset = sign * (index + 1) * step;
|
|
const color = rel.fromPlace === 0 ? "#60a5fa" : "#f87171";
|
|
connector.addConnection(fromEl, toEl, color, offset);
|
|
fightTeamOffsetMap.set(key, index + 1);
|
|
}
|
|
}
|
|
|
|
onMount(async () => {
|
|
await tick();
|
|
buildConnections();
|
|
});
|
|
</script>
|
|
|
|
{#if !grandFinal}
|
|
<p class="text-gray-400 italic">Konfiguration unvollständig (Grand Final fehlt).</p>
|
|
{:else}
|
|
{#key winnersStages.length + ":" + losersStages.length}
|
|
<!-- Build a grid where rows: winners (stages), losers (stages), with losers offset by one stage/column -->
|
|
{@const totalColumns = Math.max(winnersStages.length, losersStages.length + 1) + 1}
|
|
<div class="grid gap-x-16 gap-y-6 items-start" style={`grid-template-columns: repeat(${totalColumns}, max-content);`}>
|
|
<!-- Winners heading spans all columns -->
|
|
<h2 class="font-bold text-center">Winners Bracket</h2>
|
|
|
|
<!-- Winners stages in row 2 -->
|
|
{#each winnersStages as stage, i}
|
|
<div style={`grid-row: 2; grid-column: ${i + 1};`}>
|
|
<EventCard title={stageName(stage.length, true)}>
|
|
{#each stage as fight}
|
|
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
|
|
{/each}
|
|
</EventCard>
|
|
</div>
|
|
{/each}
|
|
|
|
<!-- Place Grand Final at the far right, aligned with winners row -->
|
|
<div style={`grid-row: 2; grid-column: ${totalColumns};`} class="self-center">
|
|
<EventCard title="Grand Final">
|
|
{#if grandFinal}
|
|
<EventFightChip fight={grandFinal} group={grandFinal.group ?? defaultGroup} />
|
|
{/if}
|
|
</EventCard>
|
|
</div>
|
|
|
|
<!-- Losers heading spans all columns -->
|
|
<h2 class="font-bold text-center" style="grid-row: 3; grid-column: 1 / {totalColumns};">Losers Bracket</h2>
|
|
|
|
<!-- Losers stages in row 4, offset by one column to the right -->
|
|
{#each losersStages as stage, j}
|
|
<div style={`grid-row: 4; grid-column: ${j + 2};`} class="mt-2">
|
|
<EventCard title={stageName(stage.length, false)}>
|
|
{#each stage as fight}
|
|
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
|
|
{/each}
|
|
</EventCard>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/key}
|
|
{/if}
|