返回提交历史
Modified
Common/DeathDomainVisualSystem.cs
+2
-2
Modified
Common/MyPlayer.cs
+85
-47
Modified
Items/DeathNecklace.cs
+16
-10
Modified
Localization/en-US.hjson
+7
-5
Modified
Localization/zh-Hans.hjson
+7
-5
Added
Projectiles/DeathDomainHarvestSlashProjectile.cs
+257
-0
Modified
README.md
+1
-1
Modified
README.zh-Hans.md
+1
-1
Modified
docs/PLAY_GUIDE.md
+2
-2
Modified
docs/PLAY_GUIDE.zh-Hans.md
+2
-2
XFEstudio/DeathMod
重做死亡领域百分比多重收割斩
a430911
代码差异
10 个文件
+380
-75
@@ -434,8 +434,8 @@ internal class DeathDomainVisualSystem : ModSystem
434
434
435
435
private static float GetMasteryProgress(int mastery)
436
436
{
437
// A fresh necklace has a mastery score of 5; a fully completed tree reaches 68.
438
return MathHelper.Clamp((mastery - 5f) / 63f, 0f, 1f);
437
// A fresh necklace has a mastery score of 5; a fully completed tree reaches 70.
438
return MathHelper.Clamp((mastery - 5f) / 65f, 0f, 1f);
439
439
}
440
440
441
441
private void ClearAnimationState()
@@ -345,12 +345,17 @@ public class MyPlayer : ModPlayer
345
345
private void UpdateDeathDomain()
346
346
{
347
347
DeathNecklace? necklace = ActiveDeathNecklace;
348
if (!DeathDomainEnabled || necklace is null || Player.dead || Player.whoAmI != Main.myPlayer)
348
if (!DeathDomainEnabled || necklace is null || Player.dead)
349
349
{
350
350
deathDomainTimer = 0;
351
351
return;
352
352
}
353
353
354
// Percentage damage is authoritative. Clients receive synchronized NPC strikes
355
// and the dedicated visual projectile spawned by the server.
356
if (Main.netMode == NetmodeID.MultiplayerClient)
357
return;
358
354
359
Item heldItem = Player.HeldItem;
355
360
NormalSickle? sickle = heldItem.ModItem as NormalSickle;
356
361
bool isLegacyDeath = heldItem.ModItem is LegacyDeath;
@@ -366,80 +371,113 @@ public class MyPlayer : ModPlayer
366
371
deathDomainTimer = 0;
367
372
368
373
List<NPC> targets = FindDeathDomainTargets(necklace);
369
if (targets.Count == 0 || CountActiveDomainProjectiles() >= necklace.MaxActiveProjectiles)
374
if (targets.Count == 0)
370
375
return;
371
376
372
377
deathDomainVolleyCounter++;
373
378
bool requiem = necklace.IsFullScreenDomain && deathDomainVolleyCounter % 4 == 0;
374
int count = necklace.VolleyCount * (requiem ? 2 : 1);
375
int damage = Math.Max(1, (int)Math.Round(Player.GetWeaponDamage(heldItem) * necklace.DamageFactor * (requiem ? 1.5f : 1f)));
376
for (int index = 0; index < count && CountActiveDomainProjectiles() < necklace.MaxActiveProjectiles; index++)
379
float visualMastery = MathHelper.Clamp((necklace.MasteryScore - 5f) / 65f, 0f, 1f);
380
foreach (NPC target in targets)
377
381
{
378
NPC target = targets[Main.rand.Next(targets.Count)];
379
Vector2 origin = GetDomainSpawnPosition(necklace, target, requiem);
380
float speed = sickle is null ? 11f : Math.Max(8f, sickle.AttackProjectileSpeed);
381
Vector2 velocity = (target.Center - origin).SafeNormalize(Vector2.UnitX * Player.direction) * speed;
382
int projectileType = isLegacyDeath
383
? ModContent.ProjectileType<LegacyDeathProjectile>()
384
: sickle!.AttackProjectileType > ProjectileID.None
385
? sickle.AttackProjectileType
386
: ModContent.ProjectileType<DomainSoulBladeProjectile>();
387
float ai0 = projectileType == ModContent.ProjectileType<DomainSoulBladeProjectile>() ? heldItem.type : 1f;
388
382
int projectileIndex = Projectile.NewProjectile(
389
383
Player.GetSource_Misc("DeathMod:DeathDomain"),
390
origin,
391
velocity,
392
projectileType,
393
damage,
394
heldItem.knockBack * 0.5f,
384
target.Center,
385
Vector2.Zero,
386
ModContent.ProjectileType<DeathDomainHarvestSlashProjectile>(),
387
0,
388
0f,
395
389
Player.whoAmI,
396
ai0,
397
MathHelper.Clamp((necklace.DamageLevel - 1) / 9f, 0f, 1f),
398
index % 2 == 0 ? 1f : -1f);
390
target.whoAmI,
391
necklace.SlashCount,
392
visualMastery + (requiem ? 2f : 0f));
399
393
if (projectileIndex >= 0 && projectileIndex < Main.maxProjectiles)
400
Main.projectile[projectileIndex].GetGlobalProjectile<MyGlobalProjectile>().ConfigureDeathDomainProjectile(Main.projectile[projectileIndex], sickle);
394
{
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;
399
}
400
401
// Resolve damage after broadcasting the visual so a lethal first cut is
402
// still shown to every client.
403
ApplyDeathDomainHarvest(target, necklace, sickle);
401
404
}
402
405
}
403
406
404
407
private List<NPC> FindDeathDomainTargets(DeathNecklace necklace)
405
408
{
406
409
List<NPC> targets = [];
407
Rectangle screen = new((int)Main.screenPosition.X - 96, (int)Main.screenPosition.Y - 96, Main.screenWidth + 192, Main.screenHeight + 192);
410
HashSet<int> selectedTargets = [];
411
float radius = necklace.IsFullScreenDomain ? 3200f : necklace.DomainRadius;
412
float radiusSquared = radius * radius;
408
413
foreach (NPC npc in Main.ActiveNPCs)
409
414
{
410
415
if (npc.friendly || npc.dontTakeDamage || npc.lifeMax <= 5 || !npc.CanBeChasedBy(Player))
411
416
continue;
412
if (necklace.IsFullScreenDomain ? screen.Intersects(npc.Hitbox) : Vector2.DistanceSquared(Player.Center, npc.Center) <= necklace.DomainRadius * necklace.DomainRadius)
413
targets.Add(npc);
417
418
NPC target = npc.realLife >= 0 && npc.realLife < Main.maxNPCs && Main.npc[npc.realLife].active
419
? Main.npc[npc.realLife]
420
: npc;
421
if (target.friendly || target.dontTakeDamage || target.lifeMax <= 5
422
|| selectedTargets.Contains(target.whoAmI)
423
|| Vector2.DistanceSquared(Player.Center, target.Center) > radiusSquared)
424
{
425
continue;
426
}
427
428
selectedTargets.Add(target.whoAmI);
429
targets.Add(target);
414
430
}
415
431
return targets;
416
432
}
417
433
418
private static Vector2 GetDomainSpawnPosition(DeathNecklace necklace, NPC target, bool requiem)
434
private void ApplyDeathDomainHarvest(NPC target, DeathNecklace necklace, NormalSickle? sickle)
419
435
{
420
if (necklace.IsFullScreenDomain)
436
bool bossOrBossPart = target.boss
437
|| NPCID.Sets.ShouldBeCountedAsBoss[target.type]
438
|| target.realLife >= 0 && target.realLife < Main.maxNPCs && Main.npc[target.realLife].boss;
439
float lifeRatio = bossOrBossPart ? necklace.BossSlashLifeRatio : necklace.NormalSlashLifeRatio;
440
int slashDamage = Math.Max(1, (int)Math.Ceiling(target.lifeMax * lifeRatio));
441
MyGlobalNPC targetData = target.GetGlobalNPC<MyGlobalNPC>();
442
targetData.RegisterSickleHit(target, Player.whoAmI);
443
target.lastInteraction = Player.whoAmI;
444
445
for (int slash = 0; slash < necklace.SlashCount && target.active && target.life > 0; slash++)
421
446
{
422
int edge = requiem ? Main.rand.Next(4) : Main.rand.Next(2);
423
return edge switch
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
NPC.HitInfo harvestHit = new()
424
462
{
425
0 => new Vector2(Main.screenPosition.X - 42f, Main.screenPosition.Y + Main.rand.NextFloat(Main.screenHeight)),
426
1 => new Vector2(Main.screenPosition.X + Main.screenWidth + 42f, Main.screenPosition.Y + Main.rand.NextFloat(Main.screenHeight)),
427
2 => new Vector2(Main.screenPosition.X + Main.rand.NextFloat(Main.screenWidth), Main.screenPosition.Y - 42f),
428
_ => new Vector2(Main.screenPosition.X + Main.rand.NextFloat(Main.screenWidth), Main.screenPosition.Y + Main.screenHeight + 42f)
463
Damage = slashDamage,
464
SourceDamage = slashDamage,
465
HitDirection = 0,
466
Knockback = 0f,
467
DamageType = DamageClass.Generic,
468
Crit = false
429
469
};
430
}
431
Vector2 direction = Main.rand.NextVector2Unit();
432
float radius = Main.rand.NextFloat(56f, Math.Min(necklace.DomainRadius * 0.8f, 340f));
433
return target.Center + direction * radius;
434
}
470
target.StrikeNPC(harvestHit, fromNet: false, noPlayerInteraction: false);
471
if (Main.netMode == NetmodeID.Server)
472
NetMessage.SendStrikeNPC(target, in harvestHit);
435
473
436
private int CountActiveDomainProjectiles()
437
{
438
int count = 0;
439
foreach (Projectile projectile in Main.ActiveProjectiles)
440
if (projectile.owner == Player.whoAmI && projectile.GetGlobalProjectile<MyGlobalProjectile>().IsDeathDomainProjectile)
441
count++;
442
return count;
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
}
443
481
}
444
482
445
483
private void SyncSoulBalance()
@@ -15,7 +15,7 @@ namespace DeathMod.Items;
15
15
public class DeathNecklace : ModItem, IDeathAltarUpgradeable
16
16
{
17
17
public const int MaxCoreLevel = 10;
18
public const int MaxVolleyLevel = 4;
18
public const int MaxVolleyLevel = 5;
19
19
20
20
private static readonly DeathAltarUpgradeType[] UpgradeTypes =
21
21
[
@@ -34,9 +34,9 @@ public class DeathNecklace : ModItem, IDeathAltarUpgradeable
34
34
35
35
public float DomainRadius => 160f + (RadiusLevel - 1) * 32f;
36
36
public int SpawnInterval => Math.Max(34, 100 - (FrequencyLevel - 1) * 7);
37
public float DamageFactor => 0.18f + (DamageLevel - 1) * 0.055f;
38
public int VolleyCount => VolleyLevel;
39
public int MaxActiveProjectiles => 3 + VolleyLevel * 3 + (SovereigntyUnlocked ? 6 : 0);
37
public float NormalSlashLifeRatio => DamageLevel * 0.01f;
38
public float BossSlashLifeRatio => DamageLevel * 0.0001f;
39
public int SlashCount => VolleyLevel;
40
40
public bool IsFullScreenDomain => SovereigntyUnlocked && AllCoreStatsMaxed;
41
41
public int MasteryScore => RadiusLevel + FrequencyLevel + DamageLevel + VolleyLevel * 2 + (SovereigntyUnlocked ? 30 : 0);
42
42
public IReadOnlyList<DeathAltarUpgradeType> AltarUpgradeTypes => UpgradeTypes;
@@ -85,8 +85,9 @@ public class DeathNecklace : ModItem, IDeathAltarUpgradeable
85
85
"Mods.DeathMod.UI.NecklaceStats",
86
86
DomainRadius / 16f,
87
87
SpawnInterval / 60f,
88
DamageFactor * 100f,
89
VolleyCount,
88
NormalSlashLifeRatio * 100f,
89
BossSlashLifeRatio * 100f,
90
SlashCount,
90
91
Language.GetTextValue(key))) { OverrideColor = new Color(110, 220, 240) });
91
92
tooltips.Add(new TooltipLine(Mod, "DeathDomainToggle", Language.GetTextValue("Mods.DeathMod.UI.DeathDomainToggleHint")));
92
93
tooltips.Add(new TooltipLine(Mod, "DeathModAltarHint", Language.GetTextValue("Mods.DeathMod.UI.AltarHint")));
@@ -136,8 +137,8 @@ public class DeathNecklace : ModItem, IDeathAltarUpgradeable
136
137
{
137
138
DeathAltarUpgradeType.NecklaceRadius => $"{DomainRadius / 16f:0.#} tiles",
138
139
DeathAltarUpgradeType.NecklaceFrequency => $"{SpawnInterval / 60f:0.##}s",
139
DeathAltarUpgradeType.NecklaceDamage => $"{DamageFactor:P0}",
140
DeathAltarUpgradeType.NecklaceVolley => $"×{VolleyCount}",
140
DeathAltarUpgradeType.NecklaceDamage => FormatSlashDamage(NormalSlashLifeRatio, BossSlashLifeRatio),
141
DeathAltarUpgradeType.NecklaceVolley => $"×{SlashCount}",
141
142
DeathAltarUpgradeType.NecklaceSovereignty => ToggleValue(SovereigntyUnlocked),
142
143
_ => string.Empty
143
144
};
@@ -146,8 +147,8 @@ public class DeathNecklace : ModItem, IDeathAltarUpgradeable
146
147
{
147
148
DeathAltarUpgradeType.NecklaceRadius => $"{(DomainRadius + 32f) / 16f:0.#} tiles",
148
149
DeathAltarUpgradeType.NecklaceFrequency => $"{Math.Max(34, SpawnInterval - 7) / 60f:0.##}s",
149
DeathAltarUpgradeType.NecklaceDamage => $"{DamageFactor + 0.055f:P0}",
150
DeathAltarUpgradeType.NecklaceVolley => $"×{VolleyCount + 1}",
150
DeathAltarUpgradeType.NecklaceDamage => FormatSlashDamage(NormalSlashLifeRatio + 0.01f, BossSlashLifeRatio + 0.0001f),
151
DeathAltarUpgradeType.NecklaceVolley => $"×{SlashCount + 1}",
151
152
DeathAltarUpgradeType.NecklaceSovereignty => Language.GetTextValue("Mods.DeathMod.UI.FullScreenRequiem"),
152
153
_ => string.Empty
153
154
};
@@ -212,4 +213,9 @@ public class DeathNecklace : ModItem, IDeathAltarUpgradeable
212
213
213
214
private static string ToggleValue(bool enabled) => Language.GetTextValue(
214
215
enabled ? "Mods.DeathMod.UI.Enabled" : "Mods.DeathMod.UI.Disabled");
216
217
private static string FormatSlashDamage(float normalRatio, float bossRatio)
218
{
219
return Language.GetTextValue("Mods.DeathMod.UI.NecklaceSlashDamage", normalRatio * 100f, bossRatio * 100f);
220
}
215
221
}
@@ -3,7 +3,7 @@ Mods: {
3
3
Items: {
4
4
DeathNecklace: {
5
5
DisplayName: Death Necklace
6
Tooltip: Equip to unfold a toggleable death domain that reproduces projectiles from the sickle in your hand and seeks enemies
6
Tooltip: Equip to toggle a death domain; while holding a DeathMod sickle, multislash harvests strike every enemy inside
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
DeathDomainHarvestSlashProjectile.DisplayName: Death Domain Harvest Cut
114
115
BoneWispProjectile.DisplayName: Bone Wisp
115
116
BloodCrescentProjectile.DisplayName: Blood Covenant Crescent
116
117
InfernalCrescentProjectile.DisplayName: Infernal Crescent
@@ -149,7 +150,8 @@ Mods: {
149
150
150
151
WingStats: Flight {0}f | Speed {1} | Acceleration {2} | Vertical power {3} | Infinite flight: {4}
151
152
RobeStats: Defense {0} | Life +{1} | Life regeneration +{2} | Debuff immunities {3}/{4} | Lava immunity: {5}
152
NecklaceStats: Domain {0:0.#} tiles | Interval {1:0.##}s | Damage {2:0.#}% | Volley ×{3} | Sovereignty: {4}
153
NecklaceStats: Domain {0:0.#} tiles | Interval {1:0.##}s | Normal slash {2:0.##}% | Boss slash {3:0.###}% | Multislash ×{4} | Sovereignty: {5}
154
NecklaceSlashDamage: Normal {0:0.##}% / Boss {1:0.###}%
153
155
DeathDomainToggleHint: Press the Death Domain keybind to enable or disable the field
154
156
FullScreenRequiem: Full screen + Death Requiem
155
157
AltarHint: Right-click a Death Altar to open its modification interface
@@ -248,9 +250,9 @@ Mods: {
248
250
RobeImmuneBrokenArmor: Immunity: Broken Armor
249
251
RobeImmuneSuffocation: Immunity: Suffocation
250
252
NecklaceRadius: Domain radius
251
NecklaceFrequency: Manifest frequency
252
NecklaceDamage: Domain damage
253
NecklaceVolley: Echo volley
253
NecklaceFrequency: Harvest frequency
254
NecklaceDamage: Slash percentage
255
NecklaceVolley: Multislash
254
256
NecklaceSovereignty: Deathly Sovereignty
255
257
}
256
258
@@ -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
DeathDomainHarvestSlashProjectile.DisplayName: 死亡领域收割斩
114
115
BoneWispProjectile.DisplayName: 亡骨幽光
115
116
BloodCrescentProjectile.DisplayName: 血契弧刃
116
117
InfernalCrescentProjectile.DisplayName: 炼狱弧刃
@@ -149,7 +150,8 @@ Mods: {
149
150
150
151
WingStats: 飞行 {0}帧 | 速度 {1} | 加速度 {2} | 垂直能力 {3} | 无限飞行:{4}
151
152
RobeStats: 防御 {0} | 生命 +{1} | 生命回复 +{2} | 减益免疫 {3}/{4} | 熔岩免疫:{5}
152
NecklaceStats: 领域 {0:0.#}格 | 间隔 {1:0.##}秒 | 伤害 {2:0.#}% | 齐射 ×{3} | 主权:{4}
153
NecklaceStats: 领域 {0:0.#}格 | 间隔 {1:0.##}秒 | 小怪斩击 {2:0.##}% | Boss斩击 {3:0.###}% | 多重斩击 ×{4} | 主权:{5}
154
NecklaceSlashDamage: 小怪 {0:0.##}% / Boss {1:0.###}%
153
155
DeathDomainToggleHint: 按“开关死亡领域”快捷键展开或收回领域
154
156
FullScreenRequiem: 全屏领域 + 死亡安魂曲
155
157
AltarHint: 在死神祭坛右键打开改造界面
@@ -248,9 +250,9 @@ Mods: {
248
250
RobeImmuneBrokenArmor: 免疫破损盔甲
249
251
RobeImmuneSuffocation: 免疫窒息
250
252
NecklaceRadius: 领域半径
251
NecklaceFrequency: 显化频率
252
NecklaceDamage: 领域伤害
253
NecklaceVolley: 回响齐射
253
NecklaceFrequency: 收割频率
254
NecklaceDamage: 斩击百分比
255
NecklaceVolley: 多重斩击
254
256
NecklaceSovereignty: 死寂主权
255
257
}
256
258
@@ -0,0 +1,257 @@
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
/// 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"/>.
17
/// </summary>
18
public class DeathDomainHarvestSlashProjectile : ModProjectile
19
{
20
private const int SlashActiveFrames = 18;
21
private const int SlashDelayFrames = 5;
22
private const int EndLingerFrames = 8;
23
24
private int TargetIndex => (int)Projectile.ai[0];
25
private int SlashCount => Math.Clamp((int)Projectile.ai[1], 1, DeathNecklaceMaxSlashCount);
26
private bool IsRequiem => Projectile.ai[2] >= 2f;
27
private float Mastery => MathHelper.Clamp(IsRequiem ? Projectile.ai[2] - 2f : Projectile.ai[2], 0f, 1f);
28
private int TotalLifetime => SlashActiveFrames + (SlashCount - 1) * SlashDelayFrames + EndLingerFrames;
29
30
// Avoid a content dependency from the visual layer back into the item type.
31
private const int DeathNecklaceMaxSlashCount = 5;
32
33
public override string Texture => "Terraria/Images/Projectile_0";
34
35
public override void SetStaticDefaults()
36
{
37
ProjectileID.Sets.DrawScreenCheckFluff[Type] = 3600;
38
}
39
40
public override void SetDefaults()
41
{
42
Projectile.width = 2;
43
Projectile.height = 2;
44
Projectile.friendly = false;
45
Projectile.hostile = false;
46
Projectile.tileCollide = false;
47
Projectile.ignoreWater = true;
48
Projectile.penetrate = -1;
49
Projectile.timeLeft = 60;
50
Projectile.netImportant = true;
51
}
52
53
public override void OnSpawn(IEntitySource source)
54
{
55
Projectile.timeLeft = TotalLifetime;
56
}
57
58
public override bool? CanDamage() => false;
59
60
public override void SendExtraAI(BinaryWriter writer)
61
{
62
// Rotation and scale are selected from the victim on the authoritative side;
63
// vanilla projectile sync does not guarantee either visual field.
64
writer.Write(Projectile.rotation);
65
writer.Write(Projectile.scale);
66
}
67
68
public override void ReceiveExtraAI(BinaryReader reader)
69
{
70
Projectile.rotation = reader.ReadSingle();
71
Projectile.scale = reader.ReadSingle();
72
}
73
74
public override void AI()
75
{
76
if (TargetIndex >= 0 && TargetIndex < Main.maxNPCs && Main.npc[TargetIndex].active)
77
Projectile.Center = Main.npc[TargetIndex].Center;
78
79
if (Main.dedServ)
80
return;
81
82
int age = TotalLifetime - Projectile.timeLeft;
83
int emittedMask = (int)Projectile.localAI[0];
84
for (int slash = 0; slash < SlashCount; slash++)
85
{
86
int bit = 1 << slash;
87
if ((emittedMask & bit) != 0 || age < slash * SlashDelayFrames)
88
continue;
89
90
emittedMask |= bit;
91
SpawnSlashBurst(slash);
92
}
93
Projectile.localAI[0] = emittedMask;
94
}
95
96
public override bool PreDraw(ref Color lightColor)
97
{
98
int age = TotalLifetime - Projectile.timeLeft;
99
for (int slash = 0; slash < SlashCount; slash++)
100
{
101
float progress = (age - slash * SlashDelayFrames) / (float)SlashActiveFrames;
102
if (progress <= 0f || progress >= 1.12f)
103
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);
111
}
112
return false;
113
}
114
115
private void DrawSlash(int slash, float progress, float opacity, bool echo)
116
{
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
}
159
160
if (echo)
161
return;
162
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++)
176
{
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;
184
}
185
}
186
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)
201
{
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);
206
for (int i = 0; i < count; i++)
207
{
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));
223
dust.noGravity = true;
224
dust.fadeIn = 0.4f;
225
}
226
}
227
228
private static void DrawSegment(Vector2 start, Vector2 end, Color color, float width)
229
{
230
Vector2 segment = end - start;
231
if (segment.LengthSquared() < 0.01f || width <= 0.01f)
232
return;
233
234
Texture2D pixel = TextureAssets.MagicPixel.Value;
235
Main.EntitySpriteDraw(
236
pixel,
237
start,
238
null,
239
color,
240
segment.ToRotation(),
241
new Vector2(0f, pixel.Height * 0.5f),
242
new Vector2(segment.Length() / pixel.Width, width / pixel.Height),
243
SpriteEffects.None,
244
0f);
245
}
246
247
private float DeterministicWave(int slash, float salt)
248
{
249
return (float)Math.Sin((Projectile.identity + 1) * 0.73f + slash * salt);
250
}
251
252
private static float Smooth01(float value)
253
{
254
value = MathHelper.Clamp(value, 0f, 1f);
255
return value * value * (3f - 2f * value);
256
}
257
}
@@ -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, the field creates that form's projectiles and directs them toward nearby targets. Radius, frequency, damage, and volley size are separate altar branches. Max all four and unlock **Deathly Sovereignty** to cover the screen with a moving soul-field background; every fourth volley becomes an empowered Death Requiem launched from the screen edges.
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.
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`)展开小范围领域。手持本模组镰刀时,领域会随机生成对应形态的弹幕并自动索敌。领域半径、发动频率、伤害和齐射数量可分别培养;四项满级后点亮“死寂主权”,领域将覆盖全屏并显示流动灵魂背景,每第四轮还会从屏幕边缘发动强化的“死亡安魂曲”。
83
佩戴死亡项链后,按“开关死亡领域”快捷键(默认 `O`)展开小范围领域。手持本模组镰刀时,每轮收割会以层叠弧月斩同时攻击领域内所有敌人,不再显化镰刀弹幕。每段斩击初始削去普通敌人 1% 最大生命、Boss 0.01% 最大生命;“斩击百分比”分支最高将其提升至普通敌人 10%、Boss 0.1%,而“多重斩击”可由每轮一斩提升至五斩。另有领域半径与收割频率两条分支;四项满级后点亮“死寂主权”,领域将覆盖全屏并显示流动灵魂背景,每第四轮收割会呈现更强烈的“死亡安魂曲”特效。
84
84
85
85
## 灵魂与多人规则
86
86
@@ -192,9 +192,9 @@ 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 and periodically manifests the projectile belonging to the mod sickle currently in hand; manifested attacks automatically select targets inside the field. The altar has separate branches for radius, frequency, damage multiplier, and volley size.
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.
196
196
197
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 changes every fourth volley into Death Requiem: twice the normal projectile count enters from screen edges at 150% of the domain's normal damage.
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
199
199
## 10. Recommended progression
200
200
@@ -192,9 +192,9 @@ Boss 不会直接掉落灵魂精华;请通过灵魂罐将奖励凝聚为精华
192
192
193
193
### 死亡项链
194
194
195
佩戴后按“开关死亡领域”快捷键(默认 `O`)。初始领域半径为 10 格,会周期性复现当前手持本模组镰刀的对应弹幕,并自动选择领域内目标。祭坛提供领域半径、发动频率、伤害倍率、齐射数量四条独立分支。
195
佩戴后按“开关死亡领域”快捷键(默认 `O`)。初始领域半径为 10 格;手持本模组镰刀时,每轮收割会以精细的层叠弧月斩同时攻击领域内所有敌人,不再复现镰刀弹幕。祭坛提供领域半径、收割频率、斩击百分比、多重斩击四条独立分支。
196
196
197
四项满级后可消耗 30 灵魂精华点亮“死寂主权”:领域扩张至整个屏幕并出现流动灵魂与符文背景;每第四轮变为“死亡安魂曲”,从屏幕边缘释放双倍数量弹幕,并造成领域常规伤害的 150%。
197
每段斩击初始对普通敌人造成其最大生命 1% 的伤害、对 Boss 造成 0.01%,斩击百分比 10 级时分别达到 10% 和 0.1%。多重斩击从每轮一斩提升至最多五斩,每段都会对所有领域内目标单独结算。四项满级后可消耗 30 灵魂精华点亮“死寂主权”:领域扩张至整个屏幕并出现流动灵魂与符文背景;每第四轮收割会呈现强化的“死亡安魂曲”视觉效果。
198
198
199
199
## 十、推荐成长顺序
200
200