zenno: pull race mechanics lib update from stam-calc

This commit is contained in:
2026-07-01 22:40:11 -04:00
parent d62e729052
commit a6d264c79d
+279 -21
View File
@@ -31,6 +31,205 @@ export enum Mood {
Great, 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 strategyphase coefficients. * Running styles for strategyphase coefficients.
* Great Escape is distinguished as a separate style even though it is * Great Escape is distinguished as a separate style even though it is
@@ -93,6 +292,86 @@ export enum Phase {
LateRace, 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 { function baseSpeed(raceLen: number): number {
return 20 - (raceLen - 2000) / 1000; return 20 - (raceLen - 2000) / 1000;
} }
@@ -260,25 +539,6 @@ export function inverseSpurtSpeed(
return Math.round(r); 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 (馬身). */ /** Meters per horse length (馬身). */
export const HORSE_LENGTH = 2.5; export const HORSE_LENGTH = 2.5;
/** Meters per course width (a constant unit of measure). */ /** 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; 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. * Calculate the max duration of spot struggle.
* Note that spot struggle ends early if the frontmost horse in it reaches a 5m lead, * Note that spot struggle ends early if the frontmost horse in it reaches a 5m lead,