Add AI factions (#1807)

* Add NPC faction tags

Some stuff isn't easy to represent by the existence of components so tags are intended to provide that functionality for AI usage.

I was 50/50 on having all tags in the 1 component or splitting it into 2. I'm leaning towards 2. This would be for stuff like say "CanMimic" so the mimic knows it's allowed to look like a specific prototype, or something like "smg" on a gun so it can say smg-specific barks for instance (as currently smgs and pistols look the same from a component perspective).

This also means combat behaviors aren't hardcoded per faction, plus it makes it easy to update faction relations during events.

* Factions command

Update faction relationships via commands.

* Remove command TODO

* Woops

Forgot to commit these items

* Serializer writing and parsing

* linq me up fam

Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
This commit is contained in:
metalgearsloth
2020-08-24 20:33:03 +10:00
committed by GitHub
parent df823d2245
commit 969eeb5528
15 changed files with 265 additions and 41 deletions

View File

@@ -162,6 +162,7 @@
"GasVapor", "GasVapor",
"MobStateManager", "MobStateManager",
"Metabolism", "Metabolism",
"AiFactionTag",
"PressureProtection", "PressureProtection",
}; };
} }

View File

@@ -14,7 +14,7 @@ namespace Content.Server.AI.Utility.BehaviorSets
// TODO: Ideally long-term we should just store the weapons in backpack // TODO: Ideally long-term we should just store the weapons in backpack
new EquipMeleeExp(), new EquipMeleeExp(),
new PickUpMeleeWeaponExp(), new PickUpMeleeWeaponExp(),
new MeleeAttackNearbyPlayerExp(), new MeleeAttackNearbyExp(),
}; };
} }
} }

View File

@@ -4,17 +4,19 @@ using Content.Server.AI.Utility.Actions;
using Content.Server.AI.Utility.Actions.Combat.Melee; using Content.Server.AI.Utility.Actions.Combat.Melee;
using Content.Server.AI.Utility.Considerations; using Content.Server.AI.Utility.Considerations;
using Content.Server.AI.Utility.Considerations.Combat.Melee; using Content.Server.AI.Utility.Considerations.Combat.Melee;
using Content.Server.AI.Utils;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States; using Content.Server.AI.WorldState.States;
using Content.Server.GameObjects.Components.AI;
using Content.Server.GameObjects.Components.Movement; using Content.Server.GameObjects.Components.Movement;
using Content.Shared.GameObjects.Components.Damage; using Content.Server.GameObjects.EntitySystems.AI;
using Robust.Server.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
{ {
public sealed class MeleeAttackNearbyPlayerExp : ExpandableUtilityAction public sealed class MeleeAttackNearbyExp : ExpandableUtilityAction
{ {
public override float Bonus => UtilityAction.CombatBonus; public override float Bonus => UtilityAction.CombatBonus;
@@ -37,13 +39,10 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(IDamageableComponent), foreach (var target in EntitySystem.Get<AiFactionTagSystem>()
controller.VisionRadius)) .GetNearbyHostiles(owner, controller.VisionRadius))
{ {
if (entity.HasComponent<BasicActorComponent>() && entity != owner) yield return new MeleeWeaponAttackEntity(owner, target, Bonus);
{
yield return new MeleeWeaponAttackEntity(owner, entity, Bonus);
}
} }
} }
} }

View File

@@ -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<UtilityAction> GetActions(Blackboard context)
{
var owner = context.GetState<SelfState>().GetValue();
foreach (var entity in context.GetState<NearbyBodiesState>().GetValue())
{
yield return new MeleeWeaponAttackEntity(owner, entity, Bonus);
}
}
}
}

View File

@@ -8,8 +8,10 @@ using Content.Server.AI.Utils;
using Content.Server.AI.WorldState; using Content.Server.AI.WorldState;
using Content.Server.AI.WorldState.States; using Content.Server.AI.WorldState.States;
using Content.Server.GameObjects.Components.Movement; using Content.Server.GameObjects.Components.Movement;
using Content.Server.GameObjects.EntitySystems.AI;
using Content.Shared.GameObjects.Components.Body; using Content.Shared.GameObjects.Components.Body;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.GameObjects.Systems;
using Robust.Shared.IoC; using Robust.Shared.IoC;
namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
@@ -37,13 +39,10 @@ namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
throw new InvalidOperationException(); throw new InvalidOperationException();
} }
foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(IBodyManagerComponent), foreach (var target in EntitySystem.Get<AiFactionTagSystem>()
controller.VisionRadius)) .GetNearbyHostiles(owner, controller.VisionRadius))
{ {
if (entity.HasComponent<BasicActorComponent>() && entity != owner) yield return new UnarmedAttackEntity(owner, target, Bonus);
{
yield return new UnarmedAttackEntity(owner, entity, Bonus);
}
} }
} }
} }

