├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── README.md ├── ggez-goodies ├── Cargo.toml └── src │ ├── bitmap_font.rs │ ├── camera.rs │ ├── input.rs │ ├── lib.rs │ ├── particle.rs │ └── scene.rs ├── misc ├── dead.png ├── gameplay-1.png ├── gameplay-2.png ├── logo.png ├── menu.png └── thumbnail.png ├── resources ├── audio │ ├── Some(explode).mp3 │ ├── Some(turbofish_shoot).mp3 │ └── dead.mp3 ├── fonts │ └── Consolas.ttf ├── images │ ├── Some(ammo).png │ ├── Some(barrel).png │ ├── Some(cloud).png │ ├── Some(ferris).png │ ├── Some(fish).png │ ├── Some(gun).png │ ├── Some(nil).png │ ├── Some(profile).png │ ├── Some(sniper).png │ ├── Some(turbofish).png │ ├── ferris_ninja.png │ ├── ferris_pacman_1.png │ ├── ferris_pacman_2.png │ ├── ferris_planet.png │ ├── gopher.png │ ├── ground_centre.png │ ├── ground_left.png │ ├── ground_right.png │ ├── logo.png │ └── menu_bg.png ├── maps │ └── 01.map └── shaders │ ├── dim.basic.glslf │ └── dim.glslf └── src ├── main.rs ├── screens ├── dead │ ├── dead.rs │ └── mod.rs ├── game │ ├── components │ │ ├── barrel.rs │ │ ├── bullet.rs │ │ ├── cloud.rs │ │ ├── enemy.rs │ │ ├── mod.rs │ │ ├── player.rs │ │ └── tile.rs │ ├── game.rs │ ├── map.rs │ ├── mod.rs │ └── physics.rs ├── menu │ ├── menu.rs │ └── mod.rs └── mod.rs └── utils.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Check formatting 20 | run: cargo fmt -- --check -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | language: rust 3 | rust: 4 | - nightly 5 | os: 6 | - windows 7 | 8 | cache: cargo 9 | script: 10 | - cargo build --release --verbose 11 | - cargo test --release --verbose -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "call_of_ferris" 3 | version = "0.1.0" 4 | authors = ["Anhad Singh "] 5 | description = """ 6 | A thrilling action game where your favorite Ferris the crab and the rust mascot got guns and has taken up the duty to find evildoer languages while managing to keep itself alive. 7 | Take part in this awesome adventure and help Ferris be the best ever! 8 | """ 9 | repository = "https://github.com/Andy-Python-Programmer/CallOfFerris" 10 | edition = "2018" 11 | 12 | [features] 13 | default = [] 14 | debug = [] 15 | 16 | [dependencies] 17 | ggez = "0.5" 18 | rand = "0.8" 19 | ggez-goodies = { path = "./ggez-goodies/" } 20 | gfx = "0.18.2" 21 | nphysics2d = "0.22.0" 22 | rapier2d = "0.9.2" 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | 5 | 6 |

