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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

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

XFEstudio/DeathMod

引入死亡项链及领域系统,重构祭坛UI

本次更新新增死亡项链装备及其领域技能树分支,支持在祭坛界面直接从背包和装备栏选择可改造物品。重构祭坛UI,优化技能树展示与多物品选择,完善多语言本地化。新增死亡领域弹幕、全屏主权领域、死亡之翼拖尾与多种视觉特效。扩展死神长袍新节点,补充相关文档。底层与网络协议增强多人环境下的同步与校验,提升交互体验与稳定性。

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

代码差异

22 个文件 +1635 -303
Modified Common/DeathAltarUpgrade.cs +39 -2
@@ -1,4 +1,5 @@
1 1 using System.Collections.Generic;
2 using System;
2 3 using Terraria;
3 4
4 5 namespace DeathMod.Common;
@@ -35,6 +36,7 @@ public enum DeathAltarUpgradeType : byte
35 36 RobeKnockbackImmunity,
36 37 RobeFallDamageImmunity,
37 38 RobeZeroMana,
39 RobeLavaImmunity,
38 40 RobeImmuneOnFire,
39 41 RobeImmunePoisoned,
40 42 RobeImmuneConfused,
@@ -49,7 +51,13 @@ public enum DeathAltarUpgradeType : byte
49 51 RobeImmuneChaosState,
50 52 RobeImmuneWet,
51 53 RobeImmuneBrokenArmor,
52 RobeImmuneSuffocation
54 RobeImmuneSuffocation,
55
56 NecklaceRadius,
57 NecklaceFrequency,
58 NecklaceDamage,
59 NecklaceVolley,
60 NecklaceSovereignty
53 61 }
54 62
55 63 public enum DeathAltarCurrency : byte
@@ -63,7 +71,36 @@ public enum DeathAltarUnavailableReason : byte
63 71 Maxed,
64 72 RequiresProjectile,
65 73 RequiresWingMastery,
66 RequiresFatedUnlock
74 RequiresFatedUnlock,
75 RequiresNecklaceMastery
76 }
77
78 public enum DeathAltarItemStorage : byte
79 {
80 Inventory,
81 Armor
82 }
83
84 public readonly record struct DeathAltarItemReference(DeathAltarItemStorage Storage, byte Slot, int ExpectedItemType)
85 {
86 public bool TryGetSlotItem(Player player, out Item item)
87 {
88 item = new Item();
89 Item[] container = Storage == DeathAltarItemStorage.Inventory ? player.inventory : player.armor;
90 int accessibleSlots = Storage == DeathAltarItemStorage.Inventory ? Math.Min(58, container.Length) : container.Length;
91 if (Slot >= accessibleSlots)
92 return false;
93
94 item = container[Slot];
95 return true;
96 }
97
98 public bool TryResolve(Player player, out Item item)
99 {
100 return TryGetSlotItem(player, out item)
101 && item.type == ExpectedItemType
102 && item.ModItem is IDeathAltarUpgradeable;
103 }
67 104 }
68 105
69 106 public readonly record struct DeathAltarPrice(int Amount, DeathAltarCurrency Currency)
Added Common/DeathDomainVisualSystem.cs +114 -0
@@ -0,0 +1,114 @@
1 using DeathMod.Items;
2 using Microsoft.Xna.Framework;
3 using Microsoft.Xna.Framework.Graphics;
4 using System;
5 using Terraria;
6 using Terraria.GameContent;
7 using Terraria.ModLoader;
8
9 namespace DeathMod.Common;
10
11 [Autoload(Side = ModSide.Client)]
12 internal class DeathDomainVisualSystem : ModSystem
13 {
14 public override void PostDrawTiles()
15 {
16 Player localPlayer = Main.LocalPlayer;
17 if (!localPlayer.active || localPlayer.dead)
18 return;
19 MyPlayer localData = localPlayer.GetModPlayer<MyPlayer>();
20 DeathNecklace? localNecklace = localData.ActiveDeathNecklace;
21 if (localNecklace is null || !localData.DeathDomainEnabled)
22 return;
23
24 SpriteBatch batch = Main.spriteBatch;
25 Texture2D pixel = TextureAssets.MagicPixel.Value;
26 if (localNecklace.IsFullScreenDomain)
27 DrawSovereignBackground(batch, pixel);
28
29 batch.Begin(
30 SpriteSortMode.Deferred,
31 BlendState.AlphaBlend,
32 SamplerState.PointClamp,
33 DepthStencilState.None,
34 RasterizerState.CullNone,
35 null,
36 Main.GameViewMatrix.TransformationMatrix);
37
38 for (int index = 0; index < Main.maxPlayers; index++)
39 {
40 Player player = Main.player[index];
41 if (!player.active || player.dead)
42 continue;
43 MyPlayer data = player.GetModPlayer<MyPlayer>();
44 DeathNecklace? necklace = data.ActiveDeathNecklace;
45 if (necklace is null || !data.DeathDomainEnabled)
46 continue;
47 if (!necklace.IsFullScreenDomain)
48 DrawDomainRing(batch, pixel, player.Center, necklace.DomainRadius, necklace.MasteryScore);
49 }
50 batch.End();
51 }
52
53 private static void DrawSovereignBackground(SpriteBatch batch, Texture2D pixel)
54 {
55 batch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone);
56 float time = Main.GlobalTimeWrappedHourly;
57 batch.Draw(pixel, new Rectangle(0, 0, Main.screenWidth, Main.screenHeight), new Color(8, 2, 18) * 0.42f);
58 batch.Draw(pixel, new Rectangle(0, 0, Main.screenWidth, Main.screenHeight / 2), new Color(28, 8, 55) * 0.18f);
59
60 for (int index = 0; index < 54; index++)
61 {
62 float seed = index * 73.31f;
63 float x = PositiveModulo(seed * 17f + time * (18f + index % 7 * 3f), Main.screenWidth + 120f) - 60f;
64 float y = PositiveModulo(seed * 11f - time * (9f + index % 5 * 2f), Main.screenHeight + 100f) - 50f;
65 float pulse = 0.45f + (float)Math.Sin(time * 2.4f + seed) * 0.25f;
66 Color color = index % 3 == 0 ? new Color(215, 30, 105) : new Color(65, 205, 245);
67 float length = 7f + index % 6 * 3f;
68 batch.Draw(pixel, new Vector2(x, y), null, color * pulse, -0.7f, Vector2.Zero, new Vector2(length, 1.2f), SpriteEffects.None, 0f);
69 }
70
71 Vector2 center = new(Main.screenWidth * 0.5f, Main.screenHeight * 0.5f);
72 for (int ring = 0; ring < 3; ring++)
73 {
74 float radius = Math.Min(Main.screenWidth, Main.screenHeight) * (0.22f + ring * 0.12f);
75 DrawCircle(batch, pixel, center, radius, 72, new Color(80 + ring * 25, 65, 155 + ring * 25) * (0.12f - ring * 0.02f), 2f, time * (ring % 2 == 0 ? 0.12f : -0.09f));
76 }
77 batch.End();
78 }
79
80 private static void DrawDomainRing(SpriteBatch batch, Texture2D pixel, Vector2 center, float radius, int mastery)
81 {
82 float time = Main.GlobalTimeWrappedHourly;
83 float pulse = 0.62f + (float)Math.Sin(time * 3.2f) * 0.14f;
84 Color color = Color.Lerp(new Color(185, 25, 80), new Color(65, 220, 242), MathHelper.Clamp(mastery / 70f, 0f, 1f));
85 DrawCircle(batch, pixel, center, radius, 80, color * pulse * 0.6f, 2.5f, time * 0.15f);
86 DrawCircle(batch, pixel, center, radius - 5f, 80, new Color(120, 50, 175) * pulse * 0.32f, 1.2f, -time * 0.1f);
87
88 for (int index = 0; index < 12; index++)
89 {
90 float angle = time * (0.18f + index % 3 * 0.04f) + index * MathHelper.TwoPi / 12f;
91 Vector2 position = center + angle.ToRotationVector2() * (radius - 4f);
92 batch.Draw(pixel, position, null, color * 0.72f, MathHelper.PiOver4, pixel.Size() * 0.5f, new Vector2(5f, 5f), SpriteEffects.None, 0f);
93 }
94 }
95
96 private static void DrawCircle(SpriteBatch batch, Texture2D pixel, Vector2 center, float radius, int segments, Color color, float width, float rotation)
97 {
98 Vector2 previous = center + rotation.ToRotationVector2() * radius;
99 for (int index = 1; index <= segments; index++)
100 {
101 float angle = rotation + MathHelper.TwoPi * index / segments;
102 Vector2 current = center + angle.ToRotationVector2() * radius;
103 Vector2 delta = current - previous;
104 batch.Draw(pixel, previous, null, color, delta.ToRotation(), Vector2.Zero, new Vector2(delta.Length(), width), SpriteEffects.None, 0f);
105 previous = current;
106 }
107 }
108
109 private static float PositiveModulo(float value, float modulus)
110 {
111 float result = value % modulus;
112 return result < 0f ? result + modulus : result;
113 }
114 }
Added Common/DeathWingTrailSystem.cs +101 -0
@@ -0,0 +1,101 @@
1 using Microsoft.Xna.Framework;
2 using Microsoft.Xna.Framework.Graphics;
3 using Terraria;
4 using Terraria.GameContent;
5 using Terraria.ModLoader;
6
7 namespace DeathMod.Common;
8
9 [Autoload(Side = ModSide.Client)]
10 internal class DeathWingTrailSystem : ModSystem
11 {
12 public override void PostDrawTiles()
13 {
14 bool hasVisibleTrail = false;
15 for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++)
16 {
17 Player player = Main.player[playerIndex];
18 if (player.active
19 && !player.dead
20 && player.GetModPlayer<MyPlayer>().DeathWingTrailCount >= 2)
21 {
22 hasVisibleTrail = true;
23 break;
24 }
25 }
26
27 if (!hasVisibleTrail)
28 return;
29
30 SpriteBatch spriteBatch = Main.spriteBatch;
31 spriteBatch.Begin(
32 SpriteSortMode.Deferred,
33 BlendState.AlphaBlend,
34 SamplerState.PointClamp,
35 DepthStencilState.None,
36 RasterizerState.CullNone,
37 null,
38 Main.GameViewMatrix.TransformationMatrix);
39
40 Texture2D pixel = TextureAssets.MagicPixel.Value;
41 for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++)
42 {
43 Player player = Main.player[playerIndex];
44 if (!player.active || player.dead)
45 continue;
46
47 MyPlayer modPlayer = player.GetModPlayer<MyPlayer>();
48 if (modPlayer.DeathWingTrailCount < 2 || modPlayer.DeathWingTrailOpacity <= 0f)
49 continue;
50
51 DrawTrail(spriteBatch, pixel, modPlayer.DeathWingUpperTrail, modPlayer);
52 DrawTrail(spriteBatch, pixel, modPlayer.DeathWingLowerTrail, modPlayer);
53 }
54
55 spriteBatch.End();
56 }
57
58 private static void DrawTrail(SpriteBatch spriteBatch, Texture2D pixel, Vector2[] positions, MyPlayer modPlayer)
59 {
60 float mastery = modPlayer.DeathWingTrailMastery;
61 int maximumPoints = 11 + (int)(mastery * 15f);
62 int pointCount = System.Math.Min(modPlayer.DeathWingTrailCount, maximumPoints);
63 float opacity = modPlayer.DeathWingTrailOpacity;
64 Color outerColor = Color.Lerp(new Color(105, 0, 22), new Color(225, 20, 100), mastery) with { A = 0 };
65 Color coreColor = Color.Lerp(new Color(235, 18, 42), new Color(255, 125, 195), mastery) with { A = 0 };
66
67 for (int index = pointCount - 1; index >= 1; index--)
68 {
69 Vector2 older = positions[index];
70 Vector2 newer = positions[index - 1];
71 if (older == Vector2.Zero || newer == Vector2.Zero)
72 continue;
73
74 Vector2 segment = newer - older;
75 float length = segment.Length();
76 if (length is < 0.35f or > 72f)
77 continue;
78
79 float strength = 1f - index / (float)pointCount;
80 float taper = (float)System.Math.Pow(strength, 0.72f);
81 float outerWidth = (2.35f + mastery * 2.1f) * taper;
82 float coreWidth = (0.68f + mastery * 0.82f) * taper;
83 DrawSegment(spriteBatch, pixel, older, segment, outerColor * opacity * strength * 0.38f, outerWidth);
84 DrawSegment(spriteBatch, pixel, older, segment, coreColor * opacity * strength * 0.88f, coreWidth);
85 }
86 }
87
88 private static void DrawSegment(SpriteBatch spriteBatch, Texture2D pixel, Vector2 start, Vector2 segment, Color color, float width)
89 {
90 spriteBatch.Draw(
91 pixel,
92 start - Main.screenPosition,
93 null,
94 color,
95 segment.ToRotation(),
96 new Vector2(0f, pixel.Height * 0.5f),
97 new Vector2(segment.Length() / pixel.Width, width / pixel.Height),
98 SpriteEffects.None,
99 0f);
100 }
101 }
Modified Common/MyGlobalNPC.cs +9 -5
@@ -37,19 +37,23 @@ public class MyGlobalNPC : GlobalNPC
37 37 FatedStackCount = 0;
38 38 }
39 39
40 public override void OnHitByItem(NPC npc, Player player, Item item, NPC.HitInfo hit, int damageDone)
40 public override void ModifyHitByItem(NPC npc, Player player, Item item, ref NPC.HitModifiers modifiers)
41 41 {
42 if (item.ModItem is NormalSickle or LegacyDeath)
43 RegisterSickleHit(npc, player.whoAmI);
44
45 42 if (Main.netMode == NetmodeID.MultiplayerClient
46 43 && player.whoAmI == Main.myPlayer
47 && item.ModItem is LegacyDeath)
44 && item.ModItem is NormalSickle or LegacyDeath)
48 45 {
46 // Send this before damage is resolved so a lethal first hit is registered in time.
49 47 DeathMod.SendSickleItemHit(npc);
50 48 }
51 49 }
52 50
51 public override void OnHitByItem(NPC npc, Player player, Item item, NPC.HitInfo hit, int damageDone)
52 {
53 if (item.ModItem is NormalSickle or LegacyDeath)
54 RegisterSickleHit(npc, player.whoAmI);
55 }
56
53 57 public override void OnHitByProjectile(NPC npc, Projectile projectile, NPC.HitInfo hit, int damageDone)
54 58 {
55 59 if (projectile.GetGlobalProjectile<MyGlobalProjectile>().IsSickleProjectile)
Modified Common/MyGlobalProjectile.cs +42 -1
@@ -14,6 +14,7 @@ public class MyGlobalProjectile : GlobalProjectile
14 14 public override bool InstancePerEntity => true;
15 15
16 16 public bool IsSickleProjectile { get; private set; }
17 public bool IsDeathDomainProjectile { get; private set; }
17 18 public int LifeStealLevel { get; private set; }
18 19 public int ArmorPenetrationLevel { get; private set; }
19 20 public int HitCooldownFrames { get; private set; }
@@ -101,6 +102,47 @@ public class MyGlobalProjectile : GlobalProjectile
101 102 projectile.localNPCHitCooldown = HitCooldownFrames;
102 103 }
103 104
105 internal void ConfigureDeathDomainProjectile(Projectile projectile, NormalSickle? sickle)
106 {
107 IsDeathDomainProjectile = true;
108 IsSickleProjectile = true;
109 if (sickle is not null)
110 {
111 LifeStealLevel = sickle.LifeStealLevel;
112 ArmorPenetrationLevel = sickle.ArmorPenetrationLevel;
113 HitCooldownFrames = sickle.HitCooldownFrames;
114 OnHitDebuffType = sickle.OnHitDebuffType;
115 OnHitDebuffDuration = sickle.OnHitDebuffDuration;
116 FatedStacksPerHit = sickle.FatedStacksPerHit;
117 FatedMaximumStacks = sickle.FatedMaximumStacks;
118 FatedDurationFrames = sickle.FatedDurationFrames;
119 }
120 else
121 {
122 HitCooldownFrames = NormalSickle.BaseHitCooldownFrames;
123 }
124
125 projectile.ArmorPenetration = ArmorPenetrationLevel;
126 projectile.usesIDStaticNPCImmunity = false;
127 projectile.usesLocalNPCImmunity = true;
128 projectile.localNPCHitCooldown = HitCooldownFrames;
129 projectile.netUpdate = true;
130 }
131
132 public override void ModifyHitNPC(Projectile projectile, NPC target, ref NPC.HitModifiers modifiers)
133 {
134 if (Main.netMode != NetmodeID.MultiplayerClient
135 || projectile.owner != Main.myPlayer
136 || (!IsSickleProjectile && projectile.ModProjectile is not SickleSwingProjectile))
137 {
138 return;
139 }
140
141 // Register the hit before vanilla sends the damage result. If this projectile kills the
142 // target in one hit, the server must already know who reaped it when OnKill is evaluated.
143 DeathMod.SendSickleHit(projectile, target);
144 }
145
104 146 public override void OnHitNPC(Projectile projectile, NPC target, NPC.HitInfo hit, int damageDone)
105 147 {
106 148 if ((!IsSickleProjectile && projectile.ModProjectile is not SickleSwingProjectile)
@@ -115,7 +157,6 @@ public class MyGlobalProjectile : GlobalProjectile
115 157
116 158 Main.player[projectile.owner].GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
117 159 LifeStealVisuals.Spawn(target.Center, projectile.owner, LifeStealLevel);
118 DeathMod.SendSickleHit(projectile, target);
119 160 return;
120 161 }
121 162
Modified Common/MyPlayer.cs +244 -1
@@ -1,9 +1,12 @@
1 1 using DeathMod.Items;
2 using DeathMod.Projectiles;
2 3 using Microsoft.Xna.Framework;
3 4 using System;
5 using System.Collections.Generic;
4 6 using Terraria;
5 7 using Terraria.Chat;
6 8 using Terraria.ID;
9 using Terraria.GameInput;
7 10 using Terraria.Localization;
8 11 using Terraria.ModLoader;
9 12 using Terraria.ModLoader.IO;
@@ -15,13 +18,29 @@ public class MyPlayer : ModPlayer
15 18 public const int StartingSouls = 150;
16 19 public const int SoulsPerEssence = 100;
17 20 public const int BossSoulReward = 150;
21 internal const int DeathWingTrailCapacity = 28;
18 22
19 23 public int Souls { get; private set; } = StartingSouls;
20 24 public int SickleAlternateCooldown { get; private set; }
21 25 public int SickleAlternateCooldownMax { get; private set; }
26 public bool DeathDomainEnabled { get; private set; }
27
28 internal DeathNecklace? ActiveDeathNecklace { get; private set; }
29
30 internal bool DeathWingsActiveThisTick { get; set; }
31 internal bool DeathWingsActiveLastTick { get; private set; }
32 internal Vector2[] DeathWingUpperTrail { get; } = new Vector2[DeathWingTrailCapacity];
33 internal Vector2[] DeathWingLowerTrail { get; } = new Vector2[DeathWingTrailCapacity];
34 internal int DeathWingTrailCount { get; private set; }
35 internal float DeathWingTrailMastery { get; private set; }
36 internal float DeathWingTrailOpacity { get; private set; }
22 37
23 38 private int lifeStealCooldown;
24 39 private int swingComboDirection = 1;
40 private bool serverSoulBalanceInitialized;
41 private ulong deathWingTrailUpdateTick = ulong.MaxValue;
42 private int deathDomainTimer;
43 private int deathDomainVolleyCounter;
25 44
26 45 public override void Initialize()
27 46 {
@@ -30,16 +49,32 @@ public class MyPlayer : ModPlayer
30 49 swingComboDirection = 1;
31 50 SickleAlternateCooldown = 0;
32 51 SickleAlternateCooldownMax = 0;
52 DeathWingsActiveThisTick = false;
53 DeathWingsActiveLastTick = false;
54 ClearDeathWingTrail();
55 serverSoulBalanceInitialized = false;
56 DeathDomainEnabled = false;
57 ActiveDeathNecklace = null;
58 deathDomainTimer = 0;
59 deathDomainVolleyCounter = 0;
60 }
61
62 public override void ResetEffects()
63 {
64 DeathWingsActiveThisTick = false;
65 ActiveDeathNecklace = null;
33 66 }
34 67
35 68 public override void SaveData(TagCompound tag)
36 69 {
37 70 tag[nameof(Souls)] = Souls;
71 tag[nameof(DeathDomainEnabled)] = DeathDomainEnabled;
38 72 }
39 73
40 74 public override void LoadData(TagCompound tag)
41 75 {
42 76 Souls = tag.ContainsKey(nameof(Souls)) ? Math.Max(0, tag.GetInt(nameof(Souls))) : StartingSouls;
77 DeathDomainEnabled = tag.GetBool(nameof(DeathDomainEnabled));
43 78 }
44 79
45 80 public override void PostUpdate()
@@ -48,17 +83,71 @@ public class MyPlayer : ModPlayer
48 83 lifeStealCooldown--;
49 84 if (SickleAlternateCooldown > 0)
50 85 SickleAlternateCooldown--;
86
87 UpdateDeathDomain();
88
89 if (Player.dead)
90 {
91 ClearDeathWingTrail();
92 }
93 else if (!DeathWingsActiveThisTick && DeathWingTrailOpacity > 0f)
94 {
95 DeathWingTrailOpacity *= 0.78f;
96 if (DeathWingTrailOpacity < 0.025f)
97 ClearDeathWingTrail();
98 }
99
100 DeathWingsActiveLastTick = DeathWingsActiveThisTick;
101 }
102
103 internal void RecordDeathWingTrail(Vector2 upperTip, Vector2 lowerTip, float mastery)
104 {
105 if (Main.dedServ || deathWingTrailUpdateTick == Main.GameUpdateCount)
106 return;
107
108 deathWingTrailUpdateTick = Main.GameUpdateCount;
109 if (DeathWingTrailCount > 0
110 && (Vector2.DistanceSquared(upperTip, DeathWingUpperTrail[0]) > 120f * 120f
111 || Vector2.DistanceSquared(lowerTip, DeathWingLowerTrail[0]) > 120f * 120f))
112 {
113 ClearDeathWingTrail();
114 }
115
116 int copyCount = Math.Min(DeathWingTrailCount, DeathWingTrailCapacity - 1);
117 if (copyCount > 0)
118 {
119 Array.Copy(DeathWingUpperTrail, 0, DeathWingUpperTrail, 1, copyCount);
120 Array.Copy(DeathWingLowerTrail, 0, DeathWingLowerTrail, 1, copyCount);
121 }
122
123 DeathWingUpperTrail[0] = upperTip;
124 DeathWingLowerTrail[0] = lowerTip;
125 DeathWingTrailCount = Math.Min(DeathWingTrailCount + 1, DeathWingTrailCapacity);
126 DeathWingTrailMastery = MathHelper.Clamp(mastery, 0f, 1f);
127 DeathWingTrailOpacity = 1f;
128 }
129
130 private void ClearDeathWingTrail()
131 {
132 DeathWingTrailCount = 0;
133 DeathWingTrailMastery = 0f;
134 DeathWingTrailOpacity = 0f;
135 deathWingTrailUpdateTick = ulong.MaxValue;
51 136 }
52 137
53 138 public override void SyncPlayer(int toWho, int fromWho, bool newPlayer)
54 139 {
55 if (Main.netMode != NetmodeID.Server)
140 if (Main.netMode == NetmodeID.SinglePlayer
141 || Main.netMode == NetmodeID.MultiplayerClient && Player.whoAmI != Main.myPlayer)
142 {
56 143 return;
144 }
57 145
58 146 ModPacket packet = Mod.GetPacket();
59 147 packet.Write((byte)DeathMod.MessageType.SyncSoulPlayer);
60 148 packet.Write((byte)Player.whoAmI);
61 149 packet.Write(Souls);
150 packet.Write(DeathDomainEnabled);
62 151 packet.Send(toWho, fromWho);
63 152 }
64 153
@@ -67,6 +156,24 @@ public class MyPlayer : ModPlayer
67 156 Souls = Math.Max(0, souls);
68 157 }
69 158
159 internal void ReceiveDeathDomainState(bool enabled)
160 {
161 DeathDomainEnabled = enabled;
162 if (!enabled)
163 deathDomainTimer = 0;
164 }
165
166 internal bool TryInitializeServerSoulBalance(int souls, bool deathDomainEnabled)
167 {
168 if (Main.netMode != NetmodeID.Server || serverSoulBalanceInitialized)
169 return false;
170
171 Souls = Math.Max(0, souls);
172 DeathDomainEnabled = deathDomainEnabled;
173 serverSoulBalanceInitialized = true;
174 return true;
175 }
176
70 177 internal bool TrySpendSouls(int amount)
71 178 {
72 179 if (amount < 0 || Souls < amount || Main.netMode == NetmodeID.MultiplayerClient)
@@ -192,6 +299,142 @@ public class MyPlayer : ModPlayer
192 299 SickleAlternateCooldownMax = frames;
193 300 }
194 301
302 public override void ProcessTriggers(TriggersSet triggersSet)
303 {
304 if (DeathMod.ToggleDeathDomainKeybind?.JustPressed != true)
305 return;
306
307 DeathNecklace? necklace = ActiveDeathNecklace ?? FindEquippedDeathNecklace();
308 if (necklace is null)
309 {
310 Main.NewText(Language.GetTextValue("Mods.DeathMod.Messages.DeathNecklaceRequired"), new Color(235, 85, 115));
311 return;
312 }
313
314 ReceiveDeathDomainState(!DeathDomainEnabled);
315 DeathMod.SendDeathDomainState(Player, DeathDomainEnabled);
316 Main.NewText(Language.GetTextValue(DeathDomainEnabled
317 ? "Mods.DeathMod.Messages.DeathDomainEnabled"
318 : "Mods.DeathMod.Messages.DeathDomainDisabled"), new Color(90, 225, 240));
319 }
320
321 internal void SetDeathNecklace(DeathNecklace necklace)
322 {
323 if (ActiveDeathNecklace is null || necklace.MasteryScore > ActiveDeathNecklace.MasteryScore)
324 ActiveDeathNecklace = necklace;
325 }
326
327 private DeathNecklace? FindEquippedDeathNecklace()
328 {
329 DeathNecklace? best = null;
330 foreach (Item item in Player.armor)
331 {
332 if (item.ModItem is DeathNecklace candidate && (best is null || candidate.MasteryScore > best.MasteryScore))
333 best = candidate;
334 }
335 return best;
336 }
337
338 private void UpdateDeathDomain()
339 {
340 DeathNecklace? necklace = ActiveDeathNecklace;
341 if (!DeathDomainEnabled || necklace is null || Player.dead || Player.whoAmI != Main.myPlayer)
342 {
343 deathDomainTimer = 0;
344 return;
345 }
346
347 Item heldItem = Player.HeldItem;
348 NormalSickle? sickle = heldItem.ModItem as NormalSickle;
349 bool isLegacyDeath = heldItem.ModItem is LegacyDeath;
350 if (sickle is null && !isLegacyDeath)
351 {
352 deathDomainTimer = 0;
353 return;
354 }
355
356 deathDomainTimer++;
357 if (deathDomainTimer < necklace.SpawnInterval)
358 return;
359 deathDomainTimer = 0;
360
361 List<NPC> targets = FindDeathDomainTargets(necklace);
362 if (targets.Count == 0 || CountActiveDomainProjectiles() >= necklace.MaxActiveProjectiles)
363 return;
364
365 deathDomainVolleyCounter++;
366 bool requiem = necklace.IsFullScreenDomain && deathDomainVolleyCounter % 4 == 0;
367 int count = necklace.VolleyCount * (requiem ? 2 : 1);
368 int damage = Math.Max(1, (int)Math.Round(Player.GetWeaponDamage(heldItem) * necklace.DamageFactor * (requiem ? 1.5f : 1f)));
369 for (int index = 0; index < count && CountActiveDomainProjectiles() < necklace.MaxActiveProjectiles; index++)
370 {
371 NPC target = targets[Main.rand.Next(targets.Count)];
372 Vector2 origin = GetDomainSpawnPosition(necklace, target, requiem);
373 float speed = sickle is null ? 11f : Math.Max(8f, sickle.AttackProjectileSpeed);
374 Vector2 velocity = (target.Center - origin).SafeNormalize(Vector2.UnitX * Player.direction) * speed;
375 int projectileType = isLegacyDeath
376 ? ModContent.ProjectileType<LegacyDeathProjectile>()
377 : sickle!.AttackProjectileType > ProjectileID.None
378 ? sickle.AttackProjectileType
379 : ModContent.ProjectileType<DomainSoulBladeProjectile>();
380 float ai0 = projectileType == ModContent.ProjectileType<DomainSoulBladeProjectile>() ? heldItem.type : 1f;
381 int projectileIndex = Projectile.NewProjectile(
382 Player.GetSource_Misc("DeathMod:DeathDomain"),
383 origin,
384 velocity,
385 projectileType,
386 damage,
387 heldItem.knockBack * 0.5f,
388 Player.whoAmI,
389 ai0,
390 MathHelper.Clamp((necklace.DamageLevel - 1) / 9f, 0f, 1f),
391 index % 2 == 0 ? 1f : -1f);
392 if (projectileIndex >= 0 && projectileIndex < Main.maxProjectiles)
393 Main.projectile[projectileIndex].GetGlobalProjectile<MyGlobalProjectile>().ConfigureDeathDomainProjectile(Main.projectile[projectileIndex], sickle);
394 }
395 }
396
397 private List<NPC> FindDeathDomainTargets(DeathNecklace necklace)
398 {
399 List<NPC> targets = [];
400 Rectangle screen = new((int)Main.screenPosition.X - 96, (int)Main.screenPosition.Y - 96, Main.screenWidth + 192, Main.screenHeight + 192);
401 foreach (NPC npc in Main.ActiveNPCs)
402 {
403 if (npc.friendly || npc.dontTakeDamage || npc.lifeMax <= 5 || !npc.CanBeChasedBy(Player))
404 continue;
405 if (necklace.IsFullScreenDomain ? screen.Intersects(npc.Hitbox) : Vector2.DistanceSquared(Player.Center, npc.Center) <= necklace.DomainRadius * necklace.DomainRadius)
406 targets.Add(npc);
407 }
408 return targets;
409 }
410
411 private static Vector2 GetDomainSpawnPosition(DeathNecklace necklace, NPC target, bool requiem)
412 {
413 if (necklace.IsFullScreenDomain)
414 {
415 int edge = requiem ? Main.rand.Next(4) : Main.rand.Next(2);
416 return edge switch
417 {
418 0 => new Vector2(Main.screenPosition.X - 42f, Main.screenPosition.Y + Main.rand.NextFloat(Main.screenHeight)),
419 1 => new Vector2(Main.screenPosition.X + Main.screenWidth + 42f, Main.screenPosition.Y + Main.rand.NextFloat(Main.screenHeight)),
420 2 => new Vector2(Main.screenPosition.X + Main.rand.NextFloat(Main.screenWidth), Main.screenPosition.Y - 42f),
421 _ => new Vector2(Main.screenPosition.X + Main.rand.NextFloat(Main.screenWidth), Main.screenPosition.Y + Main.screenHeight + 42f)
422 };
423 }
424 Vector2 direction = Main.rand.NextVector2Unit();
425 float radius = Main.rand.NextFloat(56f, Math.Min(necklace.DomainRadius * 0.8f, 340f));
426 return target.Center + direction * radius;
427 }
428
429 private int CountActiveDomainProjectiles()
430 {
431 int count = 0;
432 foreach (Projectile projectile in Main.ActiveProjectiles)
433 if (projectile.owner == Player.whoAmI && projectile.GetGlobalProjectile<MyGlobalProjectile>().IsDeathDomainProjectile)
434 count++;
435 return count;
436 }
437
195 438 private void SyncSoulBalance()
196 439 {
197 440 if (Main.netMode == NetmodeID.Server)
Modified Common/SickleUpgradeService.cs +23 -6
@@ -13,7 +13,7 @@ internal static class DeathAltarUpgradeService
13 13 {
14 14 private const float MaximumAltarDistance = 12f * 16f;
15 15
16 public static bool TryUpgrade(Player player, DeathAltarUpgradeType upgradeType, int tileX, int tileY)
16 public static bool TryUpgrade(Player player, DeathAltarUpgradeType upgradeType, DeathAltarItemReference itemReference, int tileX, int tileY)
17 17 {
18 18 if (Main.netMode == NetmodeID.MultiplayerClient)
19 19 return false;
@@ -24,6 +24,12 @@ internal static class DeathAltarUpgradeService
24 24 return false;
25 25 }
26 26
27 if (!Enum.IsDefined(typeof(DeathAltarItemStorage), itemReference.Storage))
28 {
29 DeathMod.SendOperationResult(player, DeathMod.OperationResult.HoldSickle);
30 return false;
31 }
32
27 33 if (!WorldGen.InWorld(tileX, tileY, 1))
28 34 {
29 35 DeathMod.SendOperationResult(player, DeathMod.OperationResult.TooFarFromAltar);
@@ -38,14 +44,13 @@ internal static class DeathAltarUpgradeService
38 44 return false;
39 45 }
40 46
41 int selectedSlot = player.selectedItem;
42 if (selectedSlot < 0 || selectedSlot >= 58 || player.inventory[selectedSlot].ModItem is not IDeathAltarUpgradeable upgradeable)
47 if (!itemReference.TryResolve(player, out Item targetItem)
48 || targetItem.ModItem is not IDeathAltarUpgradeable upgradeable)
43 49 {
44 50 DeathMod.SendOperationResult(player, DeathMod.OperationResult.HoldSickle);
45 51 return false;
46 52 }
47 53
48 Item heldItem = player.inventory[selectedSlot];
49 54 DeathAltarPrice price = upgradeable.GetUpgradePrice(upgradeType);
50 55 if (!price.IsAvailable)
51 56 {
@@ -67,7 +72,7 @@ internal static class DeathAltarUpgradeService
67 72 return false;
68 73 }
69 74
70 bool upgraded = upgradeable.ApplyAltarUpgrade(heldItem, upgradeType);
75 bool upgraded = upgradeable.ApplyAltarUpgrade(targetItem, upgradeType);
71 76
72 77 if (!upgraded)
73 78 {
@@ -81,7 +86,7 @@ internal static class DeathAltarUpgradeService
81 86 if (price.Currency == DeathAltarCurrency.Souls && !soulPlayer.TrySpendSouls(price.Amount))
82 87 return false;
83 88
84 SyncInventorySlot(player, selectedSlot);
89 SyncUpgradeableItem(player, itemReference);
85 90 foreach (int slot in changedEssenceSlots)
86 91 SyncInventorySlot(player, slot);
87 92
@@ -124,4 +129,16 @@ internal static class DeathAltarUpgradeService
124 129 player.inventory[slot].prefix);
125 130 DeathMod.SendInventorySlot(player, slot);
126 131 }
132
133 private static void SyncUpgradeableItem(Player player, DeathAltarItemReference itemReference)
134 {
135 if (Main.netMode != NetmodeID.Server || !itemReference.TryGetSlotItem(player, out Item item))
136 return;
137
138 int networkSlot = itemReference.Storage == DeathAltarItemStorage.Inventory
139 ? PlayerItemSlotID.Inventory0 + itemReference.Slot
140 : PlayerItemSlotID.Armor0 + itemReference.Slot;
141 NetMessage.SendData(MessageID.SyncEquipment, -1, -1, null, player.whoAmI, networkSlot, item.prefix);
142 DeathMod.SendAltarItem(player, itemReference);
143 }
127 144 }
Modified DeathMod.cs +105 -3
@@ -23,7 +23,9 @@ public class DeathMod : Mod
23 23 OperationResult,
24 24 SickleHitRequest,
25 25 SyncInventorySlot,
26 SyncDeathMark
26 SyncDeathMark,
27 SyncAltarItem,
28 SyncDeathDomainState
27 29 }
28 30
29 31 internal enum OperationResult : byte
@@ -40,14 +42,22 @@ public class DeathMod : Mod
40 42 }
41 43
42 44 public static DeathMod? Current { get; private set; }
45 public static ModKeybind? ToggleDeathDomainKeybind { get; private set; }
43 46
44 47 public DeathMod()
45 48 {
46 49 Current = this;
47 50 }
48 51
52 public override void Load()
53 {
54 if (!Main.dedServ)
55 ToggleDeathDomainKeybind = KeybindLoader.RegisterKeybind(this, "ToggleDeathDomain", "O");
56 }
57
49 58 public override void Unload()
50 59 {
60 ToggleDeathDomainKeybind = null;
51 61 Current = null;
52 62 }
53 63
@@ -60,8 +70,22 @@ public class DeathMod : Mod
60 70 {
61 71 byte playerIndex = reader.ReadByte();
62 72 int souls = reader.ReadInt32();
73 bool deathDomainEnabled = reader.ReadBoolean();
63 74 if (Main.netMode == NetmodeID.MultiplayerClient && playerIndex < Main.maxPlayers)
64 Main.player[playerIndex].GetModPlayer<MyPlayer>().ReceiveSouls(souls);
75 {
76 MyPlayer modPlayer = Main.player[playerIndex].GetModPlayer<MyPlayer>();
77 modPlayer.ReceiveSouls(souls);
78 modPlayer.ReceiveDeathDomainState(deathDomainEnabled);
79 }
80 else if (Main.netMode == NetmodeID.Server
81 && playerIndex == whoAmI
82 && playerIndex < Main.maxPlayers
83 && Main.player[playerIndex].active)
84 {
85 MyPlayer modPlayer = Main.player[playerIndex].GetModPlayer<MyPlayer>();
86 if (modPlayer.TryInitializeServerSoulBalance(souls, deathDomainEnabled))
87 modPlayer.SyncPlayer(-1, -1, false);
88 }
65 89 break;
66 90 }
67 91 case MessageType.RequestExtractEssence:
@@ -75,10 +99,16 @@ public class DeathMod : Mod
75 99 case MessageType.RequestAltarUpgrade:
76 100 {
77 101 DeathAltarUpgradeType upgradeType = (DeathAltarUpgradeType)reader.ReadByte();
102 DeathAltarItemStorage storage = (DeathAltarItemStorage)reader.ReadByte();
103 byte slot = reader.ReadByte();
104 int expectedItemType = reader.ReadInt32();
78 105 short tileX = reader.ReadInt16();
79 106 short tileY = reader.ReadInt16();
80 107 if (Main.netMode == NetmodeID.Server && whoAmI >= 0 && whoAmI < Main.maxPlayers && Main.player[whoAmI].active)
81 DeathAltarUpgradeService.TryUpgrade(Main.player[whoAmI], upgradeType, tileX, tileY);
108 {
109 DeathAltarUpgradeService.TryUpgrade(Main.player[whoAmI], upgradeType,
110 new DeathAltarItemReference(storage, slot, expectedItemType), tileX, tileY);
111 }
82 112 break;
83 113 }
84 114 case MessageType.OperationResult:
@@ -123,6 +153,44 @@ public class DeathMod : Mod
123 153 }
124 154 break;
125 155 }
156 case MessageType.SyncAltarItem:
157 {
158 if (Main.netMode != NetmodeID.MultiplayerClient)
159 break;
160
161 DeathAltarItemStorage storage = (DeathAltarItemStorage)reader.ReadByte();
162 byte slot = reader.ReadByte();
163 Item item = ItemIO.Receive(reader, readStack: true, readFavorite: true);
164 Item[] container = storage == DeathAltarItemStorage.Inventory
165 ? Main.LocalPlayer.inventory
166 : Main.LocalPlayer.armor;
167 if (slot < container.Length)
168 {
169 container[slot] = item;
170 Recipe.FindRecipes(canDelayCheck: true);
171 }
172 break;
173 }
174 case MessageType.SyncDeathDomainState:
175 {
176 if (Main.netMode == NetmodeID.Server)
177 {
178 bool enabled = reader.ReadBoolean();
179 if (whoAmI >= 0 && whoAmI < Main.maxPlayers && Main.player[whoAmI].active)
180 {
181 Main.player[whoAmI].GetModPlayer<MyPlayer>().ReceiveDeathDomainState(enabled);
182 BroadcastDeathDomainState(whoAmI, enabled);
183 }
184 }
185 else if (Main.netMode == NetmodeID.MultiplayerClient)
186 {
187 byte playerIndex = reader.ReadByte();
188 bool enabled = reader.ReadBoolean();
189 if (playerIndex < Main.maxPlayers)
190 Main.player[playerIndex].GetModPlayer<MyPlayer>().ReceiveDeathDomainState(enabled);
191 }
192 break;
193 }
126 194 default:
127 195 Logger.Warn($"Unknown DeathMod packet: {messageType}");
128 196 break;
@@ -158,6 +226,40 @@ public class DeathMod : Mod
158 226 packet.Send(player.whoAmI);
159 227 }
160 228
229 internal static void SendAltarItem(Player player, DeathAltarItemReference itemReference)
230 {
231 if (Main.netMode != NetmodeID.Server || !itemReference.TryGetSlotItem(player, out Item item))
232 return;
233
234 ModPacket packet = ModContent.GetInstance<DeathMod>().GetPacket();
235 packet.Write((byte)MessageType.SyncAltarItem);
236 packet.Write((byte)itemReference.Storage);
237 packet.Write(itemReference.Slot);
238 ItemIO.Send(item, packet, writeStack: true, writeFavorite: true);
239 packet.Send(player.whoAmI);
240 }
241
242 internal static void SendDeathDomainState(Player player, bool enabled)
243 {
244 if (Main.netMode != NetmodeID.MultiplayerClient || player.whoAmI != Main.myPlayer)
245 return;
246 ModPacket packet = ModContent.GetInstance<DeathMod>().GetPacket();
247 packet.Write((byte)MessageType.SyncDeathDomainState);
248 packet.Write(enabled);
249 packet.Send();
250 }
251
252 private static void BroadcastDeathDomainState(int playerIndex, bool enabled)
253 {
254 if (Main.netMode != NetmodeID.Server)
255 return;
256 ModPacket packet = ModContent.GetInstance<DeathMod>().GetPacket();
257 packet.Write((byte)MessageType.SyncDeathDomainState);
258 packet.Write((byte)playerIndex);
259 packet.Write(enabled);
260 packet.Send(-1, playerIndex);
261 }
262
161 263 internal static void BroadcastDeathMark(NPC npc, bool marked)
162 264 {
163 265 if (Main.netMode != NetmodeID.Server || npc.whoAmI < 0 || npc.whoAmI >= Main.maxNPCs)
Added Items/DeathNecklace.cs +205 -0
@@ -0,0 +1,205 @@
1 using DeathMod.Common;
2 using DeathMod.Tiles;
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.IO;
7 using Terraria;
8 using Terraria.ID;
9 using Terraria.Localization;
10 using Terraria.ModLoader;
11 using Terraria.ModLoader.IO;
12
13 namespace DeathMod.Items;
14
15 public class DeathNecklace : ModItem, IDeathAltarUpgradeable
16 {
17 public const int MaxCoreLevel = 10;
18 public const int MaxVolleyLevel = 4;
19
20 private static readonly DeathAltarUpgradeType[] UpgradeTypes =
21 [
22 DeathAltarUpgradeType.NecklaceRadius,
23 DeathAltarUpgradeType.NecklaceFrequency,
24 DeathAltarUpgradeType.NecklaceDamage,
25 DeathAltarUpgradeType.NecklaceVolley,
26 DeathAltarUpgradeType.NecklaceSovereignty
27 ];
28
29 public int RadiusLevel { get; private set; } = 1;
30 public int FrequencyLevel { get; private set; } = 1;
31 public int DamageLevel { get; private set; } = 1;
32 public int VolleyLevel { get; private set; } = 1;
33 public bool SovereigntyUnlocked { get; private set; }
34
35 public float DomainRadius => 160f + (RadiusLevel - 1) * 32f;
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);
40 public bool IsFullScreenDomain => SovereigntyUnlocked && AllCoreStatsMaxed;
41 public int MasteryScore => RadiusLevel + FrequencyLevel + DamageLevel + VolleyLevel * 2 + (SovereigntyUnlocked ? 30 : 0);
42 public IReadOnlyList<DeathAltarUpgradeType> AltarUpgradeTypes => UpgradeTypes;
43
44 protected override bool CloneNewInstances => true;
45
46 public override void SetDefaults()
47 {
48 Item.width = 30;
49 Item.height = 38;
50 Item.accessory = true;
51 Item.value = Item.sellPrice(gold: 8);
52 Item.rare = ItemRarityID.LightRed;
53 }
54
55 public override void UpdateAccessory(Player player, bool hideVisual)
56 {
57 player.GetModPlayer<MyPlayer>().SetDeathNecklace(this);
58 if (!Main.dedServ && SovereigntyUnlocked && Main.rand.NextBool(10))
59 {
60 Dust soul = Dust.NewDustPerfect(
61 player.Center + Main.rand.NextVector2Circular(24f, 34f),
62 DustID.DungeonSpirit,
63 -player.velocity * 0.04f,
64 70,
65 new Color(105, 225, 245),
66 0.65f);
67 soul.noGravity = true;
68 }
69 }
70
71 public override void ModifyTooltips(List<TooltipLine> tooltips)
72 {
73 string key = SovereigntyUnlocked ? "Mods.DeathMod.UI.Enabled" : "Mods.DeathMod.UI.Disabled";
74 tooltips.Add(new TooltipLine(Mod, "DeathDomainStats", Language.GetTextValue(
75 "Mods.DeathMod.UI.NecklaceStats",
76 DomainRadius / 16f,
77 SpawnInterval / 60f,
78 DamageFactor * 100f,
79 VolleyCount,
80 Language.GetTextValue(key))) { OverrideColor = new Color(110, 220, 240) });
81 tooltips.Add(new TooltipLine(Mod, "DeathDomainToggle", Language.GetTextValue("Mods.DeathMod.UI.DeathDomainToggleHint")));
82 tooltips.Add(new TooltipLine(Mod, "DeathModAltarHint", Language.GetTextValue("Mods.DeathMod.UI.AltarHint")));
83 }
84
85 public DeathAltarPrice GetUpgradePrice(DeathAltarUpgradeType type)
86 {
87 return type switch
88 {
89 DeathAltarUpgradeType.NecklaceRadius => RadiusLevel >= MaxCoreLevel ? DeathAltarPrice.Unavailable : DeathAltarPrice.Essence((RadiusLevel + 1) / 2),
90 DeathAltarUpgradeType.NecklaceFrequency => FrequencyLevel >= MaxCoreLevel ? DeathAltarPrice.Unavailable : DeathAltarPrice.Essence((FrequencyLevel + 1) / 2),
91 DeathAltarUpgradeType.NecklaceDamage => DamageLevel >= MaxCoreLevel ? DeathAltarPrice.Unavailable : DeathAltarPrice.Essence(DamageLevel),
92 DeathAltarUpgradeType.NecklaceVolley => VolleyLevel >= MaxVolleyLevel ? DeathAltarPrice.Unavailable : DeathAltarPrice.Essence(VolleyLevel * 3),
93 DeathAltarUpgradeType.NecklaceSovereignty => SovereigntyUnlocked || !AllCoreStatsMaxed ? DeathAltarPrice.Unavailable : DeathAltarPrice.Essence(30),
94 _ => DeathAltarPrice.Unavailable
95 };
96 }
97
98 public bool ApplyAltarUpgrade(Item item, DeathAltarUpgradeType type)
99 {
100 if (!GetUpgradePrice(type).IsAvailable)
101 return false;
102 switch (type)
103 {
104 case DeathAltarUpgradeType.NecklaceRadius:
105 RadiusLevel++;
106 break;
107 case DeathAltarUpgradeType.NecklaceFrequency:
108 FrequencyLevel++;
109 break;
110 case DeathAltarUpgradeType.NecklaceDamage:
111 DamageLevel++;
112 break;
113 case DeathAltarUpgradeType.NecklaceVolley:
114 VolleyLevel++;
115 break;
116 case DeathAltarUpgradeType.NecklaceSovereignty:
117 SovereigntyUnlocked = true;
118 break;
119 default:
120 return false;
121 }
122 return true;
123 }
124
125 public string GetCurrentUpgradeValue(DeathAltarUpgradeType type) => type switch
126 {
127 DeathAltarUpgradeType.NecklaceRadius => $"{DomainRadius / 16f:0.#} tiles",
128 DeathAltarUpgradeType.NecklaceFrequency => $"{SpawnInterval / 60f:0.##}s",
129 DeathAltarUpgradeType.NecklaceDamage => $"{DamageFactor:P0}",
130 DeathAltarUpgradeType.NecklaceVolley => $"×{VolleyCount}",
131 DeathAltarUpgradeType.NecklaceSovereignty => ToggleValue(SovereigntyUnlocked),
132 _ => string.Empty
133 };
134
135 public string GetNextUpgradeValue(DeathAltarUpgradeType type) => type switch
136 {
137 DeathAltarUpgradeType.NecklaceRadius => $"{(DomainRadius + 32f) / 16f:0.#} tiles",
138 DeathAltarUpgradeType.NecklaceFrequency => $"{Math.Max(34, SpawnInterval - 7) / 60f:0.##}s",
139 DeathAltarUpgradeType.NecklaceDamage => $"{DamageFactor + 0.055f:P0}",
140 DeathAltarUpgradeType.NecklaceVolley => $"×{VolleyCount + 1}",
141 DeathAltarUpgradeType.NecklaceSovereignty => Language.GetTextValue("Mods.DeathMod.UI.FullScreenRequiem"),
142 _ => string.Empty
143 };
144
145 public DeathAltarUnavailableReason GetUnavailableReason(DeathAltarUpgradeType type)
146 {
147 if (type == DeathAltarUpgradeType.NecklaceSovereignty && !SovereigntyUnlocked && !AllCoreStatsMaxed)
148 return DeathAltarUnavailableReason.RequiresNecklaceMastery;
149 return DeathAltarUnavailableReason.Maxed;
150 }
151
152 public override void SaveData(TagCompound tag)
153 {
154 tag[nameof(RadiusLevel)] = RadiusLevel;
155 tag[nameof(FrequencyLevel)] = FrequencyLevel;
156 tag[nameof(DamageLevel)] = DamageLevel;
157 tag[nameof(VolleyLevel)] = VolleyLevel;
158 tag[nameof(SovereigntyUnlocked)] = SovereigntyUnlocked;
159 }
160
161 public override void LoadData(TagCompound tag)
162 {
163 RadiusLevel = Math.Clamp(tag.GetInt(nameof(RadiusLevel)), 1, MaxCoreLevel);
164 FrequencyLevel = Math.Clamp(tag.GetInt(nameof(FrequencyLevel)), 1, MaxCoreLevel);
165 DamageLevel = Math.Clamp(tag.GetInt(nameof(DamageLevel)), 1, MaxCoreLevel);
166 VolleyLevel = Math.Clamp(tag.GetInt(nameof(VolleyLevel)), 1, MaxVolleyLevel);
167 SovereigntyUnlocked = tag.GetBool(nameof(SovereigntyUnlocked));
168 }
169
170 public override void NetSend(BinaryWriter writer)
171 {
172 writer.Write((byte)RadiusLevel);
173 writer.Write((byte)FrequencyLevel);
174 writer.Write((byte)DamageLevel);
175 writer.Write((byte)VolleyLevel);
176 writer.Write(SovereigntyUnlocked);
177 }
178
179 public override void NetReceive(BinaryReader reader)
180 {
181 RadiusLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxCoreLevel);
182 FrequencyLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxCoreLevel);
183 DamageLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxCoreLevel);
184 VolleyLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxVolleyLevel);
185 SovereigntyUnlocked = reader.ReadBoolean();
186 }
187
188 public override void AddRecipes()
189 {
190 CreateRecipe()
191 .AddIngredient(ItemID.PanicNecklace)
192 .AddIngredient(ItemID.ObsidianRose)
193 .AddIngredient<Soul>(3)
194 .AddTile(ModContent.TileType<DeathAltarTile>())
195 .Register();
196 }
197
198 private bool AllCoreStatsMaxed => RadiusLevel >= MaxCoreLevel
199 && FrequencyLevel >= MaxCoreLevel
200 && DamageLevel >= MaxCoreLevel
201 && VolleyLevel >= MaxVolleyLevel;
202
203 private static string ToggleValue(bool enabled) => Language.GetTextValue(
204 enabled ? "Mods.DeathMod.UI.Enabled" : "Mods.DeathMod.UI.Disabled");
205 }
Added Items/DeathNecklace.png +0 -0
二进制文件已变更,无法进行逐行预览。
Modified Items/DeathRobe.cs +39 -2
@@ -1,10 +1,13 @@
1 1 using DeathMod.Common;
2 2 using DeathMod.Tiles;
3 3 using Microsoft.Xna.Framework;
4 using Microsoft.Xna.Framework.Graphics;
4 5 using System;
5 6 using System.Collections.Generic;
6 7 using System.IO;
8 using System.Reflection;
7 9 using Terraria;
10 using Terraria.GameContent;
8 11 using Terraria.ID;
9 12 using Terraria.Localization;
10 13 using Terraria.ModLoader;
@@ -32,6 +35,7 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
32 35 DeathAltarUpgradeType.RobeKnockbackImmunity,
33 36 DeathAltarUpgradeType.RobeFallDamageImmunity,
34 37 DeathAltarUpgradeType.RobeZeroMana,
38 DeathAltarUpgradeType.RobeLavaImmunity,
35 39 DeathAltarUpgradeType.RobeImmuneOnFire,
36 40 DeathAltarUpgradeType.RobeImmunePoisoned,
37 41 DeathAltarUpgradeType.RobeImmuneConfused,
@@ -59,6 +63,7 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
59 63 public bool KnockbackImmunityUnlocked { get; private set; }
60 64 public bool FallDamageImmunityUnlocked { get; private set; }
61 65 public bool ZeroManaUnlocked { get; private set; }
66 public bool LavaImmunityUnlocked { get; private set; }
62 67 public ushort DebuffImmunityMask { get; private set; }
63 68
64 69 public int DefenseValue => DefenseByLevel[DefenseLevel - 1];
@@ -74,6 +79,23 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
74 79 public override void SetStaticDefaults()
75 80 {
76 81 ArmorIDs.Body.Sets.HidesTopSkin[Item.bodySlot] = true;
82
83 // These sprites came directly from the 1.3 mod. A 1.4 body equip normally treats
84 // DeathRobe_Body as a 360x224 composite sheet, but this asset is the legacy 40x1120
85 // format with separate arm frames. Keep the original framing and bind each legacy
86 // layer explicitly so the robe uses the same player-relative positions as before.
87 FieldInfo? framingField = typeof(ArmorIDs.Body.Sets).GetField(
88 "UsesNewFramingCode",
89 BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
90 if (framingField?.GetValue(null) is bool[] usesNewFramingCode)
91 usesNewFramingCode[Item.bodySlot] = false;
92 if (!Main.dedServ)
93 {
94 var bodyTexture = ModContent.Request<Texture2D>($"{Texture}_{EquipType.Body}");
95 TextureAssets.ArmorBody[Item.bodySlot] = bodyTexture;
96 TextureAssets.FemaleBody[Item.bodySlot] = bodyTexture;
97 TextureAssets.ArmorArm[Item.bodySlot] = ModContent.Request<Texture2D>($"{Texture}_Arms");
98 }
77 99 }
78 100
79 101 public override void SetDefaults()
@@ -110,6 +132,11 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
110 132 player.noFallDmg |= FallDamageImmunityUnlocked;
111 133 if (ZeroManaUnlocked)
112 134 player.manaCost = 0f;
135 if (LavaImmunityUnlocked)
136 {
137 player.lavaImmune = true;
138 player.fireWalk = true;
139 }
113 140
114 141 foreach (DeathAltarUpgradeType type in UpgradeTypes)
115 142 {
@@ -132,7 +159,8 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
132 159 MaxLifeBonus,
133 160 LifeRegenBonus,
134 161 CountDebuffImmunities(),
135 15);
162 15,
163 ToggleValue(LavaImmunityUnlocked));
136 164 tooltips.Add(new TooltipLine(Mod, "DeathRobeStats", stats) { OverrideColor = new Color(170, 95, 230) });
137 165 tooltips.Add(new TooltipLine(Mod, "DeathModAltarHint", Language.GetTextValue("Mods.DeathMod.UI.AltarHint")));
138 166 }
@@ -154,6 +182,7 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
154 182 DeathAltarUpgradeType.RobeKnockbackImmunity => KnockbackImmunityUnlocked ? DeathAltarPrice.Unavailable : DeathAltarPrice.Essence(8),
155 183 DeathAltarUpgradeType.RobeFallDamageImmunity => FallDamageImmunityUnlocked ? DeathAltarPrice.Unavailable : DeathAltarPrice.Essence(5),
156 184 DeathAltarUpgradeType.RobeZeroMana => ZeroManaUnlocked ? DeathAltarPrice.Unavailable : DeathAltarPrice.Essence(15),
185 DeathAltarUpgradeType.RobeLavaImmunity => LavaImmunityUnlocked ? DeathAltarPrice.Unavailable : DeathAltarPrice.Essence(8),
157 186 _ => DeathAltarPrice.Unavailable
158 187 };
159 188 }
@@ -202,6 +231,9 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
202 231 case DeathAltarUpgradeType.RobeZeroMana:
203 232 ZeroManaUnlocked = true;
204 233 break;
234 case DeathAltarUpgradeType.RobeLavaImmunity:
235 LavaImmunityUnlocked = true;
236 break;
205 237 default:
206 238 return false;
207 239 }
@@ -225,6 +257,7 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
225 257 DeathAltarUpgradeType.RobeKnockbackImmunity => ToggleValue(KnockbackImmunityUnlocked),
226 258 DeathAltarUpgradeType.RobeFallDamageImmunity => ToggleValue(FallDamageImmunityUnlocked),
227 259 DeathAltarUpgradeType.RobeZeroMana => ToggleValue(ZeroManaUnlocked),
260 DeathAltarUpgradeType.RobeLavaImmunity => ToggleValue(LavaImmunityUnlocked),
228 261 _ => string.Empty
229 262 };
230 263 }
@@ -243,7 +276,7 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
243 276 DeathAltarUpgradeType.RobeMinions => MinionsByLevel[Math.Min(MinionLevel + 1, MaxRobeLevel)].ToString(),
244 277 DeathAltarUpgradeType.RobeTurrets => TurretsByLevel[Math.Min(TurretLevel + 1, MaxRobeLevel)].ToString(),
245 278 DeathAltarUpgradeType.RobePlacementRange => $"+{(PlacementRangeLevel + 1) * 10}",
246 DeathAltarUpgradeType.RobeKnockbackImmunity or DeathAltarUpgradeType.RobeFallDamageImmunity or DeathAltarUpgradeType.RobeZeroMana
279 DeathAltarUpgradeType.RobeKnockbackImmunity or DeathAltarUpgradeType.RobeFallDamageImmunity or DeathAltarUpgradeType.RobeZeroMana or DeathAltarUpgradeType.RobeLavaImmunity
247 280 => Language.GetTextValue("Mods.DeathMod.UI.Enabled"),
248 281 _ => string.Empty
249 282 };
@@ -266,6 +299,7 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
266 299 tag[nameof(KnockbackImmunityUnlocked)] = KnockbackImmunityUnlocked;
267 300 tag[nameof(FallDamageImmunityUnlocked)] = FallDamageImmunityUnlocked;
268 301 tag[nameof(ZeroManaUnlocked)] = ZeroManaUnlocked;
302 tag[nameof(LavaImmunityUnlocked)] = LavaImmunityUnlocked;
269 303 tag[nameof(DebuffImmunityMask)] = (int)DebuffImmunityMask;
270 304 }
271 305
@@ -281,6 +315,7 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
281 315 KnockbackImmunityUnlocked = tag.GetBool(nameof(KnockbackImmunityUnlocked));
282 316 FallDamageImmunityUnlocked = tag.GetBool(nameof(FallDamageImmunityUnlocked));
283 317 ZeroManaUnlocked = tag.GetBool(nameof(ZeroManaUnlocked));
318 LavaImmunityUnlocked = tag.GetBool(nameof(LavaImmunityUnlocked));
284 319 DebuffImmunityMask = (ushort)Math.Clamp(tag.GetInt(nameof(DebuffImmunityMask)), 0, ushort.MaxValue);
285 320 ApplyDynamicStats();
286 321 }
@@ -297,6 +332,7 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
297 332 writer.Write(KnockbackImmunityUnlocked);
298 333 writer.Write(FallDamageImmunityUnlocked);
299 334 writer.Write(ZeroManaUnlocked);
335 writer.Write(LavaImmunityUnlocked);
300 336 writer.Write(DebuffImmunityMask);
301 337 }
302 338
@@ -312,6 +348,7 @@ public class DeathRobe : ModItem, IDeathAltarUpgradeable
312 348 KnockbackImmunityUnlocked = reader.ReadBoolean();
313 349 FallDamageImmunityUnlocked = reader.ReadBoolean();
314 350 ZeroManaUnlocked = reader.ReadBoolean();
351 LavaImmunityUnlocked = reader.ReadBoolean();
315 352 DebuffImmunityMask = reader.ReadUInt16();
316 353 ApplyDynamicStats();
317 354 }
Modified Items/DeathWings.cs +186 -12
Modified Items/NormalSickle.cs +0 -1
@@ -244,7 +244,6 @@ public class NormalSickle : ModItem, IDeathAltarUpgradeable
244 244
245 245 player.GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
246 246 LifeStealVisuals.Spawn(target.Center, player.whoAmI, LifeStealLevel);
247 DeathMod.SendSickleItemHit(target);
248 247 return;
249 248 }
250 249
Modified Localization/en-US.hjson +32 -6
@@ -1,6 +1,11 @@
1 1 Mods: {
2 2 DeathMod: {
3 3 Items: {
4 DeathNecklace: {
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
7 }
8
4 9 NormalSickle: {
5 10 DisplayName: Harvest Sickle
6 11 Tooltip: Left-click for a fast arc slash; hits apply Death Mark, and only DeathMod sickles can harvest souls
@@ -75,7 +80,7 @@ Mods: {
75 80 '''
76 81 Condensed from 100 souls
77 82 Left-click the Soul Jar to absorb it and restore 100 souls
78 Used to modify sickles, the robe and wings at the Death Altar
83 Used to modify sickles, the robe, wings and Death Necklace at the Death Altar
79 84 '''
80 85 }
81 86
@@ -105,6 +110,7 @@ Mods: {
105 110 Tiles.DeathAltarTile.MapEntry: Death Altar
106 111
107 112 Projectiles: {
113 DomainSoulBladeProjectile.DisplayName: Death Domain Echo
108 114 BoneWispProjectile.DisplayName: Bone Wisp
109 115 BloodCrescentProjectile.DisplayName: Blood Covenant Crescent
110 116 InfernalCrescentProjectile.DisplayName: Infernal Crescent
@@ -142,7 +148,10 @@ Mods: {
142 148 }
143 149
144 150 WingStats: Flight {0}f | Speed {1} | Acceleration {2} | Vertical power {3} | Infinite flight: {4}
145 RobeStats: Defense {0} | Life +{1} | Life regeneration +{2} | Debuff immunities {3}/{4}
151 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 DeathDomainToggleHint: Press the Death Domain keybind to enable or disable the field
154 FullScreenRequiem: Full screen + Death Requiem
146 155 AltarHint: Right-click a Death Altar to open its modification interface
147 156 SoulCount: Souls: {0}
148 157 SoulJarHint:
@@ -150,9 +159,14 @@ Mods: {
150 159 Left-click to absorb 1 Soul Essence (+{0} souls)
151 160 Right-click to extract 1 Soul Essence (-{0} souls)
152 161 '''
153 AltarTitle: Death Altar · Equipment Modification
154 HoldSickle: Hold the equipment you want to modify
155 HoldUpgradeableItem: Hold an upgradeable DeathMod sickle, Death Robe or Wings of Death
162 AltarTitle: Death Altar · Soul Skill Tree
163 SelectEquipment: Available equipment
164 NoUpgradeableItems: No upgradeable equipment in inventory or equipment slots
165 InventoryLocation: Inventory
166 EquipmentLocation: Equipped
167 TreePage: Skill tree {0}/{1}
168 HoldSickle: Select equipment from the altar list
169 HoldUpgradeableItem: Put an upgradeable item in your inventory or equipment slots
156 170 CurrentSickle: Current sickle: {0}
157 171 CurrentItem: Current equipment: {0}
158 172 AltarResources: Soul Essence: {0} | Character souls: {1}
@@ -183,6 +197,7 @@ Mods: {
183 197 RequiresProjectile: The current form has no projectile
184 198 RequiresWingMastery: Max all four flight attributes first
185 199 RequiresFatedUnlock: Unlock Fated first
200 RequiresNecklaceMastery: Max all four domain attributes first
186 201 }
187 202
188 203 UpgradeNames: {
@@ -214,6 +229,7 @@ Mods: {
214 229 RobeKnockbackImmunity: Knockback immunity
215 230 RobeFallDamageImmunity: Fall-damage immunity
216 231 RobeZeroMana: Zero mana cost
232 RobeLavaImmunity: Lava immunity
217 233 RobeImmuneOnFire: Immunity: On Fire!
218 234 RobeImmunePoisoned: Immunity: Poisoned
219 235 RobeImmuneConfused: Immunity: Confused
@@ -229,22 +245,32 @@ Mods: {
229 245 RobeImmuneWet: Immunity: Wet
230 246 RobeImmuneBrokenArmor: Immunity: Broken Armor
231 247 RobeImmuneSuffocation: Immunity: Suffocation
248 NecklaceRadius: Domain radius
249 NecklaceFrequency: Manifest frequency
250 NecklaceDamage: Domain damage
251 NecklaceVolley: Echo volley
252 NecklaceSovereignty: Deathly Sovereignty
232 253 }
233 254
234 255 UpgradeButtons.Generic: "{0} {1} → {2} | Costs {3}"
235 256 }
236 257
237 258 Messages: {
259 DeathDomainEnabled: Death Domain unfolded.
260 DeathDomainDisabled: Death Domain withdrawn.
261 DeathNecklaceRequired: Equip the Death Necklace before toggling its domain.
238 262 EssenceExtracted: Spent 100 souls and extracted 1 Soul Essence.
239 263 EssenceAbsorbed: Absorbed 1 Soul Essence and gained 100 souls.
240 264 NotEnoughSouls: Not enough souls: extracting Soul Essence requires 100 souls.
241 265 UpgradeSucceeded: Equipment modification complete.
242 266 NotEnoughEssence: Not enough Soul Essence.
243 267 NotEnoughUpgradeSouls: Not enough character souls for this low-tier infusion.
244 HoldSickle: Hold an item that can be modified at the Death Altar first.
268 HoldSickle: The selected altar item is no longer in that slot; select it again.
245 269 TooFarFromAltar: You are too far from the Death Altar, or it has been removed.
246 270 UpgradeMaxed: This modification is unavailable or already at maximum level.
247 271 BossSoulReward: Boss soul harvested: gained {0} souls.
248 272 }
273
274 Keybinds.ToggleDeathDomain.DisplayName: Toggle Death Domain
249 275 }
250 276 }
Modified Localization/zh-Hans.hjson +32 -6
Added Projectiles/DomainSoulBladeProjectile.cs +92 -0
Modified README.md +7 -3
Modified README.zh-Hans.md +7 -3
Modified UI/SoulJarUISystem.cs +13 -1
Modified docs/PLAY_GUIDE.md +10 -3
Modified docs/PLAY_GUIDE.zh-Hans.md +10 -3