Files
horse/zenno/src/routes/mspeed/+page.svelte

176 lines
7.3 KiB
Svelte

<script lang="ts">
import type { ComputedSeries, HorizontalRule } from '$lib/chart';
import {
acceleration,
APTITUDE_LEVELS,
AptitudeLevel,
deceleration,
HORSE_LENGTH,
moveLaneModifier,
Phase,
RunningStyle,
skillDuration,
speedGain,
spotStruggleDuration,
spotStruggleSpeed,
Stat,
} from '$lib/race';
import StatChart from '$lib/StatChart.svelte';
import SpeedDur from './SpeedDur.svelte';
let rawPower = $state(1200);
let rawGuts = $state(1200);
let raceLen = $state(1600);
let surfApt = $state(AptitudeLevel.A);
let frontApt = $state(AptitudeLevel.A);
let isRunaway = $state(false);
let isCareer = $state(false);
const careerMod = $derived(isCareer ? 400 : 0);
const powerStat = $derived(rawPower + careerMod);
const gutsStat = $derived(rawGuts + careerMod);
const style = $derived(isRunaway ? RunningStyle.GreatEscape : RunningStyle.FrontRunner);
const phases = [Phase.EarlyRace, Phase.MidRace, Phase.LateRace] as const;
const accel = $derived(phases.map((p) => acceleration(powerStat, style, surfApt, p)));
const decel = phases.map((p) => deceleration(p));
const ssBoost = $derived(spotStruggleSpeed(gutsStat));
const ssDur = $derived(spotStruggleDuration(gutsStat, frontApt));
const lcBoost = $derived(moveLaneModifier(powerStat));
const lcDur = $derived(Math.min(skillDuration(3, raceLen), 6));
const uniques = [
['Operation Cacao', 0.35, 5, Phase.MidRace],
["All Charged! It's Go Time! (Tokyo turf)", 0.45, 5, Phase.LateRace],
] as const;
const skills = [
['Fast-Paced', 0.15, 3, Phase.MidRace],
['Professor of Curvature (mid race)', 0.35, 2.4, Phase.MidRace],
["All Charged! It's Go Time! (inherited)", 0.25, 3, Phase.LateRace],
] as const;
const ssY: Array<ComputedSeries | null> = $derived([
{
label: 'Aptitude S',
y: (x) => speedGain(spotStruggleSpeed(x), accel[0], decel[0], spotStruggleDuration(x, AptitudeLevel.S)) / HORSE_LENGTH,
},
{
label: 'Aptitude A',
y: (x) => speedGain(spotStruggleSpeed(x), accel[0], decel[0], spotStruggleDuration(x, AptitudeLevel.A)) / HORSE_LENGTH,
},
frontApt < AptitudeLevel.A
? {
label: `Aptitude ${AptitudeLevel[frontApt]}`,
y: (x) => speedGain(spotStruggleSpeed(x), accel[0], decel[0], spotStruggleDuration(x, frontApt)) / HORSE_LENGTH,
}
: null,
]);
const lcY: Array<ComputedSeries | null> = $derived([
{ label: 'Ideal Lane Combo', y: (x) => speedGain(moveLaneModifier(x), accel[0], decel[0], lcDur) / HORSE_LENGTH },
]);
const yRule: HorizontalRule[] = $derived([
{ y: speedGain(0.35, accel[1], decel[1], skillDuration(2.4, raceLen)) / HORSE_LENGTH, label: 'Professor of Curvature' },
]);
</script>
<h1 class="text-4xl">Front Runner Mechanical Speed Comparator</h1>
<div class="mx-auto mt-8 grid max-w-4xl grid-cols-1 rounded-md text-center shadow-md ring md:grid-cols-8">
<div class="m-4 md:col-span-2">
<label for="powerStat">Power Stat</label>
<input type="number" id="powerStat" bind:value={rawPower} class="w-full" />
</div>
<div class="m-4 md:col-span-2">
<label for="gutsStat">Guts Stat</label>
<input type="number" id="gutsStat" bind:value={rawGuts} class="w-full" />
</div>
<div class="m-4 md:col-span-2">
<label for="surfaceApt">Surface Aptitude</label>
<select id="surfaceApt" required bind:value={surfApt} class="w-full">
{#each APTITUDE_LEVELS as apt (apt)}
<option value={apt}>{AptitudeLevel[apt]}</option>
{/each}
</select>
</div>
<div class="m-4 md:col-span-2">
<label for="frontApt">Front Runner Aptitude</label>
<select id="frontApt" required bind:value={frontApt} class="w-full">
{#each APTITUDE_LEVELS as apt (apt)}
<option value={apt}>{AptitudeLevel[apt]}</option>
{/each}
</select>
</div>
<div class="m-4 md:col-span-2 md:col-start-2">
<label for="raceLen">Race Distance</label>
<input type="number" id="raceLen" required bind:value={raceLen} class="w-full" />
</div>
<div class="m-4 self-center md:col-span-2">
<label for="isRunaway" class="mr-1 align-middle">Runaway</label>
<input type="checkbox" id="isRunaway" role="switch" bind:checked={isRunaway} class="min-h-6 min-w-6 align-middle" />
</div>
<div class="m-4 self-center md:col-span-2">
<label for="isCareer" class="mr-1 align-middle">In Career</label>
<input type="checkbox" id="isCareer" role="switch" bind:checked={isCareer} class="min-h-6 min-w-6 align-middle" />
</div>
</div>
<span class="mt-8 block w-full text-center text-lg">Mechanics</span>
<div class="mx-auto flex w-full flex-col md:flex-row md:justify-center">
<SpeedDur speed={ssBoost} dur={ssDur} accel={accel[0]} decel={[decel[0], decel[1]]}>Spot Struggle</SpeedDur>
<SpeedDur speed={lcBoost} dur={lcDur} accel={accel[0]} decel={decel[0]}>Idealized Lane Combo</SpeedDur>
</div>
<span class="mt-8 block w-full text-center text-lg">Unique Skills</span>
<div class="mx-auto flex flex-col md:flex-row md:justify-center">
{#each uniques as [name, boost, dur, phase] (name)}
<SpeedDur speed={boost} dur={skillDuration(dur, raceLen)} accel={accel[phase]} decel={decel[phase]}>{name}</SpeedDur>
{/each}
</div>
<span class="mt-8 block w-full text-center text-lg">Inherited Uniques &amp; Other Skills</span>
<div class="mx-auto flex flex-col md:flex-row md:justify-center">
{#each skills as [name, boost, dur, phase] (name)}
<SpeedDur speed={boost} dur={skillDuration(dur, raceLen)} accel={accel[phase]} decel={decel[phase]}>{name}</SpeedDur>
{/each}
</div>
<div class="mx-auto py-4 h-60 md:h-96 md:w-3xl">
<StatChart class="flex-1" stat={Stat.Guts} y={ssY} yLabel="Lengths Gained" range={[0, 1.5, 2.5]} xRule={gutsStat} {yRule} />
</div>
<div class="mx-auto mt-4 md:mt-0 py-4 h-60 md:h-96 md:w-3xl">
<StatChart class="flex-1" stat={Stat.Power} y={lcY} yLabel="Lengths Gained" range={[0, 1.5, 2.5]} xRule={powerStat} {yRule} />
</div>
<div class="mx-auto mt-12 md:mt-8 w-full max-w-4xl border-t">
<ul class="list-disc ml-4">
<li>All lengths gained include acceleration at the beginning of each speed boost and deceleration after its end.</li>
<li>Each effect is assumed to be isolated and executed on level ground.</li>
<li>
Spot struggle has two numbers to distinguish ending in early race versus ending in mid race, which gives different
deceleration values. Since spot struggle duration does not scale with race length, it is more likely to end in mid race on
shorter races.
</li>
<li>
Lane combo is idealized in the sense of assuming second lane change speed skill executes immediately after Dodging Danger
completes and the horse is never blocked.
<ul class="ml-8 list-[revert]">
<li>
The move lane modifier is capped to 6 seconds, which is the approximate observed time to move from the Dodging Danger
fixed lane back to the rail under the effect of Prudent Positioning.
</li>
<li>
On medium+ tracks, with a proper gate acceleration build, Dodging Danger should realize some lane movement speed
modifier, so the actual benefit will be more than the idealized number.
</li>
<li>
Ignited Spirit WIT has a longer duration and lower lane change speed boost than Prudent Positioning, so it is likely to
give more benefit than the idealized number.
</li>
<li>
For full simulated analysis of lane combo, see <a
href="https://lanecalc.hf-uma.net/"
target="_blank"
rel="noopener noreferrer">危険回避シミュ</a
>.
</li>
</ul>
</li>
</ul>
</div>