289 lines
8.9 KiB
TypeScript
289 lines
8.9 KiB
TypeScript
// 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 strategy–phase 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));
|
||
}
|