├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── resources ├── add-ship.png ├── clap.ogg ├── credits.txt ├── crosshair.png ├── div-ship.png ├── explosion.png ├── explosion.wav ├── fail.ogg ├── laser.ogg ├── launch.wav ├── levels.json ├── main.ttf ├── mul-ship.png ├── music.mp3 ├── number.ttf ├── spacebg1.jpg ├── spacebg2.jpg ├── spacebg3.jpg ├── spacebg4.jpg ├── spacebg5.jpg ├── stars1.png ├── stars2.png ├── sub-ship.png ├── title.ttf └── turret.png └── src ├── alien.rs ├── assets.rs ├── background.rs ├── crosshair.rs ├── explosion.rs ├── ggez_utility.rs ├── level.rs ├── main.rs ├── mbtext.rs ├── message.rs └── turret.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | 3 | name = "mathdefense" 4 | version = "0.0.1" 5 | authors = [ "Jack" ] 6 | edition="2018" 7 | 8 | [dependencies] 9 | ggez = "*" 10 | rand = "*" 11 | nalgebra = "*" 12 | serde = { version = "*", features = ["derive"] } 13 | serde_json = "*" 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jack Mott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Math game for kids 2 | 3 | In order to use this, first install rust (depends on your Operating System) 4 | 5 | Then do: 6 | 7 | cargo build 8 | 9 | Then: 10 | 11 | cargo run 12 | -------------------------------------------------------------------------------- /resources/add-ship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/add-ship.png -------------------------------------------------------------------------------- /resources/clap.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/clap.ogg -------------------------------------------------------------------------------- /resources/credits.txt: -------------------------------------------------------------------------------- 1 | All assets sourced from opengameart.org 2 | 3 | Background art by Rawdanitsu 4 | Spaceships by http://millionthvector.blogspot.com/ 5 | Explosion graphics by Cuzco 6 | Music by Telaron 7 | Sound effects by bart, Skorpio, and TinyWorlds, Blender Foundation, and LeeZh 8 | 9 | Programming / Design - Jack Mott 10 | 11 | Testing: Theo and Ada Mott 12 | 13 | Special thanks to the Mozilla Foundation for Rust, and to icefoxen for ggez 14 | -------------------------------------------------------------------------------- /resources/crosshair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/crosshair.png -------------------------------------------------------------------------------- /resources/div-ship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/div-ship.png -------------------------------------------------------------------------------- /resources/explosion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/explosion.png -------------------------------------------------------------------------------- /resources/explosion.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/explosion.wav -------------------------------------------------------------------------------- /resources/fail.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/fail.ogg -------------------------------------------------------------------------------- /resources/laser.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/laser.ogg -------------------------------------------------------------------------------- /resources/launch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/launch.wav -------------------------------------------------------------------------------- /resources/levels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "waves": [ 4 | { 5 | "groups": [ 6 | { 7 | "operation": "Add", 8 | "speed": 2.5, 9 | "num_ships": 5, 10 | "max_number": 5, 11 | "min_number": 0 12 | } 13 | ] 14 | }, 15 | { 16 | "groups": [ 17 | { 18 | "operation": "Add", 19 | "speed": 3.5, 20 | "num_ships": 8, 21 | "max_number": 5, 22 | "min_number": 0 23 | } 24 | ] 25 | }, 26 | { 27 | "groups": [ 28 | { 29 | "operation": "Add", 30 | "speed": 4.5, 31 | "num_ships": 10, 32 | "max_number": 5, 33 | "min_number": 0 34 | } 35 | ] 36 | } 37 | ], 38 | "background_file": "/spacebg1.jpg", 39 | "title": "Addition Attack!", 40 | "unlocked": [ 41 | true, 42 | true, 43 | true, 44 | true 45 | ] 46 | }, 47 | { 48 | "waves": [ 49 | { 50 | "groups": [ 51 | { 52 | "operation": "Subtract", 53 | "speed": 2.5, 54 | "num_ships": 5, 55 | "max_number": 5, 56 | "min_number": 0 57 | } 58 | ] 59 | }, 60 | { 61 | "groups": [ 62 | { 63 | "operation": "Subtract", 64 | "speed": 3.5, 65 | "num_ships": 8, 66 | "max_number": 5, 67 | "min_number": 0 68 | } 69 | ] 70 | }, 71 | { 72 | "groups": [ 73 | { 74 | "operation": "Subtract", 75 | "speed": 4.5, 76 | "num_ships": 10, 77 | "max_number": 5, 78 | "min_number": 0 79 | } 80 | ] 81 | } 82 | ], 83 | "background_file": "/spacebg2.jpg", 84 | "title": "Subtraction Subterfuge!", 85 | "unlocked": [ 86 | true, 87 | false, 88 | false, 89 | false 90 | ] 91 | }, 92 | { 93 | "waves": [ 94 | { 95 | "groups": [ 96 | { 97 | "operation": "Multiply", 98 | "speed": 2.5, 99 | "num_ships": 5, 100 | "max_number": 5, 101 | "min_number": 0 102 | } 103 | ] 104 | }, 105 | { 106 | "groups": [ 107 | { 108 | "operation": "Multiply", 109 | "speed": 3.5, 110 | "num_ships": 8, 111 | "max_number": 5, 112 | "min_number": 0 113 | } 114 | ] 115 | }, 116 | { 117 | "groups": [ 118 | { 119 | "operation": "Multiply", 120 | "speed": 4.5, 121 | "num_ships": 10, 122 | "max_number": 5, 123 | "min_number": 0 124 | } 125 | ] 126 | } 127 | ], 128 | "background_file": "/spacebg3.jpg", 129 | "title": "Multiplication Mayhem!", 130 | "unlocked": [ 131 | false, 132 | false, 133 | false, 134 | false 135 | ] 136 | }, 137 | { 138 | "waves": [ 139 | { 140 | "groups": [ 141 | { 142 | "operation": "Divide", 143 | "speed": 2.5, 144 | "num_ships": 5, 145 | "max_number": 6, 146 | "min_number": 0 147 | } 148 | ] 149 | }, 150 | { 151 | "groups": [ 152 | { 153 | "operation": "Divide", 154 | "speed": 3.5, 155 | "num_ships": 8, 156 | "max_number": 6, 157 | "min_number": 0 158 | } 159 | ] 160 | }, 161 | { 162 | "groups": [ 163 | { 164 | "operation": "Divide", 165 | "speed": 4.5, 166 | "num_ships": 10, 167 | "max_number": 6, 168 | "min_number": 0 169 | } 170 | ] 171 | } 172 | ], 173 | "background_file": "/spacebg4.jpg", 174 | "title": "Division Disaster!", 175 | "unlocked": [ 176 | false, 177 | false, 178 | false, 179 | false 180 | ] 181 | }, 182 | { 183 | "waves": [ 184 | { 185 | "groups": [ 186 | { 187 | "operation": "Add", 188 | "speed": 3.5, 189 | "num_ships": 5, 190 | "max_number": 5, 191 | "min_number": 0 192 | }, 193 | { 194 | "operation": "Subtract", 195 | "speed": 2.5, 196 | "num_ships": 5, 197 | "max_number": 5, 198 | "min_number": 0 199 | } 200 | ] 201 | }, 202 | { 203 | "groups": [ 204 | { 205 | "operation": "Add", 206 | "speed": 3.5, 207 | "num_ships": 3, 208 | "max_number": 5, 209 | "min_number": 0 210 | }, 211 | { 212 | "operation": "Subtract", 213 | "speed": 2.5, 214 | "num_ships": 3, 215 | "max_number": 5, 216 | "min_number": 0 217 | }, 218 | { 219 | "operation": "Multiply", 220 | "speed": 2.5, 221 | "num_ships": 3, 222 | "max_number": 5, 223 | "min_number": 0 224 | } 225 | ] 226 | }, 227 | { 228 | "groups": [ 229 | { 230 | "operation": "Add", 231 | "speed": 3.5, 232 | "num_ships": 3, 233 | "max_number": 5, 234 | "min_number": 0 235 | }, 236 | { 237 | "operation": "Subtract", 238 | "speed": 3.5, 239 | "num_ships": 3, 240 | "max_number": 5, 241 | "min_number": 0 242 | }, 243 | { 244 | "operation": "Multiply", 245 | "speed": 2.5, 246 | "num_ships": 3, 247 | "max_number": 5, 248 | "min_number": 0 249 | }, 250 | { 251 | "operation": "Divide", 252 | "speed": 1.5, 253 | "num_ships": 3, 254 | "max_number": 6, 255 | "min_number": 0 256 | } 257 | ] 258 | } 259 | ], 260 | "background_file": "/spacebg5.jpg", 261 | "title": "The Final Assault!", 262 | "unlocked": [ 263 | false, 264 | false, 265 | false, 266 | false 267 | ] 268 | } 269 | ] -------------------------------------------------------------------------------- /resources/main.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/main.ttf -------------------------------------------------------------------------------- /resources/mul-ship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/mul-ship.png -------------------------------------------------------------------------------- /resources/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/music.mp3 -------------------------------------------------------------------------------- /resources/number.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/number.ttf -------------------------------------------------------------------------------- /resources/spacebg1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/spacebg1.jpg -------------------------------------------------------------------------------- /resources/spacebg2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/spacebg2.jpg -------------------------------------------------------------------------------- /resources/spacebg3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/spacebg3.jpg -------------------------------------------------------------------------------- /resources/spacebg4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/spacebg4.jpg -------------------------------------------------------------------------------- /resources/spacebg5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/spacebg5.jpg -------------------------------------------------------------------------------- /resources/stars1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/stars1.png -------------------------------------------------------------------------------- /resources/stars2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/stars2.png -------------------------------------------------------------------------------- /resources/sub-ship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/sub-ship.png -------------------------------------------------------------------------------- /resources/title.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/title.ttf -------------------------------------------------------------------------------- /resources/turret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackmott/mathblaster/6fa273f092aa5eea65f837f6108b044f5b1e2efc/resources/turret.png -------------------------------------------------------------------------------- /src/alien.rs: -------------------------------------------------------------------------------- 1 | use ggez::graphics::{self, Color, DrawParam}; 2 | use ggez::nalgebra as na; 3 | 4 | use crate::assets::*; 5 | use crate::explosion::*; 6 | use crate::ggez_utility::*; 7 | use crate::level::*; 8 | use crate::turret::*; 9 | use ggez::Context; 10 | 11 | #[derive(PartialEq)] 12 | pub enum AlienState { 13 | Alive, 14 | Exploding, 15 | Dead, 16 | } 17 | pub struct Alien { 18 | pub operation: Operation, 19 | pub speed: f32, 20 | pub pos: na::Point2, 21 | pub text: graphics::Text, 22 | pub answer: i32, 23 | pub explosion: Explosion, 24 | pub state: AlienState, 25 | pub src_pixel_width: f32, 26 | pub src_pixel_height: f32, 27 | } 28 | impl Scalable for Alien { 29 | fn pct_pos(&self) -> na::Point2 { 30 | self.pos 31 | } 32 | fn pct_dimensions(&self) -> (f32, f32) { 33 | (0.045, 0.07) 34 | } 35 | fn src_pixel_dimensions(&self) -> (f32, f32) { 36 | (self.src_pixel_width, self.src_pixel_height) 37 | } 38 | } 39 | impl Alien { 40 | pub fn update(&mut self, turret: &mut Turret, ctx: &mut Context, dt: std::time::Duration) { 41 | if self.state != AlienState::Dead { 42 | let sec = dt.as_millis() as f32 / 100000.0; 43 | if self.pos[1] < 0.07 { 44 | self.pos = self.pos + na::Vector2::new(0.0, self.speed * 3. * sec); 45 | } else { 46 | self.pos = self.pos + na::Vector2::new(0.0, self.speed * sec); 47 | } 48 | if self.state == AlienState::Exploding { 49 | self.explosion.update(ctx, dt); 50 | } 51 | if self.explosion.elapsed > self.explosion.duration { 52 | self.state = AlienState::Dead; 53 | turret.state = TurretState::Resting; 54 | } 55 | } 56 | } 57 | 58 | pub fn draw(&mut self, ctx: &mut Context, assets: &mut Assets) { 59 | if self.state != AlienState::Dead { 60 | if self.explosion.elapsed < self.explosion.duration / 2.0 { 61 | let params = DrawParam::new() 62 | .color(Color::from((255, 255, 255, 255))) 63 | .dest(self.pixel_pos(graphics::size(ctx))) 64 | .scale(self.scale(graphics::size(ctx))) 65 | .offset(na::Point2::new(0.5, 0.5)); 66 | let img = match self.operation { 67 | Operation::Add => &assets.add_ship, 68 | Operation::Subtract => &assets.sub_ship, 69 | Operation::Multiply => &assets.mul_ship, 70 | Operation::Divide => &assets.div_ship, 71 | }; 72 | let _ = graphics::draw(ctx, img, params); 73 | 74 | let tw = self.text.width(ctx) as f32; 75 | let (sw, sh) = self.dest_pixel_dimensions(graphics::size(ctx)); 76 | let offsetx = -sw / 2.0 + (sw - tw) / 2.0; 77 | let offsety = -sh / 1.2; 78 | 79 | let offset = na::Vector2::new(offsetx, offsety); 80 | 81 | let text_param = DrawParam::new() 82 | .color(Color::from((255, 255, 255, 255))) 83 | .dest(self.pixel_pos(graphics::size(ctx)) + offset); 84 | let _ = graphics::draw(ctx, &self.text, text_param); 85 | } 86 | } 87 | 88 | if self.state == AlienState::Exploding { 89 | self.explosion.pos = self.pct_pos(); 90 | self.explosion.draw(ctx, assets); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/assets.rs: -------------------------------------------------------------------------------- 1 | use ggez::audio; 2 | use ggez::graphics::{self}; 3 | use ggez::Context; 4 | 5 | pub struct Assets { 6 | pub add_ship: graphics::Image, 7 | pub sub_ship: graphics::Image, 8 | pub mul_ship: graphics::Image, 9 | pub div_ship: graphics::Image, 10 | pub crosshair: graphics::Image, 11 | pub title_font: graphics::Font, 12 | pub main_font: graphics::Font, 13 | pub number_font: graphics::Font, 14 | pub turret: graphics::Image, 15 | pub background: graphics::Image, 16 | pub stars1: graphics::Image, 17 | pub stars2: graphics::Image, 18 | pub explosion: graphics::Image, 19 | pub explosion_sound: audio::Source, 20 | pub clap_sound: audio::Source, 21 | pub launch_sound: audio::Source, 22 | pub fail_sound: audio::Source, 23 | pub laser_sound: audio::Source, 24 | pub music: audio::Source, 25 | } 26 | 27 | impl Assets { 28 | pub fn new(ctx: &mut Context, start_bg: String) -> Assets { 29 | Assets { 30 | add_ship: graphics::Image::new(ctx, "/add-ship.png").unwrap(), 31 | sub_ship: graphics::Image::new(ctx, "/sub-ship.png").unwrap(), 32 | mul_ship: graphics::Image::new(ctx, "/mul-ship.png").unwrap(), 33 | crosshair: graphics::Image::new(ctx, "/crosshair.png").unwrap(), 34 | div_ship: graphics::Image::new(ctx, "/div-ship.png").unwrap(), 35 | title_font: graphics::Font::new(ctx, "/title.ttf").unwrap(), 36 | main_font: graphics::Font::new(ctx, "/main.ttf").unwrap(), 37 | number_font: graphics::Font::new(ctx, "/number.ttf").unwrap(), 38 | turret: graphics::Image::new(ctx, "/turret.png").unwrap(), 39 | background: graphics::Image::new(ctx, start_bg).unwrap(), 40 | stars1: graphics::Image::new(ctx, "/stars1.png").unwrap(), 41 | stars2: graphics::Image::new(ctx, "/stars2.png").unwrap(), 42 | explosion: graphics::Image::new(ctx, "/explosion.png").unwrap(), 43 | explosion_sound: audio::Source::new(ctx, "/explosion.wav").unwrap(), 44 | clap_sound: audio::Source::new(ctx, "/clap.ogg").unwrap(), 45 | launch_sound: audio::Source::new(ctx, "/launch.wav").unwrap(), 46 | fail_sound: audio::Source::new(ctx, "/fail.ogg").unwrap(), 47 | laser_sound: audio::Source::new(ctx, "/laser.ogg").unwrap(), 48 | music: audio::Source::new(ctx, "/music.mp3").unwrap(), 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/background.rs: -------------------------------------------------------------------------------- 1 | use crate::assets::*; 2 | use crate::ggez_utility::*; 3 | use ggez::graphics; 4 | use ggez::graphics::DrawParam; 5 | use ggez::nalgebra as na; 6 | use ggez::Context; 7 | 8 | pub struct Background { 9 | pub src_pixel_width: f32, 10 | pub src_pixel_height: f32, 11 | pub stars1_pos: f32, 12 | pub stars2_pos: f32, 13 | } 14 | 15 | impl Background { 16 | pub fn update(&mut self, dt: std::time::Duration, multiplier: f32) { 17 | //update the parallax stars are different rates 18 | let t = dt.as_millis() as f32; 19 | self.stars1_pos = ((self.stars1_pos + t / 60_000.0) * multiplier) % 1.0; 20 | self.stars2_pos = ((self.stars2_pos + t / 16_000.0) * multiplier) % 1.0; 21 | } 22 | 23 | pub fn draw_no_stars(&mut self, ctx: &mut Context, assets: &Assets) { 24 | let background_param = graphics::DrawParam::new().scale(self.scale(graphics::size(ctx))); 25 | let _ = graphics::draw(ctx, &assets.background, background_param); 26 | } 27 | 28 | pub fn draw(&mut self, ctx: &mut Context, assets: &Assets) { 29 | let background_param = graphics::DrawParam::new().scale(self.scale(graphics::size(ctx))); 30 | let _ = graphics::draw(ctx, &assets.background, background_param); 31 | 32 | let (screen_wf, screen_hf) = graphics::size(ctx); 33 | let screen_w = screen_wf as i32; 34 | let screen_h = screen_hf as i32; 35 | 36 | //stars 1 37 | let w = (assets.stars1.width() / 2) as usize; 38 | let h = (assets.stars1.height() / 2) as usize; 39 | let pixel_offset = (self.stars1_pos * h as f32) as usize; 40 | let mut y = 0; 41 | while y < screen_h { 42 | let (y_pct, delta) = if y == 0 { 43 | let y_start = (h - pixel_offset) % h; 44 | ( 45 | y_start as f32 / h as f32, 46 | if pixel_offset == 0 { h } else { pixel_offset }, 47 | ) 48 | } else { 49 | (0.0, h) 50 | }; 51 | let rect = graphics::Rect::new(0.0, y_pct, 1.0, 1.0 - y_pct); 52 | for x in (0..screen_w).step_by(w) { 53 | let star_param = DrawParam::new() 54 | .src(rect) 55 | .dest(na::Point2::new(x as f32, y as f32)) 56 | .scale(na::Vector2::new(0.5, 0.5)); 57 | let _ = graphics::draw(ctx, &assets.stars1, star_param); 58 | } 59 | 60 | y += delta as i32; 61 | } 62 | 63 | //stars 2 64 | let w = (assets.stars2.width() * 2) as usize; 65 | let h = (assets.stars2.height() * 2) as usize; 66 | let pixel_offset = (self.stars2_pos * h as f32) as usize; 67 | let mut y = 0; 68 | while y < screen_h { 69 | let (y_pct, delta) = if y == 0 { 70 | let y_start = (h - pixel_offset) % h; 71 | ( 72 | y_start as f32 / h as f32, 73 | if pixel_offset == 0 { h } else { pixel_offset }, 74 | ) 75 | } else { 76 | (0.0, h) 77 | }; 78 | let rect = graphics::Rect::new(0.0, y_pct, 1.0, 1.0 - y_pct); 79 | for x in (0..screen_w).step_by(w) { 80 | let star_param = DrawParam::new() 81 | .src(rect) 82 | .dest(na::Point2::new(x as f32, y as f32)) 83 | .scale(na::Vector2::new(2.0, 2.0)); 84 | let _ = graphics::draw(ctx, &assets.stars2, star_param); 85 | } 86 | 87 | y += delta as i32; 88 | } 89 | } 90 | } 91 | 92 | impl Scalable for Background { 93 | fn pct_pos(&self) -> na::Point2 { 94 | na::Point2::new(0.0, 0.0) 95 | } 96 | fn pct_dimensions(&self) -> (f32, f32) { 97 | (1.0, 1.0) 98 | } 99 | fn src_pixel_dimensions(&self) -> (f32, f32) { 100 | (self.src_pixel_width, self.src_pixel_height) 101 | } 102 | fn scale(&self, screen_dimensions: (f32, f32)) -> na::Vector2 { 103 | let (sw, sh) = self.dest_pixel_dimensions(screen_dimensions); 104 | let (tw, th) = self.src_pixel_dimensions(); 105 | // only use screen width for scaling 106 | na::Vector2::new(sw / tw, sh / th) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/crosshair.rs: -------------------------------------------------------------------------------- 1 | use crate::assets::*; 2 | use crate::ggez_utility::*; 3 | use ggez::graphics::{self, Color}; 4 | use ggez::nalgebra as na; 5 | use ggez::Context; 6 | 7 | pub struct Crosshair { 8 | pub elapsed: u32, 9 | pub src_pixel_width: f32, 10 | pub src_pixel_height: f32, 11 | } 12 | 13 | const CROSSHAIR_TIME: u32 = 1000; 14 | 15 | impl Crosshair { 16 | pub fn update(&mut self, dt: std::time::Duration) { 17 | self.elapsed = (self.elapsed + dt.as_millis() as u32) % CROSSHAIR_TIME; 18 | } 19 | 20 | pub fn draw(&mut self, pos: na::Point2, ctx: &mut Context, assets: &Assets) { 21 | let pct = self.elapsed as f32 / CROSSHAIR_TIME as f32; 22 | let mut color = (510.0 * pct) as u32; 23 | if color > 255 { 24 | color = 255 - (color - 255); 25 | } 26 | 27 | let crosshair_params = graphics::DrawParam::new() 28 | .color(Color::from((255, color as u8, color as u8, 255))) 29 | .dest(pos) 30 | .scale(self.scale(graphics::size(ctx))) 31 | .offset(na::Point2::new(0.5, 0.5)); 32 | let _ = graphics::draw(ctx, &assets.crosshair, crosshair_params); 33 | } 34 | } 35 | impl Scalable for Crosshair { 36 | fn pct_pos(&self) -> na::Point2 { 37 | na::Point2::new(0.0, 0.0) 38 | } 39 | fn pct_dimensions(&self) -> (f32, f32) { 40 | (0.090, 0.125) 41 | } 42 | fn src_pixel_dimensions(&self) -> (f32, f32) { 43 | (self.src_pixel_width, self.src_pixel_height) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/explosion.rs: -------------------------------------------------------------------------------- 1 | use ggez::audio::SoundSource; 2 | use ggez::graphics::{self, Color, DrawParam}; 3 | use ggez::nalgebra as na; 4 | use ggez::Context; 5 | 6 | use crate::assets::*; 7 | 8 | pub struct Explosion { 9 | pub start_time: f32, // millis 10 | pub duration: f32, //millis 11 | pub elapsed: f32, //millis 12 | pub index: usize, 13 | pub pos: na::Point2, 14 | pub sound_played: bool, 15 | } 16 | impl Explosion { 17 | pub fn new(start_time: f32, pos: na::Point2) -> Explosion { 18 | Explosion { 19 | start_time: start_time, 20 | duration: 500.0, 21 | elapsed: 0.0, 22 | index: 0, 23 | pos: pos, 24 | sound_played: false, 25 | } 26 | } 27 | pub fn get_rect(&self) -> graphics::Rect { 28 | let index = 15 - self.index; //reverse the order 29 | let x = index % 4; 30 | let y = index / 4; 31 | graphics::Rect::new( 32 | x as f32 * 64.0 / 255.0, 33 | y as f32 * 64.0 / 255.0, 34 | 64.0 / 255.0, 35 | 64.0 / 255.0, 36 | ) 37 | } 38 | 39 | pub fn update(&mut self, _ctx: &mut Context, dt: std::time::Duration) { 40 | if self.elapsed - self.start_time <= self.duration { 41 | self.elapsed += dt.as_millis() as f32; 42 | if self.elapsed >= self.start_time { 43 | let mut index = ((self.elapsed - self.start_time) / self.duration * 30.0) as i32; 44 | if index > 15 { 45 | index = 15 + (15 - index); 46 | } 47 | self.index = index as usize; 48 | } 49 | } 50 | } 51 | 52 | pub fn draw(&mut self, ctx: &mut Context, assets: &mut Assets) { 53 | if self.elapsed >= self.start_time { 54 | if !self.sound_played { 55 | let _ = assets.laser_sound.play_detached(); 56 | let _ = assets.explosion_sound.play_detached(); 57 | self.sound_played = true; 58 | } 59 | if self.elapsed - self.start_time <= self.duration { 60 | let screen = graphics::size(ctx); 61 | let param = DrawParam::new() 62 | .color(Color::from((255, 255, 255, 255))) 63 | .dest( 64 | na::Point2::new(self.pos[0] * screen.0, self.pos[1] * screen.1) 65 | - na::Vector2::new(32.0, 32.0), 66 | ) 67 | .src(self.get_rect()); 68 | let _ = graphics::draw(ctx, &assets.explosion, param); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/ggez_utility.rs: -------------------------------------------------------------------------------- 1 | use ggez::graphics::Color; 2 | use ggez::nalgebra as na; 3 | 4 | pub const WHITE: Color = Color { 5 | r: 1.0, 6 | g: 1.0, 7 | b: 1.0, 8 | a: 1.0, 9 | }; 10 | pub const BLUE: Color = Color { 11 | r: 0.75, 12 | g: 0.0, 13 | b: 0.25, 14 | a: 1.0, 15 | }; 16 | pub const GRAY: Color = Color { 17 | r: 0.5, 18 | g: 0.5, 19 | b: 0.5, 20 | a: 1.0, 21 | }; 22 | 23 | pub const DARK_GRAY: Color = Color { 24 | r: 0.25, 25 | g: 0.25, 26 | b: 0.25, 27 | a: 1.0, 28 | }; 29 | 30 | pub trait Scalable { 31 | fn pct_dimensions(&self) -> (f32, f32); 32 | fn src_pixel_dimensions(&self) -> (f32, f32); 33 | fn pct_pos(&self) -> na::Point2; 34 | fn dest_pixel_dimensions(&self, screen_dimensions: (f32, f32)) -> (f32, f32) { 35 | let (w, h) = self.pct_dimensions(); 36 | (w * screen_dimensions.0, h * screen_dimensions.1) 37 | } 38 | fn scale(&self, window_dimensions: (f32, f32)) -> na::Vector2 { 39 | let (sw, sh) = self.dest_pixel_dimensions(window_dimensions); 40 | let (tw, th) = self.src_pixel_dimensions(); 41 | // only use screen width for scaling 42 | na::Vector2::new(sw / tw, sh / th) 43 | } 44 | fn pixel_pos(&self, screen_dimensions: (f32, f32)) -> na::Point2 { 45 | let p = self.pct_pos(); 46 | na::Point2::new(p[0] * screen_dimensions.0, p[1] * screen_dimensions.1) 47 | } 48 | } 49 | 50 | pub fn to_screen_pos(pos: (f32, f32), screen_dimensions: (f32, f32)) -> na::Point2 { 51 | na::Point2::new(pos.0 * screen_dimensions.0, pos.1 * screen_dimensions.1) 52 | } 53 | 54 | pub fn lerp_color(a: Color, b: Color, pct: f32) -> Color { 55 | let red = a.r * (1.0 - pct) + b.r * pct; 56 | let green = a.g * (1.0 - pct) + b.g * pct; 57 | let blue = a.b * (1.0 - pct) + b.b * pct; 58 | let alpha = a.a * (1.0 - pct) + b.a * pct; 59 | Color::new(red, green, blue, alpha) 60 | } 61 | -------------------------------------------------------------------------------- /src/level.rs: -------------------------------------------------------------------------------- 1 | use crate::assets::*; 2 | use crate::message::*; 3 | use ggez::Context; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use std::collections::VecDeque; 7 | use std::fs::File; 8 | use std::io::{Read, Write}; 9 | use std::str; 10 | 11 | #[derive(Deserialize, Serialize, Debug, Copy, Clone, PartialEq)] 12 | pub enum Operation { 13 | Add, 14 | Subtract, 15 | Multiply, 16 | Divide, 17 | } 18 | 19 | #[derive(Deserialize, Serialize)] 20 | pub struct Level { 21 | pub waves: Vec, 22 | pub background_file: String, 23 | pub title: String, 24 | pub unlocked: [bool; 4], 25 | } 26 | 27 | #[derive(Deserialize, Serialize)] 28 | pub struct Wave { 29 | pub groups: Vec, 30 | } 31 | 32 | #[derive(Deserialize, Serialize)] 33 | pub struct WaveGroup { 34 | pub operation: Operation, 35 | pub speed: f32, 36 | pub num_ships: usize, 37 | pub max_number: i32, 38 | pub min_number: i32, 39 | } 40 | 41 | pub const DIFFICULTY_NAMES: [&str; 4] = ["Rookie", "Cadet", "Veteran", "Space Marine"]; 42 | pub const SPEED_DIFFICULTY: [f32; 4] = [1.0, 1.1, 1.25, 1.5]; 43 | pub const MAX_NUMBER_DIFFICULTY: [f32; 4] = [1.0, 1.25, 2.0, 3.0]; 44 | pub const MIN_NUMBER_DIFFICULTY: [f32; 4] = [1.0, 1.25, 2.0, 3.0]; 45 | pub const NUM_SHIPS_DIFFICULTY: [f32; 4] = [1.0, 1.25, 2.0, 3.0]; 46 | 47 | impl Level { 48 | pub fn push_title(&self, messages: &mut VecDeque, assets: &Assets, ctx: &mut Context) { 49 | messages.push_back(Message::new(self.title.clone(), 2000.0, assets, ctx)); 50 | } 51 | 52 | pub fn load_from_file() -> Vec { 53 | //if any of this fails, call new instead 54 | fn load_helper() -> Result, String> { 55 | let mut file = File::open("resources/levels.json") 56 | .map_err(|e| format!("file not found\n {}", e))?; 57 | let mut buffer = Vec::new(); 58 | file.read_to_end(&mut buffer) 59 | .map_err(|e| format!("file could not be read\n{}", e))?; 60 | let level: Vec = serde_json::from_slice(&buffer[..]) 61 | .map_err(|e| format!("file not valid\n{}", e))?; 62 | Ok(level) 63 | } 64 | 65 | let result = load_helper(); 66 | match result { 67 | Ok(levels) => levels, 68 | Err(msg) => { 69 | // If we get an error, load the default and save a new levels.json file 70 | println!("Error loading level file.\nUsing default\n{}", msg); 71 | let levels = Level::new(); 72 | Level::save_levels(&levels); 73 | levels 74 | } 75 | } 76 | } 77 | 78 | pub fn save_levels(levels: &Vec) { 79 | fn save_helper(levels: &Vec) -> Result<(),String> { 80 | let serialized = serde_json::to_string_pretty(&levels).map_err(|_| "couldn't serialize levels")?; 81 | let mut file = File::create("resources/levels.json").map_err(|_| "couldn't create save file for levels")?; 82 | file.write_all(serialized.as_bytes()).map_err(|_| "couldn't wire to save file")?; 83 | Ok(()) 84 | } 85 | match save_helper(&levels) { 86 | Err(msg) => println!("{}",msg), 87 | Ok(_) => () 88 | } 89 | } 90 | 91 | // consider validating max_number to be sure it makes sense 92 | pub fn new() -> Vec { 93 | vec![ 94 | //Level 1 95 | Level { 96 | unlocked: [true, true, true, true], 97 | title: "Addition Attack!".to_string(), 98 | background_file: "/spacebg1.jpg".to_string(), 99 | waves: vec![ 100 | Wave { 101 | groups: vec![WaveGroup { 102 | speed: 2.5, 103 | max_number: 5, 104 | min_number: 0, 105 | operation: Operation::Add, 106 | num_ships: 5, 107 | }], 108 | }, 109 | Wave { 110 | groups: vec![WaveGroup { 111 | speed: 3.5, 112 | max_number: 5, 113 | min_number: 0, 114 | operation: Operation::Add, 115 | num_ships: 8, 116 | }], 117 | }, 118 | Wave { 119 | groups: vec![WaveGroup { 120 | speed: 4.5, 121 | max_number: 5, 122 | min_number: 0, 123 | operation: Operation::Add, 124 | num_ships: 10, 125 | }], 126 | }, 127 | ], 128 | }, 129 | //Level 2 130 | Level { 131 | unlocked: [false, false, false, false], 132 | title: "Subtraction Subterfuge!".to_string(), 133 | background_file: "/spacebg2.jpg".to_string(), 134 | waves: vec![ 135 | Wave { 136 | groups: vec![WaveGroup { 137 | speed: 2.5, 138 | max_number: 5, 139 | min_number: 0, 140 | operation: Operation::Subtract, 141 | num_ships: 5, 142 | }], 143 | }, 144 | Wave { 145 | groups: vec![WaveGroup { 146 | speed: 3.5, 147 | max_number: 5, 148 | min_number: 0, 149 | operation: Operation::Subtract, 150 | num_ships: 8, 151 | }], 152 | }, 153 | Wave { 154 | groups: vec![WaveGroup { 155 | speed: 4.5, 156 | max_number: 5, 157 | min_number: 0, 158 | operation: Operation::Subtract, 159 | num_ships: 10, 160 | }], 161 | }, 162 | ], 163 | }, 164 | //Level 3 165 | Level { 166 | unlocked: [false, false, false, false], 167 | title: "Multiplication Mayhem!".to_string(), 168 | background_file: "/spacebg3.jpg".to_string(), 169 | waves: vec![ 170 | Wave { 171 | groups: vec![WaveGroup { 172 | speed: 2.5, 173 | max_number: 5, 174 | min_number: 0, 175 | operation: Operation::Multiply, 176 | num_ships: 5, 177 | }], 178 | }, 179 | Wave { 180 | groups: vec![WaveGroup { 181 | speed: 3.5, 182 | max_number: 5, 183 | min_number: 0, 184 | operation: Operation::Multiply, 185 | num_ships: 8, 186 | }], 187 | }, 188 | Wave { 189 | groups: vec![WaveGroup { 190 | speed: 4.5, 191 | max_number: 5, 192 | min_number: 0, 193 | operation: Operation::Multiply, 194 | num_ships: 10, 195 | }], 196 | }, 197 | ], 198 | }, 199 | //Level 4 200 | Level { 201 | unlocked: [false, false, false, false], 202 | title: "Division Disaster!".to_string(), 203 | background_file: "/spacebg4.jpg".to_string(), 204 | waves: vec![ 205 | Wave { 206 | groups: vec![WaveGroup { 207 | speed: 2.5, 208 | max_number: 6, 209 | min_number: 0, 210 | operation: Operation::Divide, 211 | num_ships: 5, 212 | }], 213 | }, 214 | Wave { 215 | groups: vec![WaveGroup { 216 | speed: 3.5, 217 | max_number: 6, 218 | min_number: 0, 219 | operation: Operation::Divide, 220 | num_ships: 8, 221 | }], 222 | }, 223 | Wave { 224 | groups: vec![WaveGroup { 225 | speed: 4.5, 226 | max_number: 6, 227 | min_number: 0, 228 | operation: Operation::Divide, 229 | num_ships: 10, 230 | }], 231 | }, 232 | ], 233 | }, 234 | //Level 5 235 | Level { 236 | unlocked: [false, false, false, false], 237 | title: "The Final Assault!".to_string(), 238 | background_file: "/spacebg5.jpg".to_string(), 239 | waves: vec![ 240 | Wave { 241 | groups: vec![ 242 | WaveGroup { 243 | speed: 3.5, 244 | max_number: 5, 245 | min_number: 0, 246 | operation: Operation::Add, 247 | num_ships: 5, 248 | }, 249 | WaveGroup { 250 | speed: 2.5, 251 | max_number: 5, 252 | min_number: 0, 253 | operation: Operation::Subtract, 254 | num_ships: 5, 255 | }, 256 | ], 257 | }, 258 | Wave { 259 | groups: vec![ 260 | WaveGroup { 261 | speed: 3.5, 262 | max_number: 5, 263 | min_number: 0, 264 | operation: Operation::Add, 265 | num_ships: 3, 266 | }, 267 | WaveGroup { 268 | speed: 2.5, 269 | max_number: 5, 270 | min_number: 0, 271 | operation: Operation::Subtract, 272 | num_ships: 3, 273 | }, 274 | WaveGroup { 275 | speed: 2.5, 276 | max_number: 5, 277 | min_number: 0, 278 | operation: Operation::Multiply, 279 | num_ships: 3, 280 | }, 281 | ], 282 | }, 283 | Wave { 284 | groups: vec![ 285 | WaveGroup { 286 | speed: 3.5, 287 | max_number: 5, 288 | min_number: 0, 289 | operation: Operation::Add, 290 | num_ships: 3, 291 | }, 292 | WaveGroup { 293 | speed: 3.5, 294 | max_number: 5, 295 | min_number: 0, 296 | operation: Operation::Subtract, 297 | num_ships: 3, 298 | }, 299 | WaveGroup { 300 | speed: 2.5, 301 | max_number: 5, 302 | min_number: 0, 303 | operation: Operation::Multiply, 304 | num_ships: 3, 305 | }, 306 | WaveGroup { 307 | speed: 1.5, 308 | max_number: 6, 309 | min_number: 0, 310 | operation: Operation::Divide, 311 | num_ships: 3, 312 | }, 313 | ], 314 | }, 315 | ], 316 | }, 317 | ] 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![windows_subsystem = "windows"] 2 | 3 | use ggez; 4 | use ggez::audio::SoundSource; 5 | use ggez::conf::{self}; 6 | use ggez::event::{self, KeyCode, KeyMods}; 7 | use ggez::graphics::{self, Color}; 8 | 9 | use ggez::nalgebra as na; 10 | use ggez::timer; 11 | use ggez::{Context, GameResult}; 12 | use rand::*; 13 | use std::collections::VecDeque; 14 | use std::env; 15 | use std::path; 16 | 17 | mod alien; 18 | mod assets; 19 | mod background; 20 | mod crosshair; 21 | mod explosion; 22 | mod ggez_utility; 23 | mod level; 24 | mod mbtext; 25 | mod message; 26 | mod turret; 27 | 28 | use crate::alien::*; 29 | use crate::assets::*; 30 | use crate::background::*; 31 | use crate::crosshair::*; 32 | use crate::explosion::*; 33 | use crate::ggez_utility::*; 34 | use crate::level::*; 35 | use crate::mbtext::*; 36 | use crate::message::*; 37 | use crate::turret::*; 38 | 39 | fn get_lowest_living_alien(aliens: &Vec) -> Option { 40 | match aliens 41 | .iter() 42 | .enumerate() 43 | .filter(|(_, alien)| alien.state != AlienState::Dead) 44 | .max_by_key(|(_, alien)| (alien.pos[1] * 1000.0) as i32) 45 | { 46 | Some((index, _)) => Some(index), 47 | None => None, 48 | } 49 | } 50 | 51 | fn gen_aliens(wave: &Wave, assets: &Assets, difficulty: usize) -> Vec { 52 | let mut aliens: Vec = Vec::new(); 53 | let mut rng = rand::thread_rng(); 54 | for group in &wave.groups { 55 | let alien_img = match group.operation { 56 | Operation::Add => &assets.add_ship, 57 | Operation::Subtract => &assets.sub_ship, 58 | Operation::Multiply => &assets.mul_ship, 59 | Operation::Divide => &assets.div_ship, 60 | }; 61 | let alien_img_width = alien_img.width() as f32; 62 | let alien_img_height = alien_img.height() as f32; 63 | let num_ships = (group.num_ships as f32 * NUM_SHIPS_DIFFICULTY[difficulty]) as i32; 64 | for i in 0..num_ships { 65 | let min_number = (group.min_number as f32 * MIN_NUMBER_DIFFICULTY[difficulty]) as i32; 66 | let max_number = (group.max_number as f32 * MAX_NUMBER_DIFFICULTY[difficulty]) as i32; 67 | 68 | let (mut num1, mut num2) = if group.operation == Operation::Divide { 69 | let mut a; 70 | let mut b; 71 | loop { 72 | a = rng.gen_range(min_number, max_number); 73 | b = if a == group.min_number { 74 | a 75 | } else { 76 | rng.gen_range(min_number, a) 77 | }; 78 | if b == 0 { 79 | continue; 80 | } 81 | if a % b == 0 { 82 | break; 83 | } 84 | } 85 | (a, b) 86 | } else { 87 | ( 88 | rng.gen_range(min_number, max_number), 89 | rng.gen_range(min_number, max_number), 90 | ) 91 | }; 92 | 93 | // make subtraction never negative until 3rd difficulty level 94 | if difficulty < 2 && group.operation == Operation::Subtract { 95 | let t = num2; 96 | if num2 > num1 { 97 | num2 = num1; 98 | num1 = t; 99 | } 100 | } 101 | 102 | let (answer, op) = match group.operation { 103 | Operation::Add => (num1 + num2, "+"), 104 | Operation::Subtract => (num1 - num2, "-"), 105 | Operation::Multiply => (num1 * num2, "X"), 106 | Operation::Divide => (num1 / num2, "/"), 107 | }; 108 | let text = num1.to_string() + op + &num2.to_string(); 109 | 110 | // generate an x coordinate for aliens, make 111 | // sure it isn't too close to aliens at nearby 112 | // y so they don't overlap 113 | let mut x: f32 = rng.gen_range(0.05, 0.95); 114 | while aliens 115 | .iter() 116 | .rev() 117 | .take(3) 118 | .any(|alien| (alien.pos[0] - x).abs() < 0.1) 119 | { 120 | x = rng.gen_range(0.05, 0.95); 121 | } 122 | 123 | let alien = Alien { 124 | operation: group.operation, 125 | speed: group.speed as f32 * SPEED_DIFFICULTY[difficulty], 126 | pos: na::Point2::new(x, -(i as i32) as f32 * 0.3), 127 | text: graphics::Text::new((text, assets.number_font, 24.0)), 128 | answer: answer, 129 | explosion: Explosion::new(0.0, na::Point2::new(0.0, 0.0)), 130 | state: AlienState::Alive, 131 | src_pixel_width: alien_img_width, 132 | src_pixel_height: alien_img_height, 133 | }; 134 | aliens.push(alien); 135 | } 136 | } 137 | aliens.sort_by(|a, b| a.pos[0].partial_cmp(&b.pos[0]).unwrap()); 138 | aliens 139 | } 140 | 141 | #[derive(Debug, PartialEq)] 142 | enum GameState { 143 | DifficultySelect, 144 | LevelSelect, 145 | LevelComplete, 146 | LevelTransition(f32), 147 | Playing, 148 | Dying, 149 | Dead, 150 | Won, 151 | } 152 | 153 | struct TextState { 154 | dead_text: MBText, 155 | won_text: MBText, 156 | press_enter: MBText, 157 | math_title: MBText, 158 | level_complete: MBText, 159 | level_names: Vec, 160 | difficulty_names: Vec, 161 | } 162 | struct MainState { 163 | messages: VecDeque, 164 | dt: std::time::Duration, 165 | aliens: Vec, 166 | assets: Assets, 167 | levels: Vec, 168 | current_level: usize, 169 | current_wave: usize, 170 | turret: Turret, 171 | target: Option, 172 | background: Background, 173 | state: GameState, 174 | text: TextState, 175 | lives: usize, 176 | crosshair: Crosshair, 177 | level_selection: usize, 178 | difficulty_selection: usize, 179 | up_key: Option, 180 | } 181 | 182 | impl MainState { 183 | fn new(ctx: &mut Context) -> GameResult { 184 | let levels = Level::load_from_file(); 185 | println!("levels count:{}",levels.len()); 186 | let assets = Assets::new(ctx, levels[0].background_file.clone()); 187 | let messages = VecDeque::new(); 188 | let aliens = Vec::new(); 189 | let target = get_lowest_living_alien(&aliens); 190 | 191 | Ok(MainState { 192 | messages: messages, 193 | aliens: aliens, 194 | text: TextState { 195 | dead_text: MBText::new( 196 | "You Have Died".to_string(), 197 | &assets.title_font, 198 | BLUE, 199 | 128.0, 200 | ctx, 201 | ), 202 | won_text: MBText::new("You Have Saved The Galaxy!".to_string(), &assets.main_font, BLUE, 128.0, ctx), 203 | press_enter: MBText::new( 204 | "Press Enter".to_string(), 205 | &assets.main_font, 206 | WHITE, 207 | 64.0, 208 | ctx, 209 | ), 210 | math_title: MBText::new( 211 | "Math Defense".to_string(), 212 | &assets.title_font, 213 | BLUE, 214 | 128.0, 215 | ctx, 216 | ), 217 | level_complete: MBText::new( 218 | "Level Complete!".to_string(), 219 | &assets.title_font, 220 | BLUE, 221 | 128.0, 222 | ctx, 223 | ), 224 | level_names: levels 225 | .iter() 226 | .map(|level| { 227 | MBText::new_blink( 228 | level.title.clone(), 229 | &assets.main_font, 230 | WHITE, 231 | GRAY, 232 | 64.0, 233 | ctx, 234 | ) 235 | }) 236 | .collect(), 237 | difficulty_names: DIFFICULTY_NAMES 238 | .iter() 239 | .map(|name| { 240 | MBText::new_blink( 241 | name.to_string(), 242 | &assets.main_font, 243 | WHITE, 244 | GRAY, 245 | 64.0, 246 | ctx, 247 | ) 248 | }) 249 | .collect(), 250 | }, 251 | turret: Turret::new(&assets, ctx), 252 | levels: levels, 253 | current_level: 0, 254 | current_wave: 0, 255 | target: target, 256 | background: Background { 257 | src_pixel_width: assets.background.width() as f32, 258 | src_pixel_height: assets.background.height() as f32, 259 | stars1_pos: 0.0, 260 | stars2_pos: 0.0, 261 | }, 262 | state: GameState::DifficultySelect, 263 | dt: std::time::Duration::new(0, 0), 264 | lives: 2, 265 | crosshair: Crosshair { 266 | elapsed: 0, 267 | src_pixel_width: assets.crosshair.width() as f32, 268 | src_pixel_height: assets.crosshair.height() as f32, 269 | }, 270 | assets: assets, 271 | level_selection: 0, 272 | difficulty_selection: 0, 273 | up_key: None, 274 | }) 275 | } 276 | 277 | fn load_level_wave(&mut self, level: usize, wave: usize) { 278 | self.current_level = level; 279 | self.current_wave = wave; 280 | self.target = None; 281 | let wave = &self.levels[self.current_level].waves[self.current_wave]; 282 | self.aliens = gen_aliens(wave, &self.assets, self.difficulty_selection); 283 | self.target = get_lowest_living_alien(&self.aliens); 284 | } 285 | 286 | fn set_level_wave(&mut self, level: usize, wave: usize) { 287 | if level > self.current_level { 288 | self.state = GameState::LevelComplete; 289 | let _ = self.assets.clap_sound.play_detached(); 290 | } 291 | self.load_level_wave(level, wave); 292 | } 293 | fn increment_level_wave(&mut self, ctx: &mut Context) { 294 | //if we were at the last wave already then go to next level 295 | if self.current_wave + 1 >= self.levels[self.current_level].waves.len() { 296 | if self.current_level + 1 >= self.levels.len() { 297 | self.state = GameState::Won; 298 | let _ = self.assets.clap_sound.play_detached(); 299 | } else { 300 | //unlock the next level and save the json 301 | self.levels[self.current_level + 1].unlocked[self.difficulty_selection] = true; 302 | Level::save_levels(&self.levels); 303 | self.set_level_wave(self.current_level + 1, 0) 304 | } 305 | } else { 306 | self.set_level_wave(self.current_level, self.current_wave + 1); 307 | self.messages.push_back(Message::new( 308 | "Wave Eliminated!".to_string(), 309 | 2000.0, 310 | &self.assets, 311 | ctx, 312 | )); 313 | self.messages.push_back(Message::new( 314 | "Wave ".to_string() + &(self.current_wave + 1).to_string(), 315 | 2000.0, 316 | &self.assets, 317 | ctx, 318 | )); 319 | } 320 | } 321 | fn update_difficulty_select(&mut self, _ctx: &mut Context) { 322 | for difficulty in &mut self.text.difficulty_names { 323 | difficulty.update(self.dt); 324 | } 325 | if let Some(keycode) = self.up_key { 326 | if keycode == KeyCode::Return { 327 | self.state = GameState::LevelSelect; 328 | } else if keycode == KeyCode::Down { 329 | self.difficulty_selection = 330 | (self.difficulty_selection + 1) % DIFFICULTY_NAMES.len(); 331 | } else if keycode == KeyCode::Up { 332 | self.difficulty_selection = if self.difficulty_selection == 0 { 333 | DIFFICULTY_NAMES.len() - 1 334 | } else { 335 | self.difficulty_selection - 1 336 | }; 337 | } 338 | } 339 | } 340 | 341 | fn update_level_select(&mut self, ctx: &mut Context) { 342 | let unlocked_count = self 343 | .levels 344 | .iter() 345 | .filter(|level| level.unlocked[self.difficulty_selection]) 346 | .count(); 347 | if let Some(keycode) = self.up_key { 348 | if keycode == KeyCode::Return { 349 | self.load_level_wave(self.level_selection, 0); 350 | self.assets.background = graphics::Image::new( 351 | ctx, 352 | self.levels[self.current_level].background_file.clone(), 353 | ) 354 | .unwrap(); 355 | self.messages.push_back(Message::new( 356 | self.levels[self.level_selection].title.clone(), 357 | 2000.0, 358 | &self.assets, 359 | ctx, 360 | )); 361 | self.messages.push_back(Message::new( 362 | "Wave 1".to_string(), 363 | 2000.0, 364 | &self.assets, 365 | ctx, 366 | )); 367 | self.lives = 2; 368 | self.turret = Turret::new(&self.assets, ctx); 369 | self.state = GameState::Playing; 370 | } else if keycode == KeyCode::Down { 371 | self.level_selection = (self.level_selection + 1) % unlocked_count; 372 | } else if keycode == KeyCode::Up { 373 | self.level_selection = if self.level_selection == 0 { 374 | unlocked_count - 1 375 | } else { 376 | self.level_selection - 1 377 | }; 378 | } 379 | } 380 | for level_name in &mut self.text.level_names { 381 | level_name.update(self.dt) 382 | } 383 | } 384 | fn update_won(&mut self, _ctx: &mut Context) { 385 | if let Some(keycode) = self.up_key { 386 | if keycode == KeyCode::Return { 387 | self.state = GameState::DifficultySelect; 388 | } 389 | } 390 | } 391 | fn update_level_complete(&mut self, ctx: &mut Context) { 392 | self.turret.rotation = 0.0; 393 | self.background.update(self.dt, 1.0); 394 | if let Some(keycode) = self.up_key { 395 | if keycode == KeyCode::Return { 396 | let _ = self.assets.launch_sound.play_detached(); 397 | self.state = GameState::LevelTransition(0.0); 398 | self.levels[self.current_level].push_title(&mut self.messages, &self.assets, ctx); 399 | self.messages.push_back(Message::new( 400 | "WARP SPEED".to_string(), 401 | 2000.0, 402 | &self.assets, 403 | ctx, 404 | )); 405 | } 406 | } 407 | } 408 | fn update_level_transition(&mut self, ctx: &mut Context, elapsed: f32) { 409 | self.state = GameState::LevelTransition(elapsed + self.dt.as_millis() as f32); 410 | let pct = elapsed / 3000.0; 411 | self.background.update(self.dt, 1.0 + pct * 1.0); 412 | self.turret.pos[1] -= 0.015 * pct; 413 | 414 | if elapsed >= 3000.0 { 415 | self.state = GameState::Playing; 416 | self.levels[self.current_level].push_title(&mut self.messages, &self.assets, ctx); 417 | self.messages.push_back(Message::new( 418 | "Wave 1".to_string(), 419 | 2000.0, 420 | &self.assets, 421 | ctx, 422 | )); 423 | self.assets.background = 424 | graphics::Image::new(ctx, self.levels[self.current_level].background_file.clone()) 425 | .unwrap(); 426 | self.turret.pos = na::Point2::new(0.5, 0.9); //put turret back at the bottom 427 | } 428 | } 429 | 430 | fn update_dead(&mut self, _ctx: &mut Context) { 431 | if let Some(keycode) = self.up_key { 432 | if keycode == KeyCode::Return { 433 | self.state = GameState::LevelSelect; 434 | } 435 | } 436 | } 437 | 438 | fn update_dying(&mut self, ctx: &mut Context) { 439 | for alien in &mut self.aliens { 440 | alien.update(&mut self.turret, ctx, self.dt); 441 | } 442 | self.turret.update(ctx, self.dt); 443 | for splosion in &mut self.turret.explosions { 444 | splosion.update(ctx, self.dt); 445 | } 446 | if self 447 | .turret 448 | .explosions 449 | .iter() 450 | .all(|splosion| splosion.elapsed - splosion.start_time > splosion.duration) 451 | { 452 | if self.lives > 0 { 453 | self.lives -= 1; 454 | self.turret = Turret::new(&self.assets, ctx); 455 | self.set_level_wave(self.current_level, self.current_wave); 456 | self.state = GameState::Playing; 457 | if self.lives > 0 { 458 | self.messages.push_back(Message::new( 459 | self.lives.to_string() + &" Gun Left".to_string(), 460 | 2000.0, 461 | &self.assets, 462 | ctx, 463 | )); 464 | } else { 465 | self.messages.push_back(Message::new( 466 | "Final Gun! Good Luck!".to_string(), 467 | 2000.0, 468 | &self.assets, 469 | ctx, 470 | )); 471 | } 472 | self.messages.push_back(Message::new( 473 | "Restarting Wave ".to_string() + &(self.current_wave + 1).to_string(), 474 | 2000.0, 475 | &self.assets, 476 | ctx, 477 | )); 478 | } else { 479 | self.state = GameState::Dead; 480 | } 481 | } 482 | } 483 | 484 | fn update_playing(&mut self, ctx: &mut Context) { 485 | self.background.update(self.dt, 1.0); 486 | self.crosshair.update(self.dt); 487 | if let Some(keycode) = self.up_key { 488 | if keycode == KeyCode::Return { 489 | match self.turret.raw_text.parse::() { 490 | Ok(n) => match self.target { 491 | Some(alien_index) if self.aliens[alien_index].answer == n => { 492 | self.aliens[alien_index].state = AlienState::Exploding; 493 | let _ = self.assets.explosion_sound.play_detached(); 494 | self.turret.state = TurretState::Firing; 495 | } 496 | _ => { 497 | let _ = self.assets.fail_sound.play_detached(); 498 | } 499 | }, 500 | Err(_) => (), 501 | } 502 | self.turret.raw_text = "".to_string(); 503 | self.turret.text = MBText::new( 504 | self.turret.raw_text.clone(), 505 | &self.assets.number_font, 506 | WHITE, 507 | 24.0, 508 | ctx, 509 | ); 510 | } else if keycode == KeyCode::Back { 511 | let _ = self.turret.raw_text.pop(); 512 | self.turret.text = MBText::new( 513 | self.turret.raw_text.clone(), 514 | &self.assets.number_font, 515 | WHITE, 516 | 24.0, 517 | ctx, 518 | ); 519 | } else if keycode == KeyCode::Left { 520 | match self.target { 521 | Some(index) => { 522 | if self 523 | .aliens 524 | .iter() 525 | .any(|alien| alien.state == AlienState::Alive && alien.pos[1] >= 0.0) 526 | { 527 | let mut i = if index == 0 { 528 | self.aliens.len() - 1 529 | } else { 530 | index - 1 531 | }; 532 | while self.aliens[i].state != AlienState::Alive 533 | || self.aliens[i].pos[1] < 0.0 534 | { 535 | i = if i == 0 { self.aliens.len() - 1 } else { i - 1 } 536 | } 537 | self.target = Some(i); 538 | } 539 | } 540 | _ => (), 541 | } 542 | } else if keycode == KeyCode::Right { 543 | match self.target { 544 | Some(index) => { 545 | if self 546 | .aliens 547 | .iter() 548 | .any(|alien| alien.state == AlienState::Alive && alien.pos[1] > 0.0) 549 | { 550 | let mut i = (index + 1) % self.aliens.len(); 551 | while self.aliens[i].state != AlienState::Alive 552 | || self.aliens[i].pos[1] < 0.0 553 | { 554 | i = (i + 1) % self.aliens.len(); 555 | } 556 | self.target = Some(i); 557 | } 558 | } 559 | 560 | _ => (), 561 | } 562 | } 563 | } 564 | 565 | //update aliens and turret, and message queue 566 | for alien in &mut self.aliens { 567 | alien.update(&mut self.turret, ctx, self.dt); 568 | } 569 | self.turret.update(ctx, self.dt); 570 | if !self.messages.is_empty() { 571 | self.messages[0].update(self.dt); 572 | if self.messages[0].elapsed >= self.messages[0].duration { 573 | let _ = self.messages.pop_front(); 574 | } 575 | } 576 | 577 | // If there is a target, rotate the turret to it 578 | match self.target { 579 | Some(target) if self.aliens[target].state != AlienState::Dead => { 580 | let turret_pos = self.turret.pct_pos(); 581 | let turret_vector: na::Vector2 = 582 | na::Vector2::new(turret_pos[0], turret_pos[1]); 583 | let alien_pos = self.aliens[target].pct_pos(); 584 | let alien_vector = na::Vector2::new(alien_pos[0], alien_pos[1]); 585 | let v1 = na::Vector2::new(0.0, -1.0); 586 | let v2 = alien_vector - turret_vector; 587 | let mut angle = v2.angle(&v1); 588 | if alien_pos[0] < 0.5 { 589 | angle = -angle; 590 | } 591 | self.turret.rotation = angle; 592 | } 593 | Some(_) => self.target = get_lowest_living_alien(&self.aliens), 594 | None => (), 595 | }; 596 | 597 | // Find the alien furthest down the screen, if its at the bottom, dead. 598 | match self 599 | .aliens 600 | .iter() 601 | .max_by_key(|alien| (alien.pos[1] * 1000.0) as i32) 602 | { 603 | Some(alien) => { 604 | if alien.pos[1] > 0.9 { 605 | self.state = GameState::Dying 606 | }; 607 | } 608 | None => (), 609 | } 610 | 611 | //If all aliens are dead, increment the wave/level 612 | if self 613 | .aliens 614 | .iter() 615 | .all(|alien| alien.state == AlienState::Dead) 616 | { 617 | self.increment_level_wave(ctx); 618 | } 619 | } 620 | fn draw_difficulty_select(&mut self, ctx: &mut Context) { 621 | self.background.draw_no_stars(ctx, &self.assets); 622 | let mut title_pos = self.text.math_title.center(ctx); 623 | title_pos[1] *= 0.5; 624 | self.text.math_title.draw(title_pos, ctx); 625 | 626 | let window_dimension = graphics::size(ctx); 627 | let mut y = 0.4 * window_dimension.1 as f32; 628 | 629 | for (i, difficulty_name) in self.text.difficulty_names.iter().enumerate() { 630 | let vertical_size = difficulty_name.dest_pixel_dimensions(window_dimension).1; 631 | let mut center = difficulty_name.center(ctx); 632 | center[1] = y; 633 | if i == self.difficulty_selection { 634 | difficulty_name.draw(center, ctx); 635 | } else { 636 | difficulty_name.draw_color(center, GRAY, ctx); 637 | } 638 | y += vertical_size * 1.075; 639 | } 640 | } 641 | 642 | fn draw_level_select(&mut self, ctx: &mut Context) { 643 | self.background.draw_no_stars(ctx, &self.assets); 644 | let mut title_pos = self.text.math_title.center(ctx); 645 | title_pos[1] *= 0.5; 646 | self.text.math_title.draw(title_pos, ctx); 647 | 648 | let window_dimension = graphics::size(ctx); 649 | let mut y = 0.4 * window_dimension.1 as f32; 650 | for (i, level_name) in self.text.level_names.iter().enumerate() { 651 | let vertical_size = level_name.dest_pixel_dimensions(window_dimension).1; 652 | let mut center = level_name.center(ctx); 653 | center[1] = y; 654 | if i == self.level_selection { 655 | level_name.draw(center, ctx); 656 | } else if self.levels[i].unlocked[self.difficulty_selection] { 657 | level_name.draw_color(center, GRAY, ctx); 658 | } else { 659 | level_name.draw_color(center, DARK_GRAY, ctx); 660 | } 661 | y += vertical_size * 1.075; 662 | } 663 | } 664 | fn draw_won(&mut self, ctx: &mut Context) { 665 | self.background.draw(ctx, &self.assets); 666 | let mut title_pos = self.text.won_text.center(ctx); 667 | title_pos[1] *= 0.5; 668 | self.text.press_enter.draw_center(ctx); 669 | self.text.won_text.draw(title_pos, ctx); 670 | } 671 | 672 | fn draw_level_complete(&mut self, ctx: &mut Context) { 673 | self.background.draw(ctx, &self.assets); 674 | let mut title_pos = self.text.level_complete.center(ctx); 675 | title_pos[1] *= 0.5; 676 | self.text.press_enter.draw_center(ctx); 677 | self.text.level_complete.draw(title_pos, ctx); 678 | self.turret.draw(ctx, &mut self.assets); 679 | self.turret.draw_lives(self.lives, ctx, &mut self.assets); 680 | } 681 | 682 | fn draw_level_transition(&mut self, ctx: &mut Context, elapsed: f32) { 683 | self.background.draw(ctx, &self.assets); 684 | let mut title_pos = self.text.level_complete.center(ctx); 685 | title_pos[1] *= 0.5; 686 | self.text.press_enter.draw_center(ctx); 687 | self.text.level_complete.draw(title_pos, ctx); 688 | self.turret.draw(ctx, &mut self.assets); 689 | self.turret.draw_lives(self.lives, ctx, &mut self.assets); 690 | let pct = elapsed / 3000.0; 691 | if pct > 0.75 { 692 | let r = ((elapsed * 2.0) as i32 % 255) as u8; 693 | let g = ((elapsed * 3.23) as i32 % 255) as u8; 694 | let b = ((elapsed * 5.34) as i32 % 255) as u8; 695 | graphics::clear(ctx, Color::from_rgb(r, g, b)); 696 | } 697 | } 698 | 699 | fn draw_dead(&mut self, ctx: &mut Context) { 700 | self.background.draw(ctx, &self.assets); 701 | let mut title_pos = self.text.dead_text.center(ctx); 702 | title_pos[1] *= 0.5; 703 | self.text.press_enter.draw_center(ctx); 704 | self.text.dead_text.draw(title_pos, ctx); 705 | } 706 | fn draw_playing(&mut self, ctx: &mut Context) { 707 | //Draw the background 708 | self.background.draw(ctx, &self.assets); 709 | 710 | // if we have a target, draw the crosshair 711 | match self.target { 712 | Some(target) => { 713 | let alien = &self.aliens[target]; 714 | 715 | //draw the crosshair on the target 716 | let crosshair_pos = 717 | to_screen_pos((alien.pos[0], alien.pos[1]), graphics::size(ctx)); 718 | self.crosshair.draw(crosshair_pos, ctx, &self.assets); 719 | //draw the laser if the turret is firing 720 | match self.turret.state { 721 | TurretState::Firing => { 722 | let screen_size = graphics::size(ctx); 723 | let turret_pos = self.turret.pixel_pos(screen_size); 724 | 725 | //make the lasers come out of the actual gunscar 726 | let left_pos = na::Point2::new(turret_pos[0] - 0.01*screen_size.0,turret_pos[1] - 0.01*screen_size.1); 727 | let right_pos = na::Point2::new(turret_pos[0] + 0.01*screen_size.0,turret_pos[1] - 0.01*screen_size.1); 728 | 729 | //left laser 730 | let laser = graphics::Mesh::new_line( 731 | ctx, 732 | &[ 733 | left_pos, 734 | alien.pixel_pos(screen_size), 735 | ], 736 | 4.0, 737 | graphics::Color::from((255, 0, 0, 255)), 738 | ) 739 | .unwrap(); 740 | let _ = graphics::draw(ctx, &laser, graphics::DrawParam::default()); 741 | //right laser 742 | let laser = graphics::Mesh::new_line( 743 | ctx, 744 | &[ 745 | right_pos, 746 | alien.pixel_pos(screen_size), 747 | ], 748 | 4.0, 749 | graphics::Color::from((255, 0, 0, 255)), 750 | ) 751 | .unwrap(); 752 | let _ = graphics::draw(ctx, &laser, graphics::DrawParam::default()); 753 | } 754 | TurretState::Resting => (), 755 | } 756 | } 757 | None => (), 758 | }; 759 | 760 | //draw the aliens, turrets, and messages 761 | for alien in &mut self.aliens { 762 | alien.draw(ctx, &mut self.assets); 763 | } 764 | self.turret.draw(ctx, &mut self.assets); 765 | self.turret.draw_lives(self.lives, ctx, &mut self.assets); 766 | if !self.messages.is_empty() { 767 | self.messages[0].draw(ctx); 768 | } 769 | } 770 | fn draw_dying(&mut self, ctx: &mut Context) { 771 | self.background.draw(ctx, &self.assets); 772 | for alien in &mut self.aliens { 773 | alien.draw(ctx, &mut self.assets); 774 | } 775 | self.turret.draw(ctx, &mut self.assets); 776 | self.turret.draw_lives(self.lives, ctx, &mut self.assets); 777 | for i in 0..self.turret.explosions.len() { 778 | let mut pos = self.turret.pixel_pos(graphics::size(ctx)); 779 | pos[0] += ((10 + i % 2) as f32 / 100.0) * graphics::size(ctx).0; 780 | 781 | self.turret.explosions[i].draw(ctx, &mut self.assets) 782 | } 783 | } 784 | } 785 | impl event::EventHandler for MainState { 786 | fn update(&mut self, ctx: &mut Context) -> GameResult { 787 | self.dt = timer::delta(ctx); 788 | match &self.state { 789 | GameState::DifficultySelect => self.update_difficulty_select(ctx), 790 | GameState::LevelSelect => self.update_level_select(ctx), 791 | GameState::LevelTransition(elapsed) => { 792 | let x = *elapsed; 793 | self.update_level_transition(ctx, x); 794 | } 795 | GameState::Playing => self.update_playing(ctx), 796 | GameState::Dying => self.update_dying(ctx), 797 | GameState::Dead => self.update_dead(ctx), 798 | GameState::Won => self.update_won(ctx), 799 | GameState::LevelComplete => self.update_level_complete(ctx), 800 | } 801 | //clear out the up key event, now that the update funcs have had a chance to see it 802 | match self.up_key { 803 | Some(_) => self.up_key = None, 804 | _ => (), 805 | } 806 | Ok(()) 807 | } 808 | 809 | fn draw(&mut self, ctx: &mut Context) -> GameResult { 810 | graphics::clear(ctx, [0.0, 0.0, 0.0, 1.0].into()); 811 | match &mut self.state { 812 | GameState::DifficultySelect => self.draw_difficulty_select(ctx), 813 | GameState::LevelSelect => self.draw_level_select(ctx), 814 | GameState::LevelTransition(elapsed) => { 815 | let x = *elapsed; 816 | self.draw_level_transition(ctx, x); 817 | } 818 | GameState::Playing => self.draw_playing(ctx), 819 | GameState::Dying => self.draw_dying(ctx), 820 | GameState::Dead => self.draw_dead(ctx), 821 | GameState::Won => self.draw_won(ctx), 822 | GameState::LevelComplete => self.draw_level_complete(ctx), 823 | } 824 | graphics::present(ctx)?; 825 | Ok(()) 826 | } 827 | 828 | fn resize_event(&mut self, ctx: &mut Context, width: f32, height: f32) { 829 | let new_rect = graphics::Rect::new(0.0, 0.0, width as f32, height as f32); 830 | graphics::set_screen_coordinates(ctx, new_rect).unwrap(); 831 | } 832 | 833 | fn text_input_event(&mut self, ctx: &mut Context, ch: char) { 834 | if self.state == GameState::Playing { 835 | if ('0' <= ch && ch <= '9') || ch == '-' { 836 | self.turret.raw_text += &ch.to_string(); 837 | self.turret.text = MBText::new( 838 | self.turret.raw_text.clone(), 839 | &self.assets.number_font, 840 | WHITE, 841 | 24.0, 842 | ctx, 843 | ); 844 | } 845 | } 846 | } 847 | 848 | fn key_down_event( 849 | &mut self, 850 | _ctx: &mut Context, 851 | _keycode: KeyCode, 852 | _keymod: KeyMods, 853 | _repeat: bool, 854 | ) { 855 | //just ovvering this so escape doesn't quit 856 | } 857 | 858 | fn key_up_event(&mut self, ctx: &mut Context, keycode: KeyCode, _keymod: KeyMods) { 859 | match keycode { 860 | KeyCode::Escape => match self.state { 861 | GameState::LevelSelect => self.state = GameState::DifficultySelect, 862 | GameState::DifficultySelect => event::quit(ctx), 863 | _ => self.state = GameState::LevelSelect, 864 | }, 865 | _ => self.up_key = Some(keycode), 866 | } 867 | } 868 | } 869 | 870 | pub fn main() -> GameResult { 871 | let resource_dir = if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { 872 | let mut path = path::PathBuf::from(manifest_dir); 873 | path.push("resources"); 874 | path 875 | } else { 876 | path::PathBuf::from("./resources") 877 | }; 878 | 879 | let cb = ggez::ContextBuilder::new("Math Defense", "Jack Mott") 880 | .add_resource_path(resource_dir) 881 | .window_setup( 882 | conf::WindowSetup::default() 883 | .title("Math Defense") 884 | ) 885 | .window_mode( 886 | conf::WindowMode::default() 887 | .dimensions(1920.0/1.5,1080.0/1.5) 888 | .fullscreen_type(conf::FullscreenType::Windowed) 889 | .resizable(true), 890 | ); 891 | 892 | let (ctx, event_loop) = &mut cb.build()?; 893 | let state = &mut MainState::new(ctx)?; 894 | state.assets.music.set_repeat(true); 895 | //state.assets.music.set_volume(0.07); 896 | let _ = state.assets.music.play_detached(); 897 | state.dt = std::time::Duration::new(0, 0); 898 | event::run(ctx, event_loop, state) 899 | } 900 | -------------------------------------------------------------------------------- /src/mbtext.rs: -------------------------------------------------------------------------------- 1 | use crate::ggez_utility::*; 2 | use ggez::graphics::{self, Color}; 3 | use ggez::nalgebra as na; 4 | use ggez::Context; 5 | 6 | const TEXT_TIME: u32 = 1000; 7 | 8 | pub struct MBText { 9 | pub text: graphics::Text, 10 | pub actual_w: f32, 11 | pub actual_h: f32, 12 | pub w: f32, 13 | pub h: f32, 14 | pub color1: Color, 15 | pub color2: Color, 16 | pub elapsed: u32, 17 | } 18 | 19 | impl MBText { 20 | pub fn new( 21 | text: String, 22 | font: &graphics::Font, 23 | color: Color, 24 | size: f32, 25 | context: &mut Context, 26 | ) -> MBText { 27 | MBText::new_blink(text, font, color, color, size, context) 28 | } 29 | 30 | pub fn new_blink( 31 | text: String, 32 | font: &graphics::Font, 33 | color1: Color, 34 | color2: Color, 35 | size: f32, 36 | context: &mut Context, 37 | ) -> MBText { 38 | let text = graphics::Text::new((text, *font, size)); 39 | let dim = text.dimensions(context); 40 | MBText { 41 | text: text, 42 | actual_w: dim.0 as f32, 43 | actual_h: dim.1 as f32, 44 | color1: color1, 45 | color2: color2, 46 | w: dim.0 as f32 / 1920.0, 47 | h: dim.1 as f32 / 1080.0, 48 | elapsed: 0, 49 | } 50 | } 51 | 52 | pub fn update(&mut self, dt: std::time::Duration) { 53 | self.elapsed = (self.elapsed + dt.as_millis() as u32) % TEXT_TIME; 54 | } 55 | 56 | pub fn draw(&self, pos: na::Point2, ctx: &mut Context) { 57 | let mut pct = (self.elapsed as f32 / TEXT_TIME as f32) * 2.0; 58 | if pct > 1.0 { 59 | pct = 1.0 - (pct - 1.0); 60 | } 61 | let color = lerp_color(self.color1, self.color2, pct); 62 | let _ = graphics::draw( 63 | ctx, 64 | &self.text, 65 | graphics::DrawParam::new() 66 | .color(color) 67 | .dest(pos) 68 | .scale(self.scale(graphics::size(ctx))), 69 | ); 70 | } 71 | 72 | pub fn draw_color(&self, pos: na::Point2, color: Color, ctx: &mut Context) { 73 | let _ = graphics::draw( 74 | ctx, 75 | &self.text, 76 | graphics::DrawParam::new() 77 | .color(color) 78 | .dest(pos) 79 | .scale(self.scale(graphics::size(ctx))), 80 | ); 81 | } 82 | 83 | pub fn draw_center(&self, ctx: &mut Context) { 84 | let pos = self.center(ctx); 85 | self.draw(pos, ctx); 86 | } 87 | 88 | pub fn draw_horizontal_center(&self, y: f32, ctx: &mut Context) { 89 | let center = self.center(ctx); 90 | self.draw(na::Point2::new(center[0], y), ctx); 91 | } 92 | 93 | pub fn center(&self, ctx: &mut Context) -> na::Point2 { 94 | let window_dim = graphics::size(ctx); 95 | let text_dim = self.dest_pixel_dimensions(window_dim); 96 | na::Point2::new( 97 | window_dim.0 / 2.0 - text_dim.0 as f32 / 2.0, 98 | window_dim.1 / 2.0 - text_dim.1 as f32 / 2.0, 99 | ) 100 | } 101 | } 102 | 103 | impl Scalable for MBText { 104 | fn pct_pos(&self) -> na::Point2 { 105 | na::Point2::new(0.0, 0.0) 106 | } 107 | 108 | fn pct_dimensions(&self) -> (f32, f32) { 109 | (self.w, self.h) 110 | } 111 | 112 | fn src_pixel_dimensions(&self) -> (f32, f32) { 113 | (self.actual_w, self.actual_h) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/message.rs: -------------------------------------------------------------------------------- 1 | use ggez::Context; 2 | 3 | use crate::assets::*; 4 | use crate::ggez_utility::*; 5 | use crate::mbtext::*; 6 | 7 | pub struct Message { 8 | pub text: MBText, 9 | pub duration: f32, 10 | pub elapsed: f32, 11 | } 12 | 13 | impl Message { 14 | pub fn new(text: String, duration: f32, assets: &Assets, ctx: &mut Context) -> Message { 15 | Message { 16 | text: MBText::new(text, &assets.main_font, WHITE, 128.0, ctx), 17 | duration: duration, 18 | elapsed: 0.0, 19 | } 20 | } 21 | pub fn update(&mut self, dt: std::time::Duration) { 22 | self.elapsed += dt.as_millis() as f32; 23 | } 24 | 25 | pub fn draw(&self, ctx: &mut Context) { 26 | let text_pos = self.text.center(ctx); 27 | self.text.draw(text_pos, ctx); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/turret.rs: -------------------------------------------------------------------------------- 1 | use ggez::graphics::{self, DrawParam}; 2 | use ggez::nalgebra as na; 3 | use ggez::Context; 4 | use rand::*; 5 | 6 | use crate::assets::*; 7 | use crate::explosion::*; 8 | use crate::ggez_utility::*; 9 | use crate::mbtext::*; 10 | 11 | pub enum TurretState { 12 | Firing, 13 | Resting, 14 | //todo Rotating? 15 | } 16 | 17 | pub struct Turret { 18 | pub rotation: f32, 19 | pub raw_text: String, 20 | pub text: MBText, 21 | pub explosions: Vec, 22 | pub state: TurretState, 23 | pub src_pixel_width: f32, 24 | pub src_pixel_height: f32, 25 | pub pos: na::Point2, 26 | } 27 | 28 | impl Scalable for Turret { 29 | fn pct_pos(&self) -> na::Point2 { 30 | self.pos 31 | } 32 | fn pct_dimensions(&self) -> (f32, f32) { 33 | (0.030, 0.05) 34 | } 35 | fn src_pixel_dimensions(&self) -> (f32, f32) { 36 | (self.src_pixel_width, self.src_pixel_height) 37 | } 38 | } 39 | impl Turret { 40 | pub fn new(assets: &Assets, ctx: &mut Context) -> Turret { 41 | let mut rng = rand::thread_rng(); 42 | let mut explosions = Vec::new(); 43 | for _ in 0..20 { 44 | let r1 = rng.gen_range(-0.05, 0.05); 45 | let r2 = rng.gen_range(-0.05, 0.05); 46 | let t = rng.gen_range(0.0, 1000.0); 47 | explosions.push(Explosion::new(t, na::Point2::new(0.5 + r1, 0.9 + r2))); 48 | } 49 | 50 | Turret { 51 | rotation: 0.0, 52 | raw_text: "".to_string(), 53 | text: MBText::new("".to_string(), &assets.number_font, WHITE, 24.0, ctx), 54 | explosions: explosions, 55 | state: TurretState::Resting, 56 | src_pixel_width: assets.turret.width() as f32, 57 | src_pixel_height: assets.turret.height() as f32, 58 | pos: na::Point2::new(0.5, 0.9), 59 | } 60 | } 61 | 62 | pub fn update(&mut self, _ctx: &mut Context, _dt: std::time::Duration) {} 63 | 64 | pub fn draw(&self, ctx: &mut Context, assets: &mut Assets) { 65 | let param = DrawParam::new() 66 | .color(WHITE) 67 | .scale(self.scale(graphics::size(ctx))) 68 | .offset(na::Point2::new(0.5, 0.5)) 69 | .rotation(self.rotation) 70 | .dest(self.pixel_pos(graphics::size(ctx))); 71 | let _ = graphics::draw(ctx, &assets.turret, param); 72 | self.text 73 | .draw_horizontal_center(graphics::size(ctx).1 * 0.9, ctx); 74 | } 75 | 76 | pub fn draw_lives(&self, lives: usize, ctx: &mut Context, assets: &mut Assets) { 77 | let scale = self.scale(graphics::size(ctx)); 78 | 79 | for i in 0..lives { 80 | let param = DrawParam::new() 81 | .color(WHITE) 82 | .scale(scale * 0.5) 83 | .offset(na::Point2::new(0.5, 0.5)) 84 | .dest(to_screen_pos( 85 | (0.95 + 0.03 * i as f32, 0.925), 86 | graphics::size(ctx), 87 | )); 88 | let _ = graphics::draw(ctx, &assets.turret, param); 89 | } 90 | } 91 | } 92 | --------------------------------------------------------------------------------