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