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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

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

XFEstudio/DeathMod

重构虚空终结位移与死神领域斩

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

代码差异

11 个文件 +550 -119
Modified Common/DeathDomainBackdropTextureSystem.cs +26 -0
@@ -25,6 +25,20 @@ internal sealed class DeathDomainBackdropTextureSystem : ModSystem
25 25 return layers[index] ??= CreateLayer(index);
26 26 }
27 27
28 /// <summary>
29 /// Samples the exact three procedural plates used by the necklace domain and
30 /// composites them in premultiplied-alpha order. Death weapon masks use this
31 /// method so their interior is the domain itself, not a similarly colored
32 /// substitute texture.
33 /// </summary>
34 internal static Color SampleCompositePixel(int x, int y, float u, float v)
35 {
36 Color result = BuildFarPixel(x, y, u, v);
37 result = CompositeOver(result, BuildMiddlePixel(u, v));
38 result = CompositeOver(result, BuildNearPixel(u, v));
39 return result;
40 }
41
28 42 public override void Unload()
29 43 {
30 44 Texture2D?[] oldLayers = (Texture2D?[])layers.Clone();
@@ -188,6 +202,18 @@ internal sealed class DeathDomainBackdropTextureSystem : ModSystem
188 202 alpha);
189 203 }
190 204
205 private static Color CompositeOver(Color destination, Color source)
206 {
207 Vector4 under = destination.ToVector4();
208 Vector4 over = source.ToVector4();
209 float remaining = 1f - over.W;
210 return new Color(
211 MathHelper.Clamp(over.X + under.X * remaining, 0f, 1f),
212 MathHelper.Clamp(over.Y + under.Y * remaining, 0f, 1f),
213 MathHelper.Clamp(over.Z + under.Z * remaining, 0f, 1f),
214 MathHelper.Clamp(over.W + under.W * remaining, 0f, 1f));
215 }
216
191 217 private static float Wave(float u, float v, float horizontalCycles, float verticalCycles, float phase) =>
192 218 (float)Math.Sin(MathHelper.TwoPi * (u * horizontalCycles + v * verticalCycles) + phase);
193 219
Modified Common/MyPlayer.cs +2 -2
@@ -889,9 +889,9 @@ public class MyPlayer : ModPlayer
889 889 if (DeathMod.UltimateKeybind?.JustPressed == true)
890 890 {
891 891 if (Main.netMode == NetmodeID.MultiplayerClient)
892 DeathMod.SendReaperAction(Player, ultimate: true, aim);
892 DeathMod.SendReaperAction(Player, ultimate: true, cursorOffset);
893 893 else
894 ReaperCombatService.TryStartUltimate(Player, aim);
894 ReaperCombatService.TryStartUltimate(Player, cursorOffset);
895 895 }
896 896 }
897 897
Modified Common/ReaperCombatService.cs +15 -11
@@ -218,6 +218,8 @@ public static class ReaperCombatService
218 218 {
219 219 if (Main.netMode == NetmodeID.MultiplayerClient || !CanStartAction(player, out _))
220 220 return false;
221 if (!float.IsFinite(aim.X) || !float.IsFinite(aim.Y))
222 return false;
221 223
222 224 MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
223 225 ReaperFormId form = ReaperCombatRegistry.ResolveUsableForm(modPlayer.ReaperProgression);
@@ -231,7 +233,11 @@ public static class ReaperCombatService
231 233 int damage = Math.Max(1, player.GetWeaponDamage(player.HeldItem));
232 234 float knockback = player.GetWeaponKnockback(player.HeldItem, player.HeldItem.knockBack);
233 235 SickleCombatSnapshot snapshot = SickleCombatSnapshot.Capture(player, damage, knockback);
234 aim = aim.SafeNormalize(Vector2.UnitX * player.direction);
236 float targetDistance = aim.Length();
237 if (targetDistance > 2000f)
238 aim *= 2000f / targetDistance;
239 else if (targetDistance < 2f)
240 aim = Vector2.UnitX * player.direction * 120f;
235 241 int controllerIndex = ReaperActionControllerProjectile.Spawn(
236 242 player.GetSource_Misc("DeathMod:ReaperUltimate"), player, snapshot, aim, ultimate: true);
237 243 if (controllerIndex < 0)
@@ -253,16 +259,6 @@ public static class ReaperCombatService
253 259 int lastPhase = ReaperCombatRegistry.GetPrimaryPhaseCount(snapshot.Form, snapshot.Stage) - 1;
254 260 switch (snapshot.Form)
255 261 {
256 case ReaperFormId.Death
257 when swing.ModProjectile is SickleSwingProjectile deathSwing
258 && deathSwing.DeathSpaceBreak:
259 {
260 Vector2 cutDirection = deathSwing.CurrentWeaponDirection;
261 ReaperVoidRiftProjectile.Spawn(source, swing.owner, snapshot,
262 origin + cutDirection * 190f, cutDirection,
263 380f, 19f, 72, 0f, 0.34f, phase, swing.identity);
264 break;
265 }
266 262 case ReaperFormId.Bone when snapshot.Stage >= ReaperStage.StageIII && phase == lastPhase:
267 263 // Stage III closes the assembled execution frame in place. Keep the
268 264 // former wisp's damage budget, but do not launch a homing projectile
@@ -521,6 +517,14 @@ public static class ReaperCombatService
521 517 player.whoAmI, snapshot, target.Center,
522 518 data.AttackAngle.ToRotationVector2(), tempo,
523 519 data.ReaperActionId * 397 ^ target.whoAmI);
520 if (projectile.ModProjectile is SickleSwingProjectile deathPrimary
521 && deathPrimary.DeathSpaceBreak)
522 {
523 ReaperDeathDomainHarvestProjectile.Spawn(
524 projectile.GetSource_FromThis(), player.whoAmI,
525 snapshot, target, data.AttackAngle,
526 0.65f, data.ReaperActionId);
527 }
524 528 if (data.ReaperPhase == 1)
525 529 player.GetModPlayer<ReaperCombatPlayer>().ApplyDeathBloodHealing(damageDone);
526 530 if (data.ReaperPhase == 2)
Modified Common/ReaperCrescentPrimitiveTextureSystem.cs +200 -0
@@ -20,6 +20,9 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
20 20 private const int BloodLifecycleFrames = 16;
21 21
22 22 private static readonly Texture2D?[] bloodCrescents = new Texture2D?[BloodLifecycleFrames];
23 private static readonly Texture2D?[] deathDomainCrescents = new Texture2D?[BloodLifecycleFrames];
24 private static Color[]? deathDomainSurface;
25 private static Texture2D? deathDomainBlade;
23 26
24 27 internal static void DrawBloodCrescent(
25 28 Vector2 center,
@@ -70,11 +73,86 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
70 73 screenCenter, rotation, scale * pulse, Color.White * opacity, effects);
71 74 }
72 75
76 internal static void DrawDeathDomainCrescent(
77 Vector2 center,
78 float rotation,
79 float radius,
80 float opacity,
81 int motionLayers,
82 int swingDirection,
83 float time,
84 float lifeProgress)
85 {
86 if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
87 return;
88
89 lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
90 float lifecycleFrame = lifeProgress * (BloodLifecycleFrames - 1);
91 int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
92 BloodLifecycleFrames - 1);
93 int secondFrame = Math.Min(firstFrame + 1, BloodLifecycleFrames - 1);
94 float frameBlend = lifecycleFrame - firstFrame;
95 Texture2D firstTexture = GetDeathDomainCrescent(firstFrame);
96 Texture2D secondTexture = secondFrame == firstFrame
97 ? firstTexture
98 : GetDeathDomainCrescent(secondFrame);
99 float scale = GetScale(radius);
100 Vector2 screenCenter = center - Main.screenPosition;
101 motionLayers = Math.Clamp(motionLayers, 0, 4);
102 SpriteEffects effects = swingDirection < 0
103 ? SpriteEffects.FlipVertically
104 : SpriteEffects.None;
105
106 for (int layer = motionLayers; layer >= 1; layer--)
107 {
108 float layerOpacity = opacity * (0.042f + layer * 0.014f);
109 float lag = swingDirection * (0.016f + layer * 0.017f);
110 DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
111 screenCenter, rotation - lag, scale * (1f - layer * 0.014f),
112 new Color(194, 16, 61, 0) * layerOpacity, effects);
113 }
114
115 float pulse = 0.985f + (float)Math.Sin(time * 5.8f) * 0.012f;
116 DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
117 screenCenter, rotation, scale * 1.018f,
118 new Color(235, 20, 68, 0) * (opacity * 0.16f), effects);
119 DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
120 screenCenter, rotation, scale * pulse, Color.White * opacity, effects);
121 }
122
123 internal static void DrawDeathDomainBlade(Vector2 screenCenter, float rotation,
124 float length, float width, float opacity, float completion)
125 {
126 if (Main.dedServ || length <= 0.5f || width <= 0.5f
127 || opacity <= 0.001f || completion <= 0.001f)
128 {
129 return;
130 }
131 Texture2D texture = deathDomainBlade ??= CreateDeathDomainBlade();
132 completion = MathHelper.Clamp(completion, 0f, 1f);
133 float visibleLength = Math.Max(0.5f, length * completion);
134 float widthGrowth = MathHelper.Lerp(0.22f, 1f,
135 SmoothStep(0f, 0.55f, completion));
136 Vector2 start = screenCenter
137 - rotation.ToRotationVector2() * visibleLength * 0.5f;
138 Main.EntitySpriteDraw(texture, start, null, Color.White * opacity,
139 rotation, new Vector2(0f, texture.Height * 0.5f),
140 new Vector2(visibleLength / texture.Width,
141 width * widthGrowth / texture.Height),
142 SpriteEffects.None, 0f);
143 }
144
73 145 public override void Unload()
74 146 {
75 147 Texture2D?[] oldBlood = new Texture2D?[BloodLifecycleFrames];
76 148 Array.Copy(bloodCrescents, oldBlood, BloodLifecycleFrames);
149 Texture2D?[] oldDeath = new Texture2D?[BloodLifecycleFrames];
150 Array.Copy(deathDomainCrescents, oldDeath, BloodLifecycleFrames);
151 Texture2D? oldDeathBlade = deathDomainBlade;
77 152 Array.Clear(bloodCrescents, 0, bloodCrescents.Length);
153 Array.Clear(deathDomainCrescents, 0, deathDomainCrescents.Length);
154 deathDomainSurface = null;
155 deathDomainBlade = null;
78 156 if (Main.dedServ)
79 157 return;
80 158
@@ -82,6 +160,9 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
82 160 {
83 161 foreach (Texture2D? texture in oldBlood)
84 162 texture?.Dispose();
163 foreach (Texture2D? texture in oldDeath)
164 texture?.Dispose();
165 oldDeathBlade?.Dispose();
85 166 });
86 167 }
87 168
@@ -91,6 +172,125 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
91 172 frame / (float)(BloodLifecycleFrames - 1));
92 173 }
93 174
175 private static Texture2D GetDeathDomainCrescent(int frame)
176 {
177 return deathDomainCrescents[frame] ??= CreateDeathDomainCrescent(
178 frame / (float)(BloodLifecycleFrames - 1));
179 }
180
181 private static Texture2D CreateDeathDomainCrescent(float lifeProgress)
182 {
183 Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
184 false, SurfaceFormat.Color);
185 Color[] pixels = new Color[TextureSize * TextureSize];
186 Color[] surface = deathDomainSurface ??= CreateDeathDomainSurface();
187 float antialias = 3f / TextureSize;
188 float reveal = Smooth01(lifeProgress);
189
190 for (int y = 0; y < TextureSize; y++)
191 {
192 float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
193 for (int x = 0; x < TextureSize; x++)
194 {
195 float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
196 CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
197 halfSweep: 2.43f, maximumThickness: 0.405f,
198 innerWarp: 0.018f, asymmetry: 0.22f, antialias);
199 if (sample.Body <= 0.001f)
200 continue;
201 float revealMask = 1f - SmoothStep(reveal - 0.018f,
202 reveal + 0.012f, sample.Progress);
203 float body = sample.Body * revealMask;
204 if (body <= 0.001f)
205 continue;
206
207 Vector4 scene = surface[y * TextureSize + x].ToVector4();
208 Vector3 color = new(scene.X, scene.Y, scene.Z);
209 float alpha = body * MathHelper.Lerp(0.96f, 0.78f, sample.Depth);
210 float crimsonVein = Ridge((float)Math.Sin(sample.Progress * 21f
211 - sample.Depth * 18f + 0.9f), 9f);
212 color = Vector3.Lerp(color, new Vector3(0.66f, 0.008f, 0.09f),
213 crimsonVein * 0.18f * body);
214
215 float redRim = sample.OuterGlow * revealMask;
216 float whiteRim = sample.OuterHot * revealMask;
217 color = Vector3.Lerp(color, new Vector3(0.92f, 0.025f, 0.12f),
218 redRim * 0.78f);
219 color = Vector3.Lerp(color, new Vector3(1f, 0.88f, 0.84f),
220 whiteRim * 0.90f);
221 alpha = Math.Max(alpha, Math.Max(redRim * 0.90f,
222 whiteRim * 0.98f));
223 pixels[y * TextureSize + x] = Premultiplied(color, alpha);
224 }
225 }
226
227 texture.SetData(pixels);
228 return texture;
229 }
230
231 private static Color[] CreateDeathDomainSurface()
232 {
233 Color[] surface = new Color[TextureSize * TextureSize];
234 for (int y = 0; y < TextureSize; y++)
235 {
236 float v = (y + 0.5f) / TextureSize;
237 for (int x = 0; x < TextureSize; x++)
238 {
239 float u = (x + 0.5f) / TextureSize;
240 surface[y * TextureSize + x] =
241 DeathDomainBackdropTextureSystem.SampleCompositePixel(
242 x * 2, y * 2, u, v);
243 }
244 }
245 return surface;
246 }
247
248 private static Texture2D CreateDeathDomainBlade()
249 {
250 const int width = 1024;
251 const int height = 256;
252 Texture2D texture = new(Main.instance.GraphicsDevice, width, height,
253 false, SurfaceFormat.Color);
254 Color[] pixels = new Color[width * height];
255 for (int y = 0; y < height; y++)
256 {
257 float vertical = Math.Abs((y + 0.5f) / height * 2f - 1f);
258 float v = (y + 0.5f) / height;
259 for (int x = 0; x < width; x++)
260 {
261 float progress = (x + 0.5f) / width;
262 float taper = (float)Math.Pow(Math.Max(0f,
263 Math.Sin(progress * MathHelper.Pi)), 0.56f);
264 taper *= 1f
265 + (float)Math.Sin(progress * MathHelper.TwoPi * 5.1f) * 0.035f
266 + (float)Math.Sin(progress * MathHelper.TwoPi * 13.7f) * 0.014f;
267 float normalizedDistance = vertical / Math.Max(0.0001f, taper);
268 float body = 1f - SmoothStep(0.965f, 1.018f,
269 normalizedDistance);
270 if (body <= 0.001f)
271 continue;
272
273 float u = progress;
274 Vector4 sampled = DeathDomainBackdropTextureSystem
275 .SampleCompositePixel(x * 2, y * 3, u, v).ToVector4();
276 Vector3 color = new(sampled.X, sampled.Y, sampled.Z);
277 float redRim = SmoothStep(0.70f, 0.92f, normalizedDistance)
278 * (1f - SmoothStep(0.97f, 1.02f, normalizedDistance));
279 float whiteRim = SmoothStep(0.86f, 0.965f, normalizedDistance)
280 * (1f - SmoothStep(0.985f, 1.02f, normalizedDistance));
281 color = Vector3.Lerp(color, new Vector3(0.94f, 0.018f, 0.12f),
282 redRim * 0.86f);
283 color = Vector3.Lerp(color, new Vector3(1f, 0.90f, 0.86f),
284 whiteRim * 0.88f);
285 float alpha = Math.Max(body * 0.96f,
286 Math.Max(redRim * 0.92f, whiteRim));
287 pixels[y * width + x] = Premultiplied(color, alpha);
288 }
289 }
290 texture.SetData(pixels);
291 return texture;
292 }
293
94 294 private static Texture2D CreateBloodCrescent(float lifeProgress)
95 295 {
96 296 Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
Modified Common/ReaperMovementHelper.cs +59 -0
@@ -88,6 +88,65 @@ internal static class ReaperMovementHelper
88 88 return accepted >= distance - SweepStep;
89 89 }
90 90
91 /// <summary>
92 /// Performs a true target-point teleport. Unlike a dash it may cross solid
93 /// terrain, but the destination rectangle itself must be safe. If the exact
94 /// cursor point is occupied, the nearest safe point on the cursor-to-player
95 /// line is selected so the authoritative player is never embedded in tiles.
96 /// </summary>
97 internal static bool TeleportToTarget(Player player, Vector2 requestedCenter,
98 float maximumDistance, out Vector2 startCenter, out Vector2 endCenter)
99 {
100 startCenter = player.Center;
101 endCenter = startCenter;
102 if (Main.netMode == NetmodeID.MultiplayerClient
103 || !float.IsFinite(requestedCenter.X)
104 || !float.IsFinite(requestedCenter.Y)
105 || !float.IsFinite(maximumDistance)
106 || maximumDistance <= 0f)
107 {
108 return false;
109 }
110
111 Vector2 displacement = requestedCenter - startCenter;
112 float requestedDistance = displacement.Length();
113 if (requestedDistance <= 1f)
114 return false;
115 Vector2 direction = displacement / requestedDistance;
116 float acceptedDistance = Math.Min(maximumDistance, requestedDistance);
117 Vector2 acceptedPosition = startCenter + direction * acceptedDistance
118 - player.Size * 0.5f;
119
120 while (acceptedDistance > 4f)
121 {
122 Point tile = (acceptedPosition + player.Size * 0.5f).ToTileCoordinates();
123 if (WorldGen.InWorld(tile.X, tile.Y, 2)
124 && !Collision.SolidCollision(acceptedPosition, player.width, player.height))
125 {
126 break;
127 }
128 acceptedDistance -= SweepStep;
129 acceptedPosition = startCenter + direction * acceptedDistance
130 - player.Size * 0.5f;
131 }
132
133 if (acceptedDistance <= 4f
134 || Collision.SolidCollision(acceptedPosition, player.width, player.height))
135 {
136 return false;
137 }
138
139 player.position = acceptedPosition;
140 player.velocity = Vector2.Zero;
141 endCenter = player.Center;
142 if (Main.netMode == NetmodeID.Server)
143 {
144 player.GetModPlayer<ReaperMovementAuthorityPlayer>()
145 .BeginAuthoritativeMovement(player.position, player.velocity);
146 }
147 return true;
148 }
149
91 150 private static float ResolvePath(
92 151 Player player,
93 152 Vector2 direction,
Modified DeathMod.cs +1 -2
@@ -320,8 +320,7 @@ public class DeathMod : Mod
320 320 && float.IsFinite(aim.X) && float.IsFinite(aim.Y))
321 321 {
322 322 success = action == ReaperActionRequest.Ultimate
323 ? ReaperCombatService.TryStartUltimate(player,
324 aim.SafeNormalize(Vector2.UnitX * player.direction))
323 ? ReaperCombatService.TryStartUltimate(player, aim)
325 324 : ReaperCombatService.TryStartSpecial(player, aim);
326 325 }
327 326 else
Modified Localization/en-US.hjson +1 -0
@@ -156,6 +156,7 @@ Mods: {
156 156 ReaperInfernalTrailProjectile.DisplayName: Reaper Infernal Trail Projectile
157 157 ReaperBloodArcScarProjectile.DisplayName: Reaper Blood Arc Scar Projectile
158 158 ReaperDeathPhantomProjectile.DisplayName: Reaper Death Phantom Projectile
159 ReaperDeathDomainHarvestProjectile.DisplayName: Reaper Death Domain Harvest Projectile
159 160 }
160 161
161 162 UI: {
Modified Localization/zh-Hans.hjson +1 -0
@@ -156,6 +156,7 @@ Mods: {
156 156 // ReaperInfernalTrailProjectile.DisplayName: Reaper Infernal Trail Projectile
157 157 // ReaperBloodArcScarProjectile.DisplayName: Reaper Blood Arc Scar Projectile
158 158 // ReaperDeathPhantomProjectile.DisplayName: Reaper Death Phantom Projectile
159 // ReaperDeathDomainHarvestProjectile.DisplayName: Reaper Death Domain Harvest Projectile
159 160 }
160 161
161 162 UI: {
Modified Projectiles/ReaperActionControllerProjectile.cs +33 -94
@@ -42,9 +42,9 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
42 42 private readonly Vector2[] voidDashStarts = new Vector2[3];
43 43 private readonly Vector2[] voidDashEnds = new Vector2[3];
44 44 private readonly bool[] voidDashReady = new bool[3];
45 private Vector2 deathDashStart;
46 private Vector2 deathDashEnd;
47 private bool deathDashReady;
45 private Vector2 voidUltimateStart;
46 private Vector2 voidUltimateEnd;
47 private bool voidUltimatePathReady;
48 48 private float visualWeaponAngle;
49 49 private float previousVisualWeaponAngle;
50 50 private int visualSwingDirection = 1;
@@ -205,7 +205,7 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
205 205 WritePath(writer, infernalDashReady[index], infernalDashStarts[index], infernalDashEnds[index]);
206 206 for (int index = 0; index < voidDashReady.Length; index++)
207 207 WritePath(writer, voidDashReady[index], voidDashStarts[index], voidDashEnds[index]);
208 WritePath(writer, deathDashReady, deathDashStart, deathDashEnd);
208 WritePath(writer, voidUltimatePathReady, voidUltimateStart, voidUltimateEnd);
209 209 }
210 210
211 211 public override void ReceiveExtraAI(BinaryReader reader)
@@ -247,7 +247,8 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
247 247 ReadPath(reader, out incomingVoidReady[index],
248 248 out incomingVoidStarts[index], out incomingVoidEnds[index]);
249 249 }
250 ReadPath(reader, out bool incomingDeathReady, out Vector2 incomingDeathStart, out Vector2 incomingDeathEnd);
250 ReadPath(reader, out bool incomingVoidUltimateReady,
251 out Vector2 incomingVoidUltimateStart, out Vector2 incomingVoidUltimateEnd);
251 252 if (Main.netMode == NetmodeID.Server)
252 253 return;
253 254 configured = true;
@@ -278,9 +279,9 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
278 279 voidDashStarts[index] = incomingVoidStarts[index];
279 280 voidDashEnds[index] = incomingVoidEnds[index];
280 281 }
281 deathDashReady = incomingDeathReady;
282 deathDashStart = incomingDeathStart;
283 deathDashEnd = incomingDeathEnd;
282 voidUltimatePathReady = incomingVoidUltimateReady;
283 voidUltimateStart = incomingVoidUltimateStart;
284 voidUltimateEnd = incomingVoidUltimateEnd;
284 285 if (!wasConfigured)
285 286 {
286 287 // Initial replication establishes a floor: a late-joining client does
@@ -345,9 +346,9 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
345 346 Array.Clear(voidDashStarts);
346 347 Array.Clear(voidDashEnds);
347 348 Array.Clear(voidDashReady);
348 deathDashStart = Vector2.Zero;
349 deathDashEnd = Vector2.Zero;
350 deathDashReady = false;
349 voidUltimateStart = Vector2.Zero;
350 voidUltimateEnd = Vector2.Zero;
351 voidUltimatePathReady = false;
351 352 playedVisualEvents = 0UL;
352 353 previousVisualTimer = -1;
353 354 previousVisualReleaseTimer = -1;
@@ -746,16 +747,6 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
746 747 }
747 748 FinishAt(57);
748 749 break;
749 case ReaperFormId.Death:
750 for (int index = 0; index < 6; index++)
751 StrikeDeathPathAt(4 + index * 5, player, index);
752 if (timer == 3)
753 StartDeathDash(player);
754 Vector2 finisherOrigin = deathDashReady ? deathDashEnd : origin;
755 StrikeAt(38, player, finisherOrigin - aim * 210f, aim,
756 ReaperStrikeShape.Line, 420f, 62f, 1.1f, 6);
757 FinishAt(50);
758 break;
759 750 }
760 751 }
761 752
@@ -839,14 +830,25 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
839 830 case ReaperFormId.Void:
840 831 if (timer == 32 && Main.netMode != NetmodeID.MultiplayerClient)
841 832 {
842 Vector2 riftAxis = GetVoidUltimateAxis();
843 Vector2 riftCenter = origin + aim * 150f;
844 int level = Math.Max(1, (int)snapshot.Skill3);
845 ReaperVoidRiftProjectile.Spawn(Projectile.GetSource_FromThis(),
846 player.whoAmI, snapshot, riftCenter, riftAxis,
847 2400f, 92f + level * 12f,
848 ReaperVoidArcRiftProjectile.GetLifetime(level),
849 520f + level * 45f, 0.34f, 0, Projectile.identity);
833 Vector2 requestedTarget = player.Center + specialTargetOffset;
834 voidUltimatePathReady = ReaperMovementHelper.TeleportToTarget(
835 player, requestedTarget, 2000f,
836 out voidUltimateStart, out voidUltimateEnd);
837 if (voidUltimatePathReady)
838 {
839 Vector2 displacement = voidUltimateEnd - voidUltimateStart;
840 float riftLength = displacement.Length();
841 Vector2 riftAxis = displacement.SafeNormalize(aim);
842 Vector2 riftCenter = (voidUltimateStart + voidUltimateEnd) * 0.5f;
843 int level = Math.Max(1, (int)snapshot.Skill3);
844 ReaperVoidRiftProjectile.Spawn(Projectile.GetSource_FromThis(),
845 player.whoAmI, snapshot, riftCenter, riftAxis,
846 riftLength, 92f + level * 12f,
847 ReaperVoidArcRiftProjectile.GetLifetime(level),
848 520f + level * 45f, 0.34f, 0,
849 Projectile.identity);
850 }
851 Projectile.netUpdate = true;
850 852 }
851 853 FinishAt(220);
852 854 break;
@@ -873,17 +875,6 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
873 875 origin, direction, shape, length, width, multiplier, actionId: Projectile.identity);
874 876 }
875 877
876 private Vector2 GetVoidUltimateAxis()
877 {
878 uint value = unchecked((uint)(Projectile.identity * 747796405));
879 value ^= value >> 16;
880 value *= 0x7FEB352Du;
881 value ^= value >> 15;
882 float random = (value & 0x00FFFFFFu) / 16777215f;
883 float offset = MathHelper.Lerp(-1.12f, 1.12f, random);
884 return aim.RotatedBy(offset).SafeNormalize(aim);
885 }
886
887 878 private void StrikeDeathWorldCutAt(int eventTick, Player player, int cutIndex)
888 879 {
889 880 if (timer != eventTick || Main.netMode == NetmodeID.MultiplayerClient)
@@ -1080,16 +1071,6 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
1080 1071 Projectile.netUpdate = true;
1081 1072 }
1082 1073
1083 private void StartDeathDash(Player player)
1084 {
1085 if (Main.netMode == NetmodeID.MultiplayerClient)
1086 return;
1087 float acceptedDistance = ReaperMovementHelper.MoveAlong(
1088 player, aim, 18f * 16f, out deathDashStart, out deathDashEnd);
1089 deathDashReady = acceptedDistance > 0f;
1090 Projectile.netUpdate = true;
1091 }
1092
1093 1074 private void StrikeInfernalPathAt(int eventTick, Player player, int phase, float width, float multiplier)
1094 1075 {
1095 1076 if (timer != eventTick || Main.netMode == NetmodeID.MultiplayerClient
@@ -1106,19 +1087,6 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
1106 1087 Math.Max(9f, width * 0.72f), phase, Projectile.identity);
1107 1088 }
1108 1089
1109 private void StrikeDeathPathAt(int eventTick, Player player, int phase)
1110 {
1111 if (timer != eventTick || Main.netMode == NetmodeID.MultiplayerClient)
1112 return;
1113 Vector2 position = deathDashReady
1114 ? Vector2.Lerp(deathDashStart, deathDashEnd, (phase + 0.5f) / 6f)
1115 : player.MountedCenter;
1116 Vector2 cutDirection = aim.RotatedBy(MathHelper.PiOver2);
1117 ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI, snapshot,
1118 ReaperHitKind.Special, phase, position - cutDirection * 48f, cutDirection,
1119 ReaperStrikeShape.Line, 96f, 24f, 0.55f, actionId: Projectile.identity);
1120 }
1121
1122 1090 private void FinishAt(int finishTick)
1123 1091 {
1124 1092 if (timer >= finishTick)
@@ -1762,37 +1730,10 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
1762 1730
1763 1731 private void DrawDeathSpecial(Texture2D pixel, Vector2 grip)
1764 1732 {
1765 if (deathDashReady)
1766 {
1767 float pathOpacity = WindowOpacity(timer, 2, 31, 12);
1768 DrawVisualLine(pixel, deathDashStart, deathDashEnd,
1769 new Color(20, 0, 8, 0) * pathOpacity * 0.86f, 30f);
1770 DrawVisualLine(pixel, deathDashStart, deathDashEnd,
1771 new Color(220, 20, 48, 0) * pathOpacity * 0.38f, 8f);
1772 Vector2 cutDirection = aim.RotatedBy(MathHelper.PiOver2);
1773 for (int phase = 0; phase < 6; phase++)
1774 {
1775 int eventTick = 4 + phase * 5;
1776 float opacity = WindowOpacity(timer, eventTick - 2, 4, 8);
1777 if (opacity <= 0f)
1778 continue;
1779 Vector2 position = Vector2.Lerp(deathDashStart, deathDashEnd, (phase + 0.5f) / 6f);
1780 Color phaseColor = GetDeathPhaseColor(phase) with { A = 0 };
1781 DrawVisualLine(pixel, position - cutDirection * (64f + phase * 3f),
1782 position + cutDirection * (64f + phase * 3f),
1783 Color.Black * opacity * 0.72f, 23f);
1784 DrawVisualLine(pixel, position - cutDirection * (64f + phase * 3f),
1785 position + cutDirection * (64f + phase * 3f),
1786 phaseColor * opacity * 0.88f, 8f);
1787 DrawRegularPolygon(pixel, position, 18f + phase, 6,
1788 phaseColor * opacity * 0.7f, 2.4f, phase * 0.31f);
1789 }
1790 }
1791
1792 1733 float finisher = WindowOpacity(timer, 35, 5, 11);
1793 1734 if (finisher > 0f)
1794 1735 {
1795 Vector2 center = deathDashReady ? deathDashEnd : grip;
1736 Vector2 center = grip;
1796 1737 DrawArc(pixel, center - aim * 65f, 235f, aim.ToRotation() - 1.55f, 2.95f,
1797 1738 Color.Black * finisher * 0.9f, 86f, 20);
1798 1739 DrawArc(pixel, center - aim * 65f, 238f, aim.ToRotation() - 1.55f, 2.95f,
@@ -2198,9 +2139,7 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
2198 2139 int eventTick = 4 + phase * 5;
2199 2140 if (!ReachedVisualTick(eventTick) || !ConsumeVisualEvent(4 + phase))
2200 2141 continue;
2201 Vector2 eventPosition = deathDashReady
2202 ? Vector2.Lerp(deathDashStart, deathDashEnd, (phase + 0.5f) / 6f)
2203 : position;
2142 Vector2 eventPosition = position;
2204 2143 PlayTuned(SoundID.Item71, eventPosition, 0.58f + phase * 0.045f,
2205 2144 -0.16f + phase * 0.09f, 0.04f);
2206 2145 if (phase is 0 or 2 or 4)
Added Projectiles/ReaperDeathDomainHarvestProjectile.cs +211 -0
@@ -0,0 +1,211 @@
1 using DeathMod.Common;
2 using Microsoft.Xna.Framework;
3 using Microsoft.Xna.Framework.Graphics;
4 using System;
5 using System.IO;
6 using Terraria;
7 using Terraria.Audio;
8 using Terraria.ID;
9 using Terraria.ModLoader;
10
11 namespace DeathMod.Projectiles;
12
13 /// <summary>
14 /// The max-tempo Death primary follow-up. It uses the necklace harvest's delayed
15 /// authoritative strike and hit-stop, while rendering the cut as a window into
16 /// the Death Domain instead of a black blade core.
17 /// </summary>
18 public sealed class ReaperDeathDomainHarvestProjectile : ModProjectile
19 {
20 private const int ImpactFrame = 5;
21 private const int Lifetime = 60;
22 private SickleCombatSnapshot snapshot;
23 private int targetIndex = -1;
24 private int age;
25 private int actionId;
26 private float slashLength;
27 private float slashWidth;
28 private bool impacted;
29 private bool configured;
30 private bool serverAuthorized;
31
32 public override string Texture => "Terraria/Images/Projectile_0";
33
34 internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner,
35 in SickleCombatSnapshot snapshot, NPC target, float rotation,
36 float damageMultiplier, int actionId)
37 {
38 if (Main.netMode == NetmodeID.MultiplayerClient || !target.active)
39 return -1;
40 int damage = Math.Max(1, (int)Math.Round(snapshot.Damage * damageMultiplier));
41 int index = Projectile.NewProjectile(source, target.Center, Vector2.Zero,
42 ModContent.ProjectileType<ReaperDeathDomainHarvestProjectile>(),
43 damage, 0f, owner);
44 if (index < 0 || index >= Main.maxProjectiles)
45 return -1;
46 Projectile projectile = Main.projectile[index];
47 projectile.originalDamage = snapshot.Damage;
48 projectile.rotation = rotation;
49 if (projectile.ModProjectile is ReaperDeathDomainHarvestProjectile harvest)
50 harvest.Configure(snapshot, target, actionId);
51 projectile.netUpdate = true;
52 return index;
53 }
54
55 public override void SetStaticDefaults()
56 {
57 ProjectileID.Sets.DrawScreenCheckFluff[Type] = 3600;
58 }
59
60 public override void SetDefaults()
61 {
62 Projectile.width = 2;
63 Projectile.height = 2;
64 Projectile.friendly = false;
65 Projectile.hostile = false;
66 Projectile.tileCollide = false;
67 Projectile.ignoreWater = true;
68 Projectile.penetrate = -1;
69 Projectile.timeLeft = Lifetime;
70 Projectile.netImportant = true;
71 }
72
73 public override bool ShouldUpdatePosition() => false;
74 public override bool? CanDamage() => false;
75
76 public override void AI()
77 {
78 if (Main.netMode == NetmodeID.Server && !serverAuthorized)
79 {
80 Projectile.Kill();
81 return;
82 }
83 if (!configured)
84 return;
85 age++;
86 if (targetIndex >= 0 && targetIndex < Main.maxNPCs
87 && Main.npc[targetIndex].active)
88 {
89 Projectile.Center = Main.npc[targetIndex].Center;
90 }
91
92 if (!impacted && age >= ImpactFrame)
93 {
94 impacted = true;
95 if (Projectile.owner >= 0 && Projectile.owner < Main.maxPlayers)
96 {
97 Main.player[Projectile.owner].GetModPlayer<MyPlayer>()
98 .ApplyDeathDomainStrike(targetIndex, Projectile.damage, 0);
99 }
100 if (Main.netMode != NetmodeID.Server)
101 SpawnImpactFeedback();
102 }
103 }
104
105 public override bool PreDraw(ref Color lightColor)
106 {
107 if (!configured || Main.dedServ)
108 return false;
109 float progress = age / (float)Lifetime;
110 float reveal = Smooth01((progress - 0.008f) / 0.125f);
111 float disappear = 1f - Smooth01((progress - 0.15f) / 0.81f);
112 float strength = Smooth01((progress - 0.008f) / 0.055f) * disappear;
113 float impactPulse = 1f + (float)Math.Exp(-Math.Pow(
114 (progress - ImpactFrame / (float)Lifetime) / 0.034f, 2f)) * 0.24f;
115 ReaperCrescentPrimitiveTextureSystem.DrawDeathDomainBlade(
116 Projectile.Center - Main.screenPosition, Projectile.rotation,
117 slashLength, slashWidth * impactPulse, strength, reveal);
118 return false;
119 }
120
121 public override void SendExtraAI(BinaryWriter writer)
122 {
123 writer.Write(configured);
124 if (!configured)
125 return;
126 snapshot.Write(writer);
127 writer.Write((short)targetIndex);
128 writer.Write(actionId);
129 writer.Write(slashLength);
130 writer.Write(slashWidth);
131 writer.Write((byte)Math.Clamp(age, 0, byte.MaxValue));
132 writer.Write(impacted);
133 writer.Write(Projectile.rotation);
134 }
135
136 public override void ReceiveExtraAI(BinaryReader reader)
137 {
138 bool incoming = reader.ReadBoolean();
139 if (!incoming)
140 {
141 if (Main.netMode != NetmodeID.Server)
142 configured = false;
143 return;
144 }
145 SickleCombatSnapshot incomingSnapshot = SickleCombatSnapshot.Read(reader);
146 int incomingTarget = reader.ReadInt16();
147 int incomingAction = reader.ReadInt32();
148 float incomingLength = reader.ReadSingle();
149 float incomingWidth = reader.ReadSingle();
150 int incomingAge = reader.ReadByte();
151 bool incomingImpacted = reader.ReadBoolean();
152 float incomingRotation = reader.ReadSingle();
153 if (Main.netMode == NetmodeID.Server)
154 return;
155 snapshot = incomingSnapshot;
156 targetIndex = incomingTarget;
157 actionId = incomingAction;
158 slashLength = incomingLength;
159 slashWidth = incomingWidth;
160 age = incomingAge;
161 impacted = incomingImpacted;
162 Projectile.rotation = incomingRotation;
163 configured = true;
164 }
165
166 private void Configure(in SickleCombatSnapshot value, NPC target, int sourceActionId)
167 {
168 snapshot = value;
169 targetIndex = target.whoAmI;
170 actionId = sourceActionId;
171 float scale = MathHelper.Clamp(Math.Max(target.width, target.height) / 52f,
172 0.85f, 2.4f);
173 slashLength = 296f * scale;
174 slashWidth = 34f * scale;
175 age = 0;
176 impacted = false;
177 configured = true;
178 serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
179 Projectile.Center = target.Center;
180 Projectile.timeLeft = Lifetime;
181 }
182
183 private void SpawnImpactFeedback()
184 {
185 Vector2 direction = Projectile.rotation.ToRotationVector2();
186 Vector2 normal = direction.RotatedBy(MathHelper.PiOver2);
187 for (int index = 0; index < 20; index++)
188 {
189 Dust dust = Dust.NewDustPerfect(Projectile.Center
190 + direction * Main.rand.NextFloat(-slashLength * 0.07f,
191 slashLength * 0.07f),
192 index % 4 == 0 ? DustID.Shadowflame : DustID.RedTorch,
193 normal * Main.rand.NextFloat(-4.2f, 4.2f)
194 + direction * Main.rand.NextFloat(3.5f, 8.5f),
195 70, new Color(246, 18, 66), Main.rand.NextFloat(0.72f, 1.25f));
196 dust.noGravity = true;
197 }
198 Main.LocalPlayer.GetModPlayer<MyPlayer>().TriggerDeathDomainImpact(
199 Projectile.Center, normal, 3.8f);
200 Main.LocalPlayer.GetModPlayer<MyPlayer>().PlayDeathDomainSlashSound(
201 Projectile.Center, 1f, requiem: false);
202 SoundEngine.PlaySound(SoundID.Item71 with { Volume = 0.42f, Pitch = -0.38f },
203 Projectile.Center);
204 }
205
206 private static float Smooth01(float value)
207 {
208 value = MathHelper.Clamp(value, 0f, 1f);
209 return value * value * (3f - 2f * value);
210 }
211 }
Modified Projectiles/SickleSwingProjectile.cs +1 -10
@@ -51,8 +51,6 @@ public sealed class SickleSwingProjectile : ModProjectile
51 51 internal float DeathTempo => deathTempo;
52 52 internal bool DeathSpaceBreak => snapshot.Form == ReaperFormId.Death
53 53 && deathTempo >= 2.58f;
54 internal Vector2 CurrentWeaponDirection => weaponAngle.ToRotationVector2();
55
56 54 private int ItemType => (int)Projectile.ai[2];
57 55 private int SwingDirection => Math.Sign(Projectile.ai[0]) == 0 ? 1 : Math.Sign(Projectile.ai[0]);
58 56 private int VisualStage => snapshot.Form == ReaperFormId.Death ? 3 : snapshot.StageNumber;
@@ -526,13 +524,6 @@ public sealed class SickleSwingProjectile : ModProjectile
526 524 player.velocity = rising;
527 525 return;
528 526 }
529 if (snapshot.Form == ReaperFormId.Death && phase == 2)
530 {
531 Vector2 deathDash = aimRotation.ToRotationVector2() * 96f / Math.Max(1f, duration * 0.45f);
532 if (!Collision.SolidCollision(player.position + deathDash, player.width, player.height))
533 player.velocity = deathDash;
534 return;
535 }
536 527 if (snapshot.Form == ReaperFormId.Infernal)
537 528 {
538 529 float distance = snapshot.Stage switch
@@ -944,7 +935,7 @@ public sealed class SickleSwingProjectile : ModProjectile
944 935 - normal * direction * reach * 0.10f;
945 936 float rotation = aimRotation
946 937 + MathHelper.Lerp(-0.11f, 0.09f, SmoothStep(animationProgress)) * direction;
947 ReaperCrescentPrimitiveTextureSystem.DrawBloodCrescent(center, rotation,
938 ReaperCrescentPrimitiveTextureSystem.DrawDeathDomainCrescent(center, rotation,
948 939 reach, visibility * 0.92f, 4, direction,
949 940 Main.GlobalTimeWrappedHourly, animationProgress);
950 941 }