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",
"MobStateManager",
"Metabolism",
"AiFactionTag",
"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
new EquipMeleeExp(),
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.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<AiFactionTagSystem>()
.GetNearbyHostiles(owner, controller.VisionRadius))
{
if (entity.HasComponent<BasicActorComponent>() && entity != owner)
{
yield return new MeleeWeaponAttackEntity(owner, entity, Bonus);
}
yield return new MeleeWeaponAttackEntity(owner, target, 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.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<AiFactionTagSystem>()
.GetNearbyHostiles(owner, controller.VisionRadius))
{
if (entity.HasComponent<BasicActorComponent>() && entity != owner)
{
yield return new UnarmedAttackEntity(owner, entity, Bonus);
}
yield return new UnarmedAttackEntity(owner, target, 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
- 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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