View File

@@ -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<Faction>(),
factions => factions.ForEach(faction => Factions |= faction),
() =>
{
var writeFactions = new List<Faction>();
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,
}
}

View File

@@ -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
{
/// <summary>
/// Outlines faction relationships with each other for AI.
/// </summary>
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<Faction, Faction> _hostileFactions = new Dictionary<Faction, Faction>
{
{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<IEntity> 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<AiFactionTagComponent>())
{
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 <source> <friendly/hostile> target\n" +
"faction <source> 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<AiFactionTagSystem>().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<AiFactionTagSystem>().MakeHostile(faction, targetFaction);
shell.SendText(player, Loc.GetString("Command successful"));
break;
case "list":
shell.SendText(player, EntitySystem.Get<AiFactionTagSystem>().GetHostileFactions(faction).ToString());
break;
default:
shell.SendText(player, Loc.GetString("Unknown faction arg"));
break;
}
return;
}
}
}

View File

@@ -37,6 +37,7 @@
- loc - loc
- hostlogin - hostlogin
- events - events
- factions
- Index: 100 - Index: 100
Name: Administrator Name: Administrator
@@ -97,6 +98,7 @@
- events - events
- destroymechanism - destroymechanism
- readyall - readyall
- factions
CanViewVar: true CanViewVar: true
CanAdminPlace: true CanAdminPlace: true
@@ -188,6 +190,7 @@
- events - events
- destroymechanism - destroymechanism
- readyall - readyall
- factions
CanViewVar: true CanViewVar: true
CanAdminPlace: true CanAdminPlace: true
CanScript: true CanScript: true

View File

@@ -11,6 +11,9 @@
components: components:
- type: AiController - type: AiController
logic: Civilian logic: Civilian
- type: AiFactionTag
factions:
- SimpleNeutral
- type: MovementSpeedModifier - type: MovementSpeedModifier
baseWalkSpeed : 4 baseWalkSpeed : 4
baseSprintSpeed : 4 baseSprintSpeed : 4

View File

@@ -8,6 +8,9 @@
components: components:
- type: AiController - type: AiController
logic: Xeno logic: Xeno
- type: AiFactionTag
factions:
- SimpleHostile
- type: MovementSpeedModifier - type: MovementSpeedModifier
- type: InteractionOutline - type: InteractionOutline
- type: Sprite - type: Sprite

View File

@@ -8,6 +8,10 @@
components: components:
- type: AiController - type: AiController
logic: Civilian logic: Civilian
- type: AiFactionTag
factions:
- NanoTransen
- type: entity - type: entity
save: false save: false

View File

@@ -9,6 +9,9 @@
components: components:
- type: AiController - type: AiController
logic: Mimic logic: Mimic
- type: AiFactionTag
factions:
- SimpleHostile
- type: Hands - type: Hands
- type: MovementSpeedModifier - type: MovementSpeedModifier
- type: InteractionOutline - type: InteractionOutline

View File

@@ -12,6 +12,9 @@
components: components:
- type: AiController - type: AiController
logic: Civilian logic: Civilian
- type: AiFactionTag
factions:
- SimpleNeutral
- type: MovementSpeedModifier - type: MovementSpeedModifier
baseWalkSpeed : 5 baseWalkSpeed : 5
baseSprintSpeed : 5 baseSprintSpeed : 5

View File

@@ -9,6 +9,9 @@
components: components:
- type: AiController - type: AiController
logic: Xeno logic: Xeno
- type: AiFactionTag
factions:
- Xeno
- type: Hands - type: Hands
- type: MovementSpeedModifier - type: MovementSpeedModifier
- type: InteractionOutline - type: InteractionOutline

View File

@@ -17,3 +17,6 @@
- type: CameraRecoil - type: CameraRecoil
- type: Examiner - type: Examiner
- type: HumanInventoryController - type: HumanInventoryController
- type: AiFactionTag
factions:
- NanoTransen