66using Microsoft . Xna . Framework ;
77using Microsoft . Xna . Framework . Graphics ;
88using Terraria ;
9+ using Terraria . Audio ;
910using Terraria . GameContent ;
1011using Terraria . ID ;
1112using Terraria . Localization ;
@@ -52,15 +53,26 @@ public SpawnGuardian(bool isFake) : base(isFake ? EffectID.SpawnFakeGuardian : E
5253
5354 protected override CrowdControlResponseStatus OnStart ( )
5455 {
56+ var mode = CrowdControlGuardian . GuardianMode . None ;
57+ if ( SteamUtils . IsTeebu )
58+ {
59+ mode |= CrowdControlGuardian . GuardianMode . Fishron ;
60+ }
61+
62+ if ( SteamUtils . IsKulprid && ! _isFake )
63+ {
64+ mode |= CrowdControlGuardian . GuardianMode . Persistent ;
65+ }
66+
5567 if ( NetUtils . IsSinglePlayer )
5668 {
5769 // In single-player, simply spawn the custom dungeon guardian
58- Spawn ( GetLocalPlayer ( ) , SteamUtils . IsTeebu ) ;
70+ Spawn ( GetLocalPlayer ( ) , mode ) ;
5971 }
6072 else
6173 {
6274 // If on server, spawn on server (no need to pass arguments)
63- SendPacket ( PacketID . HandleEffect , SteamUtils . IsTeebu ) ;
75+ SendPacket ( PacketID . HandleEffect , ( int ) mode ) ;
6476 }
6577
6678 return CrowdControlResponseStatus . Success ;
@@ -87,10 +99,10 @@ protected override void SendStartMessage(string viewerString, string playerStrin
8799 protected override void OnReceivePacket ( CrowdControlPlayer player , BinaryReader reader )
88100 {
89101 // Spawn the dungeon guardian on the server
90- Spawn ( player , reader . ReadBoolean ( ) ) ;
102+ Spawn ( player , ( CrowdControlGuardian . GuardianMode ) reader . ReadInt32 ( ) ) ;
91103 }
92104
93- private void Spawn ( CrowdControlPlayer player , bool isTeebu )
105+ private void Spawn ( CrowdControlPlayer player , CrowdControlGuardian . GuardianMode mode )
94106 {
95107 // Determine spawn position
96108 var circleEdge = Main . rand . NextVector2CircularEdge ( HalfRangeWidth , HalfRangeHeight ) ;
@@ -107,7 +119,9 @@ private void Spawn(CrowdControlPlayer player, bool isTeebu)
107119 // Set whether it is fake or not
108120 var guardian = ( CrowdControlGuardian ) npc . ModNPC ;
109121 guardian . IsFake = _isFake ;
110- guardian . IsTeebu = isTeebu ;
122+ guardian . Mode = mode ;
123+ var isTeebu = guardian . IsFishron ;
124+ var isKulprid = guardian . IsPersistent ;
111125
112126 if ( isTeebu )
113127 {
@@ -135,14 +149,14 @@ private void Spawn(CrowdControlPlayer player, bool isTeebu)
135149 NetMessage . SendData ( MessageID . SyncNPC , - 1 , - 1 , null , index ) ;
136150 }
137151
138- if ( ! isTeebu )
152+ if ( ! isTeebu && ! isKulprid )
139153 {
140154 return ;
141155 }
142156
143157 // Special life for teebu
144- npc . lifeMax = 69420 ;
145- npc . life = 69420 ;
158+ npc . lifeMax = isKulprid ? Main . hardMode ? 100 : 30 : 69420 ;
159+ npc . life = npc . lifeMax ;
146160
147161 if ( NetUtils . IsServer )
148162 {
@@ -157,6 +171,18 @@ private void Spawn(CrowdControlPlayer player, bool isTeebu)
157171 // ReSharper disable once ClassNeverInstantiated.Local
158172 private sealed class CrowdControlGuardian : ModNPC
159173 {
174+ #region Enums
175+
176+ [ Flags ]
177+ public enum GuardianMode
178+ {
179+ None = 0 ,
180+ Fishron = 1 ,
181+ Persistent = 2
182+ }
183+
184+ #endregion
185+
160186 #region Fields
161187
162188 private int _timeLeft ;
@@ -175,14 +201,24 @@ public bool IsFake
175201 }
176202
177203 /// <summary>
178- /// Whether the guardian is an easter egg for Teebu .
204+ /// Guardian mode .
179205 /// </summary>
180- public bool IsTeebu
206+ public GuardianMode Mode
181207 {
182- get => NPC . ai [ NPC . maxAI - 1 ] > 0f ;
183- set => NPC . ai [ NPC . maxAI - 1 ] = value ? 1f : 0f ;
208+ get => ( GuardianMode ) ( int ) NPC . ai [ NPC . maxAI - 1 ] ;
209+ set => NPC . ai [ NPC . maxAI - 1 ] = ( int ) value ;
184210 }
185211
212+ /// <summary>
213+ /// Uses a fishron overlay (Teebu easter egg).
214+ /// </summary>
215+ public bool IsFishron => Mode . HasFlag ( GuardianMode . Fishron ) ;
216+
217+ /// <summary>
218+ /// Doesn't despawn but has reduced life (Kulprid easter egg).
219+ /// </summary>
220+ public bool IsPersistent => Mode . HasFlag ( GuardianMode . Persistent ) ;
221+
186222 public override string Texture => $ "Terraria/Images/NPC_{ NPCID . DungeonGuardian } ";
187223
188224 public override LocalizedText DisplayName => Lang . GetNPCName ( NPCID . DungeonGuardian ) ;
@@ -199,7 +235,7 @@ public bool IsTeebu
199235
200236 public override void ModifyTypeName ( ref string typeName )
201237 {
202- if ( IsTeebu )
238+ if ( IsFishron )
203239 {
204240 typeName = "Teebu's Favourite Boss" ;
205241 }
@@ -216,8 +252,49 @@ public override void SetDefaults()
216252 public override bool PreAI ( )
217253 {
218254 NPC . type = NPCID . DungeonGuardian ;
219- NPC . ShowNameOnHover = NetUtils . IsSinglePlayer || ! IsTeebu ;
255+ NPC . ShowNameOnHover = NetUtils . IsSinglePlayer || ! IsFishron ;
220256
257+ if ( IsPersistent )
258+ {
259+ // Never run out of time if persistent
260+ _timeLeft = 60 * SurvivalDuration ;
261+ NPC . timeLeft = 60 * SurvivalDuration + 1 ;
262+
263+ if ( ! NetUtils . IsServer && NPC . localAI [ 0 ] == 0f )
264+ {
265+ NPC . localAI [ 0 ] = 1f ;
266+ Roar ( ) ;
267+ }
268+
269+ // Ensure vanilla AI doesn't despawn the guardian if all players are dead
270+ if ( NPC . target < 0 || NPC . target >= Main . maxPlayers || ! Main . player [ NPC . target ] . active || Main . player [ NPC . target ] . dead )
271+ {
272+ NPC . target = - 1 ;
273+ NPC . TargetClosest ( false ) ;
274+ if ( NPC . target < 0 || NPC . target >= Main . maxPlayers || ! Main . player [ NPC . target ] . active || Main . player [ NPC . target ] . dead )
275+ {
276+ NPC . target = - 1 ;
277+ NPC . rotation %= MathF . PI * 2 ;
278+ NPC . rotation *= 0.95f ;
279+ NPC . velocity *= 0.98f ;
280+ return false ;
281+ }
282+
283+ // Reacquired a target, so roar!
284+ if ( ! NetUtils . IsServer )
285+ {
286+ Roar ( ) ;
287+ }
288+ }
289+
290+ // Move towards player (emulating vanilla behaviour)
291+ var speedMult = NPC . DistanceSQ ( Main . player [ NPC . target ] . position ) > 16 * 16 * 180 * 180 ? 5f : 1f ;
292+ NPC . velocity = NPC . DirectionTo ( Main . player [ NPC . target ] . position ) * 8.75f * speedMult ;
293+ NPC . rotation += MathHelper . ToRadians ( 11 ) ;
294+
295+ return false ;
296+ }
297+
221298 // Reduce the time left timer
222299 _timeLeft -- ;
223300 if ( _timeLeft != 0 )
@@ -235,6 +312,22 @@ public override bool PreAI()
235312 return base . PreAI ( ) ;
236313 }
237314
315+ public override bool CheckActive ( )
316+ {
317+ // Don't despawn if persistent
318+ if ( IsPersistent && _timeLeft > 0 )
319+ {
320+ return false ;
321+ }
322+
323+ return base . CheckActive ( ) ;
324+ }
325+
326+ public override void OnKill ( )
327+ {
328+ Item . NewItem ( null , NPC . position , NPC . width , NPC . height , ItemID . GoldCoin , 2 ) ;
329+ }
330+
238331 public override bool CanHitPlayer ( Player target , ref int cooldownSlot )
239332 {
240333 // Ignore player if fake
@@ -249,15 +342,15 @@ public override bool CanHitNPC(NPC target)
249342
250343 public override void BossHeadSlot ( ref int index )
251344 {
252- if ( IsTeebu )
345+ if ( IsFishron )
253346 {
254347 index = NPCID . Sets . BossHeadTextures [ NPCID . DukeFishron ] ;
255348 }
256349 }
257350
258351 public override bool PreDraw ( SpriteBatch spriteBatch , Vector2 screenPos , Color drawColor )
259352 {
260- if ( ! IsTeebu )
353+ if ( ! IsFishron )
261354 {
262355 return base . PreDraw ( spriteBatch , screenPos , drawColor ) ;
263356 }
@@ -289,6 +382,12 @@ public override bool PreDraw(SpriteBatch spriteBatch, Vector2 screenPos, Color d
289382 return false ;
290383 }
291384
385+ private void Roar ( )
386+ {
387+ var pos = Main . LocalPlayer . position + Main . LocalPlayer . DirectionTo ( NPC . position ) * 16f * 20f ;
388+ SoundEngine . PlaySound ( SoundID . Roar with { SoundLimitBehavior = SoundLimitBehavior . ReplaceOldest } , pos ) ;
389+ }
390+
292391 #endregion
293392 }
294393
0 commit comments