145 lines
5.6 KiB
Svelte
145 lines
5.6 KiB
Svelte
<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>
|