diff --git a/enterprise/src/main.rs b/enterprise/src/main.rs index 23b4d1d..98b5230 100644 --- a/enterprise/src/main.rs +++ b/enterprise/src/main.rs @@ -28,6 +28,7 @@ const SHIP_TEXTURES: &[ImageSource] = &[ include_image!("../../resources/Kraken.png"), include_image!("../../resources/PTBoat.png"), include_image!("../../resources/Liberty.png"), + include_image!("../../resources/UBoat.png"), ]; const SPRITES: &[ImageSource] = &[ @@ -36,6 +37,8 @@ const SPRITES: &[ImageSource] = &[ include_image!("../../resources/Smoke.png"), ]; +const WAKE: ImageSource = include_image!("../../resources/Wake.png"); + struct Ship { coords: Pos2, angle: f32, @@ -57,6 +60,7 @@ enum MidwayMessage { Sunk(String), Radius(f32), Splash(f32, f32, f32, f32, usize, Color32), + Wake(f32, f32, f32, f32, f32, f32), } struct MidwayData { @@ -68,6 +72,7 @@ struct MidwayData { ship_data: ShipData, ships: HashMap, splashes: Vec<(f32, f32, f32, Instant, usize, Color32)>, + wakes: Vec<(f32, f32, f32, f32, Instant, f32, f32)>, } impl MidwayData { @@ -81,6 +86,7 @@ impl MidwayData { ship_data: ShipData::default(), ships: HashMap::new(), splashes: Vec::new(), + wakes: Vec::new(), } } } @@ -221,34 +227,34 @@ fn draw_midway(ui: &Ui, data: &mut MidwayData) -> Option<()> { data.stream.write_all(b"smoke\n").ok(); } if i.key_pressed(Key::Num1) { - data.stream.write_all(b"weapon 1\n").ok(); + data.stream.write_all(b"action 1\n").ok(); } if i.key_pressed(Key::Num2) { - data.stream.write_all(b"weapon 2\n").ok(); + data.stream.write_all(b"action 2\n").ok(); } if i.key_pressed(Key::Num3) { - data.stream.write_all(b"weapon 3\n").ok(); + data.stream.write_all(b"action 3\n").ok(); } if i.key_pressed(Key::Num4) { - data.stream.write_all(b"weapon 4\n").ok(); + data.stream.write_all(b"action 4\n").ok(); } if i.key_pressed(Key::Num5) { - data.stream.write_all(b"weapon 5\n").ok(); + data.stream.write_all(b"action 5\n").ok(); } if i.key_pressed(Key::Num6) { - data.stream.write_all(b"weapon 6\n").ok(); + data.stream.write_all(b"action 6\n").ok(); } if i.key_pressed(Key::Num7) { - data.stream.write_all(b"weapon 7\n").ok(); + data.stream.write_all(b"action 7\n").ok(); } if i.key_pressed(Key::Num8) { - data.stream.write_all(b"weapon 9\n").ok(); + data.stream.write_all(b"action 9\n").ok(); } if i.key_pressed(Key::Num9) { - data.stream.write_all(b"weapon 9\n").ok(); + data.stream.write_all(b"action 9\n").ok(); } if i.key_pressed(Key::Num0) { - data.stream.write_all(b"weapon 10\n").ok(); + data.stream.write_all(b"action 10\n").ok(); } if (data.scale < 25) && i.key_pressed(Key::Minus) { data.scale += 1; @@ -278,6 +284,11 @@ fn draw_midway(ui: &Ui, data: &mut MidwayData) -> Option<()> { texture, colour, )), + MidwayMessage::Wake(x, y, size, angle, duration, velocity) => { + data + .wakes + .push((x, y, size, angle, Instant::now(), duration, velocity)) + } }; } let painter = ui.painter(); @@ -314,6 +325,19 @@ fn draw_midway(ui: &Ui, data: &mut MidwayData) -> Option<()> { let Pos2 { x: _, y } = render_state.transform(pos2(0.0, y)); painter.hline(0.0..=screen_size.x, y, PathStroke::new(2.0, Color32::BLUE)); } + // Wakes + data + .wakes + .retain(|(x, y, size, angle, instant, duration, velocity)| { + let coords = render_state.transform(pos2(*x, *y)); + let elapsed = instant.elapsed().as_secs_f32(); + let scale = render_state.scale(*size + velocity * elapsed); + let rect = Rect::from_center_size(coords, Vec2::splat(scale)); + Image::new(WAKE) + .rotate(*angle, Vec2::splat(0.5)) + .paint_at(ui, rect); + elapsed <= *duration + }); // Ships for (ship, data) in &data.ships { let coords = render_state.transform(data.coords); @@ -547,6 +571,40 @@ fn handle_midway_connection(stream: TcpStream, tx: &Sender) -> Op tx.send(MidwayMessage::Splash(x, y, size, duration, texture, colour)) .ok()?; } + Some("wake") => { + let Some(x) = words.next().and_then(|w| w.parse().ok()) else { + println!("Invalid input"); + buf.clear(); + continue; + }; + let Some(y) = words.next().and_then(|w| w.parse().ok()) else { + println!("Invalid input"); + buf.clear(); + continue; + }; + let Some(size) = words.next().and_then(|w| w.parse().ok()) else { + println!("Invalid input"); + buf.clear(); + continue; + }; + let Some(angle) = words.next().and_then(|w| w.parse().ok()) else { + println!("Invalid input"); + buf.clear(); + continue; + }; + let Some(duration) = words.next().and_then(|w| w.parse().ok()) else { + println!("Invalid input"); + buf.clear(); + continue; + }; + let Some(velocity) = words.next().and_then(|w| w.parse().ok()) else { + println!("Invalid input"); + buf.clear(); + continue; + }; + tx.send(MidwayMessage::Wake(x, y, size, angle, duration, velocity)) + .ok()?; + } _ => println!("Unknown line"), } buf.clear(); diff --git a/midway/src/client.rs b/midway/src/client.rs index 0b42eb6..59a640e 100644 --- a/midway/src/client.rs +++ b/midway/src/client.rs @@ -10,7 +10,7 @@ pub enum ClientMessage { Sail(f32, f32), Anchor, Smoke, - Weapon(u32), + Action(usize), } pub struct ClientData { @@ -90,9 +90,9 @@ fn process_client(mut stream: BufReader, tx: &Sender) } Some("anchor") => tx.send(ClientMessage::Anchor).ok()?, Some("smoke") => tx.send(ClientMessage::Smoke).ok()?, - Some("weapon") => { - let weapon = words.next().and_then(|w| w.parse().ok())?; - tx.send(ClientMessage::Weapon(weapon)).ok()?; + Some("action") => { + let action = words.next().and_then(|w| w.parse().ok())?; + tx.send(ClientMessage::Action(action)).ok()?; } Some(word) => println!("Bad message {word} from client"), None => println!("Empty message from client"), diff --git a/midway/src/main.rs b/midway/src/main.rs index d764896..a231e8d 100644 --- a/midway/src/main.rs +++ b/midway/src/main.rs @@ -1,8 +1,8 @@ //! Server for WW2 naval combat simulator +use crate::stats::{get_random_ship, Action, ShipStats, Variable}; use client::{process_joining, ClientData, ClientMessage}; use rand::seq::SliceRandom; use rand::{thread_rng, Rng}; -use stats::{get_random_ship, ShipStats}; use std::cmp::Ordering; use std::collections::HashMap; use std::f32::consts::PI; @@ -28,14 +28,15 @@ const MAP_RADIUS: Option<(f32, BorderType)> = Some(( mine_spawn_chance: 0.0001, mine_damage: 2000.0, scale: 500.0, - intensity: 40.0, - dps: 1.0, + intensity: 18.0, + dps: 5.0, }), )); const KRAKEN_NAME: &str = "Kraken"; const WATER_VISCOSITY: f32 = 0.000_001; const GRAVITY: f32 = 9.81; +const GUN_ACCURACY: f32 = 0.01; #[allow(unused)] enum BorderType { @@ -67,6 +68,7 @@ struct Ship { power: f32, stats: ShipStats, sunk: bool, + submerged: bool, smoke: bool, respawn_cooldown: u32, } @@ -87,6 +89,7 @@ impl Ship { power: 0.0, stats, sunk: false, + submerged: false, smoke: false, respawn_cooldown: RESPAWN_COOLDOWN, } @@ -97,12 +100,14 @@ impl Ship { self.angle += delta_t * self.helm * self.velocity * 2.0 / self.stats.turning_circle; let reynolds_number = self.stats.length * self.velocity.abs() / WATER_VISCOSITY; let c_f = 0.075 / (reynolds_number.log10() - 2.0).powi(2); - let c_v = c_f * (1.0 + self.stats.k); - let froude_number = self.velocity / (GRAVITY * self.stats.length).sqrt(); - let c_w = self.stats.froude_scale_factor * froude_number.powi(6); - let c_total = c_v + c_w; - let r_total = c_total * 0.5 * self.velocity * self.velocity.abs() * self.stats.surface_area; - let net_power = self.power * self.stats.power; + let mut c_total = c_f * (1.0 + self.stats.k); + if !self.submerged { + let froude_number = self.velocity / (GRAVITY * self.stats.length).sqrt(); + let c_w = self.stats.froude_scale_factor * froude_number.powi(6); + c_total += c_w; + } + let r_total = c_total * 0.5 * self.velocity * self.velocity.abs() * self.surface_area(); + let net_power = self.current_power(); let q = net_power / self.stats.screw_area; let s = q.powi(2) - self.velocity.powi(6) / 27.0; let v_out = match s.total_cmp(&0.0) { @@ -120,19 +125,22 @@ impl Ship { }; let thrust = self.stats.screw_area * v_out * (v_out - self.velocity).abs(); let net_thrust = thrust - r_total; - self.velocity += net_thrust * delta_t / self.stats.mass; + self.velocity += net_thrust * delta_t / self.current_mass(); self.coords.0 += self.velocity * delta_t * self.angle.sin(); self.coords.1 -= self.velocity * delta_t * self.angle.cos(); } + #[must_use] fn energy(&self) -> f32 { - 0.5 * self.stats.mass * self.velocity.powi(2) + 0.5 * self.current_mass() * self.velocity.powi(2) } + #[must_use] fn distance_from_origin(&self) -> f32 { self.coords.0.hypot(self.coords.1) } + #[must_use] fn distance(&self, other: &Self) -> f32 { let x_distance = self.coords.0 - other.coords.0; let y_distance = self.coords.1 - other.coords.1; @@ -148,27 +156,77 @@ impl Ship { self.sunk } + #[must_use] + fn random_location(&self) -> (f32, f32) { + let mut rng = thread_rng(); + let max_length_offset = self.stats.length / 2.0; + let min_length_offset = -max_length_offset; + let max_beam_offset = self.stats.beam / 2.0; + let min_beam_offset = -max_beam_offset; + let length = rng.gen_range(min_length_offset..max_length_offset); + let beam = rng.gen_range(min_beam_offset..max_beam_offset); + let x = self.coords.0 + self.angle.sin() * length + self.angle.cos() * beam; + let y = self.coords.1 - self.angle.cos() * length + self.angle.sin() * beam; + (x, y) + } + + #[must_use] + fn is_hit(&self, mut x: f32, mut y: f32) -> bool { + x -= self.coords.0; + y -= self.coords.1; + let distance = x.hypot(y); + let angle = x.atan2(y) - self.angle; + let x_offset = distance * angle.sin(); + let y_offset = distance * angle.cos(); + x_offset.abs() <= self.stats.beam / 2.0 && y_offset.abs() <= self.stats.length / 2.0 + } + #[must_use] fn shoot(&mut self, target: &mut Self) -> ShootingState { if self.stats.cooldown <= 0.0 { + let target_location = target.random_location(); + let x_offset = target_location.0 - self.coords.0; + let y_offset = target_location.1 - self.coords.1; let mut rng = thread_rng(); + let distance = x_offset.hypot(y_offset) * (1.0 - rng.gen_range(-GUN_ACCURACY..GUN_ACCURACY)); + let angle = x_offset.atan2(y_offset) + rng.gen_range(-GUN_ACCURACY..GUN_ACCURACY); + let x_offset = distance * angle.sin(); + let y_offset = distance * angle.cos(); + let coords = (self.coords.0 + x_offset, self.coords.1 + y_offset); let damage = self.stats.gun_damage * rng.gen_range(0.5..1.5); self.stats.cooldown = rng.gen_range(self.stats.gun_reload_time.clone()); - if target.damage(damage) { - ShootingState::Sunk(damage) + if target.is_hit(coords.0, coords.1) { + if target.damage(damage) { + ShootingState::Sunk(coords, damage) + } else { + ShootingState::Hit(coords, damage) + } } else { - ShootingState::Hit(damage) + ShootingState::Miss(coords, damage) } } else { ShootingState::NotFired } } + + fn current_power(&self) -> f32 { + self.power * self.stats.power.get_value(self.submerged) + } + + fn current_mass(&self) -> f32 { + self.stats.mass.get_value(self.submerged) + } + + fn surface_area(&self) -> f32 { + self.stats.surface_area.get_value(self.submerged) + } } enum ShootingState { NotFired, - Hit(f32), - Sunk(f32), + Miss((f32, f32), f32), + Hit((f32, f32), f32), + Sunk((f32, f32), f32), } fn handle_join( @@ -212,24 +270,39 @@ fn main() { handle_join(&mut connections, stream, rx, name); } let mut disconnected = Vec::new(); - let mut splashes = Vec::new(); + let mut sunk = Vec::new(); // get updates from clients for (name, connection) in &mut connections { + let ship = &mut connection.ship; loop { match connection.rx.try_recv() { Ok(ClientMessage::Sail(power, helm)) => { - connection.ship.power = power * power.abs(); - connection.ship.helm = helm; + ship.power = power * power.abs(); + ship.helm = helm; } Ok(ClientMessage::Anchor) => { - if connection.ship.velocity.abs() < 0.5 { - connection.ship.velocity = 0.0; + if ship.velocity.abs() < 0.5 { + ship.velocity = 0.0; } } Ok(ClientMessage::Smoke) => { - connection.ship.smoke = !connection.ship.smoke; + ship.smoke = !ship.smoke; + } + Ok(ClientMessage::Action(action)) => { + if let Some(action) = ship.stats.actions.get(action - 1) { + match *action { + Action::Submerge => { + ship.submerged = if ship.submerged { + false + } else { + sunk.push(name.clone()); + ship.velocity *= ship.stats.mass.get_value(false) / ship.stats.mass.get_value(true); + true + } + } + } + } } - Ok(ClientMessage::Weapon(_)) => (), Err(TryRecvError::Empty) => break, Err(TryRecvError::Disconnected) => { println!("{name} has disconnected"); @@ -247,8 +320,9 @@ fn main() { connection.tx.send(format!("sunk {name}\n")).ok(); } } + let mut splashes = Vec::new(); + let mut wakes = Vec::new(); let mut kraken_targets = Vec::new(); - let mut sunk = Vec::new(); for (name, connection) in &mut connections { let ship = &mut connection.ship; if ship.sunk { @@ -269,20 +343,17 @@ fn main() { } if distance < ship.stats.gun_range { match ship.shoot(kraken) { - ShootingState::Sunk(damage) | ShootingState::Hit(damage) => { - let mut rng = thread_rng(); - let max_offset = kraken.stats.length / 2.0; - let min_offset = -max_offset; - let splash_x = kraken.coords.0 + rng.gen_range(min_offset..max_offset); - let splash_y = kraken.coords.1 + rng.gen_range(min_offset..max_offset); + ShootingState::Sunk(location, damage) | ShootingState::Hit(location, damage) => { let size = damage.powf(1.0 / 3.0) * 3.0; - splashes.push((splash_x, splash_y, size, 1.0, 0, "f00")); - let max_offset = ship.stats.length / 2.0; - let min_offset = -max_offset; - let location = rng.gen_range(min_offset..max_offset); - let splash_x = ship.coords.0 + location * ship.angle.sin(); - let splash_y = ship.coords.1 - location * ship.angle.cos(); - splashes.push((splash_x, splash_y, size, 1.0, 1, "fff")); + splashes.push((location.0, location.1, size, 1.0, 0, "f00")); + let location = ship.random_location(); + splashes.push((location.0, location.1, size, 1.0, 1, "fff")); + } + ShootingState::Miss(location, damage) => { + let size = damage.powf(1.0 / 3.0) * 3.0; + splashes.push((location.0, location.1, size, 1.0, 0, "fff")); + let location = ship.random_location(); + splashes.push((location.0, location.1, size, 1.0, 1, "fff")); } ShootingState::NotFired => (), } @@ -293,12 +364,28 @@ fn main() { splashes.push(( ship.coords.0, ship.coords.1, - (ship.power.abs() * ship.stats.power).sqrt() * rng.gen_range(0.5..1.5), + ship.current_power().abs().sqrt() * rng.gen_range(0.5..1.5), rng.gen_range(30.0..180.0), 2, "0009", )); } + if !ship.submerged { + let mut wake_chance = ship.velocity.abs() * delta_t / 5.0; + if wake_chance > 1.0 { + wake_chance = 1.0; + } + if rng.gen_bool(f64::from(wake_chance)) { + wakes.push(( + ship.coords.0, + ship.coords.1, + ship.stats.beam * 1.5, + ship.angle, + rng.gen_range(20.0..60.0), + ship.velocity.abs() * TIME_ACCELERATION_FACTOR / 3.0, + )); + } + } ship.step(delta_t); if let Some((radius, border)) = MAP_RADIUS { let ship_distance = ship.distance_from_origin(); @@ -320,15 +407,11 @@ fn main() { mine_chance = 1.0; } if rng.gen_bool(f64::from(mine_chance)) { - let max_offset = ship.stats.length / 2.0; - let min_offset = -max_offset; - let location = rng.gen_range(min_offset..max_offset); - let splash_x = ship.coords.0 + location * ship.angle.sin(); - let splash_y = ship.coords.1 - location * ship.angle.cos(); + let location = ship.random_location(); let damage = data.mine_damage * rng.gen_range(0.2..1.0); splashes.push(( - splash_x, - splash_y, + location.0, + location.1, damage.powf(1.0 / 3.0) * 3.0, 1.0, 0, @@ -337,7 +420,7 @@ fn main() { if ship.damage(damage) { sunk.push(name.clone()); } - ship.velocity *= ship.stats.mass / (ship.stats.mass + damage); + ship.velocity *= ship.current_mass() / (ship.current_mass() + damage); } if kraken.is_none() && kraken_cooldown <= 0.0 @@ -364,6 +447,7 @@ fn main() { 100.0 * scale_factor_sqrt, 100.0 * scale_factor_sqrt, 0.5..1.5, + Vec::new(), ); let kraken_ship = Ship { coords: (x, y), @@ -373,6 +457,7 @@ fn main() { power: 0.0, stats, sunk: false, + submerged: false, smoke: false, respawn_cooldown: RESPAWN_COOLDOWN, }; @@ -389,7 +474,7 @@ fn main() { sunk.push(name.clone()); } ship.velocity = 0.0; - ship.stats.power = 0.0; + ship.stats.power = Variable::Surface(0.0); } } } @@ -408,10 +493,17 @@ fn main() { connection.tx.send(message.clone()).ok(); } } + for (x, y, size, angle, duration, growth) in wakes { + let duration = duration / TIME_ACCELERATION_FACTOR; + let message = format!("wake {x} {y} {size} {angle} {duration} {growth}\n"); + for connection in connections.values_mut() { + connection.tx.send(message.clone()).ok(); + } + } if let Some(ref mut kraken_ship) = kraken { kraken_ship.stats.cooldown -= delta_t; if kraken_ship.sunk { - kraken_cooldown = kraken_ship.stats.mass / 50.0 + 60.0; + kraken_cooldown = kraken_ship.current_mass() / 50.0; kraken = None; let message = format!("sunk {KRAKEN_NAME}\n"); for connection in connections.values_mut() { @@ -420,16 +512,16 @@ fn main() { } else if let Some(target) = kraken_targets.choose(&mut thread_rng()) { let target_ship = &mut connections.get_mut(target).expect("Missing target").ship; match kraken_ship.shoot(target_ship) { - ShootingState::Sunk(_) => { + ShootingState::Sunk(..) => { let message = format!("sunk {target}\n"); for connection in connections.values_mut() { connection.tx.send(message.clone()).ok(); } } - ShootingState::Hit(_) | ShootingState::NotFired => (), + ShootingState::Hit(..) | ShootingState::Miss(..) | ShootingState::NotFired => (), } } else { - kraken_cooldown = (kraken_ship.stats.mass - kraken_ship.stats.health) / 100.0 + 30.0; + kraken_cooldown = (kraken_ship.current_mass() - kraken_ship.stats.health) / 100.0; kraken = None; let message = format!("sunk {KRAKEN_NAME}\n"); for connection in connections.values_mut() { @@ -438,7 +530,7 @@ fn main() { } } let mut ships = Vec::new(); - for (name, connection) in &mut connections { + for (name, connection) in &connections { ships.push((name.clone(), connection.ship.clone())); } if let Some(ref kraken) = kraken { @@ -450,14 +542,18 @@ fn main() { let velocity = ship.velocity; let texture = ship.stats.texture; let size = ship.stats.length; - let mut health = ship.stats.health / ship.stats.mass; + let mut health = ship.stats.health / ship.current_mass(); if health < 0.0 { health = 0.0; } let message = format!("ship {name} {x} {y} {angle} {velocity} {size} {texture} #{COLOUR} {health}\n"); - for connection2 in connections.values_mut() { - connection2.tx.send(message.clone()).ok(); + if ship.submerged && !ship.sunk { + connections[&name].tx.send(message).ok(); + } else { + for connection2 in connections.values() { + connection2.tx.send(message.clone()).ok(); + } } } if connections.is_empty() { diff --git a/midway/src/stats.rs b/midway/src/stats.rs index a83d9d9..2916ebb 100644 --- a/midway/src/stats.rs +++ b/midway/src/stats.rs @@ -2,7 +2,29 @@ use enum_iterator::{all, Sequence}; use random_pick::pick_from_slice; use std::ops::Range; -const WEIGHTS: &[usize] = &[15, 25, 4, 3, 1, 1, 1, 1, 10, 10]; +const WEIGHTS: &[usize] = &[15, 25, 4, 3, 1, 1, 1, 1, 10, 10, 10]; + +#[derive(Clone)] +pub enum Variable { + Surface(T), + // surface, submerged + Submersible(T, T), +} + +impl Variable { + pub fn get_value(&self, submerged: bool) -> T { + match self { + Self::Surface(x) => *x, + Self::Submersible(x, y) => { + if submerged { + *y + } else { + *x + } + } + } + } +} #[derive(Clone, Copy, Sequence)] enum ShipType { @@ -16,6 +38,12 @@ enum ShipType { Bird, PTBoat, Liberty, + UBoat, +} + +#[derive(Clone)] +pub enum Action { + Submerge, } #[derive(Clone)] @@ -23,11 +51,11 @@ pub struct ShipStats { pub texture: usize, pub length: f32, pub beam: f32, - pub mass: f32, + pub mass: Variable, pub health: f32, - pub power: f32, + pub power: Variable, pub k: f32, - pub surface_area: f32, + pub surface_area: Variable, pub screw_area: f32, pub froude_scale_factor: f32, pub turning_circle: f32, @@ -35,6 +63,7 @@ pub struct ShipStats { pub gun_range: f32, pub gun_reload_time: Range, pub cooldown: f32, + pub actions: Vec, } impl ShipStats { @@ -52,16 +81,56 @@ impl ShipStats { gun_damage: f32, gun_range: f32, gun_reload_time: Range, + actions: Vec, ) -> Self { Self { texture, length, beam, - mass, + mass: Variable::Surface(mass), health: mass, - power, + power: Variable::Surface(power), + k, + surface_area: Variable::Surface(surface_area), + screw_area, + froude_scale_factor, + turning_circle, + gun_damage, + gun_range, + gun_reload_time, + cooldown: 0.0, + actions, + } + } + + pub const fn new_submersible( + texture: usize, + length: f32, + beam: f32, + mass_surface: f32, + mass_submerged: f32, + power_surface: f32, + power_submerged: f32, + k: f32, + surface_area: f32, + submerged_area: f32, + screw_area: f32, + turning_circle: f32, + froude_scale_factor: f32, + gun_damage: f32, + gun_range: f32, + gun_reload_time: Range, + actions: Vec, + ) -> Self { + Self { + texture, + length, + beam, + mass: Variable::Submersible(mass_surface, mass_submerged), + health: mass_surface, + power: Variable::Submersible(power_surface, power_submerged), k, - surface_area, + surface_area: Variable::Submersible(surface_area, submerged_area), screw_area, froude_scale_factor, turning_circle, @@ -69,6 +138,7 @@ impl ShipStats { gun_range, gun_reload_time, cooldown: 0.0, + actions, } } } @@ -78,7 +148,7 @@ fn get_random_type() -> ShipType { .expect("Could not generate ship type") } -const fn get_stats(ship: ShipType) -> ShipStats { +fn get_stats(ship: ShipType) -> ShipStats { match ship { ShipType::Escort => ShipStats::new( 1, @@ -94,6 +164,7 @@ const fn get_stats(ship: ShipType) -> ShipStats { 27.0, 13400.0, 0.4..0.44, + Vec::new(), ), ShipType::Destroyer => ShipStats::new( 2, @@ -109,6 +180,7 @@ const fn get_stats(ship: ShipType) -> ShipStats { 125.0, 16000.0, 0.8..1.2, + Vec::new(), ), ShipType::LightCruiser => ShipStats::new( 3, @@ -124,6 +196,7 @@ const fn get_stats(ship: ShipType) -> ShipStats { 216.0, 18288.0, 0.5..0.625, + Vec::new(), ), ShipType::HeavyCruiser => ShipStats::new( 4, @@ -139,6 +212,7 @@ const fn get_stats(ship: ShipType) -> ShipStats { 512.0, 27480.0, 1.33..2.0, + Vec::new(), ), ShipType::BattleCruiser => ShipStats::new( 5, @@ -154,6 +228,7 @@ const fn get_stats(ship: ShipType) -> ShipStats { 3375.0, 30680.0, 4.0..6.0, + Vec::new(), ), ShipType::SlowBattleship => ShipStats::new( 6, @@ -169,6 +244,7 @@ const fn get_stats(ship: ShipType) -> ShipStats { 4096.0, 31364.0, 4.0..6.0, + Vec::new(), ), ShipType::FastBattleship => ShipStats::new( 6, @@ -184,6 +260,7 @@ const fn get_stats(ship: ShipType) -> ShipStats { 4096.0, 38700.0, 2.6..4.0, + Vec::new(), ), ShipType::Bird => ShipStats::new( 7, @@ -199,6 +276,7 @@ const fn get_stats(ship: ShipType) -> ShipStats { 64.0, 12660.0, 5.0..6.0, + Vec::new(), ), ShipType::PTBoat => ShipStats::new( 9, @@ -214,6 +292,7 @@ const fn get_stats(ship: ShipType) -> ShipStats { 4.096, 7160.0, 0.6..0.75, + Vec::new(), ), ShipType::Liberty => ShipStats::new( 10, @@ -229,6 +308,26 @@ const fn get_stats(ship: ShipType) -> ShipStats { 64.0, 12660.0, 5.0..6.0, + Vec::new(), + ), + ShipType::UBoat => ShipStats::new_submersible( + 11, + 67.1, + 6.2, + 769.0, + 871.0, + 1600.0, + 373.3, + 0.025, + 885.4, + 1307.0, + 1.62, + 270.0, // TODO: acquire proper value + 1.34, + 42.9, + 11950.0, + 3.0..5.0, + vec![Action::Submerge], ), } } diff --git a/resources/UBoat.png b/resources/UBoat.png new file mode 100644 index 0000000..66818af Binary files /dev/null and b/resources/UBoat.png differ diff --git a/resources/Wake.png b/resources/Wake.png new file mode 100644 index 0000000..ee720dc Binary files /dev/null and b/resources/Wake.png differ