返回提交历史
Modified
Common/DeathAltarUpgrade.cs
+39
-2
Added
Common/DeathDomainVisualSystem.cs
+114
-0
Added
Common/DeathWingTrailSystem.cs
+101
-0
Modified
Common/MyGlobalNPC.cs
+9
-5
Modified
Common/MyGlobalProjectile.cs
+42
-1
Modified
Common/MyPlayer.cs
+244
-1
Modified
Common/SickleUpgradeService.cs
+23
-6
Modified
DeathMod.cs
+105
-3
Added
Items/DeathNecklace.cs
+205
-0
Added
Items/DeathNecklace.png
+0
-0
Modified
Items/DeathRobe.cs
+39
-2
Modified
Items/DeathWings.cs
+186
-12
Modified
Items/NormalSickle.cs
+0
-1
Modified
Localization/en-US.hjson
+32
-6
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/DeathAltarUISystem.cs
+325
-245
Modified
UI/SoulJarUISystem.cs
+13
-1
Modified
docs/PLAY_GUIDE.md
+10
-3
Modified
docs/PLAY_GUIDE.zh-Hans.md
+10
-3
XFEstudio/DeathMod
引入死亡项链及领域系统,重构祭坛UI
本次更新新增死亡项链装备及其领域技能树分支,支持在祭坛界面直接从背包和装备栏选择可改造物品。重构祭坛UI,优化技能树展示与多物品选择,完善多语言本地化。新增死亡领域弹幕、全屏主权领域、死亡之翼拖尾与多种视觉特效。扩展死神长袍新节点,补充相关文档。底层与网络协议增强多人环境下的同步与校验,提升交互体验与稳定性。
eb3f73a
代码差异
22 个文件
+1635
-303
@@ -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)
@@ -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
}
@@ -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
}
@@ -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)
@@ -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
@@ -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)
@@ -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
}
@@ -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)
@@ -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
}
二进制文件已变更,无法进行逐行预览。
@@ -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
}
@@ -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
@@ -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
}