fix(game): restore physical target-bank rules

Attach recovered score and rule flags to type-2 collision records. Deactivate
complete three-line groups through their contact owner, rearm all five
bumper-value groups after completion, and rearm all four central groups after
advancing the TDK diamond.

Remove the broad side and colored-target coordinate checks those physical
records replace. Preserve exact 5,000, 100,000, bank, and wheel-trigger scores
from the initialized object ledger.

Test Plan:
- `cargo test --all-targets` -- 46 passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- `cargo build --profile production` -- passed
- target-bank deactivation, completion, and rearm test -- passed
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 21:20:15 +02:00
parent 20e751de80
commit 1ca8597080
4 changed files with 129 additions and 34 deletions
+89 -33
View File
@@ -172,6 +172,15 @@ enum PlayerEntry {
Closed,
}
fn initial_object_activity() -> [bool; 176] {
let mut active = [true; 176];
active[0] = false;
for object_id in [6, 87, 88, 149, 153, 154, 175] {
active[object_id] = false;
}
active
}
impl Default for Ball {
fn default() -> Self {
Self {
@@ -215,6 +224,7 @@ pub struct Game {
claw_rng_state: u32,
pending_flipper_kicks: [bool; 2],
trigger_contacts: [bool; 176],
object_active: [bool; 176],
}
impl Game {
@@ -255,6 +265,7 @@ impl Game {
claw_rng_state: seed.max(1),
pending_flipper_kicks: [false; 2],
trigger_contacts: [false; 176],
object_active: initial_object_activity(),
}
}
@@ -407,6 +418,9 @@ impl Game {
let mut position = old_position.add(velocity);
let mut best_collision: Option<(u8, bool, CollisionResponse)> = None;
for object_id in 1..=175 {
if !self.object_active[usize::from(object_id)] {
continue;
}
if self.claw.active && (12..=20).contains(&object_id) {
continue;
}
@@ -480,6 +494,7 @@ impl Game {
self.launcher_was_down = false;
return;
}
self.apply_wall_rule(wall_id, events);
}
if let Some(index) = hit_circle
@@ -522,26 +537,6 @@ impl Game {
}
let position = self.ball.position;
let side = [180.0, 195.0, 210.0, 225.0, 240.0];
for (index, y) in side.into_iter().enumerate() {
if position.distance_squared(vec2(286.0, y)) < 9.0_f32.powi(2)
&& !self.side_targets[index]
{
self.side_targets[index] = true;
self.add_score(2_000 + u32::try_from(index).unwrap_or(0) * 1_000);
self.ball.velocity.x = -self.ball.velocity.x.abs() - 55.0;
self.target_cooldown = 0.10;
events.push(Event::Target);
}
}
if self.side_targets.iter().all(|target| *target) {
self.side_targets.fill(false);
let player = &mut self.players[self.current_player];
player.bumper_value = (player.bumper_value + 1_000).min(10_000);
player.diamond_segments = (player.diamond_segments + 1).min(9);
self.add_score(10_000);
}
let wheel_center = vec2(114.0, 79.0);
let from_wheel = position - wheel_center;
if (25.0..=43.0).contains(&from_wheel.length()) {
@@ -579,22 +574,64 @@ impl Game {
self.target_cooldown = 0.4;
events.push(Event::Lock);
}
}
for (center, value) in [
(vec2(113.0, 285.0), 2_000),
(vec2(132.0, 270.0), 3_000),
(vec2(155.0, 258.0), 4_000),
(vec2(181.0, 270.0), 5_000),
(vec2(201.0, 285.0), 6_000),
] {
if position.distance_squared(center) < 10.0_f32.powi(2) {
self.add_score(value);
self.ball.velocity.y = -self.ball.velocity.y.abs() - 40.0;
self.target_cooldown = 0.13;
events.push(Event::Target);
break;
fn apply_wall_rule(&mut self, object_id: u8, events: &mut Vec<Event>) {
let wall = WALLS
.iter()
.find(|wall| wall.id == object_id)
.expect("a wall collision id must reference a recovered wall");
if !self.tilted && wall.score != 0 {
self.add_score(wall.score);
events.push(if object_id == 121 {
Event::Wheel
} else {
Event::Target
});
}
if wall.flags & 0x0002 != 0 {
let group = usize::from(wall.contact_group);
self.object_active[group] = false;
if wall.flags & 0x2000 != 0 {
self.object_active[group + 1] = false;
self.object_active[group + 2] = false;
}
}
if wall.flags & 0x0008 != 0
&& [90, 93, 96, 99, 102]
.into_iter()
.all(|id| !self.object_active[id])
{
let player = &mut self.players[self.current_player];
let completion_bonus = if player.bumper_value >= 6_000 {
50_000
} else {
player.bumper_value += 1_000;
0
};
self.add_score(completion_bonus);
self.side_targets.fill(false);
for active in &mut self.object_active[90..=104] {
*active = true;
}
}
if wall.flags & 0x0010 != 0
&& [109, 112, 115, 118]
.into_iter()
.all(|id| !self.object_active[id])
{
let player = &mut self.players[self.current_player];
player.diamond_segments = (player.diamond_segments + 1).min(9);
let value = u32::from(player.diamond_segments) * 10_000;
self.add_score(value);
for active in &mut self.object_active[109..=120] {
*active = true;
}
}
if wall.flags & 0x0400 != 0 {
self.wheel_animation = 0.65;
}
}
fn check_media(&mut self, events: &mut Vec<Event>) {
@@ -799,6 +836,7 @@ impl Game {
self.wheel_animation = 0.0;
self.claw = Claw::default();
self.trigger_contacts.fill(false);
self.object_active = initial_object_activity();
self.nudge_shake = 0.0;
self.launcher_charge = 0.0;
self.launcher_was_down = false;
@@ -1039,6 +1077,24 @@ mod tests {
assert_eq!(game.players[0].score, 2_000);
}
#[test]
fn recovered_target_banks_deactivate_and_rearm_their_line_groups() {
let mut game = Game::new(1);
let mut events = Vec::new();
for object_id in [90, 93, 96, 99, 102] {
game.apply_wall_rule(object_id, &mut events);
}
assert_eq!(game.player().bumper_value, 2_000);
assert!(game.object_active[90..=104].iter().all(|active| *active));
for object_id in [109, 112, 115, 118] {
game.apply_wall_rule(object_id, &mut events);
}
assert_eq!(game.player().diamond_segments, 1);
assert!(game.object_active[109..=120].iter().all(|active| *active));
}
#[test]
fn center_drain_advances_to_a_fresh_ball() {
let mut game = Game::new(1);