Compare commits

...

9 Commits

7 changed files with 53 additions and 51 deletions

View File

@@ -43,10 +43,6 @@
let height = $state(0);
let expand = $state(false);
const expandIcon = $derived(expand ? '◀' : '▶');
function clickExpand() {
expand = !expand;
}
const xLabel = $derived(Stat[stat]);
const xLines = $derived([xRule].flat(1));
@@ -85,6 +81,7 @@
Plot.plot({
width,
height,
clip: true,
...plotOptions,
x: {
domain: [xMin, xMax],
@@ -117,10 +114,7 @@
<div bind:clientWidth={width} bind:clientHeight={height} class={['flex h-full w-full flex-col md:flex-row', className]}>
<div role="img" {@attach makeChart}>
<span>Loading chart!</span>
</div>
<div class="my-5 flex h-full flex-row place-content-end md:flex-col md:place-content-start">
<button class="h-8 rounded border px-2 pb-1 align-middle" onclick={clickExpand}>{expandIcon}</button>
<span>the chart seems to have didn't</span>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { AptitudeLevel, Mood, RunningStyle } from './race';
import type { Runner } from './runner';
const aptitude_map = {
const aptMap = {
G: AptitudeLevel.G,
F: AptitudeLevel.F,
E: AptitudeLevel.E,
@@ -11,9 +11,9 @@ const aptitude_map = {
A: AptitudeLevel.A,
S: AptitudeLevel.S,
} as const;
type AptitudeString = keyof typeof aptitude_map;
type AptitudeString = keyof typeof aptMap;
const style_map = {
const styleMap = {
Nige: RunningStyle.FrontRunner,
Sentou: RunningStyle.PaceChaser,
Sasi: RunningStyle.LateSurger,
@@ -29,7 +29,7 @@ export interface ImportUma {
power: number;
guts: number;
wisdom: number;
strategy: keyof typeof style_map;
strategy: keyof typeof styleMap;
distanceAptitude: AptitudeString;
surfaceAptitude: AptitudeString;
strategyAptitude: AptitudeString;
@@ -55,23 +55,23 @@ export function load(obj: ImportUma, name?: string): Runner {
return {
name: name ?? '',
chara_card_id: obj.outfitId !== '' ? parseInt(obj.outfitId) : 0,
style: style_map[obj.strategy],
style: styleMap[obj.strategy],
mood: obj.mood,
speed: obj.speed,
stamina: obj.stamina,
power: obj.power,
guts: obj.guts,
wit: obj.wisdom,
sprint: aptitude_map[obj.aptitudes[0]],
mile: aptitude_map[obj.aptitudes[1]],
medium: aptitude_map[obj.aptitudes[2]],
long: aptitude_map[obj.aptitudes[3]],
front: aptitude_map[obj.aptitudes[4]],
pace: aptitude_map[obj.aptitudes[5]],
late: aptitude_map[obj.aptitudes[6]],
end: aptitude_map[obj.aptitudes[7]],
turf: aptitude_map[obj.aptitudes[8]],
dirt: aptitude_map[obj.aptitudes[9]],
sprint: aptMap[obj.aptitudes[0]],
mile: aptMap[obj.aptitudes[1]],
medium: aptMap[obj.aptitudes[2]],
long: aptMap[obj.aptitudes[3]],
front: aptMap[obj.aptitudes[4]],
pace: aptMap[obj.aptitudes[5]],
late: aptMap[obj.aptitudes[6]],
end: aptMap[obj.aptitudes[7]],
turf: aptMap[obj.aptitudes[8]],
dirt: aptMap[obj.aptitudes[9]],
skills: obj.skills.map((s) => parseInt(s)),
unique_level: obj.uniqueLv,
};

View File

@@ -95,21 +95,20 @@ const distanceProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.05] as cons
/**
* Calculate an Uma's last spurt target speed.
* @param rawSpeed Uma's speed stat. No accounting for mood lol.
* @param gutsStat Uma's guts stat.
* @param speedStat Adjusted speed stat
* @param gutsStat Adjusted guts stat
* @param style Running style
* @param distance Distance aptitude
* @param raceLen Length of the race
* @returns Target speed in the last spurt in m/s
*/
export function spurtSpeed(
rawSpeed: number,
speedStat: number,
gutsStat: number,
style: RunningStyle,
distance: AptitudeLevel,
raceLen: number,
): number {
const speedStat = rawSpeed <= 1200 ? rawSpeed : 1200 + (rawSpeed - 1200) * 0.5;
const spc = speedStrategyPhaseCoeff[style][Phase.LateRace];
const dpm = distanceProficiencyMod[distance];
const base = baseSpeed(raceLen);
@@ -149,9 +148,6 @@ export function inverseSpurtSpeed(
const base = baseSpeed(raceLen);
const nr = spurtSpeed - base * (1.05 * spc + 0.0105) - Math.pow(450 * gutsStat, 0.597) * 0.0001;
const r = (nr * nr) / (0.008405 * dpm * dpm);
if (r > 1200) {
return Math.round(2 * r - 1200);
}
return Math.round(r);
}
@@ -274,19 +270,25 @@ export function skillDuration(baseDur: number, raceLen: number): number {
* Calculate the distance gained from a target speed boost, including
* acceleration to the boosted target speed and deceleration back to baseline.
* @param speedBonus Difference between baseline and boosted speed in m/s
* @param accel Current acceleration value in m/s²
* @param decel Current phase-based deceleration value in m/s², a negative value.
* @param accel Current acceleration value in m/s², or null for instant acceleration
* @param decel Current phase-based deceleration value in m/s², a negative value; or null for instant deceleration
* @param dur Duration of the boosted speed
* @returns Distance gained from the speed boost in m
*/
export function speedGain(speedBonus: number, accel: number, decel: number, dur: number): number {
export function speedGain(speedBonus: number, dur: number, accel: number | null, decel: number | null): number {
// Actual effect of a target speed bonus looks like
// speed: __/-----\__
// bonus: ======
// I.e., the speed bonus duration includes acceleration to the new speed
// and does not include the acceleration back to baseline after it ends.
const accelTime = speedBonus / accel;
const decelTime = -speedBonus / decel;
const accelTime = accel !== null ? speedBonus / accel : 0;
const decelTime = decel !== null ? -speedBonus / decel : 0;
if (accelTime >= dur) {
// Acceleration is so low that the horse won't reach the boosted target
// speed before the effect ends. E.g., G surface aptitude.
const peakSpeed = (accel ?? 0) * dur;
return 0.5 * (peakSpeed * dur - peakSpeed / (decel ?? 0));
}
// speedBonus*(dur-accelTime) + speedBonus*accelTime/2 + speedBonus*decelTime/2
return speedBonus * (dur + 0.5 * (decelTime - accelTime));
}

View File

@@ -39,7 +39,7 @@ export interface Runner {
* @param base_uma Character card (trainee or uma) to use for aptitudes; otherwise all aptitudes are A
* @returns Baseline runner
*/
export function new_runner(name?: string, base_uma?: Uma): Runner {
export function newRunner(name?: string, base_uma?: Uma): Runner {
return {
name: name ?? '',
chara_card_id: base_uma?.chara_card_id ?? 0,
@@ -72,7 +72,7 @@ export function new_runner(name?: string, base_uma?: Uma): Runner {
* @param name Name or memo to apply to the runner
* @returns Imported runner, or null if import was not possible
*/
export function import_runner(obj: unknown, name?: string): Runner | null {
export function importRunner(obj: unknown, name?: string): Runner | null {
// TODO(zeph): check for keys that identify the uma source
if (typeof obj === 'object') {
try {

View File

@@ -54,25 +54,31 @@
const ssY: Array<ComputedSeries | null> = $derived([
{
label: 'Aptitude S',
y: (x) => speedGain(spotStruggleSpeed(x), accel[0], decel[0], spotStruggleDuration(x, AptitudeLevel.S)) / HORSE_LENGTH,
y: (x) => speedGain(spotStruggleSpeed(x), spotStruggleDuration(x, AptitudeLevel.S), accel[0], decel[0]) / HORSE_LENGTH,
},
{
label: 'Aptitude A',
y: (x) => speedGain(spotStruggleSpeed(x), accel[0], decel[0], spotStruggleDuration(x, AptitudeLevel.A)) / HORSE_LENGTH,
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), accel[0], decel[0], spotStruggleDuration(x, frontApt)) / HORSE_LENGTH,
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), 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' },
{ label: 'Ideal Lane Combo', y: (x) => speedGain(moveLaneModifier(x), lcDur, accel[0], 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>
@@ -103,7 +109,7 @@
</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" />
<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>
@@ -117,7 +123,7 @@
<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>
<SpeedDur speed={lcBoost} dur={lcDur} accel={accel[0]} decel={decel[0]}>Idealized Lane Combo (DDPP)</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">
@@ -132,10 +138,10 @@
{/each}
</div>
<div class="mx-auto h-60 py-4 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} />
<StatChart class="flex-1" stat={Stat.Guts} y={ssY} yLabel="Lengths Gained" range={[0, 1.5, 2.5]} xRule={gutsStat} yRule={ssYRule} />
</div>
<div class="mx-auto mt-4 h-60 py-4 md:mt-0 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} />
<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 md:mt-8">
<ul class="ml-4 list-disc">
@@ -148,7 +154,7 @@
</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.
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

View File

@@ -17,7 +17,7 @@
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 gain = $derived(decels.map((d) => speedGain(speed, dur, accel, d) / HORSE_LENGTH));
const text = $derived(gain.map(fmtp).join(' '));
</script>

View File

@@ -86,7 +86,7 @@
</div>
<div class="m-4 md:col-start-2">
<label for="raceLen">Race Distance</label>
<input type="number" id="raceLen" required bind:value={raceLen} class="w-full" />
<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>