Merge branch 'sam/small-fixes' into 'master'

Sam/small fixes part 1

Closes #525, #410, #412, and #397

See merge request veloren/veloren!1455
This commit is contained in:
Samuel Keiffer 2020-10-29 04:03:07 +00:00
commit 6566802b7f
41 changed files with 1228 additions and 1121 deletions

170
common/src/combat.rs Normal file
View File

@ -0,0 +1,170 @@
use crate::{
comp::{HealthChange, HealthSource, Loadout},
sync::Uid,
util::Dir,
};
use serde::{Deserialize, Serialize};
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)]
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 = loadout.map_or(0.0, |l| l.get_damage_reduction());
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 = loadout.map_or(0.0, |l| l.get_damage_reduction());
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 = loadout.map_or(0.0, |l| l.get_damage_reduction());
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 = loadout.map_or(0.0, |l| l.get_damage_reduction());
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 = loadout.map_or(0.0, |l| l.get_damage_reduction());
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 = loadout.map_or(0.0, |l| l.get_damage_reduction());
if (damage_reduction - 1.0).abs() < f32::EPSILON {
damage = 0.0;
}
HealthChange {
amount: -damage as i32,
cause: HealthSource::World,
}
},
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Knockback {
Away(f32),
Towards(f32),
Up(f32),
TowardsUp(f32),
}
impl Knockback {
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) => {
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)
},
}
}
}

View File

@ -8,6 +8,7 @@ use crate::{
*,
},
sys::character_behavior::JoinData,
Knockback,
};
use arraygen::Arraygen;
use serde::{Deserialize, Serialize};
@ -59,16 +60,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 +92,7 @@ pub enum CharacterAbility {
reps_remaining: u32,
},
Boost {
duration: Duration,
movement_duration: Duration,
only_up: bool,
},
DashMelee {
@ -169,7 +170,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,
@ -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,
@ -234,10 +235,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 +254,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)
@ -356,24 +365,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 +396,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: _,
@ -431,14 +453,21 @@ 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,
}),
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 +595,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 +604,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: _,

View File

@ -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,

View File

@ -3,6 +3,7 @@ use crate::{
event::{LocalEvent, ServerEvent},
states::*,
sys::character_behavior::JoinData,
Damages, Knockback,
};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage, VecStorage};
@ -152,13 +153,12 @@ 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,
pub hit_count: u32,
pub knockback: f32,
pub knockback: Knockback,
}
impl Component for Attacking {

View File

@ -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;
},
_ => {},
}
}
}

View File

@ -4,7 +4,7 @@
use crate::{
comp::{body::object, projectile, Body, CharacterAbility, Gravity, LightEmitter, Projectile},
states::combo_melee,
Explosion,
Damage, Damages, Explosion, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -174,15 +174,15 @@ 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_knockback: 10.0,
max_knockback: 20.0,
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,
@ -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,14 +293,16 @@ 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],
hit_entity: vec![
projectile::Effect::Damage((-40.0 * self.base_power()) as i32),
projectile::Effect::Knockback(10.0),
projectile::Effect::Damages(Damages::new(
Some(Damage::Projectile(40.0 * self.base_power())),
None,
)),
projectile::Effect::Knockback(Knockback::Away(10.0)),
projectile::Effect::RewardEnergy(50),
projectile::Effect::Vanish,
],
@ -317,7 +322,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),
@ -332,13 +337,15 @@ 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![
projectile::Effect::Damage((-40.0 * self.base_power()) as i32),
projectile::Effect::Knockback(10.0),
projectile::Effect::RewardEnergy(50),
projectile::Effect::Damages(Damages::new(
Some(Damage::Projectile(40.0 * self.base_power())),
None,
)),
projectile::Effect::Knockback(Knockback::Away(10.0)),
projectile::Effect::Vanish,
],
time_left: Duration::from_secs(15),
@ -355,8 +362,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 +386,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 +429,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![
@ -482,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,
@ -495,8 +501,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 +515,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,
},
@ -521,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,
@ -533,10 +541,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 +553,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,

View File

@ -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;

View File

@ -1,4 +1,4 @@
use crate::{sync::Uid, Explosion};
use crate::{sync::Uid, Damages, Explosion, Knockback};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
@ -6,8 +6,8 @@ use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Effect {
Damage(i32),
Knockback(f32),
Damages(Damages),
Knockback(Knockback),
RewardEnergy(u32),
Explode(Explosion),
Vanish,

View File

@ -1,4 +1,4 @@
use crate::sync::Uid;
use crate::{sync::Uid, Damages, Knockback};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
@ -9,8 +9,8 @@ pub struct Properties {
pub angle: f32,
pub vertical_angle: f32,
pub speed: f32,
pub damage: u32,
pub knockback: f32,
pub damages: Damages,
pub knockback: Knockback,
pub requires_ground: bool,
pub duration: Duration,
pub owner: Option<Uid>,

View File

@ -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, Knockback};
pub use explosion::Explosion;
pub use loadout_builder::LoadoutBuilder;

View File

@ -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,

View File

@ -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,
}

