feat: rebuild TDK Pinball Machine in Rust
Replace the empty Rust scaffold with a playable native reconstruction of the 1995 Win16 game. Embed the complete decoded asset set, retain the original 640x460 presentation and localized help, and implement a resizable letterboxed Macroquad frontend for Linux, macOS, and Windows. Recreate the multiplayer ball flow, flippers, nudging, target banks, wheel, robot, lock, magnets, bumper progression, TDK diamond multiplier, media extra balls, sound dispatch, settings, and high-score entry. The physics engine uses a fixed time step and 101 static collision segments transcribed from the original 175-object table. Portable JSON persistence imports and decodes the original XOR-obfuscated high-score file on first run. Document the evidence boundary explicitly: artwork and PCM samples are exact, static geometry is recovered, and several numeric scoring/impulse values remain best-evidence behavioral tuning rather than bit-identical Win16 arithmetic. Add a native three-OS CI matrix and retain every decoded resource for future fidelity work. Test Plan: - `diff -qr original/assets/decoded tdkpin-rs/assets/original` with the two intentionally added legacy root files excluded -- passed - `cargo fmt --all -- --check` -- passed - `cargo check --all-targets` -- passed - `cargo test --all-targets` -- passed, 8 tests - `cargo clippy --all-targets -- -D warnings` -- passed - `cargo build --profile production` -- passed - `cargo check --all-targets --target x86_64-pc-windows-gnu` -- passed - `cargo check --all-targets --target x86_64-apple-darwin` -- passed - `cargo run` graphical start, launch, collision, and score smoke test -- passed; audible output was unavailable because the host has no ALSA device
This commit is contained in:
@@ -0,0 +1,556 @@
|
||||
use crate::geometry::{Segment, circle_collision, segment_collision};
|
||||
use macroquad::prelude::{Vec2, vec2};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
const BALL_RADIUS: f32 = 5.0;
|
||||
const GRAVITY: f32 = 135.0;
|
||||
const MAX_SPEED: f32 = 430.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Controls {
|
||||
pub left_flipper: bool,
|
||||
pub right_flipper: bool,
|
||||
pub launch_pressed: bool,
|
||||
pub nudge: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
Flipper,
|
||||
Launch,
|
||||
Bumper,
|
||||
Target,
|
||||
Wheel,
|
||||
Robot,
|
||||
Lock,
|
||||
Media,
|
||||
ExtraBall,
|
||||
Nudge,
|
||||
Drain,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Player {
|
||||
pub score: u32,
|
||||
pub balls: u8,
|
||||
pub extra_balls: u8,
|
||||
pub bumper_value: u32,
|
||||
pub diamond_segments: u8,
|
||||
pub media_level: u8,
|
||||
}
|
||||
|
||||
impl Default for Player {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
score: 0,
|
||||
balls: 3,
|
||||
extra_balls: 0,
|
||||
bumper_value: 1_000,
|
||||
diamond_segments: 0,
|
||||
media_level: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Ball {
|
||||
pub position: Vec2,
|
||||
pub velocity: Vec2,
|
||||
pub in_launcher: bool,
|
||||
}
|
||||
|
||||
impl Default for Ball {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: vec2(157.0, 426.0),
|
||||
velocity: Vec2::ZERO,
|
||||
in_launcher: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Game {
|
||||
pub players: Vec<Player>,
|
||||
pub current_player: usize,
|
||||
pub ball: Ball,
|
||||
pub bonus: u32,
|
||||
pub wheel_holes: [bool; 8],
|
||||
pub side_targets: [bool; 5],
|
||||
pub top_targets: [bool; 3],
|
||||
pub lock_lights: u8,
|
||||
pub magnets: f32,
|
||||
pub tilt: f32,
|
||||
pub left_flipper_angle: f32,
|
||||
pub right_flipper_angle: f32,
|
||||
pub finished: bool,
|
||||
accumulator: f32,
|
||||
target_cooldown: f32,
|
||||
bumper_cooldown: f32,
|
||||
nudge_cooldown: f32,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new(player_count: usize) -> Self {
|
||||
Self {
|
||||
players: vec![Player::default(); player_count.clamp(1, 4)],
|
||||
current_player: 0,
|
||||
ball: Ball::default(),
|
||||
bonus: 0,
|
||||
wheel_holes: [false; 8],
|
||||
side_targets: [false; 5],
|
||||
top_targets: [false; 3],
|
||||
lock_lights: 0,
|
||||
magnets: 0.0,
|
||||
tilt: 0.0,
|
||||
left_flipper_angle: -2.72,
|
||||
right_flipper_angle: -0.42,
|
||||
finished: false,
|
||||
accumulator: 0.0,
|
||||
target_cooldown: 0.0,
|
||||
bumper_cooldown: 0.0,
|
||||
nudge_cooldown: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn player(&self) -> &Player {
|
||||
&self.players[self.current_player]
|
||||
}
|
||||
|
||||
pub fn update(&mut self, frame_time: f32, detail: u8, controls: Controls) -> Vec<Event> {
|
||||
if self.finished {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut events = Vec::new();
|
||||
let desired_left = if controls.left_flipper { -2.18 } else { -2.72 };
|
||||
let desired_right = if controls.right_flipper { -0.96 } else { -0.42 };
|
||||
let flipper_rate = 14.0 * frame_time;
|
||||
let old_left = self.left_flipper_angle;
|
||||
let old_right = self.right_flipper_angle;
|
||||
self.left_flipper_angle +=
|
||||
(desired_left - self.left_flipper_angle).clamp(-flipper_rate, flipper_rate);
|
||||
self.right_flipper_angle +=
|
||||
(desired_right - self.right_flipper_angle).clamp(-flipper_rate, flipper_rate);
|
||||
if (controls.left_flipper && old_left < -2.68)
|
||||
|| (controls.right_flipper && old_right > -0.46)
|
||||
{
|
||||
events.push(Event::Flipper);
|
||||
}
|
||||
|
||||
if controls.launch_pressed && self.ball.in_launcher {
|
||||
self.ball.in_launcher = false;
|
||||
self.ball.velocity = vec2(12.0, -330.0);
|
||||
events.push(Event::Launch);
|
||||
}
|
||||
if controls.nudge.abs() > 0.1 && self.nudge_cooldown <= 0.0 {
|
||||
self.ball.velocity.x += controls.nudge * 55.0;
|
||||
self.tilt += controls.nudge.abs() * 0.34;
|
||||
self.nudge_cooldown = 0.22;
|
||||
events.push(Event::Nudge);
|
||||
}
|
||||
|
||||
self.accumulator = (self.accumulator + frame_time.min(0.05)).min(0.1);
|
||||
let steps_per_second =
|
||||
[60.0, 90.0, 120.0, 180.0, 240.0][usize::from(detail.clamp(1, 5) - 1)];
|
||||
let step = 1.0 / steps_per_second;
|
||||
while self.accumulator >= step {
|
||||
self.fixed_update(step, &mut events);
|
||||
self.accumulator -= step;
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
fn fixed_update(&mut self, dt: f32, events: &mut Vec<Event>) {
|
||||
self.target_cooldown = (self.target_cooldown - dt).max(0.0);
|
||||
self.bumper_cooldown = (self.bumper_cooldown - dt).max(0.0);
|
||||
self.nudge_cooldown = (self.nudge_cooldown - dt).max(0.0);
|
||||
self.magnets = (self.magnets - dt).max(0.0);
|
||||
self.tilt = (self.tilt - dt * 0.08).max(0.0);
|
||||
|
||||
if self.ball.in_launcher {
|
||||
self.ball.position = vec2(157.0, 426.0);
|
||||
self.ball.velocity = Vec2::ZERO;
|
||||
return;
|
||||
}
|
||||
|
||||
self.ball.velocity.y += GRAVITY * dt;
|
||||
self.ball.velocity *= 1.0 - dt * 0.055;
|
||||
self.ball.velocity = self.ball.velocity.clamp_length_max(MAX_SPEED);
|
||||
self.ball.position += self.ball.velocity * dt;
|
||||
|
||||
for wall in table_walls() {
|
||||
segment_collision(
|
||||
&mut self.ball.position,
|
||||
&mut self.ball.velocity,
|
||||
BALL_RADIUS,
|
||||
*wall,
|
||||
);
|
||||
}
|
||||
|
||||
let left_flipper = flipper_segment(vec2(103.0, 394.0), self.left_flipper_angle);
|
||||
let right_flipper = flipper_segment(vec2(211.0, 394.0), self.right_flipper_angle);
|
||||
if segment_collision(
|
||||
&mut self.ball.position,
|
||||
&mut self.ball.velocity,
|
||||
BALL_RADIUS + 2.0,
|
||||
left_flipper,
|
||||
) && self.left_flipper_angle > -2.55
|
||||
{
|
||||
self.ball.velocity += vec2(-20.0, -115.0);
|
||||
}
|
||||
if segment_collision(
|
||||
&mut self.ball.position,
|
||||
&mut self.ball.velocity,
|
||||
BALL_RADIUS + 2.0,
|
||||
right_flipper,
|
||||
) && self.right_flipper_angle < -0.58
|
||||
{
|
||||
self.ball.velocity += vec2(20.0, -115.0);
|
||||
}
|
||||
|
||||
for center in [vec2(207.0, 109.0), vec2(167.0, 148.0), vec2(219.0, 167.0)] {
|
||||
if circle_collision(
|
||||
&mut self.ball.position,
|
||||
&mut self.ball.velocity,
|
||||
BALL_RADIUS,
|
||||
center,
|
||||
14.0,
|
||||
105.0,
|
||||
) && self.bumper_cooldown <= 0.0
|
||||
{
|
||||
self.add_score(self.player().bumper_value);
|
||||
self.bonus = self.bonus.saturating_add(100);
|
||||
self.bumper_cooldown = 0.08;
|
||||
events.push(Event::Bumper);
|
||||
}
|
||||
}
|
||||
|
||||
self.check_targets(events);
|
||||
self.check_media(events);
|
||||
|
||||
if self.ball.position.y > 454.0 {
|
||||
if self.magnets > 0.0 && (self.ball.position.x < 145.0 || self.ball.position.x > 175.0)
|
||||
{
|
||||
self.ball.position.y = 410.0;
|
||||
self.ball.velocity = vec2((157.0 - self.ball.position.x) * 2.0, -245.0);
|
||||
self.magnets = 0.0;
|
||||
} else {
|
||||
self.drain(events);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_targets(&mut self, events: &mut Vec<Event>) {
|
||||
if self.target_cooldown > 0.0 {
|
||||
return;
|
||||
}
|
||||
let position = self.ball.position;
|
||||
|
||||
let top = [vec2(205.0, 47.0), vec2(234.0, 50.0), vec2(262.0, 53.0)];
|
||||
for (index, center) in top.into_iter().enumerate() {
|
||||
if position.distance_squared(center) < 11.0_f32.powi(2) && !self.top_targets[index] {
|
||||
self.top_targets[index] = true;
|
||||
self.add_score(1_500);
|
||||
self.ball.velocity.y = self.ball.velocity.y.abs() + 65.0;
|
||||
self.target_cooldown = 0.12;
|
||||
events.push(Event::Target);
|
||||
}
|
||||
}
|
||||
if self.top_targets.iter().all(|target| *target) {
|
||||
self.top_targets.fill(false);
|
||||
self.magnets = 12.0;
|
||||
self.add_score(5_000);
|
||||
}
|
||||
|
||||
let side = [180.0, 195.0, 210.0, 225.0, 240.0];
|
||||
for (index, y) in side.into_iter().enumerate() {
|
||||
if position.distance_squared(vec2(286.0, y)) < 9.0_f32.powi(2)
|
||||
&& !self.side_targets[index]
|
||||
{
|
||||
self.side_targets[index] = true;
|
||||
self.add_score(2_000 + u32::try_from(index).unwrap_or(0) * 1_000);
|
||||
self.ball.velocity.x = -self.ball.velocity.x.abs() - 55.0;
|
||||
self.target_cooldown = 0.10;
|
||||
events.push(Event::Target);
|
||||
}
|
||||
}
|
||||
if self.side_targets.iter().all(|target| *target) {
|
||||
self.side_targets.fill(false);
|
||||
let player = &mut self.players[self.current_player];
|
||||
player.bumper_value = (player.bumper_value + 1_000).min(10_000);
|
||||
player.diamond_segments = (player.diamond_segments + 1).min(9);
|
||||
self.add_score(10_000);
|
||||
}
|
||||
|
||||
let wheel_center = vec2(114.0, 79.0);
|
||||
let from_wheel = position - wheel_center;
|
||||
if (25.0..=43.0).contains(&from_wheel.length()) {
|
||||
let angle =
|
||||
(from_wheel.y.atan2(from_wheel.x) + std::f32::consts::TAU) % std::f32::consts::TAU;
|
||||
let sector_boundaries = [
|
||||
std::f32::consts::TAU / 16.0,
|
||||
std::f32::consts::TAU * 3.0 / 16.0,
|
||||
std::f32::consts::TAU * 5.0 / 16.0,
|
||||
std::f32::consts::TAU * 7.0 / 16.0,
|
||||
std::f32::consts::TAU * 9.0 / 16.0,
|
||||
std::f32::consts::TAU * 11.0 / 16.0,
|
||||
std::f32::consts::TAU * 13.0 / 16.0,
|
||||
std::f32::consts::TAU * 15.0 / 16.0,
|
||||
];
|
||||
let hole = sector_boundaries.partition_point(|boundary| angle >= *boundary) % 8;
|
||||
if !self.wheel_holes[hole] {
|
||||
self.wheel_holes[hole] = true;
|
||||
self.add_score(2_500);
|
||||
self.target_cooldown = 0.15;
|
||||
events.push(Event::Wheel);
|
||||
}
|
||||
if self.wheel_holes.iter().all(|hole| *hole) {
|
||||
self.wheel_holes.fill(false);
|
||||
self.add_score(25_000);
|
||||
}
|
||||
}
|
||||
|
||||
if (270.0..=302.0).contains(&position.x) && (45.0..=105.0).contains(&position.y) {
|
||||
self.add_score(7_500);
|
||||
self.ball.velocity = vec2(-125.0, 60.0);
|
||||
self.target_cooldown = 0.3;
|
||||
events.push(Event::Robot);
|
||||
}
|
||||
|
||||
if (151.0..=220.0).contains(&position.x) && (187.0..=205.0).contains(&position.y) {
|
||||
self.lock_lights = (self.lock_lights + 1).min(4);
|
||||
self.add_score(u32::from(self.lock_lights) * 5_000);
|
||||
self.ball.position = vec2(185.0, 181.0);
|
||||
self.ball.velocity = vec2(-40.0 + f32::from(self.lock_lights) * 18.0, -120.0);
|
||||
self.target_cooldown = 0.4;
|
||||
events.push(Event::Lock);
|
||||
}
|
||||
|
||||
for (center, value) in [
|
||||
(vec2(113.0, 285.0), 2_000),
|
||||
(vec2(132.0, 270.0), 3_000),
|
||||
(vec2(155.0, 258.0), 4_000),
|
||||
(vec2(181.0, 270.0), 5_000),
|
||||
(vec2(201.0, 285.0), 6_000),
|
||||
] {
|
||||
if position.distance_squared(center) < 10.0_f32.powi(2) {
|
||||
self.add_score(value);
|
||||
self.ball.velocity.y = -self.ball.velocity.y.abs() - 40.0;
|
||||
self.target_cooldown = 0.13;
|
||||
events.push(Event::Target);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_media(&mut self, events: &mut Vec<Event>) {
|
||||
const THRESHOLDS: [u32; 5] = [360_000, 720_000, 1_440_000, 2_880_000, 5_760_000];
|
||||
let score = self.player().score;
|
||||
let level =
|
||||
u8::try_from(THRESHOLDS.partition_point(|threshold| score >= *threshold)).unwrap_or(5);
|
||||
if level > self.player().media_level {
|
||||
let player = &mut self.players[self.current_player];
|
||||
player.media_level = level;
|
||||
player.extra_balls = player.extra_balls.saturating_add(1);
|
||||
events.push(Event::Media);
|
||||
events.push(Event::ExtraBall);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_score(&mut self, points: u32) {
|
||||
let multiplier = if self.player().diamond_segments == 9 {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let player = &mut self.players[self.current_player];
|
||||
player.score = player
|
||||
.score
|
||||
.saturating_add(points.saturating_mul(multiplier));
|
||||
}
|
||||
|
||||
fn drain(&mut self, events: &mut Vec<Event>) {
|
||||
let player = &mut self.players[self.current_player];
|
||||
player.score = player.score.saturating_add(self.bonus);
|
||||
if player.extra_balls > 0 {
|
||||
player.extra_balls -= 1;
|
||||
} else {
|
||||
player.balls = player.balls.saturating_sub(1);
|
||||
}
|
||||
events.push(Event::Drain);
|
||||
self.bonus = 0;
|
||||
self.side_targets.fill(false);
|
||||
self.top_targets.fill(false);
|
||||
self.wheel_holes.fill(false);
|
||||
self.lock_lights = 0;
|
||||
self.magnets = 0.0;
|
||||
self.tilt = 0.0;
|
||||
|
||||
let mut next = (self.current_player + 1) % self.players.len();
|
||||
for _ in 0..self.players.len() {
|
||||
if self.players[next].balls > 0 || self.players[next].extra_balls > 0 {
|
||||
self.current_player = next;
|
||||
self.ball = Ball::default();
|
||||
return;
|
||||
}
|
||||
next = (next + 1) % self.players.len();
|
||||
}
|
||||
self.finished = true;
|
||||
}
|
||||
|
||||
pub fn flippers(&self) -> (Segment, Segment) {
|
||||
(
|
||||
flipper_segment(vec2(103.0, 394.0), self.left_flipper_angle),
|
||||
flipper_segment(vec2(211.0, 394.0), self.right_flipper_angle),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn flipper_segment(pivot: Vec2, angle: f32) -> Segment {
|
||||
Segment::new(pivot, pivot + vec2(angle.cos(), angle.sin()) * 49.0, 0.88)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn table_walls() -> &'static [Segment] {
|
||||
static WALLS: LazyLock<Vec<Segment>> = LazyLock::new(|| {
|
||||
vec![
|
||||
Segment::new(Vec2::new(125.0, 21.0), Vec2::new(7.0, 61.0), 0.82),
|
||||
Segment::new(Vec2::new(300.0, 3.0), Vec2::new(10.0, 3.0), 0.82),
|
||||
Segment::new(Vec2::new(120.0, 6.0), Vec2::new(120.0, 26.0), 0.82),
|
||||
Segment::new(Vec2::new(195.0, 26.0), Vec2::new(195.0, 6.0), 0.82),
|
||||
Segment::new(Vec2::new(134.0, 33.0), Vec2::new(134.0, 16.0), 0.82),
|
||||
Segment::new(Vec2::new(148.0, 11.0), Vec2::new(148.0, 33.0), 0.82),
|
||||
Segment::new(Vec2::new(164.0, 12.0), Vec2::new(165.0, 33.0), 0.82),
|
||||
Segment::new(Vec2::new(180.0, 11.0), Vec2::new(179.0, 33.0), 0.82),
|
||||
Segment::new(Vec2::new(304.0, 64.0), Vec2::new(183.0, 21.0), 0.82),
|
||||
Segment::new(Vec2::new(298.0, 417.0), Vec2::new(298.0, 313.0), 0.82),
|
||||
Segment::new(Vec2::new(298.0, 336.0), Vec2::new(310.0, 336.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 374.0), Vec2::new(291.0, 342.0), 0.82),
|
||||
Segment::new(Vec2::new(281.0, 378.0), Vec2::new(300.0, 369.0), 0.82),
|
||||
Segment::new(Vec2::new(275.0, 343.0), Vec2::new(290.0, 382.0), 0.82),
|
||||
Segment::new(Vec2::new(248.0, 364.0), Vec2::new(264.0, 339.0), 0.82),
|
||||
Segment::new(Vec2::new(257.0, 387.0), Vec2::new(249.0, 364.0), 0.82),
|
||||
Segment::new(Vec2::new(277.0, 429.0), Vec2::new(257.0, 387.0), 0.82),
|
||||
Segment::new(Vec2::new(287.0, 446.0), Vec2::new(277.0, 427.0), 0.82),
|
||||
Segment::new(Vec2::new(303.0, 430.0), Vec2::new(283.0, 436.0), 0.82),
|
||||
Segment::new(Vec2::new(320.0, 410.0), Vec2::new(302.0, 430.0), 0.82),
|
||||
Segment::new(Vec2::new(320.0, 21.0), Vec2::new(320.0, 410.0), 0.82),
|
||||
Segment::new(Vec2::new(307.0, 437.0), Vec2::new(316.0, 430.0), 0.82),
|
||||
Segment::new(Vec2::new(296.0, 442.0), Vec2::new(307.0, 437.0), 0.82),
|
||||
Segment::new(Vec2::new(285.0, 443.0), Vec2::new(296.0, 442.0), 0.82),
|
||||
Segment::new(Vec2::new(112.0, 443.0), Vec2::new(285.0, 443.0), 0.82),
|
||||
Segment::new(Vec2::new(99.0, 438.0), Vec2::new(121.0, 444.0), 0.82),
|
||||
Segment::new(Vec2::new(78.0, 427.0), Vec2::new(100.0, 438.0), 0.82),
|
||||
Segment::new(Vec2::new(66.0, 409.0), Vec2::new(79.0, 428.0), 0.82),
|
||||
Segment::new(Vec2::new(57.0, 385.0), Vec2::new(68.0, 413.0), 0.82),
|
||||
Segment::new(Vec2::new(59.0, 304.0), Vec2::new(59.0, 396.0), 0.82),
|
||||
Segment::new(Vec2::new(67.0, 288.0), Vec2::new(58.0, 311.0), 0.82),
|
||||
Segment::new(Vec2::new(41.0, 268.0), Vec2::new(64.0, 279.0), 0.82),
|
||||
Segment::new(Vec2::new(22.0, 293.0), Vec2::new(31.0, 272.0), 0.82),
|
||||
Segment::new(Vec2::new(22.0, 447.0), Vec2::new(22.0, 293.0), 0.82),
|
||||
Segment::new(Vec2::new(11.0, 445.0), Vec2::new(23.0, 445.0), 0.82),
|
||||
Segment::new(Vec2::new(13.0, 288.0), Vec2::new(13.0, 448.0), 0.82),
|
||||
Segment::new(Vec2::new(32.0, 228.0), Vec2::new(12.0, 293.0), 0.82),
|
||||
Segment::new(Vec2::new(10.0, 158.0), Vec2::new(33.0, 227.0), 0.82),
|
||||
Segment::new(Vec2::new(12.0, 51.0), Vec2::new(12.0, 197.0), 0.82),
|
||||
Segment::new(Vec2::new(37.0, 111.0), Vec2::new(37.0, 166.0), 0.82),
|
||||
Segment::new(Vec2::new(110.0, 74.0), Vec2::new(37.0, 111.0), 0.82),
|
||||
Segment::new(Vec2::new(19.0, 73.0), Vec2::new(92.0, 50.0), 0.82),
|
||||
Segment::new(Vec2::new(19.0, 167.0), Vec2::new(19.0, 73.0), 0.82),
|
||||
Segment::new(Vec2::new(141.0, 45.0), Vec2::new(112.0, 72.0), 0.82),
|
||||
Segment::new(Vec2::new(97.0, 47.0), Vec2::new(130.0, 31.0), 0.82),
|
||||
Segment::new(Vec2::new(0.0, 433.0), Vec2::new(7.0, 478.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 73.0), Vec2::new(292.0, 167.0), 0.82),
|
||||
Segment::new(Vec2::new(221.0, 50.0), Vec2::new(292.0, 73.0), 0.82),
|
||||
Segment::new(Vec2::new(275.0, 111.0), Vec2::new(205.0, 74.0), 0.82),
|
||||
Segment::new(Vec2::new(275.0, 166.0), Vec2::new(275.0, 111.0), 0.82),
|
||||
Segment::new(Vec2::new(177.0, 31.0), Vec2::new(216.0, 47.0), 0.82),
|
||||
Segment::new(Vec2::new(201.0, 72.0), Vec2::new(172.0, 45.0), 0.82),
|
||||
Segment::new(Vec2::new(40.0, 257.0), Vec2::new(73.0, 273.0), 0.82),
|
||||
Segment::new(Vec2::new(73.0, 273.0), Vec2::new(67.0, 285.0), 0.82),
|
||||
Segment::new(Vec2::new(34.0, 269.0), Vec2::new(40.0, 257.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 231.0), Vec2::new(292.0, 205.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 205.0), Vec2::new(302.0, 205.0), 0.82),
|
||||
Segment::new(Vec2::new(302.0, 231.0), Vec2::new(292.0, 231.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 247.0), Vec2::new(292.0, 222.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 222.0), Vec2::new(302.0, 222.0), 0.82),
|
||||
Segment::new(Vec2::new(302.0, 247.0), Vec2::new(292.0, 247.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 261.0), Vec2::new(292.0, 237.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 237.0), Vec2::new(302.0, 237.0), 0.82),
|
||||
Segment::new(Vec2::new(302.0, 261.0), Vec2::new(292.0, 261.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 277.0), Vec2::new(292.0, 252.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 252.0), Vec2::new(302.0, 252.0), 0.82),
|
||||
Segment::new(Vec2::new(302.0, 276.0), Vec2::new(292.0, 276.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 291.0), Vec2::new(292.0, 268.0), 0.82),
|
||||
Segment::new(Vec2::new(292.0, 269.0), Vec2::new(302.0, 268.0), 0.82),
|
||||
Segment::new(Vec2::new(302.0, 295.0), Vec2::new(292.0, 290.0), 0.82),
|
||||
Segment::new(Vec2::new(148.0, 254.0), Vec2::new(203.0, 236.0), 0.82),
|
||||
Segment::new(Vec2::new(215.0, 261.0), Vec2::new(158.0, 280.0), 0.82),
|
||||
Segment::new(Vec2::new(187.0, 232.0), Vec2::new(209.0, 225.0), 0.82),
|
||||
Segment::new(Vec2::new(209.0, 225.0), Vec2::new(214.0, 240.0), 0.82),
|
||||
Segment::new(Vec2::new(191.0, 244.0), Vec2::new(187.0, 232.0), 0.82),
|
||||
Segment::new(Vec2::new(169.0, 239.0), Vec2::new(198.0, 229.0), 0.82),
|
||||
Segment::new(Vec2::new(198.0, 229.0), Vec2::new(202.0, 241.0), 0.82),
|
||||
Segment::new(Vec2::new(173.0, 251.0), Vec2::new(169.0, 239.0), 0.82),
|
||||
Segment::new(Vec2::new(153.0, 245.0), Vec2::new(182.0, 235.0), 0.82),
|
||||
Segment::new(Vec2::new(182.0, 235.0), Vec2::new(186.0, 247.0), 0.82),
|
||||
Segment::new(Vec2::new(157.0, 257.0), Vec2::new(153.0, 245.0), 0.82),
|
||||
Segment::new(Vec2::new(139.0, 252.0), Vec2::new(163.0, 241.0), 0.82),
|
||||
Segment::new(Vec2::new(163.0, 241.0), Vec2::new(167.0, 253.0), 0.82),
|
||||
Segment::new(Vec2::new(144.0, 267.0), Vec2::new(139.0, 252.0), 0.82),
|
||||
Segment::new(Vec2::new(84.0, 318.0), Vec2::new(105.0, 329.0), 0.82),
|
||||
Segment::new(Vec2::new(105.0, 329.0), Vec2::new(101.0, 339.0), 0.82),
|
||||
Segment::new(Vec2::new(80.0, 328.0), Vec2::new(84.0, 318.0), 0.82),
|
||||
Segment::new(Vec2::new(87.0, 327.0), Vec2::new(111.0, 339.0), 0.82),
|
||||
Segment::new(Vec2::new(77.0, 370.0), Vec2::new(77.0, 331.0), 0.82),
|
||||
Segment::new(Vec2::new(87.0, 372.0), Vec2::new(77.0, 370.0), 0.82),
|
||||
Segment::new(Vec2::new(110.0, 337.0), Vec2::new(87.0, 372.0), 0.82),
|
||||
Segment::new(Vec2::new(184.0, 426.0), Vec2::new(172.0, 403.0), 0.82),
|
||||
Segment::new(Vec2::new(193.0, 396.0), Vec2::new(205.0, 417.0), 0.82),
|
||||
Segment::new(Vec2::new(213.0, 426.0), Vec2::new(201.0, 403.0), 0.82),
|
||||
Segment::new(Vec2::new(222.0, 396.0), Vec2::new(234.0, 417.0), 0.82),
|
||||
Segment::new(Vec2::new(242.0, 426.0), Vec2::new(230.0, 403.0), 0.82),
|
||||
Segment::new(Vec2::new(251.0, 396.0), Vec2::new(263.0, 417.0), 0.82),
|
||||
Segment::new(Vec2::new(291.0, 205.0), Vec2::new(298.0, 187.0), 0.82),
|
||||
Segment::new(Vec2::new(298.0, 378.0), Vec2::new(298.0, 58.0), 0.82),
|
||||
Segment::new(Vec2::new(298.0, 288.0), Vec2::new(298.0, 211.0), 0.82),
|
||||
Segment::new(Vec2::new(298.0, 313.0), Vec2::new(291.0, 290.0), 0.82),
|
||||
]
|
||||
});
|
||||
&WALLS
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn player_count_is_bounded() {
|
||||
assert_eq!(Game::new(0).players.len(), 1);
|
||||
assert_eq!(Game::new(99).players.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_is_deterministic() {
|
||||
let mut game = Game::new(1);
|
||||
let events = game.update(
|
||||
1.0 / 60.0,
|
||||
3,
|
||||
Controls {
|
||||
launch_pressed: true,
|
||||
..Controls::default()
|
||||
},
|
||||
);
|
||||
assert!(events.contains(&Event::Launch));
|
||||
assert!(!game.ball.in_launcher);
|
||||
assert!(game.ball.velocity.y < 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_diamond_doubles_score() {
|
||||
let mut game = Game::new(1);
|
||||
game.players[0].diamond_segments = 9;
|
||||
game.add_score(1_000);
|
||||
assert_eq!(game.players[0].score, 2_000);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user