Author SHA1 Message Date
zephyr 9faabbada5 zenno/chara/convo: integrate umadump talk gallery
Fixes #47.
2026-09-22 22:18:51 -04:00
zephyr 7ca6950bc1 zenno/chara/convo: working on using umadump talk gallery 2026-09-22 21:46:30 -04:00
zephyr 86583e68d1 zenno: wrapper component for race length inputs
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline failed
2026-09-22 10:59:29 -04:00
zephyr fde5b4fd20 zenno: update types for new api fields 2026-09-21 10:31:14 -04:00
zephyr 1cf3af5f79 zenno: update full charge info with new knowledge
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-09-21 09:48:56 -04:00
13 changed files with 151 additions and 90 deletions
+20
View File
@@ -0,0 +1,20 @@
<script lang="ts">
interface Props {
value: number;
}
let { value = $bindable() }: Props = $props();
</script>
<div class="flex">
<input
class="my-auto w-full grow justify-self-center-safe"
type="range"
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value
/>
<span class="my-auto flex-none pl-4 text-sm">{value}m</span>
</div>
+4
View File
@@ -4,6 +4,10 @@ import type { RegionalName } from '$lib/regional-name';
* Lobby conversation data. * Lobby conversation data.
*/ */
export interface Conversation { export interface Conversation {
/**
* Gallery ID.
*/
id: number;
/** /**
* Character who owns the conversation as a gallery entry. * Character who owns the conversation as a gallery entry.
*/ */
+16
View File
@@ -14,6 +14,10 @@ export interface Skill {
* Regional skil description. * Regional skil description.
*/ */
description: string; description: string;
/**
* Other skills in the skill group.
*/
group_skills: GroupSkill[];
/** /**
* Skill group ID. * Skill group ID.
*/ */
@@ -58,6 +62,18 @@ export interface Skill {
icon_id: number; icon_id: number;
} }
/**
* Skill reference in a skill group.
*/
export interface GroupSkill {
/** Skill ID. */
skill_id: number;
/** Group rate, as in `Skill`. */
group_rate: Skill['group_rate'];
/** Rarity, as in `Skill`. */
rarity: Skill['rarity'];
}
export enum DurationScale { export enum DurationScale {
Direct = 1, Direct = 1,
FrontDistance = 2, FrontDistance = 2,
+3 -2
View File
@@ -771,16 +771,17 @@ export function conservePowerAccel(
distanceType: Distance, distanceType: Distance,
style: RunningStyle, style: RunningStyle,
rawPower: number, rawPower: number,
basePower: number,
powerSkillBonus?: number, powerSkillBonus?: number,
spotStruggled?: boolean, spotStruggled?: boolean,
rushed?: boolean, rushed?: boolean,
): number { ): number {
const power = rawPower + (powerSkillBonus ?? 0); const power = basePower + 0.5 * Math.max(rawPower - 1200, 0) + (powerSkillBonus ?? 0);
if (power <= 1200) { if (power <= 1200) {
return 0; return 0;
} }
const sdc = conserveStratDistCoef[style][distanceType]; const sdc = conserveStratDistCoef[style][distanceType];
const activity = rushed ? 0.8 : spotStruggled ? 0.98 : 1; const activity = (rushed ? 0.8 : 1) * (spotStruggled ? 0.98 : 1);
return Math.sqrt((power - 1200) * 130) * 0.001 * sdc * activity; return Math.sqrt((power - 1200) * 130) * 0.001 * sdc * activity;
} }
+14
View File
@@ -0,0 +1,14 @@
import * as z from 'zod';
const Entry = z.strictObject({
home_story_trigger_id: z.int32(),
new_flag: z.literal(0).or(z.literal(1)),
});
export type Entry = z.infer<typeof Entry>;
export const Gallery = z.object({
talk_gallery_list: z.array(Entry),
});
export type Gallery = z.infer<typeof Gallery>;
+53 -8
View File
@@ -9,25 +9,51 @@
requiredCharas, requiredCharas,
convoCharas, convoCharas,
} from '$lib/data/convo'; } from '$lib/data/convo';
import * as talk from '$lib/umadump/talk';
import CharaPick from '$lib/CharaPick.svelte'; import CharaPick from '$lib/CharaPick.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import CharaIcon from '$lib/CharaIcon.svelte'; import CharaIcon from '$lib/CharaIcon.svelte';
import { SvelteSet } from 'svelte/reactivity';
let conversations: Conversation[] = $state([]); let allConvos: Conversation[] = $state([]);
const convoMap = $derived(byChara(conversations)); const popular = $derived(groupPopulars(allConvos));
const popular = $derived(groupPopulars(conversations));
let characters: Character[] = $state([]); let characters: Character[] = $state([]);
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
const names = $derived(charaNames(characters)); const names = $derived(charaNames(characters));
const minSuggest = 8; const minSuggest = 8;
onMount(async () => { onMount(async () => {
[conversations, characters] = await Promise.all([conversation(), character()]); [allConvos, characters] = await Promise.all([conversation(), character()]);
}); });
let chara: Character | undefined = $state(); let chara: Character | undefined = $state();
let convo = $state(1); let convo = $state(1);
let talkFiles: FileList | undefined = $state();
let readIDs = new SvelteSet<number>();
let talkFileInvalid = $state(false);
async function setRead(f: FileList) {
talkFiles = f;
readIDs.clear();
talkFileInvalid = false;
if (talkFiles == null || talkFiles.length === 0) {
return;
}
const t = await talkFiles[0].text();
const d = JSON.parse(t);
const g = talk.Gallery.safeDecode(d);
if (!g.success) {
talkFileInvalid = true;
console.warn(g.error);
return;
}
for (const e of g.data.talk_gallery_list) {
readIDs.add(e.home_story_trigger_id);
}
}
const conversations = $derived(allConvos.filter((c) => !readIDs.has(c.id)));
const convoMap = $derived(byChara(conversations));
const selCharas = $derived(characters.filter((c) => convoMap.has(c.chara_id)));
const options = $derived(convoMap.get(chara?.chara_id ?? 0) ?? []); const options = $derived(convoMap.get(chara?.chara_id ?? 0) ?? []);
const cur = $derived(options.find((c) => c.number === convo)); const cur = $derived(options.find((c) => c.number === convo));
const charaIDs = $derived(cur != null ? convoCharas(cur) : []); const charaIDs = $derived(cur != null ? convoCharas(cur) : []);
@@ -47,12 +73,12 @@
</script> </script>
<h1 class="text-4xl">Lobby Conversations</h1> <h1 class="text-4xl">Lobby Conversations</h1>
<div class="mx-auto mt-8 flex flex-col rounded-md text-center shadow-md ring md:max-w-xl md:flex-row"> <div class="mx-auto mt-8 grid grid-cols-1 gap-4 rounded-md p-4 text-center shadow-md ring md:max-w-xl md:grid-cols-6">
<div class="m-4 flex-1 md:mt-3"> <div class="md:col-span-3">
<label for="chara" class="hidden md:inline">Character</label> <label for="chara" class="hidden md:inline">Character</label>
<CharaPick id="chara" characters={selCharas} class="w-full" bind:value={chara} required /> <CharaPick id="chara" characters={selCharas} class="w-full" bind:value={chara} required />
</div> </div>
<div class="m-4 flex-1 md:mt-3"> <div class="md:col-span-3">
<label for="convo" class="hidden md:inline">Conversation</label> <label for="convo" class="hidden md:inline">Conversation</label>
<select id="convo" bind:value={convo} class="w-full"> <select id="convo" bind:value={convo} class="w-full">
{#each options as opt (opt.number)} {#each options as opt (opt.number)}
@@ -60,6 +86,25 @@
{/each} {/each}
</select> </select>
</div> </div>
<div class="col-span-full">
<label for="talkgallery" class="block">
Select <span class="inline-block font-mono">talk_gallery_data.json</span> to hide read conversations.
</label>
<input
type="file"
class="rounded-full p-4"
accept="application/json"
id="talkgallery"
bind:files={() => talkFiles ?? new DataTransfer().files, setRead}
/>
<button
class="ml-4 rounded-full bg-mist-300 p-2 hover:cursor-pointer dark:bg-mist-900"
onclick={() => setRead(new DataTransfer().files)}>Reset</button
>
</div>
{#if talkFileInvalid}
<div class="col-span-full">Invalid File</div>
{/if}
</div> </div>
{#if cur} {#if cur}
<div class="shadow-sm transition-shadow hover:shadow-md"> <div class="shadow-sm transition-shadow hover:shadow-md">
+11 -29
View File
@@ -26,6 +26,7 @@
import Article from '../Article.svelte'; import Article from '../Article.svelte';
import Sec from '../Sec.svelte'; import Sec from '../Sec.svelte';
import * as skill from '$lib/data/skill'; import * as skill from '$lib/data/skill';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let raceLen = $state(2000); let raceLen = $state(2000);
let secSpeedStyle = $state(RunningStyle.FrontRunner); let secSpeedStyle = $state(RunningStyle.FrontRunner);
@@ -224,10 +225,10 @@
}, },
]); ]);
const fcSeries: ComputedSeries[] = $derived([ const fcSeries: ComputedSeries[] = $derived([
{ label: 'Front Runner', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, x) }, { label: 'Front Runner', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.FrontRunner, raw, x) },
{ label: 'Pace Chaser', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, x) }, { label: 'Pace Chaser', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.PaceChaser, raw, x) },
{ label: 'Late Surger', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, x) }, { label: 'Late Surger', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.LateSurger, raw, x) },
{ label: 'End Closer', y: (x) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, x) }, { label: 'End Closer', y: (x, raw) => conservePowerAccel(distance(raceLen), RunningStyle.EndCloser, raw, x) },
]); ]);
</script> </script>
@@ -514,23 +515,13 @@
plotOptions={{ color: { legend: true } }} plotOptions={{ color: { legend: true } }}
/> />
<div class="mx-auto flex w-full md:max-w-2xl"> <div class="mx-auto flex w-full md:max-w-2xl">
<label class="my-auto hidden flex-none pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label> <label class="my-auto mr-2 hidden flex-none pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
<input <RaceLenSlider bind:value={raceLen} />
class="my-auto max-w-40 flex-1 md:flex-2"
type="range"
id="secSpeedRaceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="my-auto flex-none pl-2 text-sm">{raceLen}m</span>
</div> </div>
</div> </div>
<p> <p>
The exact mechanics of conserving power are unclear at this time. What has been confirmed is that rushed and <a Rushed and <a href="#spot-struggle">spot struggle</a> reduce both the duration and effectiveness of full charge, potentially to
href="#spot-struggle">spot struggle</a zero. In practice, this appears to be a concern primarily on sprints, which already have a -55% penalty to full charge duration.
> both reduce the chance for it to fire.
</p> </p>
<Sec h={3} id="lane-combo">Lane Combo</Sec> <Sec h={3} id="lane-combo">Lane Combo</Sec>
@@ -670,17 +661,8 @@
plotOptions={{ color: { legend: true } }} plotOptions={{ color: { legend: true } }}
/> />
<div class="mx-auto flex w-full md:max-w-2xl"> <div class="mx-auto flex w-full md:max-w-2xl">
<label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label> <label class="my-auto mr-2 hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
<input <RaceLenSlider bind:value={raceLen} />
class="my-auto max-w-40 flex-1 md:flex-2"
type="range"
id="secSpeedRaceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="my-auto flex-1 pl-2 text-sm">{raceLen}m</span>
<label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedStyle">Style</label> <label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedStyle">Style</label>
<select class="my-auto flex-1 text-sm" id="secSpeedStyle" bind:value={secSpeedStyle}> <select class="my-auto flex-1 text-sm" id="secSpeedStyle" bind:value={secSpeedStyle}>
{#each RUNNING_STYLES as [name, style] (style)} {#each RUNNING_STYLES as [name, style] (style)}
+16 -17
View File
@@ -5,6 +5,7 @@
import * as race from '$lib/race'; import * as race from '$lib/race';
import StatChart from '$lib/StatChart.svelte'; import StatChart from '$lib/StatChart.svelte';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
function modemean(x: number, bonus: number, p: number): number { function modemean(x: number, bonus: number, p: number): number {
return x * (1 - p) + x * bonus * p; return x * (1 - p) + x * bonus * p;
@@ -58,10 +59,10 @@
]); ]);
const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }]; const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }];
const conservePower: Array<ComputedSeries | null> = $derived([ const conservePower: Array<ComputedSeries | null> = $derived([
{ label: 'Conserved Power', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, false) }, { label: 'Conserved Power', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, false, false) },
{ label: 'Rushed', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, true) }, { label: 'Rushed', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, false, true) },
styleIsFront styleIsFront
? { label: 'Spot Struggled', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, true, false) } ? { label: 'Spot Struggled', y: (x, raw) => race.conservePowerAccel(distanceType, style, raw, x, 0, true, false) }
: null, : null,
]); ]);
const gutsSpurt: Array<ComputedSeries | null> = $derived([ const gutsSpurt: Array<ComputedSeries | null> = $derived([
@@ -174,16 +175,9 @@
/> />
<div class="mx-auto grid w-full grid-cols-3 gap-y-6 md:max-w-2xl md:grid-cols-7"> <div class="mx-auto grid w-full grid-cols-3 gap-y-6 md:max-w-2xl md:grid-cols-7">
{#if pick?.len} {#if pick?.len}
<input <div class="col-span-3 my-auto mr-4">
class="col-span-2 my-auto w-full justify-self-center-safe" <RaceLenSlider bind:value={raceLen} />
type="range" </div>
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="col-span-1 my-auto pl-2 text-sm">{raceLen}m</span>
{/if} {/if}
{#if pick?.style} {#if pick?.style}
<select class="col-span-1 my-auto text-sm md:col-span-2" id="stylePick" bind:value={style}> <select class="col-span-1 my-auto text-sm md:col-span-2" id="stylePick" bind:value={style}>
@@ -329,11 +323,16 @@
Bonus acceleration at spurt start from the Charge Up/Fully Charged mechanic. Scales with base power plus passive skills, so Bonus acceleration at spurt start from the Charge Up/Fully Charged mechanic. Scales with base power plus passive skills, so
modifiers for ground conditions and career mode do not apply. modifiers for ground conditions and career mode do not apply.
</p> </p>
<p>
This mechanic does not appear to be well understood, in particular how long the acceleration bonus lasts and how the rushed
and spot struggle multipliers apply.
</p>
{@render statChart(race.Stat.Power, conservePower, 'Acceleration (m/s²)', [0, 0.35], { len: true, style: true })} {@render statChart(race.Stat.Power, conservePower, 'Acceleration (m/s²)', [0, 0.35], { len: true, style: true })}
<p>
The duration of Fully Charged does not depend directly on stats, but rather on a hidden <i>conserved power</i> value which
increases per horse throughout the race. Conserved power does not increase while <a href="#spot-struggle">spot struggling</a>,
so for front runners, the duration may scale negatively with guts. It also does not increase while running in any
<a href="#poskeep">position keep mode</a>
other than pace-down, so its duration may have a very slight negative correlation with wit.
<a href="#rushed">Rushed</a> additionally prevents accumulating conserved power, so full charge duration may scale positively with
wit amortized across races. Otherwise, power conservation rate depends on running style.
</p>
<Sec h={2} id="guts">Guts</Sec> <Sec h={2} id="guts">Guts</Sec>
+4 -10
View File
@@ -4,6 +4,7 @@
import * as accel from '$lib/racelib/accel'; import * as accel from '$lib/racelib/accel';
import type { Attachment } from 'svelte/attachments'; import type { Attachment } from 'svelte/attachments';
import CalcInfo from '../CalcInfo.svelte'; import CalcInfo from '../CalcInfo.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawSpeed = $state(1200); let rawSpeed = $state(1200);
let rawPower = $state(1200); let rawPower = $state(1200);
@@ -231,16 +232,9 @@
</div> </div>
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-6"> <div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-6">
<input <div class="col-span-full m-4">
class="col-span-4 m-4 w-full justify-self-center-safe" <RaceLenSlider bind:value={raceLen} />
type="range" </div>
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="col-span-2 my-auto mb-4 pl-2 text-sm">{raceLen}m</span>
<select id="surface" bind:value={surface} class="col-span-2 mx-2 mb-4"> <select id="surface" bind:value={surface} class="col-span-2 mx-2 mb-4">
<option value={race.Surface.Turf}>Turf</option> <option value={race.Surface.Turf}>Turf</option>
<option value={race.Surface.Dirt}>Dirt</option> <option value={race.Surface.Dirt}>Dirt</option>
+4 -10
View File
@@ -4,6 +4,7 @@
import * as accel from '$lib/racelib/accel'; import * as accel from '$lib/racelib/accel';
import type { Attachment } from 'svelte/attachments'; import type { Attachment } from 'svelte/attachments';
import CalcInfo from '../CalcInfo.svelte'; import CalcInfo from '../CalcInfo.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawPower = $state(1200); let rawPower = $state(1200);
let rawGuts = $state(1200); let rawGuts = $state(1200);
@@ -216,16 +217,9 @@
</div> </div>
</div> </div>
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-3"> <div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-3">
<input <div class="col-span-6 mx-4 mt-4">
class="col-span-4 m-4 w-full justify-self-center-safe" <RaceLenSlider bind:value={raceLen} />
type="range" </div>
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="col-span-2 my-auto mb-4 pl-2 text-sm">{raceLen}m</span>
<select id="surface" bind:value={surface} class="col-span-2 m-4"> <select id="surface" bind:value={surface} class="col-span-2 m-4">
<option value={race.Surface.Turf}>Turf</option> <option value={race.Surface.Turf}>Turf</option>
<option value={race.Surface.Dirt}>Dirt</option> <option value={race.Surface.Dirt}>Dirt</option>
+2 -2
View File
@@ -18,6 +18,7 @@
import StatChart from '$lib/StatChart.svelte'; import StatChart from '$lib/StatChart.svelte';
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import SpeedDur from './SpeedDur.svelte'; import SpeedDur from './SpeedDur.svelte';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
let rawPower = $state(1200); let rawPower = $state(1200);
let rawGuts = $state(1200); let rawGuts = $state(1200);
@@ -112,8 +113,7 @@
</select> </select>
</div> </div>
<div class="m-4 md:col-span-2 md:col-start-2"> <div class="m-4 md:col-span-2 md:col-start-2">
<label for="raceLen">Race Distance</label> <RaceLenSlider bind:value={raceLen} />
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
</div> </div>
<div class="m-4 self-center md:col-span-2"> <div class="m-4 self-center md:col-span-2">
<label for="isRunaway" class="mr-1 align-middle">Runaway</label> <label for="isRunaway" class="mr-1 align-middle">Runaway</label>
+2 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { ComputedSeries } from '$lib/chart'; import type { ComputedSeries } from '$lib/chart';
import * as race from '$lib/race'; import * as race from '$lib/race';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
import StatChart from '$lib/StatChart.svelte'; import StatChart from '$lib/StatChart.svelte';
import CalcInfo from '../CalcInfo.svelte'; import CalcInfo from '../CalcInfo.svelte';
@@ -98,8 +99,7 @@
</select> </select>
</div> </div>
<div class="m-4 md:col-start-2"> <div class="m-4 md:col-start-2">
<label for="raceLen">Race Distance</label> <RaceLenSlider bind:value={raceLen} />
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
</div> </div>
<div class="m-4 self-center"> <div class="m-4 self-center">
<label for="isCareer" class="mr-1 align-middle">In Career</label> <label for="isCareer" class="mr-1 align-middle">In Career</label>
+2 -10
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import * as course from '$lib/course'; import * as course from '$lib/course';
import * as race from '$lib/race'; import * as race from '$lib/race';
import RaceLenSlider from '$lib/RaceLenSlider.svelte';
import * as recovery from '$lib/racelib/recovery'; import * as recovery from '$lib/racelib/recovery';
import HPChart from './HPChart.svelte'; import HPChart from './HPChart.svelte';
import InfoPanels from './InfoPanels.svelte'; import InfoPanels from './InfoPanels.svelte';
@@ -277,16 +278,7 @@
</div> </div>
<div class="mx-auto my-2 grid max-w-5xl grid-cols-1 gap-4 rounded-md text-center shadow-md ring md:grid-cols-6"> <div class="mx-auto my-2 grid max-w-5xl grid-cols-1 gap-4 rounded-md text-center shadow-md ring md:grid-cols-6">
<div class="m-4 flex md:col-span-2"> <div class="m-4 flex md:col-span-2">
<input <RaceLenSlider bind:value={raceLen} />
class="col-span-2 my-auto w-full justify-self-center-safe"
type="range"
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="my-auto pl-2 text-sm">{raceLen}m</span>
</div> </div>
<select id="surface" class="m-4" bind:value={surface}> <select id="surface" class="m-4" bind:value={surface}>
<option value={race.Surface.Turf}>Turf</option> <option value={race.Surface.Turf}>Turf</option>