Add utility AI (#806)

Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
Co-authored-by: Metal Gear Sloth <metalgearsloth@gmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
This commit is contained in:
metalgearsloth
2020-06-18 22:52:44 +10:00
committed by GitHub
parent 9b8cedf6c6
commit 5391d3c72a
211 changed files with 10335 additions and 527 deletions

View File

@@ -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
{
/// <summary>
/// This is used to handle the tooltips above AI mobs
/// </summary>
[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<ClientAiDebugSystem>();
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;
}
}
}

View File

@@ -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<ClientPathfindingDebugSystem>();
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;
}
}
}

View File

@@ -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<IEntity, PanelContainer> _aiBoxes = new Dictionary<IEntity,PanelContainer>();
public override void Update(float frameTime)
{
base.Update(frameTime);
if (_tooltips == 0)
{
return;
}
var eyeManager = IoCManager.Resolve<IEyeManager>();
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<SharedAiDebug.UtilityAiDebugMessage>(HandleUtilityAiDebugMessage);
SubscribeNetworkEvent<SharedAiDebug.AStarRouteMessage>(HandleAStarRouteMessage);
SubscribeNetworkEvent<SharedAiDebug.JpsRouteMessage>(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<IEntityManager>();
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<IEntityManager>();
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<IEntityManager>();
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<IUserInterfaceManager>();
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
}

View File

@@ -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<SharedAiDebug.AStarRouteMessage>(HandleAStarRouteMessage);
SubscribeNetworkEvent<SharedAiDebug.JpsRouteMessage>(HandleJpsRouteMessage);
SubscribeNetworkEvent<SharedAiDebug.PathfindingGraphMessage>(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<IOverlayManager>();
_overlay = new DebugPathfindingOverlay {Modes = _modes};
overlayManager.AddOverlay(_overlay);
}
private void DisableOverlay()
{
if (_overlay == null)
{
return;
}
_overlay.Modes = 0;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
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<int, List<Vector2>> Graph = new Dictionary<int, List<Vector2>>();
private readonly Dictionary<int, Color> _graphColors = new Dictionary<int, Color>();
// Route debugging
// As each pathfinder is very different you'll likely want to draw them completely different
public readonly List<SharedAiDebug.AStarRouteMessage> AStarRoutes = new List<SharedAiDebug.AStarRouteMessage>();
public readonly List<SharedAiDebug.JpsRouteMessage> JpsRoutes = new List<SharedAiDebug.JpsRouteMessage>();
public DebugPathfindingOverlay() : base(nameof(DebugPathfindingOverlay))
{
Shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
}
public void UpdateGraph(Dictionary<int, List<Vector2>> graph)
{
Graph.Clear();
_graphColors.Clear();
var robustRandom = IoCManager.Resolve<IRobustRandom>();
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<IEyeManager>();
var viewport = IoCManager.Resolve<IEyeManager>().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<IEyeManager>();
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<IEyeManager>();
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<IEyeManager>();
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<IEyeManager>();
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
}

View File

@@ -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
{
/// <summary>
/// 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.
/// </summary>
[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<IEntity> _workList = new List<IEntity>();
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;
/// <inheritdoc />
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<ITransformComponent>().WorldPosition;
var selfTransform = SelfEntity.GetComponent<ITransformComponent>();
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<ITransformComponent>();
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<IMoverComponent>())
continue;
// build the ray
var dir = entity.GetComponent<ITransformComponent>().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<IEntity> list)
{
IEntity closest = null;
var minDistSqrd = float.PositiveInfinity;
foreach (var ent in list)
{
var pos = ent.GetComponent<ITransformComponent>().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;
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
namespace Content.Server.AI.Operators
{
public abstract class AiOperator
{
private bool _hasStartup = false;
private bool _hasShutdown = false;
/// <summary>
/// Called once when the AiLogicProcessor starts this action
/// </summary>
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;
}
/// <summary>
/// Called once when the AiLogicProcessor is done with this action if the outcome is successful or fails.
/// </summary>
public virtual void Shutdown(Outcome outcome)
{
if (_hasShutdown)
{
throw new InvalidOperationException("AiOperator has already shutdown");
}
_hasShutdown = true;
}
/// <summary>
/// Called every tick for the AI
/// </summary>
/// <param name="frameTime"></param>
/// <returns></returns>
public abstract Outcome Execute(float frameTime);
}
public enum Outcome
{
Success,
Continuing,
Failed,
}
}

View File

@@ -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<AiControllerComponent>().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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.UseItemInHand(_owner, _target.Transform.GridPosition, _target.Uid);
_elapsedTime += frameTime;
return Outcome.Continuing;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Close the last EntityStorage we opened
/// This will also update the State for it (which a regular InteractWith won't do)
/// </summary>
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<LastOpenedStorageState>().GetValue();
return _target != null;
}
public override void Shutdown(Outcome outcome)
{
base.Shutdown(outcome);
var blackboard = UtilityAiHelpers.GetBlackboard(_owner);
blackboard?.GetState<LastOpenedStorageState>().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;
}
}
}

