Compare commits
8
Commits
3fc1096500
...
27d40504cc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27d40504cc | ||
|
|
158936577a | ||
|
|
17a7b3c328 | ||
|
|
0e8121d027 | ||
|
|
a8cc0fc5fc | ||
|
|
3a00be5661 | ||
|
|
1a216dc795 | ||
|
|
139e641005 |
No files matched your search
@@ -8,6 +8,45 @@ and this project adheres to
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Crazy mode: pressing the main-row `0` key during a game drops ten more balls
|
||||||
|
onto the playfield, up to 64 balls in play. High-score entries reached with
|
||||||
|
crazy mode show a red `CRAZY` label in the table.
|
||||||
|
- 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
|
||||||
|
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.
|
||||||
|
- The release-ball effect frees the ball parked in the special hole even when
|
||||||
|
several balls are already in play; the released ball keeps ownership of the
|
||||||
|
hole until it rolls out.
|
||||||
|
- The claw never drops a ball into the shooter lane while more than one ball is
|
||||||
|
in play, including balls that arrive after the capture began.
|
||||||
|
- Filling the fifth wheel hole during multiball replaces the wheel at once with
|
||||||
|
the normal animation and scoring instead of waiting for multiball to end.
|
||||||
|
Balls in play keep moving, the holes stay closed, and the wheel's outer rim
|
||||||
|
acts as a solid boundary until the replacement finishes. Locking the last
|
||||||
|
remaining ball returns it to the launcher as in single-ball play.
|
||||||
|
- A closed or filled lock hole kicks out a ball that came to rest in its pocket,
|
||||||
|
so it can no longer stay trapped next to the wheel hub.
|
||||||
|
- Release the ball parked in the special hole when the last ball in play
|
||||||
|
leaves, even if another ball is waiting in the launcher (for example after
|
||||||
|
pressing crazy mode before launching).
|
||||||
|
|
||||||
## [1.2.1] - 2026-09-01
|
## [1.2.1] - 2026-09-01
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -66,6 +66,47 @@ 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.
|
||||||
|
- The release-ball effect (effect seven) frees a parked special-hole ball
|
||||||
|
whenever one is parked, not only when exactly one ball is in play. The
|
||||||
|
released ball owns the hole's contact under its own slot number.
|
||||||
|
- The claw re-targets a shooter-lane release (terminal frame 18) to a table
|
||||||
|
release whenever more than one ball is in play during its ride, not only
|
||||||
|
when the capture began in multiball.
|
||||||
|
- A full wheel during multiball starts the 281-frame replacement immediately
|
||||||
|
(the original deferred it to the next launcher reset). The table only freezes
|
||||||
|
while every ball waits in the launcher; otherwise balls keep moving, the lock
|
||||||
|
holes do not capture, and the wheel's outer rim (record 134's center, 48 px
|
||||||
|
ball contact radius) repels balls from outside.
|
||||||
|
- A ball resting inside a filled or closed lock hole's pocket is kicked out
|
||||||
|
from the hole center, radially away from the hub. A capture that removes
|
||||||
|
the last ball of a multiball callback resets it to the launcher like a
|
||||||
|
single-ball capture instead of ending the ball.
|
||||||
|
- The special-hole respawn triggers when the last ball *in play* drains; balls
|
||||||
|
waiting in the launcher do not count. The original checked for exactly one
|
||||||
|
ball and normal scoring, which is the same thing when only one or two balls
|
||||||
|
exist.
|
||||||
|
- 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
|
||||||
|
|||||||
+8
-1
@@ -60,7 +60,7 @@ cargo run -- --simulate claw-6 --at 0.15 \
|
|||||||
Use `--step N` instead of `--at SECONDS` to reproduce one exact update. The
|
Use `--step N` instead of `--at SECONDS` to reproduce one exact update. The
|
||||||
available scenarios are `loading`, `attract`, `highscores`, `name-entry`,
|
available scenarios are `loading`, `attract`, `highscores`, `name-entry`,
|
||||||
`effect-7`, `autoplay`, `launcher`, `flippers`, `panel`, `targets`, `claw-1`, `claw-6`,
|
`effect-7`, `autoplay`, `launcher`, `flippers`, `panel`, `targets`, `claw-1`, `claw-6`,
|
||||||
`claw-7`, and `claw-18`; `--seed N` fixes random choices.
|
`claw-7`, `claw-18`, and `crazy`; `--seed N` fixes random choices.
|
||||||
`autoplay` charges each ball and operates the flippers from live ball position
|
`autoplay` charges each ball and operates the flippers from live ball position
|
||||||
for long end-to-end validation runs.
|
for long end-to-end validation runs.
|
||||||
|
|
||||||
@@ -77,6 +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 more balls | - | `0` (main row) |
|
||||||
|
|
||||||
|
Crazy mode is a modern addition. Each press drops ten balls at fixed points on
|
||||||
|
the playfield, up to 64 balls in play. They are ordinary balls: every ball runs
|
||||||
|
the same physics, sensors, holes, claw, and scoring, and the player's ball ends
|
||||||
|
only when the last one drains. A player who used crazy mode still enters the
|
||||||
|
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,9 +88,29 @@ 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;
|
||||||
|
- ball model: `Game::balls` is one ordered list of identical balls. Each
|
||||||
|
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;
|
||||||
|
- wheel replacement: the 281-frame panel freezes the table only while every
|
||||||
|
ball waits in the launcher; during multiball the lock holes close, the
|
||||||
|
wheel rim (`retain_wheel_rim`) repels balls, and closed holes kick out a
|
||||||
|
ball resting in their pocket (`kick_out_of_lock_hole`);
|
||||||
|
- 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;
|
||||||
- `flipper_physics.rs`: reconstructed moving-flipper geometry and response;
|
- `flipper_physics.rs`: reconstructed moving-flipper geometry and response;
|
||||||
- `table.rs`: source-traceable collision objects recovered from the Win16 table;
|
- `table.rs`: source-traceable collision objects recovered from the Win16 table;
|
||||||
- `persistence.rs`: platform paths, settings, high scores, and legacy import.
|
- `persistence.rs`: platform paths, settings, high scores (with the optional
|
||||||
|
crazy-mode label), and legacy import;
|
||||||
|
- `highscore-server/`: shared SQLite top-ten service; it migrates databases
|
||||||
|
from before crazy mode by adding the `crazy` column.
|
||||||
@@ -8,8 +8,12 @@ GET /api/highscores
|
|||||||
POST /api/highscores
|
POST /api/highscores
|
||||||
```
|
```
|
||||||
|
|
||||||
The POST body is JSON with a 21-character maximum name and a `u32` score. The
|
The POST body is JSON with a 21-character maximum name, a `u32` score, and an
|
||||||
response is the canonical top-ten JSON array.
|
optional `crazy` boolean (default `false`) marking a crazy-mode score. The
|
||||||
|
response is the canonical top-ten JSON array, each entry carrying `crazy`.
|
||||||
|
|
||||||
|
Databases created before crazy mode gain the `crazy` column automatically on
|
||||||
|
startup; existing rows become normal scores.
|
||||||
|
|
||||||
A newly created database starts with the original distributed demo table. An
|
A newly created database starts with the original distributed demo table. An
|
||||||
existing database, including an existing empty database, is left unchanged.
|
existing database, including an existing empty database, is left unchanged.
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ const DEFAULT_HIGH_SCORES: &[(&str, u32)] = &[
|
|||||||
pub struct HighScore {
|
pub struct HighScore {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub score: u32,
|
pub score: u32,
|
||||||
|
/// The score was reached in crazy mode. Clients that predate crazy mode
|
||||||
|
/// neither send nor read it.
|
||||||
|
#[serde(default)]
|
||||||
|
pub crazy: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -53,6 +57,21 @@ impl From<rusqlite::Error> for StoreError {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct HighScoreStore(Arc<Mutex<Connection>>);
|
pub struct HighScoreStore(Arc<Mutex<Connection>>);
|
||||||
|
|
||||||
|
/// Databases created before crazy mode lack the `crazy` column. Adding it
|
||||||
|
/// with a default keeps every existing row as a normal score.
|
||||||
|
fn add_crazy_column(connection: &Connection) -> Result<(), rusqlite::Error> {
|
||||||
|
let has_column = connection
|
||||||
|
.prepare("SELECT 1 FROM pragma_table_info('high_scores') WHERE name = 'crazy'")?
|
||||||
|
.exists([])?;
|
||||||
|
if !has_column {
|
||||||
|
connection.execute_batch(
|
||||||
|
"ALTER TABLE high_scores
|
||||||
|
ADD COLUMN crazy INTEGER NOT NULL DEFAULT 0 CHECK (crazy IN (0, 1));",
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
impl HighScoreStore {
|
impl HighScoreStore {
|
||||||
/// Open or create a SQLite-backed high-score store.
|
/// Open or create a SQLite-backed high-score store.
|
||||||
///
|
///
|
||||||
@@ -83,11 +102,13 @@ impl HighScoreStore {
|
|||||||
"CREATE TABLE IF NOT EXISTS high_scores (
|
"CREATE TABLE IF NOT EXISTS high_scores (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
score INTEGER NOT NULL CHECK (score >= 0 AND score <= 4294967295)
|
score INTEGER NOT NULL CHECK (score >= 0 AND score <= 4294967295),
|
||||||
|
crazy INTEGER NOT NULL DEFAULT 0 CHECK (crazy IN (0, 1))
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS high_scores_order
|
CREATE INDEX IF NOT EXISTS high_scores_order
|
||||||
ON high_scores (score DESC, id ASC);",
|
ON high_scores (score DESC, id ASC);",
|
||||||
)?;
|
)?;
|
||||||
|
add_crazy_column(&connection)?;
|
||||||
if seed_defaults {
|
if seed_defaults {
|
||||||
let transaction = connection.transaction()?;
|
let transaction = connection.transaction()?;
|
||||||
for &(name, score) in DEFAULT_HIGH_SCORES {
|
for &(name, score) in DEFAULT_HIGH_SCORES {
|
||||||
@@ -109,7 +130,7 @@ impl HighScoreStore {
|
|||||||
pub fn list(&self) -> Result<Vec<HighScore>, StoreError> {
|
pub fn list(&self) -> Result<Vec<HighScore>, StoreError> {
|
||||||
let connection = self.0.lock().map_err(|_| StoreError::Poisoned)?;
|
let connection = self.0.lock().map_err(|_| StoreError::Poisoned)?;
|
||||||
let mut statement = connection.prepare(
|
let mut statement = connection.prepare(
|
||||||
"SELECT name, score
|
"SELECT name, score, crazy
|
||||||
FROM high_scores
|
FROM high_scores
|
||||||
ORDER BY score DESC, id ASC
|
ORDER BY score DESC, id ASC
|
||||||
LIMIT ?1",
|
LIMIT ?1",
|
||||||
@@ -123,6 +144,7 @@ impl HighScoreStore {
|
|||||||
Ok(HighScore {
|
Ok(HighScore {
|
||||||
name: row.get(0)?,
|
name: row.get(0)?,
|
||||||
score,
|
score,
|
||||||
|
crazy: row.get(2)?,
|
||||||
})
|
})
|
||||||
})?
|
})?
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
@@ -139,8 +161,8 @@ impl HighScoreStore {
|
|||||||
let mut connection = self.0.lock().map_err(|_| StoreError::Poisoned)?;
|
let mut connection = self.0.lock().map_err(|_| StoreError::Poisoned)?;
|
||||||
let transaction = connection.transaction()?;
|
let transaction = connection.transaction()?;
|
||||||
transaction.execute(
|
transaction.execute(
|
||||||
"INSERT INTO high_scores (name, score) VALUES (?1, ?2)",
|
"INSERT INTO high_scores (name, score, crazy) VALUES (?1, ?2, ?3)",
|
||||||
params![entry.name, i64::from(entry.score)],
|
params![entry.name, i64::from(entry.score), entry.crazy],
|
||||||
)?;
|
)?;
|
||||||
transaction.execute(
|
transaction.execute(
|
||||||
"DELETE FROM high_scores
|
"DELETE FROM high_scores
|
||||||
@@ -205,6 +227,8 @@ where
|
|||||||
struct SubmitRequest {
|
struct SubmitRequest {
|
||||||
name: String,
|
name: String,
|
||||||
score: u32,
|
score: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
crazy: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -247,6 +271,7 @@ fn validate_request(request: &SubmitRequest) -> Result<HighScore, &'static str>
|
|||||||
Ok(HighScore {
|
Ok(HighScore {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
score: request.score,
|
score: request.score,
|
||||||
|
crazy: request.crazy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,11 +443,88 @@ mod tests {
|
|||||||
.map(|&(name, score)| HighScore {
|
.map(|&(name, score)| HighScore {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
score,
|
score,
|
||||||
|
crazy: false,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(scores, expected);
|
assert_eq!(scores, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_crazy_mode_database_is_migrated_in_place() {
|
||||||
|
let directory = tempdir().expect("temporary directory should exist");
|
||||||
|
let database = directory.path().join("highscores.sqlite3");
|
||||||
|
{
|
||||||
|
let connection = Connection::open(&database).expect("database should open");
|
||||||
|
connection
|
||||||
|
.execute_batch(
|
||||||
|
"CREATE TABLE high_scores (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
score INTEGER NOT NULL CHECK (score >= 0 AND score <= 4294967295)
|
||||||
|
);
|
||||||
|
INSERT INTO high_scores (name, score) VALUES ('Old', 500);",
|
||||||
|
)
|
||||||
|
.expect("legacy schema should be created");
|
||||||
|
}
|
||||||
|
|
||||||
|
let store = HighScoreStore::open(&database).expect("legacy database should open");
|
||||||
|
let scores = store
|
||||||
|
.submit(&HighScore {
|
||||||
|
name: "New".to_owned(),
|
||||||
|
score: 900,
|
||||||
|
crazy: true,
|
||||||
|
})
|
||||||
|
.expect("submission should succeed");
|
||||||
|
assert_eq!(
|
||||||
|
scores,
|
||||||
|
[
|
||||||
|
HighScore {
|
||||||
|
name: "New".to_owned(),
|
||||||
|
score: 900,
|
||||||
|
crazy: true,
|
||||||
|
},
|
||||||
|
HighScore {
|
||||||
|
name: "Old".to_owned(),
|
||||||
|
score: 500,
|
||||||
|
crazy: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
drop(store);
|
||||||
|
HighScoreStore::open(&database).expect("migrated database should reopen");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn api_accepts_crazy_flag_and_defaults_it_for_old_clients() {
|
||||||
|
let app = router(HighScoreStore::open_in_memory().expect("database should open"));
|
||||||
|
assert_eq!(submit(&app, "Old client", 10).await, StatusCode::CREATED);
|
||||||
|
let status = request(
|
||||||
|
&app,
|
||||||
|
Request::post("/api/highscores")
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(
|
||||||
|
serde_json::to_vec(&serde_json::json!({
|
||||||
|
"name": "Crazy",
|
||||||
|
"score": 20,
|
||||||
|
"crazy": true,
|
||||||
|
}))
|
||||||
|
.expect("request JSON should encode"),
|
||||||
|
))
|
||||||
|
.expect("request should build"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(status, StatusCode::CREATED);
|
||||||
|
|
||||||
|
let scores = list(&app).await;
|
||||||
|
assert_eq!(
|
||||||
|
scores
|
||||||
|
.iter()
|
||||||
|
.map(|entry| (entry.name.as_str(), entry.crazy))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
[("Crazy", true), ("Old client", false)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn existing_database_is_not_seeded() {
|
fn existing_database_is_not_seeded() {
|
||||||
let directory = tempdir().expect("temporary directory should exist");
|
let directory = tempdir().expect("temporary directory should exist");
|
||||||
|
|||||||
+27
-15
@@ -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),
|
||||||
@@ -222,6 +225,7 @@ pub struct App {
|
|||||||
last_help_click: f64,
|
last_help_click: f64,
|
||||||
name: String,
|
name: String,
|
||||||
pending_score: u32,
|
pending_score: u32,
|
||||||
|
pending_crazy: bool,
|
||||||
attract: AttractAnimation,
|
attract: AttractAnimation,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +254,7 @@ impl App {
|
|||||||
last_help_click: -1.0,
|
last_help_click: -1.0,
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
pending_score: 0,
|
pending_score: 0,
|
||||||
|
pending_crazy: false,
|
||||||
attract: AttractAnimation::default(),
|
attract: AttractAnimation::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,6 +403,9 @@ 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);
|
||||||
|
if is_key_pressed(CRAZY_MODE_KEY) && self.game.as_mut().is_some_and(Game::add_crazy_balls) {
|
||||||
|
self.assets.play(2007, self.sounds_enabled);
|
||||||
|
}
|
||||||
let nudge = if is_key_released(KeyCode::LeftShift) {
|
let nudge = 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) {
|
||||||
@@ -418,16 +426,17 @@ impl App {
|
|||||||
});
|
});
|
||||||
let mut high_score_candidate = None;
|
let mut high_score_candidate = None;
|
||||||
for event in events {
|
for event in events {
|
||||||
if let Event::HighScoreCandidate(score) = event {
|
if let Event::HighScoreCandidate { score, crazy } = event {
|
||||||
high_score_candidate = Some(score);
|
high_score_candidate = Some((score, crazy));
|
||||||
}
|
}
|
||||||
self.play_event(event);
|
self.play_event(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(score) = high_score_candidate
|
if let Some((score, crazy)) = high_score_candidate
|
||||||
&& qualifies_high_score(&self.saved.high_scores, score)
|
&& qualifies_high_score(&self.saved.high_scores, score)
|
||||||
{
|
{
|
||||||
self.pending_score = score;
|
self.pending_score = score;
|
||||||
|
self.pending_crazy = crazy;
|
||||||
"TDK Pinball Player".clone_into(&mut self.name);
|
"TDK Pinball Player".clone_into(&mut self.name);
|
||||||
self.screen = Screen::NameEntry;
|
self.screen = Screen::NameEntry;
|
||||||
return;
|
return;
|
||||||
@@ -562,6 +571,7 @@ impl App {
|
|||||||
let entry = HighScore {
|
let entry = HighScore {
|
||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
score: self.pending_score,
|
score: self.pending_score,
|
||||||
|
crazy: self.pending_crazy,
|
||||||
};
|
};
|
||||||
insert_high_score(&mut self.saved.high_scores, entry.clone());
|
insert_high_score(&mut self.saved.high_scores, entry.clone());
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@@ -859,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,
|
||||||
@@ -901,17 +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(
|
draw_texture(
|
||||||
&self.assets.ball,
|
&self.assets.ball,
|
||||||
ball.position.x - 8.0,
|
ball.position.x - 8.0,
|
||||||
@@ -1218,6 +1221,15 @@ impl App {
|
|||||||
let score = entry.score.to_string();
|
let score = entry.score.to_string();
|
||||||
let width = measure_text(&score, None, 16, 1.0).width;
|
let width = measure_text(&score, None, 16, 1.0).width;
|
||||||
draw_text(&score, 460.0 - width, y, 16.0, BLACK);
|
draw_text(&score, 460.0 - width, y, 16.0, BLACK);
|
||||||
|
if entry.crazy {
|
||||||
|
draw_text(
|
||||||
|
"CRAZY",
|
||||||
|
464.0,
|
||||||
|
y - 1.0,
|
||||||
|
11.0,
|
||||||
|
Color::from_rgba(192, 0, 0, 255),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1432
-906
@@ -47,6 +47,18 @@ const DETAIL_TIMER_SECONDS: [f32; 5] = [0.050, 0.040, 0.030, 0.020, 0.010];
|
|||||||
const DETAIL_SUBSTEPS: [u8; 5] = [5, 4, 3, 2, 1];
|
const DETAIL_SUBSTEPS: [u8; 5] = [5, 4, 3, 2, 1];
|
||||||
const CLAW_TERMINAL_FRAMES: [u8; 4] = [1, 6, 7, 18];
|
const CLAW_TERMINAL_FRAMES: [u8; 4] = [1, 6, 7, 18];
|
||||||
const CLAW_TABLE_TERMINAL_FRAMES: [u8; 3] = [1, 6, 7];
|
const CLAW_TABLE_TERMINAL_FRAMES: [u8; 3] = [1, 6, 7];
|
||||||
|
/// Claw terminal frame that drops the released ball into the shooter lane.
|
||||||
|
const CLAW_SHOOTER_LANE_FRAME: u8 = 18;
|
||||||
|
/// Wheel hub record 134; its center is also the center of the lock wheel.
|
||||||
|
const WHEEL_HUB_ID: u8 = 134;
|
||||||
|
/// Ball-center contact radius of the wheel's outer rim (about 40 px of
|
||||||
|
/// artwork plus the 8 px ball radius), so it encloses the five hole posts.
|
||||||
|
const WHEEL_RIM_CONTACT_RADIUS: f32 = 48.0;
|
||||||
|
/// How far (in millipixels) a ball must sit inside a closed lock hole's rim
|
||||||
|
/// before the hole kicks it out. Balls arriving from outside are reflected by
|
||||||
|
/// the rim before they get this deep.
|
||||||
|
const CLOSED_HOLE_KICKOUT_DEPTH_MILLI: i32 = 4_000;
|
||||||
|
const CLOSED_HOLE_KICKOUT_SPEED_MILLI: f32 = 3_000.0;
|
||||||
const ORIGINAL_BALL_SPEED_PER_SECOND: f32 = 380.0;
|
const ORIGINAL_BALL_SPEED_PER_SECOND: f32 = 380.0;
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_wrap)]
|
#[allow(clippy::cast_possible_wrap)]
|
||||||
@@ -88,7 +100,7 @@ pub enum Event {
|
|||||||
Nudge,
|
Nudge,
|
||||||
Tilt,
|
Tilt,
|
||||||
Drain,
|
Drain,
|
||||||
HighScoreCandidate(u32),
|
HighScoreCandidate { score: u32, crazy: bool },
|
||||||
StopSound,
|
StopSound,
|
||||||
Sound(u16),
|
Sound(u16),
|
||||||
}
|
}
|
||||||
@@ -111,7 +123,7 @@ impl Event {
|
|||||||
| Self::Nudge
|
| Self::Nudge
|
||||||
| Self::Tilt
|
| Self::Tilt
|
||||||
| Self::Drain
|
| Self::Drain
|
||||||
| Self::HighScoreCandidate(_)
|
| Self::HighScoreCandidate { .. }
|
||||||
| Self::StopSound => None,
|
| Self::StopSound => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,32 +142,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,
|
||||||
}
|
}
|
||||||
@@ -208,9 +206,18 @@ pub struct Player {
|
|||||||
pub bumper_value: u32,
|
pub bumper_value: u32,
|
||||||
pub diamond_segments: u8,
|
pub diamond_segments: u8,
|
||||||
pub media_level: u8,
|
pub media_level: u8,
|
||||||
|
crazy_mode: CrazyMode,
|
||||||
rules: RuleState,
|
rules: RuleState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
/// Whether this player dropped crazy-mode balls during the game. Their
|
||||||
|
/// high score is labelled accordingly.
|
||||||
|
pub fn crazy_mode_used(&self) -> bool {
|
||||||
|
self.crazy_mode == CrazyMode::Used
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for Player {
|
impl Default for Player {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -222,6 +229,7 @@ impl Default for Player {
|
|||||||
bumper_value: 1_000,
|
bumper_value: 1_000,
|
||||||
diamond_segments: 0,
|
diamond_segments: 0,
|
||||||
media_level: 0,
|
media_level: 0,
|
||||||
|
crazy_mode: CrazyMode::Unused,
|
||||||
rules: RuleState::default(),
|
rules: RuleState::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,6 +272,13 @@ enum BallDoubleState {
|
|||||||
Active,
|
Active,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
enum CrazyMode {
|
||||||
|
#[default]
|
||||||
|
Unused,
|
||||||
|
Used,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
enum SpecialHoleGate {
|
enum SpecialHoleGate {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -287,6 +302,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,
|
||||||
@@ -365,12 +403,38 @@ impl Default for Ball {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Upper bound on balls in play. Type-three contact records store a ball's
|
||||||
|
/// 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] = [
|
||||||
|
vec2(170.0, 112.0),
|
||||||
|
vec2(250.0, 140.0),
|
||||||
|
vec2(120.0, 160.0),
|
||||||
|
vec2(90.0, 215.0),
|
||||||
|
vec2(250.0, 215.0),
|
||||||
|
vec2(110.0, 260.0),
|
||||||
|
vec2(215.0, 260.0),
|
||||||
|
vec2(160.0, 300.0),
|
||||||
|
vec2(120.0, 340.0),
|
||||||
|
vec2(195.0, 335.0),
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
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
|
||||||
|
/// callback and which slot number owns a contact record. The list holds
|
||||||
|
/// at least one ball (possibly waiting in the launcher) until the game
|
||||||
|
/// finishes.
|
||||||
|
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,
|
||||||
@@ -414,8 +478,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(),
|
||||||
wheel_holes: [false; 5],
|
wheel_holes: [false; 5],
|
||||||
top_targets: [false; 3],
|
top_targets: [false; 3],
|
||||||
tilted: false,
|
tilted: false,
|
||||||
@@ -484,11 +548,43 @@ impl Game {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
if self.finished || self.balls.len() >= MAX_BALLS {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.players[self.current_player].crazy_mode = CrazyMode::Used;
|
||||||
|
let room = MAX_BALLS - self.balls.len();
|
||||||
|
self.balls.extend(
|
||||||
|
CRAZY_BALL_POSITIONS
|
||||||
|
.into_iter()
|
||||||
|
.take(room)
|
||||||
|
.map(|position| Ball {
|
||||||
|
position,
|
||||||
|
velocity: Vec2::ZERO,
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
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
|
||||||
@@ -506,8 +602,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);
|
||||||
@@ -515,7 +611,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);
|
||||||
@@ -527,8 +623,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,
|
||||||
}
|
}
|
||||||
@@ -566,7 +663,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);
|
||||||
@@ -587,7 +684,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));
|
||||||
@@ -610,72 +707,20 @@ 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 {
|
// The single-ball wheel replacement runs with its ball parked in the
|
||||||
let had_secondary_ball = self.secondary_ball.is_some();
|
// launcher and freezes the table; balls still in play keep moving.
|
||||||
let mut spawned_secondary = None;
|
if !panel_active || self.balls.iter().any(|ball| !ball.in_launcher) {
|
||||||
let primary_held = self.claw_holds_slot(ClawBallSlot::Primary);
|
self.advance_ball_slots(substeps, events);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pending_flipper_edges[0] =
|
self.pending_flipper_edges[0] =
|
||||||
@@ -703,6 +748,69 @@ 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.publish_spawned_balls();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds balls released from the special hole to the end of the slot
|
||||||
|
/// order. The released ball owns the special-hole contact until it rolls
|
||||||
|
/// out; with one ball already in play that is slot two, as in the
|
||||||
|
/// original.
|
||||||
|
fn publish_spawned_balls(&mut self) {
|
||||||
|
for ball in std::mem::take(&mut self.spawned_balls) {
|
||||||
|
if self.balls.len() >= MAX_BALLS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] =
|
||||||
|
u16::try_from(self.balls.len() + 1).unwrap_or(2);
|
||||||
|
self.balls.push(ball);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -767,7 +875,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;
|
||||||
@@ -793,9 +901,8 @@ impl Game {
|
|||||||
}
|
}
|
||||||
Nudge::None => return,
|
Nudge::None => return,
|
||||||
}
|
}
|
||||||
let primary_held = self.claw_holds_slot(ClawBallSlot::Primary);
|
let held = self.held_ball();
|
||||||
let secondary_held = self.claw_holds_slot(ClawBallSlot::Secondary);
|
if self.balls.len() > 1 {
|
||||||
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 },
|
||||||
@@ -811,20 +918,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);
|
||||||
@@ -842,70 +946,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();
|
||||||
@@ -913,90 +1023,130 @@ 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();
|
// The original continues the drained ball's response on the
|
||||||
let response_spin = self.ball.spin;
|
// ball respawned in its slot.
|
||||||
self.drain(events);
|
let respawn_in_place = self.balls.len() == 1 && self.special_respawn_pending(index);
|
||||||
if special_respawn {
|
let response_spin = ball.spin;
|
||||||
let origin = MilliVec::from_position(self.ball.position);
|
let step = self.lose_ball(index, events);
|
||||||
self.ball.position = origin.add(velocity).to_position();
|
if respawn_in_place {
|
||||||
self.ball.velocity = velocity.to_velocity_per_second();
|
let respawned = &mut self.balls[index];
|
||||||
self.ball.spin = response_spin;
|
let origin = MilliVec::from_position(respawned.position);
|
||||||
|
respawned.position = origin.add(velocity).to_position();
|
||||||
|
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, which returns to the
|
||||||
.expect("a multiball capture must leave the other slot active");
|
// launcher exactly like a single-ball capture.
|
||||||
self.promote_claw_secondary_to_primary();
|
if self.balls.len() == 1 {
|
||||||
return true;
|
if self.tilted {
|
||||||
|
return self.lose_ball(index, events);
|
||||||
|
}
|
||||||
|
self.reset_ball_to_launcher(index);
|
||||||
|
return BallStep::Stop;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// While the wheel is being replaced its holes are closed and the
|
||||||
|
/// wheel's outer rim repels balls arriving from outside.
|
||||||
|
fn retain_wheel_rim(
|
||||||
|
old_position: MilliVec,
|
||||||
|
predicted: MilliVec,
|
||||||
|
velocity: MilliVec,
|
||||||
|
best: &mut Option<(u8, bool, StaticCollisionCandidate)>,
|
||||||
|
) {
|
||||||
|
let Some(hub) = PASSIVE_CIRCLES
|
||||||
|
.iter()
|
||||||
|
.find(|circle| circle.id == WHEEL_HUB_ID)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(candidate) = circle_collision_candidate_at(
|
||||||
|
old_position,
|
||||||
|
predicted,
|
||||||
|
velocity,
|
||||||
|
hub.center,
|
||||||
|
WHEEL_RIM_CONTACT_RADIUS,
|
||||||
|
CollisionMaterial::circle(
|
||||||
|
f64::from(hub.normal_rebound),
|
||||||
|
f64::from(hub.tangent_coupling),
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
retain_first_collision(best, (WHEEL_HUB_ID, false, candidate));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn retain_static_range(
|
fn retain_static_range(
|
||||||
@@ -1017,12 +1167,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>,
|
||||||
@@ -1046,8 +1195,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,
|
||||||
@@ -1057,9 +1205,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 => {}
|
||||||
@@ -1072,8 +1220,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,
|
||||||
@@ -1084,6 +1231,9 @@ impl Game {
|
|||||||
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
*velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.panel_frame.is_some() {
|
||||||
|
Self::retain_wheel_rim(old_position, predicted, *velocity, &mut best);
|
||||||
|
}
|
||||||
self.retain_static_range(old_position, predicted, *velocity, 134..=139, &mut best);
|
self.retain_static_range(old_position, predicted, *velocity, 134..=139, &mut best);
|
||||||
if old_position.y < 250_000 {
|
if old_position.y < 250_000 {
|
||||||
ball.velocity = velocity.to_velocity_per_second();
|
ball.velocity = velocity.to_velocity_per_second();
|
||||||
@@ -1103,8 +1253,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,
|
||||||
@@ -1115,7 +1264,6 @@ 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,
|
|
||||||
old_position,
|
old_position,
|
||||||
movement_from_prediction(old_position, predicted),
|
movement_from_prediction(old_position, predicted),
|
||||||
events,
|
events,
|
||||||
@@ -1275,124 +1423,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 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>) {
|
||||||
@@ -1534,30 +1576,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.publish_spawned_balls();
|
||||||
scan.action
|
scan.action
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1565,8 +1607,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>,
|
||||||
@@ -1595,8 +1636,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,
|
||||||
@@ -1607,9 +1647,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1617,8 +1657,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,
|
||||||
@@ -1629,8 +1668,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,
|
||||||
@@ -1638,7 +1676,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, 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 }
|
||||||
}
|
}
|
||||||
@@ -1651,8 +1689,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,
|
||||||
@@ -1661,6 +1698,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);
|
||||||
@@ -1676,7 +1718,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;
|
||||||
}
|
}
|
||||||
@@ -1690,6 +1732,13 @@ impl Game {
|
|||||||
let contact = self.record_contacts[contact_index];
|
let contact = self.record_contacts[contact_index];
|
||||||
let contact_allowed =
|
let contact_allowed =
|
||||||
contact == 0 || (contact != 99 && ball_count == 1) || contact == ball_number;
|
contact == 0 || (contact != 99 && ball_count == 1) || contact == ball_number;
|
||||||
|
if (129..=133).contains(&record_id)
|
||||||
|
&& contact == 99
|
||||||
|
&& surface_distance < -CLOSED_HOLE_KICKOUT_DEPTH_MILLI
|
||||||
|
{
|
||||||
|
kick_out_of_lock_hole(ball, center_view);
|
||||||
|
return CaptureStep::Outside;
|
||||||
|
}
|
||||||
|
|
||||||
if surface_distance < -11_000
|
if surface_distance < -11_000
|
||||||
&& contact_allowed
|
&& contact_allowed
|
||||||
@@ -1699,8 +1748,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);
|
||||||
@@ -1757,16 +1805,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,
|
||||||
@@ -1785,18 +1843,34 @@ 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)>,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
) -> Option<BallAction> {
|
) -> Option<BallAction> {
|
||||||
|
if self.panel_frame.is_some() {
|
||||||
|
// The wheel is being replaced: no hole captures, and a ball that
|
||||||
|
// was resting in a hole's pocket is kicked back onto the table.
|
||||||
|
for sensor in LOCK_HOLES {
|
||||||
|
let center = MilliVec::from_position(sensor.center);
|
||||||
|
let current = MilliVec::from_position(ball.position);
|
||||||
|
let offset = MilliVec {
|
||||||
|
x: center.x.wrapping_sub(current.x),
|
||||||
|
y: center.y.wrapping_sub(current.y).wrapping_sub(2_000),
|
||||||
|
};
|
||||||
|
let radius = MilliVec::from_position(vec2(sensor.radius, 0.0)).x;
|
||||||
|
if milli_distance(offset) - radius < -CLOSED_HOLE_KICKOUT_DEPTH_MILLI {
|
||||||
|
kick_out_of_lock_hole(ball, sensor.center);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
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,
|
||||||
@@ -1808,7 +1882,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
|
||||||
}
|
}
|
||||||
@@ -1842,7 +1916,10 @@ impl Game {
|
|||||||
player.secondary_score = 0;
|
player.secondary_score = 0;
|
||||||
player.score_multiplier = player.score_multiplier.wrapping_add(1);
|
player.score_multiplier = player.score_multiplier.wrapping_add(1);
|
||||||
player.extra_balls = player.extra_balls.wrapping_add(1);
|
player.extra_balls = player.extra_balls.wrapping_add(1);
|
||||||
if ball_count == 1 {
|
// Deliberate original-game divergence: the original deferred the
|
||||||
|
// wheel replacement until multiball ended. It now starts at once,
|
||||||
|
// and balls still in play run against the closed wheel rim.
|
||||||
|
if ball_count == 1 || self.panel_frame.is_none() {
|
||||||
self.panel_frame = Some(0);
|
self.panel_frame = Some(0);
|
||||||
}
|
}
|
||||||
self.score_mode = ScoreMode::Normal;
|
self.score_mode = ScoreMode::Normal;
|
||||||
@@ -1863,8 +1940,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)>,
|
||||||
@@ -1872,8 +1948,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,
|
||||||
@@ -1887,7 +1962,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 {
|
||||||
@@ -1901,7 +1976,6 @@ impl Game {
|
|||||||
fn check_effect_sensor(
|
fn check_effect_sensor(
|
||||||
&mut self,
|
&mut self,
|
||||||
ball: &mut Ball,
|
ball: &mut Ball,
|
||||||
ball_number: u16,
|
|
||||||
old_position: MilliVec,
|
old_position: MilliVec,
|
||||||
movement_velocity: MilliVec,
|
movement_velocity: MilliVec,
|
||||||
events: &mut Vec<Event>,
|
events: &mut Vec<Event>,
|
||||||
@@ -1942,16 +2016,10 @@ impl Game {
|
|||||||
self.add_score(transfer, events);
|
self.add_score(transfer, events);
|
||||||
self.players[self.current_player].secondary_score = 0;
|
self.players[self.current_player].secondary_score = 0;
|
||||||
}
|
}
|
||||||
7 if self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] != 0
|
// Release the ball parked in the special hole, however
|
||||||
&& self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)]
|
// many balls are already in play.
|
||||||
!= ball_number
|
7 if self.special_hole_active() && self.spawned_balls.is_empty() => {
|
||||||
&& self.secondary_ball.is_none()
|
self.spawned_balls.push(Ball {
|
||||||
&& matches!(
|
|
||||||
self.multiball_state,
|
|
||||||
MultiballState::Ready | MultiballState::Active
|
|
||||||
) =>
|
|
||||||
{
|
|
||||||
self.secondary_ball = Some(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,
|
||||||
@@ -2036,7 +2104,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)
|
||||||
@@ -2045,7 +2113,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;
|
||||||
@@ -2135,55 +2203,95 @@ 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;
|
||||||
|
}
|
||||||
|
// While multiball is active the special-hole contact names the
|
||||||
|
// released ball rather than a parked one.
|
||||||
|
let special_owner = (self.multiball_state == MultiballState::Active)
|
||||||
|
.then_some(usize::from(SPECIAL_HOLE_SENSOR.id));
|
||||||
|
for contact_index in (129..=133).chain(special_owner) {
|
||||||
|
let contact = &mut self.record_contacts[contact_index];
|
||||||
|
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 {
|
let special_respawn = self.special_respawn_pending(index);
|
||||||
self.claw_holds_slot(ClawBallSlot::Secondary)
|
self.take_ball_out_of_play(index);
|
||||||
|
if special_respawn {
|
||||||
|
let released = self.release_parked_ball();
|
||||||
|
self.balls.push(released);
|
||||||
|
}
|
||||||
|
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),
|
||||||
@@ -2191,13 +2299,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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2212,11 +2317,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));
|
||||||
}
|
}
|
||||||
@@ -2224,7 +2329,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>,
|
||||||
) {
|
) {
|
||||||
@@ -2236,7 +2341,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;
|
||||||
@@ -2248,6 +2353,14 @@ impl Game {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.claw.frame_accumulator += dt;
|
self.claw.frame_accumulator += dt;
|
||||||
|
// Never send a ball down the shooter lane while other balls are in
|
||||||
|
// play, even when more balls arrived after the capture began.
|
||||||
|
if self.claw.ball_suspended
|
||||||
|
&& self.claw.target_frame == CLAW_SHOOTER_LANE_FRAME
|
||||||
|
&& self.balls.len() > 1
|
||||||
|
{
|
||||||
|
self.claw.target_frame = self.next_claw_terminal_frame(true);
|
||||||
|
}
|
||||||
while self.claw.active && self.claw.frame_accumulator >= self.claw_frame_seconds {
|
while self.claw.active && self.claw.frame_accumulator >= self.claw_frame_seconds {
|
||||||
self.claw.frame_accumulator -= self.claw_frame_seconds;
|
self.claw.frame_accumulator -= self.claw_frame_seconds;
|
||||||
self.advance_claw(events);
|
self.advance_claw(events);
|
||||||
@@ -2275,23 +2388,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));
|
||||||
@@ -2325,35 +2427,61 @@ 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()
|
let special_respawn = self.special_respawn_pending(index);
|
||||||
.expect("the claw-held secondary ball must remain in slot two");
|
if self.balls.len() > 1 {
|
||||||
self.promote_claw_secondary_to_primary();
|
self.take_ball_out_of_play(index);
|
||||||
events.push(Event::Drain);
|
events.push(Event::Drain);
|
||||||
return;
|
if special_respawn {
|
||||||
|
// Only balls waiting in the launcher are left, so the parked
|
||||||
|
// ball is released just as for a single drained ball.
|
||||||
|
let released = self.release_parked_ball();
|
||||||
|
self.balls.push(released);
|
||||||
|
}
|
||||||
|
return BallStep::Removed;
|
||||||
}
|
}
|
||||||
if let Some(remaining_ball) = self.secondary_ball.take() {
|
if special_respawn {
|
||||||
self.ball = remaining_ball;
|
self.balls[index] = self.release_parked_ball();
|
||||||
|
return BallStep::Stop;
|
||||||
|
}
|
||||||
|
self.end_ball(events);
|
||||||
|
BallStep::Stop
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a ball that left play while others remain.
|
||||||
|
fn take_ball_out_of_play(&mut self, index: usize) {
|
||||||
|
self.remove_ball(index);
|
||||||
|
if self.balls.len() == 1 {
|
||||||
self.score_mode = ScoreMode::Normal;
|
self.score_mode = ScoreMode::Normal;
|
||||||
events.push(Event::Drain);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if self.special_respawn_pending() {
|
}
|
||||||
self.ball = Ball {
|
|
||||||
position: vec2(17.0, 23.0),
|
/// Empties the special hole and returns its ball, rolling out of the
|
||||||
velocity: MilliVec { x: 0, y: 3_040 }.to_velocity_per_second(),
|
/// hole, ready to be put into play.
|
||||||
in_launcher: false,
|
fn release_parked_ball(&mut self) -> Ball {
|
||||||
spin: Real48::ZERO,
|
self.multiball_state = MultiballState::Unavailable;
|
||||||
capture_age: 0,
|
self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] = 0;
|
||||||
};
|
self.special_hole_gate = SpecialHoleGate::Suppressed;
|
||||||
self.multiball_state = MultiballState::Unavailable;
|
Ball {
|
||||||
self.record_contacts[148] = 0;
|
position: vec2(17.0, 23.0),
|
||||||
self.special_hole_gate = SpecialHoleGate::Suppressed;
|
velocity: MilliVec { x: 0, y: 3_040 }.to_velocity_per_second(),
|
||||||
return;
|
in_launcher: false,
|
||||||
|
spin: Real48::ZERO,
|
||||||
|
capture_age: 0,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
@@ -2364,21 +2492,23 @@ impl Game {
|
|||||||
player.balls = player.balls.saturating_sub(1);
|
player.balls = player.balls.saturating_sub(1);
|
||||||
}
|
}
|
||||||
let high_score_candidate =
|
let high_score_candidate =
|
||||||
(player.balls == 0 && player.extra_balls == 0).then_some(player.score);
|
(player.balls == 0 && player.extra_balls == 0).then_some(Event::HighScoreCandidate {
|
||||||
|
score: player.score,
|
||||||
|
crazy: player.crazy_mode_used(),
|
||||||
|
});
|
||||||
self.save_current_rule_state();
|
self.save_current_rule_state();
|
||||||
events.push(Event::Drain);
|
events.push(Event::Drain);
|
||||||
if !self.tilted {
|
if !self.tilted {
|
||||||
events.push(Event::Sound(2008));
|
events.push(Event::Sound(2008));
|
||||||
}
|
}
|
||||||
if let Some(score) = high_score_candidate {
|
if let Some(candidate) = high_score_candidate {
|
||||||
events.push(Event::HighScoreCandidate(score));
|
events.push(candidate);
|
||||||
self.flipper_release_latch[0] |= self.flipper_inputs.left_raised;
|
self.flipper_release_latch[0] |= self.flipper_inputs.left_raised;
|
||||||
self.flipper_release_latch[1] |= self.flipper_inputs.right_raised;
|
self.flipper_release_latch[1] |= self.flipper_inputs.right_raised;
|
||||||
self.flipper_inputs = Flippers::default();
|
self.flipper_inputs = Flippers::default();
|
||||||
}
|
}
|
||||||
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;
|
||||||
@@ -2391,7 +2521,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();
|
||||||
@@ -2399,9 +2529,16 @@ impl Game {
|
|||||||
self.finished = true;
|
self.finished = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn special_respawn_pending(&self) -> bool {
|
/// Whether the ball at `leaving` is the last one in play while a ball is
|
||||||
self.secondary_ball.is_none()
|
/// parked in the special hole. Balls waiting in the launcher are not in
|
||||||
&& self.score_mode == ScoreMode::Normal
|
/// play; a ball effect seven is about to publish is.
|
||||||
|
fn special_respawn_pending(&self, leaving: usize) -> bool {
|
||||||
|
self.spawned_balls.is_empty()
|
||||||
|
&& self
|
||||||
|
.balls
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.all(|(index, ball)| index == leaving || ball.in_launcher)
|
||||||
&& self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] != 0
|
&& self.record_contacts[usize::from(SPECIAL_HOLE_SENSOR.id)] != 0
|
||||||
&& matches!(
|
&& matches!(
|
||||||
self.multiball_state,
|
self.multiball_state,
|
||||||
@@ -2514,6 +2651,28 @@ fn claw_release(frame: u8) -> (Vec2, Vec2) {
|
|||||||
(position, direction * ORIGINAL_BALL_SPEED_PER_SECOND)
|
(position, direction * ORIGINAL_BALL_SPEED_PER_SECOND)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deliberate original-game divergence: with several balls, one can come to
|
||||||
|
/// rest at the bottom of a lock hole's pocket, between the wheel hub and a
|
||||||
|
/// post, when another ball fills that hole or the wheel is being replaced.
|
||||||
|
/// The closed hole sends it out from its center, radially away from the hub,
|
||||||
|
/// which clears both neighbouring posts.
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
fn kick_out_of_lock_hole(ball: &mut Ball, hole_center: Vec2) {
|
||||||
|
let hub = PASSIVE_CIRCLES
|
||||||
|
.iter()
|
||||||
|
.find(|circle| circle.id == WHEEL_HUB_ID)
|
||||||
|
.map_or(vec2(124.0, 76.0), |circle| circle.center);
|
||||||
|
let direction = (hole_center - hub).normalize_or_zero();
|
||||||
|
ball.position = hole_center;
|
||||||
|
ball.velocity = MilliVec {
|
||||||
|
x: (direction.x * CLOSED_HOLE_KICKOUT_SPEED_MILLI).round() as i32,
|
||||||
|
y: (direction.y * CLOSED_HOLE_KICKOUT_SPEED_MILLI).round() as i32,
|
||||||
|
}
|
||||||
|
.to_velocity_per_second();
|
||||||
|
ball.spin = Real48::ZERO;
|
||||||
|
ball.capture_age = 0;
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_flipper_response_to_ball(ball: &mut Ball, delta: i32, side: FlipperSide) {
|
fn apply_flipper_response_to_ball(ball: &mut Ball, delta: i32, side: FlipperSide) {
|
||||||
let position = MilliVec::from_position(ball.position);
|
let position = MilliVec::from_position(ball.position);
|
||||||
let velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
let velocity = MilliVec::from_velocity_per_second(ball.velocity);
|
||||||
@@ -2530,6 +2689,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 {
|
||||||
@@ -2555,9 +2741,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2570,14 +2756,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,
|
||||||
@@ -2586,10 +2771,327 @@ mod tests {
|
|||||||
&mut capture_candidate,
|
&mut capture_candidate,
|
||||||
events,
|
events,
|
||||||
);
|
);
|
||||||
game.ball = ball;
|
game.balls[0] = ball;
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crazy_balls_start_clear_of_every_static_record() {
|
||||||
|
let game = Game::new_with_seed(1, 7);
|
||||||
|
for position in CRAZY_BALL_POSITIONS {
|
||||||
|
let origin = MilliVec::from_position(position);
|
||||||
|
for (x, y) in [(3_000, 0), (-3_000, 0), (0, 3_000), (0, -3_000)] {
|
||||||
|
assert!(
|
||||||
|
game.find_static_collision_candidate(origin, MilliVec { x, y })
|
||||||
|
.is_none(),
|
||||||
|
"{position:?} touches a record moving ({x}, {y})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crazy_mode_adds_ten_ordinary_balls_and_marks_the_player() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
assert!(game.add_crazy_balls());
|
||||||
|
assert!(game.player().crazy_mode_used());
|
||||||
|
assert_eq!(game.balls.len(), 11);
|
||||||
|
assert!(game.balls[0].in_launcher);
|
||||||
|
let start: Vec<_> = game.balls.iter().map(|ball| ball.position).collect();
|
||||||
|
for _ in 0..20 {
|
||||||
|
game.update(0.03, 3, Controls::default());
|
||||||
|
}
|
||||||
|
assert!(game.balls[0].in_launcher);
|
||||||
|
assert!(
|
||||||
|
game.balls[1..]
|
||||||
|
.iter()
|
||||||
|
.zip(&start[1..])
|
||||||
|
.all(|(ball, start)| ball.position != *start)
|
||||||
|
);
|
||||||
|
for _ in 0..3_000 {
|
||||||
|
game.update(0.03, 3, Controls::default());
|
||||||
|
}
|
||||||
|
// 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_eq!(game.player().balls, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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);
|
||||||
|
game.balls[0].in_launcher = false;
|
||||||
|
game.add_crazy_balls();
|
||||||
|
let mut events = Vec::new();
|
||||||
|
game.drain(&mut events);
|
||||||
|
assert_eq!(game.balls.len(), 10);
|
||||||
|
assert_eq!(game.balls[0].position, CRAZY_BALL_POSITIONS[0]);
|
||||||
|
assert_eq!(game.player().balls, 3);
|
||||||
|
game.balls.truncate(1);
|
||||||
|
game.drain(&mut events);
|
||||||
|
assert_eq!(game.player().balls, 2);
|
||||||
|
assert_eq!(game.balls.len(), 1);
|
||||||
|
assert!(game.balls[0].in_launcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crazy_scores_are_flagged_on_the_high_score_candidate() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.add_crazy_balls();
|
||||||
|
game.balls.truncate(1);
|
||||||
|
game.players[0].balls = 1;
|
||||||
|
game.players[0].score = 1_234;
|
||||||
|
let mut events = Vec::new();
|
||||||
|
game.drain(&mut events);
|
||||||
|
assert!(events.contains(&Event::HighScoreCandidate {
|
||||||
|
score: 1_234,
|
||||||
|
crazy: true,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crazy_mode_is_capped_and_rejected_after_the_game() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
while game.add_crazy_balls() {}
|
||||||
|
assert_eq!(game.balls.len(), MAX_BALLS);
|
||||||
|
|
||||||
|
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]
|
||||||
|
fn release_ball_frees_the_parked_ball_during_multiball() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.multiball_state = MultiballState::Ready;
|
||||||
|
game.record_contacts[148] = 2;
|
||||||
|
game.target_effect = 7;
|
||||||
|
game.object_active[usize::from(EFFECT_SENSOR.id)] = true;
|
||||||
|
game.balls[0].in_launcher = false;
|
||||||
|
game.balls[0].position = EFFECT_SENSOR.center;
|
||||||
|
for position in [vec2(250.0, 215.0), vec2(160.0, 300.0)] {
|
||||||
|
game.balls.push(Ball {
|
||||||
|
position,
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut events = Vec::new();
|
||||||
|
game.check_sensor_objects(
|
||||||
|
MilliVec::from_position(EFFECT_SENSOR.center),
|
||||||
|
MilliVec::default(),
|
||||||
|
&mut events,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(game.balls.len(), 4);
|
||||||
|
assert_eq!(game.balls[3].position, vec2(17.0, 23.0));
|
||||||
|
assert_eq!(game.multiball_state, MultiballState::Active);
|
||||||
|
assert!(!game.special_hole_active());
|
||||||
|
// The released ball owns the hole, and keeps owning it when an
|
||||||
|
// earlier ball leaves the table.
|
||||||
|
assert_eq!(game.record_contacts[148], 4);
|
||||||
|
game.remove_ball(1);
|
||||||
|
assert_eq!(game.record_contacts[148], 3);
|
||||||
|
|
||||||
|
for _ in 0..30 {
|
||||||
|
game.update(0.03, 3, Controls::default());
|
||||||
|
}
|
||||||
|
assert_eq!(game.record_contacts[148], 0);
|
||||||
|
assert!(
|
||||||
|
game.balls
|
||||||
|
.iter()
|
||||||
|
.all(|ball| ball.position.distance(vec2(17.0, 23.0)) > 12.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn claw_never_drops_into_the_shooter_lane_with_several_balls() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.begin_claw_scenario(CLAW_SHOOTER_LANE_FRAME);
|
||||||
|
assert_eq!(game.claw.target_frame, CLAW_SHOOTER_LANE_FRAME);
|
||||||
|
// A second ball arrives while the claw is already on its way.
|
||||||
|
game.balls.push(Ball {
|
||||||
|
position: vec2(160.0, 300.0),
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
});
|
||||||
|
let mut events = Vec::new();
|
||||||
|
for _ in 0..80 {
|
||||||
|
game.timer_tick(CLAW_FRAME_SECONDS, 1, &mut events);
|
||||||
|
if events.contains(&Event::ClawRelease) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(events.contains(&Event::ClawRelease));
|
||||||
|
assert!(CLAW_TABLE_TERMINAL_FRAMES.contains(&game.claw.frame));
|
||||||
|
assert!(!game.balls[0].in_launcher);
|
||||||
|
assert!(game.balls[0].position.x < 300.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_wheel_during_multiball_replaces_it_while_balls_play_on() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.wheel_holes = [true, true, true, true, false];
|
||||||
|
game.record_contacts[129..=132].fill(99);
|
||||||
|
game.players[0].secondary_score = 150_000;
|
||||||
|
let mut events = Vec::new();
|
||||||
|
assert_eq!(
|
||||||
|
game.complete_lock_hole(4, 2, &mut events),
|
||||||
|
BallAction::Remove
|
||||||
|
);
|
||||||
|
assert_eq!(game.panel_frame, Some(0));
|
||||||
|
assert_eq!(game.player().secondary_score, 0);
|
||||||
|
assert_eq!(game.player().score_multiplier, 2);
|
||||||
|
|
||||||
|
// A ball heading for the wheel bounces off its outer rim.
|
||||||
|
let hub = vec2(124.0, 76.0);
|
||||||
|
game.balls[0] = Ball {
|
||||||
|
position: hub + vec2(52.0, 0.0),
|
||||||
|
velocity: MilliVec { x: -3_000, y: 0 }.to_velocity_per_second(),
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
};
|
||||||
|
let mut hit_rim = false;
|
||||||
|
for _ in 0..4 {
|
||||||
|
game.fixed_update(&mut events);
|
||||||
|
hit_rim |= game.last_collision_id == Some(WHEEL_HUB_ID);
|
||||||
|
}
|
||||||
|
assert!(hit_rim);
|
||||||
|
assert!(game.balls[0].velocity.x > 0.0);
|
||||||
|
|
||||||
|
// The holes do not capture while the wheel is replaced.
|
||||||
|
game.balls[0] = Ball {
|
||||||
|
position: LOCK_HOLES[0].center,
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
};
|
||||||
|
for _ in 0..20 {
|
||||||
|
game.fixed_update(&mut events);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
game.record_contacts[129..=133]
|
||||||
|
.iter()
|
||||||
|
.all(|contact| *contact != 2)
|
||||||
|
);
|
||||||
|
assert!(!game.balls[0].in_launcher);
|
||||||
|
|
||||||
|
// Balls keep moving during the animation, and it completes.
|
||||||
|
let mut panel = Game::new_with_seed(1, 7);
|
||||||
|
panel.panel_frame = Some(0);
|
||||||
|
panel.wheel_holes.fill(true);
|
||||||
|
panel.balls[0] = Ball {
|
||||||
|
position: vec2(160.0, 300.0),
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
};
|
||||||
|
let start = panel.balls[0].position;
|
||||||
|
panel.timer_tick(0.03, 3, &mut events);
|
||||||
|
assert_ne!(panel.balls[0].position, start);
|
||||||
|
for _ in 0..300 {
|
||||||
|
panel.timer_tick(0.03, 3, &mut events);
|
||||||
|
}
|
||||||
|
assert_eq!(panel.panel_frame, None);
|
||||||
|
assert_eq!(panel.wheel_holes, [false; 5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filled_or_closed_lock_hole_kicks_out_a_ball_resting_in_its_pocket() {
|
||||||
|
// Found by multiball stress runs: the bottom of hole 131's pocket,
|
||||||
|
// cradled between the wheel hub and post 137.
|
||||||
|
let pocket = vec2(140.85, 65.2);
|
||||||
|
let hub = vec2(124.0, 76.0);
|
||||||
|
for closed_by_panel in [false, true] {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
if closed_by_panel {
|
||||||
|
game.panel_frame = Some(0);
|
||||||
|
} else {
|
||||||
|
game.record_contacts[131] = 99;
|
||||||
|
game.wheel_holes[2] = true;
|
||||||
|
}
|
||||||
|
game.balls[0] = Ball {
|
||||||
|
position: pocket,
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
};
|
||||||
|
for _ in 0..40 {
|
||||||
|
game.update(0.03, 3, Controls::default());
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
game.balls[0].position.distance(hub) > WHEEL_RIM_CONTACT_RADIUS,
|
||||||
|
"ball still in the wheel at {:?}",
|
||||||
|
game.balls[0].position
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parked_ball_is_released_when_only_a_waiting_ball_remains() {
|
||||||
|
let mut game = Game::new_with_seed(1, 7);
|
||||||
|
game.multiball_state = MultiballState::Ready;
|
||||||
|
game.record_contacts[148] = 2;
|
||||||
|
game.tilted = true;
|
||||||
|
// Ball one waits in the launcher; ball two is the last one in play.
|
||||||
|
game.balls.push(Ball {
|
||||||
|
position: vec2(160.0, 300.0),
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
});
|
||||||
|
let mut events = Vec::new();
|
||||||
|
assert_eq!(game.lose_ball(1, &mut events), BallStep::Removed);
|
||||||
|
|
||||||
|
assert_eq!(game.balls.len(), 2);
|
||||||
|
assert!(game.balls[0].in_launcher);
|
||||||
|
assert_eq!(game.balls[1].position, vec2(17.0, 23.0));
|
||||||
|
assert!(!game.balls[1].in_launcher);
|
||||||
|
assert_eq!(game.record_contacts[148], 0);
|
||||||
|
assert_eq!(game.multiball_state, MultiballState::Unavailable);
|
||||||
|
assert_eq!(game.special_hole_gate, SpecialHoleGate::Suppressed);
|
||||||
|
assert_eq!(game.player().balls, 3);
|
||||||
|
assert!(game.tilted);
|
||||||
|
|
||||||
|
// With another ball still in play the parked ball stays put.
|
||||||
|
let mut busy = Game::new_with_seed(1, 7);
|
||||||
|
busy.multiball_state = MultiballState::Ready;
|
||||||
|
busy.record_contacts[148] = 2;
|
||||||
|
for position in [vec2(160.0, 300.0), vec2(120.0, 160.0)] {
|
||||||
|
busy.balls.push(Ball {
|
||||||
|
position,
|
||||||
|
in_launcher: false,
|
||||||
|
..Ball::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
busy.lose_ball(1, &mut events);
|
||||||
|
assert_eq!(busy.balls.len(), 2);
|
||||||
|
assert_eq!(busy.record_contacts[148], 2);
|
||||||
|
assert!(busy.special_hole_active());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn player_count_is_bounded() {
|
fn player_count_is_bounded() {
|
||||||
assert_eq!(Game::new(0).players.len(), 1);
|
assert_eq!(Game::new(0).players.len(), 1);
|
||||||
@@ -2616,8 +3118,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]
|
||||||
@@ -2630,9 +3132,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) {
|
||||||
@@ -2645,9 +3147,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);
|
||||||
@@ -2674,18 +3176,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);
|
||||||
}
|
}
|
||||||
@@ -2694,9 +3196,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;
|
||||||
@@ -2711,17 +3213,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;
|
||||||
@@ -2731,7 +3233,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!(
|
||||||
@@ -2744,30 +3246,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]
|
||||||
@@ -2782,14 +3284,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]
|
||||||
@@ -2801,15 +3303,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);
|
||||||
}
|
}
|
||||||
@@ -2843,7 +3345,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]
|
||||||
@@ -2853,21 +3355,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,
|
||||||
@@ -2879,20 +3381,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]
|
||||||
@@ -2900,16 +3436,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]
|
||||||
@@ -2918,11 +3457,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]
|
||||||
@@ -2995,7 +3534,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]
|
||||||
@@ -3018,10 +3557,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(
|
||||||
@@ -3033,17 +3572,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!(
|
||||||
@@ -3055,7 +3594,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);
|
||||||
@@ -3105,8 +3644,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(),
|
||||||
@@ -3169,7 +3708,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(
|
||||||
@@ -3193,12 +3732,11 @@ 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(
|
||||||
&mut effect_ball,
|
&mut effect_ball,
|
||||||
1,
|
|
||||||
MilliVec::from_position(EFFECT_SENSOR.center),
|
MilliVec::from_position(EFFECT_SENSOR.center),
|
||||||
MilliVec::default(),
|
MilliVec::default(),
|
||||||
&mut effect_events,
|
&mut effect_events,
|
||||||
@@ -3238,8 +3776,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,
|
||||||
@@ -3317,15 +3854,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());
|
||||||
@@ -3336,24 +3873,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);
|
||||||
@@ -3362,22 +3897,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);
|
||||||
}
|
}
|
||||||
@@ -3392,14 +3927,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());
|
||||||
|
|
||||||
@@ -3408,9 +3943,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);
|
||||||
|
|
||||||
@@ -3421,13 +3956,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]
|
||||||
@@ -3435,21 +3970,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);
|
||||||
@@ -3468,7 +4003,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);
|
||||||
@@ -3481,7 +4016,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),
|
||||||
@@ -3500,8 +4035,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,
|
||||||
@@ -3513,7 +4047,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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3535,8 +4069,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,
|
||||||
@@ -3545,7 +4078,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());
|
||||||
@@ -3564,15 +4097,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,
|
||||||
@@ -3582,7 +4114,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);
|
||||||
@@ -3593,23 +4125,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));
|
||||||
@@ -3622,18 +4154,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;
|
||||||
@@ -3641,7 +4173,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);
|
||||||
@@ -3659,7 +4191,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();
|
||||||
|
|
||||||
@@ -3671,10 +4203,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),
|
||||||
@@ -3683,7 +4215,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(
|
||||||
@@ -3692,10 +4224,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]);
|
||||||
}
|
}
|
||||||
@@ -3704,10 +4236,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();
|
||||||
@@ -3722,21 +4254,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]
|
||||||
@@ -3749,14 +4281,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));
|
||||||
}
|
}
|
||||||
@@ -3766,8 +4298,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(),
|
||||||
@@ -3775,12 +4307,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);
|
||||||
}
|
}
|
||||||
@@ -3826,7 +4358,10 @@ mod tests {
|
|||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
|
||||||
game.drain(&mut events);
|
game.drain(&mut events);
|
||||||
assert!(events.contains(&Event::HighScoreCandidate(123_456)));
|
assert!(events.contains(&Event::HighScoreCandidate {
|
||||||
|
score: 123_456,
|
||||||
|
crazy: false
|
||||||
|
}));
|
||||||
assert_eq!(game.current_player, 1);
|
assert_eq!(game.current_player, 1);
|
||||||
assert!(!game.finished);
|
assert!(!game.finished);
|
||||||
|
|
||||||
@@ -3834,58 +4369,64 @@ mod tests {
|
|||||||
game.players[1].balls = 1;
|
game.players[1].balls = 1;
|
||||||
game.players[1].score = 654_321;
|
game.players[1].score = 654_321;
|
||||||
game.drain(&mut events);
|
game.drain(&mut events);
|
||||||
assert!(events.contains(&Event::HighScoreCandidate(654_321)));
|
assert!(events.contains(&Event::HighScoreCandidate {
|
||||||
|
score: 654_321,
|
||||||
|
crazy: false
|
||||||
|
}));
|
||||||
assert!(game.finished);
|
assert!(game.finished);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3893,36 +4434,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
|
||||||
@@ -3933,10 +4477,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()) {
|
||||||
@@ -3944,7 +4488,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(game.ball.capture_age, 0);
|
assert_eq!(game.balls[0].capture_age, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -4029,9 +4573,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));
|
||||||
|
|
||||||
@@ -4039,9 +4583,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();
|
||||||
@@ -4055,11 +4599,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]
|
||||||
@@ -4083,9 +4627,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,
|
||||||
@@ -4270,14 +4814,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,
|
||||||
@@ -4290,7 +4834,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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4299,16 +4843,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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4326,7 +4870,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()
|
||||||
@@ -4337,7 +4881,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);
|
||||||
@@ -4362,7 +4906,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);
|
||||||
@@ -4395,15 +4939,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);
|
||||||
@@ -4416,13 +4960,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 {
|
||||||
@@ -4476,10 +5020,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);
|
||||||
@@ -4487,10 +5031,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] {
|
||||||
@@ -4504,9 +5048,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);
|
||||||
@@ -4521,16 +5065,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);
|
||||||
@@ -4540,22 +5084,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
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4563,15 +5107,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,
|
||||||
),
|
),
|
||||||
@@ -4583,16 +5127,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);
|
||||||
@@ -4602,40 +5146,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();
|
||||||
@@ -4648,15 +5192,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,
|
||||||
@@ -4664,23 +5207,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();
|
||||||
@@ -4693,13 +5236,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,
|
||||||
),
|
),
|
||||||
@@ -4707,10 +5250,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),
|
||||||
@@ -4719,10 +5262,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]
|
||||||
@@ -4754,7 +5294,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,
|
||||||
@@ -4762,14 +5302,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();
|
||||||
@@ -4788,7 +5328,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if game.secondary_ball.is_none() || game.finished {
|
if game.secondary().is_none() || game.finished {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4805,27 +5345,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,
|
||||||
@@ -4833,13 +5372,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,
|
||||||
@@ -4847,29 +5385,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);
|
||||||
@@ -4885,7 +5423,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);
|
||||||
@@ -4894,32 +5432,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,
|
||||||
@@ -4927,13 +5464,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,
|
||||||
@@ -4941,18 +5477,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));
|
||||||
}
|
}
|
||||||
@@ -4973,16 +5506,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);
|
||||||
@@ -4995,9 +5528,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);
|
||||||
@@ -5010,19 +5543,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
|
||||||
@@ -5049,26 +5579,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,
|
||||||
);
|
);
|
||||||
@@ -5080,9 +5610,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(
|
||||||
@@ -5096,7 +5626,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 }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -5119,19 +5649,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 },
|
||||||
@@ -5147,7 +5677,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();
|
||||||
|
|
||||||
@@ -5234,10 +5764,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 };
|
||||||
@@ -5253,7 +5783,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);
|
||||||
|
|
||||||
@@ -5261,7 +5791,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());
|
||||||
@@ -5270,14 +5800,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);
|
||||||
@@ -5320,7 +5850,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn fifth_multiball_lock_defers_panel_and_caps_survivor_reaward() {
|
fn fifth_multiball_lock_starts_the_panel_and_caps_survivor_reaward() {
|
||||||
let mut game = Game::new(1);
|
let mut game = Game::new(1);
|
||||||
game.wheel_holes = [true, true, true, true, false];
|
game.wheel_holes = [true, true, true, true, false];
|
||||||
game.record_contacts[129..=133].fill(2);
|
game.record_contacts[129..=133].fill(2);
|
||||||
@@ -5335,13 +5865,9 @@ mod tests {
|
|||||||
assert_eq!(game.player().score, 620_000);
|
assert_eq!(game.player().score, 620_000);
|
||||||
assert_eq!(game.player().secondary_score, 0);
|
assert_eq!(game.player().secondary_score, 0);
|
||||||
assert_eq!(game.player().score_multiplier, 2);
|
assert_eq!(game.player().score_multiplier, 2);
|
||||||
assert_eq!(game.panel_frame, None);
|
assert_eq!(game.panel_frame, Some(0));
|
||||||
assert_eq!(game.score_mode, ScoreMode::Normal);
|
assert_eq!(game.score_mode, ScoreMode::Normal);
|
||||||
|
|
||||||
let mut reset_by_special_hole = game.clone();
|
|
||||||
reset_by_special_hole.reset_ball_to_launcher();
|
|
||||||
assert_eq!(reset_by_special_hole.panel_frame, Some(0));
|
|
||||||
|
|
||||||
game.score_mode = ScoreMode::Normal;
|
game.score_mode = ScoreMode::Normal;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.complete_lock_hole(4, 1, &mut events),
|
game.complete_lock_hole(4, 1, &mut events),
|
||||||
@@ -5357,10 +5883,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!(
|
||||||
@@ -5375,14 +5901,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,
|
||||||
@@ -5395,10 +5921,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,
|
||||||
@@ -5411,7 +5937,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]
|
||||||
@@ -5420,10 +5946,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]);
|
||||||
@@ -5439,11 +5965,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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -5455,18 +5981,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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -5478,9 +6004,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());
|
||||||
|
|
||||||
@@ -5496,12 +6022,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);
|
||||||
@@ -5510,11 +6036,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());
|
||||||
@@ -5560,7 +6086,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);
|
||||||
@@ -5586,7 +6112,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));
|
||||||
|
|
||||||
@@ -5608,7 +6134,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);
|
||||||
@@ -5650,9 +6176,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,
|
||||||
@@ -5661,7 +6187,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);
|
||||||
@@ -5722,7 +6248,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert!(game.tilted);
|
assert!(game.tilted);
|
||||||
assert!(game.ball.in_launcher);
|
assert!(game.balls[0].in_launcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -5740,7 +6266,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,
|
||||||
@@ -5755,7 +6281,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,10 @@ impl Default for Settings {
|
|||||||
pub struct HighScore {
|
pub struct HighScore {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub score: u32,
|
pub score: u32,
|
||||||
|
/// Set when the score was reached with crazy-mode balls. Saved tables
|
||||||
|
/// and server responses from before crazy mode omit it.
|
||||||
|
#[serde(default)]
|
||||||
|
pub crazy: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
@@ -244,6 +248,7 @@ pub fn parse_original_high_scores(bytes: &[u8]) -> Vec<HighScore> {
|
|||||||
HighScore {
|
HighScore {
|
||||||
name: String::from_utf8_lossy(&name_bytes[..length]).into_owned(),
|
name: String::from_utf8_lossy(&name_bytes[..length]).into_owned(),
|
||||||
score,
|
score,
|
||||||
|
crazy: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -335,6 +340,7 @@ mod tests {
|
|||||||
HighScore {
|
HighScore {
|
||||||
name: "TEST".to_owned(),
|
name: "TEST".to_owned(),
|
||||||
score: 6_537_393,
|
score: 6_537_393,
|
||||||
|
crazy: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert_eq!(scores.len(), 10);
|
assert_eq!(scores.len(), 10);
|
||||||
@@ -349,12 +355,23 @@ mod tests {
|
|||||||
HighScore {
|
HighScore {
|
||||||
name: "WRAPPED".to_owned(),
|
name: "WRAPPED".to_owned(),
|
||||||
score: u32::MAX,
|
score: u32::MAX,
|
||||||
|
crazy: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert!(!scores.iter().any(|entry| entry.name == "WRAPPED"));
|
assert!(!scores.iter().any(|entry| entry.name == "WRAPPED"));
|
||||||
assert!(!qualifies_high_score(&scores, u32::MAX));
|
assert!(!qualifies_high_score(&scores, u32::MAX));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn high_scores_without_a_crazy_flag_load_as_normal_scores() {
|
||||||
|
let entry: HighScore = serde_json::from_str(r#"{"name":"Paul","score":42}"#)
|
||||||
|
.expect("pre-crazy-mode entries must still load");
|
||||||
|
assert!(!entry.crazy);
|
||||||
|
let crazy: HighScore = serde_json::from_str(r#"{"name":"Paul","score":42,"crazy":true}"#)
|
||||||
|
.expect("crazy entries load");
|
||||||
|
assert!(crazy.crazy);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn original_dialog_allows_an_accepted_empty_name() {
|
fn original_dialog_allows_an_accepted_empty_name() {
|
||||||
let mut scores = parse_original_high_scores(ORIGINAL_HIGHSCORES);
|
let mut scores = parse_original_high_scores(ORIGINAL_HIGHSCORES);
|
||||||
@@ -363,6 +380,7 @@ mod tests {
|
|||||||
HighScore {
|
HighScore {
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
score: 6_537_393,
|
score: 6_537_393,
|
||||||
|
crazy: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+45
-39
@@ -29,6 +29,7 @@ pub enum Scenario {
|
|||||||
Claw6,
|
Claw6,
|
||||||
Claw7,
|
Claw7,
|
||||||
Claw18,
|
Claw18,
|
||||||
|
Crazy,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scenario {
|
impl Scenario {
|
||||||
@@ -48,6 +49,7 @@ impl Scenario {
|
|||||||
Self::Claw6 => "claw-6",
|
Self::Claw6 => "claw-6",
|
||||||
Self::Claw7 => "claw-7",
|
Self::Claw7 => "claw-7",
|
||||||
Self::Claw18 => "claw-18",
|
Self::Claw18 => "claw-18",
|
||||||
|
Self::Crazy => "crazy",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +68,8 @@ impl Scenario {
|
|||||||
| Self::Launcher
|
| Self::Launcher
|
||||||
| Self::Flippers
|
| Self::Flippers
|
||||||
| Self::Panel
|
| Self::Panel
|
||||||
| Self::Targets => None,
|
| Self::Targets
|
||||||
|
| Self::Crazy => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,8 +93,9 @@ impl FromStr for Scenario {
|
|||||||
"claw-6" => Ok(Self::Claw6),
|
"claw-6" => Ok(Self::Claw6),
|
||||||
"claw-7" => Ok(Self::Claw7),
|
"claw-7" => Ok(Self::Claw7),
|
||||||
"claw-18" => Ok(Self::Claw18),
|
"claw-18" => Ok(Self::Claw18),
|
||||||
|
"crazy" => Ok(Self::Crazy),
|
||||||
_ => Err(format!(
|
_ => Err(format!(
|
||||||
"unknown scenario {value:?}; expected loading, attract, highscores, name-entry, effect-7, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, or claw-18"
|
"unknown scenario {value:?}; expected loading, attract, highscores, name-entry, effect-7, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, claw-18, or crazy"
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +179,7 @@ fn seconds_to_step(value: &str) -> Result<u64, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const fn usage() -> &'static str {
|
pub const fn usage() -> &'static str {
|
||||||
"Usage:\n tdkpin-rs\n tdkpin-rs --simulate SCENARIO [--at SECONDS | --step N] [--screenshot FILE.png] [--trace FILE.json] [--seed N]\n\nScenarios: loading, attract, highscores, name-entry, effect-7, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, claw-18"
|
"Usage:\n tdkpin-rs\n tdkpin-rs --simulate SCENARIO [--at SECONDS | --step N] [--screenshot FILE.png] [--trace FILE.json] [--seed N]\n\nScenarios: loading, attract, highscores, name-entry, effect-7, autoplay, launcher, flippers, panel, targets, claw-1, claw-6, claw-7, claw-18, crazy"
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, PartialEq)]
|
#[derive(Debug, Serialize, PartialEq)]
|
||||||
@@ -183,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,
|
||||||
@@ -237,6 +241,9 @@ impl Simulation {
|
|||||||
if scenario == Scenario::Effect7 {
|
if scenario == Scenario::Effect7 {
|
||||||
game.begin_effect_scenario(7);
|
game.begin_effect_scenario(7);
|
||||||
}
|
}
|
||||||
|
if scenario == Scenario::Crazy {
|
||||||
|
game.add_crazy_balls();
|
||||||
|
}
|
||||||
let initial_events = scenario
|
let initial_events = scenario
|
||||||
.claw_terminal()
|
.claw_terminal()
|
||||||
.map_or_else(Vec::new, |terminal| game.begin_claw_scenario(terminal));
|
.map_or_else(Vec::new, |terminal| game.begin_claw_scenario(terminal));
|
||||||
@@ -298,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,
|
||||||
@@ -333,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 {
|
||||||
@@ -342,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,7 +387,8 @@ fn controls_for(scenario: Scenario, step: u64) -> Controls {
|
|||||||
| Scenario::Claw1
|
| Scenario::Claw1
|
||||||
| Scenario::Claw6
|
| Scenario::Claw6
|
||||||
| Scenario::Claw7
|
| Scenario::Claw7
|
||||||
| Scenario::Claw18 => Controls::default(),
|
| Scenario::Claw18
|
||||||
|
| Scenario::Crazy => Controls::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,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()
|
|
||||||
})
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,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