diff --git a/.gitignore b/.gitignore index 6948eaa0bc..4422efb240 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ todo.txt # direnv /.envrc +*.bat diff --git a/CHANGELOG.md b/CHANGELOG.md index 9874892a9e..e77e82627b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added changelog - Added animated Map and Minimap position indicator - Added visuals to indicate strength compared to the player +- Added Scrolling Combat Text (SCT) & Settings for it +- Added a Death Screen and Hurt Screen +- Added randomly selected Loading Screen background images + ### Changed @@ -50,6 +54,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed ghosts when going back to character screen - Fixed not being able to unmount - Fixed non-humanoids being able to climb and glide +- Made shadows and lights use interpolated positions ### Removed diff --git a/Cargo.lock b/Cargo.lock index 038127bc5e..0344df16fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3544,6 +3544,7 @@ dependencies = [ "serde 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.102 (registry+https://github.com/rust-lang/crates.io-index)", "specs 0.15.1 (registry+https://github.com/rust-lang/crates.io-index)", + "specs-idvs 0.1.0 (git+https://gitlab.com/veloren/specs-idvs.git)", "treeculler 0.1.0 (git+https://gitlab.com/yusdacra/treeculler.git)", "vek 0.9.11 (registry+https://github.com/rust-lang/crates.io-index)", "veloren-client 0.4.0", diff --git a/assets/voxygen/background/bg_1.png b/assets/voxygen/background/bg_1.png new file mode 100644 index 0000000000..932d14be00 --- /dev/null +++ b/assets/voxygen/background/bg_1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abdedad035b9e4cd495af42e343068b783f2b09a8d7635cff895be2243cf1cc2 +size 538326 diff --git a/assets/voxygen/background/bg_load.png b/assets/voxygen/background/bg_2.png similarity index 100% rename from assets/voxygen/background/bg_load.png rename to assets/voxygen/background/bg_2.png diff --git a/assets/voxygen/background/bg_3.png b/assets/voxygen/background/bg_3.png new file mode 100644 index 0000000000..c3584377b3 --- /dev/null +++ b/assets/voxygen/background/bg_3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:258da639ea3e41ff10b42c27936229a18de7a73afb8358b67c1ffec88082ec82 +size 2406949 diff --git a/assets/voxygen/background/bg_4.png b/assets/voxygen/background/bg_4.png new file mode 100644 index 0000000000..ec2995123f --- /dev/null +++ b/assets/voxygen/background/bg_4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:94a252fcec90d115b26f64eaf92a539ecd5db52e9974a42758280dc6e25296dc +size 2179461 diff --git a/assets/voxygen/background/bg_5.png b/assets/voxygen/background/bg_5.png new file mode 100644 index 0000000000..40818d7056 --- /dev/null +++ b/assets/voxygen/background/bg_5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ccd718e84b59b156d97788f1bea39d38ff34e7cfc3aacf5380b28586e1403ef +size 1205008 diff --git a/assets/voxygen/background/bg_6.png b/assets/voxygen/background/bg_6.png new file mode 100644 index 0000000000..41250ee39d --- /dev/null +++ b/assets/voxygen/background/bg_6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4785086ec9941d7a159c47c5475d01d0a13b3b73ef1952f869a9c31fa1739d2 +size 793666 diff --git a/assets/voxygen/background/bg_7.png b/assets/voxygen/background/bg_7.png new file mode 100644 index 0000000000..40e86c134e --- /dev/null +++ b/assets/voxygen/background/bg_7.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe436e2811629b57c47af449949dedbd0ff9a3a63136e196636e072a3b8bc320 +size 1244674 diff --git a/assets/voxygen/background/bg_8.png b/assets/voxygen/background/bg_8.png new file mode 100644 index 0000000000..47c19d7db9 --- /dev/null +++ b/assets/voxygen/background/bg_8.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8096f8a7b94e49cfe2cfba3ad6f9bd152078bca834ea7ea7af9e9889bcf99ceb +size 710326 diff --git a/assets/voxygen/background/death.png b/assets/voxygen/background/death.png new file mode 100644 index 0000000000..3852c5424b --- /dev/null +++ b/assets/voxygen/background/death.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a24fc708b5c3fee34fdad50cc4be002f6df12f358522af413098ddb150b24ced +size 143395 diff --git a/assets/voxygen/background/hurt.png b/assets/voxygen/background/hurt.png new file mode 100644 index 0000000000..91e6fef724 --- /dev/null +++ b/assets/voxygen/background/hurt.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdab32e2b5443e930f7fe4d3e188b51c6dc26f0729ae8763696386024dc6bed1 +size 260584 diff --git a/assets/voxygen/background/map.png b/assets/voxygen/background/map.png index 7e284d0568..a9733ceb45 100644 --- a/assets/voxygen/background/map.png +++ b/assets/voxygen/background/map.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:664f74c2b92f6319aa30cd0775af6976c629a8d9f521febac0748bacb432c3f3 -size 747328 +oid sha256:398b577a57ad9670b3400a3772a0798ccc409d8dfcb073ae050fae8cb314fbd2 +size 906590 diff --git a/assets/voxygen/element/frames/enemybar.vox b/assets/voxygen/element/frames/enemybar-0.vox similarity index 100% rename from assets/voxygen/element/frames/enemybar.vox rename to assets/voxygen/element/frames/enemybar-0.vox diff --git a/assets/voxygen/element/frames/enemybar.png b/assets/voxygen/element/frames/enemybar.png new file mode 100644 index 0000000000..82515fb6c0 --- /dev/null +++ b/assets/voxygen/element/frames/enemybar.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bfa1034410f86cc56f8ca737054ff8a8bf71dd4d696fa3bd8195d866cb4405d3 +size 1607 diff --git a/assets/voxygen/element/icons/skull.vox b/assets/voxygen/element/icons/skull.vox index 6f12b02c2b..024f888038 100644 --- a/assets/voxygen/element/icons/skull.vox +++ b/assets/voxygen/element/icons/skull.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6eeb17f7682aa533fc805de9272f55f01d10c06d2c6afefed54ce23d46fe93ab -size 56456 +oid sha256:1708cf57ec4d132e3478f86aef7083d37cf85b890a2ddcf4278d8b5d4394f53d +size 57720 diff --git a/assets/voxygen/element/icons/skull_2.vox b/assets/voxygen/element/icons/skull_2.vox index a32adc29ff..140ab7bf2c 100644 --- a/assets/voxygen/element/icons/skull_2.vox +++ b/assets/voxygen/element/icons/skull_2.vox @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6717aca430c81b346951f22fbb03e2dca4765a3fd56b7e515f267138af9f6e18 -size 56512 +oid sha256:d9ddbabd913b6b5afaf2d9344b457cdd9068ee5a1f019141c3f13e364b463dda +size 57784 diff --git a/assets/voxygen/element/skillbar/enemy_bar_content.png b/assets/voxygen/element/skillbar/enemy_bar_content.png new file mode 100644 index 0000000000..d1ac7bf94c --- /dev/null +++ b/assets/voxygen/element/skillbar/enemy_bar_content.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7611fe460a24c1340bf5b72ed7e6f4dbdfc0b7617cc4f0e56449c3f6c3f03f13 +size 141 diff --git a/assets/voxygen/shaders/ui-vert.glsl b/assets/voxygen/shaders/ui-vert.glsl index 2411525756..bcdeca839f 100644 --- a/assets/voxygen/shaders/ui-vert.glsl +++ b/assets/voxygen/shaders/ui-vert.glsl @@ -25,6 +25,10 @@ void main() { if (w_pos.w == 1.0) { // In-game element gl_Position = proj_mat * (view_mat * w_pos + vec4(v_pos, 0.0, 0.0)); + } else if (w_pos.w == -1.0 ) { + // Fixed scale In-game element + vec4 projected_pos = proj_mat * view_mat * vec4(w_pos.xyz, 1.0); + gl_Position = vec4(projected_pos.xy / projected_pos.w + v_pos, 0.0, 1.0); } else { // Interface element gl_Position = vec4(v_pos, 0.0, 1.0); diff --git a/chat-cli/src/main.rs b/chat-cli/src/main.rs index 2f288c6500..f21da11295 100644 --- a/chat-cli/src/main.rs +++ b/chat-cli/src/main.rs @@ -65,7 +65,11 @@ fn main() { client.send_chat(msg) } - let events = match client.tick(comp::ControllerInputs::default(), clock.get_last_delta()) { + let events = match client.tick( + comp::ControllerInputs::default(), + clock.get_last_delta(), + |_| {}, + ) { Ok(events) => events, Err(err) => { error!("Error: {:?}", err); diff --git a/client/src/lib.rs b/client/src/lib.rs index 0ceed6b47b..d9691c9ed1 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -8,7 +8,7 @@ pub use crate::error::Error; pub use specs::{ join::Join, saveload::{Marker, MarkerAllocator}, - Builder, Entity as EcsEntity, ReadStorage, WorldExt, + Builder, DispatcherBuilder, Entity as EcsEntity, ReadStorage, WorldExt, }; use common::{ @@ -316,7 +316,12 @@ impl Client { } /// Execute a single client tick, handle input and update the game state by the given duration. - pub fn tick(&mut self, inputs: ControllerInputs, dt: Duration) -> Result, Error> { + pub fn tick( + &mut self, + inputs: ControllerInputs, + dt: Duration, + add_foreign_systems: impl Fn(&mut DispatcherBuilder), + ) -> Result, Error> { // This tick function is the centre of the Veloren universe. Most client-side things are // managed from here, and as such it's important that it stays organised. Please consult // the core developers before making significant changes to this code. Here is the @@ -374,7 +379,7 @@ impl Client { // 3) Update client local data // 4) Tick the client's LocalState - self.state.tick(dt, |_| {}); + self.state.tick(dt, add_foreign_systems); // 5) Terrain let pos = self diff --git a/common/src/sys/mod.rs b/common/src/sys/mod.rs index 6f2c402318..2bf9d2ac8c 100644 --- a/common/src/sys/mod.rs +++ b/common/src/sys/mod.rs @@ -12,15 +12,15 @@ mod stats; use specs::DispatcherBuilder; // System names -const AGENT_SYS: &str = "agent_sys"; -const CONTROLLER_SYS: &str = "controller_sys"; -const MOUNT_SYS: &str = "mount_sys"; -const PHYS_SYS: &str = "phys_sys"; -const MOVEMENT_SYS: &str = "movement_sys"; -const PROJECTILE_SYS: &str = "projectile_sys"; -const COMBAT_SYS: &str = "combat_sys"; -const STATS_SYS: &str = "stats_sys"; -const CLEANUP_SYS: &str = "cleanup_sys"; +pub const AGENT_SYS: &str = "agent_sys"; +pub const CONTROLLER_SYS: &str = "controller_sys"; +pub const MOUNT_SYS: &str = "mount_sys"; +pub const PHYS_SYS: &str = "phys_sys"; +pub const MOVEMENT_SYS: &str = "movement_sys"; +pub const PROJECTILE_SYS: &str = "projectile_sys"; +pub const COMBAT_SYS: &str = "combat_sys"; +pub const STATS_SYS: &str = "stats_sys"; +pub const CLEANUP_SYS: &str = "cleanup_sys"; pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) { dispatch_builder.add(agent::Sys, AGENT_SYS, &[]); diff --git a/server-cli/.gitignore b/server-cli/.gitignore index dc83325b30..8b13789179 100644 --- a/server-cli/.gitignore +++ b/server-cli/.gitignore @@ -1 +1 @@ -settings.ron + diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 9f0cc61b46..ae79ae6215 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -87,9 +87,9 @@ lazy_static! { /// Static list of chat commands available to the server. pub static ref CHAT_COMMANDS: Vec = vec![ ChatCommand::new( - "giveitem", + "give_item", "{d}", - "/giveitem \n\ + "/give_item \n\ Example: common/items/debug/boost", true, handle_give,), diff --git a/server/src/settings.rs b/server/src/settings.rs index 533c3891c4..444ba9580a 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -22,7 +22,7 @@ impl Default for ServerSettings { Self { gameserver_address: SocketAddr::from(([0; 4], 14004)), metrics_address: SocketAddr::from(([0; 4], 14005)), - world_seed: 1337, + world_seed: 5284, server_name: "Veloren Alpha".to_owned(), server_description: "This is the best Veloren server.".to_owned(), max_players: 100, diff --git a/server/src/sys/mod.rs b/server/src/sys/mod.rs index 72860c92c8..fff86de2be 100644 --- a/server/src/sys/mod.rs +++ b/server/src/sys/mod.rs @@ -24,8 +24,12 @@ const TERRAIN_SYS: &str = "server_terrain_sys"; pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { // TODO: makes some of these dependent on systems in common like the phys system - dispatch_builder.add(sentinel::Sys, SENTINEL_SYS, &[]); - dispatch_builder.add(subscription::Sys, SUBSCRIPTION_SYS, &[]); + dispatch_builder.add(sentinel::Sys, SENTINEL_SYS, &[common::sys::PHYS_SYS]); + dispatch_builder.add( + subscription::Sys, + SUBSCRIPTION_SYS, + &[common::sys::PHYS_SYS], + ); dispatch_builder.add( entity_sync::Sys, ENTITY_SYNC_SYS, diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 322575db69..b736ad5f33 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -28,6 +28,7 @@ euc = "0.3.0" # ECS specs = "0.15.1" +specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" } # Mathematics vek = { version = "0.9.9", features = ["serde"] } diff --git a/voxygen/src/ecs/comp.rs b/voxygen/src/ecs/comp.rs new file mode 100644 index 0000000000..96efe460d0 --- /dev/null +++ b/voxygen/src/ecs/comp.rs @@ -0,0 +1,34 @@ +use specs::Component; +use specs_idvs::IDVStorage; +use vek::*; + +// Floats over entity that has had a health change, rising up over time until it vanishes +#[derive(Copy, Clone, Debug)] +pub struct HpFloater { + pub timer: f32, + // Numbers of times significant damage has been dealt + pub hp_change: i32, + // Used for randomly offseting + pub rand: f32, +} +#[derive(Clone, Debug, Default)] +pub struct HpFloaterList { + // Order oldest to newest + pub floaters: Vec, + // Keep from spawning more floaters from same hp change + // Note: this can't detect a change if equivalent healing and damage take place simultaneously + pub last_hp: u32, +} +impl Component for HpFloaterList { + type Storage = IDVStorage; +} + +// Used for smooth interpolation of visual elements that are tied to entity position +#[derive(Copy, Clone, Debug)] +pub struct Interpolated { + pub pos: Vec3, + pub ori: Vec3, +} +impl Component for Interpolated { + type Storage = IDVStorage; +} diff --git a/voxygen/src/ecs/mod.rs b/voxygen/src/ecs/mod.rs new file mode 100644 index 0000000000..779bcbc2b6 --- /dev/null +++ b/voxygen/src/ecs/mod.rs @@ -0,0 +1,28 @@ +pub mod comp; +pub mod sys; + +use specs::{Entity, World, WorldExt}; + +#[derive(Copy, Clone, Debug)] +pub struct MyEntity(pub Entity); + +#[derive(Copy, Clone, Debug)] +pub struct ExpFloater { + pub exp_change: i32, // Maybe you can loose exp :p + pub timer: f32, + // Used to randomly offset position + pub rand: (f32, f32), +} +#[derive(Clone, Debug, Default)] +pub struct MyExpFloaterList { + pub floaters: Vec, + pub last_exp: u32, + pub last_level: u32, + pub last_exp_max: u32, +} + +pub fn init(world: &mut World) { + world.register::(); + world.register::(); + world.insert(MyExpFloaterList::default()); +} diff --git a/voxygen/src/ecs/sys.rs b/voxygen/src/ecs/sys.rs new file mode 100644 index 0000000000..d26f70977d --- /dev/null +++ b/voxygen/src/ecs/sys.rs @@ -0,0 +1,17 @@ +pub mod floater; +mod interpolation; + +use specs::DispatcherBuilder; + +// System names +const FLOATER_SYS: &str = "floater_voxygen_sys"; +const INTERPOLATION_SYS: &str = "interpolation_voxygen_sys"; + +pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) { + dispatch_builder.add( + interpolation::Sys, + INTERPOLATION_SYS, + &[common::sys::PHYS_SYS], + ); + dispatch_builder.add(floater::Sys, FLOATER_SYS, &[INTERPOLATION_SYS]); +} diff --git a/voxygen/src/ecs/sys/floater.rs b/voxygen/src/ecs/sys/floater.rs new file mode 100644 index 0000000000..d8ab4a03b6 --- /dev/null +++ b/voxygen/src/ecs/sys/floater.rs @@ -0,0 +1,181 @@ +use crate::ecs::{ + comp::{HpFloater, HpFloaterList}, + ExpFloater, MyEntity, MyExpFloaterList, +}; +use common::{ + comp::{HealthSource, Pos, Stats}, + state::DeltaTime, + sync::Uid, +}; +use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage}; + +// How long floaters last (in seconds) +pub const HP_SHOWTIME: f32 = 3.0; +pub const MY_HP_SHOWTIME: f32 = 2.5; +pub const MY_EXP_SHOWTIME: f32 = 4.0; + +pub struct Sys; +impl<'a> System<'a> for Sys { + type SystemData = ( + Entities<'a>, + ReadExpect<'a, MyEntity>, + Read<'a, DeltaTime>, + Write<'a, MyExpFloaterList>, + ReadStorage<'a, Uid>, + ReadStorage<'a, Pos>, + ReadStorage<'a, Stats>, + WriteStorage<'a, HpFloaterList>, + ); + + fn run( + &mut self, + (entities, my_entity, dt, mut my_exp_floater_list, uids, pos, stats, mut hp_floater_lists): Self::SystemData, + ) { + // Add hp floater lists to all entities with stats and a postion + // Note: neccessary in order to know last_hp + for (entity, last_hp) in (&entities, &stats, &pos, !&hp_floater_lists) + .join() + .map(|(e, s, _, _)| (e, s.health.current())) + .collect::>() + { + let _ = hp_floater_lists.insert( + entity, + HpFloaterList { + floaters: Vec::new(), + last_hp, + }, + ); + } + + // Add hp floaters to all entities that have been damaged + let my_uid = uids.get(my_entity.0); + for (entity, health, hp_floater_list) in (&entities, &stats, &mut hp_floater_lists) + .join() + .map(|(e, s, fl)| (e, s.health, fl)) + { + // Check if health has changed (won't work if damaged and then healed with + // equivalently in the same frame) + if hp_floater_list.last_hp != health.current() { + hp_floater_list.last_hp = health.current(); + // TODO: What if multiple health changes occured since last check here + // Also, If we make stats store a vec of the last_changes (from say the last frame), + // what if the client recieves the stats component from two different server ticks at + // once, then one will be lost (tbf this is probably a rare occurance and the results + // would just be a transient glitch in the display of these damage numbers) (maybe + // health changes could be sent to the client as a list of events) + if match health.last_change.1.cause { + HealthSource::Attack { by } => { + my_entity.0 == entity || my_uid.map_or(false, |&uid| by == uid) + } + HealthSource::Suicide => my_entity.0 == entity, + HealthSource::World => my_entity.0 == entity, + HealthSource::Revive => false, + HealthSource::Command => true, + HealthSource::LevelUp => my_entity.0 == entity, + HealthSource::Item => true, + HealthSource::Unknown => false, + } { + hp_floater_list.floaters.push(HpFloater { + timer: 0.0, + hp_change: health.last_change.1.amount, + rand: rand::random(), + }); + } + } + } + + // Remove floater lists on entities without stats or without posistion + for entity in (&entities, !&stats, &hp_floater_lists) + .join() + .map(|(e, _, _)| e) + .collect::>() + { + hp_floater_lists.remove(entity); + } + for entity in (&entities, !&pos, &hp_floater_lists) + .join() + .map(|(e, _, _)| e) + .collect::>() + { + hp_floater_lists.remove(entity); + } + + // Maintain existing floaters + for ( + entity, + HpFloaterList { + ref mut floaters, + ref last_hp, + }, + ) in (&entities, &mut hp_floater_lists).join() + { + for mut floater in floaters.iter_mut() { + // Increment timer + floater.timer += dt.0; + } + // Clear floaters if newest floater is past show time or health runs out + if floaters.last().map_or(false, |f| { + f.timer + > if entity != my_entity.0 { + HP_SHOWTIME + } else { + MY_HP_SHOWTIME + } + || *last_hp == 0 + }) { + floaters.clear(); + } + } + + // Update MyExpFloaterList + if let Some(stats) = stats.get(my_entity.0) { + let mut fl = my_exp_floater_list; + // Add a floater if exp changed + // TODO: can't handle if you level up more than once (maybe store total exp in stats) + let exp_change = if stats.level.level() != fl.last_level { + if stats.level.level() > fl.last_level { + stats.exp.current() as i32 + fl.last_exp_max as i32 - fl.last_exp as i32 + } else { + // Level down + stats.exp.current() as i32 - stats.exp.maximum() as i32 - fl.last_exp as i32 + } + } else { + stats.exp.current() as i32 - fl.last_exp as i32 + }; + + if exp_change != 0 { + fl.floaters.push(ExpFloater { + timer: 0.0, + exp_change, + rand: (rand::random(), rand::random()), + }); + } + + // Increment timers + for mut floater in &mut fl.floaters { + floater.timer += dt.0; + } + + // Clear if the newest is past show time + if fl + .floaters + .last() + .map_or(false, |f| f.timer > MY_EXP_SHOWTIME) + { + fl.floaters.clear(); + } + + // Update stored values + fl.last_exp = stats.exp.current(); + fl.last_exp_max = stats.exp.maximum(); + fl.last_level = stats.level.level(); + } else { + // Clear if stats component doesn't exist + my_exp_floater_list.floaters.clear(); + // Clear stored values + my_exp_floater_list.last_exp = 0; + my_exp_floater_list.last_exp_max = 0; + my_exp_floater_list.last_level = 0; + } + } +} diff --git a/voxygen/src/ecs/sys/interpolation.rs b/voxygen/src/ecs/sys/interpolation.rs new file mode 100644 index 0000000000..80ddfc6261 --- /dev/null +++ b/voxygen/src/ecs/sys/interpolation.rs @@ -0,0 +1,80 @@ +use crate::ecs::comp::Interpolated; +use common::{ + comp::{Ori, Pos, Vel}, + state::DeltaTime, +}; +use log::warn; +use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage}; +use vek::*; + +/// This system will allow NPCs to modify their controller +pub struct Sys; +impl<'a> System<'a> for Sys { + type SystemData = ( + Entities<'a>, + Read<'a, DeltaTime>, + ReadStorage<'a, Pos>, + ReadStorage<'a, Ori>, + ReadStorage<'a, Vel>, + WriteStorage<'a, Interpolated>, + ); + + fn run( + &mut self, + (entities, dt, positions, orientations, velocities, mut interpolated): Self::SystemData, + ) { + // Update interpolated positions and orientations + for (pos, ori, i, vel) in (&positions, &orientations, &mut interpolated, &velocities).join() + { + // Update interpolation values + if i.pos.distance_squared(pos.0) < 64.0 * 64.0 { + i.pos = Lerp::lerp(i.pos, pos.0 + vel.0 * 0.03, 10.0 * dt.0); + let ori_interp = Slerp::slerp(i.ori, ori.0, 5.0 * dt.0); + // Check for NaNs + // TODO: why are we getting NaNs here! Zero-length ori vectors? + i.ori = if !ori_interp.map(|e| e.is_nan()).reduce_or() { + ori_interp + } else { + ori.0 + }; + } else { + i.pos = pos.0; + i.ori = ori.0; + } + } + // Insert interpolation components for entities which don't have them + for (entity, pos, ori) in (&entities, &positions, &orientations, !&interpolated) + .join() + .map(|(e, p, o, _)| (e, p.0, o.0)) + .collect::>() + { + interpolated + .insert(entity, Interpolated { pos, ori }) + .err() + .map(|err| warn!("Error inserting Interpolated component: {}", err)); + } + // Remove Interpolated component from entities which don't have a position or an + // orientation or a velocity + for entity in (&entities, !&positions, &interpolated) + .join() + .map(|(e, _, _)| e) + .collect::>() + { + interpolated.remove(entity); + } + for entity in (&entities, !&orientations, &interpolated) + .join() + .map(|(e, _, _)| e) + .collect::>() + { + interpolated.remove(entity); + } + for entity in (&entities, !&velocities, &interpolated) + .join() + .map(|(e, _, _)| e) + .collect::>() + { + interpolated.remove(entity); + } + } +} diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index ac1593693f..ff0f81d731 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -231,8 +231,7 @@ image_ids! { key: "voxygen.voxel.object.key", key_gold: "voxygen.voxel.object.key_gold", - // Enemy Healthbar - enemy_health: "voxygen.element.frames.enemybar", + @@ -247,6 +246,13 @@ image_ids! { charwindow_gradient:"voxygen.element.misc_bg.charwindow", map_placeholder: "voxygen.background.map", + death_bg: "voxygen.background.death", + hurt_bg: "voxygen.background.hurt", + + // Enemy Healthbar + enemy_health: "voxygen.element.frames.enemybar", + // Enemy Bar Content: + enemy_bar: "voxygen.element.skillbar.enemy_bar_content", // Spell Book Window spellbook_icon: "voxygen.element.icons.spellbook", // Bag diff --git a/voxygen/src/hud/map.rs b/voxygen/src/hud/map.rs index 8d7eccc10c..2890b5ae22 100644 --- a/voxygen/src/hud/map.rs +++ b/voxygen/src/hud/map.rs @@ -9,7 +9,6 @@ use conrod_core::{ }; use specs::WorldExt; use vek::*; - widget_ids! { struct Ids { map_frame, @@ -37,6 +36,7 @@ pub struct Map<'a> { #[conrod(common_builder)] common: widget::CommonBuilder, pulse: f32, + velocity: f32, } impl<'a> Map<'a> { pub fn new( @@ -46,6 +46,7 @@ impl<'a> Map<'a> { world_map: Id, fonts: &'a Fonts, pulse: f32, + velocity: f32, ) -> Self { Self { _show: show, @@ -55,6 +56,7 @@ impl<'a> Map<'a> { fonts: fonts, common: widget::CommonBuilder::default(), pulse, + velocity, } } } @@ -84,6 +86,15 @@ impl<'a> Widget for Map<'a> { fn update(self, args: widget::UpdateArgs) -> Self::Event { let widget::UpdateArgs { state, ui, .. } = args; + // Set map transparency to 0.5 when player is moving + /*let vel = match self.velocity { + Some(velocity) => velocity.0.magnitude(), + None => 0.0, + };*/ + let mut fade = 1.0; + if self.velocity > 7.0 { + fade = 0.7 + }; // BG Rectangle::fill_with([824.0, 976.0], color::TRANSPARENT) @@ -96,24 +107,29 @@ impl<'a> Widget for Map<'a> { Image::new(self.imgs.map_frame_l) .top_left_with_margins_on(state.ids.map_bg, 0.0, 0.0) .w_h(412.0, 488.0) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade))) .set(state.ids.map_frame_l, ui); Image::new(self.imgs.map_frame_r) .right_from(state.ids.map_frame_l, 0.0) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade))) .w_h(412.0, 488.0) .set(state.ids.map_frame_r, ui); Image::new(self.imgs.map_frame_br) .down_from(state.ids.map_frame_r, 0.0) .w_h(412.0, 488.0) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade))) .set(state.ids.map_frame_br, ui); Image::new(self.imgs.map_frame_bl) .down_from(state.ids.map_frame_l, 0.0) .w_h(412.0, 488.0) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade))) .set(state.ids.map_frame_bl, ui); // Icon Image::new(self.imgs.map_icon) .w_h(224.0 / 3.0, 224.0 / 3.0) .top_left_with_margins_on(state.ids.map_frame, -10.0, -10.0) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade))) .set(state.ids.map_icon, ui); // X-Button @@ -121,6 +137,7 @@ impl<'a> Widget for Map<'a> { .w_h(28.0, 28.0) .hover_image(self.imgs.close_button_hover) .press_image(self.imgs.close_button_press) + .color(Color::Rgba(1.0, 1.0, 1.0, fade - 0.5)) .top_right_with_margins_on(state.ids.map_frame_r, 0.0, 0.0) .set(state.ids.map_close, ui) .was_clicked() @@ -144,9 +161,11 @@ impl<'a> Widget for Map<'a> { .color(TEXT_COLOR) .set(state.ids.location_name, ui), } + // Map Image Image::new(/*self.world_map*/ self.imgs.map_placeholder) .middle_of(state.ids.map_bg) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade))) .w_h(700.0, 700.0) .parent(state.ids.map_bg) .set(state.ids.grid, ui); @@ -183,7 +202,7 @@ impl<'a> Widget for Map<'a> { 34.0 * indic_scale }, ) - .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0))) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, fade + 0.2))) .floating(true) .parent(ui.window) .set(state.ids.indicator, ui); diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 7af4cad854..88124a4439 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -13,8 +13,7 @@ mod skillbar; mod social; mod spell; -use crate::hud::img_ids::ImgsRot; -//use rand::Rng; +use crate::{ecs::comp::HpFloaterList, hud::img_ids::ImgsRot}; pub use settings_window::ScaleChange; use std::time::Duration; @@ -36,6 +35,7 @@ use social::{Social, SocialTab}; use spell::Spell; use crate::{ + ecs::comp as vcomp, render::{AaMode, Consts, Globals, Renderer}, scene::camera::Camera, //settings::ControlSettings, @@ -60,6 +60,7 @@ use crate::{discord, discord::DiscordUpdate}; const XP_COLOR: Color = Color::Rgba(0.59, 0.41, 0.67, 1.0); const TEXT_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0); +//const TEXT_COLOR_GREY: Color = Color::Rgba(1.0, 1.0, 1.0, 0.5); const MENU_BG: Color = Color::Rgba(0.0, 0.0, 0.0, 0.4); //const TEXT_COLOR_2: Color = Color::Rgba(0.0, 0.0, 0.0, 1.0); const TEXT_COLOR_3: Color = Color::Rgba(1.0, 1.0, 1.0, 0.1); @@ -88,6 +89,7 @@ widget_ids! { // Character Names name_tags[], + name_tags_bgs[], levels[], levels_skull[], // Health Bars @@ -96,6 +98,18 @@ widget_ids! { health_bar_fronts[], health_bar_backs[], + // SCT + player_scts[], + player_sct_bgs[], + sct_exp_bgs[], + sct_exps[], + sct_lvl_bg, + sct_lvl, + hurt_bg, + death_bg, + sct_bgs[], + scts[], + // Intro Text intro_bg, intro_text, @@ -128,6 +142,7 @@ widget_ids! { // Help help, help_info, + debug_info, // Window Frames window_frame_0, @@ -200,6 +215,10 @@ pub enum Event { Intro(Intro), ToggleBarNumbers(BarNumbers), ToggleShortcutNumbers(ShortcutNumbers), + Sct(bool), + SctPlayerBatch(bool), + SctDamageBatch(bool), + ToggleDebug(bool), UiScale(ScaleChange), CharacterSelection, UseInventorySlot(usize), @@ -265,7 +284,6 @@ pub struct Show { ingame: bool, settings_tab: SettingsTab, social_tab: SocialTab, - want_grab: bool, } impl Show { @@ -424,6 +442,7 @@ pub struct Hud { force_chat_cursor: Option, pulse: f32, zoom: f32, + velocity: f32, } impl Hud { @@ -461,7 +480,7 @@ impl Hud { show: Show { help: false, intro: true, - debug: true, + debug: false, bag: false, esc_menu: false, open_windows: Windows::None, @@ -485,6 +504,7 @@ impl Hud { force_chat_cursor: None, pulse: 0.0, zoom: 1.0, + velocity: 0.0, } } @@ -499,43 +519,23 @@ impl Hud { let (ref mut ui_widgets, ref mut tooltip_manager) = self.ui.set_widgets(); // pulse time for pulsating elements self.pulse = self.pulse + dt.as_secs_f32(); + self.velocity = match debug_info.velocity { + Some(velocity) => velocity.0.magnitude(), + None => 0.0, + }; let version = format!( "{}-{}", env!("CARGO_PKG_VERSION"), common::util::GIT_VERSION.to_string() ); - if self.show.ingame { - // Crosshair - if !self.show.help { - Image::new( - // TODO: Do we want to match on this every frame? - match global_state.settings.gameplay.crosshair_type { - CrosshairType::Round => self.imgs.crosshair_outer_round, - CrosshairType::RoundEdges => self.imgs.crosshair_outer_round_edges, - CrosshairType::Edges => self.imgs.crosshair_outer_edges, - }, - ) - .w_h(21.0 * 1.5, 21.0 * 1.5) - .middle_of(ui_widgets.window) - .color(Some(Color::Rgba( - 1.0, - 1.0, - 1.0, - global_state.settings.gameplay.crosshair_transp, - ))) - .set(self.ids.crosshair_outer, ui_widgets); - Image::new(self.imgs.crosshair_inner) - .w_h(21.0 * 2.0, 21.0 * 2.0) - .middle_of(self.ids.crosshair_outer) - .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.6))) - .set(self.ids.crosshair_inner, ui_widgets); - } - // Nametags and healthbars + if self.show.ingame { let ecs = client.state().ecs(); let pos = ecs.read_storage::(); let stats = ecs.read_storage::(); + let hp_floater_lists = ecs.read_storage::(); + let interpolated = ecs.read_storage::(); let players = ecs.read_storage::(); let scales = ecs.read_storage::(); let entities = ecs.entities(); @@ -545,6 +545,62 @@ impl Hud { .get(client.entity()) .map_or(0, |stats| stats.level.level()); + if let Some(stats) = stats.get(me) { + // Hurt Frame + let hp_percentage = + stats.health.current() as f32 / stats.health.maximum() as f32 * 100.0; + if hp_percentage < 10.0 && !stats.is_dead { + let hurt_fade = + (self.pulse * (10.0 - hp_percentage as f32) * 0.1/*speed factor*/).sin() + * 0.5 + + 0.6; //Animation timer + Image::new(self.imgs.hurt_bg) + .wh_of(ui_widgets.window) + .middle_of(ui_widgets.window) + .graphics_for(ui_widgets.window) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, hurt_fade))) + .set(self.ids.hurt_bg, ui_widgets); + } + // Death Frame + if stats.is_dead { + Image::new(self.imgs.death_bg) + .wh_of(ui_widgets.window) + .middle_of(ui_widgets.window) + .graphics_for(ui_widgets.window) + .color(Some(Color::Rgba(0.0, 0.0, 0.0, 1.0))) + .set(self.ids.death_bg, ui_widgets); + } + // Crosshair + if !self.show.help && !stats.is_dead { + Image::new( + // TODO: Do we want to match on this every frame? + match global_state.settings.gameplay.crosshair_type { + CrosshairType::Round => self.imgs.crosshair_outer_round, + CrosshairType::RoundEdges => self.imgs.crosshair_outer_round_edges, + CrosshairType::Edges => self.imgs.crosshair_outer_edges, + }, + ) + .w_h(21.0 * 1.5, 21.0 * 1.5) + .middle_of(ui_widgets.window) + .color(Some(Color::Rgba( + 1.0, + 1.0, + 1.0, + global_state.settings.gameplay.crosshair_transp, + ))) + .set(self.ids.crosshair_outer, ui_widgets); + Image::new(self.imgs.crosshair_inner) + .w_h(21.0 * 2.0, 21.0 * 2.0) + .middle_of(self.ids.crosshair_outer) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.6))) + .set(self.ids.crosshair_inner, ui_widgets); + } + } + + // Nametags and healthbars + + // Max amount the sct font size increases when "flashing" + const FLASH_MAX: f32 = 25.0; // Get player position. let player_pos = client .state() @@ -553,21 +609,31 @@ impl Hud { .get(client.entity()) .map_or(Vec3::zero(), |pos| pos.0); let mut name_id_walker = self.ids.name_tags.walk(); + let mut name_id_bg_walker = self.ids.name_tags_bgs.walk(); let mut level_id_walker = self.ids.levels.walk(); let mut level_skull_id_walker = self.ids.levels_skull.walk(); let mut health_id_walker = self.ids.health_bars.walk(); let mut mana_id_walker = self.ids.mana_bars.walk(); let mut health_back_id_walker = self.ids.health_bar_backs.walk(); let mut health_front_id_walker = self.ids.health_bar_fronts.walk(); + let mut sct_bg_id_walker = self.ids.sct_bgs.walk(); + let mut sct_id_walker = self.ids.scts.walk(); // Render Health Bars - for (_entity, pos, stats, scale) in (&entities, &pos, &stats, scales.maybe()) + for (pos, stats, scale, hp_floater_list) in ( + &entities, + &pos, + interpolated.maybe(), + &stats, + scales.maybe(), + hp_floater_lists.maybe(), // Potentially move this to its own loop + ) .join() - .filter(|(entity, _, stats, _)| { + .filter(|(entity, _, _, stats, _, _)| { *entity != me && !stats.is_dead //&& stats.health.current() != stats.health.maximum() }) // Don't process health bars outside the vd (visibility further limited by ui backend) - .filter(|(_, pos, _, _)| { + .filter(|(_, pos, _, _, _, _)| { Vec2::from(pos.0 - player_pos) .map2(TerrainChunk::RECT_SIZE, |d: f32, sz| { d.abs() as f32 / sz as f32 @@ -575,9 +641,15 @@ impl Hud { .magnitude() < view_distance as f32 }) + .map(|(_, pos, interpolated, stats, scale, f)| { + ( + interpolated.map_or(pos.0, |i| i.pos), + stats, + scale.map_or(1.0, |s| s.0), + f, + ) + }) { - let scale = scale.map(|s| s.0).unwrap_or(1.0); - let back_id = health_back_id_walker.next( &mut self.ids.health_bar_backs, &mut ui_widgets.widget_id_generator(), @@ -596,20 +668,22 @@ impl Hud { ); let hp_percentage = stats.health.current() as f64 / stats.health.maximum() as f64 * 100.0; + let energy_percentage = + stats.energy.current() as f64 / stats.energy.maximum() as f64 * 100.0; let hp_ani = (self.pulse * 4.0/*speed factor*/).cos() * 0.5 + 1.0; //Animation timer let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani); // Background Rectangle::fill_with([82.0, 8.0], Color::Rgba(0.3, 0.3, 0.3, 0.5)) .x_y(0.0, -25.0) - .position_ingame(pos.0 + Vec3::new(0.0, 0.0, 1.5 * scale + 1.5)) + .position_ingame(pos + Vec3::new(0.0, 0.0, 1.5 * scale + 1.5)) .resolution(100.0) .set(back_id, ui_widgets); // % HP Filling - Image::new(self.imgs.bar_content) + Image::new(self.imgs.enemy_bar) .w_h(72.9 * (hp_percentage / 100.0), 5.9) - .x_y(4.5, -24.0) + .x_y(4.5 + (hp_percentage / 100.0 * 36.45) - 36.45, -24.0) .color(Some(if hp_percentage <= 25.0 { crit_hp_color } else if hp_percentage <= 50.0 { @@ -617,7 +691,7 @@ impl Hud { } else { HP_COLOR })) - .position_ingame(pos.0 + Vec3::new(0.0, 0.0, 1.5 * scale + 1.5)) + .position_ingame(pos + Vec3::new(0.0, 0.0, 1.5 * scale + 1.5)) .resolution(100.0) .set(health_bar_id, ui_widgets); // % Mana Filling @@ -628,8 +702,8 @@ impl Hud { ], MANA_COLOR, ) - .x_y(4.5, -28.0) - .position_ingame(pos.0 + Vec3::new(0.0, 0.0, 1.5 * scale + 1.5)) + .x_y(4.5 + (energy_percentage / 100.0 * 36.5) - 36.45, -28.0) + .position_ingame(pos + Vec3::new(0.0, 0.0, 1.5 * scale + 1.5)) .resolution(100.0) .set(mana_bar_id, ui_widgets); @@ -638,42 +712,426 @@ impl Hud { .w_h(84.0, 10.0) .x_y(0.0, -25.0) .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.99))) - .position_ingame(pos.0 + Vec3::new(0.0, 0.0, 1.5 * scale + 1.5)) + .position_ingame(pos + Vec3::new(0.0, 0.0, 1.5 * scale + 1.5)) .resolution(100.0) .set(front_id, ui_widgets); + + // Enemy SCT + if let Some(HpFloaterList { floaters, .. }) = hp_floater_list + .filter(|fl| !fl.floaters.is_empty() && global_state.settings.gameplay.sct) + { + // Colors + const WHITE: Rgb = Rgb::new(1.0, 0.9, 0.8); + const LIGHT_OR: Rgb = Rgb::new(1.0, 0.925, 0.749); + const LIGHT_MED_OR: Rgb = Rgb::new(1.0, 0.85, 0.498); + const MED_OR: Rgb = Rgb::new(1.0, 0.776, 0.247); + const DARK_ORANGE: Rgb = Rgb::new(1.0, 0.7, 0.0); + const RED_ORANGE: Rgb = Rgb::new(1.0, 0.349, 0.0); + const DAMAGE_COLORS: [Rgb; 6] = [ + WHITE, + LIGHT_OR, + LIGHT_MED_OR, + MED_OR, + DARK_ORANGE, + RED_ORANGE, + ]; + // Largest value that select the first color is 40, then it shifts colors + // every 5 + let font_col = |font_size: u32| { + DAMAGE_COLORS[(font_size.saturating_sub(36) / 5).min(5) as usize] + }; + + if global_state.settings.gameplay.sct_damage_batch { + let number_speed = 50.0; // Damage number speed + let sct_bg_id = sct_bg_id_walker + .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator()); + let sct_id = sct_id_walker + .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator()); + // Calculate total change + // Ignores healing + let hp_damage = floaters.iter().fold(0, |acc, f| { + if f.hp_change < 0 { + acc + f.hp_change + } else { + acc + } + }); + let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32; + let timer = floaters + .last() + .expect("There must be at least one floater") + .timer; + // Increase font size based on fraction of maximum health + // "flashes" by having a larger size in the first 100ms + let font_size = 30 + + (max_hp_frac * 30.0) as u32 + + if timer < 0.1 { + (FLASH_MAX * (1.0 - timer / 0.1)) as u32 + } else { + 0 + }; + let font_col = font_col(font_size); + // Timer sets the widget offset + let y = (timer as f64 / crate::ecs::sys::floater::HP_SHOWTIME as f64 + * number_speed) + + 30.0; + // Timer sets text transparency + let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - timer) * 0.25) + 0.2; + + Text::new(&format!("{}", (hp_damage).abs())) + .font_size(font_size) + .color(Color::Rgba(0.0, 0.0, 0.0, fade)) + .x_y(0.0, y - 3.0) + .position_ingame(pos + Vec3::new(0.0, 0.0, 1.5 * scale as f32 + 1.8)) + .fixed_scale() + .resolution(100.0) + .set(sct_bg_id, ui_widgets); + Text::new(&format!("{}", hp_damage.abs())) + .font_size(font_size) + .x_y(0.0, y) + .color(if hp_damage < 0 { + Color::Rgba(font_col.r, font_col.g, font_col.b, fade) + } else { + Color::Rgba(0.1, 1.0, 0.1, fade) + }) + .position_ingame(pos + Vec3::new(0.0, 0.0, 1.5 * scale as f32 + 1.8)) + .fixed_scale() + .resolution(100.0) + .set(sct_id, ui_widgets); + } else { + for floater in floaters { + let number_speed = 250.0; // Single Numbers Speed + let sct_bg_id = sct_bg_id_walker + .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator()); + let sct_id = sct_id_walker + .next(&mut self.ids.scts, &mut ui_widgets.widget_id_generator()); + // Calculate total change + let max_hp_frac = + floater.hp_change.abs() as f32 / stats.health.maximum() as f32; + // Increase font size based on fraction of maximum health + // "flashes" by having a larger size in the first 100ms + let font_size = 30 + + (max_hp_frac * 30.0) as u32 + + if floater.timer < 0.1 { + (FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32 + } else { + 0 + }; + let font_col = font_col(font_size); + // Timer sets the widget offset + let y = (floater.timer as f64 + / crate::ecs::sys::floater::HP_SHOWTIME as f64 + * number_speed) + + 30.0; + // Timer sets text transparency + let fade = ((crate::ecs::sys::floater::HP_SHOWTIME - floater.timer) + * 0.25) + + 0.2; + + Text::new(&format!("{}", (floater.hp_change).abs())) + .font_size(font_size) + .color(if floater.hp_change < 0 { + Color::Rgba(0.0, 0.0, 0.0, fade) + } else { + Color::Rgba(0.1, 1.0, 0.1, 0.0) + }) + .x_y(0.0, y - 3.0) + .position_ingame( + pos + Vec3::new(0.0, 0.0, 1.5 * scale as f32 + 1.8), + ) + .fixed_scale() + .resolution(100.0) + .set(sct_bg_id, ui_widgets); + Text::new(&format!("{}", (floater.hp_change).abs())) + .font_size(font_size) + .x_y(0.0, y) + .color(if floater.hp_change < 0 { + Color::Rgba(font_col.r, font_col.g, font_col.b, fade) + } else { + Color::Rgba(0.1, 1.0, 0.1, 0.0) + }) + .position_ingame( + pos + Vec3::new(0.0, 0.0, 1.5 * scale as f32 + 1.8), + ) + .fixed_scale() + .resolution(100.0) + .set(sct_id, ui_widgets); + } + } + } + } + + if global_state.settings.gameplay.sct { + // Render Player SCT numbers + let mut player_sct_bg_id_walker = self.ids.player_sct_bgs.walk(); + let mut player_sct_id_walker = self.ids.player_scts.walk(); + if let (Some(HpFloaterList { floaters, .. }), Some(stats)) = ( + hp_floater_lists + .get(me) + .filter(|fl| !fl.floaters.is_empty()), + stats.get(me), + ) { + if global_state.settings.gameplay.sct_player_batch { + let number_speed = 100.0; // Player Batched Numbers Speed + let player_sct_bg_id = player_sct_bg_id_walker.next( + &mut self.ids.player_sct_bgs, + &mut ui_widgets.widget_id_generator(), + ); + let player_sct_id = player_sct_id_walker.next( + &mut self.ids.player_scts, + &mut ui_widgets.widget_id_generator(), + ); + // Calculate total change + // Ignores healing + let hp_damage = floaters.iter().fold(0, |acc, f| f.hp_change.min(0) + acc); + let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32; + let timer = floaters + .last() + .expect("There must be at least one floater") + .timer; + // Increase font size based on fraction of maximum health + // "flashes" by having a larger size in the first 100ms + let font_size = 30 + + (max_hp_frac * 30.0) as u32 + + if timer < 0.1 { + (FLASH_MAX * (1.0 - timer / 0.1)) as u32 + } else { + 0 + }; + // Timer sets the widget offset + let y = timer as f64 * number_speed * -1.0; + // Timer sets text transparency + let hp_fade = + ((crate::ecs::sys::floater::MY_HP_SHOWTIME - timer) * 0.25) + 0.2; + Text::new(&format!("{}", (hp_damage).abs())) + .font_size(font_size) + .color(Color::Rgba(0.0, 0.0, 0.0, hp_fade)) + .mid_bottom_with_margin_on(ui_widgets.window, 297.0 + y) + .set(player_sct_bg_id, ui_widgets); + Text::new(&format!("{}", (hp_damage).abs())) + .font_size(font_size) + .color(if hp_damage < 0 { + Color::Rgba(1.0, 0.1, 0.0, hp_fade) + } else { + Color::Rgba(0.1, 1.0, 0.1, 0.0) + }) + .mid_bottom_with_margin_on(ui_widgets.window, 300.0 + y) + .set(player_sct_id, ui_widgets); + }; + for floater in floaters { + // Healing always single numbers so just skip damage when in batch mode + + if global_state.settings.gameplay.sct_player_batch && floater.hp_change < 0 + { + continue; + } + let number_speed = 50.0; // Player Heal Speed + let player_sct_bg_id = player_sct_bg_id_walker.next( + &mut self.ids.player_sct_bgs, + &mut ui_widgets.widget_id_generator(), + ); + let player_sct_id = player_sct_id_walker.next( + &mut self.ids.player_scts, + &mut ui_widgets.widget_id_generator(), + ); + let max_hp_frac = + floater.hp_change.abs() as f32 / stats.health.maximum() as f32; + // Increase font size based on fraction of maximum health + // "flashes" by having a larger size in the first 100ms + let font_size = 30 + + (max_hp_frac * 30.0) as u32 + + if floater.timer < 0.1 { + (FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32 + } else { + 0 + }; + // Timer sets the widget offset + let y = if floater.hp_change < 0 { + floater.timer as f64 + * number_speed + * floater.hp_change.signum() as f64 + //* -1.0 + + 300.0 + - ui_widgets.win_h * 0.5 + } else { + floater.timer as f64 + * number_speed + * floater.hp_change.signum() as f64 + * -1.0 + + 300.0 + - ui_widgets.win_h * 0.5 + }; + // Healing is offset randomly + let x = if floater.hp_change < 0 { + 0.0 + } else { + (floater.rand as f64 - 0.5) * 0.2 * ui_widgets.win_w + }; + // Timer sets text transparency + let hp_fade = ((crate::ecs::sys::floater::MY_HP_SHOWTIME - floater.timer) + * 0.25) + + 0.2; + Text::new(&format!("{}", (floater.hp_change).abs())) + .font_size(font_size) + .color(Color::Rgba(0.0, 0.0, 0.0, hp_fade)) + .x_y(x, y - 3.0) + .set(player_sct_bg_id, ui_widgets); + Text::new(&format!("{}", (floater.hp_change).abs())) + .font_size(font_size) + .color(if floater.hp_change < 0 { + Color::Rgba(1.0, 0.1, 0.0, hp_fade) + } else { + Color::Rgba(0.1, 1.0, 0.1, hp_fade) + }) + .x_y(x, y) + .set(player_sct_id, ui_widgets); + } + } + // EXP Numbers + if let (Some(floaters), Some(stats)) = ( + Some(&*ecs.read_resource::()) + .map(|l| &l.floaters) + .filter(|f| !f.is_empty()), + stats.get(me), + ) { + // TODO replace with setting + let batched_sct = false; + if batched_sct { + let number_speed = 50.0; // Number Speed for Cumulated EXP + let player_sct_bg_id = player_sct_bg_id_walker.next( + &mut self.ids.player_sct_bgs, + &mut ui_widgets.widget_id_generator(), + ); + let player_sct_id = player_sct_id_walker.next( + &mut self.ids.player_scts, + &mut ui_widgets.widget_id_generator(), + ); + // Sum xp change + let exp_change = floaters.iter().fold(0, |acc, f| f.exp_change + acc); + // Can't fail since we filtered out empty lists above + let (timer, rand) = floaters + .last() + .map(|f| (f.timer, f.rand)) + .expect("Impossible"); + // Increase font size based on fraction of maximum health + // "flashes" by having a larger size in the first 100ms + let font_size_xp = 30 + + (exp_change.abs() as f32 / stats.exp.maximum() as f32 * 50.0) as u32 + + if timer < 0.1 { + (FLASH_MAX * (1.0 - timer / 0.1)) as u32 + } else { + 0 + }; + + let y = timer as f64 * number_speed; // Timer sets the widget offset + let fade = ((4.0 - timer as f32) * 0.25) + 0.2; // Timer sets text transparency + + Text::new(&format!("{} Exp", exp_change)) + .font_size(font_size_xp) + .color(Color::Rgba(0.0, 0.0, 0.0, fade)) + .x_y( + ui_widgets.win_w * (0.5 * rand.0 as f64 - 0.25), + ui_widgets.win_h * (0.15 * rand.1 as f64) + y - 3.0, + ) + .set(player_sct_bg_id, ui_widgets); + Text::new(&format!("{} Exp", exp_change)) + .font_size(font_size_xp) + .color(Color::Rgba(0.59, 0.41, 0.67, fade)) + .x_y( + ui_widgets.win_w * (0.5 * rand.0 as f64 - 0.25), + ui_widgets.win_h * (0.15 * rand.1 as f64) + y, + ) + .set(player_sct_id, ui_widgets); + } else { + for floater in floaters { + let number_speed = 50.0; // Number Speed for Single EXP + let player_sct_bg_id = player_sct_bg_id_walker.next( + &mut self.ids.player_sct_bgs, + &mut ui_widgets.widget_id_generator(), + ); + let player_sct_id = player_sct_id_walker.next( + &mut self.ids.player_scts, + &mut ui_widgets.widget_id_generator(), + ); + // Increase font size based on fraction of maximum health + // "flashes" by having a larger size in the first 100ms + let font_size_xp = 30 + + (floater.exp_change.abs() as f32 / stats.exp.maximum() as f32 + * 50.0) as u32 + + if floater.timer < 0.1 { + (FLASH_MAX * (1.0 - floater.timer / 0.1)) as u32 + } else { + 0 + }; + + let y = floater.timer as f64 * number_speed; // Timer sets the widget offset + let fade = ((4.0 - floater.timer as f32) * 0.25) + 0.2; // Timer sets text transparency + + Text::new(&format!("{} Exp", floater.exp_change)) + .font_size(font_size_xp) + .color(Color::Rgba(0.0, 0.0, 0.0, fade)) + .x_y( + ui_widgets.win_w * (0.5 * floater.rand.0 as f64 - 0.25), + ui_widgets.win_h * (0.15 * floater.rand.1 as f64) + y - 3.0, + ) + .set(player_sct_bg_id, ui_widgets); + Text::new(&format!("{} Exp", floater.exp_change)) + .font_size(font_size_xp) + .color(Color::Rgba(0.59, 0.41, 0.67, fade)) + .x_y( + ui_widgets.win_w * (0.5 * floater.rand.0 as f64 - 0.25), + ui_widgets.win_h * (0.15 * floater.rand.1 as f64) + y, + ) + .set(player_sct_id, ui_widgets); + } + } + } } // Render Name Tags - for (pos, name, level, scale) in - (&entities, &pos, &stats, players.maybe(), scales.maybe()) - .join() - .filter(|(entity, _, stats, _, _)| *entity != me && !stats.is_dead) - // Don't process nametags outside the vd (visibility further limited by ui backend) - .filter(|(_, pos, _, _, _)| { - Vec2::from(pos.0 - player_pos) - .map2(TerrainChunk::RECT_SIZE, |d: f32, sz| { - d.abs() as f32 / sz as f32 - }) - .magnitude() - < view_distance as f32 - }) - .map(|(_, pos, stats, player, scale)| { - // TODO: This is temporary - // If the player used the default character name display their name instead - let name = if stats.name == "Character Name" { - player.map_or(&stats.name, |p| &p.alias) - } else { - &stats.name - }; - (pos.0, name, stats.level, scale) - }) + for (pos, name, level, scale) in ( + &entities, + &pos, + interpolated.maybe(), + &stats, + players.maybe(), + scales.maybe(), + ) + .join() + .filter(|(entity, _, _, stats, _, _)| *entity != me && !stats.is_dead) + // Don't process nametags outside the vd (visibility further limited by ui backend) + .filter(|(_, pos, _, _, _, _)| { + Vec2::from(pos.0 - player_pos) + .map2(TerrainChunk::RECT_SIZE, |d: f32, sz| { + d.abs() as f32 / sz as f32 + }) + .magnitude() + < view_distance as f32 + }) + .map(|(_, pos, interpolated, stats, player, scale)| { + // TODO: This is temporary + // If the player used the default character name display their name instead + let name = if stats.name == "Character Name" { + player.map_or(&stats.name, |p| &p.alias) + } else { + &stats.name + }; + ( + interpolated.map_or(pos.0, |i| i.pos), + format!("{}", name), + stats.level, + scale.map_or(1.0, |s| s.0), + ) + }) { - let name = format!("{}", name); - let scale = scale.map(|s| s.0).unwrap_or(1.0); let name_id = name_id_walker.next( &mut self.ids.name_tags, &mut ui_widgets.widget_id_generator(), ); + let name_bg_id = name_id_bg_walker.next( + &mut self.ids.name_tags_bgs, + &mut ui_widgets.widget_id_generator(), + ); let level_id = level_id_walker .next(&mut self.ids.levels, &mut ui_widgets.widget_id_generator()); let level_skull_id = level_skull_id_walker.next( @@ -682,6 +1140,13 @@ impl Hud { ); // Name + Text::new(&name) + .font_size(20) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .x_y(-1.0, -1.0) + .position_ingame(pos + Vec3::new(0.0, 0.0, 1.5 * scale + 1.5)) + .resolution(100.0) + .set(name_bg_id, ui_widgets); Text::new(&name) .font_size(20) .color(Color::Rgba(0.61, 0.61, 0.89, 1.0)) @@ -726,15 +1191,16 @@ impl Hud { } else { self.imgs.skull }) - .w_h(30.0, 30.0) - .x_y(0.0, 24.0) - .color(Some(Color::Rgba(1.0, 1.0, 1.0, 0.8))) + .w_h(18.0, 18.0) + .x_y(-39.0, -25.0) + .color(Some(Color::Rgba(1.0, 1.0, 1.0, 1.0))) .position_ingame(pos + Vec3::new(0.0, 0.0, 1.5 * scale + 1.5)) .resolution(100.0) .set(level_skull_id, ui_widgets); } } } + // Introduction Text let intro_text: &'static str = "Welcome to the Veloren Alpha!\n\ @@ -906,7 +1372,7 @@ impl Hud { } // Display debug window. - if self.show.debug { + if global_state.settings.gameplay.toggle_debug { // Alpha Version Text::new(&version) .top_left_with_margins_on(ui_widgets.window, 5.0, 5.0) @@ -1018,12 +1484,46 @@ impl Hud { .set(self.ids.num_figures, ui_widgets); // Help Window - Text::new("Press 'F1' to show Keybindings") - .color(TEXT_COLOR) - .down_from(self.ids.num_figures, 5.0) - .font_id(self.fonts.cyri) - .font_size(14) - .set(self.ids.help_info, ui_widgets); + Text::new(&format!( + "Press {:?} to show keybindings", + global_state.settings.controls.help + )) + .color(TEXT_COLOR) + .down_from(self.ids.num_figures, 5.0) + .font_id(self.fonts.cyri) + .font_size(14) + .set(self.ids.help_info, ui_widgets); + // Info about Debug Shortcut + Text::new(&format!( + "Press {:?} to toggle debug info", + global_state.settings.controls.toggle_debug + )) + .color(TEXT_COLOR) + .down_from(self.ids.help_info, 5.0) + .font_id(self.fonts.cyri) + .font_size(14) + .set(self.ids.debug_info, ui_widgets); + } else { + // Help Window + Text::new(&format!( + "Press {:?} to show keybindings", + global_state.settings.controls.help + )) + .color(TEXT_COLOR) + .top_left_with_margins_on(ui_widgets.window, 5.0, 5.0) + .font_id(self.fonts.cyri) + .font_size(16) + .set(self.ids.help_info, ui_widgets); + // Info about Debug Shortcut + Text::new(&format!( + "Press {:?} to toggle debug info", + global_state.settings.controls.toggle_debug + )) + .color(TEXT_COLOR) + .down_from(self.ids.help_info, 5.0) + .font_id(self.fonts.cyri) + .font_size(12) + .set(self.ids.debug_info, ui_widgets); } // Add Bag-Space Button. @@ -1185,6 +1685,15 @@ impl Hud { .set(self.ids.settings_window, ui_widgets) { match event { + settings_window::Event::Sct(sct) => { + events.push(Event::Sct(sct)); + } + settings_window::Event::SctPlayerBatch(sct_player_batch) => { + events.push(Event::SctPlayerBatch(sct_player_batch)); + } + settings_window::Event::SctDamageBatch(sct_damage_batch) => { + events.push(Event::SctDamageBatch(sct_damage_batch)); + } settings_window::Event::ToggleHelp => self.show.help = !self.show.help, settings_window::Event::ToggleDebug => self.show.debug = !self.show.debug, settings_window::Event::ChangeTab(tab) => self.show.open_setting_tab(tab), @@ -1319,6 +1828,7 @@ impl Hud { self.world_map, &self.fonts, self.pulse, + self.velocity, ) .set(self.ids.map, ui_widgets) { @@ -1471,7 +1981,8 @@ impl Hud { true } GameInput::ToggleDebug => { - self.show.debug = !self.show.debug; + global_state.settings.gameplay.toggle_debug = + !global_state.settings.gameplay.toggle_debug; true } GameInput::ToggleIngameUi => { diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index b63678dc58..011d8bc0ca 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -18,6 +18,7 @@ const FPS_CHOICES: [u32; 11] = [15, 30, 40, 50, 60, 90, 120, 144, 240, 300, 500] widget_ids! { struct Ids { settings_content, + settings_content_r, settings_icon, settings_button_mo, settings_close, @@ -109,7 +110,24 @@ widget_ids! { placeholder, chat_transp_title, chat_transp_text, - chat_transp_slider + chat_transp_slider, + sct_title, + sct_show_text, + sct_show_radio, + sct_single_dmg_text, + sct_single_dmg_radio, + sct_show_batch_text, + sct_show_batch_radio, + sct_batched_dmg_text, + sct_batched_dmg_radio, + sct_inc_dmg_text, + sct_inc_dmg_radio, + sct_batch_inc_text, + sct_batch_inc_radio, + sct_num_dur_text, + sct_num_dur_slider, + sct_num_dur_value, + } } @@ -178,6 +196,9 @@ pub enum Event { CrosshairType(CrosshairType), UiScale(ScaleChange), ChatTransp(f32), + Sct(bool), + SctPlayerBatch(bool), + SctDamageBatch(bool), } pub enum ScaleChange { @@ -229,6 +250,10 @@ impl<'a> Widget for SettingsWindow<'a> { .scroll_kids() .scroll_kids_vertically() .set(state.ids.settings_content, ui); + Rectangle::fill_with([198.0 * 4.0 * 0.5, 97.0 * 4.0], color::TRANSPARENT) + .top_right_with_margins_on(state.ids.settings_content, 0.0, 0.0) + .parent(state.ids.settings_content) + .set(state.ids.settings_content_r, ui); Scrollbar::y_axis(state.ids.settings_content) .thickness(5.0) .rgba(0.33, 0.33, 0.33, 1.0) @@ -279,7 +304,7 @@ impl<'a> Widget for SettingsWindow<'a> { events.push(Event::ChangeTab(SettingsTab::Interface)); } - // Contents + // Contents Left Side if let SettingsTab::Interface = self.show.settings_tab { let crosshair_transp = self.global_state.settings.gameplay.crosshair_transp; let crosshair_type = self.global_state.settings.gameplay.crosshair_type; @@ -309,7 +334,7 @@ impl<'a> Widget for SettingsWindow<'a> { events.push(Event::ToggleHelp); } - Text::new("Show Help Window") + Text::new("Help Window") .right_from(state.ids.button_help, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -333,7 +358,7 @@ impl<'a> Widget for SettingsWindow<'a> { events.push(Event::ToggleDebug); } - Text::new("Show Debug Info") + Text::new("Debug Info") .right_from(state.ids.debug_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -360,7 +385,7 @@ impl<'a> Widget for SettingsWindow<'a> { Intro::Never => events.push(Event::Intro(Intro::Show)), } }; - Text::new("Show Tips on Startup") + Text::new("Tips on Startup") .right_from(state.ids.tips_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -683,7 +708,7 @@ impl<'a> Widget for SettingsWindow<'a> { XpBar::OnGain => events.push(Event::ToggleXpBar(XpBar::Always)), } } - Text::new("Always show Experience Bar") + Text::new("Toggle Experience Bar") .right_from(state.ids.show_xpbar_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -717,7 +742,7 @@ impl<'a> Widget for SettingsWindow<'a> { } } } - Text::new("Always show Shortcuts") + Text::new("Toggle Shortcuts") .right_from(state.ids.show_shortcuts_button, 10.0) .font_size(14) .font_id(self.fonts.cyri) @@ -725,10 +750,151 @@ impl<'a> Widget for SettingsWindow<'a> { .color(TEXT_COLOR) .set(state.ids.show_shortcuts_text, ui); + Rectangle::fill_with([60.0 * 4.0, 1.0 * 4.0], color::TRANSPARENT) + .down_from(state.ids.show_shortcuts_text, 30.0) + .set(state.ids.placeholder, ui); + + // Content Right Side + + /*Scrolling Combat text + + O Show Damage Numbers + O Show single Damage Numbers + O Show batched dealt Damage + O Show incoming Damage + O Batch incoming Numbers + + Number Display Duration: 1s ----I----5s + */ + // SCT/ Scrolling Combat Text + Text::new("Scrolling Combat Text") + .top_left_with_margins_on(state.ids.settings_content_r, 5.0, 5.0) + .font_size(18) + .font_id(self.fonts.cyri) + .color(TEXT_COLOR) + .set(state.ids.sct_title, ui); + // Generally toggle the SCT + let show_sct = ToggleButton::new( + self.global_state.settings.gameplay.sct, + self.imgs.checkbox, + self.imgs.checkbox_checked, + ) + .w_h(18.0, 18.0) + .down_from(state.ids.sct_title, 20.0) + .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) + .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) + .set(state.ids.sct_show_radio, ui); + + if self.global_state.settings.gameplay.sct != show_sct { + events.push(Event::Sct(!self.global_state.settings.gameplay.sct)) + } + Text::new("Scrolling Combat Text") + .right_from(state.ids.sct_show_radio, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .graphics_for(state.ids.sct_show_radio) + .color(TEXT_COLOR) + .set(state.ids.sct_show_text, ui); + if self.global_state.settings.gameplay.sct { + // Toggle single damage numbers + let show_sct_damage_batch = !ToggleButton::new( + !self.global_state.settings.gameplay.sct_damage_batch, + self.imgs.checkbox, + self.imgs.checkbox_checked, + ) + .w_h(18.0, 18.0) + .down_from(state.ids.sct_show_text, 8.0) + .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) + .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) + .set(state.ids.sct_single_dmg_radio, ui); + + Text::new("Single Damage Numbers") + .right_from(state.ids.sct_single_dmg_radio, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .graphics_for(state.ids.sct_single_dmg_radio) + .color(TEXT_COLOR) + .set(state.ids.sct_single_dmg_text, ui); + // Toggle Batched Damage + let show_sct_damage_batch = ToggleButton::new( + show_sct_damage_batch, + self.imgs.checkbox, + self.imgs.checkbox_checked, + ) + .w_h(18.0, 18.0) + .down_from(state.ids.sct_single_dmg_radio, 8.0) + .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) + .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) + .set(state.ids.sct_show_batch_radio, ui); + + if self.global_state.settings.gameplay.sct_damage_batch != show_sct_damage_batch { + events.push(Event::SctDamageBatch( + !self.global_state.settings.gameplay.sct_damage_batch, + )) + } + Text::new("Cumulated Damage") + .right_from(state.ids.sct_show_batch_radio, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .graphics_for(state.ids.sct_batched_dmg_radio) + .color(TEXT_COLOR) + .set(state.ids.sct_show_batch_text, ui); + // Toggle Incoming Damage + let show_sct_player_batch = !ToggleButton::new( + !self.global_state.settings.gameplay.sct_player_batch, + self.imgs.checkbox, + self.imgs.checkbox_checked, + ) + .w_h(18.0, 18.0) + .down_from(state.ids.sct_show_batch_radio, 8.0) + .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) + .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) + .set(state.ids.sct_inc_dmg_radio, ui); + + Text::new("Incoming Damage") + .right_from(state.ids.sct_inc_dmg_radio, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .graphics_for(state.ids.sct_inc_dmg_radio) + .color(TEXT_COLOR) + .set(state.ids.sct_inc_dmg_text, ui); + // Toggle Batched Incoming Damage + let show_sct_player_batch = ToggleButton::new( + show_sct_player_batch, + self.imgs.checkbox, + self.imgs.checkbox_checked, + ) + .w_h(18.0, 18.0) + .down_from(state.ids.sct_inc_dmg_radio, 8.0) + .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) + .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) + .set(state.ids.sct_batch_inc_radio, ui); + + if self.global_state.settings.gameplay.sct_player_batch != show_sct_player_batch { + events.push(Event::SctPlayerBatch( + !self.global_state.settings.gameplay.sct_player_batch, + )) + } + Text::new("Cumulated Incoming Damage") + .right_from(state.ids.sct_batch_inc_radio, 10.0) + .font_size(14) + .font_id(self.fonts.cyri) + .graphics_for(state.ids.sct_batch_inc_radio) + .color(TEXT_COLOR) + .set(state.ids.sct_batch_inc_text, ui); + } + // Energybars Numbers // Hotbar text Text::new("Energybar Numbers") - .down_from(state.ids.show_shortcuts_button, 20.0) + .down_from( + if self.global_state.settings.gameplay.sct { + state.ids.sct_batch_inc_radio + } else { + state.ids.sct_show_radio + }, + 20.0, + ) .font_size(18) .font_id(self.fonts.cyri) .color(TEXT_COLOR) @@ -857,9 +1023,6 @@ impl<'a> Widget for SettingsWindow<'a> { { events.push(Event::ChatTransp(new_val)); } - Rectangle::fill_with([60.0 * 4.0, 1.0 * 4.0], color::TRANSPARENT) - .down_from(state.ids.chat_transp_title, 30.0) - .set(state.ids.placeholder, ui); } // 2) Gameplay Tab -------------------------------- diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index ff536fc45e..6b72c9ccee 100644 --- a/voxygen/src/hud/skillbar.rs +++ b/voxygen/src/hud/skillbar.rs @@ -70,14 +70,19 @@ widget_ids! { healthbar_bg, healthbar_filling, health_text, + health_text_bg, energybar_bg, energybar_filling, energy_text, + energy_text_bg, level_up, level_down, level_align, level_message, + level_message_bg, stamina_wheel, + death_bg, + hurt_bg, } } @@ -215,7 +220,7 @@ impl<'a> Widget for Skillbar<'a> { // Update last_value state.update(|s| s.last_level = current_level); state.update(|s| s.last_update_level = Instant::now()); - } + }; let seconds_level = state.last_update_level.elapsed().as_secs_f32(); let fade_level = if current_level == 1 { @@ -236,6 +241,12 @@ impl<'a> Widget for Skillbar<'a> { .middle_of(state.ids.level_align) .font_size(30) .font_id(self.fonts.cyri) + .color(Color::Rgba(0.0, 0.0, 0.0, fade_level)) + .set(state.ids.level_message_bg, ui); + Text::new(&level_up_text) + .bottom_left_with_margins_on(state.ids.level_message_bg, 2.0, 2.0) + .font_size(30) + .font_id(self.fonts.cyri) .color(Color::Rgba(1.0, 1.0, 1.0, fade_level)) .set(state.ids.level_message, ui); Image::new(self.imgs.level_up) @@ -251,35 +262,38 @@ impl<'a> Widget for Skillbar<'a> { .graphics_for(state.ids.level_align) .set(state.ids.level_down, ui); // Death message - if hp_percentage == 0.0 { + if self.stats.is_dead { Text::new("You Died") - .mid_top_with_margin_on(ui.window, 60.0) - .font_size(40) + .middle_of(ui.window) + .font_size(50) .font_id(self.fonts.cyri) .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) .set(state.ids.death_message_1_bg, ui); Text::new(&format!( - "Press {:?} to respawn.", + "Press {:?} to respawn at your Waypoint.\n\ + \n\ + Press Enter, type in /waypoint and confirm to set it here.", self.global_state.settings.controls.respawn )) - .mid_bottom_with_margin_on(state.ids.death_message_1, -30.0) - .font_size(15) + .mid_bottom_with_margin_on(state.ids.death_message_1_bg, -120.0) + .font_size(30) .font_id(self.fonts.cyri) .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) .set(state.ids.death_message_2_bg, ui); - Text::new("You Died") - .top_left_with_margins_on(state.ids.death_message_1_bg, -2.0, -2.0) - .font_size(40) + .bottom_left_with_margins_on(state.ids.death_message_1_bg, 2.0, 2.0) + .font_size(50) .font_id(self.fonts.cyri) .color(CRITICAL_HP_COLOR) .set(state.ids.death_message_1, ui); Text::new(&format!( - "Press {:?} to respawn.", + "Press {:?} to respawn at your Waypoint.\n\ + \n\ + Press Enter, type in /waypoint and confirm to set it here.", self.global_state.settings.controls.respawn )) - .top_left_with_margins_on(state.ids.death_message_2_bg, -1.5, -1.5) - .font_size(15) + .bottom_left_with_margins_on(state.ids.death_message_2_bg, 2.0, 2.0) + .font_size(30) .color(CRITICAL_HP_COLOR) .set(state.ids.death_message_2, ui); } @@ -812,7 +826,13 @@ impl<'a> Widget for Skillbar<'a> { self.stats.health.maximum() as u32 ); Text::new(&hp_text) - .mid_top_with_margin_on(state.ids.healthbar_bg, 5.0 * scale) + .mid_top_with_margin_on(state.ids.healthbar_bg, 6.0 * scale) + .font_size(14) + .font_id(self.fonts.cyri) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .set(state.ids.health_text_bg, ui); + Text::new(&hp_text) + .bottom_left_with_margins_on(state.ids.health_text_bg, 2.0, 2.0) .font_size(14) .font_id(self.fonts.cyri) .color(TEXT_COLOR) @@ -823,7 +843,13 @@ impl<'a> Widget for Skillbar<'a> { self.stats.energy.maximum() as u32 ); Text::new(&energy_text) - .mid_top_with_margin_on(state.ids.energybar_bg, 5.0 * scale) + .mid_top_with_margin_on(state.ids.energybar_bg, 6.0 * scale) + .font_size(14) + .font_id(self.fonts.cyri) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .set(state.ids.energy_text_bg, ui); + Text::new(&energy_text) + .bottom_left_with_margins_on(state.ids.energy_text_bg, 2.0, 2.0) .font_size(14) .font_id(self.fonts.cyri) .color(TEXT_COLOR) @@ -833,14 +859,26 @@ impl<'a> Widget for Skillbar<'a> { if let BarNumbers::Percent = bar_values { let hp_text = format!("{}%", hp_percentage as u32); Text::new(&hp_text) - .mid_top_with_margin_on(state.ids.healthbar_bg, 5.0 * scale) + .mid_top_with_margin_on(state.ids.healthbar_bg, 6.0 * scale) + .font_size(14) + .font_id(self.fonts.cyri) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .set(state.ids.health_text_bg, ui); + Text::new(&hp_text) + .bottom_left_with_margins_on(state.ids.health_text_bg, 2.0, 2.0) .font_size(14) .font_id(self.fonts.cyri) .color(TEXT_COLOR) .set(state.ids.health_text, ui); let energy_text = format!("{}%", energy_percentage as u32); Text::new(&energy_text) - .mid_top_with_margin_on(state.ids.energybar_bg, 5.0 * scale) + .mid_top_with_margin_on(state.ids.energybar_bg, 6.0 * scale) + .font_size(14) + .font_id(self.fonts.cyri) + .color(Color::Rgba(0.0, 0.0, 0.0, 1.0)) + .set(state.ids.energy_text_bg, ui); + Text::new(&energy_text) + .bottom_left_with_margins_on(state.ids.energy_text_bg, 2.0, 2.0) .font_size(14) .font_id(self.fonts.cyri) .color(TEXT_COLOR) diff --git a/voxygen/src/main.rs b/voxygen/src/main.rs index 70ade6247a..b6fac4e48b 100644 --- a/voxygen/src/main.rs +++ b/voxygen/src/main.rs @@ -16,6 +16,7 @@ use std::sync::Mutex; pub mod ui; pub mod anim; pub mod audio; +mod ecs; pub mod error; pub mod hud; pub mod key_state; diff --git a/voxygen/src/menu/char_selection/mod.rs b/voxygen/src/menu/char_selection/mod.rs index a80b8acbde..62af6386e9 100644 --- a/voxygen/src/menu/char_selection/mod.rs +++ b/voxygen/src/menu/char_selection/mod.rs @@ -108,11 +108,11 @@ impl PlayState for CharSelectionState { .render(global_state.window.renderer_mut(), self.scene.globals()); // Tick the client (currently only to keep the connection alive). - if let Err(err) = self - .client - .borrow_mut() - .tick(comp::ControllerInputs::default(), clock.get_last_delta()) - { + if let Err(err) = self.client.borrow_mut().tick( + comp::ControllerInputs::default(), + clock.get_last_delta(), + |_| {}, + ) { global_state.info_message = Some( "Connection lost!\nDid the server restart?\nIs the client up to date?" .to_owned(), diff --git a/voxygen/src/menu/main/mod.rs b/voxygen/src/menu/main/mod.rs index f972819a3b..511341e14e 100644 --- a/voxygen/src/menu/main/mod.rs +++ b/voxygen/src/menu/main/mod.rs @@ -69,8 +69,10 @@ impl PlayState for MainMenuState { // Poll client creation. match client_init.as_ref().and_then(|init| init.poll()) { - Some(Ok(client)) => { + Some(Ok(mut client)) => { self.main_menu_ui.connected(); + // Register voxygen components / resources + crate::ecs::init(client.state_mut().ecs_mut()); return PlayStateResult::Push(Box::new(CharSelectionState::new( global_state, std::rc::Rc::new(std::cell::RefCell::new(client)), diff --git a/voxygen/src/menu/main/ui.rs b/voxygen/src/menu/main/ui.rs index 5c62db41c1..5340c37184 100644 --- a/voxygen/src/menu/main/ui.rs +++ b/voxygen/src/menu/main/ui.rs @@ -1,3 +1,4 @@ +use crate::ui::Graphic; use crate::{ render::Renderer, ui::{ @@ -7,6 +8,7 @@ use crate::{ }, GlobalState, }; +use common::assets::load_expect; use conrod_core::{ color, color::TRANSPARENT, @@ -14,6 +16,7 @@ use conrod_core::{ widget::{text_box::Event as TextBoxEvent, Button, Image, List, Rectangle, Text, TextBox}, widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget, }; +use rand::{seq::SliceRandom, thread_rng}; use std::time::Duration; widget_ids! { @@ -84,7 +87,6 @@ image_ids! { bg: "voxygen.background.bg_main", - load: "voxygen.background.bg_load", nothing: (), @@ -129,6 +131,7 @@ pub enum PopupType { Error, ConnectionInfo, } + pub struct PopupData { msg: String, button_text: String, @@ -150,6 +153,7 @@ pub struct MainMenuUi { show_servers: bool, show_disclaimer: bool, time: f32, + bg_img_id: conrod_core::image::Id, } impl MainMenuUi { @@ -157,6 +161,18 @@ impl MainMenuUi { let window = &mut global_state.window; let networking = &global_state.settings.networking; let gameplay = &global_state.settings.gameplay; + // Randomly loaded background images + let bg_imgs = [ + "voxygen.background.bg_1", + "voxygen.background.bg_2", + "voxygen.background.bg_3", + "voxygen.background.bg_4", + "voxygen.background.bg_5", + "voxygen.background.bg_6", + "voxygen.background.bg_7", + "voxygen.background.bg_8", + ]; + let mut rng = thread_rng(); let mut ui = Ui::new(window).unwrap(); ui.set_scaling_mode(gameplay.ui_scale); @@ -165,6 +181,9 @@ impl MainMenuUi { // Load images let imgs = Imgs::load(&mut ui).expect("Failed to load images"); let rot_imgs = ImgsRot::load(&mut ui).expect("Failed to load images!"); + let bg_img_id = ui.add_graphic(Graphic::Image(load_expect( + bg_imgs.choose(&mut rng).unwrap(), + ))); // Load fonts let fonts = Fonts::load(&mut ui).expect("Failed to load fonts"); @@ -183,6 +202,7 @@ impl MainMenuUi { connect: false, time: 0.0, show_disclaimer: global_state.settings.show_disclaimer, + bg_img_id, } } @@ -225,14 +245,14 @@ impl MainMenuUi { .desc_text_color(TEXT_COLOR_2); // Background image, Veloren logo, Alpha-Version Label - Image::new(if self.connect { - self.imgs.load + self.bg_img_id } else { self.imgs.bg }) .middle_of(ui_widgets.window) .set(self.ids.bg, ui_widgets); + // Version displayed top right corner Text::new(&version) .color(TEXT_COLOR) diff --git a/voxygen/src/render/pipelines/ui.rs b/voxygen/src/render/pipelines/ui.rs index 8787def27a..1d89ed3e5f 100644 --- a/voxygen/src/render/pipelines/ui.rs +++ b/voxygen/src/render/pipelines/ui.rs @@ -43,10 +43,10 @@ impl Pipeline for UiPipeline { type Vertex = Vertex; } -impl From> for Locals { - fn from(pos: Vec3) -> Self { +impl From> for Locals { + fn from(pos: Vec4) -> Self { Self { - pos: [pos.x, pos.y, pos.z, 1.0], + pos: pos.into_array(), } } } diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 6887710b40..2ce0f36da9 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -1194,6 +1194,7 @@ impl FigureState { self.last_ori = Lerp::lerp(self.last_ori, ori, 15.0 * dt); // Update interpolation values + // TODO: use values from Interpolated component instead of recalculating if self.pos.distance_squared(pos) < 64.0 * 64.0 { self.pos = Lerp::lerp(self.pos, pos + vel * 0.03, 10.0 * dt); self.ori = Slerp::slerp(self.ori, ori, 5.0 * dt); @@ -1205,6 +1206,7 @@ impl FigureState { self.movement_time += (dt * movement_rate) as f64; self.action_time += (dt * action_rate) as f64; + // TODO: what are the interpolated ori values used for if not here??? let mat = Mat4::::identity() * Mat4::translation_3d(self.pos) * Mat4::rotation_z(-ori.x.atan2(ori.y)) diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 66aaacf40c..9be5180727 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -215,23 +215,31 @@ impl Scene { let mut lights = ( &client.state().ecs().read_storage::(), client.state().ecs().read_storage::().maybe(), + client + .state() + .ecs() + .read_storage::() + .maybe(), &client.state().ecs().read_storage::(), ) .join() - .filter(|(pos, _, _)| { + .filter(|(pos, _, _, _)| { (pos.0.distance_squared(player_pos) as f32) < self.loaded_distance.powf(2.0) + LIGHT_DIST_RADIUS }) - .map(|(pos, ori, light_emitter)| { + .map(|(pos, ori, interpolated, light_emitter)| { + // Use interpolated values if they are available + let (pos, ori) = + interpolated.map_or((pos.0, ori.map(|o| o.0)), |i| (i.pos, Some(i.ori))); let rot = { if let Some(o) = ori { - Mat3::rotation_z(-o.0.x.atan2(o.0.y)) + Mat3::rotation_z(-o.x.atan2(o.y)) } else { Mat3::identity() } }; Light::new( - pos.0 + (rot * light_emitter.offset), + pos + (rot * light_emitter.offset), light_emitter.col, light_emitter.strength, ) @@ -246,17 +254,28 @@ impl Scene { // Update shadow constants let mut shadows = ( &client.state().ecs().read_storage::(), + client + .state() + .ecs() + .read_storage::() + .maybe(), client.state().ecs().read_storage::().maybe(), &client.state().ecs().read_storage::(), &client.state().ecs().read_storage::(), ) .join() - .filter(|(_, _, _, stats)| !stats.is_dead) - .filter(|(pos, _, _, _)| { + .filter(|(_, _, _, _, stats)| !stats.is_dead) + .filter(|(pos, _, _, _, _)| { (pos.0.distance_squared(player_pos) as f32) < (self.loaded_distance.min(SHADOW_MAX_DIST) + SHADOW_DIST_RADIUS).powf(2.0) }) - .map(|(pos, scale, _, _)| Shadow::new(pos.0, scale.map(|s| s.0).unwrap_or(1.0))) + .map(|(pos, interpolated, scale, _, _)| { + Shadow::new( + // Use interpolated values pos if it is available + interpolated.map_or(pos.0, |i| i.pos), + scale.map_or(1.0, |s| s.0), + ) + }) .collect::>(); shadows.sort_by_key(|shadow| shadow.get_pos().distance_squared(player_pos) as i32); shadows.truncate(MAX_SHADOW_COUNT); diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index e3eb0bac70..ab37b411b9 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -1,4 +1,5 @@ use crate::{ + ecs::MyEntity, hud::{DebugInfo, Event as HudEvent, Hud}, key_state::KeyState, render::Renderer, @@ -40,6 +41,14 @@ impl SessionState { .camera_mut() .set_fov_deg(global_state.settings.graphics.fov); let hud = Hud::new(global_state, &client.borrow()); + { + let my_entity = client.borrow().entity(); + client + .borrow_mut() + .state_mut() + .ecs_mut() + .insert(MyEntity(my_entity)); + } Self { scene, client, @@ -55,7 +64,11 @@ impl SessionState { /// Tick the session (and the client attached to it). fn tick(&mut self, dt: Duration) -> Result<(), Error> { self.inputs.tick(dt); - for event in self.client.borrow_mut().tick(self.inputs.clone(), dt)? { + for event in self.client.borrow_mut().tick( + self.inputs.clone(), + dt, + crate::ecs::sys::add_local_systems, + )? { match event { Chat { chat_type: _, @@ -423,6 +436,22 @@ impl PlayState for SessionState { global_state.settings.gameplay.zoom_inversion = zoom_inverted; global_state.settings.save_to_file_warn(); } + HudEvent::Sct(sct) => { + global_state.settings.gameplay.sct = sct; + global_state.settings.save_to_file_warn(); + } + HudEvent::SctPlayerBatch(sct_player_batch) => { + global_state.settings.gameplay.sct_player_batch = sct_player_batch; + global_state.settings.save_to_file_warn(); + } + HudEvent::SctDamageBatch(sct_damage_batch) => { + global_state.settings.gameplay.sct_damage_batch = sct_damage_batch; + global_state.settings.save_to_file_warn(); + } + HudEvent::ToggleDebug(toggle_debug) => { + global_state.settings.gameplay.toggle_debug = toggle_debug; + global_state.settings.save_to_file_warn(); + } HudEvent::ToggleMouseYInvert(mouse_y_inverted) => { global_state.window.mouse_y_inversion = mouse_y_inverted; global_state.settings.gameplay.mouse_y_inversion = mouse_y_inverted; diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index e2a567f174..940823465f 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -85,8 +85,8 @@ impl Default for ControlSettings { screenshot: KeyMouse::Key(VirtualKeyCode::F4), toggle_ingame_ui: KeyMouse::Key(VirtualKeyCode::F6), roll: KeyMouse::Mouse(MouseButton::Middle), - respawn: KeyMouse::Mouse(MouseButton::Left), - interact: KeyMouse::Key(VirtualKeyCode::E), + respawn: KeyMouse::Key(VirtualKeyCode::Space), + interact: KeyMouse::Mouse(MouseButton::Right), toggle_wield: KeyMouse::Key(VirtualKeyCode::T), charge: KeyMouse::Key(VirtualKeyCode::V), } @@ -100,6 +100,10 @@ pub struct GameplaySettings { pub pan_sensitivity: u32, pub zoom_sensitivity: u32, pub zoom_inversion: bool, + pub toggle_debug: bool, + pub sct: bool, + pub sct_player_batch: bool, + pub sct_damage_batch: bool, pub mouse_y_inversion: bool, pub crosshair_transp: f32, pub chat_transp: f32, @@ -118,6 +122,10 @@ impl Default for GameplaySettings { zoom_sensitivity: 100, zoom_inversion: false, mouse_y_inversion: false, + toggle_debug: false, + sct: true, + sct_player_batch: true, + sct_damage_batch: false, crosshair_transp: 0.6, chat_transp: 0.4, crosshair_type: CrosshairType::Round, @@ -179,9 +187,9 @@ pub struct GraphicsSettings { impl Default for GraphicsSettings { fn default() -> Self { Self { - view_distance: 5, + view_distance: 10, max_fps: 60, - fov: 75, + fov: 50, aa_mode: AaMode::Fxaa, } } diff --git a/voxygen/src/ui/mod.rs b/voxygen/src/ui/mod.rs index 6a20dbfbf0..87b0b27697 100644 --- a/voxygen/src/ui/mod.rs +++ b/voxygen/src/ui/mod.rs @@ -305,7 +305,7 @@ impl Ui { enum Placement { Interface, // Number of primitives left to render ingame and relative scaling/resolution - InWorld(usize, Option), + InWorld(usize, Option<(f64, f64)>), }; let mut placement = Placement::Interface; @@ -399,7 +399,7 @@ impl Ui { // Functions for converting for conrod scalar coords to GL vertex coords (-1.0 to 1.0). let (ui_win_w, ui_win_h) = match placement { - Placement::InWorld(_, Some(res)) => (res as f64, res as f64), + Placement::InWorld(_, Some(res)) => res, // Behind the camera or far away Placement::InWorld(_, None) => continue, Placement::Interface => (self.ui.win_w, self.ui.win_h), @@ -624,10 +624,12 @@ impl Ui { .parameters; let pos_in_view = view_mat * Vec4::from_point(parameters.pos); + let scale_factor = self.ui.win_w as f64 / (-2.0 * pos_in_view.z as f64 * (0.5 * fov as f64).tan() + // TODO: make this have no effect for fixed scale * parameters.res as f64); // Don't process ingame elements behind the camera or very far away placement = if scale_factor > 0.2 { @@ -642,32 +644,43 @@ impl Ui { }); start = mesh.vertices().len(); // Push new position command + let mut world_pos = Vec4::from_point(parameters.pos); + if parameters.fixed_scale { + world_pos.w = -1.0 + }; + if self.ingame_locals.len() > ingame_local_index { renderer .update_consts( &mut self.ingame_locals[ingame_local_index], - &[parameters.pos.into()], + &[world_pos.into()], ) .unwrap(); } else { - self.ingame_locals.push( - renderer.create_consts(&[parameters.pos.into()]).unwrap(), - ); + self.ingame_locals + .push(renderer.create_consts(&[world_pos.into()]).unwrap()); } self.draw_commands .push(DrawCommand::WorldPos(Some(ingame_local_index))); ingame_local_index += 1; - p_scale_factor = ((scale_factor * 10.0).log2().round().powi(2) - / 10.0) - .min(1.6) - .max(0.2); + p_scale_factor = if parameters.fixed_scale { + self.scale.scale_factor_physical() + } else { + ((scale_factor * 10.0).log2().round().powi(2) / 10.0) + .min(1.6) + .max(0.2) + }; // Scale down ingame elements that are close to the camera - let res = if scale_factor > 3.2 { - parameters.res * scale_factor as f32 / 3.2 + let res = if parameters.fixed_scale { + (self.ui.win_w, self.ui.win_h) + } else if scale_factor > 3.2 { + let res = parameters.res * scale_factor as f32 / 3.2; + (res as f64, res as f64) } else { - parameters.res + let res = parameters.res; + (res as f64, res as f64) }; Placement::InWorld(parameters.num, Some(res)) diff --git a/voxygen/src/ui/widgets/ingame.rs b/voxygen/src/ui/widgets/ingame.rs index a8a41c31c1..c9e96193eb 100644 --- a/voxygen/src/ui/widgets/ingame.rs +++ b/voxygen/src/ui/widgets/ingame.rs @@ -57,6 +57,9 @@ pub struct IngameParameters { // Used for widgets that are rasterized before being sent to the gpu (text & images) // Potentially make this automatic based on distance to camera? pub res: f32, + // Whether the widgets should be scaled based on distance to the camera or if they should be a + // fixed size (res is ignored in that case) + pub fixed_scale: bool, } pub struct State { @@ -74,10 +77,15 @@ impl Ingame { num: widget.prim_count(), pos, res: 1.0, + fixed_scale: false, }, widget, } } + pub fn fixed_scale(mut self) -> Self { + self.parameters.fixed_scale = true; + self + } builder_methods! { pub resolution { parameters.res = f32 } } @@ -147,6 +155,7 @@ impl IngameAnchor { num: 0, pos, res: 1.0, + fixed_scale: false, }, } }