zenno/mspeed: calculator for front runner mechanical speed bonuses

This commit is contained in:
2026-05-22 19:08:23 -04:00
parent b720b325b3
commit 2a07f193ec
7 changed files with 433 additions and 7 deletions
+1
View File
@@ -19,6 +19,7 @@
<span class="flex-1 text-center">
<a href={resolve('/')} class="mx-8 my-1 block font-semibold md:hidden">Zenno Rob Roy</a>
<a href={resolve('/spurt')} class="mx-8 my-1 inline-block">Spurt Speed</a>
<a href={resolve('/mspeed')} class="mx-8 my-1 inline-block">Mechanical Speed</a>
<a href={resolve('/convo')} class="mx-8 my-1 inline-block">Lobby Conversations</a>
</span>
</nav>
+4
View File
@@ -10,6 +10,10 @@
<a href={resolve('/spurt')}>Spurt Speed</a> — Calculate a horse's target speed in the last spurt and compare to other distance aptitudes
and running styles.
</li>
<li>
<a href={resolve('/mspeed')}>Front Runner Mechanical Speed Comparator</a> &mdash; Compare spot struggle and lane combo to the effects
of individual skills.
</li>
<li>
<a href={resolve('/convo')}>Lobby Conversations</a> — Check participants in lobby conversations and get recommendations on unlocking
them quickly.
+173
View File
@@ -0,0 +1,173 @@
<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 flex h-60 flex-col place-content-center py-4 md:h-96 md:flex-row">
<StatChart class="flex-1" stat={Stat.Guts} y={ssY} yLabel="Lengths Gained" range={[0, 1.5, 2.5]} xRule={gutsStat} {yRule} />
<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-8 w-full max-w-4xl">
<ul class="list-disc">
<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-4 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>
+33
View File
@@ -0,0 +1,33 @@
<script lang="ts">
import { HORSE_LENGTH, speedGain } from '$lib/race';
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
speed: number;
dur: number;
accel: number;
decel: number | number[];
}
function fmtp(x: number): string {
return x >= 0 ? '+' + x.toFixed(3) : x.toFixed(3);
}
const { children, speed, dur, accel, decel }: Props = $props();
const decels = $derived([decel].flat(1));
const gain = $derived(decels.map((d) => speedGain(speed, accel, d, dur) / HORSE_LENGTH));
const text = $derived(gain.map(fmtp).join(' &ndash; '));
</script>
<div
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"
>
<div class="block">{@render children()}</div>
<span class="block text-xl">{@html text} L</span>
<div class="flex flex-row">
<span class="flex-1 text-xs">{fmtp(speed)} m/s</span>
<span class="flex-1 text-xs">{dur.toFixed(3)} s</span>
</div>
</div>