Files
personal-website/src/lib/game-tracker/EncounterGrid.svelte
Stan Runge c2ba8febb4
Some checks failed
Test and deploy staging / staging (push) Failing after 5m29s
Expand website and game trackers
2026-09-25 22:43:04 +00:00

92 lines
3.0 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { pokemonImage } from './game-art';
let {
groups,
caughtNumbers,
onselect
}: {
groups: [
number,
{
name: string;
type1: string;
type2: string | null;
minLevel: number | null;
maxLevel: number | null;
}[]
][];
caughtNumbers: Set<number>;
onselect: (number: number) => void;
} = $props();
let width = $state(600);
let height = $state(400);
// Fit every species, including broad locations with hundreds of records, into one overview.
const columns = $derived(
Math.max(
1,
Math.min(
groups.length,
Math.ceil(Math.sqrt(((groups.length * width) / Math.max(1, height)) * 0.7))
)
)
);
const rows = $derived(Math.max(1, Math.ceil(groups.length / columns)));
const cellWidth = $derived(width / columns);
const cellHeight = $derived(height / rows);
const showName = $derived(cellWidth >= 58 && cellHeight >= 42);
const showLevel = $derived(cellWidth >= 65 && cellHeight >= 64);
const spriteSize = $derived(
Math.max(
12,
Math.min(48, cellWidth - 8, cellHeight - (showName ? 14 : 0) - (showLevel ? 12 : 0) - 8)
)
);
</script>
<div class="h-full min-h-0 w-full" bind:clientWidth={width} bind:clientHeight={height}>
<div
class="encounter-grid grid gap-1"
role="group"
aria-label="All Pokémon in this area"
style:grid-template-columns={`repeat(${columns}, minmax(0,1fr))`}
style:grid-template-rows={`repeat(${rows}, minmax(0,1fr))`}
style:height={`min(100%, ${rows * 108}px)`}
>
{#each groups as [number, encounters] (number)}
{@const p = encounters[0]}
{@const levels = encounters
.flatMap((e) => [e.minLevel, e.maxLevel])
.filter((n): n is number => n !== null)}
{@const min = levels.length ? Math.min(...levels) : null}
{@const max = levels.length ? Math.max(...levels) : null}
<button
class="relative flex min-h-0 min-w-0 flex-col items-center justify-center overflow-hidden rounded-lg border bg-background p-0.5 hover:bg-muted focus-visible:z-10 focus-visible:outline-2"
class:border-emerald-500={caughtNumbers.has(number)}
aria-label={`${p.name} ${p.type1}${p.type2 ? ` / ${p.type2}` : ''} encounters${caughtNumbers.has(number) ? ', caught' : ''}`}
title={`${p.name} · Lv. ${min ?? '?'}${max !== min ? `–${max}` : ''}${caughtNumbers.has(number) ? ' · caught' : ''}`}
onclick={() => onselect(number)}
>
{#if caughtNumbers.has(number)}<span
class="absolute top-0 right-1 text-[10px] text-emerald-600"
aria-hidden="true">✓</span
>{/if}
<img
src={pokemonImage(number)}
alt=""
width="48"
height="48"
style:width={`${spriteSize}px`}
style:height={`${spriteSize}px`}
class="shrink-0 object-contain"
/>
{#if showName}<span class="block w-full truncate text-center text-[10px] font-medium"
>{p.name}</span
>{/if}
{#if showLevel}<span class="text-[10px] text-muted-foreground"
>Lv. {min ?? '?'}{max !== min ? `–${max}` : ''}</span
>{/if}
</button>
{/each}
</div>
</div>