View File

@@ -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;
}
/// <summary>
/// Requires EquipEntityOperator to put it in the active hand first
/// </summary>
/// <param name="frameTime"></param>
/// <returns></returns>
public override Outcome Execute(float frameTime)
{
if (!_owner.TryGetComponent(out HandsComponent handsComponent))
{
return Outcome.Failed;
}
return handsComponent.Drop(_entity) ? Outcome.Success : Outcome.Failed;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
{
/// <summary>
/// A Generic interacter; if you need to check stuff then make your own
/// </summary>
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<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.UseItemInHand(_owner, _useTarget.Transform.GridPosition, _useTarget.Uid);
return Outcome.Success;
}
}
}

View File

@@ -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
{
/// <summary>
/// If the target is in EntityStorage will open its parent container
/// </summary>
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<LastOpenedStorageState>().SetValue(container.Owner);
return Outcome.Success;
}
}
}

View File

@@ -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<ItemComponent>() ||
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<IEntitySystemManager>().GetEntitySystem<InteractionSystem>();
interactionSystem.Interaction(_owner, _target);
return Outcome.Success;
}
}
}

View File

@@ -0,0 +1,49 @@
using Content.Server.GameObjects;
using Robust.Shared.Interfaces.GameObjects;
namespace Content.Server.AI.Operators.Inventory
{
/// <summary>
/// Will find the item in storage, put it in an active hand, then use it
/// </summary>
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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Invoked every time we move across a tile
/// </summary>
public event Action MovedATile;
/// <summary>
/// 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.
///
/// </summary>
public float PathfindingProximity { get; set; } = 1.42f;
protected Queue<TileRef> Route = new Queue<TileRef>();
/// <summary>
/// The final spot we're trying to get to
/// </summary>
protected GridCoordinates TargetGrid;
/// <summary>
/// As the pathfinder is tilebased we'll move to each tile's grid.
/// </summary>
protected GridCoordinates NextGrid;
private const float TileTolerance = 0.2f;
// Stuck checkers
/// <summary>
/// How long we're stuck in general before trying to unstuck
/// </summary>
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<Queue<TileRef>> RouteJob;
private IMapManager _mapManager;
private PathfindingSystem _pathfinder;
private AiControllerComponent _controller;
// Input
protected IEntity Owner;
protected void Setup(IEntity owner)
{
Owner = owner;
_mapManager = IoCManager.Resolve<IMapManager>();
_pathfinder = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<PathfindingSystem>();
if (!Owner.TryGetComponent(out AiControllerComponent controllerComponent))
{
throw new InvalidOperationException();
}
_controller = controllerComponent;
}
protected void NextTile()
{
MovedATile?.Invoke();
}
/// <summary>
/// Will move the AI towards the next position
/// </summary>
/// <returns>true if movement to be done</returns>
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;
}
/// <summary>
/// Will try and get around obstacles if stuck
/// </summary>
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<IRobustRandom>().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<IRobustRandom>();
_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;
}
/// <summary>
/// Tells us we don't need to keep moving and resets everything
/// </summary>
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<TileRef>();
// 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
}
}

View File

@@ -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<IMapManager>();
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<TileRef>(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;
}
}
}

View File

@@ -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<IMapManager>();
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;
}
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new MoveToEntityOperator(owner, target),
new OpenStorageOperator(owner, target),
new PickupEntityOperator(owner, target),
});
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
namespace Content.Server.AI.Operators.Sequences
{
/// <summary>
/// Sequential chain of operators
/// Saves having to duplicate stuff like MoveTo and PickUp everywhere
/// </summary>
public abstract class SequenceOperator : AiOperator
{
public Queue<AiOperator> 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();
}
}
}
}

View File

