Merge branch 'zesterer/choose-starting-site' into 'master'

Allow new players to choose their starting site

See merge request veloren/veloren!3848
This commit is contained in:
Joshua Barretto 2023-04-02 00:45:25 +00:00
commit 17cdd3e38d
22 changed files with 500 additions and 201 deletions

View File

@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
will only lock the camera zoom while movement and combat inputs are also being pressed. will only lock the camera zoom while movement and combat inputs are also being pressed.
- Custom spots can be added without recompilation (only ron and vox files) - Custom spots can be added without recompilation (only ron and vox files)
- Setting in userdata/server/server_config/settings.ron that controls the length of each day/night cycle. - Setting in userdata/server/server_config/settings.ron that controls the length of each day/night cycle.
- Starting site can now be chosen during character creation
### Changed ### Changed
- Bats move slower and use a simple proportional controller to maintain altitude - Bats move slower and use a simple proportional controller to maintain altitude

BIN
assets/voxygen/element/ui/char_select/icons/town_marker.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -19,5 +19,10 @@ char_selection-eye_color = Eye Color
char_selection-skin = Skin char_selection-skin = Skin
char_selection-eyeshape = Eye Details char_selection-eyeshape = Eye Details
char_selection-accessories = Accessories char_selection-accessories = Accessories
char_selection-starting_site = Select Starting Area
char_selection-starting_site_next = Next
char_selection-starting_site_prev = Previous
char_selection-starting_site_name = { $name }
char_selection-starting_site_kind = Kind: { $kind }
char_selection-create_info_name = Your Character needs a name! char_selection-create_info_name = Your Character needs a name!
char_selection-version_mismatch = WARNING! This server is running a different, possibly incompatible game version. Please update your game. char_selection-version_mismatch = WARNING! This server is running a different, possibly incompatible game version. Please update your game.

View File

@ -84,11 +84,11 @@ vec4 fxaa(texture2D tex, sampler smplr, vec2 fragCoord, vec2 resolution,
float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce); float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce);
dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX), dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX),
max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX), max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX),
dir * rcpDirMin)) * inverseVP; dir * rcpDirMin)) * inverseVP * 0.75;
vec3 rgbA = 0.5 * ( vec3 rgbA = 0.5 * (
texture(sampler2D(tex, smplr), fragCoord * inverseVP + dir * (1.0 / 3.0 - 0.5)).xyz + texture(sampler2D(tex, smplr), fragCoord * inverseVP + dir * (1.0 / 3.0 - 0.5)).xyz +
texture(sampler2D(tex, smplr), fragCoord * inverseVP + dir * (2.0 / 3.0 - 0.5)).xyz); texture(sampler2D(tex, smplr), fragCoord * inverseVP + dir * (1.7 / 3.0 - 0.5)).xyz);
vec3 rgbB = rgbA * 0.5 + 0.25 * ( vec3 rgbB = rgbA * 0.5 + 0.25 * (
texture(sampler2D(tex, smplr), fragCoord * inverseVP + dir * -0.5).xyz + texture(sampler2D(tex, smplr), fragCoord * inverseVP + dir * -0.5).xyz +
texture(sampler2D(tex, smplr), fragCoord * inverseVP + dir * 0.5).xyz); texture(sampler2D(tex, smplr), fragCoord * inverseVP + dir * 0.5).xyz);

View File

@ -189,6 +189,7 @@ impl BotClient {
Some("common.items.weapons.sword.starter".to_string()), Some("common.items.weapons.sword.starter".to_string()),
None, None,
body.into(), body.into(),
None,
); );
client.load_character_list(); client.load_character_list();
} }

View File

@ -148,6 +148,7 @@ fn run_client(
Some("common.items.weapons.sword.starter".into()), Some("common.items.weapons.sword.starter".into()),
None, None,
body(), body(),
None,
); );
client.load_character_list(); client.load_character_list();

View File

