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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

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

XFEstudio/DeathMod

完善死神镰刀大招镜面破碎与伤害汇总

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

代码差异

10 个文件 +443 -68
Modified Common/DeathDomainBackdropTextureSystem.cs +40 -0
@@ -80,6 +80,46 @@ internal sealed class DeathDomainBackdropTextureSystem : ModSystem
80 80 internal static float GetLayerOpacity(int layer)
81 81 => layer switch { 0 => 1f, 1 => 0.9f, _ => 0.96f };
82 82
83 /// <summary>
84 /// Draws the same world-anchored plates used by the descended Death Domain
85 /// into a physical-screen rectangle. The ultimate uses this behind displaced
86 /// mirror shards so every opening reveals the real domain at its normal scale.
87 /// </summary>
88 internal static void DrawScreenAlignedBackdrop(SpriteBatch batch,
89 Rectangle destination, float opacity)
90 {
91 if (Main.dedServ || destination.Width <= 0 || destination.Height <= 0
92 || opacity <= 0.001f)
93 {
94 return;
95 }
96
97 Vector2 worldCenter = Main.screenPosition
98 + new Vector2(Main.screenWidth, Main.screenHeight) * 0.5f;
99 Vector2 aperture = new(Main.screenWidth, Main.screenHeight);
100 for (int layer = 0; layer < 3; layer++)
101 {
102 Texture2D texture = GetLayer(layer);
103 GetSourceFrame(layer, worldCenter, aperture,
104 out float sourceX, out float sourceY,
105 out float sourceWidth, out float sourceHeight);
106 int width = Math.Clamp((int)MathF.Round(sourceWidth), 1,
107 texture.Width);
108 int height = Math.Clamp((int)MathF.Round(sourceHeight), 1,
109 texture.Height);
110 Rectangle source = new(
111 Math.Clamp((int)MathF.Round(sourceX), 0,
112 texture.Width - width),
113 Math.Clamp((int)MathF.Round(sourceY), 0,
114 texture.Height - height),
115 width,
116 height);
117 batch.Draw(texture, destination, source,
118 Color.White * (MathHelper.Clamp(opacity, 0f, 1f)
119 * GetLayerOpacity(layer)));
120 }
121 }
122
83 123 /// <summary>Returns the necklace's source rectangle for a world aperture.</summary>
84 124 internal static void GetSourceFrame(int layer, Vector2 worldCenter,
85 125 Vector2 apertureSize, out float sourceOffsetX,
Modified Common/MyPlayer.cs +31 -9
@@ -71,14 +71,20 @@ public class MyPlayer : ModPlayer
71 71 private readonly List<PendingReaperNecklaceHarvest> pendingReaperNecklaceHarvests = [];
72 72
73 73 private readonly record struct PendingDeathDomainCut(float Rotation, int ProjectileIndex);
74 private readonly record struct PendingDeathDomainStrike(int NpcIndex, int RemainingFrames, int Damage, int LifeStealLevel);
74 private readonly record struct PendingDeathDomainStrike(
75 int NpcIndex,
76 int RemainingFrames,
77 int Damage,
78 int LifeStealLevel,
79 int DeathUltimateActionId);
75 80 private readonly record struct PendingReaperNecklaceHarvest(
76 81 int NpcIndex,
77 82 int RemainingFrames,
78 83 float Rotation,
79 84 DeathNecklace Necklace,
80 85 float VisualMastery,
81 bool Requiem);
86 bool Requiem,
87 int DeathUltimateActionId);
82 88
83 89 public override void Initialize()
84 90 {
@@ -980,7 +986,7 @@ public class MyPlayer : ModPlayer
980 986 }
981 987
982 988 internal void QueueDeathNecklaceHarvestFromReaperHit(NPC target,
983 float rotation)
989 float rotation, int deathUltimateActionId = -1)
984 990 {
985 991 if (Main.netMode == NetmodeID.MultiplayerClient || !target.active)
986 992 return;
@@ -1031,7 +1037,8 @@ public class MyPlayer : ModPlayer
1031 1037 rotation,
1032 1038 necklace,
1033 1039 visualMastery,
1034 requiem));
1040 requiem,
1041 deathUltimateActionId));
1035 1042 }
1036 1043
1037 1044 private void UpdatePendingReaperNecklaceHarvests()
@@ -1066,7 +1073,8 @@ public class MyPlayer : ModPlayer
1066 1073 continue;
1067 1074 }
1068 1075 SpawnDeathDomainHarvestSlash(target, pending.Necklace,
1069 pending.VisualMastery, pending.Requiem, pending.Rotation);
1076 pending.VisualMastery, pending.Requiem, pending.Rotation,
1077 pending.DeathUltimateActionId);
1070 1078 }
1071 1079 }
1072 1080
@@ -1172,7 +1180,9 @@ public class MyPlayer : ModPlayer
1172 1180 }
1173 1181 }
1174 1182
1175 private void SpawnDeathDomainHarvestSlash(NPC target, DeathNecklace necklace, float visualMastery, bool requiem, float rotation)
1183 private void SpawnDeathDomainHarvestSlash(NPC target, DeathNecklace necklace,
1184 float visualMastery, bool requiem, float rotation,
1185 int deathUltimateActionId = -1)
1176 1186 {
1177 1187 int slashDamage = CalculateDeathDomainSlashDamage(target, necklace);
1178 1188 float scale = MathHelper.Clamp(Math.Max(target.width, target.height) / 52f, 0.85f, 2.4f);
@@ -1187,7 +1197,8 @@ public class MyPlayer : ModPlayer
1187 1197 DeathDomainHarvestSlashProjectile.SlashImpactFrame
1188 1198 + slash * DeathDomainHarvestSlashProjectile.SlashDelayFrames,
1189 1199 slashDamage,
1190 necklace.LifeStealLevel));
1200 necklace.LifeStealLevel,
1201 deathUltimateActionId));
1191 1202 }
1192 1203
1193 1204 DeathMod.BroadcastDeathDomainHarvestSlash(
@@ -1219,6 +1230,8 @@ public class MyPlayer : ModPlayer
1219 1230 visual.rotation = rotation;
1220 1231 visual.scale = scale;
1221 1232 visual.localAI[1] = necklace.LifeStealLevel;
1233 if (visual.ModProjectile is DeathDomainHarvestSlashProjectile harvestSlash)
1234 harvestSlash.SetDeathUltimateActionId(deathUltimateActionId);
1222 1235 }
1223 1236
1224 1237 private void UpdatePendingDeathDomainStrikes()
@@ -1237,11 +1250,13 @@ public class MyPlayer : ModPlayer
1237 1250 }
1238 1251
1239 1252 pendingDeathDomainStrikes.RemoveAt(index);
1240 ApplyDeathDomainStrike(pending.NpcIndex, pending.Damage, pending.LifeStealLevel);
1253 ApplyDeathDomainStrike(pending.NpcIndex, pending.Damage,
1254 pending.LifeStealLevel, pending.DeathUltimateActionId);
1241 1255 }
1242 1256 }
1243 1257
1244 internal void ApplyDeathDomainStrike(int npcIndex, int damage, int lifeStealLevel)
1258 internal void ApplyDeathDomainStrike(int npcIndex, int damage,
1259 int lifeStealLevel, int deathUltimateActionId = -1)
1245 1260 {
1246 1261 if (npcIndex < 0 || npcIndex >= Main.maxNPCs)
1247 1262 return;
@@ -1273,6 +1288,13 @@ public class MyPlayer : ModPlayer
1273 1288 Vector2 lifeStealSource = target.Center;
1274 1289 target.StrikeNPC(harvestHit, fromNet: false, noPlayerInteraction: false);
1275 1290 int actualDamage = Math.Max(0, lifeBeforeHit - Math.Max(0, target.life));
1291 if (actualDamage > 0 && deathUltimateActionId >= 0)
1292 {
1293 NPC statusTarget = ReaperCombatService.GetStatusTarget(target);
1294 statusTarget.GetGlobalNPC<ReaperCombatGlobalNPC>()
1295 .RecordDeathUltimateCut(statusTarget, Player.whoAmI,
1296 deathUltimateActionId, actualDamage);
1297 }
1276 1298 ApplyDeathReaperLifeSteal(actualDamage, lifeStealSource, lifeStealLevel);
1277 1299 if (Main.netMode == NetmodeID.Server)
1278 1300 {
Modified Common/ReaperCombatEntities.cs +8 -2
@@ -710,8 +710,8 @@ public sealed class ReaperCombatGlobalNPC : GlobalNPC
710 710 deathUltimateActionId[owner] = actionId;
711 711 deathUltimateAccumulatedDamage[owner] = 0;
712 712 }
713 deathUltimateAccumulatedDamage[owner] = Math.Min(int.MaxValue / 4,
714 deathUltimateAccumulatedDamage[owner] + damageDone);
713 deathUltimateAccumulatedDamage[owner] = (int)Math.Min(int.MaxValue / 4L,
714 deathUltimateAccumulatedDamage[owner] + (long)damageDone);
715 715 if (deathUltimateStopTimer <= 0)
716 716 deathUltimateStoredVelocity = npc.velocity;
717 717 deathUltimateStopTimer = Math.Max(deathUltimateStopTimer, (short)180);
@@ -719,6 +719,12 @@ public sealed class ReaperCombatGlobalNPC : GlobalNPC
719 719 npc.netUpdate = true;
720 720 }
721 721
722 internal bool IsTrackedByDeathUltimate(int owner, int actionId)
723 => owner >= 0 && owner < Main.maxPlayers
724 && actionId >= 0
725 && deathUltimateActionId[owner] == actionId
726 && deathUltimateAccumulatedDamage[owner] > 0;
727
722 728 internal int ConsumeDeathUltimateDamage(NPC npc, int owner, int actionId)
723 729 {
724 730 if (owner < 0 || owner >= Main.maxPlayers
Modified Common/ReaperCombatService.cs +5 -0
@@ -522,8 +522,13 @@ public static class ReaperCombatService
522 522 {
523 523 player.GetModPlayer<ReaperCombatPlayer>().ApplyDeathUltimateHealing(damageDone);
524 524 if (data.ReaperPhase < 60)
525 {
525 526 status.RecordDeathUltimateCut(statusTarget, player.whoAmI,
526 527 data.ReaperActionId, damageDone);
528 player.GetModPlayer<MyPlayer>()
529 .QueueDeathNecklaceHarvestFromReaperHit(target,
530 data.AttackAngle, data.ReaperActionId);
531 }
527 532 }
528 533 if (data.ReaperHitKind == ReaperHitKind.Primary)
529 534 {
Added Common/ReaperDeathUltimateGeometry.cs +36 -0
@@ -0,0 +1,36 @@
1 using Microsoft.Xna.Framework;
2 using Terraria;
3
4 namespace DeathMod.Common;
5
6 /// <summary>
7 /// Shared, deterministic geometry and timing for the Death Reaper ultimate.
8 /// Combat, persistent world cuts and the final mirror shatter must all consume
9 /// these values so the screen can only break along cuts that actually happened.
10 /// </summary>
11 internal static class ReaperDeathUltimateGeometry
12 {
13 internal const int CutCount = 18;
14 internal const int FirstCutTick = 20;
15 internal const int CutInterval = 6;
16 internal const int ShatterTick = 172;
17 internal const int Duration = 190;
18 internal const float CutLength = 5200f;
19 internal const float CutCollisionWidth = 11f;
20 internal const float CutVisualWidth = 60f;
21
22 internal static int GetCutTick(int cutIndex)
23 => FirstCutTick + cutIndex * CutInterval;
24
25 internal static Vector2 GetCutAxis(int actionId, int cutIndex)
26 {
27 uint hash = unchecked((uint)(actionId * 747796405
28 + cutIndex * 2891336453));
29 hash ^= hash >> 16;
30 hash *= 0x7FEB352Du;
31 hash ^= hash >> 15;
32 float angle = (hash & 0x00FFFFFFu) / 16777215f
33 * MathHelper.Pi - MathHelper.PiOver2;
34 return angle.ToRotationVector2();
35 }
36 }
Modified Common/ReaperUltimateVisualSystem.cs +291 -29
@@ -24,6 +24,11 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
24 24 private static UltimateVisualState[] states = [];
25 25 private static Asset<Texture2D>? soulTexture;
26 26 private static CrescentMaskSet[] crescentMasks = [];
27 private static readonly float[] deathMirrorAngles =
28 new float[ReaperDeathUltimateGeometry.CutCount * 2];
29 private static readonly VertexPositionColorTexture[] deathMirrorVertices =
30 new VertexPositionColorTexture[ReaperDeathUltimateGeometry.CutCount * 6];
31 private static BasicEffect? deathMirrorEffect;
27 32
28 33 /// <summary>
29 34 /// Reports one frame of an active ultimate. This method is safe to call from
@@ -107,8 +112,12 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
107 112 // graphics thread just like the other procedural texture systems.
108 113 CrescentMaskSet[] oldMasks = crescentMasks;
109 114 crescentMasks = [];
115 BasicEffect? oldMirrorEffect = deathMirrorEffect;
116 deathMirrorEffect = null;
110 117 if (!Main.dedServ && oldMasks.Length > 0)
111 118 Main.QueueMainThreadAction(() => DisposeCrescentMasks(oldMasks));
119 if (!Main.dedServ && oldMirrorEffect is not null)
120 Main.QueueMainThreadAction(oldMirrorEffect.Dispose);
112 121 }
113 122
114 123 public override void OnWorldUnload()
@@ -191,7 +200,7 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
191 200 continue;
192 201
193 202 strongestDarkness = Math.Max(strongestDarkness,
194 GetBackdropDarkness(state.Form) * GetStateOpacity(in state));
203 GetBackdropDarkness(in state) * GetStateOpacity(in state));
195 204 }
196 205
197 206 if (strongestDarkness <= 0.001f)
@@ -210,7 +219,7 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
210 219 if (!IsVisible(index, in state, viewport, uiScale))
211 220 continue;
212 221
213 DrawState(batch, pixel, in state, viewport, viewportBounds, uiScale);
222 DrawState(batch, pixel, index, in state, viewport, viewportBounds, uiScale);
214 223 }
215 224
216 225 return true;
@@ -233,6 +242,7 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
233 242 private static void DrawState(
234 243 SpriteBatch batch,
235 244 Texture2D pixel,
245 int ownerIndex,
236 246 in UltimateVisualState state,
237 247 Vector2 viewport,
238 248 Rectangle viewportBounds,
@@ -287,7 +297,8 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
287 297 time, primary, secondary, state.Aim, state.ActionId);
288 298 break;
289 299 case ReaperFormId.Death:
290 DrawDeath(batch, pixel, focus, viewport, progress, opacity, time);
300 DrawDeath(batch, pixel, ownerIndex, in state, focus, viewport,
301 viewportBounds, progress, opacity, time);
291 302 break;
292 303 }
293 304 }
@@ -354,12 +365,14 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
354 365 TriggerImpactAt(164, previousTimer, currentTimer, direction, 5.2f, secondary, 0.14f);
355 366 break;
356 367 case ReaperFormId.Death:
357 for (int index = 0; index < 18; index++)
358 TriggerImpactAt(20 + index * 6, previousTimer, currentTimer,
368 for (int index = 0; index < ReaperDeathUltimateGeometry.CutCount; index++)
369 TriggerImpactAt(ReaperDeathUltimateGeometry.GetCutTick(index),
370 previousTimer, currentTimer,
359 371 direction.RotatedBy(index * 2.39996f), 4.6f + index * 0.08f,
360 372 new Color(225, 25, 66), 0.14f);
361 TriggerImpactAt(150, previousTimer, currentTimer, direction,
362 12f, new Color(255, 112, 136), 0.62f);
373 TriggerImpactAt(ReaperDeathUltimateGeometry.ShatterTick,
374 previousTimer, currentTimer, direction,
375 12f, new Color(190, 14, 48), 0.14f);
363 376 break;
364 377 }
365 378 }
@@ -874,24 +887,263 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
874 887 private static void DrawDeath(
875 888 SpriteBatch batch,
876 889 Texture2D pixel,
890 int ownerIndex,
891 in UltimateVisualState state,
877 892 Vector2 focus,
878 893 Vector2 viewport,
894 Rectangle viewportBounds,
879 895 float progress,
880 896 float opacity,
881 897 float time)
882 898 {
883 // The eighteen damaging projectiles own the world-space, domain-filled
884 // cuts and their track-aligned shatter. This layer is atmosphere only;
885 // screen-space blades or radial shards would drift with the camera and
886 // would not correspond to any preceding cut.
887 Rectangle screen = new(0, 0, (int)Math.Ceiling(viewport.X),
888 (int)Math.Ceiling(viewport.Y));
889 float suspended = Envelope(progress, 0.08f, 0.14f, 0.87f, 0.92f) * opacity;
890 batch.Draw(pixel, screen, new Color(2, 0, 4) * (0.46f * suspended));
891 DrawGlow(batch, focus, 220f, new Color(120, 0, 38) * (suspended * 0.22f));
892
893 float shatter = Envelope(progress, 0.87f, 0.89f, 0.985f, 1f) * opacity;
894 batch.Draw(pixel, screen, new Color(18, 0, 8) * (shatter * 0.12f));
899 float shatterStart = ReaperDeathUltimateGeometry.ShatterTick
900 / (float)ReaperDeathUltimateGeometry.Duration;
901 float isolation = Envelope(progress, 0.58f, 0.79f,
902 shatterStart - 0.015f, shatterStart + 0.012f) * opacity;
903 Texture2D? capturedWorld = Main.screenTarget;
904
905 // Each successful cut removes more of the ordinary scene. Immediately
906 // before the shatter, only small windows around the caster and recorded
907 // victims are restored from Terraria's world render target.
908 if (isolation > 0.001f)
909 {
910 batch.Draw(pixel, viewportBounds,
911 new Color(0, 0, 1) * (isolation * 0.965f));
912 if (capturedWorld is not null && !capturedWorld.IsDisposed)
913 DrawDeathUltimateActors(batch, capturedWorld, ownerIndex,
914 state.ActionId, viewport, isolation);
915 }
916
917 float shatter = Reveal(progress, shatterStart, 0.985f) * opacity;
918 if (shatter > 0.001f)
919 {
920 // The actual Death Necklace plates fill the void behind the broken
921 // game image; no substitute gradient or black fill is used here.
922 DeathDomainBackdropTextureSystem.DrawScreenAlignedBackdrop(batch,
923 viewportBounds, shatter);
924 if (capturedWorld is not null && !capturedWorld.IsDisposed)
925 DrawDeathMirrorShards(batch, capturedWorld, focus, viewport,
926 state.ActionId, shatter, opacity);
927 }
928
929 DrawDeathUltimateCutCracks(batch, pixel, in state, focus, viewport,
930 shatter, opacity);
931 DrawGlow(batch, focus, 230f,
932 new Color(125, 0, 34) * ((0.12f + shatter * 0.18f) * opacity));
933 _ = time;
934 }
935
936 private static void DrawDeathUltimateActors(SpriteBatch batch,
937 Texture2D capturedWorld, int ownerIndex, int actionId,
938 Vector2 viewport, float opacity)
939 {
940 if (ownerIndex < 0 || ownerIndex >= Main.maxPlayers)
941 return;
942 Player owner = Main.player[ownerIndex];
943 if (owner.active && !owner.dead)
944 DrawCapturedActorWindow(batch, capturedWorld, owner.getRect(),
945 viewport, opacity, 34f);
946
947 HashSet<int> drawnRoots = [];
948 foreach (NPC npc in Main.ActiveNPCs)
949 {
950 int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
951 if (root < 0 || root >= Main.maxNPCs || !drawnRoots.Add(root))
952 continue;
953 NPC target = Main.npc[root];
954 if (!target.active
955 || !target.GetGlobalNPC<ReaperCombatGlobalNPC>()
956 .IsTrackedByDeathUltimate(ownerIndex, actionId))
957 {
958 continue;
959 }
960 DrawCapturedActorWindow(batch, capturedWorld, target.Hitbox,
961 viewport, opacity, 28f);
962 }
963 }
964
965 private static void DrawCapturedActorWindow(SpriteBatch batch,
966 Texture2D capturedWorld, Rectangle worldBounds, Vector2 viewport,
967 float opacity, float padding)
968 {
969 Rectangle screenBounds = new(
970 (int)Math.Floor(worldBounds.X - Main.screenPosition.X - padding),
971 (int)Math.Floor(worldBounds.Y - Main.screenPosition.Y - padding),
972 Math.Max(1, (int)Math.Ceiling(worldBounds.Width + padding * 2f)),
973 Math.Max(1, (int)Math.Ceiling(worldBounds.Height + padding * 2f)));
974 Rectangle clipped = Rectangle.Intersect(screenBounds,
975 new Rectangle(0, 0, (int)viewport.X, (int)viewport.Y));
976 if (clipped.Width <= 0 || clipped.Height <= 0)
977 return;
978
979 const int bands = 12;
980 for (int band = 0; band < bands; band++)
981 {
982 float y0 = band / (float)bands;
983 float y1 = (band + 1f) / bands;
984 float normalizedY = (y0 + y1) - 1f;
985 float halfWidth = (float)Math.Sqrt(Math.Max(0f,
986 1f - normalizedY * normalizedY));
987 int bandTop = screenBounds.Top
988 + (int)Math.Floor(screenBounds.Height * y0);
989 int bandBottom = screenBounds.Top
990 + (int)Math.Ceiling(screenBounds.Height * y1);
991 int bandHalfWidth = Math.Max(1,
992 (int)Math.Ceiling(screenBounds.Width * 0.5f * halfWidth));
993 Rectangle destination = new(
994 screenBounds.Center.X - bandHalfWidth,
995 bandTop,
996 bandHalfWidth * 2,
997 Math.Max(1, bandBottom - bandTop));
998 destination = Rectangle.Intersect(destination, clipped);
999 if (destination.Width <= 0 || destination.Height <= 0)
1000 continue;
1001
1002 Rectangle source = new(
1003 (int)Math.Floor(destination.X / viewport.X * capturedWorld.Width),
1004 (int)Math.Floor(destination.Y / viewport.Y * capturedWorld.Height),
1005 Math.Max(1, (int)Math.Ceiling(destination.Width / viewport.X
1006 * capturedWorld.Width)),
1007 Math.Max(1, (int)Math.Ceiling(destination.Height / viewport.Y
1008 * capturedWorld.Height)));
1009 source = Rectangle.Intersect(source, capturedWorld.Bounds);
1010 if (source.Width > 0 && source.Height > 0)
1011 batch.Draw(capturedWorld, destination, source,
1012 Color.White * MathHelper.Clamp(opacity, 0f, 1f));
1013 }
1014 }
1015
1016 private static void DrawDeathMirrorShards(SpriteBatch batch,
1017 Texture2D capturedWorld, Vector2 focus, Vector2 viewport,
1018 int actionId, float shatter, float opacity)
1019 {
1020 if (Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
1021 return;
1022
1023 int rayCount = deathMirrorAngles.Length;
1024 for (int cut = 0; cut < ReaperDeathUltimateGeometry.CutCount; cut++)
1025 {
1026 float angle = ReaperDeathUltimateGeometry.GetCutAxis(actionId, cut)
1027 .ToRotation();
1028 if (angle < 0f)
1029 angle += MathHelper.TwoPi;
1030 deathMirrorAngles[cut * 2] = angle;
1031 deathMirrorAngles[cut * 2 + 1] = PositiveModulo(
1032 angle + MathHelper.Pi, MathHelper.TwoPi);
1033 }
1034 Array.Sort(deathMirrorAngles);
1035
1036 float eased = Ease(shatter);
1037 float radius = viewport.Length() * 1.35f;
1038 Color shardColor = Color.White
1039 * (opacity * MathHelper.Lerp(1f, 0.48f, eased));
1040 int vertex = 0;
1041 for (int shard = 0; shard < rayCount; shard++)
1042 {
1043 float startAngle = deathMirrorAngles[shard];
1044 float endAngle = shard == rayCount - 1
1045 ? deathMirrorAngles[0] + MathHelper.TwoPi
1046 : deathMirrorAngles[shard + 1];
1047 float middleAngle = (startAngle + endAngle) * 0.5f;
1048 uint hash = unchecked((uint)(actionId * 16777619
1049 + shard * 2246822519));
1050 hash ^= hash >> 15;
1051 float random = (hash & 0xFFFFu) / 65535f;
1052 float signed = ((hash >> 16) & 1u) == 0u ? -1f : 1f;
1053 Vector2 displacement = middleAngle.ToRotationVector2()
1054 * ((18f + random * 48f) * eased)
1055 + Vector2.UnitY * (eased * eased * (8f + random * 34f));
1056 float rotation = signed * (0.012f + random * 0.032f) * eased;
1057
1058 Vector2 originalCenter = focus;
1059 Vector2 originalStart = focus
1060 + startAngle.ToRotationVector2() * radius;
1061 Vector2 originalEnd = focus
1062 + endAngle.ToRotationVector2() * radius;
1063 WriteMirrorVertex(ref vertex,
1064 RotatePoint(originalCenter, focus, rotation) + displacement,
1065 originalCenter, viewport, shardColor);
1066 WriteMirrorVertex(ref vertex,
1067 RotatePoint(originalStart, focus, rotation) + displacement,
1068 originalStart, viewport, shardColor);
1069 WriteMirrorVertex(ref vertex,
1070 RotatePoint(originalEnd, focus, rotation) + displacement,
1071 originalEnd, viewport, shardColor);
1072 }
1073
1074 batch.End();
1075 try
1076 {
1077 deathMirrorEffect ??= new BasicEffect(graphicsDevice)
1078 {
1079 TextureEnabled = true,
1080 VertexColorEnabled = true,
1081 LightingEnabled = false,
1082 FogEnabled = false
1083 };
1084 deathMirrorEffect.World = Matrix.Identity;
1085 deathMirrorEffect.View = Matrix.Identity;
1086 deathMirrorEffect.Projection = Matrix.CreateOrthographicOffCenter(
1087 0f, viewport.X, viewport.Y, 0f, 0f, 1f);
1088 deathMirrorEffect.Texture = capturedWorld;
1089 graphicsDevice.BlendState = BlendState.AlphaBlend;
1090 graphicsDevice.DepthStencilState = DepthStencilState.None;
1091 graphicsDevice.RasterizerState = RasterizerState.CullNone;
1092 graphicsDevice.SamplerStates[0] = SamplerState.LinearClamp;
1093 foreach (EffectPass pass in deathMirrorEffect.CurrentTechnique.Passes)
1094 {
1095 pass.Apply();
1096 graphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleList,
1097 deathMirrorVertices, 0, vertex / 3);
1098 }
1099 }
1100 finally
1101 {
1102 batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend,
1103 SamplerState.LinearClamp, DepthStencilState.None,
1104 RasterizerState.CullNone, null, Matrix.Identity);
1105 }
1106 }
1107
1108 private static void WriteMirrorVertex(ref int index, Vector2 position,
1109 Vector2 samplePosition, Vector2 viewport, Color color)
1110 {
1111 Vector2 uv = new(
1112 samplePosition.X / Math.Max(1f, viewport.X),
1113 samplePosition.Y / Math.Max(1f, viewport.Y));
1114 deathMirrorVertices[index++] = new VertexPositionColorTexture(
1115 new Vector3(position, 0f), color, uv);
1116 }
1117
1118 private static Vector2 RotatePoint(Vector2 point, Vector2 origin,
1119 float rotation)
1120 => origin + (point - origin).RotatedBy(rotation);
1121
1122 private static void DrawDeathUltimateCutCracks(SpriteBatch batch,
1123 Texture2D pixel, in UltimateVisualState state, Vector2 focus,
1124 Vector2 viewport, float shatter, float opacity)
1125 {
1126 float radius = viewport.Length() * 0.72f;
1127 for (int cut = 0; cut < ReaperDeathUltimateGeometry.CutCount; cut++)
1128 {
1129 int cutTick = ReaperDeathUltimateGeometry.GetCutTick(cut);
1130 if (state.Timer < cutTick)
1131 continue;
1132 float age = state.Timer - cutTick;
1133 float fresh = (float)Math.Exp(-age / 13f);
1134 float strength = (0.12f + fresh * 0.38f + shatter * 0.74f)
1135 * opacity;
1136 Vector2 axis = ReaperDeathUltimateGeometry.GetCutAxis(
1137 state.ActionId, cut);
1138 Vector2 start = focus - axis * radius;
1139 Vector2 end = focus + axis * radius;
1140 DrawLayeredLine(batch, pixel, start, end,
1141 new Color(32, 0, 15),
1142 Color.Lerp(new Color(235, 12, 62), Color.White,
1143 shatter * 0.48f),
1144 4f + shatter * 3f, 0.9f + shatter * 0.8f,
1145 strength);
1146 }
895 1147 }
896 1148
897 1149 private static void DrawDeathLegacy(
@@ -1741,17 +1993,27 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
1741 1993 return introduction * Ease(tail);
1742 1994 }
1743 1995
1744 private static float GetBackdropDarkness(ReaperFormId form) => form switch
1996 private static float GetBackdropDarkness(in UltimateVisualState state)
1745 1997 {
1746 ReaperFormId.Bone => 0.36f,
1747 ReaperFormId.Blood => 0.42f,
1748 ReaperFormId.Infernal => 0.38f,
1749 ReaperFormId.Frost => 0.31f,
1750 ReaperFormId.Soul => 0.37f,
1751 ReaperFormId.Void => 0.52f,
1752 ReaperFormId.Death => 0.49f,
1753 _ => 0f
1754 };
1998 if (state.Form == ReaperFormId.Death)
1999 {
2000 float cutProgress = MathHelper.Clamp(
2001 (state.Timer - ReaperDeathUltimateGeometry.FirstCutTick)
2002 / (float)(ReaperDeathUltimateGeometry.ShatterTick
2003 - ReaperDeathUltimateGeometry.FirstCutTick), 0f, 1f);
2004 return MathHelper.Lerp(0.30f, 0.985f, Ease(cutProgress));
2005 }
2006 return state.Form switch
2007 {
2008 ReaperFormId.Bone => 0.36f,
2009 ReaperFormId.Blood => 0.42f,
2010 ReaperFormId.Infernal => 0.38f,
2011 ReaperFormId.Frost => 0.31f,
2012 ReaperFormId.Soul => 0.37f,
2013 ReaperFormId.Void => 0.52f,
2014 _ => 0f
2015 };
2016 }
1755 2017
1756 2018 private static float Reveal(float progress, float start, float end)
1757 2019 {
Modified Projectiles/DeathDomainHarvestSlashProjectile.cs +6 -1
@@ -22,6 +22,7 @@ public class DeathDomainHarvestSlashProjectile : ModProjectile
22 22 internal const int SlashImpactFrame = 5;
23 23 private const int EndLingerFrames = 7;
24 24 private const int MaximumSlashCount = 5;
25 private int deathUltimateActionId = -1;
25 26
26 27 private int TargetIndex => (int)Projectile.ai[0];
27 28 private int SlashCount => Math.Clamp((int)Projectile.ai[1], 1, MaximumSlashCount);
@@ -94,7 +95,8 @@ public class DeathDomainHarvestSlashProjectile : ModProjectile
94 95 .ApplyDeathDomainStrike(
95 96 TargetIndex,
96 97 Projectile.damage,
97 Main.netMode == NetmodeID.MultiplayerClient ? 0 : (int)Projectile.localAI[1]);
98 Main.netMode == NetmodeID.MultiplayerClient ? 0 : (int)Projectile.localAI[1],
99 deathUltimateActionId);
98 100 }
99 101 if (createClientEffects)
100 102 SpawnExecutionBurst(slash);
@@ -231,5 +233,8 @@ public class DeathDomainHarvestSlashProjectile : ModProjectile
231 233 return value * value * (3f - 2f * value);
232 234 }
233 235
236 internal void SetDeathUltimateActionId(int actionId)
237 => deathUltimateActionId = Math.Max(-1, actionId);
238
234 239 private static float GetSlashReveal(float progress) => Smooth01((progress - 0.008f) / 0.125f);
235 240 }
Modified Projectiles/ReaperActionControllerProjectile.cs +17 -17
@@ -778,7 +778,7 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
778 778 ReaperFormId.Frost => 120,
779 779 ReaperFormId.Soul => 116,
780 780 ReaperFormId.Void => 220,
781 ReaperFormId.Death => 170,
781 ReaperFormId.Death => ReaperDeathUltimateGeometry.Duration,
782 782 _ => 1
783 783 };
784 784 Vector2 origin = player.MountedCenter;
@@ -873,11 +873,13 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
873 873 FinishAt(220);
874 874 break;
875 875 case ReaperFormId.Death:
876 for (int index = 0; index < 18; index++)
877 StrikeDeathWorldCutAt(20 + index * 6, player, focus, index);
878 if (timer == 150 && Main.netMode != NetmodeID.MultiplayerClient)
876 for (int index = 0; index < ReaperDeathUltimateGeometry.CutCount; index++)
877 StrikeDeathWorldCutAt(ReaperDeathUltimateGeometry.GetCutTick(index),
878 player, focus, index);
879 if (timer == ReaperDeathUltimateGeometry.ShatterTick
880 && Main.netMode != NetmodeID.MultiplayerClient)
879 881 ExecuteDeathUltimateShatter(player);
880 FinishAt(170);
882 FinishAt(ReaperDeathUltimateGeometry.Duration);
881 883 break;
882 884 default:
883 885 Projectile.Kill();
@@ -900,17 +902,13 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
900 902 {
901 903 if (timer != eventTick || Main.netMode == NetmodeID.MultiplayerClient)
902 904 return;
903 uint hash = unchecked((uint)(Projectile.identity * 747796405 + cutIndex * 2891336453));
904 hash ^= hash >> 16;
905 hash *= 0x7FEB352Du;
906 hash ^= hash >> 15;
907 float randomAngle = (hash & 0x00FFFFFFu) / 16777215f * MathHelper.Pi
908 - MathHelper.PiOver2;
909 Vector2 axis = randomAngle.ToRotationVector2();
910 Vector2 start = focus - axis * 2600f;
905 Vector2 axis = ReaperDeathUltimateGeometry.GetCutAxis(
906 Projectile.identity, cutIndex);
907 Vector2 start = focus - axis * (ReaperDeathUltimateGeometry.CutLength * 0.5f);
911 908 ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI,
912 909 snapshot, ReaperHitKind.Ultimate, cutIndex, start, axis,
913 ReaperStrikeShape.Line, 5200f, 22f, 0.28f,
910 ReaperStrikeShape.Line, ReaperDeathUltimateGeometry.CutLength,
911 ReaperDeathUltimateGeometry.CutCollisionWidth, 0.28f,
914 912 actionId: Projectile.identity);
915 913 }
916 914
@@ -2307,7 +2305,8 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
2307 2305 4 => SoundID.Item8,
2308 2306 _ => SoundID.Item122
2309 2307 }, GetDeathPhaseColor(index), 5.8f);
2310 PlayUltimateFinal(listener, 156, 12, new Color(255, 230, 215), -0.38f);
2308 PlayUltimateFinal(listener, ReaperDeathUltimateGeometry.ShatterTick,
2309 12, new Color(190, 14, 48), -0.38f, flashStrength: 0.12f);
2311 2310 break;
2312 2311 }
2313 2312 }
@@ -2323,14 +2322,15 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
2323 2322 }
2324 2323
2325 2324 private void PlayUltimateFinal(Vector2 listener, int eventTick, int eventIndex,
2326 Color flash, float pitch)
2325 Color flash, float pitch, float flashStrength = 0.68f)
2327 2326 {
2328 2327 if (!ReachedVisualTick(eventTick) || !ConsumeVisualEvent(eventIndex))
2329 2328 return;
2330 2329 PlayTuned(SoundID.Item122, listener, 1f, pitch, 0.02f);
2331 2330 PlayTuned(SoundID.Item14, listener, 0.76f, pitch + 0.14f, 0.025f);
2332 2331 PlayTuned(SoundID.NPCDeath6, listener, 0.45f, pitch - 0.12f, 0.02f);
2333 ReaperVfxDirector.TriggerGlobalImpact(aim, 12f, 14, flash, 0.68f, 12, 0.58f);
2332 ReaperVfxDirector.TriggerGlobalImpact(aim, 12f, 14, flash,
2333 MathHelper.Clamp(flashStrength, 0f, 0.68f), 12, 0.58f);
2334 2334 }
2335 2335
2336 2336 private void SpawnSpecialAmbientDust(Player player)
Modified Projectiles/ReaperDeathShatterProjectile.cs +2 -3
@@ -8,7 +8,7 @@ using Terraria.ModLoader;
8 8
9 9 namespace DeathMod.Projectiles;
10 10
11 /// <summary>Target-locked shatter worth exactly three times the stored cut total.</summary>
11 /// <summary>Target-locked shatter worth exactly the stored cut-and-domain total.</summary>
12 12 public sealed class ReaperDeathShatterProjectile : ModProjectile
13 13 {
14 14 private SickleCombatSnapshot snapshot;
@@ -25,8 +25,7 @@ public sealed class ReaperDeathShatterProjectile : ModProjectile
25 25 {
26 26 if (Main.netMode == NetmodeID.MultiplayerClient || accumulatedDamage <= 0)
27 27 return -1;
28 int damage = Math.Max(1, (int)Math.Min(int.MaxValue / 4L,
29 accumulatedDamage * 3L));
28 int damage = Math.Max(1, Math.Min(int.MaxValue / 4, accumulatedDamage));
30 29 int index = Projectile.NewProjectile(source, target.Center, Vector2.Zero,
31 30 ModContent.ProjectileType<ReaperDeathShatterProjectile>(), damage,
32 31 snapshot.Knockback * 0.2f, owner);
Modified Projectiles/ReaperStrikeProjectile.cs +7 -7
@@ -13,7 +13,6 @@ namespace DeathMod.Projectiles;
13 13 public sealed class ReaperStrikeProjectile : ModProjectile
14 14 {
15 15 private const int DeathCutPointCount = 65;
16 private const float DeathUltimateCutWidth = 120f;
17 16 private readonly Vector2[] deathCutPoints = new Vector2[DeathCutPointCount];
18 17 private SickleCombatSnapshot snapshot;
19 18 private ReaperHitKind hitKind;
@@ -254,9 +253,10 @@ public sealed class ReaperStrikeProjectile : ModProjectile
254 253 {
255 254 if (!IsDeathUltimateWorldCut)
256 255 return delay + 8;
257 // Every cut remains fixed in the world until the controller's frame-150
258 // shatter. Cut N is born at 20 + N*6, hence this remaining lifetime.
259 return Math.Max(18, 132 - phase * 6);
256 // Every cut remains fixed in the world until the shared shatter tick,
257 // then fractures for the rest of the controller timeline.
258 return Math.Max(18, ReaperDeathUltimateGeometry.ShatterTick
259 - ReaperDeathUltimateGeometry.GetCutTick(phase) + 18);
260 260 }
261 261
262 262 private void RecordDeathUltimateWorldCut(int lifetime)
@@ -269,12 +269,12 @@ public sealed class ReaperStrikeProjectile : ModProjectile
269 269 }
270 270 float completion = Smooth01(age / 4f);
271 271 float fracture = Smooth01(MathHelper.Clamp(
272 (age - (lifetime - 12f)) / 12f, 0f, 1f));
272 (age - (lifetime - 18f)) / 18f, 0f, 1f));
273 273 float terminalFade = 1f - Smooth01(MathHelper.Clamp(
274 274 (fracture - 0.78f) / 0.22f, 0f, 1f));
275 275 DeathDomainTrailVisualSystem.Record(Projectile.owner,
276 276 Projectile.identity, deathCutPoints,
277 Math.Max(DeathUltimateCutWidth, width * 3.1f),
277 Math.Max(ReaperDeathUltimateGeometry.CutVisualWidth, width * 3.1f),
278 278 completion * terminalFade * 0.98f,
279 279 mergeOverlappingRims: true, fracture: fracture);
280 280 }
@@ -283,7 +283,7 @@ public sealed class ReaperStrikeProjectile : ModProjectile
283 283 {
284 284 int lifetime = GetVisualLifetime();
285 285 float fracture = Smooth01(MathHelper.Clamp(
286 (age - (lifetime - 12f)) / 12f, 0f, 1f));
286 (age - (lifetime - 18f)) / 18f, 0f, 1f));
287 287 if (fracture <= 0.001f)
288 288 return;
289 289