@@ -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
{
/// <summary>
/// Designed for a a stationary entity that regularly advertises things (vending machine).
/// </summary>
[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<string> slogans = new List<string>
{
"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;
}
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, _entity),
new UseItemInHandsOperator(Owner, _entity),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity);
}
protected override Consideration[] Considerations { get; } = {
new ClothingInSlotCon(EquipmentSlotDefines.Slots.GLOVES,
new InverseBoolCurve()),
new CanPutTargetInHandsCon(
new BoolCurve()),
};
}
}

View File

@@ -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<TargetEntityState>().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()),
};
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, _entity),
new UseItemInHandsOperator(Owner, _entity),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity);
}
protected override Consideration[] Considerations { get; } = {
new ClothingInSlotCon(EquipmentSlotDefines.Slots.HEAD,
new InverseBoolCurve()),
new CanPutTargetInHandsCon(
new BoolCurve()),
};
}
}

View File

@@ -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<TargetEntityState>().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()),
};
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, _entity),
new UseItemInHandsOperator(Owner, _entity),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity);
}
protected override Consideration[] Considerations { get; } = {
new ClothingInSlotCon(EquipmentSlotDefines.Slots.OUTERCLOTHING,
new InverseBoolCurve()),
new CanPutTargetInHandsCon(
new BoolCurve()),
};
}
}

View File

@@ -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<TargetEntityState>().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()),
};
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, _entity),
new UseItemInHandsOperator(Owner, _entity),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity);
}
protected override Consideration[] Considerations { get; } = {
new ClothingInSlotCon(EquipmentSlotDefines.Slots.SHOES,
new InverseBoolCurve()),
new CanPutTargetInHandsCon(
new BoolCurve()),
};
}
}

View File

@@ -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<TargetEntityState>().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()),
};
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, _entity)
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<WeaponEntityState>().SetValue(_entity);
context.GetState<TargetEntityState>().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)),
};
}
}

View File

@@ -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<EquippedEntityState>().GetValue();
if (equipped != null && equipped.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent))
{
moveOperator.DesiredRange = meleeWeaponComponent.Range - 0.01f;
}
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
moveOperator,
new SwingMeleeWeaponOperator(Owner, _entity),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity);
context.GetState<MoveTargetState>().SetValue(_entity);
var equipped = context.GetState<EquippedEntityState>().GetValue();
context.GetState<WeaponEntityState>().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)),
};
}
}

View File

@@ -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<TargetEntityState>().SetValue(_entity);
context.GetState<WeaponEntityState>().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)),
};
}
}

View File

@@ -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<AiOperator>(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<TargetEntityState>().SetValue(_entity);
context.GetState<MoveTargetState>().SetValue(_entity);
var equipped = context.GetState<EquippedEntityState>().GetValue();
context.GetState<WeaponEntityState>().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);
}
}
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, _entity),
new DropEntityOperator(Owner, _entity)
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity);
context.GetState<WeaponEntityState>().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()),
};
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, _entity)
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<WeaponEntityState>().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)),
};
}
}

View File

@@ -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<MoveTargetState>().SetValue(_entity);
context.GetState<TargetEntityState>().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)),
};
}
}

View File

@@ -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<MoveTargetState>().SetValue(_entity);
context.GetState<TargetEntityState>().SetValue(_entity);
context.GetState<WeaponEntityState>().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
};
}
}

View File

@@ -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<EquippedEntityState>().GetValue();
if (weapon == null || _charger.GetComponent<WeaponCapacitorChargerComponent>().HeldItem != null)
{
ActionOperators = new Queue<AiOperator>();
return;
}
ActionOperators = new Queue<AiOperator>(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<MoveTargetState>().SetValue(_charger);
context.GetState<TargetEntityState>().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)),
};
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, _entity),
new DropEntityOperator(Owner, _entity)
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity);
context.GetState<WeaponEntityState>().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()),
};
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new EquipEntityOperator(Owner, _entity)
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<WeaponEntityState>().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)),
};
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
_moveOperator,
new ShootAtEntityOperator(Owner, _entity, 0.7f),
});
InLos();
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<TargetEntityState>().SetValue(_entity);
context.GetState<MoveTargetState>().SetValue(_entity);
var equipped = context.GetState<EquippedEntityState>().GetValue();
context.GetState<WeaponEntityState>().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);
}
}
}
}

View File

@@ -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<AiOperator>(new AiOperator[]
{
new MoveToEntityOperator(Owner, _charger),
new WaitForHitscanChargeOperator(_entity),
new PickupEntityOperator(Owner, _entity),
});
}
protected override void UpdateBlackboard(Blackboard context)
{
base.UpdateBlackboard(context);
context.GetState<MoveTargetState>().SetValue(_entity);
context.GetState<TargetEntityState>().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)),
};
}
}

