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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

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

XFEstudio/DeathMod

统一镰刀刀光并重构死亡领域裂缝

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

代码差异

16 个文件 +1241 -372
Added Common/DeathDomainCrescentVisualSystem.cs +238 -0
@@ -0,0 +1,238 @@
1 using Microsoft.Xna.Framework;
2 using Microsoft.Xna.Framework.Graphics;
3 using System;
4 using System.Collections.Generic;
5 using Terraria;
6 using Terraria.ModLoader;
7
8 namespace DeathMod.Common;
9
10 /// <summary>
11 /// Draws the Death scythe's domain-filled face as geometry rather than as one
12 /// rotating sprite. The crescent mask follows the weapon, while its texture
13 /// coordinates stay anchored to world space; rapid swings therefore reveal a
14 /// stable window into the Death Necklace domain instead of rotating the scene.
15 /// </summary>
16 [Autoload(Side = ModSide.Client)]
17 internal sealed class DeathDomainCrescentVisualSystem : ModSystem
18 {
19 private const int AngularSegments = 128;
20 private const int DepthBands = 18;
21 private const float HalfSweep = 2.43f;
22 private const float OuterRadiusRatio = 0.91f;
23 private const float MaximumThicknessRatio = 0.405f;
24
25 private static readonly Dictionary<long, CrescentDrawState> drawStates = [];
26 private static BasicEffect? effect;
27 private static VertexPositionColorTexture[] vertices =
28 new VertexPositionColorTexture[(AngularSegments + 1) * (DepthBands + 1)];
29 private static short[] indices = CreateIndices();
30
31 private readonly record struct CrescentDrawState(
32 Vector2 Center,
33 float Rotation,
34 float Radius,
35 float Opacity,
36 int SwingDirection,
37 float LifeProgress,
38 ulong UpdateTick);
39
40 internal static void Record(int owner, int identity, Vector2 center,
41 float rotation, float radius, float opacity, int swingDirection,
42 float lifeProgress)
43 {
44 if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
45 return;
46
47 long key = ((long)owner << 32) | (uint)identity;
48 drawStates[key] = new CrescentDrawState(center, rotation, radius,
49 opacity, swingDirection < 0 ? -1 : 1,
50 MathHelper.Clamp(lifeProgress, 0f, 1f), Main.GameUpdateCount);
51 }
52
53 public override void PostUpdateEverything()
54 {
55 if (drawStates.Count == 0)
56 return;
57
58 List<long>? stale = null;
59 foreach ((long key, CrescentDrawState state) in drawStates)
60 {
61 if (Main.GameUpdateCount <= state.UpdateTick + 1)
62 continue;
63 stale ??= [];
64 stale.Add(key);
65 }
66 if (stale is null)
67 return;
68 foreach (long key in stale)
69 drawStates.Remove(key);
70 }
71
72 public override void OnWorldUnload() => drawStates.Clear();
73
74 public override void Unload()
75 {
76 drawStates.Clear();
77 BasicEffect? oldEffect = effect;
78 effect = null;
79 vertices = [];
80 indices = [];
81 if (oldEffect is not null && !Main.dedServ)
82 Main.QueueMainThreadAction(oldEffect.Dispose);
83 }
84
85 public override void PostDrawTiles()
86 {
87 if (Main.gameMenu || drawStates.Count == 0
88 || Main.graphics?.GraphicsDevice is not GraphicsDevice graphicsDevice)
89 {
90 return;
91 }
92
93 effect ??= new BasicEffect(graphicsDevice)
94 {
95 TextureEnabled = true,
96 VertexColorEnabled = true,
97 LightingEnabled = false,
98 FogEnabled = false
99 };
100 effect.World = Main.GameViewMatrix.TransformationMatrix;
101 effect.View = Matrix.Identity;
102 effect.Projection = Matrix.CreateOrthographicOffCenter(
103 0f, Main.screenWidth, Main.screenHeight, 0f, 0f, 1f);
104
105 graphicsDevice.BlendState = BlendState.AlphaBlend;
106 graphicsDevice.DepthStencilState = DepthStencilState.None;
107 graphicsDevice.RasterizerState = RasterizerState.CullNone;
108 graphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
109
110 foreach (CrescentDrawState state in drawStates.Values)
111 {
112 if (Main.GameUpdateCount > state.UpdateTick + 1)
113 continue;
114 BuildVertices(state);
115 DrawBackdropLayers(graphicsDevice);
116 }
117 }
118
119 private static void DrawBackdropLayers(GraphicsDevice graphicsDevice)
120 {
121 if (effect is null)
122 return;
123
124 for (int layer = 0; layer < 3; layer++)
125 {
126 effect.Texture = DeathDomainBackdropTextureSystem.GetLayer(layer);
127 foreach (EffectPass pass in effect.CurrentTechnique.Passes)
128 {
129 pass.Apply();
130 graphicsDevice.DrawUserIndexedPrimitives(
131 PrimitiveType.TriangleList,
132 vertices,
133 0,
134 vertices.Length,
135 indices,
136 0,
137 indices.Length / 3);
138 }
139 }
140 }
141
142 private static void BuildVertices(CrescentDrawState state)
143 {
144 float reveal = Smooth01(state.LifeProgress / 0.42f);
145 float erosion = Smooth01((state.LifeProgress - 0.30f) / 0.66f);
146 float rotationCos = (float)Math.Cos(state.Rotation);
147 float rotationSin = (float)Math.Sin(state.Rotation);
148
149 int vertexIndex = 0;
150 for (int segment = 0; segment <= AngularSegments; segment++)
151 {
152 float progress = segment / (float)AngularSegments;
153 float angle = MathHelper.Lerp(-HalfSweep, HalfSweep, progress)
154 * state.SwingDirection;
155 float taper = (float)Math.Pow(Math.Max(0f,
156 Math.Sin(progress * MathHelper.Pi)), 0.67f);
157 taper *= 1f + 0.22f * (progress * 2f - 1f);
158 float outerRadius = state.Radius * (OuterRadiusRatio
159 + (float)Math.Sin(progress * 29f
160 + (float)Math.Sin(progress * 8f) * 1.3f)
161 * 0.0032f * taper);
162 float innerDistortion = ((float)Math.Sin(progress * 23f + 0.7f)
163 + (float)Math.Sin(progress * 47f - 1.4f) * 0.42f)
164 * state.Radius * 0.018f * taper;
165 float thickness = state.Radius * MaximumThicknessRatio * taper;
166 float innerRadius = outerRadius - thickness + innerDistortion;
167 float revealAlpha = 1f - SmoothStep(reveal - 0.024f,
168 reveal + 0.010f, progress);
169 float capAlpha = SmoothStep(0f, 0.018f, progress)
170 * SmoothStep(0f, 0.018f, 1f - progress);
171
172 Vector2 radial = angle.ToRotationVector2();
173 for (int band = 0; band <= DepthBands; band++)
174 {
175 float depth = band / (float)DepthBands;
176 float localRadius = MathHelper.Lerp(outerRadius, innerRadius,
177 depth);
178 Vector2 local = radial * localRadius;
179 Vector2 rotated = new(
180 local.X * rotationCos - local.Y * rotationSin,
181 local.X * rotationSin + local.Y * rotationCos);
182 Vector2 world = state.Center + rotated;
183 Vector2 screen = world - Main.screenPosition;
184
185 float integrity = ReaperCrescentPrimitiveTextureSystem
186 .SampleDeathInteriorIntegrity(progress, depth, erosion);
187 float depthFade = MathHelper.Lerp(0.96f, 0.78f, depth);
188 float alpha = state.Opacity * revealAlpha * capAlpha
189 * integrity * depthFade;
190
191 // World-space UVs are deliberately independent of the current
192 // crescent angle, radius, and animation frame. Only the mask
193 // moves; the Death Domain scenery behind it never rotates.
194 Vector2 uv = new(world.X / DeathDomainBackdropTextureSystem.LayerWidth,
195 world.Y / DeathDomainBackdropTextureSystem.LayerHeight);
196 vertices[vertexIndex++] = new VertexPositionColorTexture(
197 new Vector3(screen, 0f), Color.White * alpha, uv);
198 }
199 }
200 }
201
202 private static short[] CreateIndices()
203 {
204 short[] result = new short[AngularSegments * DepthBands * 6];
205 int index = 0;
206 int stride = DepthBands + 1;
207 for (int segment = 0; segment < AngularSegments; segment++)
208 {
209 for (int band = 0; band < DepthBands; band++)
210 {
211 short topLeft = (short)(segment * stride + band);
212 short bottomLeft = (short)(topLeft + 1);
213 short topRight = (short)(topLeft + stride);
214 short bottomRight = (short)(topRight + 1);
215 result[index++] = topLeft;
216 result[index++] = bottomLeft;
217 result[index++] = topRight;
218 result[index++] = topRight;
219 result[index++] = bottomLeft;
220 result[index++] = bottomRight;
221 }
222 }
223 return result;
224 }
225
226 private static float SmoothStep(float start, float end, float value)
227 {
228 if (end <= start)
229 return value >= end ? 1f : 0f;
230 return Smooth01((value - start) / (end - start));
231 }
232
233 private static float Smooth01(float value)
234 {
235 value = MathHelper.Clamp(value, 0f, 1f);
236 return value * value * (3f - 2f * value);
237 }
238 }
Modified Common/MyPlayer.cs +117 -0
@@ -68,9 +68,17 @@ public class MyPlayer : ModPlayer
68 68 private Vector2 deathDomainScreenShakeDirection = Vector2.UnitY;
69 69 private readonly Dictionary<int, PendingDeathDomainCut> deathDomainPendingCuts = [];
70 70 private readonly List<PendingDeathDomainStrike> pendingDeathDomainStrikes = [];
71 private readonly List<PendingReaperNecklaceHarvest> pendingReaperNecklaceHarvests = [];
71 72
72 73 private readonly record struct PendingDeathDomainCut(float Rotation, int ProjectileIndex);
73 74 private readonly record struct PendingDeathDomainStrike(int NpcIndex, int RemainingFrames, int Damage, int LifeStealLevel);
75 private readonly record struct PendingReaperNecklaceHarvest(
76 int NpcIndex,
77 int RemainingFrames,
78 float Rotation,
79 DeathNecklace Necklace,
80 float VisualMastery,
81 bool Requiem);
74 82
75 83 public override void Initialize()
76 84 {
@@ -107,6 +115,7 @@ public class MyPlayer : ModPlayer
107 115 deathDomainScreenShakeDirection = Vector2.UnitY;
108 116 deathDomainPendingCuts.Clear();
109 117 pendingDeathDomainStrikes.Clear();
118 pendingReaperNecklaceHarvests.Clear();
110 119 }
111 120
112 121 public override void ResetEffects()
@@ -198,6 +207,7 @@ public class MyPlayer : ModPlayer
198 207 if (deathDomainSlashSoundCooldown > 0)
199 208 deathDomainSlashSoundCooldown--;
200 209
210 UpdatePendingReaperNecklaceHarvests();
201 211 UpdatePendingDeathDomainStrikes();
202 212 UpdateDeathDomain();
203 213
@@ -773,6 +783,7 @@ public class MyPlayer : ModPlayer
773 783 ReaperExhaustionCooldown = 0;
774 784 deathCycleMask = 0;
775 785 queuedReaperForm = null;
786 pendingReaperNecklaceHarvests.Clear();
776 787 Player.GetModPlayer<ReaperCombatPlayer>().ClearRuntime();
777 788 if (sync)
778 789 SyncReaperCombat();
@@ -926,6 +937,112 @@ public class MyPlayer : ModPlayer
926 937 return best;
927 938 }
928 939
940 internal DeathNecklace? GetDeathNecklaceForReaperRift()
941 => ActiveDeathNecklace ?? FindEquippedDeathNecklace();
942
943 internal void SpawnDeathDomainHarvestFromReaperRift(NPC target,
944 DeathNecklace necklace, bool requiem, float rotation)
945 {
946 if (Main.netMode == NetmodeID.MultiplayerClient || !target.active)
947 return;
948 float visualMastery = MathHelper.Clamp((necklace.MasteryScore - 5f) / 65f,
949 0f, 1f);
950 SpawnDeathDomainHarvestSlash(target, necklace, visualMastery, requiem,
951 rotation);
952 }
953
954 internal void QueueDeathNecklaceHarvestFromReaperHit(NPC target,
955 float rotation)
956 {
957 if (Main.netMode == NetmodeID.MultiplayerClient || !target.active)
958 return;
959
960 DeathNecklace? necklace = ActiveDeathNecklace ?? FindEquippedDeathNecklace();
961 if (necklace is null)
962 return;
963
964 const int telegraphFrames = 12;
965 float visualMastery = MathHelper.Clamp((necklace.MasteryScore - 5f) / 65f,
966 0f, 1f);
967 float scale = MathHelper.Clamp(Math.Max(target.width, target.height) / 52f,
968 0.85f, 2.4f);
969 deathDomainVolleyCounter++;
970 bool requiem = necklace.DeathRequiemUnlocked
971 && deathDomainVolleyCounter % 4 == 0;
972
973 if (Main.netMode == NetmodeID.Server)
974 {
975 DeathMod.BroadcastDeathDomainTelegraph(Player, target,
976 telegraphFrames, necklace.SlashCount, visualMastery, rotation,
977 scale);
978 }
979 else
980 {
981 int projectileIndex = Projectile.NewProjectile(
982 Player.GetSource_Misc("DeathMod:ReaperNecklaceTelegraph"),
983 target.Center,
984 Vector2.Zero,
985 ModContent.ProjectileType<DeathDomainHarvestTelegraphProjectile>(),
986 0,
987 0f,
988 Player.whoAmI,
989 target.whoAmI,
990 telegraphFrames,
991 necklace.SlashCount + visualMastery * 0.1f);
992 if (projectileIndex >= 0 && projectileIndex < Main.maxProjectiles)
993 {
994 Projectile telegraph = Main.projectile[projectileIndex];
995 telegraph.rotation = rotation;
996 telegraph.scale = scale;
997 }
998 }
999
1000 pendingReaperNecklaceHarvests.Add(new PendingReaperNecklaceHarvest(
1001 target.whoAmI,
1002 telegraphFrames,
1003 rotation,
1004 necklace,
1005 visualMastery,
1006 requiem));
1007 }
1008
1009 private void UpdatePendingReaperNecklaceHarvests()
1010 {
1011 if (Main.netMode == NetmodeID.MultiplayerClient
1012 || pendingReaperNecklaceHarvests.Count == 0)
1013 {
1014 return;
1015 }
1016
1017 for (int index = pendingReaperNecklaceHarvests.Count - 1;
1018 index >= 0; index--)
1019 {
1020 PendingReaperNecklaceHarvest pending =
1021 pendingReaperNecklaceHarvests[index];
1022 int remainingFrames = pending.RemainingFrames - 1;
1023 if (remainingFrames > 0)
1024 {
1025 pendingReaperNecklaceHarvests[index] = pending with
1026 {
1027 RemainingFrames = remainingFrames
1028 };
1029 continue;
1030 }
1031
1032 pendingReaperNecklaceHarvests.RemoveAt(index);
1033 if (pending.NpcIndex < 0 || pending.NpcIndex >= Main.maxNPCs)
1034 continue;
1035 NPC target = Main.npc[pending.NpcIndex];
1036 if (!target.active || target.friendly || target.immortal
1037 || target.dontTakeDamage || target.life <= 0)
1038 {
1039 continue;
1040 }
1041 SpawnDeathDomainHarvestSlash(target, pending.Necklace,
1042 pending.VisualMastery, pending.Requiem, pending.Rotation);
1043 }
1044 }
1045
929 1046 private void UpdateDeathDomain()
930 1047 {
931 1048 // UpdateAccessory normally supplies this reference. The direct equipment scan
Added Common/ReaperBloodTrailRenderer.cs +20 -0
@@ -0,0 +1,20 @@
1 using Microsoft.Xna.Framework;
2
3 namespace DeathMod.Common;
4
5 /// <summary>One rendering recipe shared by Blood projectiles and Death echoes.</summary>
6 internal static class ReaperBloodTrailRenderer
7 {
8 internal static void DrawSegment(Vector2 start, Vector2 end, float width,
9 float opacity)
10 {
11 if (width <= 0.01f || opacity <= 0.001f)
12 return;
13 DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
14 new Color(104, 0, 18, 0) * (opacity * 0.42f), width + 7f);
15 DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
16 new Color(246, 20, 54, 235) * (opacity * 0.94f), width + 2.8f);
17 DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
18 new Color(7, 0, 3, 252) * opacity, width);
19 }
20 }
Modified Common/ReaperCombatDefinitions.cs +1 -1
@@ -315,7 +315,7 @@ public static class ReaperCombatRegistry
315 315 ReaperFormId.Frost => 112.2f,
316 316 ReaperFormId.Soul => 124.6f,
317 317 ReaperFormId.Void => 137.8f,
318 ReaperFormId.Death => 170f,
318 ReaperFormId.Death => 230f,
319 319 _ => 157.4f
320 320 };
321 321 }
Modified Common/ReaperCombatService.cs +4 -15
@@ -510,21 +510,10 @@ public static class ReaperCombatService
510 510 }
511 511 if (data.ReaperHitKind == ReaperHitKind.Primary)
512 512 {
513 player.GetModPlayer<MyPlayer>().RecordDeathCycleHit(data.ReaperPhase);
514 float tempo = projectile.ModProjectile is SickleSwingProjectile deathSwing
515 ? deathSwing.DeathTempo : 1f;
516 ReaperDeathPhantomProjectile.Spawn(projectile.GetSource_FromThis(),
517 player.whoAmI, snapshot, target.Center,
518 data.AttackAngle.ToRotationVector2(), tempo,
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 }
513 MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
514 modPlayer.RecordDeathCycleHit(data.ReaperPhase);
515 modPlayer.QueueDeathNecklaceHarvestFromReaperHit(target,
516 data.AttackAngle);
528 517 if (data.ReaperPhase == 1)
529 518 player.GetModPlayer<ReaperCombatPlayer>().ApplyDeathBloodHealing(damageDone);
530 519 if (data.ReaperPhase == 2)
Modified Common/ReaperCrescentPrimitiveTextureSystem.cs +424 -45
@@ -1,28 +1,94 @@
1 1 using Microsoft.Xna.Framework;
2 2 using Microsoft.Xna.Framework.Graphics;
3 3 using System;
4 using System.Collections.Generic;
4 5 using Terraria;
5 6 using Terraria.ModLoader;
6 7
7 8 namespace DeathMod.Common;
8 9
9 10 /// <summary>
10 /// High-resolution, seamless crescent surfaces for Blood reaper swings. The masks
11 /// are created lazily on the draw thread: dedicated servers never own textures,
12 /// and a whole crescent costs only a few sprite draws instead of hundreds of
13 /// rectangular wedges.
11 /// High-resolution, seamless crescent surfaces shared by every non-Void reaper.
12 /// Each form keeps the Blood crescent's continuous silhouette while baking its
13 /// own material into the face. Textures are created lazily on the draw thread:
14 /// dedicated servers never own graphics resources.
14 15 /// </summary>
15 16 [Autoload(Side = ModSide.Client)]
16 17 internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
17 18 {
18 19 private const int TextureSize = 768;
20 private const int MaterialTextureSize = 512;
19 21 private const float OuterRadius = 0.91f;
20 22 private const int BloodLifecycleFrames = 16;
23 private const int MaterialLifecycleFrames = 10;
21 24
22 25 private static readonly Texture2D?[] bloodCrescents = new Texture2D?[BloodLifecycleFrames];
23 26 private static readonly Texture2D?[] deathDomainCrescents = new Texture2D?[BloodLifecycleFrames];
27 private static readonly Texture2D?[] deathDomainRimCrescents = new Texture2D?[BloodLifecycleFrames];
28 private static readonly Dictionary<ReaperFormId, Texture2D?[]> formCrescents = [];
24 29 private static Color[]? deathDomainSurface;
25 30 private static Texture2D? deathDomainBlade;
31 private static Texture2D? deathDomainRiftCrescent;
32
33 internal static void DrawFormCrescent(
34 ReaperFormId form,
35 Vector2 center,
36 float rotation,
37 float radius,
38 float opacity,
39 int motionLayers,
40 int swingDirection,
41 float time,
42 float lifeProgress)
43 {
44 if (form == ReaperFormId.Blood)
45 {
46 DrawBloodCrescent(center, rotation, radius, opacity, motionLayers,
47 swingDirection, time, lifeProgress);
48 return;
49 }
50 if (form == ReaperFormId.Death)
51 {
52 DrawDeathDomainCrescent(center, rotation, radius, opacity, motionLayers,
53 swingDirection, time, lifeProgress);
54 return;
55 }
56 if (Main.dedServ || radius <= 1f || opacity <= 0.001f
57 || form == ReaperFormId.Void)
58 {
59 return;
60 }
61
62 lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
63 float lifecycleFrame = lifeProgress * (MaterialLifecycleFrames - 1);
64 int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
65 MaterialLifecycleFrames - 1);
66 int secondFrame = Math.Min(firstFrame + 1, MaterialLifecycleFrames - 1);
67 float frameBlend = lifecycleFrame - firstFrame;
68 Texture2D firstTexture = GetFormCrescent(form, firstFrame);
69 Texture2D secondTexture = secondFrame == firstFrame
70 ? firstTexture : GetFormCrescent(form, secondFrame);
71 Vector2 screenCenter = center - Main.screenPosition;
72 float scale = GetMaterialScale(radius);
73 motionLayers = Math.Clamp(motionLayers, 0, 4);
74 SpriteEffects effects = swingDirection < 0
75 ? SpriteEffects.FlipVertically : SpriteEffects.None;
76 Color aura = ReaperCombatRegistry.GetPrimaryColor(form) with { A = 0 };
77
78 for (int layer = motionLayers; layer >= 1; layer--)
79 {
80 float lag = swingDirection * (0.015f + layer * 0.017f);
81 DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
82 screenCenter, rotation - lag, scale * (1f - layer * 0.014f),
83 aura * (opacity * (0.030f + layer * 0.012f)), effects);
84 }
85 float pulse = 0.982f + (float)Math.Sin(time * 6.4f + (int)form) * 0.012f;
86 DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
87 screenCenter, rotation, scale * 1.018f,
88 aura * (opacity * 0.16f), effects);
89 DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
90 screenCenter, rotation, scale * pulse, Color.White * opacity, effects);
91 }
26 92
27 93 internal static void DrawBloodCrescent(
28 94 Vector2 center,
@@ -120,6 +186,54 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
120 186 screenCenter, rotation, scale * pulse, Color.White * opacity, effects);
121 187 }
122 188
189 internal static void DrawDeathDomainCrescentRim(
190 Vector2 center,
191 float rotation,
192 float radius,
193 float opacity,
194 int motionLayers,
195 int swingDirection,
196 float time,
197 float lifeProgress)
198 {
199 if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
200 return;
201
202 lifeProgress = MathHelper.Clamp(lifeProgress, 0f, 1f);
203 float lifecycleFrame = lifeProgress * (BloodLifecycleFrames - 1);
204 int firstFrame = Math.Clamp((int)Math.Floor(lifecycleFrame), 0,
205 BloodLifecycleFrames - 1);
206 int secondFrame = Math.Min(firstFrame + 1, BloodLifecycleFrames - 1);
207 float frameBlend = lifecycleFrame - firstFrame;
208 Texture2D firstTexture = GetDeathDomainRimCrescent(firstFrame);
209 Texture2D secondTexture = secondFrame == firstFrame
210 ? firstTexture
211 : GetDeathDomainRimCrescent(secondFrame);
212 float scale = GetScale(radius);
213 Vector2 screenCenter = center - Main.screenPosition;
214 motionLayers = Math.Clamp(motionLayers, 0, 4);
215 SpriteEffects effects = swingDirection < 0
216 ? SpriteEffects.FlipVertically
217 : SpriteEffects.None;
218
219 for (int layer = motionLayers; layer >= 1; layer--)
220 {
221 float lag = swingDirection * (0.016f + layer * 0.017f);
222 DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
223 screenCenter, rotation - lag, scale * (1f - layer * 0.014f),
224 new Color(208, 12, 54, 0)
225 * (opacity * (0.034f + layer * 0.012f)), effects);
226 }
227
228 float pulse = 0.99f + (float)Math.Sin(time * 5.8f) * 0.008f;
229 DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
230 screenCenter, rotation, scale * 1.015f,
231 new Color(235, 20, 68, 0) * (opacity * 0.18f), effects);
232 DrawBloodFramePair(firstTexture, secondTexture, frameBlend,
233 screenCenter, rotation, scale * pulse, Color.White * opacity,
234 effects);
235 }
236
123 237 internal static void DrawDeathDomainBlade(Vector2 screenCenter, float rotation,
124 238 float length, float width, float opacity, float completion)
125 239 {
@@ -142,17 +256,42 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
142 256 SpriteEffects.None, 0f);
143 257 }
144 258
259 internal static void DrawDeathDomainRiftCrescent(Vector2 center, float rotation,
260 float radius, float opacity, int swingDirection)
261 {
262 if (Main.dedServ || radius <= 1f || opacity <= 0.001f)
263 return;
264 Texture2D texture = deathDomainRiftCrescent ??= CreateDeathDomainRiftCrescent();
265 SpriteEffects effects = swingDirection < 0
266 ? SpriteEffects.FlipVertically : SpriteEffects.None;
267 Vector2 screenCenter = center - Main.screenPosition;
268 float scale = GetScale(radius);
269 DrawTexture(texture, screenCenter, rotation, scale * 1.035f,
270 new Color(188, 9, 54, 0) * (opacity * 0.28f), effects);
271 DrawTexture(texture, screenCenter, rotation, scale, Color.White * opacity,
272 effects);
273 }
274
145 275 public override void Unload()
146 276 {
147 277 Texture2D?[] oldBlood = new Texture2D?[BloodLifecycleFrames];
148 278 Array.Copy(bloodCrescents, oldBlood, BloodLifecycleFrames);
149 279 Texture2D?[] oldDeath = new Texture2D?[BloodLifecycleFrames];
150 280 Array.Copy(deathDomainCrescents, oldDeath, BloodLifecycleFrames);
281 Texture2D?[] oldDeathRims = new Texture2D?[BloodLifecycleFrames];
282 Array.Copy(deathDomainRimCrescents, oldDeathRims, BloodLifecycleFrames);
151 283 Texture2D? oldDeathBlade = deathDomainBlade;
284 Texture2D? oldDeathRift = deathDomainRiftCrescent;
285 List<Texture2D?> oldForms = [];
286 foreach (Texture2D?[] textures in formCrescents.Values)
287 oldForms.AddRange(textures);
152 288 Array.Clear(bloodCrescents, 0, bloodCrescents.Length);
153 289 Array.Clear(deathDomainCrescents, 0, deathDomainCrescents.Length);
290 Array.Clear(deathDomainRimCrescents, 0, deathDomainRimCrescents.Length);
291 formCrescents.Clear();
154 292 deathDomainSurface = null;
155 293 deathDomainBlade = null;
294 deathDomainRiftCrescent = null;
156 295 if (Main.dedServ)
157 296 return;
158 297
@@ -162,7 +301,12 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
162 301 texture?.Dispose();
163 302 foreach (Texture2D? texture in oldDeath)
164 303 texture?.Dispose();
304 foreach (Texture2D? texture in oldDeathRims)
305 texture?.Dispose();
306 foreach (Texture2D? texture in oldForms)
307 texture?.Dispose();
165 308 oldDeathBlade?.Dispose();
309 oldDeathRift?.Dispose();
166 310 });
167 311 }
168 312
@@ -178,6 +322,143 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
178 322 frame / (float)(BloodLifecycleFrames - 1));
179 323 }
180 324
325 private static Texture2D GetDeathDomainRimCrescent(int frame)
326 {
327 return deathDomainRimCrescents[frame] ??= CreateDeathDomainRimCrescent(
328 frame / (float)(BloodLifecycleFrames - 1));
329 }
330
331 private static Texture2D GetFormCrescent(ReaperFormId form, int frame)
332 {
333 if (!formCrescents.TryGetValue(form, out Texture2D?[]? textures))
334 {
335 textures = new Texture2D?[MaterialLifecycleFrames];
336 formCrescents[form] = textures;
337 }
338 return textures[frame] ??= CreateFormCrescent(form,
339 frame / (float)(MaterialLifecycleFrames - 1));
340 }
341
342 private static Texture2D CreateFormCrescent(ReaperFormId form, float lifeProgress)
343 {
344 Texture2D texture = new(Main.instance.GraphicsDevice, MaterialTextureSize,
345 MaterialTextureSize,
346 false, SurfaceFormat.Color);
347 Color[] pixels = new Color[MaterialTextureSize * MaterialTextureSize];
348 float antialias = 3f / MaterialTextureSize;
349 float reveal = Smooth01(lifeProgress);
350
351 for (int y = 0; y < MaterialTextureSize; y++)
352 {
353 float normalizedY = (y + 0.5f) / MaterialTextureSize * 2f - 1f;
354 for (int x = 0; x < MaterialTextureSize; x++)
355 {
356 float normalizedX = (x + 0.5f) / MaterialTextureSize * 2f - 1f;
357 CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
358 halfSweep: 2.43f, maximumThickness: 0.405f,
359 innerWarp: 0.015f, asymmetry: 0.22f, antialias);
360 if (sample.Body <= 0.001f)
361 continue;
362 float revealMask = 1f - SmoothStep(reveal - 0.018f,
363 reveal + 0.012f, sample.Progress);
364 float body = sample.Body * revealMask;
365 if (body <= 0.001f)
366 continue;
367
368 Vector3 color;
369 float materialAlpha;
370 SampleFormMaterial(form, sample.Progress, sample.Depth,
371 out color, out materialAlpha);
372 float outerGlow = sample.OuterGlow * revealMask;
373 float outerHot = sample.OuterHot * revealMask;
374 Vector3 rim = form switch
375 {
376 ReaperFormId.Bone => new Vector3(0.94f, 1f, 0.89f),
377 ReaperFormId.Infernal => new Vector3(1f, 0.94f, 0.55f),
378 ReaperFormId.Frost => new Vector3(0.91f, 1f, 1f),
379 ReaperFormId.Soul => new Vector3(0.75f, 1f, 1f),
380 _ => new Vector3(0.84f, 1f, 1f)
381 };
382 Vector3 glow = ReaperCombatRegistry.GetPrimaryColor(form).ToVector3();
383 color = Vector3.Lerp(color, glow, outerGlow * 0.68f);
384 color = Vector3.Lerp(color, rim, outerHot * 0.94f);
385 float alpha = Math.Max(body * materialAlpha,
386 Math.Max(outerGlow * 0.88f, outerHot));
387 pixels[y * MaterialTextureSize + x] = Premultiplied(color, alpha);
388 }
389 }
390
391 texture.SetData(pixels);
392 return texture;
393 }
394
395 private static void SampleFormMaterial(ReaperFormId form, float progress,
396 float depth, out Vector3 color, out float alpha)
397 {
398 switch (form)
399 {
400 case ReaperFormId.Bone:
401 {
402 float joint = Ridge((float)Math.Sin(progress * 45f + 0.4f), 13f);
403 float marrow = Ridge((float)Math.Sin(progress * 22f
404 - depth * 13f + 1.1f), 8f);
405 color = Vector3.Lerp(new Vector3(0.025f, 0.20f, 0.25f),
406 new Vector3(0.91f, 0.91f, 0.72f),
407 MathHelper.Clamp(0.20f + joint * 0.62f
408 + marrow * (1f - depth) * 0.30f, 0f, 1f));
409 alpha = MathHelper.Lerp(0.82f, 0.58f, depth);
410 break;
411 }
412 case ReaperFormId.Infernal:
413 {
414 float lava = Ridge((float)Math.Sin(progress * 30f
415 + depth * 19f + (float)Math.Sin(progress * 9f)), 8f);
416 float heat = MathHelper.Clamp(lava * 0.82f
417 + (1f - depth) * 0.25f, 0f, 1f);
418 color = Vector3.Lerp(new Vector3(0.035f, 0.006f, 0.002f),
419 new Vector3(1f, 0.42f, 0.025f), heat);
420 color = Vector3.Lerp(color, new Vector3(1f, 0.96f, 0.56f),
421 lava * lava * 0.58f);
422 alpha = MathHelper.Lerp(0.92f, 0.68f, depth);
423 break;
424 }
425 case ReaperFormId.Frost:
426 {
427 float facetA = Math.Abs((float)Math.Sin(progress * 27f + depth * 8f));
428 float facetB = Math.Abs((float)Math.Sin(progress * 13f - depth * 17f));
429 float facet = MathHelper.Clamp(facetA * 0.52f + facetB * 0.38f, 0f, 1f);
430 color = Vector3.Lerp(new Vector3(0.025f, 0.17f, 0.38f),
431 new Vector3(0.62f, 0.94f, 1f), facet);
432 color = Vector3.Lerp(color, Vector3.One,
433 Ridge((float)Math.Sin(progress * 34f - depth * 24f), 14f) * 0.62f);
434 alpha = MathHelper.Lerp(0.72f, 0.43f, depth);
435 break;
436 }
437 case ReaperFormId.Soul:
438 {
439 float stream = Ridge((float)Math.Sin(progress * 21f
440 - depth * 26f + (float)Math.Sin(progress * 8f) * 1.4f), 6f);
441 float echo = Ridge((float)Math.Sin(progress * 38f
442 + depth * 15f + 2.2f), 12f);
443 color = Vector3.Lerp(new Vector3(0.10f, 0.018f, 0.28f),
444 new Vector3(0.34f, 0.28f, 1f), stream * 0.72f);
445 color = Vector3.Lerp(color, new Vector3(0.22f, 1f, 1f),
446 echo * 0.62f);
447 alpha = MathHelper.Lerp(0.76f, 0.40f, depth);
448 break;
449 }
450 default:
451 {
452 float spirit = Ridge((float)Math.Sin(progress * 25f
453 - depth * 18f), 8f);
454 color = Vector3.Lerp(new Vector3(0.025f, 0.18f, 0.21f),
455 new Vector3(0.50f, 0.96f, 1f), spirit * 0.68f);
456 alpha = MathHelper.Lerp(0.68f, 0.38f, depth);
457 break;
458 }
459 }
460 }
461
181 462 private static Texture2D CreateDeathDomainCrescent(float lifeProgress)
182 463 {
183 464 Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
@@ -185,7 +466,9 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
185 466 Color[] pixels = new Color[TextureSize * TextureSize];
186 467 Color[] surface = deathDomainSurface ??= CreateDeathDomainSurface();
187 468 float antialias = 3f / TextureSize;
188 float reveal = Smooth01(lifeProgress);
469 float reveal = Smooth01(lifeProgress / 0.42f);
470 float erosion = Smooth01((lifeProgress - 0.30f) / 0.66f);
471 float edgeFade = 1f - Smooth01((lifeProgress - 0.96f) / 0.04f);
189 472
190 473 for (int y = 0; y < TextureSize; y++)
191 474 {
@@ -200,9 +483,9 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
200 483 continue;
201 484 float revealMask = 1f - SmoothStep(reveal - 0.018f,
202 485 reveal + 0.012f, sample.Progress);
203 float body = sample.Body * revealMask;
204 if (body <= 0.001f)
205 continue;
486 float integrity = SampleDeathInteriorIntegrity(sample.Progress,
487 sample.Depth, erosion);
488 float body = sample.Body * revealMask * integrity;
206 489
207 490 Vector4 scene = surface[y * TextureSize + x].ToVector4();
208 491 Vector3 color = new(scene.X, scene.Y, scene.Z);
@@ -212,14 +495,57 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
212 495 color = Vector3.Lerp(color, new Vector3(0.66f, 0.008f, 0.09f),
213 496 crimsonVein * 0.18f * body);
214 497
215 float redRim = sample.OuterGlow * revealMask;
216 float whiteRim = sample.OuterHot * revealMask;
498 // The outer cutting edge is never eroded. Only the domain-filled
499 // face tears away, keeping one continuous hot blade silhouette.
500 float redRim = sample.OuterGlow * revealMask * edgeFade;
501 float whiteRim = sample.OuterHot * revealMask * edgeFade;
217 502 color = Vector3.Lerp(color, new Vector3(0.92f, 0.025f, 0.12f),
218 503 redRim * 0.78f);
219 504 color = Vector3.Lerp(color, new Vector3(1f, 0.88f, 0.84f),
220 505 whiteRim * 0.90f);
221 506 alpha = Math.Max(alpha, Math.Max(redRim * 0.90f,
222 507 whiteRim * 0.98f));
508 if (alpha <= 0.001f)
509 continue;
510 pixels[y * TextureSize + x] = Premultiplied(color, alpha);
511 }
512 }
513
514 texture.SetData(pixels);
515 return texture;
516 }
517
518 private static Texture2D CreateDeathDomainRimCrescent(float lifeProgress)
519 {
520 Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize,
521 TextureSize, false, SurfaceFormat.Color);
522 Color[] pixels = new Color[TextureSize * TextureSize];
523 float antialias = 3f / TextureSize;
524 float reveal = Smooth01(lifeProgress / 0.42f);
525 float edgeFade = 1f - Smooth01((lifeProgress - 0.96f) / 0.04f);
526
527 for (int y = 0; y < TextureSize; y++)
528 {
529 float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
530 for (int x = 0; x < TextureSize; x++)
531 {
532 float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
533 CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
534 halfSweep: 2.43f, maximumThickness: 0.405f,
535 innerWarp: 0.018f, asymmetry: 0.22f, antialias);
536 if (sample.Body <= 0.001f)
537 continue;
538
539 float revealMask = 1f - SmoothStep(reveal - 0.018f,
540 reveal + 0.012f, sample.Progress);
541 float redRim = sample.OuterGlow * revealMask * edgeFade;
542 float whiteRim = sample.OuterHot * revealMask * edgeFade;
543 float alpha = Math.Max(redRim * 0.90f, whiteRim * 0.98f);
544 if (alpha <= 0.001f)
545 continue;
546
547 Vector3 color = Vector3.Lerp(new Vector3(0.92f, 0.025f, 0.12f),
548 new Vector3(1f, 0.88f, 0.84f), whiteRim * 0.94f);
223 549 pixels[y * TextureSize + x] = Premultiplied(color, alpha);
224 550 }
225 551 }
@@ -291,6 +617,45 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
291 617 return texture;
292 618 }
293 619
620 private static Texture2D CreateDeathDomainRiftCrescent()
621 {
622 Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
623 false, SurfaceFormat.Color);
624 Color[] pixels = new Color[TextureSize * TextureSize];
625 float antialias = 3f / TextureSize;
626 for (int y = 0; y < TextureSize; y++)
627 {
628 float normalizedY = (y + 0.5f) / TextureSize * 2f - 1f;
629 for (int x = 0; x < TextureSize; x++)
630 {
631 float normalizedX = (x + 0.5f) / TextureSize * 2f - 1f;
632 CrescentSample sample = SampleCrescent(normalizedX, normalizedY,
633 halfSweep: 2.43f, maximumThickness: 0.15f,
634 innerWarp: 0.026f, asymmetry: 0.20f, antialias);
635 if (sample.Body <= 0.001f)
636 continue;
637 float integrity = SampleInteriorIntegrity(sample.Progress,
638 sample.Depth, 1f, 1.28f);
639 Vector4 sampled = DeathDomainBackdropTextureSystem
640 .SampleCompositePixel(x * 2, y * 2,
641 (x + 0.5f) / TextureSize, (y + 0.5f) / TextureSize)
642 .ToVector4();
643 Vector3 color = new(sampled.X, sampled.Y, sampled.Z);
644 float redRim = sample.OuterGlow;
645 float whiteRim = sample.OuterHot;
646 color = Vector3.Lerp(color, new Vector3(0.94f, 0.012f, 0.12f),
647 redRim * 0.88f);
648 color = Vector3.Lerp(color, new Vector3(1f, 0.88f, 0.84f),
649 whiteRim * 0.92f);
650 float alpha = Math.Max(sample.Body * integrity * 0.94f,
651 Math.Max(redRim * 0.94f, whiteRim));
652 pixels[y * TextureSize + x] = Premultiplied(color, alpha);
653 }
654 }
655 texture.SetData(pixels);
656 return texture;
657 }
658
294 659 private static Texture2D CreateBloodCrescent(float lifeProgress)
295 660 {
296 661 Texture2D texture = new(Main.instance.GraphicsDevice, TextureSize, TextureSize,
@@ -301,7 +666,7 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
301 666 // that clock makes the bright leading edge sit at the live blade tip
302 667 // instead of revealing most of the crescent before the hand gets there.
303 668 float reveal = Smooth01(lifeProgress);
304 float erosion = Smooth01((lifeProgress - 0.40f) / 0.55f);
669 float erosion = Smooth01((lifeProgress - 0.26f) / 0.68f);
305 670
306 671 for (int y = 0; y < TextureSize; y++)
307 672 {
@@ -318,7 +683,8 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
318 683 // Only the arc already crossed by the moving blade is visible.
319 684 // FlipVertically reverses this reveal for the opposite swing.
320 685 float revealMask = 1f - SmoothStep(reveal - 0.018f, reveal + 0.012f, sample.Progress);
321 float integrity = SampleBloodIntegrity(sample.Progress, sample.Depth, erosion) * revealMask;
686 float integrity = SampleInteriorIntegrity(sample.Progress,
687 sample.Depth, erosion, 1.12f) * revealMask;
322 688 if (integrity <= 0.001f)
323 689 continue;
324 690
@@ -341,8 +707,10 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
341 707 color = Vector3.Lerp(color, new Vector3(1f, 0.09f, 0.08f), innerHeat);
342 708 alpha = Math.Max(alpha, innerHeat * 0.72f);
343 709
344 float whiteRim = sample.OuterHot * integrity;
345 float redRim = sample.OuterGlow * integrity;
710 // Fractures consume only the liquid energy face. The hot cutting
711 // edge remains continuous from tip to tip at every lifetime frame.
712 float whiteRim = sample.OuterHot * revealMask;
713 float redRim = sample.OuterGlow * revealMask;
346 714 color = Vector3.Lerp(color, new Vector3(1f, 0.18f, 0.10f), redRim * 0.62f);
347 715 color = Vector3.Lerp(color, new Vector3(1f, 0.94f, 0.79f), whiteRim);
348 716 alpha = Math.Max(alpha, Math.Max(redRim * 0.82f, whiteRim * 0.98f));
@@ -354,7 +722,8 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
354 722 return texture;
355 723 }
356 724
357 private static float SampleBloodIntegrity(float progress, float depth, float erosion)
725 private static float SampleInteriorIntegrity(float progress, float depth,
726 float erosion, float intensity)
358 727 {
359 728 if (erosion <= 0.015f)
360 729 return 1f;
@@ -363,13 +732,13 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
363 732
364 733 // Persistent bites grow from the inner edge. Their deterministic layout
365 734 // keeps the crescent coherent from frame to frame instead of shimmering.
366 for (int index = 0; index < 13; index++)
735 for (int index = 0; index < 18; index++)
367 736 {
368 737 float hashA = Hash01((uint)index, 0xB10D51C1u);
369 738 float hashB = Hash01((uint)index, 0xA63E218Fu);
370 739 float center = 0.055f + hashA * 0.89f;
371 float activation = 0.08f + index / 12f * 0.64f;
372 float growth = Smooth01((erosion - activation) / 0.30f);
740 float activation = 0.035f + index / 17f * 0.62f;
741 float growth = Smooth01((erosion - activation) / 0.28f) * intensity;
373 742 if (growth <= 0f)
374 743 continue;
375 744
@@ -379,55 +748,62 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
379 748 continue;
380 749
381 750 float arch = (float)Math.Sqrt(Math.Max(0f, 1f - horizontal * horizontal));
382 float biteDepth = MathHelper.Lerp(0.13f, 0.42f, hashA) * growth * arch;
751 float biteDepth = MathHelper.Lerp(0.16f, 0.52f, hashA)
752 * Math.Min(1f, growth) * arch;
383 753 float boundary = 1f - biteDepth;
384 754 integrity = Math.Min(integrity, 1f - SmoothStep(boundary - 0.018f, boundary + 0.018f, depth));
385 755 }
386 756
387 757 // Late frames tear several isolated wounds through the energy face. This
388 758 // is what creates the irregular missing islands visible in the reference.
389 for (int index = 0; index < 8; index++)
759 for (int index = 0; index < 13; index++)
390 760 {
391 761 float hashA = Hash01((uint)index, 0xC04A711Du);
392 762 float hashB = Hash01((uint)index, 0xF731A2E9u);
393 float activation = 0.34f + index / 7f * 0.39f;
394 float growth = Smooth01((erosion - activation) / 0.28f);
763 float activation = 0.18f + index / 12f * 0.50f;
764 float growth = Smooth01((erosion - activation) / 0.27f) * intensity;
395 765 if (growth <= 0f)
396 766 continue;
397 767
398 768 float centerX = 0.08f + hashA * 0.84f;
399 float centerY = 0.23f + hashB * 0.60f;
400 float radiusX = MathHelper.Lerp(0.014f, 0.045f, hashB) * growth;
401 float radiusY = MathHelper.Lerp(0.050f, 0.135f, hashA) * growth;
769 // Keep every isolated wound away from depth zero: the outer blade
770 // rim may glow over a missing interior but is never itself severed.
771 float centerY = 0.30f + hashB * 0.54f;
772 float radiusX = MathHelper.Lerp(0.018f, 0.058f, hashB)
773 * Math.Min(1f, growth);
774 float radiusY = MathHelper.Lerp(0.055f, 0.15f, hashA)
775 * Math.Min(1f, growth);
402 776 float nx = (progress - centerX) / Math.Max(0.001f, radiusX);
403 777 float ny = (depth - centerY) / Math.Max(0.001f, radiusY);
404 778 float distance = (float)Math.Sqrt(nx * nx + ny * ny);
405 779 integrity = Math.Min(integrity, SmoothStep(0.82f, 1.08f, distance));
406 780 }
407 781
408 // Only the oldest slash frames lose portions of the white-hot outer edge.
409 for (int index = 0; index < 5; index++)
410 {
411 float hashA = Hash01((uint)index, 0x0A77E2D3u);
412 float hashB = Hash01((uint)index, 0x9B1D443Fu);
413 float activation = 0.58f + index * 0.065f;
414 float growth = Smooth01((erosion - activation) / 0.24f);
415 if (growth <= 0f)
416 continue;
417
418 float center = 0.10f + hashA * 0.80f;
419 float halfWidth = MathHelper.Lerp(0.015f, 0.040f, hashB) * growth;
420 float horizontal = Math.Abs(progress - center) / Math.Max(0.001f, halfWidth);
421 if (horizontal >= 1f)
422 continue;
423 float arch = (float)Math.Sqrt(Math.Max(0f, 1f - horizontal * horizontal));
424 float biteDepth = MathHelper.Lerp(0.08f, 0.20f, hashA) * growth * arch;
425 integrity = Math.Min(integrity, SmoothStep(biteDepth - 0.018f, biteDepth + 0.018f, depth));
426 }
427
428 782 return MathHelper.Clamp(integrity, 0f, 1f);
429 783 }
430 784
785 internal static float SampleDeathInteriorIntegrity(float progress,
786 float depth, float erosion)
787 {
788 float bites = SampleInteriorIntegrity(progress, depth, erosion, 1.18f);
789 if (erosion <= 0.24f)
790 return bites;
791
792 // A continuous low-frequency field turns the early isolated wounds into
793 // one spreading tear. At the end every point of the energy face has
794 // crossed the threshold; the separately drawn hot cutting edge is not
795 // sampled here and therefore never breaks into teeth or blocks.
796 float waveA = (float)Math.Sin(progress * 19.7f + depth * 11.3f + 0.6f);
797 float waveB = (float)Math.Sin(progress * 37.1f - depth * 16.9f + 2.2f);
798 float waveC = (float)Math.Sin(progress * 8.3f + depth * 31.7f - 1.1f);
799 float field = MathHelper.Clamp(0.50f + waveA * 0.19f
800 + waveB * 0.11f + waveC * 0.07f, 0.12f, 0.88f);
801 float dissolve = Smooth01((erosion - 0.24f) / 0.76f);
802 float continuousIntegrity = 1f - SmoothStep(field - 0.09f,
803 field + 0.09f, dissolve);
804 return MathHelper.Clamp(bites * continuousIntegrity, 0f, 1f);
805 }
806
431 807 private static void DrawBloodFramePair(
432 808 Texture2D first,
433 809 Texture2D second,
@@ -497,6 +873,9 @@ internal sealed class ReaperCrescentPrimitiveTextureSystem : ModSystem
497 873 private static float GetScale(float radius)
498 874 => radius / (TextureSize * 0.5f * OuterRadius);
499 875
876 private static float GetMaterialScale(float radius)
877 => radius / (MaterialTextureSize * 0.5f * OuterRadius);
878
500 879 private static float Ridge(float sine, float sharpness)
501 880 => (float)Math.Pow(MathHelper.Clamp((sine + 1f) * 0.5f, 0f, 1f), sharpness);
502 881
Modified Common/ReaperUltimateVisualSystem.cs +14 -14
@@ -30,7 +30,7 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
30 30 /// projectile AI on every net mode: dedicated servers immediately return.
31 31 /// </summary>
32 32 public static void ReportUltimate(Player owner, ReaperFormId form, Vector2 aim,
33 int actionId, int timer, int duration)
33 Vector2 focusWorld, int actionId, int timer, int duration)
34 34 {
35 35 if (Main.dedServ
36 36 || Main.gameMenu
@@ -51,6 +51,8 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
51 51 aim = fallback;
52 52 else
53 53 aim.Normalize();
54 if (!float.IsFinite(focusWorld.X) || !float.IsFinite(focusWorld.Y))
55 focusWorld = owner.MountedCenter + aim * 180f;
54 56
55 57 duration = Math.Clamp(duration, 1, 3600);
56 58 timer = Math.Clamp(timer, 0, duration);
@@ -77,6 +79,7 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
77 79 state.Form = form;
78 80 state.ActionId = actionId;
79 81 state.OriginWorld = owner.MountedCenter;
82 state.FocusWorld = focusWorld;
80 83 state.Aim = aim;
81 84 state.Timer = timer;
82 85 state.Duration = duration;
@@ -247,7 +250,12 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
247 250 {
248 251 origin = viewport * 0.5f - state.Aim * 180f;
249 252 }
250 Vector2 focus = origin + state.Aim * 180f;
253 Vector2 focus = state.FocusWorld - Main.screenPosition;
254 if (focus.X < -presentationMargin || focus.X > viewport.X + presentationMargin
255 || focus.Y < -presentationMargin || focus.Y > viewport.Y + presentationMargin)
256 {
257 focus = viewport * 0.5f;
258 }
251 259 Color primary = ReaperCombatRegistry.GetPrimaryColor(state.Form);
252 260 Color secondary = ReaperCombatRegistry.GetSecondaryColor(state.Form);
253 261 float time = Main.GlobalTimeWrappedHourly;
@@ -903,20 +911,11 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
903 911 hash ^= hash >> 15;
904 912 float angle = (hash & 0x00FFFFFFu) / 16777215f * MathHelper.Pi
905 913 - MathHelper.PiOver2;
906 Vector2 axis = angle.ToRotationVector2();
907 Vector2 normal = axis.RotatedBy(MathHelper.PiOver2);
908 float offset = MathHelper.Lerp(-Math.Min(viewport.X, viewport.Y) * 0.55f,
909 Math.Min(viewport.X, viewport.Y) * 0.55f,
910 ((hash >> 8) & 0xFFFFu) / 65535f);
911 Vector2 center = viewport * 0.5f + normal * offset;
914 Vector2 center = focus;
912 915 float width = 13f + index % 4 * 2.2f;
913 916 float completion = Ease(reveal);
914 DeathDomainPrimitiveTextureSystem.DrawBladeCentered(center, angle, reach,
915 width + 13f, new Color(32, 0, 13, 0) * (visibility * 0.62f), completion);
916 DeathDomainPrimitiveTextureSystem.DrawBladeCentered(center, angle, reach,
917 width + 4f, new Color(236, 22, 66, 0) * (visibility * 0.92f), completion);
918 DeathDomainPrimitiveTextureSystem.DrawBladeCentered(center, angle, reach,
919 Math.Max(1.6f, width * 0.18f), Color.White * (visibility * 0.78f), completion);
917 ReaperCrescentPrimitiveTextureSystem.DrawDeathDomainBlade(center,
918 angle, reach, width + 11f, visibility, completion);
920 919 }
921 920
922 921 float shatter = Envelope(progress, 0.87f, 0.89f, 0.985f, 1f) * opacity;
@@ -1882,6 +1881,7 @@ public sealed class ReaperUltimateVisualSystem : ModSystem
1882 1881 public ReaperFormId Form;
1883 1882 public int ActionId;
1884 1883 public Vector2 OriginWorld;
1884 public Vector2 FocusWorld;
1885 1885 public Vector2 Aim;
1886 1886 public int Timer;
1887 1887 public int Duration;
Modified Localization/en-US.hjson +1 -1
@@ -156,7 +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 ReaperDeathDomainRiftProjectile.DisplayName: Reaper Death Domain Rift Projectile
160 160 }
161 161
162 162 UI: {
Modified Localization/zh-Hans.hjson +1 -1
@@ -156,7 +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 // ReaperDeathDomainRiftProjectile.DisplayName: Reaper Death Domain Rift Projectile
160 160 }
161 161
162 162 UI: {
Modified Projectiles/ReaperActionControllerProjectile.cs +62 -12
@@ -156,12 +156,16 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
156 156 DrawSpecialStoryboard(player, grip);
157 157 else if (snapshot.Form == ReaperFormId.Infernal)
158 158 DrawInfernalUltimateRoute(TextureAssets.MagicPixel.Value);
159 else if (snapshot.Form == ReaperFormId.Death)
160 DrawDeathUltimateSwingCrescent(grip);
159 161
160 162 Texture2D texture = ModContent.Request<Texture2D>(
161 163 ReaperCombatRegistry.GetTexturePath(snapshot.Form, snapshot.Stage)).Value;
162 164 Vector2 normalizedAnchor = ReaperCombatRegistry.GetHandleAnchor(snapshot.Form, snapshot.Stage);
163 165 Vector2 anchor = new(texture.Width * normalizedAnchor.X, texture.Height * normalizedAnchor.Y);
164 float scale = 0.9f + snapshot.StageNumber * 0.045f + (snapshot.Form == ReaperFormId.Death ? 0.1f : 0f);
166 float scale = snapshot.Form == ReaperFormId.Death
167 ? 1.72f
168 : 0.9f + snapshot.StageNumber * 0.045f;
165 169 float correctedAngle = visualWeaponAngle
166 170 + ReaperCombatRegistry.GetTextureRotationCorrection(snapshot.Form, snapshot.Stage);
167 171 SpriteEffects weaponEffects = visualSwingDirection < 0
@@ -763,10 +767,12 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
763 767 ReaperFormId.Death => 170,
764 768 _ => 1
765 769 };
766 ReaperUltimateVisualSystem.ReportUltimate(player, snapshot.Form, aim,
767 Projectile.identity, timer, visualDuration);
768 770 Vector2 origin = player.MountedCenter;
769 Vector2 focus = origin + aim * 180f;
771 Vector2 focus = snapshot.Form == ReaperFormId.Death
772 ? player.Center + specialTargetOffset
773 : origin + aim * 180f;
774 ReaperUltimateVisualSystem.ReportUltimate(player, snapshot.Form, aim,
775 focus, Projectile.identity, timer, visualDuration);
770 776 switch (snapshot.Form)
771 777 {
772 778 case ReaperFormId.Bone:
@@ -854,7 +860,7 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
854 860 break;
855 861 case ReaperFormId.Death:
856 862 for (int index = 0; index < 18; index++)
857 StrikeDeathWorldCutAt(20 + index * 6, player, index);
863 StrikeDeathWorldCutAt(20 + index * 6, player, focus, index);
858 864 if (timer == 150 && Main.netMode != NetmodeID.MultiplayerClient)
859 865 ExecuteDeathUltimateShatter(player);
860 866 FinishAt(170);
@@ -875,7 +881,8 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
875 881 origin, direction, shape, length, width, multiplier, actionId: Projectile.identity);
876 882 }
877 883
878 private void StrikeDeathWorldCutAt(int eventTick, Player player, int cutIndex)
884 private void StrikeDeathWorldCutAt(int eventTick, Player player, Vector2 focus,
885 int cutIndex)
879 886 {
880 887 if (timer != eventTick || Main.netMode == NetmodeID.MultiplayerClient)
881 888 return;
@@ -886,10 +893,7 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
886 893 float randomAngle = (hash & 0x00FFFFFFu) / 16777215f * MathHelper.Pi
887 894 - MathHelper.PiOver2;
888 895 Vector2 axis = randomAngle.ToRotationVector2();
889 Vector2 normal = axis.RotatedBy(MathHelper.PiOver2);
890 float offset = MathHelper.Lerp(-980f, 980f,
891 ((hash >> 8) & 0xFFFFu) / 65535f);
892 Vector2 start = player.Center + normal * offset - axis * 2600f;
896 Vector2 start = focus - axis * 2600f;
893 897 ReaperStrikeProjectile.Spawn(Projectile.GetSource_FromThis(), player.whoAmI,
894 898 snapshot, ReaperHitKind.Ultimate, cutIndex, start, axis,
895 899 ReaperStrikeShape.Line, 5200f, 22f, 0.28f,
@@ -1104,8 +1108,33 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
1104 1108 visualCharge = 0f;
1105 1109 if (ultimate)
1106 1110 {
1107 float sweep = timer * 0.085f * facing;
1108 visualWeaponAngle = aimAngle - 1.65f * facing + sweep;
1111 if (snapshot.Form == ReaperFormId.Death)
1112 {
1113 if (timer < 20)
1114 {
1115 visualWeaponAngle = aimAngle - 2.25f * facing;
1116 }
1117 else if (timer < 128)
1118 {
1119 int cut = Math.Clamp((timer - 20) / 6, 0, 17);
1120 float local = MathHelper.Clamp(((timer - 20) % 6) / 5f,
1121 0f, 1f);
1122 int direction = (cut & 1) == 0 ? facing : -facing;
1123 visualWeaponAngle = MathHelper.Lerp(
1124 aimAngle - 2.35f * direction,
1125 aimAngle + 2.15f * direction,
1126 SmoothStep(local));
1127 }
1128 else
1129 {
1130 visualWeaponAngle = aimAngle - 1.35f * facing;
1131 }
1132 }
1133 else
1134 {
1135 float sweep = timer * 0.085f * facing;
1136 visualWeaponAngle = aimAngle - 1.65f * facing + sweep;
1137 }
1109 1138 }
1110 1139 else if (IsChargedSpecial)
1111 1140 {
@@ -1228,6 +1257,27 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
1228 1257 }
1229 1258 }
1230 1259
1260 private void DrawDeathUltimateSwingCrescent(Vector2 grip)
1261 {
1262 if (timer < 20 || timer >= 128)
1263 return;
1264 int cut = Math.Clamp((timer - 20) / 6, 0, 17);
1265 float local = MathHelper.Clamp(((timer - 20) % 6) / 5f, 0f, 1f);
1266 int facing = Math.Abs(aim.X) > 0.05f ? Math.Sign(aim.X) : 1;
1267 int direction = (cut & 1) == 0 ? facing : -facing;
1268 float visibility = SmoothStep(MathHelper.Clamp(local / 0.22f, 0f, 1f))
1269 * (1f - SmoothStep(MathHelper.Clamp((local - 0.76f) / 0.24f, 0f, 1f)));
1270 float radius = 430f;
1271 Vector2 normal = aim.RotatedBy(MathHelper.PiOver2);
1272 Vector2 center = grip + aim * radius * 0.06f
1273 - normal * direction * radius * 0.10f;
1274 float rotation = aim.ToRotation()
1275 + MathHelper.Lerp(-0.11f, 0.09f, SmoothStep(local)) * direction;
1276 ReaperCrescentPrimitiveTextureSystem.DrawDeathDomainCrescent(center,
1277 rotation, radius, visibility * 0.94f, 3, direction,
1278 Main.GlobalTimeWrappedHourly, local);
1279 }
1280
1231 1281 private void DrawSpecialStoryboard(Player player, Vector2 grip)
1232 1282 {
1233 1283 Texture2D pixel = TextureAssets.MagicPixel.Value;
Modified Projectiles/ReaperBloodArcScarProjectile.cs +2 -24
@@ -139,9 +139,8 @@ public sealed class ReaperBloodArcScarProjectile : ModProjectile
139 139 for (int index = 1; index <= visible; index++)
140 140 {
141 141 float width = Math.Max(0.7f, baseWidth * GetTaper(index - 0.5f));
142 DrawTube(points[index - 1], points[index], width, fade);
143 if (index < visible)
144 DrawJoint(points[index], width, fade);
142 ReaperBloodTrailRenderer.DrawSegment(points[index - 1], points[index],
143 width, fade);
145 144 }
146 145 return false;
147 146 }
@@ -206,27 +205,6 @@ public sealed class ReaperBloodArcScarProjectile : ModProjectile
206 205 Projectile.timeLeft = lifetime;
207 206 }
208 207
209 private static void DrawTube(Vector2 start, Vector2 end, float width, float opacity)
210 {
211 DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
212 new Color(92, 0, 18, 0) * (opacity * 0.58f), width * 1.55f);
213 DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
214 new Color(248, 18, 50, 235) * (opacity * 0.94f), width * 1.18f);
215 DeathDomainPrimitiveTextureSystem.DrawSmoothWorldStrip(start, end,
216 new Color(8, 0, 3, 252) * opacity, width * 0.72f);
217 }
218
219 private static void DrawJoint(Vector2 point, float width, float opacity)
220 {
221 Vector2 screen = point - Main.screenPosition;
222 DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch, screen,
223 width * 0.78f, new Color(92, 0, 18, 0) * (opacity * 0.58f));
224 DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch, screen,
225 width * 0.59f, new Color(248, 18, 50, 235) * (opacity * 0.94f));
226 DeathDomainPrimitiveTextureSystem.DrawSmoothDisc(Main.spriteBatch, screen,
227 width * 0.36f, new Color(8, 0, 3, 252) * opacity);
228 }
229
230 208 private float GetTaper(float segmentIndex)
231 209 {
232 210 float progress = segmentIndex / Math.Max(1f, points.Count - 1f);
Modified Projectiles/ReaperBloodBladeProjectile.cs +1 -8
Deleted Projectiles/ReaperDeathDomainHarvestProjectile.cs +0 -211
Added Projectiles/ReaperDeathDomainRiftProjectile.cs +255 -0
Modified Projectiles/ReaperDeathEchoScytheProjectile.cs +2 -6
Modified Projectiles/SickleSwingProjectile.cs +99 -34