Compare commits

...

9 Commits

7 changed files with 53 additions and 51 deletions

View File

@@ -43,10 +43,6 @@
let height = $state(0); let height = $state(0);
let expand = $state(false); let expand = $state(false);
const expandIcon = $derived(expand ? '◀' : '▶');
function clickExpand() {
expand = !expand;
}
const xLabel = $derived(Stat[stat]); const xLabel = $derived(Stat[stat]);
const xLines = $derived([xRule].flat(1)); const xLines = $derived([xRule].flat(1));
@@ -85,6 +81,7 @@
Plot.plot({ Plot.plot({
width, width,
height, height,
clip: true,
...plotOptions, ...plotOptions,
x: { x: {
domain: [xMin, xMax], 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 bind:clientWidth={width} bind:clientHeight={height} class={['flex h-full w-full flex-col md:flex-row', className]}>
<div role="img" {@attach makeChart}> <div role="img" {@attach makeChart}>
<span>Loading chart!</span> <span>the chart seems to have didn't</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>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
import { AptitudeLevel, Mood, RunningStyle } from './race'; import { AptitudeLevel, Mood, RunningStyle } from './race';
import type { Runner } from './runner'; import type { Runner } from './runner';
const aptitude_map = { const aptMap = {
G: AptitudeLevel.G, G: AptitudeLevel.G,
F: AptitudeLevel.F, F: AptitudeLevel.F,
E: AptitudeLevel.E, E: AptitudeLevel.E,
@@ -11,9 +11,9 @@ const aptitude_map = {
A: AptitudeLevel.A, A: AptitudeLevel.A,
S: AptitudeLevel.S, S: AptitudeLevel.S,
} as const; } as const;
type AptitudeString = keyof typeof aptitude_map; type AptitudeString = keyof typeof aptMap;
const style_map = { const styleMap = {
Nige: RunningStyle.FrontRunner, Nige: RunningStyle.FrontRunner,
Sentou: RunningStyle.PaceChaser, Sentou: RunningStyle.PaceChaser,
Sasi: RunningStyle.LateSurger, Sasi: RunningStyle.LateSurger,
@@ -29,7 +29,7 @@ export interface ImportUma {
power: number; power: number;
guts: number; guts: number;
wisdom: number; wisdom: number;
strategy: keyof typeof style_map; strategy: keyof typeof styleMap;
distanceAptitude: AptitudeString; distanceAptitude: AptitudeString;
surfaceAptitude: AptitudeString; surfaceAptitude: AptitudeString;
strategyAptitude: AptitudeString; strategyAptitude: AptitudeString;
@@ -55,23 +55,23 @@ export function load(obj: ImportUma, name?: string): Runner {
return { return {
name: name ?? '', name: name ?? '',
chara_card_id: obj.outfitId !== '' ? parseInt(obj.outfitId) : 0, chara_card_id: obj.outfitId !== '' ? parseInt(obj.outfitId) : 0,
style: style_map[obj.strategy], style: styleMap[obj.strategy],
mood: obj.mood, mood: obj.mood,
speed: obj.speed, speed: obj.speed,
stamina: obj.stamina, stamina: obj.stamina,
power: obj.power, power: obj.power,
guts: obj.guts, guts: obj.guts,
wit: obj.wisdom, wit: obj.wisdom,
sprint: aptitude_map[obj.aptitudes[0]], sprint: aptMap[obj.aptitudes[0]],
mile: aptitude_map[obj.aptitudes[1]], mile: aptMap[obj.aptitudes[1]],
medium: aptitude_map[obj.aptitudes[2]], medium: aptMap[obj.aptitudes[2]],
long: aptitude_map[obj.aptitudes[3]], long: aptMap[obj.aptitudes[3]],
front: aptitude_map[obj.aptitudes[4]], front: aptMap[obj.aptitudes[4]],
pace: aptitude_map[obj.aptitudes[5]], pace: aptMap[obj.aptitudes[5]],
late: aptitude_map[obj.aptitudes[6]], late: aptMap[obj.aptitudes[6]],
end: aptitude_map[obj.aptitudes[7]], end: aptMap[obj.aptitudes[7]],
turf: aptitude_map[obj.aptitudes[8]], turf: aptMap[obj.aptitudes[8]],
dirt: aptitude_map[obj.aptitudes[9]], dirt: aptMap[obj.aptitudes[9]],
skills: obj.skills.map((s) => parseInt(s)), skills: obj.skills.map((s) => parseInt(s)),
unique_level: obj.uniqueLv, 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. * Calculate an Uma's last spurt target speed.
* @param rawSpeed Uma's speed stat. No accounting for mood lol. * @param speedStat Adjusted speed stat
* @param gutsStat Uma's guts stat. * @param gutsStat Adjusted guts stat
* @param style Running style * @param style Running style
* @param distance Distance aptitude * @param distance Distance aptitude
* @param raceLen Length of the race * @param raceLen Length of the race
* @returns Target speed in the last spurt in m/s * @returns Target speed in the last spurt in m/s
*/ */
export function spurtSpeed( export function spurtSpeed(
rawSpeed: number, speedStat: number,
gutsStat: number, gutsStat: number,
style: RunningStyle, style: RunningStyle,
distance: AptitudeLevel, distance: AptitudeLevel,
raceLen: number, raceLen: number,
): number { ): number {
const speedStat = rawSpeed <= 1200 ? rawSpeed : 1200 + (rawSpeed - 1200) * 0.5;
const spc = speedStrategyPhaseCoeff[style][Phase.LateRace]; const spc = speedStrategyPhaseCoeff[style][Phase.LateRace];
const dpm = distanceProficiencyMod[distance]; const dpm = distanceProficiencyMod[distance];
const base = baseSpeed(raceLen); const base = baseSpeed(raceLen);
@@ -149,9 +148,6 @@ export function inverseSpurtSpeed(
const base = baseSpeed(raceLen); const base = baseSpeed(raceLen);
const nr = spurtSpeed - base * (1.05 * spc + 0.0105) - Math.pow(450 * gutsStat, 0.597) * 0.0001; 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); const r = (nr * nr) / (0.008405 * dpm * dpm);
if (r > 1200) {
return Math.round(2 * r - 1200);
}
return Math.round(r); 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 * Calculate the distance gained from a target speed boost, including
* acceleration to the boosted target speed and deceleration back to baseline. * acceleration to the boosted target speed and deceleration back to baseline.
* @param speedBonus Difference between baseline and boosted speed in m/s * @param speedBonus Difference between baseline and boosted speed in m/s
* @param accel Current acceleration value in m/s² * @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. * @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 * @param dur Duration of the boosted speed
* @returns Distance gained from the speed boost in m * @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 // Actual effect of a target speed bonus looks like
// speed: __/-----\__ // speed: __/-----\__
// bonus: ====== // bonus: ======
// I.e., the speed bonus duration includes acceleration to the new speed // I.e., the speed bonus duration includes acceleration to the new speed
// and does not include the acceleration back to baseline after it ends. // and does not include the acceleration back to baseline after it ends.
const accelTime = speedBonus / accel; const accelTime = accel !== null ? speedBonus / accel : 0;
const decelTime = -speedBonus / decel; 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 // speedBonus*(dur-accelTime) + speedBonus*accelTime/2 + speedBonus*decelTime/2
return speedBonus * (dur + 0.5 * (decelTime - accelTime)); 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 * @param base_uma Character card (trainee or uma) to use for aptitudes; otherwise all aptitudes are A
* @returns Baseline runner * @returns Baseline runner
*/ */
export function new_runner(name?: string, base_uma?: Uma): Runner { export function newRunner(name?: string, base_uma?: Uma): Runner {
return { return {
name: name ?? '', name: name ?? '',
chara_card_id: base_uma?.chara_card_id ?? 0, 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 * @param name Name or memo to apply to the runner
* @returns Imported runner, or null if import was not possible * @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 // TODO(zeph): check for keys that identify the uma source
if (typeof obj === 'object') { if (typeof obj === 'object') {
try { try {

View File

@@ -54,24 +54,30 @@
const ssY: Array<ComputedSeries | null> = $derived([ const ssY: Array<ComputedSeries | null> = $derived([
{ {
label: 'Aptitude S', 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', 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 frontApt < AptitudeLevel.A
? { ? {
label: `Aptitude ${AptitudeLevel[frontApt]}`, 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, : null,
]); ]);
const lcY: Array<ComputedSeries | null> = $derived([ const lcY: Array<ComputedSeries | null> = $derived([
{ label: 'Ideal Lane Combo', y: (x) => speedGain(moveLaneModifier(x), accel[0], decel[0], lcDur) / HORSE_LENGTH }, { label: 'Ideal Lane Combo', y: (x) => speedGain(moveLaneModifier(x), lcDur, accel[0], decel[0]) / HORSE_LENGTH },
]); ]);
const yRule: HorizontalRule[] = $derived([ const pcRuler = $derived({ y: speedGain(0.35, skillDuration(2.4, raceLen), accel[1], decel[1]) / HORSE_LENGTH, label: 'Professor of Curvature' });
{ y: speedGain(0.35, accel[1], decel[1], skillDuration(2.4, raceLen)) / 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> </script>
@@ -103,7 +109,7 @@
</div> </div>
<div class="m-4 md:col-span-2 md:col-start-2"> <div class="m-4 md:col-span-2 md:col-start-2">
<label for="raceLen">Race Distance</label> <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>
<div class="m-4 self-center md:col-span-2"> <div class="m-4 self-center md:col-span-2">
<label for="isRunaway" class="mr-1 align-middle">Runaway</label> <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> <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"> <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={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> </div>
<span class="mt-8 block w-full text-center text-lg">Unique Skills</span> <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"> <div class="mx-auto flex flex-col md:flex-row md:justify-center">
@@ -132,10 +138,10 @@
{/each} {/each}
</div> </div>
<div class="mx-auto h-60 py-4 md:h-96 md:w-3xl"> <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>
<div class="mx-auto mt-4 h-60 py-4 md:mt-0 md:h-96 md:w-3xl"> <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>
<div class="mx-auto mt-12 w-full max-w-4xl border-t md:mt-8"> <div class="mx-auto mt-12 w-full max-w-4xl border-t md:mt-8">
<ul class="ml-4 list-disc"> <ul class="ml-4 list-disc">
@@ -148,7 +154,7 @@
</li> </li>
<li> <li>
Lane combo is idealized in the sense of assuming second lane change speed skill executes immediately after Dodging Danger 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]"> <ul class="ml-8 list-[revert]">
<li> <li>
The move lane modifier is capped to 6 seconds, which is the approximate observed time to move from the Dodging Danger 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 { children, speed, dur, accel, decel }: Props = $props();
const decels = $derived([decel].flat(1)); 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(' ')); const text = $derived(gain.map(fmtp).join(' '));
</script> </script>

View File

@@ -86,7 +86,7 @@
</div> </div>
<div class="m-4 md:col-start-2"> <div class="m-4 md:col-start-2">
<label for="raceLen">Race Distance</label> <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>
<div class="m-4 self-center"> <div class="m-4 self-center">
<label for="isCareer" class="mr-1 align-middle">In Career</label> <label for="isCareer" class="mr-1 align-middle">In Career</label>