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:
61
Content.Client/Commands/DebugAiCommand.cs
Normal file
61
Content.Client/Commands/DebugAiCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
Content.Client/Commands/DebugPathfindingCommand.cs
Normal file
62
Content.Client/Commands/DebugPathfindingCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Content.Server/AI/Operators/AiOperator.cs
Normal file
55
Content.Server/AI/Operators/AiOperator.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
24
Content.Server/AI/Operators/Generic/WaitOperator.cs
Normal file
24
Content.Server/AI/Operators/Generic/WaitOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Content.Server/AI/Operators/Inventory/DropEntityOperator.cs
Normal file
32
Content.Server/AI/Operators/Inventory/DropEntityOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Content.Server/AI/Operators/Inventory/EquipEntityOperator.cs
Normal file
38
Content.Server/AI/Operators/Inventory/EquipEntityOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Content.Server/AI/Operators/Inventory/OpenStorageOperator.cs
Normal file
55
Content.Server/AI/Operators/Inventory/OpenStorageOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
295
Content.Server/AI/Operators/Movement/BaseMover.cs
Normal file
295
Content.Server/AI/Operators/Movement/BaseMover.cs
Normal 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
|
||||
}
|
||||
}
|
||||
142
Content.Server/AI/Operators/Movement/MoveToEntityOperator.cs
Normal file
142
Content.Server/AI/Operators/Movement/MoveToEntityOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
94
Content.Server/AI/Operators/Movement/MoveToGridOperator.cs
Normal file
94
Content.Server/AI/Operators/Movement/MoveToGridOperator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
44
Content.Server/AI/Operators/Sequences/SequenceOperator.cs
Normal file
44
Content.Server/AI/Operators/Sequences/SequenceOperator.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
47
Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs
Normal file
47
Content.Server/AI/Utility/Actions/Clothing/Head/EquipHead.cs
Normal 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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
55
Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs
Normal file
55
Content.Server/AI/Utility/Actions/Combat/Melee/EquipMelee.cs
Normal 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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
9
Content.Server/AI/Utility/Actions/IAiUtility.cs
Normal file
9
Content.Server/AI/Utility/Actions/IAiUtility.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Content.Server.AI.Utility.AiLogic;
|
||||
|
||||
namespace Content.Server.AI.Utility.Actions
|
||||
{
|
||||
public interface IAiUtility
|
||||
{
|
||||
float Bonus { get; }
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
83
Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs
Normal file
83
Content.Server/AI/Utility/Actions/Idle/WanderAndWait.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
149
Content.Server/AI/Utility/Actions/UtilityAction.cs
Normal file
149
Content.Server/AI/Utility/Actions/UtilityAction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Content.Server/AI/Utility/AiLogic/Civilian.cs
Normal file
21
Content.Server/AI/Utility/AiLogic/Civilian.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Content.Server/AI/Utility/AiLogic/PathingDummy.cs
Normal file
18
Content.Server/AI/Utility/AiLogic/PathingDummy.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Content.Server/AI/Utility/AiLogic/Spirate.cs
Normal file
22
Content.Server/AI/Utility/AiLogic/Spirate.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
228
Content.Server/AI/Utility/AiLogic/UtilityAI.cs
Normal file
228
Content.Server/AI/Utility/AiLogic/UtilityAI.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Content.Server/AI/Utility/BehaviorSets/BehaviorSet.cs
Normal file
22
Content.Server/AI/Utility/BehaviorSets/BehaviorSet.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Content.Server/AI/Utility/BehaviorSets/HungerBehaviorSet.cs
Normal file
20
Content.Server/AI/Utility/BehaviorSets/HungerBehaviorSet.cs
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Content.Server/AI/Utility/BehaviorSets/IdleBehaviorSet.cs
Normal file
18
Content.Server/AI/Utility/BehaviorSets/IdleBehaviorSet.cs
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs
Normal file
37
Content.Server/AI/Utility/BehaviorSets/SpirateBehaviorSet.cs
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Content.Server/AI/Utility/BehaviorSets/ThirstBehaviorSet.cs
Normal file
18
Content.Server/AI/Utility/BehaviorSets/ThirstBehaviorSet.cs
Normal 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Content.Server/AI/Utility/Considerations/Consideration.cs
Normal file
25
Content.Server/AI/Utility/Considerations/Consideration.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Content.Server/AI/Utility/Considerations/DummyCon.cs
Normal file
12
Content.Server/AI/Utility/Considerations/DummyCon.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user