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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

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

XFEstudio/DeathMod

完善灵魂奖励、祭坛升级与死亡领域特效

39c499b
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

18 个文件 +1030 -205
Modified Common/BossSoulParticipationSystem.cs +110 -3
@@ -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();
Added Common/DeathAltarReforgePreservation.cs +33 -0
@@ -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 }
Modified Common/DeathDomainVisualSystem.cs +305 -27
@@ -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 }
Modified Common/MyGlobalNPC.cs +104 -12
@@ -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 }
Modified Common/MyGlobalProjectile.cs +19 -13
@@ -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 }
Modified Common/MyPlayer.cs +15 -8
@@ -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)
Modified DeathMod.cs +6 -0
@@ -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,
Modified Items/DeathNecklace.cs +10 -0
@@ -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;
Modified Items/DeathRobe.cs +10 -0
@@ -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;
Modified Items/DeathWings.cs +24 -9
@@ -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,
Modified Items/NormalSickle.cs +36 -9
@@ -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)
Modified Projectiles/LifeStealWispProjectile.cs +39 -24
@@ -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 }
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