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