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