Stats System

In short

Every effect on the batter is a StatModifier (stat, Add or Percent, value). PlayerStats keeps a flat list and computes final = (base + sum of Adds) x max(0, 1 + sum of Percents). Batters, level perks, upgrades, synergies, ballparks and boss pitchers all feed the same list.

How it works

  1. RunState.RecalculateStats() clears Stats and adds, in order: the batter’s modifiers, the batter’s level perks (LevelBonus, fixed for the run), every owned upgrade’s modifiers, then every active synergy’s bonus (SynergyResolver.GetActive).
  2. It runs at run start, on every AddUpgrade and on the dev menu’s ResetUpgrades.
  3. When an inning starts, RunManager.PlayCurrentRound clones the run’s stats and adds the park’s batterModifiers and the pitcher’s batterModifiers. The clone goes to the at-bat in RoundContext.stats. Co-op does the same in CoopRun; the batting cage adds the park’s.
  4. Code reads a stat with stats.Get(StatType.X) or GetInt (rounded).

Order does not matter: all Adds are summed, then all Percents are summed and applied once.

The stats

StatTypeBaseShown asRead where
Power1x1.00SwingResolver.FromContact (multiplies the contact zone’s exit velocity)
Contact1x1.00SwingResolver.HitRadius (adds 0.12 m of reach around the bat’s axis per point) and SwingResolver.Classify (scales the perfect and nice zone widths, at least x0.2)
TimingWindow0.11110 msSwingResolver.HitRadius (scales that reach by sqrt(TimingWindow / 0.11), so below the base it shrinks)
LaunchBonus0+0 degSwingResolver (added to launch angle)
HomeRunMultiplier1x1.00ScoreCalculator.Score
CoinBonus00RunManager.InningCoins (coins per home run)
ExtraOuts00RunManager.PlayCurrentRound, CoopRun (outs per inning)
RerollDiscount00ShopService.RerollCost
GreenBonus0.25+25%ScoreCalculator.GreenMultiplier
GreenStreakBonus0+0% per greenScoreCalculator.GreenMultiplier

Display names come from StatInfo.DisplayName (Power, Contact, Timing Window, Launch Angle, HR Multiplier, Coins per HR, Extra Outs, Reroll Discount, Green HR Score, Green Streak Score). StatModifier.Describe() writes lines like “+20% Power” or “+10 ms Timing Window”.

Who provides modifiers

SourceFieldExample from DefaultContent
BatterBatterDefinition.modifiersBaked Bean: +20% Power, -15% Timing Window
Batter level perkBatterDefinition.levelPerksPeapod: +2% Timing Window per perk (XP and Levels)
UpgradeUpgradeDefinition.modifierssee Upgrades
SynergySynergyDefinition.bonussee Synergies
BallparkBallparkDefinition.batterModifiersMidnight Yard: -10% Timing Window
Boss pitcherPitcherDefinition.batterModifiersThe Ace: -15% Timing Window; Knuckle Sandwich: -20% Contact

Numbers

WhatValueWhere to tune
Formula(BaseValue + sum Add) x max(0, 1 + sum Percent)PlayerStats.Get
Percent value meaning0.1 = +10%ModifierOp.Percent
Green multiplier1 + GreenBonus + GreenStreakBonus x (greenStreak - 1)ScoreCalculator.GreenMultiplier

In the code

FileWhat it does
Scripts/Stats/StatType.csStatType enum; StatInfo.BaseValue, DisplayName, Format, All.
Scripts/Stats/StatModifier.csModifierOp, StatModifier.Add / Percent, Describe.
Scripts/Stats/PlayerStats.csAdd, AddRange, Clear, Clone, Get, GetInt.
Scripts/Run/RunState.csRecalculateStats.
Scripts/Upgrades/SynergyResolver.csCountTag, GetActive.

Gotchas

  • Percents can push a stat to zero but never below (max(0, ...)), whatever the stack.
  • Park and pitcher modifiers are added to a clone for one inning only. They are not in RunState.Stats, so the dev menu’s ATTRIBUTES list and BuildPanel do not show them.
  • The at-bat keeps the stats it started with. Upgrades given mid-inning (dev menu) apply from the next inning.
  • MainMenuView shows POWER, CONTACT and TIMING bars from the batter’s stats, under a style line such as “HIGH TRAJECTORY - PULL HITTER”.
  • The StatType enum comments are stale: Contact does not multiply a sweet-spot radius around the aim point (it adds reach and widens the contact zones along the bat), and TimingWindow is not seconds early/late (it only scales the reach). See Hit radius (reach) and Batting Explained.
  • Batting style (trajectory, pull or push side, curve) is not a stat: it is read straight from BatterDefinition by SwingResolver.FromContact, so upgrades can’t change it.