// 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)); }