View File

@@ -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<MoveTargetState>().SetValue(_entity);
context.GetState<TargetEntityState>().SetValue(_entity);
context.GetState<WeaponEntityState>().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)),
};
}
}

View File

@@ -0,0 +1,9 @@
using Content.Server.AI.Utility.AiLogic;
namespace Content.Server.AI.Utility.Actions
{
public interface IAiUtility
{
float Bonus { get; }
}
}

View File

@@ -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
{
/// <summary>
/// If we just picked up a bunch of stuff and have time then close it
/// </summary>
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<LastOpenedStorageState, IEntity>(
new InverseBoolCurve()),
new DistanceCon(
new QuadraticCurve(1.0f, 1.0f, 0.02f, 0.0f)),
};
public override void SetupOperators(Blackboard context)
{
var lastStorage = context.GetState<LastOpenedStorageState>().GetValue();
ActionOperators = new Queue<AiOperator>(new AiOperator[]
{
new MoveToEntityOperator(Owner, lastStorage),
new CloseLastStorageOperator(Owner),
});
}
}
}

View File

@@ -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
{
/// <summary>
/// Will move to a random spot close by
/// </summary>
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<IRobustRandom>();
waitTime = random.NextFloat() * 10;
}
else
{
waitTime = 0.0f;
}
ActionOperators = new Queue<AiOperator>(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<IMapManager>();
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<IRobustRandom>();
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;
}
}
}

View File

@@ -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<TargetEntityState>().SetValue(_entity);
}
}
}

View File

@@ -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<AiOperator>(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<TargetEntityState>().SetValue(_entity);
}
}
}

View File

@@ -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<TargetEntityState>().SetValue(_entity);
}
}
}

View File

@@ -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<AiOperator>(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<TargetEntityState>().SetValue(_entity);
}
}
}

View File

@@ -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
{
/// <summary>
/// Used for pathfinding debugging
/// </summary>
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<AiOperator>(new AiOperator[]
{
newPosOp,
originalPosOp
});
}
}
}

View File

@@ -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
{
/// <summary>
/// The same DSE can be used across multiple actions.
/// </summary>
public abstract class UtilityAction : IAiUtility
{
/// <summary>
/// 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.
/// </summary>
public virtual bool CanOverride => false;
/// <summary>
/// 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
/// </summary>
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; }
/// <summary>
/// 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.
/// </summary>
protected abstract Consideration[] Considerations { get; }
/// <summary>
/// To keep the operators simple we can chain them together here, e.g. move to can be chained with other operators.
/// </summary>
public Queue<AiOperator> ActionOperators { get; protected set; }
/// <summary>
/// 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.
/// </summary>
/// <param name="context"></param>
protected virtual void UpdateBlackboard(Blackboard context) {}
protected UtilityAction(IEntity owner)
{
Owner = owner;
}
public virtual void Shutdown() {}
/// <summary>
/// If this action is chosen then setup the operators to run. This also allows for operators to be reset.
/// </summary>
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;
}
/// <summary>
/// AKA the Decision Score Evaluator (DSE)
/// This is where the magic happens
/// </summary>
/// <param name="context"></param>
/// <param name="bonus"></param>
/// <param name="min"></param>
/// <returns></returns>
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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
/// <summary>
/// The sum of all BehaviorSets gives us what actions the AI can take
/// </summary>
public Dictionary<Type, BehaviorSet> BehaviorSets { get; } = new Dictionary<Type, BehaviorSet>();
private readonly List<IAiUtility> _availableActions = new List<IAiUtility>();
/// <summary>
/// The currently running action; most importantly are the operators.
/// </summary>
public UtilityAction CurrentAction { get; private set; }
/// <summary>
/// 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.
/// </summary>
public float PlanCooldown { get; } = 0.5f;
private float _planCooldownRemaining;
/// <summary>
/// If we've requested a plan then wait patiently for the action
/// </summary>
private AiActionRequestJob _actionRequest;
private CancellationTokenSource _actionCancellation;
/// <summary>
/// If we can't do anything then stop thinking; should probably use ActionBlocker instead
/// </summary>
private bool _isDead = false;
// These 2 methods will be used eventually if / when we get a director AI
public void AddBehaviorSet<T>(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();
}
}
/// <summary>
/// Whenever the behavior sets are changed we'll re-sort the actions by bonus
/// </summary>
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<IEntitySystemManager>().GetEntitySystem<AiActionSystem>();
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<LastUtilityScoreState>().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<LastUtilityScoreState>().SetValue(0.0f);
}
break;
case Outcome.Continuing:
break;
case Outcome.Failed:
CurrentAction.Shutdown();
CurrentAction = null;
_blackboard.GetState<LastUtilityScoreState>().SetValue(0.0f);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@@ -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
{
/// <summary>
/// AKA DecisionMaker in IAUS. Just a group of actions that can be dynamically added or taken away from an AI.
/// </summary>
public abstract class BehaviorSet
{
protected IEntity Owner;
public BehaviorSet(IEntity owner)
{
Owner = owner;
}
public IEnumerable<IAiUtility> Actions { get; protected set; }
}
}

