fix(game): restore swept target sensors

Move claw contact and all eleven initially active type-4 circle records onto a
swept millipixel sensor test with per-object enter latching. Preserve the exact
left-bank and top-gate coordinates, radii, and scores from the initialized
object ledger.

Remove the inferred top-bank completion rule. Each original top target now
scores 500 and independently starts recovered magnetic-gate timing instead of
requiring all three for an invented bonus.

Test Plan:
- `cargo test --all-targets` -- 44 passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- `cargo build --profile production` -- passed
- top sensor contact-latch and re-entry test -- passed
- 20-second deterministic launch remained in active table motion
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 21:12:41 +02:00
parent 9d91cc51c7
commit bf5b855f44
5 changed files with 189 additions and 30 deletions
+106 -28
View File
@@ -2,9 +2,9 @@ use crate::{
geometry::{Segment, closest_point},
original_physics::{
GRAVITY_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec, STEP_SECONDS,
collide_with_circle, collide_with_line,
collide_with_circle, collide_with_line, path_intersects_circle,
},
table::{BUMPERS, PASSIVE_CIRCLES, WALLS},
table::{BUMPERS, PASSIVE_CIRCLES, TARGET_SENSORS, WALLS},
};
use macroquad::prelude::{Rect, Vec2, vec2};
@@ -214,6 +214,7 @@ pub struct Game {
player_entry: PlayerEntry,
claw_rng_state: u32,
pending_flipper_kicks: [bool; 2],
trigger_contacts: [bool; 176],
}
impl Game {
@@ -253,6 +254,7 @@ impl Game {
player_entry: PlayerEntry::Open,
claw_rng_state: seed.max(1),
pending_flipper_kicks: [false; 2],
trigger_contacts: [false; 176],
}
}
@@ -401,6 +403,7 @@ impl Game {
let mut velocity = MilliVec::from_velocity_per_second(self.ball.velocity);
velocity.y += GRAVITY_MILLI_PER_STEP;
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
let movement_velocity = velocity;
let mut position = old_position.add(velocity);
let mut hit_wall = None;
let mut hit_circle = None;
@@ -487,6 +490,11 @@ impl Game {
events.push(Event::Bumper);
}
self.check_sensor_objects(old_position, movement_velocity, events);
if self.claw.ball_suspended {
return;
}
if !self.tilted {
self.check_targets(events);
self.check_media(events);
@@ -505,35 +513,11 @@ impl Game {
}
fn check_targets(&mut self, events: &mut Vec<Event>) {
if !self.claw.active
&& self.ball.position.distance_squared(CLAW_TRIGGER_CENTER)
<= CLAW_TRIGGER_RADIUS.powi(2)
{
let terminal_frame = self.next_claw_terminal_frame();
self.begin_claw_capture(terminal_frame, events);
return;
}
if self.target_cooldown > 0.0 {
return;
}
let position = self.ball.position;
let top = [vec2(205.0, 47.0), vec2(234.0, 50.0), vec2(262.0, 53.0)];
for (index, center) in top.into_iter().enumerate() {
if position.distance_squared(center) < 11.0_f32.powi(2) && !self.top_targets[index] {
self.top_targets[index] = true;
self.add_score(1_500);
self.ball.velocity.y = self.ball.velocity.y.abs() + 65.0;
self.target_cooldown = 0.12;
events.push(Event::Target);
}
}
if self.top_targets.iter().all(|target| *target) {
self.top_targets.fill(false);
self.magnets = 12.0;
self.add_score(5_000);
}
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)
@@ -623,6 +607,53 @@ impl Game {
}
}
fn check_sensor_objects(
&mut self,
old_position: MilliVec,
movement_velocity: MilliVec,
events: &mut Vec<Event>,
) {
if !self.claw.active
&& path_intersects_circle(
old_position,
movement_velocity,
CLAW_TRIGGER_CENTER,
CLAW_TRIGGER_RADIUS,
)
{
let terminal_frame = self.next_claw_terminal_frame();
self.begin_claw_capture(terminal_frame, events);
return;
}
let current_position = MilliVec::from_position(self.ball.position);
for sensor in TARGET_SENSORS {
let touched = path_intersects_circle(
old_position,
movement_velocity,
sensor.center,
sensor.radius,
);
let contact_index = usize::from(sensor.id);
let entered = touched && !self.trigger_contacts[contact_index];
self.trigger_contacts[contact_index] = path_intersects_circle(
current_position,
MilliVec::default(),
sensor.center,
sensor.radius,
);
if !entered || self.tilted {
continue;
}
self.add_score(sensor.score);
events.push(Event::Target);
if (150..=152).contains(&sensor.id) {
self.top_targets[usize::from(sensor.id - 150)] = true;
self.magnets = self.magnets.max(0.3);
}
}
}
fn next_claw_terminal_frame(&mut self) -> u8 {
let value = self.next_random_value();
CLAW_TERMINAL_FRAMES[value as usize % CLAW_TERMINAL_FRAMES.len()]
@@ -763,6 +794,7 @@ impl Game {
self.bumper_flash.fill(0.0);
self.wheel_animation = 0.0;
self.claw = Claw::default();
self.trigger_contacts.fill(false);
self.nudge_shake = 0.0;
self.launcher_charge = 0.0;
self.launcher_was_down = false;
@@ -1300,17 +1332,63 @@ mod tests {
let mut game = Game::new(1);
game.ball.in_launcher = false;
game.ball.position = CLAW_TRIGGER_CENTER + vec2(CLAW_TRIGGER_RADIUS + 0.1, 0.0);
game.check_targets(&mut Vec::new());
game.check_sensor_objects(
MilliVec::from_position(game.ball.position),
MilliVec::default(),
&mut Vec::new(),
);
assert!(!game.claw.active);
game.ball.position = CLAW_TRIGGER_CENTER;
let mut events = Vec::new();
game.check_targets(&mut events);
game.check_sensor_objects(
MilliVec::from_position(game.ball.position),
MilliVec::default(),
&mut events,
);
assert!(game.claw.active);
assert_eq!(events, [Event::ClawCapture]);
assert!(CLAW_TERMINAL_FRAMES.contains(&game.claw.target_frame));
}
#[test]
fn top_sensor_uses_the_original_score_and_contact_latch() {
let mut game = Game::new(1);
game.ball.in_launcher = false;
game.ball.position = vec2(205.0, 55.0);
let mut events = Vec::new();
game.check_sensor_objects(
MilliVec::from_position(vec2(205.0, 60.0)),
MilliVec { x: 0, y: -5_000 },
&mut events,
);
assert_eq!(game.player().score, 500);
assert_eq!(events, [Event::Target]);
assert!((game.magnets - 0.3).abs() < f32::EPSILON);
game.check_sensor_objects(
MilliVec::from_position(game.ball.position),
MilliVec::default(),
&mut events,
);
assert_eq!(game.player().score, 500, "contact must score only once");
game.ball.position = vec2(205.0, 70.0);
game.check_sensor_objects(
MilliVec::from_position(game.ball.position),
MilliVec::default(),
&mut events,
);
game.ball.position = vec2(205.0, 55.0);
game.check_sensor_objects(
MilliVec::from_position(vec2(205.0, 60.0)),
MilliVec { x: 0, y: -5_000 },
&mut events,
);
assert_eq!(game.player().score, 1_000);
}
#[test]
fn claw_release_table_decodes_the_original_thousandth_pixel_coordinates() {
for (frame, expected) in [