zenno/doc/race: interactive graphs for race mechanics

For #16.
This commit is contained in:
2026-06-14 17:35:24 -04:00
parent 7f02925ff7
commit 2c9434729a
2 changed files with 534 additions and 2 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;
}
+389
View File
@@ -0,0 +1,389 @@
<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-4 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-24 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 flex w-full md:max-w-2xl">
{#if pick?.len}
<input
class="my-auto max-w-40 flex-1 md:flex-2"
type="range"
id="raceLen"
min="1000"
max="3600"
step="100"
bind:value={raceLen}
/>
<span class="my-auto flex-1 pl-2 text-sm">{raceLen}m</span>
{/if}
{#if pick?.style}
<label class="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="stylePick">Style</label>
<select class="my-auto flex-1 text-sm" 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="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="distPick">{raceLenType}</label>
<select class="my-auto flex-1 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="my-auto hidden flex-1 pr-2 text-right text-sm md:inline" for="surfPick">{raceLenType}</label>
<select class="my-auto flex-1 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])}
<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={accel}
yLabel="Acceleration (m/s²)"
range={[0.1, 0.5]}
/>
</div>
<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>