View File

@@ -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(),
};
}
}
}

View File

@@ -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(),
};
}
}
}

View File

@@ -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),
};
}
}
}

View File

@@ -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),
};
}
}
}

View File

@@ -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(),
};
}
}
}

View File

@@ -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(),
};
}
}
}

View File

@@ -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<SelfState>().GetValue();
if (!ActionBlockerSystem.CanMove(self))
{
return 0.0f;
}
return 1.0f;
}
}
}

View File

@@ -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<InventoryState>().GetValue();
foreach (var entity in inventory)
{
if (!entity.TryGetComponent(out ClothingComponent clothingComponent))
{
continue;
}
if ((clothingComponent.SlotFlags & _slot) != 0)
{
return 1.0f;
}
}
return 0.0f;
}
}
}

View File

@@ -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<EquippedClothingState>().GetValue();
return inventory.ContainsKey(_slot) ? 1.0f : 0.0f;
}
}
}

View File

@@ -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<InventoryState>().GetValue())
{
if (item.HasComponent<MeleeWeaponComponent>())
{
return 1.0f;
}
}
return 0.0f;
}
}
}

View File

@@ -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<WeaponEntityState>().GetValue();
if (target == null || !target.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent))
{
return 0.0f;
}
// Just went with max health
return meleeWeaponComponent.Damage / 300.0f;
}
}
}

View File

@@ -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<EquippedEntityState>().GetValue();
if (equipped == null)
{
return 0.0f;
}
return equipped.HasComponent<MeleeWeaponComponent>() ? 1.0f : 0.0f;
}
}
}

View File

@@ -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<WeaponEntityState>().GetValue();
if (target == null || !target.TryGetComponent(out MeleeWeaponComponent meleeWeaponComponent))
{
return 0.0f;
}
return meleeWeaponComponent.CooldownTime / 10.0f;
}
}
}

View File

@@ -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<WeaponEntityState>().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<BallisticMagazineComponent>();
if (mag.CountLoaded == 0)
{
// TODO: Do this better
return ballistic.GetChambered(0) != null ? 1.0f : 0.0f;
}
return (float) mag.CountLoaded / mag.Capacity;
}
}
}

View File

@@ -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<EquippedEntityState>().GetValue();
if (equipped == null)
{
return 0.0f;
}
// Maybe change this to BallisticMagazineWeapon
return equipped.HasComponent<BallisticMagazineWeaponComponent>() ? 1.0f : 0.0f;
}
}
}

View File

@@ -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<EquippedEntityState>().GetValue();
if (equipped == null || !equipped.HasComponent<BallisticMagazineWeaponComponent>())
{
return 0.0f;
}
return 1.0f;
}
}
}

View File

@@ -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<SelfState>().GetValue();
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null)
{
return 0.0f;
}
return Visibility.InLineOfSight(owner, target) ? 1.0f : 0.0f;
}
}
}

View File

@@ -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<InventoryState>().GetValue())
{
if (item.HasComponent<RangedWeaponComponent>())
{
count++;
}
}
return (float) count / max;
}
}
}

View File

@@ -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<EquippedEntityState>().GetValue();
if (equipped == null || !equipped.HasComponent<HitscanWeaponComponent>())
{
return 0.0f;
}
return 1.0f;
}
}
}

View File

@@ -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<WeaponEntityState>().GetValue();
if (weapon == null || !weapon.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent))
{
return 0.0f;
}
return hitscanWeaponComponent.CapacitorComponent.Charge / hitscanWeaponComponent.CapacitorComponent.Capacity;
}
}
}

View File

@@ -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<TargetEntityState>().GetValue();
if (target == null ||
!target.TryGetComponent(out WeaponCapacitorChargerComponent chargerComponent) ||
chargerComponent.HeldItem != null)
{
return 1.0f;
}
return 0.0f;
}
}
}

