zenno: pull race mechanics lib update from stam-calc
This commit is contained in:
+279
-21
@@ -31,6 +31,205 @@ export enum Mood {
|
||||
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
|
||||
@@ -93,6 +292,86 @@ export enum Phase {
|
||||
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;
|
||||
}
|
||||
@@ -260,25 +539,6 @@ 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). */
|
||||
@@ -410,8 +670,6 @@ 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,
|
||||
|
||||
Reference in New Issue
Block a user