diff --git a/zenno/src/lib/race.ts b/zenno/src/lib/race.ts index 2632e6b..c179051 100644 --- a/zenno/src/lib/race.ts +++ b/zenno/src/lib/race.ts @@ -107,6 +107,42 @@ const speedStrategyPhaseCoeff = [ 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): 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 @@ -224,6 +260,25 @@ 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). */ @@ -295,6 +350,57 @@ export function deceleration(phase: Phase, pdm?: boolean, dead?: boolean): numbe 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; + +/** + * 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 @@ -319,6 +425,24 @@ export function spotStruggleDuration(gutsStat: number, frontAptitude: AptitudeLe 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. @@ -348,7 +472,7 @@ export function moveLaneModifier(powerStat: number): number { * @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); + const p = Math.ceil(Math.max(20, 100 - 9000 / baseWit)) / 100; return binomPMF(p, N ?? 1, n ?? 1); } @@ -389,13 +513,22 @@ export function speedGain(speedBonus: number, dur: number, accel: number | null, 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 { + return Math.ceil(15 + 0.05 * witStat) / 100; +} + /** * 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; + return Math.ceil(witStat * 0.04) / 100; } /** @@ -415,3 +548,13 @@ export function frontModeEnterChance(witStat: number): number { export function paceUpEnterChance(witStat: number): number { return 0.15 * math.log10(witStat) - 0.15; } + +/** + * 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; +} diff --git a/zenno/src/routes/doc/race/+page.svelte b/zenno/src/routes/doc/race/+page.svelte new file mode 100644 index 0000000..2bdb16f --- /dev/null +++ b/zenno/src/routes/doc/race/+page.svelte @@ -0,0 +1,389 @@ + + +{#snippet statChart( + stat: race.Stat, + y: Array, + yLabel: string, + range?: [number, number], + pick?: { len?: boolean; style?: boolean; dist?: boolean; surf?: boolean }, +)} + {#if !pick?.len && !pick?.style && !pick?.dist && !pick?.surf} +
+ +
+ {:else} +
+ +
+ {#if pick?.len} + + {raceLen}m + {/if} + {#if pick?.style} + + + {/if} + {#if pick?.dist} + + + {/if} + {#if pick?.surf} + + + {/if} +
+
+ {/if} +{/snippet} + +
+ {#snippet head()} + Race Mechanics Charts +

+ This article is an interactive version of KuromiAK's race mechanics documentation + to give a better sense of how mechanics scale with stats and aptitudes. +

+

+ Mechanics are organized in this article according to the stats that determine them. Those that vary with multiple stats, + e.g. spurt speed, appear in all relevant sections with differing horizontal axes. Mechanics that vary with race distance, + distance aptitudes, and surface aptitudes have controls to change each by their charts. +

+

+ Only numeric mechanics that vary with stats and aptitudes are shown. Descriptions of concepts and conditions are left to the + original source. +

+

+ Given the recent discovery that rushed chance uses integer RNG, I am assuming that all wit check mechanics use integer RNG + until shown otherwise. +

+ {/snippet} + + Speed + + Spurt Speed +

Target speed during the Uma's last spurt. See also the effect of Guts.

+ {@render statChart(race.Stat.Speed, spurtSpeed, 'Spurt Speed (m/s)', [20, 26], { len: true, style: true, dist: true })} + + Late Race Speed +

Base target speed during late race when the Uma is not spurting (but not dead yet).

+ {@render statChart(race.Stat.Speed, nonSpurtSpeed, 'Base Target Speed (m/s)', [18, 23], { len: true, style: true, dist: true })} + + Stamina + + HP +

Max HP, i.e. starting HP.

+ {@render statChart(race.Stat.Stamina, hp, 'HP', [1000, 5000])} + + Power + + Move Lane Modifier +

+ Target speed bonus when changing lanes while under the effect of a lane change speed skill. See lane combo. +

+ {@render statChart(race.Stat.Power, moveLane, 'Target Speed Modifier (m/s)', [0.2, 0.5])} + + Uphill Target Speed Loss +

Target speed modifier while running uphill.

+ {@render statChart(race.Stat.Power, uphill, 'Target Speed Modifier (m/s)', [-2, 0])} + + Acceleration +

Acceleration.

+ {@render statChart(race.Stat.Power, accel, 'Acceleration (m/s²)', [0.1, 0.5])} +
+ +
+ + Lane Change Target Speed +

Horizontal (rather than forward) target speed of changing lanes.

+
+ +
+ + Guts + + Spurt Speed +

Target speed during the Uma's last spurt. See also the effect of Speed.

+ {@render statChart(race.Stat.Guts, gutsSpurt, 'Spurt Speed (m/s)', [20, 26], { len: true, style: true, dist: true })} + + Minimum Speed +

Forced minimum speed, as well as target speed when out of HP.

+ {@render statChart(race.Stat.Guts, minSpeed, 'Target Speed (m/s)', [15.8, 18.4], { len: true })} + + Late Race HP Consumption Rate Modifier +

Multiplier on HP consumption rate during late race and last spurt phase.

+ {@render statChart(race.Stat.Guts, gutsHPRate, 'HP Consumption Multiplier', [1.2, 1.6])} + + Spot Struggle +

Speed boost and duration of spot struggle.

+
+ + +
+ + Dueling +

Speed boost and acceleration boost of dueling.

+
+ + +
+ + Wit + + Section Speed +

Random variance in target speed per race section.

+
+ +
+ + + {raceLen}m + + +
+
+ + Downhill Accel Mode Chance +

Chance each second to enter downhill accel mode when eligible.

+ {@render statChart(race.Stat.Wit, downhill, 'Entry Chance (% each second)', [0, 60])} + + Reduced Spurt Accept Chance +

Chance to accept each checked spurt delay and speed when not full spurting.

+ {@render statChart(race.Stat.Wit, reducedSpurt, 'Accept Chance (% each candidate)', [0, 100])} + + Skill Activation Chance +

+ Chance that each skill that has a wit check will be eligible to activate. This uses base wit, so style aptitude and passive + wit skills do not affect the chance, although mood does. +

+

TODO(zeph): Why is the 3 of 4 series getting values over 100%? Is it not just binom(3, 4, p) + binom(4, 4, p)?

+ {@render statChart(race.Stat.Wit, skillChance, 'Skill Activation Chance (%)', [0, 100])} + + Position Keep Mode Chance +

+ Chance for front runners to enter speed-up or overtake mode, or for non-front runners to enter pace-up mode (but no effect on + pace-down mode), each 2 seconds when eligible. +

+ {@render statChart(race.Stat.Wit, poskeep, 'Mode Entry Chance (%)', [0, 50])} + + Rushed Chance +

Chance for runners to become rushed at some point during the race.

+ {@render statChart(race.Stat.Wit, rushed, 'Rushed Chance (%)', [0, 30])} +