/* * Encapsulates all the game state for the vector war application inside * a single structure. This makes it trivial to implement our GGPO * save and load functions. */ use crate::vectorwar::Input; use enumflags2::BitFlags; use ggpo::game_input::FrameNum; use log::info; use sdl2::rect::Rect; use std::f64::consts::PI; pub const STARTING_HEALTH: i32 = 100; pub const ROTATE_INCREMENT: f64 = 3.; pub const SHIP_RADIUS: f64 = 15.; pub const SHIP_WIDTH: f64 = 8.; pub const SHIP_TUCK: f64 = 3.; pub const SHIP_THRUST: f64 = 0.06; pub const SHIP_MAX_THRUST: f64 = 4.0; pub const SHIP_BREAK_SPEED: f64 = 0.6; pub const BULLET_SPEED: f64 = 5.; pub const MAX_BULLETS: usize = 30; pub const BULLET_COOLDOWN: f64 = 8.; pub const BULLET_DAMAGE: f64 = 10.; pub const MAX_SHIPS: usize = 4; #[derive(Copy, Clone)] pub struct Position { pub x: f64, pub y: f64, } impl Position { pub const fn new() -> Self { Self { x: 0., y: 0. } } pub fn distance(&self, rhs: &Position) -> f64 { let x = rhs.x - self.x; let y = rhs.y - self.y; ((x * x) + (y * y)).sqrt() } } #[derive(Copy, Clone)] pub struct Velocity { pub dx: f64, pub dy: f64, } impl Velocity { pub const fn new() -> Self { Self { dx: 0., dy: 0. } } } #[derive(Copy, Clone)] pub struct Bullet { pub active: bool, pub position: Position, pub velocity: Velocity, } impl Bullet { pub const fn new() -> Self { Self { active: false, position: Position::new(), velocity: Velocity::new(), } } } #[derive(Copy, Clone)] pub struct Ship { pub position: Position, pub velocity: Velocity, pub radius: u32, pub heading: i32, pub health: i32, pub speed: i32, pub cooldown: i32, pub bullets: [Bullet; MAX_BULLETS], pub score: i32, } impl Ship { pub const fn new() -> Self { Self { position: Position::new(), velocity: Velocity::new(), radius: 0, heading: 0, health: 0, speed: 0, cooldown: 0, bullets: [Bullet::new(); MAX_BULLETS], score: 0, } } } pub struct GameState { pub frame_number: FrameNum, pub bounds: Rect, pub num_ships: u32, pub ships: [Ship; MAX_SHIPS], } impl GameState { pub fn new() -> Self { Self { frame_number: 0, bounds: Rect::new(0, 0, 0, 0), num_ships: 0, ships: [Ship::new(); MAX_SHIPS], } } pub fn init(&mut self, num_players: u32, width: u32, height: u32) { self.bounds.set_width(width); self.bounds.set_height(height); let r = height / 4; self.frame_number = 0; self.num_ships = num_players; for i in 0..self.num_ships as usize { let heading = i as u32 * 360 / num_players; let theta = heading as f64 * PI / 180.; let (sint, cost) = theta.sin_cos(); self.ships[i].position.x = (width as f64 / 2.) + r as f64 * cost; self.ships[i].position.y = (height as f64 / 2.) + r as f64 * sint; self.ships[i].heading = ((heading as f64 + 180.) % 360.) as i32; self.ships[i].health = STARTING_HEALTH; self.ships[i].radius = SHIP_RADIUS as u32; } } pub fn get_ship_ai(&self, i: usize, heading: &mut f64, thrust: &mut f64, fire: &mut bool) { *heading = ((self.ships[i].heading + 5) % 360) as f64; *thrust = 0.; *fire = false; } pub fn parse_ship_inputs( &self, inputs: BitFlags, i: usize, heading: &mut f64, thrust: &mut f64, fire: &mut bool, ) { let ship = &self.ships[i]; info!("parsing ship {} input: {:#?}.\n", i, inputs); if inputs.contains(Input::RotateRight) { *heading = (ship.heading as f64 + ROTATE_INCREMENT) % 360.; } else if inputs.contains(Input::RotateLeft) { *heading = (ship.heading as f64 - ROTATE_INCREMENT + 360.) % 360.; } else { *heading = ship.heading as f64; } if inputs.contains(Input::Thrust) { *thrust = SHIP_THRUST; } else if inputs.contains(Input::Break) { *thrust = -SHIP_THRUST; } else { *thrust = 0.; } *fire = inputs.contains(Input::Fire); } pub fn move_ship(&mut self, i: usize, heading: f64, thrust: f64, fire: bool) { let mut ship = self.ships[i]; info!( "calculation of new ship coordinates: (thrust:{:.4} heading:{:.4}).\n", thrust, heading ); ship.heading = heading as i32; if ship.cooldown == 0 { if fire { info!("firing bullet.\n"); for i in 0..MAX_BULLETS { let (dy, dx) = (ship.heading as f64).to_radians().sin_cos(); let bullet = &mut ship.bullets[i]; if !bullet.active { bullet.active = true; bullet.position.x = ship.position.x + (ship.radius as f64 * dx); bullet.position.y = ship.position.y + (ship.radius as f64 * dy); bullet.velocity.dx = ship.velocity.dx + (BULLET_SPEED * dx); bullet.velocity.dy = ship.velocity.dy + (BULLET_SPEED * dy); ship.cooldown = BULLET_COOLDOWN as i32; break; } } } } if thrust > 0. { let (dy, dx) = (ship.heading as f64).to_radians().sin_cos(); let (dx, dy) = (dx * thrust, dy * thrust); ship.velocity.dx += dx; ship.velocity.dy += dy; let mag: f64 = (ship.velocity.dx * ship.velocity.dx + ship.velocity.dy * ship.velocity.dy).sqrt(); if mag > SHIP_MAX_THRUST { ship.velocity.dx = (ship.velocity.dx * SHIP_MAX_THRUST) / mag; ship.velocity.dy = (ship.velocity.dy * SHIP_MAX_THRUST) / mag; } info!( "new ship velocity: (dx:{:.4} dy:{:.2}).\n", ship.velocity.dx, ship.velocity.dy ); ship.position.x += ship.velocity.dx; ship.position.y += ship.velocity.dy; info!( "new ship position: (dx:{:.4} dy:{:.2}).\n", ship.position.x, ship.position.y ); if ship.position.x - (ship.radius as f64) < self.bounds.y() as f64 || ship.position.y + ship.radius as f64 > self.bounds.y() as f64 + self.bounds.height() as f64 { ship.velocity.dx *= -1.; ship.position.x += ship.velocity.dx * 2.; } if ship.position.y - (ship.radius as f64) < self.bounds.y() as f64 || ship.position.y + (ship.radius as f64) > self.bounds.y() as f64 + self.bounds.height() as f64 { ship.velocity.dy *= -1.; ship.position.y += ship.velocity.dy * 2.; } for i in 0..MAX_BULLETS { let bullet = &mut ship.bullets[i]; if bullet.active { bullet.position.x += bullet.velocity.dx; bullet.position.y += bullet.velocity.dy; if bullet.position.x < self.bounds.x() as f64 || bullet.position.y < self.bounds.x() as f64 || bullet.position.x > self.bounds.x() as f64 + self.bounds.width() as f64 || bullet.position.y > self.bounds.y() as f64 + self.bounds.height() as f64 { bullet.active = false; } else { for j in 0..self.num_ships as usize { if bullet.position.distance(&self.ships[j].position) < self.ships[j].radius as f64 { ship.score += 1; self.ships[j].health -= BULLET_DAMAGE as i32; bullet.active = false; break; } } } } } } self.ships[i] = ship; } pub fn update(&mut self, inputs: &[BitFlags], disconnect_flags: i32) { self.frame_number += 1; for i in 0..self.num_ships as usize { let (mut thrust, mut heading, mut fire): (f64, f64, bool) = (0., 0., false); if (disconnect_flags & ((i as i32) << i)) > 0 { self.get_ship_ai(i, &mut heading, &mut thrust, &mut fire); } else { self.parse_ship_inputs(inputs[i], i, &mut heading, &mut thrust, &mut fire); } self.move_ship(i, heading, thrust, fire); if self.ships[i].cooldown > 0 { self.ships[i].cooldown -= 1; } } } }