7 | 8 | A thrilling action game where your favorite Ferris the crab and the rust mascot got guns and has taken up the duty to find evildoer languages while managing to keep itself alive. Take part in this awesome adventure and help Ferris be the best ever! 9 | 10 | ## Screenshots 11 | 12 | 13 | 14 | 15 | 16 | Call of Ferris also comes with super hot slow motion for great bullet time and accuracy 17 | 18 | ## Thanks 19 | - @s-mv for almost all of the assets! 20 | -------------------------------------------------------------------------------- /ggez-goodies/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ggez-goodies" 3 | description = "Various small useful add-ons for the ggez game framework" 4 | version = "0.5.0-rc.1" 5 | repository = "https://github.com/ggez/ggez-goodies" 6 | documentation = "https://docs.rs/ggez-goodies" 7 | authors = ["Simon Heath "] 8 | edition = "2018" 9 | license = "MIT" 10 | readme = "README.md" 11 | 12 | 13 | [dependencies] 14 | ggez = "0.5" 15 | nalgebra-glm = "0.11.0" 16 | rand = "0.8" 17 | 18 | [dev-dependencies] 19 | ezing = "0.2.1" 20 | -------------------------------------------------------------------------------- /ggez-goodies/src/bitmap_font.rs: -------------------------------------------------------------------------------- 1 | 2 | use std::collections::HashMap; 3 | use ggez; 4 | 5 | /// Describes the layout of characters in your 6 | /// bitmap font. 7 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 8 | pub struct TextMap { 9 | map: HashMap, 10 | } 11 | 12 | impl TextMap { 13 | /// Creates a new `TextMap` from a uniform grid of 14 | /// sprites. Takes the number of sprites wide and 15 | /// tall that the bitmap should be, and a string 16 | /// describing the characters in the map... in order, 17 | /// left to right, top to bottom. 18 | /// 19 | /// The characters do not necessarily need to fill 20 | /// the entire image. ie, if your image is 16x16 glyphs 21 | /// for 256 total, and you only use the first 150 of them, 22 | /// that's fine. 23 | /// 24 | /// The floating point math involved should always be 25 | /// exact for `Image`'s and sprites with a resolution 26 | /// that is a power of two, I think. 27 | fn from_grid(mapping: &str, width: usize, height: usize) -> Self { 28 | // Assert the given width and height can fit the listed characters. 29 | let num_chars = mapping.chars.count(); 30 | assert!(num_chars <= width * height); 31 | let rect_width = 1.0 / (width as f32); 32 | let rect_height = 1.0 / (height as f32); 33 | let mut map = HashMap::with_capacity(num_chars); 34 | let mut current_x = 0; 35 | let mut current_y = 0; 36 | for c in mapping.chars() { 37 | let x_offset = current_x as f32 * rect_width; 38 | let y_offset = current_y as f32 * rect_height; 39 | let char_rect = ggez::Rect { 40 | x: x_offset, 41 | y: y_offset, 42 | w: rect_width, 43 | h: rect_height 44 | }; 45 | map.insert(c, char_rect); 46 | current_x = (current_x + 1) % width; 47 | if current_x == 0 { 48 | current_y += 1; 49 | } 50 | } 51 | 52 | Self { 53 | map, 54 | } 55 | } 56 | } 57 | 58 | #[derive(Clone, Debug, PartialEq, Eq)] 59 | pub struct BitmapFont { 60 | bitmap: ggez::graphics::Image, 61 | batch: ggez::graphics::SpriteBatch, 62 | map: TextMap, 63 | } 64 | 65 | impl BitmapFont { 66 | 67 | } -------------------------------------------------------------------------------- /ggez-goodies/src/camera.rs: -------------------------------------------------------------------------------- 1 | //! A camera object for ggez. 2 | //! Currently ggez has no actual global camera state to use, 3 | //! so this really just does the coordinate transforms for you. 4 | //! 5 | //! Basically it translates ggez's coordinate system with the origin 6 | //! at the top-left and Y increasing downward to a coordinate system 7 | //! with the origin at the center of the screen and Y increasing 8 | //! upward. 9 | //! 10 | //! Because that makes sense, darn it. 11 | //! 12 | //! However, does not yet do any actual camera movements like 13 | //! easing, pinning, etc. 14 | //! But a great source for how such things work is this: 15 | //! http://www.gamasutra.com/blogs/ItayKeren/20150511/243083/Scroll_Back_The_Theory_and_Practice_of_Cameras_in_SideScrollers.php 16 | 17 | // TODO: Debug functions to draw world and camera grid! 18 | 19 | use ggez; 20 | use ggez::graphics; 21 | use ggez::mint; 22 | use ggez::GameResult; 23 | use nalgebra_glm::Vec2; 24 | 25 | // Used for mint interoperability. 26 | struct Vector2(Vec2); 27 | struct MintPoint2(mint::Point2); 28 | 29 | impl Into> for Vector2 { 30 | fn into(self) -> mint::Point2 { 31 | mint::Point2 { 32 | x: self.0.x, 33 | y: self.0.y, 34 | } 35 | } 36 | } 37 | 38 | impl Into for MintPoint2 { 39 | fn into(self) -> Vec2 { 40 | Vec2::new(self.0.x, self.0.y) 41 | } 42 | } 43 | 44 | // Hmm. Could, instead, use a 2d transformation 45 | // matrix, or create one of such. 46 | pub struct Camera { 47 | screen_size: Vec2, 48 | view_size: Vec2, 49 | view_center: Vec2, 50 | } 51 | 52 | impl Camera { 53 | pub fn new(screen_width: u32, screen_height: u32, view_width: f32, view_height: f32) -> Self { 54 | let screen_size = Vec2::new(screen_width as f32, screen_height as f32); 55 | let view_size = Vec2::new(view_width as f32, view_height as f32); 56 | Camera { 57 | screen_size, 58 | view_size, 59 | view_center: Vec2::new(0.0, 0.0), 60 | } 61 | } 62 | 63 | pub fn move_by(&mut self, by: Vec2) { 64 | self.view_center.x += by.x; 65 | self.view_center.y += by.y; 66 | } 67 | 68 | pub fn move_to(&mut self, to: Vec2) { 69 | self.view_center = to; 70 | } 71 | 72 | /// Translates a point in world-space to a point in 73 | /// screen-space. 74 | /// 75 | /// Does not do any clipping or anything, since it does 76 | /// not know how large the thing that might be drawn is; 77 | /// that's not its job. 78 | pub fn world_to_screen_coords(&self, from: Vec2) -> (i32, i32) { 79 | let pixels_per_unit = self.screen_size.component_div(&self.view_size); 80 | let view_offset = from - self.view_center; 81 | let view_scale = view_offset.component_mul(&pixels_per_unit); 82 | 83 | let x = view_scale.x + self.screen_size.x / 2.0; 84 | let y = self.screen_size.y + (view_scale.y - self.screen_size.y / 2.0); 85 | (x as i32, y as i32) 86 | } 87 | 88 | // p_screen = max_p - p + max_p/2 89 | // p_screen - max_p/2 = max_p - p 90 | // p_screen - max_p/2 + max_p = -p 91 | // -p_screen - max_p/2 + max_p = p 92 | pub fn screen_to_world_coords(&self, from: (i32, i32)) -> Vec2 { 93 | let (sx, sy) = from; 94 | let sx = sx as f32; 95 | let sy = sy as f32; 96 | let flipped_x = sx - (self.screen_size.x / 2.0); 97 | let flipped_y = -sy + self.screen_size.y / 2.0; 98 | let screen_coords = Vec2::new(flipped_x, flipped_y); 99 | let units_per_pixel = self.view_size.component_div(&self.screen_size); 100 | let view_scale = screen_coords.component_mul(&units_per_pixel); 101 | let view_offset = self.view_center + view_scale; 102 | 103 | view_offset 104 | } 105 | 106 | pub fn location(&self) -> Vec2 { 107 | self.view_center 108 | } 109 | 110 | pub fn calculate_dest_point(&self, location: Vec2) -> Vec2 { 111 | let (sx, sy) = self.world_to_screen_coords(location); 112 | Vec2::new(sx as f32, sy as f32) 113 | } 114 | } 115 | 116 | pub trait CameraDraw 117 | where 118 | Self: graphics::Drawable, 119 | { 120 | fn draw_ex_camera( 121 | &self, 122 | camera: &Camera, 123 | ctx: &mut ggez::Context, 124 | p: ggez::graphics::DrawParam, 125 | ) -> GameResult<()> { 126 | let dest = camera.calculate_dest_point(MintPoint2(p.dest).into()); 127 | let mut my_p = p; 128 | my_p.dest = Vector2(dest).into(); 129 | self.draw(ctx, my_p) 130 | } 131 | 132 | fn draw_camera( 133 | &self, 134 | camera: &Camera, 135 | ctx: &mut ggez::Context, 136 | dest: Vec2, 137 | rotation: f32, 138 | ) -> GameResult<()> { 139 | let dest = camera.calculate_dest_point(dest); 140 | let mut draw_param = ggez::graphics::DrawParam::default(); 141 | draw_param.dest = Vector2(dest).into(); 142 | draw_param.rotation = rotation; 143 | self.draw(ctx, draw_param) 144 | } 145 | } 146 | 147 | impl CameraDraw for T where T: graphics::Drawable {} 148 | 149 | #[cfg(test)] 150 | mod tests { 151 | use super::*; 152 | use nalgebra_glm::Vec2; 153 | 154 | #[test] 155 | fn test_coord_round_trip() { 156 | let mut c = Camera::new(640, 480, 40.0, 30.0); 157 | let p1 = (200, 300); 158 | { 159 | let p1_world = c.screen_to_world_coords(p1); 160 | assert_eq!(p1_world, Vec2::new(-7.5, -3.75)); 161 | let p1_screen = c.world_to_screen_coords(p1_world); 162 | assert_eq!(p1, p1_screen); 163 | } 164 | 165 | let p2 = Vec2::new(20.0, 10.0); 166 | { 167 | let p2_screen = c.world_to_screen_coords(p2); 168 | assert_eq!(p2_screen, (640, 80)); 169 | let p2_world = c.screen_to_world_coords(p2_screen); 170 | assert_eq!(p2_world, p2); 171 | } 172 | 173 | c.move_to(Vec2::new(5.0, 5.0)); 174 | 175 | { 176 | let p1_world = c.screen_to_world_coords(p1); 177 | assert_eq!(p1_world, Vec2::new(-2.5, 1.25)); 178 | let p1_screen = c.world_to_screen_coords(p1_world); 179 | assert_eq!(p1, p1_screen); 180 | } 181 | { 182 | let p2_screen = c.world_to_screen_coords(p2); 183 | assert_eq!(p2_screen, (560, 160)); 184 | let p2_world = c.screen_to_world_coords(p2_screen); 185 | assert_eq!(p2_world, p2); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /ggez-goodies/src/input.rs: -------------------------------------------------------------------------------- 1 | //! An abstract input state object that gets fed user 2 | //! events and updates itself based on a set of key 3 | //! bindings. 4 | //! 5 | //! The goals are: 6 | //! 7 | //! * Have a layer of abstract key bindings rather than 8 | //! looking at concrete event types 9 | //! * Use this to be able to abstract away differences 10 | //! between keyboards, joysticks and game controllers 11 | //! (rather based on Unity3D), 12 | //! * Do some tweening of input axes and stuff just for 13 | //! fun. 14 | //! * Take ggez's event-based input API, and present event- or 15 | //! state-based API so you can do whichever you want. 16 | 17 | // TODO: Handle mice, game pads, joysticks 18 | 19 | use ggez::event::KeyCode; 20 | use std::collections::HashMap; 21 | use std::hash::Hash; 22 | 23 | // Okay, but how does it actually work? 24 | // Basically we have to bind input events to buttons and axes. 25 | // Input events can be keys, mouse buttons/motion, or eventually 26 | // joystick/controller inputs. Mouse delta can be mapped to axes too. 27 | // 28 | // https://docs.unity3d.com/Manual/ConventionalGameInput.html has useful 29 | // descriptions of the exact behavior of axes. 30 | // 31 | // So to think about this more clearly, here are the default bindings: 32 | // 33 | // W, ↑: +Y axis 34 | // A, ←: -X axis 35 | // S, ↓: -Y axis 36 | // D, →: +X axis 37 | // Enter, z, LMB: Button 1 38 | // Shift, x, MMB: Button 2 39 | // Ctrl, c, RMB: Button 3 40 | // 41 | // Easy way? Hash map of event -> axis/button bindings. 42 | 43 | #[derive(Debug, Hash, Eq, PartialEq, Copy, Clone)] 44 | enum InputType { 45 | KeyEvent(KeyCode), // MouseButtonEvent, 46 | } 47 | 48 | #[derive(Debug, Copy, Clone, PartialEq)] 49 | pub enum InputEffect 50 | where 51 | Axes: Eq + Hash + Clone, 52 | Buttons: Eq + Hash + Clone, 53 | { 54 | Axis(Axes, bool), 55 | Button(Buttons), 56 | } 57 | 58 | #[derive(Debug, Copy, Clone)] 59 | struct AxisState { 60 | // Where the axis currently is, in [-1, 1] 61 | position: f32, 62 | // Where the axis is moving towards. Possible 63 | // values are -1, 0, +1 64 | // (or a continuous range for analog devices I guess) 65 | direction: f32, 66 | // Speed in units per second that the axis 67 | // moves towards the target value. 68 | acceleration: f32, 69 | // Speed in units per second that the axis will 70 | // fall back toward 0 if the input stops. 71 | gravity: f32, 72 | } 73 | 74 | impl Default for AxisState { 75 | fn default() -> Self { 76 | AxisState { 77 | position: 0.0, 78 | direction: 0.0, 79 | acceleration: 4.0, 80 | gravity: 3.0, 81 | } 82 | } 83 | } 84 | 85 | #[derive(Debug, Copy, Clone, Default)] 86 | struct ButtonState { 87 | pressed: bool, 88 | pressed_last_frame: bool, 89 | } 90 | 91 | /// A struct that contains a mapping from physical input events 92 | /// (currently just `KeyCode`s) to whatever your logical Axis/Button 93 | /// types are. 94 | pub struct InputBinding 95 | where 96 | Axes: Hash + Eq + Clone, 97 | Buttons: Hash + Eq + Clone, 98 | { 99 | // Once EnumSet is stable it should be used for these 100 | // instead of BTreeMap. ♥? 101 | // Binding of keys to input values. 102 | bindings: HashMap>, 103 | } 104 | 105 | impl InputBinding 106 | where 107 | Axes: Hash + Eq + Clone, 108 | Buttons: Hash + Eq + Clone, 109 | { 110 | pub fn new() -> Self { 111 | InputBinding { 112 | bindings: HashMap::new(), 113 | } 114 | } 115 | 116 | /// Adds a key binding connecting the given keycode to the given 117 | /// logical axis. 118 | pub fn bind_key_to_axis(mut self, keycode: KeyCode, axis: Axes, positive: bool) -> Self { 119 | self.bindings.insert( 120 | InputType::KeyEvent(keycode), 121 | InputEffect::Axis(axis.clone(), positive), 122 | ); 123 | self 124 | } 125 | 126 | /// Adds a key binding connecting the given keycode to the given 127 | /// logical button. 128 | pub fn bind_key_to_button(mut self, keycode: KeyCode, button: Buttons) -> Self { 129 | self.bindings.insert( 130 | InputType::KeyEvent(keycode), 131 | InputEffect::Button(button.clone()), 132 | ); 133 | self 134 | } 135 | 136 | /// Takes an physical input type and turns it into a logical input type (keycode -> axis/button). 137 | pub fn resolve(&self, keycode: KeyCode) -> Option> { 138 | self.bindings.get(&InputType::KeyEvent(keycode)).cloned() 139 | } 140 | } 141 | 142 | #[derive(Debug)] 143 | pub struct InputState 144 | where 145 | Axes: Hash + Eq + Clone, 146 | Buttons: Hash + Eq + Clone, 147 | { 148 | // Input state for axes 149 | axes: HashMap, 150 | // Input states for buttons 151 | buttons: HashMap, 152 | } 153 | 154 | impl InputState 155 | where 156 | Axes: Eq + Hash + Clone, 157 | Buttons: Eq + Hash + Clone, 158 | { 159 | pub fn new() -> Self { 160 | InputState { 161 | axes: HashMap::new(), 162 | buttons: HashMap::new(), 163 | } 164 | } 165 | 166 | /// Updates the logical input state based on the actual 167 | /// physical input state. Should be called in your update() 168 | /// handler. 169 | /// So, it will do things like move the axes and so on. 170 | pub fn update(&mut self, dt: f32) { 171 | for (_axis, axis_status) in self.axes.iter_mut() { 172 | if axis_status.direction != 0.0 { 173 | // Accelerate the axis towards the 174 | // input'ed direction. 175 | let vel = axis_status.acceleration * dt; 176 | let pending_position = axis_status.position 177 | + if axis_status.direction > 0.0 { 178 | vel 179 | } else { 180 | -vel 181 | }; 182 | axis_status.position = if pending_position > 1.0 { 183 | 1.0 184 | } else if pending_position < -1.0 { 185 | -1.0 186 | } else { 187 | pending_position 188 | } 189 | } else { 190 | // Gravitate back towards 0. 191 | let abs_dx = f32::min(axis_status.gravity * dt, f32::abs(axis_status.position)); 192 | let dx = if axis_status.position > 0.0 { 193 | -abs_dx 194 | } else { 195 | abs_dx 196 | }; 197 | axis_status.position += dx; 198 | } 199 | } 200 | for (_button, button_status) in self.buttons.iter_mut() { 201 | button_status.pressed_last_frame = button_status.pressed; 202 | } 203 | } 204 | 205 | /// This method should get called by your key_down_event handler. 206 | pub fn update_button_down(&mut self, button: Buttons) { 207 | self.update_effect(InputEffect::Button(button), true); 208 | } 209 | 210 | /// This method should get called by your key_up_event handler. 211 | pub fn update_button_up(&mut self, button: Buttons) { 212 | self.update_effect(InputEffect::Button(button), false); 213 | } 214 | 215 | /// This method should get called by your key_up_event handler. 216 | pub fn update_axis_start(&mut self, axis: Axes, positive: bool) { 217 | self.update_effect(InputEffect::Axis(axis, positive), true); 218 | } 219 | 220 | pub fn update_axis_stop(&mut self, axis: Axes, positive: bool) { 221 | self.update_effect(InputEffect::Axis(axis, positive), false); 222 | } 223 | 224 | /// Takes an InputEffect and actually applies it. 225 | pub fn update_effect(&mut self, effect: InputEffect, started: bool) { 226 | match effect { 227 | InputEffect::Axis(axis, positive) => { 228 | let f = || AxisState::default(); 229 | let axis_status = self.axes.entry(axis).or_insert_with(f); 230 | if started { 231 | let direction_float = if positive { 1.0 } else { -1.0 }; 232 | axis_status.direction = direction_float; 233 | } else if (positive && axis_status.direction > 0.0) 234 | || (!positive && axis_status.direction < 0.0) 235 | { 236 | axis_status.direction = 0.0; 237 | } 238 | } 239 | InputEffect::Button(button) => { 240 | let f = || ButtonState::default(); 241 | let button_status = self.buttons.entry(button).or_insert_with(f); 242 | button_status.pressed = started; 243 | } 244 | } 245 | } 246 | 247 | pub fn get_axis(&self, axis: Axes) -> f32 { 248 | let d = AxisState::default(); 249 | let axis_status = self.axes.get(&axis).unwrap_or(&d); 250 | axis_status.position 251 | } 252 | 253 | pub fn get_axis_raw(&self, axis: Axes) -> f32 { 254 | let d = AxisState::default(); 255 | let axis_status = self.axes.get(&axis).unwrap_or(&d); 256 | axis_status.direction 257 | } 258 | 259 | fn get_button(&self, button: Buttons) -> ButtonState { 260 | let d = ButtonState::default(); 261 | let button_status = self.buttons.get(&button).unwrap_or(&d); 262 | *button_status 263 | } 264 | 265 | pub fn get_button_down(&self, axis: Buttons) -> bool { 266 | self.get_button(axis).pressed 267 | } 268 | 269 | pub fn get_button_up(&self, axis: Buttons) -> bool { 270 | !self.get_button(axis).pressed 271 | } 272 | 273 | /// Returns whether or not the button was pressed this frame, 274 | /// only returning true if the press happened this frame. 275 | /// 276 | /// Basically, `get_button_down()` and `get_button_up()` are level 277 | /// triggers, this and `get_button_released()` are edge triggered. 278 | pub fn get_button_pressed(&self, axis: Buttons) -> bool { 279 | let b = self.get_button(axis); 280 | b.pressed && !b.pressed_last_frame 281 | } 282 | 283 | pub fn get_button_released(&self, axis: Buttons) -> bool { 284 | let b = self.get_button(axis); 285 | !b.pressed && b.pressed_last_frame 286 | } 287 | 288 | pub fn mouse_position() { 289 | unimplemented!() 290 | } 291 | 292 | pub fn mouse_scroll_delta() { 293 | unimplemented!() 294 | } 295 | 296 | pub fn get_mouse_button() { 297 | unimplemented!() 298 | } 299 | 300 | pub fn get_mouse_button_down() { 301 | unimplemented!() 302 | } 303 | 304 | pub fn get_mouse_button_up() { 305 | unimplemented!() 306 | } 307 | 308 | pub fn reset_input_state(&mut self) { 309 | for (_axis, axis_status) in self.axes.iter_mut() { 310 | axis_status.position = 0.0; 311 | axis_status.direction = 0.0; 312 | } 313 | 314 | for (_button, button_status) in self.buttons.iter_mut() { 315 | button_status.pressed = false; 316 | button_status.pressed_last_frame = false; 317 | } 318 | } 319 | } 320 | 321 | #[cfg(test)] 322 | mod tests { 323 | use super::*; 324 | use ggez::event::*; 325 | 326 | #[derive(Hash, Eq, PartialEq, Copy, Clone, Debug)] 327 | enum Buttons { 328 | A, 329 | B, 330 | Select, 331 | Start, 332 | } 333 | 334 | #[derive(Hash, Eq, PartialEq, Copy, Clone, Debug)] 335 | enum Axes { 336 | Horz, 337 | Vert, 338 | } 339 | 340 | fn make_input_binding() -> InputBinding { 341 | let ib = InputBinding::::new() 342 | .bind_key_to_button(KeyCode::Z, Buttons::A) 343 | .bind_key_to_button(KeyCode::X, Buttons::B) 344 | .bind_key_to_button(KeyCode::Return, Buttons::Start) 345 | .bind_key_to_button(KeyCode::RShift, Buttons::Select) 346 | .bind_key_to_button(KeyCode::LShift, Buttons::Select) 347 | .bind_key_to_axis(KeyCode::Up, Axes::Vert, true) 348 | .bind_key_to_axis(KeyCode::Down, Axes::Vert, false) 349 | .bind_key_to_axis(KeyCode::Left, Axes::Horz, false) 350 | .bind_key_to_axis(KeyCode::Right, Axes::Horz, true); 351 | ib 352 | } 353 | 354 | #[test] 355 | fn test_input_bindings() { 356 | let ib = make_input_binding(); 357 | assert_eq!( 358 | ib.resolve(KeyCode::Z), 359 | Some(InputEffect::Button(Buttons::A)) 360 | ); 361 | assert_eq!( 362 | ib.resolve(KeyCode::X), 363 | Some(InputEffect::Button(Buttons::B)) 364 | ); 365 | assert_eq!( 366 | ib.resolve(KeyCode::Return), 367 | Some(InputEffect::Button(Buttons::Start)) 368 | ); 369 | assert_eq!( 370 | ib.resolve(KeyCode::RShift), 371 | Some(InputEffect::Button(Buttons::Select)) 372 | ); 373 | assert_eq!( 374 | ib.resolve(KeyCode::LShift), 375 | Some(InputEffect::Button(Buttons::Select)) 376 | ); 377 | 378 | assert_eq!( 379 | ib.resolve(KeyCode::Up), 380 | Some(InputEffect::Axis(Axes::Vert, true)) 381 | ); 382 | assert_eq!( 383 | ib.resolve(KeyCode::Down), 384 | Some(InputEffect::Axis(Axes::Vert, false)) 385 | ); 386 | assert_eq!( 387 | ib.resolve(KeyCode::Left), 388 | Some(InputEffect::Axis(Axes::Horz, false)) 389 | ); 390 | assert_eq!( 391 | ib.resolve(KeyCode::Right), 392 | Some(InputEffect::Axis(Axes::Horz, true)) 393 | ); 394 | 395 | assert_eq!(ib.resolve(KeyCode::Q), None); 396 | assert_eq!(ib.resolve(KeyCode::W), None); 397 | } 398 | 399 | #[test] 400 | fn test_input_events() { 401 | let mut im = InputState::new(); 402 | im.update_button_down(Buttons::A); 403 | assert!(im.get_button_down(Buttons::A)); 404 | im.update_button_up(Buttons::A); 405 | assert!(!im.get_button_down(Buttons::A)); 406 | assert!(im.get_button_up(Buttons::A)); 407 | 408 | // Push the 'up' button, watch the axis 409 | // increase to 1.0 but not beyond 410 | im.update_axis_start(Axes::Vert, true); 411 | assert!(im.get_axis_raw(Axes::Vert) > 0.0); 412 | while im.get_axis(Axes::Vert) < 0.99 { 413 | im.update(0.16); 414 | assert!(im.get_axis(Axes::Vert) >= 0.0); 415 | assert!(im.get_axis(Axes::Vert) <= 1.0); 416 | } 417 | // Release it, watch it wind down 418 | im.update_axis_stop(Axes::Vert, true); 419 | while im.get_axis(Axes::Vert) > 0.01 { 420 | im.update(0.16); 421 | assert!(im.get_axis(Axes::Vert) >= 0.0) 422 | } 423 | 424 | // Do the same with the 'down' button. 425 | im.update_axis_start(Axes::Vert, false); 426 | while im.get_axis(Axes::Vert) > -0.99 { 427 | im.update(0.16); 428 | assert!(im.get_axis(Axes::Vert) <= 0.0); 429 | assert!(im.get_axis(Axes::Vert) >= -1.0); 430 | } 431 | 432 | // Test the transition from 'up' to 'down' 433 | im.update_axis_start(Axes::Vert, true); 434 | while im.get_axis(Axes::Vert) < 1.0 { 435 | im.update(0.16); 436 | } 437 | im.update_axis_start(Axes::Vert, false); 438 | im.update(0.16); 439 | assert!(im.get_axis(Axes::Vert) < 1.0); 440 | im.update_axis_stop(Axes::Vert, true); 441 | assert!(im.get_axis_raw(Axes::Vert) < 0.0); 442 | im.update_axis_stop(Axes::Vert, false); 443 | assert_eq!(im.get_axis_raw(Axes::Vert), 0.0); 444 | } 445 | 446 | #[test] 447 | fn test_button_edge_transitions() { 448 | let mut im: InputState = InputState::new(); 449 | 450 | // Push a key, confirm it's transitioned. 451 | assert!(!im.get_button_down(Buttons::A)); 452 | im.update_button_down(Buttons::A); 453 | assert!(im.get_button_down(Buttons::A)); 454 | assert!(im.get_button_pressed(Buttons::A)); 455 | assert!(!im.get_button_released(Buttons::A)); 456 | 457 | // Update, confirm it's still down but 458 | // wasn't pressed this frame 459 | im.update(0.1); 460 | assert!(im.get_button_down(Buttons::A)); 461 | assert!(!im.get_button_pressed(Buttons::A)); 462 | assert!(!im.get_button_released(Buttons::A)); 463 | 464 | // Release it 465 | im.update_button_up(Buttons::A); 466 | assert!(im.get_button_up(Buttons::A)); 467 | assert!(!im.get_button_pressed(Buttons::A)); 468 | assert!(im.get_button_released(Buttons::A)); 469 | im.update(0.1); 470 | assert!(im.get_button_up(Buttons::A)); 471 | assert!(!im.get_button_pressed(Buttons::A)); 472 | assert!(!im.get_button_released(Buttons::A)); 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /ggez-goodies/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(warnings)] 2 | 3 | extern crate ggez; 4 | pub extern crate nalgebra_glm; 5 | extern crate rand; 6 | 7 | pub mod camera; 8 | pub mod input; 9 | pub mod particle; 10 | pub mod scene; 11 | -------------------------------------------------------------------------------- /ggez-goodies/src/particle.rs: -------------------------------------------------------------------------------- 1 | // Useful references: 2 | // https://www.reddit.com/r/gamedev/comments/13ksu3/article_on_particle_systems_and_an_online_cocos2d/ 3 | // Unity3D's particle system 4 | // Cocos2d's plist file format 5 | // Oh, love2d's particle system parameters, derp. 6 | 7 | // I think this could be simplified. 8 | // For each particle property, take an easing function (perhaps just from the `ezing` crate), 9 | // and bounds to map the start and end to. 10 | // Randomization would alter the bounds per-particle. 11 | // Don't try to cover all cases right off the bat, it should be easy to add more. 12 | // 13 | // The real useful stuff here worth preserving is probably the emission stuff 14 | // and the Particle type... 15 | // StartParam might actually be useful as well maybe. 16 | 17 | use std::marker::Sized; 18 | 19 | use std::f32; 20 | 21 | use ggez::graphics; 22 | use ggez::graphics::spritebatch::SpriteBatch; 23 | use ggez::graphics::BlendMode; 24 | use ggez::mint::{Point2, Vector2}; 25 | use ggez::{Context, GameResult}; 26 | use rand; 27 | use rand::Rng; 28 | 29 | enum ValueGenerator { 30 | Fixed(T), 31 | 32 | // TODO: stepped range, a list of discrete values of which one gets chosen. 33 | UniformRange(T, T), 34 | } 35 | 36 | impl ValueGenerator { 37 | pub fn get_value(&self) -> f32 { 38 | match *self { 39 | ValueGenerator::Fixed(x) => x, 40 | ValueGenerator::UniformRange(ref low, ref high) => { 41 | let mut rng = rand::thread_rng(); 42 | rng.gen_range(*low..=*high) 43 | } 44 | } 45 | } 46 | } 47 | 48 | // Apparently implementing SampleRange for our own type 49 | // isn't something we should do, so we just define this by hand... 50 | impl ValueGenerator> { 51 | fn get_value(&self) -> Vector2 { 52 | match *self { 53 | ValueGenerator::Fixed(x) => x, 54 | ValueGenerator::UniformRange(low, high) => { 55 | let mut rng = rand::thread_rng(); 56 | let x = rng.gen_range(low.x..=high.x); 57 | let y = rng.gen_range(low.y..=high.y); 58 | Vector2 { x, y } 59 | } 60 | } 61 | } 62 | } 63 | 64 | impl ValueGenerator> { 65 | fn get_value(&self) -> Point2 { 66 | match *self { 67 | ValueGenerator::Fixed(x) => x, 68 | ValueGenerator::UniformRange(low, high) => { 69 | let mut rng = rand::thread_rng(); 70 | let x = rng.gen_range(low.x..=high.x); 71 | let y = rng.gen_range(low.y..=high.y); 72 | Point2 { x, y } 73 | } 74 | } 75 | } 76 | } 77 | 78 | impl ValueGenerator { 79 | fn get_value(&self) -> graphics::Color { 80 | match *self { 81 | ValueGenerator::Fixed(x) => x, 82 | ValueGenerator::UniformRange(low, high) => { 83 | let mut rng = rand::thread_rng(); 84 | let (lowr, lowg, lowb) = low.into(); 85 | let (hir, hig, hib) = high.into(); 86 | let r = rng.gen_range(lowr..=hir); 87 | let g = rng.gen_range(lowg..=hig); 88 | let b = rng.gen_range(lowb..=hib); 89 | (r, g, b).into() 90 | } 91 | } 92 | } 93 | } 94 | 95 | pub type EasingFn = Fn(f32) -> f32; 96 | 97 | /// Linear interpolation; assumes input value is in the range 0-1 and 98 | /// returns it interpolated to the given bounds. 99 | /// 100 | /// For example: `lerp(easing::cubic_inout(v), 0.0, 100.0)` 101 | pub fn lerp(v: f32, from: f32, to: f32) -> f32 { 102 | let delta = to - from; 103 | v * delta 104 | } 105 | 106 | /// A trait that defines a way to do some sort of 107 | /// lerp or easing function on a type. 108 | pub trait Interpolate 109 | where 110 | Self: Sized, 111 | { 112 | /// Interpolate the value. t should always be a number 113 | /// between 0.0 and 1.0, normalized for whatever actual 114 | /// value is the "end" of the interpolation. 115 | fn interp(&self, t: f32) -> Self; 116 | 117 | fn interp_between(t: f32, v1: Self, v2: Self) -> Self; 118 | 119 | /// A little shortcut that does the normalization for you. 120 | fn normalize_interp(&self, t: f32, max_t: f32) -> Self { 121 | let norm_t = t / max_t; 122 | self.interp(norm_t) 123 | } 124 | 125 | /// Combines interp_between with normalize_interp() 126 | fn normalize_interp_between(t: f32, max_t: f32, v1: Self, v2: Self) -> Self { 127 | let norm_t = t / max_t; 128 | Self::interp_between(norm_t, v1, v2) 129 | } 130 | } 131 | 132 | impl Interpolate for f32 { 133 | fn interp(&self, t: f32) -> Self { 134 | *self * t 135 | } 136 | 137 | fn interp_between(t: f32, v1: Self, v2: Self) -> Self { 138 | let val1 = v1.interp(1.0 - t); 139 | let val2 = v2.interp(t); 140 | val1 + val2 141 | } 142 | } 143 | 144 | // This function is broken; see ggj2017 code for fix. :/ 145 | impl Interpolate for graphics::Color { 146 | fn interp(&self, t: f32) -> Self { 147 | let rt = self.r * t; 148 | let gt = self.g * t; 149 | let bt = self.b * t; 150 | let at = self.a * t; 151 | graphics::Color::new(rt, gt, bt, at) 152 | } 153 | 154 | fn interp_between(t: f32, v1: Self, v2: Self) -> Self { 155 | let val1 = v1.interp(1.0 - t); 156 | let val2 = v2.interp(t); 157 | let r = val1.r + val2.r; 158 | let g = val1.g + val2.g; 159 | let b = val1.b + val2.b; 160 | let a = val1.a + val2.a; 161 | graphics::Color::new(r, g, b, a) 162 | } 163 | } 164 | 165 | /// A structure that represents a transition between 166 | /// set properties, with multiple potential defined points. 167 | /// So for instance you could use Transition and define 168 | /// a transition of colors from red to orange to grey to do smoke. 169 | /// You could also use Transition to just represent a size 170 | /// curve. 171 | /// So really this is a general-purpose easing type thing... 172 | /// It assumes that all time values range from 0 to 1. 173 | pub enum Transition { 174 | Fixed(T), 175 | Range(T, T), 176 | } 177 | 178 | impl Transition { 179 | pub fn fixed(value: T) -> Self { 180 | Transition::Fixed(value) 181 | } 182 | 183 | pub fn range(from: T, to: T) -> Self { 184 | Transition::Range(from, to) 185 | } 186 | 187 | /// t should be between 0.0 and 1.0 188 | /// or should it take the current value and a delta-t??? 189 | pub fn get(&self, t: f32) -> T { 190 | match *self { 191 | Transition::Fixed(value) => value, 192 | Transition::Range(from, to) => T::interp_between(t, from, to), 193 | } 194 | } 195 | } 196 | 197 | // Properties particles should have: 198 | // Age, position, velocity 199 | 200 | // Properties particle systems should have: 201 | // color, inc. opacity 202 | // texture (perhaps sprite?), multiplied by color 203 | // size 204 | // gravity 205 | // fade rate/color transitions 206 | // max lifespan 207 | // speed 208 | // xvel, yvel 209 | // shape??? 210 | // Gravity??? 211 | // Glow??? 212 | // x/y bounds (delete particles that go further than this) 213 | // floor and ceiling? (particles bounce off of these) 214 | // 215 | // Per love2d, which appears to cover all the basics and more: 216 | // area spread (uniform, normal) 217 | // * buffer size (number of particles) 218 | // * linear acceleration (general case of gravity) 219 | // color (of image) 220 | // colors (of non-image particle) 221 | // direction 222 | // emission rate (constant, burst) 223 | // emitter lifetime 224 | // image 225 | // insert mode (where particles are inserted; top, bottom, random) 226 | // lifetime 227 | // linear damping 228 | // particle lifetime (min, max) 229 | // position of emitter 230 | // quads (series of images to use as sprites) 231 | // radial acceeleration 232 | // ang_vel 233 | // size variations/sizes 234 | // set speed 235 | // spin, spin variation 236 | // spread 237 | // tangential acceleration 238 | // 239 | // Honestly having general purpose "create" and "update" traits 240 | // would abstract out a lot of this, and then we just define 241 | // the basics. 242 | // 243 | // It would also be very nice to be able to have a particle system 244 | // calculate in is own relative coordinate space OR world absolute space. 245 | // Though if the user defines their own worldspace coordinate system 246 | // that could get a bit sticky. :/ 247 | 248 | struct Particle { 249 | pos: Point2, 250 | vel: Vector2, 251 | color: graphics::Color, 252 | size: f32, 253 | angle: f32, 254 | ang_vel: f32, 255 | age: f32, 256 | max_age: f32, 257 | } 258 | 259 | // Aha. We have a 2x2 matrix of cases here: A particle can have a property 260 | // that's specific to each particle and calculated from some particle-specific 261 | // state, like position. It can have a property that's the same for each particle 262 | // but calculated the same for each particle, like color in a simple flame effect. 263 | // It can have a property that's not specific to each particle and calculated the 264 | // same for each particle, like gravity, or that's not specific to each particle and 265 | // calculated 266 | // 267 | // So our axes are: State per particle vs state per particle system, 268 | // and constant over time vs varying over time. 269 | // 270 | // The TRICK is that a property can optionally fit into more than one 271 | // of these values, so it has to decide at runtime. And doing that 272 | // efficiently is a pain in the ass. >:-[ 273 | // So SCREW it, we will handle the most general case. Bah! 274 | // 275 | // Hmmmm, we could handle this in a more functional way, where we define 276 | // each transition as a function, and then compose/chain them. But Rust 277 | // requires these functions to be pure. 278 | // 279 | // Okay, any thing that could be a Transition? We really want that to 280 | // be a per-particle-system thing rather than a per-particle thing. 281 | // Also it's going to be a huge pain in the ass to get the numbers 282 | // right. :/ 283 | // 284 | // While a completely valid insight that's the absolute wrong way of doing it. 285 | // The thing about particle systems that makes them useful is they're fast, and 286 | // the thing that makes them fast is the each particle more or less obeys the 287 | // same rules as all the others. 288 | 289 | impl Particle { 290 | fn new( 291 | pos: Point2, 292 | vel: Vector2, 293 | color: graphics::Color, 294 | size: f32, 295 | angle: f32, 296 | max_age: f32, 297 | ) -> Self { 298 | Particle { 299 | pos: pos, 300 | vel: vel, 301 | color: color, 302 | size: size, 303 | angle: angle, 304 | ang_vel: 0.0, 305 | age: 0.0, 306 | max_age: max_age, 307 | } 308 | } 309 | } 310 | 311 | // This probably isn't actually needed as a separate type, 312 | // at least at this point, 313 | // but it makes things clearer for the moment... Hmm. 314 | // Wow the macro system is kind of shitty though, since you 315 | // can't synthesize identifiers. 316 | pub struct ParticleSystemBuilder { 317 | system: ParticleSystem, 318 | } 319 | 320 | macro_rules! prop { 321 | ($name:ident, $rangename:ident, $typ:ty) => { 322 | pub fn $name(mut self, $name: $typ) -> Self { 323 | self.system.$name = ValueGenerator::Fixed($name); 324 | self 325 | } 326 | 327 | pub fn $rangename(mut self, start: $typ, end: $typ) -> Self { 328 | self.system.$name = ValueGenerator::UniformRange(start, end); 329 | self 330 | } 331 | } 332 | } 333 | 334 | impl ParticleSystemBuilder { 335 | pub fn new(ctx: &mut Context) -> Self { 336 | let system = ParticleSystem::new(ctx); 337 | ParticleSystemBuilder { system: system } 338 | } 339 | pub fn build(self) -> ParticleSystem { 340 | self.system 341 | } 342 | 343 | /// Set maximum number of particles. 344 | pub fn count(mut self, count: usize) -> Self { 345 | self.system.max_particles = count; 346 | self.system.particles.reserve_exact(count); 347 | self 348 | } 349 | 350 | prop!(start_color, start_color_range, graphics::Color); 351 | prop!(start_size, start_size_range, f32); 352 | prop!(start_ang_vel, start_ang_vel_range, f32); 353 | // These two need some work, 'cause, shapes. 354 | prop!(start_position, start_position_range, Point2); 355 | prop!(start_velocity, start_velocity_range, Vector2); 356 | prop!(start_max_age, start_max_age_range, f32); 357 | 358 | pub fn acceleration(mut self, accel: Vector2) -> Self { 359 | self.system.acceleration = accel; 360 | self 361 | } 362 | 363 | // This also needs some variety in life. 364 | pub fn emission_rate(mut self, start: f32) -> Self { 365 | self.system.emission_rate = start; 366 | self 367 | } 368 | 369 | pub fn delta_size(mut self, trans: Transition) -> Self { 370 | self.system.delta_size = trans; 371 | self 372 | } 373 | 374 | pub fn delta_color(mut self, trans: Transition) -> Self { 375 | self.system.delta_color = trans; 376 | self 377 | } 378 | 379 | pub fn emission_shape(mut self, shape: EmissionShape) -> Self { 380 | self.system.start_shape = shape; 381 | self 382 | } 383 | } 384 | 385 | /// Defines where a new particle should be created. 386 | /// TODO: This basic idea should be used for both initial position 387 | /// and initial velocity... Uniform, direction, cone, line... 388 | pub enum EmissionShape { 389 | // Source point 390 | Point(Point2), 391 | // min and max bounds of the line segment. 392 | Line(Point2, Point2), 393 | // Center point and radius 394 | Circle(Point2, f32), 395 | } 396 | 397 | impl EmissionShape { 398 | /// Gets a random point that complies 399 | /// with the given shape. 400 | /// TODO: This is an ideal case for unit tests. 401 | fn get_random(&self) -> Point2 { 402 | match *self { 403 | EmissionShape::Point(v) => v, 404 | EmissionShape::Line(p1, p2) => { 405 | let min_x = f32::min(p1.x, p2.x); 406 | let max_x = f32::max(p1.x, p2.x); 407 | let min_y = f32::min(p1.y, p2.y); 408 | let max_y = f32::max(p1.y, p2.y); 409 | let mut rng = rand::thread_rng(); 410 | let x: f32; 411 | let y: f32; 412 | if min_x == max_x { 413 | // Line is vertical 414 | x = min_x; 415 | y = rng.gen_range(min_y..=max_y); 416 | } else if min_y == max_y { 417 | // Line is horizontal 418 | y = max_y; 419 | x = rng.gen_range(min_x..=max_x) 420 | } else { 421 | // Line is sloped. 422 | let dy = max_y - min_y; 423 | let dx = max_x - min_x; 424 | let slope = dy / dx; 425 | x = rng.gen_range(min_x..=max_x); 426 | y = (slope * (x - min_x)) + min_y; 427 | } 428 | 429 | // This is a bit sticky 'cause we have 430 | // to find the min and max x and y that are 431 | // within the given bounding box 432 | // let x_bbox_ymin = x_from_y(min.y); 433 | // let x_bbox_ymax = x_from_y(max.y); 434 | // let x_min = f32::max(min.x, f32::min(x_bbox_ymin, x_bbox_ymax)); 435 | // let x_max = f32::min(max.x, f32::max(x_bbox_ymin, x_bbox_ymax)); 436 | 437 | // let y_bbox_xmin = y_from_x(min.x); 438 | // let y_bbox_xmax = y_from_x(max.x); 439 | // let y_min = f32::max(min.y, f32::min(y_bbox_xmin, y_bbox_xmax)); 440 | // let y_max = f32::min(max.y, f32::max(y_bbox_xmin, y_bbox_xmax)); 441 | 442 | Point2 { x, y } 443 | } 444 | EmissionShape::Circle(center, radius) => { 445 | let mut rng = rand::thread_rng(); 446 | let theta = rng.gen_range(0.0..=f32::consts::PI * 2.0); 447 | let r = rng.gen_range(0.0..=radius); 448 | let x = theta.cos() * r; 449 | let y = theta.sin() * r; 450 | Point2 { 451 | x: x + center.x, 452 | y: y + center.y, 453 | } 454 | } 455 | } 456 | } 457 | } 458 | 459 | use std::cell::{Cell, RefCell}; 460 | 461 | pub struct ParticleSystem { 462 | // Bookkeeping stuff 463 | particles: Vec, 464 | residual_particle: f32, 465 | max_particles: usize, 466 | 467 | // Parameters: 468 | // Emission parameters 469 | emission_rate: f32, 470 | start_color: ValueGenerator, 471 | start_position: ValueGenerator>, 472 | start_shape: EmissionShape, 473 | start_velocity: ValueGenerator>, 474 | start_angle: ValueGenerator, 475 | start_ang_vel: ValueGenerator, 476 | start_size: ValueGenerator, 477 | start_max_age: ValueGenerator, 478 | // Global state/update parameters 479 | acceleration: Vector2, 480 | 481 | delta_size: Transition, 482 | delta_color: Transition, 483 | 484 | sprite_batch: RefCell, 485 | sprite_batch_dirty: Cell, 486 | } 487 | 488 | impl ParticleSystem { 489 | pub fn new(ctx: &mut Context) -> Self { 490 | let image = ParticleSystem::make_image(ctx, 5); 491 | let sprite_batch = SpriteBatch::new(image); 492 | ParticleSystem { 493 | particles: Vec::new(), 494 | max_particles: 0, 495 | acceleration: Vector2 { x: 0.0, y: 0.0 }, 496 | start_color: ValueGenerator::Fixed((255, 255, 255).into()), 497 | start_position: ValueGenerator::Fixed(Point2 { x: 0.0, y: 0.0 }), 498 | start_shape: EmissionShape::Point(Point2 { x: 0.0, y: 0.0 }), 499 | start_velocity: ValueGenerator::Fixed(Vector2 { x: 1.0, y: 1.0 }), 500 | start_angle: ValueGenerator::Fixed(0.0), 501 | start_ang_vel: ValueGenerator::Fixed(0.0), 502 | start_size: ValueGenerator::Fixed(1.0), 503 | start_max_age: ValueGenerator::Fixed(1.0), 504 | emission_rate: 1.0, 505 | residual_particle: 0.0, 506 | 507 | delta_size: Transition::fixed(1.0), 508 | delta_color: Transition::fixed((255, 255, 255).into()), 509 | 510 | sprite_batch: RefCell::new(sprite_batch), 511 | sprite_batch_dirty: Cell::new(true), 512 | } 513 | } 514 | 515 | /// Makes a basic square image to represent a particle 516 | /// if we need one. 517 | fn make_image(ctx: &mut Context, size: u16) -> graphics::Image { 518 | graphics::Image::solid(ctx, size, graphics::Color::from((255, 255, 255, 255))).unwrap() 519 | } 520 | 521 | /// Number of living particles. 522 | pub fn count(&self) -> usize { 523 | return self.particles.len(); 524 | } 525 | 526 | pub fn emit_one(&mut self) { 527 | let pos = self.start_shape.get_random(); 528 | let vec = self.start_velocity.get_value(); 529 | let col = self.start_color.get_value(); 530 | let size = self.start_size.get_value(); 531 | let max_age = self.start_max_age.get_value(); 532 | let angle = self.start_angle.get_value(); 533 | let ang_vel = self.start_ang_vel.get_value(); 534 | let mut newparticle = Particle::new(pos, vec, col, size, angle, max_age); 535 | newparticle.ang_vel = ang_vel; 536 | if self.particles.len() <= self.max_particles { 537 | self.particles.push(newparticle); 538 | } 539 | } 540 | 541 | pub fn update(&mut self, dt: f32) { 542 | // This is tricky 'cause we have to keep the emission rate 543 | // correct and constant. So we "accumulate" particles over 544 | // time until we have >1 of them and then emit it. 545 | let num_to_emit = self.emission_rate * dt + self.residual_particle; 546 | let actual_num_to_emit = num_to_emit.trunc() as usize; 547 | self.residual_particle = num_to_emit.fract(); 548 | for _ in 0..actual_num_to_emit { 549 | self.emit_one() 550 | } 551 | for mut p in self.particles.iter_mut() { 552 | let life_fraction = p.age / p.max_age; 553 | p.vel.x += self.acceleration.x * dt; 554 | p.vel.y += self.acceleration.y * dt; 555 | p.pos.x += p.vel.x * dt; 556 | p.pos.y += p.vel.y * dt; 557 | p.age += dt; 558 | p.angle += p.ang_vel; 559 | 560 | p.size = self.delta_size.get(life_fraction); 561 | p.color = self.delta_color.get(life_fraction); 562 | } 563 | 564 | self.particles.retain(|p| p.age < p.max_age); 565 | self.sprite_batch_dirty.set(true); 566 | } 567 | } 568 | 569 | impl graphics::Drawable for ParticleSystem { 570 | fn draw(&self, context: &mut Context, param: graphics::DrawParam) -> GameResult<()> { 571 | // Check whether an update has been processed since our last draw call. 572 | if self.sprite_batch_dirty.get() { 573 | use std::ops::DerefMut; 574 | let mut sb_ref = self.sprite_batch.borrow_mut(); 575 | let sb = sb_ref.deref_mut(); 576 | sb.clear(); 577 | for particle in &self.particles { 578 | let drawparam = graphics::DrawParam { 579 | dest: particle.pos, 580 | rotation: particle.angle, 581 | scale: Vector2 { 582 | x: particle.size, 583 | y: particle.size, 584 | }, 585 | offset: Point2 { x: 0.5, y: 0.5 }, 586 | color: particle.color, 587 | ..Default::default() 588 | }; 589 | sb.add(drawparam); 590 | } 591 | self.sprite_batch_dirty.set(false); 592 | } 593 | 594 | self.sprite_batch.borrow().draw(context, param)?; 595 | Ok(()) 596 | } 597 | 598 | fn blend_mode(&self) -> Option { 599 | self.sprite_batch.borrow().blend_mode() 600 | } 601 | 602 | fn set_blend_mode(&mut self, mode: Option) { 603 | self.sprite_batch.borrow_mut().set_blend_mode(mode) 604 | } 605 | 606 | fn dimensions(&self, _ctx: &mut Context) -> Option { 607 | if self.particles.is_empty() { 608 | None 609 | } else { 610 | let mut x = f32::MAX; 611 | let mut y = f32::MAX; 612 | let mut size = f32::MIN; 613 | 614 | for particle in &self.particles { 615 | if particle.pos.x < x { 616 | x = particle.pos.x; 617 | } 618 | if particle.pos.y < y { 619 | y = particle.pos.y; 620 | } 621 | if particle.size > size { 622 | size = particle.size; 623 | } 624 | } 625 | 626 | Some(graphics::Rect::new(x, y, size, size)) 627 | } 628 | } 629 | } 630 | -------------------------------------------------------------------------------- /ggez-goodies/src/scene.rs: -------------------------------------------------------------------------------- 1 | //! The Scene system is basically for transitioning between 2 | //! *completely* different states that have entirely different game 3 | //! loops and but which all share a state. It operates as a stack, with new 4 | //! scenes getting pushed to the stack (while the old ones stay in 5 | //! memory unchanged). Apparently this is basically a push-down automata. 6 | //! 7 | //! Also there's no reason you can't have a Scene contain its own 8 | //! Scene subsystem to do its own indirection. With a different state 9 | //! type, as well! What fun! Though whether you want to go that deep 10 | //! down the rabbit-hole is up to you. I haven't found it necessary 11 | //! yet. 12 | //! 13 | //! This is basically identical in concept to the Amethyst engine's scene 14 | //! system, the only difference is the details of how the pieces are put 15 | //! together. 16 | 17 | use ggez; 18 | 19 | /// A command to change to a new scene, either by pushign a new one, 20 | /// popping one or replacing the current scene (pop and then push). 21 | pub enum SceneSwitch { 22 | None, 23 | Push(Box>), 24 | Replace(Box>), 25 | Pop, 26 | } 27 | 28 | /// A trait for you to implement on a scene. 29 | /// Defines the callbacks the scene uses: 30 | /// a common context type `C`, and an input event type `Ev`. 31 | pub trait Scene { 32 | fn update(&mut self, gameworld: &mut C, ctx: &mut ggez::Context) -> SceneSwitch; 33 | fn draw(&mut self, gameworld: &mut C, ctx: &mut ggez::Context) -> ggez::GameResult<()>; 34 | fn input(&mut self, gameworld: &mut C, event: Ev, started: bool); 35 | /// Only used for human-readable convenience (or not at all, tbh) 36 | fn name(&self) -> &str; 37 | /// This returns whether or not to draw the next scene down on the 38 | /// stack as well; this is useful for layers or GUI stuff that 39 | /// only partially covers the screen. 40 | fn draw_previous(&self) -> bool { 41 | false 42 | } 43 | } 44 | 45 | impl SceneSwitch { 46 | /// Convenient shortcut function for boxing scenes. 47 | /// 48 | /// Slightly nicer than writing 49 | /// `SceneSwitch::Replace(Box::new(x))` all the damn time. 50 | pub fn replace(scene: S) -> Self 51 | where 52 | S: Scene + 'static, 53 | { 54 | SceneSwitch::Replace(Box::new(scene)) 55 | } 56 | 57 | /// Same as `replace()` but returns SceneSwitch::Push 58 | pub fn push(scene: S) -> Self 59 | where 60 | S: Scene + 'static, 61 | { 62 | SceneSwitch::Push(Box::new(scene)) 63 | } 64 | } 65 | 66 | /// A stack of `Scene`'s, together with a context object. 67 | pub struct SceneStack { 68 | pub world: C, 69 | scenes: Vec>>, 70 | } 71 | 72 | impl SceneStack { 73 | pub fn new(_ctx: &mut ggez::Context, global_state: C) -> Self { 74 | Self { 75 | world: global_state, 76 | scenes: Vec::new(), 77 | } 78 | } 79 | 80 | /// Add a new scene to the top of the stack. 81 | pub fn push(&mut self, scene: Box>) { 82 | self.scenes.push(scene) 83 | } 84 | 85 | /// Remove the top scene from the stack and returns it; 86 | /// panics if there is none. 87 | pub fn pop(&mut self) -> Box> { 88 | self.scenes 89 | .pop() 90 | .expect("ERROR: Popped an empty scene stack.") 91 | } 92 | 93 | /// Returns the current scene; panics if there is none. 94 | pub fn current(&self) -> &Scene { 95 | &**self 96 | .scenes 97 | .last() 98 | .expect("ERROR: Tried to get current scene of an empty scene stack.") 99 | } 100 | 101 | /// Executes the given SceneSwitch command; if it is a pop or replace 102 | /// it returns `Some(old_scene)`, otherwise `None` 103 | pub fn switch(&mut self, next_scene: SceneSwitch) -> Option>> { 104 | match next_scene { 105 | SceneSwitch::None => None, 106 | SceneSwitch::Pop => { 107 | let s = self.pop(); 108 | Some(s) 109 | } 110 | SceneSwitch::Push(s) => { 111 | self.push(s); 112 | None 113 | } 114 | SceneSwitch::Replace(s) => { 115 | let old_scene = self.pop(); 116 | self.push(s); 117 | Some(old_scene) 118 | } 119 | } 120 | } 121 | 122 | // These functions must be on the SceneStack because otherwise 123 | // if you try to get the current scene and the world to call 124 | // update() on the current scene it causes a double-borrow. :/ 125 | pub fn update(&mut self, ctx: &mut ggez::Context) { 126 | let next_scene = { 127 | let current_scene = &mut **self 128 | .scenes 129 | .last_mut() 130 | .expect("Tried to update empty scene stack"); 131 | current_scene.update(&mut self.world, ctx) 132 | }; 133 | self.switch(next_scene); 134 | } 135 | 136 | /// We walk down the scene stack until we find a scene where we aren't 137 | /// supposed to draw the previous one, then draw them from the bottom up. 138 | /// 139 | /// This allows for layering GUI's and such. 140 | fn draw_scenes(scenes: &mut [Box>], world: &mut C, ctx: &mut ggez::Context) { 141 | assert!(scenes.len() > 0); 142 | if let Some((current, rest)) = scenes.split_last_mut() { 143 | if current.draw_previous() { 144 | SceneStack::draw_scenes(rest, world, ctx); 145 | } 146 | current 147 | .draw(world, ctx) 148 | .expect("I would hope drawing a scene never fails!"); 149 | } 150 | } 151 | 152 | /// Draw the current scene. 153 | pub fn draw(&mut self, ctx: &mut ggez::Context) { 154 | SceneStack::draw_scenes(&mut self.scenes, &mut self.world, ctx) 155 | } 156 | 157 | /// Feeds the given input event to the current scene. 158 | pub fn input(&mut self, event: Ev, started: bool) { 159 | let current_scene = &mut **self 160 | .scenes 161 | .last_mut() 162 | .expect("Tried to do input for empty scene stack"); 163 | current_scene.input(&mut self.world, event, started); 164 | } 165 | } 166 | 167 | #[cfg(test)] 168 | mod tests { 169 | use super::*; 170 | 171 | struct Thing { 172 | scenes: Vec>, 173 | } 174 | 175 | #[test] 176 | fn test1() { 177 | let x = Thing { scenes: vec![] }; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /misc/dead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/misc/dead.png -------------------------------------------------------------------------------- /misc/gameplay-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/misc/gameplay-1.png -------------------------------------------------------------------------------- /misc/gameplay-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/misc/gameplay-2.png -------------------------------------------------------------------------------- /misc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/misc/logo.png -------------------------------------------------------------------------------- /misc/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/misc/menu.png -------------------------------------------------------------------------------- /misc/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/misc/thumbnail.png -------------------------------------------------------------------------------- /resources/audio/Some(explode).mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/audio/Some(explode).mp3 -------------------------------------------------------------------------------- /resources/audio/Some(turbofish_shoot).mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/audio/Some(turbofish_shoot).mp3 -------------------------------------------------------------------------------- /resources/audio/dead.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/audio/dead.mp3 -------------------------------------------------------------------------------- /resources/fonts/Consolas.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/fonts/Consolas.ttf -------------------------------------------------------------------------------- /resources/images/Some(ammo).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/Some(ammo).png -------------------------------------------------------------------------------- /resources/images/Some(barrel).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/Some(barrel).png -------------------------------------------------------------------------------- /resources/images/Some(cloud).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/Some(cloud).png -------------------------------------------------------------------------------- /resources/images/Some(ferris).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/Some(ferris).png -------------------------------------------------------------------------------- /resources/images/Some(fish).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/Some(fish).png -------------------------------------------------------------------------------- /resources/images/Some(gun).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/Some(gun).png -------------------------------------------------------------------------------- /resources/images/Some(nil).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/Some(nil).png -------------------------------------------------------------------------------- /resources/images/Some(profile).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/Some(profile).png -------------------------------------------------------------------------------- /resources/images/Some(sniper).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/Some(sniper).png -------------------------------------------------------------------------------- /resources/images/Some(turbofish).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/Some(turbofish).png -------------------------------------------------------------------------------- /resources/images/ferris_ninja.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/ferris_ninja.png -------------------------------------------------------------------------------- /resources/images/ferris_pacman_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/ferris_pacman_1.png -------------------------------------------------------------------------------- /resources/images/ferris_pacman_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/ferris_pacman_2.png -------------------------------------------------------------------------------- /resources/images/ferris_planet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/ferris_planet.png -------------------------------------------------------------------------------- /resources/images/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/gopher.png -------------------------------------------------------------------------------- /resources/images/ground_centre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/ground_centre.png -------------------------------------------------------------------------------- /resources/images/ground_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/ground_left.png -------------------------------------------------------------------------------- /resources/images/ground_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/ground_right.png -------------------------------------------------------------------------------- /resources/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/logo.png -------------------------------------------------------------------------------- /resources/images/menu_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andy-Python-Programmer/CallOfFerris/8a2b4ca4a914feaf856adf67dfaf828ea4a69d9e/resources/images/menu_bg.png -------------------------------------------------------------------------------- /resources/maps/01.map: -------------------------------------------------------------------------------- 1 | .comment Define all of our variables 2 | 3 | .end We **rustaceans** love all animals and we do not want to disappoint them like the gophers. \nWe also have animals in our language too like Cow<>. \nWe just love the correct animals ⌐■_■ 4 | .using_weapon Turbofish Gun 5 | 6 | .comment The map 7 | [-4------]_[--8---8---*-]_[--------------8] -------------------------------------------------------------------------------- /resources/shaders/dim.basic.glslf: -------------------------------------------------------------------------------- 1 | #version 150 core 2 | 3 | in vec2 a_Pos; 4 | in vec2 a_Uv; 5 | 6 | in vec4 a_Src; 7 | in vec4 a_TCol1; 8 | in vec4 a_TCol2; 9 | in vec4 a_TCol3; 10 | in vec4 a_TCol4; 11 | in vec4 a_Color; 12 | 13 | layout (std140) uniform Globals { 14 | mat4 u_MVP; 15 | }; 16 | 17 | out vec2 v_Uv; 18 | out vec4 v_Color; 19 | 20 | void main() { 21 | v_Uv = a_Uv * a_Src.zw + a_Src.xy; 22 | v_Color = a_Color; 23 | mat4 instance_transform = mat4(a_TCol1, a_TCol2, a_TCol3, a_TCol4); 24 | vec4 position = instance_transform * vec4(a_Pos, 0.0, 1.0); 25 | 26 | gl_Position = u_MVP * position; 27 | } -------------------------------------------------------------------------------- /resources/shaders/dim.glslf: -------------------------------------------------------------------------------- 1 | #version 150 core 2 | 3 | uniform sampler2D t_Texture; 4 | in vec2 v_Uv; 5 | in vec4 v_Color; 6 | out vec4 Target0; 7 | 8 | layout (std140) uniform Globals { 9 | mat4 u_MVP; 10 | }; 11 | 12 | layout (std140) uniform Dim { 13 | float u_Rate; 14 | }; 15 | 16 | void main() { 17 | Target0 = texture(t_Texture, v_Uv) * v_Color * u_Rate; 18 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! # Call of Ferris 2 | //! 3 | //! Call of Ferris is a thrilling action game where your favorite Ferris the crab and the rust mascot got guns and has taken up the duty to find evildoer languages while managing to keep itself alive. 4 | //! Take part in this awesome adventure and help Ferris be the best ever! 5 | //! 6 | //! For a fuller outline, see the project's [README.md](https://github.com/Andy-Python-Programmer/CallOfFerris) 7 | 8 | use std::{fs, rc::Rc, sync::Mutex}; 9 | 10 | use ggez::{ 11 | conf::WindowMode, 12 | event::KeyCode, 13 | event::KeyMods, 14 | graphics::{set_screen_coordinates, Rect}, 15 | Context, ContextBuilder, GameResult, 16 | }; 17 | use ggez::{ 18 | conf::WindowSetup, 19 | event::{self, EventHandler}, 20 | }; 21 | use utils::{AssetManager, FerrisResult}; 22 | 23 | mod screens; 24 | mod utils; 25 | 26 | pub use screens::*; 27 | 28 | /// Initial window width. 29 | const INIT_WIDTH: f32 = 1000.0; 30 | /// Initial window height. 31 | const INIT_HEIGHT: f32 = 600.0; 32 | 33 | /// Minimum width. 34 | const MIN_WIDTH: f32 = 1000.0; 35 | /// Minimum height. 36 | const MIN_HEIGHT: f32 = 600.0; 37 | 38 | fn init_assets(ctx: &mut Context) -> FerrisResult { 39 | let mut asset_manager = AssetManager::new(); 40 | 41 | let images_dir = fs::read_dir("./resources/images/")?; 42 | let fonts_dir = fs::read_dir("./resources/fonts/")?; 43 | let audio_dir = fs::read_dir("./resources/audio/")?; 44 | let maps_dir = fs::read_dir("./resources/maps/")?; 45 | 46 | for image in images_dir { 47 | asset_manager.load_image(ctx, image?.file_name().to_string_lossy()); 48 | } 49 | 50 | for font in fonts_dir { 51 | asset_manager.load_font(ctx, font?.file_name().to_string_lossy()); 52 | } 53 | 54 | for audio in audio_dir { 55 | asset_manager.load_sound(ctx, audio?.file_name().to_string_lossy()); 56 | } 57 | 58 | for map in maps_dir { 59 | asset_manager.load_file(ctx, "maps", map?.file_name().to_string_lossy()); 60 | } 61 | 62 | Ok(asset_manager) 63 | } 64 | 65 | fn main() -> FerrisResult<()> { 66 | // The resources directory contains all of the assets. 67 | // Including sprites and audio files. 68 | let resource_dir = std::path::PathBuf::from("./resources"); 69 | 70 | // Make a Context and an EventLoop. 71 | let (mut ctx, mut event_loop) = ContextBuilder::new("Call of Ferris", "Borrow Checker") 72 | .add_resource_path(resource_dir) 73 | .window_mode( 74 | WindowMode::default() 75 | .dimensions(INIT_WIDTH, INIT_HEIGHT) 76 | .resizable(true) 77 | .min_dimensions(MIN_WIDTH, MIN_HEIGHT), 78 | ) 79 | .window_setup( 80 | WindowSetup::default() 81 | .title("Call of Ferris") 82 | .icon("/images/ferris_pacman_1.png"), 83 | ) 84 | .build()?; 85 | 86 | let asset_manager = init_assets(&mut ctx)?; 87 | 88 | // Create an instance of your event handler. 89 | let mut game = Game::new(&mut ctx, asset_manager); 90 | 91 | // Run! 92 | let exit = event::run(&mut ctx, &mut event_loop, &mut game); 93 | 94 | if exit.is_err() { 95 | let error_message = format!( 96 | "Call of Ferris encountered an unexpected internal error: {:?}", 97 | exit, 98 | ); 99 | 100 | Err(error_message.into()) 101 | } else { 102 | Ok(()) 103 | } 104 | } 105 | 106 | /// A enum specifying the current screen to show. 107 | pub enum Screen { 108 | /// The menu screen. 109 | Menu, 110 | /// The game screen. 111 | Play, 112 | /// The death screen. 113 | Dead, 114 | } 115 | 116 | /// The current game state. 117 | pub struct Game { 118 | /// The current screen, 119 | screen: Screen, 120 | /// Reference of the menu screen. 121 | menu_screen: menu::Menu, 122 | /// Mutable reference of the game screen. 123 | game_screen: Mutex, 124 | /// Reference of the death screen. 125 | death_screen: dead::Death, 126 | /// The asset manager. 127 | asset_manager: Rc, 128 | } 129 | 130 | impl Game { 131 | pub fn new(ctx: &mut Context, asset_manager: AssetManager) -> Self { 132 | let asset_manager = Rc::new(asset_manager); 133 | 134 | // Woah. We are cloning the asset manager. Yes that's why it's wrapped in Rc<> 135 | // Anything wrapped in a Rc<> and performs a clone it only clones its pointer, so it's fine to use clone here! 136 | Self { 137 | screen: Screen::Menu, 138 | 139 | menu_screen: menu::Menu::create(ctx, asset_manager.clone()), 140 | game_screen: game::Game::create(ctx, asset_manager.clone()), 141 | death_screen: dead::Death::spawn(ctx, asset_manager.clone()), 142 | 143 | asset_manager, 144 | } 145 | } 146 | } 147 | 148 | impl EventHandler for Game { 149 | fn update(&mut self, ctx: &mut Context) -> GameResult<()> { 150 | while ggez::timer::check_update_time(ctx, 60) { 151 | match self.screen { 152 | Screen::Menu => self.menu_screen.update(ctx)?, 153 | Screen::Play => { 154 | let change = self.game_screen.lock().unwrap().update(ctx)?; 155 | 156 | if let Some(s) = change { 157 | self.screen = s; 158 | } 159 | } 160 | Screen::Dead => self.death_screen.update(ctx)?, 161 | } 162 | } 163 | 164 | Ok(()) 165 | } 166 | 167 | fn draw(&mut self, ctx: &mut Context) -> GameResult<()> { 168 | match self.screen { 169 | Screen::Menu => self.menu_screen.draw(ctx), 170 | Screen::Play => { 171 | let change = self.game_screen.lock().unwrap().draw(ctx)?; 172 | 173 | if let Some(s) = change { 174 | self.screen = s; 175 | } 176 | 177 | Ok(()) 178 | } 179 | Screen::Dead => self.death_screen.draw(ctx), 180 | } 181 | } 182 | 183 | fn key_down_event( 184 | &mut self, 185 | ctx: &mut Context, 186 | keycode: KeyCode, 187 | _keymod: KeyMods, 188 | _repeat: bool, 189 | ) { 190 | match self.screen { 191 | Screen::Menu => { 192 | let change = self.menu_screen.key_press(keycode); 193 | 194 | if let Some(s) = change { 195 | self.screen = s; 196 | } 197 | } 198 | Screen::Play => { 199 | let change = self.game_screen.lock().unwrap().key_press(keycode); 200 | 201 | if let Some(s) = change { 202 | if let Screen::Menu = s { 203 | self.game_screen = game::Game::create(ctx, self.asset_manager.clone()); 204 | } 205 | self.screen = s; 206 | } 207 | } 208 | Screen::Dead => {} 209 | } 210 | } 211 | 212 | fn key_up_event(&mut self, _ctx: &mut Context, keycode: KeyCode, _keymods: KeyMods) { 213 | if let Screen::Play = self.screen { 214 | self.game_screen.lock().unwrap().key_up_event(keycode) 215 | } 216 | } 217 | 218 | fn resize_event(&mut self, ctx: &mut Context, width: f32, height: f32) { 219 | set_screen_coordinates(ctx, Rect::new(0.0, 0.0, width, height)).unwrap(); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/screens/dead/dead.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use ggez::{ 4 | graphics::Color, 5 | graphics::{self, Scale, Text, TextFragment}, 6 | nalgebra::Point2, 7 | Context, GameResult, 8 | }; 9 | use graphics::DrawParam; 10 | 11 | use crate::utils::AssetManager; 12 | 13 | pub struct Death { 14 | asset_manager: Rc, 15 | } 16 | 17 | impl Death { 18 | pub fn spawn(_ctx: &mut Context, asset_manager: Rc) -> Self { 19 | Self { asset_manager } 20 | } 21 | 22 | pub fn draw(&mut self, ctx: &mut Context) -> GameResult<()> { 23 | let (width, _) = graphics::drawable_size(ctx); 24 | 25 | graphics::clear(ctx, graphics::BLACK); 26 | 27 | let consolas = self.asset_manager.get_font("Consolas.ttf"); 28 | let ferris_planet = self.asset_manager.get_image("ferris_planet.png"); 29 | 30 | let dead = Text::new( 31 | TextFragment::new("YOU DEAD") 32 | .scale(Scale::uniform(35.0)) 33 | .font(consolas) 34 | .color(Color::from_rgb(255, 80, 76)), 35 | ); 36 | 37 | let unsafe_dead = Text::new( 38 | TextFragment::new("unsafe") 39 | .scale(Scale::uniform(30.0)) 40 | .font(consolas) 41 | .color(Color::from_rgb(74, 129, 191)), 42 | ); 43 | 44 | let unsafe_dead_block_start = Text::new( 45 | TextFragment::new("{") 46 | .scale(Scale::uniform(30.0)) 47 | .font(consolas) 48 | .color(Color::from_rgb(255, 255, 255)), 49 | ); 50 | 51 | let unsafe_dead_block_func = Text::new( 52 | TextFragment::new("dead()") 53 | .scale(Scale::uniform(30.0)) 54 | .font(consolas) 55 | .color(Color::from_rgb(214, 208, 132)), 56 | ); 57 | 58 | let unsafe_dead_block_end = Text::new( 59 | TextFragment::new("}") 60 | .scale(Scale::uniform(30.0)) 61 | .font(consolas) 62 | .color(Color::from_rgb(255, 255, 255)), 63 | ); 64 | 65 | graphics::draw( 66 | ctx, 67 | &dead, 68 | DrawParam::default().dest(Point2::new((width / 2.0) - 60.0, 40.0)), 69 | )?; 70 | 71 | graphics::draw( 72 | ctx, 73 | &unsafe_dead, 74 | DrawParam::default().dest(Point2::new((width / 2.0) - 200.0, 200.0)), 75 | )?; 76 | 77 | graphics::draw( 78 | ctx, 79 | &unsafe_dead_block_start, 80 | DrawParam::default().dest(Point2::new((width / 2.0) - 90.0, 200.0)), 81 | )?; 82 | 83 | graphics::draw( 84 | ctx, 85 | &unsafe_dead_block_func, 86 | DrawParam::default().dest(Point2::new((width / 2.0) - 125.0, 260.0)), 87 | )?; 88 | 89 | graphics::draw( 90 | ctx, 91 | &unsafe_dead_block_end, 92 | DrawParam::default().dest(Point2::new((width / 2.0) - 200.0, 300.0)), 93 | )?; 94 | 95 | graphics::draw( 96 | ctx, 97 | &ferris_planet, 98 | DrawParam::default().dest(Point2::new((width / 2.0) - 10.0, 240.0)), 99 | )?; 100 | 101 | graphics::present(ctx) 102 | } 103 | 104 | pub fn update(&self, _ctx: &mut Context) -> GameResult { 105 | Ok(()) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/screens/dead/mod.rs: -------------------------------------------------------------------------------- 1 | mod dead; 2 | 3 | pub use dead::*; 4 | -------------------------------------------------------------------------------- /src/screens/game/components/barrel.rs: -------------------------------------------------------------------------------- 1 | use ggez::{audio::SoundSource, graphics, nalgebra::Point2, Context, GameResult}; 2 | use ggez_goodies::{camera::Camera, nalgebra_glm::Vec2}; 3 | use graphics::DrawParam; 4 | 5 | use crate::{ 6 | game::physics::{isometry_to_point, Physics}, 7 | play, 8 | utils::{AssetManager, ParticleSystem}, 9 | }; 10 | 11 | use nphysics2d::{nalgebra as na, object::DefaultBodyHandle}; 12 | 13 | use super::{bullet::PlayerWeapon, player::Player}; 14 | 15 | pub struct Barrel { 16 | body: DefaultBodyHandle, 17 | } 18 | 19 | impl Barrel { 20 | pub fn new( 21 | ctx: &mut Context, 22 | pos_x: f32, 23 | physics: &mut Physics, 24 | asset_manager: &AssetManager, 25 | ) -> Self { 26 | let (_, height) = graphics::drawable_size(ctx); 27 | 28 | let barrel = asset_manager.get_image("Some(barrel).png"); 29 | 30 | let body = physics.create_barrel( 31 | na::Point2::new(pos_x, height / 2.0 - 155.0), 32 | barrel.width(), 33 | barrel.height(), 34 | ); 35 | 36 | Self { body } 37 | } 38 | 39 | pub fn draw( 40 | &mut self, 41 | ctx: &mut Context, 42 | camera: &Camera, 43 | physics: &mut Physics, 44 | asset_manager: &AssetManager, 45 | ) -> GameResult<()> { 46 | let barrel = asset_manager.get_image("Some(barrel).png"); 47 | 48 | let barrel_position = self.position(physics); 49 | let barrel_pos_camera = 50 | camera.calculate_dest_point(Vec2::new(barrel_position.x, barrel_position.y)); 51 | 52 | graphics::draw( 53 | ctx, 54 | &barrel, 55 | DrawParam::default() 56 | .dest(Point2::new(barrel_pos_camera.x, barrel_pos_camera.y)) 57 | .offset(Point2::new(0.5, 0.5)), 58 | )?; 59 | 60 | Ok(()) 61 | } 62 | 63 | pub fn update( 64 | &mut self, 65 | physics: &mut Physics, 66 | asset_manager: &AssetManager, 67 | particles: &mut Vec, 68 | player: &mut Player, 69 | ) -> bool { 70 | let barrel = asset_manager.get_image("Some(barrel).png"); 71 | 72 | let position = self.position(physics); 73 | 74 | for i in 0..player.weapons.len() { 75 | match &mut player.weapons[i] { 76 | PlayerWeapon::Turbofish(fish) => { 77 | if fish.is_touching(physics, self.handle()) { 78 | let explode_sound = asset_manager.get_sound("Some(explode).mp3"); 79 | 80 | // FIXME 81 | particles.push(ParticleSystem::new( 82 | physics, 83 | 100, 84 | na::Point2::new( 85 | position.x - (barrel.width() / 2) as f32, 86 | position.y - (barrel.height() / 2) as f32, 87 | ), 88 | na::Point2::new( 89 | position.x + (barrel.width() / 2) as f32, 90 | position.y + (barrel.height() / 2) as f32, 91 | ), 92 | )); 93 | 94 | play!(explode_sound); 95 | 96 | // Remove the enemy from the world 97 | self.destroy(physics); 98 | 99 | // Remove the weapon from the world 100 | fish.destroy(physics); 101 | player.weapons.remove(i); 102 | 103 | return true; 104 | } 105 | } 106 | PlayerWeapon::Grappling(_) => {} 107 | } 108 | } 109 | 110 | false 111 | } 112 | 113 | pub fn position(&self, physics: &mut Physics) -> na::Point2 { 114 | let barrel_body = physics.get_rigid_body_mut(self.body); 115 | let barrel_position = isometry_to_point(barrel_body.position()); 116 | 117 | barrel_position 118 | } 119 | 120 | pub fn handle(&self) -> DefaultBodyHandle { 121 | self.body 122 | } 123 | 124 | pub fn destroy(&self, physics: &mut Physics) { 125 | physics.destroy_body(self.body); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/screens/game/components/bullet.rs: -------------------------------------------------------------------------------- 1 | use ggez::{ 2 | graphics::{self, DrawParam}, 3 | nalgebra::Point2, 4 | Context, GameResult, 5 | }; 6 | use ggez_goodies::{camera::Camera, nalgebra_glm::Vec2}; 7 | 8 | use nphysics2d::{algebra::Velocity2, math::Velocity, nalgebra as na, object::DefaultBodyHandle}; 9 | 10 | use crate::{ 11 | game::physics::{isometry_to_point, ObjectData, Physics}, 12 | utils::AssetManager, 13 | }; 14 | 15 | pub enum PlayerWeapon { 16 | Turbofish(Turbofish), 17 | Grappling(Grappling), 18 | } 19 | 20 | pub enum WeaponType { 21 | Turbofish, 22 | Grappling, 23 | } 24 | 25 | pub struct Turbofish { 26 | body: DefaultBodyHandle, 27 | } 28 | 29 | impl Turbofish { 30 | pub fn new( 31 | pos_x: f32, 32 | pos_y: f32, 33 | physics: &mut Physics, 34 | asset_manager: &AssetManager, 35 | ) -> Self { 36 | let turbofish_bullet = asset_manager.get_image("Some(turbofish).png"); 37 | let body = physics.create_bullet( 38 | na::Point2::new(pos_x, pos_y), 39 | turbofish_bullet.width(), 40 | turbofish_bullet.height(), 41 | ); 42 | 43 | let bullet_body = physics.get_rigid_body_mut(body); 44 | bullet_body.set_velocity(Velocity2::linear(1000.0, 0.0)); 45 | 46 | Self { body } 47 | } 48 | 49 | pub fn draw( 50 | &mut self, 51 | ctx: &mut Context, 52 | camera: &Camera, 53 | physics: &mut Physics, 54 | asset_manager: &AssetManager, 55 | ) -> GameResult<()> { 56 | let turbofish_bullet = asset_manager.get_image("Some(turbofish).png"); 57 | 58 | let bullet_position = self.position(physics); 59 | let turbofish_position = 60 | camera.calculate_dest_point(Vec2::new(bullet_position.x, bullet_position.y)); 61 | 62 | graphics::draw( 63 | ctx, 64 | &turbofish_bullet, 65 | DrawParam::default() 66 | .dest(Point2::new(turbofish_position.x, turbofish_position.y)) 67 | .offset(Point2::new(0.5, 0.5)), 68 | )?; 69 | 70 | Ok(()) 71 | } 72 | 73 | pub fn update(&mut self, physics: &mut Physics) -> bool { 74 | for collision in physics.collisions(self.body) { 75 | if collision.0 .1 == ObjectData::Ground { 76 | return true; 77 | } 78 | } 79 | 80 | false 81 | } 82 | 83 | pub fn is_touching(&mut self, physics: &mut Physics, handle: DefaultBodyHandle) -> bool { 84 | for collision in physics.collisions(self.body) { 85 | if collision.1 == handle { 86 | return true; 87 | } 88 | } 89 | 90 | false 91 | } 92 | 93 | pub fn destroy(&mut self, physics: &mut Physics) { 94 | physics.destroy_body(self.body); 95 | } 96 | 97 | pub fn position(&self, physics: &mut Physics) -> na::Point2 { 98 | let bullet_body = physics.get_rigid_body_mut(self.body); 99 | let bullet_position = isometry_to_point(bullet_body.position()); 100 | 101 | bullet_position 102 | } 103 | } 104 | 105 | pub struct Grappling { 106 | grapple_to: DefaultBodyHandle, 107 | player_body: DefaultBodyHandle, 108 | } 109 | 110 | impl Grappling { 111 | pub fn new( 112 | pos_x: f32, 113 | pos_y: f32, 114 | physics: &mut Physics, 115 | handle: DefaultBodyHandle, 116 | ) -> Option { 117 | let ray_cast = physics.ray_cast(na::Point2::new(pos_x, pos_y), na::Vector2::new(1.0, 1.0)); 118 | 119 | if !ray_cast.is_empty() { 120 | for object in ray_cast { 121 | if object.0 == ObjectData::Barrel { 122 | let body = object.1.body(); 123 | let body_pos = isometry_to_point(physics.get_rigid_body(body).position()); 124 | 125 | physics 126 | .get_rigid_body_mut(body) 127 | .set_velocity(Velocity::linear(pos_x - body_pos.x, pos_y - body_pos.y)); 128 | 129 | return Some(Self { 130 | grapple_to: body, 131 | player_body: handle, 132 | }); 133 | } 134 | } 135 | 136 | None 137 | } else { 138 | None 139 | } 140 | } 141 | 142 | pub fn draw( 143 | &mut self, 144 | ctx: &mut Context, 145 | camera: &Camera, 146 | physics: &mut Physics, 147 | ) -> GameResult<()> { 148 | let player = isometry_to_point(physics.get_rigid_body(self.player_body).position()); 149 | 150 | let rect = graphics::Mesh::new_rectangle( 151 | ctx, 152 | graphics::DrawMode::fill(), 153 | graphics::Rect::new( 154 | 0.0, 155 | 0.0, 156 | physics.distance(self.player_body, self.grapple_to), 157 | 10.0, 158 | ), 159 | [1.0, 1.0, 1.0, 1.0].into(), 160 | )?; 161 | 162 | let pos = camera.calculate_dest_point(Vec2::new(player.x + 140.0, player.y)); 163 | 164 | graphics::draw( 165 | ctx, 166 | &rect, 167 | DrawParam::default().dest(Point2::new(pos.x, pos.y)), 168 | )?; 169 | 170 | Ok(()) 171 | } 172 | 173 | pub fn update(&mut self, physics: &mut Physics) { 174 | let player = isometry_to_point(physics.get_rigid_body(self.player_body).position()); 175 | let object = isometry_to_point(physics.get_rigid_body(self.grapple_to).position()); 176 | 177 | if physics.distance(self.player_body, self.grapple_to) as i32 > 1 { 178 | physics 179 | .get_rigid_body_mut(self.grapple_to) 180 | .set_velocity(Velocity::linear(player.x - object.x, player.y - object.y)); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/screens/game/components/cloud.rs: -------------------------------------------------------------------------------- 1 | use ggez::{ 2 | graphics, 3 | mint::{Point2, Vector2}, 4 | Context, GameResult, 5 | }; 6 | use graphics::DrawParam; 7 | 8 | use crate::utils::AssetManager; 9 | 10 | use nphysics2d::nalgebra as na; 11 | 12 | pub struct Cloud { 13 | position: na::Point2, 14 | 15 | scale: f32, 16 | speed: f32, 17 | } 18 | 19 | impl Cloud { 20 | pub fn new(pos_x: f32, pos_y: f32, scale: f32, speed: f32) -> Self { 21 | let position = na::Point2::new(pos_x, pos_y); 22 | 23 | Self { 24 | position, 25 | scale, 26 | speed, 27 | } 28 | } 29 | 30 | pub fn draw(&mut self, ctx: &mut Context, asset_manager: &AssetManager) -> GameResult<()> { 31 | let cloud = asset_manager.get_image("Some(cloud).png"); 32 | 33 | graphics::draw( 34 | ctx, 35 | &cloud, 36 | DrawParam::default() 37 | .scale(Vector2 { 38 | x: self.scale, 39 | y: self.scale, 40 | }) 41 | .dest(Point2 { 42 | x: self.position.x, 43 | y: self.position.y, 44 | }), 45 | )?; 46 | 47 | Ok(()) 48 | } 49 | 50 | pub fn update(&mut self, ctx: &mut Context) { 51 | let (width, _) = graphics::drawable_size(ctx); 52 | 53 | let delta_time = ggez::timer::delta(ctx).as_secs_f32(); 54 | 55 | self.position.x += delta_time * self.speed; 56 | 57 | if self.position.x > width + 100. { 58 | self.position = na::Point2::new(-100., self.position.y); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/screens/game/components/enemy.rs: -------------------------------------------------------------------------------- 1 | use ggez::{ 2 | audio::SoundSource, 3 | graphics::{self, DrawParam}, 4 | nalgebra::Point2, 5 | Context, GameResult, 6 | }; 7 | use ggez_goodies::{camera::Camera, nalgebra_glm::Vec2}; 8 | 9 | use nphysics2d::{nalgebra as na, object::DefaultBodyHandle}; 10 | 11 | use crate::{ 12 | game::physics::{isometry_to_point, Physics}, 13 | play, 14 | utils::{AssetManager, ParticleSystem}, 15 | }; 16 | 17 | use super::{bullet::PlayerWeapon, player::Player}; 18 | 19 | pub struct Enemy { 20 | body: DefaultBodyHandle, 21 | } 22 | 23 | impl Enemy { 24 | pub fn new( 25 | ctx: &mut Context, 26 | pos_x: f32, 27 | physics: &mut Physics, 28 | asset_manager: &AssetManager, 29 | ) -> Self { 30 | let (_, height) = graphics::drawable_size(ctx); 31 | 32 | let gopher = asset_manager.get_image("gopher.png"); 33 | 34 | let body = physics.create_enemy( 35 | na::Point2::new(pos_x, height / 2.0 - 155.0), 36 | gopher.width(), 37 | gopher.height(), 38 | ); 39 | 40 | Self { body } 41 | } 42 | 43 | pub fn draw( 44 | &mut self, 45 | ctx: &mut Context, 46 | camera: &Camera, 47 | physics: &mut Physics, 48 | asset_manager: &AssetManager, 49 | ) -> GameResult<()> { 50 | let gopher = asset_manager.get_image("gopher.png"); 51 | let gun = asset_manager.get_image("Some(gun).png"); 52 | 53 | let enemy_position = self.position(physics); 54 | let gopher_position = 55 | camera.calculate_dest_point(Vec2::new(enemy_position.x, enemy_position.y)); 56 | 57 | graphics::draw( 58 | ctx, 59 | &gopher, 60 | DrawParam::default() 61 | .dest(Point2::new(gopher_position.x, gopher_position.y)) 62 | .offset(Point2::new(0.5, 0.5)), 63 | )?; 64 | 65 | graphics::draw( 66 | ctx, 67 | &gun, 68 | DrawParam::default() 69 | .dest(Point2::new( 70 | gopher_position.x - 50.0, 71 | gopher_position.y + 10.0, 72 | )) 73 | .offset(Point2::new(0.5, 0.5)), 74 | )?; 75 | 76 | Ok(()) 77 | } 78 | 79 | pub fn update( 80 | &mut self, 81 | physics: &mut Physics, 82 | asset_manager: &AssetManager, 83 | particles: &mut Vec, 84 | player: &mut Player, 85 | ) -> bool { 86 | let position = self.position(physics); 87 | 88 | let gopher = asset_manager.get_image("gopher.png"); 89 | let explode_sound = asset_manager.get_sound("Some(explode).mp3"); 90 | 91 | for i in 0..player.weapons.len() { 92 | match &mut player.weapons[i] { 93 | PlayerWeapon::Turbofish(fish) => { 94 | if fish.is_touching(physics, self.handle()) { 95 | particles.push(ParticleSystem::new( 96 | physics, 97 | 50, 98 | na::Point2::new( 99 | position.x - (gopher.width() / 2) as f32, 100 | position.y - (gopher.height() / 2) as f32, 101 | ), 102 | na::Point2::new( 103 | position.x + (gopher.width() / 2) as f32, 104 | position.y + (gopher.height() / 2) as f32, 105 | ), 106 | )); 107 | 108 | play!(explode_sound); 109 | 110 | // Remove the enemy from the world 111 | self.destroy(physics); 112 | 113 | // Remove the weapon from the world 114 | fish.destroy(physics); 115 | player.weapons.remove(i); 116 | 117 | return true; 118 | } 119 | } 120 | PlayerWeapon::Grappling(_) => {} 121 | } 122 | } 123 | 124 | // Can the enemy see the player? 125 | if physics.distance(self.handle(), player.handle()) < 300.0 { 126 | // TODO: The enemy shoots the player as soon as it sees the player. 127 | } 128 | 129 | false 130 | } 131 | 132 | pub fn position(&self, physics: &mut Physics) -> na::Point2 { 133 | let enemy_body = physics.get_rigid_body_mut(self.body); 134 | let enemy_position = isometry_to_point(enemy_body.position()); 135 | 136 | enemy_position 137 | } 138 | 139 | pub fn handle(&mut self) -> DefaultBodyHandle { 140 | self.body 141 | } 142 | 143 | pub fn destroy(&mut self, physics: &mut Physics) { 144 | physics.destroy_body(self.body); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/screens/game/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod barrel; 2 | pub mod bullet; 3 | pub mod cloud; 4 | pub mod enemy; 5 | pub mod player; 6 | pub mod tile; 7 | -------------------------------------------------------------------------------- /src/screens/game/components/player.rs: -------------------------------------------------------------------------------- 1 | use ggez::{event::KeyCode, graphics, input::keyboard, nalgebra::Point2, Context, GameResult}; 2 | use ggez_goodies::{camera::Camera, nalgebra_glm::Vec2}; 3 | use graphics::DrawParam; 4 | use nphysics2d::object::DefaultBodyHandle; 5 | use nphysics2d::{algebra::Velocity2, nalgebra as na}; 6 | 7 | use crate::{ 8 | game::physics::{isometry_to_point, point_to_isometry, Physics}, 9 | utils::AssetManager, 10 | }; 11 | 12 | use super::bullet::{Grappling, PlayerWeapon, Turbofish, WeaponType}; 13 | 14 | pub enum Direction { 15 | Left, 16 | Right, 17 | None, 18 | } 19 | 20 | pub struct Player { 21 | pub ammo: f32, 22 | pub health: i32, 23 | 24 | direction: Direction, 25 | 26 | body: DefaultBodyHandle, 27 | pub weapons: Vec, 28 | } 29 | 30 | impl Player { 31 | const SHIFT_JUICE: f32 = 10.0; 32 | const JUMP_JUICE: f32 = 20.0; 33 | 34 | pub fn new( 35 | ctx: &mut Context, 36 | pos_x: f32, 37 | physics: &mut Physics, 38 | asset_manager: &AssetManager, 39 | ) -> Self { 40 | let (_, height) = graphics::drawable_size(ctx); 41 | 42 | let ferris = asset_manager.get_image("Some(ferris).png"); 43 | 44 | let body = physics.create_player( 45 | na::Point2::new(pos_x, height / 2.0 - 155.), 46 | ferris.width(), 47 | ferris.height(), 48 | ); 49 | 50 | let weapons = vec![]; 51 | 52 | Self { 53 | ammo: 10.0, 54 | health: 100, 55 | 56 | direction: Direction::None, 57 | 58 | body, 59 | weapons, 60 | } 61 | } 62 | 63 | pub fn set_direction(&mut self, direction: Direction) { 64 | self.direction = direction; 65 | } 66 | 67 | pub fn draw( 68 | &mut self, 69 | ctx: &mut Context, 70 | camera: &Camera, 71 | physics: &mut Physics, 72 | asset_manager: &AssetManager, 73 | ) -> GameResult<()> { 74 | let ferris = asset_manager.get_image("Some(ferris).png"); 75 | let turbofish_sniper = asset_manager.get_image("Some(sniper).png"); 76 | 77 | let player_position = self.position(physics); 78 | let ferris_position = 79 | camera.calculate_dest_point(Vec2::new(player_position.x, player_position.y)); 80 | 81 | // Draw the player 82 | graphics::draw( 83 | ctx, 84 | &ferris, 85 | DrawParam::default() 86 | .dest(Point2::new(ferris_position.x, ferris_position.y)) 87 | .offset(Point2::new(0.5, 0.5)), 88 | )?; 89 | 90 | graphics::draw( 91 | ctx, 92 | &turbofish_sniper, 93 | DrawParam::default() 94 | .dest(Point2::new( 95 | ferris_position.x + 30.0, 96 | ferris_position.y + 15.0, 97 | )) 98 | .offset(Point2::new(0.5, 0.5)), 99 | )?; 100 | 101 | // Draw the player weapon 102 | for weapon in &mut self.weapons { 103 | match weapon { 104 | PlayerWeapon::Turbofish(fish) => { 105 | fish.draw(ctx, camera, physics, asset_manager)?; 106 | } 107 | PlayerWeapon::Grappling(grapple) => { 108 | grapple.draw(ctx, camera, physics)?; 109 | } 110 | } 111 | } 112 | 113 | Ok(()) 114 | } 115 | 116 | pub fn init(&mut self, physics: &mut Physics) { 117 | let player_body = physics.get_rigid_body_mut(self.body); 118 | let player_position = isometry_to_point(player_body.position()); 119 | 120 | let updated_position = 121 | point_to_isometry(na::Point2::new(player_position.x, player_position.y - 40.0)); 122 | 123 | player_body.set_position(updated_position); 124 | } 125 | 126 | pub fn update(&mut self, ctx: &mut Context, physics: &mut Physics) { 127 | if keyboard::is_key_pressed(ctx, KeyCode::Left) { 128 | self.shift(physics, Direction::Left); 129 | self.set_direction(Direction::Left); 130 | } else if keyboard::is_key_pressed(ctx, KeyCode::Right) { 131 | self.shift(physics, Direction::Right); 132 | self.set_direction(Direction::Right); 133 | } 134 | 135 | // We are not adding Space key pressed in an else if statement as we want to jump while we are also moving to a specific direction in the x axis. 136 | if keyboard::is_key_pressed(ctx, KeyCode::Space) { 137 | self.go_boom(physics); 138 | self.set_direction(Direction::None); 139 | } 140 | 141 | // Same as the previous if statement. We want to shoot while moving and jumping around :) 142 | if keyboard::is_key_pressed(ctx, KeyCode::S) { 143 | // TODO: Move the shoot logic from game struct to this if statement 144 | } 145 | 146 | for i in 0..self.weapons.len() { 147 | let weapon = &mut self.weapons[i]; 148 | 149 | match weapon { 150 | PlayerWeapon::Turbofish(fish) => { 151 | if fish.update(physics) { 152 | fish.destroy(physics); 153 | self.weapons.remove(i); 154 | 155 | break; 156 | } 157 | } 158 | PlayerWeapon::Grappling(grapple) => { 159 | if keyboard::is_key_pressed(ctx, KeyCode::S) { 160 | grapple.update(physics); 161 | } else { 162 | self.weapons.remove(i); 163 | break; 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | pub fn shoot( 171 | &mut self, 172 | physics: &mut Physics, 173 | asset_manager: &AssetManager, 174 | gun: &WeaponType, 175 | ) -> Option { 176 | let player_position = self.position(physics); 177 | 178 | if self.ammo > 0.0 { 179 | match gun { 180 | WeaponType::Turbofish => Some(PlayerWeapon::Turbofish(Turbofish::new( 181 | player_position.x + 140.0, 182 | player_position.y, 183 | physics, 184 | asset_manager, 185 | ))), 186 | 187 | WeaponType::Grappling => { 188 | let gun = Grappling::new( 189 | player_position.x + 140.0, 190 | player_position.y, 191 | physics, 192 | self.handle(), 193 | ); 194 | 195 | gun.map(PlayerWeapon::Grappling) 196 | } 197 | } 198 | } else { 199 | None 200 | } 201 | } 202 | 203 | pub fn position(&mut self, physics: &mut Physics) -> na::Point2 { 204 | let player_body = physics.get_rigid_body_mut(self.body); 205 | let player_position = isometry_to_point(player_body.position()); 206 | 207 | player_position 208 | } 209 | 210 | pub fn go_boom(&mut self, physics: &mut Physics) { 211 | let player_body = physics.get_rigid_body_mut(self.body); 212 | let player_velocity = player_body.velocity(); 213 | 214 | let new_velocity = Velocity2::new( 215 | na::Vector2::new( 216 | player_velocity.linear.x, 217 | player_velocity.linear.y - Self::JUMP_JUICE, 218 | ), 219 | player_velocity.angular, 220 | ); 221 | 222 | player_body.set_velocity(new_velocity); 223 | } 224 | 225 | fn shift(&mut self, physics: &mut Physics, direction: Direction) { 226 | let player_body = physics.get_rigid_body_mut(self.body); 227 | let player_velocity = player_body.velocity(); 228 | 229 | match direction { 230 | Direction::Left => { 231 | let new_velocity = Velocity2::new( 232 | na::Vector2::new( 233 | player_velocity.linear.x - Self::SHIFT_JUICE, 234 | player_velocity.linear.y, 235 | ), 236 | player_velocity.angular, 237 | ); 238 | 239 | player_body.set_velocity(new_velocity); 240 | } 241 | Direction::Right => { 242 | let new_velocity = Velocity2::new( 243 | na::Vector2::new( 244 | player_velocity.linear.x + Self::SHIFT_JUICE, 245 | player_velocity.linear.y, 246 | ), 247 | player_velocity.angular, 248 | ); 249 | 250 | player_body.set_velocity(new_velocity); 251 | } 252 | Direction::None => { 253 | panic!("Direction::None direction was passed in the Player::move_x() function where None value of the Direction enum was not expected. Panic!"); 254 | } 255 | } 256 | } 257 | 258 | pub fn handle(&self) -> DefaultBodyHandle { 259 | self.body 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/screens/game/components/tile.rs: -------------------------------------------------------------------------------- 1 | use ggez::{graphics, nalgebra::Point2, Context, GameResult}; 2 | use ggez_goodies::{camera::Camera, nalgebra_glm::Vec2}; 3 | use graphics::DrawParam; 4 | use nphysics2d::nalgebra as na; 5 | use nphysics2d::object::DefaultBodyHandle; 6 | 7 | use crate::{ 8 | game::physics::{isometry_to_point, Physics}, 9 | utils::AssetManager, 10 | }; 11 | 12 | pub enum TileType { 13 | Left, 14 | Center, 15 | Right, 16 | } 17 | 18 | pub struct Tile { 19 | width: f32, 20 | height: f32, 21 | 22 | body: DefaultBodyHandle, 23 | tile_type: TileType, 24 | } 25 | 26 | impl Tile { 27 | pub fn new( 28 | ctx: &mut Context, 29 | pos_x: f32, 30 | physics: &mut Physics, 31 | asset_manager: &AssetManager, 32 | tile_type: TileType, 33 | ) -> Self { 34 | let (_, height) = graphics::drawable_size(ctx); 35 | 36 | let tile_width; 37 | let tile_height; 38 | 39 | let pos_y = height / 2.0 - 64.0; 40 | 41 | match tile_type { 42 | TileType::Left => { 43 | let ground_left = asset_manager.get_image("ground_left.png"); 44 | 45 | tile_width = ground_left.width(); 46 | tile_height = ground_left.height(); 47 | } 48 | TileType::Center => { 49 | let ground_centre = asset_manager.get_image("ground_centre.png"); 50 | 51 | tile_width = ground_centre.width(); 52 | tile_height = ground_centre.height(); 53 | } 54 | TileType::Right => { 55 | let ground_right = asset_manager.get_image("ground_right.png"); 56 | 57 | tile_width = ground_right.width(); 58 | tile_height = ground_right.height(); 59 | } 60 | } 61 | 62 | let body = physics.create_tile(na::Point2::new(pos_x, pos_y), tile_width, tile_height); 63 | 64 | Self { 65 | tile_type, 66 | body, 67 | 68 | width: tile_width as f32, 69 | height: tile_height as f32, 70 | } 71 | } 72 | 73 | pub fn draw( 74 | &mut self, 75 | ctx: &mut Context, 76 | camera: &Camera, 77 | physics: &mut Physics, 78 | asset_manager: &AssetManager, 79 | ) -> GameResult<()> { 80 | let ground_left = asset_manager.get_image("ground_left.png"); 81 | let ground_centre = asset_manager.get_image("ground_centre.png"); 82 | let ground_right = asset_manager.get_image("ground_right.png"); 83 | 84 | let ground_position = self.position(physics); 85 | let tile_position = 86 | camera.calculate_dest_point(Vec2::new(ground_position.x, ground_position.y)); 87 | 88 | match self.tile_type { 89 | TileType::Left => { 90 | graphics::draw( 91 | ctx, 92 | &ground_left, 93 | DrawParam::default() 94 | .dest(Point2::new(tile_position.x, tile_position.y)) 95 | .offset(Point2::new(0.5, 0.5)), 96 | )?; 97 | } 98 | 99 | TileType::Center => { 100 | graphics::draw( 101 | ctx, 102 | &ground_centre, 103 | DrawParam::default() 104 | .dest(Point2::new(tile_position.x, tile_position.y)) 105 | .offset(Point2::new(0.5, 0.5)), 106 | )?; 107 | } 108 | TileType::Right => { 109 | graphics::draw( 110 | ctx, 111 | &ground_right, 112 | DrawParam::default() 113 | .dest(Point2::new(tile_position.x, tile_position.y)) 114 | .offset(Point2::new(0.5, 0.5)), 115 | )?; 116 | } 117 | } 118 | 119 | Ok(()) 120 | } 121 | 122 | pub fn position(&self, physics: &mut Physics) -> na::Point2 { 123 | let ground_body = physics.get_rigid_body(self.body); 124 | let ground_position = isometry_to_point(ground_body.position()); 125 | 126 | ground_position 127 | } 128 | 129 | pub fn dimensions(&self) -> na::Point2 { 130 | na::Point2::new(self.width, self.height) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/screens/game/game.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, process::exit, rc::Rc, sync::Mutex}; 2 | 3 | use ggez::{ 4 | audio::SoundSource, 5 | event::KeyCode, 6 | graphics::{self, Color, DrawParam, Drawable, Shader, Text}, 7 | mint, 8 | nalgebra::Point2, 9 | timer, Context, GameResult, 10 | }; 11 | use ggez_goodies::{camera::Camera, nalgebra_glm::Vec2}; 12 | use graphics::{GlBackendSpec, Scale, ShaderGeneric, TextFragment}; 13 | use mint::Vector2; 14 | use rand::Rng; 15 | 16 | use crate::{ 17 | game::components::{ 18 | bullet::{PlayerWeapon, WeaponType}, 19 | cloud::Cloud, 20 | player::Direction, 21 | }, 22 | game::map::Map, 23 | game::physics::Physics, 24 | play, 25 | utils::{lerp, remap, AssetManager, ParticleSystem}, 26 | Screen, 27 | }; 28 | 29 | use gfx::*; 30 | 31 | gfx_defines! { 32 | constant Dim { 33 | rate: f32 = "u_Rate", 34 | } 35 | } 36 | 37 | pub struct Game { 38 | /// The game map. 39 | map: Map, 40 | /// Physics system for the game. 41 | physics: Physics, 42 | /// Camera to see the world. 43 | camera: Camera, 44 | 45 | // TODO: Refactor the rest of the fields 46 | clouds: Vec, 47 | 48 | /// Reference to the asset manager. 49 | asset_manager: Rc, 50 | 51 | elapsed_shake: Option<(f32, Vec2, f32)>, 52 | tics: Option, 53 | particles: Vec, 54 | ui_lerp: HashMap, 55 | 56 | dim_shader: ShaderGeneric, 57 | dim_constant: Dim, 58 | 59 | draw_end_text: (bool, Option, bool, bool), // Thread Sleeped?, Current Iters, Done?, Win? 60 | can_die: bool, 61 | } 62 | 63 | impl Game { 64 | pub fn create(ctx: &mut Context, asset_manager: Rc) -> Mutex { 65 | let (width, height) = graphics::drawable_size(ctx); 66 | 67 | let mut camera = Camera::new(width as u32, height as u32, width, height); 68 | 69 | let mut rng = rand::thread_rng(); 70 | 71 | let mut physics = Physics::new(); 72 | let mut map = Map::parse(ctx, "01", &mut physics, &asset_manager); 73 | 74 | let mut clouds = vec![]; 75 | 76 | let dim_constant = Dim { rate: 1.0 }; 77 | 78 | let dim_shader = Shader::new( 79 | ctx, 80 | "/shaders/dim.basic.glslf", 81 | "/shaders/dim.glslf", 82 | dim_constant, 83 | "Dim", 84 | None, 85 | ) 86 | .unwrap(); 87 | 88 | let mut ui_lerp = HashMap::new(); 89 | 90 | ui_lerp.insert(String::from("ammo"), map.player.ammo as f32); 91 | ui_lerp.insert(String::from("health"), map.player.health as f32); 92 | ui_lerp.insert(String::from("using"), map.using.as_ref().unwrap().1); 93 | 94 | map.player.init(&mut physics); 95 | 96 | camera.move_to(Vec2::new( 97 | map.player.position(&mut physics).x, 98 | map.player.position(&mut physics).y, 99 | )); 100 | 101 | for _ in 0..rng.gen_range(5..=7) { 102 | clouds.push(Cloud::new( 103 | rng.gen_range(0. ..=width), 104 | rng.gen_range(10. ..=40.), 105 | rng.gen_range(0.1..=0.3), 106 | rng.gen_range(10. ..=35.), 107 | )); 108 | } 109 | 110 | Mutex::new(Self { 111 | map, 112 | physics, 113 | 114 | clouds, 115 | 116 | asset_manager, 117 | 118 | camera, 119 | 120 | elapsed_shake: None, 121 | tics: None, 122 | particles: vec![], 123 | ui_lerp, 124 | 125 | dim_shader, 126 | dim_constant, 127 | draw_end_text: (false, None, false, false), 128 | can_die: true, 129 | }) 130 | } 131 | 132 | pub fn draw(&mut self, ctx: &mut Context) -> GameResult> { 133 | let (width, height) = graphics::drawable_size(ctx); 134 | 135 | let consolas = self.asset_manager.get_font("Consolas.ttf"); 136 | 137 | if let Some(_t) = self.tics { 138 | { 139 | let _lock = graphics::use_shader(ctx, &self.dim_shader); 140 | 141 | self.inner_draw(ctx)?; 142 | } 143 | 144 | if self.draw_end_text.0 && self.draw_end_text.3 { 145 | let mut draw_pos = 0.; 146 | 147 | // You Win 148 | let end_frag = &Text::new( 149 | TextFragment::new("You Win!") 150 | .font(consolas) 151 | .scale(Scale::uniform(50.)), 152 | ); 153 | 154 | let end_dimensions = end_frag.dimensions(ctx); 155 | 156 | graphics::draw( 157 | ctx, 158 | end_frag, 159 | DrawParam::default().dest(Point2::new( 160 | (width / 2.0) - (end_dimensions.0 / 2) as f32, 161 | 50.0, 162 | )), 163 | )?; 164 | 165 | // End quote 166 | for line in self 167 | .map 168 | .end 169 | .as_ref() 170 | .unwrap() 171 | .split("\\n") 172 | .collect::>() 173 | { 174 | let end_frag = &Text::new(TextFragment::new(line).font(consolas)); 175 | 176 | let end_dimensions = end_frag.dimensions(ctx); 177 | 178 | graphics::draw( 179 | ctx, 180 | end_frag, 181 | DrawParam::default().dest(Point2::new( 182 | (width / 2.0) - (end_dimensions.0 / 2) as f32, 183 | height / 2. + draw_pos, 184 | )), 185 | )?; 186 | 187 | draw_pos += 20.0; 188 | } 189 | 190 | // Press & to go to menu screen 191 | let menu_rect = graphics::Mesh::new_rectangle( 192 | ctx, 193 | graphics::DrawMode::fill(), 194 | graphics::Rect::new( 195 | (width / 2.) + 20., 196 | (height / 2.) + (draw_pos * 2.), 197 | 220.0, 198 | 40.0, 199 | ), 200 | [36.0 / 255.0, 36.0 / 255.0, 36.0 / 255.0, 0.9].into(), 201 | )?; 202 | 203 | let menu_rect_dim = menu_rect.dimensions(ctx).unwrap(); 204 | 205 | let menu_frag_to = 206 | &Text::new(TextFragment::new("Press & go to the").font(consolas)); 207 | 208 | let menu_screen = &Text::new( 209 | TextFragment::new("MENU SCREEN") 210 | .font(consolas) 211 | .scale(Scale::uniform(20.0)), 212 | ); 213 | 214 | graphics::draw(ctx, &menu_rect, DrawParam::default())?; 215 | graphics::draw( 216 | ctx, 217 | menu_frag_to, 218 | DrawParam::default().dest(Point2::new( 219 | (width / 2.) + 20., 220 | ((height / 2.) + (draw_pos * 2.)) - 20.0, 221 | )), 222 | )?; 223 | 224 | graphics::draw( 225 | ctx, 226 | menu_screen, 227 | DrawParam::default().dest(Point2::new( 228 | (width / 2.) + 70., 229 | ((height / 2.) + (draw_pos * 2.)) + 12.0, 230 | )), 231 | )?; 232 | 233 | // Press * to quit 234 | let quit_rect = graphics::Mesh::new_rectangle( 235 | ctx, 236 | graphics::DrawMode::fill(), 237 | graphics::Rect::new( 238 | ((width / 2.) - menu_rect_dim.w) - 20.0, 239 | (height / 2.) + (draw_pos * 2.), 240 | 220.0, 241 | 40.0, 242 | ), 243 | [36.0 / 255.0, 36.0 / 255.0, 36.0 / 255.0, 0.9].into(), 244 | )?; 245 | 246 | let quit_frag_to = &Text::new(TextFragment::new("Press * to").font(consolas)); 247 | 248 | let press_quit = &Text::new( 249 | TextFragment::new("QUIT") 250 | .font(consolas) 251 | .scale(Scale::uniform(20.)), 252 | ); 253 | 254 | graphics::draw(ctx, &quit_rect, DrawParam::default())?; 255 | graphics::draw( 256 | ctx, 257 | quit_frag_to, 258 | DrawParam::default().dest(Point2::new( 259 | ((width / 2.) - menu_rect_dim.w) - 20., 260 | ((height / 2.) + (draw_pos * 2.)) - 20., 261 | )), 262 | )?; 263 | 264 | graphics::draw( 265 | ctx, 266 | press_quit, 267 | DrawParam::default().dest(Point2::new( 268 | (((width / 2.) - menu_rect_dim.w) - 20.) + 90., 269 | (((height / 2.) + (draw_pos * 2.)) - 20.) + 30., 270 | )), 271 | )?; 272 | } 273 | } else { 274 | self.inner_draw(ctx)?; 275 | } 276 | 277 | graphics::present(ctx)?; 278 | 279 | Ok(None) 280 | } 281 | 282 | fn inner_draw(&mut self, ctx: &mut Context) -> GameResult<()> { 283 | graphics::clear(ctx, graphics::BLACK); 284 | 285 | // Clouds 286 | for cloud in &mut self.clouds { 287 | cloud.draw(ctx, &self.asset_manager)?; 288 | } 289 | 290 | // Ground 291 | for tile in &mut self.map.ground { 292 | tile.draw(ctx, &self.camera, &mut self.physics, &self.asset_manager)?; 293 | } 294 | 295 | // Enemies 296 | for enemy in &mut self.map.enemies { 297 | enemy.draw(ctx, &self.camera, &mut self.physics, &self.asset_manager)?; 298 | } 299 | 300 | // Barrel 301 | for boom in &mut self.map.barrels { 302 | boom.draw(ctx, &self.camera, &mut self.physics, &self.asset_manager)?; 303 | } 304 | 305 | // Player 306 | self.map 307 | .player 308 | .draw(ctx, &self.camera, &mut self.physics, &self.asset_manager)?; 309 | 310 | // Particles 311 | for sys in &mut self.particles { 312 | sys.draw(ctx, &mut self.physics, &mut self.camera)?; 313 | } 314 | 315 | // User Profile, etc.. 316 | self.draw_ui(ctx)?; 317 | 318 | #[cfg(feature = "debug")] 319 | self.physics.draw_colliders(ctx, &self.camera)?; 320 | 321 | Ok(()) 322 | } 323 | 324 | fn draw_ui(&mut self, ctx: &mut Context) -> GameResult<()> { 325 | let (width, _) = graphics::drawable_size(ctx); 326 | 327 | let profile = self.asset_manager.get_image("Some(profile).png"); 328 | let fish = self.asset_manager.get_image("Some(fish).png"); 329 | 330 | let consolas = self.asset_manager.get_font("Consolas.ttf"); 331 | 332 | graphics::draw( 333 | ctx, 334 | &profile, 335 | DrawParam::default() 336 | .dest(Point2::new(10.0, 10.0)) 337 | .scale(Vector2 { x: 0.5, y: 0.5 }), 338 | )?; 339 | 340 | let ammo_rect = graphics::Mesh::new_rectangle( 341 | ctx, 342 | graphics::DrawMode::fill(), 343 | graphics::Rect::new( 344 | ((profile.width() / 2) + 10) as f32, 345 | (profile.height() / 3) as f32, 346 | 150., 347 | 15., 348 | ), 349 | Color::from_rgb(54, 50, 49), 350 | )?; 351 | 352 | let hp_rect = graphics::Mesh::new_rectangle( 353 | ctx, 354 | graphics::DrawMode::fill(), 355 | graphics::Rect::new( 356 | ((profile.width() / 2) + 10) as f32, 357 | (profile.height() / 5) as f32, 358 | 150., 359 | 15., 360 | ), 361 | Color::from_rgb(54, 50, 49), 362 | )?; 363 | 364 | let cur_ammo_rect = graphics::Mesh::new_rectangle( 365 | ctx, 366 | graphics::DrawMode::fill(), 367 | graphics::Rect::new( 368 | ((profile.width() / 2) + 10) as f32, 369 | (profile.height() / 3) as f32, 370 | remap(self.map.player.ammo as f32, 0., 10., 0., 150.), 371 | 15., 372 | ), 373 | Color::from_rgb(21, 156, 228), 374 | )?; 375 | 376 | let cur_hp_rect = graphics::Mesh::new_rectangle( 377 | ctx, 378 | graphics::DrawMode::fill(), 379 | graphics::Rect::new( 380 | ((profile.width() / 2) + 10) as f32, 381 | (profile.height() / 5) as f32, 382 | remap(self.map.player.health as f32, 0., 100., 0., 150.), 383 | 15., 384 | ), 385 | Color::from_rgb(34, 205, 124), 386 | )?; 387 | 388 | graphics::draw(ctx, &ammo_rect, DrawParam::default())?; 389 | 390 | graphics::draw(ctx, &hp_rect, DrawParam::default())?; 391 | 392 | graphics::draw(ctx, &cur_ammo_rect, DrawParam::default())?; 393 | 394 | graphics::draw(ctx, &cur_hp_rect, DrawParam::default())?; 395 | 396 | graphics::draw( 397 | ctx, 398 | &fish, 399 | DrawParam::default() 400 | .dest(Point2::new( 401 | ((profile.width() / 2) - 10) as f32, 402 | (profile.height() / 3) as f32, 403 | )) 404 | .scale(Vector2 { x: 0.7, y: 0.7 }), 405 | )?; 406 | 407 | let evildoers = &Text::new( 408 | TextFragment::new(format!( 409 | "Evildoers {}/{}", 410 | self.map.enemies.len(), 411 | self.map.total_enemies 412 | )) 413 | .font(consolas) 414 | .scale(Scale::uniform(20.)), 415 | ); 416 | 417 | let evildoers_dim = evildoers.dimensions(ctx); 418 | 419 | graphics::draw( 420 | ctx, 421 | evildoers, 422 | DrawParam::default().dest(Point2::new((width - evildoers_dim.0 as f32) - 40., 20.)), 423 | )?; 424 | 425 | let info = &Text::new( 426 | TextFragment::new(format!("Using {}", self.map.using.as_ref().unwrap().0)) 427 | .font(consolas) 428 | .color([1.0, 1.0, 1.0, self.map.using.as_ref().unwrap().1].into()), 429 | ); 430 | 431 | let info_dim = info.dimensions(ctx); 432 | 433 | graphics::draw( 434 | ctx, 435 | info, 436 | DrawParam::default().dest(Point2::new((width / 2.) - (info_dim.0 / 2) as f32, 150.)), 437 | )?; 438 | 439 | Ok(()) 440 | } 441 | 442 | pub fn update(&mut self, ctx: &mut Context) -> GameResult> { 443 | if let Some(t) = self.tics { 444 | if self.tics.is_some() && self.dim_constant.rate != 0.5 { 445 | self.dim_constant.rate = lerp(self.dim_constant.rate, 0.5, 0.1); 446 | self.dim_shader.send(ctx, self.dim_constant)?; 447 | } 448 | 449 | if timer::ticks(ctx) % t as usize == 0 { 450 | return self.inner_update(ctx); 451 | } 452 | } else { 453 | return self.inner_update(ctx); 454 | } 455 | 456 | Ok(None) 457 | } 458 | 459 | fn inner_update(&mut self, ctx: &mut Context) -> GameResult> { 460 | let (_, height) = graphics::drawable_size(ctx); 461 | 462 | // Take a time step in our physics world! 463 | self.physics.step(); 464 | 465 | // Update our player 466 | self.map.player.update(ctx, &mut self.physics); 467 | self.camera.move_to(Vec2::new( 468 | self.map.player.position(&mut self.physics).x, 469 | self.map.player.position(&mut self.physics).y, 470 | )); 471 | 472 | // Update our lovely clouds 473 | for cloud in &mut self.clouds { 474 | cloud.update(ctx); 475 | } 476 | 477 | if self.map.enemies.is_empty() { 478 | self.draw_end_text.3 = true; 479 | self.can_die = false; 480 | 481 | if self.draw_end_text.1.is_none() { 482 | self.draw_end_text.1 = Some(timer::ticks(ctx)); 483 | } else if !self.draw_end_text.2 { 484 | if timer::ticks(ctx) - self.draw_end_text.1.unwrap() > 30 { 485 | self.draw_end_text.0 = true; 486 | self.draw_end_text.2 = true; 487 | } 488 | } else { 489 | self.tics = Some(1); 490 | 491 | if self.dim_constant.rate != 0.0 { 492 | self.dim_constant.rate = lerp(self.dim_constant.rate, 0.0, 0.1); 493 | self.dim_shader.send(ctx, self.dim_constant)?; 494 | } 495 | } 496 | } 497 | 498 | if self.map.player.position(&mut self.physics).y > height && self.can_die { 499 | return Ok(Some(Screen::Dead)); 500 | } 501 | 502 | for id in 0..self.map.enemies.len() { 503 | let enemy = &mut self.map.enemies[id]; 504 | 505 | if enemy.update( 506 | &mut self.physics, 507 | &self.asset_manager, 508 | &mut self.particles, 509 | &mut self.map.player, 510 | ) { 511 | self.map.enemies.remove(id); 512 | let cam_loc = self.camera.location(); 513 | let org_pos = cam_loc.data.as_slice(); 514 | 515 | self.elapsed_shake = Some((0., Vec2::new(org_pos[0], org_pos[1]), 3.)); 516 | self.camera_shakeke(); 517 | 518 | break; 519 | }; 520 | } 521 | 522 | for id in 0..self.map.barrels.len() { 523 | if self.map.barrels[id].update( 524 | &mut self.physics, 525 | &self.asset_manager, 526 | &mut self.particles, 527 | &mut self.map.player, 528 | ) { 529 | self.map.barrels.remove(id); 530 | let cam_loc = self.camera.location(); 531 | let org_pos = cam_loc.data.as_slice(); 532 | 533 | self.elapsed_shake = Some((0., Vec2::new(org_pos[0], org_pos[1]), 5.)); 534 | self.camera_shakeke(); 535 | } 536 | } 537 | 538 | if let Some(s) = self.elapsed_shake { 539 | if s.0 < 1. { 540 | self.camera_shakeke(); 541 | } else { 542 | self.camera.move_to(s.1); 543 | self.elapsed_shake = None; 544 | } 545 | } 546 | 547 | for id in 0..self.particles.len() { 548 | let sys = &mut self.particles[id]; 549 | 550 | if sys.update(ctx, &mut self.physics) { 551 | self.particles.remove(id); 552 | 553 | break; 554 | } 555 | } 556 | 557 | for v in &mut self.ui_lerp { 558 | match v.0.as_str() { 559 | "ammo" => { 560 | if self.map.player.ammo <= 0.0 { 561 | self.map.player.ammo = 0.0; 562 | } else { 563 | self.map.player.ammo = lerp(self.map.player.ammo, *v.1, 0.3); 564 | } 565 | } 566 | 567 | "health" => { 568 | // TODO: Health lerping 569 | } 570 | 571 | "using" => { 572 | self.map.using.as_mut().unwrap().1 = 573 | lerp(self.map.using.as_mut().unwrap().1, 0.0, 0.05); 574 | } 575 | 576 | _ => panic!(), 577 | } 578 | } 579 | 580 | Ok(None) 581 | } 582 | 583 | pub fn key_press(&mut self, keycode: KeyCode) -> Option { 584 | match keycode { 585 | KeyCode::S => { 586 | let ui_lerp = self.ui_lerp.clone(); 587 | let turbofish_shoot = self.asset_manager.get_sound("Some(turbofish_shoot).mp3"); 588 | 589 | if let Some(bullet) = 590 | self.map 591 | .player 592 | .shoot(&mut self.physics, &self.asset_manager, &self.map.weapon) 593 | { 594 | play!(turbofish_shoot); 595 | 596 | if let PlayerWeapon::Turbofish(_fish) = &bullet { 597 | let cur_ammo = ui_lerp.get("ammo").unwrap(); 598 | self.ui_lerp.insert(String::from("ammo"), *cur_ammo - 1.); 599 | } 600 | 601 | self.map.player.weapons.push(bullet); 602 | } 603 | } 604 | KeyCode::Up => { 605 | self.tics = Some(6); 606 | } 607 | KeyCode::Key7 => { 608 | return Some(Screen::Menu); 609 | } 610 | KeyCode::Key8 => { 611 | exit(0); 612 | } 613 | KeyCode::Down => match self.map.using.as_ref().unwrap().0.as_str() { 614 | "Turbofish Gun" => { 615 | self.map.using = Some((String::from("Grappling Gun"), 1.0)); 616 | self.map.weapon = WeaponType::Grappling; 617 | } 618 | 619 | "Grappling Gun" => { 620 | self.map.using = Some((String::from("Turbofish Gun"), 1.0)); 621 | self.map.weapon = WeaponType::Turbofish; 622 | } 623 | 624 | _ => { 625 | panic!() 626 | } 627 | }, 628 | _ => (), 629 | } 630 | 631 | None 632 | } 633 | 634 | pub fn key_up_event(&mut self, keycode: KeyCode) { 635 | if keycode == KeyCode::Up { 636 | self.tics = None; 637 | self.dim_constant.rate = 1.0; 638 | } 639 | self.map.player.set_direction(Direction::None); 640 | } 641 | 642 | /// Give the camera a shakey shakey. 643 | fn camera_shakeke(&mut self) { 644 | let mut rng = rand::thread_rng(); 645 | 646 | let elapsed = self.elapsed_shake.unwrap(); 647 | let magnitude = elapsed.2; 648 | 649 | let x = rng.gen_range(-1.0..=1.0) * magnitude; 650 | let y = rng.gen_range(-1.0..=1.0) * magnitude; 651 | 652 | self.camera.move_by(Vec2::new(x, y)); 653 | 654 | self.elapsed_shake = Some((elapsed.0 + 0.1, elapsed.1, magnitude)); 655 | } 656 | } 657 | -------------------------------------------------------------------------------- /src/screens/game/map.rs: -------------------------------------------------------------------------------- 1 | //! Helper struct that helps to parse .map files made only for Call of Ferris 2 | //! 3 | //! # Map 4 | //! `[` => Create left tile \ 5 | //! `-` => Create center tile \ 6 | //! `]` => Create right tile \ 7 | //! `_` => Increase draw x by 100.0 \ 8 | //! `8` => Push a tile with a enemy \ 9 | //! `4` => Create a tile with the player \ 10 | //! `*` => Create a tile with a barrel \ 11 | //! 12 | //! # Setter Syntax 13 | //! `.comment` => A comment \ 14 | //! `.using_weapon` => Set the current weapon \ 15 | //! `.end` => The end quote displayed on the win screen 16 | 17 | use ggez::Context; 18 | 19 | use crate::{ 20 | game::components::{ 21 | barrel::Barrel, 22 | bullet::WeaponType, 23 | enemy::Enemy, 24 | player::Player, 25 | tile::{Tile, TileType}, 26 | }, 27 | game::physics::Physics, 28 | utils::AssetManager, 29 | }; 30 | 31 | pub struct Map { 32 | pub ground: Vec, 33 | pub enemies: Vec, 34 | pub barrels: Vec, 35 | pub player: Player, 36 | 37 | pub total_enemies: i32, 38 | 39 | pub end: Option, 40 | pub using: Option<(String, f32)>, 41 | 42 | pub weapon: WeaponType, 43 | } 44 | 45 | impl Map { 46 | pub fn parse( 47 | ctx: &mut Context, 48 | map_id: &str, 49 | physics: &mut Physics, 50 | asset_manager: &AssetManager, 51 | ) -> Self { 52 | let map = asset_manager.get_file(format!("/maps/{}.map", map_id).as_str()); 53 | 54 | let mut draw_pos = 0.; 55 | 56 | #[allow(unused_assignments)] 57 | let mut draw_inc = 64.; 58 | 59 | let mut ground = vec![]; 60 | let mut enemies = vec![]; 61 | let mut total_enemies = 0; 62 | let mut barrels = vec![]; 63 | 64 | let mut player = None; 65 | 66 | let mut end = None; 67 | let mut using = None; 68 | 69 | let mut weapon = WeaponType::Turbofish; 70 | 71 | for line in map.split('\n').collect::>() { 72 | let exp = line.split(' ').collect::>(); 73 | 74 | if exp[0].starts_with(".end") { 75 | end = Some(exp[1..].join(" ")); 76 | } else if exp[0].starts_with(".using_weapon") { 77 | let using_weapon = (exp[1..].join(" ").trim().to_string(), 1.0); 78 | 79 | weapon = match using_weapon.0.as_str() { 80 | "Turbofish Gun" => WeaponType::Turbofish, 81 | "Grappling Gun" => WeaponType::Grappling, 82 | _ => panic!(""), 83 | }; 84 | 85 | using = Some(using_weapon); 86 | } else if exp[0].starts_with(".comment") { 87 | // Do nothing. ¯\_(ツ)_/¯ 88 | } else { 89 | for id in line.chars() { 90 | match id { 91 | '[' => { 92 | let tile = 93 | Tile::new(ctx, draw_pos, physics, asset_manager, TileType::Left); 94 | 95 | draw_inc = (tile.dimensions().x / 2.0) + 32.0; 96 | draw_pos += draw_inc; 97 | 98 | ground.push(tile); 99 | } 100 | 101 | '-' => { 102 | let tile = 103 | Tile::new(ctx, draw_pos, physics, asset_manager, TileType::Center); 104 | 105 | draw_inc = (tile.dimensions().x / 2.0) + 32.0; 106 | draw_pos += draw_inc; 107 | 108 | ground.push(tile); 109 | } 110 | 111 | ']' => { 112 | let tile = Tile::new( 113 | ctx, 114 | (draw_pos - 32.0) + 20.0, 115 | physics, 116 | asset_manager, 117 | TileType::Right, 118 | ); 119 | 120 | draw_inc = (tile.dimensions().x / 2.0) + 32.0; 121 | draw_pos += draw_inc; 122 | 123 | ground.push(tile); 124 | } 125 | 126 | '_' => { 127 | draw_inc = 100.0; 128 | draw_pos += draw_inc; 129 | } 130 | 131 | '8' => { 132 | let tile = 133 | Tile::new(ctx, draw_pos, physics, asset_manager, TileType::Center); 134 | 135 | draw_inc = (tile.dimensions().x / 2.0) + 32.0; 136 | 137 | ground.push(tile); 138 | enemies.push(Enemy::new(ctx, draw_pos, physics, asset_manager)); 139 | 140 | draw_pos += draw_inc; 141 | total_enemies += 1; 142 | } 143 | 144 | '4' => { 145 | let tile = 146 | Tile::new(ctx, draw_pos, physics, asset_manager, TileType::Center); 147 | 148 | player = Some(Player::new(ctx, draw_pos, physics, asset_manager)); 149 | 150 | draw_inc = tile.dimensions().x; 151 | draw_pos += draw_inc; 152 | 153 | ground.push(tile); 154 | } 155 | 156 | '*' => { 157 | let tile = 158 | Tile::new(ctx, draw_pos, physics, asset_manager, TileType::Center); 159 | 160 | draw_inc = tile.dimensions().x; 161 | 162 | ground.push(tile); 163 | barrels.push(Barrel::new(ctx, draw_pos, physics, asset_manager)); 164 | 165 | draw_pos += draw_inc; 166 | } 167 | 168 | _ => {} 169 | } 170 | } 171 | } 172 | } 173 | 174 | let player = player.unwrap(); 175 | 176 | Self { 177 | ground, 178 | enemies, 179 | barrels, 180 | player, 181 | total_enemies, 182 | end, 183 | using, 184 | weapon, 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/screens/game/mod.rs: -------------------------------------------------------------------------------- 1 | mod components; 2 | mod game; 3 | mod map; 4 | pub mod physics; 5 | 6 | pub use game::*; 7 | -------------------------------------------------------------------------------- /src/screens/game/physics.rs: -------------------------------------------------------------------------------- 1 | //! This file contains a helper physics struct and a bunch of helper conversion methods. 2 | 3 | use ggez::graphics::Color; 4 | #[cfg(feature = "debug")] 5 | use ggez::{ 6 | graphics::{self, DrawParam, Rect}, 7 | nalgebra::Point2, 8 | Context, GameResult, 9 | }; 10 | 11 | #[cfg(feature = "debug")] 12 | use ggez_goodies::{camera::Camera, nalgebra_glm::Vec2}; 13 | 14 | use ncollide2d::{pipeline::CollisionGroups, query::RayIntersection}; 15 | use nphysics2d::{ 16 | material, 17 | nalgebra::{Isometry2, Vector2}, 18 | ncollide2d::{ 19 | self, 20 | query::{ContactManifold, Ray}, 21 | shape::{Cuboid, ShapeHandle}, 22 | }, 23 | object::{ 24 | self, BodyPartHandle, BodyStatus, ColliderDesc, DefaultBodyHandle, RigidBody, RigidBodyDesc, 25 | }, 26 | world, 27 | }; 28 | 29 | use nphysics2d::nalgebra as na; 30 | use object::Collider; 31 | 32 | type N = f32; 33 | 34 | /// Enum that is made for each physics object's identity 35 | #[derive(PartialEq, Debug, Clone, Copy)] 36 | pub enum ObjectData { 37 | Ground, 38 | Player, 39 | Enemy, 40 | Bullet, 41 | Barrel, 42 | Particle(Color), 43 | } 44 | 45 | impl ObjectData { 46 | pub fn get_particle_data(&self) -> Color { 47 | match *self { 48 | ObjectData::Particle(particle) => particle, 49 | _ => unreachable!(), 50 | } 51 | } 52 | } 53 | 54 | /// Helper physics struct that makes lives easier while using nphysics2d physics engine with ggez. 55 | pub struct Physics { 56 | mechanical_world: world::DefaultMechanicalWorld, 57 | geometrical_world: world::DefaultGeometricalWorld, 58 | body_set: object::DefaultBodySet, 59 | collider_set: object::DefaultColliderSet, 60 | joint_constraint_set: nphysics2d::joint::DefaultJointConstraintSet, 61 | force_generator_set: nphysics2d::force_generator::DefaultForceGeneratorSet, 62 | } 63 | 64 | impl Physics { 65 | // TODO: Move the separate rigid body creator functions to use the create_rigid_body() and create_collider() functions in dead. 66 | 67 | /// The amount of gravity for the Y axis in the physics world. 68 | const GRAVITY: N = 300.0; 69 | 70 | /// Create a new physics struct object. 71 | pub fn new() -> Self { 72 | let geometrical_world = world::DefaultGeometricalWorld::new(); 73 | let gravity = Self::GRAVITY; 74 | 75 | let mechanical_world = world::DefaultMechanicalWorld::new(Vector2::new(0.0, gravity)); 76 | 77 | let body_set = object::DefaultBodySet::new(); 78 | let collider_set = object::DefaultColliderSet::new(); 79 | 80 | let joint_constraint_set = nphysics2d::joint::DefaultJointConstraintSet::new(); 81 | let force_generator_set = nphysics2d::force_generator::DefaultForceGeneratorSet::new(); 82 | 83 | Self { 84 | mechanical_world, 85 | geometrical_world, 86 | body_set, 87 | collider_set, 88 | joint_constraint_set, 89 | force_generator_set, 90 | } 91 | } 92 | 93 | /// Step the physics world. 94 | pub fn step(&mut self) { 95 | self.mechanical_world.step( 96 | &mut self.geometrical_world, 97 | &mut self.body_set, 98 | &mut self.collider_set, 99 | &mut self.joint_constraint_set, 100 | &mut self.force_generator_set, 101 | ); 102 | } 103 | 104 | // Creates a new tile body. 105 | pub fn create_tile( 106 | &mut self, 107 | pos: na::Point2, 108 | width: u16, 109 | height: u16, 110 | ) -> DefaultBodyHandle { 111 | let width = width as f32; 112 | let height = height as f32; 113 | 114 | let ground = RigidBodyDesc::new() 115 | .position(point_to_isometry(pos)) 116 | .status(BodyStatus::Static) 117 | .build(); 118 | let ground_handle = self.body_set.insert(ground); 119 | 120 | let shape = ShapeHandle::new(Cuboid::new(Vector2::new( 121 | width / 2.0 - 0.01, 122 | height / 2.0 - 0.01, 123 | ))); 124 | let collider = ColliderDesc::new(shape) 125 | .material(material::MaterialHandle::new(material::BasicMaterial::new( 126 | 0.0, 0.0, 127 | ))) 128 | .user_data(ObjectData::Ground) 129 | .build(BodyPartHandle(ground_handle, 0)); 130 | 131 | self.collider_set.insert(collider); 132 | 133 | ground_handle 134 | } 135 | 136 | /// Create a new player body. 137 | pub fn create_player( 138 | &mut self, 139 | pos: na::Point2, 140 | width: u16, 141 | height: u16, 142 | ) -> DefaultBodyHandle { 143 | let width = width as f32; 144 | let height = height as f32; 145 | 146 | let player = RigidBodyDesc::new() 147 | .position(point_to_isometry(pos)) 148 | .mass(10.0) 149 | .linear_damping(1.0) 150 | .status(BodyStatus::Dynamic) 151 | .build(); 152 | let player_handle = self.body_set.insert(player); 153 | 154 | let shape = ShapeHandle::new(Cuboid::new(Vector2::new( 155 | width / 2.0 - 0.01, 156 | height / 2.0 - 0.01, 157 | ))); 158 | let collider = ColliderDesc::new(shape) 159 | .material(material::MaterialHandle::new(material::BasicMaterial::new( 160 | 0.0, 0.0, 161 | ))) 162 | .user_data(ObjectData::Player) 163 | .build(BodyPartHandle(player_handle, 0)); 164 | 165 | self.collider_set.insert(collider); 166 | 167 | player_handle 168 | } 169 | 170 | /// Create a new enemy body. 171 | pub fn create_enemy( 172 | &mut self, 173 | pos: na::Point2, 174 | width: u16, 175 | height: u16, 176 | ) -> DefaultBodyHandle { 177 | let width = width as f32; 178 | let height = height as f32; 179 | 180 | let enemy = RigidBodyDesc::new() 181 | .position(point_to_isometry(pos)) 182 | .mass(10.0) 183 | .linear_damping(1.0) 184 | .status(BodyStatus::Dynamic) 185 | .build(); 186 | let enemy_handle = self.body_set.insert(enemy); 187 | 188 | let shape = ShapeHandle::new(Cuboid::new(Vector2::new( 189 | width / 2.0 - 0.01, 190 | height / 2.0 - 0.01, 191 | ))); 192 | let collider = ColliderDesc::new(shape) 193 | .material(material::MaterialHandle::new(material::BasicMaterial::new( 194 | 0.0, 0.0, 195 | ))) 196 | .user_data(ObjectData::Enemy) 197 | .build(BodyPartHandle(enemy_handle, 0)); 198 | 199 | self.collider_set.insert(collider); 200 | 201 | enemy_handle 202 | } 203 | 204 | /// Create a new enemy body. 205 | pub fn create_barrel( 206 | &mut self, 207 | pos: na::Point2, 208 | width: u16, 209 | height: u16, 210 | ) -> DefaultBodyHandle { 211 | let width = width as f32; 212 | let height = height as f32; 213 | 214 | let barrel = RigidBodyDesc::new() 215 | .position(point_to_isometry(pos)) 216 | .mass(10.0) 217 | .linear_damping(1.0) 218 | .status(BodyStatus::Dynamic) 219 | .build(); 220 | let barrel_handle = self.body_set.insert(barrel); 221 | 222 | let shape = ShapeHandle::new(Cuboid::new(Vector2::new( 223 | width / 2.0 - 0.01, 224 | height / 2.0 - 0.01, 225 | ))); 226 | let collider = ColliderDesc::new(shape) 227 | .material(material::MaterialHandle::new(material::BasicMaterial::new( 228 | 0.0, 0.0, 229 | ))) 230 | .user_data(ObjectData::Barrel) 231 | .build(BodyPartHandle(barrel_handle, 0)); 232 | 233 | self.collider_set.insert(collider); 234 | 235 | barrel_handle 236 | } 237 | 238 | /// Create a new bullet. Can be any included in crate::components::bullet::PlayerWeapon enum 239 | pub fn create_bullet( 240 | &mut self, 241 | pos: na::Point2, 242 | width: u16, 243 | height: u16, 244 | ) -> DefaultBodyHandle { 245 | let width = width as f32; 246 | let height = height as f32; 247 | 248 | let bullet = RigidBodyDesc::new() 249 | .position(point_to_isometry(pos)) 250 | .mass(10.0) 251 | .linear_damping(1.0) 252 | .status(BodyStatus::Dynamic) 253 | .build(); 254 | let bullet_handle = self.body_set.insert(bullet); 255 | 256 | let shape = ShapeHandle::new(Cuboid::new(Vector2::new( 257 | width / 2.0 - 0.01, 258 | height / 2.0 - 0.01, 259 | ))); 260 | let collider = ColliderDesc::new(shape) 261 | .material(material::MaterialHandle::new(material::BasicMaterial::new( 262 | 0.0, 0.0, 263 | ))) 264 | .user_data(ObjectData::Bullet) 265 | .build(BodyPartHandle(bullet_handle, 0)); 266 | 267 | self.collider_set.insert(collider); 268 | 269 | bullet_handle 270 | } 271 | 272 | /// Create a new rigid body 273 | pub fn create_rigid_body(&mut self, body: RigidBody) -> DefaultBodyHandle { 274 | let handle = self.body_set.insert(body); 275 | 276 | handle 277 | } 278 | 279 | /// Create a new collider 280 | pub fn create_collider(&mut self, collider: Collider) { 281 | self.collider_set.insert(collider); 282 | } 283 | 284 | /// Returns a immutable body from the handle provided by the above helper functions. 285 | pub fn get_rigid_body(&mut self, handle: DefaultBodyHandle) -> &RigidBody { 286 | let body = self.body_set.rigid_body(handle).expect("Body not found!"); 287 | 288 | body 289 | } 290 | 291 | /// Returns a mutable body from the handle provided by the above helper functions. 292 | pub fn get_rigid_body_mut(&mut self, handle: DefaultBodyHandle) -> &mut RigidBody { 293 | let body = self 294 | .body_set 295 | .rigid_body_mut(handle) 296 | .expect("Body not found!"); 297 | 298 | body 299 | } 300 | 301 | /// Simple helper function that allows you to see the colliders. 302 | /// To be able to show the colliders run Call of Ferris by `cargo run --features=["debug"]` 303 | #[cfg(feature = "debug")] 304 | pub fn draw_colliders(&self, ctx: &mut Context, camera: &Camera) -> GameResult { 305 | for (_, collider) in self.collider_set.iter() { 306 | let shape = collider.shape().aabb(collider.position()); 307 | 308 | let rect = graphics::Mesh::new_rectangle( 309 | ctx, 310 | graphics::DrawMode::Stroke(graphics::StrokeOptions::DEFAULT), 311 | Rect::new(0.0, 0.0, shape.extents().x, shape.extents().y), 312 | graphics::WHITE, 313 | )?; 314 | 315 | let pos = camera.calculate_dest_point(Vec2::new(shape.mins.x, shape.mins.y)); 316 | 317 | graphics::draw( 318 | ctx, 319 | &rect, 320 | DrawParam::default() 321 | .dest(Point2::new(pos.x, pos.y)) 322 | .offset(Point2::new(0.5, 0.5)), 323 | )?; 324 | } 325 | Ok(()) 326 | } 327 | 328 | /// Returns all of the collisions with the provided object 329 | pub fn collisions( 330 | &mut self, 331 | object: DefaultBodyHandle, 332 | ) -> Vec<( 333 | (ObjectData, ObjectData), 334 | DefaultBodyHandle, 335 | &ContactManifold, 336 | )> { 337 | self.geometrical_world 338 | .contacts_with(&self.collider_set, object, true) 339 | .into_iter() 340 | .flatten() 341 | .map(|(handle1, _, handle2, _, _, manifold)| { 342 | ( 343 | (self.get_user_data(handle1), self.get_user_data(handle2)), 344 | handle2, 345 | manifold, 346 | ) 347 | }) 348 | .collect() 349 | } 350 | 351 | /// Gets the user data of the 2 handles provided in the collisions function. 352 | pub fn get_user_data(&self, object: DefaultBodyHandle) -> ObjectData { 353 | let collider = self.collider_set.get(object).unwrap(); 354 | 355 | let data = *collider 356 | .user_data() 357 | .unwrap() 358 | .downcast_ref::() 359 | .expect("Invalid types"); 360 | 361 | data 362 | } 363 | 364 | /// Get the distance between a object 365 | pub fn distance(&mut self, object1: DefaultBodyHandle, object2: DefaultBodyHandle) -> f32 { 366 | let pos_1 = self.collider_set.get(object1).unwrap(); 367 | let pos_2 = self.collider_set.get(object2).unwrap(); 368 | 369 | ncollide2d::query::distance( 370 | pos_1.position(), 371 | pos_1.shape(), 372 | pos_2.position(), 373 | pos_2.shape(), 374 | ) 375 | } 376 | 377 | /// Perform a raycast 378 | pub fn ray_cast( 379 | &mut self, 380 | origin: na::Point2, 381 | dir: na::Vector2, 382 | ) -> Vec<( 383 | ObjectData, 384 | &Collider, 385 | RayIntersection, 386 | )> { 387 | let ray = Ray::new(origin, dir); 388 | 389 | self.geometrical_world 390 | .interferences_with_ray( 391 | &self.collider_set, 392 | &ray, 393 | f32::MAX, 394 | &CollisionGroups::default(), 395 | ) 396 | .map(|(handle, collider, intersection)| { 397 | (self.get_user_data(handle), collider, intersection) 398 | }) 399 | .collect() 400 | } 401 | 402 | pub fn destroy_body(&mut self, handle: DefaultBodyHandle) { 403 | self.body_set.remove(handle); 404 | self.collider_set.remove(handle); 405 | } 406 | } 407 | 408 | /// Converts isometry to point 409 | pub fn isometry_to_point( 410 | isometry: &Isometry2, 411 | ) -> na::Point2 { 412 | isometry.translation.vector.into() 413 | } 414 | 415 | /// Converts a point to isometry 416 | pub fn point_to_isometry( 417 | point: na::Point2, 418 | ) -> Isometry2 { 419 | Isometry2::translation(point.x, point.y) 420 | } 421 | -------------------------------------------------------------------------------- /src/screens/menu/menu.rs: -------------------------------------------------------------------------------- 1 | use ggez::{ 2 | event::KeyCode, 3 | graphics::{self, Scale, Text, TextFragment}, 4 | nalgebra::{Point2, Vector2}, 5 | Context, GameResult, 6 | }; 7 | use graphics::{Color, DrawParam}; 8 | use std::{process::exit, rc::Rc}; 9 | 10 | use crate::utils::AssetManager; 11 | use crate::Screen; 12 | 13 | pub struct Menu { 14 | asset_manager: Rc, 15 | } 16 | 17 | impl Menu { 18 | pub fn create(_ctx: &mut Context, asset_manager: Rc) -> Self { 19 | Self { asset_manager } 20 | } 21 | 22 | pub fn draw(&self, ctx: &mut Context) -> GameResult<()> { 23 | let (width, height) = graphics::drawable_size(ctx); 24 | 25 | let logo = self.asset_manager.get_image("logo.png"); 26 | let ferris_ninja = self.asset_manager.get_image("ferris_ninja.png"); 27 | let menu_bg = self.asset_manager.get_image("menu_bg.png"); 28 | 29 | let consolas = self.asset_manager.get_font("Consolas.ttf"); 30 | 31 | // Clear the screen 32 | graphics::clear(ctx, graphics::BLACK); 33 | 34 | graphics::draw( 35 | ctx, 36 | &menu_bg, 37 | DrawParam::default().scale(Vector2::new(0.6, 0.5)), 38 | )?; 39 | 40 | graphics::draw( 41 | ctx, 42 | &logo, 43 | DrawParam::default().dest(Point2::new(width - (logo.width() as f32 + 20.0), 10.0)), 44 | )?; 45 | 46 | graphics::draw( 47 | ctx, 48 | &ferris_ninja, 49 | DrawParam::default().dest(Point2::new( 50 | width - (width - 400.0), 51 | height - (ferris_ninja.height() + 140) as f32, 52 | )), 53 | )?; 54 | 55 | let press_and_to = TextFragment::new("Press & to") 56 | .font(consolas) 57 | .scale(Scale::uniform(15.0)); 58 | 59 | let press_pointer_to = TextFragment::new("Press * to") 60 | .font(consolas) 61 | .scale(Scale::uniform(15.0)); 62 | 63 | graphics::draw( 64 | ctx, 65 | &Text::new(press_and_to), 66 | DrawParam::default().dest(Point2::new( 67 | width - 200.0, 68 | height - (ferris_ninja.height() + 30) as f32, 69 | )), 70 | )?; 71 | 72 | let play_rect = graphics::Mesh::new_rectangle( 73 | ctx, 74 | graphics::DrawMode::fill(), 75 | graphics::Rect::new( 76 | width - 200.0, 77 | height - (ferris_ninja.height() + 10) as f32, 78 | 220.0, 79 | 40.0, 80 | ), 81 | Color::from_rgba(36, 36, 36, 128), 82 | )?; 83 | 84 | let quit_rect = graphics::Mesh::new_rectangle( 85 | ctx, 86 | graphics::DrawMode::fill(), 87 | graphics::Rect::new( 88 | width - 200.0, 89 | height - (ferris_ninja.height() - 70) as f32, 90 | 220.0, 91 | 40.0, 92 | ), 93 | Color::from_rgba(36, 36, 36, 128), 94 | )?; 95 | 96 | let play_text = TextFragment::new("PLAY") 97 | .font(consolas) 98 | .scale(Scale::uniform(20.0)); 99 | 100 | let quit_text = TextFragment::new("QUIT") 101 | .font(consolas) 102 | .scale(Scale::uniform(20.0)); 103 | 104 | graphics::draw(ctx, &play_rect, DrawParam::default())?; 105 | graphics::draw(ctx, &quit_rect, DrawParam::default())?; 106 | 107 | graphics::draw( 108 | ctx, 109 | &Text::new(play_text), 110 | DrawParam::default().dest(Point2::new( 111 | width - 170.0, 112 | height - ferris_ninja.height() as f32, 113 | )), 114 | )?; 115 | 116 | graphics::draw( 117 | ctx, 118 | &Text::new(press_pointer_to), 119 | DrawParam::default().dest(Point2::new( 120 | width - 200.0, 121 | height - (ferris_ninja.height() - 50) as f32, 122 | )), 123 | )?; 124 | 125 | graphics::draw( 126 | ctx, 127 | &Text::new(quit_text), 128 | DrawParam::default().dest(Point2::new( 129 | width - 170.0, 130 | height - (ferris_ninja.height() - 80) as f32, 131 | )), 132 | )?; 133 | 134 | graphics::present(ctx) 135 | } 136 | 137 | pub fn update(&self, _ctx: &mut Context) -> GameResult { 138 | Ok(()) 139 | } 140 | 141 | pub fn key_press(&self, keycode: KeyCode) -> Option { 142 | if keycode == KeyCode::Key7 { 143 | return Some(Screen::Play); 144 | } else if keycode == KeyCode::Key8 { 145 | exit(0); 146 | } 147 | 148 | None 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/screens/menu/mod.rs: -------------------------------------------------------------------------------- 1 | mod menu; 2 | 3 | pub use menu::*; 4 | -------------------------------------------------------------------------------- /src/screens/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod dead; 2 | pub mod game; 3 | pub mod menu; 4 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, collections::HashMap, error::Error, io::Read, sync::Mutex}; 2 | 3 | use ggez::{ 4 | audio::Source, 5 | graphics::{self, Color, DrawMode, Font, Image, Mesh}, 6 | nalgebra::Point2, 7 | timer, Context, GameResult, 8 | }; 9 | 10 | use ggez_goodies::{camera::Camera, nalgebra_glm::Vec2}; 11 | use graphics::DrawParam; 12 | use nphysics2d::{ 13 | algebra::Velocity2, 14 | nalgebra as na, 15 | ncollide2d::shape::{Ball, ShapeHandle}, 16 | object::{BodyPartHandle, ColliderDesc, DefaultBodyHandle, RigidBodyDesc}, 17 | }; 18 | use rand::Rng; 19 | 20 | use crate::game::physics::{isometry_to_point, point_to_isometry, ObjectData, Physics}; 21 | 22 | pub type FerrisResult = Result>; 23 | 24 | pub fn lerp(from: f32, to: f32, dt: f32) -> f32 { 25 | from + dt * (to - from) 26 | } 27 | 28 | pub fn remap(n: f32, start1: f32, stop1: f32, start2: f32, stop2: f32) -> f32 { 29 | ((n - start1) / (stop1 - start1)) * (stop2 - start2) + start2 30 | } 31 | 32 | #[macro_export] 33 | macro_rules! play { 34 | ($exp:expr) => { 35 | let mut sound = $exp 36 | .lock() 37 | .expect(format!("Cannot load {}.mp3", stringify!($exp)).as_str()); 38 | 39 | sound 40 | .play() 41 | .expect(format!("Cannot play {}.mp3", stringify!($exp)).as_str()); 42 | }; 43 | } 44 | 45 | enum Asset { 46 | Image(Image), 47 | Font(Font), 48 | Audio(Mutex), 49 | File(String), 50 | } 51 | 52 | pub struct AssetManager { 53 | assets: HashMap, 54 | } 55 | 56 | impl AssetManager { 57 | pub fn new() -> Self { 58 | Self { 59 | assets: HashMap::new(), 60 | } 61 | } 62 | 63 | pub fn load_image(&mut self, ctx: &mut Context, filename: Cow<'_, str>) { 64 | self.assets.insert( 65 | filename.to_string(), 66 | Asset::Image( 67 | Image::new(ctx, format!("/images/{}", filename)) 68 | .unwrap_or_else(|_| panic!("Cannot load {}", filename)), 69 | ), 70 | ); 71 | } 72 | 73 | pub fn load_font(&mut self, ctx: &mut Context, filename: Cow<'_, str>) { 74 | self.assets.insert( 75 | filename.to_string(), 76 | Asset::Font( 77 | Font::new(ctx, format!("/fonts/{}", filename)) 78 | .unwrap_or_else(|_| panic!("Cannot load {}", filename)), 79 | ), 80 | ); 81 | } 82 | 83 | pub fn load_sound(&mut self, ctx: &mut Context, filename: Cow<'_, str>) { 84 | self.assets.insert( 85 | filename.to_string(), 86 | Asset::Audio(Mutex::new( 87 | Source::new(ctx, format!("/audio/{}", filename)) 88 | .unwrap_or_else(|_| panic!("Cannot load {}", filename)), 89 | )), 90 | ); 91 | } 92 | 93 | pub fn load_file(&mut self, ctx: &mut Context, folder: &str, filename: Cow<'_, str>) { 94 | let path = format!("/{}/{}", folder, filename); 95 | 96 | let mut file = ggez::filesystem::open(ctx, &path).unwrap(); 97 | let mut buffer = String::new(); 98 | 99 | file.read_to_string(&mut buffer).unwrap(); 100 | 101 | self.assets.insert(path, Asset::File(buffer)); 102 | } 103 | 104 | pub fn get_image(&self, filename: &str) -> Image { 105 | match self.assets.get(&filename.to_string()).unwrap() { 106 | Asset::Image(image) => image.to_owned(), 107 | _ => panic!(), 108 | } 109 | } 110 | 111 | pub fn get_font(&self, filename: &str) -> Font { 112 | match self.assets.get(&filename.to_string()).unwrap() { 113 | Asset::Font(font) => font.to_owned(), 114 | _ => panic!(), 115 | } 116 | } 117 | 118 | pub fn get_sound(&self, filename: &str) -> &Mutex { 119 | match self.assets.get(&filename.to_string()).unwrap() { 120 | Asset::Audio(audio) => audio, 121 | _ => panic!(), 122 | } 123 | } 124 | 125 | pub fn get_file(&self, filename: &str) -> String { 126 | match self.assets.get(&filename.to_string()).unwrap() { 127 | Asset::File(file) => file.to_owned(), 128 | _ => panic!(), 129 | } 130 | } 131 | } 132 | 133 | pub struct ParticleSystem { 134 | particles: Vec, 135 | lifetime: f32, 136 | } 137 | 138 | impl ParticleSystem { 139 | const PARTICLE_JUICE: f32 = 300.0; 140 | 141 | pub fn new( 142 | physics: &mut Physics, 143 | amount: usize, 144 | min: na::Point2, 145 | max: na::Point2, 146 | ) -> Self { 147 | let rng = &mut rand::thread_rng(); 148 | let mut particles = vec![]; 149 | 150 | for _ in 0..amount { 151 | let position = 152 | na::Point2::new(rng.gen_range(min.x..=max.x), rng.gen_range(min.y..=max.y)); 153 | let mut body = RigidBodyDesc::new() 154 | .mass(0.1) 155 | .position(point_to_isometry(position)) 156 | .build(); 157 | 158 | body.set_velocity(Velocity2::linear( 159 | rng.gen_range(-Self::PARTICLE_JUICE..=Self::PARTICLE_JUICE), 160 | rng.gen_range(-Self::PARTICLE_JUICE..=Self::PARTICLE_JUICE), 161 | )); 162 | 163 | let g = rng.gen_range(0..=255); 164 | 165 | let handle = physics.create_rigid_body(body); 166 | let shape = ShapeHandle::new(Ball::new(2.0)); 167 | let collider = ColliderDesc::new(shape) 168 | .user_data(ObjectData::Particle(Color::from_rgb(255, g, 0))) 169 | .build(BodyPartHandle(handle, 0)); 170 | 171 | physics.create_collider(collider); 172 | 173 | particles.push(handle); 174 | } 175 | 176 | Self { 177 | particles, 178 | lifetime: 2.0, 179 | } 180 | } 181 | 182 | pub fn draw( 183 | &self, 184 | ctx: &mut Context, 185 | physics: &mut Physics, 186 | camera: &mut Camera, 187 | ) -> GameResult<()> { 188 | for particle in &self.particles { 189 | let body = physics.get_rigid_body(*particle); 190 | let position = isometry_to_point(body.position()); 191 | let color = physics.get_user_data(*particle).get_particle_data(); 192 | 193 | let color = Color::new(color.r, color.g, color.b, self.lifetime); 194 | 195 | let particle_mesh = Mesh::new_circle( 196 | ctx, 197 | DrawMode::fill(), 198 | Point2::new(0.0, 0.0), 199 | 2.0, 200 | 0.1, 201 | color, 202 | )?; 203 | 204 | let camera_pos = camera.calculate_dest_point(Vec2::new(position.x, position.y)); 205 | 206 | graphics::draw( 207 | ctx, 208 | &particle_mesh, 209 | DrawParam::default() 210 | .dest(Point2::new(camera_pos.x, camera_pos.y)) 211 | .offset(Point2::new(0.5, 0.5)), 212 | )?; 213 | } 214 | 215 | Ok(()) 216 | } 217 | 218 | pub fn update(&mut self, ctx: &mut Context, physics: &mut Physics) -> bool { 219 | self.lifetime -= timer::delta(ctx).as_secs_f32(); 220 | 221 | if self.lifetime <= 0.0 { 222 | for id in 0..self.particles.len() { 223 | // Destroy the particle from the world. The lifetime of the particle has been ended 224 | physics.destroy_body(self.particles[id]); 225 | } 226 | 227 | true 228 | } else { 229 | false 230 | } 231 | } 232 | } 233 | --------------------------------------------------------------------------------