863 lines
27 KiB
TypeScript
863 lines
27 KiB
TypeScript
// 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 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,
|
||
}
|
||
|
||
/**
|
||
* 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;
|
||
}
|