fix(game): dispatch sensors for both ball slots

Store capture progression with each recovered ball slot and pass the original one-based ball number through type-3 and type-4 handling. Multiball captures now remove only the captured slot, promote the survivor when slot one exits, and preserve the effect-seven argument guard that prevents ball two from spawning a third ball.

Test Plan:
- cargo test --all-targets
- cargo clippy --all-targets --all-features -- -D warnings
- rumdl check CHANGELOG.md RECONSTRUCTION.md README.md
- git diff --check
This commit is contained in:
2026-08-23 18:04:30 +02:00
parent bee3c440bf
commit 445af4a030
3 changed files with 336 additions and 60 deletions
+3
View File
@@ -10,6 +10,9 @@ and this project adheres to
### Fixed
- Run type-3 captures and type-4 triggers for both live ball slots, including
per-slot capture age/contact ownership, exact captured-slot removal, survivor
promotion, and the effect-seven ball-number guard against a third ball.
- Spawn effect-seven ball 2 from record 148 at `(17,23)` with exact
`(0,3040)` millipixel velocity, instead of launching a guessed second ball
upward from the shooter lane.
+1 -1
View File
@@ -24,7 +24,7 @@ implementation.
| Help and languages | Exact | Original resource images 1001-1005 are displayed directly. |
| Playfield collision layout | Recovered | All 109 active type-2 line objects and 40 static active type-1 circles are transcribed from the original 175-object registration table. The registration routine converts its sideways inputs with `screen = (y, x - 20)` and accumulates explicitly relative objects. Type-2 records retain every recovered Real48 normal/tangent response pair and registered one-sided orientation. Type-1 records retain their swept-circle radius, radial rebound, tangent coupling, and bumper kick. Each flipper uses its exact two line records plus moving tip circle in both positions. Moving-flipper contact ports `1000:7ed9` rather than fitting live samples: delta-specific pivots/edges, integer cross gates, radial/penetration calculations, response-record gain, and position/velocity publication are tested against all four C harness directions and the raised release geometry. Object 174 is overwritten with the live first ball and Rust handles its ball-to-ball role directly. |
| Ball launcher and nudge input | Recovered | The initial 32-bit fixed-point coordinates decode to `(325, 413)` in the right shooter lane. Each Down keydown subtracts `15*50 = 750` millipixels, release subtracts another `15*100 = 1500`, and the result follows the recovered randomized `-3800` lower and `-2280` weak upper clamp branches. The ten decoration frames use the same strict 750-millipixel thresholds. Left Shift and keypad 3 apply their directional `(50-Random(20))*15` impulses; Space uses the recovered Real48 horizontal factor and `(60-Random(20))*15` vertical impulse. Each nudge adds 25 to the wrapping 16-bit tilt counter, compares it with `30+Random(10)`, and the detail timer decrements a nonzero counter once per callback. |
| Physics arithmetic | Recovered Real48 core | Production movement uses the original 10 ms millipixel substep, `+15` vertical acceleration, Real48 `3800/speed` clamp, type-2 distance/cross gates, type-1 midpoint normal, surface-distance candidate ordering, persistent Real48 spin, and the common impulse response. Dynamic records 174/175 transfer normal impulse to the other ball before applying `normal_velocity-1000` to the moving ball; effect seven spawns record/slot 2 from record 148 coordinates `(17000,23000)` with exact `(0,3040)` velocity. The original Borland seed update, high-word `Random(n)`, and normalized Real48 random register result drive launcher variation, effects, claw terminals, magnetic fields, and trigger response. Type-4 triggers retain a separate transient entry flag; type-3 captures retain per-player 16-bit contact words, deep-inside pull/hold progression to age 300, first-contact sound, and `99`/`2` completion sentinels. Zero-spin C harnesses and retained-spin Wine probes are tested separately rather than conflated. |
| Physics arithmetic | Recovered Real48 core | Production movement uses the original 10 ms millipixel substep, `+15` vertical acceleration, Real48 `3800/speed` clamp, type-2 distance/cross gates, type-1 midpoint normal, surface-distance candidate ordering, persistent Real48 spin, and the common impulse response. Dynamic records 174/175 transfer normal impulse to the other ball before applying `normal_velocity-1000` to the moving ball; effect seven spawns record/slot 2 from record 148 coordinates `(17000,23000)` with exact `(0,3040)` velocity. Both slots traverse the same type-3/type-4 dispatcher. Capture age is saved per slot, contact words retain ball ownership, a completed multiball capture removes exactly that slot and promotes the survivor when needed, and effect seven's argument-2 guard prevents a third ball. The original Borland seed update, high-word `Random(n)`, and normalized Real48 random register result drive launcher variation, effects, claw terminals, magnetic fields, and trigger response. Type-4 triggers retain a shared transient entry flag; type-3 captures retain per-player 16-bit contact words, deep-inside pull/hold progression to age 300, first-contact sound, and `99`/`2` completion sentinels. Zero-spin C harnesses and retained-spin Wine probes are tested separately rather than conflated. |
| Rules | Recovered gameplay paths | Player count, controls, the five three-line bumper-value groups, four three-line TDK-diamond groups, five doubling-value lock holes, wheel-reset target, seven-way effect selector/consumer including multiball, permanent double scoring, and four exact media/extra-ball thresholds follow original help/code paths, globals, and object flags. The ninth diamond pays the original 24,464 completion value; the following completed bank enables double scoring, and later completions add 100,000 to the per-player secondary score. Bumpers 51-53 and targets 140-147/150-152 use the original 5/20/10 active-callback countdowns and exact overlay-A render rectangles. Record 121 runs the six-callback DAT600 target rotation with exact 91x90 frames/target points and rotates the five contact/item values at state 6. Completing all five lock holes suspends physics for the full 281-callback panel animation, clears contact/item state at the recovered boundaries, and uses the exact WAVE 2013/2012/stop sequence. Turn changes mirror the original save/load of all 175 collision record states: wheel/top targets, active/contact slots, selected effect, and multiball readiness remain attached to their player. Claw contact and all initially active type-4 targets use recovered records. The top three targets score 500 each and independently enable the left, center, or right magnetic field record; each field pulls the ball upward until it exits and then deactivates. The claw state machine and release table have live differential coverage for all four random terminals. |
| Numeric scoring | Recovered gameplay values | Static scores come from the initialized 175-object ledger. Dynamic bumper progression, target-bank completion, diamond awards, 10k-160k lock bonuses, 310k transfer, six effect values, multiball mode, and all four media thresholds are transcribed from `1000:b476`, `1000:c4e1`, `1000:bc36`, and live state probes. Lock and effect awards share the original per-player secondary score and display multiplier; the fifth hole transfers and clears it, increments the multiplier, and grants the recovered ball award. Score mutation uses the original 32-bit wrapping behavior, and each add operation can advance at most one media threshold. |
| High scores | Recovered visible flow; portable storage | The original 276-byte table is decoded as ten `IWIK`-XOR-obfuscated little-endian scores plus ten 22-byte names. Each player is checked immediately when their own last ball is lost; qualifying scores use the original signed-high/unsigned-low comparison and a `TDK Pinball Player`-prefilled name screen before the table is shown and play resumes. Persisted updates use portable JSON rather than rewriting the Win16 file. |
+329 -56
View File
@@ -190,6 +190,7 @@ pub struct Ball {
pub velocity: Vec2,
pub in_launcher: bool,
spin: Real48,
capture_age: i32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -212,6 +213,13 @@ enum CaptureStep {
Complete,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum BallAction {
Keep,
Reset,
Remove,
}
#[derive(Clone, Copy, Debug)]
struct RuleState {
wheel_holes: [bool; 5],
@@ -251,6 +259,7 @@ impl Default for Ball {
velocity: Vec2::ZERO,
in_launcher: true,
spin: Real48::ZERO,
capture_age: 0,
}
}
}
@@ -290,7 +299,6 @@ pub struct Game {
target_effect: u8,
claw_frame_seconds: f32,
multiball_state: MultiballState,
capture_age: i32,
}
impl Game {
@@ -333,7 +341,6 @@ impl Game {
target_effect: 0,
claw_frame_seconds: CLAW_FRAME_SECONDS,
multiball_state: MultiballState::Unavailable,
capture_age: 0,
}
}
@@ -796,6 +803,7 @@ impl Game {
velocity.y += GRAVITY_MILLI_PER_STEP;
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
self.apply_magnetic_fields(old_position, &mut velocity);
let movement_velocity = velocity;
let mut best_collision = self.find_static_collision(old_position, velocity, ball.spin);
let mut transferred_primary_velocity = None;
@@ -849,8 +857,18 @@ impl Game {
events.push(Event::Bumper);
events.push(Event::Sound(2006));
}
if self.check_sensor_objects_for_ball(
&mut ball,
2,
2,
old_position,
movement_velocity,
events,
) == BallAction::Keep
{
self.secondary_ball = Some(ball);
}
}
fn apply_wall_rule(&mut self, object_id: u8, events: &mut Vec<Event>) {
let wall = WALLS
@@ -955,44 +973,94 @@ impl Game {
movement_velocity: MilliVec,
events: &mut Vec<Event>,
) {
let ball_count = if self.secondary_ball.is_some() { 2 } else { 1 };
let mut ball = self.ball;
match self.check_sensor_objects_for_ball(
&mut ball,
1,
ball_count,
old_position,
movement_velocity,
events,
) {
BallAction::Keep => self.ball = ball,
BallAction::Reset => self.reset_ball_to_launcher(),
BallAction::Remove => {
self.ball = self
.secondary_ball
.take()
.expect("a multiball capture must leave the other slot active");
}
}
}
fn check_sensor_objects_for_ball(
&mut self,
ball: &mut Ball,
ball_number: u16,
ball_count: u16,
old_position: MilliVec,
movement_velocity: MilliVec,
events: &mut Vec<Event>,
) -> BallAction {
if !self.claw.active {
match self.capture_record_step(
ball,
ball_number,
ball_count,
89,
CLAW_TRIGGER_CENTER,
CLAW_TRIGGER_RADIUS,
old_position,
events,
) {
CaptureStep::Holding => return,
CaptureStep::Holding => return BallAction::Keep,
CaptureStep::Complete => {
if ball_count == 1 {
let terminal_frame = self.next_claw_terminal_frame();
self.begin_claw_capture_after_hold(terminal_frame, events);
return;
self.begin_claw_capture_after_hold(ball, terminal_frame, events);
return BallAction::Keep;
}
return BallAction::Remove;
}
CaptureStep::Outside => {}
}
}
let current_position = MilliVec::from_position(self.ball.position);
if self.check_lock_holes(old_position, events)
|| self.check_wheel_reset(old_position, events)
if let Some(action) =
self.check_lock_holes(ball, ball_number, ball_count, old_position, events)
{
return;
return action;
}
self.check_effect_sensor(old_position, current_position, movement_velocity, events);
self.check_target_sensors(old_position, current_position, movement_velocity, events);
if let Some(action) =
self.check_wheel_reset(ball, ball_number, ball_count, old_position, events)
{
return action;
}
self.check_effect_sensor(
ball,
ball_number,
old_position,
movement_velocity,
events,
);
self.check_target_sensors(ball, old_position, movement_velocity, events);
BallAction::Keep
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_possible_truncation, clippy::too_many_arguments)]
fn capture_record_step(
&mut self,
ball: &mut Ball,
ball_number: u16,
ball_count: u16,
record_id: u8,
center: Vec2,
radius: f32,
previous_position: MilliVec,
events: &mut Vec<Event>,
) -> CaptureStep {
let current_position = MilliVec::from_position(self.ball.position);
let current_position = MilliVec::from_position(ball.position);
let center = MilliVec::from_position(center);
let radius = (radius * 1_000.0).round() as i32;
let dx = center.x.wrapping_sub(current_position.x);
@@ -1003,27 +1071,28 @@ impl Game {
let surface_distance = milli_distance(MilliVec { x: dx, y: dy }) - radius;
let contact_index = usize::from(record_id);
let contact = self.record_contacts[contact_index];
let ball_count = if self.secondary_ball.is_some() { 2 } else { 1 };
let contact_allowed =
contact == 0 || (contact != 99 && ball_count == 1) || contact == 1;
contact == 0 || (contact != 99 && ball_count == 1) || contact == ball_number;
if surface_distance < -11_000 && contact_allowed {
if self.capture_age < 300 {
let mut velocity = MilliVec::from_velocity_per_second(self.ball.velocity);
if ball.capture_age < 300 {
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
let stationary = center.x.wrapping_sub(previous_position.x).wrapping_abs() < 500
&& center.y.wrapping_sub(previous_position.y).wrapping_abs() < 500
&& velocity.x.wrapping_abs() < 500
&& velocity.y.wrapping_abs() < 500;
if stationary {
velocity = MilliVec::default();
if self.capture_age == 0 {
self.record_contacts[contact_index] = 1;
if ball.capture_age == 0 {
self.record_contacts[contact_index] = ball_number;
if !self.tilted {
events.push(Event::Sound(2015));
}
self.capture_age = if record_id == 89 {
}
ball.capture_age = if record_id == 89 {
300
} else {
self.capture_age.wrapping_add(5)
ball.capture_age.wrapping_add(5)
};
} else {
let damping = Real48::from_bytes([0x80, 0x66, 0x66, 0x66, 0x66, 0x66]);
@@ -1040,29 +1109,41 @@ impl Game {
velocity.y.wrapping_sub(150)
};
}
self.ball.velocity = velocity.to_velocity_per_second();
ball.velocity = velocity.to_velocity_per_second();
return CaptureStep::Holding;
}
self.capture_age = 0;
if record_id == 89 && ball_count == 1 {
return CaptureStep::Complete;
}
ball.capture_age = 0;
self.record_contacts[contact_index] = if record_id == 148 { 2 } else { 99 };
if ball_count > 1 {
self.record_contacts[contact_index] = 2;
}
return CaptureStep::Complete;
}
if surface_distance > -11_000 && contact == 1 {
if surface_distance > -11_000 && contact == ball_number {
self.record_contacts[contact_index] = 0;
self.capture_age = 0;
ball.capture_age = 0;
}
CaptureStep::Outside
}
fn check_lock_holes(
&mut self,
ball: &mut Ball,
ball_number: u16,
ball_count: u16,
old_position: MilliVec,
events: &mut Vec<Event>,
) -> bool {
) -> Option<BallAction> {
for (index, sensor) in LOCK_HOLES.into_iter().enumerate() {
match self.capture_record_step(
ball,
ball_number,
ball_count,
sensor.id,
sensor.center,
sensor.radius,
@@ -1070,7 +1151,7 @@ impl Game {
events,
) {
CaptureStep::Outside => continue,
CaptureStep::Holding => return true,
CaptureStep::Holding => return Some(BallAction::Keep),
CaptureStep::Complete => {}
}
let filled_before = self.wheel_holes.iter().filter(|filled| **filled).count();
@@ -1087,43 +1168,60 @@ impl Game {
player.extra_balls = player.extra_balls.wrapping_add(1);
self.panel_frame = Some(0);
}
self.reset_ball_to_launcher();
events.push(Event::Lock);
return true;
return Some(if ball_count == 1 {
BallAction::Reset
} else {
BallAction::Remove
});
}
false
None
}
fn check_wheel_reset(
&mut self,
ball: &mut Ball,
ball_number: u16,
ball_count: u16,
old_position: MilliVec,
events: &mut Vec<Event>,
) -> bool {
) -> Option<BallAction> {
match self.capture_record_step(
ball,
ball_number,
ball_count,
WHEEL_RESET_SENSOR.id,
WHEEL_RESET_SENSOR.center,
WHEEL_RESET_SENSOR.radius,
old_position,
events,
) {
CaptureStep::Outside => return false,
CaptureStep::Holding => return true,
CaptureStep::Outside => return None,
CaptureStep::Holding => return Some(BallAction::Keep),
CaptureStep::Complete => {}
}
self.wheel_holes.fill(false);
self.multiball_state = MultiballState::Ready;
self.reset_ball_to_launcher();
events.push(Event::Wheel);
true
Some(if ball_count == 1 {
BallAction::Reset
} else {
BallAction::Remove
})
}
fn check_effect_sensor(
&mut self,
ball: &mut Ball,
ball_number: u16,
old_position: MilliVec,
current_position: MilliVec,
movement_velocity: MilliVec,
events: &mut Vec<Event>,
) {
if self.tilted {
return;
}
let current_position = MilliVec::from_position(ball.position);
let effect_index = usize::from(EFFECT_SENSOR.id);
if self.object_active[effect_index] {
let touched = path_intersects_circle(
@@ -1139,7 +1237,7 @@ impl Game {
EFFECT_SENSOR.center,
EFFECT_SENSOR.radius,
);
if entered && !self.tilted {
if entered {
self.add_score(EFFECT_SENSOR.score, events);
match self.target_effect {
1..=5 => {
@@ -1153,12 +1251,13 @@ impl Game {
self.add_score(transfer, events);
self.players[self.current_player].secondary_score = 0;
}
7 => {
7 if ball_number != 2 && self.multiball_state == MultiballState::Ready => {
self.secondary_ball = Some(Ball {
position: vec2(17.0, 23.0),
velocity: MilliVec { x: 0, y: 3_040 }.to_velocity_per_second(),
in_launcher: false,
spin: Real48::ZERO,
capture_age: 0,
});
self.multiball_state = MultiballState::Unavailable;
}
@@ -1168,18 +1267,22 @@ impl Game {
self.object_active[effect_index] = false;
events.push(Event::Target);
events.push(Event::Sound(2004));
self.randomize_trigger_velocity();
self.randomize_trigger_velocity(ball);
}
}
}
fn check_target_sensors(
&mut self,
ball: &mut Ball,
old_position: MilliVec,
current_position: MilliVec,
movement_velocity: MilliVec,
events: &mut Vec<Event>,
) {
if self.tilted {
return;
}
let current_position = MilliVec::from_position(ball.position);
for sensor in TARGET_SENSORS {
let touched = path_intersects_circle(
old_position,
@@ -1195,7 +1298,7 @@ impl Game {
sensor.center,
sensor.radius,
);
if !entered || self.tilted {
if !entered {
continue;
}
self.add_score(sensor.score, events);
@@ -1207,19 +1310,19 @@ impl Game {
let field_id = [153, 6, 154][usize::from(sensor.id - 150)];
self.object_active[field_id] = true;
}
self.randomize_trigger_velocity();
self.randomize_trigger_velocity(ball);
}
}
fn randomize_trigger_velocity(&mut self) {
let mut velocity = MilliVec::from_velocity_per_second(self.ball.velocity);
fn randomize_trigger_velocity(&mut self, ball: &mut Ball) {
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
let offset = Real48::from_bytes([0x81, 0x71, 0x3d, 0x0a, 0xd7, 0x03]);
let span = Real48::from_bytes([0x7d, 0x71, 0x3d, 0x0a, 0xd7, 0x23]);
let x_factor = offset.subtract(self.random.real48().multiply(span));
let y_factor = offset.subtract(self.random.real48().multiply(span));
velocity.x = Real48::from_i32(velocity.x).multiply(x_factor).round_i32();
velocity.y = Real48::from_i32(velocity.y).multiply(y_factor).round_i32();
self.ball.velocity = velocity.to_velocity_per_second();
ball.velocity = velocity.to_velocity_per_second();
}
fn reset_ball_to_launcher(&mut self) {
@@ -1319,7 +1422,12 @@ impl Game {
events.push(Event::Sound(2015));
}
fn begin_claw_capture_after_hold(&mut self, terminal_frame: u8, events: &mut Vec<Event>) {
fn begin_claw_capture_after_hold(
&mut self,
ball: &mut Ball,
terminal_frame: u8,
events: &mut Vec<Event>,
) {
debug_assert!(CLAW_TERMINAL_FRAMES.contains(&terminal_frame));
if self.claw.active {
return;
@@ -1329,8 +1437,8 @@ impl Game {
self.claw.bank = ClawSpriteBank::Closing;
self.claw.ball_suspended = true;
self.claw.frame_accumulator = 0.0;
self.ball.velocity = Vec2::ZERO;
self.ball.spin = Real48::ZERO;
ball.velocity = Vec2::ZERO;
ball.spin = Real48::ZERO;
events.push(Event::ClawCapture);
}
@@ -1419,7 +1527,6 @@ impl Game {
self.flipper_inputs = Flippers::default();
self.flippers = Flippers::default();
self.pending_flipper_edges.fill(0);
self.capture_age = 0;
self.secondary_ball = None;
self.nudge_shake = 0.0;
self.launcher_charge = 0.0;
@@ -1554,6 +1661,29 @@ mod tests {
}
}
fn capture_primary_record_step(
game: &mut Game,
record_id: u8,
center: Vec2,
radius: f32,
previous_position: MilliVec,
events: &mut Vec<Event>,
) -> CaptureStep {
let mut ball = game.ball;
let result = game.capture_record_step(
&mut ball,
1,
if game.secondary_ball.is_some() { 2 } else { 1 },
record_id,
center,
radius,
previous_position,
events,
);
game.ball = ball;
result
}
#[test]
fn player_count_is_bounded() {
assert_eq!(Game::new(0).players.len(), 1);
@@ -1863,6 +1993,140 @@ mod tests {
assert_eq!(game.ball.position, moving_ball.position);
}
#[test]
fn second_ball_runs_the_same_type_four_rule_dispatch() {
let sensor = TARGET_SENSORS
.into_iter()
.find(|sensor| sensor.id == 150)
.expect("record 150 must be present");
let mut game = Game::new_with_seed(1, 7);
let primary_velocity = game.ball.velocity;
let mut second = Ball {
position: sensor.center,
velocity: vec2(12.0, -34.0),
in_launcher: false,
spin: Real48::ZERO,
capture_age: 0,
};
let old_position = MilliVec::from_position(
sensor.center - vec2(sensor.radius + 1.0, 0.0),
);
let current_position = MilliVec::from_position(sensor.center);
let movement = MilliVec {
x: current_position.x.wrapping_sub(old_position.x),
y: current_position.y.wrapping_sub(old_position.y),
};
let mut events = Vec::new();
assert_eq!(
game.check_sensor_objects_for_ball(
&mut second,
2,
2,
old_position,
movement,
&mut events,
),
BallAction::Keep
);
assert!(game.top_targets[0]);
assert!(game.magnetic_field_active(153));
assert_eq!(game.player().score, sensor.score);
assert_eq!(game.ball.velocity, primary_velocity);
assert_ne!(second.velocity, vec2(12.0, -34.0));
}
#[test]
fn ball_two_consumes_effect_seven_without_spawning_a_third_ball() {
let mut game = Game::new_with_seed(1, 7);
game.multiball_state = MultiballState::Ready;
game.target_effect = 7;
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
let mut second = Ball {
position: EFFECT_SENSOR.center,
velocity: Vec2::ZERO,
in_launcher: false,
spin: Real48::ZERO,
capture_age: 0,
};
let mut events = Vec::new();
assert_eq!(
game.check_sensor_objects_for_ball(
&mut second,
2,
2,
MilliVec::from_position(EFFECT_SENSOR.center),
MilliVec::default(),
&mut events,
),
BallAction::Keep
);
assert!(game.secondary_ball.is_none());
assert_eq!(game.multiball_state, MultiballState::Ready);
assert_eq!(game.target_effect, 0);
assert!(!game.effect_target_active());
}
#[test]
fn multiball_capture_age_is_per_slot_and_removes_only_ball_two() {
let sensor = LOCK_HOLES[0];
let mut game = Game::new(1);
game.ball.capture_age = 17;
game.record_contacts[usize::from(sensor.id)] = 2;
game.secondary_ball = Some(Ball {
position: sensor.center,
velocity: Vec2::ZERO,
in_launcher: false,
spin: Real48::ZERO,
capture_age: 300,
});
let mut events = Vec::new();
game.advance_secondary_ball(&mut events);
assert!(game.secondary_ball.is_none());
assert_eq!(game.ball.capture_age, 17);
assert_eq!(game.record_contacts[usize::from(sensor.id)], 2);
assert!(game.wheel_holes[0]);
assert!(events.contains(&Event::Lock));
}
#[test]
fn capturing_ball_one_during_multiball_promotes_ball_two() {
let sensor = LOCK_HOLES[0];
let mut game = Game::new(1);
game.ball.in_launcher = false;
game.ball.position = sensor.center;
game.ball.velocity = Vec2::ZERO;
game.ball.capture_age = 300;
game.record_contacts[usize::from(sensor.id)] = 1;
let survivor = Ball {
position: vec2(210.0, 240.0),
velocity: vec2(-12.0, 34.0),
in_launcher: false,
spin: Real48::ZERO,
capture_age: 23,
};
game.secondary_ball = Some(survivor);
let mut events = Vec::new();
game.check_sensor_objects(
MilliVec::from_position(sensor.center),
MilliVec::default(),
&mut events,
);
assert!(game.secondary_ball.is_none());
assert_eq!(game.ball.position, survivor.position);
assert_eq!(game.ball.velocity, survivor.velocity);
assert_eq!(game.ball.capture_age, survivor.capture_age);
assert_eq!(game.record_contacts[usize::from(sensor.id)], 2);
assert!(game.wheel_holes[0]);
}
#[test]
fn center_drain_advances_to_a_fresh_ball() {
let mut game = Game::new(1);
@@ -2433,7 +2697,14 @@ mod tests {
let mut events = Vec::new();
assert_eq!(
game.capture_record_step(sensor.id, sensor.center, sensor.radius, previous, &mut events),
capture_primary_record_step(
&mut game,
sensor.id,
sensor.center,
sensor.radius,
previous,
&mut events,
),
CaptureStep::Holding
);
assert_eq!(
@@ -2441,12 +2712,13 @@ mod tests {
MilliVec { x: 750, y: -150 }
);
assert_eq!(game.record_contacts[usize::from(sensor.id)], 0);
assert_eq!(game.capture_age, 0);
assert_eq!(game.ball.capture_age, 0);
game.ball.position = sensor.center;
game.ball.velocity = Vec2::ZERO;
assert_eq!(
game.capture_record_step(
capture_primary_record_step(
&mut game,
sensor.id,
sensor.center,
sensor.radius,
@@ -2456,12 +2728,13 @@ mod tests {
CaptureStep::Holding
);
assert_eq!(game.record_contacts[usize::from(sensor.id)], 1);
assert_eq!(game.capture_age, 5);
assert_eq!(game.ball.capture_age, 5);
assert_eq!(events, [Event::Sound(2015)]);
game.capture_age = 300;
game.ball.capture_age = 300;
assert_eq!(
game.capture_record_step(
capture_primary_record_step(
&mut game,
sensor.id,
sensor.center,
sensor.radius,
@@ -2471,7 +2744,7 @@ mod tests {
CaptureStep::Complete
);
assert_eq!(game.record_contacts[usize::from(sensor.id)], 99);
assert_eq!(game.capture_age, 0);
assert_eq!(game.ball.capture_age, 0);
}
#[test]