diff --git a/Content.Client/IgnoredComponents.cs b/Content.Client/IgnoredComponents.cs index 8626c0e972..72785bce6f 100644 --- a/Content.Client/IgnoredComponents.cs +++ b/Content.Client/IgnoredComponents.cs @@ -162,6 +162,7 @@ "GasVapor", "MobStateManager", "Metabolism", + "AiFactionTag", "PressureProtection", }; } diff --git a/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs index 0c6360a6dd..29f0ec72cb 100644 --- a/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs +++ b/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs @@ -14,7 +14,7 @@ namespace Content.Server.AI.Utility.BehaviorSets // TODO: Ideally long-term we should just store the weapons in backpack new EquipMeleeExp(), new PickUpMeleeWeaponExp(), - new MeleeAttackNearbyPlayerExp(), + new MeleeAttackNearbyExp(), }; } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs similarity index 69% rename from Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs rename to Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs index cde979be3e..3d44f0429b 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyExp.cs @@ -4,17 +4,19 @@ using Content.Server.AI.Utility.Actions; using Content.Server.AI.Utility.Actions.Combat.Melee; using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations.Combat.Melee; -using Content.Server.AI.Utils; using Content.Server.AI.WorldState; using Content.Server.AI.WorldState.States; +using Content.Server.GameObjects.Components.AI; using Content.Server.GameObjects.Components.Movement; -using Content.Shared.GameObjects.Components.Damage; -using Robust.Server.GameObjects; +using Content.Server.GameObjects.EntitySystems.AI; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.IoC; namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee { - public sealed class MeleeAttackNearbyPlayerExp : ExpandableUtilityAction + public sealed class MeleeAttackNearbyExp : ExpandableUtilityAction { public override float Bonus => UtilityAction.CombatBonus; @@ -37,13 +39,10 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee throw new InvalidOperationException(); } - foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(IDamageableComponent), - controller.VisionRadius)) + foreach (var target in EntitySystem.Get() + .GetNearbyHostiles(owner, controller.VisionRadius)) { - if (entity.HasComponent() && entity != owner) - { - yield return new MeleeWeaponAttackEntity(owner, entity, Bonus); - } + yield return new MeleeWeaponAttackEntity(owner, target, Bonus); } } } diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs deleted file mode 100644 index bd78ab74ec..0000000000 --- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using Content.Server.AI.Utility.Actions; -using Content.Server.AI.Utility.Actions.Combat.Melee; -using Content.Server.AI.WorldState; -using Content.Server.AI.WorldState.States; -using Content.Server.AI.WorldState.States.Mobs; - -namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee -{ - public sealed class MeleeAttackNearbySpeciesExp : ExpandableUtilityAction - { - public override float Bonus => UtilityAction.CombatBonus; - - public override IEnumerable GetActions(Blackboard context) - { - var owner = context.GetState().GetValue(); - - foreach (var entity in context.GetState().GetValue()) - { - yield return new MeleeWeaponAttackEntity(owner, entity, Bonus); - } - } - } -} diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyPlayerExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyPlayerExp.cs index 46f1f79479..e1d1a7af0f 100644 --- a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyPlayerExp.cs +++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/UnarmedAttackNearbyPlayerExp.cs @@ -8,8 +8,10 @@ using Content.Server.AI.Utils; using Content.Server.AI.WorldState; using Content.Server.AI.WorldState.States; using Content.Server.GameObjects.Components.Movement; +using Content.Server.GameObjects.EntitySystems.AI; using Content.Shared.GameObjects.Components.Body; using Robust.Server.GameObjects; +using Robust.Shared.GameObjects.Systems; using Robust.Shared.IoC; namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee @@ -37,13 +39,10 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee throw new InvalidOperationException(); } - foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(IBodyManagerComponent), - controller.VisionRadius)) + foreach (var target in EntitySystem.Get() + .GetNearbyHostiles(owner, controller.VisionRadius)) { - if (entity.HasComponent() && entity != owner) - { - yield return new UnarmedAttackEntity(owner, entity, Bonus); - } + yield return new UnarmedAttackEntity(owner, target, Bonus); } } } diff --git a/Content.Server/GameObjects/Components/AI/AiFactionTagComponent.cs b/Content.Server/GameObjects/Components/AI/AiFactionTagComponent.cs new file mode 100644 index 0000000000..78902e8404 --- /dev/null +++ b/Content.Server/GameObjects/Components/AI/AiFactionTagComponent.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Server.GameObjects.Components.AI +{ + [RegisterComponent] + public sealed class AiFactionTagComponent : Component + { + public override string Name => "AiFactionTag"; + + public Faction Factions { get; private set; } = Faction.None; + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataReadWriteFunction( + "factions", + new List(), + factions => factions.ForEach(faction => Factions |= faction), + () => + { + var writeFactions = new List(); + foreach (Faction fac in Enum.GetValues(typeof(Faction))) + { + if ((Factions & fac) != 0) + { + writeFactions.Add(fac); + } + } + + return writeFactions; + }); + } + } + + [Flags] + public enum Faction + { + None = 0, + NanoTransen = 1 << 0, + SimpleHostile = 1 << 1, + SimpleNeutral = 1 << 2, + Syndicate = 1 << 3, + Xeno = 1 << 4, + } +} \ No newline at end of file diff --git a/Content.Server/GameObjects/EntitySystems/AI/AiFactionTagSystem.cs b/Content.Server/GameObjects/EntitySystems/AI/AiFactionTagSystem.cs new file mode 100644 index 0000000000..e12fc1633e --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/AI/AiFactionTagSystem.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Content.Server.GameObjects.Components.AI; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; + +namespace Content.Server.GameObjects.EntitySystems.AI +{ + /// + /// Outlines faction relationships with each other for AI. + /// + public sealed class AiFactionTagSystem : EntitySystem + { + /* + * Currently factions are implicitly friendly if they are not hostile. + * This may change where specified friendly factions are listed. (e.g. to get number of friendlies in area). + */ + + public Faction GetHostileFactions(Faction faction) => _hostileFactions.TryGetValue(faction, out var hostiles) ? hostiles : Faction.None; + + private Dictionary _hostileFactions = new Dictionary + { + {Faction.NanoTransen, + Faction.SimpleHostile | Faction.Syndicate | Faction.Xeno}, + {Faction.SimpleHostile, + Faction.NanoTransen | Faction.Syndicate + }, + // What makes a man turn neutral? + {Faction.SimpleNeutral, + Faction.None + }, + {Faction.Syndicate, + Faction.NanoTransen | Faction.SimpleHostile | Faction.Xeno}, + {Faction.Xeno, + Faction.NanoTransen | Faction.Syndicate}, + }; + + public Faction GetFactions(IEntity entity) => + entity.TryGetComponent(out AiFactionTagComponent factionTags) + ? factionTags.Factions + : Faction.None; + + public IEnumerable GetNearbyHostiles(IEntity entity, float range) + { + var ourFaction = GetFactions(entity); + var hostile = GetHostileFactions(ourFaction); + if (ourFaction == Faction.None || hostile == Faction.None) + { + yield break; + } + + foreach (var component in ComponentManager.EntityQuery()) + { + if ((component.Factions & hostile) == 0) + continue; + if (component.Owner.Transform.MapID != entity.Transform.MapID) + continue; + if (!component.Owner.Transform.MapPosition.InRange(entity.Transform.MapPosition, range)) + continue; + + yield return component.Owner; + } + } + + public void MakeFriendly(Faction source, Faction target) + { + if (!_hostileFactions.TryGetValue(source, out var hostileFactions)) + { + return; + } + + hostileFactions &= ~target; + _hostileFactions[source] = hostileFactions; + } + + public void MakeHostile(Faction source, Faction target) + { + if (!_hostileFactions.TryGetValue(source, out var hostileFactions)) + { + _hostileFactions[source] = target; + return; + } + + hostileFactions |= target; + _hostileFactions[source] = hostileFactions; + } + } + + public sealed class FactionCommand : IClientCommand + { + public string Command => "factions"; + public string Description => "Update / list factional relationships for NPCs."; + public string Help => "faction target\n" + + "faction list: hostile factions"; + + public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) + { + if (args.Length == 0) + { + var result = new StringBuilder(); + foreach (Faction value in Enum.GetValues(typeof(Faction))) + { + if (value == Faction.None) + continue; + result.Append(value + "\n"); + } + + shell.SendText(player, result.ToString()); + return; + } + + if (args.Length < 2) + { + shell.SendText(player, Loc.GetString("Need more args")); + return; + } + + if (!Enum.TryParse(args[0], true, out Faction faction)) + { + shell.SendText(player, Loc.GetString("Invalid faction")); + return; + } + + Faction targetFaction; + + switch (args[1]) + { + case "friendly": + if (args.Length < 3) + { + shell.SendText(player, Loc.GetString("Need to supply a target faction")); + return; + } + + if (!Enum.TryParse(args[2], true, out targetFaction)) + { + shell.SendText(player, Loc.GetString("Invalid target faction")); + return; + } + + EntitySystem.Get().MakeFriendly(faction, targetFaction); + shell.SendText(player, Loc.GetString("Command successful")); + break; + case "hostile": + if (args.Length < 3) + { + shell.SendText(player, Loc.GetString("Need to supply a target faction")); + return; + } + + if (!Enum.TryParse(args[2], true, out targetFaction)) + { + shell.SendText(player, Loc.GetString("Invalid target faction")); + return; + } + + EntitySystem.Get().MakeHostile(faction, targetFaction); + shell.SendText(player, Loc.GetString("Command successful")); + break; + case "list": + shell.SendText(player, EntitySystem.Get().GetHostileFactions(faction).ToString()); + break; + default: + shell.SendText(player, Loc.GetString("Unknown faction arg")); + break; + } + + return; + } + } +} \ No newline at end of file diff --git a/Resources/Groups/groups.yml b/Resources/Groups/groups.yml index e18adbfd95..71787fbcaa 100644 --- a/Resources/Groups/groups.yml +++ b/Resources/Groups/groups.yml @@ -37,6 +37,7 @@ - loc - hostlogin - events + - factions - Index: 100 Name: Administrator @@ -97,6 +98,7 @@ - events - destroymechanism - readyall + - factions CanViewVar: true CanAdminPlace: true @@ -188,6 +190,7 @@ - events - destroymechanism - readyall + - factions CanViewVar: true CanAdminPlace: true CanScript: true diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 59c5531583..70fba4d857 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -11,6 +11,9 @@ components: - type: AiController logic: Civilian + - type: AiFactionTag + factions: + - SimpleNeutral - type: MovementSpeedModifier baseWalkSpeed : 4 baseSprintSpeed : 4 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index bf711b3462..20f6b34c4f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -8,6 +8,9 @@ components: - type: AiController logic: Xeno + - type: AiFactionTag + factions: + - SimpleHostile - type: MovementSpeedModifier - type: InteractionOutline - type: Sprite diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/human.yml b/Resources/Prototypes/Entities/Mobs/NPCs/human.yml index cd5cdbc22e..37caacb018 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/human.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/human.yml @@ -8,6 +8,10 @@ components: - type: AiController logic: Civilian + - type: AiFactionTag + factions: + - NanoTransen + - type: entity save: false diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml index 9ac97a7d3a..6e3ed9bee7 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml @@ -9,6 +9,9 @@ components: - type: AiController logic: Mimic + - type: AiFactionTag + factions: + - SimpleHostile - type: Hands - type: MovementSpeedModifier - type: InteractionOutline diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index d7520299b8..37be27fe7b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -12,6 +12,9 @@ components: - type: AiController logic: Civilian + - type: AiFactionTag + factions: + - SimpleNeutral - type: MovementSpeedModifier baseWalkSpeed : 5 baseSprintSpeed : 5 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 2985794d4e..5100d62b5c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -9,6 +9,9 @@ components: - type: AiController logic: Xeno + - type: AiFactionTag + factions: + - Xeno - type: Hands - type: MovementSpeedModifier - type: InteractionOutline diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml index 129866f5bd..7a5ddd085c 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -17,3 +17,6 @@ - type: CameraRecoil - type: Examiner - type: HumanInventoryController + - type: AiFactionTag + factions: + - NanoTransen