fix(divergence): release the parked ball during multiball
Bug: with crazy-mode balls on the table, hitting the effect target while "release ball" (effect seven) was lit did nothing. The ball parked in the special hole stayed there. Cause: the release kept the original two-slot guard "only when no second ball exists" (balls.len() == 1), plus a check that the triggering ball's slot number differs from the hole's contact word. Both made sense when the released ball could only ever become slot two, but they block the release whenever more balls are in play. Fix: - Effect seven now releases whenever the special hole actually holds a parked ball (special_hole_active: multiball Ready and contact set), at most once per callback. The slot-number check is gone; with one ball it never mattered, and with more balls it blocked slot two. - publish_spawned_balls appends the released ball and makes it the owner of the special-hole contact under its real slot number, so it clears the contact itself as it rolls out. With one ball in play that number is 2, identical to the original's contact word. - While multiball is Active, remove_ball renumbers (or clears) that owner when an earlier ball leaves, like the claw and lock-hole owners. In Ready the word is the "ball parked" marker and stays untouched. The single-ball release path is unchanged; all existing special-hole and effect-seven tests still pass. Test Plan: - cargo test release_ball_frees_the_parked_ball_during_multiball: three balls in play, release spawns a fourth at the hole, the owner follows a removal, and the released ball leaves the hole and clears it. - Manual: in crazy mode, park a ball in the special hole (top left), light release ball, hit the effect target; a new ball rolls out.
This commit is contained in:
1 parent
a8cc0fc5fc
commit
0e8121d027
3 files changed
+77
-17
No files matched your search
@@ -31,6 +31,9 @@ and this project adheres to
|
|||||||
|
|
||||||
- Free a ball that was settling into a lock hole when another ball filled it,
|
- Free a ball that was settling into a lock hole when another ball filled it,
|
||||||
and release a lock hole whose settling ball left the table.
|
and release a lock hole whose settling ball left the table.
|
||||||
|
- The release-ball effect frees the ball parked in the special hole even when
|
||||||
|
several balls are already in play; the released ball keeps ownership of the
|
||||||
|
hole until it rolls out.
|
||||||
|
|
||||||
## [1.2.1] - 2026-09-01
|
## [1.2.1] - 2026-09-01
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ differences follow from treating every ball alike:
|
|||||||
and a lock hole the leaving ball was settling into is freed.
|
and a lock hole the leaving ball was settling into is freed.
|
||||||
- The rim of a filled lock hole only repels balls from the outside, so a ball
|
- The rim of a filled lock hole only repels balls from the outside, so a ball
|
||||||
that was settling into it when another ball filled it rolls back out.
|
that was settling into it when another ball filled it rolls back out.
|
||||||
|
- The release-ball effect (effect seven) frees a parked special-hole ball
|
||||||
|
whenever one is parked, not only when exactly one ball is in play. The
|
||||||
|
released ball owns the hole's contact under its own slot number.
|
||||||
- High-score entries carry a `crazy` flag that both local storage and the
|
- High-score entries carry a `crazy` flag that both local storage and the
|
||||||
shared service persist; older entries default to normal scores.
|
shared service persist; older entries default to normal scores.
|
||||||
|
|
||||||
|
|||||||
+71
-17
@@ -779,7 +779,22 @@ impl Game {
|
|||||||
}
|
}
|
||||||
// Effect seven publishes its spawn request during a slot pass, but
|
// Effect seven publishes its spawn request during a slot pass, but
|
||||||
// the new ball only joins once every slot pass has returned.
|
// the new ball only joins once every slot pass has returned.
|
||||||
self.balls.append(&mut self.spawned_balls);
|
self.publish_spawned_balls();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds balls released from the special hole to the end of the slot
|
||||||
|
/// order. The released ball owns the special-hole contact until it rolls
|
||||||
|
/// out; with one ball already in play that is slot two, as in the
|
||||||
|
/// original.
|
||||||
|
fn publish_spawned_balls(&mut self) {
|
||||||
|
for ball in std::mem::take(&mut self.spawned_balls) {
|
||||||
|
if self.balls.len() >= MAX_BALLS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] =
|
||||||
|
u16::try_from(self.balls.len() + 1).unwrap_or(2);
|
||||||
|
self.balls.push(ball);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_record_countdowns(&mut self) {
|
fn update_record_countdowns(&mut self) {
|
||||||
@@ -1195,7 +1210,6 @@ impl Game {
|
|||||||
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
let effect_triggered = self.check_effect_sensor(
|
let effect_triggered = self.check_effect_sensor(
|
||||||
ball,
|
ball,
|
||||||
pass.number,
|
|
||||||
old_position,
|
old_position,
|
||||||
movement_from_prediction(old_position, predicted),
|
movement_from_prediction(old_position, predicted),
|
||||||
events,
|
events,
|
||||||
@@ -1531,7 +1545,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
BallAction::Remove => self.remove_ball(0),
|
BallAction::Remove => self.remove_ball(0),
|
||||||
}
|
}
|
||||||
self.balls.append(&mut self.spawned_balls);
|
self.publish_spawned_balls();
|
||||||
scan.action
|
scan.action
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1608,7 +1622,7 @@ impl Game {
|
|||||||
) {
|
) {
|
||||||
action = completed_action;
|
action = completed_action;
|
||||||
}
|
}
|
||||||
self.check_effect_sensor(ball, pass.number, old_position, movement_velocity, events);
|
self.check_effect_sensor(ball, old_position, movement_velocity, events);
|
||||||
self.check_target_sensors(ball, old_position, movement_velocity, 150..=152, events);
|
self.check_target_sensors(ball, old_position, movement_velocity, 150..=152, events);
|
||||||
SensorScanResult { action }
|
SensorScanResult { action }
|
||||||
}
|
}
|
||||||
@@ -1880,7 +1894,6 @@ impl Game {
|
|||||||
fn check_effect_sensor(
|
fn check_effect_sensor(
|
||||||
&mut self,
|
&mut self,
|
||||||
ball: &mut Ball,
|
ball: &mut Ball,
|
||||||
ball_number: u16,
|
|
||||||
old_position: MilliVec,
|
old_position: MilliVec,
|
||||||
movement_velocity: MilliVec,
|
movement_velocity: MilliVec,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
@@ -1921,16 +1934,9 @@ impl Game {
|
|||||||
self.add_score(transfer, events);
|
self.add_score(transfer, events);
|
||||||
self.players[self.current_player].secondary_score = 0;
|
self.players[self.current_player].secondary_score = 0;
|
||||||
}
|
}
|
||||||
7 if self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] != 0
|
// Release the ball parked in the special hole, however
|
||||||
&& self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)]
|
// many balls are already in play.
|
||||||
!= ball_number
|
7 if self.special_hole_active() && self.spawned_balls.is_empty() => {
|
||||||
&& self.balls.len() == 1
|
|
||||||
&& self.spawned_balls.is_empty()
|
|
||||||
&& matches!(
|
|
||||||
self.multiball_state,
|
|
||||||
MultiballState::Ready | MultiballState::Active
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
self.spawned_balls.push(Ball {
|
self.spawned_balls.push(Ball {
|
||||||
position: vec2(17.0, 23.0),
|
position: vec2(17.0, 23.0),
|
||||||
velocity: MilliVec { x: 0, y: 3_040 }.to_velocity_per_second(),
|
velocity: MilliVec { x: 0, y: 3_040 }.to_velocity_per_second(),
|
||||||
@@ -2162,7 +2168,12 @@ impl Game {
|
|||||||
if *claw_contact != 99 && usize::from(*claw_contact) > removed_number {
|
if *claw_contact != 99 && usize::from(*claw_contact) > removed_number {
|
||||||
*claw_contact -= 1;
|
*claw_contact -= 1;
|
||||||
}
|
}
|
||||||
for contact in &mut self.record_contacts[129..=133] {
|
// While multiball is active the special-hole contact names the
|
||||||
|
// released ball rather than a parked one.
|
||||||
|
let special_owner = (self.multiball_state == MultiballState::Active)
|
||||||
|
.then_some(usize::from(SPECIAL_HOLE_SENSOR.id));
|
||||||
|
for contact_index in (129..=133).chain(special_owner) {
|
||||||
|
let contact = &mut self.record_contacts[contact_index];
|
||||||
if *contact == 99 || *contact == 0 {
|
if *contact == 99 || *contact == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -2736,6 +2747,50 @@ mod tests {
|
|||||||
assert!(!finished.player().crazy_mode_used());
|
assert!(!finished.player().crazy_mode_used());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_ball_frees_the_parked_ball_during_multiball() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.multiball_state = MultiballState::Ready;
|
||||||
|
game.record_contacts[148] = 2;
|
||||||
|
game.target_effect = 7;
|
||||||
|
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
|
||||||
|
game.balls[0].in_launcher = false;
|
||||||
|
game.balls[0].position = EFFECT_SENSOR.center;
|
||||||
|
for position in [vec2(250.0, 215.0), vec2(160.0, 300.0)] {
|
||||||
|
game.balls.push(Ball {
|
||||||
|
position,
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut events = Vec::new();
|
||||||
|
game.check_sensor_objects(
|
||||||
|
MilliVec::from_position(EFFECT_SENSOR.center),
|
||||||
|
MilliVec::default(),
|
||||||
|
&mut events,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(game.balls.len(), 4);
|
||||||
|
assert_eq!(game.balls[3].position, vec2(17.0, 23.0));
|
||||||
|
assert_eq!(game.multiball_state, MultiballState::Active);
|
||||||
|
assert!(!game.special_hole_active());
|
||||||
|
// The released ball owns the hole, and keeps owning it when an
|
||||||
|
// earlier ball leaves the table.
|
||||||
|
assert_eq!(game.record_contacts[148], 4);
|
||||||
|
game.remove_ball(1);
|
||||||
|
assert_eq!(game.record_contacts[148], 3);
|
||||||
|
|
||||||
|
for _ in 0..30 {
|
||||||
|
game.update(0.03, 3, Controls::default());
|
||||||
|
}
|
||||||
|
assert_eq!(game.record_contacts[148], 0);
|
||||||
|
assert!(
|
||||||
|
game.balls
|
||||||
|
.iter()
|
||||||
|
.all(|ball| ball.position.distance(vec2(17.0, 23.0)) > 12.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn player_count_is_bounded() {
|
fn player_count_is_bounded() {
|
||||||
assert_eq!(Game::new(0).players.len(), 1);
|
assert_eq!(Game::new(0).players.len(), 1);
|
||||||
@@ -3381,7 +3436,6 @@ mod tests {
|
|||||||
let mut effect_events = Vec::new();
|
let mut effect_events = Vec::new();
|
||||||
effect_game.check_effect_sensor(
|
effect_game.check_effect_sensor(
|
||||||
&mut effect_ball,
|
&mut effect_ball,
|
||||||
1,
|
|
||||||
MilliVec::from_position(EFFECT_SENSOR.center),
|
MilliVec::from_position(EFFECT_SENSOR.center),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut effect_events,
|
&mut effect_events,
|
||||||
|
|||||||
Reference in new issue
Block a user