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()
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;
// 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;
}
return;
var targetComp = AddComp<FactionExceptionComponent>(args.Target);
- _factionException.IgnoreEntities(targetComp, comp.Ignored);
+ _factionException.IgnoreEntities(args.Target, comp.Ignored, targetComp);
}
}
/// 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();
}
--- /dev/null
+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();
+}
--- /dev/null
+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();
+}
+++ /dev/null
-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);
- }
-}
--- /dev/null
+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);
+ }
+ }
+ }
+}
--- /dev/null
+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);
+ }
+ }
+}
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!;
_sawmill = Logger.GetSawmill("faction");
SubscribeLocalEvent<NpcFactionMemberComponent, ComponentStartup>(OnFactionStartup);
_protoManager.PrototypesReloaded += OnProtoReload;
+
+ InitializeException();
RefreshFactions();
}
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))
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
- !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: