diff --git a/Cargo.lock b/Cargo.lock index 3664f68..dd7bcb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -329,6 +329,16 @@ name = "gcc" version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "getrandom" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "gfx-backend-dx11" version = "0.3.0" @@ -492,6 +502,14 @@ dependencies = [ "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "line_drawing" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "lock_api" version = "0.3.1" @@ -615,6 +633,14 @@ name = "nodrop" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "num-traits" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "num-traits" version = "0.2.8" @@ -757,6 +783,14 @@ dependencies = [ "proc-macro2 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "range-alloc" version = "0.1.0" @@ -919,7 +953,9 @@ dependencies = [ name = "simple-invaders" version = "0.1.0" dependencies = [ + "line_drawing 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "pcx 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1071,6 +1107,11 @@ dependencies = [ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "wasi" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "wasm-bindgen" version = "0.2.50" @@ -1391,6 +1432,7 @@ dependencies = [ "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" "checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" "checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" +"checksum getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "473a1265acc8ff1e808cd0a1af8cee3c2ee5200916058a2ca113c29f2d903571" "checksum gfx-backend-dx11 0.3.0 (git+https://github.com/gfx-rs/gfx?rev=3d5db15661127c8cad8d85522a68ec36c82f6e69)" = "" "checksum gfx-backend-dx12 0.3.0 (git+https://github.com/gfx-rs/gfx?rev=3d5db15661127c8cad8d85522a68ec36c82f6e69)" = "" "checksum gfx-backend-empty 0.3.0 (git+https://github.com/gfx-rs/gfx?rev=3d5db15661127c8cad8d85522a68ec36c82f6e69)" = "" @@ -1407,6 +1449,7 @@ dependencies = [ "checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba" "checksum libloading 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" "checksum line_drawing 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5cc7ad3d82c845bdb5dde34ffdcc7a5fb4d2996e1e1ee0f19c33bc80e15196b9" +"checksum line_drawing 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f81902e542483002b103c6424d23e765c2e5a65f732923299053a601bce50ab2" "checksum lock_api 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f8912e782533a93a167888781b836336a6ca5da6175c05944c86cf28c31104dc" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" @@ -1420,6 +1463,7 @@ dependencies = [ "checksum net2 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "42550d9fb7b6684a6d404d9fa7250c2eb2646df731d1c06afc06dcee9e1bcf88" "checksum nix 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6c722bee1037d430d0f8e687bbdbf222f27cc6e4e68d5caf630857bb2b6dbdce" "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" +"checksum num-traits 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" "checksum objc 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "31d20fd2b37e07cf5125be68357b588672e8cefe9a96f8c17a9d46053b3e590d" "checksum objc_exception 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "098cd29a2fa3c230d3463ae069cecccc3fdfd64c0d2496ab5b96f82dab6a00dc" @@ -1435,6 +1479,7 @@ dependencies = [ "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" "checksum quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)" = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" "checksum range-alloc 0.1.0 (git+https://github.com/gfx-rs/gfx?rev=3d5db15661127c8cad8d85522a68ec36c82f6e69)" = "" "checksum raw-window-handle 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "af3d3b2e1053b3ff2171efc29a8bff3439ce6b2ce6a0432695134bc1c7ff8e87" "checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" @@ -1472,6 +1517,7 @@ dependencies = [ "checksum vk-shader-macros-impl 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4941ca9c0867ee70ebd9680bdcd659ff53d2d789c65586500da2ff0aa813f7b7" "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" "checksum walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9658c94fa8b940eab2250bd5a457f9c48b748420d71293b165c8cdbe2f55f71e" +"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" "checksum wasm-bindgen 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "dcddca308b16cd93c2b67b126c688e5467e4ef2e28200dc7dfe4ae284f2faefc" "checksum wasm-bindgen-backend 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "f805d9328b5fc7e5c6399960fd1889271b9b58ae17bdb2417472156cc9fafdd0" "checksum wasm-bindgen-macro 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "3ff88201a482abfc63921621f6cb18eb1efd74f136b05e5841e7f8ca434539e9" diff --git a/examples/invaders/main.rs b/examples/invaders/main.rs index 3033ebc..1c513d2 100644 --- a/examples/invaders/main.rs +++ b/examples/invaders/main.rs @@ -88,7 +88,7 @@ fn main() -> Result<(), Error> { last = now; // Update the game logic and request redraw - invaders.update(dt, &controls); + invaders.update(&dt, &controls); window.request_redraw(); } _ => (), diff --git a/simple-invaders/Cargo.toml b/simple-invaders/Cargo.toml index 27c24e8..4413b62 100644 --- a/simple-invaders/Cargo.toml +++ b/simple-invaders/Cargo.toml @@ -5,4 +5,6 @@ authors = ["Jay Oster "] edition = "2018" [dependencies] +line_drawing = "0.8" pcx = "0.2" +rand_core = { version = "0.5", features = ["std"] } diff --git a/simple-invaders/src/assets/bullet1.pcx b/simple-invaders/src/assets/bullet1.pcx new file mode 100644 index 0000000..fafd18f Binary files /dev/null and b/simple-invaders/src/assets/bullet1.pcx differ diff --git a/simple-invaders/src/assets/bullet2.pcx b/simple-invaders/src/assets/bullet2.pcx new file mode 100644 index 0000000..b3dd690 Binary files /dev/null and b/simple-invaders/src/assets/bullet2.pcx differ diff --git a/simple-invaders/src/assets/bullet3.pcx b/simple-invaders/src/assets/bullet3.pcx new file mode 100644 index 0000000..22a1f57 Binary files /dev/null and b/simple-invaders/src/assets/bullet3.pcx differ diff --git a/simple-invaders/src/assets/bullet4.pcx b/simple-invaders/src/assets/bullet4.pcx new file mode 100644 index 0000000..32e68f6 Binary files /dev/null and b/simple-invaders/src/assets/bullet4.pcx differ diff --git a/simple-invaders/src/assets/bullet5.pcx b/simple-invaders/src/assets/bullet5.pcx new file mode 100644 index 0000000..8fbdc66 Binary files /dev/null and b/simple-invaders/src/assets/bullet5.pcx differ diff --git a/simple-invaders/src/assets/laser1.pcx b/simple-invaders/src/assets/laser1.pcx new file mode 100644 index 0000000..cd289b0 Binary files /dev/null and b/simple-invaders/src/assets/laser1.pcx differ diff --git a/simple-invaders/src/assets/laser2.pcx b/simple-invaders/src/assets/laser2.pcx new file mode 100644 index 0000000..b4cbbb7 Binary files /dev/null and b/simple-invaders/src/assets/laser2.pcx differ diff --git a/simple-invaders/src/assets/laser3.pcx b/simple-invaders/src/assets/laser3.pcx new file mode 100644 index 0000000..9099c1d Binary files /dev/null and b/simple-invaders/src/assets/laser3.pcx differ diff --git a/simple-invaders/src/assets/laser4.pcx b/simple-invaders/src/assets/laser4.pcx new file mode 100644 index 0000000..f8f89b9 Binary files /dev/null and b/simple-invaders/src/assets/laser4.pcx differ diff --git a/simple-invaders/src/assets/laser5.pcx b/simple-invaders/src/assets/laser5.pcx new file mode 100644 index 0000000..ad8afc6 Binary files /dev/null and b/simple-invaders/src/assets/laser5.pcx differ diff --git a/simple-invaders/src/assets/laser6.pcx b/simple-invaders/src/assets/laser6.pcx new file mode 100644 index 0000000..78b37f7 Binary files /dev/null and b/simple-invaders/src/assets/laser6.pcx differ diff --git a/simple-invaders/src/assets/laser7.pcx b/simple-invaders/src/assets/laser7.pcx new file mode 100644 index 0000000..224a8e0 Binary files /dev/null and b/simple-invaders/src/assets/laser7.pcx differ diff --git a/simple-invaders/src/assets/laser8.pcx b/simple-invaders/src/assets/laser8.pcx new file mode 100644 index 0000000..bcdff8f Binary files /dev/null and b/simple-invaders/src/assets/laser8.pcx differ diff --git a/simple-invaders/src/lib.rs b/simple-invaders/src/lib.rs index 03f319c..3341648 100644 --- a/simple-invaders/src/lib.rs +++ b/simple-invaders/src/lib.rs @@ -4,11 +4,13 @@ //! this in practice. That said, the game is fully functional, and it should not be too difficult //! to understand the code. +use rand_core::{OsRng, RngCore}; +use std::env; use std::time::Duration; pub use controls::{Controls, Direction}; use loader::{load_assets, Assets}; -use sprites::{blit, Animation, Frame, Sprite, SpriteRef}; +use sprites::{blit, line, Animation, Frame, Sprite, SpriteRef}; mod controls; mod loader; @@ -20,7 +22,7 @@ pub const SCREEN_WIDTH: usize = 224; pub const SCREEN_HEIGHT: usize = 256; // Invader positioning -const START: Point = Point::new(24, 60); +const START: Point = Point::new(24, 64); const GRID: Point = Point::new(16, 16); const ROWS: usize = 5; const COLS: usize = 11; @@ -31,29 +33,34 @@ pub struct World { lasers: Vec, shields: Vec, player: Player, - bullets: Vec, + bullet: Option, score: u32, assets: Assets, screen: Vec, dt: Duration, + gameover: bool, + random: OsRng, + debug: bool, } -/// A tiny position vector +/// A tiny position vector. #[derive(Debug, Default, Eq, PartialEq)] struct Point { x: usize, y: usize, } -/// A formation of invaders. +/// A fleet of invaders. #[derive(Debug)] struct Invaders { grid: Vec>>, - stepper: Stepper, + stepper: Point, + direction: Direction, + descend: bool, bounds: Bounds, } -/// Everything you ever wanted to know about Invaders +/// Everything you ever wanted to know about Invaders. #[derive(Debug)] struct Invader { sprite: SpriteRef, @@ -61,22 +68,14 @@ struct Invader { score: u32, } -/// The stepper will linerly walk through the 2D vector of invaders, updating their state along the -/// way. -#[derive(Debug)] -struct Stepper { - row: usize, - col: usize, -} - /// Creates a boundary around the live invaders. /// /// Used for collision detection and minor optimizations. #[derive(Debug)] struct Bounds { - left: usize, - right: usize, - bottom: usize, + left_col: usize, + right_col: usize, + px: usize, } /// The player entity. @@ -84,7 +83,7 @@ struct Bounds { struct Player { sprite: SpriteRef, pos: Point, - last_update: usize, + dt: usize, } /// The shield entity. @@ -100,6 +99,7 @@ struct Shield { struct Laser { sprite: SpriteRef, pos: Point, + dt: usize, } /// The cannon entity. @@ -107,6 +107,7 @@ struct Laser { struct Bullet { sprite: SpriteRef, pos: Point, + dt: usize, } impl World { @@ -117,15 +118,18 @@ impl World { // Load assets first let assets = load_assets(); + // TODO: Create invaders one-at-a-time let invaders = Invaders { grid: make_invader_grid(&assets), - stepper: Stepper::default(), + stepper: Point::new(COLS - 1, 0), + direction: Direction::Right, + descend: false, bounds: Bounds::default(), }; let player = Player { sprite: SpriteRef::new(&assets, Player1, Duration::from_millis(100)), pos: Point::new(80, 216), - last_update: 0, + dt: 0, }; let shields = (0..4) .map(|i| Shield { @@ -138,16 +142,25 @@ impl World { let mut screen = Vec::new(); screen.resize_with(SCREEN_WIDTH * SCREEN_HEIGHT * 4, Default::default); + // Enable debug mode with `DEBUG=true` environment variable + let debug = env::var("DEBUG") + .unwrap_or_else(|_| "false".to_string()) + .parse() + .unwrap_or(false); + World { invaders, lasers: Vec::new(), shields, player, - bullets: Vec::new(), + bullet: None, score: 0, assets, screen, dt: Duration::default(), + gameover: false, + random: OsRng, + debug, } } @@ -157,11 +170,16 @@ impl World { /// /// * `dt`: The time delta since last update. /// * `controls`: The player inputs. - pub fn update(&mut self, dt: Duration, controls: &Controls) { + pub fn update(&mut self, dt: &Duration, controls: &Controls) { + if self.gameover { + // TODO: Add a game over screen + return; + } + let one_frame = Duration::new(0, 16_666_667); // Advance the timer by the delta time - self.dt += dt; + self.dt += *dt; // Step the invaders one by one while self.dt >= one_frame { @@ -172,8 +190,35 @@ impl World { // Handle player movement and animation self.step_player(controls, dt); - // TODO: Handle lasers and bullets - // Movements can be multiplied by the delta-time frame count, instead of looping + // Handle bullet movement + if let Some(bullet) = &mut self.bullet { + let velocity = update_dt(&mut bullet.dt, dt) * 4; + + if bullet.pos.y > velocity { + bullet.pos.y -= velocity; + bullet.sprite.animate(&self.assets, dt); + } else { + self.bullet = None; + } + } + + // Handle laser movement + let mut destroy = Vec::new(); + for (i, laser) in self.lasers.iter_mut().enumerate() { + let velocity = update_dt(&mut laser.dt, dt) * 2; + + if laser.pos.y < self.player.pos.y { + laser.pos.y += velocity; + laser.sprite.animate(&self.assets, dt); + } else { + destroy.push(i); + } + } + + // Destroy dead lasers + for i in destroy.iter().rev() { + self.lasers.remove(*i); + } } /// Draw the internal state to the screen. @@ -198,41 +243,131 @@ impl World { // Draw the player blit(&mut self.screen, &self.player.pos, &self.player.sprite); + // Draw the bullet + if let Some(bullet) = &self.bullet { + blit(&mut self.screen, &bullet.pos, &bullet.sprite); + } + + // Draw lasers + for laser in self.lasers.iter() { + blit(&mut self.screen, &laser.pos, &laser.sprite); + } + + if self.debug { + // Draw invaders bounding box + let (left, right) = self.invaders.get_bounds(); + let red = [255, 0, 0, 255]; + + let p1 = Point::new(left, START.y); + let p2 = Point::new(left, self.player.pos.y); + line(&mut self.screen, &p1, &p2, red); + + let p1 = Point::new(right, START.y); + let p2 = Point::new(right, self.player.pos.y); + line(&mut self.screen, &p1, &p2, red); + } + &self.screen } fn step_invaders(&mut self) { - // Find the next invader - let mut invader = None; - while let None = invader { - let (col, row) = self.invaders.stepper.incr(); - invader = self.invaders.grid[row][col].as_mut(); - } - let invader = invader.unwrap(); + let (left, right) = self.invaders.get_bounds(); + let (invader, is_leader) = + next_invader(&mut self.invaders.grid, &mut self.invaders.stepper); - // TODO: Move the invader + // The leader controls the fleet + if is_leader { + // The leader first commands the fleet to stop descending + self.invaders.descend = false; + + // Then the leader redirects the fleet when they reach the boundaries + match self.invaders.direction { + Direction::Left => { + if left < 2 { + self.invaders.bounds.px += 2; + self.invaders.descend = true; + self.invaders.direction = Direction::Right; + } else { + self.invaders.bounds.px -= 2; + } + } + Direction::Right => { + if right > SCREEN_WIDTH - 2 { + self.invaders.bounds.px -= 2; + self.invaders.descend = true; + self.invaders.direction = Direction::Left; + } else { + self.invaders.bounds.px += 2; + } + } + _ => unreachable!(), + } + } + + // Every invader in the fleet moves 2px per frame + match self.invaders.direction { + Direction::Left => invader.pos.x -= 2, + Direction::Right => invader.pos.x += 2, + _ => unreachable!(), + } + + // And they descend 8px on command + if self.invaders.descend { + invader.pos.y += 8; + + // One of the end scenarios + if invader.pos.y + 8 >= self.player.pos.y { + self.gameover = true; + } + } // Animate the invader invader.sprite.step_frame(&self.assets); + + // They also shoot lasers at random with a 1:50 chance + let r = self.random.next_u32() as usize; + let chance = r % 50; + if self.lasers.len() < 3 && chance == 0 { + // Pick a random column to begin searching for an invader that can fire a laser + let col = r / 50 % COLS; + let invader = self.invaders.get_closest_invader(col); + + let laser = Laser { + sprite: SpriteRef::new(&self.assets, Frame::Laser1, Duration::from_millis(16)), + pos: Point::new(invader.pos.x + 4, invader.pos.y + 10), + dt: 0, + }; + self.lasers.push(laser); + } } - fn step_player(&mut self, controls: &Controls, dt: Duration) { + fn step_player(&mut self, controls: &Controls, dt: &Duration) { + let frames = update_dt(&mut self.player.dt, dt); + match controls.direction { Direction::Left => { - if self.player.pos.x > 0 { - self.player.pos.x -= 1; + if self.player.pos.x >= frames { + self.player.pos.x -= frames; self.player.sprite.animate(&self.assets, dt); } } Direction::Right => { - if self.player.pos.x < 224 - 16 { - self.player.pos.x += 1; + if self.player.pos.x < SCREEN_WIDTH - 15 - frames { + self.player.pos.x += frames; self.player.sprite.animate(&self.assets, dt); } } _ => (), } + + if controls.fire && self.bullet.is_none() { + self.bullet = Some(Bullet { + sprite: SpriteRef::new(&self.assets, Frame::Bullet1, Duration::from_millis(32)), + pos: Point::new(self.player.pos.x + 7, self.player.pos.y), + dt: 0, + }); + } } /// Clear the screen @@ -271,27 +406,32 @@ impl std::ops::Mul for Point { } } -impl Stepper { - fn incr(&mut self) -> (usize, usize) { - self.col += 1; - if self.col >= COLS { - self.col = 0; - if self.row == 0 { - self.row = ROWS - 1; - } else { - self.row -= 1; - } - } +impl Invaders { + fn get_bounds(&self) -> (usize, usize) { + let width = (self.bounds.right_col - self.bounds.left_col + 1) * GRID.x; - (self.col, self.row) + let left = self.bounds.px; + let right = left + width; + + (left, right) } -} -impl Default for Stepper { - fn default() -> Self { - Self { - row: 0, - col: COLS - 1, + fn get_closest_invader(&self, mut col: usize) -> &Invader { + let mut row = ROWS - 1; + loop { + if self.grid[row][col].is_some() { + return self.grid[row][col].as_ref().unwrap(); + } + + if row == 0 { + row = ROWS - 1; + col += 1; + if col == COLS { + col = 0; + } + } else { + row -= 1; + } } } } @@ -299,9 +439,9 @@ impl Default for Stepper { impl Default for Bounds { fn default() -> Self { Self { - left: START.x, - right: START.x + COLS * GRID.x, - bottom: START.y + ROWS * GRID.y, + left_col: 0, + right_col: COLS - 1, + px: START.x, } } } @@ -350,3 +490,38 @@ fn make_invader_grid(assets: &Assets) -> Vec>> { })) .collect() } + +fn next_invader<'a>( + invaders: &'a mut Vec>>, + stepper: &mut Point, +) -> (&'a mut Invader, bool) { + let mut is_leader = false; + + loop { + // Iterate through the entire grid + stepper.x += 1; + if stepper.x >= COLS { + stepper.x = 0; + if stepper.y == 0 { + stepper.y = ROWS - 1; + + // After a full cycle, the next invader will be the leader + is_leader = true; + } else { + stepper.y -= 1; + } + } + + if invaders[stepper.y][stepper.x].is_some() { + return (invaders[stepper.y][stepper.x].as_mut().unwrap(), is_leader); + } + } +} + +fn update_dt(dest_dt: &mut usize, dt: &Duration) -> usize { + *dest_dt += dt.subsec_nanos() as usize; + let frames = *dest_dt / 16_666_667; + *dest_dt -= frames * 16_666_667; + + frames +} diff --git a/simple-invaders/src/loader.rs b/simple-invaders/src/loader.rs index 88b39fa..d7b0e18 100644 --- a/simple-invaders/src/loader.rs +++ b/simple-invaders/src/loader.rs @@ -37,8 +37,20 @@ pub(crate) fn load_assets() -> Assets { sprites.insert(Shield1, load_pcx(include_bytes!("assets/shield.pcx"))); - // sprites.insert(Laser1, load_pcx(include_bytes!("assets/laser1.pcx"))); - // sprites.insert(Laser2, load_pcx(include_bytes!("assets/laser2.pcx"))); + sprites.insert(Bullet1, load_pcx(include_bytes!("assets/bullet1.pcx"))); + sprites.insert(Bullet2, load_pcx(include_bytes!("assets/bullet2.pcx"))); + sprites.insert(Bullet3, load_pcx(include_bytes!("assets/bullet3.pcx"))); + sprites.insert(Bullet4, load_pcx(include_bytes!("assets/bullet4.pcx"))); + sprites.insert(Bullet5, load_pcx(include_bytes!("assets/bullet5.pcx"))); + + sprites.insert(Laser1, load_pcx(include_bytes!("assets/laser1.pcx"))); + sprites.insert(Laser2, load_pcx(include_bytes!("assets/laser2.pcx"))); + sprites.insert(Laser3, load_pcx(include_bytes!("assets/laser3.pcx"))); + sprites.insert(Laser4, load_pcx(include_bytes!("assets/laser4.pcx"))); + sprites.insert(Laser5, load_pcx(include_bytes!("assets/laser5.pcx"))); + sprites.insert(Laser6, load_pcx(include_bytes!("assets/laser6.pcx"))); + sprites.insert(Laser7, load_pcx(include_bytes!("assets/laser7.pcx"))); + sprites.insert(Laser8, load_pcx(include_bytes!("assets/laser8.pcx"))); Assets { sprites } } diff --git a/simple-invaders/src/sprites.rs b/simple-invaders/src/sprites.rs index 18a318d..081aa88 100644 --- a/simple-invaders/src/sprites.rs +++ b/simple-invaders/src/sprites.rs @@ -1,8 +1,10 @@ +use std::cmp::min; use std::rc::Rc; use std::time::Duration; use crate::loader::Assets; -use crate::{Point, SCREEN_WIDTH}; +use crate::{Point, SCREEN_HEIGHT, SCREEN_WIDTH}; +use line_drawing::Bresenham; // This is the type stored in the `Assets` hash map pub(crate) type CachedSprite = (usize, usize, Rc>); @@ -23,8 +25,21 @@ pub(crate) enum Frame { Player2, Shield1, - // Laser1, - // Laser2, + + Bullet1, + Bullet2, + Bullet3, + Bullet4, + Bullet5, + + Laser1, + Laser2, + Laser3, + Laser4, + Laser5, + Laser6, + Laser7, + Laser8, } /// Sprites can be drawn and procedurally generated. @@ -59,7 +74,7 @@ pub(crate) trait Drawable { } pub(crate) trait Animation { - fn animate(&mut self, assets: &Assets, dt: Duration); + fn animate(&mut self, assets: &Assets, dt: &Duration); } impl Sprite { @@ -105,10 +120,22 @@ impl SpriteRef { Player1 => (assets.get(&Player2).unwrap().2.clone(), Player2), Player2 => (assets.get(&Player1).unwrap().2.clone(), Player1), - // This should not happen, but here we are! - Shield1 => (assets.get(&Shield1).unwrap().2.clone(), Shield1), - // Laser1 => (assets.get(&Laser2).unwrap().2.clone(), Laser2), - // Laser2 => (assets.get(&Laser1).unwrap().2.clone(), Laser1), + Bullet1 => (assets.get(&Bullet2).unwrap().2.clone(), Bullet2), + Bullet2 => (assets.get(&Bullet3).unwrap().2.clone(), Bullet3), + Bullet3 => (assets.get(&Bullet4).unwrap().2.clone(), Bullet4), + Bullet4 => (assets.get(&Bullet5).unwrap().2.clone(), Bullet5), + Bullet5 => (assets.get(&Bullet1).unwrap().2.clone(), Bullet1), + + Laser1 => (assets.get(&Laser2).unwrap().2.clone(), Laser2), + Laser2 => (assets.get(&Laser3).unwrap().2.clone(), Laser3), + Laser3 => (assets.get(&Laser4).unwrap().2.clone(), Laser4), + Laser4 => (assets.get(&Laser5).unwrap().2.clone(), Laser5), + Laser5 => (assets.get(&Laser6).unwrap().2.clone(), Laser6), + Laser6 => (assets.get(&Laser7).unwrap().2.clone(), Laser7), + Laser7 => (assets.get(&Laser8).unwrap().2.clone(), Laser8), + Laser8 => (assets.get(&Laser1).unwrap().2.clone(), Laser1), + + _ => unreachable!(), }; self.pixels = pixels; @@ -145,11 +172,11 @@ impl Drawable for SpriteRef { } impl Animation for SpriteRef { - fn animate(&mut self, assets: &Assets, dt: Duration) { + fn animate(&mut self, assets: &Assets, dt: &Duration) { if self.duration.subsec_nanos() == 0 { self.step_frame(assets); } else { - self.dt += dt; + self.dt += *dt; while self.dt >= self.duration { self.dt -= self.duration; @@ -164,13 +191,49 @@ pub(crate) fn blit(screen: &mut [u8], dest: &Point, sprite: &S) where S: Drawable, { + assert!(dest.x + sprite.width() <= SCREEN_WIDTH); + assert!(dest.y + sprite.height() <= SCREEN_HEIGHT); + let pixels = sprite.pixels(); let width = sprite.width() * 4; let mut s = 0; for y in 0..sprite.height() { let i = dest.x * 4 + dest.y * SCREEN_WIDTH * 4 + y * SCREEN_WIDTH * 4; - screen[i..i + width].copy_from_slice(&pixels[s..s + width]); + + // Merge pixels from sprite into screen + let zipped = screen[i..i + width].iter_mut().zip(&pixels[s..s + width]); + for (left, right) in zipped { + if *right > 0 { + *left = *right; + } + } + s += width; } } + +/// Draw a line to the pixel buffer using Bresenham's algorithm. +pub(crate) fn line(screen: &mut [u8], p1: &Point, p2: &Point, color: [u8; 4]) { + let p1 = (p1.x as i64, p1.y as i64); + let p2 = (p2.x as i64, p2.y as i64); + + for (x, y) in Bresenham::new(p1, p2) { + let x = min(x as usize, SCREEN_WIDTH - 1); + let y = min(y as usize, SCREEN_HEIGHT - 1); + let i = x * 4 + y * SCREEN_WIDTH * 4; + + screen[i..i + 4].copy_from_slice(&color); + } +} + +/// Draw a rectangle to the pixel buffer using two points in opposite corners. +pub(crate) fn _rect(screen: &mut [u8], p1: &Point, p2: &Point, color: [u8; 4]) { + let p3 = Point::new(p1.x, p2.y); + let p4 = Point::new(p2.x, p1.y); + + line(screen, p1, &p3, color); + line(screen, &p3, p2, color); + line(screen, p2, &p4, color); + line(screen, &p4, p1, color); +}