diff --git a/Content.Client/Commands/DebugAiCommand.cs b/Content.Client/Commands/DebugAiCommand.cs
new file mode 100644
index 0000000000..1d463e3072
--- /dev/null
+++ b/Content.Client/Commands/DebugAiCommand.cs
@@ -0,0 +1,61 @@
+using Content.Client.GameObjects.EntitySystems.AI;
+using JetBrains.Annotations;
+using Robust.Client.Interfaces.Console;
+using Robust.Client.Player;
+using Robust.Shared.GameObjects.Systems;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Client.Commands
+{
+ ///
+ /// This is used to handle the tooltips above AI mobs
+ ///
+ [UsedImplicitly]
+ internal sealed class DebugAiCommand : IConsoleCommand
+ {
+ // ReSharper disable once StringLiteralTypo
+ public string Command => "debugai";
+ public string Description => "Handles all tooltip debugging above AI mobs";
+ public string Help => "debugai [hide/paths/thonk]";
+
+ public bool Execute(IDebugConsole console, params string[] args)
+ {
+#if DEBUG
+ if (args.Length < 1)
+ {
+ return true;
+ }
+
+ var anyAction = false;
+ var debugSystem = EntitySystem.Get();
+
+ foreach (var arg in args)
+ {
+ switch (arg)
+ {
+ case "hide":
+ debugSystem.Disable();
+ anyAction = true;
+ break;
+ // This will show the pathfinding numbers above the mob's head
+ case "paths":
+ debugSystem.ToggleTooltip(AiDebugMode.Paths);
+ anyAction = true;
+ break;
+ // Shows stats on what the AI was thinking.
+ case "thonk":
+ debugSystem.ToggleTooltip(AiDebugMode.Thonk);
+ anyAction = true;
+ break;
+ default:
+ continue;
+ }
+ }
+
+ return !anyAction;
+#endif
+ return true;
+ }
+ }
+}
diff --git a/Content.Client/Commands/DebugPathfindingCommand.cs b/Content.Client/Commands/DebugPathfindingCommand.cs
new file mode 100644
index 0000000000..8549ed1929
--- /dev/null
+++ b/Content.Client/Commands/DebugPathfindingCommand.cs
@@ -0,0 +1,62 @@
+using Content.Client.GameObjects.EntitySystems.AI;
+using JetBrains.Annotations;
+using Robust.Client.Interfaces.Console;
+using Robust.Shared.GameObjects.Systems;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Client.Commands
+{
+ [UsedImplicitly]
+ internal sealed class DebugPathfindingCommand : IConsoleCommand
+ {
+ // ReSharper disable once StringLiteralTypo
+ public string Command => "pathfinder";
+ public string Description => "Toggles visibility of pathfinding debuggers.";
+ public string Help => "pathfinder [hide/nodes/routes/graph]";
+
+ public bool Execute(IDebugConsole console, params string[] args)
+ {
+#if DEBUG
+ if (args.Length < 1)
+ {
+ return true;
+ }
+
+ var anyAction = false;
+ var debugSystem = EntitySystem.Get();
+
+ foreach (var arg in args)
+ {
+ switch (arg)
+ {
+ case "hide":
+ debugSystem.Disable();
+ anyAction = true;
+ break;
+ // Shows all nodes on the closed list
+ case "nodes":
+ debugSystem.ToggleTooltip(PathfindingDebugMode.Nodes);
+ anyAction = true;
+ break;
+ // Will show just the constructed route
+ case "routes":
+ debugSystem.ToggleTooltip(PathfindingDebugMode.Route);
+ anyAction = true;
+ break;
+ // Shows all of the pathfinding chunks
+ case "graph":
+ debugSystem.ToggleTooltip(PathfindingDebugMode.Graph);
+ anyAction = true;
+ break;
+ default:
+ continue;
+ }
+ }
+
+ return !anyAction;
+#endif
+ return true;
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/EntitySystems/AI/ClientAiDebugSystem.cs b/Content.Client/GameObjects/EntitySystems/AI/ClientAiDebugSystem.cs
new file mode 100644
index 0000000000..9e24a58c66
--- /dev/null
+++ b/Content.Client/GameObjects/EntitySystems/AI/ClientAiDebugSystem.cs
@@ -0,0 +1,200 @@
+using System;
+using System.Collections.Generic;
+using Content.Shared.AI;
+using Robust.Client.Interfaces.Graphics.ClientEye;
+using Robust.Client.Interfaces.UserInterface;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.GameObjects.Systems;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Maths;
+
+namespace Content.Client.GameObjects.EntitySystems.AI
+{
+#if DEBUG
+ public class ClientAiDebugSystem : EntitySystem
+ {
+ private AiDebugMode _tooltips = AiDebugMode.None;
+ private readonly Dictionary _aiBoxes = new Dictionary();
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ if (_tooltips == 0)
+ {
+ return;
+ }
+
+ var eyeManager = IoCManager.Resolve();
+ foreach (var (entity, panel) in _aiBoxes)
+ {
+ if (entity == null) continue;
+
+ if (!eyeManager.GetWorldViewport().Contains(entity.Transform.WorldPosition))
+ {
+ panel.Visible = false;
+ continue;
+ }
+
+ var (x, y) = eyeManager.WorldToScreen(entity.Transform.GridPosition).Position;
+ var offsetPosition = new Vector2(x - panel.Width / 2, y - panel.Height - 50f);
+ panel.Visible = true;
+
+ LayoutContainer.SetPosition(panel, offsetPosition);
+ }
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeNetworkEvent(HandleUtilityAiDebugMessage);
+ SubscribeNetworkEvent(HandleAStarRouteMessage);
+ SubscribeNetworkEvent(HandleJpsRouteMessage);
+ }
+
+ private void HandleUtilityAiDebugMessage(SharedAiDebug.UtilityAiDebugMessage message)
+ {
+ if ((_tooltips & AiDebugMode.Thonk) != 0)
+ {
+ // I guess if it's out of range we don't know about it?
+ var entityManager = IoCManager.Resolve();
+ var entity = entityManager.GetEntity(message.EntityUid);
+ if (entity == null)
+ {
+ return;
+ }
+
+ TryCreatePanel(entity);
+
+ // Probably shouldn't access by index but it's a debugging tool so eh
+ var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(0);
+ label.Text = $"Current Task: {message.FoundTask}\n" +
+ $"Task score: {message.ActionScore}\n" +
+ $"Planning time (ms): {message.PlanningTime * 1000:0.0000}\n" +
+ $"Considered {message.ConsideredTaskCount} tasks";
+ }
+ }
+
+ private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message)
+ {
+ if ((_tooltips & AiDebugMode.Paths) != 0)
+ {
+ var entityManager = IoCManager.Resolve();
+ var entity = entityManager.GetEntity(message.EntityUid);
+ if (entity == null)
+ {
+ return;
+ }
+
+ TryCreatePanel(entity);
+
+ var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(1);
+ label.Text = $"Pathfinding time (ms): {message.TimeTaken * 1000:0.0000}\n" +
+ $"Nodes traversed: {message.ClosedTiles.Count}\n" +
+ $"Nodes per ms: {message.ClosedTiles.Count / (message.TimeTaken * 1000)}";
+ }
+ }
+
+ private void HandleJpsRouteMessage(SharedAiDebug.JpsRouteMessage message)
+ {
+ if ((_tooltips & AiDebugMode.Paths) != 0)
+ {
+ var entityManager = IoCManager.Resolve();
+ var entity = entityManager.GetEntity(message.EntityUid);
+ if (entity == null)
+ {
+ return;
+ }
+
+ TryCreatePanel(entity);
+
+ var label = (Label) _aiBoxes[entity].GetChild(0).GetChild(1);
+ label.Text = $"Pathfinding time (ms): {message.TimeTaken * 1000:0.0000}\n" +
+ $"Jump Nodes: {message.JumpNodes.Count}\n" +
+ $"Jump Nodes per ms: {message.JumpNodes.Count / (message.TimeTaken * 1000)}";
+ }
+ }
+
+ public void Disable()
+ {
+ foreach (var tooltip in _aiBoxes.Values)
+ {
+ tooltip.Dispose();
+ }
+ _aiBoxes.Clear();
+ _tooltips = AiDebugMode.None;
+ }
+
+
+ private void EnableTooltip(AiDebugMode tooltip)
+ {
+ _tooltips |= tooltip;
+ }
+
+ private void DisableTooltip(AiDebugMode tooltip)
+ {
+ _tooltips &= ~tooltip;
+ }
+
+ public void ToggleTooltip(AiDebugMode tooltip)
+ {
+ if ((_tooltips & tooltip) != 0)
+ {
+ DisableTooltip(tooltip);
+ }
+ else
+ {
+ EnableTooltip(tooltip);
+ }
+ }
+
+ private bool TryCreatePanel(IEntity entity)
+ {
+ if (!_aiBoxes.ContainsKey(entity))
+ {
+ var userInterfaceManager = IoCManager.Resolve();
+
+ var actionLabel = new Label
+ {
+ MouseFilter = Control.MouseFilterMode.Ignore,
+ };
+
+ var pathfindingLabel = new Label
+ {
+ MouseFilter = Control.MouseFilterMode.Ignore,
+ };
+
+ var vBox = new VBoxContainer()
+ {
+ SeparationOverride = 15,
+ Children = {actionLabel, pathfindingLabel},
+ };
+
+ var panel = new PanelContainer
+ {
+ StyleClasses = {"tooltipBox"},
+ Children = {vBox},
+ MouseFilter = Control.MouseFilterMode.Ignore,
+ ModulateSelfOverride = Color.White.WithAlpha(0.75f),
+ };
+
+ userInterfaceManager.StateRoot.AddChild(panel);
+
+ _aiBoxes[entity] = panel;
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ [Flags]
+ public enum AiDebugMode : byte
+ {
+ None = 0,
+ Paths = 1 << 1,
+ Thonk = 1 << 2,
+ }
+#endif
+}
diff --git a/Content.Client/GameObjects/EntitySystems/AI/ClientPathfindingDebugSystem.cs b/Content.Client/GameObjects/EntitySystems/AI/ClientPathfindingDebugSystem.cs
new file mode 100644
index 0000000000..746a9b92e2
--- /dev/null
+++ b/Content.Client/GameObjects/EntitySystems/AI/ClientPathfindingDebugSystem.cs
@@ -0,0 +1,355 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Content.Shared.AI;
+using Robust.Client.Graphics.Drawing;
+using Robust.Client.Graphics.Overlays;
+using Robust.Client.Graphics.Shaders;
+using Robust.Client.Interfaces.Graphics.ClientEye;
+using Robust.Client.Interfaces.Graphics.Overlays;
+using Robust.Shared.GameObjects.Systems;
+using Robust.Shared.Interfaces.Random;
+using Robust.Shared.IoC;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timers;
+
+namespace Content.Client.GameObjects.EntitySystems.AI
+{
+#if DEBUG
+ public class ClientPathfindingDebugSystem : EntitySystem
+ {
+ private PathfindingDebugMode _modes = PathfindingDebugMode.None;
+ private float _routeDuration = 4.0f; // How long before we remove a route from the overlay
+ private DebugPathfindingOverlay _overlay;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeNetworkEvent(HandleAStarRouteMessage);
+ SubscribeNetworkEvent(HandleJpsRouteMessage);
+ SubscribeNetworkEvent(HandleGraphMessage);
+ }
+
+ private void HandleAStarRouteMessage(SharedAiDebug.AStarRouteMessage message)
+ {
+ if ((_modes & PathfindingDebugMode.Nodes) != 0 ||
+ (_modes & PathfindingDebugMode.Route) != 0)
+ {
+ _overlay.AStarRoutes.Add(message);
+ Timer.Spawn(TimeSpan.FromSeconds(_routeDuration), () =>
+ {
+ if (_overlay == null) return;
+ _overlay.AStarRoutes.Remove(message);
+ });
+ }
+ }
+
+ private void HandleJpsRouteMessage(SharedAiDebug.JpsRouteMessage message)
+ {
+ if ((_modes & PathfindingDebugMode.Nodes) != 0 ||
+ (_modes & PathfindingDebugMode.Route) != 0)
+ {
+ _overlay.JpsRoutes.Add(message);
+ Timer.Spawn(TimeSpan.FromSeconds(_routeDuration), () =>
+ {
+ if (_overlay == null) return;
+ _overlay.JpsRoutes.Remove(message);
+ });
+ }
+ }
+
+ private void HandleGraphMessage(SharedAiDebug.PathfindingGraphMessage message)
+ {
+ if ((_modes & PathfindingDebugMode.Graph) != 0)
+ {
+ _overlay.UpdateGraph(message.Graph);
+ }
+ }
+
+ private void EnableOverlay()
+ {
+ if (_overlay != null)
+ {
+ return;
+ }
+
+ var overlayManager = IoCManager.Resolve();
+ _overlay = new DebugPathfindingOverlay {Modes = _modes};
+ overlayManager.AddOverlay(_overlay);
+ }
+
+ private void DisableOverlay()
+ {
+ if (_overlay == null)
+ {
+ return;
+ }
+
+ _overlay.Modes = 0;
+ var overlayManager = IoCManager.Resolve();
+ overlayManager.RemoveOverlay(_overlay.ID);
+ _overlay = null;
+ }
+
+ public void Disable()
+ {
+ _modes = PathfindingDebugMode.None;
+ DisableOverlay();
+ }
+
+
+ private void EnableMode(PathfindingDebugMode tooltip)
+ {
+ _modes |= tooltip;
+ if (_modes != 0)
+ {
+ EnableOverlay();
+ }
+ _overlay.Modes = _modes;
+
+ if (tooltip == PathfindingDebugMode.Graph)
+ {
+ var systemMessage = new SharedAiDebug.RequestPathfindingGraphMessage();
+ EntityManager.EntityNetManager.SendSystemNetworkMessage(systemMessage);
+ }
+ }
+
+ private void DisableMode(PathfindingDebugMode mode)
+ {
+ _modes &= ~mode;
+ if (_modes == 0)
+ {
+ DisableOverlay();
+ }
+ else
+ {
+ _overlay.Modes = _modes;
+ }
+ }
+
+ public void ToggleTooltip(PathfindingDebugMode mode)
+ {
+ if ((_modes & mode) != 0)
+ {
+ DisableMode(mode);
+ }
+ else
+ {
+ EnableMode(mode);
+ }
+ }
+ }
+
+ internal sealed class DebugPathfindingOverlay : Overlay
+ {
+ // TODO: Add a box like the debug one and show the most recent path stuff
+ public override OverlaySpace Space => OverlaySpace.ScreenSpace;
+
+ public PathfindingDebugMode Modes { get; set; } = PathfindingDebugMode.None;
+
+ // Graph debugging
+ public readonly Dictionary> Graph = new Dictionary>();
+ private readonly Dictionary _graphColors = new Dictionary();
+
+ // Route debugging
+ // As each pathfinder is very different you'll likely want to draw them completely different
+ public readonly List AStarRoutes = new List();
+ public readonly List JpsRoutes = new List();
+
+ public DebugPathfindingOverlay() : base(nameof(DebugPathfindingOverlay))
+ {
+ Shader = IoCManager.Resolve().Index("unshaded").Instance();
+ }
+
+ public void UpdateGraph(Dictionary> graph)
+ {
+ Graph.Clear();
+ _graphColors.Clear();
+ var robustRandom = IoCManager.Resolve();
+ foreach (var (chunk, nodes) in graph)
+ {
+ Graph[chunk] = nodes;
+ _graphColors[chunk] = new Color(robustRandom.NextFloat(), robustRandom.NextFloat(),
+ robustRandom.NextFloat(), 0.3f);
+ }
+ }
+
+ private void DrawGraph(DrawingHandleScreen screenHandle)
+ {
+ var eyeManager = IoCManager.Resolve();
+ var viewport = IoCManager.Resolve().GetWorldViewport();
+
+ foreach (var (chunk, nodes) in Graph)
+ {
+ foreach (var tile in nodes)
+ {
+ if (!viewport.Contains(tile)) continue;
+
+ var screenTile = eyeManager.WorldToScreen(tile);
+
+ var box = new UIBox2(
+ screenTile.X - 15.0f,
+ screenTile.Y - 15.0f,
+ screenTile.X + 15.0f,
+ screenTile.Y + 15.0f);
+
+ screenHandle.DrawRect(box, _graphColors[chunk]);
+ }
+ }
+ }
+
+ #region pathfinder
+
+ private void DrawAStarRoutes(DrawingHandleScreen screenHandle)
+ {
+ var eyeManager = IoCManager.Resolve();
+ var viewport = eyeManager.GetWorldViewport();
+
+ foreach (var route in AStarRoutes)
+ {
+ // Draw box on each tile of route
+ foreach (var position in route.Route)
+ {
+ if (!viewport.Contains(position)) continue;
+ var screenTile = eyeManager.WorldToScreen(position);
+ // worldHandle.DrawLine(position, nextWorld.Value, Color.Blue);
+ var box = new UIBox2(
+ screenTile.X - 15.0f,
+ screenTile.Y - 15.0f,
+ screenTile.X + 15.0f,
+ screenTile.Y + 15.0f);
+ screenHandle.DrawRect(box, Color.Orange.WithAlpha(0.25f));
+ }
+ }
+ }
+
+ private void DrawAStarNodes(DrawingHandleScreen screenHandle)
+ {
+ var eyeManager = IoCManager.Resolve();
+ var viewport = eyeManager.GetWorldViewport();
+
+ foreach (var route in AStarRoutes)
+ {
+ var highestgScore = route.GScores.Values.Max();
+
+ foreach (var (tile, score) in route.GScores)
+ {
+ if ((route.Route.Contains(tile) && (Modes & PathfindingDebugMode.Route) != 0) ||
+ !viewport.Contains(tile))
+ {
+ continue;
+ }
+
+ var screenTile = eyeManager.WorldToScreen(tile);
+
+ var box = new UIBox2(
+ screenTile.X - 15.0f,
+ screenTile.Y - 15.0f,
+ screenTile.X + 15.0f,
+ screenTile.Y + 15.0f);
+
+ screenHandle.DrawRect(box, new Color(
+ 0.0f,
+ score / highestgScore,
+ 1.0f - (score / highestgScore),
+ 0.1f));
+ }
+ }
+ }
+
+ private void DrawJpsRoutes(DrawingHandleScreen screenHandle)
+ {
+ var eyeManager = IoCManager.Resolve();
+ var viewport = eyeManager.GetWorldViewport();
+
+ foreach (var route in JpsRoutes)
+ {
+ // Draw box on each tile of route
+ foreach (var position in route.Route)
+ {
+ if (!viewport.Contains(position)) continue;
+ var screenTile = eyeManager.WorldToScreen(position);
+ // worldHandle.DrawLine(position, nextWorld.Value, Color.Blue);
+ var box = new UIBox2(
+ screenTile.X - 15.0f,
+ screenTile.Y - 15.0f,
+ screenTile.X + 15.0f,
+ screenTile.Y + 15.0f);
+ screenHandle.DrawRect(box, Color.Orange.WithAlpha(0.25f));
+ }
+ }
+ }
+
+ private void DrawJpsNodes(DrawingHandleScreen screenHandle)
+ {
+ var eyeManager = IoCManager.Resolve();
+ var viewport = eyeManager.GetWorldViewport();
+
+ foreach (var route in JpsRoutes)
+ {
+ foreach (var tile in route.JumpNodes)
+ {
+ if ((route.Route.Contains(tile) && (Modes & PathfindingDebugMode.Route) != 0) ||
+ !viewport.Contains(tile))
+ {
+ continue;
+ }
+
+ var screenTile = eyeManager.WorldToScreen(tile);
+
+ var box = new UIBox2(
+ screenTile.X - 15.0f,
+ screenTile.Y - 15.0f,
+ screenTile.X + 15.0f,
+ screenTile.Y + 15.0f);
+
+ screenHandle.DrawRect(box, new Color(
+ 0.0f,
+ 1.0f,
+ 0.0f,
+ 0.2f));
+ }
+ }
+ }
+
+ #endregion
+
+ protected override void Draw(DrawingHandleBase handle)
+ {
+ if (Modes == 0)
+ {
+ return;
+ }
+
+ var screenHandle = (DrawingHandleScreen) handle;
+
+ if ((Modes & PathfindingDebugMode.Route) != 0)
+ {
+ DrawAStarRoutes(screenHandle);
+ DrawJpsRoutes(screenHandle);
+ }
+
+ if ((Modes & PathfindingDebugMode.Nodes) != 0)
+ {
+ DrawAStarNodes(screenHandle);
+ DrawJpsNodes(screenHandle);
+ }
+
+ if ((Modes & PathfindingDebugMode.Graph) != 0)
+ {
+ DrawGraph(screenHandle);
+ }
+ }
+ }
+
+ [Flags]
+ public enum PathfindingDebugMode {
+ None = 0,
+ Route = 1 << 0,
+ Graph = 1 << 1,
+ Nodes = 1 << 2,
+ }
+#endif
+}
diff --git a/Content.Server/AI/AimShootLifeProcessor.cs b/Content.Server/AI/AimShootLifeProcessor.cs
deleted file mode 100644
index 52f7b06d57..0000000000
--- a/Content.Server/AI/AimShootLifeProcessor.cs
+++ /dev/null
@@ -1,165 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Content.Server.Interfaces.GameObjects.Components.Movement;
-using Content.Shared.Physics;
-using Robust.Server.AI;
-using Robust.Server.Interfaces.GameObjects;
-using Robust.Shared.Interfaces.GameObjects;
-using Robust.Shared.Interfaces.GameObjects.Components;
-using Robust.Shared.Interfaces.Physics;
-using Robust.Shared.Interfaces.Timing;
-using Robust.Shared.IoC;
-using Robust.Shared.Maths;
-
-namespace Content.Server.AI
-{
- ///
- /// The object stays stationary. The object will periodically scan for *any* life forms in its radius, and engage them.
- /// The object will rotate itself to point at the locked entity, and if it has a weapon will shoot at the entity.
- ///
- [AiLogicProcessor("AimShootLife")]
- class AimShootLifeProcessor : AiLogicProcessor
- {
-#pragma warning disable 649
- [Dependency] private readonly IPhysicsManager _physMan;
- [Dependency] private readonly IServerEntityManager _entMan;
- [Dependency] private readonly IGameTiming _timeMan;
-#pragma warning restore 649
-
- private readonly List _workList = new List();
-
- private const float MaxAngSpeed = (float)(Math.PI / 2); // how fast our turret can rotate
- private const float ScanPeriod = 1.0f; // tweak this for performance and gameplay experience
- private float _lastScan;
-
- private IEntity _curTarget;
-
- ///
- public override void Update(float frameTime)
- {
- if (SelfEntity == null)
- return;
-
- DoScanning();
- DoTracking(frameTime);
- }
-
- private void DoScanning()
- {
- var curTime = _timeMan.CurTime.TotalSeconds;
- if (curTime - _lastScan > ScanPeriod)
- {
- _lastScan = (float)curTime;
- _curTarget = FindBestTarget();
- }
- }
-
- private void DoTracking(float frameTime)
- {
- // not valid entity to target.
- if (_curTarget == null || !_curTarget.IsValid())
- {
- _curTarget = null;
- return;
- }
-
- // point me at the target
- var tarPos = _curTarget.GetComponent().WorldPosition;
- var selfTransform = SelfEntity.GetComponent();
- var myPos = selfTransform.WorldPosition;
-
- var curDir = selfTransform.LocalRotation.ToVec();
- var tarDir = (tarPos - myPos).Normalized;
-
- var fwdAng = Vector2.Dot(curDir, tarDir);
-
- Vector2 newDir;
- if (fwdAng < 0) // target behind turret, just rotate in a direction to get target in front
- {
- var curRight = new Vector2(-curDir.Y, curDir.X); // right handed coord system
- var rightAngle = Vector2.Dot(curDir, new Vector2(-tarDir.Y, tarDir.X)); // right handed coord system
- var rotateSign = -Math.Sign(rightAngle);
- newDir = curDir + curRight * rotateSign * MaxAngSpeed * frameTime;
- }
- else // target in front, adjust to aim at him
- {
- newDir = MoveTowards(curDir, tarDir, MaxAngSpeed, frameTime);
- }
-
- selfTransform.LocalRotation = new Angle(newDir);
-
- if (fwdAng > -0.9999)
- {
- // TODO: shoot gun, prob need aimbot because entity rotation lags behind moving target
- }
- }
-
- private IEntity FindBestTarget()
- {
- // "best" target is the closest one with LOS
-
- var ents = _entMan.GetEntitiesInRange(SelfEntity, VisionRadius);
- var myTransform = SelfEntity.GetComponent();
- var maxRayLen = VisionRadius * 2.5f; // circle inscribed in square, square diagonal = 2*r*sqrt(2)
-
- _workList.Clear();
- foreach (var entity in ents)
- {
- // filter to "people" entities (entities with controllers)
- if (!entity.HasComponent())
- continue;
-
- // build the ray
- var dir = entity.GetComponent().WorldPosition - myTransform.WorldPosition;
- var ray = new CollisionRay(myTransform.WorldPosition, dir.Normalized, (int)(CollisionGroup.MobImpassable | CollisionGroup.Impassable));
-
- // cast the ray
- var result = _physMan.IntersectRay(myTransform.MapID, ray, maxRayLen, SelfEntity).First();
-
- // add to visible list
- if (result.HitEntity == entity)
- _workList.Add(entity);
- }
-
- // get closest entity in list
- var closestEnt = GetClosest(myTransform.WorldPosition, _workList);
-
- // return closest
- return closestEnt;
- }
-
- private static IEntity GetClosest(Vector2 origin, IEnumerable list)
- {
- IEntity closest = null;
- var minDistSqrd = float.PositiveInfinity;
-
- foreach (var ent in list)
- {
- var pos = ent.GetComponent().WorldPosition;
- var distSqrd = (pos - origin).LengthSquared;
-
- if (distSqrd > minDistSqrd)
- continue;
-
- closest = ent;
- minDistSqrd = distSqrd;
- }
-
- return closest;
- }
-
- private static Vector2 MoveTowards(Vector2 current, Vector2 target, float speed, float delta)
- {
- var maxDeltaDist = speed * delta;
- var a = target - current;
- var magnitude = a.Length;
- if (magnitude <= maxDeltaDist)
- {
- return target;
- }
-
- return current + a / magnitude * maxDeltaDist;
- }
- }
-}
diff --git a/Content.Server/AI/Operators/AiOperator.cs b/Content.Server/AI/Operators/AiOperator.cs
new file mode 100644
index 0000000000..1989846ea9
--- /dev/null
+++ b/Content.Server/AI/Operators/AiOperator.cs
@@ -0,0 +1,55 @@
+
+using System;
+
+namespace Content.Server.AI.Operators
+{
+ public abstract class AiOperator
+ {
+ private bool _hasStartup = false;
+ private bool _hasShutdown = false;
+
+ ///
+ /// Called once when the AiLogicProcessor starts this action
+ ///
+ public virtual bool TryStartup()
+ {
+ // If we've already startup then no point continuing
+ // This signals to the override that it's already startup
+ // Should probably throw but it made some code elsewhere marginally easier
+ if (_hasStartup)
+ {
+ return false;
+ }
+
+ _hasStartup = true;
+ return true;
+ }
+
+ ///
+ /// Called once when the AiLogicProcessor is done with this action if the outcome is successful or fails.
+ ///
+ public virtual void Shutdown(Outcome outcome)
+ {
+ if (_hasShutdown)
+ {
+ throw new InvalidOperationException("AiOperator has already shutdown");
+ }
+
+ _hasShutdown = true;
+ }
+
+ ///
+ /// Called every tick for the AI
+ ///
+ ///
+ ///
+ public abstract Outcome Execute(float frameTime);
+ }
+
+ public enum Outcome
+ {
+ Success,
+ Continuing,
+ Failed,
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/AI/Operators/Combat/Ranged/ShootAtEntityOperator.cs b/Content.Server/AI/Operators/Combat/Ranged/ShootAtEntityOperator.cs
new file mode 100644
index 0000000000..4756843192
--- /dev/null
+++ b/Content.Server/AI/Operators/Combat/Ranged/ShootAtEntityOperator.cs
@@ -0,0 +1,100 @@
+using Content.Server.GameObjects;
+using Content.Server.GameObjects.Components.Mobs;
+using Content.Server.GameObjects.Components.Movement;
+using Content.Server.GameObjects.Components.Weapon.Ranged;
+using Robust.Shared.Interfaces.GameObjects;
+
+
+namespace Content.Server.AI.Operators.Combat.Ranged
+{
+ public class ShootAtEntityOperator : AiOperator
+ {
+ private IEntity _owner;
+ private IEntity _target;
+ private float _accuracy;
+
+ private float _burstTime;
+
+ private float _elapsedTime;
+
+ public ShootAtEntityOperator(IEntity owner, IEntity target, float accuracy, float burstTime = 0.5f)
+ {
+ _owner = owner;
+ _target = target;
+ _accuracy = accuracy;
+ _burstTime = burstTime;
+ }
+
+ public override bool TryStartup()
+ {
+ if (!base.TryStartup())
+ {
+ return true;
+ }
+
+ if (!_owner.TryGetComponent(out CombatModeComponent combatModeComponent))
+ {
+ return false;
+ }
+
+ if (!combatModeComponent.IsInCombatMode)
+ {
+ combatModeComponent.IsInCombatMode = true;
+ }
+
+ return true;
+ }
+
+ public override void Shutdown(Outcome outcome)
+ {
+ base.Shutdown(outcome);
+ if (_owner.TryGetComponent(out CombatModeComponent combatModeComponent))
+ {
+ combatModeComponent.IsInCombatMode = false;
+ }
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ // TODO: Probably just do all the checks on first try and then after that repeat the fire.
+ if (_burstTime <= _elapsedTime)
+ {
+ return Outcome.Success;
+ }
+
+ _elapsedTime += frameTime;
+
+ if (_target.TryGetComponent(out DamageableComponent damageableComponent))
+ {
+ if (damageableComponent.IsDead())
+ {
+ return Outcome.Success;
+ }
+ }
+
+ if (!_owner.TryGetComponent(out HandsComponent hands) || hands.GetActiveHand == null)
+ {
+ return Outcome.Failed;
+ }
+
+ var equippedWeapon = hands.GetActiveHand.Owner;
+
+ if ((_target.Transform.GridPosition.Position - _owner.Transform.GridPosition.Position).Length >
+ _owner.GetComponent().VisionRadius)
+ {
+ // Not necessarily a hard fail, more of a soft fail
+ return Outcome.Failed;
+ }
+
+ // Unless RangedWeaponComponent is removed from hitscan weapons this shouldn't happen
+ if (!equippedWeapon.TryGetComponent(out RangedWeaponComponent rangedWeaponComponent))
+ {
+ return Outcome.Failed;
+ }
+
+ // TODO: Accuracy
+ rangedWeaponComponent.AiFire(_owner, _target.Transform.GridPosition);
+ return Outcome.Continuing;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Combat/Ranged/WaitForHitscanChargeOperator.cs b/Content.Server/AI/Operators/Combat/Ranged/WaitForHitscanChargeOperator.cs
new file mode 100644
index 0000000000..fc8835f118
--- /dev/null
+++ b/Content.Server/AI/Operators/Combat/Ranged/WaitForHitscanChargeOperator.cs
@@ -0,0 +1,41 @@
+using System;
+using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Operators.Combat.Ranged
+{
+ public class WaitForHitscanChargeOperator : AiOperator
+ {
+ private float _lastCharge = 0.0f;
+ private float _lastFill = 0.0f;
+ private HitscanWeaponComponent _hitscan;
+
+ public WaitForHitscanChargeOperator(IEntity entity)
+ {
+ if (!entity.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent))
+ {
+ throw new InvalidOperationException();
+ }
+
+ _hitscan = hitscanWeaponComponent;
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ if (_hitscan.CapacitorComponent.Capacity - _hitscan.CapacitorComponent.Charge < 0.01f)
+ {
+ return Outcome.Success;
+ }
+
+ // If we're not charging then just stop
+ _lastFill = _hitscan.CapacitorComponent.Charge - _lastCharge;
+ _lastCharge = _hitscan.CapacitorComponent.Charge;
+
+ if (_lastFill == 0.0f)
+ {
+ return Outcome.Failed;
+ }
+ return Outcome.Continuing;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Combat/SwingMeleeWeaponOperator.cs b/Content.Server/AI/Operators/Combat/SwingMeleeWeaponOperator.cs
new file mode 100644
index 0000000000..d4c5ade0d6
--- /dev/null
+++ b/Content.Server/AI/Operators/Combat/SwingMeleeWeaponOperator.cs
@@ -0,0 +1,74 @@
+using Content.Server.GameObjects;
+using Content.Server.GameObjects.Components.Mobs;
+using Content.Server.GameObjects.Components.Weapon.Melee;
+using Content.Server.GameObjects.EntitySystems;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Server.AI.Operators.Combat
+{
+ public class SwingMeleeWeaponOperator : AiOperator
+ {
+ private float _burstTime;
+ private float _elapsedTime;
+
+ private readonly IEntity _owner;
+ private readonly IEntity _target;
+
+ public SwingMeleeWeaponOperator(IEntity owner, IEntity target, float burstTime = 1.0f)
+ {
+ _owner = owner;
+ _target = target;
+ _burstTime = burstTime;
+ }
+
+ public override bool TryStartup()
+ {
+ if (!base.TryStartup())
+ {
+ return true;
+ }
+
+ if (!_owner.TryGetComponent(out CombatModeComponent combatModeComponent))
+ {
+ return false;
+ }
+
+ if (!combatModeComponent.IsInCombatMode)
+ {
+ combatModeComponent.IsInCombatMode = true;
+ }
+
+ return true;
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ if (_burstTime <= _elapsedTime)
+ {
+ return Outcome.Success;
+ }
+
+ if (!_owner.TryGetComponent(out HandsComponent hands) || hands.GetActiveHand == null)
+ {
+ return Outcome.Failed;
+ }
+
+ var meleeWeapon = hands.GetActiveHand.Owner;
+ meleeWeapon.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent);
+
+ if ((_target.Transform.GridPosition.Position - _owner.Transform.GridPosition.Position).Length >
+ meleeWeaponComponent.Range)
+ {
+ return Outcome.Failed;
+ }
+
+ var interactionSystem = IoCManager.Resolve().GetEntitySystem();
+
+ interactionSystem.UseItemInHand(_owner, _target.Transform.GridPosition, _target.Uid);
+ _elapsedTime += frameTime;
+ return Outcome.Continuing;
+ }
+ }
+
+}
diff --git a/Content.Server/AI/Operators/Generic/WaitOperator.cs b/Content.Server/AI/Operators/Generic/WaitOperator.cs
new file mode 100644
index 0000000000..7c93111570
--- /dev/null
+++ b/Content.Server/AI/Operators/Generic/WaitOperator.cs
@@ -0,0 +1,24 @@
+namespace Content.Server.AI.Operators.Generic
+{
+ public class WaitOperator : AiOperator
+ {
+ private readonly float _waitTime;
+ private float _accumulatedTime = 0.0f;
+
+ public WaitOperator(float waitTime)
+ {
+ _waitTime = waitTime;
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ if (_accumulatedTime < _waitTime)
+ {
+ _accumulatedTime += frameTime;
+ return Outcome.Continuing;
+ }
+
+ return Outcome.Success;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Inventory/CloseStorageOperator.cs b/Content.Server/AI/Operators/Inventory/CloseStorageOperator.cs
new file mode 100644
index 0000000000..193b45077e
--- /dev/null
+++ b/Content.Server/AI/Operators/Inventory/CloseStorageOperator.cs
@@ -0,0 +1,73 @@
+using Content.Server.AI.Utility;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects.Components;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Server.Utility;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Operators.Inventory
+{
+ ///
+ /// Close the last EntityStorage we opened
+ /// This will also update the State for it (which a regular InteractWith won't do)
+ ///
+ public sealed class CloseLastStorageOperator : AiOperator
+ {
+ private readonly IEntity _owner;
+ private IEntity _target;
+
+ public CloseLastStorageOperator(IEntity owner)
+ {
+ _owner = owner;
+ }
+
+ public override bool TryStartup()
+ {
+ if (!base.TryStartup())
+ {
+ return true;
+ }
+
+ var blackboard = UtilityAiHelpers.GetBlackboard(_owner);
+
+ if (blackboard == null)
+ {
+ return false;
+ }
+
+ _target = blackboard.GetState().GetValue();
+
+ return _target != null;
+ }
+
+ public override void Shutdown(Outcome outcome)
+ {
+ base.Shutdown(outcome);
+ var blackboard = UtilityAiHelpers.GetBlackboard(_owner);
+
+ blackboard?.GetState().SetValue(null);
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ if (!InteractionChecks.InRangeUnobstructed(_owner, _target.Transform.MapPosition))
+ {
+ return Outcome.Failed;
+ }
+
+ if (!_target.TryGetComponent(out EntityStorageComponent storageComponent) ||
+ storageComponent.IsWeldedShut)
+ {
+ return Outcome.Failed;
+ }
+
+ if (storageComponent.Open)
+ {
+ var activateArgs = new ActivateEventArgs {User = _owner, Target = _target};
+ storageComponent.Activate(activateArgs);
+ }
+
+ return Outcome.Success;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/AI/Operators/Inventory/DropEntityOperator.cs b/Content.Server/AI/Operators/Inventory/DropEntityOperator.cs
new file mode 100644
index 0000000000..759c2c28b2
--- /dev/null
+++ b/Content.Server/AI/Operators/Inventory/DropEntityOperator.cs
@@ -0,0 +1,32 @@
+using Content.Server.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Log;
+
+namespace Content.Server.AI.Operators.Inventory
+{
+ public class DropEntityOperator : AiOperator
+ {
+ private readonly IEntity _owner;
+ private readonly IEntity _entity;
+ public DropEntityOperator(IEntity owner, IEntity entity)
+ {
+ _owner = owner;
+ _entity = entity;
+ }
+
+ ///
+ /// Requires EquipEntityOperator to put it in the active hand first
+ ///
+ ///
+ ///
+ public override Outcome Execute(float frameTime)
+ {
+ if (!_owner.TryGetComponent(out HandsComponent handsComponent))
+ {
+ return Outcome.Failed;
+ }
+
+ return handsComponent.Drop(_entity) ? Outcome.Success : Outcome.Failed;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Inventory/DropHandItemsOperator.cs b/Content.Server/AI/Operators/Inventory/DropHandItemsOperator.cs
new file mode 100644
index 0000000000..4966ca172e
--- /dev/null
+++ b/Content.Server/AI/Operators/Inventory/DropHandItemsOperator.cs
@@ -0,0 +1,30 @@
+using Content.Server.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Operators.Inventory
+{
+ public class DropHandItemsOperator : AiOperator
+ {
+ private readonly IEntity _owner;
+
+ public DropHandItemsOperator(IEntity owner)
+ {
+ _owner = owner;
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ if (!_owner.TryGetComponent(out HandsComponent handsComponent))
+ {
+ return Outcome.Failed;
+ }
+
+ foreach (var item in handsComponent.GetAllHeldItems())
+ {
+ handsComponent.Drop(item.Owner);
+ }
+
+ return Outcome.Success;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Inventory/EquipEntityOperator.cs b/Content.Server/AI/Operators/Inventory/EquipEntityOperator.cs
new file mode 100644
index 0000000000..3eba859abc
--- /dev/null
+++ b/Content.Server/AI/Operators/Inventory/EquipEntityOperator.cs
@@ -0,0 +1,38 @@
+using Content.Server.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Operators.Inventory
+{
+ public sealed class EquipEntityOperator : AiOperator
+ {
+ private readonly IEntity _owner;
+ private readonly IEntity _entity;
+ public EquipEntityOperator(IEntity owner, IEntity entity)
+ {
+ _owner = owner;
+ _entity = entity;
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ if (!_owner.TryGetComponent(out HandsComponent handsComponent))
+ {
+ return Outcome.Failed;
+ }
+ // TODO: If in clothing then click on it
+ foreach (var hand in handsComponent.ActivePriorityEnumerable())
+ {
+ if (handsComponent.GetHand(hand)?.Owner == _entity)
+ {
+ handsComponent.ActiveIndex = hand;
+ return Outcome.Success;
+ }
+ }
+
+ // TODO: Get free hand count; if no hands free then fail right here
+
+ // TODO: Go through inventory
+ return Outcome.Failed;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Inventory/InteractWithEntityOperator.cs b/Content.Server/AI/Operators/Inventory/InteractWithEntityOperator.cs
new file mode 100644
index 0000000000..dc2412a335
--- /dev/null
+++ b/Content.Server/AI/Operators/Inventory/InteractWithEntityOperator.cs
@@ -0,0 +1,48 @@
+using Content.Server.GameObjects.Components.Mobs;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Server.Utility;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Server.AI.Operators.Inventory
+{
+ ///
+ /// A Generic interacter; if you need to check stuff then make your own
+ ///
+ public class InteractWithEntityOperator : AiOperator
+ {
+ private readonly IEntity _owner;
+ private readonly IEntity _useTarget;
+
+ public InteractWithEntityOperator(IEntity owner, IEntity useTarget)
+ {
+ _owner = owner;
+ _useTarget = useTarget;
+
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ if (_useTarget.Transform.GridID != _owner.Transform.GridID)
+ {
+ return Outcome.Failed;
+ }
+
+ if (!InteractionChecks.InRangeUnobstructed(_owner, _useTarget.Transform.MapPosition))
+ {
+ return Outcome.Failed;
+ }
+
+ if (_owner.TryGetComponent(out CombatModeComponent combatModeComponent))
+ {
+ combatModeComponent.IsInCombatMode = false;
+ }
+
+ // Click on da thing
+ var interactionSystem = IoCManager.Resolve().GetEntitySystem();
+ interactionSystem.UseItemInHand(_owner, _useTarget.Transform.GridPosition, _useTarget.Uid);
+
+ return Outcome.Success;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Inventory/OpenStorageOperator.cs b/Content.Server/AI/Operators/Inventory/OpenStorageOperator.cs
new file mode 100644
index 0000000000..fe2f5e24e1
--- /dev/null
+++ b/Content.Server/AI/Operators/Inventory/OpenStorageOperator.cs
@@ -0,0 +1,55 @@
+using Content.Server.AI.Utility;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects.Components;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Server.Utility;
+using Robust.Shared.Containers;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Operators.Inventory
+{
+ ///
+ /// If the target is in EntityStorage will open its parent container
+ ///
+ public sealed class OpenStorageOperator : AiOperator
+ {
+ private readonly IEntity _owner;
+ private readonly IEntity _target;
+
+ public OpenStorageOperator(IEntity owner, IEntity target)
+ {
+ _owner = owner;
+ _target = target;
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ if (!InteractionChecks.InRangeUnobstructed(_owner, _target.Transform.MapPosition))
+ {
+ return Outcome.Failed;
+ }
+
+ if (!ContainerHelpers.TryGetContainer(_target, out var container))
+ {
+ return Outcome.Success;
+ }
+
+ if (!container.Owner.TryGetComponent(out EntityStorageComponent storageComponent) ||
+ storageComponent.IsWeldedShut)
+ {
+ return Outcome.Failed;
+ }
+
+ if (!storageComponent.Open)
+ {
+ var activateArgs = new ActivateEventArgs {User = _owner, Target = _target};
+ storageComponent.Activate(activateArgs);
+ }
+
+ var blackboard = UtilityAiHelpers.GetBlackboard(_owner);
+ blackboard?.GetState().SetValue(container.Owner);
+
+ return Outcome.Success;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs b/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs
new file mode 100644
index 0000000000..ba0f40f78f
--- /dev/null
+++ b/Content.Server/AI/Operators/Inventory/PickupEntityOperator.cs
@@ -0,0 +1,65 @@
+using Content.Server.GameObjects;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Server.Utility;
+using Robust.Shared.Containers;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.IoC;
+
+namespace Content.Server.AI.Operators.Inventory
+{
+ public class PickupEntityOperator : AiOperator
+ {
+ // Input variables
+ private readonly IEntity _owner;
+ private readonly IEntity _target;
+
+ public PickupEntityOperator(IEntity owner, IEntity target)
+ {
+ _owner = owner;
+ _target = target;
+ }
+
+ // TODO: When I spawn new entities they seem to duplicate clothing or something?
+ public override Outcome Execute(float frameTime)
+ {
+ if (_target == null ||
+ _target.Deleted ||
+ !_target.HasComponent() ||
+ ContainerHelpers.IsInContainer(_target) ||
+ !InteractionChecks.InRangeUnobstructed(_owner, _target.Transform.MapPosition))
+ {
+ return Outcome.Failed;
+ }
+
+ if (!_owner.TryGetComponent(out HandsComponent handsComponent))
+ {
+ return Outcome.Failed;
+ }
+
+ var emptyHands = false;
+
+ foreach (var hand in handsComponent.ActivePriorityEnumerable())
+ {
+ if (handsComponent.GetHand(hand) == null)
+ {
+ if (handsComponent.ActiveIndex != hand)
+ {
+ handsComponent.ActiveIndex = hand;
+ }
+
+ emptyHands = true;
+ break;
+ }
+ }
+
+ if (!emptyHands)
+ {
+ return Outcome.Failed;
+ }
+
+ var interactionSystem = IoCManager.Resolve().GetEntitySystem();
+ interactionSystem.Interaction(_owner, _target);
+ return Outcome.Success;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Inventory/UseItemInHandsOperator.cs b/Content.Server/AI/Operators/Inventory/UseItemInHandsOperator.cs
new file mode 100644
index 0000000000..3e4ed39de8
--- /dev/null
+++ b/Content.Server/AI/Operators/Inventory/UseItemInHandsOperator.cs
@@ -0,0 +1,49 @@
+using Content.Server.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Operators.Inventory
+{
+ ///
+ /// Will find the item in storage, put it in an active hand, then use it
+ ///
+ public class UseItemInHandsOperator : AiOperator
+ {
+ private readonly IEntity _owner;
+ private readonly IEntity _target;
+
+ public UseItemInHandsOperator(IEntity owner, IEntity target)
+ {
+ _owner = owner;
+ _target = target;
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ if (_target == null)
+ {
+ return Outcome.Failed;
+ }
+
+ // TODO: Also have this check storage a la backpack etc.
+ if (!_owner.TryGetComponent(out HandsComponent handsComponent))
+ {
+ return Outcome.Failed;
+ }
+
+ if (_target.TryGetComponent(out ItemComponent itemComponent))
+ {
+ return Outcome.Failed;
+ }
+
+ foreach (var slot in handsComponent.ActivePriorityEnumerable())
+ {
+ if (handsComponent.GetHand(slot) != itemComponent) continue;
+ handsComponent.ActiveIndex = slot;
+ handsComponent.ActivateItem();
+ return Outcome.Success;
+ }
+
+ return Outcome.Failed;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Movement/BaseMover.cs b/Content.Server/AI/Operators/Movement/BaseMover.cs
new file mode 100644
index 0000000000..d17472378f
--- /dev/null
+++ b/Content.Server/AI/Operators/Movement/BaseMover.cs
@@ -0,0 +1,295 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Content.Server.GameObjects.Components.Movement;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Server.GameObjects.EntitySystems.AI.Pathfinding;
+using Content.Server.GameObjects.EntitySystems.AI.Pathfinding.Pathfinders;
+using Content.Server.GameObjects.EntitySystems.JobQueues;
+using Robust.Shared.GameObjects.Components;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Map;
+using Robust.Shared.Interfaces.Random;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Timer = Robust.Shared.Timers.Timer;
+
+namespace Content.Server.AI.Operators.Movement
+{
+ public abstract class BaseMover : AiOperator
+ {
+ ///
+ /// Invoked every time we move across a tile
+ ///
+ public event Action MovedATile;
+
+ ///
+ /// How close the pathfinder needs to get before returning a route
+ /// Set at 1.42f just in case there's rounding and diagonally adjacent tiles aren't counted.
+ ///
+ ///
+ public float PathfindingProximity { get; set; } = 1.42f;
+ protected Queue Route = new Queue();
+ ///
+ /// The final spot we're trying to get to
+ ///
+ protected GridCoordinates TargetGrid;
+ ///
+ /// As the pathfinder is tilebased we'll move to each tile's grid.
+ ///
+ protected GridCoordinates NextGrid;
+ private const float TileTolerance = 0.2f;
+
+ // Stuck checkers
+ ///
+ /// How long we're stuck in general before trying to unstuck
+ ///
+ private float _stuckTimerRemaining = 0.5f;
+ private GridCoordinates _ourLastPosition;
+
+ // Anti-stuck measures. See the AntiStuck() method for more details
+ private bool _tryingAntiStuck;
+ public bool IsStuck;
+ private AntiStuckMethod _antiStuckMethod = AntiStuckMethod.Angle;
+ private Angle _addedAngle = Angle.Zero;
+ public event Action Stuck;
+ private int _antiStuckAttempts = 0;
+
+ private CancellationTokenSource _routeCancelToken;
+ protected Job> RouteJob;
+ private IMapManager _mapManager;
+ private PathfindingSystem _pathfinder;
+ private AiControllerComponent _controller;
+
+ // Input
+ protected IEntity Owner;
+
+ protected void Setup(IEntity owner)
+ {
+ Owner = owner;
+ _mapManager = IoCManager.Resolve();
+ _pathfinder = IoCManager.Resolve().GetEntitySystem();
+ if (!Owner.TryGetComponent(out AiControllerComponent controllerComponent))
+ {
+ throw new InvalidOperationException();
+ }
+
+ _controller = controllerComponent;
+ }
+
+ protected void NextTile()
+ {
+ MovedATile?.Invoke();
+ }
+
+ ///
+ /// Will move the AI towards the next position
+ ///
+ /// true if movement to be done
+ protected bool TryMove()
+ {
+ // Use collidable just so we don't get stuck on corners as much
+ // var targetDiff = NextGrid.Position - _ownerCollidable.WorldAABB.Center;
+ var targetDiff = NextGrid.Position - Owner.Transform.GridPosition.Position;
+
+ // Check distance
+ if (targetDiff.Length < TileTolerance)
+ {
+ return false;
+ }
+ // Move towards it
+ if (_controller == null)
+ {
+ return false;
+ }
+ _controller.VelocityDir = _addedAngle.RotateVec(targetDiff).Normalized;
+ return true;
+
+ }
+
+ ///
+ /// Will try and get around obstacles if stuck
+ ///
+ protected void AntiStuck(float frameTime)
+ {
+ // TODO: More work because these are sketchy af
+ // TODO: Check if a wall was spawned in front of us and then immediately dump route if it was
+
+ // First check if we're still in a stuck state from last frame
+ if (IsStuck && !_tryingAntiStuck)
+ {
+ switch (_antiStuckMethod)
+ {
+ case AntiStuckMethod.None:
+ break;
+ case AntiStuckMethod.Jiggle:
+ var randomRange = IoCManager.Resolve().Next(0, 359);
+ var angle = Angle.FromDegrees(randomRange);
+ Owner.TryGetComponent(out AiControllerComponent mover);
+ mover.VelocityDir = angle.ToVec().Normalized;
+
+ break;
+ case AntiStuckMethod.PhaseThrough:
+ if (Owner.TryGetComponent(out CollidableComponent collidableComponent))
+ {
+ // TODO Fix this because they are yeeting themselves when they charge
+ // TODO: If something updates this this will fuck it
+ collidableComponent.CanCollide = false;
+
+ Timer.Spawn(100, () =>
+ {
+ if (!collidableComponent.CanCollide)
+ {
+ collidableComponent.CanCollide = true;
+ }
+ });
+ }
+ break;
+ case AntiStuckMethod.Teleport:
+ Owner.Transform.DetachParent();
+ Owner.Transform.GridPosition = NextGrid;
+ break;
+ case AntiStuckMethod.ReRoute:
+ GetRoute();
+ break;
+ case AntiStuckMethod.Angle:
+ var random = IoCManager.Resolve();
+ _addedAngle = new Angle(random.Next(-60, 60));
+ IsStuck = false;
+ Timer.Spawn(100, () =>
+ {
+ _addedAngle = Angle.Zero;
+ });
+ break;
+ default:
+ throw new InvalidOperationException();
+ }
+ }
+
+ _stuckTimerRemaining -= frameTime;
+
+ // Stuck check cooldown
+ if (_stuckTimerRemaining > 0.0f)
+ {
+ return;
+ }
+
+ _tryingAntiStuck = false;
+ _stuckTimerRemaining = 0.5f;
+
+ // Are we actually stuck
+ if ((_ourLastPosition.Position - Owner.Transform.GridPosition.Position).Length < TileTolerance)
+ {
+ _antiStuckAttempts++;
+
+ // Maybe it's just 1 tile that's borked so try next 1?
+ if (_antiStuckAttempts >= 2 && _antiStuckAttempts < 5 && Route.Count > 1)
+ {
+ var nextTile = Route.Dequeue();
+ NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices);
+ return;
+ }
+
+ if (_antiStuckAttempts >= 5 || Route.Count == 0)
+ {
+ Logger.DebugS("ai", $"{Owner} is stuck at {Owner.Transform.GridPosition}, trying new route");
+ _antiStuckAttempts = 0;
+ IsStuck = false;
+ _ourLastPosition = Owner.Transform.GridPosition;
+ GetRoute();
+ return;
+ }
+ Stuck?.Invoke();
+ IsStuck = true;
+ return;
+ }
+
+ IsStuck = false;
+
+ _ourLastPosition = Owner.Transform.GridPosition;
+ }
+
+ ///
+ /// Tells us we don't need to keep moving and resets everything
+ ///
+ public void HaveArrived()
+ {
+ _routeCancelToken?.Cancel(); // oh thank god no more pathfinding
+ Route.Clear();
+ if (_controller == null) return;
+ _controller.VelocityDir = Vector2.Zero;
+ }
+
+ protected void GetRoute()
+ {
+ _routeCancelToken?.Cancel();
+ _routeCancelToken = new CancellationTokenSource();
+ Route.Clear();
+
+ int collisionMask;
+ if (!Owner.TryGetComponent(out CollidableComponent collidableComponent))
+ {
+ collisionMask = 0;
+ }
+ else
+ {
+ collisionMask = collidableComponent.CollisionMask;
+ }
+
+ var startGrid = _mapManager.GetGrid(Owner.Transform.GridID).GetTileRef(Owner.Transform.GridPosition);
+ var endGrid = _mapManager.GetGrid(TargetGrid.GridID).GetTileRef(TargetGrid);;
+ // _routeCancelToken = new CancellationTokenSource();
+
+ RouteJob = _pathfinder.RequestPath(new PathfindingArgs(
+ Owner.Uid,
+ collisionMask,
+ startGrid,
+ endGrid,
+ PathfindingProximity
+ ), _routeCancelToken.Token);
+ }
+
+ protected void ReceivedRoute()
+ {
+ Route = RouteJob.Result;
+ RouteJob = null;
+
+ if (Route == null)
+ {
+ Route = new Queue();
+ // Couldn't find a route to target
+ return;
+ }
+
+ // Because the entity may be half on 2 tiles we'll just cut out the first tile.
+ // This may not be the best solution but sometimes if the AI is chasing for example it will
+ // stutter backwards to the first tile again.
+ Route.Dequeue();
+
+ var nextTile = Route.Peek();
+ NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices);
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ if (RouteJob != null && RouteJob.Status == JobStatus.Finished)
+ {
+ ReceivedRoute();
+ }
+
+ return !ActionBlockerSystem.CanMove(Owner) ? Outcome.Failed : Outcome.Continuing;
+ }
+ }
+
+ public enum AntiStuckMethod
+ {
+ None,
+ ReRoute,
+ Jiggle, // Just pick a random direction for a bit and hope for the best
+ Teleport, // The Half-Life 2 method
+ PhaseThrough, // Just makes it non-collidable
+ Angle, // Add a different angle for a bit
+ }
+}
diff --git a/Content.Server/AI/Operators/Movement/MoveToEntityOperator.cs b/Content.Server/AI/Operators/Movement/MoveToEntityOperator.cs
new file mode 100644
index 0000000000..317ef7d9e1
--- /dev/null
+++ b/Content.Server/AI/Operators/Movement/MoveToEntityOperator.cs
@@ -0,0 +1,142 @@
+using System.Collections.Generic;
+using Content.Server.GameObjects.EntitySystems.JobQueues;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Map;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+
+namespace Content.Server.AI.Operators.Movement
+{
+ public sealed class MoveToEntityOperator : BaseMover
+ {
+ // Instance
+ private GridCoordinates _lastTargetPosition;
+ private IMapManager _mapManager;
+
+ // Input
+ public IEntity Target { get; }
+ public float DesiredRange { get; set; }
+
+ public MoveToEntityOperator(IEntity owner, IEntity target, float desiredRange = 1.5f)
+ {
+ Setup(owner);
+ Target = target;
+ _mapManager = IoCManager.Resolve();
+ DesiredRange = desiredRange;
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ var baseOutcome = base.Execute(frameTime);
+ // TODO: Given this is probably the most common operator whatever speed boosts you can do here will be gucci
+ // Could also look at running it every other tick.
+
+ if (baseOutcome == Outcome.Failed ||
+ Target == null ||
+ Target.Deleted ||
+ Target.Transform.GridID != Owner.Transform.GridID)
+ {
+ HaveArrived();
+ return Outcome.Failed;
+ }
+
+ if (RouteJob != null)
+ {
+ if (RouteJob.Status != JobStatus.Finished)
+ {
+ return Outcome.Continuing;
+ }
+ ReceivedRoute();
+ return Route.Count == 0 ? Outcome.Failed : Outcome.Continuing;
+ }
+
+ var targetRange = (Target.Transform.GridPosition.Position - Owner.Transform.GridPosition.Position).Length;
+
+ // If they move near us
+ if (targetRange <= DesiredRange)
+ {
+ HaveArrived();
+ return Outcome.Success;
+ }
+
+ // If the target's moved we may need to re-route.
+ // First we'll check if they're near another tile on the existing route and if so
+ // we can trim up until that point.
+ if (_lastTargetPosition != default &&
+ (Target.Transform.GridPosition.Position - _lastTargetPosition.Position).Length > 1.5f)
+ {
+ var success = false;
+ // Technically it should be Route.Count - 1 but if the route's empty it'll throw
+ var newRoute = new Queue(Route.Count);
+
+ for (var i = 0; i < Route.Count; i++)
+ {
+ var tile = Route.Dequeue();
+ newRoute.Enqueue(tile);
+ var tileGrid = _mapManager.GetGrid(tile.GridIndex).GridTileToLocal(tile.GridIndices);
+
+ // Don't use DesiredRange here or above in case it's smaller than a tile;
+ // when we get close we run straight at them anyway so it shooouullddd be okay...
+ if ((Target.Transform.GridPosition.Position - tileGrid.Position).Length < 1.5f)
+ {
+ success = true;
+ break;
+ }
+ }
+
+ if (success)
+ {
+ Route = newRoute;
+ _lastTargetPosition = Target.Transform.GridPosition;
+ TargetGrid = Target.Transform.GridPosition;
+ return Outcome.Continuing;
+ }
+
+ _lastTargetPosition = default;
+ }
+
+ // If they move too far or no route
+ if (_lastTargetPosition == default)
+ {
+ // If they're further we could try pathfinding from the furthest tile potentially?
+ _lastTargetPosition = Target.Transform.GridPosition;
+ TargetGrid = Target.Transform.GridPosition;
+ GetRoute();
+ return Outcome.Continuing;
+ }
+
+ AntiStuck(frameTime);
+
+ if (IsStuck)
+ {
+ return Outcome.Continuing;
+ }
+
+ if (TryMove())
+ {
+ return Outcome.Continuing;
+ }
+
+ // If we're really close just try bee-lining it?
+ if (Route.Count == 0)
+ {
+ if (targetRange < 1.9f)
+ {
+ // TODO: If they have a phat hitbox they could block us
+ NextGrid = TargetGrid;
+ return Outcome.Continuing;
+ }
+ if (targetRange > DesiredRange)
+ {
+ HaveArrived();
+ return Outcome.Failed;
+ }
+ }
+
+ var nextTile = Route.Dequeue();
+ NextTile();
+ NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices);
+ return Outcome.Continuing;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Movement/MoveToGridOperator.cs b/Content.Server/AI/Operators/Movement/MoveToGridOperator.cs
new file mode 100644
index 0000000000..ad866573ab
--- /dev/null
+++ b/Content.Server/AI/Operators/Movement/MoveToGridOperator.cs
@@ -0,0 +1,94 @@
+using Content.Server.GameObjects.EntitySystems.JobQueues;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Map;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+
+namespace Content.Server.AI.Operators.Movement
+{
+ public class MoveToGridOperator : BaseMover
+ {
+ private IMapManager _mapManager;
+ private float _desiredRange;
+
+ public MoveToGridOperator(
+ IEntity owner,
+ GridCoordinates gridPosition,
+ float desiredRange = 1.5f)
+ {
+ Setup(owner);
+ TargetGrid = gridPosition;
+ _mapManager = IoCManager.Resolve();
+ PathfindingProximity = 0.2f; // Accept no substitutes
+ _desiredRange = desiredRange;
+ }
+
+ public void UpdateTarget(GridCoordinates newTarget)
+ {
+ TargetGrid = newTarget;
+ HaveArrived();
+ GetRoute();
+ }
+
+ public override Outcome Execute(float frameTime)
+ {
+ var baseOutcome = base.Execute(frameTime);
+
+ if (baseOutcome == Outcome.Failed ||
+ TargetGrid.GridID != Owner.Transform.GridID)
+ {
+ HaveArrived();
+ return Outcome.Failed;
+ }
+
+ if (RouteJob != null)
+ {
+ if (RouteJob.Status != JobStatus.Finished)
+ {
+ return Outcome.Continuing;
+ }
+ ReceivedRoute();
+ return Route.Count == 0 ? Outcome.Failed : Outcome.Continuing;
+ }
+
+ var targetRange = (TargetGrid.Position - Owner.Transform.GridPosition.Position).Length;
+
+ // We there
+ if (targetRange <= _desiredRange)
+ {
+ HaveArrived();
+ return Outcome.Success;
+ }
+
+ // No route
+ if (Route.Count == 0 && RouteJob == null)
+ {
+ GetRoute();
+ return Outcome.Continuing;
+ }
+
+ AntiStuck(frameTime);
+
+ if (IsStuck)
+ {
+ return Outcome.Continuing;
+ }
+
+ if (TryMove())
+ {
+ return Outcome.Continuing;
+ }
+
+ if (Route.Count == 0 && targetRange > 1.5f)
+ {
+ HaveArrived();
+ return Outcome.Failed;
+ }
+
+ var nextTile = Route.Dequeue();
+ NextTile();
+ NextGrid = _mapManager.GetGrid(nextTile.GridIndex).GridTileToLocal(nextTile.GridIndices);
+ return Outcome.Continuing;
+ }
+ }
+}
diff --git a/Content.Server/AI/Operators/Sequences/GoPickupEntitySequence.cs b/Content.Server/AI/Operators/Sequences/GoPickupEntitySequence.cs
new file mode 100644
index 0000000000..65922d2366
--- /dev/null
+++ b/Content.Server/AI/Operators/Sequences/GoPickupEntitySequence.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Operators.Movement;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Operators.Sequences
+{
+ public class GoPickupEntitySequence : SequenceOperator
+ {
+ public GoPickupEntitySequence(IEntity owner, IEntity target)
+ {
+ Sequence = new Queue(new AiOperator[]
+ {
+ new MoveToEntityOperator(owner, target),
+ new OpenStorageOperator(owner, target),
+ new PickupEntityOperator(owner, target),
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/AI/Operators/Sequences/SequenceOperator.cs b/Content.Server/AI/Operators/Sequences/SequenceOperator.cs
new file mode 100644
index 0000000000..0b103ffdb3
--- /dev/null
+++ b/Content.Server/AI/Operators/Sequences/SequenceOperator.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+
+namespace Content.Server.AI.Operators.Sequences
+{
+ ///
+ /// Sequential chain of operators
+ /// Saves having to duplicate stuff like MoveTo and PickUp everywhere
+ ///
+ public abstract class SequenceOperator : AiOperator
+ {
+ public Queue Sequence { get; protected set; }
+
+ public override Outcome Execute(float frameTime)
+ {
+ if (Sequence.Count == 0)
+ {
+ return Outcome.Success;
+ }
+
+ var op = Sequence.Peek();
+ op.TryStartup();
+ var outcome = op.Execute(frameTime);
+
+ switch (outcome)
+ {
+ case Outcome.Success:
+ op.Shutdown(outcome);
+ // Not over until all operators are done
+ Sequence.Dequeue();
+ return Outcome.Continuing;
+ case Outcome.Continuing:
+ return Outcome.Continuing;
+ case Outcome.Failed:
+ op.Shutdown(outcome);
+ Sequence.Clear();
+ return Outcome.Failed;
+
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/AI/StaticBarkerProcessor.cs b/Content.Server/AI/StaticBarkerProcessor.cs
deleted file mode 100644
index 2d0cdb163c..0000000000
--- a/Content.Server/AI/StaticBarkerProcessor.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Content.Server.Interfaces.Chat;
-using Robust.Server.AI;
-using Robust.Shared.Interfaces.Timing;
-using Robust.Shared.IoC;
-using Robust.Shared.Utility;
-using CannyFastMath;
-using Math = CannyFastMath.Math;
-using MathF = CannyFastMath.MathF;
-
-namespace Content.Server.AI
-{
- ///
- /// Designed for a a stationary entity that regularly advertises things (vending machine).
- ///
- [AiLogicProcessor("StaticBarker")]
- class StaticBarkerProcessor : AiLogicProcessor
- {
-#pragma warning disable 649
- [Dependency] private readonly IGameTiming _timeMan;
- [Dependency] private readonly IChatManager _chatMan;
-#pragma warning restore 649
-
- private static readonly TimeSpan MinimumDelay = TimeSpan.FromSeconds(15);
- private TimeSpan _nextBark;
-
-
- private static List slogans = new List
- {
- "Come try my great products today!",
- "More value for the way you live.",
- "Quality you'd expect at prices you wouldn't.",
- "The right stuff. The right price.",
- };
-
- public override void Update(float frameTime)
- {
- if(_timeMan.CurTime < _nextBark)
- return;
-
- var rngState = GenSeed();
- _nextBark = _timeMan.CurTime + MinimumDelay + TimeSpan.FromSeconds(Random01(ref rngState) * 10);
-
- var pick = (int)Math.Round(Random01(ref rngState) * (slogans.Count - 1));
- _chatMan.EntitySay(SelfEntity, slogans[pick]);
- }
-
- private uint GenSeed()
- {
- return RotateRight((uint)_timeMan.CurTick.GetHashCode(), 11) ^ (uint)SelfEntity.Uid.GetHashCode();
- }
-
- private uint RotateRight(uint n, int s)
- {
- return (n << (32 - s)) | (n >> s);
- }
-
- private float Random01(ref uint state)
- {
- DebugTools.Assert(state != 0);
-
- //xorshift32
- state ^= state << 13;
- state ^= state >> 17;
- state ^= state << 5;
- return state / (float)uint.MaxValue;
- }
- }
-}
diff --git a/Content.Server/AI/Utility/Actions/Clothing/Gloves/EquipGloves.cs b/Content.Server/AI/Utility/Actions/Clothing/Gloves/EquipGloves.cs
new file mode 100644
index 0000000000..1d382044d1
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Clothing/Gloves/EquipGloves.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Clothing;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Shared.GameObjects.Components.Inventory;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Clothing.Gloves
+{
+ public sealed class EquipGloves : UtilityAction
+ {
+ private IEntity _entity;
+
+ public EquipGloves(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new EquipEntityOperator(Owner, _entity),
+ new UseItemInHandsOperator(Owner, _entity),
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new ClothingInSlotCon(EquipmentSlotDefines.Slots.GLOVES,
+ new InverseBoolCurve()),
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs b/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs
new file mode 100644
index 0000000000..b2402d4065
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Clothing/Gloves/PickUpGloves.cs
@@ -0,0 +1,43 @@
+using Content.Server.AI.Operators.Sequences;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Clothing;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Shared.GameObjects.Components.Inventory;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Clothing.Gloves
+{
+ public sealed class PickUpGloves : UtilityAction
+ {
+ private readonly IEntity _entity;
+
+ public PickUpGloves(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new ClothingInSlotCon(EquipmentSlotDefines.Slots.GLOVES,
+ new InverseBoolCurve()),
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ new ClothingInInventoryCon(EquipmentSlotDefines.SlotFlags.GLOVES,
+ new InverseBoolCurve()),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs b/Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs
new file mode 100644
index 0000000000..95bd1a772f
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Clothing;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Shared.GameObjects.Components.Inventory;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Clothing.Head
+{
+ public sealed class EquipHead : UtilityAction
+ {
+ private IEntity _entity;
+
+ public EquipHead(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new EquipEntityOperator(Owner, _entity),
+ new UseItemInHandsOperator(Owner, _entity),
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new ClothingInSlotCon(EquipmentSlotDefines.Slots.HEAD,
+ new InverseBoolCurve()),
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs b/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs
new file mode 100644
index 0000000000..98a999cc8d
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Clothing/Head/PickUpHead.cs
@@ -0,0 +1,43 @@
+using Content.Server.AI.Operators.Sequences;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Clothing;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Shared.GameObjects.Components.Inventory;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Clothing.Head
+{
+ public sealed class PickUpHead : UtilityAction
+ {
+ private readonly IEntity _entity;
+
+ public PickUpHead(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new ClothingInSlotCon(EquipmentSlotDefines.Slots.HEAD,
+ new InverseBoolCurve()),
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ new ClothingInInventoryCon(EquipmentSlotDefines.SlotFlags.HEAD,
+ new InverseBoolCurve()),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/EquipOuterClothing.cs b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/EquipOuterClothing.cs
new file mode 100644
index 0000000000..6d1eae4445
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/EquipOuterClothing.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Clothing;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Shared.GameObjects.Components.Inventory;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing
+{
+ public sealed class EquipOuterClothing : UtilityAction
+ {
+ private IEntity _entity;
+
+ public EquipOuterClothing(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new EquipEntityOperator(Owner, _entity),
+ new UseItemInHandsOperator(Owner, _entity),
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new ClothingInSlotCon(EquipmentSlotDefines.Slots.OUTERCLOTHING,
+ new InverseBoolCurve()),
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs
new file mode 100644
index 0000000000..359c4ccdd4
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Clothing/OuterClothing/PickUpOuterClothing.cs
@@ -0,0 +1,43 @@
+using Content.Server.AI.Operators.Sequences;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Clothing;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Shared.GameObjects.Components.Inventory;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Clothing.OuterClothing
+{
+ public sealed class PickUpOuterClothing : UtilityAction
+ {
+ private readonly IEntity _entity;
+
+ public PickUpOuterClothing(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new ClothingInSlotCon(EquipmentSlotDefines.Slots.OUTERCLOTHING,
+ new InverseBoolCurve()),
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ new ClothingInInventoryCon(EquipmentSlotDefines.SlotFlags.OUTERCLOTHING,
+ new InverseBoolCurve()),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Clothing/Shoes/EquipShoes.cs b/Content.Server/AI/Utility/Actions/Clothing/Shoes/EquipShoes.cs
new file mode 100644
index 0000000000..78dd53ab73
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Clothing/Shoes/EquipShoes.cs
@@ -0,0 +1,47 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Clothing;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Shared.GameObjects.Components.Inventory;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Clothing.Shoes
+{
+ public sealed class EquipShoes : UtilityAction
+ {
+ private IEntity _entity;
+
+ public EquipShoes(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new EquipEntityOperator(Owner, _entity),
+ new UseItemInHandsOperator(Owner, _entity),
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new ClothingInSlotCon(EquipmentSlotDefines.Slots.SHOES,
+ new InverseBoolCurve()),
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs b/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs
new file mode 100644
index 0000000000..f755873688
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Clothing/Shoes/PickUpShoes.cs
@@ -0,0 +1,43 @@
+using Content.Server.AI.Operators.Sequences;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Clothing;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Shared.GameObjects.Components.Inventory;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Clothing.Shoes
+{
+ public sealed class PickUpShoes : UtilityAction
+ {
+ private readonly IEntity _entity;
+
+ public PickUpShoes(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new ClothingInSlotCon(EquipmentSlotDefines.Slots.SHOES,
+ new InverseBoolCurve()),
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ new ClothingInInventoryCon(EquipmentSlotDefines.SlotFlags.SHOES,
+ new InverseBoolCurve()),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs
new file mode 100644
index 0000000000..0fdb5e02a2
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat.Melee;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Combat;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Melee
+{
+ public sealed class EquipMelee : UtilityAction
+ {
+ private IEntity _entity;
+
+ public EquipMelee(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new EquipEntityOperator(Owner, _entity)
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new MeleeWeaponEquippedCon(
+ new InverseBoolCurve()),
+ // We'll prioritise equipping ranged weapons; If we try and score this then it'll just keep swapping between ranged and melee
+ new RangedWeaponEquippedCon(
+ new InverseBoolCurve()),
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ new MeleeWeaponSpeedCon(
+ new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
+ new MeleeWeaponDamageCon(
+ new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeAttackEntity.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeAttackEntity.cs
new file mode 100644
index 0000000000..403ad6411a
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Melee/MeleeAttackEntity.cs
@@ -0,0 +1,77 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Combat;
+using Content.Server.AI.Operators.Movement;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat;
+using Content.Server.AI.Utility.Considerations.Combat.Melee;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Combat;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.AI.WorldState.States.Movement;
+using Content.Server.GameObjects.Components.Weapon.Melee;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Melee
+{
+ public sealed class MeleeAttackEntity : UtilityAction
+ {
+ private IEntity _entity;
+
+ public MeleeAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ var moveOperator = new MoveToEntityOperator(Owner, _entity);
+ var equipped = context.GetState().GetValue();
+ if (equipped != null && equipped.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent))
+ {
+ moveOperator.DesiredRange = meleeWeaponComponent.Range - 0.01f;
+ }
+
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ moveOperator,
+ new SwingMeleeWeaponOperator(Owner, _entity),
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ var equipped = context.GetState().GetValue();
+ context.GetState().SetValue(equipped);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ // Check if we have a weapon; easy-out
+ new MeleeWeaponEquippedCon(
+ new BoolCurve()),
+ // Don't attack a dead target
+ new TargetIsDeadCon(
+ new InverseBoolCurve()),
+ // Deprioritise a target in crit
+ new TargetIsCritCon(
+ new QuadraticCurve(-0.8f, 1.0f, 1.0f, 0.0f)),
+ // Somewhat prioritise distance
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
+ // Prefer weaker targets
+ new TargetHealthCon(
+ new QuadraticCurve(1.0f, 0.4f, 0.0f, -0.02f)),
+ new MeleeWeaponSpeedCon(
+ new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
+ new MeleeWeaponDamageCon(
+ new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs b/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs
new file mode 100644
index 0000000000..ae4e8da589
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Melee/PickUpMeleeWeapon.cs
@@ -0,0 +1,52 @@
+using Content.Server.AI.Operators.Sequences;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat.Melee;
+using Content.Server.AI.Utility.Considerations.Containers;
+using Content.Server.AI.Utility.Considerations.Hands;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Combat;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Melee
+{
+ public sealed class PickUpMeleeWeapon : UtilityAction
+ {
+ private IEntity _entity;
+
+ public PickUpMeleeWeapon(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new TargetAccessibleCon(
+ new BoolCurve()),
+ new FreeHandCon(
+ new BoolCurve()),
+ new HasMeleeWeaponCon(
+ new InverseBoolCurve()),
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
+ new MeleeWeaponDamageCon(
+ new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)),
+ new MeleeWeaponSpeedCon(
+ new QuadraticCurve(-1.0f, 0.5f, 1.0f, 0.0f)),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/BallisticAttackEntity.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/BallisticAttackEntity.cs
new file mode 100644
index 0000000000..a899644074
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/BallisticAttackEntity.cs
@@ -0,0 +1,97 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Combat.Ranged;
+using Content.Server.AI.Operators.Movement;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.Utils;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Combat;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.AI.WorldState.States.Movement;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic
+{
+ public sealed class BallisticAttackEntity : UtilityAction
+ {
+ private IEntity _entity;
+ private MoveToEntityOperator _moveOperator;
+
+ public BallisticAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ if (_moveOperator != null)
+ {
+ _moveOperator.MovedATile -= InLos;
+ }
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ _moveOperator = new MoveToEntityOperator(Owner, _entity);
+ _moveOperator.MovedATile += InLos;
+
+ // TODO: Accuracy in blackboard
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ _moveOperator,
+ new ShootAtEntityOperator(Owner, _entity, 0.7f),
+ });
+
+ // We will do a quick check now to see if we even need to move which also saves a pathfind
+ InLos();
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ var equipped = context.GetState().GetValue();
+ context.GetState().SetValue(equipped);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ // Check if we have a weapon; easy-out
+ new BallisticWeaponEquippedCon(
+ new BoolCurve()),
+ new BallisticAmmoCon(
+ new QuadraticCurve(1.0f, 0.15f, 0.0f, 0.0f)),
+ // Don't attack a dead target
+ new TargetIsDeadCon(
+ new InverseBoolCurve()),
+ // Deprioritise a target in crit
+ new TargetIsCritCon(
+ new QuadraticCurve(-0.8f, 1.0f, 1.0f, 0.0f)),
+ // Somewhat prioritise distance
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.07f, 0.0f)),
+ // Prefer weaker targets
+ new TargetHealthCon(
+ new QuadraticCurve(1.0f, 0.4f, 0.0f, -0.02f)),
+ };
+
+ private void InLos()
+ {
+ // This should only be called if the movement operator is the current one;
+ // if that turns out not to be the case we can just add a check here.
+ if (Visibility.InLineOfSight(Owner, _entity))
+ {
+ _moveOperator.HaveArrived();
+ var mover = ActionOperators.Dequeue();
+ mover.Shutdown(Outcome.Success);
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/DropEmptyBallistic.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/DropEmptyBallistic.cs
new file mode 100644
index 0000000000..15de797219
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/DropEmptyBallistic.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Combat;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic
+{
+ public sealed class DropEmptyBallistic : UtilityAction
+ {
+ public sealed override float Bonus => 20.0f;
+ private IEntity _entity;
+
+ public DropEmptyBallistic(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new EquipEntityOperator(Owner, _entity),
+ new DropEntityOperator(Owner, _entity)
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new TargetInOurInventoryCon(
+ new BoolCurve()),
+ // Need to put in hands to drop
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ // Drop that sucker
+ new BallisticAmmoCon(
+ new InverseBoolCurve()),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/EquipBallistic.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/EquipBallistic.cs
new file mode 100644
index 0000000000..3c3ec8d84c
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/EquipBallistic.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat.Melee;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Combat;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic
+{
+ public sealed class EquipBallistic : UtilityAction
+ {
+ private IEntity _entity;
+
+ public EquipBallistic(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new EquipEntityOperator(Owner, _entity)
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new EquippedBallisticCon(
+ new InverseBoolCurve()),
+ new MeleeWeaponEquippedCon(
+ new QuadraticCurve(0.9f, 1.0f, 0.1f, 0.0f)),
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ new BallisticAmmoCon(
+ new QuadraticCurve(1.0f, 0.15f, 0.0f, 0.0f)),
+ new RangedWeaponFireRateCon(
+ new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpAmmo.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpAmmo.cs
new file mode 100644
index 0000000000..cf897a19f9
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpAmmo.cs
@@ -0,0 +1,46 @@
+using Content.Server.AI.Operators.Sequences;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Containers;
+using Content.Server.AI.Utility.Considerations.Hands;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Movement;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic
+{
+ public sealed class PickUpAmmo : UtilityAction
+ {
+ private IEntity _entity;
+
+ public PickUpAmmo(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ //TODO: Consider ammo's type and what guns we have
+ new TargetAccessibleCon(
+ new BoolCurve()),
+ new FreeHandCon(
+ new BoolCurve()),
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpBallisticMagWeapon.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpBallisticMagWeapon.cs
new file mode 100644
index 0000000000..ef1934565a
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Ballistic/PickUpBallisticMagWeapon.cs
@@ -0,0 +1,57 @@
+using Content.Server.AI.Operators.Sequences;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic;
+using Content.Server.AI.Utility.Considerations.Containers;
+using Content.Server.AI.Utility.Considerations.Hands;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Combat;
+using Content.Server.AI.WorldState.States.Movement;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic
+{
+ public sealed class PickUpBallisticMagWeapon : UtilityAction
+ {
+ private IEntity _entity;
+
+ public PickUpBallisticMagWeapon(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new HeldRangedWeaponsCon(
+ new QuadraticCurve(-1.0f, 1.0f, 1.0f, 0.0f)),
+ new TargetAccessibleCon(
+ new BoolCurve()),
+ new FreeHandCon(
+ new BoolCurve()),
+ // For now don't grab empty guns - at least until we can start storing stuff in inventory
+ new BallisticAmmoCon(
+ new BoolCurve()),
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
+ new RangedWeaponFireRateCon(
+ new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
+ // TODO: Ballistic accuracy? Depends how the design transitions
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/ChargeHitscan.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/ChargeHitscan.cs
new file mode 100644
index 0000000000..b8b6143dc6
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/ChargeHitscan.cs
@@ -0,0 +1,69 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Operators.Movement;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.AI.WorldState.States.Movement;
+using Content.Server.GameObjects.Components.Power.Chargers;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
+{
+ public sealed class PutHitscanInCharger : UtilityAction
+ {
+ // Maybe a bad idea to not allow override
+ public override bool CanOverride => false;
+ private readonly IEntity _charger;
+
+ public PutHitscanInCharger(IEntity owner, IEntity charger, float weight) : base(owner)
+ {
+ _charger = charger;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ var weapon = context.GetState().GetValue();
+
+ if (weapon == null || _charger.GetComponent().HeldItem != null)
+ {
+ ActionOperators = new Queue();
+ return;
+ }
+
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new MoveToEntityOperator(Owner, _charger),
+ new InteractWithEntityOperator(Owner, _charger),
+ // Separate task will deal with picking it up
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_charger);
+ context.GetState().SetValue(_charger);
+ }
+
+ protected override Consideration[] Considerations { get; } =
+ {
+ new HitscanWeaponEquippedCon(
+ new BoolCurve()),
+ new HitscanChargerFullCon(
+ new InverseBoolCurve()),
+ new HitscanChargerRateCon(
+ new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
+ new HitscanChargeCon(
+ new QuadraticCurve(-1.2f, 2.0f, 1.2f, 0.0f)),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/DropEmptyHitscan.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/DropEmptyHitscan.cs
new file mode 100644
index 0000000000..f56bde67e8
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/DropEmptyHitscan.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Combat;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
+{
+ public sealed class DropEmptyHitscan : UtilityAction
+ {
+ private IEntity _entity;
+
+ public DropEmptyHitscan(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new EquipEntityOperator(Owner, _entity),
+ new DropEntityOperator(Owner, _entity)
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new TargetInOurInventoryCon(
+ new BoolCurve()),
+ // Need to put in hands to drop
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ // If completely empty then drop that sucker
+ new HitscanChargeCon(
+ new InverseBoolCurve()),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/EquipHitscan.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/EquipHitscan.cs
new file mode 100644
index 0000000000..becf2a9e0b
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/EquipHitscan.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat.Melee;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
+using Content.Server.AI.Utility.Considerations.Inventory;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Combat;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
+{
+ public sealed class EquipHitscan : UtilityAction
+ {
+ private IEntity _entity;
+
+ public EquipHitscan(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new EquipEntityOperator(Owner, _entity)
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new EquippedHitscanCon(
+ new InverseBoolCurve()),
+ new MeleeWeaponEquippedCon(
+ new QuadraticCurve(0.9f, 1.0f, 0.1f, 0.0f)),
+ new CanPutTargetInHandsCon(
+ new BoolCurve()),
+ new HitscanChargeCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.0f, 0.0f)),
+ new RangedWeaponFireRateCon(
+ new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
+ new HitscanWeaponDamageCon(
+ new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/HitscanAttackEntity.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/HitscanAttackEntity.cs
new file mode 100644
index 0000000000..b79e4615db
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/HitscanAttackEntity.cs
@@ -0,0 +1,96 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Combat.Ranged;
+using Content.Server.AI.Operators.Movement;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.Utils;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Combat;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.AI.WorldState.States.Movement;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
+{
+ public sealed class HitscanAttackEntity : UtilityAction
+ {
+ private IEntity _entity;
+ private MoveToEntityOperator _moveOperator;
+
+ public HitscanAttackEntity(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ if (_moveOperator != null)
+ {
+ _moveOperator.MovedATile -= InLos;
+ }
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ _moveOperator = new MoveToEntityOperator(Owner, _entity);
+ _moveOperator.MovedATile += InLos;
+
+ // TODO: Accuracy in blackboard
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ _moveOperator,
+ new ShootAtEntityOperator(Owner, _entity, 0.7f),
+ });
+
+ InLos();
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ var equipped = context.GetState().GetValue();
+ context.GetState().SetValue(equipped);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ // Check if we have a weapon; easy-out
+ new HitscanWeaponEquippedCon(
+ new BoolCurve()),
+ new HitscanChargeCon(
+ new QuadraticCurve(1.0f, 0.1f, 0.0f, 0.0f)),
+ // Don't attack a dead target
+ new TargetIsDeadCon(
+ new InverseBoolCurve()),
+ // Deprioritise a target in crit
+ new TargetIsCritCon(
+ new QuadraticCurve(-0.8f, 1.0f, 1.0f, 0.0f)),
+ // Somewhat prioritise distance
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.07f, 0.0f)),
+ // Prefer weaker targets
+ new TargetHealthCon(
+ new QuadraticCurve(1.0f, 0.4f, 0.0f, -0.02f)),
+ };
+
+ private void InLos()
+ {
+ // This should only be called if the movement operator is the current one;
+ // if that turns out not to be the case we can just add a check here.
+ if (Visibility.InLineOfSight(Owner, _entity))
+ {
+ _moveOperator.HaveArrived();
+ var mover = ActionOperators.Dequeue();
+ mover.Shutdown(Outcome.Success);
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanFromCharger.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanFromCharger.cs
new file mode 100644
index 0000000000..7bd1fd48f1
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanFromCharger.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Combat.Ranged;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Operators.Movement;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
+using Content.Server.AI.Utility.Considerations.Containers;
+using Content.Server.AI.Utility.Considerations.Hands;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Movement;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
+{
+ public sealed class PickUpHitscanFromCharger : UtilityAction
+ {
+ private IEntity _entity;
+ private IEntity _charger;
+
+ public PickUpHitscanFromCharger(IEntity owner, IEntity entity, IEntity charger, float weight) : base(owner)
+ {
+ _entity = entity;
+ _charger = charger;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new MoveToEntityOperator(Owner, _charger),
+ new WaitForHitscanChargeOperator(_entity),
+ new PickupEntityOperator(Owner, _entity),
+ });
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new HeldRangedWeaponsCon(
+ new QuadraticCurve(-1.0f, 1.0f, 1.0f, 0.0f)),
+ new TargetAccessibleCon(
+ new BoolCurve()),
+ new FreeHandCon(
+ new BoolCurve()),
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
+ // TODO: ChargerHasPower
+ new RangedWeaponFireRateCon(
+ new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
+ new HitscanWeaponDamageCon(
+ new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanWeapon.cs b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanWeapon.cs
new file mode 100644
index 0000000000..463de4f0f9
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Combat/Ranged/Hitscan/PickUpHitscanWeapon.cs
@@ -0,0 +1,59 @@
+using Content.Server.AI.Operators.Sequences;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged;
+using Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan;
+using Content.Server.AI.Utility.Considerations.Containers;
+using Content.Server.AI.Utility.Considerations.Hands;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Combat;
+using Content.Server.AI.WorldState.States.Movement;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan
+{
+ public sealed class PickUpHitscanWeapon : UtilityAction
+ {
+ private IEntity _entity;
+
+ public PickUpHitscanWeapon(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
+ }
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ context.GetState().SetValue(_entity);
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new HeldRangedWeaponsCon(
+ new QuadraticCurve(-1.0f, 1.0f, 1.0f, 0.0f)),
+ new TargetAccessibleCon(
+ new BoolCurve()),
+ new FreeHandCon(
+ new BoolCurve()),
+ // For now don't grab empty guns - at least until we can start storing stuff in inventory
+ new HitscanChargeCon(
+ new BoolCurve()),
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
+ // TODO: Weapon charge level
+ new RangedWeaponFireRateCon(
+ new QuadraticCurve(1.0f, 0.5f, 0.0f, 0.0f)),
+ new HitscanWeaponDamageCon(
+ new QuadraticCurve(1.0f, 0.25f, 0.0f, 0.0f)),
+ };
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/IAiUtility.cs b/Content.Server/AI/Utility/Actions/IAiUtility.cs
new file mode 100644
index 0000000000..679d410cb7
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/IAiUtility.cs
@@ -0,0 +1,9 @@
+using Content.Server.AI.Utility.AiLogic;
+
+namespace Content.Server.AI.Utility.Actions
+{
+ public interface IAiUtility
+ {
+ float Bonus { get; }
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs b/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs
new file mode 100644
index 0000000000..1fd934cdc2
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Idle/CloseLastEntityStorage.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Operators.Movement;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Considerations.State;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Inventory;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Idle
+{
+ ///
+ /// If we just picked up a bunch of stuff and have time then close it
+ ///
+ public sealed class CloseLastEntityStorage : UtilityAction
+ {
+ public override float Bonus => 1.5f;
+
+ public CloseLastEntityStorage(IEntity owner) : base(owner) {}
+
+ protected override Consideration[] Considerations => new Consideration[]
+ {
+ new StoredStateIsNullCon(
+ new InverseBoolCurve()),
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
+ };
+ public override void SetupOperators(Blackboard context)
+ {
+ var lastStorage = context.GetState().GetValue();
+
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new MoveToEntityOperator(Owner, lastStorage),
+ new CloseLastStorageOperator(Owner),
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs b/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs
new file mode 100644
index 0000000000..52f03c4046
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Generic;
+using Content.Server.AI.Operators.Movement;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.ActionBlocker;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Map;
+using Robust.Shared.Interfaces.Random;
+using Robust.Shared.IoC;
+using Robust.Shared.Map;
+using Robust.Shared.Maths;
+using Robust.Shared.Random;
+
+namespace Content.Server.AI.Utility.Actions.Idle
+{
+ ///
+ /// Will move to a random spot close by
+ ///
+ public sealed class WanderAndWait : UtilityAction
+ {
+ public override bool CanOverride => false;
+ public override float Bonus => IdleBonus;
+
+ public WanderAndWait(IEntity owner) : base(owner)
+ {
+ // TODO: Need a Success method that gets called to update context (e.g. when we last did X)
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ var randomGrid = FindRandomGrid();
+ float waitTime;
+ if (randomGrid != GridCoordinates.InvalidGrid)
+ {
+ var random = IoCManager.Resolve();
+ waitTime = random.NextFloat() * 10;
+ }
+ else
+ {
+ waitTime = 0.0f;
+ }
+
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new MoveToGridOperator(Owner, randomGrid),
+ new WaitOperator(waitTime),
+ });
+ }
+
+ protected override Consideration[] Considerations { get; } = {
+ new CanMoveCon(
+ new BoolCurve())
+ // Last wander? If we also want to sit still
+ };
+
+ private GridCoordinates FindRandomGrid()
+ {
+ var mapManager = IoCManager.Resolve();
+ var grid = mapManager.GetGrid(Owner.Transform.GridID);
+
+ // Just find a random spot in bounds
+ // If the grid's a single-tile wide but really tall this won't really work but eh future problem
+ var gridBounds = grid.WorldBounds;
+ var robustRandom = IoCManager.Resolve();
+ var newPosition = gridBounds.BottomLeft + new Vector2(
+ robustRandom.Next((int) gridBounds.Width),
+ robustRandom.Next((int) gridBounds.Height));
+ // Conversions blah blah
+ var mapIndex = grid.WorldToTile(grid.LocalToWorld(newPosition));
+ // Didn't find one? Fuck it we're not walkin' into space
+ if (grid.GetTileRef(mapIndex).Tile.IsEmpty)
+ {
+ return GridCoordinates.InvalidGrid;
+ }
+ var target = grid.GridTileToLocal(mapIndex);
+
+ return target;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs b/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs
new file mode 100644
index 0000000000..e9c181c5b7
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Nutrition/Drink/PickUpDrink.cs
@@ -0,0 +1,49 @@
+using Content.Server.AI.Operators.Sequences;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Containers;
+using Content.Server.AI.Utility.Considerations.Hands;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Considerations.Nutrition.Drink;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Nutrition.Drink
+{
+ public sealed class PickUpDrink : UtilityAction
+ {
+ private IEntity _entity;
+
+ public PickUpDrink(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
+ }
+
+ protected override Consideration[] Considerations => new Consideration[]
+ {
+ new TargetAccessibleCon(
+ new BoolCurve()),
+ new FreeHandCon(
+ new BoolCurve()),
+ new ThirstCon(
+ new LogisticCurve(1000f, 1.3f, -1.0f, 0.5f)),
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
+ new DrinkValueCon(
+ new QuadraticCurve(1.0f, 0.4f, 0.0f, 0.0f)),
+ };
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Drink/UseDrinkInInventory.cs b/Content.Server/AI/Utility/Actions/Nutrition/Drink/UseDrinkInInventory.cs
new file mode 100644
index 0000000000..eae360a69e
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Nutrition/Drink/UseDrinkInInventory.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Hands;
+using Content.Server.AI.Utility.Considerations.Nutrition.Drink;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Nutrition.Drink
+{
+ public sealed class UseDrinkInInventory : UtilityAction
+ {
+ private IEntity _entity;
+
+ public UseDrinkInInventory(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new EquipEntityOperator(Owner, _entity),
+ new UseItemInHandsOperator(Owner, _entity),
+ });
+ }
+
+ protected override Consideration[] Considerations => new Consideration[]
+ {
+ new TargetInOurHandsCon(
+ new BoolCurve()),
+ new ThirstCon(
+ new LogisticCurve(1000f, 1.3f, -0.3f, 0.5f)),
+ new DrinkValueCon(
+ new QuadraticCurve(1.0f, 0.4f, 0.0f, 0.0f))
+ };
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs b/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs
new file mode 100644
index 0000000000..9db0b814dd
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Nutrition/Food/PickUpFood.cs
@@ -0,0 +1,49 @@
+using Content.Server.AI.Operators.Sequences;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Containers;
+using Content.Server.AI.Utility.Considerations.Hands;
+using Content.Server.AI.Utility.Considerations.Movement;
+using Content.Server.AI.Utility.Considerations.Nutrition;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Nutrition.Food
+{
+ public sealed class PickUpFood : UtilityAction
+ {
+ private IEntity _entity;
+
+ public PickUpFood(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new GoPickupEntitySequence(Owner, _entity).Sequence;
+ }
+
+ protected override Consideration[] Considerations => new Consideration[]
+ {
+ new TargetAccessibleCon(
+ new BoolCurve()),
+ new FreeHandCon(
+ new BoolCurve()),
+ new HungerCon(
+ new LogisticCurve(1000f, 1.3f, -1.0f, 0.5f)),
+ new DistanceCon(
+ new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
+ new FoodValueCon(
+ new QuadraticCurve(1.0f, 0.4f, 0.0f, 0.0f)),
+ };
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Nutrition/Food/UseFoodInInventory.cs b/Content.Server/AI/Utility/Actions/Nutrition/Food/UseFoodInInventory.cs
new file mode 100644
index 0000000000..2996cf3769
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Nutrition/Food/UseFoodInInventory.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Inventory;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Considerations.Hands;
+using Content.Server.AI.Utility.Considerations.Nutrition;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.Actions.Nutrition.Food
+{
+ public sealed class UseFoodInInventory : UtilityAction
+ {
+ private IEntity _entity;
+
+ public UseFoodInInventory(IEntity owner, IEntity entity, float weight) : base(owner)
+ {
+ _entity = entity;
+ Bonus = weight;
+ }
+
+ public override void SetupOperators(Blackboard context)
+ {
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ new EquipEntityOperator(Owner, _entity),
+ new UseItemInHandsOperator(Owner, _entity),
+ });
+ }
+
+ protected override Consideration[] Considerations => new Consideration[]
+ {
+ new TargetInOurHandsCon(
+ new BoolCurve()),
+ new HungerCon(
+ new LogisticCurve(1000f, 1.3f, -0.3f, 0.5f)),
+ new FoodValueCon(
+ new QuadraticCurve(1.0f, 0.4f, 0.0f, 0.0f))
+ };
+
+ protected override void UpdateBlackboard(Blackboard context)
+ {
+ base.UpdateBlackboard(context);
+ context.GetState().SetValue(_entity);
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/Test/MoveRightAndLeftTen.cs b/Content.Server/AI/Utility/Actions/Test/MoveRightAndLeftTen.cs
new file mode 100644
index 0000000000..a2f49c1c79
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/Test/MoveRightAndLeftTen.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Movement;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Maths;
+
+namespace Content.Server.AI.Utility.Actions.Test
+{
+ ///
+ /// Used for pathfinding debugging
+ ///
+ public class MoveRightAndLeftTen : UtilityAction
+ {
+ public override bool CanOverride => false;
+
+ public MoveRightAndLeftTen(IEntity owner) : base(owner) {}
+
+ protected override Consideration[] Considerations { get; } = {
+ new DummyCon(
+ new BoolCurve())
+ };
+
+ public override void SetupOperators(Blackboard context)
+ {
+ var currentPosition = Owner.Transform.GridPosition;
+ var nextPosition = Owner.Transform.GridPosition.Offset(new Vector2(10.0f, 0.0f));
+ var originalPosOp = new MoveToGridOperator(Owner, currentPosition, 0.25f);
+ var newPosOp = new MoveToGridOperator(Owner, nextPosition, 0.25f);
+
+ ActionOperators = new Queue(new AiOperator[]
+ {
+ newPosOp,
+ originalPosOp
+ });
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Actions/UtilityAction.cs b/Content.Server/AI/Utility/Actions/UtilityAction.cs
new file mode 100644
index 0000000000..1d5a385beb
--- /dev/null
+++ b/Content.Server/AI/Utility/Actions/UtilityAction.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Utility.Considerations;
+using Content.Server.AI.WorldState;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Utility;
+
+namespace Content.Server.AI.Utility.Actions
+{
+ ///
+ /// The same DSE can be used across multiple actions.
+ ///
+ public abstract class UtilityAction : IAiUtility
+ {
+ ///
+ /// If we're trying to find a new action can we replace a currently running one with one of the same type.
+ /// e.g. If you're already wandering you don't want to replace it with a different wander.
+ ///
+ public virtual bool CanOverride => false;
+
+ ///
+ /// This is used to sort actions; if there's a top-tier action available we won't bother checking the lower tiers.
+ /// Threshold doesn't necessarily mean we'll do an action at a higher threshold;
+ /// if it's really un-optimal (i.e. low score) then we'll also check lower tiers
+ ///
+ public virtual float Bonus { get; protected set; } = IdleBonus;
+ // For GW2 they had the bonuses close together but IMO it feels better when they're more like discrete tiers.
+
+ // These are just baselines to make mass-updates easier; actions can do whatever
+ // e.g. if you want shooting a gun to be considered before picking up a gun you could + 1.0f it or w/e
+ public const float IdleBonus = 1.0f;
+ public const float NormalBonus = 5.0f;
+ public const float NeedsBonus = 10.0f;
+ public const float CombatPrepBonus = 20.0f;
+ public const float CombatBonus = 30.0f;
+ public const float DangerBonus = 50.0f;
+
+ protected IEntity Owner { get; }
+
+ ///
+ /// All the considerations are multiplied together to get the final score; a consideration of 0.0 means the action is not possible.
+ /// Ideally you put anything that's easy to assess and can cause an early-out first just so the rest aren't evaluated.
+ ///
+ protected abstract Consideration[] Considerations { get; }
+
+ ///
+ /// To keep the operators simple we can chain them together here, e.g. move to can be chained with other operators.
+ ///
+ public Queue ActionOperators { get; protected set; }
+
+ ///
+ /// Sometimes we may need to set the target for an action or the likes.
+ /// This is mainly useful for expandable states so each one can have a separate target.
+ ///
+ ///
+ protected virtual void UpdateBlackboard(Blackboard context) {}
+
+ protected UtilityAction(IEntity owner)
+ {
+ Owner = owner;
+ }
+
+ public virtual void Shutdown() {}
+
+ ///
+ /// If this action is chosen then setup the operators to run. This also allows for operators to be reset.
+ ///
+ public abstract void SetupOperators(Blackboard context);
+
+ // Call the task's operator with Execute and get the outcome
+ public Outcome Execute(float frameTime)
+ {
+ if (!ActionOperators.TryPeek(out var op))
+ {
+ return Outcome.Success;
+ }
+
+ op.TryStartup();
+ var outcome = op.Execute(frameTime);
+
+ switch (outcome)
+ {
+ case Outcome.Success:
+ op.Shutdown(outcome);
+ ActionOperators.Dequeue();
+ break;
+ case Outcome.Continuing:
+ break;
+ case Outcome.Failed:
+ op.Shutdown(outcome);
+ ActionOperators.Clear();
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ return outcome;
+ }
+
+ ///
+ /// AKA the Decision Score Evaluator (DSE)
+ /// This is where the magic happens
+ ///
+ ///
+ ///
+ ///
+ ///
+ public float GetScore(Blackboard context, float min)
+ {
+ UpdateBlackboard(context);
+ DebugTools.Assert(Considerations.Length > 0);
+ // I used the IAUS video although I did have some confusion on how to structure it overall
+ // as some of the slides seemed contradictory
+
+ // Ideally we should early-out each action as cheaply as possible if it's not valid
+
+ // We also need some way to tell if the action isn't going to
+ // have a better score than the current action (if applicable) and early-out that way as well.
+
+ // 23:00 Building a better centaur
+ var finalScore = 1.0f;
+ var minThreshold = min / Bonus;
+ var modificationFactor = 1.0f - 1.0f / Considerations.Length;
+ // See 10:09 for this and the adjustments
+
+ foreach (var consideration in Considerations)
+ {
+ var score = consideration.GetScore(context);
+ var makeUpValue = (1.0f - score) * modificationFactor;
+ var adjustedScore = score + makeUpValue * score;
+ var response = consideration.ComputeResponseCurve(adjustedScore);
+
+ finalScore *= response;
+
+ DebugTools.Assert(!float.IsNaN(response));
+
+ // The score can only ever go down from each consideration so if we're below minimum no point continuing.
+ if (0.0f >= finalScore || finalScore < minThreshold) {
+ return 0.0f;
+ }
+ }
+
+ DebugTools.Assert(finalScore <= 1.0f);
+
+ return finalScore * Bonus;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/AiLogic/Civilian.cs b/Content.Server/AI/Utility/AiLogic/Civilian.cs
new file mode 100644
index 0000000000..03c621e8a2
--- /dev/null
+++ b/Content.Server/AI/Utility/AiLogic/Civilian.cs
@@ -0,0 +1,21 @@
+using Content.Server.AI.Utility.BehaviorSets;
+using JetBrains.Annotations;
+using Robust.Server.AI;
+
+namespace Content.Server.AI.Utility.AiLogic
+{
+ [AiLogicProcessor("Civilian")]
+ [UsedImplicitly]
+ public sealed class Civilian : UtilityAi
+ {
+ public override void Setup()
+ {
+ base.Setup();
+ AddBehaviorSet(new ClothingBehaviorSet(SelfEntity), false);
+ AddBehaviorSet(new HungerBehaviorSet(SelfEntity), false);
+ AddBehaviorSet(new ThirstBehaviorSet(SelfEntity), false);
+ AddBehaviorSet(new IdleBehaviorSet(SelfEntity), false);
+ SortActions();
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/AiLogic/PathingDummy.cs b/Content.Server/AI/Utility/AiLogic/PathingDummy.cs
new file mode 100644
index 0000000000..3c493021b3
--- /dev/null
+++ b/Content.Server/AI/Utility/AiLogic/PathingDummy.cs
@@ -0,0 +1,18 @@
+using Content.Server.AI.Utility.BehaviorSets;
+using JetBrains.Annotations;
+using Robust.Server.AI;
+
+namespace Content.Server.AI.Utility.AiLogic
+{
+ [AiLogicProcessor("PathingDummy")]
+ [UsedImplicitly]
+ public sealed class PathingDummy : UtilityAi
+ {
+ public override void Setup()
+ {
+ base.Setup();
+ BehaviorSets.Add(typeof(PathingDummyBehaviorSet), new PathingDummyBehaviorSet(SelfEntity));
+ SortActions();
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/AiLogic/Spirate.cs b/Content.Server/AI/Utility/AiLogic/Spirate.cs
new file mode 100644
index 0000000000..9a8935f57f
--- /dev/null
+++ b/Content.Server/AI/Utility/AiLogic/Spirate.cs
@@ -0,0 +1,22 @@
+using Content.Server.AI.Utility.BehaviorSets;
+using JetBrains.Annotations;
+using Robust.Server.AI;
+
+namespace Content.Server.AI.Utility.AiLogic
+{
+ [AiLogicProcessor("Spirate")]
+ [UsedImplicitly]
+ public sealed class Spirate : UtilityAi
+ {
+ public override void Setup()
+ {
+ base.Setup();
+ AddBehaviorSet(new ClothingBehaviorSet(SelfEntity), false);
+ AddBehaviorSet(new HungerBehaviorSet(SelfEntity), false);
+ AddBehaviorSet(new ThirstBehaviorSet(SelfEntity), false);
+ AddBehaviorSet(new IdleBehaviorSet(SelfEntity), false);
+ AddBehaviorSet(new SpirateBehaviorSet(SelfEntity), false);
+ SortActions();
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/AiLogic/UtilityAI.cs b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs
new file mode 100644
index 0000000000..0b76293e8f
--- /dev/null
+++ b/Content.Server/AI/Utility/AiLogic/UtilityAI.cs
@@ -0,0 +1,228 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Content.Server.AI.Operators;
+using Content.Server.AI.Operators.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.BehaviorSets;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Utility;
+using Content.Server.GameObjects;
+using Content.Server.GameObjects.EntitySystems.AI.LoadBalancer;
+using Content.Server.GameObjects.EntitySystems.JobQueues;
+using Robust.Server.AI;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Utility;
+
+namespace Content.Server.AI.Utility.AiLogic
+{
+ public abstract class UtilityAi : AiLogicProcessor
+ {
+ // TODO: Look at having ParallelOperators (probably no more than that as then you'd have a full-blown BT)
+ // Also RepeatOperators (e.g. if we're following an entity keep repeating MoveToEntity)
+ private AiActionSystem _planner;
+ public Blackboard Blackboard => _blackboard;
+ private Blackboard _blackboard;
+
+ ///
+ /// The sum of all BehaviorSets gives us what actions the AI can take
+ ///
+ public Dictionary BehaviorSets { get; } = new Dictionary();
+ private readonly List _availableActions = new List();
+
+ ///
+ /// The currently running action; most importantly are the operators.
+ ///
+ public UtilityAction CurrentAction { get; private set; }
+
+ ///
+ /// How frequently we can re-plan. If an AI's in combat you could decrease the cooldown,
+ /// or if there's no players nearby increase it.
+ ///
+ public float PlanCooldown { get; } = 0.5f;
+ private float _planCooldownRemaining;
+
+ ///
+ /// If we've requested a plan then wait patiently for the action
+ ///
+ private AiActionRequestJob _actionRequest;
+
+ private CancellationTokenSource _actionCancellation;
+
+ ///
+ /// If we can't do anything then stop thinking; should probably use ActionBlocker instead
+ ///
+ private bool _isDead = false;
+
+ // These 2 methods will be used eventually if / when we get a director AI
+ public void AddBehaviorSet(T behaviorSet, bool sort = true) where T : BehaviorSet
+ {
+ if (BehaviorSets.TryAdd(typeof(T), behaviorSet) && sort)
+ {
+ SortActions();
+ }
+ }
+
+ public void RemoveBehaviorSet(Type behaviorSet)
+ {
+ DebugTools.Assert(behaviorSet.IsAssignableFrom(typeof(BehaviorSet)));
+
+ if (BehaviorSets.ContainsKey(behaviorSet))
+ {
+ BehaviorSets.Remove(behaviorSet);
+ SortActions();
+ }
+ }
+
+ ///
+ /// Whenever the behavior sets are changed we'll re-sort the actions by bonus
+ ///
+ protected void SortActions()
+ {
+ _availableActions.Clear();
+ foreach (var set in BehaviorSets.Values)
+ {
+ foreach (var action in set.Actions)
+ {
+ var found = false;
+
+ for (var i = 0; i < _availableActions.Count; i++)
+ {
+ if (_availableActions[i].Bonus < action.Bonus)
+ {
+ _availableActions.Insert(i, action);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ _availableActions.Add(action);
+ }
+ }
+ }
+
+ _availableActions.Reverse();
+ }
+
+ public override void Setup()
+ {
+ base.Setup();
+ _planCooldownRemaining = PlanCooldown;
+ _blackboard = new Blackboard(SelfEntity);
+ _planner = IoCManager.Resolve().GetEntitySystem();
+ if (SelfEntity.TryGetComponent(out DamageableComponent damageableComponent))
+ {
+ damageableComponent.DamageThresholdPassed += DeathHandle;
+ }
+ }
+
+ public override void Shutdown()
+ {
+ // TODO: If DamageableComponent removed still need to unsubscribe?
+ if (SelfEntity.TryGetComponent(out DamageableComponent damageableComponent))
+ {
+ damageableComponent.DamageThresholdPassed -= DeathHandle;
+ }
+ }
+
+ private void DeathHandle(object sender, DamageThresholdPassedEventArgs eventArgs)
+ {
+ if (eventArgs.DamageThreshold.ThresholdType == ThresholdType.Death)
+ {
+ _isDead = true;
+ }
+
+ // TODO: If we get healed - double-check what it should be
+ if (eventArgs.DamageThreshold.ThresholdType == ThresholdType.None)
+ {
+ _isDead = false;
+ }
+ }
+
+ private void ReceivedAction()
+ {
+ var action = _actionRequest.Result;
+ _actionRequest = null;
+ // Actions with lower scores should be implicitly dumped by GetAction
+ // If we're not allowed to replace the action with an action of the same type then dump.
+ if (action == null || !action.CanOverride && CurrentAction?.GetType() == action.GetType())
+ {
+ return;
+ }
+
+ CurrentAction = action;
+ action.SetupOperators(_blackboard);
+ }
+
+ public override void Update(float frameTime)
+ {
+ // If we can't do anything then there's no point thinking
+ if (_isDead || BehaviorSets.Count == 0)
+ {
+ _actionCancellation?.Cancel();
+ _blackboard.GetState().SetValue(0.0f);
+ CurrentAction?.Shutdown();
+ CurrentAction = null;
+ return;
+ }
+
+ // If we asked for a new action we don't want to dump the existing one.
+ if (_actionRequest != null)
+ {
+ if (_actionRequest.Status != JobStatus.Finished)
+ {
+ return;
+ }
+
+ ReceivedAction();
+ // Do something next tick
+ return;
+ }
+
+ _planCooldownRemaining -= frameTime;
+
+ // Might find a better action while we're doing one already
+ if (_planCooldownRemaining <= 0.0f)
+ {
+ _planCooldownRemaining = PlanCooldown;
+ _actionCancellation = new CancellationTokenSource();
+ _actionRequest = _planner.RequestAction(new AiActionRequest(SelfEntity.Uid, _blackboard, _availableActions), _actionCancellation);
+
+ return;
+ }
+
+ // When we spawn in we won't get an action for a bit
+ if (CurrentAction == null)
+ {
+ return;
+ }
+
+ var outcome = CurrentAction.Execute(frameTime);
+
+ switch (outcome)
+ {
+ case Outcome.Success:
+ if (CurrentAction.ActionOperators.Count == 0)
+ {
+ CurrentAction.Shutdown();
+ CurrentAction = null;
+ // Nothing to compare new action to
+ _blackboard.GetState().SetValue(0.0f);
+ }
+ break;
+ case Outcome.Continuing:
+ break;
+ case Outcome.Failed:
+ CurrentAction.Shutdown();
+ CurrentAction = null;
+ _blackboard.GetState().SetValue(0.0f);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/BehaviorSets/BehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/BehaviorSet.cs
new file mode 100644
index 0000000000..0651885c7e
--- /dev/null
+++ b/Content.Server/AI/Utility/BehaviorSets/BehaviorSet.cs
@@ -0,0 +1,22 @@
+using System.Collections;
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.BehaviorSets
+{
+ ///
+ /// AKA DecisionMaker in IAUS. Just a group of actions that can be dynamically added or taken away from an AI.
+ ///
+ public abstract class BehaviorSet
+ {
+ protected IEntity Owner;
+
+ public BehaviorSet(IEntity owner)
+ {
+ Owner = owner;
+ }
+
+ public IEnumerable Actions { get; protected set; }
+ }
+}
diff --git a/Content.Server/AI/Utility/BehaviorSets/ClothingBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/ClothingBehaviorSet.cs
new file mode 100644
index 0000000000..eb0530bdb5
--- /dev/null
+++ b/Content.Server/AI/Utility/BehaviorSets/ClothingBehaviorSet.cs
@@ -0,0 +1,27 @@
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves;
+using Content.Server.AI.Utility.ExpandableActions.Clothing.Head;
+using Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing;
+using Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.BehaviorSets
+{
+ public sealed class ClothingBehaviorSet : BehaviorSet
+ {
+ public ClothingBehaviorSet(IEntity owner) : base(owner)
+ {
+ Actions = new IAiUtility[]
+ {
+ new EquipAnyHeadExp(),
+ new EquipAnyOuterClothingExp(),
+ new EquipAnyGlovesExp(),
+ new EquipAnyShoesExp(),
+ new PickUpAnyNearbyHeadExp(),
+ new PickUpAnyNearbyOuterClothingExp(),
+ new PickUpAnyNearbyGlovesExp(),
+ new PickUpAnyNearbyShoesExp(),
+ };
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/BehaviorSets/HungerBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/HungerBehaviorSet.cs
new file mode 100644
index 0000000000..a9c6117af1
--- /dev/null
+++ b/Content.Server/AI/Utility/BehaviorSets/HungerBehaviorSet.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Nutrition;
+using Content.Server.AI.Utility.ExpandableActions.Nutrition;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.BehaviorSets
+{
+ public sealed class HungerBehaviorSet : BehaviorSet
+ {
+ public HungerBehaviorSet(IEntity owner) : base(owner)
+ {
+ Actions = new IAiUtility[]
+ {
+ new PickUpNearbyFoodExp(),
+ new UseFoodInInventoryExp(),
+ };
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/BehaviorSets/IdleBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/IdleBehaviorSet.cs
new file mode 100644
index 0000000000..ac1b843585
--- /dev/null
+++ b/Content.Server/AI/Utility/BehaviorSets/IdleBehaviorSet.cs
@@ -0,0 +1,18 @@
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Idle;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.BehaviorSets
+{
+ public class IdleBehaviorSet : BehaviorSet
+ {
+ public IdleBehaviorSet(IEntity owner) : base(owner)
+ {
+ Actions = new IAiUtility[]
+ {
+ new CloseLastEntityStorage(Owner),
+ new WanderAndWait(Owner),
+ };
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/BehaviorSets/PathingDummyBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/PathingDummyBehaviorSet.cs
new file mode 100644
index 0000000000..cb90072b53
--- /dev/null
+++ b/Content.Server/AI/Utility/BehaviorSets/PathingDummyBehaviorSet.cs
@@ -0,0 +1,17 @@
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Test;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.BehaviorSets
+{
+ public sealed class PathingDummyBehaviorSet : BehaviorSet
+ {
+ public PathingDummyBehaviorSet(IEntity owner) : base(owner)
+ {
+ Actions = new IAiUtility[]
+ {
+ new MoveRightAndLeftTen(owner),
+ };
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs
new file mode 100644
index 0000000000..4228c33517
--- /dev/null
+++ b/Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Combat.Ranged;
+using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic;
+using Content.Server.AI.Utility.Actions.Combat.Ranged.Hitscan;
+using Content.Server.AI.Utility.ExpandableActions.Combat;
+using Content.Server.AI.Utility.ExpandableActions.Combat.Melee;
+using Content.Server.AI.Utility.ExpandableActions.Combat.Ranged;
+using Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic;
+using Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Hitscan;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.BehaviorSets
+{
+ public sealed class SpirateBehaviorSet : BehaviorSet
+ {
+ public SpirateBehaviorSet(IEntity owner) : base(owner)
+ {
+ Actions = new IAiUtility[]
+ {
+ new PickUpRangedExp(),
+ // TODO: Reload Ballistic
+ new DropEmptyBallisticExp(),
+ // TODO: Ideally long-term we should just store the weapons in backpack
+ new DropEmptyHitscanExp(),
+ new EquipMeleeExp(),
+ new EquipBallisticExp(),
+ new EquipHitscanExp(),
+ new PickUpHitscanFromChargersExp(),
+ new ChargeEquippedHitscanExp(),
+ new RangedAttackNearbySpeciesExp(),
+ new PickUpMeleeWeaponExp(),
+ new MeleeAttackNearbySpeciesExp(),
+ };
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/BehaviorSets/ThirstBehaviorSet.cs b/Content.Server/AI/Utility/BehaviorSets/ThirstBehaviorSet.cs
new file mode 100644
index 0000000000..7274d9034f
--- /dev/null
+++ b/Content.Server/AI/Utility/BehaviorSets/ThirstBehaviorSet.cs
@@ -0,0 +1,18 @@
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.ExpandableActions.Nutrition;
+using Robust.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.AI.Utility.BehaviorSets
+{
+ public sealed class ThirstBehaviorSet : BehaviorSet
+ {
+ public ThirstBehaviorSet(IEntity owner) : base(owner)
+ {
+ Actions = new IAiUtility[]
+ {
+ new PickUpNearbyDrinkExp(),
+ new UseDrinkInHandsExp(),
+ };
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/ActionBlocker/CanMoveCon.cs b/Content.Server/AI/Utility/Considerations/ActionBlocker/CanMoveCon.cs
new file mode 100644
index 0000000000..5702009b0a
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/ActionBlocker/CanMoveCon.cs
@@ -0,0 +1,23 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects.EntitySystems;
+
+namespace Content.Server.AI.Utility.Considerations.ActionBlocker
+{
+ public sealed class CanMoveCon : Consideration
+ {
+ public CanMoveCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var self = context.GetState().GetValue();
+ if (!ActionBlockerSystem.CanMove(self))
+ {
+ return 0.0f;
+ }
+
+ return 1.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Clothing/ClothingInInventoryCon.cs b/Content.Server/AI/Utility/Considerations/Clothing/ClothingInInventoryCon.cs
new file mode 100644
index 0000000000..7101e1ba1b
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Clothing/ClothingInInventoryCon.cs
@@ -0,0 +1,38 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects;
+using Content.Shared.GameObjects.Components.Inventory;
+
+namespace Content.Server.AI.Utility.Considerations.Clothing
+{
+ public sealed class ClothingInInventoryCon : Consideration
+ {
+ private readonly EquipmentSlotDefines.SlotFlags _slot;
+
+ public ClothingInInventoryCon(EquipmentSlotDefines.SlotFlags slotFlags, IResponseCurve curve) : base(curve)
+ {
+ _slot = slotFlags;
+ }
+
+ public override float GetScore(Blackboard context)
+ {
+ var inventory = context.GetState().GetValue();
+
+ foreach (var entity in inventory)
+ {
+ if (!entity.TryGetComponent(out ClothingComponent clothingComponent))
+ {
+ continue;
+ }
+
+ if ((clothingComponent.SlotFlags & _slot) != 0)
+ {
+ return 1.0f;
+ }
+ }
+
+ return 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Clothing/ClothingInSlotCon.cs b/Content.Server/AI/Utility/Considerations/Clothing/ClothingInSlotCon.cs
new file mode 100644
index 0000000000..11b30c0027
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Clothing/ClothingInSlotCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Clothing;
+using Content.Shared.GameObjects.Components.Inventory;
+
+namespace Content.Server.AI.Utility.Considerations.Clothing
+{
+ public class ClothingInSlotCon : Consideration
+ {
+ private EquipmentSlotDefines.Slots _slot;
+
+ public ClothingInSlotCon(EquipmentSlotDefines.Slots slot, IResponseCurve curve) : base(curve)
+ {
+ _slot = slot;
+ }
+
+ public override float GetScore(Blackboard context)
+ {
+ var inventory = context.GetState().GetValue();
+
+ return inventory.ContainsKey(_slot) ? 1.0f : 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Melee/HasMeleeWeaponCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Melee/HasMeleeWeaponCon.cs
new file mode 100644
index 0000000000..5d9567589f
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Melee/HasMeleeWeaponCon.cs
@@ -0,0 +1,25 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects.Components.Weapon.Melee;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Melee
+{
+ public sealed class HasMeleeWeaponCon : Consideration
+ {
+ public HasMeleeWeaponCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ foreach (var item in context.GetState().GetValue())
+ {
+ if (item.HasComponent())
+ {
+ return 1.0f;
+ }
+ }
+
+ return 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponDamageCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponDamageCon.cs
new file mode 100644
index 0000000000..31165ec6c0
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponDamageCon.cs
@@ -0,0 +1,25 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Combat;
+using Content.Server.GameObjects.Components.Weapon.Melee;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Melee
+{
+ public sealed class MeleeWeaponDamageCon : Consideration
+ {
+ public MeleeWeaponDamageCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var target = context.GetState().GetValue();
+
+ if (target == null || !target.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent))
+ {
+ return 0.0f;
+ }
+
+ // Just went with max health
+ return meleeWeaponComponent.Damage / 300.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponEquippedCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponEquippedCon.cs
new file mode 100644
index 0000000000..3bca99db7a
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponEquippedCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects.Components.Weapon.Melee;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Melee
+{
+ public sealed class MeleeWeaponEquippedCon : Consideration
+ {
+ public MeleeWeaponEquippedCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var equipped = context.GetState().GetValue();
+
+ if (equipped == null)
+ {
+ return 0.0f;
+ }
+
+ return equipped.HasComponent() ? 1.0f : 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponSpeedCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponSpeedCon.cs
new file mode 100644
index 0000000000..1c16870804
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Melee/MeleeWeaponSpeedCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Combat;
+using Content.Server.GameObjects.Components.Weapon.Melee;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Melee
+{
+ public sealed class MeleeWeaponSpeedCon : Consideration
+ {
+ public MeleeWeaponSpeedCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var target = context.GetState().GetValue();
+
+ if (target == null || !target.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent))
+ {
+ return 0.0f;
+ }
+
+ return meleeWeaponComponent.CooldownTime / 10.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticAmmoCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticAmmoCon.cs
new file mode 100644
index 0000000000..faa740bad6
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticAmmoCon.cs
@@ -0,0 +1,39 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Combat;
+using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic
+{
+ public class BallisticAmmoCon : Consideration
+ {
+ public BallisticAmmoCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var weapon = context.GetState().GetValue();
+
+ if (weapon == null || !weapon.TryGetComponent(out BallisticMagazineWeaponComponent ballistic))
+ {
+ return 0.0f;
+ }
+
+ var contained = ballistic.MagazineSlot.ContainedEntity;
+
+ if (contained == null)
+ {
+ return 0.0f;
+ }
+
+ var mag = contained.GetComponent();
+
+ if (mag.CountLoaded == 0)
+ {
+ // TODO: Do this better
+ return ballistic.GetChambered(0) != null ? 1.0f : 0.0f;
+ }
+
+ return (float) mag.CountLoaded / mag.Capacity;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticWeaponEquippedCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticWeaponEquippedCon.cs
new file mode 100644
index 0000000000..9e7672feaf
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/BallisticWeaponEquippedCon.cs
@@ -0,0 +1,25 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic
+{
+ public class BallisticWeaponEquippedCon : Consideration
+ {
+ public BallisticWeaponEquippedCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var equipped = context.GetState().GetValue();
+
+ if (equipped == null)
+ {
+ return 0.0f;
+ }
+
+ // Maybe change this to BallisticMagazineWeapon
+ return equipped.HasComponent() ? 1.0f : 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/EquippedBallisticCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/EquippedBallisticCon.cs
new file mode 100644
index 0000000000..d2d289fd3f
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Ballistic/EquippedBallisticCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Ballistic
+{
+ public class EquippedBallisticCon : Consideration
+ {
+ public EquippedBallisticCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var equipped = context.GetState().GetValue();
+
+ if (equipped == null || !equipped.HasComponent())
+ {
+ return 0.0f;
+ }
+
+ return 1.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/HasTargetLosCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/HasTargetLosCon.cs
new file mode 100644
index 0000000000..f27aa41256
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/HasTargetLosCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.Utils;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged
+{
+ public class HasTargetLosCon : Consideration
+ {
+ public HasTargetLosCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+ var target = context.GetState().GetValue();
+ if (target == null)
+ {
+ return 0.0f;
+ }
+
+ return Visibility.InLineOfSight(owner, target) ? 1.0f : 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/HeldRangedWeaponsCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/HeldRangedWeaponsCon.cs
new file mode 100644
index 0000000000..949db84849
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/HeldRangedWeaponsCon.cs
@@ -0,0 +1,29 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects.Components.Weapon.Melee;
+using Content.Server.GameObjects.Components.Weapon.Ranged;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged
+{
+ public sealed class HeldRangedWeaponsCon : Consideration
+ {
+ public HeldRangedWeaponsCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var count = 0;
+ const int max = 3;
+
+ foreach (var item in context.GetState().GetValue())
+ {
+ if (item.HasComponent())
+ {
+ count++;
+ }
+ }
+
+ return (float) count / max;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/EquippedHitscanCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/EquippedHitscanCon.cs
new file mode 100644
index 0000000000..ae5c0b7bf1
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/EquippedHitscanCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
+{
+ public sealed class EquippedHitscanCon : Consideration
+ {
+ public EquippedHitscanCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var equipped = context.GetState().GetValue();
+
+ if (equipped == null || !equipped.HasComponent())
+ {
+ return 0.0f;
+ }
+
+ return 1.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargeCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargeCon.cs
new file mode 100644
index 0000000000..2e24b8c8ca
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargeCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Combat;
+using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
+{
+ public sealed class HitscanChargeCon : Consideration
+ {
+ public HitscanChargeCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var weapon = context.GetState().GetValue();
+
+ if (weapon == null || !weapon.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent))
+ {
+ return 0.0f;
+ }
+
+ return hitscanWeaponComponent.CapacitorComponent.Charge / hitscanWeaponComponent.CapacitorComponent.Capacity;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerFullCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerFullCon.cs
new file mode 100644
index 0000000000..eaad383287
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerFullCon.cs
@@ -0,0 +1,26 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects.Components.Power.Chargers;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
+{
+ public sealed class HitscanChargerFullCon : Consideration
+ {
+ public HitscanChargerFullCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var target = context.GetState().GetValue();
+
+ if (target == null ||
+ !target.TryGetComponent(out WeaponCapacitorChargerComponent chargerComponent) ||
+ chargerComponent.HeldItem != null)
+ {
+ return 1.0f;
+ }
+
+ return 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerRateCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerRateCon.cs
new file mode 100644
index 0000000000..87eb9e332d
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanChargerRateCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects.Components.Power.Chargers;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
+{
+ public sealed class HitscanChargerRateCon : Consideration
+ {
+ public HitscanChargerRateCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var target = context.GetState().GetValue();
+ if (target == null || !target.TryGetComponent(out WeaponCapacitorChargerComponent weaponCharger))
+ {
+ return 0.0f;
+ }
+
+ // AI don't care about efficiency, psfft!
+ return weaponCharger.TransferRatio;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponDamageCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponDamageCon.cs
new file mode 100644
index 0000000000..d3652a4da3
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponDamageCon.cs
@@ -0,0 +1,25 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Combat;
+using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
+{
+ public sealed class HitscanWeaponDamageCon : Consideration
+ {
+ public HitscanWeaponDamageCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var weapon = context.GetState().GetValue();
+
+ if (weapon == null || !weapon.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent))
+ {
+ return 0.0f;
+ }
+
+ // Just went with max health
+ return hitscanWeaponComponent.Damage / 300.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponEquippedCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponEquippedCon.cs
new file mode 100644
index 0000000000..fae397aa5e
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/Hitscan/HitscanWeaponEquippedCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged.Hitscan
+{
+ public sealed class HitscanWeaponEquippedCon : Consideration
+ {
+ public HitscanWeaponEquippedCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var equipped = context.GetState().GetValue();
+
+ if (equipped == null)
+ {
+ return 0.0f;
+ }
+
+ return equipped.HasComponent() ? 1.0f : 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponEquippedCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponEquippedCon.cs
new file mode 100644
index 0000000000..52d2e48117
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponEquippedCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects.Components.Weapon.Ranged;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged
+{
+ public sealed class RangedWeaponEquippedCon : Consideration
+ {
+ public RangedWeaponEquippedCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var equipped = context.GetState().GetValue();
+
+ if (equipped == null || !equipped.HasComponent())
+ {
+ return 0.0f;
+ }
+
+ return 1.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponFireRateCon.cs b/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponFireRateCon.cs
new file mode 100644
index 0000000000..a44e5a9552
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/Ranged/RangedWeaponFireRateCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States.Combat;
+using Content.Server.GameObjects.Components.Weapon.Ranged;
+
+namespace Content.Server.AI.Utility.Considerations.Combat.Ranged
+{
+ public class RangedWeaponFireRateCon : Consideration
+ {
+ public RangedWeaponFireRateCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var weapon = context.GetState().GetValue();
+
+ if (weapon == null || !weapon.TryGetComponent(out RangedWeaponComponent ranged))
+ {
+ return 0.0f;
+ }
+
+ return ranged.FireRate / 100.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/TargetHealthCon.cs b/Content.Server/AI/Utility/Considerations/Combat/TargetHealthCon.cs
new file mode 100644
index 0000000000..5f02a42ac2
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/TargetHealthCon.cs
@@ -0,0 +1,26 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects;
+using Content.Shared.GameObjects;
+
+namespace Content.Server.AI.Utility.Considerations.Combat
+{
+ public sealed class TargetHealthCon : Consideration
+ {
+ public TargetHealthCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var target = context.GetState().GetValue();
+
+ if (target == null || !target.TryGetComponent(out DamageableComponent damageableComponent))
+ {
+ return 0.0f;
+ }
+
+ // Just went with max health
+ return damageableComponent.CurrentDamage[DamageType.Total] / 300.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs b/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs
new file mode 100644
index 0000000000..7528323e9f
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/TargetIsCritCon.cs
@@ -0,0 +1,30 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects;
+using Content.Shared.GameObjects;
+
+namespace Content.Server.AI.Utility.Considerations.Combat
+{
+ public sealed class TargetIsCritCon : Consideration
+ {
+ public TargetIsCritCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var target = context.GetState().GetValue();
+
+ if (target == null || !target.TryGetComponent(out SpeciesComponent speciesComponent))
+ {
+ return 0.0f;
+ }
+
+ if (speciesComponent.CurrentDamageState is CriticalState)
+ {
+ return 1.0f;
+ }
+
+ return 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs b/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs
new file mode 100644
index 0000000000..c3c7ebc80c
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Combat/TargetIsDeadCon.cs
@@ -0,0 +1,29 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects;
+
+namespace Content.Server.AI.Utility.Considerations.Combat
+{
+ public sealed class TargetIsDeadCon : Consideration
+ {
+ public TargetIsDeadCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var target = context.GetState().GetValue();
+
+ if (target == null || !target.TryGetComponent(out SpeciesComponent speciesComponent))
+ {
+ return 0.0f;
+ }
+
+ if (speciesComponent.CurrentDamageState is DeadState)
+ {
+ return 1.0f;
+ }
+
+ return 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Consideration.cs b/Content.Server/AI/Utility/Considerations/Consideration.cs
new file mode 100644
index 0000000000..25694ec5e7
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Consideration.cs
@@ -0,0 +1,25 @@
+using System;
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+
+namespace Content.Server.AI.Utility.Considerations
+{
+ public abstract class Consideration
+ {
+ protected IResponseCurve Curve { get; }
+
+ public Consideration(IResponseCurve curve)
+ {
+ Curve = curve;
+ }
+
+ public abstract float GetScore(Blackboard context);
+
+ public float ComputeResponseCurve(float score)
+ {
+ var clampedScore = Math.Clamp(score, 0.0f, 1.0f);
+ var curvedResponse = Math.Clamp(Curve.GetResponse(clampedScore), 0.0f, 1.0f);
+ return curvedResponse;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Containers/TargetAccessibleCon.cs b/Content.Server/AI/Utility/Considerations/Containers/TargetAccessibleCon.cs
new file mode 100644
index 0000000000..a9af493745
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Containers/TargetAccessibleCon.cs
@@ -0,0 +1,39 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects;
+using Content.Server.GameObjects.Components;
+using Robust.Shared.Containers;
+
+namespace Content.Server.AI.Utility.Considerations.Containers
+{
+ ///
+ /// Returns 1.0f if the item is freely accessible (e.g. in storage we can open, on ground, etc.)
+ ///
+ public sealed class TargetAccessibleCon : Consideration
+ {
+ public TargetAccessibleCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var target = context.GetState().GetValue();
+ if (target == null)
+ {
+ return 0.0f;
+ }
+
+ if (ContainerHelpers.TryGetContainer(target, out var container))
+ {
+ if (container.Owner.TryGetComponent(out EntityStorageComponent storageComponent))
+ {
+ if (storageComponent.IsWeldedShut && !storageComponent.Open)
+ {
+ return 0.0f;
+ }
+ }
+ }
+
+ return 1.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/DummyCon.cs b/Content.Server/AI/Utility/Considerations/DummyCon.cs
new file mode 100644
index 0000000000..07d0587ddb
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/DummyCon.cs
@@ -0,0 +1,12 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+
+namespace Content.Server.AI.Utility.Considerations
+{
+ public class DummyCon : Consideration
+ {
+ public DummyCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context) => 1.0f;
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Hands/FreeHandCon.cs b/Content.Server/AI/Utility/Considerations/Hands/FreeHandCon.cs
new file mode 100644
index 0000000000..2b2162d80f
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Hands/FreeHandCon.cs
@@ -0,0 +1,36 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects;
+
+namespace Content.Server.AI.Utility.Considerations.Hands
+{
+ public class FreeHandCon : Consideration
+ {
+ public FreeHandCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ if (!owner.TryGetComponent(out HandsComponent handsComponent))
+ {
+ return 0.0f;
+ }
+
+ var handCount = 0;
+ var freeCount = 0;
+
+ foreach (var hand in handsComponent.ActivePriorityEnumerable())
+ {
+ handCount++;
+ if (handsComponent.GetHand(hand) == null)
+ {
+ freeCount += 1;
+ }
+ }
+
+ return (float) freeCount / handCount;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Hands/TargetInOurHandsCon.cs b/Content.Server/AI/Utility/Considerations/Hands/TargetInOurHandsCon.cs
new file mode 100644
index 0000000000..cda872a05b
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Hands/TargetInOurHandsCon.cs
@@ -0,0 +1,30 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects;
+
+namespace Content.Server.AI.Utility.Considerations.Hands
+{
+ ///
+ /// Returns 1 if in our hands else 0
+ ///
+ public sealed class TargetInOurHandsCon : Consideration
+ {
+ public TargetInOurHandsCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+ var target = context.GetState().GetValue();
+
+ if (target == null ||
+ !target.HasComponent() ||
+ !owner.TryGetComponent(out HandsComponent handsComponent))
+ {
+ return 0.0f;
+ }
+
+ return handsComponent.IsHolding(target) ? 1.0f : 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Inventory/CanPutTargetInHandsCon.cs b/Content.Server/AI/Utility/Considerations/Inventory/CanPutTargetInHandsCon.cs
new file mode 100644
index 0000000000..646cba8506
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Inventory/CanPutTargetInHandsCon.cs
@@ -0,0 +1,38 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Hands;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects;
+
+namespace Content.Server.AI.Utility.Considerations.Inventory
+{
+ public class CanPutTargetInHandsCon : Consideration
+ {
+ public CanPutTargetInHandsCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ // First check if target in inventory already
+ // If not then check if we have a free hand
+ var target = context.GetState().GetValue();
+
+ if (target == null || !target.HasComponent())
+ {
+ return 0.0f;
+ }
+
+ var inventory = context.GetState().GetValue();
+
+ foreach (var item in inventory)
+ {
+ if (item == target)
+ {
+ return 1.0f;
+ }
+ }
+
+ return context.GetState().GetValue() ? 1.0f : 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Inventory/TargetInOurInventoryCon.cs b/Content.Server/AI/Utility/Considerations/Inventory/TargetInOurInventoryCon.cs
new file mode 100644
index 0000000000..1bd645df23
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Inventory/TargetInOurInventoryCon.cs
@@ -0,0 +1,34 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects;
+
+namespace Content.Server.AI.Utility.Considerations.Inventory
+{
+ public class TargetInOurInventoryCon : Consideration
+ {
+ public TargetInOurInventoryCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var inventory = context.GetState().GetValue();
+ var target = context.GetState().GetValue();
+
+ if (target == null || !target.HasComponent())
+ {
+ return 0.0f;
+ }
+
+ foreach (var item in inventory)
+ {
+ if (item == target)
+ {
+ return 1.0f;
+ }
+ }
+
+ return 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Movement/DistanceCon.cs b/Content.Server/AI/Utility/Considerations/Movement/DistanceCon.cs
new file mode 100644
index 0000000000..3ecb715c8a
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Movement/DistanceCon.cs
@@ -0,0 +1,25 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+
+namespace Content.Server.AI.Utility.Considerations.Movement
+{
+ public sealed class DistanceCon : Consideration
+ {
+ public DistanceCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var self = context.GetState().GetValue();
+ var target = context.GetState().GetValue();
+ if (target == null || target.Transform.GridID != self.Transform.GridID)
+ {
+ return 0.0f;
+ }
+
+ // TODO: Remove 1 -
+ // Kind of just pulled a max distance out of nowhere. Add 0.01 just in case it's reaally far and we have no choice so it'll still be considered at least.
+ return 1 - ((target.Transform.GridPosition.Position - self.Transform.GridPosition.Position).Length / 100 + 0.01f);
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Nutrition/Drink/DrinkValueCon.cs b/Content.Server/AI/Utility/Considerations/Nutrition/Drink/DrinkValueCon.cs
new file mode 100644
index 0000000000..73e6505e7e
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Nutrition/Drink/DrinkValueCon.cs
@@ -0,0 +1,32 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects.Components.Chemistry;
+
+namespace Content.Server.AI.Utility.Considerations.Nutrition.Drink
+{
+ public sealed class DrinkValueCon : Consideration
+ {
+ public DrinkValueCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var target = context.GetState().GetValue();
+
+ if (!target.TryGetComponent(out SolutionComponent drink))
+ {
+ return 0.0f;
+ }
+
+ var nutritionValue = 0;
+
+ foreach (var reagent in drink.ReagentList)
+ {
+ // TODO
+ nutritionValue += (reagent.Quantity * 30).Int();
+ }
+
+ return nutritionValue / 1000.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Nutrition/Drink/ThirstCon.cs b/Content.Server/AI/Utility/Considerations/Nutrition/Drink/ThirstCon.cs
new file mode 100644
index 0000000000..02e8719d2a
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Nutrition/Drink/ThirstCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects.Components.Nutrition;
+
+namespace Content.Server.AI.Utility.Considerations.Nutrition.Drink
+{
+ public class ThirstCon : Consideration
+ {
+ public ThirstCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ if (!owner.TryGetComponent(out ThirstComponent thirst))
+ {
+ return 0.0f;
+ }
+
+ return 1 - (thirst.CurrentThirst / thirst.ThirstThresholds[ThirstThreshold.OverHydrated]);
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Nutrition/Food/FoodValueCon.cs b/Content.Server/AI/Utility/Considerations/Nutrition/Food/FoodValueCon.cs
new file mode 100644
index 0000000000..a316799c3d
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Nutrition/Food/FoodValueCon.cs
@@ -0,0 +1,32 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects.Components.Chemistry;
+
+namespace Content.Server.AI.Utility.Considerations.Nutrition
+{
+ public sealed class FoodValueCon : Consideration
+ {
+ public FoodValueCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var target = context.GetState().GetValue();
+
+ if (!target.TryGetComponent(out SolutionComponent food))
+ {
+ return 0.0f;
+ }
+
+ var nutritionValue = 0;
+
+ foreach (var reagent in food.ReagentList)
+ {
+ // TODO
+ nutritionValue += (reagent.Quantity * 30).Int();
+ }
+
+ return nutritionValue / 1000.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/Nutrition/Food/HungerCon.cs b/Content.Server/AI/Utility/Considerations/Nutrition/Food/HungerCon.cs
new file mode 100644
index 0000000000..170e7d22b8
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/Nutrition/Food/HungerCon.cs
@@ -0,0 +1,25 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects.Components.Nutrition;
+
+namespace Content.Server.AI.Utility.Considerations.Nutrition
+{
+
+ public sealed class HungerCon : Consideration
+ {
+ public HungerCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ if (!owner.TryGetComponent(out HungerComponent hunger))
+ {
+ return 0.0f;
+ }
+
+ return 1 - (hunger.CurrentHunger / hunger.HungerThresholds[HungerThreshold.Overfed]);
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Considerations/State/StoredStateIsNullCon.cs b/Content.Server/AI/Utility/Considerations/State/StoredStateIsNullCon.cs
new file mode 100644
index 0000000000..b93757bf9e
--- /dev/null
+++ b/Content.Server/AI/Utility/Considerations/State/StoredStateIsNullCon.cs
@@ -0,0 +1,24 @@
+using Content.Server.AI.Utility.Curves;
+using Content.Server.AI.WorldState;
+
+namespace Content.Server.AI.Utility.Considerations.State
+{
+ ///
+ /// Simple NullCheck on a StoredState
+ ///
+ public sealed class StoredStateIsNullCon : Consideration where T : StoredStateData
+ {
+ public StoredStateIsNullCon(IResponseCurve curve) : base(curve) {}
+
+ public override float GetScore(Blackboard context)
+ {
+ var state = context.GetState();
+ if (state.GetValue() == null)
+ {
+ return 1.0f;
+ }
+
+ return 0.0f;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Content.Server/AI/Utility/Curves/BoolCurve.cs b/Content.Server/AI/Utility/Curves/BoolCurve.cs
new file mode 100644
index 0000000000..d7e86c3c8f
--- /dev/null
+++ b/Content.Server/AI/Utility/Curves/BoolCurve.cs
@@ -0,0 +1,13 @@
+namespace Content.Server.AI.Utility.Curves
+{
+ ///
+ /// For stuff that's a simple 0.0f or 1.0f
+ ///
+ public struct BoolCurve : IResponseCurve
+ {
+ public float GetResponse(float score)
+ {
+ return score > 0.0f ? 1.0f : 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Curves/IResponseCurve.cs b/Content.Server/AI/Utility/Curves/IResponseCurve.cs
new file mode 100644
index 0000000000..a2a78c839f
--- /dev/null
+++ b/Content.Server/AI/Utility/Curves/IResponseCurve.cs
@@ -0,0 +1,10 @@
+namespace Content.Server.AI.Utility.Curves
+{
+ ///
+ /// Using an interface also lets us define preset curves that can be re-used
+ ///
+ public interface IResponseCurve
+ {
+ float GetResponse(float score);
+ }
+}
diff --git a/Content.Server/AI/Utility/Curves/InverseBoolCurve.cs b/Content.Server/AI/Utility/Curves/InverseBoolCurve.cs
new file mode 100644
index 0000000000..4296ffee5b
--- /dev/null
+++ b/Content.Server/AI/Utility/Curves/InverseBoolCurve.cs
@@ -0,0 +1,11 @@
+namespace Content.Server.AI.Utility.Curves
+{
+ public struct InverseBoolCurve : IResponseCurve
+ {
+ public float GetResponse(float score)
+ {
+ // ReSharper disable once CompareOfFloatsByEqualityOperator
+ return score == 0.0f ? 1.0f : 0.0f;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Curves/LogisticCurve.cs b/Content.Server/AI/Utility/Curves/LogisticCurve.cs
new file mode 100644
index 0000000000..fb21e2a550
--- /dev/null
+++ b/Content.Server/AI/Utility/Curves/LogisticCurve.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace Content.Server.AI.Utility.Curves
+{
+ public struct LogisticCurve : IResponseCurve
+ {
+ private readonly float _slope;
+
+ private readonly float _exponent;
+ // Vertical shift
+ private readonly float _yOffset;
+ // Horizontal shift
+ private readonly float _xOffset;
+
+ public LogisticCurve(float slope, float exponent, float yOffset, float xOffset)
+ {
+ _slope = slope;
+ _exponent = exponent;
+ _yOffset = yOffset;
+ _xOffset = xOffset;
+ }
+
+ public float GetResponse(float score)
+ {
+ return _exponent * (1 / (1 + (float) Math.Pow(Math.Log(1000) * _slope, -1 * score + _xOffset))) + _yOffset;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/Curves/QuadraticCurve.cs b/Content.Server/AI/Utility/Curves/QuadraticCurve.cs
new file mode 100644
index 0000000000..5933915aeb
--- /dev/null
+++ b/Content.Server/AI/Utility/Curves/QuadraticCurve.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace Content.Server.AI.Utility.Curves
+{
+ ///
+ /// Also Linear
+ ///
+ public struct QuadraticCurve : IResponseCurve
+ {
+ private readonly float _slope;
+
+ private readonly float _exponent;
+ // Vertical shift
+ private readonly float _yOffset;
+ // Horizontal shift
+ private readonly float _xOffset;
+
+ public QuadraticCurve(float slope, float exponent, float yOffset, float xOffset)
+ {
+ _slope = slope;
+ _exponent = exponent;
+ _yOffset = yOffset;
+ _xOffset = xOffset;
+ }
+
+ public float GetResponse(float score)
+ {
+ return _slope * (float) Math.Pow(score - _xOffset, _exponent) + _yOffset;
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs
new file mode 100644
index 0000000000..700dff2f94
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/EquipAnyGlovesExp.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Clothing.Gloves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects;
+using Content.Server.GameObjects.Components.Movement;
+using Content.Shared.GameObjects.Components.Inventory;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves
+{
+ ///
+ /// Equip any head item currently in our inventory
+ ///
+ public sealed class EquipAnyGlovesExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.NormalBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+ if (!owner.TryGetComponent(out AiControllerComponent controller))
+ {
+ throw new InvalidOperationException();
+ }
+
+ foreach (var entity in context.GetState().GetValue())
+ {
+ if (entity.TryGetComponent(out ClothingComponent clothing) &&
+ (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.GLOVES) != 0)
+ {
+ yield return new EquipGloves(owner, entity, Bonus);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs
new file mode 100644
index 0000000000..7b3f165e4a
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Gloves/PickUpAnyNearbyGlovesExp.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Clothing.Gloves;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Clothing;
+using Content.Server.GameObjects;
+using Content.Shared.GameObjects.Components.Inventory;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Gloves
+{
+ public sealed class PickUpAnyNearbyGlovesExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.NormalBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+ foreach (var entity in context.GetState().GetValue())
+ {
+ if (entity.TryGetComponent(out ClothingComponent clothing) &&
+ (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.GLOVES) != 0)
+ {
+ yield return new PickUpGloves(owner, entity, Bonus);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs
new file mode 100644
index 0000000000..f885e7818a
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/EquipAnyHeadExp.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Clothing.Head;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects;
+using Content.Server.GameObjects.Components.Movement;
+using Content.Shared.GameObjects.Components.Inventory;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head
+{
+ ///
+ /// Equip any head item currently in our inventory
+ ///
+ public sealed class EquipAnyHeadExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.NormalBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ foreach (var entity in context.GetState().GetValue())
+ {
+ if (entity.TryGetComponent(out ClothingComponent clothing) &&
+ (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.HEAD) != 0)
+ {
+ yield return new EquipHead(owner, entity, Bonus);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs
new file mode 100644
index 0000000000..dfb2f6d2a9
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Head/PickUpAnyNearbyHeadExp.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Clothing.Head;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Clothing;
+using Content.Server.GameObjects;
+using Content.Shared.GameObjects.Components.Inventory;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Head
+{
+ public sealed class PickUpAnyNearbyHeadExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.NormalBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+ foreach (var entity in context.GetState().GetValue())
+ {
+ if (entity.TryGetComponent(out ClothingComponent clothing) &&
+ (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.HEAD) != 0)
+ {
+ yield return new PickUpHead(owner, entity, Bonus);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs
new file mode 100644
index 0000000000..b7a7980974
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/EquipAnyOuterClothingExp.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Clothing.OuterClothing;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects;
+using Content.Shared.GameObjects.Components.Inventory;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing
+{
+ ///
+ /// Equip any head item currently in our inventory
+ ///
+ public sealed class EquipAnyOuterClothingExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.NormalBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ foreach (var entity in context.GetState().GetValue())
+ {
+ if (entity.TryGetComponent(out ClothingComponent clothing) &&
+ (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.OUTERCLOTHING) != 0)
+ {
+ yield return new EquipOuterClothing(owner, entity, Bonus);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs
new file mode 100644
index 0000000000..dd1d06c0c7
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/OuterClothing/PickUpAnyNearbyOuterClothingExp.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Clothing.OuterClothing;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Clothing;
+using Content.Server.GameObjects;
+using Content.Shared.GameObjects.Components.Inventory;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Clothing.OuterClothing
+{
+ public sealed class PickUpAnyNearbyOuterClothingExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.NormalBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ foreach (var entity in context.GetState().GetValue())
+ {
+ if (entity.TryGetComponent(out ClothingComponent clothing) &&
+ (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.OUTERCLOTHING) != 0)
+ {
+ yield return new PickUpOuterClothing(owner, entity, Bonus);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs
new file mode 100644
index 0000000000..517fd735c0
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/EquipAnyShoesExp.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Clothing.Shoes;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects;
+using Content.Shared.GameObjects.Components.Inventory;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes
+{
+ ///
+ /// Equip any head item currently in our inventory
+ ///
+ public sealed class EquipAnyShoesExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.NormalBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ foreach (var entity in context.GetState().GetValue())
+ {
+ if (entity.TryGetComponent(out ClothingComponent clothing) &&
+ (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.SHOES) != 0)
+ {
+ yield return new EquipShoes(owner, entity, Bonus);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs
new file mode 100644
index 0000000000..598d180808
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Clothing/Shoes/PickUpAnyNearbyShoesExp.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Clothing.Shoes;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Clothing;
+using Content.Server.GameObjects;
+using Content.Shared.GameObjects.Components.Inventory;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Clothing.Shoes
+{
+ public sealed class PickUpAnyNearbyShoesExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.NormalBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ foreach (var entity in context.GetState().GetValue())
+ {
+ if (entity.TryGetComponent(out ClothingComponent clothing) &&
+ (clothing.SlotFlags & EquipmentSlotDefines.SlotFlags.SHOES) != 0)
+ {
+ yield return new PickUpShoes(owner, entity, Bonus);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/EquipMeleeExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/EquipMeleeExp.cs
new file mode 100644
index 0000000000..67bfac1c98
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/EquipMeleeExp.cs
@@ -0,0 +1,24 @@
+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.Inventory;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
+{
+ public sealed class EquipMeleeExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.CombatPrepBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ foreach (var entity in context.GetState().GetValue())
+ {
+ yield return new EquipMelee(owner, entity, Bonus);
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs
new file mode 100644
index 0000000000..e70dba6c86
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbyPlayerExp.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Combat.Melee;
+using Content.Server.AI.Utils;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.GameObjects;
+using Content.Server.GameObjects.Components.Movement;
+using Robust.Server.GameObjects;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
+{
+ public sealed class MeleeAttackNearbyPlayerExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.CombatBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+ if (!owner.TryGetComponent(out AiControllerComponent controller))
+ {
+ throw new InvalidOperationException();
+ }
+
+ foreach (var entity in Visibility.GetEntitiesInRange(owner.Transform.GridPosition, typeof(SpeciesComponent),
+ controller.VisionRadius))
+ {
+ if (entity.HasComponent() && entity != owner)
+ {
+ yield return new MeleeAttackEntity(owner, entity, Bonus);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs
new file mode 100644
index 0000000000..ad00223233
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/MeleeAttackNearbySpeciesExp.cs
@@ -0,0 +1,23 @@
+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 MeleeAttackEntity(owner, entity, Bonus);
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/PickUpMeleeWeaponExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/PickUpMeleeWeaponExp.cs
new file mode 100644
index 0000000000..781a72c2a1
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Melee/PickUpMeleeWeaponExp.cs
@@ -0,0 +1,24 @@
+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.Combat.Nearby;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Combat.Melee
+{
+ public sealed class PickUpMeleeWeaponExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.CombatPrepBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ foreach (var entity in context.GetState().GetValue())
+ {
+ yield return new PickUpMeleeWeapon(owner, entity, Bonus);
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/DropEmptyBallisticExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/DropEmptyBallisticExp.cs
new file mode 100644
index 0000000000..db3e281905
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/DropEmptyBallisticExp.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Inventory;
+using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic
+{
+ public sealed class DropEmptyBallisticExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.CombatPrepBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ foreach (var entity in context.GetState().GetValue())
+ {
+ if (entity.HasComponent())
+ {
+ yield return new DropEmptyBallistic(owner, entity, Bonus);
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/EquipBallisticExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/EquipBallisticExp.cs
new file mode 100644
index 0000000000..16e98819d9
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/EquipBallisticExp.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic;
+using Content.Server.AI.WorldState;
+using Content.Server.AI.WorldState.States;
+using Content.Server.AI.WorldState.States.Inventory;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic
+{
+ public sealed class EquipBallisticExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.CombatPrepBonus;
+
+ public override IEnumerable GetActions(Blackboard context)
+ {
+ var owner = context.GetState().GetValue();
+
+ foreach (var entity in context.GetState().GetValue())
+ {
+ yield return new EquipBallistic(owner, entity, Bonus);
+ }
+ }
+ }
+}
diff --git a/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/PickUpAmmoExp.cs b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/PickUpAmmoExp.cs
new file mode 100644
index 0000000000..cc770a057d
--- /dev/null
+++ b/Content.Server/AI/Utility/ExpandableActions/Combat/Ranged/Ballistic/PickUpAmmoExp.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using Content.Server.AI.Utility.Actions;
+using Content.Server.AI.Utility.Actions.Combat.Ranged.Ballistic;
+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.Components.Weapon.Ranged.Projectile;
+
+namespace Content.Server.AI.Utility.ExpandableActions.Combat.Ranged.Ballistic
+{
+ public sealed class PickUpAmmoExp : ExpandableUtilityAction
+ {
+ public override float Bonus => UtilityAction.CombatPrepBonus;
+
+ public override IEnumerable