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:
@@ -162,6 +162,7 @@
|
|||||||
"GasVapor",
|
"GasVapor",
|
||||||
"MobStateManager",
|
"MobStateManager",
|
||||||
"Metabolism",
|
"Metabolism",
|
||||||
|
"AiFactionTag",
|
||||||
"PressureProtection",
|
"PressureProtection",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -17,3 +17,6 @@
|
|||||||
- type: CameraRecoil
|
- type: CameraRecoil
|
||||||
- type: Examiner
|
- type: Examiner
|
||||||
- type: HumanInventoryController
|
- type: HumanInventoryController
|
||||||
|
- type: AiFactionTag
|
||||||
|
factions:
|
||||||
|
- NanoTransen
|
||||||
|
|||||||
Reference in New Issue
Block a user