View File

@ -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;
@ -73,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
@ -96,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
});
}
},
@ -108,10 +107,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 +122,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,
@ -137,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
@ -154,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

View File

@ -2,24 +2,39 @@ use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate},
states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
};
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 +46,87 @@ 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 {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Transitions to swing section of stage
update.character = CharacterState::BasicMelee(Data {
timer: Duration::default(),
stage_section: StageSection::Swing,
..*self
});
}
},
StageSection::Swing => {
if !self.exhausted {
update.character = CharacterState::BasicMelee(Data {
timer: Duration::default(),
exhausted: true,
..*self
});
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 {
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,
hit_count: 0,
knockback: Knockback::Away(self.static_data.knockback),
});
} else if self.timer < self.static_data.swing_duration {
// Swings
update.character = CharacterState::BasicMelee(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Transitions to recover section of stage
update.character = CharacterState::BasicMelee(Data {
timer: Duration::default(),
stage_section: StageSection::Recover,
..*self
});
}
},
StageSection::Recover => {
if self.timer < self.static_data.recover_duration {
// Recovery
update.character = CharacterState::BasicMelee(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} 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

View File

@ -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,67 @@ 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(),
..*self
});
} 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,
..*self
});
}
},
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(),
exhausted: true,
..*self
});
} 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(),
..*self
});
} else {
// Done
update.character = CharacterState::Wielding;
}
},
_ => {
// If it somehow ends up in an incorrect stage section
update.character = CharacterState::Wielding;
},
}
update

View File

@ -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))
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
only_up: self.only_up,
..*self
});
}
// 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
}
}

View File

@ -2,6 +2,7 @@ use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate},
states::utils::{StageSection, *},
sys::character_behavior::*,
Damage, Damages, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -67,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
@ -87,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
@ -105,65 +101,55 @@ 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
});
}
},
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,
hit_count: 0,
knockback,
knockback: Knockback::Away(knockback),
});
// 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

View File

@ -6,14 +6,20 @@ use crate::{
event::ServerEvent,
states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
};
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 +30,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 +38,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 +58,127 @@ 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 {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Transitions to swing section of stage
update.character = CharacterState::ChargedRanged(Data {
timer: Duration::default(),
stage_section: StageSection::Charge,
..*self
});
}
},
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::Damages(Damages::new(
Some(Damage::Projectile(damage)),
None,
)),
projectile::Effect::Knockback(Knockback::Away(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 {
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 {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
// 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 {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
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 {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Done
update.character = CharacterState::Wielding;
}
},
_ => {
// If it somehow ends up in an incorrect stage section
update.character = CharacterState::Wielding;
},
}
update

View File

@ -2,6 +2,7 @@ use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate},
states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -99,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(
@ -110,51 +109,50 @@ 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
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,
hit_count: 0,
knockback: self.static_data.stage_data[stage_index].knockback,
knockback: Knockback::Away(
self.static_data.stage_data[stage_index].knockback,
),
});
}
},
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
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(
@ -164,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
});
}
},
@ -186,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(
@ -200,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(
@ -220,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 {
@ -229,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

View File

@ -2,6 +2,7 @@ use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate},
states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -46,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
@ -78,117 +81,93 @@ impl CharacterBehavior for Data {
if self.timer < self.static_data.buildup_duration {
// Build up
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: self.stage_section,
exhausted: self.exhausted,
..*self
});
} else {
// 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(),
stage_section: StageSection::Charge,
exhausted: self.exhausted,
..*self
});
}
},
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 {
base_damage: damage as u32,
base_heal: 0,
range: self.static_data.range,
max_angle: self.static_data.angle.to_radians(),
applied: false,
hit_count: 0,
knockback,
});
}
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
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: Knockback::Away(knockback),
});
}
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: true,
..*self
})
} else if self.refresh_timer < Duration::from_millis(50) {
update.character = CharacterState::DashMelee(Data {
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(),
..*self
})
} 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 {
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,
refresh_timer: Duration::default(),
exhausted: false,
..*self
})
}
@ -200,11 +179,9 @@ impl CharacterBehavior for Data {
} else {
// Transitions to swing section of stage
update.character = CharacterState::DashMelee(Data {
static_data: self.static_data,
end_charge: self.end_charge,
timer: Duration::default(),
stage_section: StageSection::Swing,
exhausted: self.exhausted,
..*self
});
}
},
@ -212,23 +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,
end_charge: self.end_charge,
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::DashMelee(Data {
static_data: self.static_data,
end_charge: self.end_charge,
timer: Duration::default(),
stage_section: StageSection::Recover,
exhausted: self.exhausted,
..*self
});
}
},
@ -236,14 +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,
end_charge: self.end_charge,
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

View File

