Compare commits
9 Commits
7600c48cc7
...
3ab17cf9b0
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ab17cf9b0 | |||
| 4bd7962182 | |||
| 09f099171b | |||
| 4fff7069a8 | |||
| 3e2153b39c | |||
| d0fa6ab15c | |||
| 9f8024d488 | |||
| 1df3bc1db9 | |||
| a8c1b9c754 |
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -54,24 +54,30 @@
|
||||
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 },
|
||||
{ label: 'Ideal Lane Combo', y: (x) => speedGain(moveLaneModifier(x), lcDur, accel[0], decel[0]) / HORSE_LENGTH },
|
||||
]);
|
||||
const yRule: HorizontalRule[] = $derived([
|
||||
{ y: speedGain(0.35, accel[1], decel[1], skillDuration(2.4, raceLen)) / HORSE_LENGTH, label: 'Professor of Curvature' },
|
||||
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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user