From 071ed3f1edb2ed278b95062c002b445b344e6c3b Mon Sep 17 00:00:00 2001 From: Acruid Date: Thu, 15 Mar 2018 12:41:31 -0700 Subject: [PATCH] Turret Entities (#38) * Spotlight Turret. * Used new turret sprite, credit: @Lobstrex (Lob)#5692 * Moved AimShootLifeProcessor.cs to content. * Update Submodule Try 2. --- Content.Server/AI/AimShootLifeProcessor.cs | 170 +++++++++++++++++++++ Content.Server/Content.Server.csproj | 3 + Content.Server/EntryPoint.cs | 5 + Content.Server/Placement/SpawnHelpers.cs | 30 ++++ Resources/Prototypes/Entities/Turret.yml | 46 ++++++ Resources/textures/Buildings/TurrBase.png | Bin 0 -> 2703 bytes Resources/textures/Buildings/TurrLamp.png | Bin 0 -> 3268 bytes Resources/textures/Buildings/TurrTop.png | Bin 0 -> 2208 bytes engine | 2 +- 9 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 Content.Server/AI/AimShootLifeProcessor.cs create mode 100644 Content.Server/Placement/SpawnHelpers.cs create mode 100644 Resources/Prototypes/Entities/Turret.yml create mode 100644 Resources/textures/Buildings/TurrBase.png create mode 100644 Resources/textures/Buildings/TurrLamp.png create mode 100644 Resources/textures/Buildings/TurrTop.png 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 0000000000000000000000000000000000000000..8379e8bbf9a3901e6b0f2ecf8611997d2f9baee7 GIT binary patch literal 2703 zcmV;A3UKv_P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02y>eSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E-^XO*0N*(015v|L_t(|UhP|JXk=#@KD)KMHR^5?Hzt!z z=FFLMzs;HZOlBr$CX>sq!72rRs0CU4=cS^B^@1RHL7@tQMT&m}@lq^Bx>A%v#X_ln zY=5k!7P?eu?V_x;NXrJ(WOIC<@0>3?J2~lQlT9<7>zJUl;hDu{ccB-$>e)A&G@S0 zc&}%(xxmt#hx9Q$ZCTd)>Hh8EV_lX=B!*Pgs+gwrBge_Ui9*?Iegjm|nr*nhP$jD} zIy#C<`qD&Eyi--3yKUQjiW+adn*%sGR`#;3+utxGL!-x!g{4R_Loq($x*mJb^Fmx- zID}=nR~*CnrmflKsBFL35mhypi1-hYwQ;d`bFxkM1x?fpR&s1??0u&0Jl_5AW)y-# zu^QxZ#ej7S+k2LreMO#-r&uNXXiC~xq^$kAZD;@OQmnarDaaSfEY5f?Aq>O0pNbzB zR8_xsbGn3})Mr42etSM>&ddhIaxH)(bPLOLe`l%I*)WuM-l!2iN3Hoh#eWs?R;Qam z)9*0ka;1&n2*JDKt4o^Pg_!%;kMYmVf6h6H_%#+pl1H^KW4 zwa%M_Hwc#rmnfAlS(g3e(9o$wRP1f7nyW7Pt@&5m6bl*$aaHRxf$J55CJ6%ars-_i z9mGkwEr=81+1?u%5G~6IRMiODozp?N)(EB>t)Sj)GvZu?&ccU-PUrNh-{{<*NEC-C zrXyi!p;HPNdO1VAuez8LtF^638!>i@_D?ralU z^Urkrg}a(=m=TI-<{cw@o)e{YMMT)5fLxZIxDLs>>>AJT7K=n!)^+2YjM|;2-t(a-?ir#k47;QtmMIqqXjv5WsPA!TjPYsUe4)b1q73m^H8SRTtXva9 z0<+bPZFqQii3+Yz1y|{FofZ~rG!3qh^Q%7f#cm;Rjl~GkR+5wr$GasCpbN>e8qE%2 zW)!JUbKm$x<4{cNh>ZsA70TQ-#7xhwQMr{-VQ4&}b<=i-C@R0Pt?Ubw*7uz}c~YW< z#&7HX>}9k97XeF9WD8m!fyNQpDNr8-;DagrAPe})&u_D7dcE14{d;w~@gT)~oTkoi z)4=(nlE{3CzQ2bt*^P||BPWt4k|WVq!LbVW&_d&}>4v`o^{X`9?3DmO_pA{vZ(TMP zpb?t&_RGy?>wIo9=SATO6B85qsZ-lqP432yWebJxPS^cEQ2Ha~Sb&)05pW!lodR`p zKnTnTg+gVy(VTsxWRyNZu^bIUdgtwkEZcS4^`1fBM2aH7WHJ>?WVhe~gjrVjmFrfL zQ5^ecilWr0fBp^R5g1l@KpcVH0wSXez>jTv=b{ky*EG#KW0}r>F-V~c@PR6hz;1v# zk@_Id1W?@v4PoMgQ5gGYnN((u*46*Se2)Nc#ncyv$;l};zJNT$peeCGGC&!bOwz*X zAqarr_Cf&EnNDZeR2XI#));*v0F0)kbG!>6Ky-s!DFo`mxPobx*A1VM`~5crgVqm4 zu%d;__BKG<1%yT2*k{)#`{`?mOrHAT_yA;Pah z0KR|U?Kpsg5mA&_cfueWofiF>cC4e3+x{{N?gPnVAt`Uj$HMxO-^Py6#PXH z4tH?jposoEH6>n05~LsspFwz+;lD$K=$L%nb#i~o3gPK+-NDf|$4U`?p7b-8| zC2PJ|VJ|RsN_~q&z&m}syuiIeuKZl|{NP!kSbGUCEVlxV-;4R`U<&|}apZwR0R~+F z>f<*Qpjg%Y*|X0MflO-3+RApZ#Sg{~ZE&`2iLnBF0XBaPYwoM{td^0Bnc5o*PU7 zu6OLCd>M1O*8&NslFOV zxEu7$6*FW0TpO4A?cZ{CXObjU6j}Xa9BB@W4>{5LTKdkk7!?~VOO2%@QIvkLMV}9{ za`ucR^zVPA-6}gSikVM_qks6PZ0s#scHw4B!9&002ov JPDHLkV1hHV3$p+K literal 0 HcmV?d00001 diff --git a/Resources/textures/Buildings/TurrLamp.png b/Resources/textures/Buildings/TurrLamp.png new file mode 100644 index 0000000000000000000000000000000000000000..f2cc2aac1aa765c6f1f7493376ab7452e28e2579 GIT binary patch literal 3268 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 literal 0 HcmV?d00001 diff --git a/Resources/textures/Buildings/TurrTop.png b/Resources/textures/Buildings/TurrTop.png new file mode 100644 index 0000000000000000000000000000000000000000..8178030ef095e8bed33800a1f3a7641ea78f8d4f GIT binary patch literal 2208 zcmV;R2w(S!P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02y>eSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E-^XO*0N*(00#S#gW@dMGcGvd)c)jcYpTsFasvxN`G$~3*O-XE##NODBO^A)1pS5X1oP>l{ssidu zTU4Yx^rdf=D)9i~350myp^Xq#e~82B`EKr{!!m?OP3?6upY&;+ojdoQbMCp{IWrrN z3l}b2xL9JWUcLGrJrdoMZ|S<2&9`5yOXtG!(w%{dimEjcBk}v@wywpd=Jv&8Cf6n} z-5IE?^gkFj;+Hr8!~vjmHouv_UUlv29ROc-pgtO_`**&zld}0XYDi^2jYgAI^78Eu zUqEY2W%AU~)k|%i8_6&eRHZ02^78GE>S|wOq9H?DpWGoBOC+hP>NX2N!Q;8>xM4J$ zs;^%-l}rjAH_e6SHER}jHa7k4r_b!9u|t!By_Q@wT%^C~7l1N;cG0Zoo zRCSGeQWJ++$mdB{qTO$Gh$F1?m1Y`k%`J$%v$tB7gocfPiyMNBM``~l6 zwUp%msJcOQ$qe;x-z6BUOHtS#64~*(Z9I5dX2G#oY;jysD&;cei9#s!FsJLsFc|4; zcLW^t`>Aeyl?XthJ}sPoiiy9qaFiN3IHmVoNT)>>V9x1CJn?cg`qJ@u{2$nT z7+&hkfl!DHb`*^dcYpLm-*&-NW1hG(B(uut}2(O!r`rQx$?qcU2i^S zn(uS^e*pz|a%4=`Db3F2TRNzBZ4cr_#-3MJb$r;4~xXKy!7C$+a zogm)DJ6~i2ugvlJw+oq_hr?mYv7_yso2a9EvyHtX2$`l9$NRwZJSWO3Yr$tTkqB4X zGQyN;?mili{|jk)-XZlZ&cA$P_gO@6F$V&0 z)HL6l*7bU6%JP5}xsrGPPcR7e+TQg1zlbA-28%U^W~0%Wf~MUk%~^U(8%7f68fX73 zmntWNZ#9txXs=leX~r}!v#=`CoTbNbFqnDKFz98|6shTCFoG47^sB&#zoMdBAr=5(2Gbmwa(is|U%F%(Q{B;gX+P@p)G0>8) zJ5Kl>ogth*7}DO_>+?M%%~^WwKgT7myS-a<7X5JB!PzY+jwix59u z|KLEN4Aj3ALmT`_Htt;*Y1aW5ewM4l3NB1VEW<&of1l8G>S}3sbQ~pcV@y2ch;oX|vF=9@A$}9#$Uiy#i*mX0!$?ql zVLTlEJB+oLLpXz!p5qcWq-xa3kH4kP_h$3-65ofO0iC0DFNjxS=r(!okNQ2nd*pKE zhm6N_H)p|zFcyy3oj?%K5zg`sKWWCPt)u690J`(7v@c?aG%rnk5Sjpi9SQ1xluMN_ z4yfv{U@RQ9I{^wo zk$1J!A!q|K0q5Kz{teo7LyTqOeFJuXf2}$q=P!Tk_4*!%(Kx$yhwP3yv2+}SbDED5 ziyy6+-v$Bh((WGRl!dCJ{9$)Xox2f{2@pf*0s?tZ)&C5EmD86$(54Qn>aSru-rQL4 z3QAMli5LStfb%#;*!_Kezb>an>BQ*(0000