diff --git a/zenno/src/lib/race.ts b/zenno/src/lib/race.ts index 2af2974..d01543d 100644 --- a/zenno/src/lib/race.ts +++ b/zenno/src/lib/race.ts @@ -93,6 +93,20 @@ export function distance(raceLen: number): Distance { return Distance.Long; } +/** + * Get the distances at which each phase of a race ends. + * @param raceLen Length of the race in meters + * @returns Ends of each phase in meters + */ +export function phases(raceLen: number) { + const sixth = raceLen / 6; + return { + [Phase.EarlyRace]: sixth, + [Phase.MidRace]: 4 * sixth, + [Phase.LateRace]: raceLen, + } +} + /** * Race surfaces. */ @@ -372,7 +386,13 @@ export function fullSpurtHP(raceLen: number, spurtSpeed: number, hpRate: number) return spurtTime * hpRate; } -function baseSpeed(raceLen: number): number { +/** + * Calculate the base speed for a race. + * Note that this is not the base target speed for a horse. + * @param raceLen Race length in meters + * @returns Base speed for the race + */ +export function baseSpeed(raceLen: number): number { return 20 - (raceLen - 2000) / 1000; } @@ -748,13 +768,22 @@ export function duelAccelMod(gutsStat: number): number { * Calculate the speed modifier for running uphill. * Contrary to the race mechanics document, this is expressed as a negative number. * @param powerStat Final power stat - * @param slopePer Slope percentage, generally one of 0.5, 1.0, 1.5, or 2.0 + * @param slopePer Slope percentage, one of 1.0, 1.5, or 2.0 * @returns Speed modifier for running uphill, a negative value */ export function uphillMod(powerStat: number, slopePer: number): number { return (slopePer * -200) / powerStat; } +/** + * Calculate the speed modifier while running in downhill accel mode. + * @param slopePer Slope percentage, one of -1.0, -1.5, or -2.0 + * @returns Speed modifier for downhill accel mode + */ +export function downhillAccelMod(slopePer: number): number { + return 0.3 - slopePer/10; +} + /** * Calculate the forward speed boost given when moving lanewise while a skill * that grants a lane change speed boost is active. diff --git a/zenno/src/lib/racelib/accel.ts b/zenno/src/lib/racelib/accel.ts new file mode 100644 index 0000000..ddb79e4 --- /dev/null +++ b/zenno/src/lib/racelib/accel.ts @@ -0,0 +1,90 @@ +/** + * 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; +} + +/** + * 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; +} + +/** + * Advance a horse according to its forward motion characteristics. + * @param start Motion at the beginning of the segment + * @param targetSpeed Target speed to accelerate to + * @param totalAccel Total (positive) acceleration including skill bonuses + * @param maxDur Max duration of the acceleration, or infinite if not given + * @param maxDist Max distance of the acceleration, or infinite if not given + * @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}; +} + +/** + * Acceleration bonus from skills. + */ +export interface Skill { + /** + * Acceleration modifier in m/s². + */ + accel: number; + /** + * Remaining actual duration in seconds. + */ + dur: number; +} + +/** + * Accelerate up to a new target speed while under the effects of acceleration skills. + * @param startSpeed Speed at the start of the acceleration in m/s + * @param targetSpeed Speed at which the acceleration ends in m/s + * @param baseAccel Base acceleration value to which skills add, in m/s² + * @param skills Skills that are active for the acceleration period + * @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} +} diff --git a/zenno/src/lib/racelib/stat.ts b/zenno/src/lib/racelib/stat.ts new file mode 100644 index 0000000..58bad75 --- /dev/null +++ b/zenno/src/lib/racelib/stat.ts @@ -0,0 +1,29 @@ +/** + * Fundamental stats of umas. + */ +export enum Stat { + speed, + stamina, + power, + guts, + wit, +} + +/** + * Stats as a list for easy iteration. + */ +export const LIST = [Stat.speed, Stat.stamina, Stat.power, Stat.guts, Stat.wit] as const; + +declare const __stage: unique symbol; + +export type Stage = 'raw' | 'base' | 'adjusted' | 'final' | 'skills'; + +export type StatValue = number & {[__stage]: T}; + +export interface Stats { + speed: StatValue; + stamina: StatValue; + power: StatValue; + guts: StatValue; + wit: StatValue; +} diff --git a/zenno/src/routes/race/gate/+page.svelte b/zenno/src/routes/race/gate/+page.svelte new file mode 100644 index 0000000..7f4fc7b --- /dev/null +++ b/zenno/src/routes/race/gate/+page.svelte @@ -0,0 +1,267 @@ + + +

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} +
+
+
+ +{#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} +
+{/snippet} + +
+ {@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('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)}
+{/snippet} +
+ {@render seggsTable(slope < 0 ? 'Fastest Speed (Downhill)' : 'Fastest Speed', seggs[1])} + {@render seggsTable('Slowest Speed', seggs[0])} +