]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add mob retaliation (#19901)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Sat, 7 Oct 2023 00:56:18 +0000 (20:56 -0400)
committerGitHub <noreply@github.com>
Sat, 7 Oct 2023 00:56:18 +0000 (17:56 -0700)
Content.Server/Friends/Systems/PettableFriendSystem.cs
Content.Server/NPC/Components/FactionExceptionComponent.cs
Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs [new file with mode: 0644]
Content.Server/NPC/Components/NPCRetaliationComponent.cs [new file with mode: 0644]
Content.Server/NPC/Systems/FactionExceptionSystem.cs [deleted file]
Content.Server/NPC/Systems/NPCRetaliationSystem.cs [new file with mode: 0644]
Content.Server/NPC/Systems/NpcFactionSystem.Exception.cs [new file with mode: 0644]
Content.Server/NPC/Systems/NpcFactionSystem.cs
Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
Resources/Prototypes/NPCs/mob.yml

index 6ef9724877aee04da5f770231e415435549e5c66..c4f65863416eea8cbd2d27924a7122afc1733c1e 100644 (file)
@@ -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<FactionExceptionComponent>(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<FactionExceptionComponent>(args.Target);
-        _factionException.IgnoreEntities(targetComp, comp.Ignored);
+        _factionException.IgnoreEntities(args.Target, comp.Ignored, targetComp);
     }
 }
index e73b34905e119f36af01aa053d30de5a37bd2272..6abd503537c78afbb1bbd4c82984b213a5fdc85e 100644 (file)
@@ -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.
 /// </summary>
-[RegisterComponent, Access(typeof(FactionExceptionSystem))]
+[RegisterComponent, Access(typeof(NpcFactionSystem))]
 public sealed partial class FactionExceptionComponent : Component
 {
     /// <summary>
-    /// List of entities that this NPC will refuse to attack
+    /// Collection of entities that this NPC will refuse to attack
     /// </summary>
     [DataField("ignored")]
     public HashSet<EntityUid> Ignored = new();
+
+    /// <summary>
+    /// Collection of entities that this NPC will attack, regardless of faction.
+    /// </summary>
+    [DataField("hostiles")]
+    public HashSet<EntityUid> Hostiles = new();
 }
