From 55175c9c40b7455055d57be6792df336924bd6ab Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Mon, 6 Jul 2026 09:41:11 -0400 Subject: [PATCH] zenno: format & lint --- zenno/src/lib/StatChart.svelte | 4 +- zenno/src/lib/race.ts | 89 ++-- zenno/src/lib/racelib/accel.ts | 107 +++-- zenno/src/lib/racelib/stat.ts | 2 +- zenno/src/routes/doc/race/+page.svelte | 15 +- zenno/src/routes/race/gate/+page.svelte | 527 ++++++++++++---------- zenno/src/routes/race/mspeed/+page.svelte | 10 +- 7 files changed, 413 insertions(+), 341 deletions(-) diff --git a/zenno/src/lib/StatChart.svelte b/zenno/src/lib/StatChart.svelte index af94804..dfd2b37 100644 --- a/zenno/src/lib/StatChart.svelte +++ b/zenno/src/lib/StatChart.svelte @@ -73,7 +73,9 @@ const series = $derived([y].flat(1).filter((s) => s != null)); const areas = $derived([yArea].flat(1).filter((s) => s != null)); const vals = $derived(series.flatMap(({ y, label }) => xVal.map((x) => ({ x, y: y(adj(x), x), label })))); - const areaVals = $derived(areas.flatMap(({ y1, y2, label }) => xVal.map((x) => ({ x, y1: y1(adj(x), x), y2: y2(adj(x), x), label })))); + const areaVals = $derived( + areas.flatMap(({ y1, y2, label }) => xVal.map((x) => ({ x, y1: y1(adj(x), x), y2: y2(adj(x), x), label }))), + ); const yRange: [number, number] = $derived.by(() => { if (range != null) { if (range.length === 2) { diff --git a/zenno/src/lib/race.ts b/zenno/src/lib/race.ts index d01543d..7b243d9 100644 --- a/zenno/src/lib/race.ts +++ b/zenno/src/lib/race.ts @@ -34,13 +34,7 @@ export enum Mood { /** * Moods as a descending list for easy iteration. */ -export const MOODS = [ - Mood.Great, - Mood.Good, - Mood.Normal, - Mood.Bad, - Mood.Awful, -] as const; +export const MOODS = [Mood.Great, Mood.Good, Mood.Normal, Mood.Bad, Mood.Awful] as const; const MOOD_COEF = { [Mood.Awful]: 0.96, @@ -61,7 +55,7 @@ function statClamp(x: number): number { * @returns Base stat value */ export function baseStat(mood: Mood, rawStat: number): number { - const r = (math.min(rawStat, 1200) + math.max(rawStat - 1200, 0)*0.5) * MOOD_COEF[mood]; + const r = (math.min(rawStat, 1200) + math.max(rawStat - 1200, 0) * 0.5) * MOOD_COEF[mood]; return statClamp(r); } @@ -72,7 +66,7 @@ export enum Distance { Sprint, Mile, Medium, - Long + Long, } /** @@ -104,15 +98,15 @@ export function phases(raceLen: number) { [Phase.EarlyRace]: sixth, [Phase.MidRace]: 4 * sixth, [Phase.LateRace]: raceLen, - } + }; } /** * Race surfaces. */ export enum Surface { - Turf, - Dirt, + Turf, + Dirt, } /** @@ -169,46 +163,66 @@ const strategyProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.75, 0.85, 1.0, 1.1] as con /** * Compute adjusted speed for a race. - * @param speed + * @param speed * @param baseSpeed Base speed stat as computed from raw speed with baseStat * @param career Whether the horse is currently training in career * @param surface Race surface type * @param conditions Race ground conditions * @param thresholdMod Achieved stat threshold modifier for the race track */ -export function adjustedStat(speed: Stat.Speed, baseSpeed: number, career: boolean, surface: Surface, conditions: GroundConditions, thresholdMod: number): number; +export function adjustedStat( + speed: Stat.Speed, + baseSpeed: number, + career: boolean, + surface: Surface, + conditions: GroundConditions, + thresholdMod: number, +): number; /** * Compute adjusted stamina for a race. - * @param stam + * @param stam * @param baseStam Base stamina stat as computed from raw stamina with baseStat * @param career Whether the horse is currently training in career */ export function adjustedStat(stam: Stat.Stamina, baseStam: number, career: boolean): number; /** * Compute adjusted power for a race. - * @param power + * @param power * @param basePower Base power stat as computed from raw power with baseStat * @param career Whether the horse is currently training in career * @param surface Race surface type * @param conditions Race ground conditions */ -export function adjustedStat(power: Stat.Power, basePower: number, career: boolean, surface: Surface, conditions: GroundConditions): number; +export function adjustedStat( + power: Stat.Power, + basePower: number, + career: boolean, + surface: Surface, + conditions: GroundConditions, +): number; /** * Compute adjusted guts for a race. - * @param guts + * @param guts * @param baseGuts Base guts stat as computed from raw guts with baseStat * @param career Whether the horse is currently training in career */ export function adjustedStat(guts: Stat.Guts, baseGuts: number, career: boolean): number; /** * Compute adjusted wit for a race. - * @param wit + * @param wit * @param baseWit Base wit stat as computed from raw wit with baseStat * @param career Whether the horse is currently training in career * @param styleApt Aptitude for the horse's current running style */ export function adjustedStat(wit: Stat.Wit, baseWit: number, career: boolean, styleApt: AptitudeLevel): number; -export function adjustedStat(stat: Stat, baseStat: number, career: boolean, surfaceOrStyle?: Surface | AptitudeLevel, conditions?: GroundConditions, thresholdMod?: number): number { +export function adjustedStat( + stat: Stat, + baseStat: number, + career: boolean, + surfaceOrStyle?: Surface | AptitudeLevel, + conditions?: GroundConditions, + thresholdMod?: number, +): number { const careerMod = career ? 400 : 0; switch (stat) { case Stat.Speed: @@ -229,13 +243,13 @@ export function thresholdMod(stat1: number, stat2?: number): number { return 0.5 * (thresholdMod(stat1) + thresholdMod(stat2)); } if (stat1 > 900) { - return 1.20; + return 1.2; } if (stat1 > 600) { return 1.15; } if (stat1 > 300) { - return 1.10; + return 1.1; } return 1.05; } @@ -337,7 +351,7 @@ const groundHPRateMod = { [GroundConditions.Good]: 1, [GroundConditions.Soft]: 1.01, [GroundConditions.Heavy]: 1.02, - } + }, } as const; /** @@ -352,14 +366,24 @@ const groundHPRateMod = { * @param pdm Whether pace-down mode is active (default false) * @param rushed Whether the horse is currently rushed (default false) */ -export function hpPerSecond(raceLen: number, surface: Surface, conditions: GroundConditions, gutsStat: number, phase: Phase, currentSpeed: number, dam?: boolean, pdm?: boolean, rushed?: boolean): number { +export function hpPerSecond( + raceLen: number, + surface: Surface, + conditions: GroundConditions, + gutsStat: number, + phase: Phase, + currentSpeed: number, + dam?: boolean, + pdm?: boolean, + rushed?: boolean, +): number { const rushedMod = rushed ? 1.6 : 1; const pdmMod = pdm ? 0.6 : 1; const damMod = dam ? 0.4 : 1; const groundMod = groundHPRateMod[surface][conditions]; - const gutsMod = phase === Phase.LateRace ? 1 + 200/(math.sqrt(600 * gutsStat) as number) : 1; + const gutsMod = phase === Phase.LateRace ? 1 + 200 / (math.sqrt(600 * gutsStat) as number) : 1; const sp = currentSpeed - baseSpeed(raceLen) + 12; - return 20/144 * sp * sp * rushedMod * pdmMod * damMod * groundMod * gutsMod; + return (20 / 144) * sp * sp * rushedMod * pdmMod * damMod * groundMod * gutsMod; } /** @@ -695,13 +719,20 @@ const conserveStratDistCoef = { }, } as const; -export function conservePowerAccel(distanceType: Distance, style: RunningStyle, rawPower: number, powerSkillBonus?: number, spotStruggled?: boolean, rushed?: boolean): number { +export function conservePowerAccel( + distanceType: Distance, + style: RunningStyle, + rawPower: number, + powerSkillBonus?: number, + spotStruggled?: boolean, + rushed?: boolean, +): number { const power = rawPower + (powerSkillBonus ?? 0); if (power <= 1200) { return 0; } const sdc = conserveStratDistCoef[style][distanceType]; - const activity = rushed ? 0.8 : (spotStruggled ? 0.98 : 1); + const activity = rushed ? 0.8 : spotStruggled ? 0.98 : 1; return Math.sqrt((power - 1200) * 130) * 0.001 * sdc * activity; } @@ -781,7 +812,7 @@ export function uphillMod(powerStat: number, slopePer: number): number { * @returns Speed modifier for downhill accel mode */ export function downhillAccelMod(slopePer: number): number { - return 0.3 - slopePer/10; + return 0.3 - slopePer / 10; } /** diff --git a/zenno/src/lib/racelib/accel.ts b/zenno/src/lib/racelib/accel.ts index ddb79e4..f2ca89c 100644 --- a/zenno/src/lib/racelib/accel.ts +++ b/zenno/src/lib/racelib/accel.ts @@ -2,22 +2,22 @@ * Instantaneous characteristics of a horse in motion. */ export interface Instant { - /** Timestamp of the instant. */ - t: number; - /** Forward position along the course. */ - x: number; - /** Actual speed. */ - v: number; + /** Timestamp of the instant. */ + t: number; + /** Forward position along the course. */ + x: number; + /** Actual speed. */ + v: number; } /** * Forward travel characteristics under the effects of acceleration. */ export interface Travel { - start: Instant; - end: Instant; - /** Actual acceleration value applied between start and end. */ - a: number; + start: Instant; + end: Instant; + /** Actual acceleration value applied between start and end. */ + a: number; } /** @@ -30,31 +30,38 @@ export interface Travel { * @param decel Negative acceleration value for cases where target speed is below start speed * @returns Travel segment after reaching the first of the given limits */ -export function travelSegment(start: Instant, targetSpeed: number, totalAccel: number, maxDur?: number, maxDist?: number, decel?: number): Travel { - const a = start.v < targetSpeed ? totalAccel : decel ?? -1; - const dvt = (targetSpeed - start.v) / a; - const dxt = maxDist != null ? (-start.v + Math.sqrt(start.v*start.v + 4*a*maxDist)) / (2*a) : dvt; - const time = maxDur != null ? Math.min(dvt, dxt, maxDur) : Math.min(dvt, dxt); - // To address numerical imprecision, if accel time comes from velocity or distance, - // set the reached values directly. - const v = time === dvt ? targetSpeed : start.v + time * a; - const dist = maxDist != null && time === dxt ? maxDist : (start.v + 0.5 * a * time) * time; - const end = { t: start.t + time, x: start.x + dist, v }; - return {start, end, a}; +export function travelSegment( + start: Instant, + targetSpeed: number, + totalAccel: number, + maxDur?: number, + maxDist?: number, + decel?: number, +): Travel { + const a = start.v < targetSpeed ? totalAccel : (decel ?? -1); + const dvt = (targetSpeed - start.v) / a; + const dxt = maxDist != null ? (-start.v + Math.sqrt(start.v * start.v + 4 * a * maxDist)) / (2 * a) : dvt; + const time = maxDur != null ? Math.min(dvt, dxt, maxDur) : Math.min(dvt, dxt); + // To address numerical imprecision, if accel time comes from velocity or distance, + // set the reached values directly. + const v = time === dvt ? targetSpeed : start.v + time * a; + const dist = maxDist != null && time === dxt ? maxDist : (start.v + 0.5 * a * time) * time; + const end = { t: start.t + time, x: start.x + dist, v }; + return { start, end, a }; } /** * Acceleration bonus from skills. */ export interface Skill { - /** - * Acceleration modifier in m/s². - */ - accel: number; - /** - * Remaining actual duration in seconds. - */ - dur: number; + /** + * Acceleration modifier in m/s². + */ + accel: number; + /** + * Remaining actual duration in seconds. + */ + dur: number; } /** @@ -66,25 +73,25 @@ export interface Skill { * @returns Acceleration segments and remainders of skills upon reaching the target speed */ export function up(start: Instant, targetSpeed: number, baseAccel: number, skills: Skill[]) { - const segments: Travel[] = []; - const sk = skills.map((s) => ({...s})).filter((s) => s.dur > 0); - while (start.v < targetSpeed) { - const active = sk.filter((s) => s.dur > 0); - if (active.length === 0) { - segments.push(travelSegment(start, targetSpeed, baseAccel)); - break; - } - const boost = active.reduce((a, s) => a + s.accel, 0); - const time = Math.min(...active.map((s) => s.dur)); - const seg = travelSegment(start, targetSpeed, baseAccel + boost, time); - segments.push(seg); - start = seg.end - for (const s of active) { - s.dur -= seg.end.t - seg.start.t; - if (s.dur < 1e-5) { - s.dur = 0 - } - } - } - return {segments, skills: sk} + const segments: Travel[] = []; + const sk = skills.map((s) => ({ ...s })).filter((s) => s.dur > 0); + while (start.v < targetSpeed) { + const active = sk.filter((s) => s.dur > 0); + if (active.length === 0) { + segments.push(travelSegment(start, targetSpeed, baseAccel)); + break; + } + const boost = active.reduce((a, s) => a + s.accel, 0); + const time = Math.min(...active.map((s) => s.dur)); + const seg = travelSegment(start, targetSpeed, baseAccel + boost, time); + segments.push(seg); + start = seg.end; + for (const s of active) { + s.dur -= seg.end.t - seg.start.t; + if (s.dur < 1e-5) { + s.dur = 0; + } + } + } + return { segments, skills: sk }; } diff --git a/zenno/src/lib/racelib/stat.ts b/zenno/src/lib/racelib/stat.ts index 58bad75..39cbd02 100644 --- a/zenno/src/lib/racelib/stat.ts +++ b/zenno/src/lib/racelib/stat.ts @@ -18,7 +18,7 @@ declare const __stage: unique symbol; export type Stage = 'raw' | 'base' | 'adjusted' | 'final' | 'skills'; -export type StatValue = number & {[__stage]: T}; +export type StatValue = number & { [__stage]: T }; export interface Stats { speed: StatValue; diff --git a/zenno/src/routes/doc/race/+page.svelte b/zenno/src/routes/doc/race/+page.svelte index 97c7563..cf5e532 100644 --- a/zenno/src/routes/doc/race/+page.svelte +++ b/zenno/src/routes/doc/race/+page.svelte @@ -58,9 +58,11 @@ ]); const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }]; const conservePower: Array = $derived([ - { label: 'Conserved Power', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, false)}, - { label: 'Rushed', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, true)}, - styleIsFront ? { label: 'Spot Struggled', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, true, false)} : null, + { label: 'Conserved Power', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, false) }, + { label: 'Rushed', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, false, true) }, + styleIsFront + ? { label: 'Spot Struggled', y: (_raw, x) => race.conservePowerAccel(distanceType, style, x, 0, true, false) } + : null, ]); const gutsSpurt: Array = $derived([ { label: `Speed 600`, y: (x) => race.spurtSpeed(600, x, style, distanceApt, raceLen) }, @@ -283,8 +285,11 @@ Conserve Power Acceleration

Bonus acceleration at spurt start from the Conserve Power mechanic.

-

This mechanic does not appear to be well understood, in particular how long the acceleration bonus lasts and how the rushed and spot struggle multipliers apply.

- {@render statChart(race.Stat.Power, conservePower, 'Acceleration (m/s²)', [0, 0.35], {len: true, style: true})} +

+ This mechanic does not appear to be well understood, in particular how long the acceleration bonus lasts and how the rushed + and spot struggle multipliers apply. +

+ {@render statChart(race.Stat.Power, conservePower, 'Acceleration (m/s²)', [0, 0.35], { len: true, style: true })} Guts diff --git a/zenno/src/routes/race/gate/+page.svelte b/zenno/src/routes/race/gate/+page.svelte index 7efdb78..63e2781 100644 --- a/zenno/src/routes/race/gate/+page.svelte +++ b/zenno/src/routes/race/gate/+page.svelte @@ -1,274 +1,309 @@

Gate Calculator

-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - {raceLen}m - - - -
-
-
- Duration - Accel - -
- {#each rawSkills as skill, i (i)} -
- {skill.dur}s - {skill.accel} - -
- {/each} -
- - - -
-
- - Quick Setup - -
-
- {#each quick as q (q.name)} - - {/each} -
-
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {raceLen}m + + + +
+
+
+ Duration + Accel + +
+ {#each rawSkills as skill, i (i)} +
+ {skill.dur}s + {skill.accel} + +
+ {/each} +
+ + + +
+
+ + Quick Setup + +
+
+ {#each quick as q (q.name)} + + {/each} +
+
{#snippet info(name: string, unit: string, val: number, val2?: number)} -
- {name} - {#if val2 == null} - {val.toFixed(3)} {unit} - {:else} - {val.toFixed(3)} – {val2.toFixed(3)} {unit} - {/if} -
+
+ {name} + {#if val2 == null} + {val.toFixed(3)} {unit} + {:else} + {val.toFixed(3)} – {val2.toFixed(3)} {unit} + {/if} +
{/snippet}
- {@render info('Base Acceleration', 'm/s²', postAccel)} - {@render info('Target Speed', 'm/s', speeds[0], speeds[1])} + {@render info('Base Acceleration', 'm/s²', postAccel)} + {@render info('Target Speed', 'm/s', speeds[0], speeds[1])}
- {@render info('Accel Time', 's', time[0], time[1])} - {@render info('Accel Distance', 'm', distance[0], distance[1])} - {@render info('Time to Mid-Race', 's', midTime[0], midTime[1])} + {@render info('Accel Time', 's', time[0], time[1])} + {@render info('Accel Distance', 'm', distance[0], distance[1])} + {@render info('Time to Mid-Race', 's', midTime[0], midTime[1])}
- {@render info('Skill Gain', 'L', gain[0], gain[1])} + {@render info('Skill Gain', 'L', gain[0], gain[1])}
{#snippet seggsTable(name: string, s: accel.Travel[])} - - - - - - - - - - - - {#each s as {start, end, a} (start.t)} - - - - - - - {/each} - -
{name}
TimeAccelDistanceSpeed
{end.t.toFixed(3)}{a.toFixed(3)}{end.x.toFixed(3)}{end.v.toFixed(3)}
+ + + + + + + + + + + + {#each s as { start, end, a } (start.t)} + + + + + + + {/each} + +
{name}
TimeAccelDistanceSpeed
{end.t.toFixed(3)}{a.toFixed(3)}{end.x.toFixed(3)}{end.v.toFixed(3)}
{/snippet} -
- {@render seggsTable(slope < 0 ? 'Fastest Speed (Downhill)' : 'Fastest Speed', seggs[1])} - {@render seggsTable('Slowest Speed', seggs[0])} +
+ {@render seggsTable(slope < 0 ? 'Fastest Speed (Downhill)' : 'Fastest Speed', seggs[1])} + {@render seggsTable('Slowest Speed', seggs[0])}
-

This calculator integrates acceleration analytically for efficiency, but the game uses a finite step approximation. The difference has some implications:

-
    -
  • Late starts are dramatically more punishing in the game, as acceleration is not applied for the first frame.
  • -
  • Start dash always completes a single frame at the dash target speed. This calculator finds the exact moment start dash target speed would be reached and continues integrating from there, leading to a slightly higher speed.
  • -
+

+ This calculator integrates acceleration analytically for efficiency, but the game uses a finite step approximation. The + difference has some implications: +

+
    +
  • Late starts are dramatically more punishing in the game, as acceleration is not applied for the first frame.
  • +
  • + Start dash always completes a single frame at the dash target speed. This calculator finds the exact moment start dash + target speed would be reached and continues integrating from there, leading to a slightly higher speed. +
  • +
diff --git a/zenno/src/routes/race/mspeed/+page.svelte b/zenno/src/routes/race/mspeed/+page.svelte index 802c9a0..29381ac 100644 --- a/zenno/src/routes/race/mspeed/+page.svelte +++ b/zenno/src/routes/race/mspeed/+page.svelte @@ -164,15 +164,7 @@ {/each}
- +