zenno: categorize tools

This commit is contained in:
2026-06-10 14:03:42 -04:00
parent dc78d51def
commit 75024c7c11
18 changed files with 119 additions and 42 deletions
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import { resolve } from '$app/paths';
</script>
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy &mdash; Character Tools</h1>
<p>Tools related to understanding Umamusume characters.</p>
<ul class="mb-4 list-disc pl-4">
<li>
<a href={resolve('/race/spurt')}>Spurt Speed</a> &mdash; Calculate a horse's target speed in the last spurt and compare to other
distance aptitudes and running styles.
</li>
<li>
<a href={resolve('/race/mspeed')}>Front Runner Mechanical Speed Comparator</a> &mdash; Compare spot struggle and lane combo to the
effects of individual skills.
</li>
</ul>
+220
View File
@@ -0,0 +1,220 @@
<script lang="ts">
import type { ComputedSeries } 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), spotStruggleDuration(x, AptitudeLevel.S), accel[0], decel[0]) / HORSE_LENGTH,
},
{
label: 'Aptitude A',
y: (x) => speedGain(spotStruggleSpeed(x), spotStruggleDuration(x, AptitudeLevel.A), accel[0], decel[0]) / HORSE_LENGTH,
},
frontApt < AptitudeLevel.A
? {
label: `Aptitude ${AptitudeLevel[frontApt]}`,
y: (x) => speedGain(spotStruggleSpeed(x), spotStruggleDuration(x, frontApt), accel[0], decel[0]) / HORSE_LENGTH,
}
: null,
]);
const lcY: Array<ComputedSeries | null> = $derived([
{
label: 'Ideal Lane Combo',
y: (x) => speedGain(moveLaneModifier(x), lcDur, acceleration(x, style, surfApt, Phase.EarlyRace), decel[0]) / HORSE_LENGTH,
},
]);
const pcRuler = $derived({
y: speedGain(0.35, skillDuration(2.4, raceLen), accel[1], decel[1]) / HORSE_LENGTH,
label: 'Professor of Curvature',
});
const ssYRule = $derived([pcRuler, { y: speedGain(lcBoost, lcDur, accel[0], decel[0]) / HORSE_LENGTH, label: 'Lane Combo' }]);
const lcYRule = $derived([
pcRuler,
{ y: speedGain(ssBoost, ssDur, accel[0], decel[1]) / HORSE_LENGTH, label: 'Spot Struggle' },
]);
</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 min="1000" max="3600" step="100" 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 place-items-center 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 (DDPP)</SpeedDur>
</div>
<table class="mx-auto mt-1 table-fixed rounded-b-md border border-t-0 text-sm">
<caption class="rounded-t-md border border-b-0">Base Acceleration</caption>
<thead>
<tr>
<th class="w-32" scope="col">Early Race</th>
<th class="w-32" scope="col">Mid Race</th>
<th class="w-32" scope="col">Late Race</th>
</tr>
</thead>
<tbody>
<tr>
{#each accel as v, i (i)}
<td class="text-center">{v.toFixed(3)} m/s²</td>
{/each}
</tr>
<tr>
{#each decel as v, i (i)}
<td class="text-center">{v.toFixed(3)} m/s²</td>
{/each}
</tr>
</tbody>
</table>
<span class="mt-8 block w-full text-center text-lg">Unique Skills</span>
<div class="mx-auto flex flex-col place-items-center 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 place-items-center 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 grid h-96 grid-cols-1 py-4 md:grid-cols-2">
<StatChart
class="flex-1"
stat={Stat.Guts}
y={ssY}
yLabel="Lengths Gained"
range={[0, 1.5, 2.5]}
xRule={gutsStat}
yRule={ssYRule}
/>
<StatChart
class="flex-1"
stat={Stat.Power}
y={lcY}
yLabel="Lengths Gained"
range={[0, 1.5, 2.5]}
xRule={powerStat}
yRule={lcYRule}
/>
</div>
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
<ul class="ml-4 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, the horse is never blocked, and the horse returns to the rail in early race before the first corner.
<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>
@@ -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, dur, accel, d) / HORSE_LENGTH));
const text = $derived(gain.map(fmtp).join(' '));
</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">{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>
+141
View File
@@ -0,0 +1,141 @@
<script lang="ts">
import type { ComputedSeries } from '$lib/chart';
import { AptitudeLevel, inverseSpurtSpeed, RunningStyle, spurtSpeed, Stat } from '$lib/race';
import StatChart from '$lib/StatChart.svelte';
const aptsList = Object.entries(AptitudeLevel).filter(([, val]) => typeof val === 'number');
const stylesList = [
['Front Runner', RunningStyle.FrontRunner],
['Pace Chaser', RunningStyle.PaceChaser],
['Late Surger', RunningStyle.LateSurger],
['End Closer', RunningStyle.EndCloser],
['Great Escape', RunningStyle.GreatEscape],
] as const;
const skillSpeeds = [0.45, 0.35, 0.25, 0.15] as const;
let rawSpeed: number = $state(1200);
let rawGuts: number = $state(1200);
let style: RunningStyle = $state(RunningStyle.FrontRunner);
let opponentStyle: RunningStyle = $state(RunningStyle.PaceChaser);
let distanceApt: AptitudeLevel = $state(AptitudeLevel.S);
let raceLen: number = $state(2000);
let isCareer: boolean = $state(false);
const careerMod = $derived(isCareer ? 400 : 0);
const speedStat = $derived(rawSpeed + careerMod);
const gutsStat = $derived(rawGuts + careerMod);
const speed = $derived(spurtSpeed(speedStat, gutsStat, style, distanceApt, raceLen));
const sProf = $derived([
inverseSpurtSpeed(speed, gutsStat, RunningStyle.FrontRunner, AptitudeLevel.S, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.PaceChaser, AptitudeLevel.S, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.LateSurger, AptitudeLevel.S, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.EndCloser, AptitudeLevel.S, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.S, raceLen) - careerMod,
]);
const aProf = $derived([
inverseSpurtSpeed(speed, gutsStat, RunningStyle.FrontRunner, AptitudeLevel.A, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.PaceChaser, AptitudeLevel.A, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.LateSurger, AptitudeLevel.A, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.EndCloser, AptitudeLevel.A, raceLen) - careerMod,
inverseSpurtSpeed(speed, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.A, raceLen) - careerMod,
]);
const skillProf = $derived(
skillSpeeds.map((v) => [v, inverseSpurtSpeed(speed + v, gutsStat, opponentStyle, AptitudeLevel.S, raceLen) - careerMod]),
);
const y: Array<ComputedSeries | null> = $derived([
{ 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]}`, y: (x) => spurtSpeed(x, gutsStat, style, distanceApt, raceLen) }
: null,
]);
const range: [number, number] = $derived([
spurtSpeed(200, gutsStat, RunningStyle.GreatEscape, AptitudeLevel.C, raceLen),
spurtSpeed(2000, gutsStat, RunningStyle.EndCloser, AptitudeLevel.S, raceLen),
]);
</script>
<h1 class="text-4xl">Spurt Speed Calculator</h1>
<div class="mx-auto mt-8 grid max-w-4xl grid-cols-1 rounded-md text-center shadow-md ring md:grid-cols-4">
<div class="m-4">
<label for="speedStat">Speed Stat</label>
<input type="number" id="speedStat" required bind:value={rawSpeed} class="w-full" />
</div>
<div class="m-4">
<label for="gutsStat">Guts Stat</label>
<input type="number" id="gutsStat" required bind:value={rawGuts} class="w-full" />
</div>
<div class="m-4">
<label for="style">Style</label>
<select id="style" required bind:value={style} class="w-full">
{#each stylesList as [name, style] (style)}
<option value={style}>{style === RunningStyle.GreatEscape ? 'Great Escape (Runaway)' : name}</option>
{/each}
</select>
</div>
<div class="m-4">
<label for="distanceApt">Distance Aptitude</label>
<select id="distanceApt" required bind:value={distanceApt} class="w-full">
{#each aptsList.toReversed() as [name, val] (val)}
<option value={val}>{name}</option>
{/each}
</select>
</div>
<div class="m-4 md:col-start-2">
<label for="raceLen">Race Distance</label>
<input type="number" id="raceLen" required min="1000" max="3600" step="100" bind:value={raceLen} class="w-full" />
</div>
<div class="m-4 self-center">
<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">
Target spurt speed: {speed.toFixed(3)} m/s
</span>
{#each [[AptitudeLevel.A, aProf] as const, [AptitudeLevel.S, sProf] as const] as [level, inv] (level)}
<div class="mx-auto max-w-3xl">
<span class="mt-8 block w-full">
With {AptitudeLevel[level]} proficiency, the equivalent speed is
</span>
<div class="flex flex-col md:flex-row">
{#each stylesList as [styleName, s] (s)}
<div
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}
</div>
</div>
{/each}
<div class="mx-auto max-w-3xl">
<span class="mt-8 block w-full">
While a speed skill is active, the equivalent speed for a distance S
<select id="opponentStyle" required bind:value={opponentStyle}>
{#each stylesList as [name, style] (style)}
<option value={style}>{style === RunningStyle.GreatEscape ? 'Great Escape (Runaway)' : name}</option>
{/each}
</select>
is
</span>
<div class="flex flex-col md:flex-row">
{#each skillProf as [v, inv] (v)}
<div class="m-2 flex-1 rounded-md border shadow-sm transition-shadow hover:shadow-md">
<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}
</div>
</div>
<div class="mx-auto h-60 max-w-3xl place-content-center py-4 md:h-96">
<StatChart stat={Stat.Speed} {y} yLabel="Spurt Speed (m/s)" xRule={speedStat} {range} />
</div>