@ -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))
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Done
update.character = CharacterState::Wielding;
}
update

View File

@ -2,10 +2,10 @@ use crate::{
comp::{Attacking, CharacterState, StateUpdate},
states::utils::{StageSection, *},
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
};
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)]
@ -75,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
@ -141,13 +133,15 @@ 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,
hit_count: 0,
knockback: self.static_data.knockback,
knockback: Knockback::Away(self.static_data.knockback),
});
update.character = CharacterState::LeapMelee(Data {

View File

@ -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 {
@ -71,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
@ -80,14 +84,19 @@ impl CharacterBehavior for Data {
static_data: self.static_data.clone(),
timer: Duration::default(),
stage_section: StageSection::Buildup,
reps_remaining: self.reps_remaining,
..*self
});
}
},
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
@ -97,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
@ -106,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
});
}
},
@ -152,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
@ -163,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
@ -172,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
});
}
},
@ -188,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

View File

@ -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,72 @@ 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 {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Transitions to movement section of stage
update.character = CharacterState::Roll(Data {
timer: Duration::default(),
stage_section: StageSection::Movement,
..*self
});
}
},
StageSection::Movement => {
if self.timer < self.static_data.movement_duration {
// Movement
update.character = CharacterState::Roll(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} else {
// Transitions to recover section of stage
update.character = CharacterState::Roll(Data {
timer: Duration::default(),
stage_section: StageSection::Recover,
..*self
});
}
},
StageSection::Recover => {
if self.timer < self.static_data.recover_duration {
// Build up
update.character = CharacterState::Roll(Data {
timer: self
.timer
.checked_add(Duration::from_secs_f32(data.dt.0))
.unwrap_or_default(),
..*self
});
} 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

View File

@ -3,6 +3,7 @@ use crate::{
event::ServerEvent,
states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -19,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
@ -56,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
@ -70,7 +70,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),
@ -83,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
});
}
},
@ -93,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
});
}
},
@ -113,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

View File

@ -1,7 +1,11 @@
use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate},
states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData},
sys::{
character_behavior::{CharacterBehavior, JoinData},
phys::GRAVITY,
},
Damage, Damages, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -56,7 +60,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
@ -75,61 +80,60 @@ 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 {
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,
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 {
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);
}
// 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
@ -141,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(
@ -155,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
});
}
},
@ -167,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

View File

@ -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;
@ -90,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);
}
@ -161,7 +198,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;
@ -385,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,
},
}

View File

@ -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,29 @@ 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 let Some(damage) = beam_segment.damages.get_damage(same_group) {
damage
} 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 matches!(damage, Damage::Healing(_)) {
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,
@ -227,26 +203,18 @@ impl<'a> System<'a> for Sys {
EnergySource::HitEnemy,
);
}
}
if is_heal {
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: HealthChange {
amount: damage.healthchange as i32,
cause: HealthSource::Healing {
by: beam_segment.owner,
},
},
});
}
} 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

View File

@ -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,
@ -14,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
@ -59,7 +55,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 +109,34 @@ 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 let Some(damage) = attack.damages.get_damage(same_group) {
damage
} 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,18 +146,18 @@ impl<'a> System<'a> for Sys {
}
attack.hit_count += 1;
}
if attack.knockback != 0.0 && damage.healthchange != 0.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),
});
let impulse = attack.knockback.calculate_impulse(kb_dir);
if !impulse.is_approx_zero() {
server_emitter.emit(ServerEvent::Knockback { entity: b, impulse });
}
}
}
}
}
sys_metrics.combat_ns.store(
sys_metrics.melee_ns.store(
start_time.elapsed().as_nanos() as i64,
std::sync::atomic::Ordering::Relaxed,
);

View File

@ -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]);
}

View File

@ -1,20 +1,18 @@
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,
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;
@ -73,20 +71,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,51 +96,34 @@ 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 let Some(damage) = damages.get_damage(same_group) {
damage
} 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) => {
if let Some(entity) =
uid_allocator.retrieve_entity_internal(other.into())
{
local_emitter.emit(LocalEvent::ApplyImpulse {
entity,
impulse: knockback
* *Dir::slerp(ori.0, Dir::new(Vec3::unit_z()), 0.5),
});
let impulse = knockback.calculate_impulse(ori.0);
if !impulse.is_approx_zero() {
local_emitter
.emit(LocalEvent::ApplyImpulse { entity, impulse });
}
}
},
projectile::Effect::RewardEnergy(energy) => {

View File

@ -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,51 +189,32 @@ 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 let Some(damage) = shockwave.damages.get_damage(same_group) {
damage
} 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 {
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 });
let impulse = shockwave.knockback.calculate_impulse(kb_dir);
if !impulse.is_approx_zero() {
server_emitter.emit(ServerEvent::Knockback { entity: b, impulse });
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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,

View File

@ -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)

View File

@ -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,
},

View File

@ -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);