Compare commits

..

4 Commits

Author SHA1 Message Date
zephyr 8ef3936be5 zenno/doc/race: fix mobile 2026-06-14 18:57:43 -04:00
zephyr b06e4b537b zenno: link up doc/race
Fixes #16.
2026-06-14 17:55:21 -04:00
zephyr 7b4cd41d3b zenno/doc/frbm: remove stat summaries
For #16.
2026-06-14 17:45:29 -04:00
zephyr 2c9434729a zenno/doc/race: interactive graphs for race mechanics
For #16.
2026-06-14 17:36:00 -04:00
5 changed files with 545 additions and 93 deletions
+145 -2
View File
@@ -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<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
@@ -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;
}
+8
View File
@@ -43,6 +43,14 @@
<a href="https://lanecalc.hf-uma.net/" target="_blank" rel="noopener noreferrer">危険回避シミュ</a>, or Lanecalc, for precise
lane combo simulation.
</li>
<li>
<a href={resolve('/doc/race')}>Race Mechanics Charts</a> &mdash; Interactive adaptation of
<a
href="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit"
target="_blank"
rel="noopener noreferrer">KuromiAK's race mechanics documentation</a
> for easier visualization of how mechanics scale.
</li>
</ul>
<h2 class="my-4 text-4xl">About</h2>
+8
View File
@@ -14,4 +14,12 @@
<a href="https://lanecalc.hf-uma.net/" target="_blank" rel="noopener noreferrer">危険回避シミュ</a>, or Lanecalc, for precise
lane combo simulation.
</li>
<li>
<a href={resolve('/doc/race')}>Race Mechanics Charts</a> &mdash; Interactive adaptation of
<a
href="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit"
target="_blank"
rel="noopener noreferrer">KuromiAK's race mechanics documentation</a
> for easier visualization of how mechanics scale.
</li>
</ul>
+5 -91
View File
@@ -1278,97 +1278,11 @@
</li>
</ul>
<Sec h={2} id="stats">Stat Reference</Sec>
<p>
This chapter is an extremely brief reference of what front runner race mechanics each stat and aptitude applies to and how
they scale.
</p>
<p>There are four types of each stat.</p>
<ol class="mb-4 list-decimal pl-4">
<li>
<i>Raw stat</i> is the number displayed in the horse's info. Once stat caps are raised, any portion of raw stats above 1200 is
halved.
</li>
<li><i>Base stat</i> is raw stat modified by mood.</li>
<li>
<i>Adjusted stat</i> is base stat modified by surface conditions (for speed and power), track stat thresholds (for speed), and
style proficiency (for wit).
</li>
<li><i>Final stat</i> or just <i>stat</i> is adjusted stat plus the effects of green skills.</li>
</ol>
<p>
For example, a 1200 wit style S horse in great mood with the corresponding savvy skill at level 1 has 1200 raw wit, 1248 base
wit, 1372 adjusted wit, and 1412 (final) wit.
</p>
<p>
<i>Linear scaling</i> means that a given increase to a stat produces the same change in the mechanical value regardless of the
stat value.
<i>Root scaling</i> means that the mechanical value scales with the square root, 0.6 power, or 0.7 power of the stat, which
means the increase becomes smaller as the stat grows.
<i>Log scaling</i> means it scales with the logarithm of the stat; this is the fastest diminishing scaling type.
<i>Log-linear scaling</i> means it scales with the stat value multiplied by its logarithm.
<i>Negative scaling</i> of any type means that the mechanic instead decreases as the stat increases.
<i>Inverse scaling</i> means it scales with the multiplicative inverse of the stat, i.e. <span class="font-mono">1/x</span>.
Inverse scaling is negative scaling by definition, which means that negative inverse scaling (e.g. skill activation chance)
flips back to positive.
</p>
<Sec h={3} id="stats-speed">Speed</Sec>
<ul class="mb-4 list-disc pl-4">
<li>Spurt speed, root scaling</li>
<li>Late race speed when not spurting, root scaling</li>
</ul>
<Sec h={3} id="stats-stamina">Stamina</Sec>
<ul class="mb-4 list-disc pl-4">
<li>HP, linear scaling</li>
</ul>
<Sec h={3} id="stats-power">Power</Sec>
<ul class="mb-4 list-disc pl-4">
<li>Move lane modifier (<a href="#lane-combo">lane combo</a>), root scaling</li>
<li><a href="#uphills">Uphill target speed loss</a>, negative inverse scaling</li>
<li>Acceleration, root scaling</li>
<li>Lane change target speed (i.e. the horizontal rather than forward speed of changing lanes), linear</li>
</ul>
<Sec h={3} id="stats-guts">Guts</Sec>
<ul class="mb-4 list-disc pl-4">
<li>Spurt speed, root scaling (roughly 5% of the effect of the speed stat)</li>
<li>Minimum speed, root scaling</li>
<li>HP consumption multiplier during last spurt, inverse root scaling</li>
<li>Target speed gain from spot struggle, root scaling</li>
<li>Duration of spot struggle, root scaling</li>
<li>Target speed gain from dueling, root scaling</li>
<li>Acceleration gain from dueling, root scaling</li>
</ul>
<Sec h={3} id="stats-wit">Wit</Sec>
<ul class="mb-4 list-disc pl-4">
<li><a href="#section-speed">Section speed</a>, log-linear scaling</li>
<li>Chance to enter <a href="#downhills">downhill accel mode</a>, linear scaling</li>
<li>Chance to accept a spurt speed and delay when not full spurting, linear scaling</li>
<li>Chance to activate skills, negative inverse scaling, base stat</li>
<li>Chance to enter <a href="#front-modes">speed-up and overtake modes</a> during position keep, log scaling</li>
<li>
For non-fronts, chance to enter pace-up mode, log scaling; but no effect on <a href="#pdm">pace-down mode</a>, which is
purely controlled by distance to the pacemaker
</li>
<li>Chance to become rushed, inverse log-squared scaling</li>
</ul>
<Sec h={3} id="stats-surface">Surface (Turf/Dirt) Aptitude</Sec>
<p>Surface aptitude is a multiplier on acceleration. (Not the power stat, for the purposes of other mechanics using power.)</p>
<Sec h={3} id="stats-distance">Distance Aptitude</Sec>
<p>Distance aptitude is a multiplier on spurt speed. At distance E and below, it is also a multiplier on acceleration.</p>
<Sec h={3} id="stats-style">Style Aptitude</Sec>
<p>
Style aptitude directly multiplies wit in the computation of the wit stat, which means it affects everything in <a
href="#stats-wit">the wit section</a
> except for mechanics that use base wit.
</p>
{#each ['stats', 'stats-speed', 'stats-stamina', 'stats-power', 'stats-guts', 'stats-wit', 'stats-surface', 'stats-style'] as id (id)}
<h2 class="hidden target:block target:py-32" {id}>
Stat Reference: This chapter moved to <a href={resolve('/doc/race')}>its own article</a>.
</h2>
{/each}
<Sec h={2} id="cm">My CM Teams</Sec>
+379
View File
@@ -0,0 +1,379 @@
<script lang="ts">
import type { ComputedAreas, ComputedSeries } from '$lib/chart';
import Article from '../Article.svelte';
import Sec from '../Sec.svelte';
import * as race from '$lib/race';
import StatChart from '$lib/StatChart.svelte';
import { resolve } from '$app/paths';
function modemean(x: number, bonus: number, p: number): number {
return x * (1 - p) + x * bonus * p;
}
let style = $state(race.RunningStyle.FrontRunner);
let surfaceApt = $state(race.AptitudeLevel.A);
let distanceApt = $state(race.AptitudeLevel.A);
let raceLen = $state(2000);
const raceLenType = $derived.by(() => {
if (raceLen < 1500) {
return 'Sprint';
}
if (raceLen < 1900) {
return 'Mile';
}
if (raceLen < 2500) {
return 'Medium';
}
return 'Long';
});
const spurtSpeed: Array<ComputedSeries | null> = $derived([
{ label: `Guts 1200`, y: (x) => race.spurtSpeed(x, 1200, style, distanceApt, raceLen) },
{ label: `Guts 600`, y: (x) => race.spurtSpeed(x, 600, style, distanceApt, raceLen) },
distanceApt !== race.AptitudeLevel.A
? { label: `${raceLenType} A, Guts 1200`, y: (x) => race.spurtSpeed(x, 1200, style, race.AptitudeLevel.A, raceLen) }
: null,
]);
const nonSpurtSpeed: Array<ComputedSeries | null> = $derived([
{ label: `Base Target Speed`, y: (x) => race.baseTargetSpeed(raceLen, style, race.Phase.LateRace, x, distanceApt) },
distanceApt !== race.AptitudeLevel.A
? {
label: `${raceLenType} A`,
y: (x) => race.baseTargetSpeed(raceLen, style, race.Phase.LateRace, x, race.AptitudeLevel.A),
}
: null,
]);
const hp: ComputedSeries[] = $derived([
{ label: 'Front Runner', y: (x) => race.maxHP(raceLen, race.RunningStyle.FrontRunner, x) },
{ label: 'Pace Chaser', y: (x) => race.maxHP(raceLen, race.RunningStyle.PaceChaser, x) },
{ label: 'Late Surger', y: (x) => race.maxHP(raceLen, race.RunningStyle.LateSurger, x) },
{ label: 'End Closer', y: (x) => race.maxHP(raceLen, race.RunningStyle.EndCloser, x) },
{ label: 'Runaway', y: (x) => race.maxHP(raceLen, race.RunningStyle.GreatEscape, x) },
]);
const moveLane: ComputedSeries[] = [{ label: 'Move Lane Modifier', y: (x) => race.moveLaneModifier(x) }];
const uphill: ComputedSeries[] = [
{ label: '+2 Hill', y: (x) => race.uphillMod(x, 2.0) },
{ label: '+1.5 Hill', y: (x) => race.uphillMod(x, 1.5) },
{ label: '+1 Hill', y: (x) => race.uphillMod(x, 1.0) },
];
const accel: Array<ComputedSeries | null> = $derived([
{ label: 'Early Race', y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.EarlyRace) },
{ label: 'Mid Race', y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.MidRace) },
{ label: 'Late Race', y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.LateRace) },
{
label: 'Early Race Uphill',
y: (x) => race.acceleration(x, style, surfaceApt, race.Phase.EarlyRace, race.AptitudeLevel.A, true),
},
]);
const laneChange: ComputedSeries[] = [{ label: 'Lane Change Target Speed', y: (x) => race.laneChangeSpeed(x) }];
const gutsSpurt: Array<ComputedSeries | null> = $derived([
{ label: `Speed 1200`, y: (x) => race.spurtSpeed(1200, x, style, distanceApt, raceLen) },
{ label: `Speed 600`, y: (x) => race.spurtSpeed(600, x, style, distanceApt, raceLen) },
distanceApt !== race.AptitudeLevel.A
? { label: `${raceLenType} A, Speed 1200`, y: (x) => race.spurtSpeed(1200, x, style, race.AptitudeLevel.A, raceLen) }
: null,
]);
const minSpeed: ComputedSeries[] = $derived([{ label: 'Minimum Speed', y: (x) => race.minSpeed(raceLen, x) }]);
const gutsHPRate: ComputedSeries[] = [{ label: 'HP Consumption Rate Multiplier', y: (x) => race.spurtHPRateMod(x) }];
const ssBoost: ComputedSeries[] = [{ label: 'Target Speed Boost', y: (x) => race.spotStruggleSpeed(x) }];
const ssDur: ComputedSeries[] = [
{ label: 'Front Runner S', y: (x) => race.spotStruggleDuration(x, race.AptitudeLevel.S) },
{ label: 'Front Runner A', y: (x) => race.spotStruggleDuration(x, race.AptitudeLevel.A) },
];
const duelSpeed: ComputedSeries[] = [{ label: 'Dueling Speed Boost', y: (x) => race.duelSpeedMod(x) }];
const duelAccel: ComputedSeries[] = [{ label: 'Dueling Acceleration Boost', y: (x) => race.duelAccelMod(x) }];
const styleIsFront = $derived(style === race.RunningStyle.FrontRunner || style === race.RunningStyle.GreatEscape);
const secSpeed: Array<ComputedAreas | null> = $derived([
{
label: 'Early Race',
y1: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[0],
y2: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[1],
},
{
label: 'Mid Race',
y1: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[0],
y2: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[1],
},
styleIsFront
? {
label: 'Early Race + Mean Speed-Up Mode',
y1: (x) =>
modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[0], 1.04, race.frontModeEnterChance(x)),
y2: (x) =>
modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[1], 1.04, race.frontModeEnterChance(x)),
}
: {
label: 'Early Race + Mean Pace-Up Mode',
y1: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[0], 1.04, race.paceUpEnterChance(x)),
y2: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[1], 1.04, race.paceUpEnterChance(x)),
},
styleIsFront
? {
label: 'Mid Race + Mean Speed-Up Mode',
y1: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[0], 1.04, race.frontModeEnterChance(x)),
y2: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[1], 1.04, race.frontModeEnterChance(x)),
}
: {
label: 'Mid Race + Mean Pace-Up Mode',
y1: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[0], 1.04, race.paceUpEnterChance(x)),
y2: (x) => modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[1], 1.04, race.paceUpEnterChance(x)),
},
]);
const downhill: ComputedSeries[] = [
{ label: 'Style S', y: (x) => race.downhillAccelEnterChance(x * 1.1) * 100 },
{ label: 'Style A', y: (x) => race.downhillAccelEnterChance(x) * 100 },
];
const reducedSpurt: ComputedSeries[] = [
{ label: 'Style S', y: (x) => race.reducedSpurtChance(x * 1.1) * 100 },
{ label: 'Style A', y: (x) => race.reducedSpurtChance(x) * 100 },
];
const skillChance: ComputedSeries[] = [
{ label: 'Per Skill', y: (x) => race.skillWitCheck(x) * 100 },
{ label: '1 of 2 or 2 of 2', y: (x) => (race.skillWitCheck(x, 2, 1) + race.skillWitCheck(x, 2, 2)) * 100 },
{ label: '2 of 2', y: (x) => race.skillWitCheck(x, 2, 2) * 100 },
{ label: '3 of 3', y: (x) => race.skillWitCheck(x, 3, 3) * 100 },
// TODO(zeph): why is this wrong?
{ label: '3 of 3 or 3 of 4', y: (x) => (race.skillWitCheck(x, 4, 3) + race.skillWitCheck(x, 3, 3)) * 100 },
];
const poskeep: ComputedSeries[] = [
{ label: 'Front Runner S', y: (x) => 100 * race.frontModeEnterChance(x * 1.1) },
{ label: 'Front Runner A', y: (x) => 100 * race.frontModeEnterChance(x) },
{ label: 'Style S', y: (x) => 100 * race.paceUpEnterChance(x * 1.1) },
{ label: 'Style A', y: (x) => 100 * race.paceUpEnterChance(x) },
];
const rushed: ComputedSeries[] = [
{ label: 'Style S', y: (x) => race.rushedChance(x * 1.1) * 100 },
{ label: 'Style A', y: (x) => race.rushedChance(x) * 100 },
];
</script>
{#snippet statChart(
stat: race.Stat,
y: Array<ComputedSeries | null>,
yLabel: string,
range?: [number, number],
pick?: { len?: boolean; style?: boolean; dist?: boolean; surf?: boolean },
)}
{#if !pick?.len && !pick?.style && !pick?.dist && !pick?.surf}
<div class="mb-12 h-60 w-full md:mb-20 md:h-96">
<StatChart
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
{stat}
{y}
{yLabel}
{range}
plotOptions={{ color: { legend: true } }}
/>
</div>
{:else}
<div class="mb-36 h-60 w-full md:mb-28 md:h-96">
<StatChart
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
{stat}
{y}
{yLabel}
{range}
plotOptions={{ color: { legend: true } }}
/>
<div class="mx-auto grid w-full grid-cols-3 gap-y-6 md:max-w-2xl md:grid-cols-7">
{#if pick?.len}
<input
class="col-span-2 my-auto w-full justify-self-center-safe"
type="range"
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="col-span-1 my-auto pl-2 text-sm">{raceLen}m</span>
{/if}
{#if pick?.style}
<select class="col-span-1 my-auto text-sm md:col-span-2" id="stylePick" bind:value={style}>
{#each race.RUNNING_STYLES as [name, style] (style)}
<option value={style}>{name}</option>
{/each}
</select>
{/if}
{#if pick?.dist}
<label class="col-span-1 my-auto pr-2 text-right text-sm md:inline" for="distPick">{raceLenType}</label>
<select class="col-span-1 my-auto text-sm" id="distPick" bind:value={distanceApt}>
{#each race.APTITUDE_LEVELS as l (l)}
<option value={l}>{race.AptitudeLevel[l]}</option>
{/each}
</select>
{/if}
{#if pick?.surf}
<label class="col-span-1 my-auto pr-2 text-right text-sm md:inline" for="surfPick">Surface</label>
<select class="col-span-1 my-auto text-sm" id="surfPick" bind:value={surfaceApt}>
{#each race.APTITUDE_LEVELS as l (l)}
<option value={l}>{l}</option>
{/each}
</select>
{/if}
</div>
</div>
{/if}
{/snippet}
<Article>
{#snippet head()}
<Sec h={1} id="top" class="text-center">Race Mechanics Charts</Sec>
<p>
This article is an interactive version of <a
href="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit"
target="_blank"
rel="noopener noreferrer">KuromiAK's race mechanics documentation</a
>
to give a better sense of how mechanics scale with stats and aptitudes.
</p>
<p>
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.
</p>
<p>
Only numeric mechanics that vary with stats and aptitudes are shown. Descriptions of concepts and conditions are left to the
original source.
</p>
<p>
Given the recent discovery that rushed chance uses integer RNG, I am assuming that all wit check mechanics use integer RNG
until shown otherwise.
</p>
{/snippet}
<Sec h={2} id="speed">Speed</Sec>
<Sec h={3} id="spurt-speed">Spurt Speed</Sec>
<p>Target speed during the Uma's last spurt. See also <a href="#spurt-speed-guts">the effect of Guts</a>.</p>
{@render statChart(race.Stat.Speed, spurtSpeed, 'Spurt Speed (m/s)', [20, 26], { len: true, style: true, dist: true })}
<Sec h={3} id="late-race-speed">Late Race Speed</Sec>
<p>Base target speed during late race when the Uma is not spurting (but not dead yet).</p>
{@render statChart(race.Stat.Speed, nonSpurtSpeed, 'Base Target Speed (m/s)', [18, 23], { len: true, style: true, dist: true })}
<Sec h={2} id="stamina">Stamina</Sec>
<Sec h={3} id="hp">HP</Sec>
<p>Max HP, i.e. starting HP.</p>
{@render statChart(race.Stat.Stamina, hp, 'HP', [1000, 5000])}
<Sec h={2} id="power">Power</Sec>
<Sec h={3} id="move-lane-mod">Move Lane Modifier</Sec>
<p>
Target speed bonus when changing lanes while under the effect of a lane change speed skill. See <a
href={resolve('/doc/frbm#lane-combo')}>lane combo</a
>.
</p>
{@render statChart(race.Stat.Power, moveLane, 'Target Speed Modifier (m/s)', [0.2, 0.5])}
<Sec h={3} id="uphill">Uphill Target Speed Loss</Sec>
<p>Target speed modifier while running uphill.</p>
{@render statChart(race.Stat.Power, uphill, 'Target Speed Modifier (m/s)', [-2, 0])}
<Sec h={3} id="acceleration">Acceleration</Sec>
<p>Acceleration.</p>
{@render statChart(race.Stat.Power, accel, 'Acceleration (m/s²)', [0.1, 0.5])}
<Sec h={3} id="lane-change-target-speed">Lane Change Target Speed</Sec>
<p>Horizontal (rather than forward) target speed of changing lanes.</p>
<div class="mb-4 h-60 w-full md:h-96">
<StatChart
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
stat={race.Stat.Power}
y={laneChange}
yLabel="Lane Change Target Speed (CW/s)"
range={[0.01, 0.03]}
/>
</div>
<Sec h={2} id="guts">Guts</Sec>
<Sec h={3} id="spurt-speed-guts">Spurt Speed</Sec>
<p>Target speed during the Uma's last spurt. See also <a href="#spurt-speed">the effect of Speed</a>.</p>
{@render statChart(race.Stat.Guts, gutsSpurt, 'Spurt Speed (m/s)', [20, 26], { len: true, style: true, dist: true })}
<Sec h={3} id="min-speed">Minimum Speed</Sec>
<p>Forced minimum speed, as well as target speed when out of HP.</p>
{@render statChart(race.Stat.Guts, minSpeed, 'Target Speed (m/s)', [15.8, 18.4], { len: true })}
<Sec h={3} id="hp-rate-mod">Late Race HP Consumption Rate Modifier</Sec>
<p>Multiplier on HP consumption rate during late race and last spurt phase.</p>
{@render statChart(race.Stat.Guts, gutsHPRate, 'HP Consumption Multiplier', [1.2, 1.6])}
<Sec h={3} id="spot-struggle">Spot Struggle</Sec>
<p>Speed boost and duration of spot struggle.</p>
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
<StatChart stat={race.Stat.Guts} y={ssBoost} yLabel="Speed Bonus (m/s)" range={[0, 0.3]} />
<StatChart stat={race.Stat.Guts} y={ssDur} yLabel="Duration (s)" range={[0, 12]} />
</div>
<Sec h={3} id="dueling">Dueling</Sec>
<p>Speed boost and acceleration boost of dueling.</p>
<div class="mb-4 grid h-96 w-full md:grid-cols-2">
<StatChart stat={race.Stat.Guts} y={duelSpeed} yLabel="Speed Bonus (m/s)" range={[0, 0.65]} />
<StatChart stat={race.Stat.Guts} y={duelAccel} yLabel="Acceleration Bonus (m/s²)" range={[0, 0.14]} />
</div>
<Sec h={2} id="wit">Wit</Sec>
<Sec h={3} id="section-speed">Section Speed</Sec>
<p>Random variance in target speed per race section.</p>
<div class="mb-24 h-60 w-full md:mb-20 md:h-96">
<StatChart
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
stat={race.Stat.Wit}
yArea={secSpeed}
yLabel="Section Speed (m/s)"
range={[17.5, 22.5]}
plotOptions={{ color: { legend: true } }}
/>
<div class="mx-auto flex w-full md:max-w-2xl">
<label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedRaceLen">Race Length</label>
<input
class="my-auto max-w-40 flex-1 md:flex-2"
type="range"
id="secSpeedRaceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="my-auto flex-1 pl-2 text-sm">{raceLen}m</span>
<label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="secSpeedStyle">Style</label>
<select class="my-auto flex-1 text-sm" id="secSpeedStyle" bind:value={style}>
{#each race.RUNNING_STYLES as [name, style] (style)}
<option value={style}>{name}</option>
{/each}
</select>
</div>
</div>
<Sec h={3} id="downhill">Downhill Accel Mode Chance</Sec>
<p>Chance each second to enter downhill accel mode when eligible.</p>
{@render statChart(race.Stat.Wit, downhill, 'Entry Chance (% each second)', [0, 60])}
<Sec h={3} id="spurt-accept">Reduced Spurt Accept Chance</Sec>
<p>Chance to accept each checked spurt delay and speed when not full spurting.</p>
{@render statChart(race.Stat.Wit, reducedSpurt, 'Accept Chance (% each candidate)', [0, 100])}
<Sec h={3} id="skill">Skill Activation Chance</Sec>
<p>
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.
</p>
<p>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)?</p>
{@render statChart(race.Stat.Wit, skillChance, 'Skill Activation Chance (%)', [0, 100])}
<Sec h={3} id="poskeep">Position Keep Mode Chance</Sec>
<p>
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.
</p>
{@render statChart(race.Stat.Wit, poskeep, 'Mode Entry Chance (%)', [0, 50])}
<Sec h={3} id="rushed">Rushed Chance</Sec>
<p>Chance for runners to become rushed at some point during the race.</p>
{@render statChart(race.Stat.Wit, rushed, 'Rushed Chance (%)', [0, 30])}
</Article>