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:
+106
-28
@@ -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 [
|
||||
|
||||
@@ -204,6 +204,45 @@ pub fn collide_with_circle(
|
||||
true
|
||||
}
|
||||
|
||||
/// Test a non-physical circle record against the complete ball-center path.
|
||||
pub fn path_intersects_circle(
|
||||
old_position: MilliVec,
|
||||
velocity: MilliVec,
|
||||
center: Vec2,
|
||||
radius: f32,
|
||||
) -> bool {
|
||||
let center = MilliVec::from_position(center);
|
||||
let offset = subtract(old_position, center);
|
||||
let radius_milli = (radius * 1_000.0).round() as i32;
|
||||
let radius_squared = i64::from(radius_milli).pow(2);
|
||||
let old_distance_squared = i64::from(offset.x).pow(2) + i64::from(offset.y).pow(2);
|
||||
if old_distance_squared <= radius_squared {
|
||||
return true;
|
||||
}
|
||||
let next = offset.add(velocity);
|
||||
let next_distance_squared = i64::from(next.x).pow(2) + i64::from(next.y).pow(2);
|
||||
if next_distance_squared <= radius_squared {
|
||||
return true;
|
||||
}
|
||||
|
||||
let vx = f64::from(velocity.x);
|
||||
let vy = f64::from(velocity.y);
|
||||
let offset_x = f64::from(offset.x);
|
||||
let offset_y = f64::from(offset.y);
|
||||
let quadratic_a = vx * vx + vy * vy;
|
||||
if quadratic_a == 0.0 {
|
||||
return false;
|
||||
}
|
||||
let quadratic_b = 2.0 * (offset_x * vx + offset_y * vy);
|
||||
let quadratic_c = old_distance_squared as f64 - f64::from(radius_milli).powi(2);
|
||||
let discriminant = quadratic_b * quadratic_b - 4.0 * quadratic_a * quadratic_c;
|
||||
if discriminant < 0.0 {
|
||||
return false;
|
||||
}
|
||||
let progress = (-quadratic_b - discriminant.sqrt()) / (2.0 * quadratic_a);
|
||||
0.0 < progress && progress <= 1.0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -59,6 +59,25 @@ pub struct StaticCircle {
|
||||
pub normal_kick: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct TargetSensor {
|
||||
pub id: u8,
|
||||
pub center: Vec2,
|
||||
pub radius: f32,
|
||||
pub score: u32,
|
||||
}
|
||||
|
||||
impl TargetSensor {
|
||||
const fn new(id: u8, center: Vec2, radius: f32, score: u32) -> Self {
|
||||
Self {
|
||||
id,
|
||||
center,
|
||||
radius,
|
||||
score,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticCircle {
|
||||
const fn new(id: u8, center: Vec2, contact_radius: f32) -> Self {
|
||||
let (normal_rebound, tangent_coupling, normal_kick) = match id {
|
||||
@@ -242,6 +261,22 @@ pub const BUMPERS: [StaticCircle; 3] = [
|
||||
StaticCircle::new(53, Vec2::new(219.0, 165.0), 23.0),
|
||||
];
|
||||
|
||||
/// Initially active type-4 circle records. Objects 149, 153, and 154 are
|
||||
/// mechanism-controlled and begin disabled in the original table.
|
||||
pub const TARGET_SENSORS: [TargetSensor; 11] = [
|
||||
TargetSensor::new(140, Vec2::new(24.0, 182.0), 12.0, 1_000),
|
||||
TargetSensor::new(141, Vec2::new(18.0, 162.0), 12.0, 1_000),
|
||||
TargetSensor::new(142, Vec2::new(17.0, 144.0), 12.0, 1_500),
|
||||
TargetSensor::new(143, Vec2::new(17.0, 125.0), 12.0, 1_500),
|
||||
TargetSensor::new(144, Vec2::new(17.0, 106.0), 12.0, 2_000),
|
||||
TargetSensor::new(145, Vec2::new(17.0, 87.0), 12.0, 2_000),
|
||||
TargetSensor::new(146, Vec2::new(17.0, 67.0), 12.0, 2_500),
|
||||
TargetSensor::new(147, Vec2::new(17.0, 48.0), 12.0, 3_000),
|
||||
TargetSensor::new(150, Vec2::new(205.0, 47.0), 11.0, 500),
|
||||
TargetSensor::new(151, Vec2::new(233.0, 46.0), 11.0, 500),
|
||||
TargetSensor::new(152, Vec2::new(264.0, 46.0), 11.0, 500),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user