返回提交历史
Modified
Common/BossSoulParticipationSystem.cs
+110
-3
Added
Common/DeathAltarReforgePreservation.cs
+33
-0
Modified
Common/DeathDomainVisualSystem.cs
+305
-27
Modified
Common/MyGlobalNPC.cs
+104
-12
Modified
Common/MyGlobalProjectile.cs
+19
-13
Modified
Common/MyPlayer.cs
+15
-8
Modified
DeathMod.cs
+6
-0
Modified
Items/DeathNecklace.cs
+10
-0
Modified
Items/DeathRobe.cs
+10
-0
Modified
Items/DeathWings.cs
+24
-9
Modified
Items/NormalSickle.cs
+36
-9
Modified
Projectiles/LifeStealWispProjectile.cs
+39
-24
Modified
Projectiles/VoidRiftProjectile.cs
+73
-12
Modified
README.md
+10
-10
Modified
README.zh-Hans.md
+10
-10
Modified
UI/DeathAltarUISystem.cs
+180
-26
Modified
docs/PLAY_GUIDE.md
+23
-21
Modified
docs/PLAY_GUIDE.zh-Hans.md
+23
-21
XFEstudio/DeathMod
完善灵魂奖励、祭坛升级与死亡领域特效
39c499b
代码差异
18 个文件
+1030
-205
@@ -8,20 +8,55 @@ internal class BossSoulParticipationSystem : ModSystem
8
8
{
9
9
private static readonly bool[] TwinParticipants = new bool[Main.maxPlayers];
10
10
private static readonly bool[] EaterParticipants = new bool[Main.maxPlayers];
11
private static readonly bool[] BrainParticipants = new bool[Main.maxPlayers];
12
private static readonly bool[] GolemParticipants = new bool[Main.maxPlayers];
13
private static readonly bool[] MoonLordParticipants = new bool[Main.maxPlayers];
14
private static int twinEncounterLifeMax;
15
private static int eaterEncounterLifeMax;
16
private static int brainEncounterLifeMax;
17
private static int golemEncounterLifeMax;
18
private static int moonLordEncounterLifeMax;
11
19
12
20
public override void OnWorldUnload()
13
21
{
14
22
Clear(TwinParticipants);
15
23
Clear(EaterParticipants);
24
Clear(BrainParticipants);
25
Clear(GolemParticipants);
26
Clear(MoonLordParticipants);
27
twinEncounterLifeMax = 0;
28
eaterEncounterLifeMax = 0;
29
brainEncounterLifeMax = 0;
30
golemEncounterLifeMax = 0;
31
moonLordEncounterLifeMax = 0;
16
32
}
17
33
18
34
public override void PostUpdateWorld()
19
35
{
20
if (!NPC.AnyNPCs(NPCID.Retinazer) && !NPC.AnyNPCs(NPCID.Spazmatism))
36
int twinLife = SumActiveLifeMax(NPCID.Retinazer, NPCID.Spazmatism);
37
if (twinLife > 0)
38
twinEncounterLifeMax = System.Math.Max(twinEncounterLifeMax, twinLife);
39
else
40
{
21
41
Clear(TwinParticipants);
42
twinEncounterLifeMax = 0;
43
}
22
44
23
if (!NPC.AnyNPCs(NPCID.EaterofWorldsBody) && !NPC.AnyNPCs(NPCID.EaterofWorldsHead) && !NPC.AnyNPCs(NPCID.EaterofWorldsTail))
45
int eaterLife = SumActiveLifeMax(NPCID.EaterofWorldsBody, NPCID.EaterofWorldsHead, NPCID.EaterofWorldsTail);
46
if (eaterLife > 0)
47
eaterEncounterLifeMax = System.Math.Max(eaterEncounterLifeMax, eaterLife);
48
else
49
{
24
50
Clear(EaterParticipants);
51
eaterEncounterLifeMax = 0;
52
}
53
54
if (!TrackEncounterLife(ref brainEncounterLifeMax, NPCID.BrainofCthulhu, NPCID.Creeper))
55
Clear(BrainParticipants);
56
if (!TrackEncounterLife(ref golemEncounterLifeMax, NPCID.Golem, NPCID.GolemHead, NPCID.GolemHeadFree))
57
Clear(GolemParticipants);
58
if (!TrackEncounterLife(ref moonLordEncounterLifeMax, NPCID.MoonLordCore, NPCID.MoonLordHead, NPCID.MoonLordHand))
59
Clear(MoonLordParticipants);
25
60
}
26
61
27
62
internal static void RecordCompositeBossHit(NPC npc, int playerIndex)
@@ -33,22 +68,33 @@ internal class BossSoulParticipationSystem : ModSystem
33
68
TwinParticipants[playerIndex] = true;
34
69
else if (npc.type is NPCID.EaterofWorldsBody or NPCID.EaterofWorldsHead or NPCID.EaterofWorldsTail)
35
70
EaterParticipants[playerIndex] = true;
71
else if (npc.type is NPCID.BrainofCthulhu or NPCID.Creeper)
72
BrainParticipants[playerIndex] = true;
73
else if (npc.type is NPCID.Golem or NPCID.GolemHead or NPCID.GolemHeadFree)
74
GolemParticipants[playerIndex] = true;
75
else if (npc.type is NPCID.MoonLordCore or NPCID.MoonLordHead or NPCID.MoonLordHand)
76
MoonLordParticipants[playerIndex] = true;
36
77
}
37
78
38
internal static bool TryTakeCompositeParticipants(NPC npc, out bool[] participants)
79
internal static bool TryTakeCompositeParticipants(NPC npc, out bool[] participants, out int encounterLifeMax)
39
80
{
40
81
participants = [];
82
encounterLifeMax = 0;
41
83
switch (npc.type)
42
84
{
43
85
case NPCID.Retinazer:
44
86
if (NPC.AnyNPCs(NPCID.Spazmatism))
45
87
return false;
46
88
participants = Take(TwinParticipants);
89
encounterLifeMax = System.Math.Max(npc.lifeMax, twinEncounterLifeMax);
90
twinEncounterLifeMax = 0;
47
91
return true;
48
92
case NPCID.Spazmatism:
49
93
if (NPC.AnyNPCs(NPCID.Retinazer))
50
94
return false;
51
95
participants = Take(TwinParticipants);
96
encounterLifeMax = System.Math.Max(npc.lifeMax, twinEncounterLifeMax);
97
twinEncounterLifeMax = 0;
52
98
return true;
53
99
case NPCID.EaterofWorldsBody:
54
100
case NPCID.EaterofWorldsHead:
@@ -56,12 +102,73 @@ internal class BossSoulParticipationSystem : ModSystem
56
102
if (NPC.AnyNPCs(NPCID.EaterofWorldsBody) || NPC.AnyNPCs(NPCID.EaterofWorldsHead) || NPC.AnyNPCs(NPCID.EaterofWorldsTail))
57
103
return false;
58
104
participants = Take(EaterParticipants);
105
encounterLifeMax = System.Math.Max(npc.lifeMax, eaterEncounterLifeMax);
106
eaterEncounterLifeMax = 0;
107
return true;
108
case NPCID.BrainofCthulhu:
109
participants = Take(BrainParticipants);
110
encounterLifeMax = System.Math.Max(npc.lifeMax, brainEncounterLifeMax);
111
brainEncounterLifeMax = 0;
112
return true;
113
case NPCID.Golem:
114
participants = Take(GolemParticipants);
115
encounterLifeMax = System.Math.Max(npc.lifeMax, golemEncounterLifeMax);
116
golemEncounterLifeMax = 0;
117
return true;
118
case NPCID.MoonLordCore:
119
participants = Take(MoonLordParticipants);
120
encounterLifeMax = System.Math.Max(npc.lifeMax, moonLordEncounterLifeMax);
121
moonLordEncounterLifeMax = 0;
59
122
return true;
60
123
default:
61
124
return false;
62
125
}
63
126
}
64
127
128
internal static int GetEncounterLifeMax(NPC npc)
129
{
130
int trackedLife = npc.type switch
131
{
132
NPCID.BrainofCthulhu => brainEncounterLifeMax,
133
NPCID.Golem => golemEncounterLifeMax,
134
NPCID.MoonLordCore => moonLordEncounterLifeMax,
135
_ => 0
136
};
137
return System.Math.Max(npc.lifeMax, trackedLife);
138
}
139
140
private static bool TrackEncounterLife(ref int storedLifeMax, params int[] npcTypes)
141
{
142
int currentLifeMax = SumActiveLifeMax(npcTypes);
143
if (currentLifeMax > 0)
144
{
145
storedLifeMax = System.Math.Max(storedLifeMax, currentLifeMax);
146
return true;
147
}
148
else
149
{
150
storedLifeMax = 0;
151
return false;
152
}
153
}
154
155
private static int SumActiveLifeMax(params int[] npcTypes)
156
{
157
int total = 0;
158
foreach (NPC npc in Main.ActiveNPCs)
159
{
160
for (int index = 0; index < npcTypes.Length; index++)
161
{
162
if (npc.type != npcTypes[index])
163
continue;
164
165
total = (int)System.Math.Min(int.MaxValue, (long)total + npc.lifeMax);
166
break;
167
}
168
}
169
return total;
170
}
171
65
172
private static bool[] Take(bool[] source)
66
173
{
67
174
bool[] result = (bool[])source.Clone();
@@ -0,0 +1,33 @@
1
using Terraria.ModLoader;
2
using Terraria.ModLoader.IO;
3
4
namespace DeathMod.Common;
5
6
/// <summary>
7
/// Keeps altar progression independent from the vanilla prefix reset performed while reforging.
8
/// Reforging is local and synchronous, so one pending snapshot is sufficient.
9
/// </summary>
10
internal static class DeathAltarReforgePreservation
11
{
12
private static TagCompound? pendingData;
13
private static int pendingItemType;
14
15
internal static void Capture(ModItem modItem)
16
{
17
TagCompound snapshot = [];
18
modItem.SaveData(snapshot);
19
pendingData = snapshot;
20
pendingItemType = modItem.Item.type;
21
}
22
23
internal static void Restore(ModItem modItem)
24
{
25
if (pendingData is null || pendingItemType != modItem.Item.type)
26
return;
27
28
TagCompound snapshot = pendingData;
29
pendingData = null;
30
pendingItemType = 0;
31
modItem.LoadData(snapshot);
32
}
33
}
@@ -11,20 +11,80 @@ namespace DeathMod.Common;
11
11
[Autoload(Side = ModSide.Client)]
12
12
internal class DeathDomainVisualSystem : ModSystem
13
13
{
14
private const float OpenAnimationSpeed = 1f / 26f;
15
private const float CloseAnimationSpeed = 1f / 20f;
16
17
private readonly float[] domainAnimation = new float[Main.maxPlayers];
18
private readonly float[] cachedRadius = new float[Main.maxPlayers];
19
private readonly int[] cachedMastery = new int[Main.maxPlayers];
20
private readonly bool[] cachedFullScreen = new bool[Main.maxPlayers];
21
22
public override void PostUpdateEverything()
23
{
24
if (Main.gameMenu)
25
{
26
ClearAnimationState();
27
return;
28
}
29
30
for (int index = 0; index < Main.maxPlayers; index++)
31
{
32
Player player = Main.player[index];
33
if (!player.active)
34
{
35
domainAnimation[index] = 0f;
36
cachedRadius[index] = 0f;
37
cachedMastery[index] = 0;
38
cachedFullScreen[index] = false;
39
continue;
40
}
41
42
MyPlayer data = player.GetModPlayer<MyPlayer>();
43
DeathNecklace? necklace = data.ActiveDeathNecklace;
44
if (necklace is not null)
45
{
46
cachedRadius[index] = necklace.DomainRadius;
47
cachedMastery[index] = necklace.MasteryScore;
48
cachedFullScreen[index] = necklace.IsFullScreenDomain;
49
}
50
51
bool enabled = !player.dead && necklace is not null && data.DeathDomainEnabled;
52
float speed = enabled ? OpenAnimationSpeed : CloseAnimationSpeed;
53
domainAnimation[index] = MathHelper.Clamp(
54
domainAnimation[index] + (enabled ? speed : -speed),
55
0f,
56
1f);
57
}
58
}
59
60
public override void OnWorldUnload()
61
{
62
ClearAnimationState();
63
}
64
14
65
public override void PostDrawTiles()
15
66
{
16
67
Player localPlayer = Main.LocalPlayer;
17
if (!localPlayer.active || localPlayer.dead)
68
if (Main.gameMenu || !localPlayer.active)
18
69
return;
19
MyPlayer localData = localPlayer.GetModPlayer<MyPlayer>();
20
DeathNecklace? localNecklace = localData.ActiveDeathNecklace;
21
if (localNecklace is null || !localData.DeathDomainEnabled)
70
71
bool anyVisibleDomain = false;
72
for (int index = 0; index < Main.maxPlayers; index++)
73
{
74
if (domainAnimation[index] > 0.001f)
75
{
76
anyVisibleDomain = true;
77
break;
78
}
79
}
80
if (!anyVisibleDomain)
22
81
return;
23
82
24
83
SpriteBatch batch = Main.spriteBatch;
25
84
Texture2D pixel = TextureAssets.MagicPixel.Value;
26
if (localNecklace.IsFullScreenDomain)
27
DrawSovereignBackground(batch, pixel);
85
float localAnimation = domainAnimation[Main.myPlayer];
86
if (cachedFullScreen[Main.myPlayer] && localAnimation > 0.001f)
87
DrawSovereignBackground(batch, pixel, localAnimation);
28
88
29
89
batch.Begin(
30
90
SpriteSortMode.Deferred,
@@ -38,24 +98,39 @@ internal class DeathDomainVisualSystem : ModSystem
38
98
for (int index = 0; index < Main.maxPlayers; index++)
39
99
{
40
100
Player player = Main.player[index];
41
if (!player.active || player.dead)
101
float animation = domainAnimation[index];
102
if (!player.active || animation <= 0.001f)
42
103
continue;
43
MyPlayer data = player.GetModPlayer<MyPlayer>();
44
DeathNecklace? necklace = data.ActiveDeathNecklace;
45
if (necklace is null || !data.DeathDomainEnabled)
104
105
float radius = cachedRadius[index];
106
int mastery = cachedMastery[index];
107
bool fullScreen = cachedFullScreen[index];
108
if (radius <= 0f)
109
radius = 160f;
110
Vector2 screenCenter = player.Center - Main.screenPosition;
111
if (!fullScreen
112
&& (screenCenter.X < -radius
113
|| screenCenter.X > Main.screenWidth + radius
114
|| screenCenter.Y < -radius
115
|| screenCenter.Y > Main.screenHeight + radius))
116
{
46
117
continue;
47
if (!necklace.IsFullScreenDomain)
48
DrawDomainRing(batch, pixel, player.Center, necklace.DomainRadius, necklace.MasteryScore);
118
}
119
if (!fullScreen)
120
DrawDomainRing(batch, pixel, screenCenter, radius, mastery, animation);
121
else
122
DrawSovereignSeal(batch, pixel, screenCenter, mastery, animation);
49
123
}
50
124
batch.End();
51
125
}
52
126
53
private static void DrawSovereignBackground(SpriteBatch batch, Texture2D pixel)
127
private static void DrawSovereignBackground(SpriteBatch batch, Texture2D pixel, float animation)
54
128
{
55
129
batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone);
56
130
float time = Main.GlobalTimeWrappedHourly;
57
batch.Draw(pixel, new Rectangle(0, 0, Main.screenWidth, Main.screenHeight), new Color(8, 2, 18) * 0.42f);
58
batch.Draw(pixel, new Rectangle(0, 0, Main.screenWidth, Main.screenHeight / 2), new Color(28, 8, 55) * 0.18f);
131
float reveal = MathHelper.SmoothStep(0f, 1f, animation);
132
batch.Draw(pixel, new Rectangle(0, 0, Main.screenWidth, Main.screenHeight), new Color(8, 2, 18) * (0.42f * reveal));
133
batch.Draw(pixel, new Rectangle(0, 0, Main.screenWidth, Main.screenHeight / 2), new Color(28, 8, 55) * (0.18f * reveal));
59
134
60
135
for (int index = 0; index < 54; index++)
61
136
{
@@ -65,31 +140,205 @@ internal class DeathDomainVisualSystem : ModSystem
65
140
float pulse = 0.45f + (float)Math.Sin(time * 2.4f + seed) * 0.25f;
66
141
Color color = index % 3 == 0 ? new Color(215, 30, 105) : new Color(65, 205, 245);
67
142
float length = 7f + index % 6 * 3f;
68
batch.Draw(pixel, new Vector2(x, y), null, color * pulse, -0.7f, Vector2.Zero, new Vector2(length, 1.2f), SpriteEffects.None, 0f);
143
batch.Draw(pixel, new Vector2(x, y), null, color * (pulse * reveal), -0.7f, Vector2.Zero,
144
new Vector2(length / pixel.Width, 1.2f / pixel.Height), SpriteEffects.None, 0f);
69
145
}
70
146
71
147
Vector2 center = new(Main.screenWidth * 0.5f, Main.screenHeight * 0.5f);
72
148
for (int ring = 0; ring < 3; ring++)
73
149
{
74
150
float radius = Math.Min(Main.screenWidth, Main.screenHeight) * (0.22f + ring * 0.12f);
75
DrawCircle(batch, pixel, center, radius, 72, new Color(80 + ring * 25, 65, 155 + ring * 25) * (0.12f - ring * 0.02f), 2f, time * (ring % 2 == 0 ? 0.12f : -0.09f));
151
DrawCircle(batch, pixel, center, radius, 72, new Color(80 + ring * 25, 65, 155 + ring * 25) * ((0.12f - ring * 0.02f) * reveal), 2f, time * (ring % 2 == 0 ? 0.12f : -0.09f));
76
152
}
77
153
batch.End();
78
154
}
79
155
80
private static void DrawDomainRing(SpriteBatch batch, Texture2D pixel, Vector2 center, float radius, int mastery)
156
private static void DrawDomainRing(SpriteBatch batch, Texture2D pixel, Vector2 center, float targetRadius, int mastery, float animation)
81
157
{
82
158
float time = Main.GlobalTimeWrappedHourly;
83
float pulse = 0.62f + (float)Math.Sin(time * 3.2f) * 0.14f;
84
Color color = Color.Lerp(new Color(185, 25, 80), new Color(65, 220, 242), MathHelper.Clamp(mastery / 70f, 0f, 1f));
85
DrawCircle(batch, pixel, center, radius, 80, color * pulse * 0.6f, 2.5f, time * 0.15f);
86
DrawCircle(batch, pixel, center, radius - 5f, 80, new Color(120, 50, 175) * pulse * 0.32f, 1.2f, -time * 0.1f);
159
float reveal = MathHelper.SmoothStep(0f, 1f, animation);
160
float expansion = 1f - (float)Math.Pow(1f - reveal, 3f);
161
float overshoot = 1f + (float)Math.Sin(reveal * MathHelper.Pi) * 0.035f;
162
float radius = MathHelper.Lerp(8f, targetRadius, expansion) * overshoot;
163
float pulse = (0.82f + (float)Math.Sin(time * 3.2f) * 0.12f) * reveal;
164
float masteryProgress = GetMasteryProgress(mastery);
165
DrawBlackHoleField(batch, pixel, center, radius, masteryProgress, pulse, time, reveal);
166
DrawNoiseBoundary(batch, pixel, center, radius + 3f, time, 0.4f, 12f, 9f, 0.18f * pulse, masteryProgress, gaps: true);
167
DrawNoiseBoundary(batch, pixel, center, radius, time, 1.7f, 9f, 5f, 0.42f * pulse, masteryProgress, gaps: true);
168
DrawNoiseBoundary(batch, pixel, center, radius - 2f, time, 3.3f, 7f, 2.7f + masteryProgress, 0.94f * pulse, masteryProgress, gaps: false);
169
DrawNoiseBoundary(batch, pixel, center, radius - 7f, -time, 5.1f, 5f, 1.4f, 0.58f * pulse, masteryProgress, gaps: true);
170
}
87
171
88
for (int index = 0; index < 12; index++)
172
private static void DrawBlackHoleField(SpriteBatch batch, Texture2D pixel, Vector2 center, float radius, float mastery, float pulse, float time, float reveal)
173
{
174
float coreGap = MathHelper.Lerp(22f, 14f, mastery);
175
float coreRadius = radius > coreGap + 6f ? radius - coreGap : radius * 0.58f;
176
177
for (int arm = 0; arm < 5; arm++)
178
DrawAccretionSpiral(batch, pixel, center, coreRadius, Math.Max(coreRadius + 2f, radius - 3f), time, arm, mastery, pulse);
179
180
float gap = Math.Max(2f, radius - coreRadius);
181
DrawNoiseBoundary(batch, pixel, center, coreRadius + gap * 0.72f, -time * 1.35f, 9.4f, 4.5f, 7f, 0.18f * pulse, mastery, gaps: true);
182
DrawNoiseBoundary(batch, pixel, center, coreRadius + gap * 0.34f, time * 1.8f, 12.8f, 2.8f, 3.8f, 0.78f * pulse, mastery, gaps: true);
183
184
float coreNoiseTime = time * 0.8f;
185
const float coreNoiseSeed = 16.2f;
186
float coreNoiseAmplitude = Math.Min(coreRadius * 0.18f, MathHelper.Lerp(5f, 10f, mastery) * MathHelper.Lerp(0.35f, 1f, reveal));
187
float blackOpacity = MathHelper.Lerp(0.50f, 0.96f, mastery) * reveal;
188
DrawFilledNoiseDisc(batch, pixel, center, coreRadius, coreNoiseTime, coreNoiseSeed, coreNoiseAmplitude, Color.Black * blackOpacity);
189
DrawNoiseBoundary(batch, pixel, center, coreRadius, coreNoiseTime, coreNoiseSeed, coreNoiseAmplitude, 2.2f, 0.76f * pulse, mastery, gaps: false);
190
}
191
192
private static void DrawSovereignSeal(SpriteBatch batch, Texture2D pixel, Vector2 center, int mastery, float animation)
193
{
194
float time = Main.GlobalTimeWrappedHourly;
195
float reveal = MathHelper.SmoothStep(0f, 1f, animation);
196
float expansion = 1f - (float)Math.Pow(1f - reveal, 3f);
197
float pulse = (0.86f + (float)Math.Sin(time * 3.6f) * 0.1f) * reveal;
198
float targetRadius = 142f + (float)Math.Sin(time * 1.7f) * 6f;
199
float radius = MathHelper.Lerp(8f, targetRadius, expansion) * (1f + (float)Math.Sin(reveal * MathHelper.Pi) * 0.035f);
200
float masteryProgress = GetMasteryProgress(mastery);
201
DrawBlackHoleField(batch, pixel, center, radius, masteryProgress, pulse, time, reveal);
202
for (int layer = 0; layer < 5; layer++)
89
203
{
90
float angle = time * (0.18f + index % 3 * 0.04f) + index * MathHelper.TwoPi / 12f;
91
Vector2 position = center + angle.ToRotationVector2() * (radius - 4f);
92
batch.Draw(pixel, position, null, color * 0.72f, MathHelper.PiOver4, pixel.Size() * 0.5f, new Vector2(5f, 5f), SpriteEffects.None, 0f);
204
float layerProgress = layer / 4f;
205
DrawNoiseBoundary(
206
batch,
207
pixel,
208
center,
209
radius + MathHelper.Lerp(12f, -8f, layerProgress),
210
time * (layer % 2 == 0 ? 1f : -0.8f),
211
21f + layer * 1.37f,
212
MathHelper.Lerp(14f, 5f, layerProgress),
213
MathHelper.Lerp(10f, 1.6f, layerProgress),
214
MathHelper.Lerp(0.13f, 0.9f, layerProgress) * pulse,
215
1f,
216
gaps: layer != 4);
217
}
218
}
219
220
private static void DrawFilledNoiseDisc(
221
SpriteBatch batch,
222
Texture2D pixel,
223
Vector2 center,
224
float baseRadius,
225
float time,
226
float seed,
227
float noiseAmplitude,
228
Color color)
229
{
230
const int boundarySegments = 128;
231
const float preferredBandHeight = 3f;
232
Span<Vector2> boundary = stackalloc Vector2[boundarySegments];
233
float extent = 0f;
234
for (int index = 0; index < boundarySegments; index++)
235
{
236
float angle = MathHelper.TwoPi * index / boundarySegments;
237
float radius = Math.Max(1f, baseRadius + BoundaryNoise(angle, time, seed) * noiseAmplitude);
238
boundary[index] = angle.ToRotationVector2() * radius;
239
extent = Math.Max(extent, Math.Abs(boundary[index].Y));
240
}
241
242
int bands = Math.Max(1, (int)Math.Ceiling(extent * 2f / preferredBandHeight));
243
float bandHeight = extent * 2f / bands;
244
for (int index = 0; index < bands; index++)
245
{
246
float scanY = -extent + (index + 0.5f) * bandHeight;
247
float left = float.MaxValue;
248
float right = float.MinValue;
249
for (int edge = 0; edge < boundarySegments; edge++)
250
{
251
Vector2 start = boundary[edge];
252
Vector2 end = boundary[(edge + 1) % boundarySegments];
253
if ((start.Y <= scanY && end.Y > scanY) || (end.Y <= scanY && start.Y > scanY))
254
{
255
float amount = (scanY - start.Y) / (end.Y - start.Y);
256
float intersectionX = MathHelper.Lerp(start.X, end.X, amount);
257
left = Math.Min(left, intersectionX);
258
right = Math.Max(right, intersectionX);
259
}
260
}
261
262
if (left >= right)
263
continue;
264
265
Vector2 position = new(center.X + left, center.Y + scanY - bandHeight * 0.5f);
266
batch.Draw(pixel, position, null, color, 0f, Vector2.Zero,
267
new Vector2((right - left) / pixel.Width, bandHeight / pixel.Height), SpriteEffects.None, 0f);
268
}
269
}
270
271
private static void DrawNoiseBoundary(
272
SpriteBatch batch,
273
Texture2D pixel,
274
Vector2 center,
275
float baseRadius,
276
float time,
277
float seed,
278
float noiseAmplitude,
279
float width,
280
float opacity,
281
float mastery,
282
bool gaps)
283
{
284
const int segments = 128;
285
float previousAngle = 0f;
286
float previousRadius = baseRadius + BoundaryNoise(previousAngle, time, seed) * noiseAmplitude;
287
Vector2 previous = center + Vector2.UnitX * previousRadius;
288
Color darkRed = Color.Lerp(new Color(58, 0, 12), new Color(118, 0, 25), mastery);
289
Color brightRed = Color.Lerp(new Color(225, 12, 42), new Color(255, 72, 105), mastery);
290
291
for (int index = 1; index <= segments; index++)
292
{
293
float angle = MathHelper.TwoPi * index / segments;
294
float noisyRadius = baseRadius + BoundaryNoise(angle, time, seed) * noiseAmplitude;
295
Vector2 current = center + angle.ToRotationVector2() * noisyRadius;
296
Vector2 segment = current - previous;
297
float middleAngle = (previousAngle + angle) * 0.5f;
298
float flow = 0.5f + 0.5f * (float)Math.Sin(middleAngle * 3f - time * 1.9f + seed);
299
float arcMask = 0.58f + 0.42f * (float)Math.Sin(middleAngle * 2f + time * 0.47f + seed * 1.31f);
300
arcMask *= 0.72f + 0.28f * (float)Math.Sin(middleAngle * 7f - time * 0.83f + seed * 0.73f);
301
float visibility = gaps ? MathHelper.SmoothStep(0.08f, 0.62f, arcMask) : 1f;
302
if (visibility > 0.025f)
303
{
304
Color color = Color.Lerp(darkRed, brightRed, flow);
305
DrawSegment(batch, pixel, previous, segment, color * (opacity * visibility), width * (0.72f + flow * 0.42f));
306
}
307
308
previousAngle = angle;
309
previous = current;
310
}
311
}
312
313
private static float BoundaryNoise(float angle, float time, float seed)
314
{
315
float low = (float)Math.Sin(angle * 3f + time * 0.72f + seed) * 0.48f;
316
float middle = (float)Math.Sin(angle * 7f - time * 1.11f + seed * 1.73f) * 0.29f;
317
float high = (float)Math.Sin(angle * 13f + time * 1.67f + seed * 0.37f) * 0.16f;
318
float grain = (float)Math.Sin(angle * 29f - time * 2.23f + seed * 2.17f) * 0.07f;
319
return low + middle + high + grain;
320
}
321
322
private static void DrawAccretionSpiral(SpriteBatch batch, Texture2D pixel, Vector2 center, float coreRadius, float outerRadius, float time, int arm, float mastery, float pulse)
323
{
324
const int points = 34;
325
float armOffset = arm * MathHelper.TwoPi / 5f;
326
Vector2 previous = Vector2.Zero;
327
for (int index = 0; index < points; index++)
328
{
329
float progress = index / (points - 1f);
330
float angle = armOffset - time * (0.72f + arm * 0.035f) + progress * MathHelper.Pi * 1.12f;
331
float radius = MathHelper.Lerp(coreRadius * 0.88f, outerRadius, progress);
332
radius += BoundaryNoise(angle, time, arm * 2.7f) * 2.4f;
333
Vector2 current = center + angle.ToRotationVector2() * radius;
334
if (index > 0)
335
{
336
Vector2 segment = current - previous;
337
Color color = Color.Lerp(new Color(255, 70, 92), new Color(82, 0, 22), progress);
338
float fade = (float)Math.Sin(progress * MathHelper.Pi);
339
DrawSegment(batch, pixel, previous, segment, color * (fade * pulse * 0.54f), MathHelper.Lerp(2.6f + mastery, 0.7f, progress));
340
}
341
previous = current;
93
342
}
94
343
}
95
344
@@ -101,14 +350,43 @@ internal class DeathDomainVisualSystem : ModSystem
101
350
float angle = rotation + MathHelper.TwoPi * index / segments;
102
351
Vector2 current = center + angle.ToRotationVector2() * radius;
103
352
Vector2 delta = current - previous;
104
batch.Draw(pixel, previous, null, color, delta.ToRotation(), Vector2.Zero, new Vector2(delta.Length(), width), SpriteEffects.None, 0f);
353
batch.Draw(pixel, previous, null, color, delta.ToRotation(), Vector2.Zero,
354
new Vector2(delta.Length() / pixel.Width, width / pixel.Height), SpriteEffects.None, 0f);
105
355
previous = current;
106
356
}
107
357
}
108
358
359
private static void DrawSegment(SpriteBatch batch, Texture2D pixel, Vector2 start, Vector2 segment, Color color, float width)
360
{
361
batch.Draw(
362
pixel,
363
start,
364
null,
365
color,
366
segment.ToRotation(),
367
new Vector2(0f, pixel.Height * 0.5f),
368
new Vector2(segment.Length() / pixel.Width, width / pixel.Height),
369
SpriteEffects.None,
370
0f);
371
}
372
109
373
private static float PositiveModulo(float value, float modulus)
110
374
{
111
375
float result = value % modulus;
112
376
return result < 0f ? result + modulus : result;
113
377
}
378
379
private static float GetMasteryProgress(int mastery)
380
{
381
// A fresh necklace has a mastery score of 5; a fully completed tree reaches 68.
382
return MathHelper.Clamp((mastery - 5f) / 63f, 0f, 1f);
383
}
384
385
private void ClearAnimationState()
386
{
387
Array.Clear(domainAnimation, 0, domainAnimation.Length);
388
Array.Clear(cachedRadius, 0, cachedRadius.Length);
389
Array.Clear(cachedMastery, 0, cachedMastery.Length);
390
Array.Clear(cachedFullScreen, 0, cachedFullScreen.Length);
391
}
114
392
}
@@ -1,5 +1,6 @@
1
1
using DeathMod.Buffs;
2
2
using DeathMod.Items;
3
using DeathMod.Projectiles;
3
4
using Microsoft.Xna.Framework;
4
5
using Microsoft.Xna.Framework.Graphics;
5
6
using System;
@@ -21,6 +22,8 @@ public class MyGlobalNPC : GlobalNPC
21
22
22
23
private readonly bool[] sickleParticipants = new bool[Main.maxPlayers];
23
24
private readonly int[] fatedStackTimers = new int[AbsoluteMaximumFatedStacks];
25
private readonly int[] fatedStackOwners = new int[AbsoluteMaximumFatedStacks];
26
private readonly int[] fatedStackLifeStealLevels = new int[AbsoluteMaximumFatedStacks];
24
27
private int lastSicklePlayer = -1;
25
28
private int fatedSecondTimer;
26
29
private bool deathMarked;
@@ -31,6 +34,8 @@ public class MyGlobalNPC : GlobalNPC
31
34
{
32
35
Array.Clear(sickleParticipants);
33
36
Array.Clear(fatedStackTimers);
37
Array.Fill(fatedStackOwners, -1);
38
Array.Clear(fatedStackLifeStealLevels);
34
39
lastSicklePlayer = -1;
35
40
fatedSecondTimer = 0;
36
41
deathMarked = false;
@@ -39,13 +44,22 @@ public class MyGlobalNPC : GlobalNPC
39
44
40
45
public override void ModifyHitByItem(NPC npc, Player player, Item item, ref NPC.HitModifiers modifiers)
41
46
{
42
if (Main.netMode == NetmodeID.MultiplayerClient
43
&& player.whoAmI == Main.myPlayer
44
&& item.ModItem is NormalSickle or LegacyDeath)
47
if (item.ModItem is not (NormalSickle or LegacyDeath))
48
return;
49
50
if (Main.netMode == NetmodeID.MultiplayerClient)
45
51
{
52
if (player.whoAmI != Main.myPlayer)
53
return;
54
46
55
// Send this before damage is resolved so a lethal first hit is registered in time.
47
56
DeathMod.SendSickleItemHit(npc);
57
return;
48
58
}
59
60
// The server records the reaper before damage is applied. This avoids a one-shot kill
61
// racing ahead of the client's early registration packet.
62
RegisterSickleHit(npc, player.whoAmI);
49
63
}
50
64
51
65
public override void OnHitByItem(NPC npc, Player player, Item item, NPC.HitInfo hit, int damageDone)
@@ -111,12 +125,22 @@ public class MyGlobalNPC : GlobalNPC
111
125
if (Main.netMode == NetmodeID.MultiplayerClient || npc.friendly || npc.lifeMax <= 5)
112
126
return;
113
127
114
if (TryGetBossParticipants(npc, out bool[] bossParticipants))
128
// Dungeon Guardians and Paladins are exceptional normal enemies: their unusual
129
// health values should not distort the regular curve, and they always cap it.
130
if (IsFixedMaximumSoulEnemy(npc.type))
131
{
132
if (lastSicklePlayer >= 0 && lastSicklePlayer < Main.maxPlayers && Main.player[lastSicklePlayer].active)
133
Main.player[lastSicklePlayer].GetModPlayer<MyPlayer>().AddSouls(50);
134
return;
135
}
136
137
if (TryGetBossParticipants(npc, out bool[] bossParticipants, out int encounterLifeMax))
115
138
{
139
int reward = CalculateSoulReward(encounterLifeMax, boss: true);
116
140
for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++)
117
141
{
118
142
if (bossParticipants[playerIndex] && Main.player[playerIndex].active)
119
Main.player[playerIndex].GetModPlayer<MyPlayer>().AddSouls(MyPlayer.BossSoulReward, announce: true);
143
Main.player[playerIndex].GetModPlayer<MyPlayer>().AddSouls(reward, announce: true);
120
144
}
121
145
return;
122
146
}
@@ -128,7 +152,7 @@ public class MyGlobalNPC : GlobalNPC
128
152
return;
129
153
130
154
if (!npc.boss && lastSicklePlayer >= 0 && lastSicklePlayer < Main.maxPlayers && Main.player[lastSicklePlayer].active)
131
Main.player[lastSicklePlayer].GetModPlayer<MyPlayer>().AddSouls(1);
155
Main.player[lastSicklePlayer].GetModPlayer<MyPlayer>().AddSouls(CalculateSoulReward(npc.lifeMax, boss: false));
132
156
}
133
157
134
158
internal void RegisterSickleHit(NPC npc, int playerIndex, bool applyDeathMark = true)
@@ -154,7 +178,7 @@ public class MyGlobalNPC : GlobalNPC
154
178
}
155
179
}
156
180
157
internal void AddFatedStacks(NPC npc, int amount, int maximumStacks, int durationFrames)
181
internal void AddFatedStacks(NPC npc, int amount, int maximumStacks, int durationFrames, int playerIndex, int lifeStealLevel)
158
182
{
159
183
if (amount <= 0 || Main.netMode == NetmodeID.MultiplayerClient || npc.friendly || npc.lifeMax <= 5)
160
184
return;
@@ -163,7 +187,12 @@ public class MyGlobalNPC : GlobalNPC
163
187
durationFrames = Math.Clamp(durationFrames, 60, ushort.MaxValue);
164
188
int stacksToAdd = Math.Min(amount, Math.Max(0, maximumStacks - FatedStackCount));
165
189
for (int index = 0; index < stacksToAdd; index++)
166
fatedStackTimers[FatedStackCount++] = durationFrames;
190
{
191
fatedStackTimers[FatedStackCount] = durationFrames;
192
fatedStackOwners[FatedStackCount] = playerIndex is >= 0 and < Main.maxPlayers ? playerIndex : -1;
193
fatedStackLifeStealLevels[FatedStackCount] = Math.Max(0, lifeStealLevel);
194
FatedStackCount++;
195
}
167
196
168
197
for (int index = 0; index < FatedStackCount; index++)
169
198
fatedStackTimers[index] = Math.Max(fatedStackTimers[index], durationFrames);
@@ -203,7 +232,12 @@ public class MyGlobalNPC : GlobalNPC
203
232
for (int index = 0; index < FatedStackCount; index++)
204
233
{
205
234
if (--fatedStackTimers[index] > 0)
206
fatedStackTimers[writeIndex++] = fatedStackTimers[index];
235
{
236
fatedStackTimers[writeIndex] = fatedStackTimers[index];
237
fatedStackOwners[writeIndex] = fatedStackOwners[index];
238
fatedStackLifeStealLevels[writeIndex] = fatedStackLifeStealLevels[index];
239
writeIndex++;
240
}
207
241
}
208
242
209
243
if (writeIndex != FatedStackCount)
@@ -226,6 +260,20 @@ public class MyGlobalNPC : GlobalNPC
226
260
bool bossOrBossPart = npc.boss || npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].boss;
227
261
float ratePerStack = bossOrBossPart ? 0.01f : 1f / 3f;
228
262
int damage = Math.Max(1, (int)Math.Ceiling(npc.lifeMax * ratePerStack * FatedStackCount));
263
int lifeBeforeHit = npc.life;
264
Vector2 lifeStealSource = npc.Center;
265
int[] stackCountsByPlayer = new int[Main.maxPlayers];
266
int[] lifeStealLevelByPlayer = new int[Main.maxPlayers];
267
for (int index = 0; index < FatedStackCount; index++)
268
{
269
int owner = fatedStackOwners[index];
270
if (owner < 0 || owner >= Main.maxPlayers)
271
continue;
272
273
stackCountsByPlayer[owner]++;
274
lifeStealLevelByPlayer[owner] = Math.Max(lifeStealLevelByPlayer[owner], fatedStackLifeStealLevels[index]);
275
}
276
229
277
NPC.HitInfo fateHit = new()
230
278
{
231
279
Damage = damage,
@@ -238,6 +286,19 @@ public class MyGlobalNPC : GlobalNPC
238
286
npc.StrikeNPC(fateHit, fromNet: false, noPlayerInteraction: false);
239
287
if (Main.netMode == NetmodeID.Server)
240
288
NetMessage.SendStrikeNPC(npc, in fateHit);
289
290
int actualDamage = Math.Min(Math.Max(0, lifeBeforeHit), damage);
291
for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++)
292
{
293
int stackCount = stackCountsByPlayer[playerIndex];
294
int lifeStealLevel = lifeStealLevelByPlayer[playerIndex];
295
if (stackCount <= 0 || lifeStealLevel <= 0 || !Main.player[playerIndex].active || Main.player[playerIndex].dead)
296
continue;
297
298
int attributedDamage = Math.Max(1, (int)Math.Round(actualDamage * stackCount / (double)FatedStackCount));
299
int healedLife = Main.player[playerIndex].GetModPlayer<MyPlayer>().TryLifeSteal(attributedDamage, lifeStealLevel);
300
LifeStealVisuals.Spawn(lifeStealSource, playerIndex, lifeStealLevel, healedLife);
301
}
241
302
}
242
303
243
304
private void SpawnStatusParticles(NPC npc)
@@ -267,27 +328,48 @@ public class MyGlobalNPC : GlobalNPC
267
328
}
268
329
}
269
330
270
private bool TryGetBossParticipants(NPC npc, out bool[] participants)
331
internal static int CalculateSoulReward(int lifeMax, bool boss)
271
332
{
272
if (BossSoulParticipationSystem.TryTakeCompositeParticipants(npc, out participants))
333
lifeMax = Math.Max(1, lifeMax);
334
if (boss)
335
return Math.Clamp((int)Math.Ceiling(1.3d * Math.Sqrt(lifeMax)), 1, 500);
336
337
double normalizedLife = lifeMax / 25d;
338
return Math.Clamp((int)Math.Ceiling(Math.Pow(normalizedLife, 0.55d)), 1, 50);
339
}
340
341
private bool TryGetBossParticipants(NPC npc, out bool[] participants, out int encounterLifeMax)
342
{
343
if (BossSoulParticipationSystem.TryTakeCompositeParticipants(npc, out participants, out encounterLifeMax))
273
344
return true;
274
345
275
346
if (IsCompositeBossPart(npc.type))
347
{
348
encounterLifeMax = 0;
276
349
return false;
350
}
277
351
278
352
if (!npc.boss)
353
{
354
encounterLifeMax = 0;
279
355
return false;
356
}
280
357
281
358
if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs && npc.realLife != npc.whoAmI && Main.npc[npc.realLife].active)
359
{
360
encounterLifeMax = 0;
282
361
return false;
362
}
283
363
284
364
if (npc.realLife >= 0 && npc.realLife < Main.maxNPCs)
285
365
{
286
366
participants = (bool[])Main.npc[npc.realLife].GetGlobalNPC<MyGlobalNPC>().sickleParticipants.Clone();
367
encounterLifeMax = BossSoulParticipationSystem.GetEncounterLifeMax(npc);
287
368
return true;
288
369
}
289
370
290
371
participants = (bool[])sickleParticipants.Clone();
372
encounterLifeMax = BossSoulParticipationSystem.GetEncounterLifeMax(npc);
291
373
return true;
292
374
}
293
375
@@ -297,6 +379,16 @@ public class MyGlobalNPC : GlobalNPC
297
379
or NPCID.Spazmatism
298
380
or NPCID.EaterofWorldsBody
299
381
or NPCID.EaterofWorldsHead
300
or NPCID.EaterofWorldsTail;
382
or NPCID.EaterofWorldsTail
383
or NPCID.Creeper
384
or NPCID.GolemHead
385
or NPCID.GolemHeadFree
386
or NPCID.MoonLordHead
387
or NPCID.MoonLordHand;
388
}
389
390
private static bool IsFixedMaximumSoulEnemy(int npcType)
391
{
392
return npcType is NPCID.DungeonGuardian or NPCID.Paladin;
301
393
}
302
394
}
@@ -131,16 +131,22 @@ public class MyGlobalProjectile : GlobalProjectile
131
131
132
132
public override void ModifyHitNPC(Projectile projectile, NPC target, ref NPC.HitModifiers modifiers)
133
133
{
134
if (Main.netMode != NetmodeID.MultiplayerClient
135
|| projectile.owner != Main.myPlayer
136
|| (!IsSickleProjectile && projectile.ModProjectile is not SickleSwingProjectile))
134
bool sickleHit = IsSickleProjectile || projectile.ModProjectile is SickleSwingProjectile;
135
if (!sickleHit || projectile.owner < 0 || projectile.owner >= Main.maxPlayers)
136
return;
137
138
if (Main.netMode == NetmodeID.MultiplayerClient)
137
139
{
140
if (projectile.owner == Main.myPlayer)
141
{
142
// Keep the early packet as a latency-tolerant fallback for owner-authoritative hits.
143
DeathMod.SendSickleHit(projectile, target);
144
}
138
145
return;
139
146
}
140
147
141
// Register the hit before vanilla sends the damage result. If this projectile kills the
142
// target in one hit, the server must already know who reaped it when OnKill is evaluated.
143
DeathMod.SendSickleHit(projectile, target);
148
// Register directly on the authoritative simulation before damage and OnKill resolve.
149
target.GetGlobalNPC<MyGlobalNPC>().RegisterSickleHit(target, projectile.owner);
144
150
}
145
151
146
152
public override void OnHitNPC(Projectile projectile, NPC target, NPC.HitInfo hit, int damageDone)
@@ -155,20 +161,20 @@ public class MyGlobalProjectile : GlobalProjectile
155
161
if (projectile.owner != Main.myPlayer)
156
162
return;
157
163
158
Main.player[projectile.owner].GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
159
LifeStealVisuals.Spawn(target.Center, projectile.owner, LifeStealLevel);
164
int healedLife = Main.player[projectile.owner].GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
165
LifeStealVisuals.Spawn(target.Center, projectile.owner, LifeStealLevel, healedLife);
160
166
return;
161
167
}
162
168
163
169
if (Main.netMode == NetmodeID.Server)
164
170
return;
165
171
166
Main.player[projectile.owner].GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
167
LifeStealVisuals.Spawn(target.Center, projectile.owner, LifeStealLevel);
168
ApplyStatusEffects(target, OnHitDebuffType, OnHitDebuffDuration, FatedStacksPerHit, FatedMaximumStacks, FatedDurationFrames);
172
int localHealedLife = Main.player[projectile.owner].GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
173
LifeStealVisuals.Spawn(target.Center, projectile.owner, LifeStealLevel, localHealedLife);
174
ApplyStatusEffects(target, projectile.owner, LifeStealLevel, OnHitDebuffType, OnHitDebuffDuration, FatedStacksPerHit, FatedMaximumStacks, FatedDurationFrames);
169
175
}
170
176
171
internal static void ApplyStatusEffects(NPC target, int debuffType, int debuffDuration, int fatedStacks, int fatedMaximumStacks, int fatedDurationFrames)
177
internal static void ApplyStatusEffects(NPC target, int playerIndex, int lifeStealLevel, int debuffType, int debuffDuration, int fatedStacks, int fatedMaximumStacks, int fatedDurationFrames)
172
178
{
173
179
if (Main.netMode == NetmodeID.MultiplayerClient || target.friendly || target.lifeMax <= 5)
174
180
return;
@@ -182,7 +188,7 @@ public class MyGlobalProjectile : GlobalProjectile
182
188
if (debuffType > 0 && debuffDuration > 0)
183
189
statusTarget.AddBuff(debuffType, debuffDuration);
184
190
if (fatedStacks > 0 && fatedMaximumStacks > 0 && fatedDurationFrames > 0)
185
statusTarget.GetGlobalNPC<MyGlobalNPC>().AddFatedStacks(statusTarget, fatedStacks, fatedMaximumStacks, fatedDurationFrames);
191
statusTarget.GetGlobalNPC<MyGlobalNPC>().AddFatedStacks(statusTarget, fatedStacks, fatedMaximumStacks, fatedDurationFrames, playerIndex, lifeStealLevel);
186
192
187
193
MyGlobalNPC.SyncBuffState(statusTarget);
188
194
}
@@ -17,7 +17,6 @@ public class MyPlayer : ModPlayer
17
17
{
18
18
public const int StartingSouls = 150;
19
19
public const int SoulsPerEssence = 100;
20
public const int BossSoulReward = 150;
21
20
internal const int DeathWingTrailCapacity = 28;
22
21
23
22
public int Souls { get; private set; } = StartingSouls;
@@ -268,19 +267,27 @@ public class MyPlayer : ModPlayer
268
267
return true;
269
268
}
270
269
271
public void TryLifeSteal(int damageDone, int lifeStealLevel)
270
public int TryLifeSteal(int damageDone, int lifeStealLevel)
272
271
{
273
if (lifeStealLevel <= 0 || damageDone <= 0 || lifeStealCooldown > 0 || Player.whoAmI != Main.myPlayer)
274
return;
272
if (lifeStealLevel <= 0
273
|| damageDone <= 0
274
|| lifeStealCooldown > 0
275
|| Player.dead
276
|| Main.netMode == NetmodeID.MultiplayerClient && Player.whoAmI != Main.myPlayer)
277
{
278
return 0;
279
}
275
280
276
int amount = Math.Clamp((int)Math.Ceiling(damageDone * lifeStealLevel * 0.01f), 1, 2 + lifeStealLevel);
277
if (Player.statLife >= Player.statLifeMax2)
278
return;
281
int missingLife = Player.statLifeMax2 - Player.statLife;
282
if (missingLife <= 0)
283
return 0;
279
284
285
int amount = Math.Min(missingLife, Math.Max(1, (int)Math.Ceiling(damageDone * lifeStealLevel * 0.01f)));
280
286
Player.Heal(amount);
281
287
lifeStealCooldown = 12;
282
if (Main.netMode == NetmodeID.MultiplayerClient)
288
if (Main.netMode != NetmodeID.SinglePlayer)
283
289
NetMessage.SendData(MessageID.PlayerLifeMana, -1, -1, null, Player.whoAmI);
290
return amount;
284
291
}
285
292
286
293
internal int TakeNextSwingDirection(int facingDirection)
@@ -325,11 +325,13 @@ public class DeathMod : Mod
325
325
326
326
int debuffType;
327
327
int debuffDuration;
328
int lifeStealLevel;
328
329
int fatedStacks;
329
330
int fatedMaximumStacks;
330
331
int fatedDurationFrames;
331
332
if (projectileData is { IsSickleProjectile: true })
332
333
{
334
lifeStealLevel = projectileData.LifeStealLevel;
333
335
debuffType = projectileData.OnHitDebuffType;
334
336
debuffDuration = projectileData.OnHitDebuffDuration;
335
337
fatedStacks = projectileData.FatedStacksPerHit;
@@ -338,6 +340,7 @@ public class DeathMod : Mod
338
340
}
339
341
else if (heldSickle is not null)
340
342
{
343
lifeStealLevel = heldSickle.LifeStealLevel;
341
344
debuffType = heldSickle.OnHitDebuffType;
342
345
debuffDuration = heldSickle.OnHitDebuffDuration;
343
346
fatedStacks = heldSickle.FatedStacksPerHit;
@@ -346,6 +349,7 @@ public class DeathMod : Mod
346
349
}
347
350
else
348
351
{
352
lifeStealLevel = 0;
349
353
debuffType = 0;
350
354
debuffDuration = 0;
351
355
fatedStacks = 0;
@@ -356,6 +360,8 @@ public class DeathMod : Mod
356
360
target.GetGlobalNPC<MyGlobalNPC>().RegisterSickleHit(target, playerIndex, applyDeathMark: false);
357
361
MyGlobalProjectile.ApplyStatusEffects(
358
362
target,
363
playerIndex,
364
lifeStealLevel,
359
365
debuffType,
360
366
debuffDuration,
361
367
fatedStacks,
@@ -43,6 +43,16 @@ public class DeathNecklace : ModItem, IDeathAltarUpgradeable
43
43
44
44
protected override bool CloneNewInstances => true;
45
45
46
public override void PreReforge()
47
{
48
DeathAltarReforgePreservation.Capture(this);
49
}
50
51
public override void PostReforge()
52
{
53
DeathAltarReforgePreservation.Restore(this);
54
}
55
46
56
public override void SetDefaults()
47
57
{
48
58
Item.width = 30;
@@ -76,6 +76,16 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
76
76
77
77
protected override bool CloneNewInstances => true;
78
78
79
public override void PreReforge()
80
{
81
DeathAltarReforgePreservation.Capture(this);
82
}
83
84
public override void PostReforge()
85
{
86
DeathAltarReforgePreservation.Restore(this);
87
}
88
79
89
public override void SetStaticDefaults()
80
90
{
81
91
ArmorIDs.Body.Sets.HidesTopSkin[Item.bodySlot] = true;
@@ -41,6 +41,16 @@ public class DeathWings : ModItem, IDeathAltarUpgradeable
41
41
42
42
protected override bool CloneNewInstances => true;
43
43
44
public override void PreReforge()
45
{
46
DeathAltarReforgePreservation.Capture(this);
47
}
48
49
public override void PostReforge()
50
{
51
DeathAltarReforgePreservation.Restore(this);
52
}
53
44
54
public override void SetStaticDefaults()
45
55
{
46
56
ArmorIDs.Wing.Sets.Stats[Item.wingSlot] = new WingStats(60, 6f, 1f);
@@ -96,10 +106,11 @@ public class DeathWings : ModItem, IDeathAltarUpgradeable
96
106
DeathWings visibleWings = FindEquippedWings(player, player.wings) ?? this;
97
107
float mastery = visibleWings.VisualMastery;
98
108
MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
109
bool gliding = !inUse && player.controlJump && Math.Abs(player.velocity.Y) > 0.2f;
99
110
bool justStartedFlying = inUse
100
111
&& !modPlayer.DeathWingsActiveLastTick
101
112
&& !modPlayer.DeathWingsActiveThisTick;
102
modPlayer.DeathWingsActiveThisTick = inUse;
113
modPlayer.DeathWingsActiveThisTick = inUse || gliding;
103
114
104
115
if (justStartedFlying)
105
116
SpawnTakeoffBurst(player, mastery);
@@ -110,8 +121,12 @@ public class DeathWings : ModItem, IDeathAltarUpgradeable
110
121
modPlayer.RecordDeathWingTrail(upperTip, lowerTip, mastery);
111
122
SpawnFlightEffects(player, mastery, visibleWings.InfiniteFlightUnlocked);
112
123
}
113
else if (player.controlJump && Math.Abs(player.velocity.Y) > 0.2f)
124
else if (gliding)
125
{
126
GetWingTips(player, mastery, out Vector2 upperTip, out Vector2 lowerTip, out _);
127
modPlayer.RecordDeathWingTrail(upperTip, lowerTip, mastery);
114
128
SpawnGlideEffects(player, mastery);
129
}
115
130
116
131
return false;
117
132
}
@@ -301,8 +316,8 @@ public class DeathWings : ModItem, IDeathAltarUpgradeable
301
316
private static void SpawnFlightEffects(Player player, float mastery, bool infiniteFlight)
302
317
{
303
318
GetWingTips(player, mastery, out Vector2 upperTip, out Vector2 lowerTip, out Vector2 wingRoot);
304
Color emberColor = Color.Lerp(new Color(205, 15, 35), new Color(255, 75, 145), mastery);
305
Color soulColor = Color.Lerp(new Color(150, 25, 45), new Color(115, 225, 255), mastery);
319
Color emberColor = Color.Lerp(new Color(2, 2, 4), new Color(18, 18, 24), mastery);
320
Color soulColor = Color.Lerp(new Color(1, 1, 3), new Color(12, 12, 18), mastery);
306
321
int interval = mastery >= 0.35f ? 1 : 2;
307
322
308
323
if ((Main.GameUpdateCount + (ulong)player.whoAmI) % (ulong)interval == 0)
@@ -315,7 +330,7 @@ public class DeathWings : ModItem, IDeathAltarUpgradeable
315
330
{
316
331
Vector2 position = Vector2.Lerp(upperTip, lowerTip, Main.rand.NextFloat()) + Main.rand.NextVector2Circular(3f, 3f);
317
332
Vector2 velocity = -player.velocity * 0.12f + Main.rand.NextVector2Circular(0.55f, 0.55f);
318
Dust soul = Dust.NewDustPerfect(position, DustID.AncientLight, velocity, 65, soulColor, 0.55f + mastery * 0.55f);
333
Dust soul = Dust.NewDustPerfect(position, DustID.Smoke, velocity, 65, soulColor, 0.55f + mastery * 0.55f);
319
334
soul.noGravity = true;
320
335
soul.fadeIn = 0.75f + mastery * 0.25f;
321
336
}
@@ -330,7 +345,7 @@ public class DeathWings : ModItem, IDeathAltarUpgradeable
330
345
Vector2 position = player.Center
331
346
- movementDirection * Main.rand.NextFloat(14f, 30f + speed * 1.5f)
332
347
+ Main.rand.NextVector2Circular(8f, 10f);
333
Dust streak = Dust.NewDustPerfect(position, DustID.RainbowMk2, -player.velocity * 0.06f, 70, emberColor, 0.4f + mastery * 0.35f);
348
Dust streak = Dust.NewDustPerfect(position, DustID.Smoke, -player.velocity * 0.06f, 70, emberColor, 0.4f + mastery * 0.35f);
334
349
streak.noGravity = true;
335
350
streak.fadeIn = 0.65f;
336
351
}
@@ -365,11 +380,11 @@ public class DeathWings : ModItem, IDeathAltarUpgradeable
365
380
return;
366
381
367
382
GetWingTips(player, mastery, out Vector2 upperTip, out Vector2 lowerTip, out _);
368
Color color = Color.Lerp(new Color(145, 10, 25), new Color(230, 45, 105), mastery);
383
Color color = Color.Lerp(new Color(2, 2, 4), new Color(18, 18, 24), mastery);
369
384
Vector2 position = Main.rand.NextBool() ? upperTip : lowerTip;
370
385
Dust ember = Dust.NewDustPerfect(
371
386
position + Main.rand.NextVector2Circular(2f, 2f),
372
DustID.Shadowflame,
387
DustID.Smoke,
373
388
-player.velocity * 0.09f + Main.rand.NextVector2Circular(0.35f, 0.35f),
374
389
100,
375
390
color,
@@ -405,7 +420,7 @@ public class DeathWings : ModItem, IDeathAltarUpgradeable
405
420
+ new Vector2(-player.direction * Main.rand.NextFloat(0.15f, 0.8f), Main.rand.NextFloat(-0.35f, 0.35f));
406
421
Dust ember = Dust.NewDustPerfect(
407
422
position + Main.rand.NextVector2Circular(2f + mastery * 1.5f, 2f + mastery * 1.5f),
408
DustID.RainbowMk2,
423
DustID.Smoke,
409
424
velocity,
410
425
65,
411
426
color,
@@ -143,6 +143,17 @@ public class NormalSickle : ModItem, IDeathAltarUpgradeable
143
143
144
144
protected override bool CloneNewInstances => true;
145
145
146
public override void PreReforge()
147
{
148
DeathAltarReforgePreservation.Capture(this);
149
}
150
151
public override void PostReforge()
152
{
153
DeathAltarReforgePreservation.Restore(this);
154
ApplyDynamicStats();
155
}
156
146
157
public override void SetDefaults()
147
158
{
148
159
Item.damage = BaseSickleDamage;
@@ -242,17 +253,17 @@ public class NormalSickle : ModItem, IDeathAltarUpgradeable
242
253
if (player.whoAmI != Main.myPlayer)
243
254
return;
244
255
245
player.GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
246
LifeStealVisuals.Spawn(target.Center, player.whoAmI, LifeStealLevel);
256
int healedLife = player.GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
257
LifeStealVisuals.Spawn(target.Center, player.whoAmI, LifeStealLevel, healedLife);
247
258
return;
248
259
}
249
260
250
261
if (Main.netMode == NetmodeID.Server)
251
262
return;
252
263
253
player.GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
254
LifeStealVisuals.Spawn(target.Center, player.whoAmI, LifeStealLevel);
255
MyGlobalProjectile.ApplyStatusEffects(target, OnHitDebuffType, OnHitDebuffDuration, FatedStacksPerHit, FatedMaximumStacks, FatedDurationFrames);
264
int localHealedLife = player.GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
265
LifeStealVisuals.Spawn(target.Center, player.whoAmI, LifeStealLevel, localHealedLife);
266
MyGlobalProjectile.ApplyStatusEffects(target, player.whoAmI, LifeStealLevel, OnHitDebuffType, OnHitDebuffDuration, FatedStacksPerHit, FatedMaximumStacks, FatedDurationFrames);
256
267
}
257
268
258
269
public override void ModifyTooltips(List<TooltipLine> tooltips)
@@ -304,14 +315,14 @@ public class NormalSickle : ModItem, IDeathAltarUpgradeable
304
315
{
305
316
DeathAltarUpgradeType.Advancement => PrimaryNextSickleType == ItemID.None
306
317
? DeathAltarPrice.Unavailable
307
: DeathAltarPrice.Essence(1 << SickleTier),
318
: DeathAltarPrice.Essence(GetAdvancementEssenceCost()),
308
319
DeathAltarUpgradeType.AlternateAdvancement => AlternateNextSickleType == ItemID.None
309
320
? DeathAltarPrice.Unavailable
310
: DeathAltarPrice.Essence(1 << SickleTier),
321
: DeathAltarPrice.Essence(GetAdvancementEssenceCost()),
311
322
DeathAltarUpgradeType.Damage => DamageLevel >= MaxDamageLevel
312
323
? DeathAltarPrice.Unavailable
313
324
: DamageLevel < 20
314
? DeathAltarPrice.Souls(DamageLevel)
325
? DeathAltarPrice.Souls(DamageLevel * 2)
315
326
: DeathAltarPrice.Essence(DamageLevel < 30
316
327
? 1 + (DamageLevel - 20) / 2
317
328
: 6 + DamageLevel - 30),
@@ -619,11 +630,27 @@ public class NormalSickle : ModItem, IDeathAltarUpgradeable
619
630
if (level == 1)
620
631
return Math.Max(1, BaseSickleDamage);
621
632
633
const int lateGameStartLevel = 29;
634
double earlyMidGrowth = GetUnscaledDamageGrowth(Math.Min(level, lateGameStartLevel - 1)) * 0.25d;
635
double lateGameGrowth = level < lateGameStartLevel
636
? 0d
637
: (GetUnscaledDamageGrowth(level) - GetUnscaledDamageGrowth(lateGameStartLevel - 1)) * 0.70d;
638
return Math.Max(1, (int)Math.Round(BaseSickleDamage + earlyMidGrowth + lateGameGrowth));
639
}
640
641
private double GetUnscaledDamageGrowth(int level)
642
{
622
643
double progress = (level - 1d) / (MaxDamageLevel - 1d);
623
644
double targetDamage = 1_200d + SickleTier * 550d;
624
645
double guaranteedGrowth = level - 1d;
625
646
double curvedGrowth = Math.Max(0d, targetDamage - BaseSickleDamage - (MaxDamageLevel - 1d)) * Math.Pow(progress, 2.3d);
626
return Math.Max(1, (int)Math.Round(BaseSickleDamage + guaranteedGrowth + curvedGrowth));
647
return guaranteedGrowth + curvedGrowth;
648
}
649
650
private int GetAdvancementEssenceCost()
651
{
652
// Form 2 remains the inexpensive introduction. Advancing into forms 3-5 costs twice as much as before.
653
return SickleTier == 0 ? 1 : 1 << (SickleTier + 1);
627
654
}
628
655
629
656
private void SpawnLegacyProjectiles(IEntitySource source, Vector2 position, Vector2 velocity, int type, int damage, float knockback, int owner)
@@ -1,5 +1,6 @@
1
1
using Microsoft.Xna.Framework;
2
2
using Microsoft.Xna.Framework.Graphics;
3
using System;
3
4
using Terraria;
4
5
using Terraria.GameContent;
5
6
using Terraria.ID;
@@ -9,18 +10,26 @@ namespace DeathMod.Projectiles;
9
10
10
11
internal static class LifeStealVisuals
11
12
{
12
public static void Spawn(Vector2 source, int playerIndex, int lifeStealLevel)
13
public static void Spawn(Vector2 source, int playerIndex, int lifeStealLevel, int healedLife)
13
14
{
14
if (lifeStealLevel <= 0 || Main.dedServ || playerIndex != Main.myPlayer)
15
if (lifeStealLevel <= 0
16
|| healedLife <= 0
17
|| playerIndex < 0
18
|| playerIndex >= Main.maxPlayers
19
|| !Main.player[playerIndex].active
20
|| Main.netMode != NetmodeID.Server && playerIndex != Main.myPlayer)
21
{
15
22
return;
23
}
16
24
17
int count = 1 + lifeStealLevel / 2;
25
int count = Math.Max(1, (healedLife + 19) / 20);
26
Player player = Main.player[playerIndex];
18
27
for (int index = 0; index < count; index++)
19
28
{
20
29
Vector2 velocity = Main.rand.NextVector2Circular(2.8f, 2.8f);
21
30
Projectile.NewProjectile(
22
Main.LocalPlayer.GetSource_Misc("DeathMod:LifeStealFeedback"),
23
source,
31
player.GetSource_Misc("DeathMod:LifeStealFeedback"),
32
source + Main.rand.NextVector2Circular(5f, 5f),
24
33
velocity,
25
34
ModContent.ProjectileType<LifeStealWispProjectile>(),
26
35
0,
@@ -82,34 +91,40 @@ public class LifeStealWispProjectile : ModProjectile
82
91
Projectile.rotation = Projectile.velocity.ToRotation();
83
92
84
93
Color color = Color.Lerp(new Color(215, 20, 45), new Color(255, 105, 195), LifeStealLevel / 5f);
85
Lighting.AddLight(Projectile.Center, color.ToVector3() * (0.18f + LifeStealLevel * 0.06f));
86
int particleCount = LifeStealLevel >= 4 ? 2 : 1;
87
for (int index = 0; index < particleCount; index++)
94
if (!Main.dedServ)
88
95
{
89
if (!Main.rand.NextBool(System.Math.Max(1, 4 - LifeStealLevel / 2)))
90
continue;
91
92
Vector2 offset = Main.rand.NextVector2Circular(2.5f + LifeStealLevel * 0.35f, 2.5f + LifeStealLevel * 0.35f);
93
Dust dust = Dust.NewDustPerfect(Projectile.Center + offset, DustID.Blood,
94
-Projectile.velocity * Main.rand.NextFloat(0.025f, 0.075f), 80, color,
95
0.65f + LifeStealLevel * 0.07f + Main.rand.NextFloat(0.12f));
96
dust.noGravity = true;
97
98
if (LifeStealLevel >= 3 && Main.rand.NextBool(3))
96
Lighting.AddLight(Projectile.Center, color.ToVector3() * (0.18f + LifeStealLevel * 0.06f));
97
int particleCount = LifeStealLevel >= 4 ? 2 : 1;
98
for (int index = 0; index < particleCount; index++)
99
99
{
100
Dust core = Dust.NewDustPerfect(Projectile.Center - offset * 0.3f, DustID.AncientLight,
101
-Projectile.velocity * 0.018f, 110, Color.Lerp(color, Color.White, 0.45f), 0.42f + LifeStealLevel * 0.04f);
102
core.noGravity = true;
100
if (!Main.rand.NextBool(System.Math.Max(1, 4 - LifeStealLevel / 2)))
101
continue;
102
103
Vector2 offset = Main.rand.NextVector2Circular(2.5f + LifeStealLevel * 0.35f, 2.5f + LifeStealLevel * 0.35f);
104
Dust dust = Dust.NewDustPerfect(Projectile.Center + offset, DustID.Blood,
105
-Projectile.velocity * Main.rand.NextFloat(0.025f, 0.075f), 80, color,
106
0.65f + LifeStealLevel * 0.07f + Main.rand.NextFloat(0.12f));
107
dust.noGravity = true;
108
109
if (LifeStealLevel >= 3 && Main.rand.NextBool(3))
110
{
111
Dust core = Dust.NewDustPerfect(Projectile.Center - offset * 0.3f, DustID.AncientLight,
112
-Projectile.velocity * 0.018f, 110, Color.Lerp(color, Color.White, 0.45f), 0.42f + LifeStealLevel * 0.04f);
113
core.noGravity = true;
114
}
103
115
}
104
116
}
105
117
106
118
if (toPlayer.LengthSquared() > 16f * 16f)
107
119
return;
108
120
109
for (int index = 0; index < 3 + LifeStealLevel * 2; index++)
121
if (!Main.dedServ)
110
122
{
111
Dust dust = Dust.NewDustPerfect(player.Center, DustID.Blood, Main.rand.NextVector2Circular(2.5f, 2.5f), 60, color, 0.8f + Main.rand.NextFloat(0.4f));
112
dust.noGravity = true;
123
for (int index = 0; index < 3 + LifeStealLevel * 2; index++)
124
{
125
Dust dust = Dust.NewDustPerfect(player.Center, DustID.Blood, Main.rand.NextVector2Circular(2.5f, 2.5f), 60, color, 0.8f + Main.rand.NextFloat(0.4f));
126
dust.noGravity = true;
127
}
113
128
}
114
129
Projectile.Kill();
115
130
}