View File

@@ -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<TargetEntityState>().GetValue();
if (target == null || !target.TryGetComponent(out WeaponCapacitorChargerComponent weaponCharger))
{
return 0.0f;
}
// AI don't care about efficiency, psfft!
return weaponCharger.TransferRatio;
}
}
}

View File

@@ -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<WeaponEntityState>().GetValue();
if (weapon == null || !weapon.TryGetComponent(out HitscanWeaponComponent hitscanWeaponComponent))
{
return 0.0f;
}
// Just went with max health
return hitscanWeaponComponent.Damage / 300.0f;
}
}
}

View File

@@ -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<EquippedEntityState>().GetValue();
if (equipped == null)
{
return 0.0f;
}
return equipped.HasComponent<HitscanWeaponComponent>() ? 1.0f : 0.0f;
}
}
}

View File

@@ -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<EquippedEntityState>().GetValue();
if (equipped == null || !equipped.HasComponent<RangedWeaponComponent>())
{
return 0.0f;
}
return 1.0f;
}
}
}

View File

@@ -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<WeaponEntityState>().GetValue();
if (weapon == null || !weapon.TryGetComponent(out RangedWeaponComponent ranged))
{
return 0.0f;
}
return ranged.FireRate / 100.0f;
}
}
}

View File

@@ -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<TargetEntityState>().GetValue();
if (target == null || !target.TryGetComponent(out DamageableComponent damageableComponent))
{
return 0.0f;
}
// Just went with max health
return damageableComponent.CurrentDamage[DamageType.Total] / 300.0f;
}
}
}

View File

@@ -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<TargetEntityState>().GetValue();
if (target == null || !target.TryGetComponent(out SpeciesComponent speciesComponent))
{
return 0.0f;
}
if (speciesComponent.CurrentDamageState is CriticalState)
{
return 1.0f;
}
return 0.0f;
}
}
}

View File

@@ -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<TargetEntityState>().GetValue();
if (target == null || !target.TryGetComponent(out SpeciesComponent speciesComponent))
{
return 0.0f;
}
if (speciesComponent.CurrentDamageState is DeadState)
{
return 1.0f;
}
return 0.0f;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Returns 1.0f if the item is freely accessible (e.g. in storage we can open, on ground, etc.)
/// </summary>
public sealed class TargetAccessibleCon : Consideration
{
public TargetAccessibleCon(IResponseCurve curve) : base(curve) {}
public override float GetScore(Blackboard context)
{
var target = context.GetState<TargetEntityState>().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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<SelfState>().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;
}
}
}

View File

@@ -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
{
/// <summary>
/// Returns 1 if in our hands else 0
/// </summary>
public sealed class TargetInOurHandsCon : Consideration
{
public TargetInOurHandsCon(IResponseCurve curve) : base(curve) {}
public override float GetScore(Blackboard context)
{
var owner = context.GetState<SelfState>().GetValue();
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null ||
!target.HasComponent<ItemComponent>() ||
!owner.TryGetComponent(out HandsComponent handsComponent))
{
return 0.0f;
}
return handsComponent.IsHolding(target) ? 1.0f : 0.0f;
}
}
}

View File

@@ -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<TargetEntityState>().GetValue();
if (target == null || !target.HasComponent<ItemComponent>())
{
return 0.0f;
}
var inventory = context.GetState<InventoryState>().GetValue();
foreach (var item in inventory)
{
if (item == target)
{
return 1.0f;
}
}
return context.GetState<AnyFreeHandState>().GetValue() ? 1.0f : 0.0f;
}
}
}

View File

@@ -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<InventoryState>().GetValue();
var target = context.GetState<TargetEntityState>().GetValue();
if (target == null || !target.HasComponent<ItemComponent>())
{
return 0.0f;
}
foreach (var item in inventory)
{
if (item == target)
{
return 1.0f;
}
}
return 0.0f;
}
}
}

View File

@@ -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<SelfState>().GetValue();
var target = context.GetState<TargetEntityState>().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);
}
}
}

View File

@@ -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<TargetEntityState>().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;
}
}
}

View File

@@ -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<SelfState>().GetValue();
if (!owner.TryGetComponent(out ThirstComponent thirst))
{
return 0.0f;
}
return 1 - (thirst.CurrentThirst / thirst.ThirstThresholds[ThirstThreshold.OverHydrated]);
}
}
}

View File

@@ -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<TargetEntityState>().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;
}
}
}

Some files were not shown because too many files have changed in this diff Show More