diff --git a/common/src/combat.rs b/common/src/combat.rs index 25058d7f05..2272aabebd 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -1,6 +1,7 @@ use crate::{ comp::{ ability::Capability, + aura::{AuraKindVariant, EnteredAuras}, buff::{Buff, BuffChange, BuffData, BuffKind, BuffSource}, inventory::{ item::{ @@ -89,8 +90,12 @@ pub struct TargetInfo<'a> { #[derive(Clone, Copy)] pub struct AttackOptions { pub target_dodging: bool, + /// Result of [`may_harm`] pub may_harm: bool, pub target_group: GroupTarget, + /// When set to `true`, entities in the same group or pets & pet owners may + /// hit eachother albeit the target_group being OutOfGroup + pub allow_friendly_fire: bool, pub precision_mult: Option, } @@ -271,6 +276,7 @@ impl Attack { let AttackOptions { target_dodging, may_harm, + allow_friendly_fire, target_group, precision_mult, } = options; @@ -279,13 +285,13 @@ impl Attack { // "attack" has negative effects. // // so if target dodges this "attack" or we don't want to harm target, - // it should avoid such "damage" or effect + // it should avoid such "damage" or effect, unless friendly fire is enabled let avoid_damage = |attack_damage: &AttackDamage| { - matches!(attack_damage.target, Some(GroupTarget::OutOfGroup)) + (matches!(attack_damage.target, Some(GroupTarget::OutOfGroup)) && !allow_friendly_fire) && (target_dodging || !may_harm) }; let avoid_effect = |attack_effect: &AttackEffect| { - matches!(attack_effect.target, Some(GroupTarget::OutOfGroup)) + (matches!(attack_effect.target, Some(GroupTarget::OutOfGroup)) && !allow_friendly_fire) && (target_dodging || !may_harm) }; let precision_mult = attacker @@ -300,7 +306,7 @@ impl Attack { for damage in self .damages .iter() - .filter(|d| d.target.map_or(true, |t| t == target_group)) + .filter(|d| allow_friendly_fire || d.target.map_or(true, |t| t == target_group)) .filter(|d| !avoid_damage(d)) { let damage_instance = damage.instance + damage_instance_offset; @@ -594,7 +600,7 @@ impl Attack { .iter() .flat_map(|stats| stats.effects_on_attack.iter()), ) - .filter(|e| e.target.map_or(true, |t| t == target_group)) + .filter(|e| allow_friendly_fire || e.target.map_or(true, |t| t == target_group)) .filter(|e| !avoid_effect(e)) { let requirements_met = effect.requirements.iter().all(|req| match req { @@ -778,6 +784,24 @@ impl Attack { } } +pub fn allow_friendly_fire( + entered_auras: &ReadStorage, + attacker: EcsEntity, + target: EcsEntity, +) -> bool { + entered_auras + .get(attacker) + .zip(entered_auras.get(target)) + .and_then(|(attacker, target)| { + Some(( + attacker.auras.get(&AuraKindVariant::FriendlyFire)?, + target.auras.get(&AuraKindVariant::FriendlyFire)?, + )) + }) + // Only allow friendly fire if both entities are affectd by the same FriendlyFire aura + .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some()) +} + /// Function that checks for unintentional PvP between players. /// /// Returns `false` if attack will create unintentional conflict, @@ -790,6 +814,7 @@ impl Attack { pub fn may_harm( alignments: &ReadStorage, players: &ReadStorage, + entered_auras: &ReadStorage, id_maps: &IdMaps, attacker: Option, target: EcsEntity, @@ -815,17 +840,32 @@ pub fn may_harm( }; // "Dereference" to owner if this is a pet. - let attacker = owner_if_pet(attacker); - let target = owner_if_pet(target); + let attacker_owner = owner_if_pet(attacker); + let target_owner = owner_if_pet(target); - // Prevent owners from attacking their pets and vice versa - if attacker == target { - return false; + // If both players are in the same ForcePvP aura, allow them to harm eachother + if let (Some(attacker_auras), Some(target_auras)) = ( + entered_auras.get(attacker_owner), + entered_auras.get(target_owner), + ) && attacker_auras + .auras + .get(&AuraKindVariant::IgnorePvE) + .zip(target_auras.auras.get(&AuraKindVariant::IgnorePvE)) + // Only allow forced pvp if both entities are affectd by the same FriendlyFire aura + .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some()) + { + return true; + } + + // Prevent owners from attacking their pets and vice versa, unless friendly fire + // is enabled + if attacker_owner == target_owner { + return allow_friendly_fire(entered_auras, attacker, target); } // Get player components - let attacker_info = players.get(attacker); - let target_info = players.get(target); + let attacker_info = players.get(attacker_owner); + let target_info = players.get(target_owner); // Return `true` if not players. attacker_info diff --git a/common/src/comp/aura.rs b/common/src/comp/aura.rs index 5f462a97c3..53901d85ef 100644 --- a/common/src/comp/aura.rs +++ b/common/src/comp/aura.rs @@ -7,6 +7,7 @@ use crate::{ use serde::{Deserialize, Serialize}; use slotmap::{new_key_type, SlotMap}; use specs::{Component, DerefFlaggedStorage, VecStorage}; +use std::collections::{HashMap, HashSet}; new_key_type! { pub struct AuraKey; } @@ -21,12 +22,26 @@ pub enum AuraKind { category: BuffCategory, source: BuffSource, }, + /// Enables free-for-all friendly-fire. Includes group members, and pets. + /// BattleMode checks still apply. + FriendlyFire, + /// Ignores the [`crate::comp::BattleMode`] of all entities affected by this + /// aura, only player entities will be affected by this aura. + ForcePvP, /* TODO: Implement other effects here. Things to think about * are terrain/sprite effects, collision and physics, and * environmental conditions like temperature and humidity * Multiple auras can be given to an entity. */ } +/// Variants of [`AuraKind`] without data +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub enum AuraKindVariant { + Buff, + FriendlyFire, + IgnorePvE, +} + /// Aura /// Applies a buff to entities in the radius if meeting /// conditions set forth in the aura system. @@ -57,6 +72,8 @@ pub enum AuraChange { Add(Aura), /// Removes auras of these indices RemoveByKey(Vec), + EnterAura(Uid, AuraKey, AuraKindVariant), + ExitAura(Uid, AuraKey, AuraKindVariant), } /// Used by the aura system to filter entities when applying an effect. @@ -73,6 +90,13 @@ pub enum AuraTarget { All, } +// Only used for parsing in commands +pub enum SimpleAuraTarget { + Group, + OutOfGroup, + All, +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum Specifier { WardingAura, @@ -91,6 +115,16 @@ impl From<(Option, Option<&Uid>)> for AuraTarget { } } +impl AsRef for AuraKind { + fn as_ref(&self) -> &AuraKindVariant { + match self { + AuraKind::Buff { .. } => &AuraKindVariant::Buff, + AuraKind::FriendlyFire => &AuraKindVariant::FriendlyFire, + AuraKind::ForcePvP => &AuraKindVariant::IgnorePvE, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AuraData { pub duration: Option, @@ -168,6 +202,24 @@ impl AuraBuffConstructor { } } +/// Auras affecting an entity +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct EnteredAuras { + /// [`AuraKey`] is local to each [`Auras`] component, therefore we also + /// store the [`Uid`] of the aura caster + pub auras: HashMap>, +} + +impl EnteredAuras { + pub fn flatten(&self) -> impl Iterator + '_ { + self.auras.values().flat_map(|i| i.iter().copied()) + } +} + impl Component for Auras { type Storage = DerefFlaggedStorage>; } + +impl Component for EnteredAuras { + type Storage = DerefFlaggedStorage>; +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index d2bf6d5216..32151bb197 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -49,7 +49,7 @@ pub use self::{ TradingBehavior, }, anchor::Anchor, - aura::{Aura, AuraChange, AuraKind, Auras}, + aura::{Aura, AuraChange, AuraKind, Auras, EnteredAuras}, beam::Beam, body::{ arthropod, biped_large, biped_small, bird_large, bird_medium, crustacean, dragon, diff --git a/common/src/event.rs b/common/src/event.rs index 3e7c1213e0..577519aa2e 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -305,6 +305,7 @@ pub struct ExitIngameEvent { pub entity: EcsEntity, } +#[derive(Debug)] pub struct AuraEvent { pub entity: EcsEntity, pub aura_change: comp::AuraChange, diff --git a/common/src/states/basic_aura.rs b/common/src/states/basic_aura.rs index 4672897a70..077010c31d 100644 --- a/common/src/states/basic_aura.rs +++ b/common/src/states/basic_aura.rs @@ -94,6 +94,7 @@ impl CharacterBehavior for Data { data.strength *= (self.static_data.combo_at_cast.max(1) as f32).sqrt(); }, + AuraKind::FriendlyFire | AuraKind::ForcePvP => {}, } output_events.emit_server(ComboChangeEvent { entity: data.entity, diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 0d1b98438f..77d668baa5 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -243,6 +243,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/systems/src/aura.rs b/common/systems/src/aura.rs index bda03f6cfc..b90dc3bae3 100644 --- a/common/systems/src/aura.rs +++ b/common/systems/src/aura.rs @@ -1,7 +1,9 @@ +use std::collections::HashSet; + use common::{ combat, comp::{ - aura::{AuraChange, AuraKey, AuraKind, AuraTarget}, + aura::{AuraChange, AuraKey, AuraKind, AuraTarget, EnteredAuras}, buff::{Buff, BuffCategory, BuffChange, BuffSource}, group::Group, Alignment, Aura, Auras, BuffKind, Buffs, CharacterState, Health, Player, Pos, Stats, @@ -38,6 +40,7 @@ pub struct ReadData<'a> { stats: ReadStorage<'a, Stats>, buffs: ReadStorage<'a, Buffs>, auras: ReadStorage<'a, Auras>, + entered_auras: ReadStorage<'a, EnteredAuras>, } #[derive(Default)] @@ -51,6 +54,7 @@ impl<'a> System<'a> for Sys { fn run(_job: &mut Job, read_data: Self::SystemData) { let mut emitters = read_data.events.get_emitters(); + let mut active_auras: HashSet<(Uid, Uid, AuraKey)> = HashSet::new(); // Iterate through all entities with an aura for (entity, pos, auras_comp, uid) in ( @@ -75,49 +79,55 @@ impl<'a> System<'a> for Sys { .0 .in_circle_aabr(pos.0.xy(), aura.radius) .filter_map(|target| { - read_data - .positions - .get(target) - .and_then(|l| read_data.healths.get(target).map(|r| (l, r))) - .and_then(|l| read_data.uids.get(target).map(|r| (l, r))) - .map(|((target_pos, health), target_uid)| { - ( - target, - target_pos, - health, - target_uid, - read_data.stats.get(target), - ) - }) + read_data.positions.get(target).and_then(|target_pos| { + Some(( + target, + target_pos, + read_data.healths.get(target)?, + read_data.uids.get(target)?, + read_data.entered_auras.get(target)?, + read_data.stats.get(target), + )) + }) }); - target_iter.for_each(|(target, target_pos, health, target_uid, stats)| { - let target_buffs = match read_data.buffs.get(target) { - Some(buff) => buff, - None => return, - }; + target_iter.for_each( + |(target, target_pos, health, target_uid, entered_auras, stats)| { + let target_buffs = match read_data.buffs.get(target) { + Some(buff) => buff, + None => return, + }; - // Ensure entity is within the aura radius - if target_pos.0.distance_squared(pos.0) < aura.radius.powi(2) { - // Ensure the entity is in the group we want to target - let same_group = |uid: Uid| { - read_data - .id_maps - .uid_entity(uid) - .and_then(|e| read_data.groups.get(e)) - .map_or(false, |owner_group| { - Some(owner_group) == read_data.groups.get(target) + // Ensure entity is within the aura radius + if target_pos.0.distance_squared(pos.0) < aura.radius.powi(2) { + // Ensure the entity is in the group we want to target + let same_group = |uid: Uid| { + read_data + .id_maps + .uid_entity(uid) + .and_then(|e| read_data.groups.get(e)) + .map_or(false, |owner_group| { + Some(owner_group) == read_data.groups.get(target) + }) + || *target_uid == uid + }; + + let allow_friendly_fire = combat::allow_friendly_fire( + &read_data.entered_auras, + entity, + target, + ); + + if !(allow_friendly_fire && entity != target + || match aura.target { + AuraTarget::GroupOf(uid) => same_group(uid), + AuraTarget::NotGroupOf(uid) => !same_group(uid), + AuraTarget::All => true, }) - || *target_uid == uid - }; + { + return; + } - let is_target = match aura.target { - AuraTarget::GroupOf(uid) => same_group(uid), - AuraTarget::NotGroupOf(uid) => !same_group(uid), - AuraTarget::All => true, - }; - - if is_target { - activate_aura( + let did_activate = activate_aura( key, aura, *uid, @@ -125,12 +135,31 @@ impl<'a> System<'a> for Sys { health, target_buffs, stats, + allow_friendly_fire, &read_data, &mut emitters, ); + + if did_activate { + if entered_auras + .auras + .get(aura.aura_kind.as_ref()) + .map_or(true, |auras| !auras.contains(&(*uid, key))) + { + emitters.emit(AuraEvent { + entity: target, + aura_change: AuraChange::EnterAura( + *uid, + key, + *aura.aura_kind.as_ref(), + ), + }); + } + active_auras.insert((*uid, *target_uid, key)); + } } - } - }); + }, + ); } if !expired_auras.is_empty() { emitters.emit(AuraEvent { @@ -139,6 +168,30 @@ impl<'a> System<'a> for Sys { }); } } + + for (entity, entered_auras, uid) in ( + &read_data.entities, + &read_data.entered_auras, + &read_data.uids, + ) + .join() + .filter(|(_, active_auras, _)| !active_auras.auras.is_empty()) + { + emitters.emit_many( + entered_auras + .auras + .iter() + .flat_map(|(variant, entered_auras)| { + entered_auras.iter().zip(core::iter::repeat(*variant)) + }) + .filter_map(|((caster_uid, key), variant)| { + (!active_auras.contains(&(*caster_uid, *uid, *key))).then_some(AuraEvent { + entity, + aura_change: AuraChange::ExitAura(*caster_uid, *key, variant), + }) + }), + ); + } } } @@ -152,9 +205,10 @@ fn activate_aura( health: &Health, target_buffs: &Buffs, stats: Option<&Stats>, + allow_friendly_fire: bool, read_data: &ReadData, emitters: &mut impl EmitExt, -) { +) -> bool { let should_activate = match aura.aura_kind { AuraKind::Buff { kind, source, .. } => { let conditions_held = match kind { @@ -195,18 +249,24 @@ fn activate_aura( combat::may_harm( &read_data.alignments, &read_data.players, + &read_data.entered_auras, &read_data.id_maps, owner, target, ) }; - conditions_held && (kind.is_buff() || may_harm()) + conditions_held && (kind.is_buff() || allow_friendly_fire || may_harm()) + }, + AuraKind::FriendlyFire => true, + AuraKind::ForcePvP => { + // Only apply this aura to players + read_data.players.contains(target) }, }; if !should_activate { - return; + return false; } // TODO: When more aura kinds (besides Buff) are @@ -244,5 +304,9 @@ fn activate_aura( }); } }, + // No implementation needed for these auras + AuraKind::FriendlyFire | AuraKind::ForcePvP => {}, } + + true } diff --git a/common/systems/src/beam.rs b/common/systems/src/beam.rs index 51c8093373..4a09247eea 100644 --- a/common/systems/src/beam.rs +++ b/common/systems/src/beam.rs @@ -2,6 +2,7 @@ use common::{ combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo}, comp::{ agent::{Sound, SoundKind}, + aura::EnteredAuras, Alignment, Beam, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori, Player, Pos, Scale, Stats, }, @@ -59,6 +60,7 @@ pub struct ReadData<'a> { combos: ReadStorage<'a, Combo>, character_states: ReadStorage<'a, CharacterState>, buffs: ReadStorage<'a, Buffs>, + entered_auras: ReadStorage<'a, EnteredAuras>, outcomes: Read<'a, EventBus>, events: ReadAttackEvents<'a>, } @@ -203,6 +205,12 @@ impl<'a> System<'a> for Sys { >= tgt_dist; if hit { + let allow_friendly_fire = combat::allow_friendly_fire( + &read_data.entered_auras, + entity, + target, + ); + // See if entities are in the same group let same_group = group .map(|group_a| Some(group_a) == read_data.groups.get(target)) @@ -246,6 +254,7 @@ impl<'a> System<'a> for Sys { let may_harm = combat::may_harm( &read_data.alignments, &read_data.players, + &read_data.entered_auras, &read_data.id_maps, Some(entity), target, @@ -276,6 +285,7 @@ impl<'a> System<'a> for Sys { let attack_options = AttackOptions { target_dodging, may_harm, + allow_friendly_fire, target_group, precision_mult, }; diff --git a/common/systems/src/buff.rs b/common/systems/src/buff.rs index 47e3a402fa..3ef6223b4c 100644 --- a/common/systems/src/buff.rs +++ b/common/systems/src/buff.rs @@ -2,7 +2,7 @@ use common::{ combat::{self, DamageContributor}, comp::{ agent::{Sound, SoundKind}, - aura::Auras, + aura::{Auras, EnteredAuras}, body::{object, Body}, buff::{ Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffKey, BuffKind, BuffSource, @@ -61,6 +61,7 @@ pub struct ReadData<'a> { msm: ReadExpect<'a, MaterialStatManifest>, buffs: ReadStorage<'a, Buffs>, auras: ReadStorage<'a, Auras>, + entered_auras: ReadStorage<'a, EnteredAuras>, positions: ReadStorage<'a, Pos>, bodies: ReadStorage<'a, Body>, light_emitters: ReadStorage<'a, LightEmitter>, @@ -165,6 +166,7 @@ impl<'a> System<'a> for Sys { combat::may_harm( &read_data.alignments, &read_data.players, + &read_data.entered_auras, &read_data.id_maps, Some(entity), *te, diff --git a/common/systems/src/melee.rs b/common/systems/src/melee.rs index a74463d536..d05e383e6c 100644 --- a/common/systems/src/melee.rs +++ b/common/systems/src/melee.rs @@ -2,6 +2,7 @@ use common::{ combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo}, comp::{ agent::{Sound, SoundKind}, + aura::EnteredAuras, melee::MultiTarget, Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Melee, Ori, Player, Pos, Scale, Stats, @@ -59,6 +60,7 @@ pub struct ReadData<'a> { stats: ReadStorage<'a, Stats>, combos: ReadStorage<'a, Combo>, buffs: ReadStorage<'a, Buffs>, + entered_auras: ReadStorage<'a, EnteredAuras>, events: ReadAttackEvents<'a>, } @@ -186,6 +188,8 @@ impl<'a> System<'a> for Sys { && !is_blocked_by_wall(&read_data.terrain, attacker_cylinder, target_cylinder); if hit { + let allow_friendly_fire = + combat::allow_friendly_fire(&read_data.entered_auras, attacker, target); // See if entities are in the same group let same_group = read_data .groups @@ -230,6 +234,7 @@ impl<'a> System<'a> for Sys { let may_harm = combat::may_harm( &read_data.alignments, &read_data.players, + &read_data.entered_auras, &read_data.id_maps, Some(attacker), target, @@ -265,6 +270,7 @@ impl<'a> System<'a> for Sys { let attack_options = AttackOptions { target_dodging, may_harm, + allow_friendly_fire, target_group, precision_mult, }; diff --git a/common/systems/src/projectile.rs b/common/systems/src/projectile.rs index 9c7d7ccf0c..bedc7e869d 100644 --- a/common/systems/src/projectile.rs +++ b/common/systems/src/projectile.rs @@ -2,6 +2,7 @@ use common::{ combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo}, comp::{ agent::{Sound, SoundKind}, + aura::EnteredAuras, projectile, Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori, PhysicsState, Player, Pos, Projectile, Stats, Vel, }, @@ -71,6 +72,7 @@ pub struct ReadData<'a> { character_states: ReadStorage<'a, CharacterState>, terrain: ReadExpect<'a, TerrainGrid>, buffs: ReadStorage<'a, Buffs>, + entered_auras: ReadStorage<'a, EnteredAuras>, } /// This system is responsible for handling projectile effect triggers @@ -138,7 +140,20 @@ impl<'a> System<'a> for Sys { GroupTarget::OutOfGroup }; - if projectile.ignore_group && same_group { + if projectile.ignore_group + && same_group + && projectile + .owner + .and_then(|owner| { + read_data + .id_maps + .uid_entity(owner) + .zip(read_data.id_maps.uid_entity(other)) + }) + .map_or(true, |(owner, other)| { + !combat::allow_friendly_fire(&read_data.entered_auras, owner, other) + }) + { continue; } @@ -354,10 +369,15 @@ fn dispatch_hit( }); } + let allow_friendly_fire = owner.is_some_and(|owner| { + combat::allow_friendly_fire(&read_data.entered_auras, owner, target) + }); + // PvP check let may_harm = combat::may_harm( &read_data.alignments, &read_data.players, + &read_data.entered_auras, &read_data.id_maps, owner, target, @@ -448,6 +468,7 @@ fn dispatch_hit( let attack_options = AttackOptions { target_dodging, may_harm, + allow_friendly_fire, target_group: projectile_target_info.target_group, precision_mult, }; diff --git a/common/systems/src/shockwave.rs b/common/systems/src/shockwave.rs index 29e2c38292..de26deac4f 100644 --- a/common/systems/src/shockwave.rs +++ b/common/systems/src/shockwave.rs @@ -2,6 +2,7 @@ use common::{ combat::{self, AttackOptions, AttackerInfo, TargetInfo}, comp::{ agent::{Sound, SoundKind}, + aura::EnteredAuras, shockwave::ShockwaveDodgeable, Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori, PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats, @@ -62,6 +63,7 @@ pub struct ReadData<'a> { combos: ReadStorage<'a, Combo>, character_states: ReadStorage<'a, CharacterState>, buffs: ReadStorage<'a, Buffs>, + entered_auras: ReadStorage<'a, EnteredAuras>, } /// This system is responsible for handling accepted inputs like moving or @@ -193,6 +195,7 @@ impl<'a> System<'a> for Sys { // Check if it is a hit let hit = entity != target + && shockwave_owner.map_or(true, |owner| owner != target) && !health_b.is_dead && (pos_b.0 - pos.0).magnitude() < frame_end_dist + rad_b // Collision shapes @@ -208,6 +211,9 @@ impl<'a> System<'a> for Sys { }; if hit { + let allow_friendly_fire = shockwave_owner.is_some_and(|entity| { + combat::allow_friendly_fire(&read_data.entered_auras, entity, target) + }); let dir = Dir::from_unnormalized(pos_b.0 - pos.0).unwrap_or(look_dir); let attacker_info = @@ -249,6 +255,7 @@ impl<'a> System<'a> for Sys { let may_harm = combat::may_harm( &read_data.alignments, &read_data.players, + &read_data.entered_auras, &read_data.id_maps, shockwave_owner, target, @@ -258,6 +265,7 @@ impl<'a> System<'a> for Sys { let attack_options = AttackOptions { target_dodging, may_harm, + allow_friendly_fire, target_group, precision_mult, }; diff --git a/common/systems/tests/phys/utils.rs b/common/systems/tests/phys/utils.rs index 396dcef410..73324b9745 100644 --- a/common/systems/tests/phys/utils.rs +++ b/common/systems/tests/phys/utils.rs @@ -1,7 +1,8 @@ use common::{ comp::{ inventory::item::MaterialStatManifest, tool::AbilityMap, Auras, Buffs, CharacterActivity, - CharacterState, Collider, Combo, Controller, Energy, Health, Ori, Pos, Stats, Vel, + CharacterState, Collider, Combo, Controller, Energy, EnteredAuras, Health, Ori, Pos, Stats, + Vel, }, resources::{DeltaTime, GameMode, Time}, shared_server_config::ServerConstants, @@ -128,6 +129,7 @@ pub fn create_player(state: &mut State) -> Entity { .with(Buffs::default()) .with(Combo::default()) .with(Auras::default()) + .with(EnteredAuras::default()) .with(Energy::new(body)) .with(Health::new(body)) .with(skill_set) diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 3be5ed2f17..8588adab75 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -18,7 +18,9 @@ use common::{ combat, combat::{AttackSource, DamageContributor}, comp::{ - self, aura, buff, + self, + aura::{self, EnteredAuras}, + buff, chat::{KillSource, KillType}, inventory::item::{AbilityMap, MaterialStatManifest}, item::flatten_counted_items, @@ -945,6 +947,7 @@ impl ServerEvent for ExplosionEvent { ReadStorage<'a, comp::Combo>, ReadStorage<'a, Inventory>, ReadStorage<'a, Alignment>, + ReadStorage<'a, EnteredAuras>, ReadStorage<'a, comp::Buffs>, ReadStorage<'a, comp::Stats>, ReadStorage<'a, Health>, @@ -975,6 +978,7 @@ impl ServerEvent for ExplosionEvent { combos, inventories, alignments, + entered_auras, buffs, stats, healths, @@ -1199,7 +1203,9 @@ impl ServerEvent for ExplosionEvent { ), ) .join() - .filter(|(_, _, h, _)| !h.is_dead) + .filter(|(e, _, h, _)| { + !h.is_dead && owner_entity.map_or(true, |owner| owner != *e) + }) { let dist_sqrd = ev.pos.distance_squared(pos_b.0); @@ -1274,10 +1280,19 @@ impl ServerEvent for ExplosionEvent { let target_dodging = char_state_b_maybe .and_then(|cs| cs.attack_immunities()) .map_or(false, |i| i.explosions); + let allow_friendly_fire = + owner_entity.is_some_and(|owner_entity| { + combat::allow_friendly_fire( + &entered_auras, + owner_entity, + entity_b, + ) + }); // PvP check let may_harm = combat::may_harm( &alignments, &players, + &entered_auras, &id_maps, owner_entity, entity_b, @@ -1285,6 +1300,7 @@ impl ServerEvent for ExplosionEvent { let attack_options = combat::AttackOptions { target_dodging, may_harm, + allow_friendly_fire, target_group, precision_mult: None, }; @@ -1307,7 +1323,9 @@ impl ServerEvent for ExplosionEvent { }, RadiusEffect::Entity(mut effect) => { for (entity_b, pos_b, body_b_maybe) in - (&entities, &positions, bodies.maybe()).join() + (&entities, &positions, bodies.maybe()) + .join() + .filter(|(e, _, _)| owner_entity.map_or(true, |owner| owner != *e)) { let strength = if let Some(body) = body_b_maybe { cylinder_sphere_strength( @@ -1322,8 +1340,9 @@ impl ServerEvent for ExplosionEvent { 1.0 - distance_squared / ev.explosion.radius.powi(2) }; - // Player check only accounts for PvP/PvE flag, but bombs - // are intented to do friendly fire. + // Player check only accounts for PvP/PvE flag (unless in a friendly + // fire aura), but bombs are intented to do + // friendly fire. // // What exactly is friendly fire is subject to discussion. // As we probably want to minimize possibility of being dick @@ -1335,6 +1354,7 @@ impl ServerEvent for ExplosionEvent { combat::may_harm( &alignments, &players, + &entered_auras, &id_maps, owner_entity, entity_b, @@ -1504,11 +1524,16 @@ impl ServerEvent for BonkEvent { } impl ServerEvent for AuraEvent { - type SystemData<'a> = WriteStorage<'a, Auras>; + type SystemData<'a> = (WriteStorage<'a, Auras>, WriteStorage<'a, EnteredAuras>); - fn handle(events: impl ExactSizeIterator, mut auras: Self::SystemData<'_>) { + fn handle( + events: impl ExactSizeIterator, + (mut auras, mut entered_auras): Self::SystemData<'_>, + ) { for ev in events { - if let Some(mut auras) = auras.get_mut(ev.entity) { + if let (Some(mut auras), Some(mut entered_auras)) = + (auras.get_mut(ev.entity), entered_auras.get_mut(ev.entity)) + { use aura::AuraChange; match ev.aura_change { AuraChange::Add(new_aura) => { @@ -1519,6 +1544,24 @@ impl ServerEvent for AuraEvent { auras.remove(key); } }, + AuraChange::EnterAura(uid, key, variant) => { + entered_auras + .auras + .entry(variant) + .and_modify(|entered_auras| { + entered_auras.insert((uid, key)); + }) + .or_insert_with(|| <_ as Into<_>>::into([(uid, key)])); + }, + AuraChange::ExitAura(uid, key, variant) => { + if let Some(entered_auras_variant) = entered_auras.auras.get_mut(&variant) { + entered_auras_variant.remove(&(uid, key)); + + if entered_auras_variant.is_empty() { + entered_auras.auras.remove(&variant); + } + } + }, } } } diff --git a/voxygen/egui/src/lib.rs b/voxygen/egui/src/lib.rs index 330d1c2647..dcb039c96c 100644 --- a/voxygen/egui/src/lib.rs +++ b/voxygen/egui/src/lib.rs @@ -26,7 +26,10 @@ use crate::{ admin::draw_admin_commands_window, character_states::draw_char_state_group, experimental_shaders::draw_experimental_shaders_window, widgets::two_col_row, }; -use common::comp::{aura::AuraKind::Buff, Body, Fluid}; +use common::comp::{ + aura::AuraKind::{Buff, ForcePvP, FriendlyFire}, + Body, Fluid, +}; use egui_winit_platform::Platform; use std::time::Duration; #[cfg(feature = "use-dyn-lib")] @@ -711,7 +714,9 @@ fn selected_entity_window( ui.end_row(); auras.auras.iter().for_each(|(_, v)| { ui.label(match v.aura_kind { - Buff { kind, .. } => format!("Buff - {:?}", kind) + Buff { kind, .. } => format!("Buff - {:?}", kind), + FriendlyFire => "Friendly Fire".to_string(), + ForcePvP => "ForcedPvP".to_string(), }); ui.label(format!("{:1}", v.radius)); ui.label(v.end_time.map_or("-".to_owned(), |x| format!("{:1}s", x.0 - time.0)));