diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 5c3b4ef801..8c56e7af5a 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -1,4 +1,7 @@ -use crate::comp::CharacterState; +use crate::{ + comp::{CharacterState, ToolData}, + states::*, +}; use specs::{Component, DenseVecStorage, FlaggedStorage, HashMapStorage}; use std::time::Duration; @@ -12,8 +15,9 @@ pub enum CharacterAbility { Roll, ChargeAttack, TimedCombo { - /// Amount of energy required to use ability - cost: i32, + tool: ToolData, + buildup_duration: Duration, + recover_duration: Duration, }, } @@ -35,11 +39,11 @@ impl From for CharacterState { CharacterAbility::BasicAttack { buildup_duration, recover_duration, - } => CharacterState::BasicAttack { + } => CharacterState::BasicAttack(basic_attack::State { exhausted: false, buildup_duration, recover_duration, - }, + }), CharacterAbility::BasicBlock { .. } => CharacterState::BasicBlock {}, CharacterAbility::Roll { .. } => CharacterState::Roll { remaining_duration: Duration::from_millis(600), @@ -47,12 +51,18 @@ impl From for CharacterState { CharacterAbility::ChargeAttack { .. } => CharacterState::ChargeAttack { remaining_duration: Duration::from_millis(600), }, - CharacterAbility::TimedCombo { .. } => CharacterState::TimedCombo { - stage: 1, - stage_time_active: Duration::default(), + CharacterAbility::TimedCombo { + tool, + buildup_duration, + recover_duration, + } => CharacterState::TimedCombo(timed_combo::State { + tool, + buildup_duration, + recover_duration, + stage: 0, stage_exhausted: false, - can_transition: false, - }, + stage_time_active: Duration::default(), + }), } } } diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index 4ba4af1e57..fa2460a15b 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -1,6 +1,7 @@ use crate::{ comp::{Energy, Ori, Pos, ToolData, Vel}, event::{LocalEvent, ServerEvent}, + states::*, }; use serde::{Deserialize, Serialize}; use specs::{Component, FlaggedStorage, HashMapStorage, VecStorage}; @@ -33,15 +34,6 @@ pub enum CharacterState { tool: ToolData, }, Glide, - /// A basic attacking state - BasicAttack { - /// How long till the state deals damage - buildup_duration: Duration, - /// How long till the state remains after dealing damage - recover_duration: Duration, - /// Whether the attack can deal more damage - exhausted: bool, - }, /// A basic blocking state BasicBlock, ChargeAttack { @@ -52,18 +44,11 @@ pub enum CharacterState { /// How long the state has until exiting remaining_duration: Duration, }, + /// A basic attacking state + BasicAttack(basic_attack::State), /// A three-stage attack where play must click at appropriate times /// to continue attack chain. - TimedCombo { - /// `int` denoting what stage (of 3) the attack is in. - stage: i8, - /// How long current stage has been active - stage_time_active: Duration, - /// Whether current stage has exhausted its attack - stage_exhausted: bool, - /// Whether player has clicked at the proper time to go to next stage - can_transition: bool, - }, + TimedCombo(timed_combo::State), /// A three-stage attack where each attack pushes player forward /// and successive attacks increase in damage, while player holds button. TripleStrike { @@ -81,16 +66,19 @@ pub enum CharacterState { impl CharacterState { pub fn is_wield(&self) -> bool { match self { - CharacterState::Wielding { .. } => true, - CharacterState::BasicAttack { .. } => true, - CharacterState::BasicBlock { .. } => true, + CharacterState::Wielding { .. } + | CharacterState::BasicAttack(_) + | CharacterState::TimedCombo(_) + | CharacterState::BasicBlock { .. } => true, _ => false, } } pub fn is_attack(&self) -> bool { match self { - CharacterState::BasicAttack { .. } | CharacterState::ChargeAttack { .. } => true, + CharacterState::BasicAttack(_) + | CharacterState::TimedCombo(_) + | CharacterState::ChargeAttack { .. } => true, _ => false, } } diff --git a/common/src/comp/inventory/item.rs b/common/src/comp/inventory/item.rs index e24a107507..52cfd42639 100644 --- a/common/src/comp/inventory/item.rs +++ b/common/src/comp/inventory/item.rs @@ -37,7 +37,11 @@ impl ToolData { use ToolKind::*; match self.kind { - Sword(_) => vec![TimedCombo { cost: -150 }], + Sword(_) => vec![TimedCombo { + buildup_duration: Duration::from_millis(1000), + recover_duration: Duration::from_millis(500), + tool: *self, + }], Axe => vec![BasicAttack { buildup_duration: Duration::from_millis(1000), recover_duration: Duration::from_millis(500), diff --git a/common/src/states/basic_attack.rs b/common/src/states/basic_attack.rs index 782a93f00d..4bf8f67108 100644 --- a/common/src/states/basic_attack.rs +++ b/common/src/states/basic_attack.rs @@ -1,42 +1,50 @@ use crate::{ comp::{Attacking, CharacterState, EnergySource, ItemKind::Tool, StateUpdate}, states::utils::*, - sys::character_behavior::JoinData, + sys::character_behavior::*, }; use std::{collections::VecDeque, time::Duration}; -pub fn behavior(data: &JoinData) -> StateUpdate { - let mut update = StateUpdate { - pos: *data.pos, - vel: *data.vel, - ori: *data.ori, - energy: *data.energy, - character: *data.character, - local_events: VecDeque::new(), - server_events: VecDeque::new(), - }; +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)] +pub struct State { + /// How long until state should deal damage + pub buildup_duration: Duration, + /// How long the state has until exiting + pub recover_duration: Duration, + /// Whether the attack can deal more damage + pub exhausted: bool, +} + +impl CharacterBehavior for State { + fn behavior(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate { + pos: *data.pos, + vel: *data.vel, + ori: *data.ori, + energy: *data.energy, + character: *data.character, + local_events: VecDeque::new(), + server_events: VecDeque::new(), + }; - if let CharacterState::BasicAttack { - exhausted, - buildup_duration, - recover_duration, - } = data.character - { - let tool_kind = data.stats.equipment.main.as_ref().map(|i| i.kind); handle_move(data, &mut update); - if buildup_duration != &Duration::default() { + // Build up window + if self.buildup_duration != Duration::default() { // Start to swing - update.character = CharacterState::BasicAttack { - buildup_duration: buildup_duration + update.character = CharacterState::BasicAttack(State { + buildup_duration: self + .buildup_duration .checked_sub(Duration::from_secs_f32(data.dt.0)) .unwrap_or_default(), - recover_duration: *recover_duration, + recover_duration: self.recover_duration, exhausted: false, - }; - } else if !*exhausted { + }); + } + // Hit attempt window + else if !self.exhausted { // Swing hits - if let Some(Tool(tool)) = tool_kind { + if let Some(tool) = unwrap_tool_data(data) { data.updater.insert(data.entity, Attacking { weapon: Some(tool), applied: false, @@ -44,31 +52,34 @@ pub fn behavior(data: &JoinData) -> StateUpdate { }); } - update.character = CharacterState::BasicAttack { - buildup_duration: *buildup_duration, - recover_duration: *recover_duration, + update.character = CharacterState::BasicAttack(State { + buildup_duration: self.buildup_duration, + recover_duration: self.recover_duration, exhausted: true, - }; - } else if recover_duration != &Duration::default() { - // Recover from swing - update.character = CharacterState::BasicAttack { - buildup_duration: *buildup_duration, - recover_duration: recover_duration + }); + } + // Swing recovery window + else if self.recover_duration != Duration::default() { + update.character = CharacterState::BasicAttack(State { + buildup_duration: self.buildup_duration, + recover_duration: self + .recover_duration .checked_sub(Duration::from_secs_f32(data.dt.0)) .unwrap_or_default(), exhausted: true, - } - } else { - // Done - if let Some(Tool(tool)) = tool_kind { + }); + } + // Done + else { + if let Some(tool) = unwrap_tool_data(data) { update.character = CharacterState::Wielding { tool }; + // Make sure attack component is removed data.updater.remove::(data.entity); } else { update.character = CharacterState::Idle; } } - - // More handling + // Subtract energy on successful hit if let Some(attack) = data.attacking { if attack.applied && attack.hit_count > 0 { data.updater.remove::(data.entity); @@ -76,9 +87,6 @@ pub fn behavior(data: &JoinData) -> StateUpdate { } } - update - } else { - update.character = CharacterState::Idle {}; update } } diff --git a/common/src/states/timed_combo.rs b/common/src/states/timed_combo.rs index 18380a2fc0..1c70c371ac 100644 --- a/common/src/states/timed_combo.rs +++ b/common/src/states/timed_combo.rs @@ -1,98 +1,137 @@ use crate::{ - comp::{Attacking, CharacterState, ItemKind::Tool, StateUpdate}, + comp::{Attacking, CharacterState, EnergySource, StateUpdate, ToolData}, states::utils::*, - sys::character_behavior::JoinData, + sys::character_behavior::{CharacterBehavior, JoinData}, }; use std::{collections::VecDeque, time::Duration}; - -pub fn behavior(data: &JoinData) -> StateUpdate { - let mut update = StateUpdate { - pos: *data.pos, - vel: *data.vel, - ori: *data.ori, - energy: *data.energy, - character: *data.character, - local_events: VecDeque::new(), - server_events: VecDeque::new(), - }; - - if let CharacterState::TimedCombo { - stage, - stage_time_active, - stage_exhausted, - can_transition, - } = data.character - { - // Sorry adam, I don't want to fix this rn, check out basic_attack to - // see how to do it - // - /* - let mut new_can_transition = *can_transition; - let mut new_stage_exhausted = *stage_exhausted; - let new_stage_time_active = stage_time_active - .checked_add(Duration::from_secs_f32(data.dt.0)) - .unwrap_or(Duration::default()); - - match stage { - 1 => { - if new_stage_time_active > tool.attack_buildup_duration() { - if !*stage_exhausted { - // Try to deal damage - data.updater.insert(data.entity, Attacking { - weapon: Some(*tool), - applied: false, - hit_count: 0, - }); - new_stage_exhausted = true; - } else { - // Make sure to remove Attacking component - data.updater.remove::(data.entity); - } - - // Check if player has timed click right - if data.inputs.primary.is_just_pressed() { - println!("Can transition"); - new_can_transition = true; - } - } - - if new_stage_time_active > tool.attack_duration() { - if new_can_transition { - update.character = CharacterState::TimedCombo { - tool: *tool, - stage: 2, - stage_time_active: Duration::default(), - stage_exhausted: false, - can_transition: false, - } - } else { - println!("Failed"); - attempt_wield(data, &mut update); - } - } else { - update.character = CharacterState::TimedCombo { - tool: *tool, - stage: 1, - stage_time_active: new_stage_time_active, - stage_exhausted: new_stage_exhausted, - can_transition: new_can_transition, - } - } - }, - 2 => { - println!("2"); - attempt_wield(data, &mut update); - }, - 3 => { - println!("3"); - attempt_wield(data, &mut update); - }, - _ => { - // Should never get here. - }, - } - */ - } - - update +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)] +pub struct State { + /// Denotes what stage (of 3) the attack is in + pub stage: i8, + /// Whether current stage has exhausted its attack + pub stage_exhausted: bool, + /// How long state waits before it should deal damage + pub buildup_duration: Duration, + /// How long the state waits until exiting + pub recover_duration: Duration, + /// Tracks how long current stage has been active + pub stage_time_active: Duration, + /// `ToolData` to be sent to `Attacking` component + pub tool: ToolData, +} + +impl CharacterBehavior for State { + fn behavior(&self, data: &JoinData) -> StateUpdate { + let mut update = StateUpdate { + pos: *data.pos, + vel: *data.vel, + ori: *data.ori, + energy: *data.energy, + character: *data.character, + local_events: VecDeque::new(), + server_events: VecDeque::new(), + }; + + let new_stage_time_active = self + .stage_time_active + .checked_add(Duration::from_secs_f32(data.dt.0)) + .unwrap_or(Duration::default()); + + println!("Stage {:?}", self.stage); + + if self.stage < 3 { + // Build up window + if new_stage_time_active < self.buildup_duration { + // If the player is pressing primary btn + if data.inputs.primary.is_just_pressed() { + // They failed, go back to `Wielding` + update.character = CharacterState::Wielding { tool: self.tool }; + } + // Keep updating + else { + update.character = CharacterState::TimedCombo(State { + tool: self.tool, + stage: self.stage, + buildup_duration: self.buildup_duration, + recover_duration: self.recover_duration, + stage_exhausted: false, + stage_time_active: new_stage_time_active, + }); + } + } + // Hit attempt window + else if !self.stage_exhausted { + // Swing hits + data.updater.insert(data.entity, Attacking { + weapon: Some(self.tool), + applied: false, + hit_count: 0, + }); + + update.character = CharacterState::TimedCombo(State { + tool: self.tool, + stage: self.stage, + buildup_duration: self.buildup_duration, + recover_duration: self.recover_duration, + stage_exhausted: true, + stage_time_active: new_stage_time_active, + }); + } + // Swing recovery window + else if new_stage_time_active + < self + .buildup_duration + .checked_add(self.recover_duration) + .unwrap_or(Duration::default()) + { + // Try to transition to next stage + if data.inputs.primary.is_just_pressed() { + update.character = CharacterState::TimedCombo(State { + tool: self.tool, + stage: self.stage + 1, + buildup_duration: self.buildup_duration, + recover_duration: self.recover_duration, + stage_exhausted: true, + stage_time_active: Duration::default(), + }); + } + // Player didn't click this frame + else { + // Update state + update.character = CharacterState::TimedCombo(State { + tool: self.tool, + stage: self.stage, + buildup_duration: self.buildup_duration, + recover_duration: self.recover_duration, + stage_exhausted: true, + stage_time_active: new_stage_time_active, + }); + } + } + // Stage expired but missed transition to next stage + else { + // Back to `Wielding` + update.character = CharacterState::Wielding { tool: self.tool }; + // Make sure attack component is removed + data.updater.remove::(data.entity); + } + } + // Made three successful hits! + else { + // Back to `Wielding` + update.character = CharacterState::Wielding { tool: self.tool }; + // Make sure attack component is removed + data.updater.remove::(data.entity); + } + + // Subtract energy on successful hit + if let Some(attack) = data.attacking { + if attack.applied && attack.hit_count > 0 { + data.updater.remove::(data.entity); + update.energy.change_by(100, EnergySource::HitEnemy); + } + } + + update + } } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 3cf8c014b2..d8238700ce 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -1,6 +1,7 @@ use crate::{ comp::{CharacterAbility, CharacterState, EnergySource, ItemKind::Tool, StateUpdate, ToolData}, event::LocalEvent, + states::*, sys::{character_behavior::JoinData, phys::GRAVITY}, }; use std::time::Duration; @@ -236,3 +237,11 @@ pub fn attempt_dodge_ability(data: &JoinData, update: &mut StateUpdate) { update.character = ability.into(); } } + +pub fn unwrap_tool_data(data: &JoinData) -> Option { + if let Some(Tool(tool)) = data.stats.equipment.main.as_ref().map(|i| i.kind) { + Some(tool) + } else { + None + } +} diff --git a/common/src/sys/character_behavior.rs b/common/src/sys/character_behavior.rs index 63380a94df..126e72224c 100644 --- a/common/src/sys/character_behavior.rs +++ b/common/src/sys/character_behavior.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ AbilityPool, Attacking, Body, CharacterState, Controller, ControllerInputs, Energy, - Mounting, Ori, PhysicsState, Pos, Stats, Vel, + Mounting, Ori, PhysicsState, Pos, StateUpdate, Stats, Vel, }, event::{EventBus, LocalEvent, ServerEvent}, state::DeltaTime, @@ -13,6 +13,11 @@ use specs::{Entities, Entity, Join, LazyUpdate, Read, ReadStorage, System, Write // use std::collections::VecDeque; +pub trait CharacterBehavior { + fn behavior(&self, data: &JoinData) -> StateUpdate; + // fn init(data: &JoinData) -> CharacterState; +} + /// Read-Only Data sent from Character Behavior System to bahvior fn's pub struct JoinData<'a> { pub entity: Entity, @@ -173,12 +178,12 @@ impl<'a> System<'a> for Sys { CharacterState::Roll { .. } => states::roll::behavior(&j), CharacterState::Wielding { .. } => states::wielding::behavior(&j), CharacterState::Equipping { .. } => states::equipping::behavior(&j), - CharacterState::BasicAttack { .. } => states::basic_attack::behavior(&j), CharacterState::BasicBlock { .. } => states::basic_block::behavior(&j), CharacterState::ChargeAttack { .. } => states::charge_attack::behavior(&j), CharacterState::Sit { .. } => states::sit::behavior(&j), CharacterState::TripleStrike { .. } => states::triple_strike::behavior(&j), - CharacterState::TimedCombo { .. } => states::timed_combo::behavior(&j), + CharacterState::BasicAttack (state) => state.behavior(&j), + CharacterState::TimedCombo(state) => state.behavior(&j), // Do not use default match. // _ => StateUpdate { diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index dbefe7492a..718339c632 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -454,7 +454,7 @@ impl FigureMgr { skeleton_attr, ) }, - CharacterState::BasicAttack { .. } => { + CharacterState::BasicAttack(_) => { anim::character::AttackAnimation::update_skeleton( &target_base, (active_tool_kind, time), @@ -463,6 +463,22 @@ impl FigureMgr { skeleton_attr, ) }, + CharacterState::TimedCombo(s) => match s.stage { + 0 | 2 => anim::character::AttackAnimation::update_skeleton( + &target_base, + (active_tool_kind, time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ), + _ => anim::character::ChargeAnimation::update_skeleton( + &target_base, + (active_tool_kind, time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ), + }, CharacterState::BasicBlock { .. } => { anim::character::BlockIdleAnimation::update_skeleton( &CharacterSkeleton::new(),