XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/DeathMod

Knowledge cutoff: 2024-06

You are an AI programming assistant. When asked for your name, you must respond with "GitHub Copilot". Follow the user's requirements carefully & to the letter. Your expertise is strictly limited to software development topics. Follow Microsoft content policies. Avoid content that violates copyrights. For questions not related to software development, simply give a reminder that you are an AI programming assistant. Keep your answers short and to the point. Do not include any preamble, explanations, or apologies. Do not repeat or paraphrase the user's question. Do not include any URLs or links. Do not include any code unless the user explicitly asks for code. Do not include any lists unless the user explicitly asks for a list. Do not include any table unless the user explicitly asks for a table. Do not include any headings or titles unless the user explicitly asks for a heading. Do not include any formatting (such as bold, italics, or blockquotes). Do not include any text except the answer.

960b403
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

5 个文件 +173 -33
Modified Common/DeathDomainBackdropTextureSystem.cs +9 -0
@@ -37,6 +37,15 @@ internal sealed class DeathDomainBackdropTextureSystem : ModSystem
37 37 return layers[index] ??= CreateLayer(index);
38 38 }
39 39
40 internal static void Prewarm()
41 {
42 if (Main.dedServ)
43 return;
44
45 for (int layer = 0; layer < layers.Length; layer++)
46 _ = GetLayer(layer);
47 }
48
40 49 /// <summary>
41 50 /// Returns the single world-space frame used by a fully manifested Death
42 51 /// Domain. Every smaller mask (necklace aperture, weapon wound and wing
Modified Common/MyPlayer.cs +69 -3
@@ -54,6 +54,8 @@ public class MyPlayer : ModPlayer
54 54 private int swingComboDirection = 1;
55 55 private bool serverSoulBalanceInitialized;
56 56 private bool serverReaperProgressionInitialized;
57 private int clientCharacterStateUploadDelay;
58 private int clientCharacterStateUploadAttempts;
57 59 private bool reaperDeathStateCleared;
58 60 private bool reaperLegacyMigrationChecked;
59 61 private int reaperLegacyMigrationVersion;
@@ -122,6 +124,8 @@ public class MyPlayer : ModPlayer
122 124 deathWingDomainVolleyCounter = 0;
123 125 serverSoulBalanceInitialized = false;
124 126 serverReaperProgressionInitialized = false;
127 clientCharacterStateUploadDelay = 0;
128 clientCharacterStateUploadAttempts = 0;
125 129 DeathDomainProgression.Reset();
126 130 ReaperProgression.Reset();
127 131 VisibleReaperForm = ReaperFormId.Base;
@@ -204,6 +208,16 @@ public class MyPlayer : ModPlayer
204 208 public override void OnEnterWorld()
205 209 {
206 210 ClearReaperCombatRuntime(sync: false);
211 if (Main.netMode == NetmodeID.MultiplayerClient
212 && Player.whoAmI == Main.myPlayer)
213 {
214 // ModPlayer.SyncPlayer is not guaranteed to be invoked client-side
215 // after every join/reload. Explicitly retry the character-owned
216 // progression upload so the server cannot remain permanently in
217 // its uninitialized state and reject combat, forms and the domain.
218 clientCharacterStateUploadDelay = 1;
219 clientCharacterStateUploadAttempts = 0;
220 }
207 221 if (Main.netMode == NetmodeID.Server || Player.whoAmI != Main.myPlayer || reaperLegacyMigrationChecked)
208 222 return;
209 223
@@ -225,6 +239,8 @@ public class MyPlayer : ModPlayer
225 239
226 240 public override void PostUpdate()
227 241 {
242 UpdateClientCharacterStateUpload();
243
228 244 if (Main.netMode != NetmodeID.Server && Player.whoAmI == Main.myPlayer && pendingLegacyMigrationEssence > 0)
229 245 TryDeliverPendingLegacyMigrationRefund();
230 246
@@ -281,6 +297,32 @@ public class MyPlayer : ModPlayer
281 297 DeathWingsActiveLastTick = DeathWingsActiveThisTick;
282 298 }
283 299
300 private void UpdateClientCharacterStateUpload()
301 {
302 if (Main.netMode != NetmodeID.MultiplayerClient
303 || Player.whoAmI != Main.myPlayer
304 || clientCharacterStateUploadAttempts >= 3)
305 {
306 return;
307 }
308
309 if (clientCharacterStateUploadDelay > 0)
310 {
311 clientCharacterStateUploadDelay--;
312 if (clientCharacterStateUploadDelay > 0)
313 return;
314 }
315
316 SoulHarvest.SendReaperProgression(Player);
317 clientCharacterStateUploadAttempts++;
318 clientCharacterStateUploadDelay = clientCharacterStateUploadAttempts switch
319 {
320 1 => 30,
321 2 => 90,
322 _ => 0
323 };
324 }
325
284 326 private void TryDeliverPendingLegacyMigrationRefund()
285 327 {
286 328 if (pendingLegacyMigrationEssence <= 0)
@@ -962,7 +1004,13 @@ public class MyPlayer : ModPlayer
962 1004
963 1005 ulong now = Main.GameUpdateCount;
964 1006 if (lastReaperFormSwitchRequestTick != 0 && now - lastReaperFormSwitchRequestTick < 10UL)
965 return false;
1007 {
1008 // Wheel input can commit twice while closing. Treat an identical
1009 // request as an idempotent acknowledgement instead of reporting a
1010 // misleading progression/prerequisite failure to the client.
1011 if (queuedReaperForm == form || ReaperProgression.CurrentForm == form)
1012 return true;
1013 }
966 1014 lastReaperFormSwitchRequestTick = now;
967 1015
968 1016 if (IsReaperActionBusy())
@@ -1166,10 +1214,28 @@ public class MyPlayer : ModPlayer
1166 1214 }
1167 1215 else if (deathInvocationHeld && Main.GameUpdateCount % 5UL == (ulong)(Player.whoAmI % 5))
1168 1216 {
1217 int controllerType = ModContent.ProjectileType<ReaperActionControllerProjectile>();
1218 bool invocationExists = Player.ownedProjectileCounts[controllerType] > 0;
1169 1219 if (Main.netMode == NetmodeID.MultiplayerClient)
1170 SoulHarvest.SendReaperSpecialAim(Player, cursorOffset);
1171 else
1220 {
1221 // A press can reach the server during the last primary-swing
1222 // frame and be rejected legitimately. While Death's invocation
1223 // key remains held, retry the start at a bounded cadence until
1224 // its controller is replicated; afterwards packets only update
1225 // the cursor target.
1226 if (invocationExists)
1227 SoulHarvest.SendReaperSpecialAim(Player, cursorOffset);
1228 else
1229 SoulHarvest.SendReaperAction(Player, ultimate: false, cursorOffset);
1230 }
1231 else if (invocationExists)
1232 {
1172 1233 ReaperCombatService.TryUpdateSpecialTarget(Player, cursorOffset);
1234 }
1235 else
1236 {
1237 ReaperCombatService.TryStartSpecial(Player, cursorOffset);
1238 }
1173 1239 }
1174 1240
1175 1241 if (SoulHarvest.UltimateKeybind?.JustPressed == true)
Modified Common/ReaperCombatService.cs +34 -20
@@ -41,35 +41,31 @@ public static class ReaperCombatService
41 41 if (Main.netMode != NetmodeID.Server
42 42 || !player.active
43 43 || player.dead
44 || player.itemAnimation <= 0
45 || player.itemTime <= 0
46 44 || player.HeldItem.ModItem is not NormalSickle
47 45 || !player.GetModPlayer<MyPlayer>().ServerCharacterStateReady
46 || player.ownedProjectileCounts[ModContent.ProjectileType<ReaperActionControllerProjectile>()] > 0
48 47 || projectile.owner != player.whoAmI)
49 48 {
50 49 return false;
51 50 }
52 51
52 // The projectile packet can arrive after the very short item-use timer
53 // has already reached zero, especially once Death reaches its high-speed
54 // cap. Authorization is bounded by the authoritative per-owner cadence.
55 // Do not reject merely because the previous projectile is still alive on
56 // its harmless cleanup frame: at Death's fast cadence that frame overlaps
57 // the next legal request and used to turn one rejection into a 180-frame
58 // client authority wait.
53 59 int owner = player.whoAmI;
54 if (ReaperCombatSystem.HasActivePrimaryProjectile[owner])
55 {
56 int activeIndex = ReaperCombatSystem.ActivePrimaryProjectile[owner];
57 if (activeIndex >= 0 && activeIndex < Main.maxProjectiles)
58 {
59 Projectile active = Main.projectile[activeIndex];
60 if (active.active && active.whoAmI != projectile.whoAmI && active.owner == owner
61 && active.ModProjectile is SickleSwingProjectile { ServerValidated: true })
62 {
63 return false;
64 }
65 }
66 ReaperCombatSystem.HasActivePrimaryProjectile[owner] = false;
67 }
68
69 60 ulong now = Main.GameUpdateCount;
70 61 if (now < ReaperCombatSystem.NextPrimaryAuthorizationTick[owner])
71 62 return false;
72 63
64 // The cadence check above already admits at most one request for this
65 // owner in the current window. Replace the diagnostic active pointer with
66 // the newly authorized swing; the old projectile will finish naturally.
67 ReaperCombatSystem.HasActivePrimaryProjectile[owner] = false;
68
73 69 phase = TakeNextPrimaryPhase(player, snapshot, out deathTempo);
74 70 int baseDuration = ReaperCombatRegistry.GetUseTime(snapshot.Form, snapshot.Stage, phase);
75 71 float effectiveAttackSpeed = snapshot.AttackSpeedMultiplier * deathTempo;
@@ -186,6 +182,7 @@ public static class ReaperCombatService
186 182 modPlayer.RefundFailedReaperSpecial();
187 183 return false;
188 184 }
185 CancelPrimarySwingsForAction(player);
189 186 return true;
190 187 }
191 188
@@ -256,6 +253,7 @@ public static class ReaperCombatService
256 253 modPlayer.RefundFailedReaperUltimate(form);
257 254 return false;
258 255 }
256 CancelPrimarySwingsForAction(player);
259 257 player.GetModPlayer<ReaperCombatPlayer>().BeginUltimate(form);
260 258 modPlayer.SyncReaperCombatRuntime();
261 259 return true;
@@ -573,12 +571,28 @@ public static class ReaperCombatService
573 571 && !player.dead
574 572 && !player.CCed
575 573 && !player.noItems
576 && player.itemAnimation <= 0
577 && player.itemTime <= 0
578 && player.ownedProjectileCounts[ModContent.ProjectileType<SickleSwingProjectile>()] <= 0
579 574 && player.ownedProjectileCounts[ModContent.ProjectileType<ReaperActionControllerProjectile>()] <= 0;
580 575 }
581 576
577 private static void CancelPrimarySwingsForAction(Player player)
578 {
579 int swingType = ModContent.ProjectileType<SickleSwingProjectile>();
580 foreach (Projectile projectile in Main.ActiveProjectiles)
581 {
582 if (projectile.owner == player.whoAmI && projectile.type == swingType)
583 projectile.Kill();
584 }
585
586 int owner = player.whoAmI;
587 if (owner >= 0 && owner < Main.maxPlayers)
588 {
589 ReaperCombatSystem.HasActivePrimaryProjectile[owner] = false;
590 ReaperCombatSystem.ActivePrimaryProjectile[owner] = -1;
591 }
592 player.itemAnimation = 0;
593 player.itemTime = 0;
594 }
595
582 596 private static void SpawnLegacyProjectile<T>(IEntitySource source, int owner, Vector2 position, Vector2 velocity,
583 597 in SickleCombatSnapshot snapshot, float damageFactor, ReaperHitKind kind, int phase,
584 598 float ai0 = 0f, float ai1 = 0f, float ai2 = 0f) where T : ModProjectile
Modified Common/ReaperCrescentPrimitiveTextureSystem.cs +20 -0
@@ -30,6 +30,26 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
30 30 private static Texture2D? deathDomainBlade;
31 31 private static Texture2D? deathDomainRiftCrescent;
32 32
33 public override void PostSetupContent()
34 {
35 if (Main.dedServ)
36 return;
37
38 // These procedural atlases used to be built synchronously by the first
39 // Blood/Death swing. Prewarm them on the graphics thread after content
40 // setup so combat never pays several million samples and GPU uploads in
41 // a single frame. Dedicated servers remain completely texture-free.
42 Main.QueueMainThreadAction(() =>
43 {
44 DeathDomainBackdropTextureSystem.Prewarm();
45 for (int frame = 0; frame < BloodLifecycleFrames; frame++)
46 {
47 _ = GetBloodCrescent(frame);
48 _ = GetDeathDomainRimCrescent(frame);
49 }
50 });
51 }
52
33 53 internal static void DrawFormCrescent(
34 54 ReaperFormId form,
35 55 Vector2 center,
Modified Projectiles/ReaperActionControllerProjectile.cs +41 -10
@@ -18,6 +18,7 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
18 18 private const int BaseDashStartTick = 3;
19 19 private const int BaseDashTravelFrames = 7;
20 20 private const int VoidSpecialTeleportTick = 2;
21 private const int VoidSpecialVisualEndTick = 14;
21 22 private const int VoidUltimateBlackHoleTick = 32;
22 23 private SickleCombatSnapshot snapshot;
23 24 private Vector2 aim;
@@ -727,7 +728,11 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
727 728 case ReaperFormId.Void:
728 729 if (timer == VoidSpecialTeleportTick)
729 730 ExecuteVoidTeleportSpecial(player);
730 FinishAt(VoidSpecialTeleportTick);
731 // Keep the replicated controller alive long enough for clients to
732 // receive and render the cast. Killing it on the same tick as the
733 // server-side teleport made Void the only special with no held
734 // weapon, sound or opening presentation in multiplayer.
735 FinishAt(VoidSpecialVisualEndTick);
731 736 break;
732 737 }
733 738 }
@@ -994,11 +999,23 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
994 999 if (Main.netMode == NetmodeID.MultiplayerClient)
995 1000 return;
996 1001
997 Vector2 requestedTarget = player.Center + specialTargetOffset;
998 if (!ReaperMovementHelper.TeleportToTarget(player, requestedTarget, 2000f,
999 out Vector2 start, out Vector2 end))
1002 Vector2 start = player.Center;
1003 Vector2 requestedTarget = start + specialTargetOffset;
1004 bool teleported = ReaperMovementHelper.TeleportToTarget(player,
1005 requestedTarget, 2000f, out start, out Vector2 end);
1006
1007 // A blocked or very close cursor destination must not turn the whole
1008 // special into a silent no-op. Keep the server-authoritative movement
1009 // safety rule, but still tear the selected world-space path when there is
1010 // nowhere safe to place the player.
1011 if (!teleported)
1000 1012 {
1001 return;
1013 Vector2 fallback = requestedTarget - start;
1014 float fallbackLength = Math.Min(2000f, fallback.Length());
1015 Vector2 fallbackAxis = fallback.SafeNormalize(aim);
1016 if (fallbackLength <= 4f)
1017 fallbackLength = 160f;
1018 end = start + fallbackAxis * fallbackLength;
1002 1019 }
1003 1020
1004 1021 Vector2 path = end - start;
@@ -1207,7 +1224,7 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
1207 1224 ReaperFormId.Base => 28,
1208 1225 ReaperFormId.Bone => snapshot.Stage >= ReaperStage.StageIII ? 44 : snapshot.Stage >= ReaperStage.StageII ? 34 : 25,
1209 1226 ReaperFormId.Infernal => snapshot.Stage >= ReaperStage.StageIII ? 42 : snapshot.Stage >= ReaperStage.StageII ? 32 : 18,
1210 ReaperFormId.Void => VoidSpecialTeleportTick,
1227 ReaperFormId.Void => VoidSpecialVisualEndTick,
1211 1228 ReaperFormId.Death => 50,
1212 1229 _ => 30
1213 1230 };
@@ -1705,10 +1722,24 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
1705 1722
1706 1723 private void DrawVoidSpecial(Texture2D pixel, Vector2 grip)
1707 1724 {
1708 // The held blade now carves its own tip-locked scene tear and every spin
1709 // creates an actual persistent rift projectile. Do not overlay coordinate
1710 // boxes or pre-drawn straight cuts here: those divorced the visual from
1711 // the weapon pose and made the attack feel instantaneous.
1725 float reveal = SmoothStep(MathHelper.Clamp(timer / 5f, 0f, 1f));
1726 float fade = 1f - SmoothStep(MathHelper.Clamp(
1727 (timer - 8f) / Math.Max(1f, VoidSpecialVisualEndTick - 8f), 0f, 1f));
1728 float opacity = reveal * fade;
1729 if (opacity <= 0.001f)
1730 return;
1731
1732 // A compact world-sampled opening surrounds the held blade while the
1733 // authoritative persistent path arrives. It uses the same Void backdrop
1734 // renderer as ordinary and ultimate rifts, so there is no unrelated box
1735 // or flat purple placeholder.
1736 Vector2 bladeAxis = visualWeaponAngle.ToRotationVector2();
1737 Vector2 openingCenter = grip + bladeAxis * (92f + snapshot.StageNumber * 7f);
1738 ReaperVoidBackdropRenderer.DrawPersistentRift(Main.spriteBatch,
1739 openingCenter, bladeAxis, 118f + snapshot.StageNumber * 18f,
1740 20f + snapshot.StageNumber * 5f,
1741 Projectile.identity * 397 ^ Projectile.owner * 31,
1742 reveal, opacity * 0.88f);
1712 1743 }
1713 1744
1714 1745 private void DrawCoordinateGate(Texture2D pixel, Vector2 center, Vector2 forward,