diff --git a/Content.Server/Friends/Systems/PettableFriendSystem.cs b/Content.Server/Friends/Systems/PettableFriendSystem.cs index 6ef9724877..c4f6586341 100644 --- a/Content.Server/Friends/Systems/PettableFriendSystem.cs +++ b/Content.Server/Friends/Systems/PettableFriendSystem.cs @@ -9,7 +9,7 @@ namespace Content.Server.Friends.Systems; public sealed class PettableFriendSystem : EntitySystem { - [Dependency] private readonly FactionExceptionSystem _factionException = default!; + [Dependency] private readonly NpcFactionSystem _factionException = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; public override void Initialize() @@ -26,7 +26,7 @@ public sealed class PettableFriendSystem : EntitySystem if (args.Handled || !TryComp(uid, out var factionException)) return; - if (_factionException.IsIgnored(factionException, user)) + if (_factionException.IsIgnored(uid, user, factionException)) { _popup.PopupEntity(Loc.GetString(comp.FailureString, ("target", uid)), user, user); return; @@ -34,7 +34,7 @@ public sealed class PettableFriendSystem : EntitySystem // you have made a new friend :) _popup.PopupEntity(Loc.GetString(comp.SuccessString, ("target", uid)), user, user); - _factionException.IgnoreEntity(factionException, user); + _factionException.IgnoreEntity(uid, user, factionException); args.Handled = true; } @@ -45,6 +45,6 @@ public sealed class PettableFriendSystem : EntitySystem return; var targetComp = AddComp(args.Target); - _factionException.IgnoreEntities(targetComp, comp.Ignored); + _factionException.IgnoreEntities(args.Target, comp.Ignored, targetComp); } } diff --git a/Content.Server/NPC/Components/FactionExceptionComponent.cs b/Content.Server/NPC/Components/FactionExceptionComponent.cs index e73b34905e..6abd503537 100644 --- a/Content.Server/NPC/Components/FactionExceptionComponent.cs +++ b/Content.Server/NPC/Components/FactionExceptionComponent.cs @@ -6,12 +6,18 @@ namespace Content.Server.NPC.Components; /// Prevents an NPC from attacking ignored entities from enemy factions. /// Can be added to if pettable, see PettableFriendComponent. /// -[RegisterComponent, Access(typeof(FactionExceptionSystem))] +[RegisterComponent, Access(typeof(NpcFactionSystem))] public sealed partial class FactionExceptionComponent : Component { /// - /// List of entities that this NPC will refuse to attack + /// Collection of entities that this NPC will refuse to attack /// [DataField("ignored")] public HashSet Ignored = new(); + + /// + /// Collection of entities that this NPC will attack, regardless of faction. + /// + [DataField("hostiles")] + public HashSet Hostiles = new(); } diff --git a/Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs b/Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs new file mode 100644 index 0000000000..804a61b456 --- /dev/null +++ b/Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs @@ -0,0 +1,16 @@ +using Content.Server.NPC.Systems; + +namespace Content.Server.NPC.Components; + +/// +/// This is used for tracking entities stored in +/// +[RegisterComponent, Access(typeof(NpcFactionSystem))] +public sealed partial class FactionExceptionTrackerComponent : Component +{ + /// + /// entities with that are tracking this entity. + /// + [DataField("entities")] + public HashSet Entities = new(); +} diff --git a/Content.Server/NPC/Components/NPCRetaliationComponent.cs b/Content.Server/NPC/Components/NPCRetaliationComponent.cs new file mode 100644 index 0000000000..c0bf54d76e --- /dev/null +++ b/Content.Server/NPC/Components/NPCRetaliationComponent.cs @@ -0,0 +1,24 @@ +using Content.Server.NPC.Systems; + +namespace Content.Server.NPC.Components; + +/// +/// Entities with this component will retaliate against those who physically attack them. +/// It has an optional "memory" specification wherein it will only attack those entities for a specified length of time. +/// +[RegisterComponent, Access(typeof(NPCRetaliationSystem))] +public sealed partial class NPCRetaliationComponent : Component +{ + /// + /// How long after being attacked will an NPC continue to be aggressive to the attacker for. + /// + [DataField("attackMemoryLength"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan? AttackMemoryLength; + + /// + /// A dictionary that stores an entity and the time at which they will no longer be considered hostile. + /// + /// todo: this needs to support timeoffsetserializer at some point + [DataField("attackMemories")] + public Dictionary AttackMemories = new(); +} diff --git a/Content.Server/NPC/Systems/FactionExceptionSystem.cs b/Content.Server/NPC/Systems/FactionExceptionSystem.cs deleted file mode 100644 index 909fe3904d..0000000000 --- a/Content.Server/NPC/Systems/FactionExceptionSystem.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Server.NPC.Components; - -namespace Content.Server.NPC.Systems; - -/// -/// Prevents an NPC from attacking some entities from an enemy faction. -/// -public sealed class FactionExceptionSystem : EntitySystem -{ - /// - /// Returns whether the entity from an enemy faction won't be attacked - /// - public bool IsIgnored(FactionExceptionComponent comp, EntityUid target) - { - return comp.Ignored.Contains(target); - } - - /// - /// Prevents an entity from an enemy faction from being attacked - /// - public void IgnoreEntity(FactionExceptionComponent comp, EntityUid target) - { - comp.Ignored.Add(target); - } - - /// - /// Prevents a list of entities from an enemy faction from being attacked - /// - public void IgnoreEntities(FactionExceptionComponent comp, IEnumerable ignored) - { - comp.Ignored.UnionWith(ignored); - } -} diff --git a/Content.Server/NPC/Systems/NPCRetaliationSystem.cs b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs new file mode 100644 index 0000000000..a8bf1766f0 --- /dev/null +++ b/Content.Server/NPC/Systems/NPCRetaliationSystem.cs @@ -0,0 +1,90 @@ +using Content.Server.NPC.Components; +using Content.Shared.CombatMode; +using Content.Shared.Damage; +using Content.Shared.Mobs.Components; +using Robust.Shared.Collections; +using Robust.Shared.Timing; + +namespace Content.Server.NPC.Systems; + +/// +/// Handles NPC which become aggressive after being attacked. +/// +public sealed class NPCRetaliationSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly NpcFactionSystem _npcFaction = default!; + + private readonly HashSet _deAggroQueue = new(); + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnDamageChanged); + SubscribeLocalEvent(OnDisarmed); + } + + private void OnDamageChanged(EntityUid uid, NPCRetaliationComponent component, DamageChangedEvent args) + { + if (!args.DamageIncreased) + return; + + if (args.Origin is not { } origin) + return; + + TryRetaliate(uid, origin, component); + } + + private void OnDisarmed(EntityUid uid, NPCRetaliationComponent component, DisarmedEvent args) + { + TryRetaliate(uid, args.Source, component); + } + + public bool TryRetaliate(EntityUid uid, EntityUid target, NPCRetaliationComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + // don't retaliate against inanimate objects. + if (!HasComp(target)) + return false; + + if (_npcFaction.IsEntityFriendly(uid, target)) + return false; + + _npcFaction.AggroEntity(uid, target); + if (component.AttackMemoryLength is { } memoryLength) + { + component.AttackMemories[target] = _timing.CurTime + memoryLength; + } + + return true; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out var factionException, out var metaData)) + { + _deAggroQueue.Clear(); + + foreach (var ent in new ValueList(comp.AttackMemories.Keys)) + { + if (_timing.CurTime < comp.AttackMemories[ent]) + continue; + + if (TerminatingOrDeleted(ent, metaData)) + _deAggroQueue.Add(ent); + + _deAggroQueue.Add(ent); + } + + foreach (var ent in _deAggroQueue) + { + _npcFaction.DeAggroEntity(uid, ent, factionException); + } + } + } +} diff --git a/Content.Server/NPC/Systems/NpcFactionSystem.Exception.cs b/Content.Server/NPC/Systems/NpcFactionSystem.Exception.cs new file mode 100644 index 0000000000..acef9005ea --- /dev/null +++ b/Content.Server/NPC/Systems/NpcFactionSystem.Exception.cs @@ -0,0 +1,130 @@ +using System.Linq; +using Content.Server.NPC.Components; + +namespace Content.Server.NPC.Systems; + +/// +/// Prevents an NPC from attacking some entities from an enemy faction. +/// +public sealed partial class NpcFactionSystem +{ + private EntityQuery _exceptionQuery; + private EntityQuery _trackerQuery; + + public void InitializeException() + { + _exceptionQuery = GetEntityQuery(); + _trackerQuery = GetEntityQuery(); + + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnTrackerShutdown); + } + + private void OnShutdown(EntityUid uid, FactionExceptionComponent component, ComponentShutdown args) + { + foreach (var ent in component.Hostiles) + { + if (!_trackerQuery.TryGetComponent(ent, out var trackerComponent)) + continue; + trackerComponent.Entities.Remove(uid); + } + + foreach (var ent in component.Ignored) + { + if (!_trackerQuery.TryGetComponent(ent, out var trackerComponent)) + continue; + trackerComponent.Entities.Remove(uid); + } + } + + private void OnTrackerShutdown(EntityUid uid, FactionExceptionTrackerComponent component, ComponentShutdown args) + { + foreach (var ent in component.Entities) + { + if (!_exceptionQuery.TryGetComponent(ent, out var exceptionComponent)) + continue; + exceptionComponent.Ignored.Remove(uid); + exceptionComponent.Hostiles.Remove(uid); + } + } + + /// + /// Returns whether the entity from an enemy faction won't be attacked + /// + public bool IsIgnored(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) + { + if (!Resolve(uid, ref comp, false)) + return false; + + return comp.Ignored.Contains(target); + } + + /// + /// Returns the specific hostile entities for a given entity. + /// + public IEnumerable GetHostiles(EntityUid uid, FactionExceptionComponent? comp = null) + { + if (!Resolve(uid, ref comp, false)) + return Array.Empty(); + + return comp.Hostiles; + } + + /// + /// Prevents an entity from an enemy faction from being attacked + /// + public void IgnoreEntity(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) + { + comp ??= EnsureComp(uid); + comp.Ignored.Add(target); + EnsureComp(target).Entities.Add(uid); + } + + /// + /// Prevents a list of entities from an enemy faction from being attacked + /// + public void IgnoreEntities(EntityUid uid, IEnumerable ignored, FactionExceptionComponent? comp = null) + { + comp ??= EnsureComp(uid); + foreach (var ignore in ignored) + { + IgnoreEntity(uid, ignore, comp); + } + } + + /// + /// Makes an entity always be considered hostile. + /// + public void AggroEntity(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) + { + comp ??= EnsureComp(uid); + comp.Hostiles.Add(target); + EnsureComp(target).Entities.Add(uid); + } + + /// + /// Makes an entity no longer be considered hostile, if it was. + /// Doesn't apply to regular faction hostilities. + /// + public void DeAggroEntity(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null) + { + if (!Resolve(uid, ref comp, false)) + return; + if (!comp.Hostiles.Remove(target) || !_trackerQuery.TryGetComponent(target, out var tracker)) + return; + tracker.Entities.Remove(uid); + } + + /// + /// Makes a list of entities no longer be considered hostile, if it was. + /// Doesn't apply to regular faction hostilities. + /// + public void AggroEntities(EntityUid uid, IEnumerable entities, FactionExceptionComponent? comp = null) + { + comp ??= EnsureComp(uid); + foreach (var ent in entities) + { + AggroEntity(uid, ent, comp); + } + } +} diff --git a/Content.Server/NPC/Systems/NpcFactionSystem.cs b/Content.Server/NPC/Systems/NpcFactionSystem.cs index 79c4bbc7af..d6c23ca6af 100644 --- a/Content.Server/NPC/Systems/NpcFactionSystem.cs +++ b/Content.Server/NPC/Systems/NpcFactionSystem.cs @@ -1,15 +1,15 @@ using Content.Server.NPC.Components; using Robust.Shared.Prototypes; using System.Linq; +using JetBrains.Annotations; namespace Content.Server.NPC.Systems; /// /// Outlines faction relationships with each other. /// -public sealed class NpcFactionSystem : EntitySystem +public sealed partial class NpcFactionSystem : EntitySystem { - [Dependency] private readonly FactionExceptionSystem _factionException = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; @@ -26,6 +26,8 @@ public sealed class NpcFactionSystem : EntitySystem _sawmill = Logger.GetSawmill("faction"); SubscribeLocalEvent(OnFactionStartup); _protoManager.PrototypesReloaded += OnProtoReload; + + InitializeException(); RefreshFactions(); } @@ -134,12 +136,15 @@ public sealed class NpcFactionSystem : EntitySystem if (TryComp(entity, out var factionException)) { // ignore anything from enemy faction that we are explicitly friendly towards - return hostiles.Where(target => !_factionException.IsIgnored(factionException, target)); + return hostiles + .Union(GetHostiles(entity, factionException)) + .Where(target => !IsIgnored(entity, target, factionException)); } return hostiles; } + [PublicAPI] public IEnumerable GetNearbyFriendlies(EntityUid entity, float range, NpcFactionMemberComponent? component = null) { if (!Resolve(entity, ref component, false)) diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 0f6cbc0ad9..fc75798b94 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -111,6 +111,14 @@ bloodMaxVolume: 0.1 - type: MobPrice price: 50 + - type: NPCRetaliation + - type: FactionException + - type: NpcFactionMember + factions: + - Passive + - type: HTN + rootTask: + task: SimpleHostileCompound - type: Puller needsHands: true @@ -560,6 +568,8 @@ states: Alive: Base: goat + Critical: + Base: dead Dead: Base: dead - type: SolutionContainerManager @@ -592,9 +602,12 @@ - Passive - type: Body prototype: AnimalRuminant + - type: NPCRetaliation + attackMemoryLength: 5 + - type: FactionException - type: HTN rootTask: - task: RuminantCompound + task: RuminantHostileCompound # Note that we gotta make this bitch vomit someday when you feed it anthrax or sumthin. Needs to be a small item thief too and aggressive if attacked. - type: entity @@ -671,6 +684,8 @@ states: Alive: Base: crawling + Critical: + Base: dead Dead: Base: dead - type: Butcherable @@ -679,6 +694,20 @@ amount: 4 - type: Bloodstream bloodMaxVolume: 300 + # if you fuck with the gorilla he will harambe you + - type: MeleeWeapon + damage: + types: + Blunt: 20 + animation: WeaponArcFist + - type: NPCRetaliation + - type: FactionException + - type: NpcFactionMember + factions: + - Passive + - type: HTN + rootTask: + task: SimpleHostileCompound - type: Puller - type: entity @@ -760,6 +789,15 @@ soundHit: collection: BoxingHit animation: WeaponArcFist + - type: NPCRetaliation + attackMemoryLength: 10 + - type: FactionException + - type: NpcFactionMember + factions: + - Passive + - type: HTN + rootTask: + task: SimpleHostileCompound - type: entity name: boxing kangaroo @@ -768,9 +806,6 @@ components: - type: Loadout prototypes: [ BoxingKangarooGear ] - - type: HTN - rootTask: - task: SimpleHostileCompound - type: NpcFactionMember factions: - SimpleHostile @@ -857,9 +892,15 @@ - type: MonkeyAccent - type: Puller - type: CanHostGuardian + - type: NPCRetaliation + attackMemoryLength: 10 + - type: FactionException - type: NpcFactionMember factions: - - Passive + - Passive + - type: HTN + rootTask: + task: SimpleHostileCompound - type: GhostRole prob: 0.05 makeSentient: true diff --git a/Resources/Prototypes/NPCs/mob.yml b/Resources/Prototypes/NPCs/mob.yml index bba5a76ad8..5b25f85a79 100644 --- a/Resources/Prototypes/NPCs/mob.yml +++ b/Resources/Prototypes/NPCs/mob.yml @@ -39,6 +39,19 @@ - !type:HTNCompoundTask task: IdleCompound +- type: htnCompound + id: RuminantHostileCompound + branches: + - tasks: + - !type:HTNCompoundTask + task: MeleeCombatCompound + - tasks: + - !type:HTNCompoundTask + task: FoodCompound + - tasks: + - !type:HTNCompoundTask + task: IdleCompound + - type: htnCompound id: DragonCarpCompound branches: