Merge branch 'sam/attack-effects' into 'master'

Centralized attack handling

Closes #818

See merge request veloren/veloren!1746
This commit is contained in:
Samuel Keiffer 2021-02-02 22:27:43 +00:00
commit 5bb1e4a808
32 changed files with 1116 additions and 710 deletions

View File

@ -5,7 +5,7 @@ Shockwave(
recover_duration: 300,
damage: 200,
poise_damage: 0,
knockback: Away(25.0),
knockback: ( strength: 25.0, direction: Away),
shockwave_angle: 360.0,
shockwave_vertical_angle: 90.0,
shockwave_speed: 20.0,

View File

@ -5,7 +5,7 @@ Shockwave(
recover_duration: 800,
damage: 500,
poise_damage: 0,
knockback: TowardsUp(40.0),
knockback: (strength: 40.0, direction: TowardsUp),
shockwave_angle: 90.0,
shockwave_vertical_angle: 90.0,
shockwave_speed: 50.0,

View File

@ -1,5 +1,6 @@
use crate::{
comp::{
buff::{Buff, BuffChange, BuffData, BuffKind, BuffSource},
inventory::{
item::{
armor::Protection,
@ -8,13 +9,19 @@ use crate::{
},
slot::EquipSlot,
},
poise::PoiseChange,
skills::{SkillGroupKind, SkillSet},
Body, BuffKind, Health, HealthChange, HealthSource, Inventory, Stats,
Body, Energy, EnergyChange, EnergySource, Health, HealthChange, HealthSource, Inventory,
Stats,
},
event::ServerEvent,
uid::Uid,
util::Dir,
};
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
use specs::Entity as EcsEntity;
use std::time::Duration;
use vek::*;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
@ -23,11 +30,338 @@ pub enum GroupTarget {
OutOfGroup,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[derive(Copy, Clone)]
pub struct AttackerInfo<'a> {
pub entity: EcsEntity,
pub uid: Uid,
pub energy: Option<&'a Energy>,
}
#[derive(Clone, Debug, Serialize, Deserialize)] // TODO: Yeet clone derive
pub struct Attack {
damages: Vec<AttackDamage>,
effects: Vec<AttackEffect>,
crit_chance: f32,
crit_multiplier: f32,
}
impl Default for Attack {
fn default() -> Self {
Self {
damages: Vec::new(),
effects: Vec::new(),
crit_chance: 0.0,
crit_multiplier: 1.0,
}
}
}
impl Attack {
pub fn with_damage(mut self, damage: AttackDamage) -> Self {
self.damages.push(damage);
self
}
pub fn with_effect(mut self, effect: AttackEffect) -> Self {
self.effects.push(effect);
self
}
pub fn with_crit(mut self, crit_chance: f32, crit_multiplier: f32) -> Self {
self.crit_chance = crit_chance;
self.crit_multiplier = crit_multiplier;
self
}
pub fn effects(&self) -> impl Iterator<Item = &AttackEffect> { self.effects.iter() }
#[allow(clippy::too_many_arguments)]
pub fn apply_attack(
&self,
target_group: GroupTarget,
attacker_info: Option<AttackerInfo>,
target_entity: EcsEntity,
target_inventory: Option<&Inventory>,
dir: Dir,
target_dodging: bool,
// Currently just modifies damage, maybe look into modifying strength of other effects?
strength_modifier: f32,
mut emit: impl FnMut(ServerEvent),
) {
let is_crit = thread_rng().gen::<f32>() < self.crit_chance;
let mut accumulated_damage = 0.0;
for damage in self
.damages
.iter()
.filter(|d| d.target.map_or(true, |t| t == target_group))
.filter(|d| !(matches!(d.target, Some(GroupTarget::OutOfGroup)) && target_dodging))
{
let change = damage.damage.calculate_health_change(
target_inventory,
attacker_info.map(|a| a.uid),
is_crit,
self.crit_multiplier,
strength_modifier,
);
let applied_damage = -change.amount as f32;
accumulated_damage += applied_damage;
if change.amount != 0 {
emit(ServerEvent::Damage {
entity: target_entity,
change,
});
for effect in damage.effects.iter() {
match effect {
CombatEffect::Knockback(kb) => {
let impulse = kb.calculate_impulse(dir);
if !impulse.is_approx_zero() {
emit(ServerEvent::Knockback {
entity: target_entity,
impulse,
});
}
},
CombatEffect::EnergyReward(ec) => {
if let Some(attacker_entity) = attacker_info.map(|a| a.entity) {
emit(ServerEvent::EnergyChange {
entity: attacker_entity,
change: EnergyChange {
amount: *ec as i32,
source: EnergySource::HitEnemy,
},
});
}
},
CombatEffect::Buff(b) => {
if thread_rng().gen::<f32>() < b.chance {
emit(ServerEvent::Buff {
entity: target_entity,
buff_change: BuffChange::Add(
b.to_buff(attacker_info.map(|a| a.uid), applied_damage),
),
});
}
},
CombatEffect::Lifesteal(l) => {
if let Some(attacker_entity) = attacker_info.map(|a| a.entity) {
let change = HealthChange {
amount: (applied_damage * l) as i32,
cause: HealthSource::Heal {
by: attacker_info.map(|a| a.uid),
},
};
if change.amount != 0 {
emit(ServerEvent::Damage {
entity: attacker_entity,
change,
});
}
}
},
CombatEffect::Poise(p) => {
let change = PoiseChange::from_value(*p, target_inventory);
if change.amount != 0 {
emit(ServerEvent::PoiseChange {
entity: target_entity,
change,
kb_dir: *dir,
});
}
},
CombatEffect::Heal(h) => {
let change = HealthChange {
amount: *h as i32,
cause: HealthSource::Heal {
by: attacker_info.map(|a| a.uid),
},
};
if change.amount != 0 {
emit(ServerEvent::Damage {
entity: target_entity,
change,
});
}
},
}
}
}
}
for effect in self
.effects
.iter()
.filter(|e| e.target.map_or(true, |t| t == target_group))
.filter(|e| !(matches!(e.target, Some(GroupTarget::OutOfGroup)) && target_dodging))
{
if match &effect.requirement {
Some(CombatRequirement::AnyDamage) => accumulated_damage > 0.0,
Some(CombatRequirement::SufficientEnergy(r)) => {
if let Some(AttackerInfo {
entity,
energy: Some(e),
..
}) = attacker_info
{
let sufficient_energy = e.current() >= *r;
if sufficient_energy {
emit(ServerEvent::EnergyChange {
entity,
change: EnergyChange {
amount: -(*r as i32),
source: EnergySource::Ability,
},
});
}
sufficient_energy
} else {
false
}
},
None => true,
} {
match effect.effect {
CombatEffect::Knockback(kb) => {
let impulse = kb.calculate_impulse(dir);
if !impulse.is_approx_zero() {
emit(ServerEvent::Knockback {
entity: target_entity,
impulse,
});
}
},
CombatEffect::EnergyReward(ec) => {
if let Some(attacker_entity) = attacker_info.map(|a| a.entity) {
emit(ServerEvent::EnergyChange {
entity: attacker_entity,
change: EnergyChange {
amount: ec as i32,
source: EnergySource::HitEnemy,
},
});
}
},
CombatEffect::Buff(b) => {
if thread_rng().gen::<f32>() < b.chance {
emit(ServerEvent::Buff {
entity: target_entity,
buff_change: BuffChange::Add(
b.to_buff(attacker_info.map(|a| a.uid), accumulated_damage),
),
});
}
},
CombatEffect::Lifesteal(l) => {
if let Some(attacker_entity) = attacker_info.map(|a| a.entity) {
let change = HealthChange {
amount: (accumulated_damage * l) as i32,
cause: HealthSource::Heal {
by: attacker_info.map(|a| a.uid),
},
};
if change.amount != 0 {
emit(ServerEvent::Damage {
entity: attacker_entity,
change,
});
}
}
},
CombatEffect::Poise(p) => {
let change = PoiseChange::from_value(p, target_inventory);
if change.amount != 0 {
emit(ServerEvent::PoiseChange {
entity: target_entity,
change,
kb_dir: *dir,
});
}
},
CombatEffect::Heal(h) => {
let change = HealthChange {
amount: h as i32,
cause: HealthSource::Heal {
by: attacker_info.map(|a| a.uid),
},
};
if change.amount != 0 {
emit(ServerEvent::Damage {
entity: target_entity,
change,
});
}
},
}
}
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AttackDamage {
damage: Damage,
target: Option<GroupTarget>,
effects: Vec<CombatEffect>,
}
impl AttackDamage {
pub fn new(damage: Damage, target: Option<GroupTarget>) -> Self {
Self {
damage,
target,
effects: Vec::new(),
}
}
pub fn with_effect(mut self, effect: CombatEffect) -> Self {
self.effects.push(effect);
self
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AttackEffect {
target: Option<GroupTarget>,
effect: CombatEffect,
requirement: Option<CombatRequirement>,
}
impl AttackEffect {
pub fn new(target: Option<GroupTarget>, effect: CombatEffect) -> Self {
Self {
target,
effect,
requirement: None,
}
}
pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self {
self.requirement = Some(requirement);
self
}
pub fn effect(&self) -> &CombatEffect { &self.effect }
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum CombatEffect {
Heal(f32),
Buff(CombatBuff),
Knockback(Knockback),
EnergyReward(u32),
Lifesteal(f32),
Poise(f32),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum CombatRequirement {
AnyDamage,
SufficientEnergy(u32),
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum DamageSource {
Buff(BuffKind),
Melee,
Healing,
Projectile,
Explosion,
Falling,
@ -65,15 +399,22 @@ impl Damage {
}
}
pub fn modify_damage(self, inventory: Option<&Inventory>, uid: Option<Uid>) -> HealthChange {
let mut damage = self.value;
let damage_reduction = inventory.map_or(0.0, |inv| Damage::compute_damage_reduction(inv));
pub fn calculate_health_change(
self,
inventory: Option<&Inventory>,
uid: Option<Uid>,
is_crit: bool,
crit_mult: f32,
damage_modifier: f32,
) -> HealthChange {
let mut damage = self.value * damage_modifier;
let damage_reduction = inventory.map_or(0.0, Damage::compute_damage_reduction);
match self.source {
DamageSource::Melee => {
// Critical hit
let mut critdamage = 0.0;
if rand::random() {
critdamage = damage * 0.3;
if is_crit {
critdamage = damage * (crit_mult - 1.0);
}
// Armor
damage *= 1.0 - damage_reduction;
@ -93,8 +434,8 @@ impl Damage {
},
DamageSource::Projectile => {
// Critical hit
if rand::random() {
damage *= 1.2;
if is_crit {
damage *= crit_mult;
}
// Armor
damage *= 1.0 - damage_reduction;
@ -143,10 +484,6 @@ impl Damage {
},
}
},
DamageSource::Healing => HealthChange {
amount: damage as i32,
cause: HealthSource::Heal { by: uid },
},
DamageSource::Falling => {
// Armor
if (damage_reduction - 1.0).abs() < f32::EPSILON {
@ -181,38 +518,91 @@ impl Damage {
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Knockback {
Away(f32),
Towards(f32),
Up(f32),
TowardsUp(f32),
pub struct Knockback {
pub direction: KnockbackDir,
pub strength: f32,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum KnockbackDir {
Away,
Towards,
Up,
TowardsUp,
}
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)
match self.direction {
KnockbackDir::Away => self.strength * *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
KnockbackDir::Towards => {
self.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)
KnockbackDir::Up => self.strength * Vec3::unit_z(),
KnockbackDir::TowardsUp => {
self.strength * *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.85)
},
}
}
pub fn modify_strength(mut self, power: f32) -> Self {
use Knockback::*;
match self {
Away(ref mut f) | Towards(ref mut f) | Up(ref mut f) | TowardsUp(ref mut f) => {
*f *= power;
},
}
self.strength *= power;
self
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct CombatBuff {
pub kind: BuffKind,
pub dur_secs: f32,
pub strength: CombatBuffStrength,
pub chance: f32,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum CombatBuffStrength {
DamageFraction(f32),
Value(f32),
}
impl CombatBuffStrength {
fn to_strength(self, damage: f32) -> f32 {
match self {
CombatBuffStrength::DamageFraction(f) => damage * f,
CombatBuffStrength::Value(v) => v,
}
}
}
impl CombatBuff {
fn to_buff(self, uid: Option<Uid>, damage: f32) -> Buff {
// TODO: Generate BufCategoryId vec (probably requires damage overhaul?)
let source = if let Some(uid) = uid {
BuffSource::Character { by: uid }
} else {
BuffSource::Unknown
};
Buff::new(
self.kind,
BuffData::new(
self.strength.to_strength(damage),
Some(Duration::from_secs_f32(self.dur_secs)),
),
Vec::new(),
source,
)
}
pub fn default_physical() -> Self {
Self {
kind: BuffKind::Bleeding,
dur_secs: 10.0,
strength: CombatBuffStrength::DamageFraction(0.1),
chance: 0.1,
}
}
}
fn equipped_tool(inv: &Inventory, slot: EquipSlot) -> Option<&Tool> {
inv.equipped(slot).and_then(|i| {
if let ItemKind::Tool(tool) = &i.kind() {

View File

@ -1,22 +1,20 @@
use crate::{uid::Uid, Damage, GroupTarget};
use crate::{combat::Attack, uid::Uid};
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage;
use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Properties {
pub attack: Attack,
pub angle: f32,
pub speed: f32,
pub damages: Vec<(Option<GroupTarget>, Damage)>,
pub lifesteal_eff: f32,
pub energy_regen: u32,
pub energy_cost: u32,
pub duration: Duration,
pub owner: Option<Uid>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
// TODO: Separate components out for cheaper network syncing
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BeamSegment {
pub properties: Properties,
#[serde(skip)]

View File

@ -1,8 +1,8 @@
use crate::{
comp::{Energy, Ori, PoiseChange, Pos, Vel},
combat::Attack,
comp::{Energy, Ori, Pos, Vel},
event::{LocalEvent, ServerEvent},
states::{behavior::JoinData, *},
Damage, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage, VecStorage};
@ -169,16 +169,15 @@ impl Component for CharacterState {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Attacking {
pub effects: Vec<(Option<GroupTarget>, Damage, PoiseChange)>,
#[derive(Debug, Serialize, Deserialize)]
pub struct Melee {
pub attack: Attack,
pub range: f32,
pub max_angle: f32,
pub applied: bool,
pub hit_count: u32,
pub knockback: Knockback,
}
impl Component for Attacking {
impl Component for Melee {
type Storage = VecStorage<Self>;
}

View File

@ -40,7 +40,7 @@ pub use buff::{
Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffId, BuffKind, BuffSource, Buffs,
ModifierKind,
};
pub use character_state::{Attacking, CharacterState, StateUpdate};
pub use character_state::{CharacterState, Melee, StateUpdate};
pub use chat::{
ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg,
};

View File

@ -21,8 +21,7 @@ pub struct PoiseChange {
impl PoiseChange {
/// Alters poise damage as a result of armor poise damage reduction
pub fn modify_poise_damage(self, inventory: Option<&Inventory>) -> PoiseChange {
let poise_damage_reduction =
inventory.map_or(0.0, |inv| Poise::compute_poise_damage_reduction(inv));
let poise_damage_reduction = inventory.map_or(0.0, Poise::compute_poise_damage_reduction);
let poise_damage = self.amount as f32 * (1.0 - poise_damage_reduction);
// Add match on poise source when different calculations per source
// are needed/wanted
@ -31,6 +30,16 @@ impl PoiseChange {
source: self.source,
}
}
/// Creates a poise change from a float
pub fn from_value(poise_damage: f32, inventory: Option<&Inventory>) -> Self {
let poise_damage_reduction = inventory.map_or(0.0, Poise::compute_poise_damage_reduction);
let poise_change = -poise_damage * (1.0 - poise_damage_reduction);
Self {
amount: poise_change as i32,
source: PoiseSource::Attack,
}
}
}
/// Sources of poise change

View File

@ -1,30 +1,26 @@
use crate::{
comp::buff::{BuffCategory, BuffData, BuffKind},
effect::{self, BuffEffect},
combat::{
Attack, AttackDamage, AttackEffect, CombatBuff, CombatEffect, CombatRequirement, Damage,
DamageSource, GroupTarget, Knockback, KnockbackDir,
},
uid::Uid,
Damage, DamageSource, Explosion, GroupTarget, Knockback, RadiusEffect,
Explosion, RadiusEffect,
};
use serde::{Deserialize, Serialize};
use specs::Component;
use specs_idvs::IdvStorage;
use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub enum Effect {
Damage(Option<GroupTarget>, Damage),
Knockback(Knockback),
RewardEnergy(u32),
Attack(Attack),
Explode(Explosion),
Vanish,
Stick,
Possess,
Buff {
buff: BuffEffect,
chance: Option<f32>,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct Projectile {
// TODO: use SmallVec for these effects
pub hit_solid: Vec<Effect>,
@ -74,29 +70,34 @@ impl ProjectileConstructor {
knockback,
energy_regen,
} => {
let buff = BuffEffect {
kind: BuffKind::Bleeding,
data: BuffData {
strength: damage / 2.0,
duration: Some(Duration::from_secs(5)),
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
strength: knockback,
direction: KnockbackDir::Away,
}),
)
.with_requirement(CombatRequirement::AnyDamage);
let energy = AttackEffect::new(None, CombatEffect::EnergyReward(energy_regen))
.with_requirement(CombatRequirement::AnyDamage);
let buff = CombatEffect::Buff(CombatBuff::default_physical());
let damage = AttackDamage::new(
Damage {
source: DamageSource::Projectile,
value: damage,
},
cat_ids: vec![BuffCategory::Physical],
};
Some(GroupTarget::OutOfGroup),
)
.with_effect(buff);
let attack = Attack::default()
.with_damage(damage)
.with_crit(0.5, 1.2)
.with_effect(energy)
.with_effect(knockback);
Projectile {
hit_solid: vec![Effect::Stick],
hit_entity: vec![
Effect::Damage(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Projectile,
value: damage,
}),
Effect::Knockback(Knockback::Away(knockback)),
Effect::RewardEnergy(energy_regen),
Effect::Vanish,
Effect::Buff {
buff,
chance: Some(0.10),
},
],
hit_entity: vec![Effect::Attack(attack), Effect::Vanish],
time_left: Duration::from_secs(15),
owner,
ignore_group: true,
@ -106,113 +107,80 @@ impl ProjectileConstructor {
damage,
radius,
energy_regen,
} => Projectile {
hit_solid: vec![
Effect::Explode(Explosion {
effects: vec![
RadiusEffect::Entity(
Some(GroupTarget::OutOfGroup),
effect::Effect::Damage(Damage {
source: DamageSource::Explosion,
value: damage,
}),
),
RadiusEffect::TerrainDestruction(2.0),
],
radius,
energy_regen,
}),
Effect::Vanish,
],
hit_entity: vec![
Effect::Explode(Explosion {
effects: vec![RadiusEffect::Entity(
Some(GroupTarget::OutOfGroup),
effect::Effect::Damage(Damage {
source: DamageSource::Explosion,
value: damage,
}),
)],
radius,
energy_regen,
}),
Effect::Vanish,
],
time_left: Duration::from_secs(10),
owner,
ignore_group: true,
} => {
let energy = AttackEffect::new(None, CombatEffect::EnergyReward(energy_regen))
.with_requirement(CombatRequirement::AnyDamage);
let damage = AttackDamage::new(
Damage {
source: DamageSource::Explosion,
value: damage,
},
Some(GroupTarget::OutOfGroup),
);
let attack = Attack::default().with_damage(damage).with_effect(energy);
let explosion = Explosion {
effects: vec![
RadiusEffect::Attack(attack),
RadiusEffect::TerrainDestruction(2.0),
],
radius,
};
Projectile {
hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish],
hit_entity: vec![Effect::Explode(explosion), Effect::Vanish],
time_left: Duration::from_secs(10),
owner,
ignore_group: true,
}
},
Firebolt {
damage,
energy_regen,
} => Projectile {
hit_solid: vec![Effect::Vanish],
hit_entity: vec![
Effect::Damage(Some(GroupTarget::OutOfGroup), Damage {
} => {
let energy = AttackEffect::new(None, CombatEffect::EnergyReward(energy_regen))
.with_requirement(CombatRequirement::AnyDamage);
let damage = AttackDamage::new(
Damage {
source: DamageSource::Energy,
value: damage,
}),
Effect::RewardEnergy(energy_regen),
Effect::Vanish,
],
time_left: Duration::from_secs(10),
owner,
ignore_group: true,
},
Some(GroupTarget::OutOfGroup),
);
let attack = Attack::default().with_damage(damage).with_effect(energy);
Projectile {
hit_solid: vec![Effect::Vanish],
hit_entity: vec![Effect::Attack(attack), Effect::Vanish],
time_left: Duration::from_secs(10),
owner,
ignore_group: true,
}
},
Heal {
heal,
damage,
radius,
} => Projectile {
hit_solid: vec![
Effect::Explode(Explosion {
effects: vec![
RadiusEffect::Entity(
Some(GroupTarget::OutOfGroup),
effect::Effect::Damage(Damage {
source: DamageSource::Explosion,
value: damage,
}),
),
RadiusEffect::Entity(
Some(GroupTarget::InGroup),
effect::Effect::Damage(Damage {
source: DamageSource::Healing,
value: heal,
}),
),
],
radius,
energy_regen: 0,
}),
Effect::Vanish,
],
hit_entity: vec![
Effect::Explode(Explosion {
effects: vec![
RadiusEffect::Entity(
Some(GroupTarget::OutOfGroup),
effect::Effect::Damage(Damage {
source: DamageSource::Explosion,
value: damage,
}),
),
RadiusEffect::Entity(
Some(GroupTarget::InGroup),
effect::Effect::Damage(Damage {
source: DamageSource::Healing,
value: heal,
}),
),
],
radius,
energy_regen: 0,
}),
Effect::Vanish,
],
time_left: Duration::from_secs(10),
owner,
ignore_group: true,
} => {
let damage = AttackDamage::new(
Damage {
source: DamageSource::Explosion,
value: damage,
},
Some(GroupTarget::OutOfGroup),
);
let heal = AttackEffect::new(Some(GroupTarget::InGroup), CombatEffect::Heal(heal));
let attack = Attack::default().with_damage(damage).with_effect(heal);
let explosion = Explosion {
effects: vec![RadiusEffect::Attack(attack)],
radius,
};
Projectile {
hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish],
hit_entity: vec![Effect::Explode(explosion), Effect::Vanish],
time_left: Duration::from_secs(10),
owner,
ignore_group: false,
}
},
Possess => Projectile {
hit_solid: vec![Effect::Stick],

View File

@ -1,22 +1,21 @@
use crate::{comp::PoiseChange, uid::Uid, Damage, GroupTarget, Knockback};
use crate::{combat::Attack, uid::Uid};
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage;
use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Properties {
pub angle: f32,
pub vertical_angle: f32,
pub speed: f32,
pub effects: Vec<(Option<GroupTarget>, Damage, PoiseChange)>,
pub knockback: Knockback,
pub attack: Attack,
pub requires_ground: bool,
pub duration: Duration,
pub owner: Option<Uid>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Shockwave {
pub properties: Properties,
#[serde(skip)]

View File

@ -1,15 +1,15 @@
use crate::{combat::GroupTarget, effect::Effect};
use crate::{combat::Attack, effect::Effect};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Explosion {
pub effects: Vec<RadiusEffect>,
pub radius: f32,
pub energy_regen: u32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum RadiusEffect {
TerrainDestruction(f32),
Entity(Option<GroupTarget>, Effect),
Entity(Effect),
Attack(Attack),
}

View File

@ -55,7 +55,7 @@ pub mod util;
pub mod vol;
pub mod volumes;
pub use combat::{Damage, DamageSource, GroupTarget, Knockback};
pub use combat::{Damage, DamageSource, GroupTarget, Knockback, KnockbackDir};
pub use comp::inventory::loadout_builder::LoadoutBuilder;
pub use explosion::{Explosion, RadiusEffect};
pub use skillset_builder::SkillSetBuilder;

View File

@ -1,4 +1,8 @@
use crate::{
combat::{
Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageSource,
GroupTarget,
},
comp::{beam, Body, CharacterState, EnergyChange, EnergySource, Ori, Pos, StateUpdate},
event::ServerEvent,
states::{
@ -6,7 +10,6 @@ use crate::{
utils::*,
},
uid::Uid,
Damage, DamageSource, GroupTarget,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -119,26 +122,41 @@ 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 = Damage {
source: DamageSource::Energy,
value: self.static_data.base_dps as f32 / self.static_data.tick_rate,
};
let heal = Damage {
source: DamageSource::Healing,
value: self.static_data.base_hps as f32 / self.static_data.tick_rate,
};
let speed =
self.static_data.range / self.static_data.beam_duration.as_secs_f32();
let energy = AttackEffect::new(
None,
CombatEffect::EnergyReward(self.static_data.energy_regen),
)
.with_requirement(CombatRequirement::AnyDamage);
let lifesteal = CombatEffect::Lifesteal(self.static_data.lifesteal_eff);
let damage = AttackDamage::new(
Damage {
source: DamageSource::Energy,
value: self.static_data.base_dps as f32 / self.static_data.tick_rate,
},
Some(GroupTarget::OutOfGroup),
)
.with_effect(lifesteal);
let heal = AttackEffect::new(
Some(GroupTarget::InGroup),
CombatEffect::Heal(
self.static_data.base_hps as f32 / self.static_data.tick_rate,
),
)
.with_requirement(CombatRequirement::SufficientEnergy(
self.static_data.energy_cost,
));
let attack = Attack::default()
.with_damage(damage)
.with_effect(energy)
.with_effect(heal);
let properties = beam::Properties {
attack,
angle: self.static_data.max_angle.to_radians(),
speed,
damages: vec![
(Some(GroupTarget::OutOfGroup), damage),
(Some(GroupTarget::InGroup), heal),
],
lifesteal_eff: self.static_data.lifesteal_eff,
energy_regen: self.static_data.energy_regen,
energy_cost: self.static_data.energy_cost,
duration: self.static_data.beam_duration,
owner: Some(*data.uid),
};

View File

@ -1,13 +1,11 @@
use crate::{
comp::{
Attacking, CharacterState, EnergyChange, EnergySource, PoiseChange, PoiseSource,
StateUpdate,
},
combat::{Attack, AttackDamage, AttackEffect, CombatBuff, CombatEffect, CombatRequirement},
comp::{CharacterState, Melee, StateUpdate},
states::{
behavior::{CharacterBehavior, JoinData},
utils::*,
},
Damage, DamageSource, GroupTarget, Knockback,
Damage, DamageSource, GroupTarget, Knockback, KnockbackDir,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -92,24 +90,44 @@ impl CharacterBehavior for Data {
..*self
});
let poise = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Poise(self.static_data.base_poise_damage as f32),
)
.with_requirement(CombatRequirement::AnyDamage);
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
strength: self.static_data.knockback,
direction: KnockbackDir::Away,
}),
)
.with_requirement(CombatRequirement::AnyDamage);
let energy = AttackEffect::new(None, CombatEffect::EnergyReward(50))
.with_requirement(CombatRequirement::AnyDamage);
let buff = CombatEffect::Buff(CombatBuff::default_physical());
let damage = AttackDamage::new(
Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
},
Some(GroupTarget::OutOfGroup),
)
.with_effect(buff);
let attack = Attack::default()
.with_damage(damage)
.with_crit(0.5, 1.3)
.with_effect(energy)
.with_effect(poise)
.with_effect(knockback);
// Hit attempt
data.updater.insert(data.entity, Attacking {
effects: vec![(
Some(GroupTarget::OutOfGroup),
Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
},
PoiseChange {
amount: -(self.static_data.base_poise_damage as i32),
source: PoiseSource::Attack,
},
)],
data.updater.insert(data.entity, Melee {
attack,
range: self.static_data.range,
max_angle: 180_f32.to_radians(),
max_angle: self.static_data.max_angle,
applied: false,
hit_count: 0,
knockback: Knockback::Away(self.static_data.knockback),
});
} else if self.timer < self.static_data.swing_duration {
// Swings
@ -143,28 +161,17 @@ impl CharacterBehavior for Data {
// Done
update.character = CharacterState::Wielding;
// Make sure attack component is removed
data.updater.remove::<Attacking>(data.entity);
data.updater.remove::<Melee>(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);
data.updater.remove::<Melee>(data.entity);
},
}
// 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(EnergyChange {
amount: 50,
source: EnergySource::HitEnemy,
});
}
}
update
}
}

View File

@ -1,7 +1,7 @@
use crate::{
comp::{
Attacking, Beam, Body, CharacterState, ControlAction, Controller, ControllerInputs, Energy,
Health, Inventory, Ori, PhysicsState, Pos, StateUpdate, Stats, Vel,
Beam, Body, CharacterState, ControlAction, Controller, ControllerInputs, Energy, Health,
Inventory, Melee, Ori, PhysicsState, Pos, StateUpdate, Stats, Vel,
},
resources::DeltaTime,
uid::Uid,
@ -57,7 +57,7 @@ pub struct JoinData<'a> {
pub inventory: &'a Inventory,
pub body: &'a Body,
pub physics: &'a PhysicsState,
pub attacking: Option<&'a Attacking>,
pub melee_attack: Option<&'a Melee>,
pub updater: &'a LazyUpdate,
pub stats: &'a Stats,
}
@ -84,7 +84,7 @@ pub type JoinTuple<'a> = (
&'a Health,
&'a Body,
&'a PhysicsState,
Option<&'a Attacking>,
Option<&'a Melee>,
Option<&'a Beam>,
&'a Stats,
);
@ -105,7 +105,7 @@ impl<'a> JoinData<'a> {
health: j.9,
body: j.10,
physics: j.11,
attacking: j.12,
melee_attack: j.12,
stats: j.14,
updater,
dt,

View File

@ -1,13 +1,11 @@
use crate::{
comp::{
Attacking, CharacterState, EnergyChange, EnergySource, PoiseChange, PoiseSource,
StateUpdate,
},
combat::{Attack, AttackDamage, AttackEffect, CombatBuff, CombatEffect, CombatRequirement},
comp::{CharacterState, EnergyChange, EnergySource, Melee, StateUpdate},
states::{
behavior::{CharacterBehavior, JoinData},
utils::{StageSection, *},
},
Damage, DamageSource, GroupTarget, Knockback,
Damage, DamageSource, GroupTarget, Knockback, KnockbackDir,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -155,28 +153,46 @@ impl CharacterBehavior for Data {
exhausted: true,
..*self
});
let damage = Damage {
source: DamageSource::Melee,
value: self.static_data.initial_damage as f32
+ self.charge_amount * self.static_data.scaled_damage as f32,
};
let poise_damage = PoiseChange {
amount: -(self.static_data.initial_poise_damage as f32
+ self.charge_amount * self.static_data.scaled_poise_damage as f32)
as i32,
source: PoiseSource::Attack,
};
let knockback = self.static_data.initial_knockback
+ self.charge_amount * self.static_data.scaled_knockback;
let poise = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Poise(
self.static_data.initial_poise_damage as f32
+ self.charge_amount * self.static_data.scaled_poise_damage as f32,
),
)
.with_requirement(CombatRequirement::AnyDamage);
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
strength: self.static_data.initial_knockback
+ self.charge_amount * self.static_data.scaled_knockback,
direction: KnockbackDir::Away,
}),
)
.with_requirement(CombatRequirement::AnyDamage);
let buff = CombatEffect::Buff(CombatBuff::default_physical());
let damage = AttackDamage::new(
Damage {
source: DamageSource::Melee,
value: self.static_data.initial_damage as f32
+ self.charge_amount * self.static_data.scaled_damage as f32,
},
Some(GroupTarget::OutOfGroup),
)
.with_effect(buff);
let attack = Attack::default()
.with_damage(damage)
.with_crit(0.5, 1.3)
.with_effect(poise)
.with_effect(knockback);
// Hit attempt
data.updater.insert(data.entity, Attacking {
effects: vec![(Some(GroupTarget::OutOfGroup), damage, poise_damage)],
data.updater.insert(data.entity, Melee {
attack,
range: self.static_data.range,
max_angle: self.static_data.max_angle.to_radians(),
applied: false,
hit_count: 0,
knockback: Knockback::Away(knockback),
});
} else if self.timer < self.static_data.swing_duration {
// Swings
@ -210,14 +226,14 @@ impl CharacterBehavior for Data {
// Done
update.character = CharacterState::Wielding;
// Make sure attack component is removed
data.updater.remove::<Attacking>(data.entity);
data.updater.remove::<Melee>(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);
data.updater.remove::<Melee>(data.entity);
},
}

View File

@ -1,16 +1,17 @@
use crate::{
combat::{
Attack, AttackDamage, AttackEffect, CombatBuff, CombatEffect, CombatRequirement, Damage,
DamageSource, GroupTarget, Knockback, KnockbackDir,
},
comp::{
buff::{BuffCategory, BuffData, BuffKind},
projectile, Body, CharacterState, EnergyChange, EnergySource, Gravity, LightEmitter,
Projectile, StateUpdate,
},
effect::BuffEffect,
event::ServerEvent,
states::{
behavior::{CharacterBehavior, JoinData},
utils::*,
},
Damage, DamageSource, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -102,31 +103,36 @@ impl CharacterBehavior for Data {
let charge_frac = (self.timer.as_secs_f32()
/ self.static_data.charge_duration.as_secs_f32())
.min(1.0);
let damage = Damage {
source: DamageSource::Projectile,
value: self.static_data.initial_damage as f32
+ charge_frac * self.static_data.scaled_damage as f32,
};
let knockback = self.static_data.initial_knockback
+ charge_frac * self.static_data.scaled_knockback;
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
strength: self.static_data.initial_knockback
+ charge_frac * self.static_data.scaled_knockback,
direction: KnockbackDir::Away,
}),
)
.with_requirement(CombatRequirement::AnyDamage);
let buff = CombatEffect::Buff(CombatBuff::default_physical());
let damage = AttackDamage::new(
Damage {
source: DamageSource::Projectile,
value: self.static_data.initial_damage as f32
+ charge_frac * self.static_data.scaled_damage as f32,
},
Some(GroupTarget::OutOfGroup),
)
.with_effect(buff);
let attack = Attack::default()
.with_damage(damage)
.with_crit(0.5, 1.2)
.with_effect(knockback);
// Fire
let projectile = Projectile {
hit_solid: vec![projectile::Effect::Stick],
hit_entity: vec![
projectile::Effect::Damage(Some(GroupTarget::OutOfGroup), damage),
projectile::Effect::Knockback(Knockback::Away(knockback)),
projectile::Effect::Attack(attack),
projectile::Effect::Vanish,
projectile::Effect::Buff {
buff: BuffEffect {
kind: BuffKind::Bleeding,
data: BuffData {
strength: damage.value / 5.0,
duration: Some(Duration::from_secs(5)),
},
cat_ids: vec![BuffCategory::Physical],
},
chance: Some(0.10),
},
],
time_left: Duration::from_secs(15),
owner: Some(*data.uid),

View File

@ -1,13 +1,11 @@
use crate::{
comp::{
Attacking, CharacterState, EnergyChange, EnergySource, PoiseChange, PoiseSource,
StateUpdate,
},
combat::{Attack, AttackDamage, AttackEffect, CombatBuff, CombatEffect, CombatRequirement},
comp::{CharacterState, Melee, StateUpdate},
states::{
behavior::{CharacterBehavior, JoinData},
utils::*,
},
Damage, DamageSource, GroupTarget, Knockback,
Damage, DamageSource, GroupTarget, Knockback, KnockbackDir,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -174,32 +172,53 @@ impl CharacterBehavior for Data {
.scales_from_combo
.min(self.combo / self.static_data.num_stages)
* self.static_data.stage_data[stage_index].damage_increase;
let poise_damage = self.static_data.stage_data[stage_index].base_poise_damage
let poise = self.static_data.stage_data[stage_index].base_poise_damage
+ self
.static_data
.scales_from_combo
.min(self.combo / self.static_data.num_stages)
* self.static_data.stage_data[stage_index].poise_damage_increase;
data.updater.insert(data.entity, Attacking {
effects: vec![(
Some(GroupTarget::OutOfGroup),
Damage {
source: DamageSource::Melee,
value: damage as f32,
},
PoiseChange {
amount: -(poise_damage as i32),
source: PoiseSource::Attack,
},
)],
let poise = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Poise(poise as f32),
)
.with_requirement(CombatRequirement::AnyDamage);
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
strength: self.static_data.stage_data[stage_index].knockback,
direction: KnockbackDir::Away,
}),
)
.with_requirement(CombatRequirement::AnyDamage);
let energy = self.static_data.max_energy_gain.min(
self.static_data.initial_energy_gain
+ self.combo * self.static_data.energy_increase,
);
let energy = AttackEffect::new(None, CombatEffect::EnergyReward(energy))
.with_requirement(CombatRequirement::AnyDamage);
let buff = CombatEffect::Buff(CombatBuff::default_physical());
let damage = AttackDamage::new(
Damage {
source: DamageSource::Melee,
value: damage as f32,
},
Some(GroupTarget::OutOfGroup),
)
.with_effect(buff);
let attack = Attack::default()
.with_damage(damage)
.with_crit(0.5, 1.3)
.with_effect(energy)
.with_effect(poise)
.with_effect(knockback);
data.updater.insert(data.entity, Melee {
attack,
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: Knockback::Away(
self.static_data.stage_data[stage_index].knockback,
),
});
}
},
@ -272,24 +291,20 @@ impl CharacterBehavior for Data {
// Done
update.character = CharacterState::Wielding;
// Make sure attack component is removed
data.updater.remove::<Attacking>(data.entity);
data.updater.remove::<Melee>(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);
data.updater.remove::<Melee>(data.entity);
},
}
// Grant energy on successful hit
if let Some(attack) = data.attacking {
if let Some(attack) = data.melee_attack {
if attack.applied && attack.hit_count > 0 {
let energy = self.static_data.max_energy_gain.min(
self.static_data.initial_energy_gain
+ self.combo * self.static_data.energy_increase,
) as i32;
update.character = CharacterState::ComboMelee(Data {
static_data: self.static_data.clone(),
stage: self.stage,
@ -298,11 +313,7 @@ impl CharacterBehavior for Data {
stage_section: self.stage_section,
next_stage: self.next_stage,
});
data.updater.remove::<Attacking>(data.entity);
update.energy.change_by(EnergyChange {
amount: energy,
source: EnergySource::HitEnemy,
});
data.updater.remove::<Melee>(data.entity);
}
}

View File

@ -1,13 +1,11 @@
use crate::{
comp::{
Attacking, CharacterState, EnergyChange, EnergySource, PoiseChange, PoiseSource,
StateUpdate,
},
combat::{Attack, AttackDamage, AttackEffect, CombatBuff, CombatEffect, CombatRequirement},
comp::{CharacterState, EnergyChange, EnergySource, Melee, StateUpdate},
states::{
behavior::{CharacterBehavior, JoinData},
utils::*,
},
Damage, DamageSource, GroupTarget, Knockback,
Damage, DamageSource, GroupTarget, Knockback, KnockbackDir,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -132,30 +130,45 @@ impl CharacterBehavior for Data {
if !self.exhausted {
// Hit attempt (also checks if player is moving)
if update.vel.0.distance_squared(Vec3::zero()) > 1.0 {
let damage = Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32
+ charge_frac * self.static_data.scaled_damage as f32,
};
let poise_damage = PoiseChange {
amount: -(self.static_data.base_poise_damage as f32
+ charge_frac * self.static_data.scaled_poise_damage as f32)
as i32,
source: PoiseSource::Attack,
};
let knockback = self.static_data.base_knockback
+ charge_frac * self.static_data.scaled_knockback;
data.updater.insert(data.entity, Attacking {
effects: vec![(
Some(GroupTarget::OutOfGroup),
damage,
poise_damage,
)],
let poise = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Poise(
self.static_data.base_poise_damage as f32
+ charge_frac * self.static_data.scaled_poise_damage as f32,
),
)
.with_requirement(CombatRequirement::AnyDamage);
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
strength: self.static_data.base_knockback
+ charge_frac * self.static_data.scaled_knockback,
direction: KnockbackDir::Away,
}),
)
.with_requirement(CombatRequirement::AnyDamage);
let buff = CombatEffect::Buff(CombatBuff::default_physical());
let damage = AttackDamage::new(
Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32
+ charge_frac * self.static_data.scaled_damage as f32,
},
Some(GroupTarget::OutOfGroup),
)
.with_effect(buff);
let attack = Attack::default()
.with_damage(damage)
.with_crit(0.5, 1.3)
.with_effect(poise)
.with_effect(knockback);
data.updater.insert(data.entity, Melee {
attack,
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 {
@ -235,14 +248,14 @@ impl CharacterBehavior for Data {
// Done
update.character = CharacterState::Wielding;
// Make sure attack component is removed
data.updater.remove::<Attacking>(data.entity);
data.updater.remove::<Melee>(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);
data.updater.remove::<Melee>(data.entity);
},
}

View File

@ -1,10 +1,11 @@
use crate::{
comp::{Attacking, CharacterState, PoiseChange, PoiseSource, StateUpdate},
combat::{Attack, AttackDamage, AttackEffect, CombatBuff, CombatEffect, CombatRequirement},
comp::{CharacterState, Melee, StateUpdate},
states::{
behavior::{CharacterBehavior, JoinData},
utils::{StageSection, *},
},
Damage, DamageSource, GroupTarget, Knockback,
Damage, DamageSource, GroupTarget, Knockback, KnockbackDir,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -147,24 +148,41 @@ impl CharacterBehavior for Data {
},
StageSection::Recover => {
if !self.exhausted {
let poise = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Poise(self.static_data.base_poise_damage as f32),
)
.with_requirement(CombatRequirement::AnyDamage);
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
strength: self.static_data.knockback,
direction: KnockbackDir::Away,
}),
)
.with_requirement(CombatRequirement::AnyDamage);
let buff = CombatEffect::Buff(CombatBuff::default_physical());
let damage = AttackDamage::new(
Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
},
Some(GroupTarget::OutOfGroup),
)
.with_effect(buff);
let attack = Attack::default()
.with_damage(damage)
.with_crit(0.5, 1.3)
.with_effect(poise)
.with_effect(knockback);
// Hit attempt, when animation plays
data.updater.insert(data.entity, Attacking {
effects: vec![(
Some(GroupTarget::OutOfGroup),
Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
},
PoiseChange {
amount: -(self.static_data.base_poise_damage as i32),
source: PoiseSource::Attack,
},
)],
data.updater.insert(data.entity, Melee {
attack,
range: self.static_data.range,
max_angle: self.static_data.max_angle.to_radians(),
applied: false,
hit_count: 0,
knockback: Knockback::Away(self.static_data.knockback),
});
update.character = CharacterState::LeapMelee(Data {
@ -188,14 +206,14 @@ impl CharacterBehavior for Data {
// Done
update.character = CharacterState::Wielding;
// Make sure attack component is removed
data.updater.remove::<Attacking>(data.entity);
data.updater.remove::<Melee>(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);
data.updater.remove::<Melee>(data.entity);
},
}

View File

@ -1,11 +1,14 @@
use crate::{
comp::{shockwave, CharacterState, PoiseChange, PoiseSource, StateUpdate},
combat::{
Attack, AttackDamage, AttackEffect, CombatEffect, CombatRequirement, Damage, DamageSource,
GroupTarget, Knockback,
},
comp::{shockwave, CharacterState, StateUpdate},
event::ServerEvent,
states::{
behavior::{CharacterBehavior, JoinData},
utils::*,
},
Damage, DamageSource, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -80,23 +83,33 @@ impl CharacterBehavior for Data {
});
} else {
// Attack
let poise = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Poise(self.static_data.poise_damage as f32),
)
.with_requirement(CombatRequirement::AnyDamage);
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(self.static_data.knockback),
)
.with_requirement(CombatRequirement::AnyDamage);
let damage = AttackDamage::new(
Damage {
source: DamageSource::Shockwave,
value: self.static_data.damage as f32,
},
Some(GroupTarget::OutOfGroup),
);
let attack = Attack::default()
.with_damage(damage)
.with_effect(poise)
.with_effect(knockback);
let properties = shockwave::Properties {
angle: self.static_data.shockwave_angle,
vertical_angle: self.static_data.shockwave_vertical_angle,
speed: self.static_data.shockwave_speed,
duration: self.static_data.shockwave_duration,
effects: vec![(
Some(GroupTarget::OutOfGroup),
Damage {
source: DamageSource::Shockwave,
value: self.static_data.damage as f32,
},
PoiseChange {
amount: -(self.static_data.poise_damage as i32),
source: PoiseSource::Attack,
},
)],
knockback: self.static_data.knockback,
attack,
requires_ground: self.static_data.requires_ground,
owner: Some(*data.uid),
};

View File

@ -1,14 +1,12 @@
use crate::{
comp::{
Attacking, CharacterState, EnergyChange, EnergySource, PoiseChange, PoiseSource,
StateUpdate,
},
combat::{Attack, AttackDamage, AttackEffect, CombatBuff, CombatEffect, CombatRequirement},
comp::{CharacterState, EnergyChange, EnergySource, Melee, StateUpdate},
consts::GRAVITY,
states::{
behavior::{CharacterBehavior, JoinData},
utils::*,
},
Damage, DamageSource, GroupTarget, Knockback,
Damage, DamageSource, GroupTarget, Knockback, KnockbackDir,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -114,24 +112,42 @@ impl CharacterBehavior for Data {
exhausted: true,
..*self
});
let poise = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Poise(self.static_data.base_poise_damage as f32),
)
.with_requirement(CombatRequirement::AnyDamage);
let knockback = AttackEffect::new(
Some(GroupTarget::OutOfGroup),
CombatEffect::Knockback(Knockback {
strength: self.static_data.knockback,
direction: KnockbackDir::Away,
}),
)
.with_requirement(CombatRequirement::AnyDamage);
let buff = CombatEffect::Buff(CombatBuff::default_physical());
let damage = AttackDamage::new(
Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
},
Some(GroupTarget::OutOfGroup),
)
.with_effect(buff);
let attack = Attack::default()
.with_damage(damage)
.with_crit(0.5, 1.3)
.with_effect(poise)
.with_effect(knockback);
// Hit attempt
data.updater.insert(data.entity, Attacking {
effects: vec![(
Some(GroupTarget::OutOfGroup),
Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
},
PoiseChange {
amount: -(self.static_data.base_poise_damage as i32),
source: PoiseSource::Attack,
},
)],
data.updater.insert(data.entity, Melee {
attack,
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 {
if matches!(
@ -201,14 +217,14 @@ impl CharacterBehavior for Data {
// Done
update.character = CharacterState::Wielding;
// Make sure attack component is removed
data.updater.remove::<Attacking>(data.entity);
data.updater.remove::<Melee>(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);
data.updater.remove::<Melee>(data.entity);
},
}

View File

@ -1,7 +1,8 @@
use common::{
combat::AttackerInfo,
comp::{
group, Beam, BeamSegment, Body, Energy, EnergyChange, EnergySource, Health, HealthChange,
HealthSource, Inventory, Last, Ori, Pos, Scale,
group, Beam, BeamSegment, Body, Energy, Health, HealthSource, Inventory, Last, Ori, Pos,
Scale,
},
event::{EventBus, ServerEvent},
resources::{DeltaTime, Time},
@ -112,7 +113,16 @@ impl<'a> System<'a> for Sys {
};
// Go through all other effectable entities
for (b, uid_b, pos_b, last_pos_b_maybe, scale_b_maybe, health_b, body_b) in (
for (
b,
uid_b,
pos_b,
last_pos_b_maybe,
scale_b_maybe,
health_b,
body_b,
inventory_b_maybe,
) in (
&entities,
&uids,
&positions,
@ -121,6 +131,7 @@ impl<'a> System<'a> for Sys {
scales.maybe(),
&healths,
&bodies,
inventories.maybe(),
)
.join()
{
@ -158,62 +169,27 @@ impl<'a> System<'a> for Sys {
continue;
}
for (target, damage) in beam_segment.damages.iter() {
if let Some(target) = target {
if *target != target_group {
continue;
}
}
let attacker_info =
beam_owner
.zip(beam_segment.owner)
.map(|(entity, uid)| AttackerInfo {
entity,
uid,
energy: energies.get(entity),
});
// Modify damage
let change = damage.modify_damage(inventories.get(b), beam_segment.owner);
match target {
Some(GroupTarget::OutOfGroup) => {
server_emitter.emit(ServerEvent::Damage { entity: b, change });
if let Some(entity) = beam_owner {
server_emitter.emit(ServerEvent::Damage {
entity,
change: HealthChange {
amount: (-change.amount as f32
* beam_segment.lifesteal_eff)
as i32,
cause: HealthSource::Heal {
by: beam_segment.owner,
},
},
});
server_emitter.emit(ServerEvent::EnergyChange {
entity,
change: EnergyChange {
amount: beam_segment.energy_regen as i32,
source: EnergySource::HitEnemy,
},
});
}
},
Some(GroupTarget::InGroup) => {
if let Some(energy) = beam_owner.and_then(|o| energies.get(o)) {
if energy.current() > beam_segment.energy_cost {
server_emitter.emit(ServerEvent::EnergyChange {
entity: beam_owner.unwrap(), /* If it's able to get an energy
* component, the entity exists */
change: EnergyChange {
amount: -(beam_segment.energy_cost as i32), // Stamina use
source: EnergySource::Ability,
},
});
server_emitter
.emit(ServerEvent::Damage { entity: b, change });
}
}
},
None => {},
}
beam_segment.properties.attack.apply_attack(
target_group,
attacker_info,
b,
inventory_b_maybe,
ori.0,
false,
1.0,
|e| server_emitter.emit(e),
);
// Adds entities that were hit to the hit_entities list on the beam, sees if
// it needs to purge the hit_entities list
hit_entities.push(*uid_b);
}
hit_entities.push(*uid_b);
}
}
}

View File

@ -3,8 +3,8 @@ use specs::{Entities, Join, LazyUpdate, Read, ReadExpect, ReadStorage, System, W
use common::{
comp::{
inventory::slot::{EquipSlot, Slot},
Attacking, Beam, Body, CharacterState, Controller, Energy, Health, Inventory, Mounting,
Ori, PhysicsState, Poise, PoiseState, Pos, StateUpdate, Stats, Vel,
Beam, Body, CharacterState, Controller, Energy, Health, Inventory, Melee, Mounting, Ori,
PhysicsState, Poise, PoiseState, Pos, StateUpdate, Stats, Vel,
},
event::{EventBus, LocalEvent, ServerEvent},
metrics::SysMetrics,
@ -69,7 +69,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Poise>,
ReadStorage<'a, Body>,
ReadStorage<'a, PhysicsState>,
ReadStorage<'a, Attacking>,
ReadStorage<'a, Melee>,
ReadStorage<'a, Beam>,
ReadStorage<'a, Uid>,
ReadStorage<'a, Mounting>,

View File

@ -1,5 +1,6 @@
use common::{
comp::{buff, group, Attacking, Body, CharacterState, Health, Inventory, Ori, Pos, Scale},
combat::AttackerInfo,
comp::{group, Body, CharacterState, Energy, Health, Inventory, Melee, Ori, Pos, Scale},
event::{EventBus, LocalEvent, ServerEvent},
metrics::SysMetrics,
span,
@ -7,9 +8,7 @@ use common::{
util::Dir,
GroupTarget,
};
use rand::{thread_rng, Rng};
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage};
use std::time::Duration;
use vek::*;
/// This system is responsible for handling accepted inputs like moving or
@ -28,9 +27,10 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Scale>,
ReadStorage<'a, Body>,
ReadStorage<'a, Health>,
ReadStorage<'a, Energy>,
ReadStorage<'a, Inventory>,
ReadStorage<'a, group::Group>,
WriteStorage<'a, Attacking>,
WriteStorage<'a, Melee>,
ReadStorage<'a, CharacterState>,
);
@ -47,6 +47,7 @@ impl<'a> System<'a> for Sys {
scales,
bodies,
healths,
energies,
inventories,
groups,
mut attacking_storage,
@ -75,13 +76,22 @@ impl<'a> System<'a> for Sys {
attack.applied = true;
// Go through all other entities
for (b, pos_b, scale_b_maybe, health_b, body_b, char_state_b_maybe) in (
for (
b,
pos_b,
scale_b_maybe,
health_b,
body_b,
char_state_b_maybe,
inventory_b_maybe,
) in (
&entities,
&positions,
scales.maybe(),
&healths,
&bodies,
char_states.maybe(),
inventories.maybe(),
)
.join()
{
@ -118,54 +128,26 @@ impl<'a> System<'a> for Sys {
GroupTarget::OutOfGroup
};
for (target, damage, poise_change) in attack.effects.iter() {
if let Some(target) = target {
if *target != target_group
|| (!matches!(target, GroupTarget::InGroup) && is_dodge)
{
continue;
}
}
let dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
let change = damage.modify_damage(inventories.get(b), Some(*uid));
let attacker_info = Some(AttackerInfo {
entity,
uid: *uid,
energy: energies.get(entity),
});
server_emitter.emit(ServerEvent::Damage { entity: b, change });
attack.attack.apply_attack(
target_group,
attacker_info,
b,
inventory_b_maybe,
dir,
is_dodge,
1.0,
|e| server_emitter.emit(e),
);
// Apply bleeding buff on melee hits with 10% chance
// TODO: Don't have buff uniformly applied on all melee attacks
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: -change.amount as f32 / 10.0,
duration: Some(Duration::from_secs(10)),
},
vec![BuffCategory::Physical],
BuffSource::Character { by: *uid },
)),
});
}
let kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
let impulse = attack.knockback.calculate_impulse(kb_dir);
if !impulse.is_approx_zero() {
server_emitter.emit(ServerEvent::Knockback { entity: b, impulse });
}
let poise_change = poise_change.modify_poise_damage(inventories.get(b));
if poise_change.amount.abs() > 0 {
server_emitter.emit(ServerEvent::PoiseChange {
entity: b,
change: poise_change,
kb_dir: *kb_dir,
});
}
attack.hit_count += 1;
}
attack.hit_count += 1;
}
}
}

View File

@ -1,8 +1,7 @@
use common::{
combat::AttackerInfo,
comp::{
buff::{Buff, BuffChange, BuffSource},
projectile, EnergyChange, EnergySource, Group, HealthSource, Inventory, Ori, PhysicsState,
Pos, Projectile, Vel,
projectile, Energy, Group, HealthSource, Inventory, Ori, PhysicsState, Pos, Projectile, Vel,
},
event::{EventBus, ServerEvent},
metrics::SysMetrics,
@ -11,7 +10,6 @@ use common::{
uid::UidAllocator,
GroupTarget,
};
use rand::{thread_rng, Rng};
use specs::{
saveload::MarkerAllocator, Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage,
};
@ -34,6 +32,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Projectile>,
ReadStorage<'a, Inventory>,
ReadStorage<'a, Group>,
ReadStorage<'a, Energy>,
);
fn run(
@ -51,6 +50,7 @@ impl<'a> System<'a> for Sys {
mut projectiles,
inventories,
groups,
energies,
): Self::SystemData,
) {
let start_time = std::time::Instant::now();
@ -103,54 +103,33 @@ impl<'a> System<'a> for Sys {
let projectile = &mut *projectile;
for effect in projectile.hit_entity.drain(..) {
match effect {
projectile::Effect::Damage(target, damage) => {
if Some(other) == projectile.owner {
continue;
}
if let Some(target) = target {
if target != target_group {
continue;
}
}
if let Some(other_entity) =
projectile::Effect::Attack(attack) => {
if let Some(target_entity) =
uid_allocator.retrieve_entity_internal(other.into())
{
let other_entity_inventory = inventories.get(other_entity);
let change =
damage.modify_damage(other_entity_inventory, projectile.owner);
server_emitter.emit(ServerEvent::Damage {
entity: other_entity,
change,
});
}
},
projectile::Effect::Knockback(knockback) => {
if let Some(other_entity) =
uid_allocator.retrieve_entity_internal(other.into())
{
let impulse = knockback.calculate_impulse(ori.0);
if !impulse.is_approx_zero() {
server_emitter.emit(ServerEvent::Knockback {
entity: other_entity,
impulse,
let owner_entity = projectile
.owner
.and_then(|u| uid_allocator.retrieve_entity_internal(u.into()));
let attacker_info =
owner_entity.zip(projectile.owner).map(|(entity, uid)| {
AttackerInfo {
entity,
uid,
energy: energies.get(entity),
}
});
}
}
},
projectile::Effect::RewardEnergy(energy) => {
if let Some(entity_owner) = projectile
.owner
.and_then(|u| uid_allocator.retrieve_entity_internal(u.into()))
{
server_emitter.emit(ServerEvent::EnergyChange {
entity: entity_owner,
change: EnergyChange {
amount: energy as i32,
source: EnergySource::HitEnemy,
},
});
attack.apply_attack(
target_group,
attacker_info,
target_entity,
inventories.get(target_entity),
ori.0,
false,
1.0,
|e| server_emitter.emit(e),
);
}
},
projectile::Effect::Explode(e) => {
@ -175,26 +154,6 @@ impl<'a> System<'a> for Sys {
}
}
},
// TODO: Change to effect after !1472 merges
projectile::Effect::Buff { buff, chance } => {
if let Some(entity) =
uid_allocator.retrieve_entity_internal(other.into())
{
if chance.map_or(true, |c| thread_rng().gen::<f32>() < c) {
let source = if let Some(owner) = projectile.owner {
BuffSource::Character { by: owner }
} else {
BuffSource::Unknown
};
let buff =
Buff::new(buff.kind, buff.data, buff.cat_ids, source);
server_emitter.emit(ServerEvent::Buff {
entity,
buff_change: BuffChange::Add(buff),
});
}
}
},
_ => {},
}
}

View File

@ -1,6 +1,7 @@
use common::{
combat::AttackerInfo,
comp::{
group, Body, Health, HealthSource, Inventory, Last, Ori, PhysicsState, Pos, Scale,
group, Body, Energy, Health, HealthSource, Inventory, Last, Ori, PhysicsState, Pos, Scale,
Shockwave, ShockwaveHitEntities,
},
event::{EventBus, LocalEvent, ServerEvent},
@ -36,6 +37,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, PhysicsState>,
WriteStorage<'a, Shockwave>,
WriteStorage<'a, ShockwaveHitEntities>,
ReadStorage<'a, Energy>,
);
fn run(
@ -59,6 +61,7 @@ impl<'a> System<'a> for Sys {
physics_states,
mut shockwaves,
mut shockwave_hit_lists,
energies,
): Self::SystemData,
) {
let mut server_emitter = server_bus.emitter();
@ -68,9 +71,8 @@ impl<'a> System<'a> for Sys {
let dt = dt.0;
// Shockwaves
for (entity, uid, pos, ori, shockwave, shockwave_hit_list) in (
for (entity, pos, ori, shockwave, shockwave_hit_list) in (
&entities,
&uids,
&positions,
&orientations,
&shockwaves,
@ -114,12 +116,13 @@ impl<'a> System<'a> for Sys {
end: frame_end_dist,
};
let shockwave_owner = shockwave
.owner
.and_then(|uid| uid_allocator.retrieve_entity_internal(uid.into()));
// Group to ignore collisions with
// Might make this more nuanced if shockwaves are used for non damage effects
let group = shockwave
.owner
.and_then(|uid| uid_allocator.retrieve_entity_internal(uid.into()))
.and_then(|e| groups.get(e));
let group = shockwave_owner.and_then(|e| groups.get(e));
// Go through all other effectable entities
for (
@ -191,31 +194,29 @@ impl<'a> System<'a> for Sys {
&& (!shockwave.requires_ground || physics_state_b.on_ground);
if hit {
for (target, damage, poise_damage) in shockwave.effects.iter() {
if let Some(target) = target {
if *target != target_group {
continue;
}
}
let dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
let owner_uid = shockwave.owner.unwrap_or(*uid);
let change = damage.modify_damage(inventories.get(b), Some(owner_uid));
let poise_change = poise_damage.modify_poise_damage(inventories.get(b));
let attacker_info =
shockwave_owner
.zip(shockwave.owner)
.map(|(entity, uid)| AttackerInfo {
entity,
uid,
energy: energies.get(entity),
});
server_emitter.emit(ServerEvent::Damage { entity: b, change });
shockwave_hit_list.hit_entities.push(*uid_b);
shockwave.properties.attack.apply_attack(
target_group,
attacker_info,
b,
inventories.get(b),
dir,
false,
1.0,
|e| server_emitter.emit(e),
);
let kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
let impulse = shockwave.knockback.calculate_impulse(kb_dir);
if !impulse.is_approx_zero() {
server_emitter.emit(ServerEvent::Knockback { entity: b, impulse });
}
server_emitter.emit(ServerEvent::PoiseChange {
entity: b,
change: poise_change,
kb_dir: *kb_dir,
});
}
shockwave_hit_list.hit_entities.push(*uid_b);
}
}
}

View File

@ -163,7 +163,7 @@ impl State {
ecs.register::<comp::Admin>();
ecs.register::<comp::Waypoint>();
ecs.register::<comp::Projectile>();
ecs.register::<comp::Attacking>();
ecs.register::<comp::Melee>();
ecs.register::<comp::ItemDrop>();
ecs.register::<comp::ChatMode>();
ecs.register::<comp::Faction>();

View File

@ -1360,17 +1360,13 @@ fn handle_explosion(
pos: pos.0,
explosion: Explosion {
effects: vec![
RadiusEffect::Entity(
None,
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 100.0 * power,
}),
),
RadiusEffect::Entity(Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 100.0 * power,
})),
RadiusEffect::TerrainDestruction(power),
],
radius: 3.0 * power,
energy_regen: 0,
},
owner: ecs.read_storage::<Uid>().get(target).copied(),
reagent: None,

View File

@ -1,6 +1,7 @@
use crate::{sys, Server, StateExt};
use common::{
character::CharacterId,
combat,
comp::{
self,
aura::{Aura, AuraKind, AuraTarget},
@ -170,7 +171,10 @@ pub fn handle_beam(server: &mut Server, properties: beam::Properties, pos: Pos,
let ecs = state.ecs();
ecs.write_resource::<Vec<Outcome>>().push(Outcome::Beam {
pos: pos.0,
heal: properties.lifesteal_eff > 0.0,
heal: properties
.attack
.effects()
.any(|e| matches!(e.effect(), combat::CombatEffect::Heal(h) if *h > 0.0)),
});
state.create_beam(properties, pos, ori).build();
}

View File

@ -16,12 +16,13 @@ use common::{
object, Alignment, Body, CharacterState, Energy, EnergyChange, Group, Health, HealthChange,
HealthSource, Inventory, Item, Player, Poise, PoiseChange, PoiseSource, Pos, Stats,
},
effect::Effect,
event::{EventBus, ServerEvent},
lottery::Lottery,
outcome::Outcome,
rtsim::RtSimEntity,
terrain::{Block, TerrainGrid},
uid::{Uid, UidAllocator},
util::Dir,
vol::ReadVol,
Damage, DamageSource, Explosion, GroupTarget, RadiusEffect,
};
@ -509,7 +510,8 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>)
source: DamageSource::Falling,
value: falldmg,
};
let change = damage.modify_damage(inventories.get(entity), None);
let change =
damage.calculate_health_change(inventories.get(entity), None, false, 0.0, 1.0);
health.change_by(change);
}
// Handle poise change
@ -577,22 +579,11 @@ pub fn handle_explosion(
// Add an outcome
// Uses radius as outcome power, makes negative if explosion has healing effect
let outcome_power = explosion.radius
* if explosion.effects.iter().any(|e| {
matches!(
e,
RadiusEffect::Entity(
_,
Effect::Damage(Damage {
source: DamageSource::Healing,
..
})
)
)
}) {
-1.0
} else {
1.0
};
* if explosion.effects.iter().any(|e| matches!(e, RadiusEffect::Attack(a) if a.effects().any(|e| matches!(e.effect(), combat::CombatEffect::Heal(h) if *h > 0.0)))) {
-1.0
} else {
1.0
};
ecs.write_resource::<Vec<Outcome>>()
.push(Outcome::Explosion {
pos,
@ -601,7 +592,7 @@ pub fn handle_explosion(
is_attack: explosion
.effects
.iter()
.any(|e| matches!(e, RadiusEffect::Entity(_, Effect::Damage(_)))),
.any(|e| matches!(e, RadiusEffect::Attack(_))),
reagent,
});
let owner_entity = owner.and_then(|uid| {
@ -685,54 +676,78 @@ pub fn handle_explosion(
.cast();
}
},
RadiusEffect::Entity(target, mut effect) => {
for (entity_b, pos_b) in (&ecs.entities(), &ecs.read_storage::<comp::Pos>()).join()
RadiusEffect::Attack(attack) => {
let energies = &ecs.read_storage::<comp::Energy>();
for (entity_b, pos_b, _health_b, inventory_b_maybe) in (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
&ecs.read_storage::<comp::Health>(),
ecs.read_storage::<comp::Inventory>().maybe(),
)
.join()
.filter(|(_, _, h, _)| !h.is_dead)
{
// See if entities are in the same group
let mut same_group = owner_entity
.and_then(|e| groups.get(e))
.map_or(false, |group_a| Some(group_a) == groups.get(entity_b));
if let Some(entity) = owner_entity {
if entity == entity_b {
same_group = true;
}
}
let target_group = if same_group {
GroupTarget::InGroup
} else {
GroupTarget::OutOfGroup
};
// Check if it is a hit
let distance_squared = pos.distance_squared(pos_b.0);
let strength = 1.0 - distance_squared / explosion.radius.powi(2);
if strength > 0.0 {
// See if entities are in the same group
let same_group = owner_entity
.and_then(|e| groups.get(e))
.map(|group_a| Some(group_a) == groups.get(entity_b))
.unwrap_or(Some(entity_b) == owner_entity);
if let Some(target) = target {
if target != target_group {
continue;
}
}
let target_group = if same_group {
GroupTarget::InGroup
} else {
GroupTarget::OutOfGroup
};
let dir = Dir::new(
(pos_b.0 - pos)
.try_normalized()
.unwrap_or_else(Vec3::unit_z),
);
let attacker_info =
owner_entity
.zip(owner)
.map(|(entity, uid)| combat::AttackerInfo {
entity,
uid,
energy: energies.get(entity),
});
let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>();
attack.apply_attack(
target_group,
attacker_info,
entity_b,
inventory_b_maybe,
dir,
false,
strength,
|e| server_eventbus.emit_now(e),
);
}
}
},
RadiusEffect::Entity(mut effect) => {
for (entity_b, pos_b, _health_b) in (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
&ecs.read_storage::<comp::Health>(),
)
.join()
.filter(|(_, _, h)| !h.is_dead)
{
let distance_squared = pos.distance_squared(pos_b.0);
let strength = 1.0 - distance_squared / explosion.radius.powi(2);
if strength > 0.0 {
let is_alive = ecs
.read_storage::<comp::Health>()
.get(entity_b)
.map_or(false, |h| !h.is_dead);
if is_alive {
effect.modify_strength(strength);
server.state().apply_effect(entity_b, effect.clone(), owner);
// Apply energy change
if let Some(owner) = owner_entity {
if let Some(mut energy) =
ecs.write_storage::<comp::Energy>().get_mut(owner)
{
energy.change_by(EnergyChange {
amount: explosion.energy_regen as i32,
source: comp::EnergySource::HitEnemy,
});
}
}
}
effect.modify_strength(strength);
server.state().apply_effect(entity_b, effect.clone(), owner);
}
}
},

View File

@ -90,7 +90,13 @@ impl StateExt for State {
},
Effect::Damage(damage) => {
let inventories = self.ecs().read_storage::<Inventory>();
let change = damage.modify_damage(inventories.get(entity), source);
let change = damage.calculate_health_change(
inventories.get(entity),
source,
false,
0.0,
1.0,
);
self.ecs()
.write_storage::<comp::Health>()
.get_mut(entity)

View File

@ -49,24 +49,17 @@ impl<'a> System<'a> for Sys {
pos: pos.0,
explosion: Explosion {
effects: vec![
RadiusEffect::Entity(
None,
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 500.0,
}),
),
RadiusEffect::Entity(
None,
Effect::PoiseChange(PoiseChange {
source: PoiseSource::Explosion,
amount: -100,
}),
),
RadiusEffect::Entity(Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 500.0,
})),
RadiusEffect::Entity(Effect::PoiseChange(PoiseChange {
source: PoiseSource::Explosion,
amount: -100,
})),
RadiusEffect::TerrainDestruction(4.0),
],
radius: 12.0,
energy_regen: 0,
},
owner: *owner,
reagent: None,
@ -83,24 +76,17 @@ impl<'a> System<'a> for Sys {
pos: pos.0,
explosion: Explosion {
effects: vec![
RadiusEffect::Entity(
None,
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 50.0,
}),
),
RadiusEffect::Entity(
None,
Effect::PoiseChange(PoiseChange {
source: PoiseSource::Explosion,
amount: -40,
}),
),
RadiusEffect::Entity(Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 50.0,
})),
RadiusEffect::Entity(Effect::PoiseChange(PoiseChange {
source: PoiseSource::Explosion,
amount: -40,
})),
RadiusEffect::TerrainDestruction(4.0),
],
radius: 12.0,
energy_regen: 0,
},
owner: *owner,
reagent: Some(*reagent),