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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

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

XFEstudio/DeathMod

重做死亡领域百分比多重收割斩

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

代码差异

10 个文件 +380 -75
Modified Common/DeathDomainVisualSystem.cs +2 -2
@@ -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()
Modified Common/MyPlayer.cs +85 -47
@@ -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()
Modified Items/DeathNecklace.cs +16 -10
@@ -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 }
Modified Localization/en-US.hjson +7 -5
@@ -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
Modified Localization/zh-Hans.hjson +7 -5
@@ -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
Added Projectiles/DeathDomainHarvestSlashProjectile.cs +257 -0
@@ -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 }
Modified README.md +1 -1
@@ -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
Modified README.zh-Hans.md +1 -1
@@ -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
Modified docs/PLAY_GUIDE.md +2 -2
@@ -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
Modified docs/PLAY_GUIDE.zh-Hans.md +2 -2
@@ -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