Compare commits

..

3 Commits

Author SHA1 Message Date
zephyr 2c1b4b9261 zenno/doc/race: lint fix
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-07-18 16:58:20 -04:00
zephyr e5128b6783 zenno/race/spurt: add dueling info 2026-07-18 16:58:12 -04:00
zephyr b22c8307fd zenno/race: common component for calculator outputs 2026-07-18 16:38:07 -04:00
5 changed files with 74 additions and 47 deletions
-10
View File
@@ -215,16 +215,6 @@
{/if} {/if}
{/snippet} {/snippet}
{#snippet docref(h: string)}
<p class="ml-auto text-right text-sm italic">
<a
href={`https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=${h}`}
target="_blank"
rel="noopener noreferrer">ref &UpperRightArrow;</a
>
</p>
{/snippet}
<Article> <Article>
{#snippet head()} {#snippet head()}
<Sec h={1} id="top" class="text-center">Race Mechanics Charts</Sec> <Sec h={1} id="top" class="text-center">Race Mechanics Charts</Sec>
+32
View File
@@ -0,0 +1,32 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { ClassValue } from 'svelte/elements';
interface Props {
title: Snippet | string;
children: Snippet;
additional?: Snippet;
class?: ClassValue | null;
}
let { title, children, additional, class: className }: Props = $props();
const cl = $derived([
'm-2 flex flex-col rounded-md border p-2 text-center shadow-sm transition-shadow hover:shadow-md',
className,
]);
</script>
<div class={cl}>
<span class="block">
{#if typeof title === 'string'}
{title}
{:else}
{@render title()}
{/if}
</span>
<span class="block text-xl">
{@render children()}
</span>
{@render additional?.()}
</div>
+3 -3
View File
@@ -3,6 +3,7 @@
import * as race from '$lib/race'; import * as race from '$lib/race';
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';
let rawPower = $state(1200); let rawPower = $state(1200);
let rawGuts = $state(1200); let rawGuts = $state(1200);
@@ -284,14 +285,13 @@
</form> </form>
{#snippet info(name: string, unit: string, val: number, val2?: number)} {#snippet info(name: string, unit: string, val: number, val2?: number)}
<div class="m-2 max-w-72 flex-1 flex-col rounded-md border p-2 text-center shadow-sm transition-shadow hover:shadow-md"> <CalcInfo title={name} class="max-w-72 flex-1">
<span class="block">{name}</span>
{#if val2 == null} {#if val2 == null}
<span class="block text-xl">{val.toFixed(3)} {unit}</span> <span class="block text-xl">{val.toFixed(3)} {unit}</span>
{:else} {:else}
<span class="block text-xl">{val.toFixed(3)} &ndash; {val2.toFixed(3)} {unit}</span> <span class="block text-xl">{val.toFixed(3)} &ndash; {val2.toFixed(3)} {unit}</span>
{/if} {/if}
</div> </CalcInfo>
{/snippet} {/snippet}
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center"> <div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
+10 -10
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { HORSE_LENGTH, speedGain } from '$lib/race'; import { HORSE_LENGTH, speedGain } from '$lib/race';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import CalcInfo from '../CalcInfo.svelte';
interface Props { interface Props {
children: Snippet; children: Snippet;
@@ -21,13 +22,12 @@
const text = $derived(gain.map(fmtp).join(' ')); const text = $derived(gain.map(fmtp).join(' '));
</script> </script>
<div <CalcInfo title={children} class="max-w-80 flex-1">
class="m-2 flex h-full w-full max-w-80 flex-1 flex-col rounded-md border p-2 text-center shadow-sm transition-shadow hover:shadow-md" {text} L
> {#snippet additional()}
<div class="block">{@render children()}</div> <div class="flex flex-row">
<span class="block text-xl">{text} L</span> <span class="flex-1 text-xs">{fmtp(speed)} m/s</span>
<div class="flex flex-row"> <span class="flex-1 text-xs">{dur.toFixed(3)} s</span>
<span class="flex-1 text-xs">{fmtp(speed)} m/s</span> </div>
<span class="flex-1 text-xs">{dur.toFixed(3)} s</span> {/snippet}
</div> </CalcInfo>
</div>
+29 -24
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { ComputedSeries } from '$lib/chart'; import type { ComputedSeries } from '$lib/chart';
import { AptitudeLevel, inverseSpurtSpeed, RunningStyle, spurtSpeed, Stat } from '$lib/race'; import { AptitudeLevel, duelSpeedMod, inverseSpurtSpeed, RunningStyle, spurtSpeed, Stat } from '$lib/race';
import StatChart from '$lib/StatChart.svelte'; import StatChart from '$lib/StatChart.svelte';
import CalcInfo from '../CalcInfo.svelte';
const aptsList = Object.entries(AptitudeLevel).filter(([, val]) => typeof val === 'number'); const aptsList = Object.entries(AptitudeLevel).filter(([, val]) => typeof val === 'number');
const stylesList = [ const stylesList = [
@@ -25,6 +26,7 @@
const speedStat = $derived(rawSpeed + careerMod); const speedStat = $derived(rawSpeed + careerMod);
const gutsStat = $derived(rawGuts + careerMod); const gutsStat = $derived(rawGuts + careerMod);
const speed = $derived(spurtSpeed(speedStat, gutsStat, style, distanceApt, raceLen)); const speed = $derived(spurtSpeed(speedStat, gutsStat, style, distanceApt, raceLen));
const duelBonus = $derived(duelSpeedMod(gutsStat));
const sProf = $derived([ const sProf = $derived([
inverseSpurtSpeed(speed, gutsStat, RunningStyle.FrontRunner, AptitudeLevel.S, raceLen) - careerMod, inverseSpurtSpeed(speed, gutsStat, RunningStyle.FrontRunner, AptitudeLevel.S, raceLen) - careerMod,
@@ -44,17 +46,30 @@
skillSpeeds.map((v) => [v, inverseSpurtSpeed(speed + v, gutsStat, opponentStyle, AptitudeLevel.S, raceLen) - careerMod]), skillSpeeds.map((v) => [v, inverseSpurtSpeed(speed + v, gutsStat, opponentStyle, AptitudeLevel.S, raceLen) - careerMod]),
); );
const y: Array<ComputedSeries | null> = $derived([ const speedChart: Array<ComputedSeries | null> = $derived([
{ label: 'Aptitude S', y: (x) => spurtSpeed(x, gutsStat, style, AptitudeLevel.S, raceLen) }, { label: 'Aptitude S', y: (x) => spurtSpeed(x, gutsStat, style, AptitudeLevel.S, raceLen) },
{ label: 'Aptitude A', y: (x) => spurtSpeed(x, gutsStat, style, AptitudeLevel.A, raceLen) }, {
distanceApt < AptitudeLevel.A label: `Aptitude ${AptitudeLevel[distanceApt]} + Dueling`,
y: (x) => spurtSpeed(x, gutsStat, style, distanceApt, raceLen) + duelBonus,
},
distanceApt !== AptitudeLevel.S
? { label: `Aptitude ${AptitudeLevel[distanceApt]}`, y: (x) => spurtSpeed(x, gutsStat, style, distanceApt, raceLen) } ? { label: `Aptitude ${AptitudeLevel[distanceApt]}`, y: (x) => spurtSpeed(x, gutsStat, style, distanceApt, raceLen) }
: null, : null,
]); ]);
const gutsChart: Array<ComputedSeries | null> = $derived([
{ label: 'Aptitude S', y: (x) => spurtSpeed(speedStat, x, style, AptitudeLevel.S, raceLen) },
{
label: `Aptitude ${AptitudeLevel[distanceApt]} + Dueling`,
y: (x) => spurtSpeed(speedStat, x, style, distanceApt, raceLen) + duelSpeedMod(x),
},
distanceApt !== AptitudeLevel.S
? { label: `Aptitude ${AptitudeLevel[distanceApt]}`, y: (x) => spurtSpeed(speedStat, x, style, distanceApt, raceLen) }
: null,
]);
const range: [number, number] = $derived([ const range: [number, number] = $derived([
spurtSpeed(200, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.C, raceLen), spurtSpeed(200, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.C, raceLen),
spurtSpeed(2000, gutsStat, RunningStyle.EndCloser, AptitudeLevel.S, raceLen), spurtSpeed(2000, gutsStat, RunningStyle.EndCloser, AptitudeLevel.S, raceLen) + duelBonus,
]); ]);
</script> </script>
@@ -93,9 +108,10 @@
<input type="checkbox" id="isCareer" role="switch" bind:checked={isCareer} class="min-h-6 min-w-6 align-middle" /> <input type="checkbox" id="isCareer" role="switch" bind:checked={isCareer} class="min-h-6 min-w-6 align-middle" />
</div> </div>
</div> </div>
<span class="mt-8 block w-full text-center text-lg"> <div class="mx-auto mt-8 flex max-w-3xl flex-col md:flex-row">
Target spurt speed: {speed.toFixed(3)} m/s <CalcInfo title="Target Spurt Speed" class="flex-1">{speed.toFixed(3)} m/s</CalcInfo>
</span> <CalcInfo title="Dueling Bonus" class="flex-1">+{duelBonus.toFixed(3)} m/s</CalcInfo>
</div>
{#each [[AptitudeLevel.A, aProf] as const, [AptitudeLevel.S, sProf] as const] as [level, inv] (level)} {#each [[AptitudeLevel.A, aProf] as const, [AptitudeLevel.S, sProf] as const] as [level, inv] (level)}
<div class="mx-auto max-w-3xl"> <div class="mx-auto max-w-3xl">
<span class="mt-8 block w-full"> <span class="mt-8 block w-full">
@@ -103,14 +119,7 @@
</span> </span>
<div class="flex flex-col md:flex-row"> <div class="flex flex-col md:flex-row">
{#each stylesList as [styleName, s] (s)} {#each stylesList as [styleName, s] (s)}
<div <CalcInfo title={styleName} class={['flex-1', { 'border-2': s === style }]}>{inv[s]}</CalcInfo>
class={['m-2 flex-1 rounded-md border shadow-sm transition-shadow hover:shadow-md', s === style ? 'border-2' : null]}
>
<div class="h-full w-full flex-col text-center">
<span class="block text-lg">{styleName}</span>
<span class="block text-2xl">{inv[s]}</span>
</div>
</div>
{/each} {/each}
</div> </div>
</div> </div>
@@ -127,15 +136,11 @@
</span> </span>
<div class="flex flex-col md:flex-row"> <div class="flex flex-col md:flex-row">
{#each skillProf as [v, inv] (v)} {#each skillProf as [v, inv] (v)}
<div class="m-2 flex-1 rounded-md border shadow-sm transition-shadow hover:shadow-md"> <CalcInfo title={`+${v.toFixed(2)}`} class="flex-1">{inv}</CalcInfo>
<div class="h-full w-full flex-col text-center">
<span class="block text-lg">+{v.toFixed(2)}</span>
<span class="block text-2xl">{inv}</span>
</div>
</div>
{/each} {/each}
</div> </div>
</div> </div>
<div class="mx-auto h-60 max-w-3xl place-content-center py-4 md:h-96"> <div class="mx-auto grid h-96 grid-cols-1 py-4 md:grid-cols-2">
<StatChart stat={Stat.Speed} {y} yLabel="Spurt Speed (m/s)" xRule={speedStat} {range} /> <StatChart stat={Stat.Speed} y={speedChart} yLabel="Spurt Speed (m/s)" xRule={speedStat} {range} />
<StatChart stat={Stat.Guts} y={gutsChart} yLabel="Spurt Speed (m/s)" xRule={gutsStat} {range} />
</div> </div>