From 2bf8e1865f8bcabb131b8c7ed816a0de1003fd33 Mon Sep 17 00:00:00 2001 From: Christof Petig Date: Fri, 27 May 2022 17:19:52 +0000 Subject: [PATCH] Skiing and ice skating --- CHANGELOG.md | 1 + .../common/items/armor/misc/foot/iceskate.ron | 15 +++ assets/common/items/armor/misc/foot/ski.ron | 16 +++ assets/voxygen/item_image_manifest.ron | 8 ++ .../voxel/armor/misc/foot/iceskate.vox | 3 + assets/voxygen/voxel/armor/misc/foot/ski.vox | 3 + .../voxel/humanoid_armor_foot_manifest.ron | 8 ++ common/src/comp/ability.rs | 1 + common/src/comp/character_state.rs | 43 +++++-- common/src/comp/inventory/item/armor.rs | 53 ++++++++ common/src/comp/phys.rs | 8 +- common/src/states/climb.rs | 4 +- common/src/states/dance.rs | 4 +- common/src/states/glide.rs | 4 +- common/src/states/glide_wield.rs | 4 +- common/src/states/idle.rs | 22 +++- common/src/states/mod.rs | 1 + common/src/states/roll.rs | 2 + common/src/states/sit.rs | 4 +- common/src/states/skate.rs | 118 ++++++++++++++++++ common/src/states/sprite_interact.rs | 3 +- common/src/states/stunned.rs | 4 +- common/src/states/talk.rs | 6 +- common/src/states/use_item.rs | 3 +- common/src/states/utils.rs | 40 +++++- common/src/states/wallrun.rs | 2 +- common/src/states/wielding.rs | 2 + common/systems/src/character_behavior.rs | 2 +- common/systems/src/phys.rs | 103 +++++++++++++-- common/systems/src/stats.rs | 1 + voxygen/egui/src/lib.rs | 3 +- .../audio/sfx/event_mapper/movement/tests.rs | 10 +- voxygen/src/scene/figure/mod.rs | 50 +++----- 33 files changed, 461 insertions(+), 90 deletions(-) create mode 100644 assets/common/items/armor/misc/foot/iceskate.ron create mode 100644 assets/common/items/armor/misc/foot/ski.ron create mode 100644 assets/voxygen/voxel/armor/misc/foot/iceskate.vox create mode 100644 assets/voxygen/voxel/armor/misc/foot/ski.vox create mode 100644 common/src/states/skate.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 319daaddc9..9b84829b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Water caves - Modular weapons - Added Thai translation +- Skiing and ice skating ### Changed diff --git a/assets/common/items/armor/misc/foot/iceskate.ron b/assets/common/items/armor/misc/foot/iceskate.ron new file mode 100644 index 0000000000..5dcbe4d806 --- /dev/null +++ b/assets/common/items/armor/misc/foot/iceskate.ron @@ -0,0 +1,15 @@ +ItemDef( + name: "Ice Skates", + description: "Best used on a frozen lake.", + kind: Armor(( + kind: Foot, + stats: ( + protection: Some(Normal(3.0)), + ground_contact: Skate, + ), + )), + quality: Moderate, + tags: [ + Material(Steel), + ], +) diff --git a/assets/common/items/armor/misc/foot/ski.ron b/assets/common/items/armor/misc/foot/ski.ron new file mode 100644 index 0000000000..4fea9c1388 --- /dev/null +++ b/assets/common/items/armor/misc/foot/ski.ron @@ -0,0 +1,16 @@ +ItemDef( + name: "Wooden skis", + description: "Best used downhill on snow.", + kind: Armor(( + kind: Foot, + stats: ( + protection: Some(Normal(3.0)), + ground_contact: Ski, + ), + )), + quality: Moderate, + tags: [ + Material(Wood), +// SalvageInto(Twigs), + ], +) diff --git a/assets/voxygen/item_image_manifest.ron b/assets/voxygen/item_image_manifest.ron index 1c7db9d146..d28341f06b 100644 --- a/assets/voxygen/item_image_manifest.ron +++ b/assets/voxygen/item_image_manifest.ron @@ -2699,6 +2699,14 @@ "voxel.armor.misc.pants.grayscale", (0.0, 1.0, 0.0), (-120.0, 210.0,15.0), 0.9, ), + Simple("common.items.armor.misc.foot.ski"): VoxTrans( + "voxel.armor.misc.foot.ski", + (0.0, 0.0, 0.0), (-120.0, 210.0,15.0), 0.9, + ), + Simple("common.items.armor.misc.foot.iceskate"): VoxTrans( + "voxel.armor.misc.foot.iceskate", + (0.0, 0.0, 0.0), (-120.0, 210.0,15.0), 0.9, + ), // Backs Simple("common.items.armor.misc.back.short_0"): VoxTrans( "voxel.armor.misc.back.short-0", diff --git a/assets/voxygen/voxel/armor/misc/foot/iceskate.vox b/assets/voxygen/voxel/armor/misc/foot/iceskate.vox new file mode 100644 index 0000000000..c7cdeaec4c --- /dev/null +++ b/assets/voxygen/voxel/armor/misc/foot/iceskate.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c09d41a50285049e51249219d3ecfb5a034913491d35d67948705c957d42c8f +size 1596 diff --git a/assets/voxygen/voxel/armor/misc/foot/ski.vox b/assets/voxygen/voxel/armor/misc/foot/ski.vox new file mode 100644 index 0000000000..cdbb40415c --- /dev/null +++ b/assets/voxygen/voxel/armor/misc/foot/ski.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16768cda33af1d245e2aee7aa92eea497f604227517b276a9b164cd6ab850320 +size 23322 diff --git a/assets/voxygen/voxel/humanoid_armor_foot_manifest.ron b/assets/voxygen/voxel/humanoid_armor_foot_manifest.ron index 5f42c91d7c..d4b95c17d3 100644 --- a/assets/voxygen/voxel/humanoid_armor_foot_manifest.ron +++ b/assets/voxygen/voxel/humanoid_armor_foot_manifest.ron @@ -164,5 +164,13 @@ vox_spec: ("armor.merchant.foot", (-2.5, -3.5, -2.0)), color: None ), + "common.items.armor.misc.foot.ski": ( + vox_spec: ("armor.misc.foot.ski", (-2.5, -15.5, -2.0)), + color: None + ), + "common.items.armor.misc.foot.iceskate": ( + vox_spec: ("armor.misc.foot.iceskate", (-2.5, -3.5, -2.0)), + color: None + ), }, )) diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 1c1daacf9b..95be90faeb 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -402,6 +402,7 @@ impl From<&CharacterState> for CharacterAbilityType { | CharacterState::SpriteSummon(_) | CharacterState::UseItem(_) | CharacterState::SpriteInteract(_) + | CharacterState::Skate(_) | CharacterState::Wallrun(_) => Self::Other, } } diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 24acfec446..115fea7714 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -1,6 +1,7 @@ use crate::{ comp::{ - item::ConsumableKind, ControlAction, Density, Energy, InputAttr, InputKind, Ori, Pos, Vel, + inventory::item::armor::Friction, item::ConsumableKind, ControlAction, Density, Energy, + InputAttr, InputKind, Ori, Pos, Vel, }, event::{LocalEvent, ServerEvent}, states::{ @@ -123,6 +124,8 @@ pub enum CharacterState { SpriteInteract(sprite_interact::Data), /// Runs on the wall Wallrun(wallrun::Data), + /// Ice skating or skiing + Skate(skate::Data), } impl CharacterState { @@ -153,15 +156,16 @@ impl CharacterState { pub fn is_stealthy(&self) -> bool { matches!( self, - CharacterState::Idle(idle::Data { is_sneaking: true }) - | CharacterState::Wielding(wielding::Data { - is_sneaking: true, - .. - }) - | CharacterState::Roll(roll::Data { - is_sneaking: true, - .. - }) + CharacterState::Idle(idle::Data { + is_sneaking: true, + footwear: _ + }) | CharacterState::Wielding(wielding::Data { + is_sneaking: true, + .. + }) | CharacterState::Roll(roll::Data { + is_sneaking: true, + .. + }) ) } @@ -227,6 +231,8 @@ impl CharacterState { pub fn is_glide(&self) -> bool { matches!(self, CharacterState::Glide(_)) } + pub fn is_skate(&self) -> bool { matches!(self, CharacterState::Skate(_)) } + pub fn is_melee_dodge(&self) -> bool { matches!(self, CharacterState::Roll(d) if d.static_data.immune_melee) } @@ -329,6 +335,7 @@ impl CharacterState { CharacterState::SpriteSummon(data) => data.behavior(j, output_events), CharacterState::UseItem(data) => data.behavior(j, output_events), CharacterState::SpriteInteract(data) => data.behavior(j, output_events), + CharacterState::Skate(data) => data.behavior(j, output_events), } } @@ -375,12 +382,26 @@ impl CharacterState { CharacterState::SpriteSummon(data) => data.handle_event(j, output_events, action), CharacterState::UseItem(data) => data.handle_event(j, output_events, action), CharacterState::SpriteInteract(data) => data.handle_event(j, output_events, action), + CharacterState::Skate(data) => data.handle_event(j, output_events, action), + } + } + + pub fn footwear(&self) -> Option { + match &self { + CharacterState::Idle(data) => data.footwear, + CharacterState::Skate(data) => Some(data.footwear), + _ => None, } } } impl Default for CharacterState { - fn default() -> Self { Self::Idle(idle::Data { is_sneaking: false }) } + fn default() -> Self { + Self::Idle(idle::Data { + is_sneaking: false, + footwear: None, + }) + } } impl Component for CharacterState { diff --git a/common/src/comp/inventory/item/armor.rs b/common/src/comp/inventory/item/armor.rs index 4ae071fb94..ebdb2f8b61 100644 --- a/common/src/comp/inventory/item/armor.rs +++ b/common/src/comp/inventory/item/armor.rs @@ -1,3 +1,7 @@ +use crate::{ + comp::item::Rgb, + terrain::{Block, BlockKind}, +}; use serde::{Deserialize, Serialize}; use std::{cmp::Ordering, ops::Sub}; @@ -26,6 +30,45 @@ impl Armor { } } +/// longitudinal and lateral friction, only meaningful for footwear +#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] +pub enum Friction { + Normal, + Ski, + Skate, + // Snowshoe, + // Spikes, +} + +impl Default for Friction { + fn default() -> Self { Self::Normal } +} + +impl Friction { + pub fn can_skate_on(&self, b: BlockKind) -> bool { + match self { + Friction::Ski => matches!(b, BlockKind::Snow | BlockKind::Ice | BlockKind::Air), + Friction::Skate => b == BlockKind::Ice, + _ => false, + } + } + + /// longitudinal (forward) and lateral (side) friction + pub fn get_friction(&self, b: BlockKind) -> (f32, f32) { + match (self, b) { + (Friction::Ski, BlockKind::Snow) => (0.01, 0.95), + (Friction::Ski, BlockKind::Ice) => (0.001, 0.5), + (Friction::Ski, BlockKind::Water) => (0.1, 0.7), + (Friction::Ski, BlockKind::Air) => (0.0, 0.0), + (Friction::Skate, BlockKind::Ice) => (0.001, 0.99), + _ => { + let non_directional_friction = Block::new(b, Rgb::new(0, 0, 0)).get_friction(); + (non_directional_friction, non_directional_friction) + }, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct Stats { /// Protection is non-linearly transformed (following summation) to a damage @@ -46,6 +89,9 @@ pub struct Stats { /// Stealth is summed along with the base stealth bonus (2.0), and then /// the agent's perception distance is divided by this value stealth: Option, + /// Ground contact type, mostly for shoes + #[serde(default)] + ground_contact: Friction, } impl Stats { @@ -66,6 +112,7 @@ impl Stats { energy_reward, crit_power, stealth, + ground_contact: Friction::Normal, } } @@ -80,6 +127,8 @@ impl Stats { pub fn crit_power(&self) -> Option { self.crit_power } pub fn stealth(&self) -> Option { self.stealth } + + pub fn ground_contact(&self) -> Friction { self.ground_contact } } impl Sub for Stats { @@ -99,6 +148,7 @@ impl Sub for Stats { .map(|(a, b)| a - b), crit_power: self.crit_power.zip(other.crit_power).map(|(a, b)| a - b), stealth: self.stealth.zip(other.stealth).map(|(a, b)| a - b), + ground_contact: Friction::Normal, } } } @@ -159,6 +209,8 @@ impl Armor { pub fn stealth(&self) -> Option { self.stats.stealth } + pub fn ground_contact(&self) -> Friction { self.stats.ground_contact } + #[cfg(test)] pub fn test_armor( kind: ArmorKind, @@ -174,6 +226,7 @@ impl Armor { energy_reward: None, crit_power: None, stealth: None, + ground_contact: Friction::Normal, }, } } diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index 423fbaf6a9..d020d8e089 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -1,6 +1,9 @@ use super::{Fluid, Ori}; use crate::{ - comp::body::ship::figuredata::VoxelCollider, consts::WATER_DENSITY, terrain::Block, uid::Uid, + comp::{body::ship::figuredata::VoxelCollider, inventory::item::armor::Friction}, + consts::WATER_DENSITY, + terrain::Block, + uid::Uid, }; use hashbrown::HashSet; use serde::{Deserialize, Serialize}; @@ -182,6 +185,9 @@ pub struct PhysicsState { pub touch_entities: HashSet, pub in_fluid: Option, pub ground_vel: Vec3, + pub footwear: Friction, + pub skating_last_height: f32, + pub skating_active: bool, } impl PhysicsState { diff --git a/common/src/states/climb.rs b/common/src/states/climb.rs index 0f4bbbb4c4..68c2dd0be0 100644 --- a/common/src/states/climb.rs +++ b/common/src/states/climb.rs @@ -79,7 +79,7 @@ impl CharacterBehavior for Data { CLIMB_BOOST_JUMP_FACTOR * impulse / data.mass.0, )); }; - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); return update; }; // Move player @@ -103,7 +103,7 @@ impl CharacterBehavior for Data { .try_change_by(-energy_use * data.dt.0) .is_err() { - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); } // Set orientation direction based on wall direction diff --git a/common/src/states/dance.rs b/common/src/states/dance.rs index 0dc8653c3c..171e04bb39 100644 --- a/common/src/states/dance.rs +++ b/common/src/states/dance.rs @@ -20,7 +20,7 @@ impl CharacterBehavior for Data { // Try to Fall/Stand up/Move if data.physics.on_ground.is_none() || data.inputs.move_dir.magnitude_squared() > 0.0 { - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); } update @@ -52,7 +52,7 @@ impl CharacterBehavior for Data { fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); // Try to Fall/Stand up/Move - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); update } } diff --git a/common/src/states/glide.rs b/common/src/states/glide.rs index 35ecfd3ffd..81f437a198 100644 --- a/common/src/states/glide.rs +++ b/common/src/states/glide.rs @@ -89,7 +89,7 @@ impl CharacterBehavior for Data { .and_then(|inv| inv.equipped(EquipSlot::Glider)) .is_none() { - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); } else if !handle_climb(data, &mut update) { let air_flow = data .physics @@ -207,7 +207,7 @@ impl CharacterBehavior for Data { pos: data.pos.0, wielded: false, })); - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); update } } diff --git a/common/src/states/glide_wield.rs b/common/src/states/glide_wield.rs index 50261ecae5..f0d0e0b361 100644 --- a/common/src/states/glide_wield.rs +++ b/common/src/states/glide_wield.rs @@ -74,7 +74,7 @@ impl CharacterBehavior for Data { ..*self }) } else { - CharacterState::Idle(idle::Data { is_sneaking: false }) + CharacterState::Idle(idle::Data::default()) }; } @@ -98,7 +98,7 @@ impl CharacterBehavior for Data { pos: data.pos.0, wielded: false, })); - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); update } diff --git a/common/src/states/idle.rs b/common/src/states/idle.rs index 207f544264..4d46446694 100644 --- a/common/src/states/idle.rs +++ b/common/src/states/idle.rs @@ -1,19 +1,25 @@ use super::utils::*; use crate::{ - comp::{character_state::OutputEvents, CharacterState, InventoryAction, StateUpdate}, + comp::{ + character_state::OutputEvents, inventory::item::armor::Friction, CharacterState, + InventoryAction, StateUpdate, + }, states::behavior::{CharacterBehavior, JoinData}, }; use serde::{Deserialize, Serialize}; -#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Default)] pub struct Data { pub is_sneaking: bool, + // None means unknown + pub(crate) footwear: Option, } impl CharacterBehavior for Data { fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); + handle_skating(data, &mut update); handle_orientation(data, &mut update, 1.0, None); handle_move(data, &mut update, if self.is_sneaking { 0.4 } else { 1.0 }); handle_jump(data, output_events, &mut update, 1.0); @@ -26,7 +32,10 @@ impl CharacterBehavior for Data { if self.is_sneaking && (data.physics.on_ground.is_none() || data.physics.in_liquid().is_some()) { - update.character = CharacterState::Idle(Data { is_sneaking: false }); + update.character = CharacterState::Idle(Data { + is_sneaking: false, + footwear: self.footwear, + }); } update @@ -75,13 +84,16 @@ impl CharacterBehavior for Data { fn sneak(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); - update.character = CharacterState::Idle(Data { is_sneaking: true }); + update.character = CharacterState::Idle(Data { + is_sneaking: true, + footwear: self.footwear, + }); update } fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); - update.character = CharacterState::Idle(Data { is_sneaking: false }); + update.character = CharacterState::Idle(Data::default()); update } diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index 10ec478c17..50920d6b11 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -23,6 +23,7 @@ pub mod roll; pub mod self_buff; pub mod shockwave; pub mod sit; +pub mod skate; pub mod spin_melee; pub mod sprite_interact; pub mod sprite_summon; diff --git a/common/src/states/roll.rs b/common/src/states/roll.rs index dbd96c527f..1d44e1428c 100644 --- a/common/src/states/roll.rs +++ b/common/src/states/roll.rs @@ -142,6 +142,7 @@ impl CharacterBehavior for Data { } else { CharacterState::Idle(idle::Data { is_sneaking: self.is_sneaking, + footwear: None, }) } } @@ -151,6 +152,7 @@ impl CharacterBehavior for Data { // If it somehow ends up in an incorrect stage section update.character = CharacterState::Idle(idle::Data { is_sneaking: self.is_sneaking, + footwear: None, }); }, } diff --git a/common/src/states/sit.rs b/common/src/states/sit.rs index a3f59648dd..3d19ae94f5 100644 --- a/common/src/states/sit.rs +++ b/common/src/states/sit.rs @@ -20,7 +20,7 @@ impl CharacterBehavior for Data { // Try to Fall/Stand up/Move if data.physics.on_ground.is_none() || data.inputs.move_dir.magnitude_squared() > 0.0 { - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); } update @@ -52,7 +52,7 @@ impl CharacterBehavior for Data { fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); // Try to Fall/Stand up/Move - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); update } } diff --git a/common/src/states/skate.rs b/common/src/states/skate.rs new file mode 100644 index 0000000000..c78e842de0 --- /dev/null +++ b/common/src/states/skate.rs @@ -0,0 +1,118 @@ +use super::utils::*; +use crate::{ + comp::{ + character_state::OutputEvents, item::armor::Friction, CharacterState, InventoryAction, + StateUpdate, + }, + states::{ + behavior::{CharacterBehavior, JoinData}, + idle, + }, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Data { + pub(crate) footwear: Friction, + // hints for animation + pub turn: f32, // negative to left, positive to right, 1.0=45° + pub accelerate: f32, // negative to brake + pub sidewalk: f32, // negative to left +} + +impl Data { + pub fn new(_: &JoinData, footwear: Friction) -> Self { + Self { + footwear, + turn: Default::default(), + accelerate: Default::default(), + sidewalk: Default::default(), + } + } +} + +impl CharacterBehavior for Data { + fn behavior(&self, data: &JoinData, output_events: &mut OutputEvents) -> StateUpdate { + let mut update = StateUpdate::from(data); + + handle_wield(data, &mut update); + handle_jump(data, output_events, &mut update, 1.0); + + if !data.physics.skating_active { + update.character = CharacterState::Idle(idle::Data { + is_sneaking: false, + footwear: Some(self.footwear), + }); + } else { + let plane_ori = data.inputs.look_dir.xy(); + let orthogonal = vek::Vec2::new(plane_ori.y, -plane_ori.x); + update.ori = vek::Vec3::new(plane_ori.x, plane_ori.y, 0.0).into(); + let current_planar_velocity = data.vel.0.xy().magnitude(); + let long_input = data.inputs.move_dir.dot(plane_ori); + let lat_input = data.inputs.move_dir.dot(orthogonal); + let acceleration = if long_input.abs() > lat_input.abs() { + if long_input > 0.0 { + if let CharacterState::Skate(data) = &mut update.character { + data.accelerate = 1.0; + data.sidewalk = 0.0; + } + // forward, max at 8u/s + (data.dt.0 * 3.0) + .min(8.0 - current_planar_velocity) + .max(0.0) + } else { + if let CharacterState::Skate(data) = &mut update.character { + data.accelerate = -1.0; + data.sidewalk = 0.0; + } + //brake up to 4u/s², but never backwards + (data.dt.0 * 4.0).min(current_planar_velocity) + } + } else { + if let CharacterState::Skate(data) = &mut update.character { + data.accelerate = 0.0; + data.sidewalk = lat_input; + } + // sideways: constant speed + (0.5 - current_planar_velocity).max(0.0) + }; + if let CharacterState::Skate(skate_data) = &mut update.character { + skate_data.turn = orthogonal.dot(data.vel.0.xy()); + } + let delta_vel = acceleration * data.inputs.move_dir; + update.vel.0 += vek::Vec3::new(delta_vel.x, delta_vel.y, 0.0); + } + + update + } + + fn manipulate_loadout( + &self, + data: &JoinData, + output_events: &mut OutputEvents, + inv_action: InventoryAction, + ) -> StateUpdate { + let mut update = StateUpdate::from(data); + handle_manipulate_loadout(data, output_events, &mut update, inv_action); + update + } + + fn wield(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_wield(data, &mut update); + update + } + + fn sit(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { + let mut update = StateUpdate::from(data); + attempt_sit(data, &mut update); + update + } + + fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { + let mut update = StateUpdate::from(data); + // Try to Fall/Stand up/Move + update.character = CharacterState::Idle(idle::Data::default()); + update + } +} diff --git a/common/src/states/sprite_interact.rs b/common/src/states/sprite_interact.rs index 68a556e8cd..8faa350d18 100644 --- a/common/src/states/sprite_interact.rs +++ b/common/src/states/sprite_interact.rs @@ -106,13 +106,14 @@ impl CharacterBehavior for Data { } else { CharacterState::Idle(idle::Data { is_sneaking: self.static_data.was_sneak, + footwear: None, }) } } }, _ => { // If it somehow ends up in an incorrect stage section - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); }, } diff --git a/common/src/states/stunned.rs b/common/src/states/stunned.rs index e899895145..0c694b71b3 100644 --- a/common/src/states/stunned.rs +++ b/common/src/states/stunned.rs @@ -72,7 +72,7 @@ impl CharacterBehavior for Data { update.character = CharacterState::Wielding(wielding::Data { is_sneaking: false }); } else { - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); } } }, @@ -82,7 +82,7 @@ impl CharacterBehavior for Data { update.character = CharacterState::Wielding(wielding::Data { is_sneaking: false }); } else { - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); } }, } diff --git a/common/src/states/talk.rs b/common/src/states/talk.rs index 25a43a1e6f..270166825c 100644 --- a/common/src/states/talk.rs +++ b/common/src/states/talk.rs @@ -42,14 +42,14 @@ impl CharacterBehavior for Data { fn sit(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); attempt_sit(data, &mut update); update } fn dance(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); attempt_dance(data, &mut update); update } @@ -57,7 +57,7 @@ impl CharacterBehavior for Data { fn stand(&self, data: &JoinData, _: &mut OutputEvents) -> StateUpdate { let mut update = StateUpdate::from(data); // Try to Fall/Stand up/Move - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); update } } diff --git a/common/src/states/use_item.rs b/common/src/states/use_item.rs index 6279bcebf6..5d6a2fac9c 100644 --- a/common/src/states/use_item.rs +++ b/common/src/states/use_item.rs @@ -132,13 +132,14 @@ impl CharacterBehavior for Data { } else { CharacterState::Idle(idle::Data { is_sneaking: self.static_data.was_sneak, + footwear: None, }) } } }, _ => { // If it somehow ends up in an incorrect stage section - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); }, } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 4537a37e7a..45b3813e5c 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -4,8 +4,8 @@ use crate::{ comp::{ arthropod, biped_large, biped_small, character_state::OutputEvents, - inventory::slot::{EquipSlot, Slot}, - item::{Hands, ItemKind, ToolKind}, + inventory::slot::{ArmorSlot, EquipSlot, Slot}, + item::{armor::Friction, Hands, ItemKind, ToolKind}, quadruped_low, quadruped_medium, quadruped_small, skills::{Skill, SwimSkill, SKILL_MODIFIERS}, theropod, Body, CharacterAbility, CharacterState, Density, InputAttr, InputKind, @@ -14,7 +14,7 @@ use crate::{ consts::{FRIC_GROUND, GRAVITY, MAX_PICKUP_RANGE}, event::{LocalEvent, ServerEvent}, outcome::Outcome, - states::{behavior::JoinData, *}, + states::{behavior::JoinData, utils::CharacterState::Idle, *}, util::Dir, vol::ReadVol, }; @@ -301,6 +301,35 @@ impl Body { } } +/// set footwear in idle data and potential state change to Skate +pub fn handle_skating(data: &JoinData, update: &mut StateUpdate) { + if let Idle(crate::states::idle::Data { + is_sneaking, + mut footwear, + }) = data.character + { + if footwear.is_none() { + footwear = data.inventory.and_then(|inv| { + inv.equipped(EquipSlot::Armor(ArmorSlot::Feet)) + .map(|armor| match armor.kind().as_ref() { + ItemKind::Armor(a) => a.ground_contact(), + _ => crate::comp::inventory::item::armor::Friction::Normal, + }) + }); + update.character = Idle(crate::states::idle::Data { + is_sneaking: *is_sneaking, + footwear, + }); + } + if data.physics.skating_active { + update.character = CharacterState::Skate(crate::states::skate::Data::new( + data, + footwear.unwrap_or(Friction::Normal), + )); + } + } +} + /// Handles updating `Components` to move player based on state of `JoinData` pub fn handle_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) { let submersion = data @@ -635,7 +664,10 @@ pub fn attempt_talk(data: &JoinData<'_>, update: &mut StateUpdate) { pub fn attempt_sneak(data: &JoinData<'_>, update: &mut StateUpdate) { if data.physics.on_ground.is_some() && data.body.is_humanoid() { - update.character = CharacterState::Idle(idle::Data { is_sneaking: true }); + update.character = CharacterState::Idle(idle::Data { + is_sneaking: true, + footwear: data.character.footwear(), + }); } } diff --git a/common/src/states/wallrun.rs b/common/src/states/wallrun.rs index 7c53447022..adbba0adee 100644 --- a/common/src/states/wallrun.rs +++ b/common/src/states/wallrun.rs @@ -38,7 +38,7 @@ impl CharacterBehavior for Data { || data.physics.on_ground.is_some() || data.physics.in_liquid().is_some() { - update.character = CharacterState::Idle(idle::Data { is_sneaking: false }); + update.character = CharacterState::Idle(idle::Data::default()); } update diff --git a/common/src/states/wielding.rs b/common/src/states/wielding.rs index c750cfe42b..0cfea7f298 100644 --- a/common/src/states/wielding.rs +++ b/common/src/states/wielding.rs @@ -58,6 +58,7 @@ impl CharacterBehavior for Data { ) => { update.character = CharacterState::Idle(idle::Data { is_sneaking: self.is_sneaking, + footwear: None, }); }, _ => (), @@ -76,6 +77,7 @@ impl CharacterBehavior for Data { let mut update = StateUpdate::from(data); update.character = CharacterState::Idle(idle::Data { is_sneaking: self.is_sneaking, + footwear: None, }); update } diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index b82c849978..af268fb325 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -213,7 +213,7 @@ impl<'a> System<'a> for Sys { // If mounted, character state is controlled by mount if is_rider.is_some() && !join_struct.char_state.can_perform_mounted() { // TODO: A better way to swap between mount inputs and rider inputs - *join_struct.char_state = CharacterState::Idle(idle::Data { is_sneaking: false }); + *join_struct.char_state = CharacterState::Idle(idle::Data::default()); continue; } diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 3343050c5f..86fb126403 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -2,6 +2,7 @@ use common::{ comp::{ body::ship::figuredata::{VoxelCollider, VOXEL_COLLIDER_MANIFEST}, fluid_dynamics::{Fluid, LiquidKind, Wings}, + inventory::item::armor::Friction, Body, CharacterState, Collider, Density, Immovable, Mass, Ori, PhysicsState, Pos, PosVelOriDefer, PreviousPhysCache, Projectile, Scale, Stats, Sticky, Vel, }, @@ -12,7 +13,7 @@ use common::{ outcome::Outcome, resources::DeltaTime, states, - terrain::{Block, TerrainGrid}, + terrain::{Block, BlockKind, TerrainGrid}, uid::Uid, util::{Projection, SpatialGrid}, vol::{BaseVol, ReadVol}, @@ -780,6 +781,13 @@ impl<'a> PhysicsData<'a> { 1.0 }; + if let Some(state) = character_state { + let footwear = state.footwear().unwrap_or(Friction::Normal); + if footwear != physics_state.footwear { + physics_state.footwear = footwear; + } + } + let in_loaded_chunk = read .terrain .get_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) @@ -846,6 +854,7 @@ impl<'a> PhysicsData<'a> { climbing, |entity, vel| land_on_ground = Some((entity, vel)), read, + &ori, ); tgt_pos = cpos.0; }, @@ -878,6 +887,7 @@ impl<'a> PhysicsData<'a> { climbing, |entity, vel| land_on_ground = Some((entity, vel)), read, + &ori, ); // Sticky things shouldn't move when on a surface @@ -1142,6 +1152,7 @@ impl<'a> PhysicsData<'a> { Some((entity, Vel(ori_from.mul_direction(vel.0)))); }, read, + &ori, ); cpos.0 = transform_from.mul_point(cpos.0) + wpos; @@ -1339,6 +1350,7 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( climbing: bool, mut land_on_ground: impl FnMut(Entity, Vel), read: &PhysicsRead, + ori: &Ori, ) { // FIXME: Review these #![allow( @@ -1700,19 +1712,84 @@ fn box_voxel_collision<'a, T: BaseVol + ReadVol>( physics_state.on_wall = on_wall; let fric_mod = read.stats.get(entity).map_or(1.0, |s| s.friction_modifier); - let ground_fric = physics_state - .on_ground - .map(|b| b.get_friction()) - .unwrap_or(0.0); - let wall_fric = if physics_state.on_wall.is_some() && climbing { - FRIC_GROUND + // skating (ski) + if !vel.0.xy().is_approx_zero() + && physics_state + .on_ground + .map_or(false, |g| physics_state.footwear.can_skate_on(g.kind())) + { + const DT_SCALE: f32 = 1.0; // other areas use 60.0??? + const POTENTIAL_TO_KINETIC: f32 = 8.0; // * 2.0 * GRAVITY; + + let kind = physics_state.on_ground.map_or(BlockKind::Air, |g| g.kind()); + let (longitudinal_friction, lateral_friction) = physics_state.footwear.get_friction(kind); + // the amount of longitudinal speed preserved + let longitudinal_friction_factor_squared = + (1.0 - longitudinal_friction).powf(dt.0 * DT_SCALE * 2.0); + let lateral_friction_factor = (1.0 - lateral_friction).powf(dt.0 * DT_SCALE); + let groundplane_velocity = vel.0.xy(); + let mut longitudinal_dir = ori.look_vec().xy(); + if longitudinal_dir.is_approx_zero() { + // fall back to travelling dir (in case we look up) + longitudinal_dir = groundplane_velocity; + } + let longitudinal_dir = longitudinal_dir.normalized(); + let lateral_dir = Vec2::new(longitudinal_dir.y, -longitudinal_dir.x); + let squared_velocity = groundplane_velocity.magnitude_squared(); + // if we crossed an edge up or down accelerate in travelling direction, + // as potential energy is converted into kinetic energy we compare it with the + // square of velocity + let vertical_difference = physics_state.skating_last_height - pos.0.z; + // might become negative when skating slowly uphill + let height_factor_squared = if vertical_difference != 0.0 { + // E=½mv², we scale both energies by ½m + let kinetic = squared_velocity; + // positive accelerate, negative decelerate, ΔE=mgΔh + let delta_potential = vertical_difference.max(-1.0).min(2.0) * POTENTIAL_TO_KINETIC; + let new_energy = kinetic + delta_potential; + physics_state.skating_last_height = pos.0.z; + new_energy / kinetic + } else { + 1.0 + }; + + // we calculate these squared as we need to combined them Euclidianly anyway, + // skiing: separate speed into longitudinal and lateral component + let long_speed = groundplane_velocity.dot(longitudinal_dir); + let lat_speed = groundplane_velocity.dot(lateral_dir); + let long_speed_squared = long_speed.powi(2); + + // lateral speed is reduced by lateral_friction, + let new_lateral = lat_speed * lateral_friction_factor; + let lateral_speed_reduction = lat_speed - new_lateral; + // we convert this reduction partically (by the cosine of the angle) into + // longitudinal (elastic collision) and the remainder into heat + let cosine_squared_aoa = long_speed_squared / squared_velocity; + let converted_lateral_squared = cosine_squared_aoa * lateral_speed_reduction.powi(2); + let new_longitudinal_squared = longitudinal_friction_factor_squared + * (long_speed_squared + converted_lateral_squared) + * height_factor_squared; + let new_longitudinal = + new_longitudinal_squared.signum() * new_longitudinal_squared.abs().sqrt(); + let new_ground_speed = new_longitudinal * longitudinal_dir + new_lateral * lateral_dir; + physics_state.skating_active = true; + vel.0 = Vec3::new(new_ground_speed.x, new_ground_speed.y, 0.0); } else { - 0.0 - }; - let fric = ground_fric.max(wall_fric); - if fric > 0.0 { - vel.0 *= (1.0 - fric.min(1.0) * fric_mod).powf(dt.0 * 60.0); - physics_state.ground_vel = ground_vel; + let ground_fric = physics_state + .on_ground + .map(|b| b.get_friction()) + .unwrap_or(0.0); + let wall_fric = if physics_state.on_wall.is_some() && climbing { + FRIC_GROUND + } else { + 0.0 + }; + let fric = ground_fric.max(wall_fric); + if fric > 0.0 { + vel.0 *= (1.0 - fric.min(1.0) * fric_mod).powf(dt.0 * 60.0); + physics_state.ground_vel = ground_vel; + } + physics_state.skating_active = false; } physics_state.in_fluid = liquid diff --git a/common/systems/src/stats.rs b/common/systems/src/stats.rs index 77e29c3bf9..a6346ea2d1 100644 --- a/common/systems/src/stats.rs +++ b/common/systems/src/stats.rs @@ -147,6 +147,7 @@ impl<'a> System<'a> for Sys { | CharacterState::Sit { .. } | CharacterState::Dance { .. } | CharacterState::Glide { .. } + | CharacterState::Skate { .. } | CharacterState::GlideWield { .. } | CharacterState::Wielding { .. } | CharacterState::Equipping { .. } diff --git a/voxygen/egui/src/lib.rs b/voxygen/egui/src/lib.rs index c309887c80..b7aaf52d6e 100644 --- a/voxygen/egui/src/lib.rs +++ b/voxygen/egui/src/lib.rs @@ -11,7 +11,7 @@ mod widgets; use client::{Client, Join, World, WorldExt}; use common::{ comp, - comp::{Poise, PoiseState}, + comp::{inventory::item::armor::Friction, Poise, PoiseState}, }; use core::mem; use egui::{ @@ -708,6 +708,7 @@ fn selected_entity_window( Some(Fluid::Liquid { depth, kind, .. }) => format!("{:?} (Depth: {:.1})", kind, depth), _ => "None".to_owned() }); }); + two_col_row(ui, "Footwear", match physics_state.footwear{ Friction::Ski => "Ski", Friction::Skate => "Skate", /* Friction::Snowshoe => "Snowshoe", Friction::Spikes => "Spikes", */ Friction::Normal=>"Normal",}.to_string()); }); } }); diff --git a/voxygen/src/audio/sfx/event_mapper/movement/tests.rs b/voxygen/src/audio/sfx/event_mapper/movement/tests.rs index 1a393dcef0..efee782eb8 100644 --- a/voxygen/src/audio/sfx/event_mapper/movement/tests.rs +++ b/voxygen/src/audio/sfx/event_mapper/movement/tests.rs @@ -93,7 +93,7 @@ fn same_previous_event_elapsed_emits() { #[test] fn maps_idle() { let result = MovementEventMapper::map_movement_event( - &CharacterState::Idle(common::states::idle::Data { is_sneaking: false }), + &CharacterState::Idle(common::states::idle::Data::default()), &PhysicsState { on_ground: Some(Block::empty()), ..Default::default() @@ -115,7 +115,7 @@ fn maps_idle() { #[test] fn maps_run_with_sufficient_velocity() { let result = MovementEventMapper::map_movement_event( - &CharacterState::Idle(common::states::idle::Data { is_sneaking: false }), + &CharacterState::Idle(common::states::idle::Data::default()), &PhysicsState { on_ground: Some(Block::empty()), ..Default::default() @@ -137,7 +137,7 @@ fn maps_run_with_sufficient_velocity() { #[test] fn does_not_map_run_with_insufficient_velocity() { let result = MovementEventMapper::map_movement_event( - &CharacterState::Idle(common::states::idle::Data { is_sneaking: false }), + &CharacterState::Idle(common::states::idle::Data::default()), &PhysicsState { on_ground: Some(Block::empty()), ..Default::default() @@ -159,7 +159,7 @@ fn does_not_map_run_with_insufficient_velocity() { #[test] fn does_not_map_run_with_sufficient_velocity_but_not_on_ground() { let result = MovementEventMapper::map_movement_event( - &CharacterState::Idle(common::states::idle::Data { is_sneaking: false }), + &CharacterState::Idle(common::states::idle::Data::default()), &Default::default(), &PreviousEntityState { event: SfxEvent::Idle, @@ -214,7 +214,7 @@ fn maps_roll() { #[test] fn maps_land_on_ground_to_run() { let result = MovementEventMapper::map_movement_event( - &CharacterState::Idle(common::states::idle::Data { is_sneaking: false }), + &CharacterState::Idle(common::states::idle::Data::default()), &PhysicsState { on_ground: Some(Block::empty()), ..Default::default() diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 6ab216b478..42d2017765 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -976,9 +976,10 @@ impl FigureMgr { rel_vel.magnitude_squared() > 0.01, // Moving physics.in_liquid().is_some(), // In water is_rider.is_some(), + physics.skating_active, ) { - // Standing - (true, false, false, false) => { + // Standing or Skating + (true, false, false, false, _) | (_, _, false, false, true) => { anim::character::StandAnimation::update_skeleton( &CharacterSkeleton::new(holding_lantern), ( @@ -997,7 +998,7 @@ impl FigureMgr { ) }, // Running - (true, true, false, false) => { + (true, true, false, false, _) => { anim::character::RunAnimation::update_skeleton( &CharacterSkeleton::new(holding_lantern), ( @@ -1019,7 +1020,7 @@ impl FigureMgr { ) }, // In air - (false, _, false, false) => { + (false, _, false, false, _) => { anim::character::JumpAnimation::update_skeleton( &CharacterSkeleton::new(holding_lantern), ( @@ -1038,7 +1039,7 @@ impl FigureMgr { ) }, // Swim - (_, _, true, false) => anim::character::SwimAnimation::update_skeleton( + (_, _, true, false, _) => anim::character::SwimAnimation::update_skeleton( &CharacterSkeleton::new(holding_lantern), ( active_tool_kind, @@ -1056,7 +1057,7 @@ impl FigureMgr { skeleton_attr, ), // Mount - (_, _, _, true) => anim::character::MountAnimation::update_skeleton( + (_, _, _, true, _) => anim::character::MountAnimation::update_skeleton( &CharacterSkeleton::new(holding_lantern), ( active_tool_kind, @@ -1260,7 +1261,9 @@ impl FigureMgr { skeleton_attr, ) }, - CharacterState::Idle(idle::Data { is_sneaking: true }) => { + CharacterState::Idle(idle::Data { + is_sneaking: true, .. + }) => { anim::character::SneakAnimation::update_skeleton( &target_base, ( @@ -5257,16 +5260,11 @@ impl FigureMgr { // Average velocity relative to the current ground let _rel_avg_vel = state.avg_vel - physics.ground_vel; + let idlestate = CharacterState::Idle(common::states::idle::Data::default()); + let last = Last(idlestate.clone()); let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), - _ => ( - &CharacterState::Idle(common::states::idle::Data { - is_sneaking: false, - }), - &Last(CharacterState::Idle(common::states::idle::Data { - is_sneaking: false, - })), - ), + _ => (&idlestate, &last), }; if !character.same_variant(&last_character.0) { @@ -5388,16 +5386,11 @@ impl FigureMgr { // Average velocity relative to the current ground let _rel_avg_vel = state.avg_vel - physics.ground_vel; + let idle_state = CharacterState::Idle(common::states::idle::Data::default()); + let last = Last(idle_state.clone()); let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), - _ => ( - &CharacterState::Idle(common::states::idle::Data { - is_sneaking: false, - }), - &Last(CharacterState::Idle(common::states::idle::Data { - is_sneaking: false, - })), - ), + _ => (&idle_state, &last), }; if !character.same_variant(&last_character.0) { @@ -5486,16 +5479,11 @@ impl FigureMgr { // Average velocity relative to the current ground let _rel_avg_vel = state.avg_vel - physics.ground_vel; + let idlestate = CharacterState::Idle(common::states::idle::Data::default()); + let last = Last(idlestate.clone()); let (character, last_character) = match (character, last_character) { (Some(c), Some(l)) => (c, l), - _ => ( - &CharacterState::Idle(common::states::idle::Data { - is_sneaking: false, - }), - &Last(CharacterState::Idle(common::states::idle::Data { - is_sneaking: false, - })), - ), + _ => (&idlestate, &last), }; if !character.same_variant(&last_character.0) {