zenno/doc/race: interactive graphs for race mechanics

For #16.
This commit is contained in:
2026-06-14 17:35:24 -04:00
parent 7f02925ff7
commit 2c9434729a
2 changed files with 534 additions and 2 deletions
+145 -2
View File
@@ -107,6 +107,42 @@ const speedStrategyPhaseCoeff = [
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
@@ -224,6 +260,25 @@ 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). */
@@ -295,6 +350,57 @@ export function deceleration(phase: Phase, pdm?: boolean, dead?: boolean): numbe
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;
/**
* 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
@@ -319,6 +425,24 @@ export function spotStruggleDuration(gutsStat: number, frontAptitude: AptitudeLe
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.
@@ -348,7 +472,7 @@ export function moveLaneModifier(powerStat: number): number {
* @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);
const p = Math.ceil(Math.max(20, 100 - 9000 / baseWit)) / 100;
return binomPMF(p, N ?? 1, n ?? 1);
}
@@ -389,13 +513,22 @@ export function speedGain(speedBonus: number, dur: number, accel: number | null,
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 {
return Math.ceil(15 + 0.05 * witStat) / 100;
}
/**
* 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;
return Math.ceil(witStat * 0.04) / 100;
}
/**
@@ -415,3 +548,13 @@ export function frontModeEnterChance(witStat: number): number {
export function paceUpEnterChance(witStat: number): number {
return 0.15 * math.log10(witStat) - 0.15;
}
/**
* 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;
}