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.
* @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 style Horse's running style
* @param phase Phase of the current section
*/
export function sectionSpeed(
raceLen: number,
baseWit: number,
witStat: number,
style: RunningStyle,
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.
* @param raceLen Length of the race in meters
* @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 style Horse's running style
* @param distance Hores's distance proficiency aptitude
@@ -529,7 +526,6 @@ export function sectionSpeed(
export function sectionSpeed(
raceLen: number,
speedStat: number,
baseWit: number,
witStat: number,
style: RunningStyle,
distance: AptitudeLevel,
@@ -537,16 +533,14 @@ export function sectionSpeed(
): [number, number];
export function sectionSpeed(
raceLen: number,
speedStatOrBaseWit: number,
baseWitOrWitStat: number,
speedStatOrWitStat: number,
witStatOrStyle: number | RunningStyle,
styleOrPhase: RunningStyle | Phase,
distance?: AptitudeLevel,
lateRace?: Phase.LateRace,
): [number, number] {
const speedStat = lateRace !== undefined ? speedStatOrBaseWit : 0;
const baseWit = lateRace !== undefined ? baseWitOrWitStat : speedStatOrBaseWit;
const witStat = lateRace !== undefined ? witStatOrStyle : baseWitOrWitStat;
const speedStat = lateRace !== undefined ? speedStatOrWitStat : 0;
const witStat = lateRace !== undefined ? witStatOrStyle : speedStatOrWitStat;
const style = lateRace !== undefined ? (styleOrPhase as RunningStyle) : (witStatOrStyle as RunningStyle);
const phase = lateRace !== undefined ? lateRace : (styleOrPhase as Phase);
const base = baseSpeed(raceLen);
@@ -555,7 +549,7 @@ export function sectionSpeed(
phase === Phase.LateRace
? (math.sqrt(500 * speedStat) as number) * distanceProficiencyMod[distance ?? AptitudeLevel.A] * 0.002
: 0;
const u = (witStat / 550000) * math.log10(baseWit * 0.1);
const u = (witStat / 550000) * math.log10(witStat * 0.1);
const l = u - 0.0065;
return [baseTarget + late + base * l, baseTarget + late + base * u];
}
+17 -20
View File
@@ -123,7 +123,7 @@
const uphillSlowPow = 800;
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 = [
{ per: 2, len: 110 },
{ per: 1.5, len: 200 },
@@ -137,13 +137,13 @@
const secSpeedS = 1200;
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 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 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 aSpeedupSpeed = modemean(aSecSpeed, 1.04, aMode);
const r = 2 / (sOvertakeSpeed - aSpeedupSpeed);
@@ -183,35 +183,35 @@
const secSpeedSeries: Array<ComputedAreas | null> = $derived([
{
label: 'Early Race',
y1: (x) => sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[0],
y2: (x) => sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[1],
y1: (x) => sectionSpeed(raceLen, x, secSpeedStyle, Phase.EarlyRace)[0],
y2: (x) => sectionSpeed(raceLen, x, secSpeedStyle, Phase.EarlyRace)[1],
},
{
label: 'Mid Race',
y1: (x) => sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[0],
y2: (x) => sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[1],
y1: (x) => sectionSpeed(raceLen, x, secSpeedStyle, Phase.MidRace)[0],
y2: (x) => sectionSpeed(raceLen, x, secSpeedStyle, Phase.MidRace)[1],
},
secIsFront
? {
label: 'Early Race + Mean Speed-Up Mode',
y1: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[0], 1.04, frontModeEnterChance(x)),
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[1], 1.04, frontModeEnterChance(x)),
y1: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.EarlyRace)[0], 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',
y1: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[0], 1.04, paceUpEnterChance(x)),
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.EarlyRace)[1], 1.04, paceUpEnterChance(x)),
y1: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.EarlyRace)[0], 1.04, paceUpEnterChance(x)),
y2: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.EarlyRace)[1], 1.04, paceUpEnterChance(x)),
},
secIsFront
? {
label: 'Mid Race + Mean Speed-Up Mode',
y1: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[0], 1.04, frontModeEnterChance(x)),
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[1], 1.04, frontModeEnterChance(x)),
y1: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.MidRace)[0], 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',
y1: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[0], 1.04, paceUpEnterChance(x)),
y2: (x) => modemean(sectionSpeed(raceLen, x, x, secSpeedStyle, Phase.MidRace)[1], 1.04, paceUpEnterChance(x)),
y1: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.MidRace)[0], 1.04, paceUpEnterChance(x)),
y2: (x) => modemean(sectionSpeed(raceLen, x, secSpeedStyle, Phase.MidRace)[1], 1.04, paceUpEnterChance(x)),
},
]);
const fcSeries: ComputedSeries[] = $derived([
@@ -649,10 +649,7 @@
ref="https://docs.google.com/document/d/15VzW9W2tXBBTibBRbZ8IVpW6HaMX8H0RP03kq6Az7Xg/edit?tab=t.0#heading=h.xdha40q2176g"
>Section Speed</Sec
>
<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>
<p>Each section, each horse gets a random modifier to target speed. The modifier's range is determined by the wit stat.</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"
+12 -14
View File
@@ -86,37 +86,35 @@
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],
y1: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[0],
y2: (x) => race.sectionSpeed(raceLen, 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],
y1: (x) => race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[0],
y2: (x) => race.sectionSpeed(raceLen, 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)),
y1: (x) => modemean(race.sectionSpeed(raceLen, 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)),
}
: {
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)),
y1: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.EarlyRace)[0], 1.04, race.paceUpEnterChance(x)),
y2: (x) => modemean(race.sectionSpeed(raceLen, 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)),
y1: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[0], 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',
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)),
y1: (x) => modemean(race.sectionSpeed(raceLen, x, style, race.Phase.MidRace)[0], 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[] = [
+1 -1
View File
@@ -74,7 +74,7 @@
const damCarry = $derived(allowDAM && slope < 0 ? race.downhillAccelMod(slope) : 0);
const carry = $derived(skillCarry + damCarry);
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;
return [s[0] + u, s[1] + u] as const;
});
+1 -1
View File
@@ -83,7 +83,7 @@
}
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 startInstant: accel.Instant = { t: 0, x: 0, v: 3 };
+19 -10
View File
@@ -96,14 +96,8 @@
const distType = $derived(race.distance(raceLen));
const phaseSpeed = $derived({
[race.Phase.EarlyRace]: race.sectionSpeed(
raceLen,
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.EarlyRace]: race.sectionSpeed(raceLen, stats[race.Stat.Wit], style, race.Phase.EarlyRace),
[race.Phase.MidRace]: race.sectionSpeed(raceLen, stats[race.Stat.Wit], style, race.Phase.MidRace),
[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),
@@ -130,7 +124,8 @@
}
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 r = course.splitSectionSlopes(raceLen, slopesRaw).map((s) => {
@@ -448,7 +443,21 @@
</table>
-->
{#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} />
</div>
{/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>