Files
horse/zenno/src/lib/race.ts

289 lines
8.9 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
/**
* 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,
}
/**
* 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,
}
/**
* 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,
}
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;
/**
* 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 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;
}
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,
* 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 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 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²
* @param decel Current phase-based deceleration value in m/s², a negative value.
* @param dur Duration of the boosted speed
* @returns Distance gained from the speed boost in m
*/
export function speedGain(speedBonus: number, accel: number, decel: number, dur: number): 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 = speedBonus / accel;
const decelTime = -speedBonus / decel;
// speedBonus*(dur-accelTime) + speedBonus*accelTime/2 + speedBonus*decelTime/2
return speedBonus * (dur + 0.5 * (decelTime - accelTime));
}