From 63011241ea26680b9f570be53bff0c7839aff25a Mon Sep 17 00:00:00 2001
From: Sam <samuelkeiffer@gmail.com>
Date: Fri, 16 Oct 2020 21:29:14 -0500
Subject: [PATCH 01/10] Added keyframes to all states that were lacking them.

---
 common/src/comp/ability.rs             | 108 +++++----
 common/src/comp/inventory/item/tool.rs | 124 +++++-----
 common/src/loadout_builder.rs          |  10 +-
 common/src/states/basic_melee.rs       | 165 ++++++++-----
 common/src/states/basic_ranged.rs      | 160 ++++++-------
 common/src/states/boost.rs             |  44 ++--
 common/src/states/charged_ranged.rs    | 309 ++++++++++++-------------
 common/src/states/equipping.rs         |  34 ++-
 common/src/states/roll.rs              | 115 +++++++--
 common/src/states/utils.rs             |   6 +-
 voxygen/src/hud/hotbar.rs              |   2 +-
 voxygen/src/hud/skillbar.rs            |  34 +--
 voxygen/src/hud/slots.rs               |   5 +-
 13 files changed, 626 insertions(+), 490 deletions(-)

diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs
index 04cf6e708f..32659deaee 100644
--- a/common/src/comp/ability.rs
+++ b/common/src/comp/ability.rs
@@ -59,16 +59,16 @@ pub enum CharacterAbility {
     BasicMelee {
         energy_cost: u32,
         buildup_duration: Duration,
+        swing_duration: Duration,
         recover_duration: Duration,
-        base_healthchange: i32,
+        base_damage: u32,
         knockback: f32,
         range: f32,
         max_angle: f32,
     },
     BasicRanged {
         energy_cost: u32,
-        holdable: bool,
-        prepare_duration: Duration,
+        buildup_duration: Duration,
         recover_duration: Duration,
         projectile: Projectile,
         projectile_body: Body,
@@ -91,7 +91,7 @@ pub enum CharacterAbility {
         reps_remaining: u32,
     },
     Boost {
-        duration: Duration,
+        movement_duration: Duration,
         only_up: bool,
     },
     DashMelee {
@@ -169,7 +169,7 @@ pub enum CharacterAbility {
         max_damage: u32,
         initial_knockback: f32,
         max_knockback: f32,
-        prepare_duration: Duration,
+        buildup_duration: Duration,
         charge_duration: Duration,
         recover_duration: Duration,
         projectile_body: Body,
@@ -356,24 +356,29 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
         match ability {
             CharacterAbility::BasicMelee {
                 buildup_duration,
+                swing_duration,
                 recover_duration,
-                base_healthchange,
+                base_damage,
                 knockback,
                 range,
                 max_angle,
                 energy_cost: _,
             } => CharacterState::BasicMelee(basic_melee::Data {
+                static_data: basic_melee::StaticData {
+                    buildup_duration: *buildup_duration,
+                    swing_duration: *swing_duration,
+                    recover_duration: *recover_duration,
+                    base_damage: *base_damage,
+                    knockback: *knockback,
+                    range: *range,
+                    max_angle: *max_angle,
+                },
+                timer: Duration::default(),
+                stage_section: StageSection::Buildup,
                 exhausted: false,
-                buildup_duration: *buildup_duration,
-                recover_duration: *recover_duration,
-                base_healthchange: *base_healthchange,
-                knockback: *knockback,
-                range: *range,
-                max_angle: *max_angle,
             }),
             CharacterAbility::BasicRanged {
-                holdable,
-                prepare_duration,
+                buildup_duration,
                 recover_duration,
                 projectile,
                 projectile_body,
@@ -382,21 +387,29 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
                 projectile_speed,
                 energy_cost: _,
             } => CharacterState::BasicRanged(basic_ranged::Data {
+                static_data: basic_ranged::StaticData {
+                    buildup_duration: *buildup_duration,
+                    recover_duration: *recover_duration,
+                    projectile: projectile.clone(),
+                    projectile_body: *projectile_body,
+                    projectile_light: *projectile_light,
+                    projectile_gravity: *projectile_gravity,
+                    projectile_speed: *projectile_speed,
+                    ability_key: key,
+                },
+                timer: Duration::default(),
+                stage_section: StageSection::Buildup,
                 exhausted: false,
-                prepare_timer: Duration::default(),
-                holdable: *holdable,
-                prepare_duration: *prepare_duration,
-                recover_duration: *recover_duration,
-                projectile: projectile.clone(),
-                projectile_body: *projectile_body,
-                projectile_light: *projectile_light,
-                projectile_gravity: *projectile_gravity,
-                projectile_speed: *projectile_speed,
-                ability_key: key,
             }),
-            CharacterAbility::Boost { duration, only_up } => CharacterState::Boost(boost::Data {
-                duration: *duration,
-                only_up: *only_up,
+            CharacterAbility::Boost {
+                movement_duration,
+                only_up,
+            } => CharacterState::Boost(boost::Data {
+                static_data: boost::StaticData {
+                    movement_duration: *movement_duration,
+                    only_up: *only_up,
+                },
+                timer: Duration::default(),
             }),
             CharacterAbility::DashMelee {
                 energy_cost: _,
@@ -438,7 +451,13 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
             }),
             CharacterAbility::BasicBlock => CharacterState::BasicBlock,
             CharacterAbility::Roll => CharacterState::Roll(roll::Data {
-                remaining_duration: Duration::from_millis(500),
+                static_data: roll::StaticData {
+                    buildup_duration: Duration::from_millis(100),
+                    movement_duration: Duration::from_millis(300),
+                    recover_duration: Duration::from_millis(100),
+                },
+                timer: Duration::default(),
+                stage_section: StageSection::Buildup,
                 was_wielded: false, // false by default. utils might set it to true
             }),
             CharacterAbility::ComboMelee {
@@ -566,7 +585,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
                 max_damage,
                 initial_knockback,
                 max_knockback,
-                prepare_duration,
+                buildup_duration,
                 charge_duration,
                 recover_duration,
                 projectile_body,
@@ -575,21 +594,24 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
                 initial_projectile_speed,
                 max_projectile_speed,
             } => CharacterState::ChargedRanged(charged_ranged::Data {
+                static_data: charged_ranged::StaticData {
+                    buildup_duration: *buildup_duration,
+                    charge_duration: *charge_duration,
+                    recover_duration: *recover_duration,
+                    energy_drain: *energy_drain,
+                    initial_damage: *initial_damage,
+                    max_damage: *max_damage,
+                    initial_knockback: *initial_knockback,
+                    max_knockback: *max_knockback,
+                    projectile_body: *projectile_body,
+                    projectile_light: *projectile_light,
+                    projectile_gravity: *projectile_gravity,
+                    initial_projectile_speed: *initial_projectile_speed,
+                    max_projectile_speed: *max_projectile_speed,
+                },
+                timer: Duration::default(),
+                stage_section: StageSection::Buildup,
                 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,
-                projectile_gravity: *projectile_gravity,
-                initial_projectile_speed: *initial_projectile_speed,
-                max_projectile_speed: *max_projectile_speed,
             }),
             CharacterAbility::RepeaterRanged {
                 energy_cost: _,
diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs
index 9e847bce6d..4cc50a926d 100644
--- a/common/src/comp/inventory/item/tool.rs
+++ b/common/src/comp/inventory/item/tool.rs
@@ -206,9 +206,10 @@ impl Tool {
             Axe(_) => vec![
                 BasicMelee {
                     energy_cost: 0,
-                    buildup_duration: Duration::from_millis(700),
+                    buildup_duration: Duration::from_millis(600),
+                    swing_duration: Duration::from_millis(100),
                     recover_duration: Duration::from_millis(300),
-                    base_healthchange: (-120.0 * self.base_power()) as i32,
+                    base_damage: (120.0 * self.base_power()) as u32,
                     knockback: 0.0,
                     range: 3.5,
                     max_angle: 20.0,
@@ -244,9 +245,10 @@ impl Tool {
             Hammer(_) => vec![
                 BasicMelee {
                     energy_cost: 0,
-                    buildup_duration: Duration::from_millis(700),
+                    buildup_duration: Duration::from_millis(600),
+                    swing_duration: Duration::from_millis(100),
                     recover_duration: Duration::from_millis(300),
-                    base_healthchange: (-120.0 * self.base_power()) as i32,
+                    base_damage: (120.0 * self.base_power()) as u32,
                     knockback: 0.0,
                     range: 3.5,
                     max_angle: 20.0,
@@ -280,9 +282,10 @@ impl Tool {
             ],
             Farming(_) => vec![BasicMelee {
                 energy_cost: 1,
-                buildup_duration: Duration::from_millis(700),
+                buildup_duration: Duration::from_millis(600),
+                swing_duration: Duration::from_millis(100),
                 recover_duration: Duration::from_millis(150),
-                base_healthchange: (-50.0 * self.base_power()) as i32,
+                base_damage: (50.0 * self.base_power()) as u32,
                 knockback: 0.0,
                 range: 3.5,
                 max_angle: 20.0,
@@ -290,8 +293,7 @@ impl Tool {
             Bow(_) => vec![
                 BasicRanged {
                     energy_cost: 0,
-                    holdable: true,
-                    prepare_duration: Duration::from_millis(100),
+                    buildup_duration: Duration::from_millis(100),
                     recover_duration: Duration::from_millis(400),
                     projectile: Projectile {
                         hit_solid: vec![projectile::Effect::Stick],
@@ -317,7 +319,7 @@ impl Tool {
                     max_damage: (200.0 * self.base_power()) as u32,
                     initial_knockback: 10.0,
                     max_knockback: 20.0,
-                    prepare_duration: Duration::from_millis(100),
+                    buildup_duration: Duration::from_millis(100),
                     charge_duration: Duration::from_millis(1500),
                     recover_duration: Duration::from_millis(500),
                     projectile_body: Body::Object(object::Body::MultiArrow),
@@ -355,8 +357,9 @@ impl Tool {
             Dagger(_) => vec![BasicMelee {
                 energy_cost: 0,
                 buildup_duration: Duration::from_millis(100),
-                recover_duration: Duration::from_millis(400),
-                base_healthchange: (-50.0 * self.base_power()) as i32,
+                swing_duration: Duration::from_millis(100),
+                recover_duration: Duration::from_millis(300),
+                base_damage: (50.0 * self.base_power()) as u32,
                 knockback: 0.0,
                 range: 3.5,
                 max_angle: 20.0,
@@ -378,8 +381,7 @@ impl Tool {
                 },
                 BasicRanged {
                     energy_cost: 800,
-                    holdable: true,
-                    prepare_duration: Duration::from_millis(800),
+                    buildup_duration: Duration::from_millis(800),
                     recover_duration: Duration::from_millis(50),
                     projectile: Projectile {
                         hit_solid: vec![
@@ -422,8 +424,7 @@ impl Tool {
             Staff(_) => vec![
                 BasicRanged {
                     energy_cost: 0,
-                    holdable: false,
-                    prepare_duration: Duration::from_millis(500),
+                    buildup_duration: Duration::from_millis(500),
                     recover_duration: Duration::from_millis(350),
                     projectile: Projectile {
                         hit_solid: vec![
@@ -495,8 +496,9 @@ impl Tool {
                 BasicMelee {
                     energy_cost: 0,
                     buildup_duration: Duration::from_millis(100),
-                    recover_duration: Duration::from_millis(400),
-                    base_healthchange: (-40.0 * self.base_power()) as i32,
+                    swing_duration: Duration::from_millis(100),
+                    recover_duration: Duration::from_millis(300),
+                    base_damage: (40.0 * self.base_power()) as u32,
                     knockback: 0.0,
                     range: 3.0,
                     max_angle: 120.0,
@@ -508,10 +510,11 @@ impl Tool {
                     vec![
                         BasicMelee {
                             energy_cost: 0,
-                            buildup_duration: Duration::from_millis(500),
+                            buildup_duration: Duration::from_millis(400),
+                            swing_duration: Duration::from_millis(100),
                             recover_duration: Duration::from_millis(250),
                             knockback: 25.0,
-                            base_healthchange: -200,
+                            base_damage: 200,
                             range: 5.0,
                             max_angle: 120.0,
                         },
@@ -533,10 +536,11 @@ impl Tool {
                 } else if kind == "BeastClaws" {
                     vec![BasicMelee {
                         energy_cost: 0,
-                        buildup_duration: Duration::from_millis(500),
+                        buildup_duration: Duration::from_millis(250),
+                        swing_duration: Duration::from_millis(250),
                         recover_duration: Duration::from_millis(250),
                         knockback: 25.0,
-                        base_healthchange: -200,
+                        base_damage: 200,
                         range: 5.0,
                         max_angle: 120.0,
                     }]
@@ -544,58 +548,50 @@ impl Tool {
                     vec![BasicMelee {
                         energy_cost: 0,
                         buildup_duration: Duration::from_millis(100),
-                        recover_duration: Duration::from_millis(300),
-                        base_healthchange: -10,
+                        swing_duration: Duration::from_millis(100),
+                        recover_duration: Duration::from_millis(200),
+                        base_damage: 10,
                         knockback: 0.0,
                         range: 1.0,
                         max_angle: 30.0,
                     }]
                 }
             },
-            Debug(kind) => {
-                if kind == "Boost" {
-                    vec![
-                        CharacterAbility::Boost {
-                            duration: Duration::from_millis(50),
-                            only_up: false,
-                        },
-                        CharacterAbility::Boost {
-                            duration: Duration::from_millis(50),
-                            only_up: true,
-                        },
-                        BasicRanged {
-                            energy_cost: 0,
-                            holdable: false,
-                            prepare_duration: Duration::from_millis(0),
-                            recover_duration: Duration::from_millis(10),
-                            projectile: Projectile {
-                                hit_solid: vec![projectile::Effect::Stick],
-                                hit_entity: vec![
-                                    projectile::Effect::Stick,
-                                    projectile::Effect::Possess,
-                                ],
-                                time_left: Duration::from_secs(10),
-                                owner: None,
-                                ignore_group: false,
-                            },
-                            projectile_body: Body::Object(object::Body::ArrowSnake),
-                            projectile_light: Some(LightEmitter {
-                                col: (0.0, 1.0, 0.33).into(),
-                                ..Default::default()
-                            }),
-                            projectile_gravity: None,
-                            projectile_speed: 100.0,
-                        },
-                    ]
-                } else {
-                    vec![]
-                }
-            },
+            Debug(_) => vec![
+                CharacterAbility::Boost {
+                    movement_duration: Duration::from_millis(50),
+                    only_up: false,
+                },
+                CharacterAbility::Boost {
+                    movement_duration: Duration::from_millis(50),
+                    only_up: true,
+                },
+                BasicRanged {
+                    energy_cost: 0,
+                    buildup_duration: Duration::from_millis(0),
+                    recover_duration: Duration::from_millis(10),
+                    projectile: Projectile {
+                        hit_solid: vec![projectile::Effect::Stick],
+                        hit_entity: vec![projectile::Effect::Stick, projectile::Effect::Possess],
+                        time_left: Duration::from_secs(10),
+                        owner: None,
+                        ignore_group: false,
+                    },
+                    projectile_body: Body::Object(object::Body::ArrowSnake),
+                    projectile_light: Some(LightEmitter {
+                        col: (0.0, 1.0, 0.33).into(),
+                        ..Default::default()
+                    }),
+                    projectile_gravity: None,
+                    projectile_speed: 100.0,
+                },
+            ],
             Empty => vec![BasicMelee {
                 energy_cost: 0,
                 buildup_duration: Duration::from_millis(0),
-                recover_duration: Duration::from_millis(1000),
-                base_healthchange: -20,
+                swing_duration: Duration::from_millis(100),
+                recover_duration: Duration::from_millis(900),
+                base_damage: 20,
                 knockback: 0.0,
                 range: 3.5,
                 max_angle: 15.0,
diff --git a/common/src/loadout_builder.rs b/common/src/loadout_builder.rs
index 3fcb1c2fbb..5511a7826d 100644
--- a/common/src/loadout_builder.rs
+++ b/common/src/loadout_builder.rs
@@ -163,8 +163,9 @@ impl LoadoutBuilder {
                 ability1: Some(CharacterAbility::BasicMelee {
                     energy_cost: 0,
                     buildup_duration: Duration::from_millis(0),
-                    recover_duration: Duration::from_millis(400),
-                    base_healthchange: -40,
+                    swing_duration: Duration::from_millis(100),
+                    recover_duration: Duration::from_millis(300),
+                    base_damage: 40,
                     knockback: 0.0,
                     range: 3.5,
                     max_angle: 15.0,
@@ -343,9 +344,10 @@ impl LoadoutBuilder {
                 item: Item::new_from_asset_expect("common.items.weapons.empty.empty"),
                 ability1: Some(CharacterAbility::BasicMelee {
                     energy_cost: 10,
-                    buildup_duration: Duration::from_millis(600),
+                    buildup_duration: Duration::from_millis(500),
+                    swing_duration: Duration::from_millis(100),
                     recover_duration: Duration::from_millis(100),
-                    base_healthchange: -(body.base_dmg() as i32),
+                    base_damage: body.base_dmg(),
                     knockback: 0.0,
                     range: body.base_range(),
                     max_angle: 20.0,
diff --git a/common/src/states/basic_melee.rs b/common/src/states/basic_melee.rs
index 0d98e0aeac..dc3f6a4041 100644
--- a/common/src/states/basic_melee.rs
+++ b/common/src/states/basic_melee.rs
@@ -6,20 +6,34 @@ use crate::{
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
 
+/// Separated out to condense update portions of character state
 #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
-pub struct Data {
+pub struct StaticData {
     /// How long until state should deal damage
     pub buildup_duration: Duration,
+    /// How long the state is swinging for
+    pub swing_duration: Duration,
     /// How long the state has until exiting
     pub recover_duration: Duration,
-    /// Base damage (negative) or healing (positive)
-    pub base_healthchange: i32,
+    /// Base damage
+    pub base_damage: u32,
     /// Knockback
     pub knockback: f32,
     /// Max range
     pub range: f32,
     /// Max angle (45.0 will give you a 90.0 angle window)
     pub max_angle: f32,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Data {
+    /// Struct containing data that does not change over the course of the
+    /// character state
+    pub static_data: StaticData,
+    /// Timer for each stage
+    pub timer: Duration,
+    /// What section the character stage is in
+    pub stage_section: StageSection,
     /// Whether the attack can deal more damage
     pub exhausted: bool,
 }
@@ -31,65 +45,94 @@ impl CharacterBehavior for Data {
         handle_move(data, &mut update, 0.7);
         handle_jump(data, &mut update);
 
-        if self.buildup_duration != Duration::default() {
-            // Build up
-            update.character = CharacterState::BasicMelee(Data {
-                buildup_duration: self
-                    .buildup_duration
-                    .checked_sub(Duration::from_secs_f32(data.dt.0))
-                    .unwrap_or_default(),
-                recover_duration: self.recover_duration,
-                base_healthchange: self.base_healthchange,
-                knockback: self.knockback,
-                range: self.range,
-                max_angle: self.max_angle,
-                exhausted: false,
-            });
-        } else if !self.exhausted {
-            let (damage, heal) = if self.base_healthchange > 0 {
-                (0, self.base_healthchange as u32)
-            } else {
-                ((-self.base_healthchange) as u32, 0)
-            };
-            // Hit attempt
-            data.updater.insert(data.entity, Attacking {
-                base_damage: damage,
-                base_heal: heal,
-                range: self.range,
-                max_angle: self.max_angle.to_radians(),
-                applied: false,
-                hit_count: 0,
-                knockback: self.knockback,
-            });
+        match self.stage_section {
+            StageSection::Buildup => {
+                if self.timer < self.static_data.buildup_duration {
+                    // Build up
+                    update.character = CharacterState::BasicMelee(Data {
+                        static_data: self.static_data,
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        exhausted: self.exhausted,
+                    });
+                } else {
+                    // Transitions to swing section of stage
+                    update.character = CharacterState::BasicMelee(Data {
+                        static_data: self.static_data,
+                        timer: Duration::default(),
+                        stage_section: StageSection::Swing,
+                        exhausted: self.exhausted,
+                    });
+                }
+            },
+            StageSection::Swing => {
+                if !self.exhausted {
+                    update.character = CharacterState::BasicMelee(Data {
+                        static_data: self.static_data,
+                        timer: Duration::default(),
+                        stage_section: self.stage_section,
+                        exhausted: true,
+                    });
 
-            update.character = CharacterState::BasicMelee(Data {
-                buildup_duration: self.buildup_duration,
-                recover_duration: self.recover_duration,
-                base_healthchange: self.base_healthchange,
-                knockback: self.knockback,
-                range: self.range,
-                max_angle: self.max_angle,
-                exhausted: true,
-            });
-        } else if self.recover_duration != Duration::default() {
-            // Recovery
-            update.character = CharacterState::BasicMelee(Data {
-                buildup_duration: self.buildup_duration,
-                recover_duration: self
-                    .recover_duration
-                    .checked_sub(Duration::from_secs_f32(data.dt.0))
-                    .unwrap_or_default(),
-                base_healthchange: self.base_healthchange,
-                knockback: self.knockback,
-                range: self.range,
-                max_angle: self.max_angle,
-                exhausted: true,
-            });
-        } else {
-            // Done
-            update.character = CharacterState::Wielding;
-            // Make sure attack component is removed
-            data.updater.remove::<Attacking>(data.entity);
+                    // Hit attempt
+                    data.updater.insert(data.entity, Attacking {
+                        base_damage: self.static_data.base_damage,
+                        base_heal: 0,
+                        range: self.static_data.range,
+                        max_angle: 180_f32.to_radians(),
+                        applied: false,
+                        hit_count: 0,
+                        knockback: self.static_data.knockback,
+                    });
+                } else if self.timer < self.static_data.swing_duration {
+                    // Swings
+                    update.character = CharacterState::BasicMelee(Data {
+                        static_data: self.static_data,
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        exhausted: self.exhausted,
+                    });
+                } else {
+                    // Transitions to recover section of stage
+                    update.character = CharacterState::BasicMelee(Data {
+                        static_data: self.static_data,
+                        timer: Duration::default(),
+                        stage_section: StageSection::Recover,
+                        exhausted: self.exhausted,
+                    });
+                }
+            },
+            StageSection::Recover => {
+                if self.timer < self.static_data.recover_duration {
+                    // Recovery
+                    update.character = CharacterState::BasicMelee(Data {
+                        static_data: self.static_data,
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        exhausted: self.exhausted,
+                    });
+                } else {
+                    // Done
+                    update.character = CharacterState::Wielding;
+                    // Make sure attack component is removed
+                    data.updater.remove::<Attacking>(data.entity);
+                }
+            },
+            _ => {
+                // If it somehow ends up in an incorrect stage section
+                update.character = CharacterState::Wielding;
+                // Make sure attack component is removed
+                data.updater.remove::<Attacking>(data.entity);
+            },
         }
 
         // Grant energy on successful hit
diff --git a/common/src/states/basic_ranged.rs b/common/src/states/basic_ranged.rs
index cbfc6afc7a..bb0950e605 100644
--- a/common/src/states/basic_ranged.rs
+++ b/common/src/states/basic_ranged.rs
@@ -7,27 +7,36 @@ use crate::{
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
 
+/// Separated out to condense update portions of character state
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
-pub struct Data {
-    /// Can you hold the ability beyond the prepare duration
-    pub holdable: bool,
-    /// How long we have to prepare the weapon
-    pub prepare_duration: Duration,
-    /// How long we prepared the weapon already
-    pub prepare_timer: Duration,
+pub struct StaticData {
+    /// How much buildup is required before the attack
+    pub buildup_duration: Duration,
     /// How long the state has until exiting
     pub recover_duration: Duration,
+    /// Projectile variables
     pub projectile: Projectile,
     pub projectile_body: Body,
     pub projectile_light: Option<LightEmitter>,
     pub projectile_gravity: Option<Gravity>,
     pub projectile_speed: f32,
-    /// Whether the attack fired already
-    pub exhausted: bool,
     /// What key is used to press ability
     pub ability_key: AbilityKey,
 }
 
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Data {
+    /// Struct containing data that does not change over the course of the
+    /// character state
+    pub static_data: StaticData,
+    /// Timer for each stage
+    pub timer: Duration,
+    /// What section the character stage is in
+    pub stage_section: StageSection,
+    /// Whether the attack fired already
+    pub exhausted: bool,
+}
+
 impl CharacterBehavior for Data {
     fn behavior(&self, data: &JoinData) -> StateUpdate {
         let mut update = StateUpdate::from(data);
@@ -35,77 +44,70 @@ impl CharacterBehavior for Data {
         handle_move(data, &mut update, 0.3);
         handle_jump(data, &mut update);
 
-        if !self.exhausted
-            && if self.holdable {
-                ability_key_is_pressed(data, self.ability_key)
-                    || self.prepare_timer < self.prepare_duration
-            } else {
-                self.prepare_timer < self.prepare_duration
-            }
-        {
-            // Prepare (draw the bow)
-            update.character = CharacterState::BasicRanged(Data {
-                prepare_timer: self.prepare_timer + Duration::from_secs_f32(data.dt.0),
-                holdable: self.holdable,
-                prepare_duration: self.prepare_duration,
-                recover_duration: self.recover_duration,
-                projectile: self.projectile.clone(),
-                projectile_body: self.projectile_body,
-                projectile_light: self.projectile_light,
-                projectile_gravity: self.projectile_gravity,
-                projectile_speed: self.projectile_speed,
-                exhausted: false,
-                ability_key: self.ability_key,
-            });
-        } else if !self.exhausted {
-            // Fire
-            let mut projectile = self.projectile.clone();
-            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: self.projectile_gravity,
-                speed: self.projectile_speed,
-            });
+        match self.stage_section {
+            StageSection::Buildup => {
+                if self.timer < self.static_data.buildup_duration {
+                    // Build up
+                    update.character = CharacterState::BasicRanged(Data {
+                        static_data: self.static_data.clone(),
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        exhausted: self.exhausted,
+                    });
+                } else {
+                    // Transitions to recover section of stage
+                    update.character = CharacterState::BasicRanged(Data {
+                        static_data: self.static_data.clone(),
+                        timer: Duration::default(),
+                        stage_section: StageSection::Recover,
+                        exhausted: self.exhausted,
+                    });
+                }
+            },
+            StageSection::Recover => {
+                if !self.exhausted {
+                    // Fire
+                    let mut projectile = self.static_data.projectile.clone();
+                    projectile.owner = Some(*data.uid);
+                    update.server_events.push_front(ServerEvent::Shoot {
+                        entity: data.entity,
+                        dir: data.inputs.look_dir,
+                        body: self.static_data.projectile_body,
+                        projectile,
+                        light: self.static_data.projectile_light,
+                        gravity: self.static_data.projectile_gravity,
+                        speed: self.static_data.projectile_speed,
+                    });
 
-            update.character = CharacterState::BasicRanged(Data {
-                prepare_timer: self.prepare_timer,
-                holdable: self.holdable,
-                prepare_duration: self.prepare_duration,
-                recover_duration: self.recover_duration,
-                projectile: self.projectile.clone(),
-                projectile_body: self.projectile_body,
-                projectile_light: self.projectile_light,
-                projectile_gravity: self.projectile_gravity,
-                projectile_speed: self.projectile_speed,
-                exhausted: true,
-                ability_key: self.ability_key,
-            });
-        } else if self.recover_duration != Duration::default() {
-            // Recovery
-            update.character = CharacterState::BasicRanged(Data {
-                prepare_timer: self.prepare_timer,
-                holdable: self.holdable,
-                prepare_duration: self.prepare_duration,
-                recover_duration: self
-                    .recover_duration
-                    .checked_sub(Duration::from_secs_f32(data.dt.0))
-                    .unwrap_or_default(),
-                projectile: self.projectile.clone(),
-                projectile_body: self.projectile_body,
-                projectile_light: self.projectile_light,
-                projectile_gravity: self.projectile_gravity,
-                projectile_speed: self.projectile_speed,
-                exhausted: true,
-                ability_key: self.ability_key,
-            });
-            return update;
-        } else {
-            // Done
-            update.character = CharacterState::Wielding;
+                    update.character = CharacterState::BasicRanged(Data {
+                        static_data: self.static_data.clone(),
+                        timer: self.timer,
+                        stage_section: self.stage_section,
+                        exhausted: true,
+                    });
+                } else if self.timer < self.static_data.recover_duration {
+                    // Recovers
+                    update.character = CharacterState::BasicRanged(Data {
+                        static_data: self.static_data.clone(),
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        exhausted: self.exhausted,
+                    });
+                } else {
+                    // Done
+                    update.character = CharacterState::Wielding;
+                }
+            },
+            _ => {
+                // If it somehow ends up in an incorrect stage section
+                update.character = CharacterState::Wielding;
+            },
         }
 
         update
diff --git a/common/src/states/boost.rs b/common/src/states/boost.rs
index 52f1994152..564a714c57 100644
--- a/common/src/states/boost.rs
+++ b/common/src/states/boost.rs
@@ -1,16 +1,25 @@
 use crate::{
-    comp::{Attacking, CharacterState, EnergySource, StateUpdate},
+    comp::{CharacterState, StateUpdate},
     states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
 
+/// Separated out to condense update portions of character state
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct StaticData {
+    pub movement_duration: Duration,
+    pub only_up: bool,
+}
+
 #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
 pub struct Data {
-    /// How long the state has until exiting
-    pub duration: Duration,
-    pub only_up: bool,
+    /// Struct containing data that does not change over the course of the
+    /// character state
+    pub static_data: StaticData,
+    /// Timer for each stage
+    pub timer: Duration,
 }
 
 impl CharacterBehavior for Data {
@@ -19,34 +28,25 @@ impl CharacterBehavior for Data {
 
         handle_move(data, &mut update, 1.0);
 
-        // Still going
-        if self.duration != Duration::default() {
-            if self.only_up {
+        if self.timer < self.static_data.movement_duration {
+            // Movement
+            if self.static_data.only_up {
                 update.vel.0.z += 500.0 * data.dt.0;
             } else {
                 update.vel.0 += *data.inputs.look_dir * 500.0 * data.dt.0;
             }
             update.character = CharacterState::Boost(Data {
-                duration: self
-                    .duration
-                    .checked_sub(Duration::from_secs_f32(data.dt.0))
+                static_data: self.static_data,
+                timer: self
+                    .timer
+                    .checked_add(Duration::from_secs_f32(data.dt.0))
                     .unwrap_or_default(),
-                only_up: self.only_up,
             });
-        }
-        // Done
-        else {
+        } else {
+            // Done
             update.character = CharacterState::Wielding;
         }
 
-        // Grant energy on successful hit
-        if let Some(attack) = data.attacking {
-            if attack.applied && attack.hit_count > 0 {
-                data.updater.remove::<Attacking>(data.entity);
-                update.energy.change_by(100, EnergySource::HitEnemy);
-            }
-        }
-
         update
     }
 }
diff --git a/common/src/states/charged_ranged.rs b/common/src/states/charged_ranged.rs
index 1386b8b4d5..5413eaa386 100644
--- a/common/src/states/charged_ranged.rs
+++ b/common/src/states/charged_ranged.rs
@@ -10,10 +10,15 @@ use crate::{
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
 
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
-pub struct Data {
-    /// Whether the attack fired already
-    pub exhausted: bool,
+/// Separated out to condense update portions of character state
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct StaticData {
+    /// How long the weapon needs to be prepared for
+    pub buildup_duration: Duration,
+    /// How long it takes to charge the weapon to max damage and knockback
+    pub charge_duration: Duration,
+    /// How long the state has until exiting
+    pub recover_duration: Duration,
     /// How much energy is drained per second when charging
     pub energy_drain: u32,
     /// How much damage is dealt with no charge
@@ -24,14 +29,6 @@ pub struct Data {
     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<LightEmitter>,
@@ -40,6 +37,19 @@ pub struct Data {
     pub max_projectile_speed: f32,
 }
 
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Data {
+    /// Struct containing data that does not change over the course of the
+    /// character state
+    pub static_data: StaticData,
+    /// Timer for each stage
+    pub timer: Duration,
+    /// What section the character stage is in
+    pub stage_section: StageSection,
+    /// Whether the attack fired already
+    pub exhausted: bool,
+}
+
 impl CharacterBehavior for Data {
     fn behavior(&self, data: &JoinData) -> StateUpdate {
         let mut update = StateUpdate::from(data);
@@ -47,160 +57,133 @@ impl CharacterBehavior for 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,
-                projectile_gravity: self.projectile_gravity,
-                initial_projectile_speed: self.initial_projectile_speed,
-                max_projectile_speed: self.max_projectile_speed,
-            });
-        } 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,
-                projectile_gravity: self.projectile_gravity,
-                initial_projectile_speed: self.initial_projectile_speed,
-                max_projectile_speed: self.max_projectile_speed,
-            });
+        match self.stage_section {
+            StageSection::Buildup => {
+                if self.timer < self.static_data.buildup_duration {
+                    // Build up
+                    update.character = CharacterState::ChargedRanged(Data {
+                        static_data: self.static_data,
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        exhausted: self.exhausted,
+                    });
+                } else {
+                    // Transitions to swing section of stage
+                    update.character = CharacterState::ChargedRanged(Data {
+                        static_data: self.static_data,
+                        timer: Duration::default(),
+                        stage_section: StageSection::Charge,
+                        exhausted: self.exhausted,
+                    });
+                }
+            },
+            StageSection::Charge => {
+                if !data.inputs.secondary.is_pressed() && !self.exhausted {
+                    let charge_frac = (self.timer.as_secs_f32()
+                        / self.static_data.charge_duration.as_secs_f32())
+                    .min(1.0);
+                    let damage = self.static_data.initial_damage as f32
+                        + (charge_frac
+                            * (self.static_data.max_damage - self.static_data.initial_damage)
+                                as f32);
+                    let knockback = self.static_data.initial_knockback as f32
+                        + (charge_frac
+                            * (self.static_data.max_knockback - self.static_data.initial_knockback)
+                                as f32);
+                    // Fire
+                    let mut projectile = Projectile {
+                        hit_solid: vec![projectile::Effect::Stick],
+                        hit_entity: vec![
+                            projectile::Effect::Damage(-damage as i32),
+                            projectile::Effect::Knockback(knockback),
+                            projectile::Effect::Vanish,
+                        ],
+                        time_left: Duration::from_secs(15),
+                        owner: None,
+                        ignore_group: true,
+                    };
+                    projectile.owner = Some(*data.uid);
+                    update.server_events.push_front(ServerEvent::Shoot {
+                        entity: data.entity,
+                        dir: data.inputs.look_dir,
+                        body: self.static_data.projectile_body,
+                        projectile,
+                        light: self.static_data.projectile_light,
+                        gravity: self.static_data.projectile_gravity,
+                        speed: self.static_data.initial_projectile_speed
+                            + charge_frac
+                                * (self.static_data.max_projectile_speed
+                                    - self.static_data.initial_projectile_speed),
+                    });
 
-            // 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,
-                projectile_gravity: self.projectile_gravity,
-                initial_projectile_speed: self.initial_projectile_speed,
-                max_projectile_speed: self.max_projectile_speed,
-            });
+                    update.character = CharacterState::ChargedRanged(Data {
+                        static_data: self.static_data,
+                        timer: Duration::default(),
+                        stage_section: StageSection::Recover,
+                        exhausted: true,
+                    });
+                } else if self.timer < self.static_data.charge_duration
+                    && data.inputs.secondary.is_pressed()
+                {
+                    // Charges
+                    update.character = CharacterState::ChargedRanged(Data {
+                        static_data: self.static_data,
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        exhausted: self.exhausted,
+                    });
 
-            // 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,
-                ignore_group: true,
-            };
-            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: self.projectile_gravity,
-                speed: self.initial_projectile_speed
-                    + charge_amount * (self.max_projectile_speed - self.initial_projectile_speed),
-            });
+                    // Consumes energy if there's enough left and RMB is held down
+                    update.energy.change_by(
+                        -(self.static_data.energy_drain as f32 * data.dt.0) as i32,
+                        EnergySource::Ability,
+                    );
+                } else if data.inputs.secondary.is_pressed() {
+                    // Holds charge
+                    update.character = CharacterState::ChargedRanged(Data {
+                        static_data: self.static_data,
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        exhausted: self.exhausted,
+                    });
 
-            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,
-                projectile_gravity: self.projectile_gravity,
-                initial_projectile_speed: self.initial_projectile_speed,
-                max_projectile_speed: self.max_projectile_speed,
-            });
-        } 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,
-                projectile_gravity: self.projectile_gravity,
-                initial_projectile_speed: self.initial_projectile_speed,
-                max_projectile_speed: self.max_projectile_speed,
-            });
-        } else {
-            // Done
-            update.character = CharacterState::Wielding;
+                    // Consumes energy if there's enough left and RMB is held down
+                    update.energy.change_by(
+                        -(self.static_data.energy_drain as f32 * data.dt.0 / 5.0) as i32,
+                        EnergySource::Ability,
+                    );
+                }
+            },
+            StageSection::Recover => {
+                if self.timer < self.static_data.recover_duration {
+                    // Recovers
+                    update.character = CharacterState::ChargedRanged(Data {
+                        static_data: self.static_data,
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        exhausted: self.exhausted,
+                    });
+                } else {
+                    // Done
+                    update.character = CharacterState::Wielding;
+                }
+            },
+            _ => {
+                // If it somehow ends up in an incorrect stage section
+                update.character = CharacterState::Wielding;
+            },
         }
 
         update
diff --git a/common/src/states/equipping.rs b/common/src/states/equipping.rs
index ebc91062ff..b0e63c11ea 100644
--- a/common/src/states/equipping.rs
+++ b/common/src/states/equipping.rs
@@ -6,10 +6,20 @@ use crate::{
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
 
-#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)]
+/// Separated out to condense update portions of character state
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct StaticData {
+    /// Time required to draw weapon
+    pub buildup_duration: Duration,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
 pub struct Data {
-    /// Time left before next state
-    pub time_left: Duration,
+    /// Struct containing data that does not change over the course of the
+    /// character state
+    pub static_data: StaticData,
+    /// Timer for each stage
+    pub timer: Duration,
 }
 
 impl CharacterBehavior for Data {
@@ -19,18 +29,18 @@ impl CharacterBehavior for Data {
         handle_move(&data, &mut update, 1.0);
         handle_jump(&data, &mut update);
 
-        if self.time_left == Duration::default() {
-            // Wield delay has expired
-            update.character = CharacterState::Wielding;
-        } else {
-            // Wield delay hasn't expired yet
-            // Update wield delay
+        if self.timer < self.static_data.buildup_duration {
+            // Draw weapon
             update.character = CharacterState::Equipping(Data {
-                time_left: self
-                    .time_left
-                    .checked_sub(Duration::from_secs_f32(data.dt.0))
+                static_data: self.static_data,
+                timer: self
+                    .timer
+                    .checked_add(Duration::from_secs_f32(data.dt.0))
                     .unwrap_or_default(),
             });
+        } else {
+            // Done
+            update.character = CharacterState::Wielding;
         }
 
         update
diff --git a/common/src/states/roll.rs b/common/src/states/roll.rs
index 3dc7887e7f..2a49f44445 100644
--- a/common/src/states/roll.rs
+++ b/common/src/states/roll.rs
@@ -1,5 +1,6 @@
 use crate::{
     comp::{CharacterState, StateUpdate},
+    states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
     util::Dir,
 };
@@ -8,10 +9,27 @@ use std::time::Duration;
 use vek::Vec3;
 
 const ROLL_SPEED: f32 = 25.0;
-#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)]
+
+/// Separated out to condense update portions of character state
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct StaticData {
+    /// How long until state should roll
+    pub buildup_duration: Duration,
+    /// How long state is rolling for
+    pub movement_duration: Duration,
+    /// How long it takes to recover from roll
+    pub recover_duration: Duration,
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
 pub struct Data {
-    /// How long the state has until exiting
-    pub remaining_duration: Duration,
+    /// Struct containing data that does not change over the course of the
+    /// character state
+    pub static_data: StaticData,
+    /// Timer for each stage
+    pub timer: Duration,
+    /// What section the character stage is in
+    pub stage_section: StageSection,
     /// Had weapon
     pub was_wielded: bool,
 }
@@ -31,23 +49,80 @@ impl CharacterBehavior for Data {
         // Smooth orientation
         update.ori.0 = Dir::slerp_to_vec3(update.ori.0, update.vel.0.xy().into(), 9.0 * data.dt.0);
 
-        if self.remaining_duration == Duration::default() {
-            // Roll duration has expired
-            update.vel.0 *= 0.3;
-            if self.was_wielded {
-                update.character = CharacterState::Wielding;
-            } else {
-                update.character = CharacterState::Idle;
-            }
-        } else {
-            // Otherwise, tick down remaining_duration
-            update.character = CharacterState::Roll(Data {
-                remaining_duration: self
-                    .remaining_duration
-                    .checked_sub(Duration::from_secs_f32(data.dt.0))
-                    .unwrap_or_default(),
-                was_wielded: self.was_wielded,
-            });
+        match self.stage_section {
+            StageSection::Buildup => {
+                if self.timer < self.static_data.buildup_duration {
+                    // Build up
+                    update.character = CharacterState::Roll(Data {
+                        static_data: self.static_data,
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        was_wielded: self.was_wielded,
+                    });
+                } else {
+                    // Transitions to movement section of stage
+                    update.character = CharacterState::Roll(Data {
+                        static_data: self.static_data,
+                        timer: Duration::default(),
+                        stage_section: StageSection::Movement,
+                        was_wielded: self.was_wielded,
+                    });
+                }
+            },
+            StageSection::Movement => {
+                if self.timer < self.static_data.movement_duration {
+                    // Movement
+                    update.character = CharacterState::Roll(Data {
+                        static_data: self.static_data,
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        was_wielded: self.was_wielded,
+                    });
+                } else {
+                    // Transitions to recover section of stage
+                    update.character = CharacterState::Roll(Data {
+                        static_data: self.static_data,
+                        timer: Duration::default(),
+                        stage_section: StageSection::Recover,
+                        was_wielded: self.was_wielded,
+                    });
+                }
+            },
+            StageSection::Recover => {
+                if self.timer < self.static_data.recover_duration {
+                    // Build up
+                    update.character = CharacterState::Roll(Data {
+                        static_data: self.static_data,
+                        timer: self
+                            .timer
+                            .checked_add(Duration::from_secs_f32(data.dt.0))
+                            .unwrap_or_default(),
+                        stage_section: self.stage_section,
+                        was_wielded: self.was_wielded,
+                    });
+                } else {
+                    // Done
+                    if self.was_wielded {
+                        update.character = CharacterState::Wielding;
+                    } else {
+                        update.character = CharacterState::Idle;
+                    }
+                }
+            },
+            _ => {
+                // If it somehow ends up in an incorrect stage section
+                if self.was_wielded {
+                    update.character = CharacterState::Wielding;
+                } else {
+                    update.character = CharacterState::Idle;
+                }
+            },
         }
 
         update
diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs
index c0b71b7b5c..13e0f50269 100644
--- a/common/src/states/utils.rs
+++ b/common/src/states/utils.rs
@@ -9,6 +9,7 @@ use crate::{
     util::Dir,
 };
 use serde::{Deserialize, Serialize};
+use std::time::Duration;
 use vek::*;
 
 pub const MOVEMENT_THRESHOLD_VEL: f32 = 3.0;
@@ -161,7 +162,10 @@ pub fn handle_wield(data: &JoinData, update: &mut StateUpdate) {
 pub fn attempt_wield(data: &JoinData, update: &mut StateUpdate) {
     if let Some(ItemKind::Tool(tool)) = data.loadout.active_item.as_ref().map(|i| i.item.kind()) {
         update.character = CharacterState::Equipping(equipping::Data {
-            time_left: tool.equip_time(),
+            static_data: equipping::StaticData {
+                buildup_duration: tool.equip_time(),
+            },
+            timer: Duration::default(),
         });
     } else {
         update.character = CharacterState::Idle;
diff --git a/voxygen/src/hud/hotbar.rs b/voxygen/src/hud/hotbar.rs
index e17245c2a8..7be45e7de9 100644
--- a/voxygen/src/hud/hotbar.rs
+++ b/voxygen/src/hud/hotbar.rs
@@ -79,7 +79,7 @@ impl State {
                     if let ItemKind::Tool(kind) = kind {
                         match &kind.kind {
                             ToolKind::Staff(_) => true,
-                            ToolKind::Debug(kind) => kind == "Boost",
+                            ToolKind::Debug(_) => true,
                             ToolKind::Sword(_) => true,
                             ToolKind::Hammer(_) => true,
                             ToolKind::Axe(_) => true,
diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs
index 172671622e..ad8045517d 100644
--- a/voxygen/src/hud/skillbar.rs
+++ b/voxygen/src/hud/skillbar.rs
@@ -524,6 +524,14 @@ impl<'a> Widget for Skillbar<'a> {
                         .map(|i| i.item.kind())
                         .and_then(|kind| match kind {
                             ItemKind::Tool(Tool { kind, .. }) => match kind {
+                                ToolKind::Hammer(_) => Some((
+                                    "Smash of Doom",
+                                    "\nAn AOE attack with knockback. \nLeaps to position of \
+                                     cursor.",
+                                )),
+                                ToolKind::Axe(_) => {
+                                    Some(("Spin Leap", "\nA slashing running spin leap."))
+                                },
                                 ToolKind::Staff(_) => Some((
                                     "Firebomb",
                                     "\nWhirls a big fireball into the air. \nExplodes the ground \
@@ -533,14 +541,14 @@ impl<'a> Widget for Skillbar<'a> {
                                     "Whirlwind",
                                     "\nMove forward while spinning with \n your sword.",
                                 )),
-                                ToolKind::Debug(kind) => match kind.as_ref() {
-                                    "Boost" => Some((
-                                        "Possessing Arrow",
-                                        "\nShoots a poisonous arrow.\nLets you control your \
-                                         target.",
-                                    )),
-                                    _ => None,
-                                },
+                                ToolKind::Bow(_) => Some((
+                                    "Burst",
+                                    "\nLaunches a burst of arrows at the top \nof a running leap.",
+                                )),
+                                ToolKind::Debug(_) => Some((
+                                    "Possessing Arrow",
+                                    "\nShoots a poisonous arrow.\nLets you control your target.",
+                                )),
                                 _ => None,
                             },
                             _ => None,
@@ -621,10 +629,7 @@ impl<'a> Widget for Skillbar<'a> {
                     ToolKind::Bow(_) => self.imgs.bow_m1,
                     ToolKind::Sceptre(_) => self.imgs.heal_0,
                     ToolKind::Staff(_) => self.imgs.fireball,
-                    ToolKind::Debug(kind) => match kind.as_ref() {
-                        "Boost" => self.imgs.flyingrod_m1,
-                        _ => self.imgs.nothing,
-                    },
+                    ToolKind::Debug(_) => self.imgs.flyingrod_m1,
                     _ => self.imgs.nothing,
                 },
                 _ => self.imgs.nothing,
@@ -671,10 +676,7 @@ impl<'a> Widget for Skillbar<'a> {
             Some(ToolKind::Bow(_)) => self.imgs.bow_m2,
             Some(ToolKind::Sceptre(_)) => self.imgs.heal_bomb,
             Some(ToolKind::Staff(_)) => self.imgs.flamethrower,
-            Some(ToolKind::Debug(kind)) => match kind.as_ref() {
-                "Boost" => self.imgs.flyingrod_m2,
-                _ => self.imgs.nothing,
-            },
+            Some(ToolKind::Debug(_)) => self.imgs.flyingrod_m2,
             _ => self.imgs.nothing,
         })
         .w_h(36.0, 36.0)
diff --git a/voxygen/src/hud/slots.rs b/voxygen/src/hud/slots.rs
index ab31c28cda..56bd3dda56 100644
--- a/voxygen/src/hud/slots.rs
+++ b/voxygen/src/hud/slots.rs
@@ -116,10 +116,7 @@ impl<'a> SlotKey<HotbarSource<'a>, HotbarImageSource<'a>> for HotbarSlot {
                             ToolKind::Hammer(_) => Some(HotbarImage::HammerLeap),
                             ToolKind::Axe(_) => Some(HotbarImage::AxeLeapSlash),
                             ToolKind::Bow(_) => Some(HotbarImage::BowJumpBurst),
-                            ToolKind::Debug(kind) => match kind.as_ref() {
-                                "Boost" => Some(HotbarImage::SnakeArrow),
-                                _ => None,
-                            },
+                            ToolKind::Debug(_) => Some(HotbarImage::SnakeArrow),
                             ToolKind::Sword(_) => Some(HotbarImage::SwordWhirlwind),
                             _ => None,
                         },

From 555bc559f5519a12d63c6ecd04e4a5096d924444 Mon Sep 17 00:00:00 2001
From: Sam <samuelkeiffer@gmail.com>
Date: Sat, 17 Oct 2020 11:11:53 -0500
Subject: [PATCH 02/10] Axe no longer sets vertical velocity to 0, but instead
 preserves vertical velocity.

---
 common/src/states/spin_melee.rs | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/common/src/states/spin_melee.rs b/common/src/states/spin_melee.rs
index 8c97b99c0a..28c07f43fd 100644
--- a/common/src/states/spin_melee.rs
+++ b/common/src/states/spin_melee.rs
@@ -1,7 +1,10 @@
 use crate::{
     comp::{Attacking, CharacterState, EnergySource, StateUpdate},
     states::utils::*,
-    sys::character_behavior::{CharacterBehavior, JoinData},
+    sys::{
+        character_behavior::{CharacterBehavior, JoinData},
+        phys::GRAVITY,
+    },
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -56,7 +59,8 @@ impl CharacterBehavior for Data {
         let mut update = StateUpdate::from(data);
 
         if self.static_data.is_helicopter {
-            update.vel.0 = Vec3::new(data.inputs.move_dir.x, data.inputs.move_dir.y, 0.0) * 5.0;
+            update.vel.0 = Vec3::new(0.0, 0.0, update.vel.0.z + GRAVITY * data.dt.0)
+                + data.inputs.move_dir * 5.0;
         }
 
         // Allows for other states to interrupt this state

From 15286a094a2cda17544f5a734d6218507b596b42 Mon Sep 17 00:00:00 2001
From: Sam <samuelkeiffer@gmail.com>
Date: Sat, 17 Oct 2020 11:29:15 -0500
Subject: [PATCH 03/10] Leaps now require a non-negative vertical velocity to
 use.

---
 common/src/comp/ability.rs | 24 ++++++++++++++++--------
 1 file changed, 16 insertions(+), 8 deletions(-)

diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs
index 32659deaee..51844340a2 100644
--- a/common/src/comp/ability.rs
+++ b/common/src/comp/ability.rs
@@ -234,10 +234,13 @@ impl CharacterAbility {
                 .energy
                 .try_change_by(-(*energy_cost as i32), EnergySource::Ability)
                 .is_ok(),
-            CharacterAbility::LeapMelee { energy_cost, .. } => update
-                .energy
-                .try_change_by(-(*energy_cost as i32), EnergySource::Ability)
-                .is_ok(),
+            CharacterAbility::LeapMelee { energy_cost, .. } => {
+                update.vel.0.z >= 0.0
+                    && update
+                        .energy
+                        .try_change_by(-(*energy_cost as i32), EnergySource::Ability)
+                        .is_ok()
+            },
             CharacterAbility::SpinMelee { energy_cost, .. } => update
                 .energy
                 .try_change_by(-(*energy_cost as i32), EnergySource::Ability)
@@ -250,10 +253,15 @@ impl CharacterAbility {
                 .energy
                 .try_change_by(-(*energy_cost as i32), EnergySource::Ability)
                 .is_ok(),
-            CharacterAbility::RepeaterRanged { energy_cost, .. } => update
-                .energy
-                .try_change_by(-(*energy_cost as i32), EnergySource::Ability)
-                .is_ok(),
+            CharacterAbility::RepeaterRanged {
+                energy_cost, leap, ..
+            } => {
+                (leap.is_none() || update.vel.0.z >= 0.0)
+                    && update
+                        .energy
+                        .try_change_by(-(*energy_cost as i32), EnergySource::Ability)
+                        .is_ok()
+            },
             CharacterAbility::Shockwave { energy_cost, .. } => update
                 .energy
                 .try_change_by(-(*energy_cost as i32), EnergySource::Ability)

From 844e6f2b60a8290429eacff78bacda4cc13213e8 Mon Sep 17 00:00:00 2001
From: Sam <samuelkeiffer@gmail.com>
Date: Sat, 17 Oct 2020 11:39:25 -0500
Subject: [PATCH 04/10] Slightly nerfed sword dash. Reduced particle count on
 fire aoe by factor of 3.

---
 common/src/comp/inventory/item/tool.rs | 4 ++--
 voxygen/src/scene/particle.rs          | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs
index 4cc50a926d..85ed41c40e 100644
--- a/common/src/comp/inventory/item/tool.rs
+++ b/common/src/comp/inventory/item/tool.rs
@@ -173,8 +173,8 @@ impl Tool {
                 },
                 DashMelee {
                     energy_cost: 200,
-                    base_damage: (120.0 * self.base_power()) as u32,
-                    max_damage: (260.0 * self.base_power()) as u32,
+                    base_damage: (100.0 * self.base_power()) as u32,
+                    max_damage: (250.0 * self.base_power()) as u32,
                     base_knockback: 10.0,
                     max_knockback: 20.0,
                     range: 5.0,
diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs
index e9002961a5..e757a727f6 100644
--- a/voxygen/src/scene/particle.rs
+++ b/voxygen/src/scene/particle.rs
@@ -594,8 +594,8 @@ impl ParticleMgr {
                         ));
                     }
                 } else {
-                    for d in 0..10 * distance as i32 {
-                        let arc_position = theta - radians / 2.0 + dtheta * d as f32 / 10.0;
+                    for d in 0..3 * distance as i32 {
+                        let arc_position = theta - radians / 2.0 + dtheta * d as f32 / 3.0;
 
                         let position = pos.0
                             + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0);

From a7e3e55a1299e531e51ca7244e0465535648b795 Mon Sep 17 00:00:00 2001
From: Sam <samuelkeiffer@gmail.com>
Date: Sat, 17 Oct 2020 17:42:43 -0500
Subject: [PATCH 05/10] Transitioned damage and healing from u32/i32s to enums.

---
 common/src/combat.rs                     | 161 +++++++++++++++++++++++
 common/src/comp/beam.rs                  |   5 +-
 common/src/comp/character_state.rs       |   4 +-
 common/src/comp/damage.rs                |  79 -----------
 common/src/comp/inventory/item/tool.rs   |  12 +-
 common/src/comp/mod.rs                   |   2 -
 common/src/comp/projectile.rs            |   4 +-
 common/src/comp/shockwave.rs             |   4 +-
 common/src/lib.rs                        |   2 +
 common/src/metrics.rs                    |   2 +-
 common/src/states/basic_beam.rs          |  14 +-
 common/src/states/basic_melee.rs         |   7 +-
 common/src/states/charged_melee.rs       |  10 +-
 common/src/states/charged_ranged.rs      |   6 +-
 common/src/states/combo_melee.rs         |  13 +-
 common/src/states/dash_melee.rs          |   4 +-
 common/src/states/leap_melee.rs          |   7 +-
 common/src/states/shockwave.rs           |   6 +-
 common/src/states/spin_melee.rs          |   7 +-
 common/src/sys/beam.rs                   |  57 +++-----
 common/src/sys/{combat.rs => melee.rs}   |  52 +++-----
 common/src/sys/mod.rs                    |   6 +-
 common/src/sys/projectile.rs             |  78 +++++------
 common/src/sys/shockwave.rs              |  39 ++----
 server/src/events/entity_manipulation.rs |  63 +++------
 server/src/lib.rs                        |  10 +-
 26 files changed, 331 insertions(+), 323 deletions(-)
 create mode 100644 common/src/combat.rs
 delete mode 100644 common/src/comp/damage.rs
 rename common/src/sys/{combat.rs => melee.rs} (74%)

diff --git a/common/src/combat.rs b/common/src/combat.rs
new file mode 100644
index 0000000000..3702d2e978
--- /dev/null
+++ b/common/src/combat.rs
@@ -0,0 +1,161 @@
+use crate::{
+    comp::{HealthChange, HealthSource, Loadout},
+    sync::Uid,
+};
+use serde::{Deserialize, Serialize};
+
+pub const BLOCK_EFFICIENCY: f32 = 0.9;
+
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct Damages {
+    pub enemy: Option<Damage>,
+    pub group: Option<Damage>,
+}
+
+impl Damages {
+    pub fn new(enemy: Option<Damage>, group: Option<Damage>) -> Self { Damages { enemy, group } }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub enum Damage {
+    Melee(f32),
+    Healing(f32),
+    Projectile(f32),
+    Explosion(f32),
+    Falling(f32),
+    Shockwave(f32),
+    Energy(f32),
+}
+
+impl Damage {
+    pub fn modify_damage(
+        self,
+        block: bool,
+        loadout: Option<&Loadout>,
+        uid: Option<Uid>,
+    ) -> HealthChange {
+        match self {
+            Damage::Melee(damage) => {
+                let mut damage = damage;
+                // Critical hit
+                let mut critdamage = 0.0;
+                if rand::random() {
+                    critdamage = damage * 0.3;
+                }
+                // Block
+                if block {
+                    damage *= 1.0 - BLOCK_EFFICIENCY
+                }
+                // Armor
+                let damage_reduction = if let Some(loadout) = loadout {
+                    loadout.get_damage_reduction()
+                } else {
+                    0.0
+                };
+                damage *= 1.0 - damage_reduction;
+
+                // Critical damage applies after armor for melee
+                if (damage_reduction - 1.0).abs() > f32::EPSILON {
+                    damage += critdamage;
+                }
+
+                HealthChange {
+                    amount: -damage as i32,
+                    cause: HealthSource::Attack { by: uid.unwrap() },
+                }
+            },
+            Damage::Projectile(damage) => {
+                let mut damage = damage;
+                // Critical hit
+                if rand::random() {
+                    damage *= 1.2;
+                }
+                // Block
+                if block {
+                    damage *= 1.0 - BLOCK_EFFICIENCY
+                }
+                // Armor
+                let damage_reduction = if let Some(loadout) = loadout {
+                    loadout.get_damage_reduction()
+                } else {
+                    0.0
+                };
+                damage *= 1.0 - damage_reduction;
+
+                HealthChange {
+                    amount: -damage as i32,
+                    cause: HealthSource::Projectile { owner: uid },
+                }
+            },
+            Damage::Explosion(damage) => {
+                let mut damage = damage;
+                // Block
+                if block {
+                    damage *= 1.0 - BLOCK_EFFICIENCY
+                }
+                // Armor
+                let damage_reduction = if let Some(loadout) = loadout {
+                    loadout.get_damage_reduction()
+                } else {
+                    0.0
+                };
+                damage *= 1.0 - damage_reduction;
+
+                HealthChange {
+                    amount: -damage as i32,
+                    cause: HealthSource::Explosion { owner: uid },
+                }
+            },
+            Damage::Shockwave(damage) => {
+                let mut damage = damage;
+                // Armor
+                let damage_reduction = if let Some(loadout) = loadout {
+                    loadout.get_damage_reduction()
+                } else {
+                    0.0
+                };
+                damage *= 1.0 - damage_reduction;
+
+                HealthChange {
+                    amount: -damage as i32,
+                    cause: HealthSource::Attack { by: uid.unwrap() },
+                }
+            },
+            Damage::Energy(damage) => {
+                let mut damage = damage;
+                // Armor
+                let damage_reduction = if let Some(loadout) = loadout {
+                    loadout.get_damage_reduction()
+                } else {
+                    0.0
+                };
+                damage *= 1.0 - damage_reduction;
+
+                HealthChange {
+                    amount: -damage as i32,
+                    cause: HealthSource::Energy { owner: uid },
+                }
+            },
+            Damage::Healing(heal) => HealthChange {
+                amount: heal as i32,
+                cause: HealthSource::Healing { by: uid },
+            },
+            Damage::Falling(damage) => {
+                let mut damage = damage;
+                // Armor
+                let damage_reduction = if let Some(loadout) = loadout {
+                    loadout.get_damage_reduction()
+                } else {
+                    0.0
+                };
+                if (damage_reduction - 1.0).abs() < f32::EPSILON {
+                    damage = 0.0;
+                }
+                HealthChange {
+                    amount: -damage as i32,
+                    cause: HealthSource::World,
+                }
+            },
+        }
+    }
+}
diff --git a/common/src/comp/beam.rs b/common/src/comp/beam.rs
index 1a0d311ea4..2c15b7fede 100644
--- a/common/src/comp/beam.rs
+++ b/common/src/comp/beam.rs
@@ -1,4 +1,4 @@
-use crate::sync::Uid;
+use crate::{sync::Uid, Damages};
 use serde::{Deserialize, Serialize};
 use specs::{Component, FlaggedStorage};
 use specs_idvs::IdvStorage;
@@ -8,8 +8,7 @@ use std::time::Duration;
 pub struct Properties {
     pub angle: f32,
     pub speed: f32,
-    pub damage: u32,
-    pub heal: u32,
+    pub damages: Damages,
     pub lifesteal_eff: f32,
     pub energy_regen: u32,
     pub energy_cost: u32,
diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs
index 53ecc899c3..7b459b60ee 100644
--- a/common/src/comp/character_state.rs
+++ b/common/src/comp/character_state.rs
@@ -3,6 +3,7 @@ use crate::{
     event::{LocalEvent, ServerEvent},
     states::*,
     sys::character_behavior::JoinData,
+    Damages,
 };
 use serde::{Deserialize, Serialize};
 use specs::{Component, FlaggedStorage, VecStorage};
@@ -152,8 +153,7 @@ impl Component for CharacterState {
 
 #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
 pub struct Attacking {
-    pub base_damage: u32,
-    pub base_heal: u32,
+    pub damages: Damages,
     pub range: f32,
     pub max_angle: f32,
     pub applied: bool,
diff --git a/common/src/comp/damage.rs b/common/src/comp/damage.rs
deleted file mode 100644
index 6fae9d3e71..0000000000
--- a/common/src/comp/damage.rs
+++ /dev/null
@@ -1,79 +0,0 @@
-use crate::comp::Loadout;
-use serde::{Deserialize, Serialize};
-
-pub const BLOCK_EFFICIENCY: f32 = 0.9;
-
-pub struct Damage {
-    pub healthchange: f32,
-    pub source: DamageSource,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
-pub enum DamageSource {
-    Melee,
-    Healing,
-    Projectile,
-    Explosion,
-    Falling,
-    Shockwave,
-    Energy,
-}
-
-impl Damage {
-    pub fn modify_damage(&mut self, block: bool, loadout: &Loadout) {
-        match self.source {
-            DamageSource::Melee => {
-                // Critical hit
-                let mut critdamage = 0.0;
-                if rand::random() {
-                    critdamage = self.healthchange * 0.3;
-                }
-                // Block
-                if block {
-                    self.healthchange *= 1.0 - BLOCK_EFFICIENCY
-                }
-                // Armor
-                let damage_reduction = loadout.get_damage_reduction();
-                self.healthchange *= 1.0 - damage_reduction;
-
-                // Critical damage applies after armor for melee
-                if (damage_reduction - 1.0).abs() > f32::EPSILON {
-                    self.healthchange += critdamage;
-                }
-            },
-            DamageSource::Projectile => {
-                // Critical hit
-                if rand::random() {
-                    self.healthchange *= 1.2;
-                }
-                // Block
-                if block {
-                    self.healthchange *= 1.0 - BLOCK_EFFICIENCY
-                }
-                // Armor
-                let damage_reduction = loadout.get_damage_reduction();
-                self.healthchange *= 1.0 - damage_reduction;
-            },
-            DamageSource::Explosion => {
-                // Block
-                if block {
-                    self.healthchange *= 1.0 - BLOCK_EFFICIENCY
-                }
-                // Armor
-                let damage_reduction = loadout.get_damage_reduction();
-                self.healthchange *= 1.0 - damage_reduction;
-            },
-            DamageSource::Shockwave => {
-                // Armor
-                let damage_reduction = loadout.get_damage_reduction();
-                self.healthchange *= 1.0 - damage_reduction;
-            },
-            DamageSource::Energy => {
-                // Armor
-                let damage_reduction = loadout.get_damage_reduction();
-                self.healthchange *= 1.0 - damage_reduction;
-            },
-            _ => {},
-        }
-    }
-}
diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs
index 85ed41c40e..f8e37ce635 100644
--- a/common/src/comp/inventory/item/tool.rs
+++ b/common/src/comp/inventory/item/tool.rs
@@ -4,7 +4,7 @@
 use crate::{
     comp::{body::object, projectile, Body, CharacterAbility, Gravity, LightEmitter, Projectile},
     states::combo_melee,
-    Explosion,
+    Damage, Damages, Explosion,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -298,7 +298,10 @@ impl Tool {
                     projectile: Projectile {
                         hit_solid: vec![projectile::Effect::Stick],
                         hit_entity: vec![
-                            projectile::Effect::Damage((-40.0 * self.base_power()) as i32),
+                            projectile::Effect::Damages(Damages::new(
+                                Some(Damage::Projectile(40.0 * self.base_power())),
+                                None,
+                            )),
                             projectile::Effect::Knockback(10.0),
                             projectile::Effect::RewardEnergy(50),
                             projectile::Effect::Vanish,
@@ -338,7 +341,10 @@ impl Tool {
                     projectile: Projectile {
                         hit_solid: vec![projectile::Effect::Stick],
                         hit_entity: vec![
-                            projectile::Effect::Damage((-40.0 * self.base_power()) as i32),
+                            projectile::Effect::Damages(Damages::new(
+                                Some(Damage::Projectile(40.0 * self.base_power())),
+                                None,
+                            )),
                             projectile::Effect::Knockback(10.0),
                             projectile::Effect::RewardEnergy(50),
                             projectile::Effect::Vanish,
diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs
index 329546a54e..3aa9556d99 100644
--- a/common/src/comp/mod.rs
+++ b/common/src/comp/mod.rs
@@ -7,7 +7,6 @@ pub mod buff;
 mod character_state;
 pub mod chat;
 mod controller;
-mod damage;
 mod energy;
 pub mod group;
 mod inputs;
@@ -44,7 +43,6 @@ pub use controller::{
     Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input,
     InventoryManip, MountState, Mounting,
 };
-pub use damage::{Damage, DamageSource};
 pub use energy::{Energy, EnergySource};
 pub use group::Group;
 pub use inputs::CanBuild;
diff --git a/common/src/comp/projectile.rs b/common/src/comp/projectile.rs
index 6241af9c01..121a6bdbb9 100644
--- a/common/src/comp/projectile.rs
+++ b/common/src/comp/projectile.rs
@@ -1,4 +1,4 @@
-use crate::{sync::Uid, Explosion};
+use crate::{sync::Uid, Damages, Explosion};
 use serde::{Deserialize, Serialize};
 use specs::{Component, FlaggedStorage};
 use specs_idvs::IdvStorage;
@@ -6,7 +6,7 @@ use std::time::Duration;
 
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
 pub enum Effect {
-    Damage(i32),
+    Damages(Damages),
     Knockback(f32),
     RewardEnergy(u32),
     Explode(Explosion),
diff --git a/common/src/comp/shockwave.rs b/common/src/comp/shockwave.rs
index 78e38e44bf..15161bc673 100644
--- a/common/src/comp/shockwave.rs
+++ b/common/src/comp/shockwave.rs
@@ -1,4 +1,4 @@
-use crate::sync::Uid;
+use crate::{sync::Uid, Damages};
 use serde::{Deserialize, Serialize};
 use specs::{Component, FlaggedStorage};
 use specs_idvs::IdvStorage;
@@ -9,7 +9,7 @@ pub struct Properties {
     pub angle: f32,
     pub vertical_angle: f32,
     pub speed: f32,
-    pub damage: u32,
+    pub damages: Damages,
     pub knockback: f32,
     pub requires_ground: bool,
     pub duration: Duration,
diff --git a/common/src/lib.rs b/common/src/lib.rs
index 4ea0d8e30c..407328bd66 100644
--- a/common/src/lib.rs
+++ b/common/src/lib.rs
@@ -22,6 +22,7 @@ pub mod astar;
 pub mod character;
 pub mod clock;
 pub mod cmd;
+pub mod combat;
 pub mod comp;
 pub mod effect;
 pub mod event;
@@ -51,5 +52,6 @@ pub mod util;
 pub mod vol;
 pub mod volumes;
 
+pub use combat::{Damage, Damages};
 pub use explosion::Explosion;
 pub use loadout_builder::LoadoutBuilder;
diff --git a/common/src/metrics.rs b/common/src/metrics.rs
index 672f13d7c5..e2ec3ec27e 100644
--- a/common/src/metrics.rs
+++ b/common/src/metrics.rs
@@ -9,5 +9,5 @@ pub struct SysMetrics {
     pub stats_ns: AtomicI64,
     pub phys_ns: AtomicI64,
     pub projectile_ns: AtomicI64,
-    pub combat_ns: AtomicI64,
+    pub melee_ns: AtomicI64,
 }
diff --git a/common/src/states/basic_beam.rs b/common/src/states/basic_beam.rs
index e215fae41f..37c84d6264 100644
--- a/common/src/states/basic_beam.rs
+++ b/common/src/states/basic_beam.rs
@@ -4,6 +4,7 @@ use crate::{
     states::utils::*,
     sync::Uid,
     sys::character_behavior::{CharacterBehavior, JoinData},
+    Damage, Damages,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -108,10 +109,12 @@ impl CharacterBehavior for Data {
                 if ability_key_is_pressed(data, self.static_data.ability_key)
                     && (self.static_data.energy_drain == 0 || update.energy.current() > 0)
                 {
-                    let damage =
-                        (self.static_data.base_dps as f32 / self.static_data.tick_rate) as u32;
-                    let heal =
-                        (self.static_data.base_hps as f32 / self.static_data.tick_rate) as u32;
+                    let damage = Damage::Energy(
+                        self.static_data.base_dps as f32 / self.static_data.tick_rate,
+                    );
+                    let heal = Damage::Healing(
+                        self.static_data.base_hps as f32 / self.static_data.tick_rate,
+                    );
                     let energy_regen =
                         (self.static_data.energy_regen as f32 / self.static_data.tick_rate) as u32;
                     let energy_cost =
@@ -121,8 +124,7 @@ impl CharacterBehavior for Data {
                     let properties = beam::Properties {
                         angle: self.static_data.max_angle.to_radians(),
                         speed,
-                        damage,
-                        heal,
+                        damages: Damages::new(Some(damage), Some(heal)),
                         lifesteal_eff: self.static_data.lifesteal_eff,
                         energy_regen,
                         energy_cost,
diff --git a/common/src/states/basic_melee.rs b/common/src/states/basic_melee.rs
index dc3f6a4041..e99ec7a03d 100644
--- a/common/src/states/basic_melee.rs
+++ b/common/src/states/basic_melee.rs
@@ -2,6 +2,7 @@ use crate::{
     comp::{Attacking, CharacterState, EnergySource, StateUpdate},
     states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
+    Damage, Damages,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -79,8 +80,10 @@ impl CharacterBehavior for Data {
 
                     // Hit attempt
                     data.updater.insert(data.entity, Attacking {
-                        base_damage: self.static_data.base_damage,
-                        base_heal: 0,
+                        damages: Damages::new(
+                            Some(Damage::Melee(self.static_data.base_damage as f32)),
+                            None,
+                        ),
                         range: self.static_data.range,
                         max_angle: 180_f32.to_radians(),
                         applied: false,
diff --git a/common/src/states/charged_melee.rs b/common/src/states/charged_melee.rs
index e266fe99bf..62f5b3e3fe 100644
--- a/common/src/states/charged_melee.rs
+++ b/common/src/states/charged_melee.rs
@@ -2,6 +2,7 @@ use crate::{
     comp::{Attacking, CharacterState, EnergySource, StateUpdate},
     states::utils::{StageSection, *},
     sys::character_behavior::*,
+    Damage, Damages,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -115,17 +116,16 @@ impl CharacterBehavior for Data {
             },
             StageSection::Swing => {
                 if !self.exhausted {
-                    let damage = self.static_data.initial_damage
-                        + ((self.static_data.max_damage - self.static_data.initial_damage) as f32
-                            * self.charge_amount) as u32;
+                    let damage = self.static_data.initial_damage as f32
+                        + (self.static_data.max_damage - self.static_data.initial_damage) as f32
+                            * self.charge_amount;
                     let knockback = self.static_data.initial_knockback
                         + (self.static_data.max_knockback - self.static_data.initial_knockback)
                             * self.charge_amount;
 
                     // Hit attempt
                     data.updater.insert(data.entity, Attacking {
-                        base_damage: damage as u32,
-                        base_heal: 0,
+                        damages: Damages::new(Some(Damage::Melee(damage)), None),
                         range: self.static_data.range,
                         max_angle: self.static_data.max_angle.to_radians(),
                         applied: false,
diff --git a/common/src/states/charged_ranged.rs b/common/src/states/charged_ranged.rs
index 5413eaa386..5805338bc8 100644
--- a/common/src/states/charged_ranged.rs
+++ b/common/src/states/charged_ranged.rs
@@ -6,6 +6,7 @@ use crate::{
     event::ServerEvent,
     states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
+    Damage, Damages,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -97,7 +98,10 @@ impl CharacterBehavior for Data {
                     let mut projectile = Projectile {
                         hit_solid: vec![projectile::Effect::Stick],
                         hit_entity: vec![
-                            projectile::Effect::Damage(-damage as i32),
+                            projectile::Effect::Damages(Damages::new(
+                                Some(Damage::Projectile(damage)),
+                                None,
+                            )),
                             projectile::Effect::Knockback(knockback),
                             projectile::Effect::Vanish,
                         ],
diff --git a/common/src/states/combo_melee.rs b/common/src/states/combo_melee.rs
index fd59e00b3f..bf5610a5e4 100644
--- a/common/src/states/combo_melee.rs
+++ b/common/src/states/combo_melee.rs
@@ -2,6 +2,7 @@ use crate::{
     comp::{Attacking, CharacterState, EnergySource, StateUpdate},
     states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
+    Damage, Damages,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -125,13 +126,13 @@ impl CharacterBehavior for Data {
                     });
 
                     // Hit attempt
+                    let damage = self.static_data.stage_data[stage_index].max_damage.min(
+                        self.static_data.stage_data[stage_index].base_damage
+                            + self.combo / self.static_data.num_stages
+                                * self.static_data.stage_data[stage_index].damage_increase,
+                    );
                     data.updater.insert(data.entity, Attacking {
-                        base_damage: self.static_data.stage_data[stage_index].max_damage.min(
-                            self.static_data.stage_data[stage_index].base_damage
-                                + self.combo / self.static_data.num_stages
-                                    * self.static_data.stage_data[stage_index].damage_increase,
-                        ),
-                        base_heal: 0,
+                        damages: Damages::new(Some(Damage::Melee(damage as f32)), None),
                         range: self.static_data.stage_data[stage_index].range,
                         max_angle: self.static_data.stage_data[stage_index].angle.to_radians(),
                         applied: false,
diff --git a/common/src/states/dash_melee.rs b/common/src/states/dash_melee.rs
index 2b09a10203..278a2568e4 100644
--- a/common/src/states/dash_melee.rs
+++ b/common/src/states/dash_melee.rs
@@ -2,6 +2,7 @@ use crate::{
     comp::{Attacking, CharacterState, EnergySource, StateUpdate},
     states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
+    Damage, Damages,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -121,8 +122,7 @@ impl CharacterBehavior for Data {
                             * charge_frac
                             + self.static_data.base_knockback;
                         data.updater.insert(data.entity, Attacking {
-                            base_damage: damage as u32,
-                            base_heal: 0,
+                            damages: Damages::new(Some(Damage::Melee(damage)), None),
                             range: self.static_data.range,
                             max_angle: self.static_data.angle.to_radians(),
                             applied: false,
diff --git a/common/src/states/leap_melee.rs b/common/src/states/leap_melee.rs
index 4f76f22daa..1df206992a 100644
--- a/common/src/states/leap_melee.rs
+++ b/common/src/states/leap_melee.rs
@@ -2,6 +2,7 @@ use crate::{
     comp::{Attacking, CharacterState, StateUpdate},
     states::utils::{StageSection, *},
     sys::character_behavior::{CharacterBehavior, JoinData},
+    Damage, Damages,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -141,8 +142,10 @@ impl CharacterBehavior for Data {
                 if !self.exhausted {
                     // Hit attempt, when animation plays
                     data.updater.insert(data.entity, Attacking {
-                        base_damage: self.static_data.base_damage,
-                        base_heal: 0,
+                        damages: Damages::new(
+                            Some(Damage::Melee(self.static_data.base_damage as f32)),
+                            None,
+                        ),
                         range: self.static_data.range,
                         max_angle: self.static_data.max_angle.to_radians(),
                         applied: false,
diff --git a/common/src/states/shockwave.rs b/common/src/states/shockwave.rs
index fc2f72e10c..ab3c72e905 100644
--- a/common/src/states/shockwave.rs
+++ b/common/src/states/shockwave.rs
@@ -3,6 +3,7 @@ use crate::{
     event::ServerEvent,
     states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
+    Damage, Damages,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -70,7 +71,10 @@ impl CharacterBehavior for Data {
                         vertical_angle: self.static_data.shockwave_vertical_angle,
                         speed: self.static_data.shockwave_speed,
                         duration: self.static_data.shockwave_duration,
-                        damage: self.static_data.damage,
+                        damages: Damages::new(
+                            Some(Damage::Shockwave(self.static_data.damage as f32)),
+                            None,
+                        ),
                         knockback: self.static_data.knockback,
                         requires_ground: self.static_data.requires_ground,
                         owner: Some(*data.uid),
diff --git a/common/src/states/spin_melee.rs b/common/src/states/spin_melee.rs
index 28c07f43fd..168b71cab7 100644
--- a/common/src/states/spin_melee.rs
+++ b/common/src/states/spin_melee.rs
@@ -5,6 +5,7 @@ use crate::{
         character_behavior::{CharacterBehavior, JoinData},
         phys::GRAVITY,
     },
+    Damage, Damages,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -110,8 +111,10 @@ impl CharacterBehavior for Data {
                     });
                     // Hit attempt
                     data.updater.insert(data.entity, Attacking {
-                        base_damage: self.static_data.base_damage,
-                        base_heal: 0,
+                        damages: Damages::new(
+                            Some(Damage::Melee(self.static_data.base_damage as f32)),
+                            None,
+                        ),
                         range: self.static_data.range,
                         max_angle: 180_f32.to_radians(),
                         applied: false,
diff --git a/common/src/sys/beam.rs b/common/src/sys/beam.rs
index d39a8e299f..e6abc2ecf0 100644
--- a/common/src/sys/beam.rs
+++ b/common/src/sys/beam.rs
@@ -1,11 +1,12 @@
 use crate::{
     comp::{
-        group, Beam, BeamSegment, Body, CharacterState, Damage, DamageSource, Energy, EnergySource,
-        HealthChange, HealthSource, Last, Loadout, Ori, Pos, Scale, Stats,
+        group, Beam, BeamSegment, Body, CharacterState, Energy, EnergySource, HealthChange,
+        HealthSource, Last, Loadout, Ori, Pos, Scale, Stats,
     },
     event::{EventBus, ServerEvent},
     state::{DeltaTime, Time},
     sync::{Uid, UidAllocator},
+    Damage,
 };
 use specs::{saveload::MarkerAllocator, Entities, Join, Read, ReadStorage, System, WriteStorage};
 use std::time::Duration;
@@ -166,54 +167,31 @@ impl<'a> System<'a> for Sys {
                     if Some(*uid_b) == beam_segment.owner {
                         continue;
                     }
-                    // Don't heal if outside group
-                    // Don't damage in the same group
-                    let is_damage = !same_group && (beam_segment.damage > 0);
-                    let is_heal = same_group && (beam_segment.heal > 0);
-                    if !is_heal && !is_damage {
+
+                    let damage = if !same_group && beam_segment.damages.enemy.is_some() {
+                        beam_segment.damages.enemy.unwrap()
+                    } else if same_group && beam_segment.damages.group.is_some() {
+                        beam_segment.damages.group.unwrap()
+                    } else {
                         continue;
-                    }
-
-                    // Weapon gives base damage
-                    let source = if is_heal {
-                        DamageSource::Healing
-                    } else {
-                        DamageSource::Energy
-                    };
-                    let healthchange = if is_heal {
-                        beam_segment.heal as f32
-                    } else {
-                        -(beam_segment.damage as f32)
-                    };
-
-                    let mut damage = Damage {
-                        healthchange,
-                        source,
                     };
 
                     let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false)
                         // TODO: investigate whether this calculation is proper for beams
                         && ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0;
 
-                    if let Some(loadout) = loadouts.get(b) {
-                        damage.modify_damage(block, loadout);
-                    }
+                    let change = damage.modify_damage(block, loadouts.get(b), beam_segment.owner);
 
-                    if is_damage {
+                    if let Damage::Energy(_) = damage {
                         server_emitter.emit(ServerEvent::Damage {
                             uid: *uid_b,
-                            change: HealthChange {
-                                amount: damage.healthchange as i32,
-                                cause: HealthSource::Energy {
-                                    owner: beam_segment.owner,
-                                },
-                            },
+                            change,
                         });
                         if beam_segment.lifesteal_eff > 0.0 {
                             server_emitter.emit(ServerEvent::Damage {
                                 uid: beam_segment.owner.unwrap_or(*uid),
                                 change: HealthChange {
-                                    amount: (-damage.healthchange * beam_segment.lifesteal_eff)
+                                    amount: (-change.amount as f32 * beam_segment.lifesteal_eff)
                                         as i32,
                                     cause: HealthSource::Healing {
                                         by: beam_segment.owner,
@@ -228,7 +206,7 @@ impl<'a> System<'a> for Sys {
                             );
                         }
                     }
-                    if is_heal {
+                    if let Damage::Healing(_) = damage {
                         if let Some(energy_mut) = beam_owner.and_then(|o| energies.get_mut(o)) {
                             if energy_mut
                                 .try_change_by(
@@ -239,12 +217,7 @@ impl<'a> System<'a> for Sys {
                             {
                                 server_emitter.emit(ServerEvent::Damage {
                                     uid: *uid_b,
-                                    change: HealthChange {
-                                        amount: damage.healthchange as i32,
-                                        cause: HealthSource::Healing {
-                                            by: beam_segment.owner,
-                                        },
-                                    },
+                                    change,
                                 });
                             }
                         }
diff --git a/common/src/sys/combat.rs b/common/src/sys/melee.rs
similarity index 74%
rename from common/src/sys/combat.rs
rename to common/src/sys/melee.rs
index cfa469a910..66d2c20b32 100644
--- a/common/src/sys/combat.rs
+++ b/common/src/sys/melee.rs
@@ -1,8 +1,5 @@
 use crate::{
-    comp::{
-        buff, group, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange,
-        HealthSource, Loadout, Ori, Pos, Scale, Stats,
-    },
+    comp::{buff, group, Attacking, Body, CharacterState, Loadout, Ori, Pos, Scale, Stats},
     event::{EventBus, LocalEvent, ServerEvent},
     metrics::SysMetrics,
     span,
@@ -59,7 +56,7 @@ impl<'a> System<'a> for Sys {
         ): Self::SystemData,
     ) {
         let start_time = std::time::Instant::now();
-        span!(_guard, "run", "combat::Sys::run");
+        span!(_guard, "run", "melee::Sys::run");
         let mut server_emitter = server_bus.emitter();
         let mut _local_emitter = local_bus.emitter();
         // Attacks
@@ -113,56 +110,36 @@ impl<'a> System<'a> for Sys {
                         .get(entity)
                         .map(|group_a| Some(group_a) == groups.get(b))
                         .unwrap_or(false);
-                    // Don't heal if outside group
-                    // Don't damage in the same group
-                    let is_damage = !same_group && (attack.base_damage > 0);
-                    let is_heal = same_group && (attack.base_heal > 0);
-                    if !is_heal && !is_damage {
-                        continue;
-                    }
 
-                    // Weapon gives base damage
-                    let (source, healthchange) = if is_heal {
-                        (DamageSource::Healing, attack.base_heal as f32)
+                    let damage = if !same_group && attack.damages.enemy.is_some() {
+                        attack.damages.enemy.unwrap()
+                    } else if same_group && attack.damages.group.is_some() {
+                        attack.damages.group.unwrap()
                     } else {
-                        (DamageSource::Melee, -(attack.base_damage as f32))
-                    };
-                    let mut damage = Damage {
-                        healthchange,
-                        source,
+                        continue;
                     };
 
                     let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false)
                         && ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0;
 
-                    if let Some(loadout) = loadouts.get(b) {
-                        damage.modify_damage(block, loadout);
-                    }
+                    let change = damage.modify_damage(block, loadouts.get(b), Some(*uid));
 
-                    if damage.healthchange != 0.0 {
-                        let cause = if is_heal {
-                            HealthSource::Healing { by: Some(*uid) }
-                        } else {
-                            HealthSource::Attack { by: *uid }
-                        };
+                    if change.amount != 0 {
                         server_emitter.emit(ServerEvent::Damage {
                             uid: *uid_b,
-                            change: HealthChange {
-                                amount: damage.healthchange as i32,
-                                cause,
-                            },
+                            change,
                         });
 
                         // Apply bleeding buff on melee hits with 10% chance
                         // TODO: Don't have buff uniformly applied on all melee attacks
-                        if thread_rng().gen::<f32>() < 0.1 {
+                        if change.amount < 0 && thread_rng().gen::<f32>() < 0.1 {
                             use buff::*;
                             server_emitter.emit(ServerEvent::Buff {
                                 entity: b,
                                 buff_change: BuffChange::Add(Buff::new(
                                     BuffKind::Bleeding,
                                     BuffData {
-                                        strength: attack.base_damage as f32 / 10.0,
+                                        strength: -change.amount as f32 / 10.0,
                                         duration: Some(Duration::from_secs(10)),
                                     },
                                     vec![BuffCategory::Physical],
@@ -172,7 +149,8 @@ impl<'a> System<'a> for Sys {
                         }
                         attack.hit_count += 1;
                     }
-                    if attack.knockback != 0.0 && damage.healthchange != 0.0 {
+
+                    if attack.knockback != 0.0 && change.amount != 0 {
                         let kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
                         server_emitter.emit(ServerEvent::Knockback {
                             entity: b,
@@ -183,7 +161,7 @@ impl<'a> System<'a> for Sys {
                 }
             }
         }
-        sys_metrics.combat_ns.store(
+        sys_metrics.melee_ns.store(
             start_time.elapsed().as_nanos() as i64,
             std::sync::atomic::Ordering::Relaxed,
         );
diff --git a/common/src/sys/mod.rs b/common/src/sys/mod.rs
index 98c70fd145..50f222744d 100644
--- a/common/src/sys/mod.rs
+++ b/common/src/sys/mod.rs
@@ -2,8 +2,8 @@ pub mod agent;
 mod beam;
 mod buff;
 pub mod character_behavior;
-pub mod combat;
 pub mod controller;
+pub mod melee;
 mod mount;
 pub mod phys;
 mod projectile;
@@ -15,7 +15,7 @@ use specs::DispatcherBuilder;
 
 // System names
 pub const CHARACTER_BEHAVIOR_SYS: &str = "character_behavior_sys";
-pub const COMBAT_SYS: &str = "combat_sys";
+pub const MELEE_SYS: &str = "melee_sys";
 pub const AGENT_SYS: &str = "agent_sys";
 pub const BEAM_SYS: &str = "beam_sys";
 pub const CONTROLLER_SYS: &str = "controller_sys";
@@ -39,5 +39,5 @@ pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) {
     dispatch_builder.add(projectile::Sys, PROJECTILE_SYS, &[PHYS_SYS]);
     dispatch_builder.add(shockwave::Sys, SHOCKWAVE_SYS, &[PHYS_SYS]);
     dispatch_builder.add(beam::Sys, BEAM_SYS, &[PHYS_SYS]);
-    dispatch_builder.add(combat::Sys, COMBAT_SYS, &[PROJECTILE_SYS]);
+    dispatch_builder.add(melee::Sys, MELEE_SYS, &[PROJECTILE_SYS]);
 }
diff --git a/common/src/sys/projectile.rs b/common/src/sys/projectile.rs
index 5936575c33..59d8e8cbe2 100644
--- a/common/src/sys/projectile.rs
+++ b/common/src/sys/projectile.rs
@@ -1,7 +1,7 @@
 use crate::{
     comp::{
-        projectile, Damage, DamageSource, Energy, EnergySource, Group, HealthChange, HealthSource,
-        Loadout, Ori, PhysicsState, Pos, Projectile, Vel,
+        projectile, Energy, EnergySource, Group, HealthSource, Loadout, Ori, PhysicsState, Pos,
+        Projectile, Vel,
     },
     event::{EventBus, LocalEvent, ServerEvent},
     metrics::SysMetrics,
@@ -73,20 +73,21 @@ impl<'a> System<'a> for Sys {
         {
             // Hit entity
             for other in physics.touch_entities.iter().copied() {
+                let same_group = projectile
+                    .owner
+                    // Note: somewhat inefficient since we do the lookup for every touching
+                    // entity, but if we pull this out of the loop we would want to do it only
+                    // if there is at least one touching entity
+                    .and_then(|uid| uid_allocator.retrieve_entity_internal(uid.into()))
+                    .and_then(|e| groups.get(e))
+                    .map_or(false, |owner_group|
+                        Some(owner_group) == uid_allocator
+                        .retrieve_entity_internal(other.into())
+                        .and_then(|e| groups.get(e))
+                    );
                 if projectile.ignore_group
                     // Skip if in the same group
-                    && projectile
-                        .owner
-                        // Note: somewhat inefficient since we do the lookup for every touching
-                        // entity, but if we pull this out of the loop we would want to do it only
-                        // if there is at least one touching entity
-                        .and_then(|uid| uid_allocator.retrieve_entity_internal(uid.into()))
-                        .and_then(|e| groups.get(e))
-                        .map_or(false, |owner_group|
-                            Some(owner_group) == uid_allocator
-                            .retrieve_entity_internal(other.into())
-                            .and_then(|e| groups.get(e))
-                        )
+                    && same_group
                 {
                     continue;
                 }
@@ -97,40 +98,25 @@ impl<'a> System<'a> for Sys {
 
                 for effect in projectile.hit_entity.drain(..) {
                     match effect {
-                        projectile::Effect::Damage(healthchange) => {
-                            let owner_uid = projectile.owner.unwrap();
-                            let mut damage = Damage {
-                                healthchange: healthchange as f32,
-                                source: DamageSource::Projectile,
-                            };
-
-                            let other_entity = uid_allocator.retrieve_entity_internal(other.into());
-                            if let Some(loadout) = other_entity.and_then(|e| loadouts.get(e)) {
-                                damage.modify_damage(false, loadout);
+                        projectile::Effect::Damages(damages) => {
+                            if Some(other) == projectile.owner {
+                                continue;
                             }
+                            let damage = if !same_group && damages.enemy.is_some() {
+                                damages.enemy.unwrap()
+                            } else if same_group && damages.group.is_some() {
+                                damages.group.unwrap()
+                            } else {
+                                continue;
+                            };
+                            let other_entity_loadout = uid_allocator
+                                .retrieve_entity_internal(other.into())
+                                .and_then(|e| loadouts.get(e));
+                            let change =
+                                damage.modify_damage(false, other_entity_loadout, projectile.owner);
 
-                            if other != owner_uid {
-                                if damage.healthchange < 0.0 {
-                                    server_emitter.emit(ServerEvent::Damage {
-                                        uid: other,
-                                        change: HealthChange {
-                                            amount: damage.healthchange as i32,
-                                            cause: HealthSource::Projectile {
-                                                owner: Some(owner_uid),
-                                            },
-                                        },
-                                    });
-                                } else if damage.healthchange > 0.0 {
-                                    server_emitter.emit(ServerEvent::Damage {
-                                        uid: other,
-                                        change: HealthChange {
-                                            amount: damage.healthchange as i32,
-                                            cause: HealthSource::Healing {
-                                                by: Some(owner_uid),
-                                            },
-                                        },
-                                    });
-                                }
+                            if change.amount != 0 {
+                                server_emitter.emit(ServerEvent::Damage { uid: other, change });
                             }
                         },
                         projectile::Effect::Knockback(knockback) => {
diff --git a/common/src/sys/shockwave.rs b/common/src/sys/shockwave.rs
index 1dbf0530cb..dbdea44459 100644
--- a/common/src/sys/shockwave.rs
+++ b/common/src/sys/shockwave.rs
@@ -1,7 +1,7 @@
 use crate::{
     comp::{
-        group, Body, CharacterState, Damage, DamageSource, HealthChange, HealthSource, Last,
-        Loadout, Ori, PhysicsState, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
+        group, Body, CharacterState, HealthSource, Last, Loadout, Ori, PhysicsState, Pos, Scale,
+        Shockwave, ShockwaveHitEntities, Stats,
     },
     event::{EventBus, LocalEvent, ServerEvent},
     state::{DeltaTime, Time},
@@ -189,42 +189,31 @@ impl<'a> System<'a> for Sys {
                         })
                     }
                     && (pos_b_ground - pos.0).angle_between(pos_b.0 - pos.0) < max_angle
-                    && (!shockwave.requires_ground || physics_state_b.on_ground)
-                    && !same_group;
+                    && (!shockwave.requires_ground || physics_state_b.on_ground);
 
                 if hit {
-                    let mut damage = Damage {
-                        healthchange: -(shockwave.damage as f32),
-                        source: DamageSource::Shockwave,
+                    let damage = if !same_group && shockwave.damages.enemy.is_some() {
+                        shockwave.damages.enemy.unwrap()
+                    } else if same_group && shockwave.damages.group.is_some() {
+                        shockwave.damages.group.unwrap()
+                    } else {
+                        continue;
                     };
 
                     let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false)
                         && ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0;
 
-                    if let Some(loadout) = loadouts.get(b) {
-                        damage.modify_damage(block, loadout);
-                    }
+                    let owner_uid = shockwave.owner.unwrap_or(*uid);
+                    let change = damage.modify_damage(block, loadouts.get(b), Some(owner_uid));
 
-                    if damage.healthchange != 0.0 {
-                        let cause = if damage.healthchange < 0.0 {
-                            HealthSource::Attack {
-                                by: shockwave.owner.unwrap_or(*uid),
-                            }
-                        } else {
-                            HealthSource::Healing {
-                                by: Some(shockwave.owner.unwrap_or(*uid)),
-                            }
-                        };
+                    if change.amount != 0 {
                         server_emitter.emit(ServerEvent::Damage {
                             uid: *uid_b,
-                            change: HealthChange {
-                                amount: damage.healthchange as i32,
-                                cause,
-                            },
+                            change,
                         });
                         shockwave_hit_list.hit_entities.push(*uid_b);
                     }
-                    if shockwave.knockback != 0.0 && damage.healthchange != 0.0 {
+                    if shockwave.knockback != 0.0 {
                         let kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
                         let impulse = if shockwave.knockback < 0.0 {
                             shockwave.knockback
diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs
index e32e7318b7..cb175c4c20 100644
--- a/server/src/events/entity_manipulation.rs
+++ b/server/src/events/entity_manipulation.rs
@@ -8,18 +8,17 @@ use common::{
     comp::{
         self, buff,
         chat::{KillSource, KillType},
-        object, Alignment, Body, Damage, DamageSource, Group, HealthChange, HealthSource, Item,
-        Player, Pos, Stats,
+        object, Alignment, Body, Group, HealthChange, HealthSource, Item, Player, Pos, Stats,
     },
     lottery::Lottery,
     msg::{PlayerListUpdate, ServerGeneral},
     outcome::Outcome,
     state::BlockChange,
     sync::{Uid, UidAllocator, WorldSyncExt},
-    sys::combat::BLOCK_ANGLE,
+    sys::melee::BLOCK_ANGLE,
     terrain::{Block, TerrainGrid},
     vol::ReadVol,
-    Explosion,
+    Damage, Explosion,
 };
 use comp::item::Reagent;
 use rand::prelude::*;
@@ -456,17 +455,10 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>)
     if vel.z <= -30.0 {
         if let Some(stats) = state.ecs().write_storage::<comp::Stats>().get_mut(entity) {
             let falldmg = (vel.z.powi(2) / 20.0 - 40.0) * 10.0;
-            let mut damage = Damage {
-                healthchange: -falldmg,
-                source: DamageSource::Falling,
-            };
-            if let Some(loadout) = state.ecs().read_storage::<comp::Loadout>().get(entity) {
-                damage.modify_damage(false, loadout);
-            }
-            stats.health.change_by(comp::HealthChange {
-                amount: damage.healthchange as i32,
-                cause: comp::HealthSource::World,
-            });
+            let damage = Damage::Falling(falldmg);
+            let loadouts = state.ecs().read_storage::<comp::Loadout>();
+            let change = damage.modify_damage(false, loadouts.get(entity), None);
+            stats.health.change_by(change);
         }
     }
 }
@@ -576,43 +568,26 @@ pub fn handle_explosion(
                 continue;
             }
 
-            // Weapon gives base damage
-            let source = if is_heal {
-                DamageSource::Healing
-            } else {
-                DamageSource::Explosion
-            };
             let strength = 1.0 - distance_squared / explosion.radius.powi(2);
-            let healthchange = if is_heal {
-                explosion.min_heal as f32
-                    + (explosion.max_heal - explosion.min_heal) as f32 * strength
+            let damage = if is_heal {
+                Damage::Healing(
+                    explosion.min_heal as f32
+                        + (explosion.max_heal - explosion.min_heal) as f32 * strength,
+                )
             } else {
-                -(explosion.min_damage as f32
-                    + (explosion.max_damage - explosion.min_damage) as f32 * strength)
-            };
-
-            let mut damage = Damage {
-                healthchange,
-                source,
+                Damage::Explosion(
+                    explosion.min_damage as f32
+                        + (explosion.max_damage - explosion.min_damage) as f32 * strength,
+                )
             };
 
             let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false)
                 && ori_b.0.angle_between(pos - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0;
 
-            if let Some(loadout) = loadout_b {
-                damage.modify_damage(block, loadout);
-            }
+            let change = damage.modify_damage(block, loadout_b, owner);
 
-            if damage.healthchange != 0.0 {
-                let cause = if is_heal {
-                    HealthSource::Healing { by: owner }
-                } else {
-                    HealthSource::Explosion { owner }
-                };
-                stats_b.health.change_by(HealthChange {
-                    amount: damage.healthchange as i32,
-                    cause,
-                });
+            if change.amount != 0 {
+                stats_b.health.change_by(change);
                 if let Some(owner) = owner_entity {
                     if let Some(energy) = ecs.write_storage::<comp::Energy>().get_mut(owner) {
                         energy
diff --git a/server/src/lib.rs b/server/src/lib.rs
index d2a6111f05..e3acfa2f02 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -717,7 +717,7 @@ impl Server {
             let stats_ns = res.stats_ns.load(Ordering::Relaxed);
             let phys_ns = res.phys_ns.load(Ordering::Relaxed);
             let projectile_ns = res.projectile_ns.load(Ordering::Relaxed);
-            let combat_ns = res.combat_ns.load(Ordering::Relaxed);
+            let melee_ns = res.melee_ns.load(Ordering::Relaxed);
 
             c.with_label_values(&[common::sys::AGENT_SYS])
                 .inc_by(agent_ns);
@@ -733,8 +733,8 @@ impl Server {
                 .inc_by(phys_ns);
             c.with_label_values(&[common::sys::PROJECTILE_SYS])
                 .inc_by(projectile_ns);
-            c.with_label_values(&[common::sys::COMBAT_SYS])
-                .inc_by(combat_ns);
+            c.with_label_values(&[common::sys::MELEE_SYS])
+                .inc_by(melee_ns);
 
             const NANOSEC_PER_SEC: f64 = Duration::from_secs(1).as_nanos() as f64;
             let h = &self.state_tick_metrics.state_tick_time_hist;
@@ -752,8 +752,8 @@ impl Server {
                 .observe(phys_ns as f64 / NANOSEC_PER_SEC);
             h.with_label_values(&[common::sys::PROJECTILE_SYS])
                 .observe(projectile_ns as f64 / NANOSEC_PER_SEC);
-            h.with_label_values(&[common::sys::COMBAT_SYS])
-                .observe(combat_ns as f64 / NANOSEC_PER_SEC);
+            h.with_label_values(&[common::sys::MELEE_SYS])
+                .observe(melee_ns as f64 / NANOSEC_PER_SEC);
         }
 
         // Report other info

From 1ccbdec35cd1ec8dbeb919485d81f3e282df300d Mon Sep 17 00:00:00 2001
From: Sam <samuelkeiffer@gmail.com>
Date: Sun, 18 Oct 2020 11:46:28 -0500
Subject: [PATCH 06/10] Tweaked dash melee some more.

---
 common/src/comp/ability.rs             |   3 +-
 common/src/comp/inventory/item/tool.rs |  10 +-
 common/src/states/dash_melee.rs        | 136 ++++++++++++-------------
 3 files changed, 70 insertions(+), 79 deletions(-)

diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs
index 51844340a2..453e905c05 100644
--- a/common/src/comp/ability.rs
+++ b/common/src/comp/ability.rs
@@ -452,8 +452,9 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
                     recover_duration: *recover_duration,
                     is_interruptible: *is_interruptible,
                 },
-                end_charge: false,
+                auto_charge: false,
                 timer: Duration::default(),
+                refresh_timer: Duration::default(),
                 stage_section: StageSection::Buildup,
                 exhausted: false,
             }),
diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs
index f8e37ce635..c1b7833d55 100644
--- a/common/src/comp/inventory/item/tool.rs
+++ b/common/src/comp/inventory/item/tool.rs
@@ -173,16 +173,16 @@ impl Tool {
                 },
                 DashMelee {
                     energy_cost: 200,
-                    base_damage: (100.0 * self.base_power()) as u32,
-                    max_damage: (250.0 * self.base_power()) as u32,
-                    base_knockback: 10.0,
-                    max_knockback: 20.0,
+                    base_damage: (120.0 * self.base_power()) as u32,
+                    max_damage: (240.0 * self.base_power()) as u32,
+                    base_knockback: 8.0,
+                    max_knockback: 15.0,
                     range: 5.0,
                     angle: 45.0,
                     energy_drain: 500,
                     forward_speed: 4.0,
                     buildup_duration: Duration::from_millis(250),
-                    charge_duration: Duration::from_millis(400),
+                    charge_duration: Duration::from_millis(600),
                     swing_duration: Duration::from_millis(100),
                     recover_duration: Duration::from_millis(500),
                     infinite_charge: true,
diff --git a/common/src/states/dash_melee.rs b/common/src/states/dash_melee.rs
index 278a2568e4..f117dc19b4 100644
--- a/common/src/states/dash_melee.rs
+++ b/common/src/states/dash_melee.rs
@@ -47,9 +47,11 @@ pub struct Data {
     /// character state
     pub static_data: StaticData,
     /// Whether the charge should end
-    pub end_charge: bool,
+    pub auto_charge: bool,
     /// Timer for each stage
     pub timer: Duration,
+    /// Timer used to limit how often another attack will be applied
+    pub refresh_timer: Duration,
     /// What section the character stage is in
     pub stage_section: StageSection,
     /// Whether the state should attempt attacking again
@@ -80,11 +82,12 @@ impl CharacterBehavior for Data {
                     // Build up
                     update.character = CharacterState::DashMelee(Data {
                         static_data: self.static_data,
-                        end_charge: self.end_charge,
+                        auto_charge: self.auto_charge,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
+                        refresh_timer: self.refresh_timer,
                         stage_section: self.stage_section,
                         exhausted: self.exhausted,
                     });
@@ -92,103 +95,86 @@ impl CharacterBehavior for Data {
                     // Transitions to charge section of stage
                     update.character = CharacterState::DashMelee(Data {
                         static_data: self.static_data,
-                        end_charge: self.end_charge,
+                        auto_charge: !data.inputs.secondary.is_pressed(),
                         timer: Duration::default(),
+                        refresh_timer: self.refresh_timer,
                         stage_section: StageSection::Charge,
                         exhausted: self.exhausted,
                     });
                 }
             },
             StageSection::Charge => {
-                if (self.timer < self.static_data.charge_duration
-                    || (self.static_data.infinite_charge && data.inputs.secondary.is_pressed()))
+                if (self.static_data.infinite_charge
+                    || self.timer < self.static_data.charge_duration)
+                    && (data.inputs.secondary.is_pressed()
+                        || (self.auto_charge && self.timer < self.static_data.charge_duration))
                     && update.energy.current() > 0
-                    && !self.end_charge
                 {
                     // Forward movement
                     forward_move(data, &mut update, 0.1, self.static_data.forward_speed);
 
-                    // Hit attempt (also checks if player is moving)
-                    if !self.exhausted && update.vel.0.distance_squared(Vec3::zero()) > 1.0 {
-                        let charge_frac = (self.timer.as_secs_f32()
-                            / self.static_data.charge_duration.as_secs_f32())
-                        .min(1.0);
-                        let damage = (self.static_data.max_damage as f32
-                            - self.static_data.base_damage as f32)
-                            * charge_frac
-                            + self.static_data.base_damage as f32;
-                        let knockback = (self.static_data.max_knockback
-                            - self.static_data.base_knockback)
-                            * charge_frac
-                            + self.static_data.base_knockback;
-                        data.updater.insert(data.entity, Attacking {
-                            damages: Damages::new(Some(Damage::Melee(damage)), None),
-                            range: self.static_data.range,
-                            max_angle: self.static_data.angle.to_radians(),
-                            applied: false,
-                            hit_count: 0,
-                            knockback,
-                        });
-                    }
-
                     // This logic basically just decides if a charge should end, and prevents the
                     // character state spamming attacks while checking if it has hit something
                     if !self.exhausted {
+                        // Hit attempt (also checks if player is moving)
+                        if update.vel.0.distance_squared(Vec3::zero()) > 1.0 {
+                            let charge_frac = (self.timer.as_secs_f32()
+                                / self.static_data.charge_duration.as_secs_f32())
+                            .min(1.0);
+                            let damage = (self.static_data.max_damage as f32
+                                - self.static_data.base_damage as f32)
+                                * charge_frac
+                                + self.static_data.base_damage as f32;
+                            let knockback = (self.static_data.max_knockback
+                                - self.static_data.base_knockback)
+                                * charge_frac
+                                + self.static_data.base_knockback;
+                            data.updater.insert(data.entity, Attacking {
+                                damages: Damages::new(Some(Damage::Melee(damage)), None),
+                                range: self.static_data.range,
+                                max_angle: self.static_data.angle.to_radians(),
+                                applied: false,
+                                hit_count: 0,
+                                knockback,
+                            });
+                        }
                         update.character = CharacterState::DashMelee(Data {
                             static_data: self.static_data,
-                            end_charge: self.end_charge,
+                            auto_charge: self.auto_charge,
                             timer: self
                                 .timer
                                 .checked_add(Duration::from_secs_f32(data.dt.0))
                                 .unwrap_or_default(),
-                            stage_section: StageSection::Charge,
+                            refresh_timer: self.refresh_timer,
+                            stage_section: self.stage_section,
                             exhausted: true,
                         })
-                    } else if let Some(attack) = data.attacking {
-                        if attack.applied && attack.hit_count > 0 {
-                            update.character = CharacterState::DashMelee(Data {
-                                static_data: self.static_data,
-                                end_charge: !self.static_data.infinite_charge,
-                                timer: self
-                                    .timer
-                                    .checked_add(Duration::from_secs_f32(data.dt.0))
-                                    .unwrap_or_default(),
-                                stage_section: StageSection::Charge,
-                                exhausted: false,
-                            })
-                        } else if attack.applied {
-                            update.character = CharacterState::DashMelee(Data {
-                                static_data: self.static_data,
-                                end_charge: self.end_charge,
-                                timer: self
-                                    .timer
-                                    .checked_add(Duration::from_secs_f32(data.dt.0))
-                                    .unwrap_or_default(),
-                                stage_section: StageSection::Charge,
-                                exhausted: false,
-                            })
-                        } else {
-                            update.character = CharacterState::DashMelee(Data {
-                                static_data: self.static_data,
-                                end_charge: !self.static_data.infinite_charge,
-                                timer: self
-                                    .timer
-                                    .checked_add(Duration::from_secs_f32(data.dt.0))
-                                    .unwrap_or_default(),
-                                stage_section: StageSection::Charge,
-                                exhausted: self.exhausted,
-                            })
-                        }
+                    } else if self.refresh_timer < Duration::from_millis(50) {
+                        update.character = CharacterState::DashMelee(Data {
+                            static_data: self.static_data,
+                            auto_charge: self.auto_charge,
+                            timer: self
+                                .timer
+                                .checked_add(Duration::from_secs_f32(data.dt.0))
+                                .unwrap_or_default(),
+                            refresh_timer: self
+                                .refresh_timer
+                                .checked_add(Duration::from_secs_f32(data.dt.0))
+                                .unwrap_or_default(),
+                            stage_section: self.stage_section,
+                            exhausted: self.exhausted,
+                        })
                     } else {
                         update.character = CharacterState::DashMelee(Data {
                             static_data: self.static_data,
-                            end_charge: !self.static_data.infinite_charge,
+                            auto_charge: self.auto_charge,
                             timer: self
                                 .timer
                                 .checked_add(Duration::from_secs_f32(data.dt.0))
                                 .unwrap_or_default(),
-                            stage_section: StageSection::Charge,
-                            exhausted: self.exhausted,
+                            refresh_timer: Duration::default(),
+                            stage_section: self.stage_section,
+                            exhausted: false,
                         })
                     }
 
@@ -201,8 +187,9 @@ impl CharacterBehavior for Data {
                     // Transitions to swing section of stage
                     update.character = CharacterState::DashMelee(Data {
                         static_data: self.static_data,
-                        end_charge: self.end_charge,
+                        auto_charge: self.auto_charge,
                         timer: Duration::default(),
+                        refresh_timer: self.refresh_timer,
                         stage_section: StageSection::Swing,
                         exhausted: self.exhausted,
                     });
@@ -213,11 +200,12 @@ impl CharacterBehavior for Data {
                     // Swings
                     update.character = CharacterState::DashMelee(Data {
                         static_data: self.static_data,
-                        end_charge: self.end_charge,
+                        auto_charge: self.auto_charge,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
+                        refresh_timer: self.refresh_timer,
                         stage_section: self.stage_section,
                         exhausted: self.exhausted,
                     });
@@ -225,8 +213,9 @@ impl CharacterBehavior for Data {
                     // Transitions to recover section of stage
                     update.character = CharacterState::DashMelee(Data {
                         static_data: self.static_data,
-                        end_charge: self.end_charge,
+                        auto_charge: self.auto_charge,
                         timer: Duration::default(),
+                        refresh_timer: self.refresh_timer,
                         stage_section: StageSection::Recover,
                         exhausted: self.exhausted,
                     });
@@ -237,11 +226,12 @@ impl CharacterBehavior for Data {
                     // Recover
                     update.character = CharacterState::DashMelee(Data {
                         static_data: self.static_data,
-                        end_charge: self.end_charge,
+                        auto_charge: self.auto_charge,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
+                        refresh_timer: self.refresh_timer,
                         stage_section: self.stage_section,
                         exhausted: self.exhausted,
                     });

From 981eee59369db089950bdf64182e2f0da70f50d6 Mon Sep 17 00:00:00 2001
From: Sam <samuelkeiffer@gmail.com>
Date: Sun, 18 Oct 2020 13:21:58 -0500
Subject: [PATCH 07/10] Transitioned knockback to an enum.

---
 common/src/combat.rs                   | 25 +++++++++++++++++++++++++
 common/src/comp/ability.rs             |  3 ++-
 common/src/comp/character_state.rs     |  4 ++--
 common/src/comp/inventory/item/tool.rs | 11 +++++------
 common/src/comp/projectile.rs          |  4 ++--
 common/src/comp/shockwave.rs           |  4 ++--
 common/src/lib.rs                      |  2 +-
 common/src/states/basic_melee.rs       |  4 ++--
 common/src/states/charged_melee.rs     |  4 ++--
 common/src/states/charged_ranged.rs    |  4 ++--
 common/src/states/combo_melee.rs       |  6 ++++--
 common/src/states/dash_melee.rs        |  4 ++--
 common/src/states/leap_melee.rs        |  4 ++--
 common/src/states/shockwave.rs         |  4 ++--
 common/src/states/spin_melee.rs        |  4 ++--
 common/src/sys/melee.rs                |  5 ++---
 common/src/sys/projectile.rs           |  5 +----
 common/src/sys/shockwave.rs            | 14 ++++----------
 common/src/util/dir.rs                 |  7 +++++++
 19 files changed, 71 insertions(+), 47 deletions(-)

diff --git a/common/src/combat.rs b/common/src/combat.rs
index 3702d2e978..e80de3abe6 100644
--- a/common/src/combat.rs
+++ b/common/src/combat.rs
@@ -1,8 +1,10 @@
 use crate::{
     comp::{HealthChange, HealthSource, Loadout},
     sync::Uid,
+    util::Dir,
 };
 use serde::{Deserialize, Serialize};
+use vek::*;
 
 pub const BLOCK_EFFICIENCY: f32 = 0.9;
 
@@ -159,3 +161,26 @@ impl Damage {
         }
     }
 }
+
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub enum Knockback {
+    Away(f32),
+    Towards(f32),
+    Up(f32),
+    TowardsUp(f32),
+}
+
+impl Knockback {
+    pub fn get_knockback(self, dir: Dir) -> Vec3<f32> {
+        match self {
+            Knockback::Away(strength) => strength * *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
+            Knockback::Towards(strength) => {
+                strength * *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5)
+            },
+            Knockback::Up(strength) => strength * Vec3::unit_z(),
+            Knockback::TowardsUp(strength) => {
+                strength * *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.85)
+            },
+        }
+    }
+}
diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs
index 453e905c05..ab0f92cd5d 100644
--- a/common/src/comp/ability.rs
+++ b/common/src/comp/ability.rs
@@ -8,6 +8,7 @@ use crate::{
         *,
     },
     sys::character_behavior::JoinData,
+    Knockback,
 };
 use arraygen::Arraygen;
 use serde::{Deserialize, Serialize};
@@ -184,7 +185,7 @@ pub enum CharacterAbility {
         swing_duration: Duration,
         recover_duration: Duration,
         damage: u32,
-        knockback: f32,
+        knockback: Knockback,
         shockwave_angle: f32,
         shockwave_vertical_angle: f32,
         shockwave_speed: f32,
diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs
index 7b459b60ee..f439fbcfdf 100644
--- a/common/src/comp/character_state.rs
+++ b/common/src/comp/character_state.rs
@@ -3,7 +3,7 @@ use crate::{
     event::{LocalEvent, ServerEvent},
     states::*,
     sys::character_behavior::JoinData,
-    Damages,
+    Damages, Knockback,
 };
 use serde::{Deserialize, Serialize};
 use specs::{Component, FlaggedStorage, VecStorage};
@@ -158,7 +158,7 @@ pub struct Attacking {
     pub max_angle: f32,
     pub applied: bool,
     pub hit_count: u32,
-    pub knockback: f32,
+    pub knockback: Knockback,
 }
 
 impl Component for Attacking {
diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs
index c1b7833d55..78d0d40e96 100644
--- a/common/src/comp/inventory/item/tool.rs
+++ b/common/src/comp/inventory/item/tool.rs
@@ -4,7 +4,7 @@
 use crate::{
     comp::{body::object, projectile, Body, CharacterAbility, Gravity, LightEmitter, Projectile},
     states::combo_melee,
-    Damage, Damages, Explosion,
+    Damage, Damages, Explosion, Knockback,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -302,7 +302,7 @@ impl Tool {
                                 Some(Damage::Projectile(40.0 * self.base_power())),
                                 None,
                             )),
-                            projectile::Effect::Knockback(10.0),
+                            projectile::Effect::Knockback(Knockback::Away(10.0)),
                             projectile::Effect::RewardEnergy(50),
                             projectile::Effect::Vanish,
                         ],
@@ -345,8 +345,7 @@ impl Tool {
                                 Some(Damage::Projectile(40.0 * self.base_power())),
                                 None,
                             )),
-                            projectile::Effect::Knockback(10.0),
-                            projectile::Effect::RewardEnergy(50),
+                            projectile::Effect::Knockback(Knockback::Away(10.0)),
                             projectile::Effect::Vanish,
                         ],
                         time_left: Duration::from_secs(15),
@@ -489,7 +488,7 @@ impl Tool {
                     swing_duration: Duration::from_millis(100),
                     recover_duration: Duration::from_millis(300),
                     damage: (200.0 * self.base_power()) as u32,
-                    knockback: 25.0,
+                    knockback: Knockback::Away(25.0),
                     shockwave_angle: 360.0,
                     shockwave_vertical_angle: 90.0,
                     shockwave_speed: 20.0,
@@ -530,7 +529,7 @@ impl Tool {
                             swing_duration: Duration::from_millis(200),
                             recover_duration: Duration::from_millis(800),
                             damage: 500,
-                            knockback: -40.0,
+                            knockback: Knockback::TowardsUp(40.0),
                             shockwave_angle: 90.0,
                             shockwave_vertical_angle: 15.0,
                             shockwave_speed: 20.0,
diff --git a/common/src/comp/projectile.rs b/common/src/comp/projectile.rs
index 121a6bdbb9..3b5888a456 100644
--- a/common/src/comp/projectile.rs
+++ b/common/src/comp/projectile.rs
@@ -1,4 +1,4 @@
-use crate::{sync::Uid, Damages, Explosion};
+use crate::{sync::Uid, Damages, Explosion, Knockback};
 use serde::{Deserialize, Serialize};
 use specs::{Component, FlaggedStorage};
 use specs_idvs::IdvStorage;
@@ -7,7 +7,7 @@ use std::time::Duration;
 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
 pub enum Effect {
     Damages(Damages),
-    Knockback(f32),
+    Knockback(Knockback),
     RewardEnergy(u32),
     Explode(Explosion),
     Vanish,
diff --git a/common/src/comp/shockwave.rs b/common/src/comp/shockwave.rs
index 15161bc673..2d5de87726 100644
--- a/common/src/comp/shockwave.rs
+++ b/common/src/comp/shockwave.rs
@@ -1,4 +1,4 @@
-use crate::{sync::Uid, Damages};
+use crate::{sync::Uid, Damages, Knockback};
 use serde::{Deserialize, Serialize};
 use specs::{Component, FlaggedStorage};
 use specs_idvs::IdvStorage;
@@ -10,7 +10,7 @@ pub struct Properties {
     pub vertical_angle: f32,
     pub speed: f32,
     pub damages: Damages,
-    pub knockback: f32,
+    pub knockback: Knockback,
     pub requires_ground: bool,
     pub duration: Duration,
     pub owner: Option<Uid>,
diff --git a/common/src/lib.rs b/common/src/lib.rs
index 407328bd66..01b986408a 100644
--- a/common/src/lib.rs
+++ b/common/src/lib.rs
@@ -52,6 +52,6 @@ pub mod util;
 pub mod vol;
 pub mod volumes;
 
-pub use combat::{Damage, Damages};
+pub use combat::{Damage, Damages, Knockback};
 pub use explosion::Explosion;
 pub use loadout_builder::LoadoutBuilder;
diff --git a/common/src/states/basic_melee.rs b/common/src/states/basic_melee.rs
index e99ec7a03d..744c30c3b8 100644
--- a/common/src/states/basic_melee.rs
+++ b/common/src/states/basic_melee.rs
@@ -2,7 +2,7 @@ use crate::{
     comp::{Attacking, CharacterState, EnergySource, StateUpdate},
     states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
-    Damage, Damages,
+    Damage, Damages, Knockback,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -88,7 +88,7 @@ impl CharacterBehavior for Data {
                         max_angle: 180_f32.to_radians(),
                         applied: false,
                         hit_count: 0,
-                        knockback: self.static_data.knockback,
+                        knockback: Knockback::Away(self.static_data.knockback),
                     });
                 } else if self.timer < self.static_data.swing_duration {
                     // Swings
diff --git a/common/src/states/charged_melee.rs b/common/src/states/charged_melee.rs
index 62f5b3e3fe..0da5ada515 100644
--- a/common/src/states/charged_melee.rs
+++ b/common/src/states/charged_melee.rs
@@ -2,7 +2,7 @@ use crate::{
     comp::{Attacking, CharacterState, EnergySource, StateUpdate},
     states::utils::{StageSection, *},
     sys::character_behavior::*,
-    Damage, Damages,
+    Damage, Damages, Knockback,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -130,7 +130,7 @@ impl CharacterBehavior for Data {
                         max_angle: self.static_data.max_angle.to_radians(),
                         applied: false,
                         hit_count: 0,
-                        knockback,
+                        knockback: Knockback::Away(knockback),
                     });
 
                     // Starts swinging
diff --git a/common/src/states/charged_ranged.rs b/common/src/states/charged_ranged.rs
index 5805338bc8..3b19fa8268 100644
--- a/common/src/states/charged_ranged.rs
+++ b/common/src/states/charged_ranged.rs
@@ -6,7 +6,7 @@ use crate::{
     event::ServerEvent,
     states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
-    Damage, Damages,
+    Damage, Damages, Knockback,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -102,7 +102,7 @@ impl CharacterBehavior for Data {
                                 Some(Damage::Projectile(damage)),
                                 None,
                             )),
-                            projectile::Effect::Knockback(knockback),
+                            projectile::Effect::Knockback(Knockback::Away(knockback)),
                             projectile::Effect::Vanish,
                         ],
                         time_left: Duration::from_secs(15),
diff --git a/common/src/states/combo_melee.rs b/common/src/states/combo_melee.rs
index bf5610a5e4..4ae0d24efd 100644
--- a/common/src/states/combo_melee.rs
+++ b/common/src/states/combo_melee.rs
@@ -2,7 +2,7 @@ use crate::{
     comp::{Attacking, CharacterState, EnergySource, StateUpdate},
     states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
-    Damage, Damages,
+    Damage, Damages, Knockback,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -137,7 +137,9 @@ impl CharacterBehavior for Data {
                         max_angle: self.static_data.stage_data[stage_index].angle.to_radians(),
                         applied: false,
                         hit_count: 0,
-                        knockback: self.static_data.stage_data[stage_index].knockback,
+                        knockback: Knockback::Away(
+                            self.static_data.stage_data[stage_index].knockback,
+                        ),
                     });
                 }
             },
diff --git a/common/src/states/dash_melee.rs b/common/src/states/dash_melee.rs
index f117dc19b4..fbd83a2652 100644
--- a/common/src/states/dash_melee.rs
+++ b/common/src/states/dash_melee.rs
@@ -2,7 +2,7 @@ use crate::{
     comp::{Attacking, CharacterState, EnergySource, StateUpdate},
     states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
-    Damage, Damages,
+    Damage, Damages, Knockback,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -135,7 +135,7 @@ impl CharacterBehavior for Data {
                                 max_angle: self.static_data.angle.to_radians(),
                                 applied: false,
                                 hit_count: 0,
-                                knockback,
+                                knockback: Knockback::Away(knockback),
                             });
                         }
                         update.character = CharacterState::DashMelee(Data {
diff --git a/common/src/states/leap_melee.rs b/common/src/states/leap_melee.rs
index 1df206992a..d6b6f297af 100644
--- a/common/src/states/leap_melee.rs
+++ b/common/src/states/leap_melee.rs
@@ -2,7 +2,7 @@ use crate::{
     comp::{Attacking, CharacterState, StateUpdate},
     states::utils::{StageSection, *},
     sys::character_behavior::{CharacterBehavior, JoinData},
-    Damage, Damages,
+    Damage, Damages, Knockback,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -150,7 +150,7 @@ impl CharacterBehavior for Data {
                         max_angle: self.static_data.max_angle.to_radians(),
                         applied: false,
                         hit_count: 0,
-                        knockback: self.static_data.knockback,
+                        knockback: Knockback::Away(self.static_data.knockback),
                     });
 
                     update.character = CharacterState::LeapMelee(Data {
diff --git a/common/src/states/shockwave.rs b/common/src/states/shockwave.rs
index ab3c72e905..a9bb69cd07 100644
--- a/common/src/states/shockwave.rs
+++ b/common/src/states/shockwave.rs
@@ -3,7 +3,7 @@ use crate::{
     event::ServerEvent,
     states::utils::*,
     sys::character_behavior::{CharacterBehavior, JoinData},
-    Damage, Damages,
+    Damage, Damages, Knockback,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -20,7 +20,7 @@ pub struct StaticData {
     /// Base damage
     pub damage: u32,
     /// Knockback
-    pub knockback: f32,
+    pub knockback: Knockback,
     /// Angle of the shockwave
     pub shockwave_angle: f32,
     /// Vertical angle of the shockwave
diff --git a/common/src/states/spin_melee.rs b/common/src/states/spin_melee.rs
index 168b71cab7..73765d1607 100644
--- a/common/src/states/spin_melee.rs
+++ b/common/src/states/spin_melee.rs
@@ -5,7 +5,7 @@ use crate::{
         character_behavior::{CharacterBehavior, JoinData},
         phys::GRAVITY,
     },
-    Damage, Damages,
+    Damage, Damages, Knockback,
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
@@ -119,7 +119,7 @@ impl CharacterBehavior for Data {
                         max_angle: 180_f32.to_radians(),
                         applied: false,
                         hit_count: 0,
-                        knockback: self.static_data.knockback,
+                        knockback: Knockback::Away(self.static_data.knockback),
                     });
                 } else if self.timer < self.static_data.swing_duration {
                     if !self.static_data.is_helicopter {
diff --git a/common/src/sys/melee.rs b/common/src/sys/melee.rs
index 66d2c20b32..35b77e8b61 100644
--- a/common/src/sys/melee.rs
+++ b/common/src/sys/melee.rs
@@ -150,12 +150,11 @@ impl<'a> System<'a> for Sys {
                         attack.hit_count += 1;
                     }
 
-                    if attack.knockback != 0.0 && change.amount != 0 {
+                    if change.amount != 0 {
                         let kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
                         server_emitter.emit(ServerEvent::Knockback {
                             entity: b,
-                            impulse: attack.knockback
-                                * *Dir::slerp(kb_dir, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5),
+                            impulse: attack.knockback.get_knockback(kb_dir),
                         });
                     }
                 }
diff --git a/common/src/sys/projectile.rs b/common/src/sys/projectile.rs
index 59d8e8cbe2..725919c551 100644
--- a/common/src/sys/projectile.rs
+++ b/common/src/sys/projectile.rs
@@ -8,13 +8,11 @@ use crate::{
     span,
     state::DeltaTime,
     sync::UidAllocator,
-    util::Dir,
 };
 use specs::{
     saveload::MarkerAllocator, Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage,
 };
 use std::time::Duration;
-use vek::*;
 
 /// This system is responsible for handling projectile effect triggers
 pub struct Sys;
@@ -125,8 +123,7 @@ impl<'a> System<'a> for Sys {
                             {
                                 local_emitter.emit(LocalEvent::ApplyImpulse {
                                     entity,
-                                    impulse: knockback
-                                        * *Dir::slerp(ori.0, Dir::new(Vec3::unit_z()), 0.5),
+                                    impulse: knockback.get_knockback(ori.0),
                                 });
                             }
                         },
diff --git a/common/src/sys/shockwave.rs b/common/src/sys/shockwave.rs
index dbdea44459..35a24691a7 100644
--- a/common/src/sys/shockwave.rs
+++ b/common/src/sys/shockwave.rs
@@ -212,17 +212,11 @@ impl<'a> System<'a> for Sys {
                             change,
                         });
                         shockwave_hit_list.hit_entities.push(*uid_b);
-                    }
-                    if shockwave.knockback != 0.0 {
                         let kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
-                        let impulse = if shockwave.knockback < 0.0 {
-                            shockwave.knockback
-                                * *Dir::slerp(kb_dir, Dir::new(Vec3::new(0.0, 0.0, -1.0)), 0.85)
-                        } else {
-                            shockwave.knockback
-                                * *Dir::slerp(kb_dir, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5)
-                        };
-                        server_emitter.emit(ServerEvent::Knockback { entity: b, impulse });
+                        server_emitter.emit(ServerEvent::Knockback {
+                            entity: b,
+                            impulse: shockwave.knockback.get_knockback(kb_dir),
+                        });
                     }
                 }
             }
diff --git a/common/src/util/dir.rs b/common/src/util/dir.rs
index af9a29ced0..30cb4cfe07 100644
--- a/common/src/util/dir.rs
+++ b/common/src/util/dir.rs
@@ -98,6 +98,13 @@ impl std::ops::Deref for Dir {
 impl From<Vec3<f32>> for Dir {
     fn from(dir: Vec3<f32>) -> Self { Dir::new(dir) }
 }
+
+impl std::ops::Neg for Dir {
+    type Output = Dir;
+
+    fn neg(self) -> Dir { Dir::new(-self.0) }
+}
+
 /// Begone ye NaN's
 /// Slerp two `Vec3`s skipping the slerp if their directions are very close
 /// This avoids a case where `vek`s slerp produces NaN's

From 1a8cf33a60df2da385fb5925a34680c70c81f508 Mon Sep 17 00:00:00 2001
From: Sam <samuelkeiffer@gmail.com>
Date: Sun, 25 Oct 2020 15:22:40 -0500
Subject: [PATCH 08/10] Transitioned forced movement to an enum.

---
 common/src/comp/inventory/item/tool.rs |  2 +-
 common/src/states/combo_melee.rs       |  6 ++-
 common/src/states/dash_melee.rs        |  9 +++-
 common/src/states/leap_melee.rs        | 37 +++++--------
 common/src/states/repeater_ranged.rs   | 26 ++++++---
 common/src/states/spin_melee.rs        |  9 +++-
 common/src/states/utils.rs             | 73 ++++++++++++++++++++++----
 7 files changed, 115 insertions(+), 47 deletions(-)

diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs
index 78d0d40e96..3b4ab95573 100644
--- a/common/src/comp/inventory/item/tool.rs
+++ b/common/src/comp/inventory/item/tool.rs
@@ -337,7 +337,7 @@ impl Tool {
                     buildup_duration: Duration::from_millis(200),
                     shoot_duration: Duration::from_millis(200),
                     recover_duration: Duration::from_millis(800),
-                    leap: Some(10.0),
+                    leap: Some(5.0),
                     projectile: Projectile {
                         hit_solid: vec![projectile::Effect::Stick],
                         hit_entity: vec![
diff --git a/common/src/states/combo_melee.rs b/common/src/states/combo_melee.rs
index 4ae0d24efd..ca66efae6e 100644
--- a/common/src/states/combo_melee.rs
+++ b/common/src/states/combo_melee.rs
@@ -146,11 +146,13 @@ impl CharacterBehavior for Data {
             StageSection::Swing => {
                 if self.timer < self.static_data.stage_data[stage_index].base_swing_duration {
                     // Forward movement
-                    forward_move(
+                    handle_forced_movement(
                         data,
                         &mut update,
+                        ForcedMovement::Forward {
+                            strength: self.static_data.stage_data[stage_index].forward_movement,
+                        },
                         0.3,
-                        self.static_data.stage_data[stage_index].forward_movement,
                     );
 
                     // Swings
diff --git a/common/src/states/dash_melee.rs b/common/src/states/dash_melee.rs
index fbd83a2652..820117704e 100644
--- a/common/src/states/dash_melee.rs
+++ b/common/src/states/dash_melee.rs
@@ -111,7 +111,14 @@ impl CharacterBehavior for Data {
                     && update.energy.current() > 0
                 {
                     // Forward movement
-                    forward_move(data, &mut update, 0.1, self.static_data.forward_speed);
+                    handle_forced_movement(
+                        data,
+                        &mut update,
+                        ForcedMovement::Forward {
+                            strength: self.static_data.forward_speed,
+                        },
+                        0.1,
+                    );
 
                     // This logic basically just decides if a charge should end, and prevents the
                     // character state spamming attacks while checking if it has hit something
diff --git a/common/src/states/leap_melee.rs b/common/src/states/leap_melee.rs
index d6b6f297af..0739761281 100644
--- a/common/src/states/leap_melee.rs
+++ b/common/src/states/leap_melee.rs
@@ -6,7 +6,6 @@ use crate::{
 };
 use serde::{Deserialize, Serialize};
 use std::time::Duration;
-use vek::Vec3;
 
 /// Separated out to condense update portions of character state
 #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
@@ -76,28 +75,20 @@ impl CharacterBehavior for Data {
             },
             StageSection::Movement => {
                 if self.timer < self.static_data.movement_duration {
-                    // Apply jumping force while in Movement portion of state
-                    update.vel.0 = Vec3::new(
-                        data.inputs.look_dir.x,
-                        data.inputs.look_dir.y,
-                        self.static_data.vertical_leap_strength,
-                    ) * 2.0
-                        // Multiply decreasing amount linearly over time of
-                        // movement duration
-                        * (1.0
-                            - self.timer.as_secs_f32()
-                                / self.static_data.movement_duration.as_secs_f32())
-                        // Apply inputted movement directions at 0.25 strength
-                        + (update.vel.0 * Vec3::new(2.0, 2.0, 0.0)
-                            + 0.25 * data.inputs.move_dir.try_normalized().unwrap_or_default())
-                        .try_normalized()
-                        .unwrap_or_default()
-                        // Multiply by forward leap strength
-                            * self.static_data.forward_leap_strength
-                        // Control forward movement based on look direction.
-                        // This allows players to stop moving forward when they
-                        // look downward at target
-                            * (1.0 - data.inputs.look_dir.z.abs());
+                    // Apply jumping force
+                    let progress = 1.0
+                        - self.timer.as_secs_f32()
+                            / self.static_data.movement_duration.as_secs_f32();
+                    handle_forced_movement(
+                        data,
+                        &mut update,
+                        ForcedMovement::Leap {
+                            vertical: self.static_data.vertical_leap_strength,
+                            forward: self.static_data.forward_leap_strength,
+                            progress,
+                        },
+                        0.15,
+                    );
 
                     // Increment duration
                     // If we were to set a timeout for state, this would be
diff --git a/common/src/states/repeater_ranged.rs b/common/src/states/repeater_ranged.rs
index c2c4e5d81b..5adfdde10a 100644
--- a/common/src/states/repeater_ranged.rs
+++ b/common/src/states/repeater_ranged.rs
@@ -54,13 +54,18 @@ impl CharacterBehavior for Data {
             StageSection::Movement => {
                 // Jumping
                 if let Some(leap_strength) = self.static_data.leap {
-                    update.vel.0 = Vec3::new(
-                        data.vel.0.x,
-                        data.vel.0.y,
-                        leap_strength
-                            * (1.0
-                                - self.timer.as_secs_f32()
-                                    / self.static_data.movement_duration.as_secs_f32()),
+                    let progress = 1.0
+                        - self.timer.as_secs_f32()
+                            / self.static_data.movement_duration.as_secs_f32();
+                    handle_forced_movement(
+                        data,
+                        &mut update,
+                        ForcedMovement::Leap {
+                            vertical: leap_strength,
+                            forward: 10.0,
+                            progress,
+                        },
+                        1.0,
                     );
                 }
                 if self.timer < self.static_data.movement_duration {
@@ -87,7 +92,12 @@ impl CharacterBehavior for Data {
             StageSection::Buildup => {
                 // Aim gliding
                 if self.static_data.leap.is_some() {
-                    update.vel.0 = Vec3::new(data.vel.0.x, data.vel.0.y, 0.0);
+                    handle_forced_movement(
+                        data,
+                        &mut update,
+                        ForcedMovement::Hover { move_input: 0.1 },
+                        1.0,
+                    );
                 }
                 if self.timer < self.static_data.buildup_duration {
                     // Buildup to attack
diff --git a/common/src/states/spin_melee.rs b/common/src/states/spin_melee.rs
index 73765d1607..d685d5dbb4 100644
--- a/common/src/states/spin_melee.rs
+++ b/common/src/states/spin_melee.rs
@@ -123,7 +123,14 @@ impl CharacterBehavior for Data {
                     });
                 } else if self.timer < self.static_data.swing_duration {
                     if !self.static_data.is_helicopter {
-                        forward_move(data, &mut update, 0.1, self.static_data.forward_speed);
+                        handle_forced_movement(
+                            data,
+                            &mut update,
+                            ForcedMovement::Forward {
+                                strength: self.static_data.forward_speed,
+                            },
+                            0.1,
+                        );
                         handle_orientation(data, &mut update, 1.0);
                     }
 
diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs
index 13e0f50269..b2dd5ecb9e 100644
--- a/common/src/states/utils.rs
+++ b/common/src/states/utils.rs
@@ -91,18 +91,54 @@ fn basic_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
     handle_orientation(data, update, data.body.base_ori_rate());
 }
 
-/// Similar to basic_move function, but with forced forward movement
-pub fn forward_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32, forward: f32) {
-    let accel = if data.physics.on_ground {
-        data.body.base_accel()
-    } else {
-        BASE_HUMANOID_AIR_ACCEL
-    };
-
-    update.vel.0 += Vec2::broadcast(data.dt.0)
-        * accel
-        * (data.inputs.move_dir * efficiency + (*update.ori.0).xy() * forward);
+/// Handles forced movement
+pub fn handle_forced_movement(
+    data: &JoinData,
+    update: &mut StateUpdate,
+    movement: ForcedMovement,
+    efficiency: f32,
+) {
+    match movement {
+        ForcedMovement::Forward { strength } => {
+            let accel = if data.physics.on_ground {
+                data.body.base_accel()
+            } else {
+                BASE_HUMANOID_AIR_ACCEL
+            };
 
+            update.vel.0 += Vec2::broadcast(data.dt.0)
+                * accel
+                * (data.inputs.move_dir * efficiency + (*update.ori.0).xy() * strength);
+        },
+        ForcedMovement::Leap {
+            vertical,
+            forward,
+            progress,
+        } => {
+            // Apply jumping force
+            update.vel.0 = Vec3::new(
+                data.inputs.look_dir.x,
+                data.inputs.look_dir.y,
+                vertical,
+            )
+                // Multiply decreasing amount linearly over time (with average of 1)
+                * 2.0 * progress
+                // Apply inputted movement directions with some efficiency
+                + (data.inputs.move_dir.try_normalized().unwrap_or_default() + update.vel.0.xy())
+                .try_normalized()
+                .unwrap_or_default()
+                // Multiply by forward leap strength
+                    * forward
+                // Control forward movement based on look direction.
+                // This allows players to stop moving forward when they
+                // look downward at target
+                    * (1.0 - data.inputs.look_dir.z.abs());
+        },
+        ForcedMovement::Hover { move_input } => {
+            update.vel.0 = Vec3::new(data.vel.0.x, data.vel.0.y, 0.0)
+                + move_input * data.inputs.move_dir.try_normalized().unwrap_or_default();
+        },
+    }
     handle_orientation(data, update, data.body.base_ori_rate() * efficiency);
 }
 
@@ -389,3 +425,18 @@ pub enum AbilityKey {
     Skill1,
     Dodge,
 }
+
+#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
+pub enum ForcedMovement {
+    Forward {
+        strength: f32,
+    },
+    Leap {
+        vertical: f32,
+        forward: f32,
+        progress: f32,
+    },
+    Hover {
+        move_input: f32,
+    },
+}

From 7971034fde457651748feedaccdafd73592fa4b4 Mon Sep 17 00:00:00 2001
From: Sam <samuelkeiffer@gmail.com>
Date: Tue, 27 Oct 2020 17:04:52 -0500
Subject: [PATCH 09/10] Fixed audio tests.

---
 .../audio/sfx/event_mapper/combat/tests.rs    | 22 +++++++++++++------
 .../audio/sfx/event_mapper/movement/tests.rs  |  8 ++++++-
 2 files changed, 22 insertions(+), 8 deletions(-)

diff --git a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs
index ad03ea0ea5..448c976f6b 100644
--- a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs
+++ b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs
@@ -23,7 +23,10 @@ fn maps_wield_while_equipping() {
 
     let result = CombatEventMapper::map_event(
         &CharacterState::Equipping(states::equipping::Data {
-            time_left: Duration::from_millis(10),
+            static_data: states::equipping::StaticData {
+                buildup_duration: Duration::from_millis(10),
+            },
+            timer: Duration::default(),
         }),
         &PreviousEntityState {
             event: SfxEvent::Idle,
@@ -77,12 +80,17 @@ fn maps_basic_melee() {
 
     let result = CombatEventMapper::map_event(
         &CharacterState::BasicMelee(states::basic_melee::Data {
-            buildup_duration: Duration::default(),
-            recover_duration: Duration::default(),
-            knockback: 0.0,
-            base_healthchange: 10,
-            range: 1.0,
-            max_angle: 1.0,
+            static_data: states::basic_melee::StaticData {
+                buildup_duration: Duration::default(),
+                swing_duration: Duration::default(),
+                recover_duration: Duration::default(),
+                base_damage: 10,
+                knockback: 0.0,
+                range: 1.0,
+                max_angle: 1.0,
+            },
+            timer: Duration::default(),
+            stage_section: states::utils::StageSection::Buildup,
             exhausted: false,
         }),
         &PreviousEntityState {
diff --git a/voxygen/src/audio/sfx/event_mapper/movement/tests.rs b/voxygen/src/audio/sfx/event_mapper/movement/tests.rs
index 40ef0d5edb..1ac43e80a3 100644
--- a/voxygen/src/audio/sfx/event_mapper/movement/tests.rs
+++ b/voxygen/src/audio/sfx/event_mapper/movement/tests.rs
@@ -153,7 +153,13 @@ fn does_not_map_run_with_sufficient_velocity_but_not_on_ground() {
 fn maps_roll() {
     let result = MovementEventMapper::map_movement_event(
         &CharacterState::Roll(states::roll::Data {
-            remaining_duration: Duration::from_millis(300),
+            static_data: states::roll::StaticData {
+                buildup_duration: Duration::default(),
+                movement_duration: Duration::default(),
+                recover_duration: Duration::default(),
+            },
+            timer: Duration::default(),
+            stage_section: states::utils::StageSection::Buildup,
             was_wielded: true,
         }),
         &PhysicsState {

From 52c93f613e60c4f003d7c95b9c742d596717c209 Mon Sep 17 00:00:00 2001
From: Sam <samuelkeiffer@gmail.com>
Date: Tue, 27 Oct 2020 17:16:17 -0500
Subject: [PATCH 10/10] Addressed comments.

---
 common/src/combat.rs                 | 46 +++++++++-------------------
 common/src/states/basic_beam.rs      | 17 +++-------
 common/src/states/basic_melee.rs     | 21 ++++---------
 common/src/states/basic_ranged.rs    | 11 +++----
 common/src/states/boost.rs           |  2 +-
 common/src/states/charged_melee.rs   | 31 +++++--------------
 common/src/states/charged_ranged.rs  | 21 ++++---------
 common/src/states/combo_melee.rs     | 29 +++++-------------
 common/src/states/dash_melee.rs      | 46 ++++++----------------------
 common/src/states/equipping.rs       |  2 +-
 common/src/states/repeater_ranged.rs | 20 +++++-------
 common/src/states/roll.rs            | 18 +++--------
 common/src/states/shockwave.rs       | 13 +++-----
 common/src/states/spin_melee.rs      | 30 +++++-------------
 common/src/sys/beam.rs               | 35 +++++++++------------
 common/src/sys/melee.rs              | 15 ++++-----
 common/src/sys/projectile.rs         | 15 +++++----
 common/src/sys/shockwave.rs          | 14 ++++-----
 18 files changed, 120 insertions(+), 266 deletions(-)

diff --git a/common/src/combat.rs b/common/src/combat.rs
index e80de3abe6..bebda0aa11 100644
--- a/common/src/combat.rs
+++ b/common/src/combat.rs
@@ -8,14 +8,22 @@ use vek::*;
 
 pub const BLOCK_EFFICIENCY: f32 = 0.9;
 
+/// Each section of this struct determines what damage is applied to a
+/// particular target, using some identifier
 #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
 pub struct Damages {
+    /// Targets enemies, and all other creatures not in your group
     pub enemy: Option<Damage>,
+    /// Targets people in the same group as you, and any pets you have
     pub group: Option<Damage>,
 }
 
 impl Damages {
     pub fn new(enemy: Option<Damage>, group: Option<Damage>) -> Self { Damages { enemy, group } }
+
+    pub fn get_damage(self, same_group: bool) -> Option<Damage> {
+        if same_group { self.group } else { self.enemy }
+    }
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
@@ -49,11 +57,7 @@ impl Damage {
                     damage *= 1.0 - BLOCK_EFFICIENCY
                 }
                 // Armor
-                let damage_reduction = if let Some(loadout) = loadout {
-                    loadout.get_damage_reduction()
-                } else {
-                    0.0
-                };
+                let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
                 damage *= 1.0 - damage_reduction;
 
                 // Critical damage applies after armor for melee
@@ -77,11 +81,7 @@ impl Damage {
                     damage *= 1.0 - BLOCK_EFFICIENCY
                 }
                 // Armor
-                let damage_reduction = if let Some(loadout) = loadout {
-                    loadout.get_damage_reduction()
-                } else {
-                    0.0
-                };
+                let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
                 damage *= 1.0 - damage_reduction;
 
                 HealthChange {
@@ -96,11 +96,7 @@ impl Damage {
                     damage *= 1.0 - BLOCK_EFFICIENCY
                 }
                 // Armor
-                let damage_reduction = if let Some(loadout) = loadout {
-                    loadout.get_damage_reduction()
-                } else {
-                    0.0
-                };
+                let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
                 damage *= 1.0 - damage_reduction;
 
                 HealthChange {
@@ -111,11 +107,7 @@ impl Damage {
             Damage::Shockwave(damage) => {
                 let mut damage = damage;
                 // Armor
-                let damage_reduction = if let Some(loadout) = loadout {
-                    loadout.get_damage_reduction()
-                } else {
-                    0.0
-                };
+                let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
                 damage *= 1.0 - damage_reduction;
 
                 HealthChange {
@@ -126,11 +118,7 @@ impl Damage {
             Damage::Energy(damage) => {
                 let mut damage = damage;
                 // Armor
-                let damage_reduction = if let Some(loadout) = loadout {
-                    loadout.get_damage_reduction()
-                } else {
-                    0.0
-                };
+                let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
                 damage *= 1.0 - damage_reduction;
 
                 HealthChange {
@@ -145,11 +133,7 @@ impl Damage {
             Damage::Falling(damage) => {
                 let mut damage = damage;
                 // Armor
-                let damage_reduction = if let Some(loadout) = loadout {
-                    loadout.get_damage_reduction()
-                } else {
-                    0.0
-                };
+                let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
                 if (damage_reduction - 1.0).abs() < f32::EPSILON {
                     damage = 0.0;
                 }
@@ -171,7 +155,7 @@ pub enum Knockback {
 }
 
 impl Knockback {
-    pub fn get_knockback(self, dir: Dir) -> Vec3<f32> {
+    pub fn calculate_impulse(self, dir: Dir) -> Vec3<f32> {
         match self {
             Knockback::Away(strength) => strength * *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
             Knockback::Towards(strength) => {
diff --git a/common/src/states/basic_beam.rs b/common/src/states/basic_beam.rs
index 37c84d6264..4a387ba7da 100644
--- a/common/src/states/basic_beam.rs
+++ b/common/src/states/basic_beam.rs
@@ -74,14 +74,12 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.buildup_duration {
                     // Build up
                     update.character = CharacterState::BasicBeam(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
                         particle_ori: Some(*data.inputs.look_dir),
-                        offset: self.offset,
+                        ..*self
                     });
                 } else {
                     // Creates beam
@@ -97,11 +95,11 @@ impl CharacterBehavior for Data {
                     };
                     // Build up
                     update.character = CharacterState::BasicBeam(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
                         stage_section: StageSection::Cast,
                         particle_ori: Some(*data.inputs.look_dir),
                         offset: eye_height * 0.55,
+                        ..*self
                     });
                 }
             },
@@ -139,14 +137,12 @@ impl CharacterBehavior for Data {
                         ori: Ori(data.inputs.look_dir),
                     });
                     update.character = CharacterState::BasicBeam(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
                         particle_ori: Some(*data.inputs.look_dir),
-                        offset: self.offset,
+                        ..*self
                     });
 
                     // Consumes energy if there's enough left and ability key is held down
@@ -156,25 +152,22 @@ impl CharacterBehavior for Data {
                     );
                 } else {
                     update.character = CharacterState::BasicBeam(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
                         stage_section: StageSection::Recover,
                         particle_ori: Some(*data.inputs.look_dir),
-                        offset: self.offset,
+                        ..*self
                     });
                 }
             },
             StageSection::Recover => {
                 if self.timer < self.static_data.recover_duration {
                     update.character = CharacterState::BasicBeam(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
                         particle_ori: Some(*data.inputs.look_dir),
-                        offset: self.offset,
+                        ..*self
                     });
                 } else {
                     // Done
diff --git a/common/src/states/basic_melee.rs b/common/src/states/basic_melee.rs
index 744c30c3b8..228a10a204 100644
--- a/common/src/states/basic_melee.rs
+++ b/common/src/states/basic_melee.rs
@@ -51,31 +51,27 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.buildup_duration {
                     // Build up
                     update.character = CharacterState::BasicMelee(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Transitions to swing section of stage
                     update.character = CharacterState::BasicMelee(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
                         stage_section: StageSection::Swing,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 }
             },
             StageSection::Swing => {
                 if !self.exhausted {
                     update.character = CharacterState::BasicMelee(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
-                        stage_section: self.stage_section,
                         exhausted: true,
+                        ..*self
                     });
 
                     // Hit attempt
@@ -93,21 +89,18 @@ impl CharacterBehavior for Data {
                 } else if self.timer < self.static_data.swing_duration {
                     // Swings
                     update.character = CharacterState::BasicMelee(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Transitions to recover section of stage
                     update.character = CharacterState::BasicMelee(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
                         stage_section: StageSection::Recover,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 }
             },
@@ -115,13 +108,11 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.recover_duration {
                     // Recovery
                     update.character = CharacterState::BasicMelee(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Done
diff --git a/common/src/states/basic_ranged.rs b/common/src/states/basic_ranged.rs
index bb0950e605..19fff5e2b5 100644
--- a/common/src/states/basic_ranged.rs
+++ b/common/src/states/basic_ranged.rs
@@ -54,8 +54,7 @@ impl CharacterBehavior for Data {
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Transitions to recover section of stage
@@ -63,7 +62,7 @@ impl CharacterBehavior for Data {
                         static_data: self.static_data.clone(),
                         timer: Duration::default(),
                         stage_section: StageSection::Recover,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 }
             },
@@ -84,9 +83,8 @@ impl CharacterBehavior for Data {
 
                     update.character = CharacterState::BasicRanged(Data {
                         static_data: self.static_data.clone(),
-                        timer: self.timer,
-                        stage_section: self.stage_section,
                         exhausted: true,
+                        ..*self
                     });
                 } else if self.timer < self.static_data.recover_duration {
                     // Recovers
@@ -96,8 +94,7 @@ impl CharacterBehavior for Data {
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Done
diff --git a/common/src/states/boost.rs b/common/src/states/boost.rs
index 564a714c57..4b0110c605 100644
--- a/common/src/states/boost.rs
+++ b/common/src/states/boost.rs
@@ -36,11 +36,11 @@ impl CharacterBehavior for Data {
                 update.vel.0 += *data.inputs.look_dir * 500.0 * data.dt.0;
             }
             update.character = CharacterState::Boost(Data {
-                static_data: self.static_data,
                 timer: self
                     .timer
                     .checked_add(Duration::from_secs_f32(data.dt.0))
                     .unwrap_or_default(),
+                ..*self
             });
         } else {
             // Done
diff --git a/common/src/states/charged_melee.rs b/common/src/states/charged_melee.rs
index 0da5ada515..e0255e89a4 100644
--- a/common/src/states/charged_melee.rs
+++ b/common/src/states/charged_melee.rs
@@ -68,14 +68,12 @@ impl CharacterBehavior for Data {
 
                     // Charge the attack
                     update.character = CharacterState::ChargedMelee(Data {
-                        static_data: self.static_data,
-                        stage_section: self.stage_section,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        exhausted: self.exhausted,
                         charge_amount: charge,
+                        ..*self
                     });
 
                     // Consumes energy if there's enough left and RMB is held down
@@ -88,14 +86,11 @@ impl CharacterBehavior for Data {
                 {
                     // Maintains charge
                     update.character = CharacterState::ChargedMelee(Data {
-                        static_data: self.static_data,
-                        stage_section: self.stage_section,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        exhausted: self.exhausted,
-                        charge_amount: self.charge_amount,
+                        ..*self
                     });
 
                     // Consumes energy if there's enough left and RMB is held down
@@ -106,11 +101,9 @@ impl CharacterBehavior for Data {
                 } else {
                     // Transitions to swing
                     update.character = CharacterState::ChargedMelee(Data {
-                        static_data: self.static_data,
                         stage_section: StageSection::Swing,
                         timer: Duration::default(),
-                        exhausted: self.exhausted,
-                        charge_amount: self.charge_amount,
+                        ..*self
                     });
                 }
             },
@@ -135,35 +128,28 @@ impl CharacterBehavior for Data {
 
                     // Starts swinging
                     update.character = CharacterState::ChargedMelee(Data {
-                        static_data: self.static_data,
-                        stage_section: self.stage_section,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
                         exhausted: true,
-                        charge_amount: self.charge_amount,
+                        ..*self
                     });
                 } else if self.timer < self.static_data.swing_duration {
                     // Swings
                     update.character = CharacterState::ChargedMelee(Data {
-                        static_data: self.static_data,
-                        stage_section: self.stage_section,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        exhausted: self.exhausted,
-                        charge_amount: self.charge_amount,
+                        ..*self
                     });
                 } else {
                     // Transitions to recover
                     update.character = CharacterState::ChargedMelee(Data {
-                        static_data: self.static_data,
                         stage_section: StageSection::Recover,
                         timer: Duration::default(),
-                        exhausted: self.exhausted,
-                        charge_amount: self.charge_amount,
+                        ..*self
                     });
                 }
             },
@@ -171,14 +157,11 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.recover_duration {
                     // Recovers
                     update.character = CharacterState::ChargedMelee(Data {
-                        static_data: self.static_data,
-                        stage_section: self.stage_section,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        exhausted: self.exhausted,
-                        charge_amount: self.charge_amount,
+                        ..*self
                     });
                 } else {
                     // Done
diff --git a/common/src/states/charged_ranged.rs b/common/src/states/charged_ranged.rs
index 3b19fa8268..036f9ac5bc 100644
--- a/common/src/states/charged_ranged.rs
+++ b/common/src/states/charged_ranged.rs
@@ -63,21 +63,18 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.buildup_duration {
                     // Build up
                     update.character = CharacterState::ChargedRanged(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Transitions to swing section of stage
                     update.character = CharacterState::ChargedRanged(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
                         stage_section: StageSection::Charge,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 }
             },
@@ -124,23 +121,21 @@ impl CharacterBehavior for Data {
                     });
 
                     update.character = CharacterState::ChargedRanged(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
                         stage_section: StageSection::Recover,
                         exhausted: true,
+                        ..*self
                     });
                 } else if self.timer < self.static_data.charge_duration
                     && data.inputs.secondary.is_pressed()
                 {
                     // Charges
                     update.character = CharacterState::ChargedRanged(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
 
                     // Consumes energy if there's enough left and RMB is held down
@@ -151,13 +146,11 @@ impl CharacterBehavior for Data {
                 } else if data.inputs.secondary.is_pressed() {
                     // Holds charge
                     update.character = CharacterState::ChargedRanged(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
 
                     // Consumes energy if there's enough left and RMB is held down
@@ -171,13 +164,11 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.recover_duration {
                     // Recovers
                     update.character = CharacterState::ChargedRanged(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Done
diff --git a/common/src/states/combo_melee.rs b/common/src/states/combo_melee.rs
index ca66efae6e..285804e278 100644
--- a/common/src/states/combo_melee.rs
+++ b/common/src/states/combo_melee.rs
@@ -100,8 +100,6 @@ impl CharacterBehavior for Data {
                     // Build up
                     update.character = CharacterState::ComboMelee(Data {
                         static_data: self.static_data.clone(),
-                        stage: self.stage,
-                        combo: self.combo,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(
@@ -111,18 +109,15 @@ impl CharacterBehavior for Data {
                                     * data.dt.0,
                             ))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        next_stage: self.next_stage,
+                        ..*self
                     });
                 } else {
                     // Transitions to swing section of stage
                     update.character = CharacterState::ComboMelee(Data {
                         static_data: self.static_data.clone(),
-                        stage: self.stage,
-                        combo: self.combo,
                         timer: Duration::default(),
                         stage_section: StageSection::Swing,
-                        next_stage: self.next_stage,
+                        ..*self
                     });
 
                     // Hit attempt
@@ -158,8 +153,6 @@ impl CharacterBehavior for Data {
                     // Swings
                     update.character = CharacterState::ComboMelee(Data {
                         static_data: self.static_data.clone(),
-                        stage: self.stage,
-                        combo: self.combo,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(
@@ -169,18 +162,15 @@ impl CharacterBehavior for Data {
                                     * data.dt.0,
                             ))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        next_stage: self.next_stage,
+                        ..*self
                     });
                 } else {
                     // Transitions to recover section of stage
                     update.character = CharacterState::ComboMelee(Data {
                         static_data: self.static_data.clone(),
-                        stage: self.stage,
-                        combo: self.combo,
                         timer: Duration::default(),
                         stage_section: StageSection::Recover,
-                        next_stage: self.next_stage,
+                        ..*self
                     });
                 }
             },
@@ -191,8 +181,6 @@ impl CharacterBehavior for Data {
                         // Checks if state will transition to next stage after recover
                         update.character = CharacterState::ComboMelee(Data {
                             static_data: self.static_data.clone(),
-                            stage: self.stage,
-                            combo: self.combo,
                             timer: self
                                 .timer
                                 .checked_add(Duration::from_secs_f32(
@@ -205,14 +193,12 @@ impl CharacterBehavior for Data {
                                         * data.dt.0,
                                 ))
                                 .unwrap_or_default(),
-                            stage_section: self.stage_section,
                             next_stage: true,
+                            ..*self
                         });
                     } else {
                         update.character = CharacterState::ComboMelee(Data {
                             static_data: self.static_data.clone(),
-                            stage: self.stage,
-                            combo: self.combo,
                             timer: self
                                 .timer
                                 .checked_add(Duration::from_secs_f32(
@@ -225,8 +211,7 @@ impl CharacterBehavior for Data {
                                         * data.dt.0,
                                 ))
                                 .unwrap_or_default(),
-                            stage_section: self.stage_section,
-                            next_stage: self.next_stage,
+                            ..*self
                         });
                     }
                 } else if self.next_stage {
@@ -234,10 +219,10 @@ impl CharacterBehavior for Data {
                     update.character = CharacterState::ComboMelee(Data {
                         static_data: self.static_data.clone(),
                         stage: (self.stage % self.static_data.num_stages) + 1,
-                        combo: self.combo,
                         timer: Duration::default(),
                         stage_section: StageSection::Buildup,
                         next_stage: false,
+                        ..*self
                     });
                 } else {
                     // Done
diff --git a/common/src/states/dash_melee.rs b/common/src/states/dash_melee.rs
index 820117704e..8a99b80bd9 100644
--- a/common/src/states/dash_melee.rs
+++ b/common/src/states/dash_melee.rs
@@ -81,25 +81,19 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.buildup_duration {
                     // Build up
                     update.character = CharacterState::DashMelee(Data {
-                        static_data: self.static_data,
-                        auto_charge: self.auto_charge,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        refresh_timer: self.refresh_timer,
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Transitions to charge section of stage
                     update.character = CharacterState::DashMelee(Data {
-                        static_data: self.static_data,
                         auto_charge: !data.inputs.secondary.is_pressed(),
                         timer: Duration::default(),
-                        refresh_timer: self.refresh_timer,
                         stage_section: StageSection::Charge,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 }
             },
@@ -146,20 +140,15 @@ impl CharacterBehavior for Data {
                             });
                         }
                         update.character = CharacterState::DashMelee(Data {
-                            static_data: self.static_data,
-                            auto_charge: self.auto_charge,
                             timer: self
                                 .timer
                                 .checked_add(Duration::from_secs_f32(data.dt.0))
                                 .unwrap_or_default(),
-                            refresh_timer: self.refresh_timer,
-                            stage_section: self.stage_section,
                             exhausted: true,
+                            ..*self
                         })
                     } else if self.refresh_timer < Duration::from_millis(50) {
                         update.character = CharacterState::DashMelee(Data {
-                            static_data: self.static_data,
-                            auto_charge: self.auto_charge,
                             timer: self
                                 .timer
                                 .checked_add(Duration::from_secs_f32(data.dt.0))
@@ -168,20 +157,17 @@ impl CharacterBehavior for Data {
                                 .refresh_timer
                                 .checked_add(Duration::from_secs_f32(data.dt.0))
                                 .unwrap_or_default(),
-                            stage_section: self.stage_section,
-                            exhausted: self.exhausted,
+                            ..*self
                         })
                     } else {
                         update.character = CharacterState::DashMelee(Data {
-                            static_data: self.static_data,
-                            auto_charge: self.auto_charge,
                             timer: self
                                 .timer
                                 .checked_add(Duration::from_secs_f32(data.dt.0))
                                 .unwrap_or_default(),
                             refresh_timer: Duration::default(),
-                            stage_section: self.stage_section,
                             exhausted: false,
+                            ..*self
                         })
                     }
 
@@ -193,12 +179,9 @@ impl CharacterBehavior for Data {
                 } else {
                     // Transitions to swing section of stage
                     update.character = CharacterState::DashMelee(Data {
-                        static_data: self.static_data,
-                        auto_charge: self.auto_charge,
                         timer: Duration::default(),
-                        refresh_timer: self.refresh_timer,
                         stage_section: StageSection::Swing,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 }
             },
@@ -206,25 +189,18 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.swing_duration {
                     // Swings
                     update.character = CharacterState::DashMelee(Data {
-                        static_data: self.static_data,
-                        auto_charge: self.auto_charge,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        refresh_timer: self.refresh_timer,
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Transitions to recover section of stage
                     update.character = CharacterState::DashMelee(Data {
-                        static_data: self.static_data,
-                        auto_charge: self.auto_charge,
                         timer: Duration::default(),
-                        refresh_timer: self.refresh_timer,
                         stage_section: StageSection::Recover,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 }
             },
@@ -232,15 +208,11 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.recover_duration {
                     // Recover
                     update.character = CharacterState::DashMelee(Data {
-                        static_data: self.static_data,
-                        auto_charge: self.auto_charge,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        refresh_timer: self.refresh_timer,
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Done
diff --git a/common/src/states/equipping.rs b/common/src/states/equipping.rs
index b0e63c11ea..fac06e5351 100644
--- a/common/src/states/equipping.rs
+++ b/common/src/states/equipping.rs
@@ -32,11 +32,11 @@ impl CharacterBehavior for Data {
         if self.timer < self.static_data.buildup_duration {
             // Draw weapon
             update.character = CharacterState::Equipping(Data {
-                static_data: self.static_data,
                 timer: self
                     .timer
                     .checked_add(Duration::from_secs_f32(data.dt.0))
                     .unwrap_or_default(),
+                ..*self
             });
         } else {
             // Done
diff --git a/common/src/states/repeater_ranged.rs b/common/src/states/repeater_ranged.rs
index 5adfdde10a..b6ee044680 100644
--- a/common/src/states/repeater_ranged.rs
+++ b/common/src/states/repeater_ranged.rs
@@ -76,8 +76,7 @@ impl CharacterBehavior for Data {
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        reps_remaining: self.reps_remaining,
+                        ..*self
                     });
                 } else {
                     // Transition to buildup
@@ -85,7 +84,7 @@ impl CharacterBehavior for Data {
                         static_data: self.static_data.clone(),
                         timer: Duration::default(),
                         stage_section: StageSection::Buildup,
-                        reps_remaining: self.reps_remaining,
+                        ..*self
                     });
                 }
             },
@@ -107,8 +106,7 @@ impl CharacterBehavior for Data {
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        reps_remaining: self.reps_remaining,
+                        ..*self
                     });
                 } else {
                     // Transition to shoot
@@ -116,7 +114,7 @@ impl CharacterBehavior for Data {
                         static_data: self.static_data.clone(),
                         timer: Duration::default(),
                         stage_section: StageSection::Shoot,
-                        reps_remaining: self.reps_remaining,
+                        ..*self
                     });
                 }
             },
@@ -162,8 +160,8 @@ impl CharacterBehavior for Data {
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
                         reps_remaining: self.reps_remaining - 1,
+                        ..*self
                     });
                 } else if self.timer < self.static_data.shoot_duration {
                     // Finish shooting
@@ -173,8 +171,7 @@ impl CharacterBehavior for Data {
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        reps_remaining: self.reps_remaining,
+                        ..*self
                     });
                 } else {
                     // Transition to recover
@@ -182,7 +179,7 @@ impl CharacterBehavior for Data {
                         static_data: self.static_data.clone(),
                         timer: Duration::default(),
                         stage_section: StageSection::Recover,
-                        reps_remaining: self.reps_remaining,
+                        ..*self
                     });
                 }
             },
@@ -198,8 +195,7 @@ impl CharacterBehavior for Data {
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        reps_remaining: self.reps_remaining,
+                        ..*self
                     });
                 } else {
                     // Done
diff --git a/common/src/states/roll.rs b/common/src/states/roll.rs
index 2a49f44445..cb18a441f5 100644
--- a/common/src/states/roll.rs
+++ b/common/src/states/roll.rs
@@ -54,21 +54,18 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.buildup_duration {
                     // Build up
                     update.character = CharacterState::Roll(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        was_wielded: self.was_wielded,
+                        ..*self
                     });
                 } else {
                     // Transitions to movement section of stage
                     update.character = CharacterState::Roll(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
                         stage_section: StageSection::Movement,
-                        was_wielded: self.was_wielded,
+                        ..*self
                     });
                 }
             },
@@ -76,21 +73,18 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.movement_duration {
                     // Movement
                     update.character = CharacterState::Roll(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        was_wielded: self.was_wielded,
+                        ..*self
                     });
                 } else {
                     // Transitions to recover section of stage
                     update.character = CharacterState::Roll(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
                         stage_section: StageSection::Recover,
-                        was_wielded: self.was_wielded,
+                        ..*self
                     });
                 }
             },
@@ -98,13 +92,11 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.recover_duration {
                     // Build up
                     update.character = CharacterState::Roll(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
-                        was_wielded: self.was_wielded,
+                        ..*self
                     });
                 } else {
                     // Done
diff --git a/common/src/states/shockwave.rs b/common/src/states/shockwave.rs
index a9bb69cd07..de0cbf3ab5 100644
--- a/common/src/states/shockwave.rs
+++ b/common/src/states/shockwave.rs
@@ -57,12 +57,11 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.buildup_duration {
                     // Build up
                     update.character = CharacterState::Shockwave(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
+                        ..*self
                     });
                 } else {
                     // Attack
@@ -87,9 +86,9 @@ impl CharacterBehavior for Data {
 
                     // Transitions to swing
                     update.character = CharacterState::Shockwave(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
                         stage_section: StageSection::Swing,
+                        ..*self
                     });
                 }
             },
@@ -97,19 +96,18 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.swing_duration {
                     // Swings
                     update.character = CharacterState::Shockwave(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
+                        ..*self
                     });
                 } else {
                     // Transitions to recover
                     update.character = CharacterState::Shockwave(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
                         stage_section: StageSection::Recover,
+                        ..*self
                     });
                 }
             },
@@ -117,12 +115,11 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.swing_duration {
                     // Recovers
                     update.character = CharacterState::Shockwave(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        stage_section: self.stage_section,
+                        ..*self
                     });
                 } else {
                     // Done
diff --git a/common/src/states/spin_melee.rs b/common/src/states/spin_melee.rs
index d685d5dbb4..14c4bec054 100644
--- a/common/src/states/spin_melee.rs
+++ b/common/src/states/spin_melee.rs
@@ -80,34 +80,27 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.buildup_duration {
                     // Build up
                     update.character = CharacterState::SpinMelee(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        spins_remaining: self.spins_remaining,
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Transitions to swing section of stage
                     update.character = CharacterState::SpinMelee(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
-                        spins_remaining: self.spins_remaining,
                         stage_section: StageSection::Swing,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 }
             },
             StageSection::Swing => {
                 if !self.exhausted {
                     update.character = CharacterState::SpinMelee(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
-                        spins_remaining: self.spins_remaining,
-                        stage_section: self.stage_section,
                         exhausted: true,
+                        ..*self
                     });
                     // Hit attempt
                     data.updater.insert(data.entity, Attacking {
@@ -136,14 +129,11 @@ impl CharacterBehavior for Data {
 
                     // Swings
                     update.character = CharacterState::SpinMelee(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        spins_remaining: self.spins_remaining,
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else if update.energy.current() >= self.static_data.energy_cost
                     && (self.spins_remaining != 0
@@ -155,11 +145,10 @@ impl CharacterBehavior for Data {
                         self.spins_remaining - 1
                     };
                     update.character = CharacterState::SpinMelee(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
                         spins_remaining: new_spins_remaining,
-                        stage_section: self.stage_section,
                         exhausted: false,
+                        ..*self
                     });
                     // Consumes energy if there's enough left and RMB is held down
                     update.energy.change_by(
@@ -169,11 +158,9 @@ impl CharacterBehavior for Data {
                 } else {
                     // Transitions to recover section of stage
                     update.character = CharacterState::SpinMelee(Data {
-                        static_data: self.static_data,
                         timer: Duration::default(),
-                        spins_remaining: self.spins_remaining,
                         stage_section: StageSection::Recover,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 }
             },
@@ -181,14 +168,11 @@ impl CharacterBehavior for Data {
                 if self.timer < self.static_data.recover_duration {
                     // Recover
                     update.character = CharacterState::SpinMelee(Data {
-                        static_data: self.static_data,
                         timer: self
                             .timer
                             .checked_add(Duration::from_secs_f32(data.dt.0))
                             .unwrap_or_default(),
-                        spins_remaining: self.spins_remaining,
-                        stage_section: self.stage_section,
-                        exhausted: self.exhausted,
+                        ..*self
                     });
                 } else {
                     // Done
diff --git a/common/src/sys/beam.rs b/common/src/sys/beam.rs
index e6abc2ecf0..ea6f5dd3b2 100644
--- a/common/src/sys/beam.rs
+++ b/common/src/sys/beam.rs
@@ -168,10 +168,8 @@ impl<'a> System<'a> for Sys {
                         continue;
                     }
 
-                    let damage = if !same_group && beam_segment.damages.enemy.is_some() {
-                        beam_segment.damages.enemy.unwrap()
-                    } else if same_group && beam_segment.damages.group.is_some() {
-                        beam_segment.damages.group.unwrap()
+                    let damage = if let Some(damage) = beam_segment.damages.get_damage(same_group) {
+                        damage
                     } else {
                         continue;
                     };
@@ -182,7 +180,7 @@ impl<'a> System<'a> for Sys {
 
                     let change = damage.modify_damage(block, loadouts.get(b), beam_segment.owner);
 
-                    if let Damage::Energy(_) = damage {
+                    if matches!(damage, Damage::Healing(_)) {
                         server_emitter.emit(ServerEvent::Damage {
                             uid: *uid_b,
                             change,
@@ -205,21 +203,18 @@ impl<'a> System<'a> for Sys {
                                 EnergySource::HitEnemy,
                             );
                         }
-                    }
-                    if let Damage::Healing(_) = damage {
-                        if let Some(energy_mut) = beam_owner.and_then(|o| energies.get_mut(o)) {
-                            if energy_mut
-                                .try_change_by(
-                                    -(beam_segment.energy_cost as i32), // Stamina use
-                                    EnergySource::Ability,
-                                )
-                                .is_ok()
-                            {
-                                server_emitter.emit(ServerEvent::Damage {
-                                    uid: *uid_b,
-                                    change,
-                                });
-                            }
+                    } else if let Some(energy_mut) = beam_owner.and_then(|o| energies.get_mut(o)) {
+                        if energy_mut
+                            .try_change_by(
+                                -(beam_segment.energy_cost as i32), // Stamina use
+                                EnergySource::Ability,
+                            )
+                            .is_ok()
+                        {
+                            server_emitter.emit(ServerEvent::Damage {
+                                uid: *uid_b,
+                                change,
+                            });
                         }
                     }
                     // Adds entities that were hit to the hit_entities list on the beam, sees if it
diff --git a/common/src/sys/melee.rs b/common/src/sys/melee.rs
index 35b77e8b61..b974ca6bd4 100644
--- a/common/src/sys/melee.rs
+++ b/common/src/sys/melee.rs
@@ -11,7 +11,6 @@ use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage}
 use std::time::Duration;
 use vek::*;
 
-pub const BLOCK_EFFICIENCY: f32 = 0.9;
 pub const BLOCK_ANGLE: f32 = 180.0;
 
 /// This system is responsible for handling accepted inputs like moving or
@@ -111,10 +110,8 @@ impl<'a> System<'a> for Sys {
                         .map(|group_a| Some(group_a) == groups.get(b))
                         .unwrap_or(false);
 
-                    let damage = if !same_group && attack.damages.enemy.is_some() {
-                        attack.damages.enemy.unwrap()
-                    } else if same_group && attack.damages.group.is_some() {
-                        attack.damages.group.unwrap()
+                    let damage = if let Some(damage) = attack.damages.get_damage(same_group) {
+                        damage
                     } else {
                         continue;
                     };
@@ -152,10 +149,10 @@ impl<'a> System<'a> for Sys {
 
                     if change.amount != 0 {
                         let kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
-                        server_emitter.emit(ServerEvent::Knockback {
-                            entity: b,
-                            impulse: attack.knockback.get_knockback(kb_dir),
-                        });
+                        let impulse = attack.knockback.calculate_impulse(kb_dir);
+                        if !impulse.is_approx_zero() {
+                            server_emitter.emit(ServerEvent::Knockback { entity: b, impulse });
+                        }
                     }
                 }
             }
diff --git a/common/src/sys/projectile.rs b/common/src/sys/projectile.rs
index 725919c551..cfae192ec9 100644
--- a/common/src/sys/projectile.rs
+++ b/common/src/sys/projectile.rs
@@ -100,10 +100,8 @@ impl<'a> System<'a> for Sys {
                             if Some(other) == projectile.owner {
                                 continue;
                             }
-                            let damage = if !same_group && damages.enemy.is_some() {
-                                damages.enemy.unwrap()
-                            } else if same_group && damages.group.is_some() {
-                                damages.group.unwrap()
+                            let damage = if let Some(damage) = damages.get_damage(same_group) {
+                                damage
                             } else {
                                 continue;
                             };
@@ -121,10 +119,11 @@ impl<'a> System<'a> for Sys {
                             if let Some(entity) =
                                 uid_allocator.retrieve_entity_internal(other.into())
                             {
-                                local_emitter.emit(LocalEvent::ApplyImpulse {
-                                    entity,
-                                    impulse: knockback.get_knockback(ori.0),
-                                });
+                                let impulse = knockback.calculate_impulse(ori.0);
+                                if !impulse.is_approx_zero() {
+                                    local_emitter
+                                        .emit(LocalEvent::ApplyImpulse { entity, impulse });
+                                }
                             }
                         },
                         projectile::Effect::RewardEnergy(energy) => {
diff --git a/common/src/sys/shockwave.rs b/common/src/sys/shockwave.rs
index 35a24691a7..0da87146bf 100644
--- a/common/src/sys/shockwave.rs
+++ b/common/src/sys/shockwave.rs
@@ -192,10 +192,8 @@ impl<'a> System<'a> for Sys {
                     && (!shockwave.requires_ground || physics_state_b.on_ground);
 
                 if hit {
-                    let damage = if !same_group && shockwave.damages.enemy.is_some() {
-                        shockwave.damages.enemy.unwrap()
-                    } else if same_group && shockwave.damages.group.is_some() {
-                        shockwave.damages.group.unwrap()
+                    let damage = if let Some(damage) = shockwave.damages.get_damage(same_group) {
+                        damage
                     } else {
                         continue;
                     };
@@ -213,10 +211,10 @@ impl<'a> System<'a> for Sys {
                         });
                         shockwave_hit_list.hit_entities.push(*uid_b);
                         let kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
-                        server_emitter.emit(ServerEvent::Knockback {
-                            entity: b,
-                            impulse: shockwave.knockback.get_knockback(kb_dir),
-                        });
+                        let impulse = shockwave.knockback.calculate_impulse(kb_dir);
+                        if !impulse.is_approx_zero() {
+                            server_emitter.emit(ServerEvent::Knockback { entity: b, impulse });
+                        }
                     }
                 }
             }