Add a Special Ball

In short

Special balls come from two lists of BallModifier (effect, chance, pitch ids, optional alternate): a park’s BallparkDefinition.balls and a pitcher’s own PitcherDefinition.balls. Each pitch rolls both on their own, and the two results stack. Using an existing effect is one line in DefaultContent. A new effect needs a BallEffect value, its label and colour, its flight, its HUD, sound, intro and index text, and a co-op protocol bump.

A: Put an existing ball on a park or a boss

  1. In Assets/_Project/Scripts/Data/DefaultContent.cs, add Ball(effect, chance, params pitchIds):
    moon.balls.Add(Ball(BallEffect.AntiGravity, 1f / 3f, "curveball", "sinker", "changeup")); // a park's ball
    benny.balls.Add(Ball(BallEffect.Wiffle, 1f / 4f));                                       // a boss's own ball
    Pitch ids are PitchTypeDefinition.id values (fastball, sinker, changeup, curveball, slider, knuckleball). No ids = every pitch.
  2. Optional: one of two effects. Set alternate: when the roll wins, it is the alternate half the time. The Gale:
    var gust = Ball(BallEffect.Headwind, 1f / 8f);
    gust.alternate = BallEffect.Tailwind;
    gale.balls.Add(gust);
  3. That’s all. The map card, briefing and HUD describe it (BallModifier.Describe, e.g. “Anti-Gravity balls: Curveballs, Sinkers, Changeups (1 in 3)”, “Headwind / Tailwind balls: any pitch (1 in 8)”); a boss’s lines come from PitcherDefinition.DescribeBalls as ”• His own …“. The INDEX’s SPECIAL BALLS entry lists every park and pitcher that throws it (Index Screen).

B: A brand-new kind of ball

  1. Enum value. Add it at the end of enum BallEffect in Assets/_Project/Scripts/Data/BallEffect.cs.
  2. Label and colour. Add cases to BallEffects.Label (shown in caps on the pitch pill) and BallEffects.Tint (trail, glow). Put tuning constants in BallEffects too, like SuperFastSpeed, GhostFadeOut or GustAt.
  3. The flight. Change the path in Pitching/PitchData.cs so the bat, timing ring, replay and spectators all agree. Always test with Has(BallEffect.X), which checks both slots (map and boss):
    • Progress for pace along the path (Sticky, Headwind, Tailwind; BallEffects.LateSpeedProgress keeps both ends fixed so effects stack),
    • Break for the shape of the break (Anti-Gravity),
    • PathAt for an extra offset (Wiffle’s WiffleOffset),
    • OpacityAt for fading (Ghost). A speed change goes in PitcherAI.CreatePitch, as SuperFast does:
    if (effect == BallEffect.SuperFast || bossEffect == BallEffect.SuperFast) mph *= BallEffects.SuperFastSpeed;
    A reward on a home run goes in AtBatController (see how swing.pitch.Has(BallEffect.Money) pays MoneyBallCoins).
  4. HUD colour. Add a case to HudView.PillColor (UI/HudView.cs).
  5. Sound. Add a pitch for its windup cue in GameAudio.SpecialBallPitch (Audio/GameAudio.cs); otherwise it plays at pitch 1.
  6. Match intro line. Add a case to MatchIntroCard.EffectText (Intro/MatchIntroCard.cs, a short caps line like “SWERVES HARD NEAR THE PLATE”); otherwise the intro card shows no effect text for it. See Cutscenes.
  7. Index text. Add a case to IndexCatalog.BallDescription (Data/IndexCatalog.cs); without one it reads “A special ball.” The entry itself appears automatically. If it was a planned ball (Heavy, Ice and Fire are listed as “Coming soon” in IndexCatalog.AddPlanned), delete its Planned(...) line.
  8. Put it on a park or a boss as in part A.
  9. Bump the co-op protocol. Both slots travel as bytes in AtBatPitch (SpectateMessages.Pitch: effect, bossEffect), so an older build would read an unknown value. Change NetSession.Protocol (today HRD-COOP-7; see Add a Net Message).

Numbers

WhatValueWhere to tune
Chance at Medium / Hardx1.5 / x2 the listed chance (park and boss balls, and decoys)Difficulties.BallChance
alternate split50 / 50PitcherAI.Roll
Pitch lean onto a ball’s pitch types80% of draws (only for balls with a pitch list)PitcherAI.FavouredShare
Super Fast speedx1.15BallEffects.SuperFastSpeed
$$$ home run bonus3 coinsBallEffects.MoneyBallCoins
Ghost fadeout 0.2 to 0.38 of the flight, 12% opacity until 0.62, back in by 0.82GhostFadeOut, GhostFaintFrom, GhostFaintTo, GhostFadeIn, GhostFaintest
Sticky slows at / to0.6 of the flight / 0.45 speedStickyAt, StickyDrag
Wiffle driftup to 0.6 m x 0.65 by 0.65 of the flight, then back to 0 at the plateWiffleSwerve, WiffleAt
Headwind / Tailwindfrom 0.7 of the flight, 0.45x / 1.9x paceGustAt, HeadwindSpeed, TailwindSpeed

Check it works

  • Tools → Home Run Derby → Run Ball Effect Test: checks each flight (anti-gravity, sticky, ghost, wiffle, gusts), that boss and park balls stack, the pitch lean, decoys, that only named pitches get the ball, that ball rolls never change which pitches come, the chances, the co-op pitch message, Describe / DescribeBalls, and that every park has its own boss. Add a check for a new effect’s flight here.
  • Press Play and F1 → FORCE A SPECIAL BALL to get it on every pitch (runs and the batting cage). The dev menu lists every BallEffect value automatically. It forces the map slot only.

Gotchas

  • PitcherAI.Roll takes the first ball in a list whose roll wins, so list order matters when one list has several balls on the same pitch. The park list and the pitcher list are separate slots, so one of each can land together.
  • The pitcher’s ball wins the look: colour, glow, sound and pill colour come from PitchData.Primary. The pill text names both (“WIFFLE GHOST FASTBALL”).
  • The rolls use their own seeded stream (BallStream), so a ball’s roll never changes the pitch sequence. But a ball limited to some pitch types makes pitchers lean on those types, and that lean draws from the pitch stream: adding or changing such a ball does change which pitches come (and the daily seed’s pitches).
  • Append enum values, never insert: the value is a number in exported assets and in the net message.