diff --git a/Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs b/Content.Server/NPC/Components/FactionExceptionTrackerComponent.cs
new file mode 100644 (file)
index 0000000..804a61b
--- /dev/null
@@ -0,0 +1,16 @@
+using Content.Server.NPC.Systems;
+
+namespace Content.Server.NPC.Components;
+
+/// <summary>
+/// This is used for tracking entities stored in <see cref="FactionExceptionComponent"/>
+/// </summary>
+[RegisterComponent, Access(typeof(NpcFactionSystem))]
+public sealed partial class FactionExceptionTrackerComponent : Component
+{
+    /// <summary>
+    /// entities with <see cref="FactionExceptionComponent"/> that are tracking this entity.
+    /// </summary>
+    [DataField("entities")]
+    public HashSet<EntityUid> Entities = new();
+}
diff --git a/Content.Server/NPC/Components/NPCRetaliationComponent.cs b/Content.Server/NPC/Components/NPCRetaliationComponent.cs
new file mode 100644 (file)
index 0000000..c0bf54d
--- /dev/null
@@ -0,0 +1,24 @@
+using Content.Server.NPC.Systems;
+
+namespace Content.Server.NPC.Components;
+
+/// <summary>
+/// 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.
+/// </summary>
+[RegisterComponent, Access(typeof(NPCRetaliationSystem))]
+public sealed partial class NPCRetaliationComponent : Component
+{
+    /// <summary>
+    /// How long after being attacked will an NPC continue to be aggressive to the attacker for.
+    /// </summary>
+    [DataField("attackMemoryLength"), ViewVariables(VVAccess.ReadWrite)]
+    public TimeSpan? AttackMemoryLength;
+
+    /// <summary>
+    /// A dictionary that stores an entity and the time at which they will no longer be considered hostile.
+    /// </summary>
+    /// todo: this needs to support timeoffsetserializer at some point
+    [DataField("attackMemories")]
+    public Dictionary<EntityUid, TimeSpan> AttackMemories = new();
+}
diff --git a/Content.Server/NPC/Systems/FactionExceptionSystem.cs b/Content.Server/NPC/Systems/FactionExceptionSystem.cs
deleted file mode 100644 (file)
index 909fe39..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-using Content.Server.NPC.Components;
-
-namespace Content.Server.NPC.Systems;
-
-/// <summary>
-/// Prevents an NPC from attacking some entities from an enemy faction.
-/// </summary>
-public sealed class FactionExceptionSystem : EntitySystem
-{
-    /// <summary>
-    /// Returns whether the entity from an enemy faction won't be attacked
-    /// </summary>
-    public bool IsIgnored(FactionExceptionComponent comp, EntityUid target)
-    {
-        return comp.Ignored.Contains(target);
-    }
-
-    /// <summary>
-    /// Prevents an entity from an enemy faction from being attacked
-    /// </summary>
-    public void IgnoreEntity(FactionExceptionComponent comp, EntityUid target)
-    {
-        comp.Ignored.Add(target);
-    }
-
-    /// <summary>
-    /// Prevents a list of entities from an enemy faction from being attacked
-    /// </summary>
-    public void IgnoreEntities(FactionExceptionComponent comp, IEnumerable<EntityUid> 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 (file)
index 0000000..a8bf176
--- /dev/null
@@ -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;
+
+/// <summary>
+/// Handles NPC which become aggressive after being attacked.
+/// </summary>
+public sealed class NPCRetaliationSystem : EntitySystem
+{
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
+
+    private readonly HashSet<EntityUid> _deAggroQueue = new();
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<NPCRetaliationComponent, DamageChangedEvent>(OnDamageChanged);
+        SubscribeLocalEvent<NPCRetaliationComponent, DisarmedEvent>(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<MobStateComponent>(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<NPCRetaliationComponent, FactionExceptionComponent, MetaDataComponent>();
+        while (query.MoveNext(out var uid, out var comp, out var factionException, out var metaData))
+        {
+            _deAggroQueue.Clear();
+
+            foreach (var ent in new ValueList<EntityUid>(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 (file)
index 0000000..acef900
--- /dev/null
@@ -0,0 +1,130 @@
+using System.Linq;
+using Content.Server.NPC.Components;
+
+namespace Content.Server.NPC.Systems;
+
+/// <summary>
+/// Prevents an NPC from attacking some entities from an enemy faction.
+/// </summary>
+public sealed partial class NpcFactionSystem
+{
+    private EntityQuery<FactionExceptionComponent> _exceptionQuery;
+    private EntityQuery<FactionExceptionTrackerComponent> _trackerQuery;
+
+    public void InitializeException()
+    {
+        _exceptionQuery = GetEntityQuery<FactionExceptionComponent>();
+        _trackerQuery = GetEntityQuery<FactionExceptionTrackerComponent>();
+
+        SubscribeLocalEvent<FactionExceptionComponent, ComponentShutdown>(OnShutdown);
+        SubscribeLocalEvent<FactionExceptionTrackerComponent, ComponentShutdown>(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);
+        }
+    }
+
+    /// <summary>
+    /// Returns whether the entity from an enemy faction won't be attacked
+    /// </summary>
+    public bool IsIgnored(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null)
+    {
+        if (!Resolve(uid, ref comp, false))
+            return false;
+
+        return comp.Ignored.Contains(target);
+    }
+
+    /// <summary>
+    /// Returns the specific hostile entities for a given entity.
+    /// </summary>
+    public IEnumerable<EntityUid> GetHostiles(EntityUid uid, FactionExceptionComponent? comp = null)
+    {
+        if (!Resolve(uid, ref comp, false))
+            return Array.Empty<EntityUid>();
+
+        return comp.Hostiles;
+    }
+
+    /// <summary>
+    /// Prevents an entity from an enemy faction from being attacked
+    /// </summary>
+    public void IgnoreEntity(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null)
+    {
+        comp ??= EnsureComp<FactionExceptionComponent>(uid);
+        comp.Ignored.Add(target);
+        EnsureComp<FactionExceptionTrackerComponent>(target).Entities.Add(uid);
+    }
+
+    /// <summary>
+    /// Prevents a list of entities from an enemy faction from being attacked
+    /// </summary>
+    public void IgnoreEntities(EntityUid uid, IEnumerable<EntityUid> ignored, FactionExceptionComponent? comp = null)
+    {
+        comp ??= EnsureComp<FactionExceptionComponent>(uid);
+        foreach (var ignore in ignored)
+        {
+            IgnoreEntity(uid, ignore, comp);
+        }
+    }
+
+    /// <summary>
+    /// Makes an entity always be considered hostile.
+    /// </summary>
+    public void AggroEntity(EntityUid uid, EntityUid target, FactionExceptionComponent? comp = null)
+    {
+        comp ??= EnsureComp<FactionExceptionComponent>(uid);
+        comp.Hostiles.Add(target);
+        EnsureComp<FactionExceptionTrackerComponent>(target).Entities.Add(uid);
+    }
+
+    /// <summary>
+    /// Makes an entity no longer be considered hostile, if it was.
+    /// Doesn't apply to regular faction hostilities.
+    /// </summary>
+    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);
+    }
+
+    /// <summary>
+    /// Makes a list of entities no longer be considered hostile, if it was.
+    /// Doesn't apply to regular faction hostilities.
+    /// </summary>
+    public void AggroEntities(EntityUid uid, IEnumerable<EntityUid> entities, FactionExceptionComponent? comp = null)
+    {
+        comp ??= EnsureComp<FactionExceptionComponent>(uid);
+        foreach (var ent in entities)
+        {
+            AggroEntity(uid, ent, comp);
+        }
+    }
+}
index 79c4bbc7af0859db8bb0e2c1fd0c59b4d379f647..d6c23ca6afcf8877b2e58687a589d2f3b7816ea0 100644 (file)
@@ -1,15 +1,15 @@
 using Content.Server.NPC.Components;
 using Robust.Shared.Prototypes;
 using System.Linq;
+using JetBrains.Annotations;
 
 namespace Content.Server.NPC.Systems;
 
 /// <summary>
 ///     Outlines faction relationships with each other.
 /// </summary>
-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<NpcFactionMemberComponent, ComponentStartup>(OnFactionStartup);
         _protoManager.PrototypesReloaded += OnProtoReload;
+
+        InitializeException();
         RefreshFactions();
     }
 
@@ -134,12 +136,15 @@ public sealed class NpcFactionSystem : EntitySystem
         if (TryComp<FactionExceptionComponent>(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<EntityUid> GetNearbyFriendlies(EntityUid entity, float range, NpcFactionMemberComponent? component = null)
     {
         if (!Resolve(entity, ref component, false))
index 0f6cbc0ad9c176a401ed6c9f007f2dea6f7d003c..fc75798b947d2237b4bcce70fba27a024a1e9a0f 100644 (file)
     bloodMaxVolume: 0.1
   - type: MobPrice
     price: 50
+  - type: NPCRetaliation
+  - type: FactionException
+  - type: NpcFactionMember
+    factions:
+    - Passive
+  - type: HTN
+    rootTask:
+      task: SimpleHostileCompound
   - type: Puller
     needsHands: true
 
     states:
       Alive:
         Base: goat
+      Critical:
+        Base: dead
       Dead:
         Base: dead
   - type: SolutionContainerManager
     - 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
     states:
       Alive:
         Base: crawling
+      Critical:
+        Base: dead
       Dead:
         Base: dead
   - type: Butcherable
       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
     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
   components:
   - type: Loadout
     prototypes: [ BoxingKangarooGear ]
-  - type: HTN
-    rootTask:
-      task: SimpleHostileCompound
   - type: NpcFactionMember
     factions:
     - SimpleHostile
   - 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
index bba5a76ad8ea290dc25897955c473109505d5d29..5b25f85a795a8019fcdd630b732bf4ad2eeb2080 100644 (file)
     - !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: