zenno/race/carryover: calculator for late race carrover and accel
This commit is contained in:
@@ -44,6 +44,11 @@ export const PAGES = {
|
|||||||
name: 'Gate Calculator',
|
name: 'Gate Calculator',
|
||||||
description: 'Calculate the importance of acceleration effects at the starting gate.',
|
description: 'Calculate the importance of acceleration effects at the starting gate.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
route: resolve('/race/carryover'),
|
||||||
|
name: 'Carryover Calculator',
|
||||||
|
description: 'Calculate the importance of acceleration and speed carryover at the start of late race.',
|
||||||
|
},
|
||||||
] satisfies Page[],
|
] satisfies Page[],
|
||||||
career: [
|
career: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import * as Plot from '@observablehq/plot';
|
||||||
|
import * as race from '$lib/race';
|
||||||
|
import * as accel from '$lib/racelib/accel';
|
||||||
|
import type { Attachment } from 'svelte/attachments';
|
||||||
|
import CalcInfo from '../CalcInfo.svelte';
|
||||||
|
|
||||||
|
let rawSpeed = $state(1200);
|
||||||
|
let rawPower = $state(1200);
|
||||||
|
let rawGuts = $state(1200);
|
||||||
|
let rawWit = $state(1200);
|
||||||
|
let speedBonus = $state(0);
|
||||||
|
let powerBonus = $state(0);
|
||||||
|
let gutsBonus = $state(0);
|
||||||
|
let witBonus = $state(0);
|
||||||
|
let style = $state(race.RunningStyle.FrontRunner);
|
||||||
|
let surfApt = $state(race.AptitudeLevel.A);
|
||||||
|
let distApt = $state(race.AptitudeLevel.A);
|
||||||
|
let styleApt = $state(race.AptitudeLevel.A);
|
||||||
|
let mood = $state(race.Mood.Normal);
|
||||||
|
let isCareer = $state(false);
|
||||||
|
|
||||||
|
let raceLen = $state(2000);
|
||||||
|
let surface = $state(race.Surface.Turf);
|
||||||
|
let conditions = $state(race.GroundConditions.Firm);
|
||||||
|
let slope = $state(0); // TODO(zeph): where does it end?
|
||||||
|
let allowDAM = $state(false);
|
||||||
|
let threshMod = $state(1);
|
||||||
|
|
||||||
|
const phases = $derived(race.phases(raceLen));
|
||||||
|
const distanceType = $derived(race.distance(raceLen));
|
||||||
|
|
||||||
|
let skillCarry = $state(0);
|
||||||
|
let rawAccels: accel.Skill[] = $state([]);
|
||||||
|
let accelToAdd = $state({ x: 0, accel: 0.2, dur: 3 });
|
||||||
|
const quick = [
|
||||||
|
{
|
||||||
|
name: 'Angling/Beyond',
|
||||||
|
skills: [{ x: 0, accel: 0.4, dur: 5 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Angling/Beyond Inherit',
|
||||||
|
skills: [{ x: 0, accel: 0.2, dur: 3 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Clear',
|
||||||
|
skills: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
function addAccel() {
|
||||||
|
rawAccels.push(accelToAdd);
|
||||||
|
accelToAdd = { x: 0, accel: 0.2, dur: 3 };
|
||||||
|
}
|
||||||
|
function removeAccel(i: number) {
|
||||||
|
rawAccels.splice(i, 1);
|
||||||
|
}
|
||||||
|
function setAccelList(l: accel.Skill[]) {
|
||||||
|
rawAccels = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseSpeed = $derived(race.baseStat(mood, rawSpeed));
|
||||||
|
const basePower = $derived(race.baseStat(mood, rawPower));
|
||||||
|
const baseGuts = $derived(race.baseStat(mood, rawGuts));
|
||||||
|
const baseWit = $derived(race.baseStat(mood, rawWit));
|
||||||
|
const adjSpeed = $derived(race.adjustedStat(race.Stat.Speed, baseSpeed, isCareer, surface, conditions, threshMod));
|
||||||
|
const adjPower = $derived(race.adjustedStat(race.Stat.Power, basePower, isCareer, surface, conditions));
|
||||||
|
const adjGuts = $derived(race.adjustedStat(race.Stat.Guts, baseGuts, isCareer));
|
||||||
|
const adjWit = $derived(race.adjustedStat(race.Stat.Wit, baseWit, isCareer, styleApt));
|
||||||
|
const speed = $derived(race.finalStat(adjSpeed, speedBonus));
|
||||||
|
const power = $derived(race.finalStat(adjPower, powerBonus));
|
||||||
|
const guts = $derived(race.finalStat(adjGuts, gutsBonus));
|
||||||
|
const wit = $derived(race.finalStat(adjWit, witBonus));
|
||||||
|
|
||||||
|
const damCarry = $derived(allowDAM && slope < 0 ? race.downhillAccelMod(slope) : 0);
|
||||||
|
const carry = $derived(skillCarry + damCarry);
|
||||||
|
const midSpeed = $derived.by(() => {
|
||||||
|
const s = race.sectionSpeed(raceLen, baseWit, wit, style, race.Phase.MidRace) satisfies [number, number];
|
||||||
|
const u = slope > 0 ? race.uphillMod(power, slope) : 0;
|
||||||
|
return [s[0] + u, s[1] + u] as const;
|
||||||
|
});
|
||||||
|
const spurtSpeed = $derived(race.spurtSpeed(speed, guts, style, distApt, raceLen));
|
||||||
|
const lateAccel = $derived(race.acceleration(power, style, surfApt, race.Phase.LateRace, distApt, slope > 0));
|
||||||
|
|
||||||
|
function doSeggs(startSpeed: number, accels: accel.Skill[]) {
|
||||||
|
const start: accel.Instant = { x: phases[race.Phase.MidRace], v: startSpeed, t: 0 };
|
||||||
|
return accel.up(start, spurtSpeed, lateAccel, accels).segments;
|
||||||
|
}
|
||||||
|
function last<T>(l: T[]): T {
|
||||||
|
return l[l.length - 1];
|
||||||
|
}
|
||||||
|
const accels = $derived(
|
||||||
|
rawAccels.map((s) => ({ ...s, x: (s.x ?? 0) + phases[race.Phase.MidRace], dur: race.skillDuration(s.dur, raceLen) })),
|
||||||
|
);
|
||||||
|
const seggs = $derived([doSeggs(midSpeed[0] + carry, accels), doSeggs(midSpeed[1] + carry, accels)] as const);
|
||||||
|
const uncarried = $derived([doSeggs(midSpeed[0], accels), doSeggs(midSpeed[1], accels)] as const);
|
||||||
|
const unskilled = $derived([doSeggs(midSpeed[0], []), doSeggs(midSpeed[1], [])] as const);
|
||||||
|
|
||||||
|
const distance = $derived([last(seggs[0]).end.x, last(seggs[1]).end.x] as const);
|
||||||
|
const time = $derived([last(seggs[0]).end.t, last(seggs[1]).end.t] as const);
|
||||||
|
const finishTime = $derived([
|
||||||
|
time[0] + (phases[race.Phase.LateRace] - distance[0]) / spurtSpeed,
|
||||||
|
time[1] + (phases[race.Phase.LateRace] - distance[1]) / spurtSpeed,
|
||||||
|
] as const);
|
||||||
|
function gainLengths(fast: accel.Instant, slow: accel.Instant): number {
|
||||||
|
const m = fast.v * (slow.t - fast.t) + fast.x - slow.x;
|
||||||
|
return m / race.HORSE_LENGTH;
|
||||||
|
}
|
||||||
|
const skillGain = $derived([
|
||||||
|
gainLengths(last(seggs[0]).end, last(unskilled[0]).end),
|
||||||
|
gainLengths(last(seggs[1]).end, last(unskilled[1]).end),
|
||||||
|
]);
|
||||||
|
const carryoverGain = $derived([
|
||||||
|
gainLengths(last(seggs[0]).end, last(uncarried[0]).end),
|
||||||
|
gainLengths(last(seggs[1]).end, last(uncarried[1]).end),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let width = $state(1);
|
||||||
|
let height = $state(1);
|
||||||
|
const plotLines = $derived([
|
||||||
|
{ t: 0, x: phases[race.Phase.MidRace], v: midSpeed[1] + carry, label: 'Fastest Speed' },
|
||||||
|
...seggs[1].map((t) => ({ t: t.end.t, x: t.end.x, v: t.end.v, label: 'Fastest Speed' })),
|
||||||
|
{ t: finishTime[1], x: phases[race.Phase.LateRace], v: spurtSpeed, label: 'Fastest Speed' },
|
||||||
|
{ t: 0, x: phases[race.Phase.MidRace], v: midSpeed[1], label: 'No Carryover' },
|
||||||
|
...uncarried[1].map((t) => ({ t: t.end.t, x: t.end.x, v: t.end.v, label: 'No Carryover' })),
|
||||||
|
{ t: finishTime[1], x: phases[race.Phase.LateRace], v: spurtSpeed, label: 'No Carryover' },
|
||||||
|
{ t: 0, x: phases[race.Phase.MidRace], v: midSpeed[1], label: 'No Skills' },
|
||||||
|
...unskilled[1].map((t) => ({ t: t.end.t, x: t.end.x, v: t.end.v, label: 'No Skills' })),
|
||||||
|
{ t: finishTime[1], x: phases[race.Phase.LateRace], v: spurtSpeed, label: 'No Skills' },
|
||||||
|
]);
|
||||||
|
const makeChart: Attachment = (el) => {
|
||||||
|
$effect(() => {
|
||||||
|
el?.firstChild?.remove();
|
||||||
|
el?.append(
|
||||||
|
Plot.plot({
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
clip: true,
|
||||||
|
x: {
|
||||||
|
domain: [0, finishTime[1]],
|
||||||
|
label: 'Time (s)',
|
||||||
|
line: true,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
domain: [17, 27],
|
||||||
|
label: 'Speed (m/s)',
|
||||||
|
line: true,
|
||||||
|
},
|
||||||
|
color: { legend: true },
|
||||||
|
marks: [Plot.frame(), Plot.line(plotLines, { x: 't', y: 'v', stroke: 'label', strokeWidth: 3 })],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h1 class="text-4xl">Carryover Calculator</h1>
|
||||||
|
<form class="mx-auto my-4 grid max-w-4xl grid-cols-1 gap-4 rounded-md p-4 shadow-sm md:grid-cols-12">
|
||||||
|
<div class="col-span-full grid rounded-md text-center shadow-md ring md:grid-cols-5">
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="speedStat">Speed</label>
|
||||||
|
<input type="number" id="speedStat" bind:value={rawSpeed} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="powerStat">Power</label>
|
||||||
|
<input type="number" id="powerStat" bind:value={rawPower} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="gutsStat">Guts</label>
|
||||||
|
<input type="number" id="gutsStat" bind:value={rawGuts} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="witStat">Wit</label>
|
||||||
|
<input type="number" id="witStat" bind:value={rawWit} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="col-start-1 m-4">
|
||||||
|
<label for="speedBonus">Speed Greens</label>
|
||||||
|
<input type="number" id="speedBonus" bind:value={speedBonus} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="powerBonus">Power Greens</label>
|
||||||
|
<input type="number" id="powerBonus" bind:value={powerBonus} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="gutsBonus">Guts Greens</label>
|
||||||
|
<input type="number" id="gutsBonus" bind:value={gutsBonus} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="witBonus">Wit Greens</label>
|
||||||
|
<input type="number" id="witBonus" bind:value={witBonus} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="col-start-1 m-4">
|
||||||
|
<label for="style">Style</label>
|
||||||
|
<select id="style" required bind:value={style} class="w-full">
|
||||||
|
{#each race.RUNNING_STYLES as [name, style] (style)}
|
||||||
|
<option value={style}>{name}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="distanceApt">{race.Distance[distanceType]}</label>
|
||||||
|
<select id="distanceApt" required bind:value={distApt} class="w-full">
|
||||||
|
{#each race.APTITUDE_LEVELS as apt (apt)}
|
||||||
|
<option value={apt}>{race.AptitudeLevel[apt]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="surfaceApt">{race.Surface[surface]}</label>
|
||||||
|
<select id="surfaceApt" required bind:value={surfApt} class="w-full">
|
||||||
|
{#each race.APTITUDE_LEVELS as apt (apt)}
|
||||||
|
<option value={apt}>{race.AptitudeLevel[apt]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="styleApt">Style</label>
|
||||||
|
<select id="styleApt" required bind:value={styleApt} class="w-full">
|
||||||
|
{#each race.APTITUDE_LEVELS as apt (apt)}
|
||||||
|
<option value={apt}>{race.AptitudeLevel[apt]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="m-4">
|
||||||
|
<label for="mood">Mood</label>
|
||||||
|
<select id="mood" required bind:value={mood} class="w-full">
|
||||||
|
{#each race.MOODS as mood (mood)}
|
||||||
|
<option value={mood}>{race.Mood[mood]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-auto grid max-w-4xl grid-cols-6 rounded-md text-center shadow-md ring md:col-span-6">
|
||||||
|
<input
|
||||||
|
class="col-span-4 m-4 w-full justify-self-center-safe"
|
||||||
|
type="range"
|
||||||
|
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">
|
||||||
|
<option value={race.Surface.Turf}>Turf</option>
|
||||||
|
<option value={race.Surface.Dirt}>Dirt</option>
|
||||||
|
</select>
|
||||||
|
<select id="conditions" bind:value={conditions} class="col-span-2 mx-2 mb-4">
|
||||||
|
{#each race.GROUND_CONDITONS as cond (cond)}
|
||||||
|
<option value={cond}>{race.GroundConditions[cond]}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<select id="slope" bind:value={slope} class="col-span-2 mx-2 mb-4">
|
||||||
|
{#each [2, 1.5, 1, 0, -1, -1.5, -2] as per (per)}
|
||||||
|
<option value={per}>{per > 0 ? `Uphill +${per}` : per < 0 ? `Downhill ${per}` : 'Flat'}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-auto flex w-full max-w-4xl flex-col rounded-md text-center shadow-md ring md:col-span-6">
|
||||||
|
<div class="mx-4 mt-2 flex flex-row gap-2">
|
||||||
|
<span class="w-24 flex-1">Delay (m)</span>
|
||||||
|
<span class="w-24 flex-1">Duration</span>
|
||||||
|
<span class="w-24 flex-1">Accel</span>
|
||||||
|
<span class="w-8 flex-none"><!-- filler to align other columns --></span>
|
||||||
|
</div>
|
||||||
|
{#each rawAccels as skill, i (i)}
|
||||||
|
<div class="mx-4 mt-2 flex flex-row gap-2">
|
||||||
|
<span class="my-auto w-24 flex-1">{skill.x}m</span>
|
||||||
|
<span class="my-auto w-24 flex-1">{skill.dur}s</span>
|
||||||
|
<span class="my-auto w-24 flex-1">{skill.accel}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="my-auto h-8 w-8 flex-none rounded-full bg-mist-300 text-2xl hover:cursor-pointer dark:bg-mist-900"
|
||||||
|
onclick={() => removeAccel(i)}>×</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="mx-4 mt-2 flex flex-row gap-2">
|
||||||
|
<input type="number" class="my-auto w-24 flex-1" min="0" max={raceLen} step="1" bind:value={accelToAdd.x} />
|
||||||
|
<input type="number" class="my-auto w-24 flex-1" min="0.9" step="0.3" bind:value={accelToAdd.dur} />
|
||||||
|
<input type="number" class="my-auto w-24 flex-1" min="0" step="0.05" bind:value={accelToAdd.accel} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="my-auto h-8 w-8 flex-none rounded-full bg-mist-300 text-2xl hover:cursor-pointer dark:bg-mist-900"
|
||||||
|
onclick={addAccel}>+</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 flex flex-row">
|
||||||
|
<span class="my-auto h-0 flex-1 border"></span>
|
||||||
|
<span class="mx-2 flex-none">Quick Setup</span>
|
||||||
|
<span class="my-auto h-0 flex-1 border"></span>
|
||||||
|
</div>
|
||||||
|
<div class="m-2 flex flex-row place-content-center-safe gap-2">
|
||||||
|
{#each quick as q (q.name)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md bg-mist-300 p-2 hover:cursor-pointer dark:bg-mist-900"
|
||||||
|
onclick={() => setAccelList(q.skills)}>{q.name}</button
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full max-w-4xl rounded-md text-center shadow-md ring md:col-span-8 md:col-start-3">
|
||||||
|
<div class="m-4 flex-1">
|
||||||
|
<label for="carryover">Carryover Speed</label>
|
||||||
|
<input type="number" id="carryover" min="0" step="0.05" bind:value={skillCarry} class="w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="m-4 my-auto flex-1">
|
||||||
|
<input type="checkbox" id="dam" bind:checked={allowDAM} disabled={slope >= 0} class="peer my-auto h-6 w-6 align-middle" />
|
||||||
|
<label for="dam" class="my-auto align-middle peer-disabled:opacity-75">Downhill Mode Carryover</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#snippet info(name: string, unit: string, val: number, val2?: number)}
|
||||||
|
<CalcInfo title={name} class="max-w-72 flex-1">
|
||||||
|
{#if val2 == null}
|
||||||
|
<span class="block text-xl">{val.toFixed(3)} {unit}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="block text-xl">{val.toFixed(3)} – {val2.toFixed(3)} {unit}</span>
|
||||||
|
{/if}
|
||||||
|
</CalcInfo>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||||
|
{@render info('Mid-Race Speed', 'm/s', midSpeed[0], midSpeed[1])}
|
||||||
|
{@render info('Base Acceleration', 'm/s²', lateAccel)}
|
||||||
|
{@render info('Spurt Speed', 'm/s', spurtSpeed)}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||||
|
{@render info('Accel Distance', 'm', distance[1] - phases[race.Phase.MidRace], distance[0] - phases[race.Phase.MidRace])}
|
||||||
|
{@render info('Accel Time', 's', time[1], time[0])}
|
||||||
|
{@render info('Finish Time', 's', finishTime[1], finishTime[0])}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto flex w-full flex-col place-items-center md:flex-row md:justify-center">
|
||||||
|
{@render info('Carryover Gain', 'L', carryoverGain[1], carryoverGain[0])}
|
||||||
|
{@render info('Carryover + Accel Gain', 'L', skillGain[1], skillGain[0])}
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto mb-12 h-72 max-w-4xl" bind:clientWidth={width} bind:clientHeight={height}>
|
||||||
|
<div role="img" {@attach makeChart}>the chart seems to have didn't</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user