fix(divergence): replace a full wheel immediately during multiball

Bug: filling the fifth wheel hole with several balls in play awarded the
transfer, multiplier, and extra ball, but did not run the wheel
replacement. The original deferred the 281-frame panel until a later
launcher reset, which with crazy-mode balls could be a long time, while
the full wheel kept every hole closed.

Requirement: a full wheel must be replaced with the same animation and
scoring as in single-ball play. Balls still in play keep moving, with
the wheel's outer border as a solid boundary and no hole captures until
the replacement is done.

Changes:
- complete_lock_hole starts the panel for a multiball fill too (if one
  is not already running). The single-ball path is unchanged and still
  restarts it unconditionally, as in the original. Scoring was already
  identical and is untouched.
- timer_tick only freezes physics during the panel while every ball
  waits in the launcher. That is always the case for the single-ball
  replacement (the locking ball was reset), so it is frozen exactly as
  before; during multiball the balls in play keep integrating.
- While the panel runs, check_lock_holes captures nothing and
  retain_wheel_rim adds a circle around the wheel hub (record 134's
  center, 48 px ball-center radius, enclosing the five hole posts) with
  the hub's material. It only repels balls moving towards it, so a ball
  already inside rolls out instead of being caged.
- Stress runs then showed balls trapped for good at the bottom of a hole
  pocket, cradled between the hub and a post, after another ball filled
  that hole or the panel closed it. The pocket bottom lies outside the
  hole's capture zone, so nothing ever moved them. A filled or closed
  hole now kicks such a ball (more than 4 px inside its rim) out of its
  center, radially away from the hub at 3 px/step, a path that clears
  both neighbouring posts. Balls arriving from outside are reflected by
  the rim before getting that deep.
- A lock or special-hole capture that removes the last remaining ball
  of a multiball callback now resets it to the launcher (or finishes it
  when tilted), exactly like the single-ball capture, instead of ending
  the player's ball.

Test Plan:
- cargo test (164 tests), including
  full_wheel_during_multiball_replaces_it_while_balls_play_on (rim
  bounce, no captures, balls move, wheel cleared after the panel),
  filled_or_closed_lock_hole_kicks_out_a_ball_resting_in_its_pocket
  (the exact trapped position seen in stress runs), and the updated
  fifth_multiball_lock_starts_the_panel_and_caps_survivor_reaward.
- A temporary release-mode stress run (not committed): 40 seeded
  two-player games with crazy mode, autoplay flippers, and nudges all
  finished; 62 wheel replacements happened during multiball; no
  invariant violations.
- Manual: in crazy mode fill the fifth wheel hole; the replacement
  animation plays while the other balls keep moving and bounce off the
  wheel instead of dropping into its holes.
This commit is contained in:
ddidderr committed 2026-10-10 11:46:28 +02:00
1 parent 17a7b3c328
commit 158936577a
4 files changed
+222 -10

No files matched your search

+9
View File
@@ -91,6 +91,15 @@ differences follow from treating every ball alike:
- The claw re-targets a shooter-lane release (terminal frame 18) to a table
release whenever more than one ball is in play during its ride, not only
when the capture began in multiball.
- A full wheel during multiball starts the 281-frame replacement immediately
(the original deferred it to the next launcher reset). The table only freezes
while every ball waits in the launcher; otherwise balls keep moving, the lock
holes do not capture, and the wheel's outer rim (record 134's center, 48 px
ball contact radius) repels balls from outside.
- A ball resting inside a filled or closed lock hole's pocket is kicked out
from the hole center, radially away from the hub. A capture that removes
the last ball of a multiball callback resets it to the launcher like a
single-ball capture instead of ending the ball.
- High-score entries carry a `crazy` flag that both local storage and the
shared service persist; older entries default to normal scores.