zenno/affinity: new tool

This commit is contained in:
2026-06-08 16:42:04 -04:00
parent 6db0ca5230
commit 5f0ad1b0e0
3 changed files with 494 additions and 1 deletions

View File

@@ -16,7 +16,7 @@
<select {id} class={className} bind:value {required}> <select {id} class={className} bind:value {required}>
{#if !required} {#if !required}
<option value="0" class={optionClass}></option> <option value={0} class={optionClass}></option>
{/if} {/if}
{#each characters ?? [] as c (c.chara_id)} {#each characters ?? [] as c (c.chara_id)}
<option value={c.chara_id} class={optionClass}>{c.name}</option> <option value={c.chara_id} class={optionClass}>{c.name}</option>

View File

@@ -32,3 +32,352 @@ export function lookup(aff: Affinity[], chara_a: number, chara_b: number, chara_
const r = aff.find((v) => v.chara_a === a && v.chara_b === b && (v.chara_c ?? Infinity) === c); const r = aff.find((v) => v.chara_a === a && v.chara_b === b && (v.chara_c ?? Infinity) === c);
return r?.affinity ?? 0; return r?.affinity ?? 0;
} }
/**
* Individual affinity entry.
*/
export interface AffinityDetail {
/** Character to which the entry applies. */
chara_id: number;
/** Succession relation that the character has. */
relation: number;
/** Value of the succession relation. */
affinity: number;
}
export async function affinityDetail(): Promise<AffinityDetail[]> {
const resp = await fetch('/api/global/affinity-detail');
return resp.json();
}
export interface AffinityRelationDescription {
name: string;
note?: string;
}
export const AFFINITY_RELATION_DESCRIPTIONS = new Map<number, AffinityRelationDescription>([
[2001, { name: 'Shared Parent', note: 'Sunday Silence' }],
[2002, { name: 'Shared Parent', note: 'Tony Bin' }],
[2003, { name: 'Shared Parent', note: 'Sakura Yutaka O' }],
[2004, { name: 'Shared Parent', note: 'Mejiro Ryan' }],
[2005, { name: 'Shared Parent' }],
[2006, { name: 'Shared Parent' }],
[2007, { name: 'Shared Parent', note: 'Symboli Rudolf' }],
[2008, { name: 'Shared Parent', note: 'Tosho Boy' }],
[2009, { name: 'Shared Parent', note: "Brian's Time" }],
[2010, { name: 'Shared Parent', note: 'Gold Allure' }],
[2011, { name: 'Shared Parent', note: 'Soccer Boy' }],
[2101, { name: 'Shared Grandparent', note: 'Sunday Silence (grandsire)' }],
[2102, { name: 'Shared Grandparent', note: 'Maruzensky (damsire)' }],
[2103, { name: 'Shared Grandparent', note: 'Roberto (grandsire)' }],
[2104, { name: 'Shared Grandparent', note: 'Blushing Groom (damsire)' }],
[2105, { name: 'Shared Grandparent', note: 'Tesco Boy (damsire)' }],
[2106, { name: 'Shared Grandparent', note: 'Northern Taste (damsire)' }],
[2107, { name: 'Shared Grandparent', note: 'Royal Ski (damsire)' }],
[2108, { name: 'Shared Grandparent', note: 'Never Beat (damsire)' }],
[2109, { name: 'Shared Grandparent', note: 'Affirmed (damsire)' }],
[2110, { name: 'Shared Grandparent', note: 'Sancy (damsire)' }],
[2111, { name: 'Shared Grandparent', note: 'Last Tycoon (grandsire)' }],
[2201, { name: 'Parent/Child' }],
[2202, { name: 'Parent/Child' }],
[2203, { name: 'Grandparent/Grandchild' }],
[2204, { name: 'Parent/Child' }],
[2205, { name: 'Grandparent/Grandchild' }],
[2206, { name: 'Parent/Child' }],
[2208, { name: 'Parent/Child' }],
[2209, { name: 'Parent/Child' }],
[2210, { name: 'Parent/Child' }],
[2211, { name: 'Grandparent/Grandchild' }],
[2212, { name: 'Parent/Child' }],
[2213, { name: 'Parent/Child' }],
[2214, { name: 'Grandparent/Grandchild' }],
[2215, { name: 'Grandparent/Grandchild' }],
[2216, { name: 'Grandparent/Grandchild' }],
[2217, { name: 'Grandparent/Grandchild' }],
[2218, { name: 'Parent/Child' }],
[2301, { name: 'Grandparent/Grandchild' }],
[2302, { name: 'Grandparent/Grandchild' }],
[2303, { name: 'Grandparent/Grandchild' }],
[2401, { name: 'Racing Generation', note: 'Pre-1980' }],
[2402, { name: 'Racing Generation', note: '1983-86' }],
[2403, { name: 'Racing Generation', note: '1987-90' }],
[2404, { name: 'Racing Generation', note: '1990-93' }],
[2405, { name: 'Racing Generation', note: '1995-97' }],
[2406, { name: 'Racing Generation', note: '1997-99' }],
[2407, { name: 'Racing Generation', note: '1998-2004' }],
[2408, { name: 'Racing Generation', note: '2001-04' }],
[2409, { name: 'Racing Generation', note: '2006-10' }],
[2410, { name: 'Racing Generation' }],
[2411, { name: 'Racing Generation' }],
[2412, { name: 'Racing Generation' }],
[2413, { name: 'Racing Generation' }],
[2501, { name: 'G1 Winner', note: 'February Stakes' }],
[2502, { name: 'G1 Winner', note: 'Takamatsunomiya Kinen' }],
[2503, { name: 'G1 Winner', note: 'Osaka Hai' }],
[2504, { name: 'G1 Winner', note: 'Oka Sho' }],
[2505, { name: 'G1 Winner', note: 'Satsuki Sho' }],
[2506, { name: 'G1 Winner', note: 'Tenno Sho (Spring)' }],
[2507, { name: 'G1 Winner', note: 'NHK Mile Cup' }],
[2508, { name: 'G1 Winner', note: 'Victoria Mile' }],
[2509, { name: 'G1 Winner', note: 'Japanese Oaks' }],
[2510, { name: 'G1 Winner', note: 'Tokyo Yushun (Japanese Derby)' }],
[2511, { name: 'G1 Winner', note: 'Yasuda Kinen' }],
[2512, { name: 'G1 Winner', note: 'Takarazuka Kinen' }],
[2513, { name: 'G1 Winner', note: 'Sprinters Stakes' }],
[2514, { name: 'G1 Winner', note: 'Shuka Sho' }],
[2515, { name: 'G1 Winner', note: 'Kikuka Sho' }],
[2516, { name: 'G1 Winner', note: 'Tenno Sho (Autumn)' }],
[2517, { name: 'G1 Winner', note: 'Queen Elizabeth II Cup' }],
[2518, { name: 'G1 Winner', note: 'Mile Championship' }],
[2519, { name: 'G1 Winner', note: 'Japan Cup' }],
[2520, { name: 'G1 Winner', note: 'Champions Cup' }],
[2521, { name: 'G1 Winner', note: 'Hanshin Juvenile Fillies' }],
[2522, { name: 'G1 Winner', note: 'Asahi Hai Futurity Stakes' }],
[2523, { name: 'G1 Winner', note: 'Arima Kinen' }],
[2524, { name: 'G1 Winner', note: 'Hopeful Stakes' }],
[2525, { name: 'G1 Winner', note: 'Teio Sho' }],
[2526, { name: 'G1 Winner', note: 'Japan Dirt Derby' }],
[2527, { name: 'G1 Winner', note: 'JBC Ladies Classic' }],
[2528, { name: 'G1 Winner', note: 'JBC Sprint' }],
[2529, { name: 'G1 Winner', note: 'JBC Classic' }],
[2530, { name: 'G1 Winner', note: 'Tokyo Daishoten' }],
[2531, { name: 'G1 Winner', note: 'Kawasaki Kinen' }],
[2532, { name: 'G1 Winner', note: 'Mile Championship Nambu Hai' }],
[2533, { name: 'G1 Winner', note: 'Kashiwa Kinen' }],
[2534, { name: 'G1 Winner', note: 'Zen-Nippon Junior Yushun' }],
[2601, { name: 'Stallion' }],
[2602, { name: 'Mare' }],
[2702, { name: '7 G1 + Triple Crown winner' }],
[2801, { name: 'Classic Year', note: '1986' }],
[2802, { name: 'Classic Year', note: '1987' }],
[2803, { name: 'Classic Year', note: '1990' }],
[2804, { name: 'Classic Year', note: '1991' }],
[2805, { name: 'Classic Year', note: '1992' }],
[2806, { name: 'Classic Year', note: '1993' }],
[2807, { name: 'Classic Year', note: '1994' }],
[2808, { name: 'Classic Year', note: '1995' }],
[2809, { name: 'Classic Year', note: '1996' }],
[2810, { name: 'Classic Year', note: '1997' }],
[2811, { name: 'Classic Year', note: '1998' }],
[2812, { name: 'Classic Year', note: '1999' }],
[2813, { name: 'Classic Year', note: '2000' }],
[2814, { name: 'Classic Year', note: '2001' }],
[2815, { name: 'Classic Year', note: '2007' }],
[2816, { name: 'Classic Year', note: '2009' }],
[2817, { name: 'Classic Year', note: '2010' }],
[2818, { name: 'Classic Year' }],
[2819, { name: 'Classic Year' }],
[2820, { name: 'Classic Year' }],
[2821, { name: 'Classic Year' }],
[2822, { name: 'Classic Year', note: '2002' }],
[2823, { name: 'Classic Year' }],
[2824, { name: 'Classic Year' }],
[2825, { name: 'Classic Year' }],
[2826, { name: 'Classic Year' }],
[2827, { name: 'Classic Year' }],
[2828, { name: 'Classic Year' }],
[2829, { name: 'Classic Year' }],
[2830, { name: 'Classic Year' }],
[2831, { name: 'Classic Year' }],
[2832, { name: 'Classic Year' }],
[2833, { name: 'Classic Year' }],
[3201, { name: 'Birth Month', note: 'January' }],
[3202, { name: 'Birth Month', note: 'February' }],
[3203, { name: 'Birth Month', note: 'March' }],
[3204, { name: 'Birth Month', note: 'April' }],
[3205, { name: 'Birth Month', note: 'May' }],
[3206, { name: 'Birth Month', note: 'June' }],
[3301, { name: 'No G1 Wins' }],
[3501, { name: 'Classic Triple Crown Runner' }],
[3502, { name: 'Triple Tiara Runner' }],
[3503, { name: 'Spring Triple Crown Runner' }],
[3504, { name: 'Autumn Triple Crown Runner' }],
[101, { name: 'Tracen Class' }],
[102, { name: 'Tracen Class' }],
[103, { name: 'Tracen Class' }],
[104, { name: 'Tracen Class' }],
[105, { name: 'Tracen Class' }],
[106, { name: 'Tracen Class' }],
[107, { name: 'Tracen Class' }],
[201, { name: 'Residence', note: 'Miho Dorm' }],
[202, { name: 'Residence', note: 'Ritto Dorm' }],
[203, { name: 'Residence', note: 'Lives Alone' }],
[204, { name: 'Residence' }],
[205, { name: 'Residence', note: 'Not In Dorm' }],
[301, { name: 'Roommate' }],
[302, { name: 'Roommate' }],
[303, { name: 'Roommate' }],
[304, { name: 'Roommate' }],
[305, { name: 'Roommate' }],
[306, { name: 'Roommate' }],
[307, { name: 'Roommate' }],
[308, { name: 'Roommate' }],
[309, { name: 'Roommate' }],
[310, { name: 'Roommate' }],
[311, { name: 'Roommate' }],
[312, { name: 'Roommate' }],
[313, { name: 'Roommate' }],
[314, { name: 'Roommate' }],
[315, { name: 'Roommate' }],
[316, { name: 'Roommate' }],
[317, { name: 'Roommate' }],
[318, { name: 'Roommate' }],
[319, { name: 'Roommate' }],
[320, { name: 'Roommate' }],
[321, { name: 'Roommate' }],
[322, { name: 'Roommate' }],
[323, { name: 'Roommate' }],
[324, { name: 'Roommate' }],
[325, { name: 'Roommate' }],
[326, { name: 'Roommate' }],
[327, { name: 'Roommate' }],
[328, { name: 'Roommate' }],
[329, { name: 'Roommate' }],
[330, { name: 'Roommate' }],
[331, { name: 'Roommate' }],
[332, { name: 'Roommate' }],
[333, { name: 'Roommate' }],
[334, { name: 'Roommate' }],
[335, { name: 'Roommate' }],
[336, { name: 'Roommate' }],
[337, { name: 'Roommate' }],
[338, { name: 'Roommate' }],
[339, { name: 'Roommate' }],
[340, { name: 'Roommate' }],
[341, { name: 'Roommate' }],
[342, { name: 'Roommate' }],
[343, { name: 'Roommate' }],
[344, { name: 'Roommate' }],
[345, { name: 'Roommate' }],
[346, { name: 'Roommate' }],
[347, { name: 'Roommate' }],
[348, { name: 'Roommate' }],
[349, { name: 'Roommate' }],
[350, { name: 'Roommate' }],
[351, { name: 'Roommate' }],
[352, { name: 'Roommate' }],
[353, { name: 'Roommate' }],
[354, { name: 'Roommate' }],
[355, { name: 'Roommate' }],
[356, { name: 'Roommate' }],
[357, { name: 'Roommate' }],
[358, { name: 'Roommate' }],
[359, { name: 'Roommate' }],
[360, { name: 'Roommate' }],
[361, { name: 'Roommate' }],
[362, { name: 'Roommate' }],
[363, { name: 'Roommate' }],
[364, { name: 'Roommate' }],
[365, { name: 'Roommate' }],
[366, { name: 'Roommate' }],
[367, { name: 'Roommate' }],
[368, { name: 'Roommate' }],
[369, { name: 'Roommate' }],
[370, { name: 'Roommate' }],
[371, { name: 'Roommate' }],
[372, { name: 'Roommate' }],
[373, { name: 'Roommate' }],
[374, { name: 'Roommate' }],
[375, { name: 'Roommate' }],
[376, { name: 'Roommate' }],
[377, { name: 'Roommate' }],
[378, { name: 'Roommate' }],
[379, { name: 'Roommate' }],
[380, { name: 'Roommate' }],
[381, { name: 'Roommate' }],
[382, { name: 'Roommate' }],
[383, { name: 'Roommate' }],
[384, { name: 'Roommate' }],
[385, { name: 'Roommate' }],
[386, { name: 'Roommate' }],
[387, { name: 'Roommate' }],
[388, { name: 'Roommate' }],
[401, { name: 'Clique', note: 'Golden Generation' }],
[402, { name: 'Clique' }],
[403, { name: 'Clique' }],
[404, { name: 'Clique' }],
[405, { name: 'Clique' }],
[406, { name: 'Clique', note: 'Old People' }],
[407, { name: 'Clique' }],
[408, { name: 'Clique' }],
[409, { name: 'Clique' }],
[410, { name: 'Clique' }],
[411, { name: 'Clique' }],
[412, { name: 'Clique' }],
[413, { name: 'Clique' }],
[414, { name: 'Clique' }],
[415, { name: 'Clique' }],
[416, { name: 'Clique' }],
[417, { name: 'Clique' }],
[418, { name: 'Clique' }],
[419, { name: 'Clique' }],
[420, { name: 'Clique' }],
[421, { name: 'Clique' }],
[422, { name: 'Clique' }],
[423, { name: 'Clique' }],
[424, { name: 'Clique' }],
[501, { name: 'Story Group', note: 'Student Council' }],
[502, { name: 'Story Group', note: 'Golden Generation' }],
[503, { name: 'Story Group', note: 'Mejiro Family' }],
[504, { name: 'Story Group', note: 'Rich People' }],
[505, { name: 'Story Group', note: 'Dorm Leaders' }],
[506, { name: 'Story Group', note: 'Heisei Big Three' }],
[507, { name: 'Story Group', note: 'BNW' }],
[511, { name: 'Story Group', note: '"Sakura"' }],
[512, { name: 'Story Group' }],
[513, { name: 'Story Group', note: 'Team Canopus' }],
[514, { name: 'Story Group', note: 'Runaway Bakas' }],
[515, { name: 'Story Group', note: 'Kita and Dia' }],
[516, { name: 'Story Group', note: 'McQueen and Dia' }],
[517, { name: 'Story Group', note: 'Teio and Kitasan' }],
[518, { name: 'Story Group', note: 'Notable Front Runners' }],
[519, { name: 'Story Group' }],
[520, { name: 'Story Group', note: 'Notable Dirt Runners' }],
[521, { name: 'Story Group' }],
[522, { name: 'Story Group', note: 'Satono Family' }],
[523, { name: 'Story Group', note: 'Team Sirius' }],
[524, { name: 'Story Group', note: 'Team Rigil' }],
[525, { name: 'Story Group', note: 'Team Spica' }],
[526, { name: 'Story Group', note: 'Notable Runaways' }],
[3505, { name: 'Ran on Dirt' }],
[3506, { name: 'Overseas' }],
[3508, { name: 'Ran a Sprint' }],
[2901, { name: 'Strategy', note: 'Front Runner' }],
[2902, { name: 'Strategy', note: 'Pace Chaser' }],
[2903, { name: 'Strategy', note: 'Late Surger' }],
[2904, { name: 'Strategy', note: 'End Closer' }],
[3001, { name: 'Distance', note: 'Sprint' }],
[3002, { name: 'Distance', note: 'Mile' }],
[3003, { name: 'Distance', note: 'Medium' }],
[3004, { name: 'Distance', note: 'Long' }],
[3101, { name: 'Surface', note: 'Turf' }],
[3102, { name: 'Surface', note: 'Dirt' }],
[508, { name: 'Haru Urara', note: 'Haru Urara' }],
[509, { name: 'Haru Urara', note: 'Haru Urara' }],
[510, { name: 'Haru Urara', note: 'Haru Urara' }],
[527, { name: 'Haru Urara', note: 'Haru Urara' }],
[528, { name: 'Haru Urara', note: 'Haru Urara' }],
[529, { name: 'Haru Urara', note: 'Haru Urara' }],
[530, { name: 'Haru Urara', note: 'Haru Urara' }],
[531, { name: 'Haru Urara', note: 'Haru Urara' }],
[532, { name: 'Haru Urara', note: 'Haru Urara' }],
[533, { name: 'Haru Urara', note: 'Haru Urara' }],
[534, { name: 'Haru Urara', note: 'Haru Urara' }],
[535, { name: 'Haru Urara', note: 'Haru Urara' }],
[536, { name: 'Haru Urara', note: 'Haru Urara' }],
[537, { name: 'Haru Urara', note: 'Haru Urara' }],
[538, { name: 'Haru Urara', note: 'Haru Urara' }],
[539, { name: 'Haru Urara', note: 'Haru Urara' }],
[540, { name: 'Haru Urara', note: 'Haru Urara' }],
[541, { name: 'Haru Urara', note: 'Haru Urara' }],
[542, { name: 'Haru Urara', note: 'Haru Urara' }],
[543, { name: 'Haru Urara', note: 'Haru Urara' }],
[544, { name: 'Haru Urara', note: 'Haru Urara' }],
[545, { name: 'Haru Urara', note: 'Haru Urara' }],
[546, { name: 'Haru Urara', note: 'Haru Urara' }],
[547, { name: 'Haru Urara', note: 'Haru Urara' }],
[548, { name: 'Haru Urara', note: 'Haru Urara' }],
[549, { name: 'Haru Urara', note: 'Haru Urara' }],
[550, { name: 'Haru Urara', note: 'Haru Urara' }],
[551, { name: 'Haru Urara', note: 'Haru Urara' }],
]);

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>