FriendlyFire and ForcePvP auras

This commit is contained in:
crabman
2024-03-22 21:38:28 +00:00
parent 7afadf5b56
commit af4f147fda
15 changed files with 327 additions and 71 deletions

View File

@ -1,6 +1,7 @@
use crate::{ use crate::{
comp::{ comp::{
ability::Capability, ability::Capability,
aura::{AuraKindVariant, EnteredAuras},
buff::{Buff, BuffChange, BuffData, BuffKind, BuffSource}, buff::{Buff, BuffChange, BuffData, BuffKind, BuffSource},
inventory::{ inventory::{
item::{ item::{
@ -89,8 +90,12 @@ pub struct TargetInfo<'a> {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct AttackOptions { pub struct AttackOptions {
pub target_dodging: bool, pub target_dodging: bool,
/// Result of [`may_harm`]
pub may_harm: bool, pub may_harm: bool,
pub target_group: GroupTarget, 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<f32>, pub precision_mult: Option<f32>,
} }
@ -271,6 +276,7 @@ impl Attack {
let AttackOptions { let AttackOptions {
target_dodging, target_dodging,
may_harm, may_harm,
allow_friendly_fire,
target_group, target_group,
precision_mult, precision_mult,
} = options; } = options;
@ -279,13 +285,13 @@ impl Attack {
// "attack" has negative effects. // "attack" has negative effects.
// //
// so if target dodges this "attack" or we don't want to harm target, // 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| { 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) && (target_dodging || !may_harm)
}; };
let avoid_effect = |attack_effect: &AttackEffect| { 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) && (target_dodging || !may_harm)
}; };
let precision_mult = attacker let precision_mult = attacker
@ -300,7 +306,7 @@ impl Attack {
for damage in self for damage in self
.damages .damages
.iter() .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)) .filter(|d| !avoid_damage(d))
{ {
let damage_instance = damage.instance + damage_instance_offset; let damage_instance = damage.instance + damage_instance_offset;
@ -594,7 +600,7 @@ impl Attack {
.iter() .iter()
.flat_map(|stats| stats.effects_on_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)) .filter(|e| !avoid_effect(e))
{ {
let requirements_met = effect.requirements.iter().all(|req| match req { let requirements_met = effect.requirements.iter().all(|req| match req {
@ -778,6 +784,24 @@ impl Attack {
} }
} }
pub fn allow_friendly_fire(
entered_auras: &ReadStorage<EnteredAuras>,
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. /// Function that checks for unintentional PvP between players.
/// ///
/// Returns `false` if attack will create unintentional conflict, /// Returns `false` if attack will create unintentional conflict,
@ -790,6 +814,7 @@ impl Attack {
pub fn may_harm( pub fn may_harm(
alignments: &ReadStorage<Alignment>, alignments: &ReadStorage<Alignment>,
players: &ReadStorage<Player>, players: &ReadStorage<Player>,
entered_auras: &ReadStorage<EnteredAuras>,
id_maps: &IdMaps, id_maps: &IdMaps,
attacker: Option<EcsEntity>, attacker: Option<EcsEntity>,
target: EcsEntity, target: EcsEntity,
@ -815,17 +840,32 @@ pub fn may_harm(
}; };
// "Dereference" to owner if this is a pet. // "Dereference" to owner if this is a pet.
let attacker = owner_if_pet(attacker); let attacker_owner = owner_if_pet(attacker);
let target = owner_if_pet(target); let target_owner = owner_if_pet(target);
// Prevent owners from attacking their pets and vice versa // If both players are in the same ForcePvP aura, allow them to harm eachother
if attacker == target { if let (Some(attacker_auras), Some(target_auras)) = (
return false; 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 // Get player components
let attacker_info = players.get(attacker); let attacker_info = players.get(attacker_owner);
let target_info = players.get(target); let target_info = players.get(target_owner);
// Return `true` if not players. // Return `true` if not players.
attacker_info attacker_info

View File

@ -7,6 +7,7 @@ use crate::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use slotmap::{new_key_type, SlotMap}; use slotmap::{new_key_type, SlotMap};
use specs::{Component, DerefFlaggedStorage, VecStorage}; use specs::{Component, DerefFlaggedStorage, VecStorage};
use std::collections::{HashMap, HashSet};
new_key_type! { pub struct AuraKey; } new_key_type! { pub struct AuraKey; }
@ -21,12 +22,26 @@ pub enum AuraKind {
category: BuffCategory, category: BuffCategory,
source: BuffSource, 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 /* TODO: Implement other effects here. Things to think about
* are terrain/sprite effects, collision and physics, and * are terrain/sprite effects, collision and physics, and
* environmental conditions like temperature and humidity * environmental conditions like temperature and humidity
* Multiple auras can be given to an entity. */ * 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 /// Aura
/// Applies a buff to entities in the radius if meeting /// Applies a buff to entities in the radius if meeting
/// conditions set forth in the aura system. /// conditions set forth in the aura system.
@ -57,6 +72,8 @@ pub enum AuraChange {
Add(Aura), Add(Aura),
/// Removes auras of these indices /// Removes auras of these indices
RemoveByKey(Vec<AuraKey>), RemoveByKey(Vec<AuraKey>),
EnterAura(Uid, AuraKey, AuraKindVariant),
ExitAura(Uid, AuraKey, AuraKindVariant),
} }
/// Used by the aura system to filter entities when applying an effect. /// Used by the aura system to filter entities when applying an effect.
@ -73,6 +90,13 @@ pub enum AuraTarget {
All, All,
} }
// Only used for parsing in commands
pub enum SimpleAuraTarget {
Group,
OutOfGroup,
All,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum Specifier { pub enum Specifier {
WardingAura, WardingAura,
@ -91,6 +115,16 @@ impl From<(Option<GroupTarget>, Option<&Uid>)> for AuraTarget {
} }
} }
impl AsRef<AuraKindVariant> 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuraData { pub struct AuraData {
pub duration: Option<Secs>, pub duration: Option<Secs>,
@ -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<AuraKindVariant, HashSet<(Uid, AuraKey)>>,
}
impl EnteredAuras {
pub fn flatten(&self) -> impl Iterator<Item = (Uid, AuraKey)> + '_ {
self.auras.values().flat_map(|i| i.iter().copied())
}
}
impl Component for Auras { impl Component for Auras {
type Storage = DerefFlaggedStorage<Self, VecStorage<Self>>; type Storage = DerefFlaggedStorage<Self, VecStorage<Self>>;
} }
impl Component for EnteredAuras {
type Storage = DerefFlaggedStorage<Self, VecStorage<Self>>;
}

View File

@ -49,7 +49,7 @@ pub use self::{
TradingBehavior, TradingBehavior,
}, },
anchor::Anchor, anchor::Anchor,
aura::{Aura, AuraChange, AuraKind, Auras}, aura::{Aura, AuraChange, AuraKind, Auras, EnteredAuras},
beam::Beam, beam::Beam,
body::{ body::{
arthropod, biped_large, biped_small, bird_large, bird_medium, crustacean, dragon, arthropod, biped_large, biped_small, bird_large, bird_medium, crustacean, dragon,

View File

@ -305,6 +305,7 @@ pub struct ExitIngameEvent {
pub entity: EcsEntity, pub entity: EcsEntity,
} }
#[derive(Debug)]
pub struct AuraEvent { pub struct AuraEvent {
pub entity: EcsEntity, pub entity: EcsEntity,
pub aura_change: comp::AuraChange, pub aura_change: comp::AuraChange,

View File

@ -94,6 +94,7 @@ impl CharacterBehavior for Data {
data.strength *= data.strength *=
(self.static_data.combo_at_cast.max(1) as f32).sqrt(); (self.static_data.combo_at_cast.max(1) as f32).sqrt();
}, },
AuraKind::FriendlyFire | AuraKind::ForcePvP => {},
} }
output_events.emit_server(ComboChangeEvent { output_events.emit_server(ComboChangeEvent {
entity: data.entity, entity: data.entity,

View File

@ -243,6 +243,7 @@ impl State {
ecs.register::<comp::ActiveAbilities>(); ecs.register::<comp::ActiveAbilities>();
ecs.register::<comp::Buffs>(); ecs.register::<comp::Buffs>();
ecs.register::<comp::Auras>(); ecs.register::<comp::Auras>();
ecs.register::<comp::EnteredAuras>();
ecs.register::<comp::Energy>(); ecs.register::<comp::Energy>();
ecs.register::<comp::Combo>(); ecs.register::<comp::Combo>();
ecs.register::<comp::Health>(); ecs.register::<comp::Health>();

View File

@ -1,7 +1,9 @@
use std::collections::HashSet;
use common::{ use common::{
combat, combat,
comp::{ comp::{
aura::{AuraChange, AuraKey, AuraKind, AuraTarget}, aura::{AuraChange, AuraKey, AuraKind, AuraTarget, EnteredAuras},
buff::{Buff, BuffCategory, BuffChange, BuffSource}, buff::{Buff, BuffCategory, BuffChange, BuffSource},
group::Group, group::Group,
Alignment, Aura, Auras, BuffKind, Buffs, CharacterState, Health, Player, Pos, Stats, Alignment, Aura, Auras, BuffKind, Buffs, CharacterState, Health, Player, Pos, Stats,
@ -38,6 +40,7 @@ pub struct ReadData<'a> {
stats: ReadStorage<'a, Stats>, stats: ReadStorage<'a, Stats>,
buffs: ReadStorage<'a, Buffs>, buffs: ReadStorage<'a, Buffs>,
auras: ReadStorage<'a, Auras>, auras: ReadStorage<'a, Auras>,
entered_auras: ReadStorage<'a, EnteredAuras>,
} }
#[derive(Default)] #[derive(Default)]
@ -51,6 +54,7 @@ impl<'a> System<'a> for Sys {
fn run(_job: &mut Job<Self>, read_data: Self::SystemData) { fn run(_job: &mut Job<Self>, read_data: Self::SystemData) {
let mut emitters = read_data.events.get_emitters(); 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 // Iterate through all entities with an aura
for (entity, pos, auras_comp, uid) in ( for (entity, pos, auras_comp, uid) in (
@ -75,22 +79,19 @@ impl<'a> System<'a> for Sys {
.0 .0
.in_circle_aabr(pos.0.xy(), aura.radius) .in_circle_aabr(pos.0.xy(), aura.radius)
.filter_map(|target| { .filter_map(|target| {
read_data read_data.positions.get(target).and_then(|target_pos| {
.positions Some((
.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,
target_pos, target_pos,
health, read_data.healths.get(target)?,
target_uid, read_data.uids.get(target)?,
read_data.entered_auras.get(target)?,
read_data.stats.get(target), read_data.stats.get(target),
) ))
}) })
}); });
target_iter.for_each(|(target, target_pos, health, target_uid, stats)| { target_iter.for_each(
|(target, target_pos, health, target_uid, entered_auras, stats)| {
let target_buffs = match read_data.buffs.get(target) { let target_buffs = match read_data.buffs.get(target) {
Some(buff) => buff, Some(buff) => buff,
None => return, None => return,
@ -110,14 +111,23 @@ impl<'a> System<'a> for Sys {
|| *target_uid == uid || *target_uid == uid
}; };
let is_target = match aura.target { 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::GroupOf(uid) => same_group(uid),
AuraTarget::NotGroupOf(uid) => !same_group(uid), AuraTarget::NotGroupOf(uid) => !same_group(uid),
AuraTarget::All => true, AuraTarget::All => true,
}; })
{
return;
}
if is_target { let did_activate = activate_aura(
activate_aura(
key, key,
aura, aura,
*uid, *uid,
@ -125,13 +135,32 @@ impl<'a> System<'a> for Sys {
health, health,
target_buffs, target_buffs,
stats, stats,
allow_friendly_fire,
&read_data, &read_data,
&mut emitters, &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() { if !expired_auras.is_empty() {
emitters.emit(AuraEvent { emitters.emit(AuraEvent {
entity, entity,
@ -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, health: &Health,
target_buffs: &Buffs, target_buffs: &Buffs,
stats: Option<&Stats>, stats: Option<&Stats>,
allow_friendly_fire: bool,
read_data: &ReadData, read_data: &ReadData,
emitters: &mut impl EmitExt<BuffEvent>, emitters: &mut impl EmitExt<BuffEvent>,
) { ) -> bool {
let should_activate = match aura.aura_kind { let should_activate = match aura.aura_kind {
AuraKind::Buff { kind, source, .. } => { AuraKind::Buff { kind, source, .. } => {
let conditions_held = match kind { let conditions_held = match kind {
@ -195,18 +249,24 @@ fn activate_aura(
combat::may_harm( combat::may_harm(
&read_data.alignments, &read_data.alignments,
&read_data.players, &read_data.players,
&read_data.entered_auras,
&read_data.id_maps, &read_data.id_maps,
owner, owner,
target, 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 { if !should_activate {
return; return false;
} }
// TODO: When more aura kinds (besides Buff) are // 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
} }

View File

@ -2,6 +2,7 @@ use common::{
combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo}, combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
comp::{ comp::{
agent::{Sound, SoundKind}, agent::{Sound, SoundKind},
aura::EnteredAuras,
Alignment, Beam, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori, Alignment, Beam, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori,
Player, Pos, Scale, Stats, Player, Pos, Scale, Stats,
}, },
@ -59,6 +60,7 @@ pub struct ReadData<'a> {
combos: ReadStorage<'a, Combo>, combos: ReadStorage<'a, Combo>,
character_states: ReadStorage<'a, CharacterState>, character_states: ReadStorage<'a, CharacterState>,
buffs: ReadStorage<'a, Buffs>, buffs: ReadStorage<'a, Buffs>,
entered_auras: ReadStorage<'a, EnteredAuras>,
outcomes: Read<'a, EventBus<Outcome>>, outcomes: Read<'a, EventBus<Outcome>>,
events: ReadAttackEvents<'a>, events: ReadAttackEvents<'a>,
} }
@ -203,6 +205,12 @@ impl<'a> System<'a> for Sys {
>= tgt_dist; >= tgt_dist;
if hit { if hit {
let allow_friendly_fire = combat::allow_friendly_fire(
&read_data.entered_auras,
entity,
target,
);
// See if entities are in the same group // See if entities are in the same group
let same_group = group let same_group = group
.map(|group_a| Some(group_a) == read_data.groups.get(target)) .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( let may_harm = combat::may_harm(
&read_data.alignments, &read_data.alignments,
&read_data.players, &read_data.players,
&read_data.entered_auras,
&read_data.id_maps, &read_data.id_maps,
Some(entity), Some(entity),
target, target,
@ -276,6 +285,7 @@ impl<'a> System<'a> for Sys {
let attack_options = AttackOptions { let attack_options = AttackOptions {
target_dodging, target_dodging,
may_harm, may_harm,
allow_friendly_fire,
target_group, target_group,
precision_mult, precision_mult,
}; };

View File

@ -2,7 +2,7 @@ use common::{
combat::{self, DamageContributor}, combat::{self, DamageContributor},
comp::{ comp::{
agent::{Sound, SoundKind}, agent::{Sound, SoundKind},
aura::Auras, aura::{Auras, EnteredAuras},
body::{object, Body}, body::{object, Body},
buff::{ buff::{
Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffKey, BuffKind, BuffSource, Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffKey, BuffKind, BuffSource,
@ -61,6 +61,7 @@ pub struct ReadData<'a> {
msm: ReadExpect<'a, MaterialStatManifest>, msm: ReadExpect<'a, MaterialStatManifest>,
buffs: ReadStorage<'a, Buffs>, buffs: ReadStorage<'a, Buffs>,
auras: ReadStorage<'a, Auras>, auras: ReadStorage<'a, Auras>,
entered_auras: ReadStorage<'a, EnteredAuras>,
positions: ReadStorage<'a, Pos>, positions: ReadStorage<'a, Pos>,
bodies: ReadStorage<'a, Body>, bodies: ReadStorage<'a, Body>,
light_emitters: ReadStorage<'a, LightEmitter>, light_emitters: ReadStorage<'a, LightEmitter>,
@ -165,6 +166,7 @@ impl<'a> System<'a> for Sys {
combat::may_harm( combat::may_harm(
&read_data.alignments, &read_data.alignments,
&read_data.players, &read_data.players,
&read_data.entered_auras,
&read_data.id_maps, &read_data.id_maps,
Some(entity), Some(entity),
*te, *te,

View File

@ -2,6 +2,7 @@ use common::{
combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo}, combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
comp::{ comp::{
agent::{Sound, SoundKind}, agent::{Sound, SoundKind},
aura::EnteredAuras,
melee::MultiTarget, melee::MultiTarget,
Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Melee, Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Melee,
Ori, Player, Pos, Scale, Stats, Ori, Player, Pos, Scale, Stats,
@ -59,6 +60,7 @@ pub struct ReadData<'a> {
stats: ReadStorage<'a, Stats>, stats: ReadStorage<'a, Stats>,
combos: ReadStorage<'a, Combo>, combos: ReadStorage<'a, Combo>,
buffs: ReadStorage<'a, Buffs>, buffs: ReadStorage<'a, Buffs>,
entered_auras: ReadStorage<'a, EnteredAuras>,
events: ReadAttackEvents<'a>, events: ReadAttackEvents<'a>,
} }
@ -186,6 +188,8 @@ impl<'a> System<'a> for Sys {
&& !is_blocked_by_wall(&read_data.terrain, attacker_cylinder, target_cylinder); && !is_blocked_by_wall(&read_data.terrain, attacker_cylinder, target_cylinder);
if hit { if hit {
let allow_friendly_fire =
combat::allow_friendly_fire(&read_data.entered_auras, attacker, target);
// See if entities are in the same group // See if entities are in the same group
let same_group = read_data let same_group = read_data
.groups .groups
@ -230,6 +234,7 @@ impl<'a> System<'a> for Sys {
let may_harm = combat::may_harm( let may_harm = combat::may_harm(
&read_data.alignments, &read_data.alignments,
&read_data.players, &read_data.players,
&read_data.entered_auras,
&read_data.id_maps, &read_data.id_maps,
Some(attacker), Some(attacker),
target, target,
@ -265,6 +270,7 @@ impl<'a> System<'a> for Sys {
let attack_options = AttackOptions { let attack_options = AttackOptions {
target_dodging, target_dodging,
may_harm, may_harm,
allow_friendly_fire,
target_group, target_group,
precision_mult, precision_mult,
}; };

View File

@ -2,6 +2,7 @@ use common::{
combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo}, combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
comp::{ comp::{
agent::{Sound, SoundKind}, agent::{Sound, SoundKind},
aura::EnteredAuras,
projectile, Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, projectile, Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health,
Inventory, Ori, PhysicsState, Player, Pos, Projectile, Stats, Vel, Inventory, Ori, PhysicsState, Player, Pos, Projectile, Stats, Vel,
}, },
@ -71,6 +72,7 @@ pub struct ReadData<'a> {
character_states: ReadStorage<'a, CharacterState>, character_states: ReadStorage<'a, CharacterState>,
terrain: ReadExpect<'a, TerrainGrid>, terrain: ReadExpect<'a, TerrainGrid>,
buffs: ReadStorage<'a, Buffs>, buffs: ReadStorage<'a, Buffs>,
entered_auras: ReadStorage<'a, EnteredAuras>,
} }
/// This system is responsible for handling projectile effect triggers /// This system is responsible for handling projectile effect triggers
@ -138,7 +140,20 @@ impl<'a> System<'a> for Sys {
GroupTarget::OutOfGroup 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; 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 // PvP check
let may_harm = combat::may_harm( let may_harm = combat::may_harm(
&read_data.alignments, &read_data.alignments,
&read_data.players, &read_data.players,
&read_data.entered_auras,
&read_data.id_maps, &read_data.id_maps,
owner, owner,
target, target,
@ -448,6 +468,7 @@ fn dispatch_hit(
let attack_options = AttackOptions { let attack_options = AttackOptions {
target_dodging, target_dodging,
may_harm, may_harm,
allow_friendly_fire,
target_group: projectile_target_info.target_group, target_group: projectile_target_info.target_group,
precision_mult, precision_mult,
}; };

View File

@ -2,6 +2,7 @@ use common::{
combat::{self, AttackOptions, AttackerInfo, TargetInfo}, combat::{self, AttackOptions, AttackerInfo, TargetInfo},
comp::{ comp::{
agent::{Sound, SoundKind}, agent::{Sound, SoundKind},
aura::EnteredAuras,
shockwave::ShockwaveDodgeable, shockwave::ShockwaveDodgeable,
Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori, Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori,
PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats, PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
@ -62,6 +63,7 @@ pub struct ReadData<'a> {
combos: ReadStorage<'a, Combo>, combos: ReadStorage<'a, Combo>,
character_states: ReadStorage<'a, CharacterState>, character_states: ReadStorage<'a, CharacterState>,
buffs: ReadStorage<'a, Buffs>, buffs: ReadStorage<'a, Buffs>,
entered_auras: ReadStorage<'a, EnteredAuras>,
} }
/// This system is responsible for handling accepted inputs like moving or /// 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 // Check if it is a hit
let hit = entity != target let hit = entity != target
&& shockwave_owner.map_or(true, |owner| owner != target)
&& !health_b.is_dead && !health_b.is_dead
&& (pos_b.0 - pos.0).magnitude() < frame_end_dist + rad_b && (pos_b.0 - pos.0).magnitude() < frame_end_dist + rad_b
// Collision shapes // Collision shapes
@ -208,6 +211,9 @@ impl<'a> System<'a> for Sys {
}; };
if hit { 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 dir = Dir::from_unnormalized(pos_b.0 - pos.0).unwrap_or(look_dir);
let attacker_info = let attacker_info =
@ -249,6 +255,7 @@ impl<'a> System<'a> for Sys {
let may_harm = combat::may_harm( let may_harm = combat::may_harm(
&read_data.alignments, &read_data.alignments,
&read_data.players, &read_data.players,
&read_data.entered_auras,
&read_data.id_maps, &read_data.id_maps,
shockwave_owner, shockwave_owner,
target, target,
@ -258,6 +265,7 @@ impl<'a> System<'a> for Sys {
let attack_options = AttackOptions { let attack_options = AttackOptions {
target_dodging, target_dodging,
may_harm, may_harm,
allow_friendly_fire,
target_group, target_group,
precision_mult, precision_mult,
}; };

View File

@ -1,7 +1,8 @@
use common::{ use common::{
comp::{ comp::{
inventory::item::MaterialStatManifest, tool::AbilityMap, Auras, Buffs, CharacterActivity, 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}, resources::{DeltaTime, GameMode, Time},
shared_server_config::ServerConstants, shared_server_config::ServerConstants,
@ -128,6 +129,7 @@ pub fn create_player(state: &mut State) -> Entity {
.with(Buffs::default()) .with(Buffs::default())
.with(Combo::default()) .with(Combo::default())
.with(Auras::default()) .with(Auras::default())
.with(EnteredAuras::default())
.with(Energy::new(body)) .with(Energy::new(body))
.with(Health::new(body)) .with(Health::new(body))
.with(skill_set) .with(skill_set)

View File

@ -18,7 +18,9 @@ use common::{
combat, combat,
combat::{AttackSource, DamageContributor}, combat::{AttackSource, DamageContributor},
comp::{ comp::{
self, aura, buff, self,
aura::{self, EnteredAuras},
buff,
chat::{KillSource, KillType}, chat::{KillSource, KillType},
inventory::item::{AbilityMap, MaterialStatManifest}, inventory::item::{AbilityMap, MaterialStatManifest},
item::flatten_counted_items, item::flatten_counted_items,
@ -945,6 +947,7 @@ impl ServerEvent for ExplosionEvent {
ReadStorage<'a, comp::Combo>, ReadStorage<'a, comp::Combo>,
ReadStorage<'a, Inventory>, ReadStorage<'a, Inventory>,
ReadStorage<'a, Alignment>, ReadStorage<'a, Alignment>,
ReadStorage<'a, EnteredAuras>,
ReadStorage<'a, comp::Buffs>, ReadStorage<'a, comp::Buffs>,
ReadStorage<'a, comp::Stats>, ReadStorage<'a, comp::Stats>,
ReadStorage<'a, Health>, ReadStorage<'a, Health>,
@ -975,6 +978,7 @@ impl ServerEvent for ExplosionEvent {
combos, combos,
inventories, inventories,
alignments, alignments,
entered_auras,
buffs, buffs,
stats, stats,
healths, healths,
@ -1199,7 +1203,9 @@ impl ServerEvent for ExplosionEvent {
), ),
) )
.join() .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); 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 let target_dodging = char_state_b_maybe
.and_then(|cs| cs.attack_immunities()) .and_then(|cs| cs.attack_immunities())
.map_or(false, |i| i.explosions); .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 // PvP check
let may_harm = combat::may_harm( let may_harm = combat::may_harm(
&alignments, &alignments,
&players, &players,
&entered_auras,
&id_maps, &id_maps,
owner_entity, owner_entity,
entity_b, entity_b,
@ -1285,6 +1300,7 @@ impl ServerEvent for ExplosionEvent {
let attack_options = combat::AttackOptions { let attack_options = combat::AttackOptions {
target_dodging, target_dodging,
may_harm, may_harm,
allow_friendly_fire,
target_group, target_group,
precision_mult: None, precision_mult: None,
}; };
@ -1307,7 +1323,9 @@ impl ServerEvent for ExplosionEvent {
}, },
RadiusEffect::Entity(mut effect) => { RadiusEffect::Entity(mut effect) => {
for (entity_b, pos_b, body_b_maybe) in 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 { let strength = if let Some(body) = body_b_maybe {
cylinder_sphere_strength( cylinder_sphere_strength(
@ -1322,8 +1340,9 @@ impl ServerEvent for ExplosionEvent {
1.0 - distance_squared / ev.explosion.radius.powi(2) 1.0 - distance_squared / ev.explosion.radius.powi(2)
}; };
// Player check only accounts for PvP/PvE flag, but bombs // Player check only accounts for PvP/PvE flag (unless in a friendly
// are intented to do friendly fire. // fire aura), but bombs are intented to do
// friendly fire.
// //
// What exactly is friendly fire is subject to discussion. // What exactly is friendly fire is subject to discussion.
// As we probably want to minimize possibility of being dick // As we probably want to minimize possibility of being dick
@ -1335,6 +1354,7 @@ impl ServerEvent for ExplosionEvent {
combat::may_harm( combat::may_harm(
&alignments, &alignments,
&players, &players,
&entered_auras,
&id_maps, &id_maps,
owner_entity, owner_entity,
entity_b, entity_b,
@ -1504,11 +1524,16 @@ impl ServerEvent for BonkEvent {
} }
impl ServerEvent for AuraEvent { impl ServerEvent for AuraEvent {
type SystemData<'a> = WriteStorage<'a, Auras>; type SystemData<'a> = (WriteStorage<'a, Auras>, WriteStorage<'a, EnteredAuras>);
fn handle(events: impl ExactSizeIterator<Item = Self>, mut auras: Self::SystemData<'_>) { fn handle(
events: impl ExactSizeIterator<Item = Self>,
(mut auras, mut entered_auras): Self::SystemData<'_>,
) {
for ev in events { 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; use aura::AuraChange;
match ev.aura_change { match ev.aura_change {
AuraChange::Add(new_aura) => { AuraChange::Add(new_aura) => {
@ -1519,6 +1544,24 @@ impl ServerEvent for AuraEvent {
auras.remove(key); 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);
}
}
},
} }
} }
} }

View File

@ -26,7 +26,10 @@ use crate::{
admin::draw_admin_commands_window, character_states::draw_char_state_group, admin::draw_admin_commands_window, character_states::draw_char_state_group,
experimental_shaders::draw_experimental_shaders_window, widgets::two_col_row, 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 egui_winit_platform::Platform;
use std::time::Duration; use std::time::Duration;
#[cfg(feature = "use-dyn-lib")] #[cfg(feature = "use-dyn-lib")]
@ -711,7 +714,9 @@ fn selected_entity_window(
ui.end_row(); ui.end_row();
auras.auras.iter().for_each(|(_, v)| { auras.auras.iter().for_each(|(_, v)| {
ui.label(match v.aura_kind { 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(format!("{:1}", v.radius));
ui.label(v.end_time.map_or("-".to_owned(), |x| format!("{:1}s", x.0 - time.0))); ui.label(v.end_time.map_or("-".to_owned(), |x| format!("{:1}s", x.0 - time.0)));