feat(divergence): make every ball identical and bind crazy mode to 0
Crazy mode first shipped as a side list of "crazy balls" with reduced physics (walls, bumpers, flippers only) next to the original primary and secondary slots. That is not what was wanted: a ball in the machine is a ball, with no main/second/extra distinction. This replaces the two-slot engine with one ordered list and moves the shortcut to a single key. Ball model (src/game.rs) - Game::balls: Vec<Ball> replaces `ball`, `secondary_ball`, and `crazy_balls`. The list is never empty while the game runs; a waiting ball is simply a ball with in_launcher set. - advance_ball_slots runs one timer callback: every ball, in slot order, integrates up to `substeps` steps through the single integrator step_ball (record scan, ball-to-ball contact, rule dispatch, drain) and stops after its first collision, exactly like the original slot pass. - BallPass carries the ball's current index plus the slot number and ball count from the start of the callback. The original keeps slot two numbered 2 for the rest of a callback even after slot one drained, and type-three contact records publish that number as their owner; keeping it per pass preserves that. - Effect seven's ball goes to `spawned_balls` and joins after all slot passes, as the original publishes slot two only after slot one's pass. - lose_ball removes a drained ball while others remain (Drain event, normal scoring once one ball is left) and only the last ball takes the special respawn or ends the player's ball. remove_ball keeps the claw's captured/entry indices and the claw and lock-hole contact owners pointing at the same physical ball when an earlier slot leaves. - The claw stores a ball index instead of a Primary/Secondary enum. - Nudge, flipper kicks, multiball scoring, and the "no third ball from effect seven" guard are expressed over the list. With one or two balls the original order, random draws, and ownership rules are unchanged; all existing parity and multiball tests pass after a mechanical port (test-only shims fixed_update, advance_secondary_ball, drain, secondary/set_secondary/take_secondary keep them readable). - Crazy mode adds ten ordinary balls (capped at MAX_BALLS = 64 because contact records reserve 99 as a sentinel) and marks the player. Deliberate differences from the original (also in PARITY_AUDIT.md), mostly because there is no longer a special slot one: - A ball resting on the shooter stop returns to an empty launcher from any slot; the original deleted a slot-two ball there. If the launcher is occupied the ball leaves the table without costing a ball. - Flipper kicks, record countdowns, and target rotation keep running for balls in play while another ball waits in the launcher. - Ball-to-ball contact skips only the claw-held ball; every ball's hit is published as last_collision_id. - Stress runs with 10-64 balls showed balls trapped forever: two balls settle into one lock hole, one fills it (contact 99), and the rim then reflects the other from the inside. A filled hole's rim now only repels balls moving towards its center. Lock-hole owners of a removed ball are cleared/renumbered like the claw owner. Input (src/app.rs) - Crazy mode is the bare main-row 0 key. It overlaps no original or modern binding and has no browser default action, so the WASM build receives it. The Ctrl+Shift+0 combo and its Shift-release nudge suppression are gone. - Rendering draws every ball not held by the claw. Simulator: snapshots report `balls: [...]` instead of `ball` and `secondary_ball`; autoplay flips for any ball near a flipper. Test Plan: - cargo test (160 tests), including new ones: a_ball_moves_the_same_in_any_slot (same ball in slot one vs. behind a waiting ball follows the identical trajectory), crazy_mode_adds_ten_ordinary_balls_and_marks_the_player, the_turn_ends_only_when_the_last_ball_drains, shooter_stop_rearms_a_returning_ball_from_any_slot, crazy_mode_is_capped_and_rejected_after_the_game. - A temporary release-mode stress test (not committed) ran 12 seeded two-player games with crazy mode, autoplay flippers, and nudges to completion, and 4 seeds at the 64-ball cap for 30k frames, asserting finite state, a non-empty ball list, and valid claw indices. - Manual: during a game press 0 (also in the browser build); ten balls appear, each can be captured by the claw, lock holes, and targets, and the turn ends only after the last ball drains.
This commit is contained in:
1 parent
3a00be5661
commit
a8cc0fc5fc
7 files changed
+1043
-1084
No files matched your search
+18
-4
@@ -10,14 +10,28 @@ and this project adheres to
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Crazy mode: Ctrl+Shift+0 drops ten extra balls onto the playfield. They bounce
|
- Crazy mode: pressing the main-row `0` key during a game drops ten more balls
|
||||||
and score off walls, bumpers, and flippers and replace the main ball when it
|
onto the playfield, up to 64 balls in play. High-score entries reached with
|
||||||
drains. High-score entries of players who used it show a red `CRAZY` label in
|
crazy mode show a red `CRAZY` label in the table.
|
||||||
the table and are shared like any other score.
|
|
||||||
- The high-score service stores a crazy-mode flag per entry; it adds the column
|
- The high-score service stores a crazy-mode flag per entry; it adds the column
|
||||||
to existing databases on startup and treats submissions from older clients as
|
to existing databases on startup and treats submissions from older clients as
|
||||||
normal scores.
|
normal scores.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Replace the separate primary and secondary ball slots with one ordered list
|
||||||
|
of identical balls. Every ball runs the same physics, sensors, holes, claw,
|
||||||
|
ball-to-ball contact, and drain rules; the player's ball ends only when the
|
||||||
|
last ball drains. One- and two-ball behavior keeps the recovered slot order.
|
||||||
|
- A ball resting on the shooter stop returns to an empty launcher from any slot,
|
||||||
|
and flippers keep kicking balls in play while another ball waits there.
|
||||||
|
- The simulator trace now reports every ball in a `balls` array.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Free a ball that was settling into a lock hole when another ball filled it,
|
||||||
|
and release a lock hole whose settling ball left the table.
|
||||||
|
|
||||||
## [1.2.1] - 2026-09-01
|
## [1.2.1] - 2026-09-01
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -66,6 +66,28 @@ ledger in `../original/C_RECONSTRUCTION_FINAL_AUDIT.md`.
|
|||||||
on those hosts is delegated to the repository's three-OS CI matrix. The
|
on those hosts is delegated to the repository's three-OS CI matrix. The
|
||||||
semantic core is platform-independent and has no target-specific branches.
|
semantic core is platform-independent and has no target-specific branches.
|
||||||
|
|
||||||
|
## Modern additions and their divergences
|
||||||
|
|
||||||
|
Crazy mode (main-row `0`) adds up to 64 live balls. To make every ball behave
|
||||||
|
identically, the two original timer slots became one ordered ball list. One-
|
||||||
|
and two-ball play keeps the recovered slot order, slot-numbered contact
|
||||||
|
ownership, deferred effect-seven spawn, survivor identity, nudge draws, and
|
||||||
|
multiball scoring, and all multiball tests above still pass. These deliberate
|
||||||
|
differences follow from treating every ball alike:
|
||||||
|
|
||||||
|
- A ball resting on the shooter stop returns to the launcher from any slot (the
|
||||||
|
original deleted a slot-two ball there) unless another ball already waits.
|
||||||
|
- Flipper kicks, record countdowns, and target rotation continue for balls in
|
||||||
|
play while another ball waits in the launcher.
|
||||||
|
- Ball-to-ball contact ignores only the claw-held ball, and every ball's
|
||||||
|
collision is published as the last collision record.
|
||||||
|
- When a ball leaves, claw and lock-hole contact owners move with their ball,
|
||||||
|
and a lock hole the leaving ball was settling into is freed.
|
||||||
|
- The rim of a filled lock hole only repels balls from the outside, so a ball
|
||||||
|
that was settling into it when another ball filled it rolls back out.
|
||||||
|
- High-score entries carry a `crazy` flag that both local storage and the
|
||||||
|
shared service persist; older entries default to normal scores.
|
||||||
|
|
||||||
## Reproducible final gates
|
## Reproducible final gates
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
+6
-6
@@ -77,13 +77,13 @@ for long end-to-end validation runs.
|
|||||||
| Settings | `TDKPIN.INI` before launch | F10 opens the portable settings screen |
|
| Settings | `TDKPIN.INI` before launch | F10 opens the portable settings screen |
|
||||||
| High scores | Automatic when a player finishes | F9 opens the portable table viewer |
|
| High scores | Automatic when a player finishes | F9 opens the portable table viewer |
|
||||||
| Sound | F12 | - |
|
| Sound | F12 | - |
|
||||||
| Crazy mode: drop 10 extra balls | - | Ctrl+Shift+0 |
|
| Crazy mode: drop 10 more balls | - | `0` (main row) |
|
||||||
|
|
||||||
Crazy mode is a modern addition. Its balls bounce off walls, bumpers, and
|
Crazy mode is a modern addition. Each press drops ten balls at fixed points on
|
||||||
flippers and score through them, but skip sensors, holes, and the claw. When
|
the playfield, up to 64 balls in play. They are ordinary balls: every ball runs
|
||||||
the main ball drains, a crazy ball takes its place before the turn ends. A
|
the same physics, sensors, holes, claw, and scoring, and the player's ball ends
|
||||||
player who used crazy mode still enters the high-score table, with their entry
|
only when the last one drains. A player who used crazy mode still enters the
|
||||||
labelled `CRAZY`.
|
high-score table, with their entry labelled `CRAZY`.
|
||||||
|
|
||||||
The five original speed choices remain available in settings. Physics now uses
|
The five original speed choices remain available in settings. Physics now uses
|
||||||
the original invariant 100 Hz millipixel substep, batched as 5/4/3/2/1 steps on
|
the original invariant 100 Hz millipixel substep, batched as 5/4/3/2/1 steps on
|
||||||
|
|||||||
@@ -88,8 +88,19 @@ decoded, build-ready subset; it does not replace that evidence archive.
|
|||||||
- `app.rs`: screen flow, controls, rendering, audio dispatch, and overlays;
|
- `app.rs`: screen flow, controls, rendering, audio dispatch, and overlays;
|
||||||
- `assets.rs`: compile-time original asset embedding;
|
- `assets.rs`: compile-time original asset embedding;
|
||||||
- `game.rs`: fixed-step game state, recovered walls, rules, and scoring;
|
- `game.rs`: fixed-step game state, recovered walls, rules, and scoring;
|
||||||
- crazy mode (a modern addition): extra balls outside the original two ball
|
- ball model: `Game::balls` is one ordered list of identical balls. Each
|
||||||
slots, with static collisions, wall/bumper rules, and flipper kicks only;
|
timer callback (`advance_ball_slots`) runs every ball's substep batch in
|
||||||
|
slot order through the single integrator `step_ball`: record scan,
|
||||||
|
ball-to-ball contact, rule dispatch, and drain handling. A ball keeps the
|
||||||
|
slot number it started the callback with, which the type-three contact
|
||||||
|
records publish as their owner. With one or two balls this is the
|
||||||
|
original slot-one/slot-two timer; more balls extend the same order;
|
||||||
|
- ball lifetime: `lose_ball` takes a drained ball off the table while
|
||||||
|
others remain and ends the player's ball only for the last one;
|
||||||
|
`remove_ball` keeps the claw and contact-record owners pointing at the
|
||||||
|
same physical ball; effect seven's ball joins after all slot passes;
|
||||||
|
- crazy mode (a modern addition): `add_crazy_balls` drops ten ordinary balls
|
||||||
|
and marks the current player, whose high score is labelled;
|
||||||
- `geometry.rs`: segment/circle collision primitives;
|
- `geometry.rs`: segment/circle collision primitives;
|
||||||
- `original_physics.rs`: millipixel integration and recovered type-2 responses;
|
- `original_physics.rs`: millipixel integration and recovered type-2 responses;
|
||||||
- `real48.rs`: bit-exact Borland six-byte arithmetic, conversion, and square root;
|
- `real48.rs`: bit-exact Borland six-byte arithmetic, conversion, and square root;
|
||||||
|
|||||||
+10
-39
@@ -20,6 +20,9 @@ const WIDTH: f32 = 640.0;
|
|||||||
const HEIGHT: f32 = 460.0;
|
const HEIGHT: f32 = 460.0;
|
||||||
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 ADD_PLAYER_KEYS: [KeyCode; 3] = [KeyCode::KpAdd, KeyCode::RightBracket, KeyCode::Equal];
|
const ADD_PLAYER_KEYS: [KeyCode; 3] = [KeyCode::KpAdd, KeyCode::RightBracket, KeyCode::Equal];
|
||||||
|
/// Modern addition: drops ten crazy-mode balls. A bare main-row `0` is free
|
||||||
|
/// in the original key map and has no browser default action.
|
||||||
|
const CRAZY_MODE_KEY: KeyCode = KeyCode::Key0;
|
||||||
const TARGET_POSITIONS: [[(f32, f32); 5]; 6] = [
|
const TARGET_POSITIONS: [[(f32, f32); 5]; 6] = [
|
||||||
[
|
[
|
||||||
(9.0, 41.0),
|
(9.0, 41.0),
|
||||||
@@ -224,8 +227,6 @@ pub struct App {
|
|||||||
pending_score: u32,
|
pending_score: u32,
|
||||||
pending_crazy: bool,
|
pending_crazy: bool,
|
||||||
attract: AttractAnimation,
|
attract: AttractAnimation,
|
||||||
/// Ctrl+Shift+0 holds Shift, whose release would otherwise nudge.
|
|
||||||
crazy_shift_nudge_suppressed: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -255,7 +256,6 @@ impl App {
|
|||||||
pending_score: 0,
|
pending_score: 0,
|
||||||
pending_crazy: false,
|
pending_crazy: false,
|
||||||
attract: AttractAnimation::default(),
|
attract: AttractAnimation::default(),
|
||||||
crazy_shift_nudge_suppressed: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,21 +403,10 @@ impl App {
|
|||||||
|| is_key_down(KeyCode::Enter)
|
|| is_key_down(KeyCode::Enter)
|
||||||
|| is_key_down(KeyCode::D)
|
|| is_key_down(KeyCode::D)
|
||||||
|| is_key_down(KeyCode::Right);
|
|| is_key_down(KeyCode::Right);
|
||||||
let shift_down = is_key_down(KeyCode::LeftShift) || is_key_down(KeyCode::RightShift);
|
if is_key_pressed(CRAZY_MODE_KEY) && self.game.as_mut().is_some_and(Game::add_crazy_balls) {
|
||||||
let ctrl_down = is_key_down(KeyCode::LeftControl) || is_key_down(KeyCode::RightControl);
|
|
||||||
if ctrl_down
|
|
||||||
&& shift_down
|
|
||||||
&& is_key_pressed(KeyCode::Key0)
|
|
||||||
&& self.game.as_mut().is_some_and(Game::add_crazy_balls)
|
|
||||||
{
|
|
||||||
self.crazy_shift_nudge_suppressed = true;
|
|
||||||
self.assets.play(2007, self.sounds_enabled);
|
self.assets.play(2007, self.sounds_enabled);
|
||||||
}
|
}
|
||||||
let nudge = if self.crazy_shift_nudge_suppressed
|
let nudge = if is_key_released(KeyCode::LeftShift) {
|
||||||
&& (is_key_released(KeyCode::LeftShift) || is_key_released(KeyCode::RightShift))
|
|
||||||
{
|
|
||||||
Nudge::None
|
|
||||||
} else if is_key_released(KeyCode::LeftShift) {
|
|
||||||
Nudge::Left
|
Nudge::Left
|
||||||
} else if is_key_released(KeyCode::RightShift) || is_key_released(KeyCode::Kp3) {
|
} else if is_key_released(KeyCode::RightShift) || is_key_released(KeyCode::Kp3) {
|
||||||
Nudge::Right
|
Nudge::Right
|
||||||
@@ -426,9 +415,6 @@ impl App {
|
|||||||
} else {
|
} else {
|
||||||
Nudge::None
|
Nudge::None
|
||||||
};
|
};
|
||||||
if !shift_down {
|
|
||||||
self.crazy_shift_nudge_suppressed = false;
|
|
||||||
}
|
|
||||||
let controls = Controls {
|
let controls = Controls {
|
||||||
left_flipper,
|
left_flipper,
|
||||||
right_flipper,
|
right_flipper,
|
||||||
@@ -883,7 +869,7 @@ impl App {
|
|||||||
if game.special_hole_active() {
|
if game.special_hole_active() {
|
||||||
self.draw_active_table_region(9, 14, 17, 17);
|
self.draw_active_table_region(9, 14, 17, 17);
|
||||||
}
|
}
|
||||||
if game.ball.in_launcher {
|
if game.balls.iter().any(|ball| ball.in_launcher) {
|
||||||
draw_texture_ex(
|
draw_texture_ex(
|
||||||
&self.assets.plunger,
|
&self.assets.plunger,
|
||||||
314.0,
|
314.0,
|
||||||
@@ -925,25 +911,10 @@ impl App {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !game.primary_ball_suspended() {
|
for (index, ball) in game.balls.iter().enumerate() {
|
||||||
draw_texture(
|
if game.ball_suspended(index) {
|
||||||
&self.assets.ball,
|
continue;
|
||||||
game.ball.position.x - 8.0,
|
}
|
||||||
game.ball.position.y - 8.0,
|
|
||||||
WHITE,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(ball) = game.secondary_ball
|
|
||||||
&& !game.secondary_ball_suspended()
|
|
||||||
{
|
|
||||||
draw_texture(
|
|
||||||
&self.assets.ball,
|
|
||||||
ball.position.x - 8.0,
|
|
||||||
ball.position.y - 8.0,
|
|
||||||
WHITE,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for ball in &game.crazy_balls {
|
|
||||||
draw_texture(
|
draw_texture(
|
||||||
&self.assets.ball,
|
&self.assets.ball,
|
||||||
ball.position.x - 8.0,
|
ball.position.x - 8.0,
|
||||||
|
|||||||
+941
-998
@@ -130,32 +130,18 @@ pub enum ClawSpriteBank {
|
|||||||
Opening,
|
Opening,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
enum ClawBallSlot {
|
|
||||||
Primary,
|
|
||||||
Secondary,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClawBallSlot {
|
|
||||||
const fn from_ball_number(ball_number: u16) -> Self {
|
|
||||||
if ball_number == 2 {
|
|
||||||
Self::Secondary
|
|
||||||
} else {
|
|
||||||
Self::Primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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>,
|
/// Index into `Game::balls` of the captured ball. Kept in step with the
|
||||||
|
/// list when an earlier ball leaves the table.
|
||||||
|
captured_slot: Option<usize>,
|
||||||
// The deep zone is less than 16 px wide while ball contact is 17 px, so
|
// The deep zone is less than 16 px wide while ball contact is 17 px, so
|
||||||
// only one physically non-overlapping ball can own a pending entry.
|
// only one physically non-overlapping ball can own a pending entry.
|
||||||
capture_entry: Option<(ClawBallSlot, bool)>,
|
capture_entry: Option<(usize, bool)>,
|
||||||
target_frame: u8,
|
target_frame: u8,
|
||||||
frame_accumulator: f32,
|
frame_accumulator: f32,
|
||||||
}
|
}
|
||||||
@@ -304,6 +290,29 @@ enum BallAction {
|
|||||||
Finish,
|
Finish,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Identity of one ball for the duration of its slot pass in a timer callback.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct BallPass {
|
||||||
|
/// Current position of the ball in `Game::balls`.
|
||||||
|
index: usize,
|
||||||
|
/// One-based slot number the ball held when the callback started. The
|
||||||
|
/// original publishes it as the owner in type-three contact records.
|
||||||
|
number: u16,
|
||||||
|
/// Number of live balls when the callback started.
|
||||||
|
count: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of one physics substep for one ball.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum BallStep {
|
||||||
|
/// Keep integrating this ball in the current batch.
|
||||||
|
Continue,
|
||||||
|
/// The ball is still at its index but its batch ends here.
|
||||||
|
Stop,
|
||||||
|
/// The ball left `Game::balls`; later balls moved down one index.
|
||||||
|
Removed,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
struct SensorScanResult {
|
struct SensorScanResult {
|
||||||
action: BallAction,
|
action: BallAction,
|
||||||
@@ -382,8 +391,12 @@ impl Default for Ball {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop points for the Ctrl+Shift+0 crazy mode, spread over open playfield
|
/// Upper bound on balls in play. Type-three contact records store a ball's
|
||||||
/// between the bumpers, slingshots, and lanes.
|
/// one-based slot number and reserve 99 as a sentinel, so slots stay below it.
|
||||||
|
pub const MAX_BALLS: usize = 64;
|
||||||
|
|
||||||
|
/// Drop points for crazy mode, spread over open playfield between the
|
||||||
|
/// bumpers, slingshots, and lanes.
|
||||||
pub const CRAZY_BALL_POSITIONS: [Vec2; 10] = [
|
pub const CRAZY_BALL_POSITIONS: [Vec2; 10] = [
|
||||||
vec2(170.0, 112.0),
|
vec2(170.0, 112.0),
|
||||||
vec2(250.0, 140.0),
|
vec2(250.0, 140.0),
|
||||||
@@ -401,12 +414,15 @@ pub const CRAZY_BALL_POSITIONS: [Vec2; 10] = [
|
|||||||
pub struct Game {
|
pub struct Game {
|
||||||
pub players: Vec<Player>,
|
pub players: Vec<Player>,
|
||||||
pub current_player: usize,
|
pub current_player: usize,
|
||||||
pub ball: Ball,
|
/// Every ball in the machine, in timer slot order. All balls run the
|
||||||
pub secondary_ball: Option<Ball>,
|
/// same physics and rules; the order only decides who moves first in a
|
||||||
/// Extra balls added by crazy mode. They are not part of the original
|
/// callback and which slot number owns a contact record. The list holds
|
||||||
/// two-slot engine: they collide with static records, score through the
|
/// at least one ball (possibly waiting in the launcher) until the game
|
||||||
/// wall and bumper rules, and replace the primary ball when it drains.
|
/// finishes.
|
||||||
pub crazy_balls: Vec<Ball>,
|
pub balls: Vec<Ball>,
|
||||||
|
/// Balls created during the current callback. The original publishes
|
||||||
|
/// effect seven's ball only after every slot pass has returned.
|
||||||
|
spawned_balls: Vec<Ball>,
|
||||||
pub wheel_holes: [bool; 5],
|
pub wheel_holes: [bool; 5],
|
||||||
pub top_targets: [bool; 3],
|
pub top_targets: [bool; 3],
|
||||||
pub tilted: bool,
|
pub tilted: bool,
|
||||||
@@ -450,9 +466,8 @@ impl Game {
|
|||||||
Self {
|
Self {
|
||||||
players: vec![Player::default(); player_count.clamp(1, 4)],
|
players: vec![Player::default(); player_count.clamp(1, 4)],
|
||||||
current_player: 0,
|
current_player: 0,
|
||||||
ball: Ball::default(),
|
balls: vec![Ball::default()],
|
||||||
secondary_ball: None,
|
spawned_balls: Vec::new(),
|
||||||
crazy_balls: Vec::new(),
|
|
||||||
wheel_holes: [false; 5],
|
wheel_holes: [false; 5],
|
||||||
top_targets: [false; 3],
|
top_targets: [false; 3],
|
||||||
tilted: false,
|
tilted: false,
|
||||||
@@ -521,27 +536,43 @@ impl Game {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crazy mode: drop ten extra balls onto the playfield.
|
/// Crazy mode: drop ten more balls onto the playfield. They are ordinary
|
||||||
|
/// balls in every respect; the current player's score is marked as a
|
||||||
|
/// crazy-mode score. Returns false when nothing was added.
|
||||||
pub fn add_crazy_balls(&mut self) -> bool {
|
pub fn add_crazy_balls(&mut self) -> bool {
|
||||||
if self.finished {
|
if self.finished || self.balls.len() >= MAX_BALLS {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.players[self.current_player].crazy_mode = CrazyMode::Used;
|
self.players[self.current_player].crazy_mode = CrazyMode::Used;
|
||||||
self.crazy_balls
|
let room = MAX_BALLS - self.balls.len();
|
||||||
.extend(CRAZY_BALL_POSITIONS.into_iter().map(|position| Ball {
|
self.balls.extend(
|
||||||
position,
|
CRAZY_BALL_POSITIONS
|
||||||
velocity: Vec2::ZERO,
|
.into_iter()
|
||||||
in_launcher: false,
|
.take(room)
|
||||||
..Ball::default()
|
.map(|position| Ball {
|
||||||
}));
|
position,
|
||||||
|
velocity: Vec2::ZERO,
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the claw currently holds the ball at `index` off the table.
|
||||||
|
pub fn ball_suspended(&self, index: usize) -> bool {
|
||||||
|
self.claw_holds(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn launcher_ball(&self) -> Option<usize> {
|
||||||
|
self.balls.iter().position(|ball| ball.in_launcher)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(crate) fn begin_claw_scenario(&mut self, terminal_frame: u8) -> Vec<Event> {
|
pub(crate) fn begin_claw_scenario(&mut self, terminal_frame: u8) -> Vec<Event> {
|
||||||
self.ball.in_launcher = false;
|
self.balls[0].in_launcher = false;
|
||||||
self.ball.position = CLAW_TRIGGER_CENTER;
|
self.balls[0].position = CLAW_TRIGGER_CENTER;
|
||||||
self.ball.velocity = Vec2::ZERO;
|
self.balls[0].velocity = Vec2::ZERO;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
self.begin_claw_capture(terminal_frame, &mut events);
|
self.begin_claw_capture(terminal_frame, &mut events);
|
||||||
events
|
events
|
||||||
@@ -559,8 +590,8 @@ impl Game {
|
|||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(crate) fn begin_target_scenario(&mut self) {
|
pub(crate) fn begin_target_scenario(&mut self) {
|
||||||
self.ball.in_launcher = false;
|
self.balls[0].in_launcher = false;
|
||||||
self.ball.position = vec2(170.0, 230.0);
|
self.balls[0].position = vec2(170.0, 230.0);
|
||||||
self.players[0].bumper_value = 6_000;
|
self.players[0].bumper_value = 6_000;
|
||||||
self.wheel_holes = [true, false, true, false, false];
|
self.wheel_holes = [true, false, true, false, false];
|
||||||
self.target_rotation_state = Some(0);
|
self.target_rotation_state = Some(0);
|
||||||
@@ -568,7 +599,7 @@ impl Game {
|
|||||||
self.object_active[109] = false;
|
self.object_active[109] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fire_launcher(&mut self) {
|
fn fire_launcher(&mut self, index: usize) {
|
||||||
self.launcher_velocity_milli = self
|
self.launcher_velocity_milli = self
|
||||||
.launcher_velocity_milli
|
.launcher_velocity_milli
|
||||||
.wrapping_sub(LAUNCHER_RELEASE_IMPULSE_MILLI);
|
.wrapping_sub(LAUNCHER_RELEASE_IMPULSE_MILLI);
|
||||||
@@ -580,8 +611,9 @@ impl Game {
|
|||||||
launch_velocity =
|
launch_velocity =
|
||||||
launch_velocity.wrapping_sub(i32::from(self.random.below(3_800) / 40));
|
launch_velocity.wrapping_sub(i32::from(self.random.below(3_800) / 40));
|
||||||
}
|
}
|
||||||
self.ball.in_launcher = false;
|
let ball = &mut self.balls[index];
|
||||||
self.ball.velocity = MilliVec {
|
ball.in_launcher = false;
|
||||||
|
ball.velocity = MilliVec {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: launch_velocity,
|
y: launch_velocity,
|
||||||
}
|
}
|
||||||
@@ -619,7 +651,7 @@ impl Game {
|
|||||||
// The original key handlers keep accepting the launcher key while
|
// The original key handlers keep accepting the launcher key while
|
||||||
// tilted; Tilt disables flippers and scoring, but must not strand a
|
// tilted; Tilt disables flippers and scoring, but must not strand a
|
||||||
// ball that is still waiting in the shooter lane.
|
// ball that is still waiting in the shooter lane.
|
||||||
if self.ball.in_launcher {
|
if let Some(launcher_ball) = self.launcher_ball() {
|
||||||
if controls.launch_down {
|
if controls.launch_down {
|
||||||
if self.launcher_was_down {
|
if self.launcher_was_down {
|
||||||
self.launcher_hold_seconds += frame_time.min(0.05);
|
self.launcher_hold_seconds += frame_time.min(0.05);
|
||||||
@@ -640,7 +672,7 @@ impl Game {
|
|||||||
self.launcher_charge =
|
self.launcher_charge =
|
||||||
f32::from(i16::try_from(charge_milli).unwrap_or(3_800)) / f32::from(3_800_i16);
|
f32::from(i16::try_from(charge_milli).unwrap_or(3_800)) / f32::from(3_800_i16);
|
||||||
} else if self.launcher_was_down {
|
} else if self.launcher_was_down {
|
||||||
self.fire_launcher();
|
self.fire_launcher(launcher_ball);
|
||||||
events.push(Event::Launch);
|
events.push(Event::Launch);
|
||||||
if !self.tilted {
|
if !self.tilted {
|
||||||
events.push(Event::Sound(2002));
|
events.push(Event::Sound(2002));
|
||||||
@@ -663,75 +695,18 @@ impl Game {
|
|||||||
|
|
||||||
fn timer_tick(&mut self, elapsed: f32, substeps: u8, events: &mut Vec<Event>) {
|
fn timer_tick(&mut self, elapsed: f32, substeps: u8, events: &mut Vec<Event>) {
|
||||||
self.update_claw(elapsed, events);
|
self.update_claw(elapsed, events);
|
||||||
if !self.ball.in_launcher {
|
if self.balls.iter().any(|ball| !ball.in_launcher) {
|
||||||
self.update_record_countdowns();
|
self.update_record_countdowns();
|
||||||
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);
|
||||||
// A held claw ball still counts as the second live ball until the
|
// A held claw ball stays in the list, so it still counts as a live
|
||||||
// other slot disappears or the claw releases it.
|
// ball until the claw releases it or the others drain.
|
||||||
if self.secondary_ball.is_none()
|
if self.balls.len() < 2 {
|
||||||
&& self.claw.captured_slot != Some(ClawBallSlot::Secondary)
|
|
||||||
&& self.score_mode == ScoreMode::Multiball
|
|
||||||
{
|
|
||||||
self.score_mode = ScoreMode::Normal;
|
self.score_mode = ScoreMode::Normal;
|
||||||
}
|
}
|
||||||
if !panel_active {
|
if !panel_active {
|
||||||
let had_secondary_ball = self.secondary_ball.is_some();
|
self.advance_ball_slots(substeps, events);
|
||||||
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 {
|
|
||||||
self.score_mode = ScoreMode::Multiball;
|
|
||||||
}
|
|
||||||
if !primary_held {
|
|
||||||
for _ in 0..substeps {
|
|
||||||
let stop = self.fixed_update(events);
|
|
||||||
// Effect seven publishes its spawn request during record 149,
|
|
||||||
// but the original timer does not create slot two until the
|
|
||||||
// complete slot-one simulation pass has returned.
|
|
||||||
if !had_secondary_ball && spawned_secondary.is_none() {
|
|
||||||
spawned_secondary = self.secondary_ball.take();
|
|
||||||
}
|
|
||||||
if stop || self.finished || self.claw_holds_slot(ClawBallSlot::Primary) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if had_secondary_ball && !self.finished {
|
|
||||||
if self.secondary_ball.is_some() && !self.claw_holds_slot(ClawBallSlot::Secondary) {
|
|
||||||
for _ in 0..substeps {
|
|
||||||
if self.advance_secondary_ball_slot(events, !primary_held) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if !self.claw_holds_slot(ClawBallSlot::Primary)
|
|
||||||
&& !self.claw_holds_slot(ClawBallSlot::Secondary)
|
|
||||||
{
|
|
||||||
self.secondary_ball = Some(self.ball);
|
|
||||||
for _ in 0..substeps {
|
|
||||||
if self.advance_secondary_ball_slot(events, false) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(survivor) = self.secondary_ball.take() {
|
|
||||||
self.ball = survivor;
|
|
||||||
} else {
|
|
||||||
self.drain(events);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(spawned) = spawned_secondary {
|
|
||||||
debug_assert!(self.secondary_ball.is_none());
|
|
||||||
self.secondary_ball = Some(spawned);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The two slots are mutually exclusive while the claw is holding
|
|
||||||
// a ball.
|
|
||||||
debug_assert!(!(primary_held && secondary_held));
|
|
||||||
if !self.finished {
|
|
||||||
self.advance_crazy_balls(substeps, events);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pending_flipper_edges[0] =
|
self.pending_flipper_edges[0] =
|
||||||
@@ -759,6 +734,54 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs one timer callback's physics batch for every ball, in slot
|
||||||
|
/// order. Each ball integrates up to `substeps` steps and stops after its
|
||||||
|
/// first collision, exactly like the original single-slot integrator.
|
||||||
|
fn advance_ball_slots(&mut self, substeps: u8, events: &mut Vec<Event>) {
|
||||||
|
let ball_count = self.balls.len();
|
||||||
|
if ball_count > 1 {
|
||||||
|
self.score_mode = ScoreMode::Multiball;
|
||||||
|
}
|
||||||
|
let count = u16::try_from(ball_count).unwrap_or(u16::MAX);
|
||||||
|
let mut index = 0;
|
||||||
|
for number in 1..=count {
|
||||||
|
if self.finished || index >= self.balls.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if self.claw_holds(index) {
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// A ball keeps the slot number it started the callback with,
|
||||||
|
// even when an earlier slot drained during this callback.
|
||||||
|
let pass = BallPass {
|
||||||
|
index,
|
||||||
|
number,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
let mut removed = false;
|
||||||
|
for _ in 0..substeps {
|
||||||
|
match self.step_ball(pass, events) {
|
||||||
|
BallStep::Continue => {}
|
||||||
|
BallStep::Stop => break,
|
||||||
|
BallStep::Removed => {
|
||||||
|
removed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.finished || self.claw_holds(index) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !removed {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Effect seven publishes its spawn request during a slot pass, but
|
||||||
|
// the new ball only joins once every slot pass has returned.
|
||||||
|
self.balls.append(&mut self.spawned_balls);
|
||||||
|
}
|
||||||
|
|
||||||
fn update_record_countdowns(&mut self) {
|
fn update_record_countdowns(&mut self) {
|
||||||
for countdown in &mut self.record_countdowns[1..] {
|
for countdown in &mut self.record_countdowns[1..] {
|
||||||
if *countdown != 0 {
|
if *countdown != 0 {
|
||||||
@@ -823,7 +846,7 @@ impl Game {
|
|||||||
fn apply_nudge(&mut self, nudge: Nudge, events: &mut Vec<Event>) {
|
fn apply_nudge(&mut self, nudge: Nudge, events: &mut Vec<Event>) {
|
||||||
const SCALAR: i32 = 15;
|
const SCALAR: i32 = 15;
|
||||||
events.push(Event::Sound(2019));
|
events.push(Event::Sound(2019));
|
||||||
let mut velocity = MilliVec::from_velocity_per_second(self.ball.velocity);
|
let mut velocity = MilliVec::from_velocity_per_second(self.balls[0].velocity);
|
||||||
match nudge {
|
match nudge {
|
||||||
Nudge::Left | Nudge::Right => {
|
Nudge::Left | Nudge::Right => {
|
||||||
let amount = (50 - i32::from(self.random.below(20))) * SCALAR;
|
let amount = (50 - i32::from(self.random.below(20))) * SCALAR;
|
||||||
@@ -849,19 +872,8 @@ impl Game {
|
|||||||
}
|
}
|
||||||
Nudge::None => return,
|
Nudge::None => return,
|
||||||
}
|
}
|
||||||
let primary_velocity = MilliVec::from_velocity_per_second(self.ball.velocity);
|
let held = self.held_ball();
|
||||||
let impulse = MilliVec {
|
if self.balls.len() > 1 {
|
||||||
x: velocity.x.wrapping_sub(primary_velocity.x),
|
|
||||||
y: velocity.y.wrapping_sub(primary_velocity.y),
|
|
||||||
};
|
|
||||||
for ball in &mut self.crazy_balls {
|
|
||||||
ball.velocity = MilliVec::from_velocity_per_second(ball.velocity)
|
|
||||||
.add(impulse)
|
|
||||||
.to_velocity_per_second();
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
let slot_impulse = match nudge {
|
let slot_impulse = match nudge {
|
||||||
Nudge::Left | Nudge::Right => MilliVec {
|
Nudge::Left | Nudge::Right => MilliVec {
|
||||||
x: if nudge == Nudge::Left { -600 } else { 600 },
|
x: if nudge == Nudge::Left { -600 } else { 600 },
|
||||||
@@ -877,20 +889,17 @@ impl Game {
|
|||||||
}
|
}
|
||||||
Nudge::None => MilliVec::default(),
|
Nudge::None => MilliVec::default(),
|
||||||
};
|
};
|
||||||
let mut primary_slot = MilliVec::from_velocity_per_second(self.ball.velocity);
|
for (index, ball) in self.balls.iter_mut().enumerate() {
|
||||||
primary_slot.x = primary_slot.x.wrapping_add(slot_impulse.x);
|
if held == Some(index) {
|
||||||
primary_slot.y = primary_slot.y.wrapping_add(slot_impulse.y);
|
continue;
|
||||||
if !primary_held {
|
}
|
||||||
self.ball.velocity = primary_slot.to_velocity_per_second();
|
let mut slot = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
|
slot.x = slot.x.wrapping_add(slot_impulse.x);
|
||||||
|
slot.y = slot.y.wrapping_add(slot_impulse.y);
|
||||||
|
ball.velocity = slot.to_velocity_per_second();
|
||||||
}
|
}
|
||||||
let mut secondary_slot = MilliVec::from_velocity_per_second(secondary.velocity);
|
} else if held != Some(0) {
|
||||||
secondary_slot.x = secondary_slot.x.wrapping_add(slot_impulse.x);
|
self.balls[0].velocity = velocity.to_velocity_per_second();
|
||||||
secondary_slot.y = secondary_slot.y.wrapping_add(slot_impulse.y);
|
|
||||||
if !secondary_held {
|
|
||||||
secondary.velocity = secondary_slot.to_velocity_per_second();
|
|
||||||
}
|
|
||||||
} else if !primary_held {
|
|
||||||
self.ball.velocity = velocity.to_velocity_per_second();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tilt_counter = self.tilt_counter.wrapping_add(25);
|
self.tilt_counter = self.tilt_counter.wrapping_add(25);
|
||||||
@@ -908,70 +917,76 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Integrates one physics substep for the ball described by `pass`.
|
||||||
|
///
|
||||||
|
/// This is the single per-ball integrator: every ball, whichever slot it
|
||||||
|
/// occupies, runs the same record scan, ball-to-ball contact, rule
|
||||||
|
/// dispatch, and drain handling.
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn fixed_update(&mut self, events: &mut Vec<Event>) -> bool {
|
fn step_ball(&mut self, pass: BallPass, events: &mut Vec<Event>) -> BallStep {
|
||||||
if self.ball.in_launcher {
|
let index = pass.index;
|
||||||
self.ball.position = LAUNCHER_POSITION;
|
let mut ball = self.balls[index];
|
||||||
self.ball.velocity = Vec2::ZERO;
|
if ball.in_launcher {
|
||||||
return true;
|
ball.position = LAUNCHER_POSITION;
|
||||||
|
ball.velocity = Vec2::ZERO;
|
||||||
|
self.balls[index] = ball;
|
||||||
|
return BallStep::Stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
let old_position = MilliVec::from_position(self.ball.position);
|
let old_position = MilliVec::from_position(ball.position);
|
||||||
let initial_secondary = self.secondary_ball;
|
|
||||||
let ball_count = if initial_secondary.is_some() { 2 } else { 1 };
|
|
||||||
let mut ball = self.ball;
|
|
||||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
velocity.y += GRAVITY_MILLI_PER_STEP;
|
velocity.y += GRAVITY_MILLI_PER_STEP;
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||||
let speed_before_collision = milli_distance(velocity);
|
let speed_before_collision = milli_distance(velocity);
|
||||||
if predicted_outside_board(old_position.add(velocity)) {
|
if predicted_outside_board(old_position.add(velocity)) {
|
||||||
self.drain(events);
|
return self.lose_ball(index, events);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
let (best_static, scan, predicted) = self.scan_ordered_records_for_ball(
|
let (best_static, scan, predicted) = self.scan_ordered_records_for_ball(
|
||||||
&mut ball,
|
&mut ball,
|
||||||
1,
|
pass,
|
||||||
ball_count,
|
|
||||||
old_position,
|
old_position,
|
||||||
&mut velocity,
|
&mut velocity,
|
||||||
events,
|
events,
|
||||||
);
|
);
|
||||||
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);
|
// Ball-to-ball contact only competes when no static record was hit.
|
||||||
let mut transferred_secondary_velocity = None;
|
// Objects 174 and 175 are the original's moving circle records for
|
||||||
if let Some(secondary) = initial_secondary
|
// slot one and every later slot.
|
||||||
&& !primary_held
|
let mut transferred_velocity = None;
|
||||||
&& !secondary_held
|
if best_collision.is_none() {
|
||||||
{
|
for (other_index, other) in self.balls.iter().enumerate() {
|
||||||
let other_position = MilliVec::from_position(secondary.position);
|
if other_index == index || self.claw_holds(other_index) {
|
||||||
let response = ball_collision_response(
|
continue;
|
||||||
old_position,
|
}
|
||||||
velocity,
|
let other_position = MilliVec::from_position(other.position);
|
||||||
ball.spin,
|
if predicted.x >= other_position.x.wrapping_sub(21_000)
|
||||||
other_position,
|
&& predicted.x <= other_position.x.wrapping_add(21_000)
|
||||||
MilliVec::from_velocity_per_second(secondary.velocity),
|
&& predicted.y >= other_position.y.wrapping_sub(21_000)
|
||||||
secondary.capture_age <= 0,
|
&& predicted.y <= other_position.y.wrapping_add(21_000)
|
||||||
);
|
&& let Some(response) = ball_collision_response(
|
||||||
if predicted.x >= other_position.x.wrapping_sub(21_000)
|
old_position,
|
||||||
&& predicted.x <= other_position.x.wrapping_add(21_000)
|
velocity,
|
||||||
&& predicted.y >= other_position.y.wrapping_sub(21_000)
|
ball.spin,
|
||||||
&& predicted.y <= other_position.y.wrapping_add(21_000)
|
other_position,
|
||||||
&& let Some(response) = response
|
MilliVec::from_velocity_per_second(other.velocity),
|
||||||
&& best_collision.is_none()
|
other.capture_age <= 0,
|
||||||
{
|
)
|
||||||
transferred_secondary_velocity = Some(response.other_velocity);
|
{
|
||||||
best_collision = Some((
|
transferred_velocity = Some((other_index, response.other_velocity));
|
||||||
175,
|
best_collision = Some((
|
||||||
false,
|
if other_index == 0 { 174 } else { 175 },
|
||||||
CollisionResponse {
|
false,
|
||||||
surface_distance: response.surface_distance,
|
CollisionResponse {
|
||||||
velocity: response.moving_velocity,
|
surface_distance: response.surface_distance,
|
||||||
spin: response.moving_spin,
|
velocity: response.moving_velocity,
|
||||||
auxiliary_fired: false,
|
spin: response.moving_spin,
|
||||||
},
|
auxiliary_fired: false,
|
||||||
));
|
},
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let collided = best_collision.is_some();
|
let collided = best_collision.is_some();
|
||||||
@@ -979,90 +994,93 @@ impl Game {
|
|||||||
ball.capture_age = 0;
|
ball.capture_age = 0;
|
||||||
}
|
}
|
||||||
let mut auxiliary_fired = false;
|
let mut auxiliary_fired = false;
|
||||||
let (hit_wall, hit_circle) = if let Some((object_id, is_wall, response)) = best_collision {
|
let mut hit = None;
|
||||||
|
if let Some((object_id, is_wall, response)) = best_collision {
|
||||||
velocity = response.velocity;
|
velocity = response.velocity;
|
||||||
ball.spin = response.spin;
|
ball.spin = response.spin;
|
||||||
auxiliary_fired = response.auxiliary_fired;
|
auxiliary_fired = response.auxiliary_fired;
|
||||||
self.last_collision_id = Some(object_id);
|
self.last_collision_id = Some(object_id);
|
||||||
if object_id == 175
|
hit = Some((object_id, is_wall));
|
||||||
&& let (Some(secondary), Some(transferred)) =
|
|
||||||
(&mut self.secondary_ball, transferred_secondary_velocity)
|
|
||||||
&& !secondary_held
|
|
||||||
&& !primary_held
|
|
||||||
{
|
|
||||||
secondary.velocity = transferred.to_velocity_per_second();
|
|
||||||
}
|
|
||||||
if is_wall {
|
|
||||||
(Some(object_id), None)
|
|
||||||
} else {
|
|
||||||
(None, Some(object_id))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
if hit_wall == Some(25) && speed_before_collision >= 1_000 {
|
|
||||||
velocity.x = 0;
|
|
||||||
}
|
}
|
||||||
if hit_wall == Some(25) && speed_before_collision < 1_000 {
|
if let Some((other_index, transferred)) = transferred_velocity {
|
||||||
|
self.balls[other_index].velocity = transferred.to_velocity_per_second();
|
||||||
|
}
|
||||||
|
if hit == Some((25, true)) {
|
||||||
velocity.x = 0;
|
velocity.x = 0;
|
||||||
velocity.y = 0;
|
if speed_before_collision < 1_000 {
|
||||||
|
velocity.y = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||||
ball.position = old_position.add(velocity).to_position();
|
ball.position = old_position.add(velocity).to_position();
|
||||||
ball.velocity = velocity.to_velocity_per_second();
|
ball.velocity = velocity.to_velocity_per_second();
|
||||||
self.ball = ball;
|
self.balls[index] = ball;
|
||||||
|
|
||||||
if let Some(wall_id) = hit_wall {
|
match hit {
|
||||||
if wall_id == 2 {
|
Some((2, true)) => {
|
||||||
let special_respawn = self.special_respawn_pending();
|
let special_respawn = self.special_respawn_pending();
|
||||||
let response_spin = self.ball.spin;
|
let response_spin = ball.spin;
|
||||||
self.drain(events);
|
let step = self.lose_ball(index, events);
|
||||||
if special_respawn {
|
if special_respawn {
|
||||||
let origin = MilliVec::from_position(self.ball.position);
|
let respawned = &mut self.balls[index];
|
||||||
self.ball.position = origin.add(velocity).to_position();
|
let origin = MilliVec::from_position(respawned.position);
|
||||||
self.ball.velocity = velocity.to_velocity_per_second();
|
respawned.position = origin.add(velocity).to_position();
|
||||||
self.ball.spin = response_spin;
|
respawned.velocity = velocity.to_velocity_per_second();
|
||||||
|
respawned.spin = response_spin;
|
||||||
}
|
}
|
||||||
return true;
|
return step;
|
||||||
}
|
}
|
||||||
if wall_id == 25 && speed_before_collision < 1_000 {
|
Some((25, true)) if speed_before_collision < 1_000 => {
|
||||||
self.ball = Ball::default();
|
return self.return_ball_to_launcher(index);
|
||||||
self.launcher_charge = 0.0;
|
|
||||||
self.launcher_was_down = false;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
self.apply_wall_rule(wall_id, events);
|
Some((wall_id, true)) => {
|
||||||
if auxiliary_fired && matches!(wall_id, 55 | 74 | 107) {
|
self.apply_wall_rule(wall_id, events);
|
||||||
events.push(Event::Sound(2019));
|
if auxiliary_fired && matches!(wall_id, 55 | 74 | 107) {
|
||||||
|
events.push(Event::Sound(2019));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Some((circle_id, false)) => self.apply_bumper_rule(circle_id, auxiliary_fired, events),
|
||||||
|
None => {}
|
||||||
if let Some(circle_id) = hit_circle {
|
|
||||||
self.apply_bumper_rule(circle_id, auxiliary_fired, events);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match scan.action {
|
match scan.action {
|
||||||
BallAction::Keep => {}
|
BallAction::Keep => {}
|
||||||
BallAction::Suspend => return true,
|
BallAction::Suspend => return BallStep::Stop,
|
||||||
BallAction::Reset => {
|
BallAction::Reset => {
|
||||||
self.reset_ball_to_launcher();
|
self.reset_ball_to_launcher(index);
|
||||||
return true;
|
return BallStep::Stop;
|
||||||
}
|
|
||||||
BallAction::Finish => {
|
|
||||||
self.drain(events);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
BallAction::Finish => return self.lose_ball(index, events),
|
||||||
BallAction::Remove => {
|
BallAction::Remove => {
|
||||||
self.ball = self
|
// A capture during multiball takes the ball off the table
|
||||||
.secondary_ball
|
// without a drain. If the other balls already left during
|
||||||
.take()
|
// this callback, it was the last ball after all.
|
||||||
.expect("a multiball capture must leave the other slot active");
|
if self.balls.len() == 1 {
|
||||||
self.promote_claw_secondary_to_primary();
|
return self.lose_ball(index, events);
|
||||||
return true;
|
}
|
||||||
|
self.remove_ball(index);
|
||||||
|
return BallStep::Removed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
collided
|
if collided {
|
||||||
|
BallStep::Stop
|
||||||
|
} else {
|
||||||
|
BallStep::Continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test shim: one substep of slot one, as the original slot-one
|
||||||
|
/// integrator. Returns whether its batch would stop.
|
||||||
|
#[cfg(test)]
|
||||||
|
fn fixed_update(&mut self, events: &mut Vec<Event>) -> bool {
|
||||||
|
let count = u16::try_from(self.balls.len()).unwrap_or(u16::MAX);
|
||||||
|
let pass = BallPass {
|
||||||
|
index: 0,
|
||||||
|
number: 1,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
self.step_ball(pass, events) != BallStep::Continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fn retain_static_range(
|
fn retain_static_range(
|
||||||
@@ -1083,12 +1101,11 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn scan_ordered_records_for_ball(
|
fn scan_ordered_records_for_ball(
|
||||||
&mut self,
|
&mut self,
|
||||||
ball: &mut Ball,
|
ball: &mut Ball,
|
||||||
ball_number: u16,
|
pass: BallPass,
|
||||||
ball_count: u16,
|
|
||||||
old_position: MilliVec,
|
old_position: MilliVec,
|
||||||
velocity: &mut MilliVec,
|
velocity: &mut MilliVec,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
@@ -1112,8 +1129,7 @@ impl Game {
|
|||||||
ball.velocity = velocity.to_velocity_per_second();
|
ball.velocity = velocity.to_velocity_per_second();
|
||||||
match self.capture_record_step(
|
match self.capture_record_step(
|
||||||
ball,
|
ball,
|
||||||
ball_number,
|
pass,
|
||||||
ball_count,
|
|
||||||
89,
|
89,
|
||||||
CLAW_TRIGGER_CENTER,
|
CLAW_TRIGGER_CENTER,
|
||||||
CLAW_TRIGGER_RADIUS,
|
CLAW_TRIGGER_RADIUS,
|
||||||
@@ -1123,9 +1139,9 @@ impl Game {
|
|||||||
events,
|
events,
|
||||||
) {
|
) {
|
||||||
CaptureStep::Complete => {
|
CaptureStep::Complete => {
|
||||||
let terminal_frame = self
|
let terminal_frame =
|
||||||
.next_claw_terminal_frame(self.claw_capture_restricted_for(ball_number));
|
self.next_claw_terminal_frame(self.claw_capture_restricted_for(pass.index));
|
||||||
self.begin_claw_capture_after_hold(ball, ball_number, terminal_frame, events);
|
self.begin_claw_capture_after_hold(ball, pass.index, terminal_frame, events);
|
||||||
action = BallAction::Suspend;
|
action = BallAction::Suspend;
|
||||||
}
|
}
|
||||||
CaptureStep::Outside | CaptureStep::Holding => {}
|
CaptureStep::Outside | CaptureStep::Holding => {}
|
||||||
@@ -1138,8 +1154,7 @@ impl Game {
|
|||||||
ball.velocity = velocity.to_velocity_per_second();
|
ball.velocity = velocity.to_velocity_per_second();
|
||||||
if let Some(completed_action) = self.check_lock_holes(
|
if let Some(completed_action) = self.check_lock_holes(
|
||||||
ball,
|
ball,
|
||||||
ball_number,
|
pass,
|
||||||
ball_count,
|
|
||||||
old_position,
|
old_position,
|
||||||
predicted,
|
predicted,
|
||||||
&mut capture_candidate,
|
&mut capture_candidate,
|
||||||
@@ -1169,8 +1184,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
if let Some(completed_action) = self.check_special_hole(
|
if let Some(completed_action) = self.check_special_hole(
|
||||||
ball,
|
ball,
|
||||||
ball_number,
|
pass,
|
||||||
ball_count,
|
|
||||||
old_position,
|
old_position,
|
||||||
predicted,
|
predicted,
|
||||||
&mut capture_candidate,
|
&mut capture_candidate,
|
||||||
@@ -1181,7 +1195,7 @@ impl Game {
|
|||||||
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
let effect_triggered = self.check_effect_sensor(
|
let effect_triggered = self.check_effect_sensor(
|
||||||
ball,
|
ball,
|
||||||
ball_number,
|
pass.number,
|
||||||
old_position,
|
old_position,
|
||||||
movement_from_prediction(old_position, predicted),
|
movement_from_prediction(old_position, predicted),
|
||||||
events,
|
events,
|
||||||
@@ -1341,192 +1355,18 @@ impl Game {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test shim: one substep of slot two during a two-ball callback.
|
||||||
#[cfg(test)]
|
#[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, !self.claw_holds_slot(ClawBallSlot::Primary))
|
if self.balls.len() < 2 {
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn advance_secondary_ball_slot(
|
|
||||||
&mut self,
|
|
||||||
events: &mut Vec<Event>,
|
|
||||||
primary_slot_active: bool,
|
|
||||||
) -> bool {
|
|
||||||
let Some(mut ball) = self.secondary_ball.take() else {
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
let pass = BallPass {
|
||||||
|
index: 1,
|
||||||
|
number: 2,
|
||||||
|
count: 2,
|
||||||
};
|
};
|
||||||
let primary_position = self.ball.position;
|
self.step_ball(pass, events) != BallStep::Continue
|
||||||
let primary_velocity = self.ball.velocity;
|
|
||||||
let primary_capture_age = self.ball.capture_age;
|
|
||||||
let old_position = MilliVec::from_position(ball.position);
|
|
||||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
|
||||||
velocity.y += GRAVITY_MILLI_PER_STEP;
|
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
|
||||||
let speed_before_collision = milli_distance(velocity);
|
|
||||||
if predicted_outside_board(old_position.add(velocity)) {
|
|
||||||
self.score_mode = ScoreMode::Normal;
|
|
||||||
events.push(Event::Drain);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
let (best_static, scan, predicted) = self.scan_ordered_records_for_ball(
|
|
||||||
&mut ball,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
old_position,
|
|
||||||
&mut velocity,
|
|
||||||
events,
|
|
||||||
);
|
|
||||||
let mut best_collision = best_static
|
|
||||||
.map(|(id, is_wall, candidate)| (id, is_wall, candidate.resolve(velocity, ball.spin)));
|
|
||||||
|
|
||||||
let mut transferred_primary_velocity = None;
|
|
||||||
let other_position = MilliVec::from_position(primary_position);
|
|
||||||
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_add(21_000)
|
|
||||||
&& predicted.y >= other_position.y.wrapping_sub(21_000)
|
|
||||||
&& predicted.y <= other_position.y.wrapping_add(21_000)
|
|
||||||
&& let Some(response) = ball_collision_response(
|
|
||||||
old_position,
|
|
||||||
velocity,
|
|
||||||
ball.spin,
|
|
||||||
other_position,
|
|
||||||
MilliVec::from_velocity_per_second(primary_velocity),
|
|
||||||
primary_capture_age <= 0,
|
|
||||||
)
|
|
||||||
&& best_collision.is_none()
|
|
||||||
{
|
|
||||||
transferred_primary_velocity = Some(response.other_velocity);
|
|
||||||
best_collision = Some((
|
|
||||||
174,
|
|
||||||
false,
|
|
||||||
CollisionResponse {
|
|
||||||
surface_distance: response.surface_distance,
|
|
||||||
velocity: response.moving_velocity,
|
|
||||||
spin: response.moving_spin,
|
|
||||||
auxiliary_fired: false,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let mut hit = None;
|
|
||||||
let collided = best_collision.is_some();
|
|
||||||
if collided {
|
|
||||||
ball.capture_age = 0;
|
|
||||||
}
|
|
||||||
let mut auxiliary_fired = false;
|
|
||||||
if let Some((object_id, is_wall, response)) = best_collision {
|
|
||||||
velocity = response.velocity;
|
|
||||||
ball.spin = response.spin;
|
|
||||||
auxiliary_fired = response.auxiliary_fired;
|
|
||||||
hit = Some((object_id, is_wall));
|
|
||||||
if object_id == 174
|
|
||||||
&& let Some(transferred) = transferred_primary_velocity
|
|
||||||
{
|
|
||||||
self.ball.velocity = transferred.to_velocity_per_second();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hit == Some((25, true)) && speed_before_collision >= 1_000 {
|
|
||||||
velocity.x = 0;
|
|
||||||
}
|
|
||||||
if hit == Some((25, true)) && speed_before_collision < 1_000 {
|
|
||||||
velocity.x = 0;
|
|
||||||
velocity.y = 0;
|
|
||||||
}
|
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
|
||||||
ball.position = old_position.add(velocity).to_position();
|
|
||||||
ball.velocity = velocity.to_velocity_per_second();
|
|
||||||
if hit == Some((2, true)) {
|
|
||||||
self.score_mode = ScoreMode::Normal;
|
|
||||||
events.push(Event::Drain);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if let Some((object_id, true)) = hit {
|
|
||||||
if object_id == 25 && speed_before_collision < 1_000 {
|
|
||||||
self.score_mode = ScoreMode::Normal;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
self.apply_wall_rule(object_id, events);
|
|
||||||
if auxiliary_fired && matches!(object_id, 55 | 74 | 107) {
|
|
||||||
events.push(Event::Sound(2019));
|
|
||||||
}
|
|
||||||
} else if let Some((object_id, false)) = hit {
|
|
||||||
self.apply_bumper_rule(object_id, auxiliary_fired, events);
|
|
||||||
}
|
|
||||||
match scan.action {
|
|
||||||
BallAction::Keep | BallAction::Suspend => self.secondary_ball = Some(ball),
|
|
||||||
BallAction::Reset | BallAction::Remove | BallAction::Finish => return true,
|
|
||||||
}
|
|
||||||
collided || scan.action == BallAction::Suspend
|
|
||||||
}
|
|
||||||
|
|
||||||
fn advance_crazy_balls(&mut self, substeps: u8, events: &mut Vec<Event>) {
|
|
||||||
let mut balls = std::mem::take(&mut self.crazy_balls);
|
|
||||||
balls.retain_mut(|ball| {
|
|
||||||
for _ in 0..substeps {
|
|
||||||
match self.advance_crazy_ball(ball, events) {
|
|
||||||
None => return false,
|
|
||||||
Some(true) => break,
|
|
||||||
Some(false) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
});
|
|
||||||
debug_assert!(self.crazy_balls.is_empty());
|
|
||||||
self.crazy_balls = balls;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Moves one crazy ball by one physics substep. Returns `None` once the
|
|
||||||
/// ball has left the table, otherwise whether it collided.
|
|
||||||
fn advance_crazy_ball(&mut self, ball: &mut Ball, events: &mut Vec<Event>) -> Option<bool> {
|
|
||||||
let old_position = MilliVec::from_position(ball.position);
|
|
||||||
let mut velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
|
||||||
velocity.y += GRAVITY_MILLI_PER_STEP;
|
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
|
||||||
let speed_before_collision = milli_distance(velocity);
|
|
||||||
let predicted = old_position.add(velocity);
|
|
||||||
if predicted_outside_board(predicted) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut best = None;
|
|
||||||
for record_ids in [1..=5, 7..=88, 90..=128, 134..=139, 155..=173] {
|
|
||||||
self.retain_static_range(old_position, predicted, velocity, record_ids, &mut best);
|
|
||||||
}
|
|
||||||
let mut hit = None;
|
|
||||||
let mut auxiliary_fired = false;
|
|
||||||
if let Some((object_id, is_wall, candidate)) = best {
|
|
||||||
let response = candidate.resolve(velocity, ball.spin);
|
|
||||||
velocity = response.velocity;
|
|
||||||
ball.spin = response.spin;
|
|
||||||
auxiliary_fired = response.auxiliary_fired;
|
|
||||||
hit = Some((object_id, is_wall));
|
|
||||||
}
|
|
||||||
if hit == Some((25, true)) {
|
|
||||||
velocity.x = 0;
|
|
||||||
if speed_before_collision < 1_000 {
|
|
||||||
// Resting on the shooter stop would park it in the launcher.
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
|
||||||
ball.position = old_position.add(velocity).to_position();
|
|
||||||
ball.velocity = velocity.to_velocity_per_second();
|
|
||||||
match hit {
|
|
||||||
Some((2, true)) => {
|
|
||||||
events.push(Event::Drain);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some((object_id, true)) => {
|
|
||||||
self.apply_wall_rule(object_id, events);
|
|
||||||
if auxiliary_fired && matches!(object_id, 55 | 74 | 107) {
|
|
||||||
events.push(Event::Sound(2019));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some((object_id, false)) => self.apply_bumper_rule(object_id, auxiliary_fired, events),
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
Some(hit.is_some())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_bumper_rule(&mut self, object_id: u8, auxiliary_fired: bool, events: &mut Vec<Event>) {
|
fn apply_bumper_rule(&mut self, object_id: u8, auxiliary_fired: bool, events: &mut Vec<Event>) {
|
||||||
@@ -1668,30 +1508,30 @@ impl Game {
|
|||||||
movement_velocity: MilliVec,
|
movement_velocity: MilliVec,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
) -> BallAction {
|
) -> BallAction {
|
||||||
let ball_count = if self.secondary_ball.is_some() { 2 } else { 1 };
|
let count = u16::try_from(self.balls.len()).unwrap_or(u16::MAX);
|
||||||
let mut ball = self.ball;
|
let pass = BallPass {
|
||||||
|
index: 0,
|
||||||
|
number: 1,
|
||||||
|
count,
|
||||||
|
};
|
||||||
|
let mut ball = self.balls[0];
|
||||||
let scan = self.check_sensor_objects_for_ball(
|
let scan = self.check_sensor_objects_for_ball(
|
||||||
&mut ball,
|
&mut ball,
|
||||||
1,
|
pass,
|
||||||
ball_count,
|
|
||||||
old_position,
|
old_position,
|
||||||
movement_velocity,
|
movement_velocity,
|
||||||
events,
|
events,
|
||||||
);
|
);
|
||||||
match scan.action {
|
match scan.action {
|
||||||
BallAction::Keep | BallAction::Suspend => self.ball = ball,
|
BallAction::Keep | BallAction::Suspend => self.balls[0] = ball,
|
||||||
BallAction::Reset => self.reset_ball_to_launcher(),
|
BallAction::Reset => self.reset_ball_to_launcher(0),
|
||||||
BallAction::Finish => {
|
BallAction::Finish => {
|
||||||
self.ball = ball;
|
self.balls[0] = ball;
|
||||||
self.drain(events);
|
self.lose_ball(0, events);
|
||||||
}
|
|
||||||
BallAction::Remove => {
|
|
||||||
self.ball = self
|
|
||||||
.secondary_ball
|
|
||||||
.take()
|
|
||||||
.expect("a multiball capture must leave the other slot active");
|
|
||||||
}
|
}
|
||||||
|
BallAction::Remove => self.remove_ball(0),
|
||||||
}
|
}
|
||||||
|
self.balls.append(&mut self.spawned_balls);
|
||||||
scan.action
|
scan.action
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1699,8 +1539,7 @@ impl Game {
|
|||||||
fn check_sensor_objects_for_ball(
|
fn check_sensor_objects_for_ball(
|
||||||
&mut self,
|
&mut self,
|
||||||
ball: &mut Ball,
|
ball: &mut Ball,
|
||||||
ball_number: u16,
|
pass: BallPass,
|
||||||
ball_count: u16,
|
|
||||||
old_position: MilliVec,
|
old_position: MilliVec,
|
||||||
movement_velocity: MilliVec,
|
movement_velocity: MilliVec,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
@@ -1729,8 +1568,7 @@ impl Game {
|
|||||||
if !self.claw.active {
|
if !self.claw.active {
|
||||||
match self.capture_record_step(
|
match self.capture_record_step(
|
||||||
ball,
|
ball,
|
||||||
ball_number,
|
pass,
|
||||||
ball_count,
|
|
||||||
89,
|
89,
|
||||||
CLAW_TRIGGER_CENTER,
|
CLAW_TRIGGER_CENTER,
|
||||||
CLAW_TRIGGER_RADIUS,
|
CLAW_TRIGGER_RADIUS,
|
||||||
@@ -1741,9 +1579,9 @@ impl Game {
|
|||||||
) {
|
) {
|
||||||
CaptureStep::Holding | CaptureStep::Outside => {}
|
CaptureStep::Holding | CaptureStep::Outside => {}
|
||||||
CaptureStep::Complete => {
|
CaptureStep::Complete => {
|
||||||
let terminal_frame = self
|
let terminal_frame =
|
||||||
.next_claw_terminal_frame(self.claw_capture_restricted_for(ball_number));
|
self.next_claw_terminal_frame(self.claw_capture_restricted_for(pass.index));
|
||||||
self.begin_claw_capture_after_hold(ball, ball_number, terminal_frame, events);
|
self.begin_claw_capture_after_hold(ball, pass.index, terminal_frame, events);
|
||||||
action = BallAction::Suspend;
|
action = BallAction::Suspend;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1751,8 +1589,7 @@ impl Game {
|
|||||||
|
|
||||||
if let Some(completed_action) = self.check_lock_holes(
|
if let Some(completed_action) = self.check_lock_holes(
|
||||||
ball,
|
ball,
|
||||||
ball_number,
|
pass,
|
||||||
ball_count,
|
|
||||||
old_position,
|
old_position,
|
||||||
predicted_position,
|
predicted_position,
|
||||||
&mut capture_candidate,
|
&mut capture_candidate,
|
||||||
@@ -1763,8 +1600,7 @@ impl Game {
|
|||||||
self.check_target_sensors(ball, old_position, movement_velocity, 140..=147, events);
|
self.check_target_sensors(ball, old_position, movement_velocity, 140..=147, events);
|
||||||
if let Some(completed_action) = self.check_special_hole(
|
if let Some(completed_action) = self.check_special_hole(
|
||||||
ball,
|
ball,
|
||||||
ball_number,
|
pass,
|
||||||
ball_count,
|
|
||||||
old_position,
|
old_position,
|
||||||
predicted_position,
|
predicted_position,
|
||||||
&mut capture_candidate,
|
&mut capture_candidate,
|
||||||
@@ -1772,7 +1608,7 @@ impl Game {
|
|||||||
) {
|
) {
|
||||||
action = completed_action;
|
action = completed_action;
|
||||||
}
|
}
|
||||||
self.check_effect_sensor(ball, ball_number, old_position, movement_velocity, events);
|
self.check_effect_sensor(ball, pass.number, old_position, movement_velocity, events);
|
||||||
self.check_target_sensors(ball, old_position, movement_velocity, 150..=152, events);
|
self.check_target_sensors(ball, old_position, movement_velocity, 150..=152, events);
|
||||||
SensorScanResult { action }
|
SensorScanResult { action }
|
||||||
}
|
}
|
||||||
@@ -1785,8 +1621,7 @@ impl Game {
|
|||||||
fn capture_record_step(
|
fn capture_record_step(
|
||||||
&mut self,
|
&mut self,
|
||||||
ball: &mut Ball,
|
ball: &mut Ball,
|
||||||
ball_number: u16,
|
pass: BallPass,
|
||||||
ball_count: u16,
|
|
||||||
record_id: u8,
|
record_id: u8,
|
||||||
center: Vec2,
|
center: Vec2,
|
||||||
radius: f32,
|
radius: f32,
|
||||||
@@ -1795,6 +1630,11 @@ impl Game {
|
|||||||
best_capture: &mut Option<(u8, StaticCollisionCandidate)>,
|
best_capture: &mut Option<(u8, StaticCollisionCandidate)>,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
) -> CaptureStep {
|
) -> CaptureStep {
|
||||||
|
let BallPass {
|
||||||
|
index: ball_index,
|
||||||
|
number: ball_number,
|
||||||
|
count: ball_count,
|
||||||
|
} = pass;
|
||||||
let current_position = MilliVec::from_position(ball.position);
|
let current_position = MilliVec::from_position(ball.position);
|
||||||
let center_view = center;
|
let center_view = center;
|
||||||
let center = MilliVec::from_position(center_view);
|
let center = MilliVec::from_position(center_view);
|
||||||
@@ -1810,7 +1650,7 @@ impl Game {
|
|||||||
&& ball.capture_age == 0
|
&& ball.capture_age == 0
|
||||||
&& self.record_contacts[usize::from(record_id)] == 0
|
&& self.record_contacts[usize::from(record_id)] == 0
|
||||||
{
|
{
|
||||||
self.clear_claw_capture_entry_for(ball_number);
|
self.clear_claw_capture_entry_for(ball_index);
|
||||||
}
|
}
|
||||||
return CaptureStep::Outside;
|
return CaptureStep::Outside;
|
||||||
}
|
}
|
||||||
@@ -1833,8 +1673,7 @@ impl Game {
|
|||||||
// Latch the restriction when the ball first enters the deep
|
// Latch the restriction when the ball first enters the deep
|
||||||
// capture zone, before the pull/hold phase can outlive the
|
// capture zone, before the pull/hold phase can outlive the
|
||||||
// other active ball.
|
// other active ball.
|
||||||
self.claw.capture_entry =
|
self.claw.capture_entry = Some((ball_index, ball_count > 1));
|
||||||
Some((ClawBallSlot::from_ball_number(ball_number), 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);
|
||||||
@@ -1891,16 +1730,26 @@ impl Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if record_id == 89 && surface_distance >= -11_000 {
|
if record_id == 89 && surface_distance >= -11_000 {
|
||||||
self.clear_claw_capture_entry_for(ball_number);
|
self.clear_claw_capture_entry_for(ball_index);
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
// Deliberate original-game divergence: a ball that was settling into
|
||||||
|
// a lock hole when another ball filled it would bounce off the rim
|
||||||
|
// from the inside forever. With many balls that is common, so the
|
||||||
|
// rim of a filled hole only repels balls moving towards its center.
|
||||||
let velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
let velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
|
let leaving_filled_hole = (129..=133).contains(&record_id)
|
||||||
|
&& contact == 99
|
||||||
|
&& i64::from(velocity.x) * i64::from(current_position.x.wrapping_sub(center.x))
|
||||||
|
+ i64::from(velocity.y) * i64::from(current_position.y.wrapping_sub(center.y))
|
||||||
|
> 0;
|
||||||
if surface_distance <= milli_distance(velocity)
|
if surface_distance <= milli_distance(velocity)
|
||||||
&& surface_distance >= -11_000
|
&& surface_distance >= -11_000
|
||||||
&& contact != 0
|
&& contact != 0
|
||||||
|
&& !leaving_filled_hole
|
||||||
&& let Some(candidate) = capture_collision_candidate(
|
&& let Some(candidate) = capture_collision_candidate(
|
||||||
current_position,
|
current_position,
|
||||||
velocity,
|
velocity,
|
||||||
@@ -1919,8 +1768,7 @@ impl Game {
|
|||||||
fn check_lock_holes(
|
fn check_lock_holes(
|
||||||
&mut self,
|
&mut self,
|
||||||
ball: &mut Ball,
|
ball: &mut Ball,
|
||||||
ball_number: u16,
|
pass: BallPass,
|
||||||
ball_count: u16,
|
|
||||||
old_position: MilliVec,
|
old_position: MilliVec,
|
||||||
predicted_position: MilliVec,
|
predicted_position: MilliVec,
|
||||||
best_capture: &mut Option<(u8, StaticCollisionCandidate)>,
|
best_capture: &mut Option<(u8, StaticCollisionCandidate)>,
|
||||||
@@ -1929,8 +1777,7 @@ impl Game {
|
|||||||
for (index, sensor) in LOCK_HOLES.into_iter().enumerate() {
|
for (index, sensor) in LOCK_HOLES.into_iter().enumerate() {
|
||||||
match self.capture_record_step(
|
match self.capture_record_step(
|
||||||
ball,
|
ball,
|
||||||
ball_number,
|
pass,
|
||||||
ball_count,
|
|
||||||
sensor.id,
|
sensor.id,
|
||||||
sensor.center,
|
sensor.center,
|
||||||
sensor.radius,
|
sensor.radius,
|
||||||
@@ -1942,7 +1789,7 @@ impl Game {
|
|||||||
CaptureStep::Outside | CaptureStep::Holding => continue,
|
CaptureStep::Outside | CaptureStep::Holding => continue,
|
||||||
CaptureStep::Complete => {}
|
CaptureStep::Complete => {}
|
||||||
}
|
}
|
||||||
return Some(self.complete_lock_hole(index, ball_count, events));
|
return Some(self.complete_lock_hole(index, pass.count, events));
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -1997,8 +1844,7 @@ impl Game {
|
|||||||
fn check_special_hole(
|
fn check_special_hole(
|
||||||
&mut self,
|
&mut self,
|
||||||
ball: &mut Ball,
|
ball: &mut Ball,
|
||||||
ball_number: u16,
|
pass: BallPass,
|
||||||
ball_count: u16,
|
|
||||||
old_position: MilliVec,
|
old_position: MilliVec,
|
||||||
predicted_position: MilliVec,
|
predicted_position: MilliVec,
|
||||||
best_capture: &mut Option<(u8, StaticCollisionCandidate)>,
|
best_capture: &mut Option<(u8, StaticCollisionCandidate)>,
|
||||||
@@ -2006,8 +1852,7 @@ impl Game {
|
|||||||
) -> Option<BallAction> {
|
) -> Option<BallAction> {
|
||||||
match self.capture_record_step(
|
match self.capture_record_step(
|
||||||
ball,
|
ball,
|
||||||
ball_number,
|
pass,
|
||||||
ball_count,
|
|
||||||
SPECIAL_HOLE_SENSOR.id,
|
SPECIAL_HOLE_SENSOR.id,
|
||||||
SPECIAL_HOLE_SENSOR.center,
|
SPECIAL_HOLE_SENSOR.center,
|
||||||
SPECIAL_HOLE_SENSOR.radius,
|
SPECIAL_HOLE_SENSOR.radius,
|
||||||
@@ -2021,7 +1866,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
self.multiball_state = MultiballState::Ready;
|
self.multiball_state = MultiballState::Ready;
|
||||||
events.push(Event::Wheel);
|
events.push(Event::Wheel);
|
||||||
Some(if ball_count == 1 {
|
Some(if pass.count == 1 {
|
||||||
if self.tilted {
|
if self.tilted {
|
||||||
BallAction::Finish
|
BallAction::Finish
|
||||||
} else {
|
} else {
|
||||||
@@ -2079,13 +1924,14 @@ impl Game {
|
|||||||
7 if self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] != 0
|
7 if self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] != 0
|
||||||
&& self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)]
|
&& self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)]
|
||||||
!= ball_number
|
!= ball_number
|
||||||
&& self.secondary_ball.is_none()
|
&& self.balls.len() == 1
|
||||||
|
&& self.spawned_balls.is_empty()
|
||||||
&& matches!(
|
&& matches!(
|
||||||
self.multiball_state,
|
self.multiball_state,
|
||||||
MultiballState::Ready | MultiballState::Active
|
MultiballState::Ready | MultiballState::Active
|
||||||
) =>
|
) =>
|
||||||
{
|
{
|
||||||
self.secondary_ball = Some(Ball {
|
self.spawned_balls.push(Ball {
|
||||||
position: vec2(17.0, 23.0),
|
position: vec2(17.0, 23.0),
|
||||||
velocity: MilliVec { x: 0, y: 3_040 }.to_velocity_per_second(),
|
velocity: MilliVec { x: 0, y: 3_040 }.to_velocity_per_second(),
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
@@ -2170,7 +2016,7 @@ impl Game {
|
|||||||
ball.velocity = velocity.to_velocity_per_second();
|
ball.velocity = velocity.to_velocity_per_second();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_ball_to_launcher(&mut self) {
|
fn reset_ball_to_launcher(&mut self, index: usize) {
|
||||||
if self.record_contacts[129..=133]
|
if self.record_contacts[129..=133]
|
||||||
.iter()
|
.iter()
|
||||||
.all(|contact| *contact != 0)
|
.all(|contact| *contact != 0)
|
||||||
@@ -2179,7 +2025,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
self.tilted = false;
|
self.tilted = false;
|
||||||
self.tilt_counter = 0;
|
self.tilt_counter = 0;
|
||||||
self.ball = Ball::default();
|
self.balls[index] = Ball::default();
|
||||||
self.launcher_charge = 0.0;
|
self.launcher_charge = 0.0;
|
||||||
self.launcher_was_down = false;
|
self.launcher_was_down = false;
|
||||||
self.launcher_hold_seconds = 0.0;
|
self.launcher_hold_seconds = 0.0;
|
||||||
@@ -2269,55 +2115,88 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn claw_holds_slot(&self, slot: ClawBallSlot) -> bool {
|
fn held_ball(&self) -> Option<usize> {
|
||||||
self.claw.ball_suspended && self.claw.captured_slot == Some(slot)
|
self.claw
|
||||||
|
.ball_suspended
|
||||||
|
.then_some(self.claw.captured_slot)
|
||||||
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn claw_capture_restricted_for(&self, ball_number: u16) -> bool {
|
fn claw_holds(&self, index: usize) -> bool {
|
||||||
self.claw.capture_entry == Some((ClawBallSlot::from_ball_number(ball_number), true))
|
self.held_ball() == Some(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_claw_capture_entry_for(&mut self, ball_number: u16) {
|
fn claw_capture_restricted_for(&self, index: usize) -> bool {
|
||||||
let slot = ClawBallSlot::from_ball_number(ball_number);
|
self.claw.capture_entry == Some((index, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_claw_capture_entry_for(&mut self, index: usize) {
|
||||||
if self
|
if self
|
||||||
.claw
|
.claw
|
||||||
.capture_entry
|
.capture_entry
|
||||||
.is_some_and(|(owner, _)| owner == slot)
|
.is_some_and(|(owner, _)| owner == index)
|
||||||
{
|
{
|
||||||
self.claw.capture_entry = None;
|
self.claw.capture_entry = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn promote_claw_secondary_to_primary(&mut self) {
|
/// Takes the ball at `index` out of play without any drain rules and
|
||||||
if self.claw.captured_slot == Some(ClawBallSlot::Secondary) {
|
/// keeps every index-based reference pointing at the same physical ball.
|
||||||
self.claw.captured_slot = Some(ClawBallSlot::Primary);
|
fn remove_ball(&mut self, index: usize) {
|
||||||
if let Some((ClawBallSlot::Secondary, restricted)) = self.claw.capture_entry {
|
self.balls.remove(index);
|
||||||
self.claw.capture_entry = Some((ClawBallSlot::Primary, restricted));
|
let shift = |slot: Option<usize>| match slot {
|
||||||
|
Some(slot) if slot == index => None,
|
||||||
|
Some(slot) if slot > index => Some(slot - 1),
|
||||||
|
slot => slot,
|
||||||
|
};
|
||||||
|
self.claw.captured_slot = shift(self.claw.captured_slot);
|
||||||
|
self.claw.capture_entry = self
|
||||||
|
.claw
|
||||||
|
.capture_entry
|
||||||
|
.and_then(|(owner, restricted)| shift(Some(owner)).map(|owner| (owner, restricted)));
|
||||||
|
// The claw and lock-hole contacts name their owner by slot number;
|
||||||
|
// later slots move down one number with their ball. A lock hole the
|
||||||
|
// removed ball was still settling into is free again.
|
||||||
|
let removed_number = index + 1;
|
||||||
|
let claw_contact = &mut self.record_contacts[89];
|
||||||
|
if *claw_contact != 99 && usize::from(*claw_contact) > removed_number {
|
||||||
|
*claw_contact -= 1;
|
||||||
|
}
|
||||||
|
for contact in &mut self.record_contacts[129..=133] {
|
||||||
|
if *contact == 99 || *contact == 0 {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if self.record_contacts[89] == 2 {
|
match usize::from(*contact).cmp(&removed_number) {
|
||||||
self.record_contacts[89] = 1;
|
std::cmp::Ordering::Equal => *contact = 0,
|
||||||
|
std::cmp::Ordering::Greater => *contact -= 1,
|
||||||
|
std::cmp::Ordering::Less => {}
|
||||||
}
|
}
|
||||||
self.score_mode = ScoreMode::Normal;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn primary_ball_suspended(&self) -> bool {
|
/// A ball came to rest on the shooter stop. It waits in the launcher
|
||||||
self.claw_holds_slot(ClawBallSlot::Primary)
|
/// unless another ball already occupies it.
|
||||||
}
|
fn return_ball_to_launcher(&mut self, index: usize) -> BallStep {
|
||||||
|
if self.launcher_ball().is_some_and(|waiting| waiting != index) {
|
||||||
pub fn secondary_ball_suspended(&self) -> bool {
|
self.remove_ball(index);
|
||||||
self.claw_holds_slot(ClawBallSlot::Secondary)
|
if self.balls.len() == 1 {
|
||||||
|
self.score_mode = ScoreMode::Normal;
|
||||||
|
}
|
||||||
|
return BallStep::Removed;
|
||||||
|
}
|
||||||
|
self.balls[index] = Ball::default();
|
||||||
|
self.launcher_charge = 0.0;
|
||||||
|
self.launcher_was_down = false;
|
||||||
|
BallStep::Stop
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_flipper_kicks(&mut self) {
|
fn apply_flipper_kicks(&mut self) {
|
||||||
let pending = self.pending_flipper_edges;
|
let pending = self.pending_flipper_edges;
|
||||||
self.pending_flipper_edges.fill(0);
|
self.pending_flipper_edges.fill(0);
|
||||||
if self.ball.in_launcher || self.tilted {
|
if self.tilted {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let primary_held = self.claw_holds_slot(ClawBallSlot::Primary);
|
let held = self.held_ball();
|
||||||
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),
|
||||||
(i32::from(pending[1]), FlipperSide::Right),
|
(i32::from(pending[1]), FlipperSide::Right),
|
||||||
@@ -2325,16 +2204,10 @@ impl Game {
|
|||||||
if delta == 0 {
|
if delta == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !primary_held {
|
for (index, ball) in self.balls.iter_mut().enumerate() {
|
||||||
apply_flipper_response_to_ball(&mut self.ball, delta, side);
|
if !ball.in_launcher && held != Some(index) {
|
||||||
}
|
apply_flipper_response_to_ball(ball, delta, side);
|
||||||
if let Some(secondary) = &mut self.secondary_ball
|
}
|
||||||
&& !secondary_held
|
|
||||||
{
|
|
||||||
apply_flipper_response_to_ball(secondary, delta, side);
|
|
||||||
}
|
|
||||||
for ball in &mut self.crazy_balls {
|
|
||||||
apply_flipper_response_to_ball(ball, delta, side);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2349,11 +2222,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(ClawBallSlot::Primary);
|
self.claw.captured_slot = Some(0);
|
||||||
self.claw.capture_entry = None;
|
self.claw.capture_entry = None;
|
||||||
self.claw.frame_accumulator = 0.0;
|
self.claw.frame_accumulator = 0.0;
|
||||||
self.ball.velocity = Vec2::ZERO;
|
self.balls[0].velocity = Vec2::ZERO;
|
||||||
self.ball.spin = Real48::ZERO;
|
self.balls[0].spin = Real48::ZERO;
|
||||||
events.push(Event::ClawCapture);
|
events.push(Event::ClawCapture);
|
||||||
events.push(Event::Sound(2015));
|
events.push(Event::Sound(2015));
|
||||||
}
|
}
|
||||||
@@ -2361,7 +2234,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,
|
index: usize,
|
||||||
terminal_frame: u8,
|
terminal_frame: u8,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
) {
|
) {
|
||||||
@@ -2373,7 +2246,7 @@ 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::from_ball_number(ball_number));
|
self.claw.captured_slot = Some(index);
|
||||||
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;
|
||||||
@@ -2412,23 +2285,12 @@ impl Game {
|
|||||||
self.claw.target_frame = 10;
|
self.claw.target_frame = 10;
|
||||||
self.claw.bank = ClawSpriteBank::Opening;
|
self.claw.bank = ClawSpriteBank::Opening;
|
||||||
let release = claw_release(release_frame);
|
let release = claw_release(release_frame);
|
||||||
match self.claw.captured_slot {
|
let index = match self.claw.captured_slot {
|
||||||
Some(ClawBallSlot::Primary) => {
|
Some(index) if index < self.balls.len() => index,
|
||||||
(self.ball.position, self.ball.velocity) = release;
|
_ => 0,
|
||||||
}
|
};
|
||||||
Some(ClawBallSlot::Secondary) => {
|
self.claw.captured_slot = Some(index);
|
||||||
if let Some(ball) = &mut self.secondary_ball {
|
(self.balls[index].position, self.balls[index].velocity) = release;
|
||||||
(ball.position, ball.velocity) = release;
|
|
||||||
} else {
|
|
||||||
(self.ball.position, self.ball.velocity) = release;
|
|
||||||
self.promote_claw_secondary_to_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));
|
||||||
@@ -2462,24 +2324,20 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drain(&mut self, events: &mut Vec<Event>) {
|
/// The ball at `index` drained. While other balls remain it simply
|
||||||
if self.claw_holds_slot(ClawBallSlot::Secondary) {
|
/// leaves the table; the last ball either takes the special respawn or
|
||||||
self.ball = self
|
/// ends the player's ball.
|
||||||
.secondary_ball
|
fn lose_ball(&mut self, index: usize, events: &mut Vec<Event>) -> BallStep {
|
||||||
.take()
|
if self.balls.len() > 1 {
|
||||||
.expect("the claw-held secondary ball must remain in slot two");
|
self.remove_ball(index);
|
||||||
self.promote_claw_secondary_to_primary();
|
if self.balls.len() == 1 {
|
||||||
|
self.score_mode = ScoreMode::Normal;
|
||||||
|
}
|
||||||
events.push(Event::Drain);
|
events.push(Event::Drain);
|
||||||
return;
|
return BallStep::Removed;
|
||||||
}
|
|
||||||
if let Some(remaining_ball) = self.secondary_ball.take() {
|
|
||||||
self.ball = remaining_ball;
|
|
||||||
self.score_mode = ScoreMode::Normal;
|
|
||||||
events.push(Event::Drain);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if self.special_respawn_pending() {
|
if self.special_respawn_pending() {
|
||||||
self.ball = Ball {
|
self.balls[index] = Ball {
|
||||||
position: vec2(17.0, 23.0),
|
position: vec2(17.0, 23.0),
|
||||||
velocity: MilliVec { x: 0, y: 3_040 }.to_velocity_per_second(),
|
velocity: MilliVec { x: 0, y: 3_040 }.to_velocity_per_second(),
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
@@ -2489,13 +2347,20 @@ impl Game {
|
|||||||
self.multiball_state = MultiballState::Unavailable;
|
self.multiball_state = MultiballState::Unavailable;
|
||||||
self.record_contacts[148] = 0;
|
self.record_contacts[148] = 0;
|
||||||
self.special_hole_gate = SpecialHoleGate::Suppressed;
|
self.special_hole_gate = SpecialHoleGate::Suppressed;
|
||||||
return;
|
return BallStep::Stop;
|
||||||
}
|
|
||||||
if let Some(crazy_ball) = self.crazy_balls.pop() {
|
|
||||||
self.ball = crazy_ball;
|
|
||||||
events.push(Event::Drain);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
self.end_ball(events);
|
||||||
|
BallStep::Stop
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test shim: slot one drains.
|
||||||
|
#[cfg(test)]
|
||||||
|
fn drain(&mut self, events: &mut Vec<Event>) {
|
||||||
|
self.lose_ball(0, events);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_ball(&mut self, events: &mut Vec<Event>) {
|
||||||
|
self.spawned_balls.clear();
|
||||||
self.ball_double = BallDoubleState::Inactive;
|
self.ball_double = BallDoubleState::Inactive;
|
||||||
self.multiball_state = MultiballState::Unavailable;
|
self.multiball_state = MultiballState::Unavailable;
|
||||||
self.special_hole_gate = SpecialHoleGate::Enabled;
|
self.special_hole_gate = SpecialHoleGate::Enabled;
|
||||||
@@ -2523,7 +2388,6 @@ impl Game {
|
|||||||
}
|
}
|
||||||
self.tilted = false;
|
self.tilted = false;
|
||||||
self.tilt_counter = 0;
|
self.tilt_counter = 0;
|
||||||
self.secondary_ball = None;
|
|
||||||
self.score_mode = ScoreMode::Normal;
|
self.score_mode = ScoreMode::Normal;
|
||||||
self.launcher_charge = 0.0;
|
self.launcher_charge = 0.0;
|
||||||
self.launcher_was_down = false;
|
self.launcher_was_down = false;
|
||||||
@@ -2536,7 +2400,7 @@ impl Game {
|
|||||||
if self.players[next].balls > 0 || self.players[next].extra_balls > 0 {
|
if self.players[next].balls > 0 || self.players[next].extra_balls > 0 {
|
||||||
self.current_player = next;
|
self.current_player = next;
|
||||||
self.load_current_rule_state();
|
self.load_current_rule_state();
|
||||||
self.ball = Ball::default();
|
self.balls = vec![Ball::default()];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
next = (next + 1) % self.players.len();
|
next = (next + 1) % self.players.len();
|
||||||
@@ -2545,7 +2409,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn special_respawn_pending(&self) -> bool {
|
fn special_respawn_pending(&self) -> bool {
|
||||||
self.secondary_ball.is_none()
|
self.balls.len() == 1
|
||||||
&& self.score_mode == ScoreMode::Normal
|
&& self.score_mode == ScoreMode::Normal
|
||||||
&& self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] != 0
|
&& self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] != 0
|
||||||
&& matches!(
|
&& matches!(
|
||||||
@@ -2675,6 +2539,33 @@ fn apply_flipper_response_to_ball(ball: &mut Ball, delta: i32, side: FlipperSide
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl BallPass {
|
||||||
|
fn slot(number: u16, count: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
index: usize::from(number) - 1,
|
||||||
|
number,
|
||||||
|
count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
impl Game {
|
||||||
|
fn secondary(&self) -> Option<Ball> {
|
||||||
|
self.balls.get(1).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_secondary(&mut self, ball: Option<Ball>) {
|
||||||
|
self.balls.truncate(1);
|
||||||
|
self.balls.extend(ball);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_secondary(&mut self) -> Option<Ball> {
|
||||||
|
(self.balls.len() > 1).then(|| self.balls.remove(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -2700,9 +2591,9 @@ mod tests {
|
|||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
) {
|
) {
|
||||||
for _ in 0..=60 {
|
for _ in 0..=60 {
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = center;
|
game.balls[0].position = center;
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.check_sensor_objects(MilliVec::from_position(center), MilliVec::default(), events);
|
game.check_sensor_objects(MilliVec::from_position(center), MilliVec::default(), events);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2715,14 +2606,13 @@ mod tests {
|
|||||||
previous_position: MilliVec,
|
previous_position: MilliVec,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
) -> CaptureStep {
|
) -> CaptureStep {
|
||||||
let mut ball = game.ball;
|
let mut ball = game.balls[0];
|
||||||
let mut capture_candidate = None;
|
let mut capture_candidate = None;
|
||||||
let predicted_position =
|
let predicted_position =
|
||||||
previous_position.add(MilliVec::from_velocity_per_second(ball.velocity));
|
previous_position.add(MilliVec::from_velocity_per_second(ball.velocity));
|
||||||
let result = game.capture_record_step(
|
let result = game.capture_record_step(
|
||||||
&mut ball,
|
&mut ball,
|
||||||
1,
|
BallPass::slot(1, if game.secondary().is_some() { 2 } else { 1 }),
|
||||||
if game.secondary_ball.is_some() { 2 } else { 1 },
|
|
||||||
record_id,
|
record_id,
|
||||||
center,
|
center,
|
||||||
radius,
|
radius,
|
||||||
@@ -2731,7 +2621,7 @@ mod tests {
|
|||||||
&mut capture_candidate,
|
&mut capture_candidate,
|
||||||
events,
|
events,
|
||||||
);
|
);
|
||||||
game.ball = ball;
|
game.balls[0] = ball;
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2751,52 +2641,78 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn crazy_mode_adds_ten_balls_that_fall_and_score() {
|
fn crazy_mode_adds_ten_ordinary_balls_and_marks_the_player() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
assert!(game.add_crazy_balls());
|
assert!(game.add_crazy_balls());
|
||||||
assert!(game.player().crazy_mode_used());
|
assert!(game.player().crazy_mode_used());
|
||||||
assert_eq!(game.crazy_balls.len(), 10);
|
assert_eq!(game.balls.len(), 11);
|
||||||
let start: Vec<_> = game.crazy_balls.iter().map(|ball| ball.position).collect();
|
assert!(game.balls[0].in_launcher);
|
||||||
|
let start: Vec<_> = game.balls.iter().map(|ball| ball.position).collect();
|
||||||
for _ in 0..20 {
|
for _ in 0..20 {
|
||||||
game.update(0.03, 3, Controls::default());
|
game.update(0.03, 3, Controls::default());
|
||||||
}
|
}
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
assert!(
|
assert!(
|
||||||
game.crazy_balls
|
game.balls[1..]
|
||||||
.iter()
|
.iter()
|
||||||
.zip(&start)
|
.zip(&start[1..])
|
||||||
.all(|(ball, start)| ball.position != *start)
|
.all(|(ball, start)| ball.position != *start)
|
||||||
);
|
);
|
||||||
for _ in 0..2_000 {
|
for _ in 0..3_000 {
|
||||||
game.update(0.03, 3, Controls::default());
|
game.update(0.03, 3, Controls::default());
|
||||||
}
|
}
|
||||||
assert!(game.crazy_balls.is_empty());
|
// Every dropped ball drained; the waiting ball never counted as lost.
|
||||||
|
assert_eq!(game.balls.len(), 1);
|
||||||
|
assert!(game.balls[0].in_launcher);
|
||||||
assert!(game.player().score > 0);
|
assert!(game.player().score > 0);
|
||||||
assert_eq!(game.player().balls, 3);
|
assert_eq!(game.player().balls, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn draining_primary_ball_promotes_a_crazy_ball() {
|
fn a_ball_moves_the_same_in_any_slot() {
|
||||||
|
let ball = Ball {
|
||||||
|
position: vec2(120.0, 160.0),
|
||||||
|
velocity: vec2(40.0, -120.0),
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
};
|
||||||
|
let mut alone = Game::new_with_seed(1, 7);
|
||||||
|
alone.balls = vec![ball];
|
||||||
|
let mut behind_launcher = Game::new_with_seed(1, 7);
|
||||||
|
behind_launcher.balls = vec![Ball::default(), ball];
|
||||||
|
for _ in 0..150 {
|
||||||
|
alone.update(0.03, 3, Controls::default());
|
||||||
|
behind_launcher.update(0.03, 3, Controls::default());
|
||||||
|
if alone.balls[0].in_launcher || behind_launcher.balls.len() < 2 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
assert_eq!(alone.balls[0].position, behind_launcher.balls[1].position);
|
||||||
|
assert_eq!(alone.balls[0].velocity, behind_launcher.balls[1].velocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_turn_ends_only_when_the_last_ball_drains() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.add_crazy_balls();
|
game.add_crazy_balls();
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
game.drain(&mut events);
|
game.drain(&mut events);
|
||||||
assert_eq!(game.crazy_balls.len(), 9);
|
assert_eq!(game.balls.len(), 10);
|
||||||
assert_eq!(game.ball.position, CRAZY_BALL_POSITIONS[9]);
|
assert_eq!(game.balls[0].position, CRAZY_BALL_POSITIONS[0]);
|
||||||
assert!(!game.ball.in_launcher);
|
|
||||||
assert_eq!(game.player().balls, 3);
|
assert_eq!(game.player().balls, 3);
|
||||||
game.crazy_balls.clear();
|
game.balls.truncate(1);
|
||||||
game.drain(&mut events);
|
game.drain(&mut events);
|
||||||
assert_eq!(game.player().balls, 2);
|
assert_eq!(game.player().balls, 2);
|
||||||
assert!(game.ball.in_launcher);
|
assert_eq!(game.balls.len(), 1);
|
||||||
|
assert!(game.balls[0].in_launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn crazy_scores_are_flagged_on_the_high_score_candidate() {
|
fn crazy_scores_are_flagged_on_the_high_score_candidate() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.add_crazy_balls();
|
game.add_crazy_balls();
|
||||||
game.crazy_balls.clear();
|
game.balls.truncate(1);
|
||||||
game.players[0].balls = 1;
|
game.players[0].balls = 1;
|
||||||
game.players[0].score = 1_234;
|
game.players[0].score = 1_234;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
@@ -2808,12 +2724,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn finished_game_rejects_crazy_mode() {
|
fn crazy_mode_is_capped_and_rejected_after_the_game() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.finished = true;
|
while game.add_crazy_balls() {}
|
||||||
assert!(!game.add_crazy_balls());
|
assert_eq!(game.balls.len(), MAX_BALLS);
|
||||||
assert!(game.crazy_balls.is_empty());
|
|
||||||
assert!(!game.player().crazy_mode_used());
|
let mut finished = Game::new_with_seed(1, 7);
|
||||||
|
finished.finished = true;
|
||||||
|
assert!(!finished.add_crazy_balls());
|
||||||
|
assert_eq!(finished.balls.len(), 1);
|
||||||
|
assert!(!finished.player().crazy_mode_used());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2842,8 +2762,8 @@ mod tests {
|
|||||||
fn ball_starts_in_the_recovered_shooter_lane() {
|
fn ball_starts_in_the_recovered_shooter_lane() {
|
||||||
let game = Game::new(1);
|
let game = Game::new(1);
|
||||||
|
|
||||||
assert_eq!(game.ball.position, Vec2::new(325.0, 413.0));
|
assert_eq!(game.balls[0].position, Vec2::new(325.0, 413.0));
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2856,9 +2776,9 @@ mod tests {
|
|||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.object_active[usize::from(wall.id)] = true;
|
game.object_active[usize::from(wall.id)] = true;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = midpoint - normal;
|
game.balls[0].position = midpoint - normal;
|
||||||
game.ball.velocity =
|
game.balls[0].velocity =
|
||||||
MilliVec::from_velocity_per_second(normal * 200.0).to_velocity_per_second();
|
MilliVec::from_velocity_per_second(normal * 200.0).to_velocity_per_second();
|
||||||
game.fixed_update(&mut Vec::new());
|
game.fixed_update(&mut Vec::new());
|
||||||
if game.last_collision_id != Some(wall.id) {
|
if game.last_collision_id != Some(wall.id) {
|
||||||
@@ -2871,9 +2791,9 @@ mod tests {
|
|||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.object_active[usize::from(circle.id)] = true;
|
game.object_active[usize::from(circle.id)] = true;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = circle.center + vec2(circle.contact_radius + 1.0, 0.0);
|
game.balls[0].position = circle.center + vec2(circle.contact_radius + 1.0, 0.0);
|
||||||
game.ball.velocity = MilliVec { x: -2_000, y: 0 }.to_velocity_per_second();
|
game.balls[0].velocity = MilliVec { x: -2_000, y: 0 }.to_velocity_per_second();
|
||||||
game.fixed_update(&mut Vec::new());
|
game.fixed_update(&mut Vec::new());
|
||||||
if game.last_collision_id != Some(circle.id) {
|
if game.last_collision_id != Some(circle.id) {
|
||||||
missed_circles.push(circle.id);
|
missed_circles.push(circle.id);
|
||||||
@@ -2900,18 +2820,18 @@ mod tests {
|
|||||||
(5, 0.010, 1),
|
(5, 0.010, 1),
|
||||||
] {
|
] {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(200.0, 250.0);
|
game.balls[0].position = vec2(200.0, 250.0);
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
|
|
||||||
game.update(interval - 0.001, detail, Controls::default());
|
game.update(interval - 0.001, detail, Controls::default());
|
||||||
assert_eq!(game.ball.position, vec2(200.0, 250.0));
|
assert_eq!(game.balls[0].position, vec2(200.0, 250.0));
|
||||||
assert_eq!(game.ball.velocity, Vec2::ZERO);
|
assert_eq!(game.balls[0].velocity, Vec2::ZERO);
|
||||||
|
|
||||||
game.update(0.001_1, detail, Controls::default());
|
game.update(0.001_1, detail, Controls::default());
|
||||||
let velocity = MilliVec::from_velocity_per_second(game.ball.velocity);
|
let velocity = MilliVec::from_velocity_per_second(game.balls[0].velocity);
|
||||||
let position = MilliVec::from_position(game.ball.position);
|
let position = MilliVec::from_position(game.balls[0].position);
|
||||||
assert_eq!(velocity.y, 15 * substeps);
|
assert_eq!(velocity.y, 15 * substeps);
|
||||||
assert_eq!(position.y, 250_000 + 15 * substeps * (substeps + 1) / 2);
|
assert_eq!(position.y, 250_000 + 15 * substeps * (substeps + 1) / 2);
|
||||||
}
|
}
|
||||||
@@ -2920,9 +2840,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn detail_batch_stops_after_the_first_collision_like_integrate_one_ball() {
|
fn detail_batch_stops_after_the_first_collision_like_integrate_one_ball() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(270.0, 30.0);
|
game.balls[0].position = vec2(270.0, 30.0);
|
||||||
game.ball.velocity = vec2(0.0, -380.0);
|
game.balls[0].velocity = vec2(0.0, -380.0);
|
||||||
let mut expected = game.clone();
|
let mut expected = game.clone();
|
||||||
let mut expected_events = Vec::new();
|
let mut expected_events = Vec::new();
|
||||||
let mut stopped = false;
|
let mut stopped = false;
|
||||||
@@ -2937,17 +2857,17 @@ mod tests {
|
|||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
game.timer_tick(0.050, 5, &mut events);
|
game.timer_tick(0.050, 5, &mut events);
|
||||||
|
|
||||||
assert_eq!(game.ball.position, expected.ball.position);
|
assert_eq!(game.balls[0].position, expected.balls[0].position);
|
||||||
assert_eq!(game.ball.velocity, expected.ball.velocity);
|
assert_eq!(game.balls[0].velocity, expected.balls[0].velocity);
|
||||||
assert_eq!(events, expected_events);
|
assert_eq!(events, expected_events);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn effect_seven_spawn_waits_until_the_next_timer_callback() {
|
fn effect_seven_spawn_waits_until_the_next_timer_callback() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = EFFECT_SENSOR.center;
|
game.balls[0].position = EFFECT_SENSOR.center;
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
|
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
|
||||||
game.multiball_state = MultiballState::Ready;
|
game.multiball_state = MultiballState::Ready;
|
||||||
@@ -2957,7 +2877,7 @@ mod tests {
|
|||||||
game.timer_tick(0.050, 5, &mut Vec::new());
|
game.timer_tick(0.050, 5, &mut Vec::new());
|
||||||
|
|
||||||
let spawned = game
|
let spawned = game
|
||||||
.secondary_ball
|
.secondary()
|
||||||
.expect("the effect-seven request must create slot two after simulation");
|
.expect("the effect-seven request must create slot two after simulation");
|
||||||
assert_eq!(spawned.position, vec2(17.0, 23.0));
|
assert_eq!(spawned.position, vec2(17.0, 23.0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -2970,30 +2890,30 @@ mod tests {
|
|||||||
fn survivor_keeps_ball_two_identity_for_the_rest_of_the_callback() {
|
fn survivor_keeps_ball_two_identity_for_the_rest_of_the_callback() {
|
||||||
let lock = LOCK_HOLES[0];
|
let lock = LOCK_HOLES[0];
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = lock.center;
|
game.balls[0].position = lock.center;
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.ball.capture_age = 300;
|
game.balls[0].capture_age = 300;
|
||||||
game.record_contacts[usize::from(lock.id)] = 1;
|
game.record_contacts[usize::from(lock.id)] = 1;
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: EFFECT_SENSOR.center,
|
position: EFFECT_SENSOR.center,
|
||||||
velocity: Vec2::ZERO,
|
velocity: Vec2::ZERO,
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
|
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
|
||||||
game.multiball_state = MultiballState::Ready;
|
game.multiball_state = MultiballState::Ready;
|
||||||
game.target_effect = 7;
|
game.target_effect = 7;
|
||||||
|
|
||||||
game.timer_tick(0.050, 5, &mut Vec::new());
|
game.timer_tick(0.050, 5, &mut Vec::new());
|
||||||
|
|
||||||
assert!(game.secondary_ball.is_none());
|
assert!(game.secondary().is_none());
|
||||||
assert_eq!(game.target_effect, 0);
|
assert_eq!(game.target_effect, 0);
|
||||||
assert!(!game.effect_target_active());
|
assert!(!game.effect_target_active());
|
||||||
assert_eq!(game.multiball_state, MultiballState::Ready);
|
assert_eq!(game.multiball_state, MultiballState::Ready);
|
||||||
assert!(game.wheel_holes[0]);
|
assert!(game.wheel_holes[0]);
|
||||||
assert!(game.ball.position.y > EFFECT_SENSOR.center.y);
|
assert!(game.balls[0].position.y > EFFECT_SENSOR.center.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3008,14 +2928,14 @@ mod tests {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
assert!(game.launcher_charge > 0.0);
|
assert!(game.launcher_charge > 0.0);
|
||||||
|
|
||||||
let events = game.update(1.0 / 60.0, 3, Controls::default());
|
let events = game.update(1.0 / 60.0, 3, Controls::default());
|
||||||
|
|
||||||
assert!(events.contains(&Event::Launch));
|
assert!(events.contains(&Event::Launch));
|
||||||
assert!(!game.ball.in_launcher);
|
assert!(!game.balls[0].in_launcher);
|
||||||
assert!(game.ball.velocity.y < 0.0);
|
assert!(game.balls[0].velocity.y < 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3027,15 +2947,15 @@ mod tests {
|
|||||||
};
|
};
|
||||||
quick.update(0.0, 3, held);
|
quick.update(0.0, 3, held);
|
||||||
quick.update(0.0, 3, Controls::default());
|
quick.update(0.0, 3, Controls::default());
|
||||||
let quick_speed = -quick.ball.velocity.y;
|
let quick_speed = -quick.balls[0].velocity.y;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(quick.ball.velocity),
|
MilliVec::from_velocity_per_second(quick.balls[0].velocity),
|
||||||
MilliVec { x: 0, y: -2_270 }
|
MilliVec { x: 0, y: -2_270 }
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut charged = Game::new_with_seed(1, 7);
|
let mut charged = Game::new_with_seed(1, 7);
|
||||||
launch_ball(&mut charged, 60);
|
launch_ball(&mut charged, 60);
|
||||||
let charged_speed = -charged.ball.velocity.y;
|
let charged_speed = -charged.balls[0].velocity.y;
|
||||||
|
|
||||||
assert!(charged_speed > quick_speed + 80.0);
|
assert!(charged_speed > quick_speed + 80.0);
|
||||||
}
|
}
|
||||||
@@ -3069,7 +2989,7 @@ mod tests {
|
|||||||
launch_ball(&mut first, 60);
|
launch_ball(&mut first, 60);
|
||||||
launch_ball(&mut second, 60);
|
launch_ball(&mut second, 60);
|
||||||
|
|
||||||
assert_ne!(first.ball.velocity, second.ball.velocity);
|
assert_ne!(first.balls[0].velocity, second.balls[0].velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3079,21 +2999,21 @@ mod tests {
|
|||||||
launch_ball(&mut game, 60);
|
launch_ball(&mut game, 60);
|
||||||
let mut entered_table = false;
|
let mut entered_table = false;
|
||||||
let mut returned_to_launcher = false;
|
let mut returned_to_launcher = false;
|
||||||
let mut minimum_y = game.ball.position.y;
|
let mut minimum_y = game.balls[0].position.y;
|
||||||
|
|
||||||
for _ in 0..480 {
|
for _ in 0..480 {
|
||||||
game.update(1.0 / 120.0, detail, Controls::default());
|
game.update(1.0 / 120.0, detail, Controls::default());
|
||||||
minimum_y = minimum_y.min(game.ball.position.y);
|
minimum_y = minimum_y.min(game.balls[0].position.y);
|
||||||
if game.ball.position.x < 280.0 && game.ball.position.y > 30.0 {
|
if game.balls[0].position.x < 280.0 && game.balls[0].position.y > 30.0 {
|
||||||
entered_table = true;
|
entered_table = true;
|
||||||
}
|
}
|
||||||
returned_to_launcher |= game.ball.in_launcher && !entered_table;
|
returned_to_launcher |= game.balls[0].in_launcher && !entered_table;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
entered_table,
|
entered_table,
|
||||||
"detail {detail}: the shooter curve should guide the ball onto the table; position={:?}, velocity={:?}, minimum_y={minimum_y}",
|
"detail {detail}: the shooter curve should guide the ball onto the table; position={:?}, velocity={:?}, minimum_y={minimum_y}",
|
||||||
game.ball.position, game.ball.velocity
|
game.balls[0].position, game.balls[0].velocity
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!returned_to_launcher,
|
!returned_to_launcher,
|
||||||
@@ -3105,20 +3025,54 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn shooter_stop_rearms_a_returning_ball() {
|
fn shooter_stop_rearms_a_returning_ball() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = Vec2::new(325.0, 410.0);
|
game.balls[0].position = Vec2::new(325.0, 410.0);
|
||||||
game.ball.velocity = Vec2::new(0.0, 80.0);
|
game.balls[0].velocity = Vec2::new(0.0, 80.0);
|
||||||
|
|
||||||
for _ in 0..8 {
|
for _ in 0..8 {
|
||||||
game.update(1.0 / 120.0, 5, Controls::default());
|
game.update(1.0 / 120.0, 5, Controls::default());
|
||||||
if game.ball.in_launcher {
|
if game.balls[0].in_launcher {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
assert_eq!(game.ball.position, LAUNCHER_POSITION);
|
assert_eq!(game.balls[0].position, LAUNCHER_POSITION);
|
||||||
assert_eq!(game.ball.velocity, Vec2::ZERO);
|
assert_eq!(game.balls[0].velocity, Vec2::ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shooter_stop_rearms_a_returning_ball_from_any_slot() {
|
||||||
|
let returning = Ball {
|
||||||
|
position: Vec2::new(325.0, 410.0),
|
||||||
|
velocity: Vec2::new(0.0, 80.0),
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
};
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.balls = vec![
|
||||||
|
Ball {
|
||||||
|
position: vec2(170.0, 112.0),
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
},
|
||||||
|
returning,
|
||||||
|
];
|
||||||
|
for _ in 0..8 {
|
||||||
|
game.update(1.0 / 120.0, 5, Controls::default());
|
||||||
|
}
|
||||||
|
assert_eq!(game.balls.len(), 2);
|
||||||
|
assert!(game.balls[1].in_launcher);
|
||||||
|
|
||||||
|
// With the launcher already occupied the ball leaves the table.
|
||||||
|
let mut occupied = Game::new_with_seed(1, 7);
|
||||||
|
occupied.balls = vec![Ball::default(), returning];
|
||||||
|
for _ in 0..8 {
|
||||||
|
occupied.update(1.0 / 120.0, 5, Controls::default());
|
||||||
|
}
|
||||||
|
assert_eq!(occupied.balls.len(), 1);
|
||||||
|
assert!(occupied.balls[0].in_launcher);
|
||||||
|
assert_eq!(occupied.player().balls, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3126,16 +3080,19 @@ mod tests {
|
|||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.object_active[25] = true;
|
game.object_active[25] = true;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = Vec2::new(320.0, 412.0);
|
game.balls[0].position = Vec2::new(320.0, 412.0);
|
||||||
game.ball.velocity = MilliVec { x: 500, y: 1_000 }.to_velocity_per_second();
|
game.balls[0].velocity = MilliVec { x: 500, y: 1_000 }.to_velocity_per_second();
|
||||||
|
|
||||||
assert!(game.fixed_update(&mut Vec::new()));
|
assert!(game.fixed_update(&mut Vec::new()));
|
||||||
|
|
||||||
assert!(!game.ball.in_launcher);
|
assert!(!game.balls[0].in_launcher);
|
||||||
assert!(game.ball.velocity.y < 0.0);
|
assert!(game.balls[0].velocity.y < 0.0);
|
||||||
assert_eq!(MilliVec::from_velocity_per_second(game.ball.velocity).x, 0);
|
assert_eq!(
|
||||||
assert_eq!(MilliVec::from_position(game.ball.position).x, 320_000);
|
MilliVec::from_velocity_per_second(game.balls[0].velocity).x,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(MilliVec::from_position(game.balls[0].position).x, 320_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3144,11 +3101,11 @@ mod tests {
|
|||||||
game.tilted = true;
|
game.tilted = true;
|
||||||
game.tilt_counter = 39;
|
game.tilt_counter = 39;
|
||||||
|
|
||||||
game.reset_ball_to_launcher();
|
game.reset_ball_to_launcher(0);
|
||||||
|
|
||||||
assert!(!game.tilted);
|
assert!(!game.tilted);
|
||||||
assert_eq!(game.tilt_counter, 0);
|
assert_eq!(game.tilt_counter, 0);
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3221,7 +3178,7 @@ mod tests {
|
|||||||
special.record_contacts[148] = 2;
|
special.record_contacts[148] = 2;
|
||||||
special.drain(&mut Vec::new());
|
special.drain(&mut Vec::new());
|
||||||
assert_eq!(special.ball_double, BallDoubleState::Active);
|
assert_eq!(special.ball_double, BallDoubleState::Active);
|
||||||
assert_eq!(special.ball.position, vec2(17.0, 23.0));
|
assert_eq!(special.balls[0].position, vec2(17.0, 23.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3244,10 +3201,10 @@ mod tests {
|
|||||||
fn tilted_type_three_completion_finishes_or_special_respawns() {
|
fn tilted_type_three_completion_finishes_or_special_respawns() {
|
||||||
let mut lock = Game::new(1);
|
let mut lock = Game::new(1);
|
||||||
lock.tilted = true;
|
lock.tilted = true;
|
||||||
lock.ball.in_launcher = false;
|
lock.balls[0].in_launcher = false;
|
||||||
lock.ball.position = LOCK_HOLES[0].center;
|
lock.balls[0].position = LOCK_HOLES[0].center;
|
||||||
lock.ball.velocity = Vec2::ZERO;
|
lock.balls[0].velocity = Vec2::ZERO;
|
||||||
lock.ball.capture_age = 300;
|
lock.balls[0].capture_age = 300;
|
||||||
let mut lock_events = Vec::new();
|
let mut lock_events = Vec::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
lock.check_sensor_objects(
|
lock.check_sensor_objects(
|
||||||
@@ -3259,17 +3216,17 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(lock.player().balls, 2);
|
assert_eq!(lock.player().balls, 2);
|
||||||
assert_eq!(lock.player().secondary_score, 10_000);
|
assert_eq!(lock.player().secondary_score, 10_000);
|
||||||
assert!(lock.ball.in_launcher);
|
assert!(lock.balls[0].in_launcher);
|
||||||
assert!(!lock.tilted);
|
assert!(!lock.tilted);
|
||||||
assert!(lock_events.contains(&Event::Drain));
|
assert!(lock_events.contains(&Event::Drain));
|
||||||
assert!(!lock_events.contains(&Event::Sound(2008)));
|
assert!(!lock_events.contains(&Event::Sound(2008)));
|
||||||
|
|
||||||
let mut special = Game::new(1);
|
let mut special = Game::new(1);
|
||||||
special.tilted = true;
|
special.tilted = true;
|
||||||
special.ball.in_launcher = false;
|
special.balls[0].in_launcher = false;
|
||||||
special.ball.position = SPECIAL_HOLE_SENSOR.center;
|
special.balls[0].position = SPECIAL_HOLE_SENSOR.center;
|
||||||
special.ball.velocity = Vec2::ZERO;
|
special.balls[0].velocity = Vec2::ZERO;
|
||||||
special.ball.capture_age = 300;
|
special.balls[0].capture_age = 300;
|
||||||
let balls = special.player().balls;
|
let balls = special.player().balls;
|
||||||
let mut special_events = Vec::new();
|
let mut special_events = Vec::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -3281,7 +3238,7 @@ mod tests {
|
|||||||
BallAction::Finish
|
BallAction::Finish
|
||||||
);
|
);
|
||||||
assert_eq!(special.player().balls, balls);
|
assert_eq!(special.player().balls, balls);
|
||||||
assert_eq!(special.ball.position, vec2(17.0, 23.0));
|
assert_eq!(special.balls[0].position, vec2(17.0, 23.0));
|
||||||
assert!(special.tilted);
|
assert!(special.tilted);
|
||||||
assert_eq!(special.multiball_state, MultiballState::Unavailable);
|
assert_eq!(special.multiball_state, MultiballState::Unavailable);
|
||||||
assert_eq!(special.record_contacts[148], 0);
|
assert_eq!(special.record_contacts[148], 0);
|
||||||
@@ -3331,8 +3288,8 @@ mod tests {
|
|||||||
assert!(game.effect_target_active());
|
assert!(game.effect_target_active());
|
||||||
game.target_effect = 3;
|
game.target_effect = 3;
|
||||||
assert_eq!(game.player_effect(), 3);
|
assert_eq!(game.player_effect(), 3);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = EFFECT_SENSOR.center;
|
game.balls[0].position = EFFECT_SENSOR.center;
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(EFFECT_SENSOR.center),
|
MilliVec::from_position(EFFECT_SENSOR.center),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
@@ -3395,7 +3352,7 @@ mod tests {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|sensor| sensor.id == 150)
|
.find(|sensor| sensor.id == 150)
|
||||||
.expect("record 150 must be present");
|
.expect("record 150 must be present");
|
||||||
let mut target_ball = target_game.ball;
|
let mut target_ball = target_game.balls[0];
|
||||||
target_ball.position = sensor.center;
|
target_ball.position = sensor.center;
|
||||||
let mut target_events = Vec::new();
|
let mut target_events = Vec::new();
|
||||||
target_game.check_target_sensors(
|
target_game.check_target_sensors(
|
||||||
@@ -3419,7 +3376,7 @@ mod tests {
|
|||||||
let mut effect_game = Game::new_with_seed(1, 7);
|
let mut effect_game = Game::new_with_seed(1, 7);
|
||||||
effect_game.players[0].score = 139_500;
|
effect_game.players[0].score = 139_500;
|
||||||
effect_game.begin_effect_scenario(1);
|
effect_game.begin_effect_scenario(1);
|
||||||
let mut effect_ball = effect_game.ball;
|
let mut effect_ball = effect_game.balls[0];
|
||||||
effect_ball.position = EFFECT_SENSOR.center;
|
effect_ball.position = EFFECT_SENSOR.center;
|
||||||
let mut effect_events = Vec::new();
|
let mut effect_events = Vec::new();
|
||||||
effect_game.check_effect_sensor(
|
effect_game.check_effect_sensor(
|
||||||
@@ -3464,8 +3421,7 @@ mod tests {
|
|||||||
|
|
||||||
let (_, _, predicted) = game.scan_ordered_records_for_ball(
|
let (_, _, predicted) = game.scan_ordered_records_for_ball(
|
||||||
&mut ball,
|
&mut ball,
|
||||||
1,
|
BallPass::slot(1, 1),
|
||||||
1,
|
|
||||||
old_position,
|
old_position,
|
||||||
&mut velocity,
|
&mut velocity,
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -3543,15 +3499,15 @@ mod tests {
|
|||||||
game.apply_wall_rule(84, &mut events);
|
game.apply_wall_rule(84, &mut events);
|
||||||
assert_eq!(game.target_effect, 7);
|
assert_eq!(game.target_effect, 7);
|
||||||
|
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = EFFECT_SENSOR.center;
|
game.balls[0].position = EFFECT_SENSOR.center;
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(EFFECT_SENSOR.center),
|
MilliVec::from_position(EFFECT_SENSOR.center),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
);
|
);
|
||||||
let spawned = game
|
let spawned = game
|
||||||
.secondary_ball
|
.secondary()
|
||||||
.expect("effect seven should spawn a second ball");
|
.expect("effect seven should spawn a second ball");
|
||||||
assert_eq!(game.multiball_state, MultiballState::Active);
|
assert_eq!(game.multiball_state, MultiballState::Active);
|
||||||
assert!(!game.special_hole_active());
|
assert!(!game.special_hole_active());
|
||||||
@@ -3562,24 +3518,22 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
game.advance_secondary_ball(&mut events);
|
game.advance_secondary_ball(&mut events);
|
||||||
let moving_ball = game
|
let moving_ball = game.secondary().expect("second ball should remain active");
|
||||||
.secondary_ball
|
|
||||||
.expect("second ball should remain active");
|
|
||||||
assert!(moving_ball.position.y > spawned.position.y);
|
assert!(moving_ball.position.y > spawned.position.y);
|
||||||
|
|
||||||
let balls_before = game.player().balls;
|
let balls_before = game.player().balls;
|
||||||
game.drain(&mut events);
|
game.drain(&mut events);
|
||||||
assert!(game.secondary_ball.is_none());
|
assert!(game.secondary().is_none());
|
||||||
assert_eq!(game.player().balls, balls_before);
|
assert_eq!(game.player().balls, balls_before);
|
||||||
assert_eq!(game.ball.position, moving_ball.position);
|
assert_eq!(game.balls[0].position, moving_ball.position);
|
||||||
|
|
||||||
events.clear();
|
events.clear();
|
||||||
game.drain(&mut events);
|
game.drain(&mut events);
|
||||||
assert!(events.is_empty());
|
assert!(events.is_empty());
|
||||||
assert_eq!(game.player().balls, balls_before);
|
assert_eq!(game.player().balls, balls_before);
|
||||||
assert_eq!(game.ball.position, vec2(17.0, 23.0));
|
assert_eq!(game.balls[0].position, vec2(17.0, 23.0));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec { x: 0, y: 3_040 }
|
MilliVec { x: 0, y: 3_040 }
|
||||||
);
|
);
|
||||||
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
||||||
@@ -3588,22 +3542,22 @@ mod tests {
|
|||||||
assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);
|
assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);
|
||||||
|
|
||||||
game.fixed_update(&mut events);
|
game.fixed_update(&mut events);
|
||||||
assert!(!game.ball.in_launcher);
|
assert!(!game.balls[0].in_launcher);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec {
|
MilliVec {
|
||||||
x: 17_000,
|
x: 17_000,
|
||||||
y: 26_055,
|
y: 26_055,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec { x: 0, y: 3_055 }
|
MilliVec { x: 0, y: 3_055 }
|
||||||
);
|
);
|
||||||
assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);
|
assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);
|
||||||
|
|
||||||
game.ball.position = vec2(17.0, 31.0);
|
game.balls[0].position = vec2(17.0, 31.0);
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.fixed_update(&mut events);
|
game.fixed_update(&mut events);
|
||||||
assert_eq!(game.special_hole_gate, SpecialHoleGate::Enabled);
|
assert_eq!(game.special_hole_gate, SpecialHoleGate::Enabled);
|
||||||
}
|
}
|
||||||
@@ -3618,14 +3572,14 @@ mod tests {
|
|||||||
assert_eq!(game.target_effect, 7);
|
assert_eq!(game.target_effect, 7);
|
||||||
assert!(game.special_hole_active());
|
assert!(game.special_hole_active());
|
||||||
|
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = EFFECT_SENSOR.center;
|
game.balls[0].position = EFFECT_SENSOR.center;
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(EFFECT_SENSOR.center),
|
MilliVec::from_position(EFFECT_SENSOR.center),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
);
|
);
|
||||||
assert!(game.secondary_ball.is_some());
|
assert!(game.secondary().is_some());
|
||||||
assert_eq!(game.target_effect, 0);
|
assert_eq!(game.target_effect, 0);
|
||||||
assert!(!game.special_hole_active());
|
assert!(!game.special_hole_active());
|
||||||
|
|
||||||
@@ -3634,9 +3588,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
assert_eq!(game.record_contacts[148], 0);
|
assert_eq!(game.record_contacts[148], 0);
|
||||||
game.drain(&mut events);
|
game.drain(&mut events);
|
||||||
assert!(game.secondary_ball.is_none());
|
assert!(game.secondary().is_none());
|
||||||
game.drain(&mut events);
|
game.drain(&mut events);
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
assert_eq!(game.player().balls, 2);
|
assert_eq!(game.player().balls, 2);
|
||||||
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
||||||
|
|
||||||
@@ -3647,13 +3601,13 @@ mod tests {
|
|||||||
|
|
||||||
game.target_effect = 7;
|
game.target_effect = 7;
|
||||||
game.trigger_flags[usize::from(EFFECT_SENSOR.id)] = false;
|
game.trigger_flags[usize::from(EFFECT_SENSOR.id)] = false;
|
||||||
game.ball.position = EFFECT_SENSOR.center;
|
game.balls[0].position = EFFECT_SENSOR.center;
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(EFFECT_SENSOR.center),
|
MilliVec::from_position(EFFECT_SENSOR.center),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
);
|
);
|
||||||
assert!(game.secondary_ball.is_none());
|
assert!(game.secondary().is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3661,21 +3615,21 @@ mod tests {
|
|||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.multiball_state = MultiballState::Active;
|
game.multiball_state = MultiballState::Active;
|
||||||
game.record_contacts[148] = 2;
|
game.record_contacts[148] = 2;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(157.0, 453.0);
|
game.balls[0].position = vec2(157.0, 453.0);
|
||||||
game.ball.velocity = MilliVec { x: 0, y: 3_000 }.to_velocity_per_second();
|
game.balls[0].velocity = MilliVec { x: 0, y: 3_000 }.to_velocity_per_second();
|
||||||
|
|
||||||
assert!(game.fixed_update(&mut Vec::new()));
|
assert!(game.fixed_update(&mut Vec::new()));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec {
|
MilliVec {
|
||||||
x: 16_698,
|
x: 16_698,
|
||||||
y: 21_191,
|
y: 21_191,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec { x: -302, y: -1_809 }
|
MilliVec { x: -302, y: -1_809 }
|
||||||
);
|
);
|
||||||
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
||||||
@@ -3694,7 +3648,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(events.is_empty());
|
assert!(events.is_empty());
|
||||||
assert_eq!(game.player().balls, balls);
|
assert_eq!(game.player().balls, balls);
|
||||||
assert_eq!(game.ball.position, vec2(17.0, 23.0));
|
assert_eq!(game.balls[0].position, vec2(17.0, 23.0));
|
||||||
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
||||||
assert_eq!(game.record_contacts[148], 0);
|
assert_eq!(game.record_contacts[148], 0);
|
||||||
assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);
|
assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);
|
||||||
@@ -3707,7 +3661,7 @@ mod tests {
|
|||||||
.find(|sensor| sensor.id == 150)
|
.find(|sensor| sensor.id == 150)
|
||||||
.expect("record 150 must be present");
|
.expect("record 150 must be present");
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
let primary_velocity = game.ball.velocity;
|
let primary_velocity = game.balls[0].velocity;
|
||||||
let mut second = Ball {
|
let mut second = Ball {
|
||||||
position: sensor.center,
|
position: sensor.center,
|
||||||
velocity: vec2(12.0, -34.0),
|
velocity: vec2(12.0, -34.0),
|
||||||
@@ -3726,8 +3680,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects_for_ball(
|
game.check_sensor_objects_for_ball(
|
||||||
&mut second,
|
&mut second,
|
||||||
2,
|
BallPass::slot(2, 2),
|
||||||
2,
|
|
||||||
old_position,
|
old_position,
|
||||||
movement,
|
movement,
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -3739,7 +3692,7 @@ mod tests {
|
|||||||
assert!(game.top_targets[0]);
|
assert!(game.top_targets[0]);
|
||||||
assert!(game.magnetic_field_active(153));
|
assert!(game.magnetic_field_active(153));
|
||||||
assert_eq!(game.player().score, sensor.score);
|
assert_eq!(game.player().score, sensor.score);
|
||||||
assert_eq!(game.ball.velocity, primary_velocity);
|
assert_eq!(game.balls[0].velocity, primary_velocity);
|
||||||
assert_ne!(second.velocity, vec2(12.0, -34.0));
|
assert_ne!(second.velocity, vec2(12.0, -34.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3761,8 +3714,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects_for_ball(
|
game.check_sensor_objects_for_ball(
|
||||||
&mut second,
|
&mut second,
|
||||||
2,
|
BallPass::slot(2, 2),
|
||||||
2,
|
|
||||||
MilliVec::from_position(EFFECT_SENSOR.center),
|
MilliVec::from_position(EFFECT_SENSOR.center),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -3771,7 +3723,7 @@ mod tests {
|
|||||||
BallAction::Keep
|
BallAction::Keep
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(game.secondary_ball.is_none());
|
assert!(game.secondary().is_none());
|
||||||
assert_eq!(game.multiball_state, MultiballState::Ready);
|
assert_eq!(game.multiball_state, MultiballState::Ready);
|
||||||
assert_eq!(game.target_effect, 0);
|
assert_eq!(game.target_effect, 0);
|
||||||
assert!(!game.effect_target_active());
|
assert!(!game.effect_target_active());
|
||||||
@@ -3790,15 +3742,14 @@ mod tests {
|
|||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
};
|
};
|
||||||
game.secondary_ball = Some(secondary);
|
game.set_secondary(Some(secondary));
|
||||||
let mut primary = game.ball;
|
let mut primary = game.balls[0];
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects_for_ball(
|
game.check_sensor_objects_for_ball(
|
||||||
&mut primary,
|
&mut primary,
|
||||||
1,
|
BallPass::slot(1, 2),
|
||||||
2,
|
|
||||||
MilliVec::from_position(EFFECT_SENSOR.center),
|
MilliVec::from_position(EFFECT_SENSOR.center),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -3808,7 +3759,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.secondary_ball.map(|ball| ball.position),
|
game.secondary().map(|ball| ball.position),
|
||||||
Some(secondary.position)
|
Some(secondary.position)
|
||||||
);
|
);
|
||||||
assert_eq!(game.multiball_state, MultiballState::Active);
|
assert_eq!(game.multiball_state, MultiballState::Active);
|
||||||
@@ -3819,23 +3770,23 @@ mod tests {
|
|||||||
fn multiball_capture_age_is_per_slot_and_removes_only_ball_two() {
|
fn multiball_capture_age_is_per_slot_and_removes_only_ball_two() {
|
||||||
let sensor = LOCK_HOLES[0];
|
let sensor = LOCK_HOLES[0];
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.capture_age = 17;
|
game.balls[0].capture_age = 17;
|
||||||
game.record_contacts[usize::from(sensor.id)] = 2;
|
game.record_contacts[usize::from(sensor.id)] = 2;
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: sensor.center,
|
position: sensor.center,
|
||||||
velocity: Vec2::ZERO,
|
velocity: Vec2::ZERO,
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 300,
|
capture_age: 300,
|
||||||
});
|
}));
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
game.advance_secondary_ball(&mut events);
|
game.advance_secondary_ball(&mut events);
|
||||||
|
|
||||||
assert!(game.secondary_ball.is_none());
|
assert!(game.secondary().is_none());
|
||||||
assert_eq!(game.score_mode, ScoreMode::Multiball);
|
assert_eq!(game.score_mode, ScoreMode::Multiball);
|
||||||
assert_eq!(game.ball.capture_age, 17);
|
assert_eq!(game.balls[0].capture_age, 17);
|
||||||
assert_eq!(game.record_contacts[usize::from(sensor.id)], 99);
|
assert_eq!(game.record_contacts[usize::from(sensor.id)], 99);
|
||||||
assert!(game.wheel_holes[0]);
|
assert!(game.wheel_holes[0]);
|
||||||
assert!(events.contains(&Event::Lock));
|
assert!(events.contains(&Event::Lock));
|
||||||
@@ -3848,18 +3799,18 @@ mod tests {
|
|||||||
fn captured_ball_one_keeps_multiball_scoring_for_ball_two_slot_pass() {
|
fn captured_ball_one_keeps_multiball_scoring_for_ball_two_slot_pass() {
|
||||||
let sensor = LOCK_HOLES[0];
|
let sensor = LOCK_HOLES[0];
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = sensor.center;
|
game.balls[0].position = sensor.center;
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.ball.capture_age = 300;
|
game.balls[0].capture_age = 300;
|
||||||
game.record_contacts[usize::from(sensor.id)] = 1;
|
game.record_contacts[usize::from(sensor.id)] = 1;
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: EFFECT_SENSOR.center,
|
position: EFFECT_SENSOR.center,
|
||||||
velocity: Vec2::ZERO,
|
velocity: Vec2::ZERO,
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
game.target_effect = 1;
|
game.target_effect = 1;
|
||||||
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
|
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
@@ -3867,7 +3818,7 @@ mod tests {
|
|||||||
|
|
||||||
game.timer_tick(0.01, 1, &mut events);
|
game.timer_tick(0.01, 1, &mut events);
|
||||||
|
|
||||||
assert!(game.secondary_ball.is_none());
|
assert!(game.secondary().is_none());
|
||||||
assert_eq!(game.player().score, 1_000);
|
assert_eq!(game.player().score, 1_000);
|
||||||
assert_eq!(game.player().secondary_score, 20_000);
|
assert_eq!(game.player().secondary_score, 20_000);
|
||||||
assert_eq!(game.score_mode, ScoreMode::Multiball);
|
assert_eq!(game.score_mode, ScoreMode::Multiball);
|
||||||
@@ -3885,7 +3836,7 @@ mod tests {
|
|||||||
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.ball_suspended = true;
|
||||||
game.claw.captured_slot = Some(ClawBallSlot::Primary);
|
game.claw.captured_slot = Some(0);
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
@@ -3897,10 +3848,10 @@ mod tests {
|
|||||||
fn capturing_ball_one_during_multiball_promotes_ball_two() {
|
fn capturing_ball_one_during_multiball_promotes_ball_two() {
|
||||||
let sensor = LOCK_HOLES[0];
|
let sensor = LOCK_HOLES[0];
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = sensor.center;
|
game.balls[0].position = sensor.center;
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.ball.capture_age = 300;
|
game.balls[0].capture_age = 300;
|
||||||
game.record_contacts[usize::from(sensor.id)] = 1;
|
game.record_contacts[usize::from(sensor.id)] = 1;
|
||||||
let survivor = Ball {
|
let survivor = Ball {
|
||||||
position: vec2(210.0, 240.0),
|
position: vec2(210.0, 240.0),
|
||||||
@@ -3909,7 +3860,7 @@ mod tests {
|
|||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 23,
|
capture_age: 23,
|
||||||
};
|
};
|
||||||
game.secondary_ball = Some(survivor);
|
game.set_secondary(Some(survivor));
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
@@ -3918,10 +3869,10 @@ mod tests {
|
|||||||
&mut events,
|
&mut events,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(game.secondary_ball.is_none());
|
assert!(game.secondary().is_none());
|
||||||
assert_eq!(game.ball.position, survivor.position);
|
assert_eq!(game.balls[0].position, survivor.position);
|
||||||
assert_eq!(game.ball.velocity, survivor.velocity);
|
assert_eq!(game.balls[0].velocity, survivor.velocity);
|
||||||
assert_eq!(game.ball.capture_age, survivor.capture_age);
|
assert_eq!(game.balls[0].capture_age, survivor.capture_age);
|
||||||
assert_eq!(game.record_contacts[usize::from(sensor.id)], 99);
|
assert_eq!(game.record_contacts[usize::from(sensor.id)], 99);
|
||||||
assert!(game.wheel_holes[0]);
|
assert!(game.wheel_holes[0]);
|
||||||
}
|
}
|
||||||
@@ -3930,10 +3881,10 @@ mod tests {
|
|||||||
fn occupied_multiball_wheel_hole_rejects_the_surviving_ball() {
|
fn occupied_multiball_wheel_hole_rejects_the_surviving_ball() {
|
||||||
let sensor = LOCK_HOLES[0];
|
let sensor = LOCK_HOLES[0];
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = sensor.center;
|
game.balls[0].position = sensor.center;
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.ball.capture_age = 300;
|
game.balls[0].capture_age = 300;
|
||||||
game.record_contacts[usize::from(sensor.id)] = 99;
|
game.record_contacts[usize::from(sensor.id)] = 99;
|
||||||
game.wheel_holes[0] = true;
|
game.wheel_holes[0] = true;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
@@ -3948,21 +3899,21 @@ mod tests {
|
|||||||
assert_eq!(game.record_contacts[usize::from(sensor.id)], 99);
|
assert_eq!(game.record_contacts[usize::from(sensor.id)], 99);
|
||||||
assert_eq!(game.player().secondary_score, 0);
|
assert_eq!(game.player().secondary_score, 0);
|
||||||
assert!(!events.contains(&Event::Lock));
|
assert!(!events.contains(&Event::Lock));
|
||||||
assert!(!game.ball.in_launcher);
|
assert!(!game.balls[0].in_launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn center_drain_advances_to_a_fresh_ball() {
|
fn center_drain_advances_to_a_fresh_ball() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(157.0, 450.0);
|
game.balls[0].position = vec2(157.0, 450.0);
|
||||||
game.ball.velocity = vec2(0.0, 300.0);
|
game.balls[0].velocity = vec2(0.0, 300.0);
|
||||||
|
|
||||||
let events = game.update(1.0 / 30.0, 5, Controls::default());
|
let events = game.update(1.0 / 30.0, 5, Controls::default());
|
||||||
|
|
||||||
assert!(events.contains(&Event::Drain));
|
assert!(events.contains(&Event::Drain));
|
||||||
assert_eq!(game.players[0].balls, 2);
|
assert_eq!(game.players[0].balls, 2);
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3975,14 +3926,14 @@ mod tests {
|
|||||||
] {
|
] {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = position;
|
game.balls[0].position = position;
|
||||||
game.ball.velocity = velocity.to_velocity_per_second();
|
game.balls[0].velocity = velocity.to_velocity_per_second();
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
assert!(game.fixed_update(&mut events));
|
assert!(game.fixed_update(&mut events));
|
||||||
|
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
assert_eq!(game.player().balls, 2);
|
assert_eq!(game.player().balls, 2);
|
||||||
assert!(events.contains(&Event::Drain));
|
assert!(events.contains(&Event::Drain));
|
||||||
}
|
}
|
||||||
@@ -3992,8 +3943,8 @@ mod tests {
|
|||||||
fn prediction_bounds_remove_only_the_escaping_multiball_slot() {
|
fn prediction_bounds_remove_only_the_escaping_multiball_slot() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(200.0, 200.0);
|
game.balls[0].position = vec2(200.0, 200.0);
|
||||||
let second = Ball {
|
let second = Ball {
|
||||||
position: vec2(1.0, 200.0),
|
position: vec2(1.0, 200.0),
|
||||||
velocity: MilliVec { x: -2_000, y: 0 }.to_velocity_per_second(),
|
velocity: MilliVec { x: -2_000, y: 0 }.to_velocity_per_second(),
|
||||||
@@ -4001,12 +3952,12 @@ mod tests {
|
|||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
};
|
};
|
||||||
game.secondary_ball = Some(second);
|
game.set_secondary(Some(second));
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
|
|
||||||
assert!(game.advance_secondary_ball(&mut Vec::new()));
|
assert!(game.advance_secondary_ball(&mut Vec::new()));
|
||||||
|
|
||||||
assert!(game.secondary_ball.is_none());
|
assert!(game.secondary().is_none());
|
||||||
assert_eq!(game.player().balls, 3);
|
assert_eq!(game.player().balls, 3);
|
||||||
assert_eq!(game.score_mode, ScoreMode::Normal);
|
assert_eq!(game.score_mode, ScoreMode::Normal);
|
||||||
}
|
}
|
||||||
@@ -4054,7 +4005,7 @@ mod tests {
|
|||||||
game.drain(&mut events);
|
game.drain(&mut events);
|
||||||
assert!(events.contains(&Event::HighScoreCandidate {
|
assert!(events.contains(&Event::HighScoreCandidate {
|
||||||
score: 123_456,
|
score: 123_456,
|
||||||
crazy: false,
|
crazy: false
|
||||||
}));
|
}));
|
||||||
assert_eq!(game.current_player, 1);
|
assert_eq!(game.current_player, 1);
|
||||||
assert!(!game.finished);
|
assert!(!game.finished);
|
||||||
@@ -4065,7 +4016,7 @@ mod tests {
|
|||||||
game.drain(&mut events);
|
game.drain(&mut events);
|
||||||
assert!(events.contains(&Event::HighScoreCandidate {
|
assert!(events.contains(&Event::HighScoreCandidate {
|
||||||
score: 654_321,
|
score: 654_321,
|
||||||
crazy: false,
|
crazy: false
|
||||||
}));
|
}));
|
||||||
assert!(game.finished);
|
assert!(game.finished);
|
||||||
}
|
}
|
||||||
@@ -4073,51 +4024,54 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn fast_ball_cannot_tunnel_through_the_top_rail() {
|
fn fast_ball_cannot_tunnel_through_the_top_rail() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(270.0, 30.0);
|
game.balls[0].position = vec2(270.0, 30.0);
|
||||||
game.ball.velocity = vec2(0.0, -380.0);
|
game.balls[0].velocity = vec2(0.0, -380.0);
|
||||||
|
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
game.fixed_update(&mut Vec::new());
|
game.fixed_update(&mut Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(game.ball.position.y >= 15.0);
|
assert!(game.balls[0].position.y >= 15.0);
|
||||||
assert!(game.ball.velocity.y > 0.0);
|
assert!(game.balls[0].velocity.y > 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn relative_record_57_matches_live_two_substep_transition() {
|
fn relative_record_57_matches_live_two_substep_transition() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(72.530, 354.750);
|
game.balls[0].position = vec2(72.530, 354.750);
|
||||||
game.ball.velocity = MilliVec { x: 3_000, y: 0 }.to_velocity_per_second();
|
game.balls[0].velocity = MilliVec { x: 3_000, y: 0 }.to_velocity_per_second();
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
assert!(!game.fixed_update(&mut events));
|
assert!(!game.fixed_update(&mut events));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec {
|
MilliVec {
|
||||||
x: 75_530,
|
x: 75_530,
|
||||||
y: 354_765,
|
y: 354_765,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec { x: 3_000, y: 15 }
|
MilliVec { x: 3_000, y: 15 }
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(game.fixed_update(&mut events));
|
assert!(game.fixed_update(&mut events));
|
||||||
assert_eq!(game.last_collision_id, Some(57));
|
assert_eq!(game.last_collision_id, Some(57));
|
||||||
assert_eq!(game.ball.spin.bytes(), [0x83, 0x5d, 0x8f, 0xc2, 0xf5, 0xa8]);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_position(game.ball.position),
|
game.balls[0].spin.bytes(),
|
||||||
|
[0x83, 0x5d, 0x8f, 0xc2, 0xf5, 0xa8]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec {
|
MilliVec {
|
||||||
x: 77_369,
|
x: 77_369,
|
||||||
y: 356_597,
|
y: 356_597,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec { x: 1_839, y: 1_832 }
|
MilliVec { x: 1_839, y: 1_832 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4125,36 +4079,39 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn relative_record_72_matches_live_two_substep_transition() {
|
fn relative_record_72_matches_live_two_substep_transition() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(238.348, 356.810);
|
game.balls[0].position = vec2(238.348, 356.810);
|
||||||
game.ball.velocity = MilliVec { x: -3_000, y: 0 }.to_velocity_per_second();
|
game.balls[0].velocity = MilliVec { x: -3_000, y: 0 }.to_velocity_per_second();
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
assert!(!game.fixed_update(&mut events));
|
assert!(!game.fixed_update(&mut events));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec {
|
MilliVec {
|
||||||
x: 235_348,
|
x: 235_348,
|
||||||
y: 356_825,
|
y: 356_825,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec { x: -3_000, y: 15 }
|
MilliVec { x: -3_000, y: 15 }
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(game.fixed_update(&mut events));
|
assert!(game.fixed_update(&mut events));
|
||||||
assert_eq!(game.last_collision_id, Some(72));
|
assert_eq!(game.last_collision_id, Some(72));
|
||||||
assert_eq!(game.ball.spin.bytes(), [0x83, 0x86, 0xeb, 0x51, 0xb8, 0x2e]);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_position(game.ball.position),
|
game.balls[0].spin.bytes(),
|
||||||
|
[0x83, 0x86, 0xeb, 0x51, 0xb8, 0x2e]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec {
|
MilliVec {
|
||||||
x: 233_253,
|
x: 233_253,
|
||||||
y: 358_494,
|
y: 358_494,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec {
|
MilliVec {
|
||||||
x: -2_095,
|
x: -2_095,
|
||||||
y: 1_669
|
y: 1_669
|
||||||
@@ -4165,10 +4122,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn retained_collision_candidate_clears_the_slot_capture_age() {
|
fn retained_collision_candidate_clears_the_slot_capture_age() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(270.0, 30.0);
|
game.balls[0].position = vec2(270.0, 30.0);
|
||||||
game.ball.velocity = vec2(0.0, -380.0);
|
game.balls[0].velocity = vec2(0.0, -380.0);
|
||||||
game.ball.capture_age = 42;
|
game.balls[0].capture_age = 42;
|
||||||
|
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
if game.fixed_update(&mut Vec::new()) {
|
if game.fixed_update(&mut Vec::new()) {
|
||||||
@@ -4176,7 +4133,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(game.ball.capture_age, 0);
|
assert_eq!(game.balls[0].capture_age, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4261,9 +4218,9 @@ mod tests {
|
|||||||
weak.object_active.fill(false);
|
weak.object_active.fill(false);
|
||||||
weak.object_active[51] = true;
|
weak.object_active[51] = true;
|
||||||
weak.players[0].bumper_value = 2_000;
|
weak.players[0].bumper_value = 2_000;
|
||||||
weak.ball.in_launcher = false;
|
weak.balls[0].in_launcher = false;
|
||||||
weak.ball.position = vec2(165.0, 171.1);
|
weak.balls[0].position = vec2(165.0, 171.1);
|
||||||
weak.ball.velocity = vec2(0.0, -20.0);
|
weak.balls[0].velocity = vec2(0.0, -20.0);
|
||||||
let mut weak_events = Vec::new();
|
let mut weak_events = Vec::new();
|
||||||
assert!(weak.fixed_update(&mut weak_events));
|
assert!(weak.fixed_update(&mut weak_events));
|
||||||
|
|
||||||
@@ -4271,9 +4228,9 @@ mod tests {
|
|||||||
fast.object_active.fill(false);
|
fast.object_active.fill(false);
|
||||||
fast.object_active[51] = true;
|
fast.object_active[51] = true;
|
||||||
fast.players[0].bumper_value = 2_000;
|
fast.players[0].bumper_value = 2_000;
|
||||||
fast.ball.in_launcher = false;
|
fast.balls[0].in_launcher = false;
|
||||||
fast.ball.position = vec2(165.0, 172.015);
|
fast.balls[0].position = vec2(165.0, 172.015);
|
||||||
fast.ball.velocity = vec2(0.0, -198.5);
|
fast.balls[0].velocity = vec2(0.0, -198.5);
|
||||||
let mut tilted = fast.clone();
|
let mut tilted = fast.clone();
|
||||||
tilted.tilted = true;
|
tilted.tilted = true;
|
||||||
let mut fast_events = Vec::new();
|
let mut fast_events = Vec::new();
|
||||||
@@ -4287,11 +4244,11 @@ mod tests {
|
|||||||
assert!(fast_events.contains(&Event::Sound(2006)));
|
assert!(fast_events.contains(&Event::Sound(2006)));
|
||||||
assert_eq!(weak.record_countdown(51), 5);
|
assert_eq!(weak.record_countdown(51), 5);
|
||||||
assert_eq!(fast.record_countdown(51), 5);
|
assert_eq!(fast.record_countdown(51), 5);
|
||||||
assert!(fast.ball.velocity.y > weak.ball.velocity.y);
|
assert!(fast.balls[0].velocity.y > weak.balls[0].velocity.y);
|
||||||
assert_eq!(tilted.player().score, 0);
|
assert_eq!(tilted.player().score, 0);
|
||||||
assert_eq!(tilted.record_countdown(51), 0);
|
assert_eq!(tilted.record_countdown(51), 0);
|
||||||
assert!(!tilted_events.contains(&Event::Sound(2006)));
|
assert!(!tilted_events.contains(&Event::Sound(2006)));
|
||||||
assert!(fast.ball.velocity.y > tilted.ball.velocity.y);
|
assert!(fast.balls[0].velocity.y > tilted.balls[0].velocity.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4315,9 +4272,9 @@ mod tests {
|
|||||||
game.flippers.left_raised = true;
|
game.flippers.left_raised = true;
|
||||||
game.flippers.right_raised = true;
|
game.flippers.right_raised = true;
|
||||||
let score = game.player().score;
|
let score = game.player().score;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(201.0, 285.0);
|
game.balls[0].position = vec2(201.0, 285.0);
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.update(
|
game.update(
|
||||||
1.0 / 30.0,
|
1.0 / 30.0,
|
||||||
5,
|
5,
|
||||||
@@ -4502,14 +4459,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn raising_flipper_uses_the_reconstructed_moving_response() {
|
fn raising_flipper_uses_the_reconstructed_moving_response() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(125.0, 390.0);
|
game.balls[0].position = vec2(125.0, 390.0);
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.pending_flipper_edges[0] = -1;
|
game.pending_flipper_edges[0] = -1;
|
||||||
|
|
||||||
game.apply_flipper_kicks();
|
game.apply_flipper_kicks();
|
||||||
let velocity_after_edge = game.ball.velocity;
|
let velocity_after_edge = game.balls[0].velocity;
|
||||||
assert_eq!(game.ball.position, vec2(138.124, 355.0));
|
assert_eq!(game.balls[0].position, vec2(138.124, 355.0));
|
||||||
let raw_velocity = MilliVec::from_velocity_per_second(velocity_after_edge);
|
let raw_velocity = MilliVec::from_velocity_per_second(velocity_after_edge);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
raw_velocity,
|
raw_velocity,
|
||||||
@@ -4522,7 +4479,7 @@ mod tests {
|
|||||||
|
|
||||||
game.apply_flipper_kicks();
|
game.apply_flipper_kicks();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.ball.velocity, velocity_after_edge,
|
game.balls[0].velocity, velocity_after_edge,
|
||||||
"holding a raised flipper must not add another impulse"
|
"holding a raised flipper must not add another impulse"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4531,16 +4488,16 @@ mod tests {
|
|||||||
fn returning_flipper_uses_the_raised_record_geometry() {
|
fn returning_flipper_uses_the_raised_record_geometry() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.flippers.left_raised = true;
|
game.flippers.left_raised = true;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(110.0, 410.0);
|
game.balls[0].position = vec2(110.0, 410.0);
|
||||||
game.ball.velocity = vec2(100.0, 200.0);
|
game.balls[0].velocity = vec2(100.0, 200.0);
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
game.timer_tick(0.030, 0, &mut events);
|
game.timer_tick(0.030, 0, &mut events);
|
||||||
|
|
||||||
assert_eq!(events, [Event::FlipperMove, Event::Sound(2021)]);
|
assert_eq!(events, [Event::FlipperMove, Event::Sound(2021)]);
|
||||||
let raw_velocity = MilliVec::from_velocity_per_second(game.ball.velocity);
|
let raw_velocity = MilliVec::from_velocity_per_second(game.balls[0].velocity);
|
||||||
assert_eq!(raw_velocity, MilliVec { x: 765, y: 3_997 });
|
assert_eq!(raw_velocity, MilliVec { x: 765, y: 3_997 });
|
||||||
assert_eq!(game.ball.position, vec2(110.380, 411.984));
|
assert_eq!(game.balls[0].position, vec2(110.380, 411.984));
|
||||||
assert_eq!(game.pending_flipper_edges, [0, 0]);
|
assert_eq!(game.pending_flipper_edges, [0, 0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4558,7 +4515,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn nudge_and_tilt_emit_the_original_ordered_sound_sequence() {
|
fn nudge_and_tilt_emit_the_original_ordered_sound_sequence() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
let controls = Controls {
|
let controls = Controls {
|
||||||
nudge: Nudge::Right,
|
nudge: Nudge::Right,
|
||||||
..Controls::default()
|
..Controls::default()
|
||||||
@@ -4569,7 +4526,7 @@ mod tests {
|
|||||||
[Event::Sound(2019), Event::Nudge]
|
[Event::Sound(2019), Event::Nudge]
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec { x: 690, y: 0 }
|
MilliVec { x: 690, y: 0 }
|
||||||
);
|
);
|
||||||
assert_eq!(game.tilt_counter, 25);
|
assert_eq!(game.tilt_counter, 25);
|
||||||
@@ -4594,7 +4551,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(events, [Event::Sound(2019), Event::Nudge]);
|
assert_eq!(events, [Event::Sound(2019), Event::Nudge]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec { x: -84, y: -660 }
|
MilliVec { x: -84, y: -660 }
|
||||||
);
|
);
|
||||||
assert_eq!(game.tilt_counter, 25);
|
assert_eq!(game.tilt_counter, 25);
|
||||||
@@ -4627,15 +4584,15 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn multiball_nudge_consumes_active_draws_then_updates_both_saved_slots() {
|
fn multiball_nudge_consumes_active_draws_then_updates_both_saved_slots() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.velocity = MilliVec { x: 100, y: 200 }.to_velocity_per_second();
|
game.balls[0].velocity = MilliVec { x: 100, y: 200 }.to_velocity_per_second();
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: vec2(200.0, 200.0),
|
position: vec2(200.0, 200.0),
|
||||||
velocity: MilliVec { x: -300, y: 400 }.to_velocity_per_second(),
|
velocity: MilliVec { x: -300, y: 400 }.to_velocity_per_second(),
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
let mut reference = BorlandRandom::new(7);
|
let mut reference = BorlandRandom::new(7);
|
||||||
let _discarded_active_x = reference.real48();
|
let _discarded_active_x = reference.real48();
|
||||||
let _discarded_active_y = reference.below(20);
|
let _discarded_active_y = reference.below(20);
|
||||||
@@ -4648,13 +4605,13 @@ mod tests {
|
|||||||
game.apply_nudge(Nudge::Center, &mut Vec::new());
|
game.apply_nudge(Nudge::Center, &mut Vec::new());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec {
|
MilliVec {
|
||||||
x: 100 + shared_impulse.x,
|
x: 100 + shared_impulse.x,
|
||||||
y: 200 + shared_impulse.y,
|
y: 200 + shared_impulse.y,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
let secondary = game.secondary_ball.expect("slot two must remain active");
|
let secondary = game.secondary().expect("slot two must remain active");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(secondary.velocity),
|
MilliVec::from_velocity_per_second(secondary.velocity),
|
||||||
MilliVec {
|
MilliVec {
|
||||||
@@ -4708,10 +4665,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn claw_capture_holds_releases_and_returns_in_original_frame_order() {
|
fn claw_capture_holds_releases_and_returns_in_original_frame_order() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER;
|
game.balls[0].position = CLAW_TRIGGER_CENTER;
|
||||||
game.ball.velocity = vec2(120.0, -40.0);
|
game.balls[0].velocity = vec2(120.0, -40.0);
|
||||||
let held_position = game.ball.position;
|
let held_position = game.balls[0].position;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
game.begin_claw_capture(6, &mut events);
|
game.begin_claw_capture(6, &mut events);
|
||||||
@@ -4719,10 +4676,10 @@ mod tests {
|
|||||||
assert_eq!(game.claw.frame, 10);
|
assert_eq!(game.claw.frame, 10);
|
||||||
assert_eq!(game.claw.bank, ClawSpriteBank::Closing);
|
assert_eq!(game.claw.bank, ClawSpriteBank::Closing);
|
||||||
assert!(game.claw.ball_suspended);
|
assert!(game.claw.ball_suspended);
|
||||||
assert_eq!(game.ball.velocity, Vec2::ZERO);
|
assert_eq!(game.balls[0].velocity, Vec2::ZERO);
|
||||||
|
|
||||||
game.timer_tick(CLAW_FRAME_SECONDS / 3.0, 0, &mut events);
|
game.timer_tick(CLAW_FRAME_SECONDS / 3.0, 0, &mut events);
|
||||||
assert_eq!(game.ball.position, held_position);
|
assert_eq!(game.balls[0].position, held_position);
|
||||||
assert_eq!(game.claw.frame, 10);
|
assert_eq!(game.claw.frame, 10);
|
||||||
|
|
||||||
for expected_frame in [9, 8, 7, 6] {
|
for expected_frame in [9, 8, 7, 6] {
|
||||||
@@ -4736,9 +4693,9 @@ mod tests {
|
|||||||
assert_eq!(game.claw.frame, 6);
|
assert_eq!(game.claw.frame, 6);
|
||||||
assert_eq!(game.claw.bank, ClawSpriteBank::Opening);
|
assert_eq!(game.claw.bank, ClawSpriteBank::Opening);
|
||||||
assert!(!game.claw.ball_suspended);
|
assert!(!game.claw.ball_suspended);
|
||||||
assert_eq!(game.ball.position, vec2(270.0, 94.0));
|
assert_eq!(game.balls[0].position, vec2(270.0, 94.0));
|
||||||
assert!((game.ball.velocity.x - -152.0).abs() < 0.001);
|
assert!((game.balls[0].velocity.x - -152.0).abs() < 0.001);
|
||||||
assert!((game.ball.velocity.y - 228.0).abs() < 0.001);
|
assert!((game.balls[0].velocity.y - 228.0).abs() < 0.001);
|
||||||
|
|
||||||
for expected_frame in [7, 8, 9, 10] {
|
for expected_frame in [7, 8, 9, 10] {
|
||||||
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
||||||
@@ -4753,16 +4710,16 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn multiball_claw_capture_keeps_the_ball_and_uses_only_table_releases() {
|
fn multiball_claw_capture_keeps_the_ball_and_uses_only_table_releases() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER;
|
game.balls[0].position = CLAW_TRIGGER_CENTER;
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: vec2(210.0, 240.0),
|
position: vec2(210.0, 240.0),
|
||||||
velocity: vec2(12.0, -34.0),
|
velocity: vec2(12.0, -34.0),
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
@@ -4772,22 +4729,22 @@ mod tests {
|
|||||||
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
BallAction::Keep
|
BallAction::Keep
|
||||||
);
|
);
|
||||||
assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true)));
|
assert_eq!(game.claw.capture_entry, Some((0, true)));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
BallAction::Suspend
|
BallAction::Suspend
|
||||||
);
|
);
|
||||||
assert!(game.claw.active);
|
assert!(game.claw.active);
|
||||||
assert!(game.primary_ball_suspended());
|
assert!(game.ball_suspended(0));
|
||||||
assert!(!game.secondary_ball_suspended());
|
assert!(!game.ball_suspended(1));
|
||||||
assert!(matches!(game.claw.target_frame, 1 | 6 | 7));
|
assert!(matches!(game.claw.target_frame, 1 | 6 | 7));
|
||||||
assert_eq!(game.record_contacts[89], 1);
|
assert_eq!(game.record_contacts[89], 1);
|
||||||
|
|
||||||
let survivor_before = game.secondary_ball.expect("survivor must remain").position;
|
let survivor_before = game.secondary().expect("survivor must remain").position;
|
||||||
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
game.secondary_ball.expect("survivor must remain").position,
|
game.secondary().expect("survivor must remain").position,
|
||||||
survivor_before
|
survivor_before
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4795,15 +4752,15 @@ mod tests {
|
|||||||
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
||||||
}
|
}
|
||||||
assert!(events.contains(&Event::ClawRelease));
|
assert!(events.contains(&Event::ClawRelease));
|
||||||
assert!(!game.primary_ball_suspended());
|
assert!(!game.ball_suspended(0));
|
||||||
assert!(game.secondary_ball.is_some());
|
assert!(game.secondary().is_some());
|
||||||
|
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER + vec2(20.0, 0.0);
|
game.balls[0].position = CLAW_TRIGGER_CENTER + vec2(20.0, 0.0);
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.ball.capture_age = 0;
|
game.balls[0].capture_age = 0;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
),
|
),
|
||||||
@@ -4815,16 +4772,16 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn multiball_claw_restriction_is_latched_before_the_other_ball_drains() {
|
fn multiball_claw_restriction_is_latched_before_the_other_ball_drains() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER;
|
game.balls[0].position = CLAW_TRIGGER_CENTER;
|
||||||
game.ball.velocity = vec2(100.0, 0.0);
|
game.balls[0].velocity = vec2(100.0, 0.0);
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: vec2(210.0, 240.0),
|
position: vec2(210.0, 240.0),
|
||||||
velocity: Vec2::ZERO,
|
velocity: Vec2::ZERO,
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
@@ -4834,40 +4791,40 @@ mod tests {
|
|||||||
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
BallAction::Keep
|
BallAction::Keep
|
||||||
);
|
);
|
||||||
game.secondary_ball = None;
|
game.set_secondary(None);
|
||||||
game.score_mode = ScoreMode::Normal;
|
game.score_mode = ScoreMode::Normal;
|
||||||
|
|
||||||
game.ball.velocity = vec2(100.0, 0.0);
|
game.balls[0].velocity = vec2(100.0, 0.0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
BallAction::Keep
|
BallAction::Keep
|
||||||
);
|
);
|
||||||
assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true)));
|
assert_eq!(game.claw.capture_entry, Some((0, true)));
|
||||||
|
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.ball.capture_age = 300;
|
game.balls[0].capture_age = 300;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
BallAction::Suspend
|
BallAction::Suspend
|
||||||
);
|
);
|
||||||
assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true)));
|
assert_eq!(game.claw.capture_entry, Some((0, true)));
|
||||||
assert!(matches!(game.claw.target_frame, 1 | 6 | 7));
|
assert!(matches!(game.claw.target_frame, 1 | 6 | 7));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn other_multiball_slot_cannot_clear_the_claw_entry_latch() {
|
fn other_multiball_slot_cannot_clear_the_claw_entry_latch() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER;
|
game.balls[0].position = CLAW_TRIGGER_CENTER;
|
||||||
game.ball.velocity = vec2(100.0, 0.0);
|
game.balls[0].velocity = vec2(100.0, 0.0);
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: vec2(210.0, 100.0),
|
position: vec2(210.0, 100.0),
|
||||||
velocity: Vec2::ZERO,
|
velocity: Vec2::ZERO,
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
@@ -4880,15 +4837,14 @@ mod tests {
|
|||||||
),
|
),
|
||||||
BallAction::Keep
|
BallAction::Keep
|
||||||
);
|
);
|
||||||
assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true)));
|
assert_eq!(game.claw.capture_entry, Some((0, true)));
|
||||||
|
|
||||||
let mut secondary = game.secondary_ball.take().expect("secondary ball");
|
let mut secondary = game.take_secondary().expect("secondary ball");
|
||||||
let secondary_position = MilliVec::from_position(secondary.position);
|
let secondary_position = MilliVec::from_position(secondary.position);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects_for_ball(
|
game.check_sensor_objects_for_ball(
|
||||||
&mut secondary,
|
&mut secondary,
|
||||||
2,
|
BallPass::slot(2, 2),
|
||||||
2,
|
|
||||||
secondary_position,
|
secondary_position,
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -4896,23 +4852,23 @@ mod tests {
|
|||||||
.action,
|
.action,
|
||||||
BallAction::Keep
|
BallAction::Keep
|
||||||
);
|
);
|
||||||
game.secondary_ball = Some(secondary);
|
game.set_secondary(Some(secondary));
|
||||||
assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true)));
|
assert_eq!(game.claw.capture_entry, Some((0, true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shallow_claw_exit_clears_only_the_owning_entry_latch() {
|
fn shallow_claw_exit_clears_only_the_owning_entry_latch() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER;
|
game.balls[0].position = CLAW_TRIGGER_CENTER;
|
||||||
game.ball.velocity = vec2(100.0, 0.0);
|
game.balls[0].velocity = vec2(100.0, 0.0);
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: vec2(210.0, 240.0),
|
position: vec2(210.0, 240.0),
|
||||||
velocity: Vec2::ZERO,
|
velocity: Vec2::ZERO,
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
@@ -4925,13 +4881,13 @@ mod tests {
|
|||||||
),
|
),
|
||||||
BallAction::Keep
|
BallAction::Keep
|
||||||
);
|
);
|
||||||
assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true)));
|
assert_eq!(game.claw.capture_entry, Some((0, true)));
|
||||||
|
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER + vec2(20.0, 0.0);
|
game.balls[0].position = CLAW_TRIGGER_CENTER + vec2(20.0, 0.0);
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
),
|
),
|
||||||
@@ -4939,10 +4895,10 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(game.claw.capture_entry, None);
|
assert_eq!(game.claw.capture_entry, None);
|
||||||
|
|
||||||
game.secondary_ball = None;
|
game.set_secondary(None);
|
||||||
game.score_mode = ScoreMode::Normal;
|
game.score_mode = ScoreMode::Normal;
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER;
|
game.balls[0].position = CLAW_TRIGGER_CENTER;
|
||||||
game.ball.velocity = vec2(100.0, 0.0);
|
game.balls[0].velocity = vec2(100.0, 0.0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(CLAW_TRIGGER_CENTER),
|
MilliVec::from_position(CLAW_TRIGGER_CENTER),
|
||||||
@@ -4951,10 +4907,7 @@ mod tests {
|
|||||||
),
|
),
|
||||||
BallAction::Keep
|
BallAction::Keep
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(game.claw.capture_entry, Some((0, false)));
|
||||||
game.claw.capture_entry,
|
|
||||||
Some((ClawBallSlot::Primary, false))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4986,7 +4939,7 @@ mod tests {
|
|||||||
for first_speed in speeds {
|
for first_speed in speeds {
|
||||||
for second_speed in speeds {
|
for second_speed in speeds {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball = Ball {
|
game.balls[0] = Ball {
|
||||||
position: first_position,
|
position: first_position,
|
||||||
velocity: (capture_center - first_position).normalize()
|
velocity: (capture_center - first_position).normalize()
|
||||||
* first_speed,
|
* first_speed,
|
||||||
@@ -4994,14 +4947,14 @@ mod tests {
|
|||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
};
|
};
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: second_position,
|
position: second_position,
|
||||||
velocity: (capture_center - second_position).normalize()
|
velocity: (capture_center - second_position).normalize()
|
||||||
* second_speed,
|
* second_speed,
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
@@ -5020,7 +4973,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if game.secondary_ball.is_none() || game.finished {
|
if game.secondary().is_none() || game.finished {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5037,27 +4990,26 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn secondary_multiball_ball_can_be_held_by_the_claw_without_hiding_primary() {
|
fn secondary_multiball_ball_can_be_held_by_the_claw_without_hiding_primary() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(210.0, 240.0);
|
game.balls[0].position = vec2(210.0, 240.0);
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: CLAW_TRIGGER_CENTER,
|
position: CLAW_TRIGGER_CENTER,
|
||||||
velocity: Vec2::ZERO,
|
velocity: Vec2::ZERO,
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
let mut secondary = game.secondary_ball.take().expect("secondary ball");
|
let mut secondary = game.take_secondary().expect("secondary ball");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects_for_ball(
|
game.check_sensor_objects_for_ball(
|
||||||
&mut secondary,
|
&mut secondary,
|
||||||
2,
|
BallPass::slot(2, 2),
|
||||||
2,
|
|
||||||
claw_position,
|
claw_position,
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -5065,13 +5017,12 @@ mod tests {
|
|||||||
.action,
|
.action,
|
||||||
BallAction::Keep
|
BallAction::Keep
|
||||||
);
|
);
|
||||||
game.secondary_ball = Some(secondary);
|
game.set_secondary(Some(secondary));
|
||||||
let mut secondary = game.secondary_ball.take().expect("secondary ball");
|
let mut secondary = game.take_secondary().expect("secondary ball");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects_for_ball(
|
game.check_sensor_objects_for_ball(
|
||||||
&mut secondary,
|
&mut secondary,
|
||||||
2,
|
BallPass::slot(2, 2),
|
||||||
2,
|
|
||||||
claw_position,
|
claw_position,
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -5079,29 +5030,29 @@ mod tests {
|
|||||||
.action,
|
.action,
|
||||||
BallAction::Suspend
|
BallAction::Suspend
|
||||||
);
|
);
|
||||||
game.secondary_ball = Some(secondary);
|
game.set_secondary(Some(secondary));
|
||||||
|
|
||||||
assert!(!game.primary_ball_suspended());
|
assert!(!game.ball_suspended(0));
|
||||||
assert!(game.secondary_ball_suspended());
|
assert!(game.ball_suspended(1));
|
||||||
assert!(matches!(game.claw.target_frame, 1 | 6 | 7));
|
assert!(matches!(game.claw.target_frame, 1 | 6 | 7));
|
||||||
let primary_before = game.ball.position;
|
let primary_before = game.balls[0].position;
|
||||||
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
||||||
assert_ne!(game.ball.position, primary_before);
|
assert_ne!(game.balls[0].position, primary_before);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn held_claw_ball_survives_the_other_ball_draining() {
|
fn held_claw_ball_survives_the_other_ball_draining() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER;
|
game.balls[0].position = CLAW_TRIGGER_CENTER;
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: vec2(340.0, 100.0),
|
position: vec2(340.0, 100.0),
|
||||||
velocity: vec2(1.0, 0.0),
|
velocity: vec2(1.0, 0.0),
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
@@ -5117,7 +5068,7 @@ mod tests {
|
|||||||
let balls_before = game.player().balls;
|
let balls_before = game.player().balls;
|
||||||
|
|
||||||
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
||||||
assert!(game.secondary_ball.is_none());
|
assert!(game.secondary().is_none());
|
||||||
assert_eq!(game.player().balls, balls_before);
|
assert_eq!(game.player().balls, balls_before);
|
||||||
assert_eq!(game.score_mode, ScoreMode::Normal);
|
assert_eq!(game.score_mode, ScoreMode::Normal);
|
||||||
assert!(game.claw.ball_suspended);
|
assert!(game.claw.ball_suspended);
|
||||||
@@ -5126,32 +5077,31 @@ mod tests {
|
|||||||
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
||||||
}
|
}
|
||||||
assert!(events.contains(&Event::ClawRelease));
|
assert!(events.contains(&Event::ClawRelease));
|
||||||
assert!(!game.ball.in_launcher);
|
assert!(!game.balls[0].in_launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn held_secondary_claw_ball_is_promoted_if_primary_drains() {
|
fn held_secondary_claw_ball_is_promoted_if_primary_drains() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(340.0, 100.0);
|
game.balls[0].position = vec2(340.0, 100.0);
|
||||||
game.ball.velocity = vec2(1.0, 0.0);
|
game.balls[0].velocity = vec2(1.0, 0.0);
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: CLAW_TRIGGER_CENTER,
|
position: CLAW_TRIGGER_CENTER,
|
||||||
velocity: Vec2::ZERO,
|
velocity: Vec2::ZERO,
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
let mut secondary = game.secondary_ball.take().expect("secondary ball");
|
let mut secondary = game.take_secondary().expect("secondary ball");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects_for_ball(
|
game.check_sensor_objects_for_ball(
|
||||||
&mut secondary,
|
&mut secondary,
|
||||||
2,
|
BallPass::slot(2, 2),
|
||||||
2,
|
|
||||||
claw_position,
|
claw_position,
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -5159,13 +5109,12 @@ mod tests {
|
|||||||
.action,
|
.action,
|
||||||
BallAction::Keep
|
BallAction::Keep
|
||||||
);
|
);
|
||||||
game.secondary_ball = Some(secondary);
|
game.set_secondary(Some(secondary));
|
||||||
let mut secondary = game.secondary_ball.take().expect("secondary ball");
|
let mut secondary = game.take_secondary().expect("secondary ball");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects_for_ball(
|
game.check_sensor_objects_for_ball(
|
||||||
&mut secondary,
|
&mut secondary,
|
||||||
2,
|
BallPass::slot(2, 2),
|
||||||
2,
|
|
||||||
claw_position,
|
claw_position,
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
@@ -5173,18 +5122,15 @@ mod tests {
|
|||||||
.action,
|
.action,
|
||||||
BallAction::Suspend
|
BallAction::Suspend
|
||||||
);
|
);
|
||||||
game.secondary_ball = Some(secondary);
|
game.set_secondary(Some(secondary));
|
||||||
assert_eq!(game.record_contacts[89], 2);
|
assert_eq!(game.record_contacts[89], 2);
|
||||||
assert_eq!(
|
assert_eq!(game.claw.capture_entry, Some((1, true)));
|
||||||
game.claw.capture_entry,
|
|
||||||
Some((ClawBallSlot::Secondary, true))
|
|
||||||
);
|
|
||||||
|
|
||||||
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
||||||
assert!(game.secondary_ball.is_none());
|
assert!(game.secondary().is_none());
|
||||||
assert!(game.primary_ball_suspended());
|
assert!(game.ball_suspended(0));
|
||||||
assert_eq!(game.record_contacts[89], 1);
|
assert_eq!(game.record_contacts[89], 1);
|
||||||
assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true)));
|
assert_eq!(game.claw.capture_entry, Some((0, true)));
|
||||||
assert_eq!(game.score_mode, ScoreMode::Normal);
|
assert_eq!(game.score_mode, ScoreMode::Normal);
|
||||||
assert!(events.contains(&Event::Drain));
|
assert!(events.contains(&Event::Drain));
|
||||||
}
|
}
|
||||||
@@ -5205,16 +5151,16 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn claw_reentry_after_multiball_uses_the_current_ball_count() {
|
fn claw_reentry_after_multiball_uses_the_current_ball_count() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER;
|
game.balls[0].position = CLAW_TRIGGER_CENTER;
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.secondary_ball = Some(Ball {
|
game.set_secondary(Some(Ball {
|
||||||
position: vec2(210.0, 240.0),
|
position: vec2(210.0, 240.0),
|
||||||
velocity: Vec2::ZERO,
|
velocity: Vec2::ZERO,
|
||||||
in_launcher: false,
|
in_launcher: false,
|
||||||
spin: Real48::ZERO,
|
spin: Real48::ZERO,
|
||||||
capture_age: 0,
|
capture_age: 0,
|
||||||
});
|
}));
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.score_mode = ScoreMode::Multiball;
|
game.score_mode = ScoreMode::Multiball;
|
||||||
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
let claw_position = MilliVec::from_position(CLAW_TRIGGER_CENTER);
|
||||||
@@ -5227,9 +5173,9 @@ mod tests {
|
|||||||
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
BallAction::Suspend
|
BallAction::Suspend
|
||||||
);
|
);
|
||||||
assert_eq!(game.claw.capture_entry, Some((ClawBallSlot::Primary, true)));
|
assert_eq!(game.claw.capture_entry, Some((0, true)));
|
||||||
|
|
||||||
game.secondary_ball = None;
|
game.set_secondary(None);
|
||||||
game.score_mode = ScoreMode::Normal;
|
game.score_mode = ScoreMode::Normal;
|
||||||
while game.claw.active {
|
while game.claw.active {
|
||||||
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
game.update_claw(CLAW_FRAME_SECONDS, &mut events);
|
||||||
@@ -5242,19 +5188,16 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.expect("a seed must select the launcher release");
|
.expect("a seed must select the launcher release");
|
||||||
game.random = BorlandRandom::new(unrestricted_seed);
|
game.random = BorlandRandom::new(unrestricted_seed);
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER;
|
game.balls[0].position = CLAW_TRIGGER_CENTER;
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
game.ball.capture_age = 0;
|
game.balls[0].capture_age = 0;
|
||||||
game.record_contacts[89] = 0;
|
game.record_contacts[89] = 0;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
BallAction::Keep
|
BallAction::Keep
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(game.claw.capture_entry, Some((0, false)));
|
||||||
game.claw.capture_entry,
|
|
||||||
Some((ClawBallSlot::Primary, false))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
game.check_sensor_objects(claw_position, MilliVec::default(), &mut events),
|
||||||
BallAction::Suspend
|
BallAction::Suspend
|
||||||
@@ -5281,26 +5224,26 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn claw_uses_the_original_circular_trigger_not_a_broad_rectangle() {
|
fn claw_uses_the_original_circular_trigger_not_a_broad_rectangle() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER + vec2(CLAW_TRIGGER_RADIUS + 0.1, 0.0);
|
game.balls[0].position = CLAW_TRIGGER_CENTER + vec2(CLAW_TRIGGER_RADIUS + 0.1, 0.0);
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut Vec::new(),
|
&mut Vec::new(),
|
||||||
);
|
);
|
||||||
assert!(!game.claw.active);
|
assert!(!game.claw.active);
|
||||||
|
|
||||||
game.ball.position = CLAW_TRIGGER_CENTER;
|
game.balls[0].position = CLAW_TRIGGER_CENTER;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
);
|
);
|
||||||
assert!(!game.claw.active);
|
assert!(!game.claw.active);
|
||||||
assert_eq!(events, [Event::Sound(2015)]);
|
assert_eq!(events, [Event::Sound(2015)]);
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
);
|
);
|
||||||
@@ -5312,9 +5255,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn top_sensor_uses_the_original_score_and_contact_latch() {
|
fn top_sensor_uses_the_original_score_and_contact_latch() {
|
||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(205.0, 55.0);
|
game.balls[0].position = vec2(205.0, 55.0);
|
||||||
game.ball.velocity = vec2(100.0, 200.0);
|
game.balls[0].velocity = vec2(100.0, 200.0);
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
@@ -5328,7 +5271,7 @@ mod tests {
|
|||||||
assert!(!game.object_active[6]);
|
assert!(!game.object_active[6]);
|
||||||
assert!(!game.object_active[154]);
|
assert!(!game.object_active[154]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec { x: 1_012, y: 1_926 }
|
MilliVec { x: 1_012, y: 1_926 }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -5351,19 +5294,19 @@ mod tests {
|
|||||||
assert!(!game.object_active[153]);
|
assert!(!game.object_active[153]);
|
||||||
|
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
);
|
);
|
||||||
assert_eq!(game.player().score, 500, "contact must score only once");
|
assert_eq!(game.player().score, 500, "contact must score only once");
|
||||||
|
|
||||||
game.ball.position = vec2(205.0, 62.0);
|
game.balls[0].position = vec2(205.0, 62.0);
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut events,
|
&mut events,
|
||||||
);
|
);
|
||||||
game.ball.position = vec2(205.0, 55.0);
|
game.balls[0].position = vec2(205.0, 55.0);
|
||||||
game.check_sensor_objects(
|
game.check_sensor_objects(
|
||||||
MilliVec::from_position(vec2(205.0, 60.0)),
|
MilliVec::from_position(vec2(205.0, 60.0)),
|
||||||
MilliVec { x: 0, y: -5_000 },
|
MilliVec { x: 0, y: -5_000 },
|
||||||
@@ -5379,7 +5322,7 @@ mod tests {
|
|||||||
.find(|sensor| sensor.id == 140)
|
.find(|sensor| sensor.id == 140)
|
||||||
.expect("record 140 must be present");
|
.expect("record 140 must be present");
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
let mut primary = game.ball;
|
let mut primary = game.balls[0];
|
||||||
primary.position = sensor.center;
|
primary.position = sensor.center;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
@@ -5466,10 +5409,10 @@ mod tests {
|
|||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.object_active[5] = true;
|
game.object_active[5] = true;
|
||||||
game.object_active[6] = true;
|
game.object_active[6] = true;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(149.0, 430.0);
|
game.balls[0].position = vec2(149.0, 430.0);
|
||||||
game.ball.velocity = MilliVec { x: -2_000, y: 0 }.to_velocity_per_second();
|
game.balls[0].velocity = MilliVec { x: -2_000, y: 0 }.to_velocity_per_second();
|
||||||
let old_position = MilliVec::from_position(game.ball.position);
|
let old_position = MilliVec::from_position(game.balls[0].position);
|
||||||
|
|
||||||
let mut expected_game = game.clone();
|
let mut expected_game = game.clone();
|
||||||
let mut expected_motion = MilliVec { x: -2_000, y: 15 };
|
let mut expected_motion = MilliVec { x: -2_000, y: 15 };
|
||||||
@@ -5485,7 +5428,7 @@ mod tests {
|
|||||||
assert!(expected_game.apply_magnetic_record(6, old_position, &mut expected_motion));
|
assert!(expected_game.apply_magnetic_record(6, old_position, &mut expected_motion));
|
||||||
let expected_response = candidate
|
let expected_response = candidate
|
||||||
.2
|
.2
|
||||||
.resolve(expected_motion, expected_game.ball.spin);
|
.resolve(expected_motion, expected_game.balls[0].spin);
|
||||||
let mut expected_velocity = expected_response.velocity;
|
let mut expected_velocity = expected_response.velocity;
|
||||||
expected_velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
expected_velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||||
|
|
||||||
@@ -5493,7 +5436,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(game.last_collision_id, Some(5));
|
assert_eq!(game.last_collision_id, Some(5));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
expected_velocity
|
expected_velocity
|
||||||
);
|
);
|
||||||
assert_eq!(game.random.seed(), expected_game.random.seed());
|
assert_eq!(game.random.seed(), expected_game.random.seed());
|
||||||
@@ -5502,14 +5445,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn wheel_holes_accumulate_and_transfer_the_per_player_secondary_score() {
|
fn wheel_holes_accumulate_and_transfer_the_per_player_secondary_score() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
let expected_bonus = [10_000, 30_000, 70_000, 150_000, 0];
|
let expected_bonus = [10_000, 30_000, 70_000, 150_000, 0];
|
||||||
|
|
||||||
for (index, sensor) in LOCK_HOLES.into_iter().enumerate() {
|
for (index, sensor) in LOCK_HOLES.into_iter().enumerate() {
|
||||||
complete_stationary_type_three_capture(&mut game, sensor.center, &mut events);
|
complete_stationary_type_three_capture(&mut game, sensor.center, &mut events);
|
||||||
assert_eq!(game.player().secondary_score, expected_bonus[index]);
|
assert_eq!(game.player().secondary_score, expected_bonus[index]);
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
}
|
}
|
||||||
assert_eq!(game.player().score, 310_000);
|
assert_eq!(game.player().score, 310_000);
|
||||||
assert_eq!(game.player().score_multiplier, 2);
|
assert_eq!(game.player().score_multiplier, 2);
|
||||||
@@ -5571,7 +5514,7 @@ mod tests {
|
|||||||
assert_eq!(game.score_mode, ScoreMode::Normal);
|
assert_eq!(game.score_mode, ScoreMode::Normal);
|
||||||
|
|
||||||
let mut reset_by_special_hole = game.clone();
|
let mut reset_by_special_hole = game.clone();
|
||||||
reset_by_special_hole.reset_ball_to_launcher();
|
reset_by_special_hole.reset_ball_to_launcher(0);
|
||||||
assert_eq!(reset_by_special_hole.panel_frame, Some(0));
|
assert_eq!(reset_by_special_hole.panel_frame, Some(0));
|
||||||
|
|
||||||
game.score_mode = ScoreMode::Normal;
|
game.score_mode = ScoreMode::Normal;
|
||||||
@@ -5589,10 +5532,10 @@ mod tests {
|
|||||||
fn type_three_capture_pulls_holds_and_publishes_contact_sentinels() {
|
fn type_three_capture_pulls_holds_and_publishes_contact_sentinels() {
|
||||||
let sensor = LOCK_HOLES[0];
|
let sensor = LOCK_HOLES[0];
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = sensor.center + vec2(5.0, 0.0);
|
game.balls[0].position = sensor.center + vec2(5.0, 0.0);
|
||||||
game.ball.velocity = vec2(100.0, 0.0);
|
game.balls[0].velocity = vec2(100.0, 0.0);
|
||||||
let previous = MilliVec::from_position(game.ball.position);
|
let previous = MilliVec::from_position(game.balls[0].position);
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -5607,14 +5550,14 @@ mod tests {
|
|||||||
CaptureStep::Holding
|
CaptureStep::Holding
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
MilliVec { x: 750, y: -150 }
|
MilliVec { x: 750, y: -150 }
|
||||||
);
|
);
|
||||||
assert_eq!(game.record_contacts[usize::from(sensor.id)], 0);
|
assert_eq!(game.record_contacts[usize::from(sensor.id)], 0);
|
||||||
assert_eq!(game.ball.capture_age, 0);
|
assert_eq!(game.balls[0].capture_age, 0);
|
||||||
|
|
||||||
game.ball.position = sensor.center;
|
game.balls[0].position = sensor.center;
|
||||||
game.ball.velocity = Vec2::ZERO;
|
game.balls[0].velocity = Vec2::ZERO;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
capture_primary_record_step(
|
capture_primary_record_step(
|
||||||
&mut game,
|
&mut game,
|
||||||
@@ -5627,10 +5570,10 @@ mod tests {
|
|||||||
CaptureStep::Holding
|
CaptureStep::Holding
|
||||||
);
|
);
|
||||||
assert_eq!(game.record_contacts[usize::from(sensor.id)], 1);
|
assert_eq!(game.record_contacts[usize::from(sensor.id)], 1);
|
||||||
assert_eq!(game.ball.capture_age, 5);
|
assert_eq!(game.balls[0].capture_age, 5);
|
||||||
assert_eq!(events, [Event::Sound(2015)]);
|
assert_eq!(events, [Event::Sound(2015)]);
|
||||||
|
|
||||||
game.ball.capture_age = 300;
|
game.balls[0].capture_age = 300;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
capture_primary_record_step(
|
capture_primary_record_step(
|
||||||
&mut game,
|
&mut game,
|
||||||
@@ -5643,7 +5586,7 @@ mod tests {
|
|||||||
CaptureStep::Complete
|
CaptureStep::Complete
|
||||||
);
|
);
|
||||||
assert_eq!(game.record_contacts[usize::from(sensor.id)], 99);
|
assert_eq!(game.record_contacts[usize::from(sensor.id)], 99);
|
||||||
assert_eq!(game.ball.capture_age, 0);
|
assert_eq!(game.balls[0].capture_age, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -5652,10 +5595,10 @@ mod tests {
|
|||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.object_active[usize::from(sensor.id)] = true;
|
game.object_active[usize::from(sensor.id)] = true;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = sensor.center + vec2(7.5, 0.0);
|
game.balls[0].position = sensor.center + vec2(7.5, 0.0);
|
||||||
game.ball.velocity = MilliVec { x: 3_800, y: 0 }.to_velocity_per_second();
|
game.balls[0].velocity = MilliVec { x: 3_800, y: 0 }.to_velocity_per_second();
|
||||||
let old_position = MilliVec::from_position(game.ball.position);
|
let old_position = MilliVec::from_position(game.balls[0].position);
|
||||||
let mut expected_velocity = MilliVec { x: 3_800, y: 15 };
|
let mut expected_velocity = MilliVec { x: 3_800, y: 15 };
|
||||||
expected_velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
expected_velocity.clamp_speed(MAXIMUM_SPEED_MILLI_PER_STEP);
|
||||||
let damping = Real48::from_bytes([0x80, 0x66, 0x66, 0x66, 0x66, 0x66]);
|
let damping = Real48::from_bytes([0x80, 0x66, 0x66, 0x66, 0x66, 0x66]);
|
||||||
@@ -5671,11 +5614,11 @@ mod tests {
|
|||||||
assert!(!game.fixed_update(&mut Vec::new()));
|
assert!(!game.fixed_update(&mut Vec::new()));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
expected_velocity
|
expected_velocity
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
old_position.add(expected_velocity)
|
old_position.add(expected_velocity)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -5687,18 +5630,18 @@ mod tests {
|
|||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.object_active[usize::from(sensor.id)] = true;
|
game.object_active[usize::from(sensor.id)] = true;
|
||||||
game.record_contacts[usize::from(sensor.id)] = 99;
|
game.record_contacts[usize::from(sensor.id)] = 99;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = sensor.center + vec2(17.0, 0.0);
|
game.balls[0].position = sensor.center + vec2(17.0, 0.0);
|
||||||
game.ball.velocity = MilliVec { x: -3_000, y: 0 }.to_velocity_per_second();
|
game.balls[0].velocity = MilliVec { x: -3_000, y: 0 }.to_velocity_per_second();
|
||||||
let old_position = MilliVec::from_position(game.ball.position);
|
let old_position = MilliVec::from_position(game.balls[0].position);
|
||||||
|
|
||||||
assert!(game.fixed_update(&mut Vec::new()));
|
assert!(game.fixed_update(&mut Vec::new()));
|
||||||
|
|
||||||
let velocity = MilliVec::from_velocity_per_second(game.ball.velocity);
|
let velocity = MilliVec::from_velocity_per_second(game.balls[0].velocity);
|
||||||
assert!(velocity.x > 0, "the type-three rim must reflect the ball");
|
assert!(velocity.x > 0, "the type-three rim must reflect the ball");
|
||||||
assert_eq!(game.last_collision_id, Some(sensor.id));
|
assert_eq!(game.last_collision_id, Some(sensor.id));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
old_position.add(velocity)
|
old_position.add(velocity)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -5710,9 +5653,9 @@ mod tests {
|
|||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.object_active[usize::from(sensor.id)] = true;
|
game.object_active[usize::from(sensor.id)] = true;
|
||||||
game.record_contacts[usize::from(sensor.id)] = 1;
|
game.record_contacts[usize::from(sensor.id)] = 1;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = sensor.center + vec2(23.0, 0.0);
|
game.balls[0].position = sensor.center + vec2(23.0, 0.0);
|
||||||
game.ball.velocity = MilliVec { x: 3_800, y: 0 }.to_velocity_per_second();
|
game.balls[0].velocity = MilliVec { x: 3_800, y: 0 }.to_velocity_per_second();
|
||||||
|
|
||||||
game.fixed_update(&mut Vec::new());
|
game.fixed_update(&mut Vec::new());
|
||||||
|
|
||||||
@@ -5728,12 +5671,12 @@ mod tests {
|
|||||||
let mut game = Game::new_with_seed(1, 7);
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
game.object_active.fill(false);
|
game.object_active.fill(false);
|
||||||
game.object_active[usize::from(sensor.id)] = true;
|
game.object_active[usize::from(sensor.id)] = true;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(205.0, 58.0);
|
game.balls[0].position = vec2(205.0, 58.0);
|
||||||
game.ball.velocity = MilliVec { x: 0, y: -2_000 }.to_velocity_per_second();
|
game.balls[0].velocity = MilliVec { x: 0, y: -2_000 }.to_velocity_per_second();
|
||||||
let old_position = MilliVec::from_position(game.ball.position);
|
let old_position = MilliVec::from_position(game.balls[0].position);
|
||||||
let mut expected_game = game.clone();
|
let mut expected_game = game.clone();
|
||||||
let mut expected_ball = game.ball;
|
let mut expected_ball = game.balls[0];
|
||||||
expected_ball.velocity = MilliVec { x: 0, y: -1_985 }.to_velocity_per_second();
|
expected_ball.velocity = MilliVec { x: 0, y: -1_985 }.to_velocity_per_second();
|
||||||
expected_game.randomize_trigger_velocity(&mut expected_ball);
|
expected_game.randomize_trigger_velocity(&mut expected_ball);
|
||||||
let expected_velocity = MilliVec::from_velocity_per_second(expected_ball.velocity);
|
let expected_velocity = MilliVec::from_velocity_per_second(expected_ball.velocity);
|
||||||
@@ -5742,11 +5685,11 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(game.player().score, 500);
|
assert_eq!(game.player().score, 500);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_velocity_per_second(game.ball.velocity),
|
MilliVec::from_velocity_per_second(game.balls[0].velocity),
|
||||||
expected_velocity
|
expected_velocity
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MilliVec::from_position(game.ball.position),
|
MilliVec::from_position(game.balls[0].position),
|
||||||
old_position.add(expected_velocity)
|
old_position.add(expected_velocity)
|
||||||
);
|
);
|
||||||
assert_eq!(game.random.seed(), expected_game.random.seed());
|
assert_eq!(game.random.seed(), expected_game.random.seed());
|
||||||
@@ -5792,7 +5735,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn target_rotation_uses_six_callbacks_and_rotates_player_state() {
|
fn target_rotation_uses_six_callbacks_and_rotates_player_state() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.wheel_holes = [true, false, true, false, false];
|
game.wheel_holes = [true, false, true, false, false];
|
||||||
game.record_contacts[129..=133].copy_from_slice(&[11, 22, 33, 44, 55]);
|
game.record_contacts[129..=133].copy_from_slice(&[11, 22, 33, 44, 55]);
|
||||||
game.target_rotation_state = Some(0);
|
game.target_rotation_state = Some(0);
|
||||||
@@ -5818,7 +5761,7 @@ mod tests {
|
|||||||
game.timer_tick(0.030, 0, &mut Vec::new());
|
game.timer_tick(0.030, 0, &mut Vec::new());
|
||||||
assert_eq!(game.target_rotation_state, Some(2));
|
assert_eq!(game.target_rotation_state, Some(2));
|
||||||
|
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.timer_tick(0.030, 0, &mut Vec::new());
|
game.timer_tick(0.030, 0, &mut Vec::new());
|
||||||
assert_eq!(game.target_rotation_state, Some(3));
|
assert_eq!(game.target_rotation_state, Some(3));
|
||||||
|
|
||||||
@@ -5840,7 +5783,7 @@ mod tests {
|
|||||||
"launcher idle pauses countdowns"
|
"launcher idle pauses countdowns"
|
||||||
);
|
);
|
||||||
|
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
for expected in (0..5).rev() {
|
for expected in (0..5).rev() {
|
||||||
game.timer_tick(0.030, 0, &mut Vec::new());
|
game.timer_tick(0.030, 0, &mut Vec::new());
|
||||||
assert_eq!(game.record_countdown(51), expected);
|
assert_eq!(game.record_countdown(51), expected);
|
||||||
@@ -5882,9 +5825,9 @@ mod tests {
|
|||||||
fn drain_clears_latched_tilt() {
|
fn drain_clears_latched_tilt() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.tilted = true;
|
game.tilted = true;
|
||||||
game.ball.in_launcher = false;
|
game.balls[0].in_launcher = false;
|
||||||
game.ball.position = vec2(157.0, 454.0);
|
game.balls[0].position = vec2(157.0, 454.0);
|
||||||
game.ball.velocity = vec2(0.0, 200.0);
|
game.balls[0].velocity = vec2(0.0, 200.0);
|
||||||
|
|
||||||
let held = Controls {
|
let held = Controls {
|
||||||
left_flipper: true,
|
left_flipper: true,
|
||||||
@@ -5893,7 +5836,7 @@ mod tests {
|
|||||||
let events = game.update(0.030, 3, held);
|
let events = game.update(0.030, 3, held);
|
||||||
|
|
||||||
assert!(!game.tilted);
|
assert!(!game.tilted);
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
assert!(!events.contains(&Event::Sound(2008)));
|
assert!(!events.contains(&Event::Sound(2008)));
|
||||||
assert!(game.flipper_release_latch[0]);
|
assert!(game.flipper_release_latch[0]);
|
||||||
game.update(0.0, 3, held);
|
game.update(0.0, 3, held);
|
||||||
@@ -5954,7 +5897,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert!(game.tilted);
|
assert!(game.tilted);
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -5972,7 +5915,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
assert!(game.tilted);
|
assert!(game.tilted);
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
|
|
||||||
game.update(
|
game.update(
|
||||||
0.0,
|
0.0,
|
||||||
@@ -5987,7 +5930,7 @@ mod tests {
|
|||||||
let events = game.update(0.0, 3, Controls::default());
|
let events = game.update(0.0, 3, Controls::default());
|
||||||
assert!(events.contains(&Event::Launch));
|
assert!(events.contains(&Event::Launch));
|
||||||
assert!(!events.contains(&Event::Sound(2002)));
|
assert!(!events.contains(&Event::Sound(2002)));
|
||||||
assert!(!game.ball.in_launcher);
|
assert!(!game.balls[0].in_launcher);
|
||||||
assert!(game.tilted);
|
assert!(game.tilted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+33
-35
@@ -187,8 +187,8 @@ pub struct Snapshot {
|
|||||||
pub scenario: &'static str,
|
pub scenario: &'static str,
|
||||||
pub step: u64,
|
pub step: u64,
|
||||||
pub seconds: f64,
|
pub seconds: f64,
|
||||||
pub ball: BallSnapshot,
|
/// Every ball in slot order, including one held by the claw.
|
||||||
pub secondary_ball: Option<BallSnapshot>,
|
pub balls: Vec<BallSnapshot>,
|
||||||
pub claw: ClawSnapshot,
|
pub claw: ClawSnapshot,
|
||||||
pub flippers: FlipperSnapshot,
|
pub flippers: FlipperSnapshot,
|
||||||
pub launcher_charge: f32,
|
pub launcher_charge: f32,
|
||||||
@@ -305,20 +305,18 @@ impl Simulation {
|
|||||||
scenario: self.scenario.name(),
|
scenario: self.scenario.name(),
|
||||||
step: self.step,
|
step: self.step,
|
||||||
seconds: f64::from(step) / SIMULATION_HZ_F64,
|
seconds: f64::from(step) / SIMULATION_HZ_F64,
|
||||||
ball: BallSnapshot {
|
balls: self
|
||||||
x: self.game.ball.position.x,
|
.game
|
||||||
y: self.game.ball.position.y,
|
.balls
|
||||||
velocity_x: self.game.ball.velocity.x,
|
.iter()
|
||||||
velocity_y: self.game.ball.velocity.y,
|
.map(|ball| BallSnapshot {
|
||||||
in_launcher: self.game.ball.in_launcher,
|
x: ball.position.x,
|
||||||
},
|
y: ball.position.y,
|
||||||
secondary_ball: self.game.secondary_ball.map(|ball| BallSnapshot {
|
velocity_x: ball.velocity.x,
|
||||||
x: ball.position.x,
|
velocity_y: ball.velocity.y,
|
||||||
y: ball.position.y,
|
in_launcher: ball.in_launcher,
|
||||||
velocity_x: ball.velocity.x,
|
})
|
||||||
velocity_y: ball.velocity.y,
|
.collect(),
|
||||||
in_launcher: ball.in_launcher,
|
|
||||||
}),
|
|
||||||
claw: ClawSnapshot {
|
claw: ClawSnapshot {
|
||||||
active: self.game.claw.active,
|
active: self.game.claw.active,
|
||||||
frame: self.game.claw.frame,
|
frame: self.game.claw.frame,
|
||||||
@@ -340,7 +338,7 @@ impl Simulation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn autoplay_controls(&mut self) -> Controls {
|
fn autoplay_controls(&mut self) -> Controls {
|
||||||
if self.game.ball.in_launcher {
|
if self.game.balls.iter().any(|ball| ball.in_launcher) {
|
||||||
let launch_down = self.autoplay_launcher_frames < 132;
|
let launch_down = self.autoplay_launcher_frames < 132;
|
||||||
self.autoplay_launcher_frames += 1;
|
self.autoplay_launcher_frames += 1;
|
||||||
Controls {
|
Controls {
|
||||||
@@ -349,11 +347,15 @@ impl Simulation {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.autoplay_launcher_frames = 0;
|
self.autoplay_launcher_frames = 0;
|
||||||
|
let near_flipper = |x_range: std::ops::Range<f32>| {
|
||||||
|
self.game
|
||||||
|
.balls
|
||||||
|
.iter()
|
||||||
|
.any(|ball| ball.position.y > 340.0 && x_range.contains(&ball.position.x))
|
||||||
|
};
|
||||||
Controls {
|
Controls {
|
||||||
left_flipper: self.game.ball.position.y > 340.0
|
left_flipper: near_flipper(0.0..175.0),
|
||||||
&& self.game.ball.position.x < 175.0,
|
right_flipper: near_flipper(140.0..f32::INFINITY),
|
||||||
right_flipper: self.game.ball.position.y > 340.0
|
|
||||||
&& self.game.ball.position.x > 140.0,
|
|
||||||
..Controls::default()
|
..Controls::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -456,16 +458,12 @@ mod tests {
|
|||||||
assert!(event_count("Target") >= 1);
|
assert!(event_count("Target") >= 1);
|
||||||
assert!(event_count("Bumper") >= 1);
|
assert!(event_count("Bumper") >= 1);
|
||||||
assert!(simulation.trace.iter().all(|snapshot| {
|
assert!(simulation.trace.iter().all(|snapshot| {
|
||||||
snapshot.ball.x.is_finite()
|
snapshot.balls.iter().all(|ball| {
|
||||||
&& snapshot.ball.y.is_finite()
|
ball.x.is_finite()
|
||||||
&& snapshot.ball.velocity_x.is_finite()
|
&& ball.y.is_finite()
|
||||||
&& snapshot.ball.velocity_y.is_finite()
|
&& ball.velocity_x.is_finite()
|
||||||
&& snapshot.secondary_ball.as_ref().is_none_or(|ball| {
|
&& ball.velocity_y.is_finite()
|
||||||
ball.x.is_finite()
|
})
|
||||||
&& ball.y.is_finite()
|
|
||||||
&& ball.velocity_x.is_finite()
|
|
||||||
&& ball.velocity_y.is_finite()
|
|
||||||
})
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,11 +471,11 @@ mod tests {
|
|||||||
fn launcher_scenario_holds_for_one_second_then_releases() {
|
fn launcher_scenario_holds_for_one_second_then_releases() {
|
||||||
let mut simulation = Simulation::new(Scenario::Launcher, 1);
|
let mut simulation = Simulation::new(Scenario::Launcher, 1);
|
||||||
simulation.advance_to(60);
|
simulation.advance_to(60);
|
||||||
assert!(simulation.game.ball.in_launcher);
|
assert!(simulation.game.balls[0].in_launcher);
|
||||||
assert!((0.19..=0.21).contains(&simulation.game.launcher_charge));
|
assert!((0.19..=0.21).contains(&simulation.game.launcher_charge));
|
||||||
|
|
||||||
simulation.advance_to(121);
|
simulation.advance_to(121);
|
||||||
assert!(!simulation.game.ball.in_launcher);
|
assert!(!simulation.game.balls[0].in_launcher);
|
||||||
assert!(simulation.trace[121].events.contains(&"Launch".to_owned()));
|
assert!(simulation.trace[121].events.contains(&"Launch".to_owned()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,8 +592,8 @@ mod tests {
|
|||||||
scenario.name()
|
scenario.name()
|
||||||
);
|
);
|
||||||
assert!(captures >= releases);
|
assert!(captures >= releases);
|
||||||
assert!(simulation.game.ball.position.is_finite());
|
assert!(simulation.game.balls[0].position.is_finite());
|
||||||
assert!(simulation.game.ball.velocity.is_finite());
|
assert!(simulation.game.balls[0].velocity.is_finite());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user