返回提交历史
Modified
Common/MyPlayer.cs
+127
-43
Modified
Localization/en-US.hjson
+2
-1
Modified
Localization/zh-Hans.hjson
+2
-1
Modified
Projectiles/DeathDomainHarvestSlashProjectile.cs
+74
-123
Added
Projectiles/DeathDomainHarvestTelegraphProjectile.cs
+141
-0
Modified
README.md
+1
-1
Modified
README.zh-Hans.md
+1
-1
Modified
docs/PLAY_GUIDE.md
+1
-1
Modified
docs/PLAY_GUIDE.zh-Hans.md
+1
-1
XFEstudio/DeathMod
修复服务器领域收割并重做待斩刀光
b237181
代码差异
9 个文件
+350
-172
@@ -40,6 +40,9 @@ public class MyPlayer : ModPlayer
40
40
private ulong deathWingTrailUpdateTick = ulong.MaxValue;
41
41
private int deathDomainTimer;
42
42
private int deathDomainVolleyCounter;
43
private readonly Dictionary<int, PendingDeathDomainCut> deathDomainPendingCuts = [];
44
45
private readonly record struct PendingDeathDomainCut(float Rotation, int ProjectileIndex);
43
46
44
47
public override void Initialize()
45
48
{
@@ -56,6 +59,7 @@ public class MyPlayer : ModPlayer
56
59
ActiveDeathNecklace = null;
57
60
deathDomainTimer = 0;
58
61
deathDomainVolleyCounter = 0;
62
deathDomainPendingCuts.Clear();
59
63
}
60
64
61
65
public override void ResetEffects()
@@ -344,10 +348,14 @@ public class MyPlayer : ModPlayer
344
348
345
349
private void UpdateDeathDomain()
346
350
{
347
DeathNecklace? necklace = ActiveDeathNecklace;
351
// UpdateAccessory normally supplies this reference. The direct equipment scan
352
// keeps dedicated-server attacks stable on ticks where accessory recalculation
353
// did not expose the ModItem instance before PostUpdate.
354
DeathNecklace? necklace = ActiveDeathNecklace ?? FindEquippedDeathNecklace();
348
355
if (!DeathDomainEnabled || necklace is null || Player.dead)
349
356
{
350
357
deathDomainTimer = 0;
358
ClearDeathDomainTelegraphs();
351
359
return;
352
360
}
353
361
@@ -356,51 +364,145 @@ public class MyPlayer : ModPlayer
356
364
if (Main.netMode == NetmodeID.MultiplayerClient)
357
365
return;
358
366
359
Item heldItem = Player.HeldItem;
360
NormalSickle? sickle = heldItem.ModItem as NormalSickle;
361
bool isLegacyDeath = heldItem.ModItem is LegacyDeath;
362
if (sickle is null && !isLegacyDeath)
367
if (Player.HeldItem.ModItem is not (NormalSickle or LegacyDeath))
363
368
{
364
deathDomainTimer = 0;
369
ClearDeathDomainTelegraphs();
365
370
return;
366
371
}
367
372
373
List<NPC> targets = FindDeathDomainTargets(necklace);
374
float visualMastery = MathHelper.Clamp((necklace.MasteryScore - 5f) / 65f, 0f, 1f);
375
UpdateDeathDomainTelegraphs(targets, necklace, visualMastery);
368
376
deathDomainTimer++;
369
377
if (deathDomainTimer < necklace.SpawnInterval)
370
378
return;
371
379
deathDomainTimer = 0;
372
380
373
List<NPC> targets = FindDeathDomainTargets(necklace);
374
381
if (targets.Count == 0)
382
{
383
ClearDeathDomainTelegraphs();
375
384
return;
385
}
376
386
377
387
deathDomainVolleyCounter++;
378
388
bool requiem = necklace.IsFullScreenDomain && deathDomainVolleyCounter % 4 == 0;
379
float visualMastery = MathHelper.Clamp((necklace.MasteryScore - 5f) / 65f, 0f, 1f);
380
389
foreach (NPC target in targets)
381
390
{
391
float rotation = deathDomainPendingCuts.TryGetValue(target.whoAmI, out PendingDeathDomainCut pending)
392
? pending.Rotation
393
: Main.rand.NextFloat(MathHelper.TwoPi);
394
SpawnDeathDomainHarvestSlash(target, necklace, visualMastery, requiem, rotation);
395
396
// Resolve damage after broadcasting the visual so a lethal first cut is
397
// still shown to every client.
398
ApplyDeathDomainHarvest(target, necklace);
399
}
400
ClearDeathDomainTelegraphs();
401
}
402
403
private void UpdateDeathDomainTelegraphs(List<NPC> targets, DeathNecklace necklace, float visualMastery)
404
{
405
HashSet<int> activeTargets = [];
406
foreach (NPC target in targets)
407
activeTargets.Add(target.whoAmI);
408
409
List<int> staleTargets = [];
410
foreach (int npcIndex in deathDomainPendingCuts.Keys)
411
if (!activeTargets.Contains(npcIndex))
412
staleTargets.Add(npcIndex);
413
foreach (int npcIndex in staleTargets)
414
RemoveDeathDomainTelegraph(npcIndex);
415
416
int remainingFrames = Math.Max(1, necklace.SpawnInterval - deathDomainTimer);
417
foreach (NPC target in targets)
418
{
419
if (deathDomainPendingCuts.ContainsKey(target.whoAmI))
420
continue;
421
422
float rotation = Main.rand.NextFloat(MathHelper.TwoPi);
382
423
int projectileIndex = Projectile.NewProjectile(
383
Player.GetSource_Misc("DeathMod:DeathDomain"),
424
Player.GetSource_Misc("DeathMod:DeathDomainTelegraph"),
384
425
target.Center,
385
426
Vector2.Zero,
386
ModContent.ProjectileType<DeathDomainHarvestSlashProjectile>(),
427
ModContent.ProjectileType<DeathDomainHarvestTelegraphProjectile>(),
387
428
0,
388
429
0f,
389
430
Player.whoAmI,
390
431
target.whoAmI,
391
necklace.SlashCount,
392
visualMastery + (requiem ? 2f : 0f));
432
remainingFrames,
433
visualMastery);
393
434
if (projectileIndex >= 0 && projectileIndex < Main.maxProjectiles)
394
435
{
395
Projectile visual = Main.projectile[projectileIndex];
396
visual.rotation = Main.rand.NextFloat(MathHelper.TwoPi);
397
visual.scale = MathHelper.Clamp(Math.Max(target.width, target.height) / 52f, 0.85f, 2.4f);
398
visual.netUpdate = true;
436
Projectile telegraph = Main.projectile[projectileIndex];
437
telegraph.rotation = rotation;
438
telegraph.scale = MathHelper.Clamp(Math.Max(target.width, target.height) / 52f, 0.85f, 2.4f);
439
SynchronizeDeathDomainVisual(projectileIndex);
399
440
}
441
deathDomainPendingCuts[target.whoAmI] = new PendingDeathDomainCut(rotation, projectileIndex);
442
}
443
}
400
444
401
// Resolve damage after broadcasting the visual so a lethal first cut is
402
// still shown to every client.
403
ApplyDeathDomainHarvest(target, necklace, sickle);
445
private void SpawnDeathDomainHarvestSlash(NPC target, DeathNecklace necklace, float visualMastery, bool requiem, float rotation)
446
{
447
int projectileIndex = Projectile.NewProjectile(
448
Player.GetSource_Misc("DeathMod:DeathDomain"),
449
target.Center,
450
Vector2.Zero,
451
ModContent.ProjectileType<DeathDomainHarvestSlashProjectile>(),
452
0,
453
0f,
454
Player.whoAmI,
455
target.whoAmI,
456
necklace.SlashCount,
457
visualMastery + (requiem ? 2f : 0f));
458
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
459
return;
460
461
Projectile visual = Main.projectile[projectileIndex];
462
visual.rotation = rotation;
463
visual.scale = MathHelper.Clamp(Math.Max(target.width, target.height) / 52f, 0.85f, 2.4f);
464
SynchronizeDeathDomainVisual(projectileIndex);
465
}
466
467
private static void SynchronizeDeathDomainVisual(int projectileIndex)
468
{
469
if (Main.netMode != NetmodeID.Server)
470
return;
471
472
// Domain visuals are short lived, so send the complete state in their spawn
473
// tick rather than waiting for the periodic projectile synchronization pass.
474
NetMessage.SendData(MessageID.SyncProjectile, -1, -1, null, projectileIndex);
475
Main.projectile[projectileIndex].netUpdate = false;
476
}
477
478
private void ClearDeathDomainTelegraphs()
479
{
480
if (deathDomainPendingCuts.Count == 0)
481
return;
482
483
foreach (PendingDeathDomainCut pending in deathDomainPendingCuts.Values)
484
KillDeathDomainTelegraph(pending.ProjectileIndex);
485
deathDomainPendingCuts.Clear();
486
}
487
488
private void RemoveDeathDomainTelegraph(int npcIndex)
489
{
490
if (!deathDomainPendingCuts.Remove(npcIndex, out PendingDeathDomainCut pending))
491
return;
492
KillDeathDomainTelegraph(pending.ProjectileIndex);
493
}
494
495
private void KillDeathDomainTelegraph(int projectileIndex)
496
{
497
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
498
return;
499
500
Projectile projectile = Main.projectile[projectileIndex];
501
if (projectile.active
502
&& projectile.owner == Player.whoAmI
503
&& projectile.ModProjectile is DeathDomainHarvestTelegraphProjectile)
504
{
505
projectile.Kill();
404
506
}
405
507
}
406
508
@@ -412,13 +514,16 @@ public class MyPlayer : ModPlayer
412
514
float radiusSquared = radius * radius;
413
515
foreach (NPC npc in Main.ActiveNPCs)
414
516
{
415
if (npc.friendly || npc.dontTakeDamage || npc.lifeMax <= 5 || !npc.CanBeChasedBy(Player))
517
// chaseable is intentionally not used: vanilla NPCs toggle it during
518
// movement and transition states, which made dedicated-server harvests
519
// appear to skip otherwise valid enemies from one cycle to the next.
520
if (npc.friendly || npc.immortal || npc.dontTakeDamage || npc.lifeMax <= 5 || npc.life <= 0)
416
521
continue;
417
522
418
523
NPC target = npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].active
419
524
? Main.npc[npc.realLife]
420
525
: npc;
421
if (target.friendly || target.dontTakeDamage || target.lifeMax <= 5
526
if (target.friendly || target.immortal || target.dontTakeDamage || target.lifeMax <= 5 || target.life <= 0
422
527
|| selectedTargets.Contains(target.whoAmI)
423
528
|| Vector2.DistanceSquared(Player.Center, target.Center) > radiusSquared)
424
529
{
@@ -431,7 +536,7 @@ public class MyPlayer : ModPlayer
431
536
return targets;
432
537
}
433
538
434
private void ApplyDeathDomainHarvest(NPC target, DeathNecklace necklace, NormalSickle? sickle)
539
private void ApplyDeathDomainHarvest(NPC target, DeathNecklace necklace)
435
540
{
436
541
bool bossOrBossPart = target.boss
437
542
|| NPCID.Sets.ShouldBeCountedAsBoss[target.type]
@@ -444,20 +549,6 @@ public class MyPlayer : ModPlayer
444
549
445
550
for (int slash = 0; slash < necklace.SlashCount && target.active && target.life > 0; slash++)
446
551
{
447
if (sickle is not null)
448
{
449
MyGlobalProjectile.ApplyStatusEffects(
450
target,
451
Player.whoAmI,
452
sickle.LifeStealLevel,
453
sickle.OnHitDebuffType,
454
sickle.OnHitDebuffDuration,
455
sickle.FatedStacksPerHit,
456
sickle.FatedMaximumStacks,
457
sickle.FatedDurationFrames);
458
}
459
460
int lifeBeforeHit = target.life;
461
552
NPC.HitInfo harvestHit = new()
462
553
{
463
554
Damage = slashDamage,
@@ -470,13 +561,6 @@ public class MyPlayer : ModPlayer
470
561
target.StrikeNPC(harvestHit, fromNet: false, noPlayerInteraction: false);
471
562
if (Main.netMode == NetmodeID.Server)
472
563
NetMessage.SendStrikeNPC(target, in harvestHit);
473
474
if (sickle is not null)
475
{
476
int actualDamage = Math.Min(Math.Max(0, lifeBeforeHit), slashDamage);
477
int healedLife = Player.GetModPlayer<MyPlayer>().TryLifeSteal(actualDamage, sickle.LifeStealLevel);
478
LifeStealVisuals.Spawn(target.Center, Player.whoAmI, sickle.LifeStealLevel, healedLife);
479
}
480
564
}
481
565
}
482
566
@@ -3,7 +3,7 @@ Mods: {
3
3
Items: {
4
4
DeathNecklace: {
5
5
DisplayName: Death Necklace
6
Tooltip: Equip to toggle a death domain; while holding a DeathMod sickle, multislash harvests strike every enemy inside
6
Tooltip: Equip to toggle a death domain; multislash harvests strike every enemy inside and apply only Death Mark
7
7
}
8
8
9
9
NormalSickle: {
@@ -111,6 +111,7 @@ Mods: {
111
111
112
112
Projectiles: {
113
113
DomainSoulBladeProjectile.DisplayName: Death Domain Echo
114
DeathDomainHarvestTelegraphProjectile.DisplayName: Pending Cut Mark
114
115
DeathDomainHarvestSlashProjectile.DisplayName: Death Domain Harvest Cut
115
116
BoneWispProjectile.DisplayName: Bone Wisp
116
117
BloodCrescentProjectile.DisplayName: Blood Covenant Crescent
@@ -3,7 +3,7 @@ Mods: {
3
3
Items: {
4
4
DeathNecklace: {
5
5
DisplayName: 死亡项链
6
Tooltip: 佩戴后可开关死亡领域;手持本模组镰刀时,以多重收割斩同时处决领域内所有敌人
6
Tooltip: 佩戴后可开关死亡领域;多重收割斩同时攻击领域内所有敌人,仅施加死亡标记
7
7
}
8
8
9
9
NormalSickle: {
@@ -111,6 +111,7 @@ Mods: {
111
111
112
112
Projectiles: {
113
113
DomainSoulBladeProjectile.DisplayName: 死亡领域回响
114
DeathDomainHarvestTelegraphProjectile.DisplayName: 待斩刻线
114
115
DeathDomainHarvestSlashProjectile.DisplayName: 死亡领域收割斩
115
116
BoneWispProjectile.DisplayName: 亡骨幽光
116
117
BloodCrescentProjectile.DisplayName: 血契弧刃
@@ -11,25 +11,22 @@ using Terraria.ModLoader;
11
11
namespace DeathMod.Projectiles;
12
12
13
13
/// <summary>
14
/// A purely visual, networked harvest cut. One projectile renders every staggered
15
/// slash for a target so a crowded domain does not need dozens of damaging entities.
16
/// Damage is resolved authoritatively by <see cref="Common.MyPlayer"/>.
14
/// A straight, double-pointed execution blade: crimson at the edge and completely
15
/// black through its core. Damage remains server-authoritative in Common.MyPlayer.
17
16
/// </summary>
18
17
public class DeathDomainHarvestSlashProjectile : ModProjectile
19
18
{
20
private const int SlashActiveFrames = 18;
19
private const int SlashActiveFrames = 16;
21
20
private const int SlashDelayFrames = 5;
22
private const int EndLingerFrames = 8;
21
private const int EndLingerFrames = 7;
22
private const int MaximumSlashCount = 5;
23
23
24
24
private int TargetIndex => (int)Projectile.ai[0];
25
private int SlashCount => Math.Clamp((int)Projectile.ai[1], 1, DeathNecklaceMaxSlashCount);
25
private int SlashCount => Math.Clamp((int)Projectile.ai[1], 1, MaximumSlashCount);
26
26
private bool IsRequiem => Projectile.ai[2] >= 2f;
27
27
private float Mastery => MathHelper.Clamp(IsRequiem ? Projectile.ai[2] - 2f : Projectile.ai[2], 0f, 1f);
28
28
private int TotalLifetime => SlashActiveFrames + (SlashCount - 1) * SlashDelayFrames + EndLingerFrames;
29
29
30
// Avoid a content dependency from the visual layer back into the item type.
31
private const int DeathNecklaceMaxSlashCount = 5;
32
33
30
public override string Texture => "Terraria/Images/Projectile_0";
34
31
35
32
public override void SetStaticDefaults()
@@ -59,8 +56,6 @@ public class DeathDomainHarvestSlashProjectile : ModProjectile
59
56
60
57
public override void SendExtraAI(BinaryWriter writer)
61
58
{
62
// Rotation and scale are selected from the victim on the authoritative side;
63
// vanilla projectile sync does not guarantee either visual field.
64
59
writer.Write(Projectile.rotation);
65
60
writer.Write(Projectile.scale);
66
61
}
@@ -88,7 +83,7 @@ public class DeathDomainHarvestSlashProjectile : ModProjectile
88
83
continue;
89
84
90
85
emittedMask |= bit;
91
SpawnSlashBurst(slash);
86
SpawnExecutionBurst();
92
87
}
93
88
Projectile.localAI[0] = emittedMask;
94
89
}
@@ -99,129 +94,90 @@ public class DeathDomainHarvestSlashProjectile : ModProjectile
99
94
for (int slash = 0; slash < SlashCount; slash++)
100
95
{
101
96
float progress = (age - slash * SlashDelayFrames) / (float)SlashActiveFrames;
102
if (progress <= 0f || progress >= 1.12f)
97
if (progress <= 0f || progress >= 1f)
103
98
continue;
104
105
// Three curved echoes make the cut feel like a blade moving through space,
106
// instead of a straight MagicPixel beam.
107
DrawSlash(slash, progress - 0.18f, 0.12f, echo: true);
108
DrawSlash(slash, progress - 0.11f, 0.22f, echo: true);
109
DrawSlash(slash, progress - 0.055f, 0.38f, echo: true);
110
DrawSlash(slash, progress, 1f, echo: false);
99
DrawExecutionBlade(progress, slash);
111
100
}
112
101
return false;
113
102
}
114
103
115
private void DrawSlash(int slash, float progress, float opacity, bool echo)
104
private void DrawExecutionBlade(float progress, int slash)
116
105
{
117
if (progress <= 0f || progress >= 1f)
118
return;
119
120
float smoothHead = Smooth01(progress / 0.48f);
121
float smoothTail = Smooth01((progress - 0.30f) / 0.70f);
122
if (smoothHead <= smoothTail)
123
return;
124
125
float pulse = (float)Math.Sin(MathHelper.Pi * progress);
126
float brightness = pulse * opacity;
127
float widthScale = 0.86f + Mastery * 0.48f + (IsRequiem ? 0.22f : 0f);
128
float spread = slash - (SlashCount - 1) * 0.5f;
129
float rotation = Projectile.rotation
130
+ spread * 0.31f
131
+ DeterministicWave(slash, 1.71f) * 0.17f;
132
int bendDirection = ((Projectile.identity + slash) & 1) == 0 ? 1 : -1;
133
float sweep = MathHelper.Lerp(-24f, 24f, Smooth01(progress)) * Projectile.scale;
134
135
const int segments = 24;
136
Vector2 previous = GetArcPoint(slash, smoothTail, rotation, bendDirection, sweep);
137
for (int segment = 1; segment <= segments; segment++)
138
{
139
float t = MathHelper.Lerp(smoothTail, smoothHead, segment / (float)segments);
140
Vector2 current = GetArcPoint(slash, t, rotation, bendDirection, sweep);
141
float along = (t - smoothTail) / Math.Max(0.001f, smoothHead - smoothTail);
142
float taper = 0.30f + 0.70f * (float)Math.Sin(MathHelper.Pi * MathHelper.Clamp(along, 0f, 1f));
143
144
Color outer = IsRequiem
145
? Color.Lerp(new Color(64, 0, 52, 0), new Color(130, 20, 220, 0), along)
146
: Color.Lerp(new Color(45, 0, 15, 0), new Color(145, 0, 36, 0), along);
147
Color blade = IsRequiem
148
? Color.Lerp(new Color(230, 16, 70, 0), new Color(85, 225, 255, 0), along)
149
: Color.Lerp(new Color(170, 0, 30, 0), new Color(255, 48, 95, 0), along);
150
Color core = Color.Lerp(new Color(255, 112, 142, 0), Color.White, 0.58f + 0.32f * along);
151
152
DrawSegment(previous, current, outer * (brightness * 0.72f), 20f * widthScale * taper);
153
DrawSegment(previous, current, blade * brightness, 10f * widthScale * taper);
154
if (!echo)
155
DrawSegment(previous, current, core * (brightness * 0.92f), 3.2f * widthScale * taper);
156
157
previous = current;
158
}
106
float appear = Smooth01(progress / 0.16f);
107
float disappear = 1f - Smooth01((progress - 0.52f) / 0.48f);
108
float strength = appear * disappear;
109
float impact = (float)Math.Pow(Math.Max(0f, Math.Sin(MathHelper.Pi * progress)), 0.58f);
110
float repetitionPulse = 0.94f + 0.06f * (float)Math.Sin((slash + 1) * 2.7f);
111
float length = (126f + Mastery * 76f + (IsRequiem ? 46f : 0f)) * Projectile.scale * repetitionPulse;
112
float width = (28f + Mastery * 18f + (IsRequiem ? 12f : 0f)) * Projectile.scale * impact;
113
Vector2 center = Projectile.Center - Main.screenPosition;
114
115
// Three passes over the exact same tapered silhouette produce one integrated
116
// blade rather than a bundle of separate straight laser strips.
117
DrawTaperedBladeLayer(center, length, width * 1.52f,
118
new Color(92, 0, 22, 0) * (strength * 0.54f));
119
DrawTaperedBladeLayer(center, length, width,
120
new Color(255, 8, 48, 255) * strength);
121
DrawTaperedBladeLayer(center, length * 0.985f, width * 0.62f,
122
Color.Black * Math.Min(1f, strength * 1.32f));
123
124
Vector2 direction = Projectile.rotation.ToRotationVector2();
125
Vector2 normal = direction.RotatedBy(MathHelper.PiOver2);
126
float endpointDistance = length * 0.5f;
127
float spark = (8f + Mastery * 5f) * Projectile.scale * strength;
128
DrawSegment(center + direction * endpointDistance - normal * spark,
129
center + direction * endpointDistance + normal * spark,
130
new Color(255, 12, 52, 0) * strength,
131
1.6f + Mastery);
132
DrawSegment(center - direction * endpointDistance - normal * spark,
133
center - direction * endpointDistance + normal * spark,
134
new Color(255, 12, 52, 0) * strength,
135
1.6f + Mastery);
136
}
159
137
160
if (echo)
138
private void DrawTaperedBladeLayer(Vector2 center, float length, float maximumWidth, Color color)
139
{
140
if (maximumWidth <= 0.05f)
161
141
return;
162
142
163
Vector2 head = GetArcPoint(slash, smoothHead, rotation, bendDirection, sweep);
164
Vector2 beforeHead = GetArcPoint(slash, Math.Max(smoothTail, smoothHead - 0.018f), rotation, bendDirection, sweep);
165
Vector2 tangent = (head - beforeHead).SafeNormalize(Vector2.UnitX);
166
Vector2 normal = tangent.RotatedBy(MathHelper.PiOver2);
167
float sparkLength = (13f + Mastery * 8f + (IsRequiem ? 7f : 0f)) * Projectile.scale * pulse;
168
DrawSegment(head - normal * sparkLength, head + normal * sparkLength, new Color(255, 30, 80, 0) * brightness, 4.6f * widthScale);
169
DrawSegment(head - normal * sparkLength * 0.62f, head + normal * sparkLength * 0.62f, Color.White * brightness, 1.5f * widthScale);
170
171
// A second, shorter crescent rides beside the main edge and gives the harvest
172
// slash a forged-blade silhouette rather than a single luminous stroke.
173
float edgeOffset = bendDirection * (5f + Mastery * 3f) * Projectile.scale;
174
Vector2 edgePrevious = GetArcPoint(slash, smoothTail + (smoothHead - smoothTail) * 0.18f, rotation, bendDirection, sweep + edgeOffset);
175
for (int segment = 1; segment <= 12; segment++)
143
Vector2 direction = Projectile.rotation.ToRotationVector2();
144
Vector2 start = center - direction * length * 0.5f;
145
const int pieces = 36;
146
for (int piece = 0; piece < pieces; piece++)
176
147
{
177
float t = MathHelper.Lerp(
178
smoothTail + (smoothHead - smoothTail) * 0.18f,
179
smoothHead - (smoothHead - smoothTail) * 0.10f,
180
segment / 12f);
181
Vector2 current = GetArcPoint(slash, t, rotation, bendDirection, sweep + edgeOffset);
182
DrawSegment(edgePrevious, current, new Color(255, 44, 110, 0) * (brightness * 0.75f), 2.2f * widthScale);
183
edgePrevious = current;
148
float t0 = piece / (float)pieces;
149
float t1 = (piece + 1f) / pieces;
150
float middle = (t0 + t1) * 0.5f;
151
float taper = (float)Math.Pow(Math.Max(0f, Math.Sin(MathHelper.Pi * middle)), 0.64f);
152
DrawSegment(
153
start + direction * (length * t0),
154
start + direction * (length * t1 + 0.8f),
155
color,
156
Math.Max(0.15f, maximumWidth * taper));
184
157
}
185
158
}
186
159
187
private Vector2 GetArcPoint(int slash, float t, float rotation, int bendDirection, float sweep)
188
{
189
float scale = Projectile.scale;
190
float length = (104f + Mastery * 62f + (IsRequiem ? 36f : 0f)) * scale;
191
float bend = (38f + Mastery * 22f + (IsRequiem ? 10f : 0f)) * scale;
192
float seedWave = DeterministicWave(slash, 4.19f);
193
float x = MathHelper.Lerp(-length * 0.5f, length * 0.5f, t);
194
float y = bendDirection * (-(float)Math.Sin(MathHelper.Pi * t) * bend
195
+ (float)Math.Sin(MathHelper.TwoPi * t + seedWave) * 3.5f * scale);
196
Vector2 local = new(x, y + sweep);
197
return Projectile.Center - Main.screenPosition + local.RotatedBy(rotation);
198
}
199
200
private void SpawnSlashBurst(int slash)
160
private void SpawnExecutionBurst()
201
161
{
202
float spread = slash - (SlashCount - 1) * 0.5f;
203
float rotation = Projectile.rotation + spread * 0.31f + DeterministicWave(slash, 1.71f) * 0.17f;
204
int bendDirection = ((Projectile.identity + slash) & 1) == 0 ? 1 : -1;
205
int count = 8 + (int)(Mastery * 7f) + (IsRequiem ? 4 : 0);
162
Vector2 direction = Projectile.rotation.ToRotationVector2();
163
Vector2 normal = direction.RotatedBy(MathHelper.PiOver2);
164
float length = (126f + Mastery * 76f + (IsRequiem ? 46f : 0f)) * Projectile.scale;
165
int count = 10 + (int)(Mastery * 8f) + (IsRequiem ? 5 : 0);
206
166
for (int i = 0; i < count; i++)
207
167
{
208
float t = (i + Main.rand.NextFloat()) / count;
209
// GetArcPoint is screen-space because it is also used by PreDraw.
210
Vector2 worldPosition = GetArcPoint(slash, t, rotation, bendDirection, -20f * Projectile.scale) + Main.screenPosition;
211
Vector2 radial = (worldPosition - Projectile.Center).SafeNormalize(Vector2.UnitY);
212
Vector2 velocity = radial.RotatedByRandom(0.7f) * Main.rand.NextFloat(1.2f, 4.8f);
213
int dustType = i % 3 switch
214
{
215
0 => DustID.Blood,
216
1 => DustID.Shadowflame,
217
_ => DustID.AncientLight
218
};
219
Color color = IsRequiem && i % 3 == 2
220
? new Color(75, 220, 255)
221
: Color.Lerp(new Color(130, 0, 30), new Color(255, 45, 100), Main.rand.NextFloat());
222
Dust dust = Dust.NewDustPerfect(worldPosition, dustType, velocity, 70, color, Main.rand.NextFloat(0.75f, 1.35f));
168
float along = Main.rand.NextFloat(-0.5f, 0.5f) * length;
169
Vector2 position = Projectile.Center + direction * along;
170
Vector2 velocity = normal * Main.rand.NextFloat(-4.8f, 4.8f) + direction * Main.rand.NextFloat(-0.8f, 0.8f);
171
int dustType = i % 3 == 0 ? DustID.Shadowflame : DustID.RedTorch;
172
Dust dust = Dust.NewDustPerfect(
173
position,
174
dustType,
175
velocity,
176
75,
177
i % 3 == 0 ? new Color(30, 0, 12) : new Color(255, 12, 54),
178
Main.rand.NextFloat(0.75f, 1.35f));
223
179
dust.noGravity = true;
224
dust.fadeIn = 0.4f;
180
dust.fadeIn = 0.35f;
225
181
}
226
182
}
227
183
@@ -244,11 +200,6 @@ public class DeathDomainHarvestSlashProjectile : ModProjectile
244
200
0f);
245
201
}
246
202
247
private float DeterministicWave(int slash, float salt)
248
{
249
return (float)Math.Sin((Projectile.identity + 1) * 0.73f + slash * salt);
250
}
251
252
203
private static float Smooth01(float value)
253
204
{
254
205
value = MathHelper.Clamp(value, 0f, 1f);
@@ -0,0 +1,141 @@
1
using Microsoft.Xna.Framework;
2
using Microsoft.Xna.Framework.Graphics;
3
using System;
4
using System.IO;
5
using Terraria;
6
using Terraria.DataStructures;
7
using Terraria.GameContent;
8
using Terraria.ID;
9
using Terraria.ModLoader;
10
11
namespace DeathMod.Projectiles;
12
13
/// <summary>
14
/// The thin crimson cut line placed on every enemy awaiting the next domain harvest.
15
/// Its rotation is reused by the actual blade so the execution lands exactly on the
16
/// warning rather than choosing a new direction at the last moment.
17
/// </summary>
18
public class DeathDomainHarvestTelegraphProjectile : ModProjectile
19
{
20
private int TargetIndex => (int)Projectile.ai[0];
21
private int TelegraphFrames => Math.Clamp((int)Projectile.ai[1], 1, 180);
22
private float Mastery => MathHelper.Clamp(Projectile.ai[2], 0f, 1f);
23
24
public override string Texture => "Terraria/Images/Projectile_0";
25
26
public override void SetStaticDefaults()
27
{
28
ProjectileID.Sets.DrawScreenCheckFluff[Type] = 3600;
29
}
30
31
public override void SetDefaults()
32
{
33
Projectile.width = 2;
34
Projectile.height = 2;
35
Projectile.friendly = false;
36
Projectile.hostile = false;
37
Projectile.tileCollide = false;
38
Projectile.ignoreWater = true;
39
Projectile.penetrate = -1;
40
Projectile.timeLeft = 120;
41
Projectile.netImportant = true;
42
}
43
44
public override void OnSpawn(IEntitySource source)
45
{
46
Projectile.timeLeft = TelegraphFrames + 6;
47
}
48
49
public override bool? CanDamage() => false;
50
51
public override void SendExtraAI(BinaryWriter writer)
52
{
53
writer.Write(Projectile.rotation);
54
writer.Write(Projectile.scale);
55
}
56
57
public override void ReceiveExtraAI(BinaryReader reader)
58
{
59
Projectile.rotation = reader.ReadSingle();
60
Projectile.scale = reader.ReadSingle();
61
}
62
63
public override void AI()
64
{
65
if (TargetIndex >= 0 && TargetIndex < Main.maxNPCs && Main.npc[TargetIndex].active)
66
Projectile.Center = Main.npc[TargetIndex].Center;
67
68
if (Main.dedServ || !Main.rand.NextBool(18))
69
return;
70
71
float halfLength = GetLength() * 0.5f;
72
Vector2 direction = Projectile.rotation.ToRotationVector2();
73
Vector2 end = Projectile.Center + direction * (Main.rand.NextBool() ? halfLength : -halfLength);
74
Dust dust = Dust.NewDustPerfect(
75
end,
76
DustID.RedTorch,
77
direction.RotatedByRandom(0.45f) * Main.rand.NextFloat(0.25f, 1.2f),
78
95,
79
new Color(255, 15, 50),
80
Main.rand.NextFloat(0.45f, 0.8f));
81
dust.noGravity = true;
82
}
83
84
public override bool PreDraw(ref Color lightColor)
85
{
86
float remaining = MathHelper.Clamp((Projectile.timeLeft - 5f) / Math.Max(1f, TelegraphFrames), 0f, 1f);
87
float urgency = 1f - remaining;
88
float pulse = 0.72f + (float)Math.Sin(Main.GlobalTimeWrappedHourly * (7f + urgency * 9f) + Projectile.identity) * 0.20f;
89
float length = GetLength();
90
Vector2 center = Projectile.Center - Main.screenPosition;
91
92
DrawTaperedLine(center, Projectile.rotation, length, (6f + Mastery * 2f) * Projectile.scale,
93
new Color(120, 0, 25, 0) * (0.24f + urgency * 0.22f));
94
DrawTaperedLine(center, Projectile.rotation, length, (1.35f + urgency * 0.85f) * Projectile.scale,
95
new Color(255, 8, 48, 0) * pulse);
96
return false;
97
}
98
99
private float GetLength()
100
{
101
return (118f + Mastery * 66f) * Projectile.scale;
102
}
103
104
private static void DrawTaperedLine(Vector2 center, float rotation, float length, float maximumWidth, Color color)
105
{
106
Vector2 direction = rotation.ToRotationVector2();
107
Vector2 start = center - direction * length * 0.5f;
108
const int pieces = 28;
109
for (int piece = 0; piece < pieces; piece++)
110
{
111
float t0 = piece / (float)pieces;
112
float t1 = (piece + 1f) / pieces;
113
float middle = (t0 + t1) * 0.5f;
114
float taper = (float)Math.Pow(Math.Max(0f, Math.Sin(MathHelper.Pi * middle)), 0.62f);
115
DrawSegment(
116
start + direction * (length * t0),
117
start + direction * (length * t1 + 0.8f),
118
color,
119
Math.Max(0.15f, maximumWidth * taper));
120
}
121
}
122
123
private static void DrawSegment(Vector2 start, Vector2 end, Color color, float width)
124
{
125
Vector2 segment = end - start;
126
if (segment.LengthSquared() < 0.01f)
127
return;
128
129
Texture2D pixel = TextureAssets.MagicPixel.Value;
130
Main.EntitySpriteDraw(
131
pixel,
132
start,
133
null,
134
color,
135
segment.ToRotation(),
136
new Vector2(0f, pixel.Height * 0.5f),
137
new Vector2(segment.Length() / pixel.Width, width / pixel.Height),
138
SpriteEffects.None,
139
0f);
140
}
141
}
@@ -102,7 +102,7 @@ Projectile count, projectile lifetime, percentage penetration, hit cooldown, and
102
102
103
103
After entering Hardmode, craft Wings of Death from Souls of Flight and train flight time, horizontal speed, acceleration, and vertical power. Max all four branches to unlock infinite flight. The Death Robe can be trained in defense, maximum life, regeneration, mobility, minions, sentries, placement range, lava immunity, utility immunities, and 15 separately purchased debuff immunities.
104
104
105
The Death Necklace unfolds a small field while equipped and enabled with the **Toggle Death Domain** keybind (`O` by default). While holding a mod sickle, every harvest cycle strikes all enemies in the field with layered crescent cuts instead of manifesting sickle projectiles. Each cut begins at 1% of a normal enemy's maximum life and 0.01% of a boss's maximum life; the slash-percentage branch raises those values to 10% and 0.1%, while Multislash grows from one to five cuts per cycle. Radius and frequency are the other two branches. Max all four and unlock **Deathly Sovereignty** to cover the screen with a moving soul-field background; every fourth harvest receives the more intense Death Requiem presentation.
105
The Death Necklace unfolds a small field while equipped and enabled with the **Toggle Death Domain** keybind (`O` by default). While holding a mod sickle, every enemy entering the field receives a thin crimson cut mark at a random angle. When the harvest cycle expires, a straight double-pointed blade—red at its edge and pure black through its center—executes along that exact line instead of manifesting sickle projectiles. Domain cuts apply Death Mark for soul harvesting but do not inherit Fated, life steal, form debuffs, or other sickle hit effects. Each cut begins at 1% of a normal enemy's maximum life and 0.01% of a boss's maximum life; the slash-percentage branch raises those values to 10% and 0.1%, while Multislash grows from one to five cuts per cycle. Radius and frequency are the other two branches. Max all four and unlock **Deathly Sovereignty** to cover the screen with a moving soul-field background; every fourth harvest receives the more intense Death Requiem presentation.
106
106
107
107
## Soul and boss rules
108
108
@@ -80,7 +80,7 @@ Soul Harvest(内部模组名 `DeathMod`)是一款围绕死神镰刀、人物
80
80
81
81
困难模式后可以制作死亡之翼,分别提升飞行时间、水平速度、加速度与垂直能力,四项满级后解锁无限飞行。死神长袍则可提升防御、生命、回复、机动、仆从、哨兵、放置距离、熔岩免疫及功能免疫;15 种减益免疫必须逐项购买。
82
82
83
佩戴死亡项链后,按“开关死亡领域”快捷键(默认 `O`)展开小范围领域。手持本模组镰刀时,每轮收割会以层叠弧月斩同时攻击领域内所有敌人,不再显化镰刀弹幕。每段斩击初始削去普通敌人 1% 最大生命、Boss 0.01% 最大生命;“斩击百分比”分支最高将其提升至普通敌人 10%、Boss 0.1%,而“多重斩击”可由每轮一斩提升至五斩。另有领域半径与收割频率两条分支;四项满级后点亮“死寂主权”,领域将覆盖全屏并显示流动灵魂背景,每第四轮收割会呈现更强烈的“死亡安魂曲”特效。
83
佩戴死亡项链后,按“开关死亡领域”快捷键(默认 `O`)展开小范围领域。手持本模组镰刀时,每个进入领域的敌人都会从随机角度出现一条纤细的猩红待斩刻线;收割计时结束后,外缘猩红、中心纯黑、两端尖锐且中段宽阔的直线劈斩会沿原刻线落下,不再显化镰刀弹幕。领域斩击只会施加用于灵魂收割的死亡标记,不继承命定、吸血、形态 Debuff 或其他镰刀命中效果。每段斩击初始削去普通敌人 1% 最大生命、Boss 0.01% 最大生命;“斩击百分比”分支最高将其提升至普通敌人 10%、Boss 0.1%,而“多重斩击”可由每轮一斩提升至五斩。另有领域半径与收割频率两条分支;四项满级后点亮“死寂主权”,领域将覆盖全屏并显示流动灵魂背景,每第四轮收割会呈现更强烈的“死亡安魂曲”特效。
84
84
85
85
## 灵魂与多人规则
86
86
@@ -192,7 +192,7 @@ The following 15 immunities are separate 1-essence purchases: On Fire, Poisoned,
192
192
193
193
### Death Necklace
194
194
195
Equip the necklace and press **Toggle Death Domain** (`O` by default). Its initial field reaches 10 tiles. While a DeathMod sickle is held, every harvest cycle applies a polished, staggered crescent slash to every enemy inside instead of spawning that sickle's projectiles. The four altar branches improve radius, frequency, slash percentage, and Multislash.
195
Equip the necklace and press **Toggle Death Domain** (`O` by default). Its initial field reaches 10 tiles. While a DeathMod sickle is held, enemies entering the domain receive a thin crimson warning line at a stable random angle. At harvest time, a straight, double-pointed red-and-black execution blade falls on that exact mark instead of spawning the sickle's projectiles. These domain cuts apply Death Mark but no Fated stacks, life steal, form debuff, or other inherited on-hit effect. The four altar branches improve radius, frequency, slash percentage, and Multislash.
196
196
197
197
Slash percentage starts at 1% maximum life per cut against normal enemies and 0.01% against bosses, reaching 10% and 0.1% at level 10. Multislash starts at one cut per cycle and reaches five; each cut resolves its percentage separately against every target. Max all four branches to unlock **Deathly Sovereignty** for 30 Soul Essence. It expands the field to the full screen, adds a moving soul-and-rune background, and gives every fourth harvest the intensified Death Requiem presentation.
198
198
@@ -192,7 +192,7 @@ Boss 不会直接掉落灵魂精华;请通过灵魂罐将奖励凝聚为精华
192
192
193
193
### 死亡项链
194
194
195
佩戴后按“开关死亡领域”快捷键(默认 `O`)。初始领域半径为 10 格;手持本模组镰刀时,每轮收割会以精细的层叠弧月斩同时攻击领域内所有敌人,不再复现镰刀弹幕。祭坛提供领域半径、收割频率、斩击百分比、多重斩击四条独立分支。
195
佩戴后按“开关死亡领域”快捷键(默认 `O`)。初始领域半径为 10 格;手持本模组镰刀时,敌人进入领域便会从稳定的随机角度出现一条纤细猩红待斩刻线。收割时,外缘猩红、中心纯黑、两端尖锐的直线处决刀光会沿原刻线落下,不再复现镰刀弹幕。领域斩击只施加死亡标记,不继承命定层数、吸血、形态 Debuff 或其他命中效果。祭坛提供领域半径、收割频率、斩击百分比、多重斩击四条独立分支。
196
196
197
197
每段斩击初始对普通敌人造成其最大生命 1% 的伤害、对 Boss 造成 0.01%,斩击百分比 10 级时分别达到 10% 和 0.1%。多重斩击从每轮一斩提升至最多五斩,每段都会对所有领域内目标单独结算。四项满级后可消耗 30 灵魂精华点亮“死寂主权”:领域扩张至整个屏幕并出现流动灵魂与符文背景;每第四轮收割会呈现强化的“死亡安魂曲”视觉效果。
198
198