fix(rules): scope double scoring to the active ball
The binary stores the diamond-bank double flag at receiver +0x0bdc and clears it in normal_ball_end. Rust stored that flag on Player, so it incorrectly survived every normal drain and player turn. The special effect-seven respawn is intentionally different because it bypasses normal_ball_end. Represent the flag as an explicit ball-double state on Game. Clear it only on the normal ball-end path, preserve it through the special respawn, and make the first completed diamond bank after a normal drain re-enable double scoring without adding 100,000 secondary points. Keep the independent multiball factor, so both active factors still stack to 4x. Test Plan: - `cargo test --workspace --all-targets --all-features` -- 117 passed - `cargo clippy --workspace --all-targets --all-features -- -D warnings` -- passed - `rumdl check --flavor commonmark RECONSTRUCTION.md CHANGELOG.md` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
+47
-13
@@ -170,7 +170,6 @@ pub struct Player {
|
||||
pub extra_balls: u8,
|
||||
pub bumper_value: u32,
|
||||
pub diamond_segments: u8,
|
||||
pub double_score: bool,
|
||||
pub media_level: u8,
|
||||
rules: RuleState,
|
||||
}
|
||||
@@ -185,7 +184,6 @@ impl Default for Player {
|
||||
extra_balls: 0,
|
||||
bumper_value: 1_000,
|
||||
diamond_segments: 0,
|
||||
double_score: false,
|
||||
media_level: 0,
|
||||
rules: RuleState::default(),
|
||||
}
|
||||
@@ -222,6 +220,13 @@ enum ScoreMode {
|
||||
Multiball,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum BallDoubleState {
|
||||
#[default]
|
||||
Inactive,
|
||||
Active,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum WheelResetGate {
|
||||
#[default]
|
||||
@@ -350,6 +355,7 @@ pub struct Game {
|
||||
multiball_state: MultiballState,
|
||||
wheel_reset_gate: WheelResetGate,
|
||||
score_mode: ScoreMode,
|
||||
ball_double: BallDoubleState,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
@@ -392,6 +398,7 @@ impl Game {
|
||||
multiball_state: MultiballState::Unavailable,
|
||||
wheel_reset_gate: WheelResetGate::Enabled,
|
||||
score_mode: ScoreMode::Normal,
|
||||
ball_double: BallDoubleState::Inactive,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1311,18 +1318,18 @@ impl Game {
|
||||
};
|
||||
self.add_score(award, events);
|
||||
if segments == 8 {
|
||||
self.players[self.current_player].double_score = true;
|
||||
self.ball_double = BallDoubleState::Active;
|
||||
for object_id in [153, 6, 154] {
|
||||
self.object_active[object_id] = true;
|
||||
}
|
||||
}
|
||||
events.push(Event::Sound(2017));
|
||||
} else {
|
||||
let player = &mut self.players[self.current_player];
|
||||
if player.double_score {
|
||||
if self.ball_double == BallDoubleState::Active {
|
||||
let player = &mut self.players[self.current_player];
|
||||
player.secondary_score = player.secondary_score.wrapping_add(100_000);
|
||||
} else {
|
||||
player.double_score = true;
|
||||
self.ball_double = BallDoubleState::Active;
|
||||
}
|
||||
events.push(Event::Sound(2007));
|
||||
}
|
||||
@@ -1358,7 +1365,7 @@ impl Game {
|
||||
|
||||
// 1000:91eb dispatches b476's flag-driven rules before it adds the
|
||||
// selected record's static score. This is observable when the ninth
|
||||
// diamond enables permanent double scoring: that collision's 1,500
|
||||
// diamond enables ball-scoped double scoring: that collision's 1,500
|
||||
// points are already doubled, while the preceding 24,464 award is not.
|
||||
self.apply_static_wall_score(object_id, wall.score, events);
|
||||
}
|
||||
@@ -2013,8 +2020,12 @@ impl Game {
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let permanent_multiplier = if self.player().double_score { 2 } else { 1 };
|
||||
let multiplier = multiball_multiplier * permanent_multiplier;
|
||||
let ball_multiplier = if self.ball_double == BallDoubleState::Active {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let multiplier = multiball_multiplier * ball_multiplier;
|
||||
let player = &mut self.players[self.current_player];
|
||||
player.score = player.score.wrapping_add(points.wrapping_mul(multiplier));
|
||||
let level = usize::from(player.media_level);
|
||||
@@ -2046,6 +2057,7 @@ impl Game {
|
||||
self.wheel_reset_gate = WheelResetGate::Suppressed;
|
||||
return;
|
||||
}
|
||||
self.ball_double = BallDoubleState::Inactive;
|
||||
self.wheel_reset_gate = WheelResetGate::Enabled;
|
||||
let player = &mut self.players[self.current_player];
|
||||
if player.extra_balls > 0 {
|
||||
@@ -2491,7 +2503,7 @@ mod tests {
|
||||
game.players[0].diamond_segments = 8;
|
||||
let mut events = Vec::new();
|
||||
|
||||
assert!(!game.player().double_score);
|
||||
assert_eq!(game.ball_double, BallDoubleState::Inactive);
|
||||
assert!(!game.magnetic_field_active(153));
|
||||
assert!(!game.magnetic_field_active(6));
|
||||
assert!(!game.magnetic_field_active(154));
|
||||
@@ -2501,7 +2513,7 @@ mod tests {
|
||||
}
|
||||
assert_eq!(game.player().diamond_segments, 9);
|
||||
assert_eq!(game.player().score, 3 * 1_500 + 24_464 + 2 * 1_500);
|
||||
assert!(game.player().double_score);
|
||||
assert_eq!(game.ball_double, BallDoubleState::Active);
|
||||
assert!(game.magnetic_field_active(153));
|
||||
assert!(game.magnetic_field_active(6));
|
||||
assert!(game.magnetic_field_active(154));
|
||||
@@ -2519,14 +2531,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn score_helper_stacks_multiball_and_permanent_double_and_honors_tilt() {
|
||||
fn score_helper_stacks_multiball_and_ball_double_and_honors_tilt() {
|
||||
let mut game = Game::new(1);
|
||||
let mut events = Vec::new();
|
||||
game.score_mode = ScoreMode::Multiball;
|
||||
game.add_score(100, &mut events);
|
||||
assert_eq!(game.player().score, 200);
|
||||
|
||||
game.players[0].double_score = true;
|
||||
game.ball_double = BallDoubleState::Active;
|
||||
game.add_score(100, &mut events);
|
||||
assert_eq!(game.player().score, 600);
|
||||
|
||||
@@ -2535,6 +2547,28 @@ mod tests {
|
||||
assert_eq!(game.player().score, 600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_score_resets_on_normal_ball_end_but_survives_special_respawn() {
|
||||
let mut normal = Game::new(1);
|
||||
normal.players[0].diamond_segments = 9;
|
||||
normal.players[0].secondary_score = 200_000;
|
||||
normal.ball_double = BallDoubleState::Active;
|
||||
normal.drain(&mut Vec::new());
|
||||
assert_eq!(normal.ball_double, BallDoubleState::Inactive);
|
||||
for object_id in [109, 112, 115, 118] {
|
||||
normal.apply_wall_rule(object_id, &mut Vec::new());
|
||||
}
|
||||
assert_eq!(normal.ball_double, BallDoubleState::Active);
|
||||
assert_eq!(normal.player().secondary_score, 200_000);
|
||||
|
||||
let mut special = Game::new(1);
|
||||
special.ball_double = BallDoubleState::Active;
|
||||
special.multiball_state = MultiballState::Ready;
|
||||
special.drain(&mut Vec::new());
|
||||
assert_eq!(special.ball_double, BallDoubleState::Active);
|
||||
assert_eq!(special.ball.position, vec2(17.0, 23.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tilted_collision_dispatch_leaves_rule_records_unchanged() {
|
||||
let mut game = Game::new(1);
|
||||
|
||||
Reference in New Issue
Block a user