zenno: categorize tools

This commit is contained in:
2026-06-10 14:03:42 -04:00
parent dc78d51def
commit 75024c7c11
18 changed files with 119 additions and 42 deletions

View File

@@ -0,0 +1,144 @@
<script lang="ts">
import CharaPick from '$lib/CharaPick.svelte';
import { AFFINITY_RELATION_DESCRIPTIONS, affinityDetail, type AffinityDetail } from '$lib/data/affinity';
import { character, charaNames, type Character } from '$lib/data/character';
import { onMount } from 'svelte';
import { SvelteSet } from 'svelte/reactivity';
let characters: Character[] = $state([]);
let affs: AffinityDetail[] = $state([]);
onMount(async () => {
const [charas, affinities] = await Promise.all([character(), affinityDetail()]);
affs = affinities;
characters = charas.filter((c) => affs.some((d) => d.chara_id === c.chara_id));
});
const names = $derived(charaNames(characters));
let charA: number = $state(1001);
let charB: number = $state(0);
let charC: number = $state(0);
const nameA = $derived(names.get(charA)?.en ?? `Character ${charA}`);
const nameB = $derived(names.get(charB)?.en ?? `Character ${charB}`);
const nameC = $derived(names.get(charC)?.en ?? `Character ${charC}`);
function groupsFor(affs: AffinityDetail[], chara: number): AffinityDetail[] {
return chara !== 0 ? affs.filter((d) => d.chara_id === chara) : [];
}
const groupsA = $derived(groupsFor(affs, charA));
const groupsB = $derived(groupsFor(affs, charB));
const groupsC = $derived(groupsFor(affs, charC));
const allGroups = $derived.by(() => {
const seen = new SvelteSet<number>();
const r = [];
for (const g of [groupsA, groupsB, groupsC]) {
for (const d of g) {
if (seen.has(d.relation)) {
continue;
}
seen.add(d.relation);
r.push(d);
}
}
return r;
});
const selCount = $derived(1 + (charB === 0 ? 0 : 1) + (charC === 0 ? 0 : 1));
const duplicate = $derived(charA === charB || charA === charC || (charB !== 0 && charB === charC));
const infos = $derived(
allGroups
.map((d) => ({
...d,
description: AFFINITY_RELATION_DESCRIPTIONS.get(d.relation),
a: groupsA.some(({ relation }) => relation === d.relation),
b: groupsB.some(({ relation }) => relation === d.relation),
c: groupsC.some(({ relation }) => relation === d.relation),
}))
.map((d) => ({
...d,
counted: (d.a ? 1 : 0) + (d.b ? 1 : 0) + (d.c ? 1 : 0),
}))
.toSorted((a, b) => (a.counted !== b.counted ? b.counted - a.counted : a.relation - b.relation)),
);
const total = $derived(infos.filter((d) => d.counted === selCount).reduce((t, d) => t + d.affinity, 0));
</script>
<h1 class="text-4xl">Affinity Details</h1>
<div class="mx-auto mt-8 flex flex-col rounded-md text-center shadow-md ring md:max-w-2xl md:flex-row">
<div class="m-4 flex-1 md:mt-3">
<label for="charA" class="hidden md:inline">Character 1</label>
<CharaPick id="charA" {characters} class="w-full" bind:value={charA} required />
</div>
<div class="m-4 flex-1 md:mt-3">
<label for="charB" class="hidden md:inline">Character 2</label>
<CharaPick id="charB" {characters} class="w-full" bind:value={charB} />
</div>
<div class="m-4 flex-1 md:mt-3">
<label for="charC" class="hidden md:inline">Character 3</label>
<CharaPick id="charC" {characters} class="w-full" bind:value={charC} />
</div>
</div>
<svelte:boundary>
{#if duplicate}
<span class="mt-8 block w-full text-center text-lg">Duplicate characters always have zero affinity.</span>
{:else if selCount > 1}
<span class="mt-8 block w-full text-center text-lg">Total {selCount === 2 ? 'pair' : 'trio'} affinity: {total}</span>
{/if}
<table class="mx-auto mt-8 table-fixed">
<thead>
<tr class="py-2">
<th class="w-32 px-2" scope="col">Group ID</th>
<th class="w-96 px-2" scope="col">Description</th>
{#if selCount > 1}
<th class="w-56 px-2" scope="col">{nameA}</th>
{#if charB !== 0}
<th class="w-56 px-2" scope="col">{nameB}</th>
{/if}
{#if charC !== 0}
<th class="w-56 px-2" scope="col">{nameC}</th>
{/if}
{/if}
<th class="w-32 px-2" scope="col">Affinity</th>
</tr>
</thead>
<tbody>
{#each infos as { relation, affinity, description, a, b, c, counted } (relation)}
<tr class="even:bg-mist-300 dark:even:bg-mist-900">
<td class="px-4 text-center">{relation}</td>
{#if description != null}
<td class="px-4">{[description.name, description.note].filter((s) => s != null).join(' ')}</td>
{:else}
<td class="px-4"></td>
{/if}
{#if selCount > 1}
<td class="px-4 text-center">{a ? '✓' : ''}</td>
{#if charB !== 0}
<td class="px-4 text-center">{b ? '✓' : ''}</td>
{/if}
{#if charC !== 0}
<td class="px-4 text-center">{c ? '✓' : ''}</td>
{/if}
{/if}
<td class="px-4 text-center">{!duplicate && selCount > 1 && counted === selCount ? '+' : ''}{affinity}</td>
</tr>
{/each}
</tbody>
</table>
{#snippet pending()}
<div>Loading data...</div>
{/snippet}
</svelte:boundary>
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
<p class="text-center">
Descriptions are inferred from the horses in the respective groups. They are not included in the game data.
</p>
<p class="text-center">
Some characters are not in affinity groups that it seems like they should be in, e.g. parent/child pairs and G1 winners for
various races. Other characters are in affinity groups that it seems like they should not be in, e.g. Mihono Bourbon in the
sprint group. Inclusion in a group is automatically generated from the game data and hence is known to correspond to game
calculations, even where they don't make sense.
</p>
<p class="text-center">
Thanks to princess_fox, hell259, Werseter, thegenosys, Lofi (Class: Voyager), and Ordalca on the GameTora Discord server, and
Damhain on the CirnoTV server, for figuring out most of the groups.
</p>
</div>