Files
horse/zenno/src/lib/race.ts
T
2026-07-01 23:39:31 -04:00

863 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Umamusume race mechanics adapted from KuromiAK's doc:
// https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?usp=sharing
import * as math from 'mathjs';
import { binomPMF } from './prob';
/**
* Fundamental stats of umas.
*/
export enum Stat {
Speed,
Stamina,
Power,
Guts,
Wit,
}
/**
* Stats as a list for easy iteration.
*/
export const StatList = [Stat.Speed, Stat.Stamina, Stat.Power, Stat.Guts, Stat.Wit] as const;
/**
* Mood levels.
*/
export enum Mood {
Awful = -2,
Bad,
Normal,
Good,
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.
* Great Escape is distinguished as a separate style even though it is
* mechanically identical to Front Runner.
*/
export enum RunningStyle {
FrontRunner,
PaceChaser,
LateSurger,
EndCloser,
GreatEscape,
}
/**
* Running styles with their proper names, for easy iterating.
*/
export const RUNNING_STYLES = [
['Front Runner', RunningStyle.FrontRunner],
['Pace Chaser', RunningStyle.PaceChaser],
['Late Surger', RunningStyle.LateSurger],
['End Closer', RunningStyle.EndCloser],
['Great Escape', RunningStyle.GreatEscape],
] as const;
/**
* Aptitude or proficiency levels.
*/
export enum AptitudeLevel {
G,
F,
E,
D,
C,
B,
A,
S,
}
/**
* Aptitude levels as a descending list for easy iterating.
*/
export const APTITUDE_LEVELS = [
AptitudeLevel.S,
AptitudeLevel.A,
AptitudeLevel.B,
AptitudeLevel.C,
AptitudeLevel.D,
AptitudeLevel.E,
AptitudeLevel.F,
AptitudeLevel.G,
] as const;
/**
* Race phases.
* While last spurt phase is also a phase, it is not distinguished here.
*/
export enum Phase {
EarlyRace,
MidRace,
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;
}
const speedStrategyPhaseCoeff = [
[1.0, 0.98, 0.962],
[0.978, 0.991, 0.975],
[0.938, 0.998, 0.994],
[0.931, 1.0, 1.0],
[1.063, 0.962, 0.95],
] as const;
const distanceProficiencyMod = [0.1, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0, 1.05] as const;
/**
* Get a horse's base target speed outside of late race.
* @param raceLen Length of the race in meters
* @param style Horse's running style
* @param phase Phase to calculate for
*/
export function baseTargetSpeed(raceLen: number, style: RunningStyle, phase: Exclude<Phase, Phase.LateRace>): number;
/**
* Get a horse's base target speed when not spurting during late race.
* @param raceLen Length of the race in meters
* @param style Horse's running style
* @param phase Phase to calculate for
* @param speedStat Final speed stat
* @param distanceApt Horse's aptitude for the distance
*/
export function baseTargetSpeed(
raceLen: number,
style: RunningStyle,
phase: Phase.LateRace,
speedStat: number,
distanceApt: AptitudeLevel,
): number;
export function baseTargetSpeed(
raceLen: number,
style: RunningStyle,
phase: Phase,
speedStat?: number,
distanceApt?: number,
): number {
const base = baseSpeed(raceLen);
const baseTarget = base * speedStrategyPhaseCoeff[style][phase];
const late =
phase === Phase.LateRace ? (math.sqrt(500 * speedStat!) as number) * distanceProficiencyMod[distanceApt!] * 0.002 : 0;
return baseTarget + late;
}
/**
* Calculate the range of section speed values for a horse in early or mid race.
* @param raceLen Length of the race in meters
* @param baseWit Base wit, after mood but before other modifiers
* @param witStat Final wit stat, including strategy proficiency and skills
* @param style Horse's running style
* @param phase Phase of the current section
*/
export function sectionSpeed(
raceLen: number,
baseWit: number,
witStat: number,
style: RunningStyle,
phase: Exclude<Phase, Phase.LateRace>,
): [number, number];
/**
* Calculate the range of section speed values for a horse not spurting during late race.
* @param raceLen Length of the race in meters
* @param speedStat Final speed stat
* @param baseWit Base wit, after mood but before other modifiers
* @param witStat Final wit stat, including strategy proficiency and skills
* @param style Horse's running style
* @param distance Hores's distance proficiency aptitude
* @param phase Phase.LateRace
*/
export function sectionSpeed(
raceLen: number,
speedStat: number,
baseWit: number,
witStat: number,
style: RunningStyle,
distance: AptitudeLevel,
phase: Phase.LateRace,
): [number, number];
export function sectionSpeed(
raceLen: number,
speedStatOrBaseWit: number,
baseWitOrWitStat: number,
witStatOrStyle: number | RunningStyle,
styleOrPhase: RunningStyle | Phase,
distance?: AptitudeLevel,
lateRace?: Phase.LateRace,
): [number, number] {
const speedStat = lateRace !== undefined ? speedStatOrBaseWit : 0;
const baseWit = lateRace !== undefined ? baseWitOrWitStat : speedStatOrBaseWit;
const witStat = lateRace !== undefined ? witStatOrStyle : baseWitOrWitStat;
const style = lateRace !== undefined ? (styleOrPhase as RunningStyle) : (witStatOrStyle as RunningStyle);
const phase = lateRace !== undefined ? lateRace : (styleOrPhase as Phase);
const base = baseSpeed(raceLen);
const baseTarget = base * speedStrategyPhaseCoeff[style][phase];
const late =
phase === Phase.LateRace
? (math.sqrt(500 * speedStat) as number) * distanceProficiencyMod[distance ?? AptitudeLevel.A] * 0.002
: 0;
const u = (witStat / 550000) * math.log10(baseWit * 0.1);
const l = u - 0.0065;
return [baseTarget + late + base * l, baseTarget + late + base * u];
}
/**
* Calculate an Uma's last spurt target speed.
* @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(
speedStat: number,
gutsStat: number,
style: RunningStyle,
distance: AptitudeLevel,
raceLen: number,
): number {
const spc = speedStrategyPhaseCoeff[style][Phase.LateRace];
const dpm = distanceProficiencyMod[distance];
const base = baseSpeed(raceLen);
// Expand and rearrange terms from the doccy to make solving for the inverse easier.
// (lateBaseTarget + 0.01*base) * 1.05 + sqrt(500*speedStat) * dpm * 0.002
// = (base * spc + sqrt(500*speedStat) * dpm * 0.002 + 0.01 * base) * 1.05 + sqrt(500*speedStat) * dpm * 0.002
// = 1.05*base*spc + 1.05*sqrt(500*speedStat)*dpm*0.002 + 0.0105*base + sqrt(500*speedStat)*dpm*0.002
// = base * (1.05*spc + 0.0105) + 0.0041 * sqrt(500*speedStat) * dpm
// plus the guts component.
// TODO(zephyr): numerical precision?
return base * (1.05 * spc + 0.0105) + 0.0041 * Math.sqrt(500 * speedStat) * dpm + Math.pow(450 * gutsStat, 0.597) * 0.0001;
}
/**
* Calculate the speed stat which produces a given target speed in the last spurt.
* Inverse of spurtSpeed.
* @param spurtSpeed Target speed in the last spurt in m/s
* @param gutsStat Uma's guts stat. No accounting for mood or stat thresholds.
* @param style Running style
* @param distance Distance aptitude
* @param raceLen Length of the race in meters
* @returns Speed stat which produces the target speed
*/
export function inverseSpurtSpeed(
spurtSpeed: number,
gutsStat: number,
style: RunningStyle,
distance: AptitudeLevel,
raceLen: number,
): number {
// spurtSpeed = base * (1.05*spc + 0.0105) + 0.0041*sqrt(500*speedStat)*dpm + pow(450*gutsStat, 0.597)*0.0001
// spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001 = 0.0041*sqrt(500*speedStat)*dpm
// (spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001)²/(0.0041²*dpm²*500) = speedStat
// (spurtSpeed - base * (1.05*spc + 0.0105) - pow(450*gutsStat, 0.597)*0.0001)²/(0.008405*dpm²) = speedStat
const spc = speedStrategyPhaseCoeff[style][Phase.LateRace];
const dpm = distanceProficiencyMod[distance];
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);
return Math.round(r);
}
/** Meters per horse length (馬身). */
export const HORSE_LENGTH = 2.5;
/** Meters per course width (a constant unit of measure). */
export const COURSE_WIDTH = 11.25;
/** Meters per lane width. */
export const LANE_WIDTH = COURSE_WIDTH / 18;
const accelStrategyPhaseCoeff = {
[RunningStyle.FrontRunner]: [1.0, 1.0, 0.996],
[RunningStyle.PaceChaser]: [0.985, 1.0, 0.996],
[RunningStyle.LateSurger]: [0.975, 1.0, 1.0],
[RunningStyle.EndCloser]: [0.945, 1.0, 0.997],
[RunningStyle.GreatEscape]: [1.17, 0.94, 0.956],
} as const;
const surfaceProficiencyMod = [0.1, 0.3, 0.5, 0.7, 0.8, 0.9, 1.0, 1.05] as const;
const accelDistanceProficiencyMod = [0.4, 0.5, 0.6, 1, 1, 1, 1, 1] as const;
/**
* Calculate a horse's instantaneous acceleration value.
* @param powerStat Final power stat
* @param style Running style
* @param phase Current race phase for this frame
* @param surfaceAptitude Surface aptitude
* @param distanceAptitude Distance aptitude; no effect if not given
* @param uphill Whether this frame has a positive SlopePer value
* @param startDash Whether this frame is in the start dash period, i.e. current speed has not yet reached 85% of the race's base speed
* @returns Acceleration in m/s²
*/
export function acceleration(
powerStat: number,
style: RunningStyle,
surfaceAptitude: AptitudeLevel,
phase: Phase,
distanceAptitude?: AptitudeLevel,
uphill?: boolean,
startDash?: boolean,
): number {
const baseAccel = uphill ? 0.0004 : 0.0006;
const startDashMod = startDash ? 24.0 : 0;
const spc = accelStrategyPhaseCoeff[style][phase];
const spm = surfaceProficiencyMod[surfaceAptitude];
const dpm = accelDistanceProficiencyMod[distanceAptitude ?? AptitudeLevel.A];
return baseAccel * Math.sqrt(500 * powerStat) * spc * spm * dpm + startDashMod;
}
const phaseDecel = {
[Phase.EarlyRace]: -1.2,
[Phase.MidRace]: -0.8,
[Phase.LateRace]: -1.0,
} as const;
/**
* Get the phase-based deceleration value.
* @param phase Race phase that the horse is running this frame
* @param pdm Whether the horse is currently running in pace-down mode
* @param dead Whether the horse has zero or less HP
* @returns Current deceleration value in m/s², a negative value
*/
export function deceleration(phase: Phase, pdm?: boolean, dead?: boolean): number {
if (dead) {
return -1.2;
}
if (pdm) {
// This isn't until 1.5anni.
// return -0.5;
return phaseDecel[phase];
}
return phaseDecel[phase];
}
/**
* Calculate a horse's lane change target speed before the first move lane point.
* @param powerStat Final power stat
* @param maxLane Max lane value on the race track in CW
* @param currentLane Current lane on the race track in CW
* @returns Lane change speed in CW/s
*/
export function laneChangeSpeed(powerStat: number, maxLane: number, currentLane: number): number;
/**
* Calculate a horse's lane change target speed during late race and final spurt phase.
* @param powerStat Final power stat
* @param order Current order on the field
* @returns Lane change speed in CW/s
*/
export function laneChangeSpeed(powerStat: number, order: number): number;
/**
* Calculate a horse's lane change target speed between the first move lane point and late race.
* @param powerStat Final power stat
* @returns Lane change speed in CW/s
*/
export function laneChangeSpeed(powerStat: number): number;
export function laneChangeSpeed(powerStat: number, maxLaneOrOrder?: number, currentLane?: number): number {
const laneMod = currentLane != null ? 1 + (currentLane / maxLaneOrOrder!) * 0.05 : 1;
const orderMod = currentLane == null ? 1 + (maxLaneOrOrder ?? 0) * 0.05 : 1;
return 0.02 * (0.3 + 0.001 * powerStat) * laneMod * orderMod;
}
/**
* Acceleration of lane change (horizontal) current speed.
*/
export const LANE_CHANGE_ACCEL = 0.03;
const conserveStratDistCoef = {
[RunningStyle.FrontRunner]: {
[Distance.Sprint]: 1,
[Distance.Mile]: 1,
[Distance.Medium]: 1,
[Distance.Long]: 1,
},
[RunningStyle.PaceChaser]: {
[Distance.Sprint]: 0.7,
[Distance.Mile]: 0.8,
[Distance.Medium]: 0.9,
[Distance.Long]: 0.9,
},
[RunningStyle.LateSurger]: {
[Distance.Sprint]: 0.75,
[Distance.Mile]: 0.7,
[Distance.Medium]: 0.875,
[Distance.Long]: 1,
},
[RunningStyle.EndCloser]: {
[Distance.Sprint]: 0.7,
[Distance.Mile]: 0.75,
[Distance.Medium]: 0.86,
[Distance.Long]: 0.9,
},
[RunningStyle.GreatEscape]: {
[Distance.Sprint]: 1,
[Distance.Mile]: 1,
[Distance.Medium]: 1,
[Distance.Long]: 1,
},
} as const;
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);
return Math.sqrt((power - 1200) * 130) * 0.001 * sdc * activity;
}
/**
* Calculate a horse's minimum speed for a race.
* @param raceLen Length of the race in meters
* @param gutsStat Final guts stat
* @returns Minimum speed in m/s
*/
export function minSpeed(raceLen: number, gutsStat: number): number {
return 0.85 * baseSpeed(raceLen) + Math.sqrt(200 * gutsStat) * 0.001;
}
/**
* Calculate a horse's HP consumption multiplier for late race and last spurt phase.
* @param gutsStat Final guts stat
* @returns HP consumption multiplier
*/
export function spurtHPRateMod(gutsStat: number): number {
return 1 + 200 / Math.sqrt(600 * gutsStat);
}
/**
* Calculate the speed boost gained from spot struggle.
* @param gutsStat Final guts stat
* @returns Spot struggle speed boost in m/s
*/
export function spotStruggleSpeed(gutsStat: number): number {
return Math.pow(500 * gutsStat, 0.6) * 0.0001;
}
/**
* Calculate the max duration of spot struggle.
* Note that spot struggle ends early if the frontmost horse in it reaches a 5m lead,
* or at the start of section 9.
* @param gutsStat Final guts stat
* @param frontAptitude Front runner aptitude level
* @returns Spot struggle duration in s
*/
export function spotStruggleDuration(gutsStat: number, frontAptitude: AptitudeLevel): number {
// https://hakuraku.moe/notes/spot-struggle
return Math.sqrt(700 * gutsStat) * 0.012 * strategyProficiencyMod[frontAptitude];
}
/**
* Calculate the target speed bonus of dueling.
* @param gutsStat Final guts stat
* @returns Modifier to target speed while dueling in m/s
*/
export function duelSpeedMod(gutsStat: number): number {
return Math.pow(200 * gutsStat, 0.708) * 0.0001;
}
/**
* Calculate the acceleration bonus of dueling.
* @param gutsStat Final guts stat
* @returns Modifier to acceleration while dueling in m/s²
*/
export function duelAccelMod(gutsStat: number): number {
return Math.pow(160 * gutsStat, 0.59) * 0.0001;
}
/**
* 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
* @returns Speed modifier for running uphill, a negative value
*/
export function uphillMod(powerStat: number, slopePer: number): number {
return (slopePer * -200) / powerStat;
}
/**
* Calculate the forward speed boost given when moving lanewise while a skill
* that grants a lane change speed boost is active.
* @param powerStat Final power stat
* @returns Move-lane speed modifier in m/s
*/
export function moveLaneModifier(powerStat: number): number {
return Math.sqrt(0.0002 * powerStat);
}
/**
* Calculate the probability of n of N skills activating.
* @param baseWit Base wit stat
* @param N Number of skills available, default 1
* @param n Number of skills activating, default 1
* @returns Probability of exactly n skills out of N passing wit checks
*/
export function skillWitCheck(baseWit: number, N?: number, n?: number): number {
const p = Math.max(0.2, 1 - 90 / baseWit);
return binomPMF(p, N ?? 1, n ?? 1);
}
/**
* Calculate a skill's actual duration scaled to race length.
* @param baseDur Skill's listed duration in s
* @param raceLen Length of the race in m
* @returns Actual skill duration in s
*/
export function skillDuration(baseDur: number, raceLen: number): number {
return baseDur * raceLen * 0.001;
}
/**
* 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², 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, 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 = 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));
}
/**
* Calculate the probability of accepting a reduced spurt candidate.
* @param witStat Final wit stat
* @returns Probability per candidate to accept
*/
export function reducedSpurtChance(witStat: number): number {
// 1900 wit style S exceeds 100% chance if we don't explicitly cap.
return Math.min(0.15 + 0.0005 * witStat, 1);
}
/**
* Calculate the chance to enter downhill accel mode each second while running downhill.
* @param witStat Final wit stat, including style aptitude modifier
* @returns Probability each eligible tick to enter downhill accel mode
*/
export function downhillAccelEnterChance(witStat: number): number {
return witStat * 0.0004;
}
/**
* Calculate the chance for a front runner to enter speed-up or overtake mode when eligible.
* @param witStat Final wit stat, including style aptitude modifier
* @returns Probability each eligible tick to enter speed-up or overtake mode
*/
export function frontModeEnterChance(witStat: number): number {
return 0.2 * math.log10(witStat * 0.1);
}
/**
* Calculate the chance for a non-front runner to enter pace-up mode when eligible.
* @param witStat Final wit stat, including style aptitude modifier
* @returns Probability each eligible tick to enter pace-up mode
*/
export function paceUpEnterChance(witStat: number): number {
return 0.15 * math.log10(witStat * 0.1);
}
/**
* Calculate the chance for a horse to become rushed during a given race.
* @param witStat Final wit stat, including style aptitude modifier
* @returns Probability to become rushed during the race
*/
export function rushedChance(witStat: number): number {
const r = 6.5 / Math.log10(0.1 * witStat + 1);
return Math.ceil(r * r) / 100;
}