diff --git a/CHANGELOG.md b/CHANGELOG.md index da63bfdf8b..c7f4912b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added hit_timing to BasicMelee abilities - A tavern building where npcs go to relax. - Toggle for walking instead of running (Default: `I`). +- Added day duration slider configuration on map creation UI. ### Changed diff --git a/assets/voxygen/i18n/en/main.ftl b/assets/voxygen/i18n/en/main.ftl index 7c7ef8a1c8..bb137293ff 100644 --- a/assets/voxygen/i18n/en/main.ftl +++ b/assets/voxygen/i18n/en/main.ftl @@ -7,30 +7,30 @@ main-tip = Tip: main-unbound_key_tip = unbound main-notice = Welcome to the alpha version of Veloren! - + Before you dive into the fun, please keep a few things in mind: - + - This is a very early alpha. Expect bugs, extremely unfinished gameplay, unpolished mechanics, and missing features. - + - If you have constructive feedback or bug reports, you can contact us via Reddit, GitLab, or our community Discord server. - + - Veloren is licensed under the GPL 3 open-source licence. That means you're free to play, modify, and redistribute the game however you wish (provided derived work is also under GPL 3). - + - Veloren is a non-profit community project, and everybody working on it is a volunteer. If you like what you see, you're welcome to join the development or art teams! - + Thanks for taking the time to read this notice, we hope you enjoy the game! - + ~ The Veloren Devs main-login_process = Information on the Login Process: - + Please note that you need an account to play on auth-enabled servers. - + You can create an account over at - + https://veloren.net/account/. main-singleplayer-new = New main-singleplayer-delete = Delete @@ -38,6 +38,7 @@ main-singleplayer-regenerate = Regenerate main-singleplayer-create_custom = Create Custom main-singleplayer-invalid_name = Error: Invalid name main-singleplayer-seed = Seed +main-singleplayer-day_length = Day duration main-singleplayer-random_seed = Random main-singleplayer-size_lg = Logarithmic size main-singleplayer-map_large_warning = Warning: Large worlds will take a long time to start for the first time diff --git a/common/src/consts.rs b/common/src/consts.rs index 8da03b3598..ea0017acdd 100644 --- a/common/src/consts.rs +++ b/common/src/consts.rs @@ -35,3 +35,6 @@ pub const ENERGY_PER_LEVEL: u16 = 5; pub const HP_PER_LEVEL: u16 = 5; pub const TELEPORTER_RADIUS: f32 = 3.; + +// Map settings +pub const DAY_LENGTH_DEFAULT: f64 = 30.0; diff --git a/server/src/settings.rs b/server/src/settings.rs index 4756011a66..011f4e32f3 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -16,6 +16,7 @@ pub use whitelist::{Whitelist, WhitelistInfo, WhitelistRecord}; use chrono::Utc; use common::{ calendar::{Calendar, CalendarEvent}, + consts::DAY_LENGTH_DEFAULT, resources::BattleMode, rtsim::WorldSettings, }; @@ -205,7 +206,7 @@ impl Default for Settings { world_seed: DEFAULT_WORLD_SEED, server_name: "Veloren Server".into(), max_players: 100, - day_length: 30.0, + day_length: DAY_LENGTH_DEFAULT, start_time: 9.0 * 3600.0, map_file: None, max_view_distance: Some(65), diff --git a/voxygen/src/menu/main/ui/mod.rs b/voxygen/src/menu/main/ui/mod.rs index cf83b03dd3..ff5181d575 100644 --- a/voxygen/src/menu/main/ui/mod.rs +++ b/voxygen/src/menu/main/ui/mod.rs @@ -92,6 +92,7 @@ const BG_IMGS: [&str; 14] = [ pub enum WorldChange { Name(String), Seed(u32), + DayLength(f64), SizeX(u32), SizeY(u32), Scale(f64), @@ -108,6 +109,7 @@ impl WorldChange { match self { WorldChange::Name(name) => world.name = name, WorldChange::Seed(seed) => world.seed = seed, + WorldChange::DayLength(d) => world.day_length = d, WorldChange::SizeX(s) => gen_opts.x_lg = s, WorldChange::SizeY(s) => gen_opts.y_lg = s, WorldChange::Scale(scale) => gen_opts.scale = scale, diff --git a/voxygen/src/menu/main/ui/world_selector.rs b/voxygen/src/menu/main/ui/world_selector.rs index 1bff0c83db..1e4c36f19b 100644 --- a/voxygen/src/menu/main/ui/world_selector.rs +++ b/voxygen/src/menu/main/ui/world_selector.rs @@ -47,6 +47,7 @@ pub struct Screen { world_name: text_input::State, map_seed: text_input::State, + day_length: slider::State, random_seed_button: button::State, world_size_x: slider::State, world_size_y: slider::State, @@ -196,6 +197,9 @@ impl Screen { const SLIDER_BAR_PAD: u16 = 0; // Height of interactable area const SLIDER_HEIGHT: u16 = 30; + // Day length slider values + pub const DAY_LENGTH_MIN: f64 = 10.0; + pub const DAY_LENGTH_MAX: f64 = 60.0; let mut gen_content = vec![ BackgroundContainer::new( @@ -288,6 +292,42 @@ impl Screen { gen_content.push(Row::with_children(seed_content).into()); if let Some(gen_opts) = world.gen_opts.as_ref() { + // Day length setting label + gen_content.push( + Text::new(format!( + "{}: {}", + i18n.get_msg("main-singleplayer-day_length"), + world.day_length + )) + .size(SLIDER_TEXT_SIZE) + .horizontal_alignment(iced::HorizontalAlignment::Center) + .into(), + ); + + // Day length setting slider + if can_edit { + gen_content.push( + Row::with_children(vec![ + Slider::new( + &mut self.day_length, + DAY_LENGTH_MIN..=DAY_LENGTH_MAX, + world.day_length, + move |d| message(WorldChange::DayLength(d)), + ) + .height(SLIDER_HEIGHT) + .style(style::slider::Style::images( + imgs.slider_indicator, + imgs.slider_range, + SLIDER_BAR_PAD, + SLIDER_CURSOR_SIZE, + SLIDER_BAR_HEIGHT, + )) + .into(), + ]) + .into(), + ) + } + gen_content.push( Text::new(format!( "{}: x: {}, y: {}", diff --git a/voxygen/src/singleplayer/mod.rs b/voxygen/src/singleplayer/mod.rs index 6920188078..9c0280295c 100644 --- a/voxygen/src/singleplayer/mod.rs +++ b/voxygen/src/singleplayer/mod.rs @@ -87,6 +87,7 @@ impl SingleplayerState { settings.map_file = Some(file_opts); settings.world_seed = world.seed; + settings.day_length = world.day_length; let (stop_server_s, stop_server_r) = unbounded(); diff --git a/voxygen/src/singleplayer/singleplayer_world.rs b/voxygen/src/singleplayer/singleplayer_world.rs index 8885064b08..4f76ee1198 100644 --- a/voxygen/src/singleplayer/singleplayer_world.rs +++ b/voxygen/src/singleplayer/singleplayer_world.rs @@ -1,9 +1,10 @@ use std::{ fs, + io::Read, path::{Path, PathBuf}, }; -use common::assets::ASSETS_PATH; +use common::{assets::ASSETS_PATH, consts::DAY_LENGTH_DEFAULT}; use serde::{Deserialize, Serialize}; use server::{FileOpts, GenOpts, DEFAULT_WORLD_MAP}; use tracing::error; @@ -18,6 +19,7 @@ struct World0 { pub struct SingleplayerWorld { pub name: String, pub gen_opts: Option, + pub day_length: f64, pub seed: u32, pub is_generated: bool, pub path: PathBuf, @@ -40,7 +42,12 @@ fn load_map(path: &Path) -> Option { return None; }; - version::try_load(&f, path) + let Ok(bytes) = f.bytes().collect::, _>>() else { + error!("Failed to read {}", meta_path.to_string_lossy()); + return None; + }; + + version::try_load(std::io::Cursor::new(bytes), path) } fn write_world_meta(world: &SingleplayerWorld) { @@ -78,11 +85,13 @@ fn migrate_old_singleplayer(from: &Path, to: &Path) { } let mut seed = 0; + let mut day_length = DAY_LENGTH_DEFAULT; let (map_file, gen_opts) = fs::read_to_string(to.join("server_config/settings.ron")) .ok() .and_then(|settings| { let settings: server::Settings = ron::from_str(&settings).ok()?; seed = settings.world_seed; + day_length = settings.day_length; Some(match settings.map_file? { FileOpts::LoadOrGenerate { name, opts, .. } => { (Some(PathBuf::from(name)), Some(opts)) @@ -107,6 +116,7 @@ fn migrate_old_singleplayer(from: &Path, to: &Path) { name: "singleplayer world".to_string(), gen_opts, seed, + day_length, path: to.to_path_buf(), // Isn't persisted so doesn't matter what it's set to. is_generated: false, @@ -226,6 +236,7 @@ impl SingleplayerWorlds { let new_world = SingleplayerWorld { name: "New World".to_string(), gen_opts: None, + day_length: DAY_LENGTH_DEFAULT, seed: 0, is_generated: false, map_path: path.join("map.bin"), @@ -251,13 +262,13 @@ mod version { use super::*; - pub type Current = V1; + pub type Current = V2; type LoadWorldFn = fn(R, &Path) -> Result; fn loaders<'a, R: std::io::Read + Clone>() -> &'a [LoadWorldFn] { // Step [4] - &[load_raw::] + &[load_raw::, load_raw::] } #[derive(Deserialize, Serialize)] @@ -269,18 +280,6 @@ mod version { seed: u32, } - impl V1 { - /// This function is only needed for the current version - pub fn from_world(world: &SingleplayerWorld) -> Self { - V1 { - version: 1, - name: world.name.clone(), - gen_opts: world.gen_opts.clone(), - seed: world.seed, - } - } - } - impl ToWorld for V1 { fn to_world(self, path: PathBuf) -> SingleplayerWorld { let map_path = path.join("map.bin"); @@ -290,6 +289,46 @@ mod version { name: self.name, gen_opts: self.gen_opts, seed: self.seed, + day_length: DAY_LENGTH_DEFAULT, + is_generated, + path, + map_path, + } + } + } + + #[derive(Deserialize, Serialize)] + pub struct V2 { + #[serde(deserialize_with = "version::<_, 2>")] + version: u64, + name: String, + gen_opts: Option, + seed: u32, + day_length: f64, + } + + impl V2 { + pub fn from_world(world: &SingleplayerWorld) -> Self { + V2 { + version: 2, + name: world.name.clone(), + gen_opts: world.gen_opts.clone(), + seed: world.seed, + day_length: world.day_length, + } + } + } + + impl ToWorld for V2 { + fn to_world(self, path: PathBuf) -> SingleplayerWorld { + let map_path = path.join("map.bin"); + let is_generated = fs::metadata(&map_path).is_ok_and(|f| f.is_file()); + + SingleplayerWorld { + name: self.name, + gen_opts: self.gen_opts, + seed: self.seed, + day_length: self.day_length, is_generated, path, map_path,