diff --git a/Content.Server/AI/AimShootLifeProcessor.cs b/Content.Server/AI/AimShootLifeProcessor.cs
new file mode 100644
index 0000000000..a22acbd011
--- /dev/null
+++ b/Content.Server/AI/AimShootLifeProcessor.cs
@@ -0,0 +1,170 @@
+using System;
+using System.Collections.Generic;
+using SS14.Server.AI;
+using SS14.Server.GameObjects;
+using SS14.Server.Interfaces.GameObjects;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.Interfaces.GameObjects.Components;
+using SS14.Shared.Interfaces.Physics;
+using SS14.Shared.Interfaces.Timing;
+using SS14.Shared.IoC;
+using SS14.Shared.Maths;
+
+namespace Content.Server.AI
+{
+ ///
+ /// The object stays stationary. The object will periodically scan for *any* life forms in its radius, and engage them.
+ /// The object will rotate itself to point at the locked entity, and if it has a weapon will shoot at the entity.
+ ///
+ [AiLogicProcessor("AimShootLife")]
+ class AimShootLifeProcessor : AiLogicProcessor
+ {
+ private readonly ICollisionManager _physMan;
+ private readonly IServerEntityManager _entMan;
+ private readonly IGameTiming _timeMan;
+
+ private readonly List _workList = new List();
+
+ private const float MaxAngSpeed = (float) (Math.PI / 2); // how fast our turret can rotate
+ private const float ScanPeriod = 1.0f; // tweak this for performance and gameplay experience
+ private float _lastScan;
+
+ private IEntity _curTarget;
+
+ ///
+ /// Creates an instance of this LogicProcessor.
+ ///
+ public AimShootLifeProcessor()
+ {
+ _physMan = IoCManager.Resolve();
+ _entMan = IoCManager.Resolve();
+ _timeMan = IoCManager.Resolve();
+ }
+
+ ///
+ public override void Update(float frameTime)
+ {
+ if (SelfEntity == null)
+ return;
+
+ DoScanning();
+ DoTracking(frameTime);
+ }
+
+ private void DoScanning()
+ {
+ var curTime = _timeMan.CurTime.TotalSeconds;
+ if (curTime - _lastScan > ScanPeriod)
+ {
+ _lastScan = (float) curTime;
+ _curTarget = FindBestTarget();
+ }
+ }
+
+ private void DoTracking(float frameTime)
+ {
+ // not valid entity to target.
+ if (_curTarget == null || !_curTarget.IsValid())
+ {
+ _curTarget = null;
+ return;
+ }
+
+ // point me at the target
+ var tarPos = _curTarget.GetComponent().WorldPosition;
+ var myPos = SelfEntity.GetComponent().WorldPosition;
+
+ var curDir = SelfEntity.GetComponent().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);
+ }
+
+ SelfEntity.GetComponent().LocalRotation = new Angle(newDir);
+
+ if (fwdAng > -0.9999)
+ {
+ // TODO: shoot gun, prob need aimbot because entity rotation lags behind moving target
+ }
+ }
+
+ private IEntity FindBestTarget()
+ {
+ // "best" target is the closest one with LOS
+
+ var ents = _entMan.GetEntitiesInRange(SelfEntity, VisionRadius);
+ var myTransform = SelfEntity.GetComponent();
+ var maxRayLen = VisionRadius * 2.5f; // circle inscribed in square, square diagonal = 2*r*sqrt(2)
+
+ _workList.Clear();
+ foreach (var entity in ents)
+ {
+ // filter to "people" entities (entities with controllers)
+ if (!entity.HasComponent())
+ continue;
+
+ // build the ray
+ var dir = entity.GetComponent().WorldPosition - myTransform.WorldPosition;
+ var ray = new Ray(myTransform.WorldPosition, dir.Normalized);
+
+ // cast the ray
+ var result = _physMan.IntersectRay(ray, maxRayLen);
+
+ // add to visible list
+ if (result.HitEntity == entity)
+ _workList.Add(entity);
+ }
+
+ // get closest entity in list
+ var closestEnt = GetClosest(myTransform.WorldPosition, _workList);
+
+ // return closest
+ return closestEnt;
+ }
+
+ private static IEntity GetClosest(Vector2 origin, IEnumerable list)
+ {
+ IEntity closest = null;
+ var minDistSqrd = float.PositiveInfinity;
+
+ foreach (var ent in list)
+ {
+ var pos = ent.GetComponent().WorldPosition;
+ var distSqrd = (pos - origin).LengthSquared;
+
+ if (distSqrd > minDistSqrd)
+ continue;
+
+ closest = ent;
+ minDistSqrd = distSqrd;
+ }
+
+ return closest;
+ }
+
+ private static Vector2 MoveTowards(Vector2 current, Vector2 target, float speed, float delta)
+ {
+ var maxDeltaDist = speed * delta;
+ var a = target - current;
+ var magnitude = a.Length;
+ if (magnitude <= maxDeltaDist)
+ {
+ return target;
+ }
+
+ return current + a / magnitude * maxDeltaDist;
+ }
+ }
+}
diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj
index 2d4a2dccbc..7206cee6ea 100644
--- a/Content.Server/Content.Server.csproj
+++ b/Content.Server/Content.Server.csproj
@@ -55,6 +55,7 @@
+
@@ -84,6 +85,7 @@
+
@@ -119,4 +121,5 @@
+
\ No newline at end of file
diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs
index 554ae3b68d..368cab7cb5 100644
--- a/Content.Server/EntryPoint.cs
+++ b/Content.Server/EntryPoint.cs
@@ -2,6 +2,7 @@
using Content.Server.GameObjects.Components.Power;
using Content.Server.GameObjects.Components.Interactable.Tools;
using Content.Server.Interfaces.GameObjects;
+using Content.Server.Placement;
using SS14.Server;
using SS14.Server.Interfaces;
using SS14.Server.Interfaces.Chat;
@@ -19,6 +20,7 @@ using SS14.Shared.Log;
using SS14.Shared.Map;
using SS14.Shared.Timers;
using SS14.Shared.Interfaces.Timing;
+using SS14.Shared.Maths;
namespace Content.Server
{
@@ -100,6 +102,9 @@ namespace Content.Server
var newMap = mapMan.CreateMap(new MapId(2));
mapLoader.LoadBlueprint(newMap, new GridId(4), "Maps/Demo/DemoGrid.yaml");
+
+ var grid = newMap.GetGrid(new GridId(4));
+ SpawnHelpers.SpawnLightTurret(grid, new Vector2(-15, 15));
}
var timeSpan = timing.RealTime - startTime;
Logger.Info($"Loaded map in {timeSpan.TotalMilliseconds:N2}ms.");
diff --git a/Content.Server/Placement/SpawnHelpers.cs b/Content.Server/Placement/SpawnHelpers.cs
new file mode 100644
index 0000000000..cbcba32a5c
--- /dev/null
+++ b/Content.Server/Placement/SpawnHelpers.cs
@@ -0,0 +1,30 @@
+using SS14.Server.Interfaces.GameObjects;
+using SS14.Shared.Interfaces.Map;
+using SS14.Shared.IoC;
+using SS14.Shared.Map;
+using SS14.Shared.Maths;
+
+namespace Content.Server.Placement
+{
+ ///
+ /// Helper function for spawning more complex multi-entity structures
+ ///
+ public static class SpawnHelpers
+ {
+ ///
+ /// Spawns a spotlight ground turret that will track any living entities in range.
+ ///
+ ///
+ ///
+ public static void SpawnLightTurret(IMapGrid grid, Vector2 localPosition)
+ {
+ var entMan = IoCManager.Resolve();
+ var tBase = entMan.SpawnEntity("TurretBase");
+ tBase.GetComponent().LocalPosition = new LocalCoordinates(localPosition, grid);
+
+ var tTop = entMan.SpawnEntity("TurretTopLight");
+ tTop.GetComponent().LocalPosition = new LocalCoordinates(localPosition, grid);
+ tTop.GetComponent().AttachParent(tBase);
+ }
+ }
+}
diff --git a/Resources/Prototypes/Entities/Turret.yml b/Resources/Prototypes/Entities/Turret.yml
new file mode 100644
index 0000000000..2f45a71ba5
--- /dev/null
+++ b/Resources/Prototypes/Entities/Turret.yml
@@ -0,0 +1,46 @@
+- type: entity
+ id: TurretBase
+ name: Turret Base
+ components:
+ - type: Transform
+ - type: Clickable
+ - type: BoundingBox
+ - type: Sprite
+ drawdepth: FloorPlaceable
+ sprites:
+ - TurrBase
+
+- type: entity
+ id: TurretTopGun
+ name: Turret (Gun)
+ components:
+ - type: Transform
+ - type: Clickable
+ - type: BoundingBox
+ - type: Sprite
+ drawdepth: WallMountedItems
+ sprites:
+ - TurrTop
+ - type: AiController
+ logic: AimShootLife
+ vision: 6.0
+
+- type: entity
+ id: TurretTopLight
+ name: Turret (Light)
+ components:
+ - type: Transform
+ - type: Clickable
+ - type: BoundingBox
+ - type: Sprite
+ drawdepth: WallMountedItems
+ sprites:
+ - TurrLamp
+ - type: AiController
+ logic: AimShootLife
+ vision: 6.0
+ - type: PointLight
+ radius: 512
+ mask: flashlight_mask
+ autoRot: true
+
\ No newline at end of file
diff --git a/Resources/textures/Buildings/TurrBase.png b/Resources/textures/Buildings/TurrBase.png
new file mode 100644
index 0000000000..8379e8bbf9
Binary files /dev/null and b/Resources/textures/Buildings/TurrBase.png differ
diff --git a/Resources/textures/Buildings/TurrLamp.png b/Resources/textures/Buildings/TurrLamp.png
new file mode 100644
index 0000000000..f2cc2aac1a
Binary files /dev/null and b/Resources/textures/Buildings/TurrLamp.png differ
diff --git a/Resources/textures/Buildings/TurrTop.png b/Resources/textures/Buildings/TurrTop.png
new file mode 100644
index 0000000000..8178030ef0
Binary files /dev/null and b/Resources/textures/Buildings/TurrTop.png differ
diff --git a/engine b/engine
index fc361882b8..93c7aab5d0 160000
--- a/engine
+++ b/engine
@@ -1 +1 @@
-Subproject commit fc361882b879dd3183d00546a89b35df15d7ddb5
+Subproject commit 93c7aab5d0ec1fcad237ea84715378d0b95f99f9