返回提交历史
Added
Common/DeathDomainCrescentVisualSystem.cs
+238
-0
Modified
Common/MyPlayer.cs
+117
-0
Added
Common/ReaperBloodTrailRenderer.cs
+20
-0
Modified
Common/ReaperCombatDefinitions.cs
+1
-1
Modified
Common/ReaperCombatService.cs
+4
-15
Modified
Common/ReaperCrescentPrimitiveTextureSystem.cs
+424
-45
Modified
Common/ReaperUltimateVisualSystem.cs
+14
-14
Modified
Localization/en-US.hjson
+1
-1
Modified
Localization/zh-Hans.hjson
+1
-1
Modified
Projectiles/ReaperActionControllerProjectile.cs
+62
-12
Modified
Projectiles/ReaperBloodArcScarProjectile.cs
+2
-24
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
XFEstudio/DeathMod
统一镰刀刀光并重构死亡领域裂缝
7bce33f
代码差异
16 个文件
+1241
-372
@@ -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
}
@@ -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
@@ -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
}
@@ -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
}
@@ -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)
@@ -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
@@ -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;
@@ -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: {
@@ -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: {
@@ -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;
@@ -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);