From a6d264c79d6e8d1f9cb6bc6f6a58f5010e396cd1 Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Wed, 1 Jul 2026 22:40:11 -0400 Subject: [PATCH] zenno: pull race mechanics lib update from stam-calc --- zenno/src/lib/race.ts | 300 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 279 insertions(+), 21 deletions(-) diff --git a/zenno/src/lib/race.ts b/zenno/src/lib/race.ts index 708d03d..8fb8fa2 100644 --- a/zenno/src/lib/race.ts +++ b/zenno/src/lib/race.ts @@ -31,6 +31,205 @@ export enum Mood { Great, } +/** + * Moods as a descending list for easy iteration. + */ +export const MOODS = [ + Mood.Great, + Mood.Good, + Mood.Normal, + Mood.Bad, + Mood.Awful, +] as const; + +const MOOD_COEF = { + [Mood.Awful]: 0.96, + [Mood.Bad]: 0.98, + [Mood.Normal]: 1, + [Mood.Good]: 1.02, + [Mood.Great]: 1.04, +} as const; + +function statClamp(x: number): number { + return Math.round(math.max(1, math.min(2000, x))); +} + +/** + * Calculate a horse's base stat value from her mood and raw stat value. + * @param mood Horse's mood + * @param rawStat Raw value of the stat, i.e. the displayed stat + * @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]; + return statClamp(r); +} + +/** + * Race distance types. + */ +export enum Distance { + Sprint, + Mile, + Medium, + Long +} + +/** + * Get the race distance type for a race length. + * @param raceLen Length of the race in meters + * @returns Distance type corresponding to the race length + */ +export function distance(raceLen: number): Distance { + if (raceLen < 1500) { + return Distance.Sprint; + } + if (raceLen < 1900) { + return Distance.Mile; + } + if (raceLen < 2500) { + return Distance.Medium; + } + return Distance.Long; +} + +/** + * Race surfaces. + */ +export enum Surface { + Turf, + Dirt, +} + +/** + * Race ground condition types. + */ +export enum GroundConditions { + Firm, + Good, + Soft, + Heavy, +} + +/** + * Ground condition types as a list for easy iterating. + */ +export const GROUND_CONDITONS = [ + GroundConditions.Firm, + GroundConditions.Good, + GroundConditions.Soft, + GroundConditions.Heavy, +] as const; + +const speedStatGroundMod = { + [Surface.Turf]: { + [GroundConditions.Firm]: 0, + [GroundConditions.Good]: 0, + [GroundConditions.Soft]: 0, + [GroundConditions.Heavy]: -50, + }, + [Surface.Dirt]: { + [GroundConditions.Firm]: 0, + [GroundConditions.Good]: 0, + [GroundConditions.Soft]: 0, + [GroundConditions.Heavy]: -50, + }, +} as const; + +const powerStatGroundMod = { + [Surface.Turf]: { + [GroundConditions.Firm]: 0, + [GroundConditions.Good]: -50, + [GroundConditions.Soft]: -50, + [GroundConditions.Heavy]: -50, + }, + [Surface.Dirt]: { + [GroundConditions.Firm]: -100, + [GroundConditions.Good]: -50, + [GroundConditions.Soft]: -100, + [GroundConditions.Heavy]: -100, + }, +} as const; + +const strategyProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.75, 0.85, 1.0, 1.1] as const; + +/** + * Compute adjusted speed for a race. + * @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; +/** + * Compute adjusted stamina for a race. + * @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 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; +/** + * Compute adjusted guts for a race. + * @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 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 { + const careerMod = career ? 400 : 0; + switch (stat) { + case Stat.Speed: + return statClamp(baseStat * thresholdMod! + speedStatGroundMod[surfaceOrStyle as Surface][conditions!] + careerMod); + case Stat.Stamina: + return statClamp(baseStat + careerMod); + case Stat.Power: + return statClamp(baseStat + powerStatGroundMod[surfaceOrStyle as Surface][conditions!] + careerMod); + case Stat.Guts: + return statClamp(baseStat + careerMod); + case Stat.Wit: + return statClamp(baseStat * strategyProficiencyMod[surfaceOrStyle as AptitudeLevel] + careerMod); + } +} + +export function thresholdMod(stat1: number, stat2?: number): number { + if (stat2 != null) { + return 0.5 * (thresholdMod(stat1) + thresholdMod(stat2)); + } + if (stat1 > 900) { + return 1.20; + } + if (stat1 > 600) { + return 1.15; + } + if (stat1 > 300) { + return 1.10; + } + return 1.05; +} + +export function finalStat(adjStat: number, skillPassive: number): number { + return statClamp(adjStat + skillPassive); +} + /** * Running styles for strategy–phase coefficients. * Great Escape is distinguished as a separate style even though it is @@ -93,6 +292,86 @@ export enum Phase { LateRace, } +const hpStrategyCoeff = { + [RunningStyle.FrontRunner]: 0.95, + [RunningStyle.PaceChaser]: 0.89, + [RunningStyle.LateSurger]: 1.0, + [RunningStyle.EndCloser]: 0.995, + [RunningStyle.GreatEscape]: 0.86, +} as const; + +/** + * Calculate a horse's max (starting) HP for a race. + * @param raceLen Length of the race in meters + * @param style Horse's running style + * @param stamStat Horse's final stamina stat + * @returns Max HP + */ +export function maxHP(raceLen: number, style: RunningStyle, stamStat: number): number { + return 0.8 * hpStrategyCoeff[style] * stamStat + raceLen; +} + +const groundHPRateMod = { + [Surface.Turf]: { + [GroundConditions.Firm]: 1, + [GroundConditions.Good]: 1, + [GroundConditions.Soft]: 1.02, + [GroundConditions.Heavy]: 1.02, + }, + [Surface.Dirt]: { + [GroundConditions.Firm]: 1, + [GroundConditions.Good]: 1, + [GroundConditions.Soft]: 1.01, + [GroundConditions.Heavy]: 1.02, + } +} as const; + +/** + * Calculate HP consumption in HP/s. + * @param raceLen Length of the race in meters + * @param surface Surface type of the race + * @param conditions Race ground conditions + * @param gutsStat Final guts stat, ignored outside of late race + * @param phase Current race phase + * @param currentSpeed Current speed in m/s, not including the effects of current speed skills + * @param dam Whether downhill accel mode is active (default false) + * @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 { + 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 sp = currentSpeed - baseSpeed(raceLen) + 12; + return 20/144 * sp * sp * rushedMod * pdmMod * damMod * groundMod * gutsMod; +} + +/** + * Calculate the HP required for a full spurt. + * @param raceLen Length of the race in meters + * @param spurtSpeed Target speed that a full spurt would produce + * @param hpRate HP usage per second during late race + * @returns HP threshold required for a full spurt + */ +export function fullSpurtHP(raceLen: number, spurtSpeed: number, hpRate: number): number { + // Per Aya: + /* + on hakuraku I do 62 here over 60 since we need the distance from last + spurt start to 60m before the end, not late race start to 60m to the + end, so since umas take an average of 1.5 frames to check their spurt + after late race and run at about 20 m/s mid race on most courses I + shifted it over by 1.5/15*20=2 + forgot which CM it was but I checked what would be the best fit + empirically one time and got like 62.1 + */ + // https://discord.com/channels/1493222996662419576/1493222998688137301/1517348759229436036 + const spurtLen = raceLen / 3 - 62; + const spurtTime = spurtLen / spurtSpeed; + return spurtTime * hpRate; +} + function baseSpeed(raceLen: number): number { return 20 - (raceLen - 2000) / 1000; } @@ -260,25 +539,6 @@ export function inverseSpurtSpeed( return Math.round(r); } -const hpStrategyCoeff = { - [RunningStyle.FrontRunner]: 0.95, - [RunningStyle.PaceChaser]: 0.89, - [RunningStyle.LateSurger]: 1.0, - [RunningStyle.EndCloser]: 0.995, - [RunningStyle.GreatEscape]: 0.86, -} as const; - -/** - * Calculate a horse's max HP (starting HP) for a race. - * @param raceLen Length of the race in meters - * @param style Horse's running style - * @param stamStat Final stamina stat - * @returns Max HP - */ -export function maxHP(raceLen: number, style: RunningStyle, stamStat: number): number { - return 0.8 * hpStrategyCoeff[style] * stamStat + raceLen; -} - /** Meters per horse length (馬身). */ export const HORSE_LENGTH = 2.5; /** Meters per course width (a constant unit of measure). */ @@ -410,8 +670,6 @@ export function spotStruggleSpeed(gutsStat: number): number { return Math.pow(500 * gutsStat, 0.6) * 0.0001; } -const strategyProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.75, 0.85, 1.0, 1.1] as const; - /** * Calculate the max duration of spot struggle. * Note that spot struggle ends early if the frontmost horse in it reaches a 5m lead,