Compare commits

..
2 Commits
Author SHA1 Message Date
zephyr d0c11aa527 zenno: section speed does not use base wit
ci/woodpecker/push/zenno Pipeline was successful
ci/woodpecker/push/horsebot Pipeline was successful
Per analysis by Aya, only final wit is used for section speed.

Fixes #40.
2026-09-03 15:30:29 -04:00
zephyr c7a847f677 zenno/race/stam: add disclaimers
Also minor adjustments to chart size and how effective HP is calculated.
2026-09-03 15:10:47 -04:00
6 changed files with 54 additions and 56 deletions
+4 -10
View File
@@ -504,14 +504,12 @@ export function baseTargetSpeed(
/** /**
* Calculate the range of section speed values for a horse in early or mid race. * Calculate the range of section speed values for a horse in early or mid race.
* @param raceLen Length of the race in meters * @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 witStat Final wit stat, including strategy proficiency and skills
* @param style Horse's running style * @param style Horse's running style
* @param phase Phase of the current section * @param phase Phase of the current section
*/ */
export function sectionSpeed( export function sectionSpeed(
raceLen: number, raceLen: number,
baseWit: number,
witStat: number, witStat: number,
style: RunningStyle, style: RunningStyle,
phase: Exclude<Phase, Phase.LateRace>, phase: Exclude<Phase, Phase.LateRace>,
@@ -520,7 +518,6 @@ export function sectionSpeed(
* Calculate the range of section speed values for a horse not spurting during late race. * Calculate the range of section speed values for a horse not spurting during late race.
* @param raceLen Length of the race in meters * @param raceLen Length of the race in meters
* @param speedStat Final speed stat * @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 witStat Final wit stat, including strategy proficiency and skills
* @param style Horse's running style * @param style Horse's running style
* @param distance Hores's distance proficiency aptitude * @param distance Hores's distance proficiency aptitude
@@ -529,7 +526,6 @@ export function sectionSpeed(
export function sectionSpeed( export function sectionSpeed(
raceLen: number, raceLen: number,
speedStat: number, speedStat: number,
baseWit: number,
witStat: number, witStat: number,
style: RunningStyle, style: RunningStyle,
distance: AptitudeLevel, distance: AptitudeLevel,
@@ -537,16 +533,14 @@ export function sectionSpeed(
): [number, number]; ): [number, number];
export function sectionSpeed( export function sectionSpeed(
raceLen: number, raceLen: number,
speedStatOrBaseWit: number, speedStatOrWitStat: number,
baseWitOrWitStat: number,
witStatOrStyle: number | RunningStyle, witStatOrStyle: number | RunningStyle,
styleOrPhase: RunningStyle | Phase, styleOrPhase: RunningStyle | Phase,
distance?: AptitudeLevel, distance?: AptitudeLevel,
lateRace?: Phase.LateRace, lateRace?: Phase.LateRace,
): [number, number] { ): [number, number] {
const speedStat = lateRace !== undefined ? speedStatOrBaseWit : 0; const speedStat = lateRace !== undefined ? speedStatOrWitStat : 0;
const baseWit = lateRace !== undefined ? baseWitOrWitStat : speedStatOrBaseWit; const witStat = lateRace !== undefined ? witStatOrStyle : speedStatOrWitStat;
const witStat = lateRace !== undefined ? witStatOrStyle : baseWitOrWitStat;
const style = lateRace !== undefined ? (styleOrPhase as RunningStyle) : (witStatOrStyle as RunningStyle); const style = lateRace !== undefined ? (styleOrPhase as RunningStyle) : (witStatOrStyle as RunningStyle);
const phase = lateRace !== undefined ? lateRace : (styleOrPhase as Phase); const phase = lateRace !== undefined ? lateRace : (styleOrPhase as Phase);
const base = baseSpeed(raceLen); const base = baseSpeed(raceLen);
@@ -555,7 +549,7 @@ export function sectionSpeed(
phase === Phase.LateRace phase === Phase.LateRace
? (math.sqrt(500 * speedStat) as number) * distanceProficiencyMod[distance ?? AptitudeLevel.A] * 0.002 ? (math.sqrt(500 * speedStat) as number) * distanceProficiencyMod[distance ?? AptitudeLevel.A] * 0.002
: 0; : 0;
const u = (witStat / 550000) * math.log10(baseWit * 0.1); const u = (witStat / 550000) * math.log10(witStat * 0.1);
const l = u - 0.0065; const l = u - 0.0065;
return [baseTarget + late + base * l, baseTarget + late + base * u]; return [baseTarget + late + base * l, baseTarget + late + base * u];
} }
+17 -20
View File
@@ -123,7 +123,7 @@
const uphillSlowPow = 800; const uphillSlowPow = 800;
const uphillFastPow = 1200; const uphillFastPow = 1200;
const uphillBaseSpeed = mean2(sectionSpeed(2500, 1200, 1200, RunningStyle.FrontRunner, Phase.MidRace)); const uphillBaseSpeed = mean2(sectionSpeed(2500, 1200, RunningStyle.FrontRunner, Phase.MidRace));
const uphillSlopes = [ const uphillSlopes = [
{ per: 2, len: 110 }, { per: 2, len: 110 },
{ per: 1.5, len: 200 }, { per: 1.5, len: 200 },
@@ -137,13 +137,13 @@
const secSpeedS = 1200; const secSpeedS = 1200;
const secSpeedA = 1000; const secSpeedA = 1000;
const secSpeedExample = $derived(sectionSpeed(raceLen, secSpeedS, secSpeedS * 1.1, RunningStyle.FrontRunner, Phase.EarlyRace)); const secSpeedExample = $derived(sectionSpeed(raceLen, secSpeedS * 1.1, RunningStyle.FrontRunner, Phase.EarlyRace));
const secSpeedInfo = $derived(secSpeedExample.map((x) => x.toFixed(2)).join(' to ')); const secSpeedInfo = $derived(secSpeedExample.map((x) => x.toFixed(2)).join(' to '));
const secSpeedPassTime = $derived.by(() => { const secSpeedPassTime = $derived.by(() => {
const sSecSpeed = mean2(sectionSpeed(raceLen, secSpeedS, secSpeedS * 1.1, RunningStyle.FrontRunner, Phase.MidRace)); const sSecSpeed = mean2(sectionSpeed(raceLen, secSpeedS * 1.1, RunningStyle.FrontRunner, Phase.MidRace));
const sMode = frontModeEnterChance(secSpeedS * 1.1); const sMode = frontModeEnterChance(secSpeedS * 1.1);
const sOvertakeSpeed = modemean(sSecSpeed, 1.05, sMode); const sOvertakeSpeed = modemean(sSecSpeed, 1.05, sMode);
const aSecSpeed = mean2(sectionSpeed(raceLen, secSpeedA, secSpeedA, RunningStyle.FrontRunner, Phase.MidRace)); const aSecSpeed = mean2(sectionSpeed(raceLen, secSpeedA, RunningStyle.FrontRunner, Phase.MidRace));
const aMode = frontModeEnterChance(secSpeedA); const aMode = frontModeEnterChance(secSpeedA);
const aSpeedupSpeed = modemean(aSecSpeed, 1.04, aMode); const aSpeedupSpeed = modemean(aSecSpeed, 1.04, aMode);
const r = 2 / (sOvertakeSpeed - aSpeedupSpeed); const r = 2 / (sOvertakeSpeed - aSpeedupSpeed);
@@ -183,35 +183,35 @@
const secSpeedSeries: Array<ComputedAreas | null> = $derived([ const secSpeedSeries: Array<ComputedAreas | null> = $derived([
{ {
label: 'Early Race', label: 'Early Race',
y1: (x) => sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[0], y1: (x) => sectionSpeed(raceLen, x, secSpeedStyle, Phase.EarlyRace)[0],
y2: (x) => sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[1], y2: (x) => sectionSpeed(raceLen, x, secSpeedStyle, Phase.EarlyRace)[1],
}, },
{ {
label: 'Mid Race', label: 'Mid Race',
y1: (x) => sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[0], y1: (x) => sectionSpeed(raceLen, x, secSpeedStyle, Phase.MidRace)[0],
y2: (x) => sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[1], y2: (x) => sectionSpeed(raceLen, x, secSpeedStyle, Phase.MidRace)[1],
}, },
secIsFront secIsFront
? { ? {
label: 'Early Race + Mean Speed-Up Mode', label: 'Early Race + Mean Speed-Up Mode',
y1: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[0], 1.04, frontModeEnterChance(x)), y1: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.EarlyRace)[0], 1.04, frontModeEnterChance(x)),
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[1], 1.04, frontModeEnterChance(x)), y2: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.EarlyRace)[1], 1.04, frontModeEnterChance(x)),
} }
: { : {
label: 'Early Race + Mean Pace-Up Mode', label: 'Early Race + Mean Pace-Up Mode',
y1: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[0], 1.04, paceUpEnterChance(x)), y1: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.EarlyRace)[0], 1.04, paceUpEnterChance(x)),
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[1], 1.04, paceUpEnterChance(x)), y2: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.EarlyRace)[1], 1.04, paceUpEnterChance(x)),
}, },
secIsFront secIsFront
? { ? {
label: 'Mid Race + Mean Speed-Up Mode', label: 'Mid Race + Mean Speed-Up Mode',
y1: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[0], 1.04, frontModeEnterChance(x)), y1: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.MidRace)[0], 1.04, frontModeEnterChance(x)),
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[1], 1.04, frontModeEnterChance(x)), y2: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.MidRace)[1], 1.04, frontModeEnterChance(x)),
} }
: { : {
label: 'Mid Race + Mean Pace-Up Mode', label: 'Mid Race + Mean Pace-Up Mode',
y1: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[0], 1.04, paceUpEnterChance(x)), y1: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.MidRace)[0], 1.04, paceUpEnterChance(x)),
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[1], 1.04, paceUpEnterChance(x)), y2: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.MidRace)[1], 1.04, paceUpEnterChance(x)),
}, },
]); ]);
const fcSeries: ComputedSeries[] = $derived([ const fcSeries: ComputedSeries[] = $derived([
@@ -649,10 +649,7 @@
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.xdha40q2176g" ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.xdha40q2176g"
>Section Speed</Sec >Section Speed</Sec
> >
<p> <p>Each section, each horse gets a random modifier to target speed. The modifier's range is determined by the wit stat.</p>
Each section, each horse gets a random modifier to target speed. The modifier's range is determined by the wit stat.
(Curiously, the calculation uses both wit as modified by style proficiency and green skills as well as base wit.)
</p>
<div class="mb-24 h-60 w-full md:mb-20 md:h-96"> <div class="mb-24 h-60 w-full md:mb-20 md:h-96">
<StatChart <StatChart
class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10" class="mx-auto mb-12 h-full w-full max-w-3xl md:mb-10"
+12 -14
View File
@@ -86,37 +86,35 @@
const secSpeed: Array<ComputedAreas | null> = $derived([ const secSpeed: Array<ComputedAreas | null> = $derived([
{ {
label: 'Early Race', label: 'Early Race',
y1: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[0], y1: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[0],
y2: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[1], y2: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[1],
}, },
{ {
label: 'Mid Race', label: 'Mid Race',
y1: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[0], y1: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[0],
y2: (x) => race.sectionSpeed(raceLen, x, x, style, race.Phase.MidRace)[1], y2: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[1],
}, },
styleIsFront styleIsFront
? { ? {
label: 'Early Race + Mean Speed-Up Mode', label: 'Early Race + Mean Speed-Up Mode',
y1: (x) => y1: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[0], 1.04, race.frontModeEnterChance(x)),
modemean(race.sectionSpeed(raceLen, x, x, style, race.Phase.EarlyRace)[0], 1.04, race.frontModeEnterChance(x)), y2: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[1], 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', 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)), y1: (x) => modemean(race.sectionSpeed(raceLen, 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)), y2: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[1], 1.04, race.paceUpEnterChance(x)),
}, },
styleIsFront styleIsFront
? { ? {
label: 'Mid Race + Mean Speed-Up Mode', 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)), y1: (x) => modemean(race.sectionSpeed(raceLen, 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)), y2: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[1], 1.04, race.frontModeEnterChance(x)),
} }
: { : {
label: 'Mid Race + Mean Pace-Up Mode', 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)), y1: (x) => modemean(race.sectionSpeed(raceLen, 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)), y2: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[1], 1.04, race.paceUpEnterChance(x)),
}, },
]); ]);
const downhill: ComputedSeries[] = [ const downhill: ComputedSeries[] = [
+1 -1
View File
@@ -74,7 +74,7 @@
const damCarry = $derived(allowDAM && slope < 0 ? race.downhillAccelMod(slope) : 0); const damCarry = $derived(allowDAM && slope < 0 ? race.downhillAccelMod(slope) : 0);
const carry = $derived(skillCarry + damCarry); const carry = $derived(skillCarry + damCarry);
const midSpeed = $derived.by(() => { const midSpeed = $derived.by(() => {
const s = race.sectionSpeed(raceLen, baseWit, wit, style, race.Phase.MidRace) satisfies [number, number]; const s = race.sectionSpeed(raceLen, wit, style, race.Phase.MidRace) satisfies [number, number];
const u = slope > 0 ? race.uphillMod(power, slope) : 0; const u = slope > 0 ? race.uphillMod(power, slope) : 0;
return [s[0] + u, s[1] + u] as const; return [s[0] + u, s[1] + u] as const;
}); });
+1 -1
View File
@@ -83,7 +83,7 @@
} }
return [0, 0]; return [0, 0];
}); });
const earlyTarget = $derived(race.sectionSpeed(raceLen, baseWit, wit, style, race.Phase.EarlyRace)); const earlyTarget = $derived(race.sectionSpeed(raceLen, wit, style, race.Phase.EarlyRace));
const speeds: [number, number] = $derived([earlyTarget[0] + hillMod[0], earlyTarget[1] + hillMod[1]]); const speeds: [number, number] = $derived([earlyTarget[0] + hillMod[0], earlyTarget[1] + hillMod[1]]);
const startInstant: accel.Instant = { t: 0, x: 0, v: 3 }; const startInstant: accel.Instant = { t: 0, x: 0, v: 3 };
+19 -10
View File
@@ -96,14 +96,8 @@
const distType = $derived(race.distance(raceLen)); const distType = $derived(race.distance(raceLen));
const phaseSpeed = $derived({ const phaseSpeed = $derived({
[race.Phase.EarlyRace]: race.sectionSpeed( [race.Phase.EarlyRace]: race.sectionSpeed(raceLen, stats[race.Stat.Wit], style, race.Phase.EarlyRace),
raceLen, [race.Phase.MidRace]: race.sectionSpeed(raceLen, stats[race.Stat.Wit], style, race.Phase.MidRace),
baseStats[race.Stat.Wit],
stats[race.Stat.Wit],
style,
race.Phase.EarlyRace,
),
[race.Phase.MidRace]: race.sectionSpeed(raceLen, baseStats[race.Stat.Wit], stats[race.Stat.Wit], style, race.Phase.MidRace),
[race.Phase.LateRace]: [ [race.Phase.LateRace]: [
race.spurtSpeed(stats[race.Stat.Speed], stats[race.Stat.Guts], style, distanceApt, raceLen), race.spurtSpeed(stats[race.Stat.Speed], stats[race.Stat.Guts], style, distanceApt, raceLen),
race.spurtSpeed(stats[race.Stat.Speed], stats[race.Stat.Guts], style, distanceApt, raceLen), race.spurtSpeed(stats[race.Stat.Speed], stats[race.Stat.Guts], style, distanceApt, raceLen),
@@ -130,7 +124,8 @@
} }
return { mode: (mr * maxHP) / 10000, modePer: mp * 100, mean }; return { mode: (mr * maxHP) / 10000, modePer: mp * 100, mean };
}); });
const effStam = $derived(race.effectiveStam(raceLen, style, maxHP + statistics.mode)); const debuffHP = $derived((maxHP * debuffCounts.reduce((a, s) => a + s.r * s.n, 0)) / 10000);
const effStam = $derived(race.effectiveStam(raceLen, style, maxHP + statistics.mode - debuffHP));
const sections = $derived.by(() => { const sections = $derived.by(() => {
const r = course.splitSectionSlopes(raceLen, slopesRaw).map((s) => { const r = course.splitSectionSlopes(raceLen, slopesRaw).map((s) => {
@@ -448,7 +443,21 @@
</table> </table>
--> -->
{#if skills.length != 0 || risky} {#if skills.length != 0 || risky}
<div class="mx-auto h-96 max-w-5xl"> <div class="mx-auto h-96 max-w-3xl">
<HPChart {distribution} {maxHP} /> <HPChart {distribution} {maxHP} />
</div> </div>
{/if} {/if}
<div class="mx-auto mt-12 w-full max-w-4xl border-t pt-4 md:mt-8">
<p>
Mode HP from Skills gives the most frequent single result of recoveries and debuffs, along with the probability of that
result.
</p>
<p>
Effective Stamina gives the stamina stat which yields the same HP as the input configuration plus the mode HP from recovery
skills alone, ignoring debuffs. It can be considered the HP that removes wit checks from the equivalent HP pool.
</p>
<p>
As of now, this calculator does not handle recoveries that have no wit check, nor the HP effects of rushed, spot struggle, and
downhill running.
</p>
</div>