//! Game objects and resource enums use std::collections::HashMap; use sdl2::{ event::Event, keyboard::Scancode, rect::Rect, render::{ Canvas, TextureCreator }, video::{ Window, WindowContext } }; use ycraft::{ collision::CollisionShape, obj::{ ControlObjectBehavior, Frame, GameObjectBehavior, GameObjectState, Sprite }, res::{ Font, Image, Sound }, room::Room }; const MOVE_SPD: f64 = 512.0; const JUMP_SPD: f64 = 1500.0; const ACC: f64 = 50.0; const GRAVITY: f64 = 4096.0; const EXTRA_GRAV: f64 = 13000.0; const BRICK_POS: [(f64, f64); 6] = [ (96.0, 900.0), (160.0, 1000.0), (224.0, 1000.0), (288.0, 1000.0), (352.0, 1000.0), (416.0, 1000.0) ]; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum Img { Brick, Character } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum Snd { Music, Jump } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum Fnt { Geist } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum Spr { Brick, Idle, Walk } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub enum Rm { Room0 } #[derive(Clone, Copy)] pub enum Data { Brick(usize), Player } #[derive(Clone)] struct Brick { state: GameObjectState, def_pos: (f64, f64), should_die: bool } impl Brick { pub fn new(def_pos: (f64, f64), id: usize) -> Self { Self { state: GameObjectState { name: "brick".to_string(), pos: def_pos, collider: CollisionShape::Rect { center: (0, 0), size: (64, 64) }, cur_spr: Spr::Brick, sprs: HashMap::from([( Spr::Brick, Sprite::new( vec![ Frame::new(Img::Brick, Rect::new(0, 0, 32, 32), (64, 64)) ], 0.0, (16, 16) ) )]), custom: Data::Brick(id) }, def_pos, should_die: false } } } impl GameObjectBehavior for Brick { fn state(&self) -> GameObjectState { self.state.clone() } fn set_state(&mut self, new_state: &GameObjectState) { self.state = new_state.clone(); } fn handle_sdl_event(&mut self, event: &Event) { match event { Event::KeyUp { scancode, .. } if *scancode == Some(Scancode::Escape) => { self.should_die = true; }, _ => {} } } fn should_remove(&self) -> bool { self.should_die } fn on_reset(&mut self) -> bool { self.state.pos = self.def_pos; self.should_die = false; false } } #[derive(Clone)] struct Player { state: GameObjectState, def_pos: (f64, f64), vel: (f64, f64), right: bool, left: bool, grounded: bool, extra_grav: bool, facing_right: bool, play_jump_sound: bool, should_reset: bool } impl Player { pub fn new(def_pos: (f64, f64)) -> Self { Self { state: GameObjectState { name: "player".to_string(), pos: def_pos, collider: CollisionShape::Rect { center: (0, 0), size: (64, 64) }, cur_spr: Spr::Idle, sprs: HashMap::from([ ( Spr::Idle, Sprite::new( vec![Frame::new(Img::Character, Rect::new(0, 0, 32, 32), (128, 128))], 0.0, (16, 16) ) ), ( Spr::Walk, Sprite::new( vec![ Frame::new(Img::Character, Rect::new(0, 0, 32, 32), (128, 128)), Frame::new(Img::Character, Rect::new(32, 0, 32, 32), (128, 128)) ], 12.0, (16, 16) ) ) ]), custom: Data::Player }, def_pos, vel: (0.0, 0.0), right: false, left: false, grounded: false, extra_grav: false, facing_right: true, play_jump_sound: false, should_reset: false } } } impl GameObjectBehavior for Player { fn state(&self) -> GameObjectState { self.state.clone() } fn set_state(&mut self, new_state: &GameObjectState) { self.state = new_state.clone(); } fn on_reset(&mut self) -> bool { self.state.pos = self.def_pos; self.right = false; self.left = false; self.grounded = false; self.state.cur_spr = Spr::Idle; self.extra_grav = false; self.facing_right = true; self.play_jump_sound = false; self.should_reset = false; false } fn handle_sdl_event(&mut self, event: &Event) { match event { Event::KeyDown { scancode, .. } if *scancode == Some(Scancode::Up) => { if self.grounded { self.state.pos.1 -= 1.0; self.vel.1 = -JUMP_SPD; self.play_jump_sound = true; } }, Event::KeyDown { scancode, .. } if *scancode == Some(Scancode::Left) => { self.left = true; }, Event::KeyDown { scancode, .. } if *scancode == Some(Scancode::Right) => { self.right = true; }, Event::KeyUp { scancode, .. } if *scancode == Some(Scancode::Up) => { if self.vel.1 < -0.1 { self.extra_grav = true; } }, Event::KeyUp { scancode, .. } if *scancode == Some(Scancode::Left) => { self.left = false; }, Event::KeyUp { scancode, .. } if *scancode == Some(Scancode::Right) => { self.right = false; }, Event::KeyUp { scancode, .. } if *scancode == Some(Scancode::R) => { self.should_reset = true; }, _ => {} } } fn update( &mut self, delta: f64, _ctl_objs: &Vec>>, others: &Vec>>) -> ( Option, Vec>> ) { if self.should_reset { // We can destroy bricks, so when we reset, we want to respawn the bricks that have // been destroyed. It should be all of them, but technically we don't know, so we have // to formulaically figure it out. let mut existing_bricks = Vec::new(); for obj in others.iter() { if let Data::Brick(id) = obj.state().custom { existing_bricks.push(id); } } let to_respawn = (0..=5).into_iter() .map(|id| if !existing_bricks.contains(&id) { Some( Box::new(Brick::new(BRICK_POS[id], id)) as Box> ) } else { None }).filter(|brick| brick.is_some()) .map(|brick| brick.unwrap()) .collect::>>(); return (Some(Rm::Room0), to_respawn); } self.state.pos.0 += self.vel.0 * delta; self.state.pos.1 += self.vel.1 * delta; // Animate if let Some(spr) = self.state.sprs.get_mut(&self.state.cur_spr) { spr.scale = (0.5, 0.5); spr.flip = (!self.facing_right, false); } if !self.grounded && self.state.cur_spr != Spr::Idle { self.state.cur_spr = Spr::Idle; } else if self.grounded { if self.vel.0.abs() < MOVE_SPD * 0.8 && self.state.cur_spr != Spr::Idle { self.state.cur_spr = Spr::Idle; } else if self.vel.0.abs() >= MOVE_SPD * 0.8 && self.state.cur_spr != Spr::Walk { self.state.cur_spr = Spr::Walk; self.facing_right = self.vel.0 > 0.1; } } // Update vel at the end, so collisions can affect it! let hor = (if self.right { 1.0 } else { 0.0 }) + (if self.left { -1.0 } else { 0.0 }); self.vel.0 = ycraft::util::lerp(self.vel.0, hor * MOVE_SPD, ACC * delta); if let CollisionShape::Rect { center, size } = self.state.collider { let gnd_check = CollisionShape::Rect { center: ( center.0 + self.state.pos.0 as i32, center.1 + (self.state.pos.1 + self.vel.1 * delta) as i32 + 1 ), size }; self.grounded = false; for other in others.iter() { if self.state.pos.1 > other.state().pos.1 - size.1 as f64 * 0.8 { continue; } if let Data::Brick(_) = other.state().custom { let mut other_col = other.state().collider.clone(); if let CollisionShape::Rect { center: ref mut other_center, .. } = other_col { other_center.0 += other.state().pos.0 as i32; other_center.1 += other.state().pos.1 as i32; }; if gnd_check.collides_with(&other_col) { if let CollisionShape::Rect { center, size } = other_col { self.grounded = true; self.vel.1 = 0.0; self.state.pos.1 = (center.1 - size.1 as i32 - 1) as f64; } } } } } if self.grounded || self.vel.1 > 0.1 { self.extra_grav = false; } if self.extra_grav { self.vel.1 += EXTRA_GRAV * delta; } else { self.vel.1 += GRAVITY * delta; } (None, vec![]) } fn on_collision( &mut self, other: &Box>) { if let Data::Brick(_) = other.state().custom { if (other.state().pos.0 <= self.state.pos.0 && self.vel.0 < -0.1) || (other.state().pos.0 >= self.state.pos.0 && self.vel.0 > 0.1) { self.state.pos.0 -= if self.vel.0 > 0.1 { 1.0 } else { -1.0 }; self.vel.0 = 0.0; } } } fn render( &mut self, cnv: &mut Canvas, imgs: &HashMap, snds: &HashMap, _fonts: &HashMap, _creator: &TextureCreator, elapsed: f64) -> Result<(), String> { if self.play_jump_sound { snds[&Snd::Jump].play()?; self.play_jump_sound = false; } if !Sound::is_music_playing() { snds[&Snd::Music].play()?; } // Default render let mut state = self.state().clone(); let GameObjectState { ref mut sprs, ref mut cur_spr, pos, .. } = state; if let Some(spr) = sprs.get_mut(cur_spr) { spr.update(elapsed); spr.render(cnv, imgs, (pos.0 as i32, pos.1 as i32))?; } self.set_state(&state); Ok(()) } } pub fn room0() -> Room { Room::new( vec![ Box::new(Player::new((256.0, 900.0))), Box::new(Brick::new(BRICK_POS[0], 0)), Box::new(Brick::new(BRICK_POS[1], 1)), Box::new(Brick::new(BRICK_POS[2], 2)), Box::new(Brick::new(BRICK_POS[3], 3)), Box::new(Brick::new(BRICK_POS[4], 4)), Box::new(Brick::new(BRICK_POS[5], 5)) ], false ) }