@ -149,6 +149,8 @@ impl WorldData {
pub fn map_image(&self) -> &Arc<DynamicImage> { &self.map.0[0] } pub fn map_image(&self) -> &Arc<DynamicImage> { &self.map.0[0] }
pub fn topo_map_image(&self) -> &Arc<DynamicImage> { &self.map.0[1] }
pub fn min_chunk_alt(&self) -> f32 { self.map.2.x } pub fn min_chunk_alt(&self) -> f32 { self.map.2.x }
pub fn max_chunk_alt(&self) -> f32 { self.map.2.y } pub fn max_chunk_alt(&self) -> f32 { self.map.2.y }
@ -953,6 +955,7 @@ impl Client {
mainhand: Option<String>, mainhand: Option<String>,
offhand: Option<String>, offhand: Option<String>,
body: comp::Body, body: comp::Body,
start_site: Option<SiteId>,
) { ) {
self.character_list.loading = true; self.character_list.loading = true;
self.send_msg(ClientGeneral::CreateCharacter { self.send_msg(ClientGeneral::CreateCharacter {
@ -960,6 +963,7 @@ impl Client {
mainhand, mainhand,
offhand, offhand,
body, body,
start_site,
}); });
} }

View File

@ -48,6 +48,7 @@ pub enum ClientGeneral {
mainhand: Option<String>, mainhand: Option<String>,
offhand: Option<String>, offhand: Option<String>,
body: comp::Body, body: comp::Body,
start_site: Option<SiteId>,
}, },
DeleteCharacter(CharacterId), DeleteCharacter(CharacterId),
EditCharacter { EditCharacter {

View File

@ -272,7 +272,6 @@ pub enum ServerChatCommand {
GroupPromote, GroupPromote,
Health, Health,
Help, Help,
Home,
JoinFaction, JoinFaction,
Jump, Jump,
Kick, Kick,
@ -294,6 +293,7 @@ pub enum ServerChatCommand {
Region, Region,
ReloadChunks, ReloadChunks,
RemoveLights, RemoveLights,
Respawn,
RevokeBuild, RevokeBuild,
RevokeBuildAll, RevokeBuildAll,
Safezone, Safezone,
@ -487,7 +487,7 @@ impl ServerChatCommand {
"Display information about commands", "Display information about commands",
None, None,
), ),
ServerChatCommand::Home => cmd(vec![], "Return to the home town", Some(Moderator)), ServerChatCommand::Respawn => cmd(vec![], "Teleport to your waypoint", Some(Moderator)),
ServerChatCommand::JoinFaction => ChatCommandData::new( ServerChatCommand::JoinFaction => ChatCommandData::new(
vec![Any("faction", Optional)], vec![Any("faction", Optional)],
"Join/leave the specified faction", "Join/leave the specified faction",
@ -762,7 +762,7 @@ impl ServerChatCommand {
ServerChatCommand::GroupLeave => "group_leave", ServerChatCommand::GroupLeave => "group_leave",
ServerChatCommand::Health => "health", ServerChatCommand::Health => "health",
ServerChatCommand::Help => "help", ServerChatCommand::Help => "help",
ServerChatCommand::Home => "home", ServerChatCommand::Respawn => "respawn",
ServerChatCommand::JoinFaction => "join_faction", ServerChatCommand::JoinFaction => "join_faction",
ServerChatCommand::Jump => "jump", ServerChatCommand::Jump => "jump",
ServerChatCommand::Kick => "kick", ServerChatCommand::Kick => "kick",

View File

@ -1,7 +1,10 @@
use crate::persistence::{character_updater::CharacterUpdater, PersistedComponents}; use crate::persistence::{character_updater::CharacterUpdater, PersistedComponents};
use common::{ use common::{
character::CharacterId, character::CharacterId,
comp::{inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats}, comp::{
inventory::loadout_builder::LoadoutBuilder, Body, Inventory, Item, SkillSet, Stats,
Waypoint,
},
}; };
use specs::{Entity, WriteExpect}; use specs::{Entity, WriteExpect};
@ -32,6 +35,7 @@ pub fn create_character(
character_offhand: Option<String>, character_offhand: Option<String>,
body: Body, body: Body,
character_updater: &mut WriteExpect<'_, CharacterUpdater>, character_updater: &mut WriteExpect<'_, CharacterUpdater>,
waypoint: Option<Waypoint>,
) -> Result<(), CreationError> { ) -> Result<(), CreationError> {
// quick fix whitelist validation for now; eventually replace the // quick fix whitelist validation for now; eventually replace the
// `Option<String>` with an index into a server-provided list of starter // `Option<String>` with an index into a server-provided list of starter
@ -63,7 +67,6 @@ pub fn create_character(
.push(Item::new_from_asset_expect("common.items.food.cheese")) .push(Item::new_from_asset_expect("common.items.food.cheese"))
.expect("Inventory has at least 1 slot left!"); .expect("Inventory has at least 1 slot left!");
let waypoint = None;
let map_marker = None; let map_marker = None;
character_updater.create_character(entity, player_uuid, character_alias, PersistedComponents { character_updater.create_character(entity, player_uuid, character_alias, PersistedComponents {

View File

@ -13,7 +13,7 @@ use crate::{
weather::WeatherSim, weather::WeatherSim,
wiring, wiring,
wiring::OutputFormula, wiring::OutputFormula,
Server, Settings, SpawnPoint, StateExt, Server, Settings, StateExt,
}; };
use assets::AssetExt; use assets::AssetExt;
use authc::Uuid; use authc::Uuid;
@ -150,7 +150,7 @@ fn do_command(
ServerChatCommand::GroupPromote => handle_group_promote, ServerChatCommand::GroupPromote => handle_group_promote,
ServerChatCommand::Health => handle_health, ServerChatCommand::Health => handle_health,
ServerChatCommand::Help => handle_help, ServerChatCommand::Help => handle_help,
ServerChatCommand::Home => handle_home, ServerChatCommand::Respawn => handle_respawn,
ServerChatCommand::JoinFaction => handle_join_faction, ServerChatCommand::JoinFaction => handle_join_faction,
ServerChatCommand::Jump => handle_jump, ServerChatCommand::Jump => handle_jump,
ServerChatCommand::Kick => handle_kick, ServerChatCommand::Kick => handle_kick,
@ -874,25 +874,23 @@ fn handle_site(
Ok(()) Ok(())
} }
fn handle_home( fn handle_respawn(
server: &mut Server, server: &mut Server,
_client: EcsEntity, _client: EcsEntity,
target: EcsEntity, target: EcsEntity,
_args: Vec<String>, _args: Vec<String>,
_action: &ServerChatCommand, _action: &ServerChatCommand,
) -> CmdResult<()> { ) -> CmdResult<()> {
let home_pos = server.state.mut_resource::<SpawnPoint>().0; let waypoint = server
let time = *server.state.mut_resource::<Time>(); .state
.read_storage::<comp::Waypoint>()
.get(target)
.ok_or("No waypoint set")?
.get_pos();
position_mut(server, target, "target", |current_pos| { position_mut(server, target, "target", |current_pos| {
current_pos.0 = home_pos current_pos.0 = waypoint;
})?; })
insert_or_replace_component(
server,
target,
comp::Waypoint::temp_new(home_pos, time),
"target",
)
} }
fn handle_kill( fn handle_kill(

View File

@ -135,6 +135,9 @@ use world::{
IndexOwned, World, IndexOwned, World,
}; };
/// SpawnPoint corresponds to the default location that players are positioned
/// at if they have no waypoint. Players *should* always have a waypoint, so
/// this should basically never be used in practice.
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct SpawnPoint(pub Vec3<f32>); pub struct SpawnPoint(pub Vec3<f32>);
@ -395,6 +398,8 @@ impl Server {
#[cfg(feature = "worldgen")] #[cfg(feature = "worldgen")]
let spawn_point = SpawnPoint({ let spawn_point = SpawnPoint({
use world::civ::SiteKind;
let index = index.as_index_ref(); let index = index.as_index_ref();
// NOTE: all of these `.map(|e| e as [type])` calls should compile into no-ops, // NOTE: all of these `.map(|e| e as [type])` calls should compile into no-ops,
// but are needed to be explicit about casting (and to make the compiler stop // but are needed to be explicit about casting (and to make the compiler stop
@ -402,27 +407,14 @@ impl Server {
// Search for town defined by spawn_town server setting. If this fails, or is // Search for town defined by spawn_town server setting. If this fails, or is
// None, set spawn to the nearest town to the centre of the world // None, set spawn to the nearest town to the centre of the world
let spawn_chunk = match settings.spawn_town.as_ref().and_then(|spawn_town| { let center_chunk = world.sim().map_size_lg().chunks().map(i32::from) / 2;
world.civs().sites().find(|site| { let spawn_chunk = world
site.site_tmp .civs()
.map_or(false, |id| index.sites[id].name() == spawn_town) .sites()
}) .filter(|site| matches!(site.kind, SiteKind::Settlement | SiteKind::Refactor))
}) { .map(|site| site.center)
Some(t) => t.center, .min_by_key(|site_pos| site_pos.distance_squared(center_chunk))
None => { .unwrap_or(center_chunk);
let center_chunk = world.sim().map_size_lg().chunks().map(i32::from) / 2;
use world::civ::SiteKind;
world
.civs()
.sites()
.filter(|site| {
matches!(site.kind, SiteKind::Settlement | SiteKind::Refactor)
})
.map(|site| site.center)
.min_by_key(|site_pos| site_pos.distance_squared(center_chunk))
.unwrap_or(center_chunk)
},
};
world.find_accessible_pos(index, TerrainChunkSize::center_wpos(spawn_chunk), false) world.find_accessible_pos(index, TerrainChunkSize::center_wpos(spawn_chunk), false)
}); });
@ -558,7 +550,7 @@ impl Server {
// Initiate real-time world simulation // Initiate real-time world simulation
#[cfg(feature = "worldgen")] #[cfg(feature = "worldgen")]
{ {
rtsim::init(&mut state, &world, index.as_index_ref(), spawn_point); rtsim::init(&mut state, &world, index.as_index_ref());
weather::init(&mut state, &world); weather::init(&mut state, &world);
} }
#[cfg(not(feature = "worldgen"))] #[cfg(not(feature = "worldgen"))]

View File

@ -110,7 +110,6 @@ pub fn init(
state: &mut State, state: &mut State,
#[cfg(feature = "worldgen")] world: &world::World, #[cfg(feature = "worldgen")] world: &world::World,
#[cfg(feature = "worldgen")] index: world::IndexRef, #[cfg(feature = "worldgen")] index: world::IndexRef,
#[cfg(feature = "worldgen")] spawn_point: crate::SpawnPoint,
) { ) {
#[cfg(feature = "worldgen")] #[cfg(feature = "worldgen")]
let mut rtsim = RtSim::new(world.sim().get_size()); let mut rtsim = RtSim::new(world.sim().get_size());
@ -153,21 +152,6 @@ pub fn init(
.filter_map(|(site_id, site)| site.site_tmp.map(|id| (site_id, &index.sites[id]))) .filter_map(|(site_id, site)| site.site_tmp.map(|id| (site_id, &index.sites[id])))
{ {
use world::site::SiteKind; use world::site::SiteKind;
let spawn_town_id = world
.civs()
.sites
.iter()
.filter(|(_, site)| site.is_settlement())
.min_by_key(|(_, site)| {
let wpos = site
.center
.as_::<i64>()
.map2(TerrainChunk::RECT_SIZE.as_::<i64>(), |e, sz| {
e * sz + sz / 2
});
wpos.distance_squared(spawn_point.0.xy().map(|x| x as i64))
})
.map(|(id, _)| id);
match &site.kind { match &site.kind {
#[allow(clippy::single_match)] #[allow(clippy::single_match)]
SiteKind::Dungeon(dungeon) => match dungeon.dungeon_difficulty() { SiteKind::Dungeon(dungeon) => match dungeon.dungeon_difficulty() {
@ -177,12 +161,7 @@ pub fn init(
.civs() .civs()
.sites .sites
.iter() .iter()
.filter(|&(site_id, site)| { .filter(|(_, site)| site.is_settlement())
site.is_settlement()
// TODO: Remove this later, starting town should not be
// special-cased
&& spawn_town_id.map_or(false, |spawn_id| spawn_id != site_id)
})
.min_by_key(|(_, site)| { .min_by_key(|(_, site)| {
let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32); let wpos = site.center * TerrainChunk::RECT_SIZE.map(|e| e as i32);
wpos.map(|e| e as f32) wpos.map(|e| e as f32)

View File

@ -81,8 +81,6 @@ pub struct GameplaySettings {
#[serde(default)] #[serde(default)]
pub battle_mode: ServerBattleMode, pub battle_mode: ServerBattleMode,
#[serde(default)] #[serde(default)]
pub safe_spawn: bool,
#[serde(default)]
pub explosion_burn_marks: bool, pub explosion_burn_marks: bool,
} }
@ -90,7 +88,6 @@ impl Default for GameplaySettings {
fn default() -> Self { fn default() -> Self {
Self { Self {
battle_mode: ServerBattleMode::default(), battle_mode: ServerBattleMode::default(),
safe_spawn: false,
explosion_burn_marks: true, explosion_burn_marks: true,
} }
} }
@ -175,7 +172,6 @@ pub struct Settings {
pub max_view_distance: Option<u32>, pub max_view_distance: Option<u32>,
pub max_player_group_size: u32, pub max_player_group_size: u32,
pub client_timeout: Duration, pub client_timeout: Duration,
pub spawn_town: Option<String>,
pub max_player_for_kill_broadcast: Option<usize>, pub max_player_for_kill_broadcast: Option<usize>,
pub calendar_mode: CalendarMode, pub calendar_mode: CalendarMode,
@ -213,7 +209,6 @@ impl Default for Settings {
max_player_group_size: 6, max_player_group_size: 6,
calendar_mode: CalendarMode::Auto, calendar_mode: CalendarMode::Auto,
client_timeout: Duration::from_secs(40), client_timeout: Duration::from_secs(40),
spawn_town: None,
max_player_for_kill_broadcast: None, max_player_for_kill_broadcast: None,
experimental_terrain_persistence: false, experimental_terrain_persistence: false,
gameplay: GameplaySettings::default(), gameplay: GameplaySettings::default(),

View File

@ -1,3 +1,8 @@
#[cfg(not(feature = "worldgen"))]
use crate::test_world::{IndexOwned, World};
#[cfg(feature = "worldgen")]
use world::{IndexOwned, World};
use crate::{ use crate::{
automod::AutoMod, automod::AutoMod,
character_creator, character_creator,
@ -7,17 +12,20 @@ use crate::{
EditableSettings, EditableSettings,
}; };
use common::{ use common::{
comp::{Admin, AdminRole, ChatType, Player, UnresolvedChatMsg}, comp::{Admin, AdminRole, ChatType, Player, UnresolvedChatMsg, Waypoint},
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
resources::Time,
terrain::TerrainChunkSize,
uid::Uid, uid::Uid,
}; };
use common_ecs::{Job, Origin, Phase, System}; use common_ecs::{Job, Origin, Phase, System};
use common_net::msg::{ClientGeneral, ServerGeneral}; use common_net::msg::{ClientGeneral, ServerGeneral};
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage};
use std::sync::{atomic::Ordering, Arc}; use std::sync::{atomic::Ordering, Arc};
use tracing::debug; use tracing::{debug, error};
impl Sys { impl Sys {
#[allow(clippy::too_many_arguments)] // Shhhh, go bother someone else clippy
fn handle_client_character_screen_msg( fn handle_client_character_screen_msg(
server_emitter: &mut common::event::Emitter<'_, ServerEvent>, server_emitter: &mut common::event::Emitter<'_, ServerEvent>,
entity: specs::Entity, entity: specs::Entity,
@ -32,6 +40,9 @@ impl Sys {
censor: &ReadExpect<'_, Arc<censor::Censor>>, censor: &ReadExpect<'_, Arc<censor::Censor>>,
automod: &AutoMod, automod: &AutoMod,
msg: ClientGeneral, msg: ClientGeneral,
time: Time,
index: &ReadExpect<'_, IndexOwned>,
world: &ReadExpect<'_, Arc<World>>,
) -> Result<(), crate::error::Error> { ) -> Result<(), crate::error::Error> {
let mut send_join_messages = || -> Result<(), crate::error::Error> { let mut send_join_messages = || -> Result<(), crate::error::Error> {
// Give the player a welcome message // Give the player a welcome message
@ -135,6 +146,7 @@ impl Sys {
mainhand, mainhand,
offhand, offhand,
body, body,
start_site,
} => { } => {
if censor.check(&alias) { if censor.check(&alias) {
debug!(?alias, "denied alias as it contained a banned word"); debug!(?alias, "denied alias as it contained a banned word");
@ -151,6 +163,22 @@ impl Sys {
offhand.clone(), offhand.clone(),
body, body,
character_updater, character_updater,
start_site.and_then(|site_idx| {
// TODO: This corresponds to the ID generation logic in `world/src/lib.rs`
// Really, we should have a way to consistently refer to sites, but that's a job for rtsim2
// and the site changes that it will require. Until then, this code is very hacky.
world.civs().sites.iter()
.find(|(_, site)| site.site_tmp.map(|i| i.id()) == Some(site_idx))
.map(Some)
.unwrap_or_else(|| {
error!("Tried to create character with starting site index {}, but such a site does not exist", site_idx);
None
})
.map(|(_, site)| {
let wpos2d = TerrainChunkSize::center_wpos(site.center);
Waypoint::new(world.find_accessible_pos(index.as_index_ref(), wpos2d, false), time)
})
}),
) { ) {
debug!( debug!(
?error, ?error,
@ -226,6 +254,9 @@ impl<'a> System<'a> for Sys {
ReadExpect<'a, EditableSettings>, ReadExpect<'a, EditableSettings>,
ReadExpect<'a, Arc<censor::Censor>>, ReadExpect<'a, Arc<censor::Censor>>,
ReadExpect<'a, AutoMod>, ReadExpect<'a, AutoMod>,
ReadExpect<'a, Time>,
ReadExpect<'a, IndexOwned>,
ReadExpect<'a, Arc<World>>,
); );
const NAME: &'static str = "msg::character_screen"; const NAME: &'static str = "msg::character_screen";
@ -247,6 +278,9 @@ impl<'a> System<'a> for Sys {
editable_settings, editable_settings,
censor, censor,
automod, automod,
time,
index,
world,
): Self::SystemData, ): Self::SystemData,
) { ) {
let mut server_emitter = server_event_bus.emitter(); let mut server_emitter = server_event_bus.emitter();
@ -267,6 +301,9 @@ impl<'a> System<'a> for Sys {
&censor, &censor,
&automod, &automod,
msg, msg,
*time,
&index,
&world,
) )
}); });
} }

View File

@ -12,7 +12,7 @@ use crate::{
presence::{Presence, RepositionOnChunkLoad}, presence::{Presence, RepositionOnChunkLoad},
rtsim::RtSim, rtsim::RtSim,
settings::Settings, settings::Settings,
ChunkRequest, SpawnPoint, Tick, ChunkRequest, Tick,
}; };
use common::{ use common::{
calendar::Calendar, calendar::Calendar,
@ -62,7 +62,6 @@ impl<'a> System<'a> for Sys {
type SystemData = ( type SystemData = (
Read<'a, EventBus<ServerEvent>>, Read<'a, EventBus<ServerEvent>>,
Read<'a, Tick>, Read<'a, Tick>,
Read<'a, SpawnPoint>,
Read<'a, Settings>, Read<'a, Settings>,
Read<'a, TimeOfDay>, Read<'a, TimeOfDay>,
Read<'a, Calendar>, Read<'a, Calendar>,
@ -95,7 +94,6 @@ impl<'a> System<'a> for Sys {
( (
server_event_bus, server_event_bus,
tick, tick,
spawn_point,
server_settings, server_settings,
time_of_day, time_of_day,
calendar, calendar,
@ -227,14 +225,6 @@ impl<'a> System<'a> for Sys {
}, },
} }
} }
// Insert a safezone if chunk contains the spawn position
if server_settings.gameplay.safe_spawn && is_spawn_chunk(key, *spawn_point) {
server_emitter.emit(ServerEvent::CreateSafezone {
range: Some(SAFE_ZONE_RADIUS),
pos: Pos(spawn_point.0),
});
}
} }
// TODO: Consider putting this in another system since this forces us to take // TODO: Consider putting this in another system since this forces us to take
@ -711,9 +701,3 @@ pub fn chunk_in_vd(player_chunk_pos: Vec2<i16>, player_vd_sqr: i32, chunk_pos: V
adjusted_dist_sqr <= player_vd_sqr adjusted_dist_sqr <= player_vd_sqr
} }
fn is_spawn_chunk(chunk_pos: Vec2<i32>, spawn_pos: SpawnPoint) -> bool {
// FIXME: Ensure spawn_pos doesn't overflow before performing this cast.
let spawn_chunk_pos = TerrainGrid::chunk_key(spawn_pos.0.map(|e| e as i32));
chunk_pos == spawn_chunk_pos
}

View File

@ -796,8 +796,7 @@ impl<'a> Widget for Map<'a> {
.graphics_for(state.ids.show_peaks_box) .graphics_for(state.ids.show_peaks_box)
.color(TEXT_COLOR) .color(TEXT_COLOR)
.set(state.ids.show_peaks_text, ui); .set(state.ids.show_peaks_text, ui);
// Voxel map (TODO: enable this once Pfau approves the final UI, and once
// there's a non-placeholder graphic for the checkbox)
const EXPOSE_VOXEL_MAP_TOGGLE_IN_UI: bool = false; const EXPOSE_VOXEL_MAP_TOGGLE_IN_UI: bool = false;
if EXPOSE_VOXEL_MAP_TOGGLE_IN_UI { if EXPOSE_VOXEL_MAP_TOGGLE_IN_UI {
Image::new(self.imgs.mmap_poi_peak) Image::new(self.imgs.mmap_poi_peak)

View File

@ -222,6 +222,9 @@ const SPEECH_BUBBLE_RANGE: f32 = NAMETAG_RANGE;
const EXP_FLOATER_LIFETIME: f32 = 2.0; const EXP_FLOATER_LIFETIME: f32 = 2.0;
const EXP_ACCUMULATION_DURATION: f32 = 0.5; const EXP_ACCUMULATION_DURATION: f32 = 0.5;
// TODO: Don't hard code this
pub fn default_water_color() -> Rgba<f32> { srgba_to_linear(Rgba::new(0.0, 0.18, 0.37, 1.0)) }
widget_ids! { widget_ids! {
struct Ids { struct Ids {
// Crosshair // Crosshair
@ -1303,15 +1306,15 @@ impl Hud {
ui.set_scaling_mode(settings.interface.ui_scale); ui.set_scaling_mode(settings.interface.ui_scale);
// Generate ids. // Generate ids.
let ids = Ids::new(ui.id_generator()); let ids = Ids::new(ui.id_generator());
// NOTE: Use a border the same color as the LOD ocean color (but with a
// translucent alpha since UI have transparency and LOD doesn't).
let water_color = srgba_to_linear(Rgba::new(0.0, 0.18, 0.37, 1.0));
// Load world map // Load world map
let mut layers = Vec::new(); let mut layers = Vec::new();
for layer in client.world_data().map_layers() { for layer in client.world_data().map_layers() {
layers.push( // NOTE: Use a border the same color as the LOD ocean color (but with a
ui.add_graphic_with_rotations(Graphic::Image(Arc::clone(layer), Some(water_color))), // translucent alpha since UI have transparency and LOD doesn't).
); layers.push(ui.add_graphic_with_rotations(Graphic::Image(
Arc::clone(layer),
Some(default_water_color()),
)));
} }
let world_map = (layers, client.world_data().chunk_size().map(|e| e as u32)); let world_map = (layers, client.world_data().chunk_size().map(|e| e as u32));
// Load images. // Load images.

View File

@ -117,10 +117,11 @@ impl PlayState for CharSelectionState {
mainhand, mainhand,
offhand, offhand,
body, body,
start_site,
} => { } => {
self.client self.client
.borrow_mut() .borrow_mut()
.create_character(alias, mainhand, offhand, body); .create_character(alias, mainhand, offhand, body, start_site);
}, },
ui::Event::EditCharacter { ui::Event::EditCharacter {
alias, alias,

View File

@ -1,4 +1,5 @@
use crate::{ use crate::{
hud::default_water_color,
render::UiDrawer, render::UiDrawer,
ui::{ ui::{
self, self,
@ -16,6 +17,7 @@ use crate::{
Element, IcedRenderer, IcedUi as Ui, Element, IcedRenderer, IcedUi as Ui,
}, },
img_ids::ImageGraphic, img_ids::ImageGraphic,
Graphic, GraphicId,
}, },
window, GlobalState, window, GlobalState,
}; };
@ -23,18 +25,22 @@ use client::{Client, ServerInfo};
use common::{ use common::{
character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER, MAX_NAME_LENGTH}, character::{CharacterId, CharacterItem, MAX_CHARACTERS_PER_PLAYER, MAX_NAME_LENGTH},
comp::{self, humanoid, inventory::slot::EquipSlot, Inventory, Item}, comp::{self, humanoid, inventory::slot::EquipSlot, Inventory, Item},
terrain::TerrainChunkSize,
vol::RectVolSize,
LoadoutBuilder, LoadoutBuilder,
}; };
use common_net::msg::world_msg::{SiteId, SiteInfo, SiteKind};
use i18n::{Localization, LocalizationHandle}; use i18n::{Localization, LocalizationHandle};
//ImageFrame, Tooltip, //ImageFrame, Tooltip,
use crate::settings::Settings; use crate::settings::Settings;
//use std::time::Duration; //use std::time::Duration;
//use ui::ice::widget; //use ui::ice::widget;
use iced::{ use iced::{
button, scrollable, slider, text_input, Align, Button, Column, Container, HorizontalAlignment, button, scrollable, slider, text_input, Align, Button, Color, Column, Container,
Length, Row, Scrollable, Slider, Space, Text, TextInput, HorizontalAlignment, Length, Row, Scrollable, Slider, Space, Text, TextInput,
}; };
use vek::Rgba; use std::sync::Arc;
use vek::{Rgba, Vec2};
pub const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0); pub const TEXT_COLOR: iced::Color = iced::Color::from_rgb(1.0, 1.0, 1.0);
pub const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2); pub const DISABLED_TEXT_COLOR: iced::Color = iced::Color::from_rgba(1.0, 1.0, 1.0, 0.2);
@ -121,6 +127,9 @@ image_ids_ice! {
// Tooltips // Tooltips
tt_edge: "voxygen.element.ui.generic.frames.tooltip.edge", tt_edge: "voxygen.element.ui.generic.frames.tooltip.edge",
tt_corner: "voxygen.element.ui.generic.frames.tooltip.corner", tt_corner: "voxygen.element.ui.generic.frames.tooltip.corner",
// Startzone Selection
town_marker: "voxygen.element.ui.char_select.icons.town_marker",
} }
} }
@ -133,6 +142,7 @@ pub enum Event {
mainhand: Option<String>, mainhand: Option<String>,
offhand: Option<String>, offhand: Option<String>,
body: comp::Body, body: comp::Body,
start_site: Option<SiteId>,
}, },
EditCharacter { EditCharacter {
alias: String, alias: String,
@ -168,13 +178,20 @@ enum Mode {
species_buttons: [button::State; 6], species_buttons: [button::State; 6],
tool_buttons: [button::State; 6], tool_buttons: [button::State; 6],
sliders: Sliders, sliders: Sliders,
scroll: scrollable::State, left_scroll: scrollable::State,
right_scroll: scrollable::State,
name_input: text_input::State, name_input: text_input::State,
back_button: button::State, back_button: button::State,
create_button: button::State, create_button: button::State,
rand_character_button: button::State, rand_character_button: button::State,
rand_name_button: button::State, rand_name_button: button::State,
prev_starting_site_button: button::State,
next_starting_site_button: button::State,
/// `character_id.is_some()` can be used to determine if we're in edit
/// mode as opposed to create mode.
// TODO: Something less janky? Express the problem domain better!
character_id: Option<CharacterId>, character_id: Option<CharacterId>,
start_site_idx: usize,
}, },
} }
@ -217,13 +234,17 @@ impl Mode {
species_buttons: Default::default(), species_buttons: Default::default(),
tool_buttons: Default::default(), tool_buttons: Default::default(),
sliders: Default::default(), sliders: Default::default(),
scroll: Default::default(), left_scroll: Default::default(),
right_scroll: Default::default(),
name_input: Default::default(), name_input: Default::default(),
back_button: Default::default(), back_button: Default::default(),
create_button: Default::default(), create_button: Default::default(),
rand_character_button: Default::default(), rand_character_button: Default::default(),
rand_name_button: Default::default(), rand_name_button: Default::default(),
prev_starting_site_button: Default::default(),
next_starting_site_button: Default::default(),
character_id: None, character_id: None,
start_site_idx: 0,
} }
} }
@ -243,13 +264,17 @@ impl Mode {
species_buttons: Default::default(), species_buttons: Default::default(),
tool_buttons: Default::default(), tool_buttons: Default::default(),
sliders: Default::default(), sliders: Default::default(),
scroll: Default::default(), left_scroll: Default::default(),
right_scroll: Default::default(),
name_input: Default::default(), name_input: Default::default(),
back_button: Default::default(), back_button: Default::default(),
create_button: Default::default(), create_button: Default::default(),
rand_character_button: Default::default(), rand_character_button: Default::default(),
rand_name_button: Default::default(), rand_name_button: Default::default(),
prev_starting_site_button: Default::default(),
next_starting_site_button: Default::default(),
character_id: Some(character_id), character_id: Some(character_id),
start_site_idx: 0,
} }
} }
} }
@ -279,6 +304,9 @@ struct Controls {
// Id of the selected character // Id of the selected character
selected: Option<CharacterId>, selected: Option<CharacterId>,
default_name: String, default_name: String,
map_img: GraphicId,
possible_starting_sites: Vec<SiteInfo>,
world_sz: Vec2<u32>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -309,6 +337,9 @@ enum Message {
EyeColor(u8), EyeColor(u8),
Accessory(u8), Accessory(u8),
Beard(u8), Beard(u8),
StartingSite(usize),
PrevStartingSite,
NextStartingSite,
// Workaround for widgets that require a message but we don't want them to actually do // Workaround for widgets that require a message but we don't want them to actually do
// anything // anything
DoNothing, DoNothing,
@ -321,6 +352,9 @@ impl Controls {
selected: Option<CharacterId>, selected: Option<CharacterId>,
default_name: String, default_name: String,
server_info: &ServerInfo, server_info: &ServerInfo,
map_img: GraphicId,
possible_starting_sites: Vec<SiteInfo>,
world_sz: Vec2<u32>,
) -> Self { ) -> Self {
let version = common::util::DISPLAY_VERSION_LONG.clone(); let version = common::util::DISPLAY_VERSION_LONG.clone();
let alpha = format!("Veloren {}", common::util::DISPLAY_VERSION.as_str()); let alpha = format!("Veloren {}", common::util::DISPLAY_VERSION.as_str());
@ -339,6 +373,9 @@ impl Controls {
mode: Mode::select(Some(InfoContent::LoadingCharacters)), mode: Mode::select(Some(InfoContent::LoadingCharacters)),
selected, selected,
default_name, default_name,
map_img,
possible_starting_sites,
world_sz,
} }
} }
@ -393,6 +430,35 @@ impl Controls {
]) ])
.width(Length::Fill); .width(Length::Fill);
let mut warning_container = if let Some(mismatched_version) =
&self.server_mismatched_version
{
let warning = Text::<IcedRenderer>::new(format!(
"{}\n{}: {} {}: {}",
i18n.get_msg("char_selection-version_mismatch"),
i18n.get_msg("main-login-server_version"),
mismatched_version,
i18n.get_msg("main-login-client_version"),
*common::util::GIT_HASH
))
.size(self.fonts.cyri.scale(18))
.color(iced::Color::from_rgb(1.0, 0.0, 0.0))
.width(Length::Fill)
.horizontal_alignment(HorizontalAlignment::Center);
Some(
Container::new(
Container::new(Row::with_children(vec![warning.into()]).width(Length::Fill))
.style(style::container::Style::color(Rgba::new(0, 0, 0, 217)))
.padding(12)
.width(Length::Fill)
.center_x(),
)
.padding(16),
)
} else {
None
};
let content = match &mut self.mode { let content = match &mut self.mode {
Mode::Select { Mode::Select {
ref mut info_content, ref mut info_content,
@ -669,8 +735,8 @@ impl Controls {
let left_column = Column::with_children(vec![server.into(), characters.into()]) let left_column = Column::with_children(vec![server.into(), characters.into()])
.spacing(10) .spacing(10)
.width(Length::Units(322)) // TODO: see if we can get iced to work with settings below .width(Length::Units(322)) // TODO: see if we can get iced to work with settings below
//.max_width(360) // .max_width(360)
//.width(Length::Fill) // .width(Length::Fill)
.height(Length::Fill); .height(Length::Fill);
let top = Row::with_children(vec![ let top = Row::with_children(vec![
@ -831,7 +897,8 @@ impl Controls {
inventory: _, inventory: _,
mainhand, mainhand,
offhand: _, offhand: _,
ref mut scroll, ref mut left_scroll,
ref mut right_scroll,
ref mut body_type_buttons, ref mut body_type_buttons,
ref mut species_buttons, ref mut species_buttons,
ref mut tool_buttons, ref mut tool_buttons,
@ -841,7 +908,10 @@ impl Controls {
ref mut create_button, ref mut create_button,
ref mut rand_character_button, ref mut rand_character_button,
ref mut rand_name_button, ref mut rand_name_button,
ref mut prev_starting_site_button,
ref mut next_starting_site_button,
character_id, character_id,
start_site_idx,
} => { } => {
let unselected_style = style::button::Style::new(imgs.icon_border) let unselected_style = style::button::Style::new(imgs.icon_border)
.hover_image(imgs.icon_border_mo) .hover_image(imgs.icon_border_mo)
@ -1070,6 +1140,31 @@ impl Controls {
// Height of interactable area // Height of interactable area
const SLIDER_HEIGHT: u16 = 30; const SLIDER_HEIGHT: u16 = 30;
fn starter_slider<'a>(
text: String,
size: u16,
state: &'a mut slider::State,
max: u32,
selected_val: u32,
on_change: impl 'static + Fn(u32) -> Message,
imgs: &Imgs,
) -> Element<'a, Message> {
Column::with_children(vec![
Text::new(text).size(size).into(),
Slider::new(state, 0..=max, selected_val, on_change)
.height(SLIDER_HEIGHT)
.style(style::slider::Style::images(
imgs.slider_indicator,
imgs.slider_range,
SLIDER_BAR_PAD,
SLIDER_CURSOR_SIZE,
SLIDER_BAR_HEIGHT,
))
.into(),
])
.align_items(Align::Center)
.into()
}
fn char_slider<'a>( fn char_slider<'a>(
text: String, text: String,
state: &'a mut slider::State, state: &'a mut slider::State,
@ -1115,19 +1210,21 @@ impl Controls {
.into(), .into(),
// "Disabled" slider // "Disabled" slider
// TODO: add iced support for disabled sliders (like buttons) // TODO: add iced support for disabled sliders (like buttons)
Slider::new(state, 0..=max, selected_val, |_| Message::DoNothing) Slider::new(state, 0..=max.into(), selected_val.into(), |_| {
.height(SLIDER_HEIGHT) Message::DoNothing
.style(style::slider::Style { })
cursor: style::slider::Cursor::Color(Rgba::zero()), .height(SLIDER_HEIGHT)
bar: style::slider::Bar::Image( .style(style::slider::Style {
imgs.slider_range, cursor: style::slider::Cursor::Color(Rgba::zero()),
Rgba::from_translucent(255, 51), bar: style::slider::Bar::Image(
SLIDER_BAR_PAD, imgs.slider_range,
), Rgba::from_translucent(255, 51),
labels: false, SLIDER_BAR_PAD,
..Default::default() ),
}) labels: false,
.into(), ..Default::default()
})
.into(),
]) ])
.align_items(Align::Center) .align_items(Align::Center)
.into() .into()
@ -1213,7 +1310,7 @@ impl Controls {
tooltip::text(&tooltip_text, tooltip_style) tooltip::text(&tooltip_text, tooltip_style)
}); });
let column_content = vec![ let left_column_content = vec![
body_type.into(), body_type.into(),
tool.into(), tool.into(),
species.into(), species.into(),
@ -1221,47 +1318,215 @@ impl Controls {
rand_character.into(), rand_character.into(),
]; ];
let left_column = Container::new( let right_column_content = if character_id.is_none() {
Scrollable::new(scroll) let site_slider = starter_slider(
.push( i18n.get_msg("char_selection-starting_site").into_owned(),
Column::with_children(column_content) 30,
.align_items(Align::Center) &mut sliders.starting_site,
.width(Length::Fill) self.possible_starting_sites.len() as u32 - 1,
.spacing(5), *start_site_idx as u32,
) |x| Message::StartingSite(x as usize),
.padding(5) imgs,
.width(Length::Fill) );
.align_items(Align::Center) let map_sz = Vec2::new(500, 500);
.style(style::scrollable::Style { let map_img = Image::new(self.map_img)
track: None, .height(Length::Units(map_sz.x))
scroller: style::scrollable::Scroller::Color(UI_MAIN), .width(Length::Units(map_sz.y));
}), let site_name = Text::new(
) self.possible_starting_sites[*start_site_idx]
.width(Length::Units(320)) // TODO: see if we can get iced to work with settings below .name
//.max_width(360) .as_deref()
//.width(Length::Fill) .unwrap_or("Unknown")
.height(Length::Fill); )
.horizontal_alignment(HorizontalAlignment::Left)
.color(Color::from_rgb(131.0, 102.0, 0.0))
/* .stroke(Stroke {
color: Color::WHITE,
width: 1.0,
}) */;
//TODO: Add text-outline here whenever we updated iced to a version supporting
// this
let left_column = Column::with_children(vec![ let map = if let Some(info) = self.possible_starting_sites.get(*start_site_idx)
Container::new(left_column) {
.style(style::container::Style::color(Rgba::from_translucent( let pos_frac = info
0, .wpos
BANNER_ALPHA, .map2(self.world_sz * TerrainChunkSize::RECT_SIZE, |e, sz| {
))) e as f32 / sz as f32
.width(Length::Units(320)) });
.center_x() let point = Vec2::new(pos_frac.x, 1.0 - pos_frac.y)
.into(), .map2(map_sz, |e, sz| e * sz as f32 - 12.0);
Image::new(imgs.frame_bottom) let marker_img = Image::new(imgs.town_marker)
.height(Length::Units(40)) .height(Length::Units(27))
.width(Length::Units(320)) .width(Length::Units(16));
.color(Rgba::from_translucent(0, BANNER_ALPHA)) let marker_content: Column<Message, IcedRenderer> = Column::new()
.into(), .spacing(2)
]) .push(site_name)
.height(Length::Fill); .push(marker_img)
.align_items(Align::Center);
Overlay::new(
Container::new(marker_content)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y(),
map_img,
)
.over_position(iced::Point::new(point.x, point.y - 34.0))
.into()
} else {
map_img.into()
};
if self.possible_starting_sites.is_empty() {
vec![map]
} else {
let site_buttons = Row::with_children(vec![
neat_button(
prev_starting_site_button,
i18n.get_msg("char_selection-starting_site_prev")
.into_owned(),
FILL_FRAC_ONE,
button_style,
Some(Message::PrevStartingSite),
),
neat_button(
next_starting_site_button,
i18n.get_msg("char_selection-starting_site_next")
.into_owned(),
FILL_FRAC_ONE,
button_style,
Some(Message::NextStartingSite),
),
])
.max_height(60)
.padding(15)
.into();
// Todo: use this to change the site icon if we use different starting site
// types
/* let site_kind = Text::new(i18n
.get_msg_ctx("char_selection-starting_site_kind", &i18n::fluent_args! {
"kind" => match self.possible_starting_sites[*start_site_idx].kind {
SiteKind::Town => i18n.get_msg("hud-map-town").into_owned(),
SiteKind::Castle => i18n.get_msg("hud-map-castle").into_owned(),
SiteKind::Bridge => i18n.get_msg("hud-map-bridge").into_owned(),
_ => "Unknown".to_string(),
},
})
.into_owned())
.size(fonts.cyri.scale(SLIDER_TEXT_SIZE))
.into(); */
vec![site_slider, map, site_buttons]
}
} else {
// If we're editing an existing character, don't display the world column
Vec::new()
};
let column_left = |column_content, scroll| {
let column = Container::new(
Scrollable::new(scroll)
.push(
Column::with_children(column_content)
.align_items(Align::Center)
.width(Length::Fill)
.spacing(5)
.padding(5),
)
.padding(5)
.width(Length::Fill)
.align_items(Align::Center)
.style(style::scrollable::Style {
track: None,
scroller: style::scrollable::Scroller::Color(UI_MAIN),
}),
)
.width(Length::Units(320)) // TODO: see if we can get iced to work with settings below
// .max_width(360)
// .width(Length::Fill)
.height(Length::Fill);
Column::with_children(vec![
Container::new(column)
.style(style::container::Style::color(Rgba::from_translucent(
0,
BANNER_ALPHA,
)))
.width(Length::Units(320))
.center_x()
.into(),
Image::new(imgs.frame_bottom)
.height(Length::Units(40))
.width(Length::Units(320))
.color(Rgba::from_translucent(0, BANNER_ALPHA))
.into(),
])
.height(Length::Fill)
};
let column_right = |column_content, scroll| {
let column = Container::new(
Scrollable::new(scroll)
.push(
Column::with_children(column_content)
.align_items(Align::Center)
.width(Length::Fill)
.spacing(5)
.padding(5),
)
.padding(5)
.width(Length::Fill)
.align_items(Align::Center)
.style(style::scrollable::Style {
track: None,
scroller: style::scrollable::Scroller::Color(UI_MAIN),
}),
)
.width(Length::Units(520)) // TODO: see if we can get iced to work with settings below
// .max_width(360)
// .width(Length::Fill)
.height(Length::Fill);
if character_id.is_none() {
Column::with_children(vec![
Container::new(column)
.style(style::container::Style::color(Rgba::from_translucent(
0,
BANNER_ALPHA,
)))
.width(Length::Units(520))
.center_x()
.into(),
Image::new(imgs.frame_bottom)
.height(Length::Units(40))
.width(Length::Units(520))
.color(Rgba::from_translucent(0, BANNER_ALPHA))
.into(),
])
.height(Length::Fill)
} else {
Column::with_children(vec![Container::new(column).into()])
}
};
let mouse_area =
MouseDetector::new(&mut self.mouse_detector, Length::Fill, Length::Fill);
let top = Row::with_children(vec![ let top = Row::with_children(vec![
left_column.into(), column_left(left_column_content, left_scroll).into(),
MouseDetector::new(&mut self.mouse_detector, Length::Fill, Length::Fill).into(), Column::with_children(
if let Some(warning_container) = warning_container.take() {
vec![warning_container.into(), mouse_area.into()]
} else {
vec![mouse_area.into()]
},
)
.width(Length::Fill)
.height(Length::Fill)
.into(),
column_right(right_column_content, right_scroll)
.width(Length::Units(520))
.into(),
]) ])
.padding(10) .padding(10)
.width(Length::Fill) .width(Length::Fill)
@ -1373,46 +1638,20 @@ impl Controls {
}, },
}; };
// TODO: There is probably a better way to conditionally add in the warning box let children = if let Some(warning_container) = warning_container {
// here vec![top_text.into(), warning_container.into(), content]
if let Some(mismatched_version) = &self.server_mismatched_version {
let warning = Text::<IcedRenderer>::new(format!(
"{}\n{}: {} {}: {}",
i18n.get_msg("char_selection-version_mismatch"),
i18n.get_msg("main-login-server_version"),
mismatched_version,
i18n.get_msg("main-login-client_version"),
*common::util::GIT_HASH
))
.size(self.fonts.cyri.scale(18))
.color(iced::Color::from_rgb(1.0, 0.0, 0.0))
.width(Length::Fill)
.horizontal_alignment(HorizontalAlignment::Center);
let warning_container =
Container::new(Row::with_children(vec![warning.into()]).width(Length::Fill))
.style(style::container::Style::color(Rgba::new(0, 0, 0, 217)))
.padding(12)
.center_x()
.width(Length::Fill);
Container::new(
Column::with_children(vec![top_text.into(), warning_container.into(), content])
.spacing(3)
.width(Length::Fill)
.height(Length::Fill),
)
.padding(3)
.into()
} else { } else {
Container::new( vec![top_text.into(), content]
Column::with_children(vec![top_text.into(), content]) };
.spacing(3)
.width(Length::Fill) Container::new(
.height(Length::Fill), Column::with_children(children)
) .spacing(3)
.padding(3) .width(Length::Fill)
.into() .height(Length::Fill),
} )
.padding(3)
.into()
} }
fn update(&mut self, message: Message, events: &mut Vec<Event>, characters: &[CharacterItem]) { fn update(&mut self, message: Message, events: &mut Vec<Event>, characters: &[CharacterItem]) {
@ -1516,6 +1755,7 @@ impl Controls {
body, body,
mainhand, mainhand,
offhand, offhand,
start_site_idx,
.. ..
} = &self.mode } = &self.mode
{ {
@ -1524,6 +1764,10 @@ impl Controls {
mainhand: mainhand.map(String::from), mainhand: mainhand.map(String::from),
offhand: offhand.map(String::from), offhand: offhand.map(String::from),
body: comp::Body::Humanoid(*body), body: comp::Body::Humanoid(*body),
start_site: self
.possible_starting_sites
.get(*start_site_idx)
.map(|info| info.id),
}); });
self.mode = Mode::select(Some(InfoContent::CreatingCharacter)); self.mode = Mode::select(Some(InfoContent::CreatingCharacter));
} }
@ -1643,6 +1887,29 @@ impl Controls {
body.validate(); body.validate();
} }
}, },
Message::StartingSite(idx) => {
if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
*start_site_idx = idx;
}
},
Message::PrevStartingSite => {
if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
if !self.possible_starting_sites.is_empty() {
*start_site_idx = (*start_site_idx + self.possible_starting_sites.len()
- 1)
% self.possible_starting_sites.len();
}
}
},
Message::NextStartingSite => {
if let Mode::CreateOrEdit { start_site_idx, .. } = &mut self.mode {
if !self.possible_starting_sites.is_empty() {
*start_site_idx =
(*start_site_idx + self.possible_starting_sites.len() + 1)
% self.possible_starting_sites.len();
}
}
},
} }
} }
@ -1707,6 +1974,17 @@ impl CharSelectionUi {
selected_character, selected_character,
default_name, default_name,
client.server_info(), client.server_info(),
ui.add_graphic(Graphic::Image(
Arc::clone(client.world_data().topo_map_image()),
Some(default_water_color()),
)),
client.sites()
.values()
// TODO: Enforce this server-side and add some way to customise it?
.filter(|info| matches!(&info.site.kind, SiteKind::Town /*| SiteKind::Castle | SiteKind::Bridge*/))
.map(|info| info.site.clone())
.collect(),
client.world_data().chunk_size().as_(),
); );
Self { Self {
@ -1814,4 +2092,5 @@ struct Sliders {
eye_color: slider::State, eye_color: slider::State,
accessory: slider::State, accessory: slider::State,
beard: slider::State, beard: slider::State,
starting_site: slider::State,
} }

View File

@ -19,6 +19,7 @@ pub struct Overlay<'a, M, R: Renderer> {
vertical_alignment: Align, vertical_alignment: Align,
over: Element<'a, M, R>, over: Element<'a, M, R>,
under: Element<'a, M, R>, under: Element<'a, M, R>,
pos: Option<Point>,
// add style etc as needed // add style etc as needed
} }
@ -41,9 +42,16 @@ where
vertical_alignment: Align::Start, vertical_alignment: Align::Start,
over: over.into(), over: over.into(),
under: under.into(), under: under.into(),
pos: None,
} }
} }
#[must_use]
pub fn over_position(mut self, pos: Point) -> Self {
self.pos = Some(pos);
self
}
#[must_use] #[must_use]
pub fn padding<P: Into<Padding>>(mut self, pad: P) -> Self { pub fn padding<P: Into<Padding>>(mut self, pad: P) -> Self {
self.padding = pad.into(); self.padding = pad.into();
@ -130,10 +138,10 @@ where
.pad(self.padding), .pad(self.padding),
); );
over.move_to(Point::new( over.move_to(
self.padding.left.into(), self.pos
self.padding.top.into(), .unwrap_or_else(|| Point::new(self.padding.left.into(), self.padding.top.into())),
)); );
over.align(self.horizontal_alignment, self.vertical_alignment, size); over.align(self.horizontal_alignment, self.vertical_alignment, size);
layout::Node::with_children(size, vec![over, under]) layout::Node::with_children(size, vec![over, under])

View File

@ -219,6 +219,11 @@ impl World {
pub fn sample_blocks(&self) -> BlockGen { BlockGen::new(ColumnGen::new(&self.sim)) } pub fn sample_blocks(&self) -> BlockGen { BlockGen::new(ColumnGen::new(&self.sim)) }
/// Find a position that's accessible to a player at the given world
/// position by searching blocks vertically.
///
/// If `ascending` is `true`, we try to find the highest accessible position
/// instead of the lowest.
pub fn find_accessible_pos( pub fn find_accessible_pos(
&self, &self,
index: IndexRef, index: IndexRef,