fix(divergence): preserve multiball Greifarm captures
The original port consumed a ball as soon as record 89 completed during multiball, which skipped the Greifarm animation and lost the ball permanently. Keep the captured slot through the full animation instead, latch whether the capture began with two live balls, and exclude the launcher release (frame 18) for that capture. The surviving slot continues physics, scoring, nudging, and flipper responses while the held slot is suspended; drain and promotion paths also preserve the held ball. A later Greifarm entry makes a fresh ball-count decision. Update slot-aware rendering and refresh the browser WASM artifact. Test Plan: - `just --justfile tdkpin-rs/justfile test` -- passed (149 game/service tests) - `just --justfile tdkpin-rs/justfile clippy` -- passed - `just --justfile tdkpin-rs/justfile build-production` -- passed - `just --justfile tdkpin-rs/justfile web-build` -- passed - `cargo +nightly fmt --manifest-path tdkpin-rs/Cargo.toml --all -- --check` -- passed - `rumdl check --flavor commonmark tdkpin-rs/CHANGELOG.md tdkpin-rs/AGENTS.md` -- passed - `git diff --cached --check` -- passed
This commit is contained in:
@@ -8,6 +8,13 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Keep a ball captured by the Greifarm during multiball instead of consuming it;
|
||||||
|
latch the two-ball capture rule at each entry, exclude the launcher release
|
||||||
|
while that capture is active, and let later single-ball re-entries use all
|
||||||
|
four original release choices.
|
||||||
|
|
||||||
## [1.2.0] - 2026-08-31
|
## [1.2.0] - 2026-08-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -901,7 +901,7 @@ impl App {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !game.claw.ball_suspended {
|
if !game.primary_ball_suspended() {
|
||||||
draw_texture(
|
draw_texture(
|
||||||
&self.assets.ball,
|
&self.assets.ball,
|
||||||
game.ball.position.x - 8.0,
|
game.ball.position.x - 8.0,
|
||||||
@@ -909,7 +909,9 @@ impl App {
|
|||||||
WHITE,
|
WHITE,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if let Some(ball) = game.secondary_ball {
|
if let Some(ball) = game.secondary_ball
|
||||||
|
&& !game.secondary_ball_suspended()
|
||||||
|
{
|
||||||
draw_texture(
|
draw_texture(
|
||||||
&self.assets.ball,
|
&self.assets.ball,
|
||||||
ball.position.x - 8.0,
|
ball.position.x - 8.0,
|
||||||
|
|||||||
+467
-38
@@ -46,6 +46,7 @@ const CLAW_FRAME_SECONDS: f32 = 0.030;
|
|||||||
const DETAIL_TIMER_SECONDS: [f32; 5] = [0.050, 0.040, 0.030, 0.020, 0.010];
|
const DETAIL_TIMER_SECONDS: [f32; 5] = [0.050, 0.040, 0.030, 0.020, 0.010];
|
||||||
const DETAIL_SUBSTEPS: [u8; 5] = [5, 4, 3, 2, 1];
|
const DETAIL_SUBSTEPS: [u8; 5] = [5, 4, 3, 2, 1];
|
||||||
const CLAW_TERMINAL_FRAMES: [u8; 4] = [1, 6, 7, 18];
|
const CLAW_TERMINAL_FRAMES: [u8; 4] = [1, 6, 7, 18];
|
||||||
|
const CLAW_TABLE_TERMINAL_FRAMES: [u8; 3] = [1, 6, 7];
|
||||||
const ORIGINAL_BALL_SPEED_PER_SECOND: f32 = 380.0;
|
const ORIGINAL_BALL_SPEED_PER_SECOND: f32 = 380.0;
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_wrap)]
|
#[allow(clippy::cast_possible_wrap)]
|
||||||
@@ -129,12 +130,20 @@ pub enum ClawSpriteBank {
|
|||||||
Opening,
|
Opening,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum ClawBallSlot {
|
||||||
|
Primary,
|
||||||
|
Secondary,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Claw {
|
pub struct Claw {
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
pub frame: u8,
|
pub frame: u8,
|
||||||
pub bank: ClawSpriteBank,
|
pub bank: ClawSpriteBank,
|
||||||
pub ball_suspended: bool,
|
pub ball_suspended: bool,
|
||||||
|
captured_slot: Option<ClawBallSlot>,
|
||||||
|
capture_restricted: bool,
|
||||||
target_frame: u8,
|
target_frame: u8,
|
||||||
frame_accumulator: f32,
|
frame_accumulator: f32,
|
||||||
}
|
}
|
||||||
@@ -146,6 +155,8 @@ impl Default for Claw {
|
|||||||
frame: 10,
|
frame: 10,
|
||||||
bank: ClawSpriteBank::Opening,
|
bank: ClawSpriteBank::Opening,
|
||||||
ball_suspended: false,
|
ball_suspended: false,
|
||||||
|
captured_slot: None,
|
||||||
|
capture_restricted: false,
|
||||||
target_frame: 10,
|
target_frame: 10,
|
||||||
frame_accumulator: 0.0,
|
frame_accumulator: 0.0,
|
||||||
}
|
}
|
||||||
@@ -592,19 +603,23 @@ impl Game {
|
|||||||
self.update_target_rotation(events);
|
self.update_target_rotation(events);
|
||||||
}
|
}
|
||||||
let panel_active = self.update_panel_completion(events);
|
let panel_active = self.update_panel_completion(events);
|
||||||
// The original collapses an inactive two-ball slot immediately before
|
// A held claw ball still counts as the second live ball until the
|
||||||
// the next physics pass. Capture removal does not clear multiball
|
// other slot disappears or the claw releases it.
|
||||||
// scoring until that collapse, so the surviving slot retains double
|
if self.secondary_ball.is_none()
|
||||||
// scoring for the remainder of the callback in which it was captured.
|
&& self.claw.captured_slot != Some(ClawBallSlot::Secondary)
|
||||||
if self.secondary_ball.is_none() && self.score_mode == ScoreMode::Multiball {
|
&& self.score_mode == ScoreMode::Multiball
|
||||||
|
{
|
||||||
self.score_mode = ScoreMode::Normal;
|
self.score_mode = ScoreMode::Normal;
|
||||||
}
|
}
|
||||||
if !self.claw.ball_suspended && !panel_active {
|
if !panel_active {
|
||||||
let had_secondary_ball = self.secondary_ball.is_some();
|
let had_secondary_ball = self.secondary_ball.is_some();
|
||||||
let mut spawned_secondary = None;
|
let mut spawned_secondary = None;
|
||||||
|
let primary_held = self.claw_holds_slot(ClawBallSlot::Primary);
|
||||||
|
let secondary_held = self.claw_holds_slot(ClawBallSlot::Secondary);
|
||||||
if had_secondary_ball {
|
if had_secondary_ball {
|
||||||
self.score_mode = ScoreMode::Multiball;
|
self.score_mode = ScoreMode::Multiball;
|
||||||
}
|
}
|
||||||
|
if !primary_held {
|
||||||
for _ in 0..substeps {
|
for _ in 0..substeps {
|
||||||
let stop = self.fixed_update(events);
|
let stop = self.fixed_update(events);
|
||||||
// Effect seven publishes its spawn request during record 149,
|
// Effect seven publishes its spawn request during record 149,
|
||||||
@@ -613,18 +628,21 @@ impl Game {
|
|||||||
if !had_secondary_ball && spawned_secondary.is_none() {
|
if !had_secondary_ball && spawned_secondary.is_none() {
|
||||||
spawned_secondary = self.secondary_ball.take();
|
spawned_secondary = self.secondary_ball.take();
|
||||||
}
|
}
|
||||||
if stop || self.finished || self.claw.ball_suspended {
|
if stop || self.finished || self.claw_holds_slot(ClawBallSlot::Primary) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if had_secondary_ball && !self.finished && !self.claw.ball_suspended {
|
}
|
||||||
if self.secondary_ball.is_some() {
|
if had_secondary_ball && !self.finished {
|
||||||
|
if self.secondary_ball.is_some() && !self.claw_holds_slot(ClawBallSlot::Secondary) {
|
||||||
for _ in 0..substeps {
|
for _ in 0..substeps {
|
||||||
if self.advance_secondary_ball(events) {
|
if self.advance_secondary_ball_slot(events, !primary_held) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if !self.claw_holds_slot(ClawBallSlot::Primary)
|
||||||
|
&& !self.claw_holds_slot(ClawBallSlot::Secondary)
|
||||||
|
{
|
||||||
self.secondary_ball = Some(self.ball);
|
self.secondary_ball = Some(self.ball);
|
||||||
for _ in 0..substeps {
|
for _ in 0..substeps {
|
||||||
if self.advance_secondary_ball_slot(events, false) {
|
if self.advance_secondary_ball_slot(events, false) {
|
||||||
@@ -642,6 +660,10 @@ impl Game {
|
|||||||
debug_assert!(self.secondary_ball.is_none());
|
debug_assert!(self.secondary_ball.is_none());
|
||||||
self.secondary_ball = Some(spawned);
|
self.secondary_ball = Some(spawned);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The two slots are mutually exclusive while the claw is holding
|
||||||
|
// a ball.
|
||||||
|
debug_assert!(!(primary_held && secondary_held));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pending_flipper_edges[0] =
|
self.pending_flipper_edges[0] =
|
||||||
@@ -759,6 +781,8 @@ impl Game {
|
|||||||
}
|
}
|
||||||
Nudge::None => return,
|
Nudge::None => return,
|
||||||
}
|
}
|
||||||
|
let primary_held = self.claw_holds_slot(ClawBallSlot::Primary);
|
||||||
|
let secondary_held = self.claw_holds_slot(ClawBallSlot::Secondary);
|
||||||
if let Some(secondary) = &mut self.secondary_ball {
|
if let Some(secondary) = &mut self.secondary_ball {
|
||||||
let slot_impulse = match nudge {
|
let slot_impulse = match nudge {
|
||||||
Nudge::Left | Nudge::Right => MilliVec {
|
Nudge::Left | Nudge::Right => MilliVec {
|
||||||
@@ -778,12 +802,16 @@ impl Game {
|
|||||||
let mut primary_slot = MilliVec::from_velocity_per_second(self.ball.velocity);
|
let mut primary_slot = MilliVec::from_velocity_per_second(self.ball.velocity);
|
||||||
primary_slot.x = primary_slot.x.wrapping_add(slot_impulse.x);
|
primary_slot.x = primary_slot.x.wrapping_add(slot_impulse.x);
|
||||||
primary_slot.y = primary_slot.y.wrapping_add(slot_impulse.y);
|
primary_slot.y = primary_slot.y.wrapping_add(slot_impulse.y);
|
||||||
|
if !primary_held {
|
||||||
self.ball.velocity = primary_slot.to_velocity_per_second();
|
self.ball.velocity = primary_slot.to_velocity_per_second();
|
||||||
|
}
|
||||||
let mut secondary_slot = MilliVec::from_velocity_per_second(secondary.velocity);
|
let mut secondary_slot = MilliVec::from_velocity_per_second(secondary.velocity);
|
||||||
secondary_slot.x = secondary_slot.x.wrapping_add(slot_impulse.x);
|
secondary_slot.x = secondary_slot.x.wrapping_add(slot_impulse.x);
|
||||||
secondary_slot.y = secondary_slot.y.wrapping_add(slot_impulse.y);
|
secondary_slot.y = secondary_slot.y.wrapping_add(slot_impulse.y);
|
||||||
|
if !secondary_held {
|
||||||
secondary.velocity = secondary_slot.to_velocity_per_second();
|
secondary.velocity = secondary_slot.to_velocity_per_second();
|
||||||
} else {
|
}
|
||||||
|
} else if !primary_held {
|
||||||
self.ball.velocity = velocity.to_velocity_per_second();
|
self.ball.velocity = velocity.to_velocity_per_second();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,8 +860,13 @@ impl Game {
|
|||||||
);
|
);
|
||||||
let mut best_collision = best_static
|
let mut best_collision = best_static
|
||||||
.map(|(id, is_wall, candidate)| (id, is_wall, candidate.resolve(velocity, ball.spin)));
|
.map(|(id, is_wall, candidate)| (id, is_wall, candidate.resolve(velocity, ball.spin)));
|
||||||
|
let primary_held = self.claw_holds_slot(ClawBallSlot::Primary);
|
||||||
|
let secondary_held = self.claw_holds_slot(ClawBallSlot::Secondary);
|
||||||
let mut transferred_secondary_velocity = None;
|
let mut transferred_secondary_velocity = None;
|
||||||
if let Some(secondary) = initial_secondary {
|
if let Some(secondary) = initial_secondary
|
||||||
|
&& !primary_held
|
||||||
|
&& !secondary_held
|
||||||
|
{
|
||||||
let other_position = MilliVec::from_position(secondary.position);
|
let other_position = MilliVec::from_position(secondary.position);
|
||||||
let response = ball_collision_response(
|
let response = ball_collision_response(
|
||||||
old_position,
|
old_position,
|
||||||
@@ -876,6 +909,8 @@ impl Game {
|
|||||||
if object_id == 175
|
if object_id == 175
|
||||||
&& let (Some(secondary), Some(transferred)) =
|
&& let (Some(secondary), Some(transferred)) =
|
||||||
(&mut self.secondary_ball, transferred_secondary_velocity)
|
(&mut self.secondary_ball, transferred_secondary_velocity)
|
||||||
|
&& !secondary_held
|
||||||
|
&& !primary_held
|
||||||
{
|
{
|
||||||
secondary.velocity = transferred.to_velocity_per_second();
|
secondary.velocity = transferred.to_velocity_per_second();
|
||||||
}
|
}
|
||||||
@@ -944,6 +979,10 @@ impl Game {
|
|||||||
.secondary_ball
|
.secondary_ball
|
||||||
.take()
|
.take()
|
||||||
.expect("a multiball capture must leave the other slot active");
|
.expect("a multiball capture must leave the other slot active");
|
||||||
|
if self.claw.captured_slot == Some(ClawBallSlot::Secondary) {
|
||||||
|
self.claw.captured_slot = Some(ClawBallSlot::Primary);
|
||||||
|
self.score_mode = ScoreMode::Normal;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1008,12 +1047,12 @@ impl Game {
|
|||||||
&mut capture_candidate,
|
&mut capture_candidate,
|
||||||
events,
|
events,
|
||||||
) {
|
) {
|
||||||
CaptureStep::Complete if ball_count == 1 => {
|
CaptureStep::Complete => {
|
||||||
let terminal_frame = self.next_claw_terminal_frame();
|
let terminal_frame =
|
||||||
self.begin_claw_capture_after_hold(ball, terminal_frame, events);
|
self.next_claw_terminal_frame(self.claw.capture_restricted);
|
||||||
|
self.begin_claw_capture_after_hold(ball, ball_number, terminal_frame, events);
|
||||||
action = BallAction::Suspend;
|
action = BallAction::Suspend;
|
||||||
}
|
}
|
||||||
CaptureStep::Complete => action = BallAction::Remove,
|
|
||||||
CaptureStep::Outside | CaptureStep::Holding => {}
|
CaptureStep::Outside | CaptureStep::Holding => {}
|
||||||
}
|
}
|
||||||
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
@@ -1227,8 +1266,9 @@ impl Game {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn advance_secondary_ball(&mut self, events: &mut Vec<Event>) -> bool {
|
fn advance_secondary_ball(&mut self, events: &mut Vec<Event>) -> bool {
|
||||||
self.advance_secondary_ball_slot(events, true)
|
self.advance_secondary_ball_slot(events, !self.claw_holds_slot(ClawBallSlot::Primary))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
@@ -1267,6 +1307,8 @@ impl Game {
|
|||||||
let mut transferred_primary_velocity = None;
|
let mut transferred_primary_velocity = None;
|
||||||
let other_position = MilliVec::from_position(primary_position);
|
let other_position = MilliVec::from_position(primary_position);
|
||||||
if primary_slot_active
|
if primary_slot_active
|
||||||
|
&& !self.claw_holds_slot(ClawBallSlot::Primary)
|
||||||
|
&& !self.claw_holds_slot(ClawBallSlot::Secondary)
|
||||||
&& predicted.x >= other_position.x.wrapping_sub(21_000)
|
&& predicted.x >= other_position.x.wrapping_sub(21_000)
|
||||||
&& predicted.x <= other_position.x.wrapping_add(21_000)
|
&& predicted.x <= other_position.x.wrapping_add(21_000)
|
||||||
&& predicted.y >= other_position.y.wrapping_sub(21_000)
|
&& predicted.y >= other_position.y.wrapping_sub(21_000)
|
||||||
@@ -1556,13 +1598,10 @@ impl Game {
|
|||||||
) {
|
) {
|
||||||
CaptureStep::Holding | CaptureStep::Outside => {}
|
CaptureStep::Holding | CaptureStep::Outside => {}
|
||||||
CaptureStep::Complete => {
|
CaptureStep::Complete => {
|
||||||
if ball_count == 1 {
|
let terminal_frame =
|
||||||
let terminal_frame = self.next_claw_terminal_frame();
|
self.next_claw_terminal_frame(self.claw.capture_restricted);
|
||||||
self.begin_claw_capture_after_hold(ball, terminal_frame, events);
|
self.begin_claw_capture_after_hold(ball, ball_number, terminal_frame, events);
|
||||||
action = BallAction::Suspend;
|
action = BallAction::Suspend;
|
||||||
} else {
|
|
||||||
action = BallAction::Remove;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1595,7 +1634,11 @@ impl Game {
|
|||||||
SensorScanResult { action }
|
SensorScanResult { action }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation, clippy::too_many_arguments)]
|
#[allow(
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
clippy::too_many_lines
|
||||||
|
)]
|
||||||
fn capture_record_step(
|
fn capture_record_step(
|
||||||
&mut self,
|
&mut self,
|
||||||
ball: &mut Ball,
|
ball: &mut Ball,
|
||||||
@@ -1637,6 +1680,12 @@ impl Game {
|
|||||||
&& contact_allowed
|
&& contact_allowed
|
||||||
&& !(record_id == 148 && self.special_hole_gate == SpecialHoleGate::Suppressed)
|
&& !(record_id == 148 && self.special_hole_gate == SpecialHoleGate::Suppressed)
|
||||||
{
|
{
|
||||||
|
if record_id == 89 && ball.capture_age == 0 {
|
||||||
|
// Latch the restriction when the ball first enters the deep
|
||||||
|
// capture zone, before the pull/hold phase can outlive the
|
||||||
|
// other active ball.
|
||||||
|
self.claw.capture_restricted = ball_count > 1;
|
||||||
|
}
|
||||||
if ball.capture_age < 300 {
|
if ball.capture_age < 300 {
|
||||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
let stationary = center.x.wrapping_sub(previous_position.x).wrapping_abs() < 500
|
let stationary = center.x.wrapping_sub(previous_position.x).wrapping_abs() < 500
|
||||||
@@ -1679,16 +1728,24 @@ impl Game {
|
|||||||
return CaptureStep::Complete;
|
return CaptureStep::Complete;
|
||||||
}
|
}
|
||||||
ball.capture_age = 0;
|
ball.capture_age = 0;
|
||||||
self.record_contacts[contact_index] = if record_id == 148 { 2 } else { 99 };
|
self.record_contacts[contact_index] = if record_id == 89 && ball_count > 1 {
|
||||||
if ball_count > 1 {
|
// Keep ownership with the held slot so its later release can
|
||||||
self.record_contacts[contact_index] = 2;
|
// clear the claw contact before a fresh entry.
|
||||||
}
|
ball_number
|
||||||
|
} else if record_id == 148 || ball_count > 1 {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
99
|
||||||
|
};
|
||||||
return CaptureStep::Complete;
|
return CaptureStep::Complete;
|
||||||
}
|
}
|
||||||
|
|
||||||
if surface_distance > -11_000 && contact == ball_number {
|
if surface_distance > -11_000 && contact == ball_number {
|
||||||
self.record_contacts[contact_index] = 0;
|
self.record_contacts[contact_index] = 0;
|
||||||
ball.capture_age = 0;
|
ball.capture_age = 0;
|
||||||
|
if record_id == 89 {
|
||||||
|
self.claw.capture_restricted = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
let velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
if surface_distance <= milli_distance(velocity)
|
if surface_distance <= milli_distance(velocity)
|
||||||
@@ -2052,9 +2109,27 @@ impl Game {
|
|||||||
self.target_effect.min(7)
|
self.target_effect.min(7)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_claw_terminal_frame(&mut self) -> u8 {
|
fn next_claw_terminal_frame(&mut self, restricted: bool) -> u8 {
|
||||||
|
if restricted {
|
||||||
|
// Frame 18 sends the released ball down the shooter lane. During
|
||||||
|
// a capture that began with two live balls, keep it on the table.
|
||||||
|
CLAW_TABLE_TERMINAL_FRAMES[usize::from(self.random.below(3))]
|
||||||
|
} else {
|
||||||
CLAW_TERMINAL_FRAMES[usize::from(self.random.below(4))]
|
CLAW_TERMINAL_FRAMES[usize::from(self.random.below(4))]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn claw_holds_slot(&self, slot: ClawBallSlot) -> bool {
|
||||||
|
self.claw.ball_suspended && self.claw.captured_slot == Some(slot)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn primary_ball_suspended(&self) -> bool {
|
||||||
|
self.claw_holds_slot(ClawBallSlot::Primary)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn secondary_ball_suspended(&self) -> bool {
|
||||||
|
self.claw_holds_slot(ClawBallSlot::Secondary)
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_flipper_kicks(&mut self) {
|
fn apply_flipper_kicks(&mut self) {
|
||||||
let pending = self.pending_flipper_edges;
|
let pending = self.pending_flipper_edges;
|
||||||
@@ -2062,6 +2137,8 @@ impl Game {
|
|||||||
if self.ball.in_launcher || self.tilted {
|
if self.ball.in_launcher || self.tilted {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let primary_held = self.claw_holds_slot(ClawBallSlot::Primary);
|
||||||
|
let secondary_held = self.claw_holds_slot(ClawBallSlot::Secondary);
|
||||||
|
|
||||||
for (delta, side) in [
|
for (delta, side) in [
|
||||||
(i32::from(pending[0]), FlipperSide::Left),
|
(i32::from(pending[0]), FlipperSide::Left),
|
||||||
@@ -2070,8 +2147,12 @@ impl Game {
|
|||||||
if delta == 0 {
|
if delta == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if !primary_held {
|
||||||
apply_flipper_response_to_ball(&mut self.ball, delta, side);
|
apply_flipper_response_to_ball(&mut self.ball, delta, side);
|
||||||
if let Some(secondary) = &mut self.secondary_ball {
|
}
|
||||||
|
if let Some(secondary) = &mut self.secondary_ball
|
||||||
|
&& !secondary_held
|
||||||
|
{
|
||||||
apply_flipper_response_to_ball(secondary, delta, side);
|
apply_flipper_response_to_ball(secondary, delta, side);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2087,6 +2168,8 @@ impl Game {
|
|||||||
self.claw.target_frame = terminal_frame;
|
self.claw.target_frame = terminal_frame;
|
||||||
self.claw.bank = ClawSpriteBank::Closing;
|
self.claw.bank = ClawSpriteBank::Closing;
|
||||||
self.claw.ball_suspended = true;
|
self.claw.ball_suspended = true;
|
||||||
|
self.claw.captured_slot = Some(ClawBallSlot::Primary);
|
||||||
|
self.claw.capture_restricted = false;
|
||||||
self.claw.frame_accumulator = 0.0;
|
self.claw.frame_accumulator = 0.0;
|
||||||
self.ball.velocity = Vec2::ZERO;
|
self.ball.velocity = Vec2::ZERO;
|
||||||
self.ball.spin = Real48::ZERO;
|
self.ball.spin = Real48::ZERO;
|
||||||
@@ -2097,6 +2180,7 @@ impl Game {
|
|||||||
fn begin_claw_capture_after_hold(
|
fn begin_claw_capture_after_hold(
|
||||||
&mut self,
|
&mut self,
|
||||||
ball: &mut Ball,
|
ball: &mut Ball,
|
||||||
|
ball_number: u16,
|
||||||
terminal_frame: u8,
|
terminal_frame: u8,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
) {
|
) {
|
||||||
@@ -2108,6 +2192,11 @@ impl Game {
|
|||||||
self.claw.target_frame = terminal_frame;
|
self.claw.target_frame = terminal_frame;
|
||||||
self.claw.bank = ClawSpriteBank::Closing;
|
self.claw.bank = ClawSpriteBank::Closing;
|
||||||
self.claw.ball_suspended = true;
|
self.claw.ball_suspended = true;
|
||||||
|
self.claw.captured_slot = Some(if ball_number == 2 {
|
||||||
|
ClawBallSlot::Secondary
|
||||||
|
} else {
|
||||||
|
ClawBallSlot::Primary
|
||||||
|
});
|
||||||
self.claw.frame_accumulator = 0.0;
|
self.claw.frame_accumulator = 0.0;
|
||||||
ball.velocity = Vec2::ZERO;
|
ball.velocity = Vec2::ZERO;
|
||||||
ball.spin = Real48::ZERO;
|
ball.spin = Real48::ZERO;
|
||||||
@@ -2115,7 +2204,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update_claw(&mut self, dt: f32, events: &mut Vec<Event>) {
|
fn update_claw(&mut self, dt: f32, events: &mut Vec<Event>) {
|
||||||
if !self.claw.active || self.score_mode == ScoreMode::Multiball {
|
if !self.claw.active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.claw.frame_accumulator += dt;
|
self.claw.frame_accumulator += dt;
|
||||||
@@ -2145,7 +2234,24 @@ impl Game {
|
|||||||
let release_frame = self.claw.frame;
|
let release_frame = self.claw.frame;
|
||||||
self.claw.target_frame = 10;
|
self.claw.target_frame = 10;
|
||||||
self.claw.bank = ClawSpriteBank::Opening;
|
self.claw.bank = ClawSpriteBank::Opening;
|
||||||
(self.ball.position, self.ball.velocity) = claw_release(release_frame);
|
let release = claw_release(release_frame);
|
||||||
|
match self.claw.captured_slot {
|
||||||
|
Some(ClawBallSlot::Primary) => {
|
||||||
|
(self.ball.position, self.ball.velocity) = release;
|
||||||
|
}
|
||||||
|
Some(ClawBallSlot::Secondary) => {
|
||||||
|
if let Some(ball) = &mut self.secondary_ball {
|
||||||
|
(ball.position, ball.velocity) = release;
|
||||||
|
} else {
|
||||||
|
(self.ball.position, self.ball.velocity) = release;
|
||||||
|
self.claw.captured_slot = Some(ClawBallSlot::Primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
(self.ball.position, self.ball.velocity) = release;
|
||||||
|
self.claw.captured_slot = Some(ClawBallSlot::Primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
self.claw.ball_suspended = false;
|
self.claw.ball_suspended = false;
|
||||||
events.push(Event::ClawRelease);
|
events.push(Event::ClawRelease);
|
||||||
events.push(Event::Sound(2016));
|
events.push(Event::Sound(2016));
|
||||||
@@ -2180,6 +2286,16 @@ impl Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn drain(&mut self, events: &mut Vec<Event>) {
|
fn drain(&mut self, events: &mut Vec<Event>) {
|
||||||
|
if self.claw_holds_slot(ClawBallSlot::Secondary) {
|
||||||
|
self.ball = self
|
||||||
|
.secondary_ball
|
||||||
|
.take()
|
||||||
|
.expect("the claw-held secondary ball must remain in slot two");
|
||||||
|
self.claw.captured_slot = Some(ClawBallSlot::Primary);
|
||||||
|
self.score_mode = ScoreMode::Normal;
|
||||||
|
events.push(Event::Drain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(remaining_ball) = self.secondary_ball.take() {
|
if let Some(remaining_ball) = self.secondary_ball.take() {
|
||||||
self.ball = remaining_ball;
|
self.ball = remaining_ball;
|
||||||
self.score_mode = ScoreMode::Normal;
|
self.score_mode = ScoreMode::Normal;
|
||||||
@@ -3496,20 +3612,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn active_multiball_pauses_the_returning_claw_until_slot_collapse() {
|
fn active_multiball_advances_the_returning_claw_while_both_balls_live() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.claw.active = true;
|
game.claw.active = true;
|
||||||
game.claw.frame = 1;
|
game.claw.frame = 1;
|
||||||
game.claw.target_frame = 10;
|
game.claw.target_frame = 10;
|
||||||
game.claw.bank = ClawSpriteBank::Opening;
|
game.claw.bank = ClawSpriteBank::Opening;
|
||||||
game.claw.frame_accumulator = 0.0;
|
game.claw.frame_accumulator = 0.0;
|
||||||
|
game.claw.ball_suspended = true;
|
||||||
|
game.claw.captured_slot = Some(ClawBallSlot::Primary);
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
|
||||||
assert_eq!(game.claw.frame, 1);
|
|
||||||
|
|
||||||
game.score_mode = ScoreMode::Normal;
|
|
||||||
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
||||||
assert_eq!(game.claw.frame, 2);
|
assert_eq!(game.claw.frame, 2);
|
||||||
}
|
}
|
||||||
@@ -4365,6 +4479,321 @@ mod tests {
|
|||||||
assert!(!game.claw.active);
|
assert!(!game.claw.active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiball_claw_capture_keeps_the_ball_and_uses_only_table_releases() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.ball.in_launcher = false;
|
||||||
|
game.ball.position = CLAW_TRIGGER_CENTER;
|
||||||
|
game.ball.velocity = Vec2::ZERO;
|
||||||
|
game.secondary_ball = Some(Ball {
|
||||||
|
position: vec2(210.0, 240.0),
|
||||||
|
velocity: vec2(12.0, -34.0),
|
||||||
|
in_launcher: false,
|
||||||
|
spin: Real48::ZERO,
|
||||||
|
capture_age: 0,
|
||||||
|
});
|
||||||
|
game.object_active.fill(false);
|
||||||
|
game.score_mode = ScoreMode::Multiball;
|
||||||
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
|
BallAction::Keep
|
||||||
|
);
|
||||||
|
assert!(game.claw.capture_restricted);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
|
BallAction::Suspend
|
||||||
|
);
|
||||||
|
assert!(game.claw.active);
|
||||||
|
assert!(game.primary_ball_suspended());
|
||||||
|
assert!(!game.secondary_ball_suspended());
|
||||||
|
assert!(matches!(game.claw.target_frame, 1 | 6 | 7));
|
||||||
|
assert_eq!(game.record_contacts[89], 1);
|
||||||
|
|
||||||
|
let survivor_before = game.secondary_ball.expect("survivor must remain").position;
|
||||||
|
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
||||||
|
assert_ne!(
|
||||||
|
game.secondary_ball.expect("survivor must remain").position,
|
||||||
|
survivor_before
|
||||||
|
);
|
||||||
|
|
||||||
|
while game.claw.active {
|
||||||
|
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
||||||
|
}
|
||||||
|
assert!(events.contains(&Event::ClawRelease));
|
||||||
|
assert!(!game.primary_ball_suspended());
|
||||||
|
assert!(game.secondary_ball.is_some());
|
||||||
|
|
||||||
|
game.ball.position = CLAW_TRIGGER_CENTER + vec2(20.0, 0.0);
|
||||||
|
game.ball.velocity = Vec2::ZERO;
|
||||||
|
game.ball.capture_age = 0;
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects(
|
||||||
|
MilliVec::from_position(game.ball.position),
|
||||||
|
MilliVec::default(),
|
||||||
|
&mut events,
|
||||||
|
),
|
||||||
|
BallAction::Keep
|
||||||
|
);
|
||||||
|
assert_eq!(game.record_contacts[89], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiball_claw_restriction_is_latched_before_the_other_ball_drains() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.ball.in_launcher = false;
|
||||||
|
game.ball.position = CLAW_TRIGGER_CENTER;
|
||||||
|
game.ball.velocity = vec2(100.0, 0.0);
|
||||||
|
game.secondary_ball = Some(Ball {
|
||||||
|
position: vec2(210.0, 240.0),
|
||||||
|
velocity: Vec2::ZERO,
|
||||||
|
in_launcher: false,
|
||||||
|
spin: Real48::ZERO,
|
||||||
|
capture_age: 0,
|
||||||
|
});
|
||||||
|
game.object_active.fill(false);
|
||||||
|
game.score_mode = ScoreMode::Multiball;
|
||||||
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
|
BallAction::Keep
|
||||||
|
);
|
||||||
|
game.secondary_ball = None;
|
||||||
|
game.score_mode = ScoreMode::Normal;
|
||||||
|
game.ball.velocity = Vec2::ZERO;
|
||||||
|
game.ball.capture_age = 300;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
|
BallAction::Suspend
|
||||||
|
);
|
||||||
|
assert!(game.claw.capture_restricted);
|
||||||
|
assert!(matches!(game.claw.target_frame, 1 | 6 | 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn secondary_multiball_ball_can_be_held_by_the_claw_without_hiding_primary() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.ball.in_launcher = false;
|
||||||
|
game.ball.position = vec2(210.0, 240.0);
|
||||||
|
game.ball.velocity = Vec2::ZERO;
|
||||||
|
game.secondary_ball = Some(Ball {
|
||||||
|
position: CLAW_TRIGGER_CENTER,
|
||||||
|
velocity: Vec2::ZERO,
|
||||||
|
in_launcher: false,
|
||||||
|
spin: Real48::ZERO,
|
||||||
|
capture_age: 0,
|
||||||
|
});
|
||||||
|
game.object_active.fill(false);
|
||||||
|
game.score_mode = ScoreMode::Multiball;
|
||||||
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
|
let mut events = Vec::new();
|
||||||
|
let mut secondary = game.secondary_ball.take().expect("secondary ball");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects_for_ball(
|
||||||
|
&mut secondary,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
claw_position,
|
||||||
|
MilliVec::default(),
|
||||||
|
&mut events,
|
||||||
|
)
|
||||||
|
.action,
|
||||||
|
BallAction::Keep
|
||||||
|
);
|
||||||
|
game.secondary_ball = Some(secondary);
|
||||||
|
let mut secondary = game.secondary_ball.take().expect("secondary ball");
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects_for_ball(
|
||||||
|
&mut secondary,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
claw_position,
|
||||||
|
MilliVec::default(),
|
||||||
|
&mut events,
|
||||||
|
)
|
||||||
|
.action,
|
||||||
|
BallAction::Suspend
|
||||||
|
);
|
||||||
|
game.secondary_ball = Some(secondary);
|
||||||
|
|
||||||
|
assert!(!game.primary_ball_suspended());
|
||||||
|
assert!(game.secondary_ball_suspended());
|
||||||
|
assert!(matches!(game.claw.target_frame, 1 | 6 | 7));
|
||||||
|
let primary_before = game.ball.position;
|
||||||
|
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
||||||
|
assert_ne!(game.ball.position, primary_before);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn held_claw_ball_survives_the_other_ball_draining() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.ball.in_launcher = false;
|
||||||
|
game.ball.position = CLAW_TRIGGER_CENTER;
|
||||||
|
game.ball.velocity = Vec2::ZERO;
|
||||||
|
game.secondary_ball = Some(Ball {
|
||||||
|
position: vec2(340.0, 100.0),
|
||||||
|
velocity: vec2(1.0, 0.0),
|
||||||
|
in_launcher: false,
|
||||||
|
spin: Real48::ZERO,
|
||||||
|
capture_age: 0,
|
||||||
|
});
|
||||||
|
game.object_active.fill(false);
|
||||||
|
game.score_mode = ScoreMode::Multiball;
|
||||||
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
|
let mut events = Vec::new();
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
|
BallAction::Keep
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
|
BallAction::Suspend
|
||||||
|
);
|
||||||
|
let balls_before = game.player().balls;
|
||||||
|
|
||||||
|
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
||||||
|
assert!(game.secondary_ball.is_none());
|
||||||
|
assert_eq!(game.player().balls, balls_before);
|
||||||
|
assert_eq!(game.score_mode, ScoreMode::Normal);
|
||||||
|
assert!(game.claw.ball_suspended);
|
||||||
|
|
||||||
|
while game.claw.ball_suspended {
|
||||||
|
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
||||||
|
}
|
||||||
|
assert!(events.contains(&Event::ClawRelease));
|
||||||
|
assert!(!game.ball.in_launcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn held_secondary_claw_ball_is_promoted_if_primary_drains() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.ball.in_launcher = false;
|
||||||
|
game.ball.position = vec2(340.0, 100.0);
|
||||||
|
game.ball.velocity = vec2(1.0, 0.0);
|
||||||
|
game.secondary_ball = Some(Ball {
|
||||||
|
position: CLAW_TRIGGER_CENTER,
|
||||||
|
velocity: Vec2::ZERO,
|
||||||
|
in_launcher: false,
|
||||||
|
spin: Real48::ZERO,
|
||||||
|
capture_age: 0,
|
||||||
|
});
|
||||||
|
game.object_active.fill(false);
|
||||||
|
game.score_mode = ScoreMode::Multiball;
|
||||||
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
|
let mut events = Vec::new();
|
||||||
|
let mut secondary = game.secondary_ball.take().expect("secondary ball");
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects_for_ball(
|
||||||
|
&mut secondary,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
claw_position,
|
||||||
|
MilliVec::default(),
|
||||||
|
&mut events,
|
||||||
|
)
|
||||||
|
.action,
|
||||||
|
BallAction::Keep
|
||||||
|
);
|
||||||
|
game.secondary_ball = Some(secondary);
|
||||||
|
let mut secondary = game.secondary_ball.take().expect("secondary ball");
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects_for_ball(
|
||||||
|
&mut secondary,
|
||||||
|
2,
|
||||||
|
2,
|
||||||
|
claw_position,
|
||||||
|
MilliVec::default(),
|
||||||
|
&mut events,
|
||||||
|
)
|
||||||
|
.action,
|
||||||
|
BallAction::Suspend
|
||||||
|
);
|
||||||
|
game.secondary_ball = Some(secondary);
|
||||||
|
|
||||||
|
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
||||||
|
assert!(game.secondary_ball.is_none());
|
||||||
|
assert!(game.primary_ball_suspended());
|
||||||
|
assert_eq!(game.score_mode, ScoreMode::Normal);
|
||||||
|
assert!(events.contains(&Event::Drain));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unrestricted_claw_capture_retains_the_launcher_release_choice() {
|
||||||
|
let mut saw_launcher_release = false;
|
||||||
|
for seed in 0..64 {
|
||||||
|
let mut game = Game::new_with_seed(1, seed);
|
||||||
|
if game.next_claw_terminal_frame(false) == 18 {
|
||||||
|
saw_launcher_release = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(saw_launcher_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn claw_reentry_after_multiball_uses_the_current_ball_count() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.ball.in_launcher = false;
|
||||||
|
game.ball.position = CLAW_TRIGGER_CENTER;
|
||||||
|
game.ball.velocity = Vec2::ZERO;
|
||||||
|
game.secondary_ball = Some(Ball {
|
||||||
|
position: vec2(210.0, 240.0),
|
||||||
|
velocity: Vec2::ZERO,
|
||||||
|
in_launcher: false,
|
||||||
|
spin: Real48::ZERO,
|
||||||
|
capture_age: 0,
|
||||||
|
});
|
||||||
|
game.object_active.fill(false);
|
||||||
|
game.score_mode = ScoreMode::Multiball;
|
||||||
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
|
let mut events = Vec::new();
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
|
BallAction::Keep
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
|
BallAction::Suspend
|
||||||
|
);
|
||||||
|
assert!(game.claw.capture_restricted);
|
||||||
|
|
||||||
|
game.secondary_ball = None;
|
||||||
|
game.score_mode = ScoreMode::Normal;
|
||||||
|
while game.claw.active {
|
||||||
|
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
||||||
|
}
|
||||||
|
|
||||||
|
let unrestricted_seed = (0..64)
|
||||||
|
.find(|seed| {
|
||||||
|
let mut candidate = Game::new_with_seed(1, *seed);
|
||||||
|
candidate.next_claw_terminal_frame(false) == 18
|
||||||
|
})
|
||||||
|
.expect("a seed must select the launcher release");
|
||||||
|
game.random = BorlandRandom::new(unrestricted_seed);
|
||||||
|
game.ball.position = CLAW_TRIGGER_CENTER;
|
||||||
|
game.ball.velocity = Vec2::ZERO;
|
||||||
|
game.ball.capture_age = 0;
|
||||||
|
game.record_contacts[89] = 0;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
|
BallAction::Keep
|
||||||
|
);
|
||||||
|
assert!(!game.claw.capture_restricted);
|
||||||
|
assert_eq!(
|
||||||
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
|
BallAction::Suspend
|
||||||
|
);
|
||||||
|
assert_eq!(game.claw.target_frame, 18);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn claw_frame_cadence_follows_the_five_original_timer_choices() {
|
fn claw_frame_cadence_follows_the_five_original_timer_choices() {
|
||||||
for (detail, frame_seconds) in DETAIL_TIMER_SECONDS.into_iter().enumerate() {
|
for (detail, frame_seconds) in DETAIL_TIMER_SECONDS.into_iter().enumerate() {
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user