fix(game): restore wheel locks and reset target

Replace the inferred eight-sector wheel and central lock rectangle with the six
recovered type-3 records. Fill the five lock holes at their exact centers,
double each successive bonus from 10,000 through 160,000, transfer the complete
310,000 on the fifth, and return a launcher ball without consuming one.

Restore object 148 as the wheel-reset target and clear all filled-hole state on
contact. Keep these sensors on the same swept path and contact-latch machinery
as the claw and type-4 targets.

Test Plan:
- `cargo test --all-targets` -- 47 passed
- `cargo clippy --all-targets -- -D warnings` -- passed
- `cargo build --profile production` -- passed
- live five-hole bonus/score sequence matched exactly
- live object-148 reset cleared all five contact states
- `git diff --cached --check` -- passed
This commit is contained in:
2026-08-22 21:26:20 +02:00
parent 1ca8597080
commit 5e63772967
4 changed files with 136 additions and 63 deletions
+104 -61
View File
@@ -4,7 +4,7 @@ use crate::{
CollisionResponse, GRAVITY_MILLI_PER_STEP, MAXIMUM_SPEED_MILLI_PER_STEP, MilliVec,
STEP_SECONDS, circle_collision_response, line_collision_response, path_intersects_circle,
},
table::{BUMPERS, PASSIVE_CIRCLES, TARGET_SENSORS, WALLS},
table::{BUMPERS, LOCK_HOLES, PASSIVE_CIRCLES, TARGET_SENSORS, WALLS, WHEEL_RESET_SENSOR},
};
use macroquad::prelude::{Rect, Vec2, vec2};
@@ -197,10 +197,8 @@ pub struct Game {
pub current_player: usize,
pub ball: Ball,
pub bonus: u32,
pub wheel_holes: [bool; 8],
pub side_targets: [bool; 5],
pub wheel_holes: [bool; 5],
pub top_targets: [bool; 3],
pub lock_lights: u8,
pub magnets: f32,
pub tilted: bool,
pub flippers: Flippers,
@@ -212,7 +210,6 @@ pub struct Game {
pub finished: bool,
pub last_collision_id: Option<u8>,
accumulator: f32,
target_cooldown: f32,
bumper_cooldown: f32,
nudge_cooldown: f32,
nudge_meter: f32,
@@ -238,10 +235,8 @@ impl Game {
current_player: 0,
ball: Ball::default(),
bonus: 0,
wheel_holes: [false; 8],
side_targets: [false; 5],
wheel_holes: [false; 5],
top_targets: [false; 3],
lock_lights: 0,
magnets: 0.0,
tilted: false,
flippers: Flippers::default(),
@@ -253,7 +248,6 @@ impl Game {
finished: false,
last_collision_id: None,
accumulator: 0.0,
target_cooldown: 0.0,
bumper_cooldown: 0.0,
nudge_cooldown: 0.0,
nudge_meter: 0.0,
@@ -385,7 +379,6 @@ impl Game {
#[allow(clippy::too_many_lines)]
fn fixed_update(&mut self, dt: f32, events: &mut Vec<Event>) {
self.target_cooldown = (self.target_cooldown - dt).max(0.0);
self.bumper_cooldown = (self.bumper_cooldown - dt).max(0.0);
self.nudge_cooldown = (self.nudge_cooldown - dt).max(0.0);
self.magnets = (self.magnets - dt).max(0.0);
@@ -515,7 +508,6 @@ impl Game {
}
if !self.tilted {
self.check_targets(events);
self.check_media(events);
}
if self.claw.ball_suspended {
@@ -531,51 +523,6 @@ impl Game {
}
}
fn check_targets(&mut self, events: &mut Vec<Event>) {
if self.target_cooldown > 0.0 {
return;
}
let position = self.ball.position;
let wheel_center = vec2(114.0, 79.0);
let from_wheel = position - wheel_center;
if (25.0..=43.0).contains(&from_wheel.length()) {
let angle =
(from_wheel.y.atan2(from_wheel.x) + std::f32::consts::TAU) % std::f32::consts::TAU;
let sector_boundaries = [
std::f32::consts::TAU / 16.0,
std::f32::consts::TAU * 3.0 / 16.0,
std::f32::consts::TAU * 5.0 / 16.0,
std::f32::consts::TAU * 7.0 / 16.0,
std::f32::consts::TAU * 9.0 / 16.0,
std::f32::consts::TAU * 11.0 / 16.0,
std::f32::consts::TAU * 13.0 / 16.0,
std::f32::consts::TAU * 15.0 / 16.0,
];
let hole = sector_boundaries.partition_point(|boundary| angle >= *boundary) % 8;
if !self.wheel_holes[hole] {
self.wheel_holes[hole] = true;
self.add_score(2_500);
self.target_cooldown = 0.15;
self.wheel_animation = 0.65;
events.push(Event::Wheel);
}
if self.wheel_holes.iter().all(|hole| *hole) {
self.wheel_holes.fill(false);
self.add_score(25_000);
}
}
if (151.0..=220.0).contains(&position.x) && (187.0..=205.0).contains(&position.y) {
self.lock_lights = (self.lock_lights + 1).min(4);
self.add_score(u32::from(self.lock_lights) * 5_000);
self.ball.position = vec2(185.0, 181.0);
self.ball.velocity = vec2(-40.0 + f32::from(self.lock_lights) * 18.0, -120.0);
self.target_cooldown = 0.4;
events.push(Event::Lock);
}
}
fn apply_wall_rule(&mut self, object_id: u8, events: &mut Vec<Event>) {
let wall = WALLS
.iter()
@@ -611,7 +558,6 @@ impl Game {
0
};
self.add_score(completion_bonus);
self.side_targets.fill(false);
for active in &mut self.object_active[90..=104] {
*active = true;
}
@@ -668,6 +614,61 @@ impl Game {
}
let current_position = MilliVec::from_position(self.ball.position);
for (index, sensor) in LOCK_HOLES.into_iter().enumerate() {
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.wheel_holes[index];
self.trigger_contacts[contact_index] = path_intersects_circle(
current_position,
MilliVec::default(),
sensor.center,
sensor.radius,
);
if !entered || self.tilted {
continue;
}
let filled_before = self.wheel_holes.iter().filter(|filled| **filled).count();
self.wheel_holes[index] = true;
self.bonus = self
.bonus
.saturating_add(10_000_u32 << u32::try_from(filled_before).unwrap_or_default());
if self.wheel_holes.iter().all(|filled| *filled) {
self.add_score(self.bonus);
self.bonus = 0;
}
self.reset_ball_to_launcher();
events.push(Event::Lock);
return;
}
let reset_touched = path_intersects_circle(
old_position,
movement_velocity,
WHEEL_RESET_SENSOR.center,
WHEEL_RESET_SENSOR.radius,
);
let reset_index = usize::from(WHEEL_RESET_SENSOR.id);
let reset_entered = reset_touched && !self.trigger_contacts[reset_index];
self.trigger_contacts[reset_index] = path_intersects_circle(
current_position,
MilliVec::default(),
WHEEL_RESET_SENSOR.center,
WHEEL_RESET_SENSOR.radius,
);
if reset_entered && !self.tilted {
self.wheel_holes.fill(false);
self.wheel_animation = 0.65;
self.reset_ball_to_launcher();
events.push(Event::Wheel);
return;
}
for sensor in TARGET_SENSORS {
let touched = path_intersects_circle(
old_position,
@@ -695,6 +696,15 @@ impl Game {
}
}
fn reset_ball_to_launcher(&mut self) {
self.ball = Ball::default();
self.launcher_charge = 0.0;
self.launcher_was_down = false;
self.launcher_hold_seconds = 0.0;
self.launcher_next_repeat = LAUNCHER_REPEAT_DELAY_SECONDS;
self.launcher_velocity_milli = 0;
}
fn next_claw_terminal_frame(&mut self) -> u8 {
let value = self.next_random_value();
CLAW_TERMINAL_FRAMES[value as usize % CLAW_TERMINAL_FRAMES.len()]
@@ -825,10 +835,8 @@ impl Game {
}
events.push(Event::Drain);
self.bonus = 0;
self.side_targets.fill(false);
self.top_targets.fill(false);
self.wheel_holes.fill(false);
self.lock_lights = 0;
self.magnets = 0.0;
self.tilted = false;
self.nudge_meter = 0.0;
@@ -1023,7 +1031,7 @@ mod tests {
#[test]
fn charged_ball_clears_the_shooter_lane() {
for detail in 1..=5 {
let mut game = Game::new(1);
let mut game = Game::new_with_seed(1, u32::from(detail));
launch_ball(&mut game, 60);
let mut entered_table = false;
let mut returned_to_launcher = false;
@@ -1035,7 +1043,7 @@ mod tests {
if game.ball.position.x < 280.0 && game.ball.position.y > 30.0 {
entered_table = true;
}
returned_to_launcher |= game.ball.in_launcher;
returned_to_launcher |= game.ball.in_launcher && !entered_table;
}
assert!(
@@ -1449,6 +1457,41 @@ mod tests {
assert_eq!(game.player().score, 1_000);
}
#[test]
fn wheel_holes_double_bonus_then_transfer_all_310k() {
let mut game = Game::new(1);
game.ball.in_launcher = false;
let mut events = Vec::new();
let expected_bonus = [10_000, 30_000, 70_000, 150_000, 0];
for (index, sensor) in LOCK_HOLES.into_iter().enumerate() {
game.ball.position = sensor.center;
game.check_sensor_objects(
MilliVec::from_position(sensor.center),
MilliVec::default(),
&mut events,
);
assert_eq!(game.bonus, expected_bonus[index]);
assert!(game.ball.in_launcher);
}
assert_eq!(game.player().score, 310_000);
assert!(game.wheel_holes.iter().all(|filled| *filled));
assert_eq!(
events.iter().filter(|event| **event == Event::Lock).count(),
5
);
game.ball.in_launcher = false;
game.ball.position = WHEEL_RESET_SENSOR.center;
game.check_sensor_objects(
MilliVec::from_position(WHEEL_RESET_SENSOR.center),
MilliVec::default(),
&mut events,
);
assert!(game.wheel_holes.iter().all(|filled| !*filled));
assert_eq!(events.last(), Some(&Event::Wheel));
}
#[test]
fn claw_release_table_decodes_the_original_thousandth_pixel_coordinates() {
for (frame, expected) in [