diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b36a920b0..48b76f69e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Pathfinding is much smoother and pets are cleverer - Animals run/turn at different speeds - Updated windowing library (winit 0.19 -> 0.22) +- Bow M2 is now a charged attack that scales the longer it's held ### Removed diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 575d83b8c5..56a1ad3eda 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -18,6 +18,7 @@ pub enum CharacterAbilityType { BasicMelee, BasicRanged, Boost, + ChargedRanged, DashMelee, BasicBlock, TripleStrike(Stage), @@ -36,6 +37,7 @@ impl From<&CharacterState> for CharacterAbilityType { CharacterState::LeapMelee(_) => Self::LeapMelee, CharacterState::TripleStrike(data) => Self::TripleStrike(data.stage), CharacterState::SpinMelee(_) => Self::SpinMelee, + CharacterState::ChargedRanged(_) => Self::ChargedRanged, _ => Self::BasicMelee, } } @@ -90,6 +92,19 @@ pub enum CharacterAbility { recover_duration: Duration, base_damage: u32, }, + ChargedRanged { + energy_cost: u32, + energy_drain: u32, + initial_damage: u32, + max_damage: u32, + initial_knockback: f32, + max_knockback: f32, + prepare_duration: Duration, + charge_duration: Duration, + recover_duration: Duration, + projectile_body: Body, + projectile_light: Option, + }, } impl CharacterAbility { @@ -131,6 +146,10 @@ impl CharacterAbility { .energy .try_change_by(-(*energy_cost as i32), EnergySource::Ability) .is_ok(), + CharacterAbility::ChargedRanged { energy_cost, .. } => update + .energy + .try_change_by(-(*energy_cost as i32), EnergySource::Ability) + .is_ok(), _ => true, } } @@ -311,6 +330,32 @@ impl From<&CharacterAbility> for CharacterState { * this value can be removed if ability moved to * skillbar */ }), + CharacterAbility::ChargedRanged { + energy_cost: _, + energy_drain, + initial_damage, + max_damage, + initial_knockback, + max_knockback, + prepare_duration, + charge_duration, + recover_duration, + projectile_body, + projectile_light, + } => CharacterState::ChargedRanged(charged_ranged::Data { + exhausted: false, + energy_drain: *energy_drain, + initial_damage: *initial_damage, + max_damage: *max_damage, + initial_knockback: *initial_knockback, + max_knockback: *max_knockback, + prepare_duration: *prepare_duration, + charge_duration: *charge_duration, + charge_timer: Duration::default(), + recover_duration: *recover_duration, + projectile_body: *projectile_body, + projectile_light: *projectile_light, + }), } } } diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index ee132711fd..a77263578d 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -66,6 +66,8 @@ pub enum CharacterState { LeapMelee(leap_melee::Data), /// Spin around, dealing damage to enemies surrounding you SpinMelee(spin_melee::Data), + /// A charged ranged attack (e.g. bow) + ChargedRanged(charged_ranged::Data), } impl CharacterState { @@ -78,7 +80,8 @@ impl CharacterState { | CharacterState::TripleStrike(_) | CharacterState::BasicBlock | CharacterState::LeapMelee(_) - | CharacterState::SpinMelee(_) => true, + | CharacterState::SpinMelee(_) + | CharacterState::ChargedRanged(_) => true, _ => false, } } @@ -90,7 +93,8 @@ impl CharacterState { | CharacterState::DashMelee(_) | CharacterState::TripleStrike(_) | CharacterState::LeapMelee(_) - | CharacterState::SpinMelee(_) => true, + | CharacterState::SpinMelee(_) + | CharacterState::ChargedRanged(_) => true, _ => false, } } @@ -102,7 +106,8 @@ impl CharacterState { | CharacterState::DashMelee(_) | CharacterState::TripleStrike(_) | CharacterState::BasicBlock - | CharacterState::LeapMelee(_) => true, + | CharacterState::LeapMelee(_) + | CharacterState::ChargedRanged(_) => true, _ => false, } } diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index 3c92e7c32b..f5dcefb4fd 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -273,25 +273,18 @@ impl Tool { projectile_light: None, projectile_gravity: Some(Gravity(0.2)), }, - BasicRanged { - energy_cost: 350, - holdable: true, - prepare_duration: Duration::from_millis(250), - recover_duration: Duration::from_millis(700), - projectile: Projectile { - hit_solid: vec![projectile::Effect::Stick], - hit_entity: vec![ - projectile::Effect::Damage(-9), - projectile::Effect::Knockback(15.0), - projectile::Effect::RewardEnergy(50), - projectile::Effect::Vanish, - ], - time_left: Duration::from_secs(15), - owner: None, - }, + ChargedRanged { + energy_cost: 0, + energy_drain: 300, + initial_damage: 3, + max_damage: 15, + initial_knockback: 10.0, + max_knockback: 20.0, + prepare_duration: Duration::from_millis(100), + charge_duration: Duration::from_millis(1500), + recover_duration: Duration::from_millis(500), projectile_body: Body::Object(object::Body::Arrow), projectile_light: None, - projectile_gravity: Some(Gravity(0.05)), }, ], Dagger(_) => vec![ diff --git a/common/src/states/charged_ranged.rs b/common/src/states/charged_ranged.rs new file mode 100644 index 0000000000..7014f8de58 --- /dev/null +++ b/common/src/states/charged_ranged.rs @@ -0,0 +1,192 @@ +use crate::{ + comp::{ + projectile, Body, CharacterState, EnergySource, Gravity, LightEmitter, Projectile, + StateUpdate, + }, + event::ServerEvent, + states::utils::*, + sys::character_behavior::*, +}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +const MAX_GRAVITY: f32 = 0.2; +const MIN_GRAVITY: f32 = 0.05; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Data { + /// Whether the attack fired already + pub exhausted: bool, + /// How much energy is drained per second when charging + pub energy_drain: u32, + /// How much damage is dealt with no charge + pub initial_damage: u32, + /// How much damage is dealt with max charge + pub max_damage: u32, + /// How much knockback there is with no chage + pub initial_knockback: f32, + /// How much knockback there is at max charge + pub max_knockback: f32, + /// How long the weapon needs to be prepared for + pub prepare_duration: Duration, + /// How long it takes to charge the weapon to max damage and knockback + pub charge_duration: Duration, + /// How long the state has been charging + pub charge_timer: Duration, + /// How long the state has until exiting + pub recover_duration: Duration, + /// Projectile information + pub projectile_body: Body, + pub projectile_light: Option, +} + +impl CharacterBehavior for Data { + fn behavior(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate::from(data); + + handle_move(data, &mut update, 0.3); + handle_jump(data, &mut update); + + if self.prepare_duration != Duration::default() { + // Prepare (draw the bow) + update.character = CharacterState::ChargedRanged(Data { + exhausted: self.exhausted, + energy_drain: self.energy_drain, + initial_damage: self.initial_damage, + max_damage: self.max_damage, + initial_knockback: self.initial_knockback, + max_knockback: self.max_knockback, + prepare_duration: self + .prepare_duration + .checked_sub(Duration::from_secs_f32(data.dt.0)) + .unwrap_or_default(), + charge_duration: self.charge_duration, + charge_timer: self.charge_timer, + recover_duration: self.recover_duration, + projectile_body: self.projectile_body, + projectile_light: self.projectile_light, + }); + } else if data.inputs.secondary.is_pressed() + && self.charge_timer < self.charge_duration + && update.energy.current() > 0 + { + // Charge the bow + update.character = CharacterState::ChargedRanged(Data { + exhausted: self.exhausted, + energy_drain: self.energy_drain, + initial_damage: self.initial_damage, + max_damage: self.max_damage, + initial_knockback: self.initial_knockback, + max_knockback: self.max_knockback, + prepare_duration: self.prepare_duration, + charge_timer: self + .charge_timer + .checked_add(Duration::from_secs_f32(data.dt.0)) + .unwrap_or_default(), + charge_duration: self.charge_duration, + recover_duration: self.recover_duration, + projectile_body: self.projectile_body, + projectile_light: self.projectile_light, + }); + + // Consumes energy if there's enough left and RMB is held down + update.energy.change_by( + -(self.energy_drain as f32 * data.dt.0) as i32, + EnergySource::Ability, + ); + } else if data.inputs.secondary.is_pressed() { + // Charge the bow + update.character = CharacterState::ChargedRanged(Data { + exhausted: self.exhausted, + energy_drain: self.energy_drain, + initial_damage: self.initial_damage, + max_damage: self.max_damage, + initial_knockback: self.initial_knockback, + max_knockback: self.max_knockback, + prepare_duration: self.prepare_duration, + charge_timer: self.charge_timer, + charge_duration: self.charge_duration, + recover_duration: self.recover_duration, + projectile_body: self.projectile_body, + projectile_light: self.projectile_light, + }); + + // Consumes energy if there's enough left and RMB is held down + update.energy.change_by( + -(self.energy_drain as f32 * data.dt.0 / 5.0) as i32, + EnergySource::Ability, + ); + } else if !self.exhausted { + let charge_amount = + (self.charge_timer.as_secs_f32() / self.charge_duration.as_secs_f32()).min(1.0); + // Fire + let mut projectile = Projectile { + hit_solid: vec![projectile::Effect::Stick], + hit_entity: vec![ + projectile::Effect::Damage( + -(self.initial_damage as i32 + + (charge_amount * (self.max_damage - self.initial_damage) as f32) + as i32), + ), + projectile::Effect::Knockback( + self.initial_knockback + + charge_amount * (self.max_knockback - self.initial_knockback), + ), + projectile::Effect::Vanish, + ], + time_left: Duration::from_secs(15), + owner: None, + }; + projectile.owner = Some(*data.uid); + update.server_events.push_front(ServerEvent::Shoot { + entity: data.entity, + dir: data.inputs.look_dir, + body: self.projectile_body, + projectile, + light: self.projectile_light, + gravity: Some(Gravity( + MAX_GRAVITY - charge_amount * (MAX_GRAVITY - MIN_GRAVITY), + )), + }); + + update.character = CharacterState::ChargedRanged(Data { + exhausted: true, + energy_drain: self.energy_drain, + initial_damage: self.initial_damage, + max_damage: self.max_damage, + initial_knockback: self.initial_knockback, + max_knockback: self.max_knockback, + prepare_duration: self.prepare_duration, + charge_timer: self.charge_timer, + charge_duration: self.charge_duration, + recover_duration: self.recover_duration, + projectile_body: self.projectile_body, + projectile_light: self.projectile_light, + }); + } else if self.recover_duration != Duration::default() { + // Recovery + update.character = CharacterState::ChargedRanged(Data { + exhausted: self.exhausted, + energy_drain: self.energy_drain, + initial_damage: self.initial_damage, + max_damage: self.max_damage, + initial_knockback: self.initial_knockback, + max_knockback: self.max_knockback, + prepare_duration: self.prepare_duration, + charge_timer: self.charge_timer, + charge_duration: self.charge_duration, + recover_duration: self + .recover_duration + .checked_sub(Duration::from_secs_f32(data.dt.0)) + .unwrap_or_default(), + projectile_body: self.projectile_body, + projectile_light: self.projectile_light, + }); + } else { + // Done + update.character = CharacterState::Wielding; + } + + update + } +} diff --git a/common/src/states/mod.rs b/common/src/states/mod.rs index 35676a2d17..b88bfe7514 100644 --- a/common/src/states/mod.rs +++ b/common/src/states/mod.rs @@ -2,6 +2,7 @@ pub mod basic_block; pub mod basic_melee; pub mod basic_ranged; pub mod boost; +pub mod charged_ranged; pub mod climb; pub mod dance; pub mod dash_melee; diff --git a/common/src/sys/character_behavior.rs b/common/src/sys/character_behavior.rs index 496a3f2207..e79d31aa29 100644 --- a/common/src/sys/character_behavior.rs +++ b/common/src/sys/character_behavior.rs @@ -245,6 +245,7 @@ impl<'a> System<'a> for Sys { CharacterState::DashMelee(data) => data.handle_event(&j, action), CharacterState::LeapMelee(data) => data.handle_event(&j, action), CharacterState::SpinMelee(data) => data.handle_event(&j, action), + CharacterState::ChargedRanged(data) => data.handle_event(&j, action), }; local_emitter.append(&mut state_update.local_events); server_emitter.append(&mut state_update.server_events); @@ -271,6 +272,7 @@ impl<'a> System<'a> for Sys { CharacterState::DashMelee(data) => data.behavior(&j), CharacterState::LeapMelee(data) => data.behavior(&j), CharacterState::SpinMelee(data) => data.behavior(&j), + CharacterState::ChargedRanged(data) => data.behavior(&j), }; local_emitter.append(&mut state_update.local_events); diff --git a/common/src/sys/stats.rs b/common/src/sys/stats.rs index f6621cd34c..3c83d45bb9 100644 --- a/common/src/sys/stats.rs +++ b/common/src/sys/stats.rs @@ -105,7 +105,8 @@ impl<'a> System<'a> for Sys { | CharacterState::LeapMelee { .. } | CharacterState::SpinMelee { .. } | CharacterState::TripleStrike { .. } - | CharacterState::BasicRanged { .. } => { + | CharacterState::BasicRanged { .. } + | CharacterState::ChargedRanged { .. } => { if energy.get_unchecked().regen_rate != 0.0 { energy.get_mut_unchecked().regen_rate = 0.0 } diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 5cbcf39661..279018068d 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -681,6 +681,32 @@ impl FigureMgr { ) } }, + CharacterState::ChargedRanged(data) => { + if data.exhausted { + anim::character::ShootAnimation::update_skeleton( + &target_base, + (active_tool_kind, second_tool_kind, vel.0.magnitude(), time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ) + } else { + anim::character::ChargeAnimation::update_skeleton( + &target_base, + ( + active_tool_kind, + second_tool_kind, + vel.0.magnitude(), + ori, + state.last_ori, + time, + ), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ) + } + }, CharacterState::Boost(_) => { anim::character::AlphaAnimation::update_skeleton( &target_base,