返回提交历史
Modified
Common/MyGlobalProjectile.cs
+1
-0
Modified
Common/ReaperCombatService.cs
+7
-3
Modified
Projectiles/ReaperActionControllerProjectile.cs
+39
-12
Modified
Projectiles/SickleSwingProjectile.cs
+10
-4
Modified
Projectiles/SoulSickleProjectile.cs
+236
-23
XFEstudio/DeathMod
重制灵魂镰弹幕与虚影连斩
eb3d69e
代码差异
5 个文件
+293
-42
@@ -260,6 +260,7 @@ public class MyGlobalProjectile : GlobalProjectile
260
260
bool requiresAuthorization = projectile.ModProjectile is ReaperActionControllerProjectile
261
261
or ReaperStrikeProjectile
262
262
or ReaperFrostShardProjectile
263
or SoulSickleProjectile
263
264
or ReaperSoulFamiliarProjectile
264
265
or ReaperSoulShardProjectile
265
266
or ReaperVoidRiftProjectile
@@ -308,10 +308,14 @@ public static class ReaperCombatService
308
308
}
309
309
case ReaperFormId.Soul:
310
310
{
311
int delay = snapshot.Skill1 switch { 2 => 10, >= 3 => 8, _ => 12 };
312
311
float factor = snapshot.Skill1 switch { 2 => 0.625f, >= 3 => 0.75f, _ => 0.50f };
313
ReaperStrikeProjectile.Spawn(source, swing.owner, snapshot, ReaperHitKind.PrimaryDerived, phase,
314
origin + aim * 66f, aim, ReaperStrikeShape.Circle, 62f, 18f, factor, delay);
312
SoulSickleProjectile.Spawn(source, swing.owner, snapshot,
313
origin + aim * 42f, aim * (14f + snapshot.StageNumber), phase,
314
swing.identity, ReaperHitKind.PrimaryDerived, factor,
315
variant: phase);
316
ReaperSkillCueProjectile.Spawn(source, swing.owner, snapshot, 0,
317
origin + aim * 54f, aim, variant: phase,
318
actionId: swing.identity);
315
319
if (snapshot.Stage >= ReaperStage.StageII && phase == lastPhase)
316
320
ReaperSoulFamiliarProjectile.Spawn(source, swing.owner, snapshot, origin);
317
321
if (snapshot.Stage >= ReaperStage.StageIII && phase == lastPhase)
@@ -177,12 +177,21 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
177
177
// scythes own all movement and entrance animation, so even decorative
178
178
// weapon echoes would read as another hand swing here.
179
179
int heldEchoCount = !ultimate && snapshot.Form == ReaperFormId.Death
180
? 0 : 2;
180
? 0
181
: snapshot.Form == ReaperFormId.Soul
182
? 3 + snapshot.StageNumber
183
: 2;
181
184
for (int echo = heldEchoCount; echo >= 1; echo--)
182
185
{
183
186
float offset = (0.018f + visualCharge * 0.014f) * echo;
187
Color echoColor = snapshot.Form == ReaperFormId.Soul
188
? Color.Lerp(primary, secondary,
189
echo / (float)(heldEchoCount + 1))
190
: primary;
184
191
Main.EntitySpriteDraw(texture, grip - Main.screenPosition, null,
185
primary * (0.12f / echo), correctedAngle - offset, anchor,
192
echoColor * (snapshot.Form == ReaperFormId.Soul
193
? 0.18f / Math.Max(1f, echo * 0.72f)
194
: 0.12f / echo), correctedAngle - offset, anchor,
186
195
scale * (1f + echo * 0.018f), weaponEffects);
187
196
}
188
197
Main.EntitySpriteDraw(texture, grip - Main.screenPosition, null, lightColor,
@@ -488,18 +497,14 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
488
497
float maximum = snapshot.Stage switch { ReaperStage.StageI => 2.3f, ReaperStage.StageII => 3.15f, _ => 4.2f };
489
498
float total = MathHelper.Lerp(minimum, maximum, MathHelper.Clamp(chargeFrames / 45f, 0f, 1f));
490
499
int fullChargePhaseOffset = chargeFrames >= 45 ? 8 : 0;
500
int bladesPerBeat = chargeFrames >= 45 ? 2 : 1;
491
501
for (int index = 0; index < circles; index++)
492
502
{
493
float radius = snapshot.Stage switch
494
{
495
ReaperStage.StageI => 92f + index * 48f,
496
ReaperStage.StageII => 92f + index * 58f,
497
_ => 96f + index * 70f
498
};
499
ChargedStrikeAt(6 + index * 8, player, origin,
500
aim.RotatedBy(index * MathHelper.TwoPi / circles), ReaperStrikeShape.Circle,
501
radius, 23f + snapshot.StageNumber * 2f, total / circles,
502
fullChargePhaseOffset + index);
503
Vector2 launchDirection = aim.RotatedBy(index
504
* MathHelper.TwoPi / circles);
505
SoulVolleyAt(6 + index * 8, player, origin,
506
launchDirection, total / circles, fullChargePhaseOffset + index,
507
bladesPerBeat);
503
508
}
504
509
FinishChargedAt(14 + circles * 8);
505
510
break;
@@ -903,6 +908,28 @@ public sealed class ReaperActionControllerProjectile : ModProjectile
903
908
actionId: Projectile.identity);
904
909
}
905
910
911
private void SoulVolleyAt(int eventTick, Player player, Vector2 origin,
912
Vector2 direction, float multiplier, int phase, int bladeCount)
913
{
914
if (releaseTimer != eventTick || Main.netMode == NetmodeID.MultiplayerClient)
915
return;
916
917
bladeCount = Math.Clamp(bladeCount, 1, 2);
918
float perBladeMultiplier = multiplier / bladeCount;
919
Vector2 normal = direction.RotatedBy(MathHelper.PiOver2);
920
for (int blade = 0; blade < bladeCount; blade++)
921
{
922
float centered = blade - (bladeCount - 1) * 0.5f;
923
Vector2 launch = direction.RotatedBy(centered * 0.16f);
924
SoulSickleProjectile.Spawn(Projectile.GetSource_FromThis(),
925
player.whoAmI, snapshot,
926
origin + normal * centered * 24f + launch * 32f,
927
launch * (15f + snapshot.StageNumber), phase,
928
Projectile.identity, ReaperHitKind.Special, perBladeMultiplier,
929
variant: phase * 2 + blade);
930
}
931
}
932
906
933
private void ExecuteDeathUltimateShatter(Player player)
907
934
{
908
935
HashSet<int> processedRoots = [];
@@ -1240,7 +1240,9 @@ public sealed class SickleSwingProjectile : ModProjectile
1240
1240
1241
1241
private void DrawWeaponAfterimages(Texture2D texture, float scale)
1242
1242
{
1243
int copies = HasStageVisual(ReaperStage.StageIII) ? 4
1243
bool soulTrail = snapshot.Form == ReaperFormId.Soul;
1244
int copies = soulTrail ? 2 + VisualStage * 2
1245
: HasStageVisual(ReaperStage.StageIII) ? 4
1244
1246
: HasStageVisual(ReaperStage.StageII) ? 2 : 0;
1245
1247
if (copies <= 0 || validHistorySamples < 3)
1246
1248
return;
@@ -1250,11 +1252,15 @@ public sealed class SickleSwingProjectile : ModProjectile
1250
1252
float terminalFade = MathHelper.Clamp((0.985f - animationProgress) / 0.12f, 0f, 1f);
1251
1253
for (int copy = copies; copy >= 1; copy--)
1252
1254
{
1253
int historyIndex = Math.Min(validHistorySamples - 1, copy * 2);
1255
int historyIndex = Math.Min(validHistorySamples - 1,
1256
soulTrail ? copy : copy * 2);
1254
1257
float strength = (copies - copy + 1f) / (copies + 1f) * terminalFade;
1255
Color color = Color.Lerp(primary, secondary, copy / (float)(copies + 1)) * (strength * 0.34f);
1258
float opacity = soulTrail ? 0.52f : 0.34f;
1259
Color color = Color.Lerp(primary, secondary,
1260
copy / (float)(copies + 1)) * (strength * opacity);
1256
1261
DrawWeapon(texture, grip, angleHistory[historyIndex], color,
1257
scale * (0.96f + strength * 0.04f), GetWeaponEffects(historyIndex));
1262
scale * (soulTrail ? 1f : 0.96f + strength * 0.04f),
1263
GetWeaponEffects(historyIndex));
1258
1264
}
1259
1265
}
1260
1266
@@ -1,15 +1,61 @@
1
using DeathMod.Common;
1
2
using Microsoft.Xna.Framework;
3
using System;
4
using System.Collections.Generic;
5
using System.IO;
2
6
using Terraria;
3
7
using Terraria.ID;
4
8
using Terraria.ModLoader;
5
9
6
10
namespace DeathMod.Projectiles;
7
11
8
public class SoulSickleProjectile : ModProjectile
12
public sealed class SoulSickleProjectile : ModProjectile
9
13
{
14
private const int HomingDelayFrames = 8;
15
16
private SickleCombatSnapshot snapshot;
17
private ReaperHitKind hitKind;
18
private int phase;
19
private int actionId;
20
private int variant;
21
private int timer;
22
private int targetIndex = -1;
23
private bool configured;
24
private bool serverAuthorized;
25
private int authoritativeDamage;
26
private Vector2 authoritativeCenter;
27
private Vector2 authoritativeVelocity;
28
private readonly HashSet<int> hitRoots = [];
29
30
internal static int Spawn(Terraria.DataStructures.IEntitySource source, int owner,
31
in SickleCombatSnapshot snapshot, Vector2 position, Vector2 velocity,
32
int phase, int actionId, ReaperHitKind hitKind, float damageMultiplier,
33
int variant = 0)
34
{
35
if (Main.netMode == NetmodeID.MultiplayerClient)
36
return -1;
37
38
int projectileIndex = Projectile.NewProjectile(source, position, velocity,
39
ModContent.ProjectileType<SoulSickleProjectile>(),
40
Math.Max(1, (int)Math.Round(snapshot.Damage * damageMultiplier)),
41
snapshot.Knockback * 0.45f, owner);
42
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
43
return -1;
44
45
Projectile projectile = Main.projectile[projectileIndex];
46
projectile.originalDamage = snapshot.Damage;
47
projectile.CritChance = snapshot.CritChance;
48
if (projectile.ModProjectile is SoulSickleProjectile soulBlade)
49
soulBlade.Configure(snapshot, hitKind, phase, actionId, variant);
50
projectile.GetGlobalProjectile<MyGlobalProjectile>().ConfigureReaperProjectile(
51
projectile, snapshot, hitKind, phase, velocity.ToRotation(), actionId);
52
projectile.netUpdate = true;
53
return projectileIndex;
54
}
55
10
56
public override void SetStaticDefaults()
11
57
{
12
ProjectileID.Sets.TrailCacheLength[Type] = 18;
58
ProjectileID.Sets.TrailCacheLength[Type] = 24;
13
59
ProjectileID.Sets.TrailingMode[Type] = 2;
14
60
}
15
61
@@ -18,48 +64,215 @@ public class SoulSickleProjectile : ModProjectile
18
64
Projectile.width = 42;
19
65
Projectile.height = 42;
20
66
Projectile.friendly = true;
67
Projectile.hostile = false;
21
68
Projectile.tileCollide = false;
22
Projectile.penetrate = 6;
69
Projectile.penetrate = 4;
23
70
Projectile.ignoreWater = true;
24
Projectile.timeLeft = 260;
71
Projectile.timeLeft = 150;
25
72
Projectile.DamageType = DamageClass.Melee;
26
73
Projectile.netImportant = true;
74
Projectile.usesLocalNPCImmunity = true;
75
Projectile.localNPCHitCooldown = 14;
27
76
}
28
77
78
public override bool ShouldUpdatePosition() => false;
79
public override bool? CanDamage() => configured ? null : false;
80
29
81
public override void AI()
30
82
{
31
Projectile.localAI[0]++;
32
float timer = Projectile.localAI[0];
33
float curveDirection = System.Math.Sign(Projectile.ai[2] == 0f ? 1f : Projectile.ai[2]);
34
int phase = (int)(timer / 24f) % 3;
35
NPC? target = ReaperProjectileHelper.FindTarget(Projectile, Projectile.ai[0] > 0f ? 900f : 680f);
36
if (phase == 0)
83
if (Main.netMode == NetmodeID.Server)
37
84
{
38
Projectile.velocity = Projectile.velocity.RotatedBy(curveDirection * (0.018f + Projectile.ai[1] * 0.01f));
85
if (!serverAuthorized)
86
{
87
Projectile.Kill();
88
return;
89
}
90
Projectile.damage = authoritativeDamage;
91
Projectile.Center = authoritativeCenter;
92
Projectile.velocity = authoritativeVelocity;
39
93
}
40
else if (target is not null && phase == 1)
94
if (!configured)
95
return;
96
97
timer++;
98
int curveDirection = (variant & 1) == 0 ? 1 : -1;
99
if (timer <= HomingDelayFrames)
41
100
{
42
float speed = System.Math.Max(13f, Projectile.velocity.Length());
43
Vector2 desired = (target.Center - Projectile.Center).SafeNormalize(Vector2.UnitX) * speed;
44
Projectile.velocity = Vector2.Lerp(Projectile.velocity, desired, 0.09f + Projectile.ai[1] * 0.055f);
101
Projectile.velocity = Projectile.velocity.RotatedBy(curveDirection
102
* (0.024f + snapshot.StageNumber * 0.004f));
45
103
}
46
else if (phase == 2 && Projectile.velocity.Length() < 24f)
104
else
47
105
{
48
Projectile.velocity *= 1.018f;
106
NPC? target = GetOrFindTarget();
107
if (target is not null)
108
{
109
float speed = Math.Max(14f + snapshot.StageNumber,
110
Projectile.velocity.Length());
111
Vector2 desired = (target.Center - Projectile.Center)
112
.SafeNormalize(Projectile.velocity.SafeNormalize(Vector2.UnitX))
113
* speed;
114
Projectile.velocity = Vector2.Lerp(Projectile.velocity, desired,
115
0.085f + snapshot.StageNumber * 0.018f);
116
}
117
else
118
{
119
Projectile.velocity = Projectile.velocity.RotatedBy(
120
curveDirection * 0.006f);
121
}
49
122
}
123
50
124
Projectile.rotation = Projectile.velocity.ToRotation();
125
Projectile.GetGlobalProjectile<MyGlobalProjectile>()
126
.UpdateReaperAttackAngle(Projectile.rotation);
127
Projectile.position += Projectile.velocity;
128
if (Main.netMode == NetmodeID.Server)
129
{
130
authoritativeCenter = Projectile.Center;
131
authoritativeVelocity = Projectile.velocity;
132
}
133
51
134
if (!Main.dedServ)
52
135
{
53
Lighting.AddLight(Projectile.Center, new Vector3(0.25f, 0.38f, 0.9f));
54
int dustCount = Projectile.ai[0] > 0f ? 2 : 1;
55
for (int index = 0; index < dustCount; index++)
136
Lighting.AddLight(Projectile.Center, new Vector3(0.22f, 0.34f, 0.82f));
137
if (Main.rand.NextBool(2))
56
138
{
57
Color color = Main.rand.NextBool() ? new Color(85, 225, 255) : new Color(175, 85, 255);
58
Dust dust = Dust.NewDustPerfect(Projectile.Center + Main.rand.NextVector2Circular(13f, 13f), DustID.AncientLight, -Projectile.velocity * 0.07f, 45, color, 0.95f + Projectile.ai[1] * 0.3f);
139
Color color = Main.rand.NextBool()
140
? new Color(85, 225, 255)
141
: new Color(175, 85, 255);
142
Dust dust = Dust.NewDustPerfect(Projectile.Center
143
+ Main.rand.NextVector2Circular(11f, 11f), DustID.AncientLight,
144
-Projectile.velocity * 0.08f, 55, color, 0.95f);
59
145
dust.noGravity = true;
60
146
}
61
147
}
62
148
}
63
149
64
public override bool PreDraw(ref Color lightColor) => ReaperProjectileHelper.DrawTrail(Projectile, new Color(115, 115, 255), scaleMultiplier: 1f + Projectile.ai[1] * 0.25f, copies: 16);
150
public override bool PreDraw(ref Color lightColor)
151
=> ReaperProjectileHelper.DrawTrail(Projectile, new Color(120, 115, 255),
152
scaleMultiplier: 1.04f + snapshot.StageNumber * 0.05f, copies: 22);
153
154
public override void OnHitNPC(NPC target, NPC.HitInfo hit, int damageDone)
155
{
156
int root = target.realLife >= 0 ? target.realLife : target.whoAmI;
157
hitRoots.Add(root);
158
if (targetIndex == target.whoAmI)
159
targetIndex = -1;
160
if (Main.netMode != NetmodeID.MultiplayerClient)
161
Projectile.netUpdate = true;
162
}
163
164
public override void SendExtraAI(BinaryWriter writer)
165
{
166
writer.Write(configured);
167
if (!configured)
168
return;
169
snapshot.Write(writer);
170
writer.Write((byte)hitKind);
171
writer.Write((byte)Math.Clamp(phase, 0, byte.MaxValue));
172
writer.Write(actionId);
173
writer.Write((byte)Math.Clamp(variant, 0, byte.MaxValue));
174
writer.Write((short)Math.Clamp(timer, 0, short.MaxValue));
175
writer.Write((short)targetIndex);
176
}
177
178
public override void ReceiveExtraAI(BinaryReader reader)
179
{
180
bool incomingConfigured = reader.ReadBoolean();
181
if (!incomingConfigured)
182
{
183
if (Main.netMode != NetmodeID.Server)
184
configured = false;
185
return;
186
}
187
188
SickleCombatSnapshot incomingSnapshot = SickleCombatSnapshot.Read(reader);
189
ReaperHitKind incomingHitKind = (ReaperHitKind)reader.ReadByte();
190
int incomingPhase = reader.ReadByte();
191
int incomingActionId = reader.ReadInt32();
192
int incomingVariant = reader.ReadByte();
193
int incomingTimer = reader.ReadInt16();
194
int incomingTarget = reader.ReadInt16();
195
if (Main.netMode == NetmodeID.Server)
196
return;
197
198
snapshot = incomingSnapshot;
199
hitKind = incomingHitKind;
200
phase = incomingPhase;
201
actionId = incomingActionId;
202
variant = incomingVariant;
203
timer = incomingTimer;
204
targetIndex = incomingTarget;
205
configured = true;
206
}
207
208
private void Configure(in SickleCombatSnapshot value, ReaperHitKind kind,
209
int attackPhase, int parentActionId, int projectileVariant)
210
{
211
snapshot = value;
212
hitKind = kind;
213
phase = Math.Max(0, attackPhase);
214
actionId = parentActionId;
215
variant = Math.Max(0, projectileVariant);
216
timer = 0;
217
targetIndex = -1;
218
hitRoots.Clear();
219
configured = true;
220
serverAuthorized = Main.netMode != NetmodeID.MultiplayerClient;
221
authoritativeDamage = Math.Max(1, Projectile.damage);
222
authoritativeCenter = Projectile.Center;
223
authoritativeVelocity = Projectile.velocity;
224
}
225
226
private NPC? GetOrFindTarget()
227
{
228
float retentionRadius = 760f + snapshot.StageNumber * 80f;
229
int previousTarget = targetIndex;
230
if (targetIndex >= 0 && targetIndex < Main.maxNPCs)
231
{
232
NPC current = Main.npc[targetIndex];
233
int currentRoot = current.realLife >= 0
234
? current.realLife
235
: current.whoAmI;
236
if (!hitRoots.Contains(currentRoot)
237
&& ReaperTargeting.IsValidWeaponTarget(current)
238
&& (ReaperTargeting.IsTrainingDummy(current)
239
|| current.CanBeChasedBy(Projectile))
240
&& Vector2.DistanceSquared(Projectile.Center, current.Center)
241
<= retentionRadius * retentionRadius)
242
{
243
return current;
244
}
245
targetIndex = -1;
246
}
247
248
NPC? target = FindUnhitTarget(520f + snapshot.StageNumber * 90f);
249
targetIndex = target?.whoAmI ?? -1;
250
if (targetIndex != previousTarget
251
&& Main.netMode != NetmodeID.MultiplayerClient)
252
Projectile.netUpdate = true;
253
return target;
254
}
255
256
private NPC? FindUnhitTarget(float maximumDistance)
257
{
258
NPC? target = null;
259
foreach (NPC npc in Main.ActiveNPCs)
260
{
261
int root = npc.realLife >= 0 ? npc.realLife : npc.whoAmI;
262
if (hitRoots.Contains(root)
263
|| !ReaperTargeting.IsValidWeaponTarget(npc)
264
|| !ReaperTargeting.IsTrainingDummy(npc)
265
&& !npc.CanBeChasedBy(Projectile))
266
{
267
continue;
268
}
269
270
float distance = Vector2.Distance(npc.Center, Projectile.Center);
271
if (distance >= maximumDistance)
272
continue;
273
maximumDistance = distance;
274
target = npc;
275
}
276
return target;
277
}
65
278
}