diff --git a/assets/mainmenu.png b/assets/main_menu.png similarity index 100% rename from assets/mainmenu.png rename to assets/main_menu.png diff --git a/game/Cargo.toml b/game/Cargo.toml index 055c4b5..4352eec 100644 --- a/game/Cargo.toml +++ b/game/Cargo.toml @@ -10,10 +10,13 @@ ggez = { git="https://github.com/ggez/ggez", branch="devel" } serde = "1.0.145" serde_yaml = "0.9.13" tracing = "0.1.37" -tracing-subscriber = "0.3.16" +tracing-subscriber = { version = "0.3.16", features = ['env-filter'] } fastrand = "1.8.0" chrono = "0.4.23" +[dev-dependencies] +tempdir = { version = "0.3.7" } + [build-dependencies] fs_extra = "1.2.0" @@ -23,4 +26,4 @@ codegen-units = 1 [profile.release] lto = true -panic = "abort" \ No newline at end of file +panic = "abort" diff --git a/game/src/backend/constants.rs b/game/src/backend/constants.rs index dc3ba4c..4d1f68f 100644 --- a/game/src/backend/constants.rs +++ b/game/src/backend/constants.rs @@ -2,11 +2,57 @@ use crate::backend::rlcolor::RLColor; use crate::game_core::player::gen_inventory; use crate::game_core::resources::Resources; -use crate::languages::german::MACHINE_NAMES; +use crate::languages::{machine_names, Lang}; use crate::machines::machine::{Machine, State}; use crate::machines::trade::Trade; use ggez::graphics::{Color, Rect}; -use std::string::ToString; + +#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum PopupType { + Warning, + Nasa, + Mars, +} + +#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum ObjectId { + OxygenGenerator = 0, + PowerGenerator = 1, + WorkMachine = 2, + Printer3D = 3, + CommunicationModule = 4, + NorthHole = 5, + SouthHole = 6, +} + +impl ObjectId { + pub fn t(self, lng: Lang) -> &'static str { + machine_names(lng)[self as usize] + } + + pub fn is_hole(self) -> bool { + matches!(self, ObjectId::NorthHole | ObjectId::SouthHole) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum TradeId { + NoTrade = -1, + RepairOxygen = 0, + StartOxygen = 1, + StopOxygen = 2, + FuelingPowerGenerator = 3, + StartPowerGenerator = 4, + StopPowerGenerator = 5, + RepairWorkMachine = 6, + ProduceSuperglue = 7, + Repair3dPrinter = 8, + Produce3dPart = 9, + RepairCommunicationModule = 10, + EmergencySignalOff = 11, + RepairNorthHole = 12, + RepairSouthHole = 13, +} /// Contains the screen resolution of the game. /// The game is designed to be played in 1920x1080. @@ -38,7 +84,7 @@ pub const MOVEMENT_SPEED: usize = 10; pub(crate) const TIME_POSITION: (f32, f32) = (1205., 960.); /// Change rate fot the event Sandsturm -pub(crate) const SANDSTURM_CR: Resources = Resources { +pub(crate) const SANDSTORM_CR: Resources = Resources { oxygen: 10, energy: 0, life: 0, @@ -48,11 +94,11 @@ pub(crate) const SANDSTURM_CR: Resources = Resources { /// Generates all machines with all their name, position, trades and resources. /// # Returns /// A Vector of `Machine`s -pub(crate) fn gen_all_machines() -> Vec { +pub(crate) fn gen_all_machines(lng: Lang) -> Vec { vec![ // Oxygen machine Machine::new_by_const(( - MACHINE_NAMES[0].to_string(), + ObjectId::OxygenGenerator, Rect { x: 280.0, y: 230.0, @@ -61,28 +107,28 @@ pub(crate) fn gen_all_machines() -> Vec { }, vec![ Trade::new( - "repair_Oxygen".to_string(), + TradeId::RepairOxygen, 100, State::Broken, State::Idle, false, - gen_inventory(2, 0, 0), + gen_inventory(2, 0, 0, lng), ), Trade::new( - "start_Oxygen".to_string(), + TradeId::StartOxygen, 0, State::Idle, State::Running, true, - gen_inventory(0, 0, 0), + gen_inventory(0, 0, 0, lng), ), Trade::new( - "stop_Oxygen".to_string(), + TradeId::StopOxygen, 0, State::Running, State::Idle, true, - gen_inventory(0, 0, 0), + gen_inventory(0, 0, 0, lng), ), ], Resources { @@ -93,7 +139,7 @@ pub(crate) fn gen_all_machines() -> Vec { )), // Electricity machine Machine::new_by_const(( - MACHINE_NAMES[1].to_string(), + ObjectId::PowerGenerator, Rect { x: 282.0, y: 752.0, @@ -102,28 +148,28 @@ pub(crate) fn gen_all_machines() -> Vec { }, vec![ Trade::new( - "fueling_Stromgenerator".to_string(), + TradeId::FuelingPowerGenerator, 700, State::Broken, State::Running, true, - gen_inventory(0, 1, 0), + gen_inventory(0, 1, 0, lng), ), Trade::new( - "start_Stromgenerator".to_string(), + TradeId::StartPowerGenerator, 1, State::Idle, State::Running, true, - gen_inventory(0, 0, 0), + gen_inventory(0, 0, 0, lng), ), Trade::new( - "stop_Stromgenerator".to_string(), + TradeId::StopPowerGenerator, 0, State::Running, State::Idle, true, - gen_inventory(0, 0, 0), + gen_inventory(0, 0, 0, lng), ), ], Resources { @@ -134,7 +180,7 @@ pub(crate) fn gen_all_machines() -> Vec { )), // Worker machine Machine::new_by_const(( - MACHINE_NAMES[2].to_string(), + ObjectId::WorkMachine, Rect { x: 1000.0, y: 780.0, @@ -143,20 +189,20 @@ pub(crate) fn gen_all_machines() -> Vec { }, vec![ Trade::new( - "repair_werkermaschine".to_string(), + TradeId::RepairWorkMachine, 100, State::Broken, State::Idle, false, - gen_inventory(0, 0, 1), + gen_inventory(0, 0, 1, lng), ), Trade::new( - "produce_superglue".to_string(), + TradeId::ProduceSuperglue, 120, State::Idle, State::Running, true, - gen_inventory(-1, 0, 0), + gen_inventory(-1, 0, 0, lng), ), ], Resources { @@ -167,7 +213,7 @@ pub(crate) fn gen_all_machines() -> Vec { )), // 3d Printer machine Machine::new_by_const(( - MACHINE_NAMES[3].to_string(), + ObjectId::Printer3D, Rect { x: 930.0, y: 230.0, @@ -176,20 +222,20 @@ pub(crate) fn gen_all_machines() -> Vec { }, vec![ Trade::new( - "repair_3d_printer".to_string(), + TradeId::Repair3dPrinter, 300, State::Broken, State::Idle, false, - gen_inventory(2, 0, 0), + gen_inventory(2, 0, 0, lng), ), Trade::new( - "produce_3d_teil".to_string(), + TradeId::Produce3dPart, 200, State::Idle, State::Running, true, - gen_inventory(2, 0, -1), + gen_inventory(2, 0, -1, lng), ), ], Resources { @@ -200,7 +246,7 @@ pub(crate) fn gen_all_machines() -> Vec { )), // Communication module Machine::new_by_const(( - MACHINE_NAMES[4].to_string(), + ObjectId::CommunicationModule, Rect { x: 1640.0, y: 320.0, @@ -209,20 +255,20 @@ pub(crate) fn gen_all_machines() -> Vec { }, vec![ Trade::new( - "Kommunikationsmodul_reparieren".to_string(), + TradeId::RepairCommunicationModule, 400, State::Broken, State::Idle, false, - gen_inventory(5, 0, 3), + gen_inventory(5, 0, 3, lng), ), Trade::new( - "Notfall_signal_absetzen".to_string(), + TradeId::EmergencySignalOff, 1000, State::Idle, State::Running, true, - gen_inventory(1, 0, 1), + gen_inventory(1, 0, 1, lng), ), ], Resources { @@ -233,7 +279,7 @@ pub(crate) fn gen_all_machines() -> Vec { )), // First hole Machine::new_by_const(( - MACHINE_NAMES[5].to_string(), + ObjectId::NorthHole, Rect { x: 780.0, y: 230.0, @@ -241,12 +287,12 @@ pub(crate) fn gen_all_machines() -> Vec { h: 18.0, }, vec![Trade::new( - "repair_Loch".to_string(), + TradeId::RepairNorthHole, 100, State::Running, State::Idle, false, - gen_inventory(2, 0, 0), + gen_inventory(2, 0, 0, lng), )], Resources { oxygen: -15, @@ -256,7 +302,7 @@ pub(crate) fn gen_all_machines() -> Vec { )), // Second hole Machine::new_by_const(( - MACHINE_NAMES[6].to_string(), + ObjectId::SouthHole, Rect { x: 680.0, y: 900.0, @@ -264,12 +310,12 @@ pub(crate) fn gen_all_machines() -> Vec { h: 18.0, }, vec![Trade::new( - "repair_Loch".to_string(), + TradeId::RepairSouthHole, 100, State::Running, State::Idle, false, - gen_inventory(2, 0, 0), + gen_inventory(2, 0, 0, lng), )], Resources { oxygen: -15, diff --git a/game/src/backend/error.rs b/game/src/backend/error.rs index 2094bdb..d227964 100644 --- a/game/src/backend/error.rs +++ b/game/src/backend/error.rs @@ -7,6 +7,7 @@ use tracing::error; /// All Red Life errors #[warn(clippy::enum_variant_names)] +#[allow(clippy::pedantic)] #[derive(Debug)] pub enum RLError { /// All Errors caused by Drawing diff --git a/game/src/backend/gamestate.rs b/game/src/backend/gamestate.rs index 2f06310..34c56fb 100644 --- a/game/src/backend/gamestate.rs +++ b/game/src/backend/gamestate.rs @@ -1,6 +1,6 @@ //! Contains the game logic, updates the game and draws the current board use crate::backend::constants::{ - COLORS, DESIRED_FPS, MAP_BORDER, RESOURCE_POSITION, TIME_POSITION, + ObjectId, COLORS, DESIRED_FPS, MAP_BORDER, RESOURCE_POSITION, TIME_POSITION, }; use crate::backend::rlcolor::RLColor; use crate::backend::screen::{Popup, StackCommand}; @@ -13,9 +13,9 @@ use crate::game_core::infoscreen::InfoScreen; use crate::game_core::item::Item; use crate::game_core::player::Player; use crate::game_core::resources::Resources; -use crate::languages::german::{ - FIRST_MILESTONE_HANDBOOK_TEXT, MACHINE_NAMES, RESOURCE_NAME, SECOND_MILESTONE_HANDBOOK_TEXT, - TIME_NAME, +use crate::languages::{ + first_milestone_handbook_text, resource_name, second_milestone_handbook_text, send_msg_failure, + time_name, Lang, }; use crate::machines::machine::Machine; use crate::machines::machine::State::Broken; @@ -28,6 +28,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::fs::read_dir; +use std::path::Path; use std::sync::mpsc::{channel, Receiver, Sender}; use tracing::info; @@ -39,7 +40,7 @@ pub enum GameCommand { } /// This is the game state. It contains all the data that is needed to run the game. -#[derive(Debug, Default, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize)] pub struct GameState { /// Contains the current player position, resources(air, energy, life) and the inventory and their change rates pub player: Player, @@ -61,6 +62,8 @@ pub struct GameState { pub(crate) sender: Option>, /// Defines if the handbook is currently open pub handbook_invisible: bool, + #[serde(default)] + pub lng: Lang, } impl PartialEq for GameState { @@ -71,6 +74,20 @@ impl PartialEq for GameState { } impl GameState { + pub fn new_with_lang(lng: Lang) -> Self { + Self { + player: Player::new(lng), + events: vec![], + machines: vec![], + assets: HashMap::with_capacity(64), + screen_sender: None, + receiver: None, + sender: None, + handbook_invisible: false, + lng, + } + } + /// Gets the screen sender /// # Returns /// * `RLResult>`: The screen sender in a `RLResult` to handle Initialization errors @@ -92,13 +109,19 @@ impl GameState { /// It loads all the assets and creates the areas of the machines. /// # Returns /// * `RLResult`: The new game state initialized in a `RLResult` to handle setup errors - pub fn new(ctx: &mut Context) -> RLResult { + pub fn new(ctx: &mut Context, lng: Lang) -> RLResult { info!("Creating new gamestate"); let (sender, receiver) = channel(); let mut result = GameState { + player: Player::new(lng), + events: vec![], + machines: vec![], + assets: HashMap::with_capacity(64), sender: Some(sender), receiver: Some(receiver), - ..Default::default() + lng, + screen_sender: None, + handbook_invisible: false, }; result.init(ctx)?; Ok(result) @@ -109,6 +132,8 @@ impl GameState { /// # Returns /// * `RLResult`: A `RLResult` to validate the success of the tick function pub fn tick(&mut self) -> RLResult { + let lng = self.lng; + // Update Resources self.player.resources = self .player @@ -134,11 +159,12 @@ impl GameState { } }; if self.player.resources.life == 0 { - let gamestate = GameState::load(true).unwrap_or_default(); - gamestate.save(false)?; + let game_state = + GameState::load(true).unwrap_or_else(|_| GameState::new_with_lang(lng)); + game_state.save(false)?; let cloned_sender = self.get_screen_sender()?.clone(); self.get_screen_sender()?.send(StackCommand::Push(Box::new( - InfoScreen::new_deathscreen(empty_resource, cloned_sender), + InfoScreen::new_death_screen(empty_resource, cloned_sender, game_state.lng), )))?; }; } else if self.player.resources_change.life < 0 { @@ -162,7 +188,8 @@ impl GameState { GameCommand::Winning => match self.player.milestone { 1 => { let sender = self.get_screen_sender()?; - let popup = Popup::new(RLColor::GREEN, "Die Nachricht kann nicht gesendet werden solange das System nicht wiederhergestellt ist".to_string(), 5); + let popup = + Popup::new(RLColor::GREEN, send_msg_failure(lng).to_string(), 5); sender.send(StackCommand::Popup(popup))?; } 2 => { @@ -176,7 +203,7 @@ impl GameState { // Regenerate life if applicable self.player - .life_regeneration(&self.screen_sender.as_ref().unwrap().clone())?; + .life_regeneration(&self.screen_sender.as_ref().unwrap().clone(), lng)?; for machine in &mut self.machines { machine.tick()?; } @@ -192,23 +219,29 @@ impl GameState { /// * `ctx`: The `Context` of the game /// # Returns /// * `RLResult`: A `RLResult` to validate the success of the paint function - fn draw_resources(&self, canvas: &mut Canvas, scale: Vec2, ctx: &mut Context) -> RLResult { + fn draw_resources(&self, canvas: &mut Canvas, scale: Vec2, ctx: &mut Context) { self.player .resources .into_iter() .enumerate() .map(|(i, resource)| -> RLResult<()> { + let resource = f64::from(resource); let mut color = COLORS[i]; if i == 2 && self.player.resources_change.life > 0 { color = RLColor::GREEN; }; - let rect = Rect::new(RESOURCE_POSITION[i], 961.0, resource as f32 * 0.00435, 12.6); + let rect = Rect::new( + RESOURCE_POSITION[i], + 961.0, + (resource * 0.00435_f64) as f32, + 12.6, + ); let mesh = Mesh::new_rounded_rectangle(ctx, DrawMode::fill(), rect, 3.0, color)?; draw!(canvas, &mesh, scale); let text = graphics::Text::new(format!( "{}: {:.1}", - RESOURCE_NAME[i], - (resource as f32 / u16::MAX as f32) * 100.0 + resource_name(self.lng)[i], + (resource / f64::from(u16::MAX)) * 100.0 )); draw!( canvas, @@ -219,7 +252,6 @@ impl GameState { Ok(()) }) .for_each(drop); - Ok(()) } /// Draws the handbook while pressing the H key /// # Arguments @@ -227,21 +259,20 @@ impl GameState { /// * `ctx`: The `Context` of the game /// # Returns /// * `RLResult`: A `RLResult` to validate the success of the function - pub fn open_handbook(&self, canvas: &mut Canvas, ctx: &mut Context) -> RLResult { + pub fn open_handbook(&self, canvas: &mut Canvas, ctx: &mut Context) { let scale = get_scale(ctx); let image = self.assets.get("Handbook.png").unwrap(); draw!(canvas, image, Vec2::new(700.0, 300.0), scale); match self.player.milestone { 1 => { - self.draw_handbook_text(canvas, scale, &FIRST_MILESTONE_HANDBOOK_TEXT); + Self::draw_handbook_text(canvas, scale, first_milestone_handbook_text(self.lng)); } 2 => { - self.draw_handbook_text(canvas, scale, &SECOND_MILESTONE_HANDBOOK_TEXT); + Self::draw_handbook_text(canvas, scale, second_milestone_handbook_text(self.lng)); } _ => {} } - Ok(()) } /// Draws the text for the current milestone on the handbook on the screen. /// # Arguments @@ -250,7 +281,7 @@ impl GameState { /// * `handbook_text`: The text to draw on the screen /// # Returns /// * `RLResult`: A `RLResult` to validate the success of the function - pub fn draw_handbook_text(&self, canvas: &mut Canvas, scale: Vec2, handbook_text: &[&str]) { + pub fn draw_handbook_text(canvas: &mut Canvas, scale: Vec2, handbook_text: &[&str]) { handbook_text .iter() .enumerate() @@ -273,7 +304,7 @@ impl GameState { /// * `ctx` - The current game context /// # Returns /// * `RLResult` - validates if the drawing was successful - fn draw_items(&self, canvas: &mut Canvas, ctx: &mut Context) -> RLResult { + fn draw_items(&self, canvas: &mut Canvas, ctx: &mut Context) { self.player .inventory .clone() @@ -297,7 +328,6 @@ impl GameState { ); }) .for_each(drop); - Ok(()) } /// Draws the current time on the screen @@ -308,7 +338,7 @@ impl GameState { let time = self.player.time / DESIRED_FPS; let time_text = format!( "{}: {}h {}m {}s", - TIME_NAME[0], + time_name(self.lng)[0], time / 3600, time / 60, time % 60 @@ -349,7 +379,7 @@ impl GameState { let machine_assets: Vec> = self .machines .iter() - .map(|m| m.name.clone()) + .map(|m| m.id.t(Lang::De)) .map(|name| { info!("Loading assets for {}", name); if self.assets.contains_key(&format!("{name}.png")) { @@ -391,18 +421,25 @@ impl GameState { /// # Returns /// * `RLResult` - validates if the save was successful pub(crate) fn save(&self, milestone: bool) -> RLResult { + self.save_with_root(milestone, ".") + } + + pub(crate) fn save_with_root>(&self, milestone: bool, root: P) -> RLResult { let save_data = serde_yaml::to_string(self)?; + let root = root.as_ref().join("saves"); + // Create the folder if it doesn't exist - fs::create_dir_all("./saves")?; + fs::create_dir_all(&root)?; if milestone { - fs::write("./saves/milestone.yaml", save_data)?; + fs::write(root.join("milestone.yaml"), save_data)?; info!("Saved game state as milestone"); } else { - fs::write("./saves/autosave.yaml", save_data)?; + fs::write(root.join("autosave.yaml"), save_data)?; info!("Saved game state as autosave"); } Ok(()) } + /// Loads a game state from a file. The boolean value "milestone" determines whether this is a milestone or an autosave. /// If the file doesn't exist, it will return a default game state. /// # Arguments @@ -410,17 +447,22 @@ impl GameState { /// # Returns /// * `RLResult` containing the loaded game state or a default game state if the file doesn't exist. pub fn load(milestone: bool) -> RLResult { + Self::load_from_dir(milestone, Path::new(".")) + } + + fn load_from_dir>(milestone: bool, root: P) -> RLResult { let save_data = if milestone { info!("Loading milestone..."); - fs::read_to_string("./saves/milestone.yaml") + fs::read_to_string(root.as_ref().join("saves/milestone.yaml")) } else { info!("Loading autosave..."); - fs::read_to_string("./saves/autosave.yaml") + fs::read_to_string(root.as_ref().join("saves/autosave.yaml")) }?; let game_state: GameState = serde_yaml::from_str(&save_data)?; Ok(game_state) } + /// Returns the area the player needs to stand in to interact with a machine /// # Returns /// * `Option<&mut Machine>` - The machines the player can interact with if one exists or None @@ -464,17 +506,17 @@ impl GameState { /// contain the vec of machines needed to reach the next milestone. /// # Arguments /// * `milestone_machines` - A vec of machines needed to reach the next milestone - pub fn check_on_milestone_machines(&mut self, milestone_machines: &[String]) -> bool { + pub fn check_on_milestone_machines(&mut self, milestone_machines: &[ObjectId]) -> bool { let running_machine = self .machines .iter() .filter(|m| m.state != Broken) - .map(|m| &m.name) - .collect::>(); + .map(|m| m.id) + .collect::>(); if milestone_machines .iter() - .all(|machine| running_machine.contains(&machine)) + .all(|machine| running_machine.contains(machine)) { return true; } @@ -489,6 +531,8 @@ impl GameState { /// Decides what happens if a certain milestone is reached /// divided into 3 milestones fn get_current_milestone(&mut self) -> RLResult { + let lng = self.lang(); + match self.player.milestone { 0 => { self.player.resources_change.oxygen = -1; @@ -498,8 +542,8 @@ impl GameState { } 1 => { if self.check_on_milestone_machines(&[ - MACHINE_NAMES[0].to_string(), - MACHINE_NAMES[1].to_string(), + ObjectId::OxygenGenerator, + ObjectId::PowerGenerator, ]) { self.increase_milestone()?; } @@ -509,7 +553,7 @@ impl GameState { self.player.milestone += 1; let cloned_sender = self.get_screen_sender()?.clone(); self.get_screen_sender()?.send(StackCommand::Push(Box::new( - InfoScreen::new_winningscreen(cloned_sender), + InfoScreen::new_winning_screen(cloned_sender, lng), )))?; } _ => {} @@ -560,11 +604,11 @@ impl Screen for GameState { Vec2::from([self.player.position.0 as f32, self.player.position.1 as f32]), scale ); - self.draw_resources(&mut canvas, scale, ctx)?; + self.draw_resources(&mut canvas, scale, ctx); self.draw_machines(&mut canvas, scale, ctx)?; - self.draw_items(&mut canvas, ctx)?; + self.draw_items(&mut canvas, ctx); if !self.handbook_invisible { - self.open_handbook(&mut canvas, ctx)?; + self.open_handbook(&mut canvas, ctx); } #[cfg(debug_assertions)] { @@ -602,6 +646,10 @@ impl Screen for GameState { self.screen_sender = Some(sender); self.init_all_machines(); } + + fn lang(&self) -> Lang { + self.lng + } } #[cfg(test)] @@ -610,31 +658,51 @@ mod test { #[test] fn test_gamestate() { - let _gamestate = GameState::default(); + let _gamestate = GameState::new_with_lang(Lang::De); } #[test] fn test_save_autosave() { - let gamestate = GameState::default(); - gamestate.save(false).unwrap(); + let tmp = tempdir::TempDir::new("test_save_autosave") + .unwrap() + .path() + .to_path_buf(); + let gamestate = GameState::new_with_lang(Lang::De); + gamestate.save_with_root(false, tmp).unwrap(); } #[test] fn test_save_milestone() { - let gamestate = GameState::default(); - gamestate.save(true).unwrap(); + let tmp = tempdir::TempDir::new("test_save_milestone") + .unwrap() + .path() + .to_path_buf(); + let gamestate = GameState::new_with_lang(Lang::De); + gamestate.save_with_root(true, tmp).unwrap(); } #[test] fn test_load_autosave() { - GameState::default().save(false).unwrap(); - let _gamestate_loaded = GameState::load(false).unwrap(); + let tmp = tempdir::TempDir::new("test_load_autosave") + .unwrap() + .path() + .to_path_buf(); + GameState::new_with_lang(Lang::De) + .save_with_root(false, tmp.clone()) + .unwrap(); + let _gamestate_loaded = GameState::load_from_dir(false, tmp).unwrap(); } #[test] fn test_load_milestone() { - GameState::default().save(true).unwrap(); - let _gamestate_loaded = GameState::load(true).unwrap(); + let tmp = tempdir::TempDir::new("test_load_milestone") + .unwrap() + .path() + .to_path_buf(); + GameState::new_with_lang(Lang::De) + .save_with_root(true, tmp.clone()) + .unwrap(); + let _gamestate_loaded = GameState::load_from_dir(true, tmp.to_path_buf()).unwrap(); } #[test] diff --git a/game/src/backend/generate_machines.rs b/game/src/backend/generate_machines.rs index 8880083..14fd263 100644 --- a/game/src/backend/generate_machines.rs +++ b/game/src/backend/generate_machines.rs @@ -13,7 +13,7 @@ impl GameState { /// Creates all Machines for initial creation and pushes them into a list pub fn create_machine(&mut self) { info!("Generating all Machines"); - self.machines = gen_all_machines(); + self.machines = gen_all_machines(self.lng); } /// Paints the machine sprites and if applicable it shows the state or time remaining @@ -31,7 +31,7 @@ impl GameState { y: machine.hitbox.y, }; draw!(canvas, image, pos, scale); - if !machine.name.contains("Loch") { + if !machine.id.is_hole() { // Draws the machine status on top of the machine let status = Mesh::new_circle( ctx, diff --git a/game/src/backend/movement.rs b/game/src/backend/movement.rs index 927dcb0..82afe16 100644 --- a/game/src/backend/movement.rs +++ b/game/src/backend/movement.rs @@ -1,7 +1,7 @@ //! This file contains the movement system, which is responsible for moving the player around the map and to interact with objects. use crate::backend::constants::MOVEMENT_SPEED; use crate::backend::gamestate::GameState; -use crate::backend::screen::StackCommand; +use crate::backend::screen::{Screen, StackCommand}; use crate::RLResult; use ggez::winit::event::VirtualKeyCode; use ggez::Context; @@ -16,6 +16,7 @@ impl GameState { /// # Returns /// * `RLResult<()>` - Returns okay, if no Error occurred pub fn move_player(&mut self, ctx: &mut Context) -> RLResult { + let lng = self.lang(); if ctx.keyboard.is_key_just_pressed(VirtualKeyCode::Escape) { info!("Exiting..."); self.save(false)?; @@ -25,7 +26,7 @@ impl GameState { info!("Interacting with Area: {:?}", self.get_interactable()); let player_ref = &self.player.clone(); if let Some(interactable) = self.get_interactable() { - interactable.interact(player_ref)?; + interactable.interact(player_ref, lng)?; } } if ctx.keyboard.is_key_just_pressed(VirtualKeyCode::H) { diff --git a/game/src/backend/screen.rs b/game/src/backend/screen.rs index b4aa829..7af90b0 100644 --- a/game/src/backend/screen.rs +++ b/game/src/backend/screen.rs @@ -2,9 +2,10 @@ use crate::backend::rlcolor::RLColor; use crate::backend::utils::{get_draw_params, get_scale}; use crate::error::RLError; -use crate::main_menu::mainmenu::MainMenu; +use crate::main_menu::main_menu::MainMenu; use crate::{draw, RLResult}; +use crate::languages::Lang; use ggez::glam::vec2; use ggez::graphics::Color; use ggez::{event, graphics, Context}; @@ -32,11 +33,14 @@ pub trait Screen: Debug { /// # Arguments /// * `sender` - The sender of the screen. fn set_sender(&mut self, sender: Sender); + + fn lang(&self) -> Lang; } /// A Screenstack contains multiple `Screen`s and `Popup`s, the last one of which is drawn to the screen and /// updated. -pub struct Screenstack { +#[allow(clippy::module_name_repetitions)] +pub struct ScreenStack { screens: Vec>, popup: Vec, receiver: Receiver, @@ -104,7 +108,7 @@ impl Popup { } } } -impl Screenstack { +impl ScreenStack { /// Draws all `Popups` at the top left of the screen with their given text and color /// The popups will be removed after the given duration /// # Arguments @@ -190,7 +194,7 @@ pub enum StackCommand { Pop, } -impl event::EventHandler for Screenstack { +impl event::EventHandler for ScreenStack { /// Redirect the update function to the last screen and handle the returned `StackCommand` /// # Arguments /// * `ctx` - The ggez game context @@ -198,10 +202,8 @@ impl event::EventHandler for Screenstack { /// `RLResult` - Returns an `RlResult` fn update(&mut self, ctx: &mut Context) -> RLResult { self.remove_popups(); - self.screens - .last_mut() - .expect("Failed to get a screen") - .update(ctx)?; + let screen = self.screens.last_mut().expect("Failed to get a screen"); + screen.update(ctx)?; if let Ok(message) = self.receiver.try_recv() { self.process_command(message); } @@ -230,15 +232,15 @@ impl event::EventHandler for Screenstack { } } -impl Default for Screenstack { - /// Creates a new `Screenstack` with a `MainMenu` screen. +impl ScreenStack { + /// Creates a new `Screen stack` with a `MainMenu` screen. /// # Returns - /// `Screenstack` - Returns a new `Screenstack`. - fn default() -> Self { - info!("Default Screenstack created"); + /// `Screen stack` - Returns a new `Screen stack`. + pub fn new_with_lang(lng: Lang) -> Self { + info!("Default Screen stack created"); let (sender, receiver) = channel(); Self { - screens: vec![Box::new(MainMenu::new(sender.clone()))], + screens: vec![Box::new(MainMenu::new(sender.clone(), lng))], popup: vec![], receiver, sender, @@ -251,8 +253,8 @@ mod test { use super::*; #[test] - fn test_screenstack() { - let screenstack = Screenstack::default(); - assert_eq!(1, screenstack.screens.len()); + fn test_screen_stack() { + let screen_stack = ScreenStack::new_with_lang(Lang::De); + assert_eq!(1, screen_stack.screens.len()); } } diff --git a/game/src/backend/utils.rs b/game/src/backend/utils.rs index e46a484..99fa7d0 100644 --- a/game/src/backend/utils.rs +++ b/game/src/backend/utils.rs @@ -10,7 +10,6 @@ use ggez::Context; /// let scale = get_scale(ctx); /// graphics::draw(ctx, &self.img, graphics::DrawParam::default().scale(scale))?; /// ``` -#[inline(always)] pub fn get_scale(ctx: &Context) -> Vec2 { let (width, height) = ctx.gfx.drawable_size(); Vec2::new(width / SCREEN_RESOLUTION.0, height / SCREEN_RESOLUTION.1) @@ -23,7 +22,6 @@ pub fn get_scale(ctx: &Context) -> Vec2 { /// # Returns /// * `true` if the player collides with an area /// * `false` if the player does not collide with an area -#[inline(always)] pub fn is_colliding(player_pos: (usize, usize), area: &Rect) -> bool { area.x < player_pos.0 as f32 + PLAYER_ICON_SIZE.0 as f32 && area.x + area.w > player_pos.0 as f32 diff --git a/game/src/game_core/event.rs b/game/src/game_core/event.rs index 8aad0c7..9ed58a4 100644 --- a/game/src/game_core/event.rs +++ b/game/src/game_core/event.rs @@ -1,14 +1,13 @@ -use crate::backend::constants::{DESIRED_FPS, SANDSTURM_CR}; +use crate::backend::constants::{ObjectId, PopupType, DESIRED_FPS, SANDSTORM_CR}; use crate::backend::gamestate::GameState; use crate::backend::screen::{Popup, StackCommand}; use crate::game_core::resources::Resources; -use crate::languages::german::{ - INFORMATIONSPOPUP_MARS, INFORMATIONSPOPUP_NASA, KOMETENEINSCHLAG, SANDSTURM, STROMAUSFALL, +use crate::languages::{ + comet_strike, informations_popup_mars, informations_popup_nasa, mars_info, nasa_info, + power_failure, sandstorm, warnings, Lang, }; -use crate::languages::german::{MARS_INFO, NASA_INFO, WARNINGS}; use crate::machines::machine::State; use crate::RLResult; -use ggez::graphics::Color; use ggez::Context; use serde::{Deserialize, Serialize}; use std::sync::mpsc::Sender; @@ -25,7 +24,7 @@ pub(crate) struct Event { info_text: String, pub(crate) resources: Option>, duration: u32, - popup_type: String, + popup_type: PopupType, popup_message: String, } @@ -40,7 +39,7 @@ impl Event { pub fn new( event: [&str; 2], popup_message: &str, - popup_type: &str, + popup_type: PopupType, resources: Option>, duration: u32, ) -> Self { @@ -54,42 +53,48 @@ impl Event { info_text: event[1].to_string(), resources, duration: duration * DESIRED_FPS, - popup_type: popup_type.to_string(), + popup_type, popup_message: popup_message.to_string(), } } /// if no Event is active it either chooses a random event of the Event enum or nothing every 60 seconds - pub fn event_generator() -> Option { + #[allow(clippy::pedantic)] + pub fn event_generator(lng: Lang) -> Option { let rng = fastrand::Rng::new(); - let event = rng.usize(..15); - match event { + match rng.usize(..15) { 8 => Some(Event::new( - SANDSTURM, - WARNINGS[2], - "warning", - Some(SANDSTURM_CR), + *sandstorm(lng), + warnings(lng)[2], + PopupType::Warning, + Some(SANDSTORM_CR), 5, )), 0 | 3 => Some(Event::new( - KOMETENEINSCHLAG, - WARNINGS[0], - "warning", + *comet_strike(lng), + warnings(lng)[0], + PopupType::Warning, None, 0, )), 1 => Some(Event::new( - INFORMATIONSPOPUP_NASA, - NASA_INFO[rng.usize(..4)], - "nasa", + *informations_popup_nasa(lng), + nasa_info(lng)[rng.usize(..4)], + PopupType::Nasa, + None, + 0, + )), + 2 | 9 | 7 => Some(Event::new( + *power_failure(lng), + warnings(lng)[1], + PopupType::Warning, None, 0, )), - 2 | 9 | 7 => Some(Event::new(STROMAUSFALL, WARNINGS[1], "warning", None, 0)), 4 => Some(Event::new( - INFORMATIONSPOPUP_MARS, - MARS_INFO[rng.usize(..5)], - "mars", + *informations_popup_mars(lng), + mars_info(lng)[rng.usize(..5)], + PopupType::Mars, None, 0, )), @@ -106,18 +111,18 @@ impl Event { pub fn send_popup( popup_message: &str, sender: &Sender, - popup_type: &str, + popup_type: PopupType, event_name: &str, ) -> RLResult { let popup = match popup_type { - "warning" => Popup::warning(popup_message.to_string()), - "nasa" => Popup::nasa(popup_message.to_string()), - "mars" => Popup::mars(popup_message.to_string()), - _ => Popup::new(Color::RED, "Error".to_string(), 10), + PopupType::Warning => Popup::warning(popup_message.to_string()), + PopupType::Nasa => Popup::nasa(popup_message.to_string()), + PopupType::Mars => Popup::mars(popup_message.to_string()), + // _ => Popup::new(Color::RED, "Error".to_string(), 10), }; sender.send(StackCommand::Popup(popup))?; info!( - "Event Popup sent: name: {}, Popup-Message: {}, Popup-Type: {}", + "Event Popup sent: name: {}, Popup-Message: {}, Popup-Type: {:?}", event_name, popup_message.to_string(), popup_type @@ -135,33 +140,34 @@ impl Event { /// * `restore` - If true the event will be deactivated and the resources will be restored /// * `gamestate` - The gamestate which is used to access the player and the machines pub fn action(&self, restore: bool, gamestate: &mut GameState) -> RLResult { - const KOMETENEINSCHLAG_NAME: &str = KOMETENEINSCHLAG[0]; - const STROMAUSFALL_NAME: &str = STROMAUSFALL[0]; + let lng = gamestate.lng; + let comet_strike: &str = comet_strike(lng)[0]; + let power_failure: &str = power_failure(lng)[0]; let sender = gamestate.get_screen_sender()?.clone(); // handle event effects match self.name.as_str() { - KOMETENEINSCHLAG_NAME => { + s if comet_strike == s => { if let Some(one_hole) = gamestate .machines .iter_mut() - .find(|machine| machine.name == "Loch" && machine.state != State::Running) + .find(|machine| machine.id.is_hole() && machine.state != State::Running) { // event not triggered if both machine are already running - Event::send_popup(&self.popup_message, &sender, &self.popup_type, &self.name) + Event::send_popup(&self.popup_message, &sender, self.popup_type, &self.name) .unwrap(); one_hole.change_state_to(&State::Running); } } - STROMAUSFALL_NAME => { + s if s == power_failure => { gamestate.machines.iter_mut().for_each(|machine| { // if machine is running it will b use tracing::{info, Id};e stopped // event not triggered if machine is broken or idling - if machine.name == "Stromgenerator" && machine.state == State::Running { + if machine.id == ObjectId::PowerGenerator && machine.state == State::Running { Event::send_popup( &self.popup_message, &sender, - &self.popup_type, + self.popup_type, &self.name, ) .unwrap(); @@ -171,7 +177,7 @@ impl Event { } // apply direct resource changes if there are any and the event is not handled above _ => { - Event::send_popup(&self.popup_message, &sender, &self.popup_type, &self.name)?; + Event::send_popup(&self.popup_message, &sender, self.popup_type, &self.name)?; if let Some(resources) = self.resources { if restore { gamestate.player.resources_change = @@ -197,6 +203,7 @@ impl Event { /// * `gamestate` - The gamestate which is used to access the events vector /// * `context` - The game context which is used to access the current tick pub fn update_events(ctx: &Context, gamestate: &mut GameState) -> RLResult { + let lng = gamestate.lng; if ctx.time.ticks() % 20 == 0 { gamestate.events.iter_mut().for_each(|event| { event.duration = event.duration.saturating_sub(20); @@ -225,7 +232,7 @@ impl Event { if ctx.time.ticks() >= 400 && ctx.time.ticks() % 200 == 0 { // generate new event // might not return an event - let gen_event = Event::event_generator(); + let gen_event = Event::event_generator(lng); // if event is not none, add it to the gamestates events vector and activate apply its effect if let Some(event) = gen_event { event.action(false, gamestate)?; diff --git a/game/src/game_core/infoscreen.rs b/game/src/game_core/infoscreen.rs index bc3bea0..9283ec7 100644 --- a/game/src/game_core/infoscreen.rs +++ b/game/src/game_core/infoscreen.rs @@ -1,17 +1,15 @@ use crate::backend::gamestate::{GameCommand, GameState}; use crate::backend::screen::{Screen, StackCommand}; use crate::backend::utils::{get_draw_params, get_scale}; -use crate::languages::german::{ - ADDITIONAL_INFO_STRING, AIR_AND_ENERGY_STRING, AIR_STRING, BUTTON_INFO, DEATH_REASON_STRING, - ENERGY_STRING, INTRO_TEXT, TUTORIAL_TEXT, WINNING_TEXT, +use crate::languages::{ + additional_info_string, air_and_energy_string, air_string, button_info, death_reason_string, + energy_string, intro_text, tutorial_text, winning_text, Lang, }; - -use crate::main_menu::mainmenu::MainMenu; +use crate::main_menu::main_menu::MainMenu; use crate::{draw, RLResult}; use ggez::glam::Vec2; use ggez::winit::event::VirtualKeyCode; use ggez::{graphics, Context}; -use std::fmt::{Display, Formatter}; use std::fs; use std::sync::mpsc::Sender; use tracing::info; @@ -23,16 +21,17 @@ pub enum DeathReason { Energy, Both, } -impl Display for DeathReason { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + +impl DeathReason { + fn t(self, lng: Lang) -> &'static str { match self { - DeathReason::Oxygen => write!(f, "{AIR_STRING}"), - DeathReason::Energy => write!(f, "{ENERGY_STRING}"), - DeathReason::Both => write!(f, "{AIR_AND_ENERGY_STRING}"), + DeathReason::Oxygen => air_string(lng), + DeathReason::Energy => energy_string(lng), + DeathReason::Both => air_and_energy_string(lng), } } } -/// Defines the type of Screen which is Infoscreen currently showing +/// Defines the type of Screen which is Info screen currently showing #[derive(Copy, Clone, Debug, PartialEq)] pub enum ScreenType { Death, @@ -40,76 +39,92 @@ pub enum ScreenType { Winning, } -/// Create `DeathScreen`, `IntroScreen` or `WinningSreen`. `DeathScreen` needs the reason of death from `DeathReason` enum. +/// Create `DeathScreen`, `IntroScreen` or `WinningScreen`. `DeathScreen` needs the reason of death from `DeathReason` enum. #[derive(Debug)] pub struct InfoScreen { - background: String, + background: &'static str, main_message: graphics::Text, additional_text: graphics::Text, sender: Sender, - screentype: ScreenType, + screen_type: ScreenType, background_image: Option, + lng: Lang, } impl InfoScreen { - /// Creates a new `DeathScreen` using `InfoScreen` with a `Deathreason` + /// Creates a new `DeathScreen` using `InfoScreen` with a `Death reason` /// # Arguments /// * `death_reason` - The reason for the death of the player /// * `sender` - The sender to send the command to the `ScreenStack` - pub fn new_deathscreen(death_reason: DeathReason, sender: Sender) -> Self { - info!("The player died due to a lack of : {:?}", death_reason); + pub fn new_death_screen( + death_reason: DeathReason, + sender: Sender, + lng: Lang, + ) -> Self { + info!( + "The player died due to a lack of : {:?}", + death_reason.t(lng) + ); - let mut main_message = graphics::Text::new(format!("{DEATH_REASON_STRING} {death_reason}")); + let mut main_message = graphics::Text::new(format!( + "{} {}", + death_reason_string(lng), + death_reason.t(lng) + )); main_message.set_scale(70.); - let mut additional_text = graphics::Text::new(ADDITIONAL_INFO_STRING); + let mut additional_text = graphics::Text::new(additional_info_string(lng)); additional_text.set_scale(70.); - let background = "deathscreen".to_string(); + let background = "deathscreen"; let screentype = ScreenType::Death; Self { background, main_message, additional_text, sender, - screentype, + screen_type: screentype, background_image: None, + lng, } } /// Creates a new `IntroScreen` using `InfoScreen` /// # Arguments /// * `sender` - The sender to send the command to the `ScreenStack` - pub fn new_introscreen(sender: Sender) -> Self { - let mut main_message = graphics::Text::new(format!("{INTRO_TEXT} \n{TUTORIAL_TEXT}")); + pub fn new_intro_screen(sender: Sender, lng: Lang) -> Self { + let mut main_message = + graphics::Text::new(format!("{} \n{}", intro_text(lng), tutorial_text(lng))); main_message.set_scale(50.); - let mut additional_text = graphics::Text::new(BUTTON_INFO); + let mut additional_text = graphics::Text::new(button_info(lng)); additional_text.set_scale(50.); - let background = "Introscreen".to_string(); - let screentype = ScreenType::Intro; + let background = "Introscreen"; + let screen_type = ScreenType::Intro; Self { background, main_message, additional_text, sender, - screentype, + screen_type, background_image: None, + lng, } } /// Creates a new Winning using `InfoScreen` /// # Arguments /// * `sender` - The sender to send the command to the `ScreenStack` - pub fn new_winningscreen(sender: Sender) -> Self { - let mut main_message = graphics::Text::new(WINNING_TEXT); + pub fn new_winning_screen(sender: Sender, lng: Lang) -> Self { + let mut main_message = graphics::Text::new(winning_text(lng)); main_message.set_scale(70.); - let mut additional_text = graphics::Text::new(ADDITIONAL_INFO_STRING); + let mut additional_text = graphics::Text::new(additional_info_string(lng)); additional_text.set_scale(70.); - let background = "Winningscreen".to_string(); - let screentype = ScreenType::Winning; + let background = "Winningscreen"; + let screen_type = ScreenType::Winning; Self { background, main_message, additional_text, sender, - screentype, + screen_type, background_image: None, + lng, } } } @@ -121,6 +136,7 @@ impl Screen for InfoScreen { /// # Returns /// `RLResult` - Returns an `RLResult`. fn update(&mut self, ctx: &mut Context) -> RLResult { + let lng = self.lng; if self.background_image.is_none() { self.background_image = Some(graphics::Image::from_bytes( ctx, @@ -128,12 +144,12 @@ impl Screen for InfoScreen { )?); } let keys = ctx.keyboard.pressed_keys(); - // Here we only use the first pressed key, but in the infoscreen this is fine - match (self.screentype, keys.iter().next()) { + // Here we only use the first pressed key, but in the info screen this is fine + match (self.screen_type, keys.iter().next()) { (ScreenType::Intro, Some(&VirtualKeyCode::Space)) => { self.sender.send(StackCommand::Pop)?; self.sender.send(StackCommand::Push(Box::new({ - let mut gamestate = GameState::new(ctx)?; + let mut gamestate = GameState::new(ctx, lng)?; gamestate.init(ctx)?; gamestate.create_machine(); gamestate @@ -145,12 +161,13 @@ impl Screen for InfoScreen { })))?; } (ScreenType::Death | ScreenType::Winning, Some(&VirtualKeyCode::Escape)) => { - if self.screentype == ScreenType::Winning { + if self.screen_type == ScreenType::Winning { GameState::delete_saves()?; } self.sender.send(StackCommand::Pop)?; self.sender.send(StackCommand::Push(Box::new(MainMenu::new( self.sender.clone(), + lng, ))))?; } _ => {} @@ -169,7 +186,7 @@ impl Screen for InfoScreen { if let Some(background) = &self.background_image { canvas.draw(background, graphics::DrawParam::default().scale(scale)); } - if self.screentype == ScreenType::Intro { + if self.screen_type == ScreenType::Intro { draw!(canvas, &self.main_message, Vec2::new(300., 300.), scale); } else { draw!(canvas, &self.main_message, Vec2::new(220., 500.), scale); @@ -185,4 +202,8 @@ impl Screen for InfoScreen { fn set_sender(&mut self, sender: Sender) { self.sender = sender; } + + fn lang(&self) -> Lang { + self.lng + } } diff --git a/game/src/game_core/player.rs b/game/src/game_core/player.rs index f79e20f..85b32e1 100644 --- a/game/src/game_core/player.rs +++ b/game/src/game_core/player.rs @@ -3,8 +3,7 @@ use crate::backend::rlcolor::RLColor; use crate::backend::screen::{Popup, StackCommand}; use crate::game_core::item::Item; use crate::game_core::resources::Resources; -use crate::languages::german::GAME_INFO; -use crate::languages::german::{BENZIN, GEDRUCKTESTEIL, SUPER_GLUE}; +use crate::languages::{game_info, petrol, printed_part, super_glue, Lang}; use crate::RLResult; use serde::{Deserialize, Serialize}; use std::sync::mpsc::Sender; @@ -28,14 +27,15 @@ pub struct Player { /// contains the current ingame time pub(crate) time: u32, } -impl Default for Player { - fn default() -> Self { + +impl Player { + pub fn new(lng: Lang) -> Self { info!("Default Player created"); Self { inventory: vec![ - (Item::new(SUPER_GLUE), 0), - (Item::new(BENZIN), 3), - (Item::new(GEDRUCKTESTEIL), 1), + (Item::new(*super_glue(lng)), 0), + (Item::new(*petrol(lng)), 3), + (Item::new(*printed_part(lng)), 1), ], position: (600, 500), resources: Resources { @@ -53,15 +53,16 @@ impl Default for Player { time: 0, } } -} - -impl Player { /// Checks whether the player has taken damage in the past few seconds and if not so start the regeneration /// # Arguments /// * `sender` - The sender of the screen, needed to send a `Popup` to the screen. /// # Returns /// * `RLResult` - validates if life regeneration was started correctly - pub(crate) fn life_regeneration(&mut self, sender: &Sender) -> RLResult { + pub(crate) fn life_regeneration( + &mut self, + sender: &Sender, + lng: Lang, + ) -> RLResult { match ( self.resources_change.life, self.last_damage, @@ -83,8 +84,8 @@ impl Player { (0, last_damage, _) if last_damage >= 8 * DESIRED_FPS => { self.resources_change.life += 5; self.last_damage = 0; - let popup = Popup::new(RLColor::GREEN, GAME_INFO[0].to_string(), 5); - info!("Player startet healing"); + let popup = Popup::new(RLColor::GREEN, game_info(lng)[0].to_string(), 5); + info!("Player started healing"); sender.send(StackCommand::Popup(popup))?; } // If player takes damage, increase last damage point @@ -127,11 +128,16 @@ impl Player { /// * `super_glue` - The amount of super glue /// * `benzin` - The amount of benzin /// * `gedrucktesteil` - The amount of the printed part -pub fn gen_inventory(super_glue: i32, benzin: i32, gedrucktesteil: i32) -> Vec<(Item, i32)> { +pub fn gen_inventory( + super_glue_amount: i32, + petrol_amount: i32, + printed_parts_amount: i32, + lng: Lang, +) -> Vec<(Item, i32)> { vec![ - (Item::new(SUPER_GLUE), super_glue), - (Item::new(BENZIN), benzin), - (Item::new(GEDRUCKTESTEIL), gedrucktesteil), + (Item::new(*super_glue(lng)), super_glue_amount), + (Item::new(*petrol(lng)), petrol_amount), + (Item::new(*printed_part(lng)), printed_parts_amount), ] } @@ -143,7 +149,7 @@ mod test { use std::sync::mpsc::{channel, Receiver}; fn setup_gamestate() -> (GameState, Receiver) { - let mut gamestate = GameState::default(); + let mut gamestate = GameState::new_with_lang(Lang::De); let channel = channel(); gamestate.set_sender(channel.0); (gamestate, channel.1) @@ -151,12 +157,12 @@ mod test { #[test] fn test_case_one_life_regeneration() { let (mut gamestate, _) = setup_gamestate(); - let mut player = Player::default(); + let mut player = Player::new(Lang::De); player.resources.life = u16::MAX; player.resources_change.life = 5; player.last_damage = 1000; player - .life_regeneration(&gamestate.get_screen_sender().unwrap().clone()) + .life_regeneration(&gamestate.get_screen_sender().unwrap().clone(), Lang::De) .unwrap(); assert_eq!(player.resources_change.life, 0); assert_eq!(player.last_damage, 0); @@ -165,12 +171,12 @@ mod test { #[test] fn test_case_two_life_regeneration() { let (mut gamestate, _) = setup_gamestate(); - let mut player = Player::default(); + let mut player = Player::new(Lang::De); player.resources.life = 1000; player.resources_change.life = 5; player.last_damage = 1000; player - .life_regeneration(&gamestate.get_screen_sender().unwrap().clone()) + .life_regeneration(&gamestate.get_screen_sender().unwrap().clone(), Lang::De) .unwrap(); assert_eq!(player.last_damage, 0); } @@ -178,12 +184,12 @@ mod test { #[test] fn test_case_three_life_regeneration() { let (mut gamestate, _receiver) = setup_gamestate(); - let mut player = Player::default(); + let mut player = Player::new(Lang::De); player.resources.life = 1000; player.resources_change.life = 0; player.last_damage = 900; player - .life_regeneration(&gamestate.get_screen_sender().unwrap().clone()) + .life_regeneration(&gamestate.get_screen_sender().unwrap().clone(), Lang::De) .unwrap(); assert_eq!(player.resources_change.life, 5); assert_eq!(player.last_damage, 0); @@ -192,12 +198,12 @@ mod test { #[test] fn test_case_four_life_regeneration() { let (mut gamestate, _) = setup_gamestate(); - let mut player = Player::default(); + let mut player = Player::new(Lang::De); player.resources.life = 20000; player.last_damage = 400; player.resources_change.life = 0; player - .life_regeneration(&gamestate.get_screen_sender().unwrap().clone()) + .life_regeneration(&gamestate.get_screen_sender().unwrap().clone(), Lang::De) .unwrap(); assert_eq!(player.resources_change.life, 0); assert_eq!(player.last_damage, 401); @@ -214,10 +220,10 @@ mod test { life: -1, ..Default::default() }, - ..Player::default() + ..Player::new(Lang::De) }; player - .life_regeneration(&gamestate.get_screen_sender().unwrap().clone()) + .life_regeneration(&gamestate.get_screen_sender().unwrap().clone(), Lang::De) .unwrap(); assert_eq!(player.resources_change.life, -1); assert_eq!(player.last_damage, 0); diff --git a/game/src/languages/english.rs b/game/src/languages/english.rs new file mode 100644 index 0000000..8f31207 --- /dev/null +++ b/game/src/languages/english.rs @@ -0,0 +1,140 @@ +//! Contains constants for the language "English". + +/// Constant for item `Gedrucktesteil`. +pub const GEDRUCKTESTEIL: [&str; 3] = [ + "3D-printed part", + "A 3D-printed part that can be used to repair the communication module", + "3D-gedrucktes-Teil.png", +]; +/// Constant for the item `Superglue` +pub const SUPER_GLUE: [&str; 3] = [ + "SuperGlue", + "SuperGlue can be used to repair the machines or holes", + "SuperGlue.png", +]; +/// Constant for the item `Benzin` +pub const PETROL: [&str; 3] = [ + "Petrol", + "Petrol can be used with the emergency generator to generate electricity", + "Benzin.png", +]; + +/// Constant for the resource names. +pub(crate) const RESOURCE_NAME: [&str; 3] = ["Air", "Energy", "Life"]; + +/// The text for the warning-`Popup`s that appears in the top left corner. +pub const WARNINGS: [&str; 4] = [ + "A comet is on its way!", + "The power's out!", + "A sandstorm is on its way!", + "A machine is down!", +]; +/// The text for the mars-info-`Popup`s that appears in the top left corner. +pub const MARS_INFO: [&str; 5] = [ + "Mars is the 4th planet in our solar system", + "Mars is one of the Earth-like planets", + "The diameter of Mars is just under 6800 km", + "The mass of Mars is about one-tenth that of Earth", + "The distance to Mars is on average 228 million km", +]; +/// The text for the nasa-info-`Popup`s that appears in the top left corner. +pub const NASA_INFO: [&str; 5] = [ + "NASA stands for: National Aeronautics and Space Administration", + "NASA was founded in 1958", + "NASA is headquartered in Washington, D.C.", + "As part of the Apollo missions, NASA succeeded in putting the first man on the moon", + " NASA has over 17,000 employees", +]; +/// The text for the game-info-`Popup`s that appears in the top left corner. +pub const GAME_INFO: [&str; 1] = ["Life regeneration started"]; + +/// Constants for all strings used in deathscreen +pub const AIR_STRING: &str = "too little air"; +pub const ENERGY_STRING: &str = "Cold"; +pub const AIR_AND_ENERGY_STRING: &str = "Cold and too little air"; +pub const DEATH_REASON_STRING: &str = "You died of"; +pub const ADDITIONAL_INFO_STRING: &str = "Please press ESC!"; +pub const RESUME_ERROR_STRING: &str = "You need a score first"; + +/// Constant for all strings used in `IntroScreen` +pub const INTRO_TEXT: &str = "You are stranded on Mars and you have to survive. +To do that, you need to restore oxygen production. +Hopefully, you'll be able to repair communications, +so that you can be rescued."; + +pub const TUTORIAL_TEXT: &str = + "Move around with WASD. Interact with E.\nFor reference, you have your manual on H."; + +/// Constant for the Text used in the `Button` info +pub const BUTTON_INFO: &str = "Please press the space bar!"; + +/// Constants for all strings used in `WinningScreen` +pub const WINNING_TEXT: &str = "You've been saved!"; + +/// Constants for the events that can occur. +pub const COMET_STRIKE: [&str; 2] = [ + "COMET STRIKE", + "A comet strike has hit the earth and created a hole in the wall", +]; +pub const SANDSTORM: [&str; 2] = [ + "Sandstorm", + "A sandstorm, which leads to a malfunction of the oxygen generator", +]; +pub const INFORMATIONS_POPUP_NASA: [&str; 2] = [ + "Informations popup NASA", + "An information pop-up about NASA containing facts and information about NASA", +]; +pub const POWER_FAILURE: [&str; 2] = [ + "Power failure", + "A power failure resulting in a malfunction of the oxygen generator", +]; +pub const INFORMATIONS_POPUP_MARS: [&str; 2] = [ + "Informations popup Mars", + "An information popup about Mars containing facts and information about Mars", +]; +/// Constants for the trade conflict. +pub const TRADE_CONFLICT_POPUP: [&str; 1] = + ["The following items are missing to execute the trade:"]; + +/// Constants for the `time_name`. +pub const TIME_NAME: [&str; 1] = ["Time"]; + +/// Constants for the text of the button in the main menu +pub const BUTTON_TEXT: [&str; 4] = ["Continue", "New Game", "Exit", "German"]; + +/// Contains all machine names as a vec of strings. +pub(crate) const MACHINE_NAMES: [&str; 7] = [ + "Oxygen generator", + "power generator", + "work machine", + "3D printer", + "communication module", + "Hole", + "Hole", +]; + +/// Contains the Messages that are displayed in the Handbook +pub(crate) const FIRST_MILESTONE_HANDBOOK_TEXT: [&str; 10] = [ + "- Repair the oxygen generator (top left)", + "- Repair the electricity generator (bottom left)", + "- Comets create holes in the walls", + "- Repair holes with SuperGlue", + "- In case of a power failure", + "you must restart the power generator", + "- Remember to use petrol sparingly", + "- You can stop the generator briefly,", + " if you have enough power", + "\n\n Press H to close", +]; + +pub(crate) const SECOND_MILESTONE_HANDBOOK_TEXT: [&str; 7] = [ + "- Repair the communication system (right)", + "- Send a message to be rescued", + "- Your power may still fail,", + "while you're sending the message!", + "- When you send the message,", + "you automatically win.", + "\n\n Press H to close", +]; +pub(crate) const SEND_MSG_FAILURE: &str = + "The message cannot be sent until the system is restored."; diff --git a/game/src/languages/german.rs b/game/src/languages/german.rs index 05f952f..579f218 100644 --- a/game/src/languages/german.rs +++ b/game/src/languages/german.rs @@ -13,7 +13,7 @@ pub const SUPER_GLUE: [&str; 3] = [ "SuperGlue.png", ]; /// Constant for the item `Benzin` -pub const BENZIN: [&str; 3] = [ +pub const PETROL: [&str; 3] = [ "Benzin", "Benzin kann mit dem Notstromgenerator verwendet werden um Strom zu generieren", "Benzin.png", @@ -68,23 +68,23 @@ pub const BUTTON_INFO: &str = "Bitte drücke die Leertaste!"; pub const WINNING_TEXT: &str = "Du wurdest gerettet!"; /// Constants for the events that can occur. -pub const KOMETENEINSCHLAG: [&str; 2] = [ +pub const COMET_STRIKE: [&str; 2] = [ "KOMETENEINSCHLAG", "Ein KOMETENEINSCHLAG hat die Erde getroffen und hat ein Loch in der Wand erzeugt", ]; -pub const SANDSTURM: [&str; 2] = [ +pub const SANDSTORM: [&str; 2] = [ "Sandsturm", "Ein Sandsturm, welcher zu einer Störung des Sauerstoffgenerators führt", ]; -pub const INFORMATIONSPOPUP_NASA: [&str; 2] = [ +pub const INFORMATIONS_POPUP_NASA: [&str; 2] = [ "InformationspopupNASA", "Ein Informationspopup über die NASA, welches Fakten und Informationen über die NASA enthält", ]; -pub const STROMAUSFALL: [&str; 2] = [ +pub const POWER_FAILURE: [&str; 2] = [ "Stromausfall", "Ein Stromausfall, welcher zu einer Störung des Sauerstoffgenerators führt", ]; -pub const INFORMATIONSPOPUP_MARS: [&str; 2] = [ +pub const INFORMATIONS_POPUP_MARS: [&str; 2] = [ "InformationspopupMars", "Ein Informationspopup über Mars, welches Fakten und Informationen über den Mars enthält", ]; @@ -93,7 +93,7 @@ pub const TRADE_CONFLICT_POPUP: [&str; 1] = ["Es fehlen folgende Items, um den T /// Constants for the `time_name`. pub const TIME_NAME: [&str; 1] = ["Zeit"]; /// Constants for the text of the button in the main menu -pub const BUTTON_TEXT: [&str; 3] = ["Fortsetzen", "Neues Spiel", "Beenden"]; +pub const BUTTON_TEXT: [&str; 4] = ["Fortsetzen", "Neues Spiel", "Beenden", "English"]; /// Contains all machine names as a vec of strings. pub(crate) const MACHINE_NAMES: [&str; 7] = [ "Sauerstoffgenerator", @@ -127,3 +127,5 @@ pub(crate) const SECOND_MILESTONE_HANDBOOK_TEXT: [&str; 7] = [ " gewinnst du automatisch.", "\n\n Drücke H zum schließen", ]; +pub(crate) const SEND_MSG_FAILURE: &str = + "Die Nachricht kann nicht gesendet werden solange das System nicht wiederhergestellt ist"; diff --git a/game/src/languages/mod.rs b/game/src/languages/mod.rs index 789e328..35494fd 100644 --- a/game/src/languages/mod.rs +++ b/game/src/languages/mod.rs @@ -1 +1,75 @@ +pub(crate) mod english; pub(crate) mod german; + +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] +pub enum Lang { + De, + En, +} + +impl Default for Lang { + fn default() -> Self { + Self::En + } +} + +macro_rules! t { + ($lng: expr, $target: ident) => { + match $lng { + Lang::En => &english::$target, + Lang::De => &german::$target, + } + }; + + ($name: ident, $len: expr, $target: ident) => { + pub fn $name(lng: Lang) -> &'static [&'static str; $len] { + t!(lng, $target) + } + }; + + ($name: ident => $target: ident) => { + pub fn $name(lng: Lang) -> &'static str { + t!(lng, $target) + } + }; +} + +t!(petrol, 3, PETROL); +t!(printed_part, 3, GEDRUCKTESTEIL); +t!(super_glue, 3, SUPER_GLUE); +t!(machine_names, 7, MACHINE_NAMES); +t!(game_info, 1, GAME_INFO); +t!(mars_info, 5, MARS_INFO); +t!(nasa_info, 5, NASA_INFO); +t!(warnings, 4, WARNINGS); +t!(button_text, 4, BUTTON_TEXT); +t!(trade_conflict_popup, 1, TRADE_CONFLICT_POPUP); +t!( + first_milestone_handbook_text, + 10, + FIRST_MILESTONE_HANDBOOK_TEXT +); +t!( + second_milestone_handbook_text, + 7, + SECOND_MILESTONE_HANDBOOK_TEXT +); +t!(time_name, 1, TIME_NAME); +t!(informations_popup_mars, 2, INFORMATIONS_POPUP_MARS); +t!(informations_popup_nasa, 2, INFORMATIONS_POPUP_NASA); +t!(sandstorm, 2, SANDSTORM); +t!(comet_strike, 2, COMET_STRIKE); +t!(power_failure, 2, POWER_FAILURE); +t!(resource_name, 3, RESOURCE_NAME); + +t!(button_info => BUTTON_INFO); +t!(winning_text => WINNING_TEXT); +t!(additional_info_string => ADDITIONAL_INFO_STRING); +t!(resume_error_string => RESUME_ERROR_STRING); +t!(air_string => AIR_STRING); +t!(energy_string => ENERGY_STRING); +t!(air_and_energy_string => AIR_AND_ENERGY_STRING); +t!(death_reason_string => DEATH_REASON_STRING); +t!(intro_text => INTRO_TEXT); +t!(tutorial_text => TUTORIAL_TEXT); +t!(send_msg_failure => SEND_MSG_FAILURE); diff --git a/game/src/machines/machine.rs b/game/src/machines/machine.rs index 2d1c229..785eefb 100644 --- a/game/src/machines/machine.rs +++ b/game/src/machines/machine.rs @@ -1,5 +1,5 @@ //! This File handels everything about Machine -use crate::backend::constants::PLAYER_INTERACTION_RADIUS; +use crate::backend::constants::{ObjectId, TradeId, PLAYER_INTERACTION_RADIUS}; use crate::backend::gamestate::GameCommand; use crate::backend::rlcolor::RLColor; use crate::backend::screen::{Popup, StackCommand}; @@ -7,7 +7,7 @@ use crate::backend::utils::is_colliding; use crate::game_core::item::Item; use crate::game_core::player::Player; use crate::game_core::resources::Resources; -use crate::languages::german::TRADE_CONFLICT_POPUP; +use crate::languages::{trade_conflict_popup, Lang}; use crate::machines::machine::State::{Broken, Idle, Running}; use crate::machines::machine_sprite::MachineSprite; use crate::machines::trade::Trade; @@ -50,8 +50,8 @@ impl From for Color { /// we can reuse the same code for it #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Machine { - /// The name of the machine is used as a reference on which assets to load - pub name: String, + /// Information about machine type + pub id: ObjectId, /// Contains the current state pub state: State, /// The hitbox is the area the player is prevented from walking into @@ -89,20 +89,20 @@ impl Machine { /// # Returns /// * 'Machine' fn new( - name: String, - hitbox: Rect, + id: ObjectId, + hit_box: Rect, trades: Vec, running_resources: Resources, ) -> Self { - info!("Creating new machine: name: {}", name); + info!("Creating new machine: name: {:?}", id); Self { - name, - hitbox, + id, + hitbox: hit_box, interaction_area: Rect { - x: hitbox.x - PLAYER_INTERACTION_RADIUS, - y: hitbox.y - PLAYER_INTERACTION_RADIUS, - w: hitbox.w + (PLAYER_INTERACTION_RADIUS * 2.), - h: hitbox.h + (PLAYER_INTERACTION_RADIUS * 2.), + x: hit_box.x - PLAYER_INTERACTION_RADIUS, + y: hit_box.y - PLAYER_INTERACTION_RADIUS, + w: hit_box.w + (PLAYER_INTERACTION_RADIUS * 2.), + h: hit_box.h + (PLAYER_INTERACTION_RADIUS * 2.), }, state: Broken, sprite: None, @@ -118,13 +118,13 @@ impl Machine { /// Alternative new constructor for the machine using one parameter Tupel /// # Arguments - /// * `(name, hit_box, trades, running_resources)` - Tupel containing the same arguments as `new()` + /// * `(name, hit_box, trades, running_resources)` - Tuple containing the same arguments as `new()` /// # Returns /// * 'Machine' pub(crate) fn new_by_const( - (name, hit_box, trades, running_resources): (String, Rect, Vec, Resources), + (id, hit_box, trades, running_resources): (ObjectId, Rect, Vec, Resources), ) -> Self { - Machine::new(name, hit_box, trades, running_resources) + Self::new(id, hit_box, trades, running_resources) } /// initialises the Maschine with the data that is not Serialize @@ -142,9 +142,9 @@ impl Machine { self.sprite = Some(images.into()); self.sender = Some(sender); self.screen_sender = Some(screen_sender); - if self.name == "Loch" { + if self.id.is_hole() { // Constant (pos of hole) - if self.hitbox.x == 780. { + if (self.hitbox.x - 780.).abs() < f32::EPSILON { self.change_state_to(&Running); } else { self.change_state_to(&Idle); @@ -180,22 +180,22 @@ impl Machine { is_colliding(pos, &self.interaction_area) } - /// Handel's the interaction of the Maschine and the player + /// Handel's the interaction of the Machine and the player /// # Arguments /// * `player` - of type `& Player` is a reference to the player - pub(crate) fn interact(&mut self, player: &Player) -> RLResult { + pub(crate) fn interact(&mut self, player: &Player, lng: Lang) -> RLResult { // Check if there is a possible trade let trade = match self.trades.iter().find(|t| t.initial_state == self.state) { Some(t) => t.clone(), None => return Ok(()), }; - if trade.name == *"no_Trade" { + if trade.id == TradeId::NoTrade { return Ok(()); } + // Check if the player has energy (and its needed) - if player.resources.energy == 0 && self.running_resources.energy < 0 && self.name != "Loch" - { + if player.resources.energy == 0 && self.running_resources.energy < 0 && self.id.is_hole() { return Ok(()); } // dif = the different between items the player has and the cost of the trade @@ -211,7 +211,7 @@ impl Machine { dif.iter() .map(|(item, amount)| format!("*{} {}\n", amount * -1, item.name)) .for_each(|x| missing_items.push_str(&x)); - let popup = Popup::info(format!("{}\n{missing_items}", TRADE_CONFLICT_POPUP[0])); + let popup = Popup::info(format!("{}\n{missing_items}", trade_conflict_popup(lng)[0])); info!( "Popup for Trade conflict sent: Missing Items: {}", missing_items @@ -224,7 +224,7 @@ impl Machine { } // At this point all checks have passed and continue with executing the trade - info!("Executing trade:{} ", trade.name); + info!("Executing trade: {:?} ", trade.id); // Remove the cost of the trade from the players inventory by sending the demand to the AddItem GameCommand let items_cost = trade @@ -270,7 +270,7 @@ impl Machine { if self.last_trade.return_after_timer { // handel edge case for wining the game - if self.last_trade.name == "Notfall_signal_absetzen" { + if self.last_trade.id == TradeId::EmergencySignalOff { return Ok(self.sender.as_ref().unwrap().send(GameCommand::Winning)?); } self.change_state_to(&self.last_trade.initial_state.clone()); @@ -307,7 +307,7 @@ impl Machine { } /// A helper funktion to disable every funktion in case there is no energy in the system pub(crate) fn no_energy(&mut self) { - if self.running_resources.energy < 0 && self.name != "Loch" { + if self.running_resources.energy < 0 && !self.id.is_hole() { // If there is no energy available but this machine needs some, stop this machine. if self.state == Running { self.change_state_to(&Idle); diff --git a/game/src/machines/trade.rs b/game/src/machines/trade.rs index e8b5fbe..7dbc439 100644 --- a/game/src/machines/trade.rs +++ b/game/src/machines/trade.rs @@ -1,4 +1,5 @@ //! This File contains the structure `Trade` +use crate::backend::constants::TradeId; use crate::game_core::item::Item; use crate::machines::machine::State; use serde::{Deserialize, Serialize}; @@ -8,8 +9,9 @@ use serde::{Deserialize, Serialize}; /// This means that repairing, starting or pausing a machine are all trades /// Trade itself does not contain any logic and is mostly a construct to group values pub struct Trade { - /// is used for debugging and login purposes - pub(crate) name: String, + pub id: TradeId, + // /// is used for debugging and login purposes + // pub(crate) name: String, /// the time it takes for the trade to conclude 0 = instant pub time_ticks: i16, /// the Machine needs to be in `initial_state` for the trade to be accessible @@ -32,7 +34,7 @@ impl Default for Trade { /// default values have no meaning and should never be checked on fn default() -> Self { Self { - name: "no_Trade".to_string(), + id: TradeId::NoTrade, time_ticks: 0, initial_state: State::Broken, resulting_state: State::Running, @@ -45,7 +47,7 @@ impl Default for Trade { impl Trade { ///initialises a new Trade using values passed in pub fn new( - name: String, + id: TradeId, time_ticks: i16, initial_state: State, resulting_state: State, @@ -53,7 +55,7 @@ impl Trade { cost: Vec<(Item, i32)>, ) -> Self { Self { - name, + id, time_ticks, initial_state, resulting_state, diff --git a/game/src/main.rs b/game/src/main.rs index 3404896..49e7ef8 100644 --- a/game/src/main.rs +++ b/game/src/main.rs @@ -24,9 +24,10 @@ mod machines; mod main_menu; use crate::backend::constants::SCREEN_RESOLUTION; -use crate::backend::{error, screen::Screenstack}; +use crate::backend::{error, screen::ScreenStack}; use chrono::Local; +use crate::languages::Lang; #[cfg_attr(debug_assertions, allow(unused_imports))] use ggez::conf::FullscreenType; use ggez::{event, Context}; @@ -65,7 +66,8 @@ pub fn main() -> RLResult { let (mut ctx, event_loop) = cb.build()?; info!("New Event Loop created"); window_setup(&mut ctx)?; - let screen_stack = Screenstack::default(); + let lng = Lang::En; + let screen_stack = ScreenStack::new_with_lang(lng); event::run(ctx, event_loop, screen_stack); } /// Sets the window size to resizeable in debug mode and fullscreen mode for release mode diff --git a/game/src/main_menu/button.rs b/game/src/main_menu/button.rs index 0d3b725..faafbed 100644 --- a/game/src/main_menu/button.rs +++ b/game/src/main_menu/button.rs @@ -1,5 +1,5 @@ use crate::backend::utils::{get_draw_params, get_scale}; -use crate::main_menu::mainmenu::Message; +use crate::main_menu::main_menu::Message; use crate::{draw, RLResult}; use ggez::glam::f32::Vec2; use ggez::graphics::{Canvas, Color, Text, TextFragment}; diff --git a/game/src/main_menu/mainmenu.rs b/game/src/main_menu/main_menu.rs similarity index 66% rename from game/src/main_menu/mainmenu.rs rename to game/src/main_menu/main_menu.rs index 83af6bc..c2d0bd0 100644 --- a/game/src/main_menu/mainmenu.rs +++ b/game/src/main_menu/main_menu.rs @@ -5,12 +5,11 @@ use crate::backend::{ utils::get_scale, }; use crate::main_menu::button::Button; -use crate::main_menu::mainmenu::Message::{Exit, NewGame, Resume}; use crate::RLResult; use crate::backend::screen::Popup; use crate::game_core::infoscreen::InfoScreen; -use crate::languages::german::{BUTTON_TEXT, RESUME_ERROR_STRING}; +use crate::languages::{button_text, resume_error_string, Lang}; use ggez::{graphics, Context}; use std::sync::mpsc::{channel, Receiver, Sender}; @@ -20,6 +19,7 @@ pub enum Message { Exit, NewGame, Resume, + ChangeLanguage, } /// Main menu screen of the game with buttons to start a new game, load a game or exit the game. @@ -27,8 +27,10 @@ pub enum Message { pub struct MainMenu { buttons: Vec