返回提交历史
Modified
Common/MyGlobalProjectile.cs
+10
-0
Modified
Common/MyPlayer.cs
+10
-0
Modified
Common/SickleUpgradeService.cs
+16
-4
Modified
Common/SickleUpgradeType.cs
+17
-1
Modified
DeathMod.cs
+1
-0
Modified
Items/DeathAltarItem.cs
+0
-2
Added
Items/DeathAltarItem.png
+0
-0
Modified
Items/NormalSickle.cs
+94
-12
Modified
Localization/en-US.hjson
+18
-6
Modified
Localization/zh-Hans.hjson
+18
-6
Modified
README.md
+5
-1
Modified
UI/DeathAltarUISystem.cs
+60
-27
Modified
docs/PLAY_GUIDE.md
+32
-42
XFEstudio/DeathMod
feat: expand Death Altar upgrades
f6c64ad
代码差异
13 个文件
+281
-101
@@ -14,6 +14,7 @@ public class MyGlobalProjectile : GlobalProjectile
14
14
public bool IsSickleProjectile { get; private set; }
15
15
public int LifeStealLevel { get; private set; }
16
16
public int ArmorPenetrationLevel { get; private set; }
17
public int HitCooldownFrames { get; private set; }
17
18
18
19
public override void OnSpawn(Projectile projectile, IEntitySource source)
19
20
{
@@ -23,7 +24,11 @@ public class MyGlobalProjectile : GlobalProjectile
23
24
IsSickleProjectile = true;
24
25
LifeStealLevel = sickle.LifeStealLevel;
25
26
ArmorPenetrationLevel = sickle.ArmorPenetrationLevel;
27
HitCooldownFrames = sickle.HitCooldownFrames;
26
28
projectile.ArmorPenetration = ArmorPenetrationLevel;
29
projectile.usesIDStaticNPCImmunity = false;
30
projectile.usesLocalNPCImmunity = true;
31
projectile.localNPCHitCooldown = HitCooldownFrames;
27
32
}
28
33
29
34
public override void SendExtraAI(Projectile projectile, BitWriter bitWriter, BinaryWriter binaryWriter)
@@ -34,6 +39,7 @@ public class MyGlobalProjectile : GlobalProjectile
34
39
35
40
binaryWriter.Write((byte)LifeStealLevel);
36
41
binaryWriter.Write((byte)ArmorPenetrationLevel);
42
binaryWriter.Write((byte)HitCooldownFrames);
37
43
}
38
44
39
45
public override void ReceiveExtraAI(Projectile projectile, BitReader bitReader, BinaryReader binaryReader)
@@ -44,7 +50,11 @@ public class MyGlobalProjectile : GlobalProjectile
44
50
45
51
LifeStealLevel = binaryReader.ReadByte();
46
52
ArmorPenetrationLevel = binaryReader.ReadByte();
53
HitCooldownFrames = binaryReader.ReadByte();
47
54
projectile.ArmorPenetration = ArmorPenetrationLevel;
55
projectile.usesIDStaticNPCImmunity = false;
56
projectile.usesLocalNPCImmunity = true;
57
projectile.localNPCHitCooldown = HitCooldownFrames;
48
58
}
49
59
50
60
public override void OnHitNPC(Projectile projectile, NPC target, NPC.HitInfo hit, int damageDone)
@@ -59,6 +59,16 @@ public class MyPlayer : ModPlayer
59
59
Souls = Math.Max(0, souls);
60
60
}
61
61
62
internal bool TrySpendSouls(int amount)
63
{
64
if (amount < 0 || Souls < amount || Main.netMode == NetmodeID.MultiplayerClient)
65
return false;
66
67
Souls -= amount;
68
SyncSoulBalance();
69
return true;
70
}
71
62
72
public void AddSouls(int amount, bool announce = false)
63
73
{
64
74
if (amount <= 0 || Main.netMode == NetmodeID.MultiplayerClient)
@@ -45,15 +45,22 @@ internal static class SickleUpgradeService
45
45
return false;
46
46
}
47
47
48
int cost = sickle.GetUpgradeCost(upgradeType);
49
if (cost < 0)
48
SickleUpgradePrice price = sickle.GetUpgradePrice(upgradeType);
49
if (!price.IsAvailable)
50
50
{
51
51
DeathMod.SendOperationResult(player, DeathMod.OperationResult.UpgradeMaxed);
52
52
return false;
53
53
}
54
54
55
MyPlayer soulPlayer = player.GetModPlayer<MyPlayer>();
55
56
int essenceType = ModContent.ItemType<Soul>();
56
if (player.CountItem(essenceType, cost) < cost)
57
if (price.Currency == SickleUpgradeCurrency.Souls && soulPlayer.Souls < price.Amount)
58
{
59
DeathMod.SendOperationResult(player, DeathMod.OperationResult.NotEnoughUpgradeSouls);
60
return false;
61
}
62
63
if (price.Currency == SickleUpgradeCurrency.SoulEssence && player.CountItem(essenceType, price.Amount) < price.Amount)
57
64
{
58
65
DeathMod.SendOperationResult(player, DeathMod.OperationResult.NotEnoughEssence);
59
66
return false;
@@ -69,7 +76,12 @@ internal static class SickleUpgradeService
69
76
return false;
70
77
}
71
78
72
List<int> changedEssenceSlots = ConsumeEssence(player, essenceType, cost);
79
List<int> changedEssenceSlots = price.Currency == SickleUpgradeCurrency.SoulEssence
80
? ConsumeEssence(player, essenceType, price.Amount)
81
: [];
82
if (price.Currency == SickleUpgradeCurrency.Souls && !soulPlayer.TrySpendSouls(price.Amount))
83
return false;
84
73
85
SyncInventorySlot(player, selectedSlot);
74
86
foreach (int slot in changedEssenceSlots)
75
87
SyncInventorySlot(player, slot);
@@ -6,5 +6,21 @@ public enum SickleUpgradeType : byte
6
6
Damage,
7
7
ProjectileCount,
8
8
ArmorPenetration,
9
LifeSteal
9
LifeSteal,
10
AttackSpeed,
11
Knockback,
12
HitCooldown
13
}
14
15
public enum SickleUpgradeCurrency : byte
16
{
17
Souls,
18
SoulEssence
19
}
20
21
public readonly record struct SickleUpgradePrice(int Amount, SickleUpgradeCurrency Currency)
22
{
23
public bool IsAvailable => Amount >= 0;
24
25
public static SickleUpgradePrice Unavailable => new(-1, SickleUpgradeCurrency.SoulEssence);
10
26
}
@@ -24,6 +24,7 @@ public class DeathMod : Mod
24
24
NotEnoughSouls,
25
25
UpgradeSucceeded,
26
26
NotEnoughEssence,
27
NotEnoughUpgradeSouls,
27
28
HoldSickle,
28
29
TooFarFromAltar,
29
30
UpgradeMaxed
@@ -7,8 +7,6 @@ namespace DeathMod.Items;
7
7
8
8
public class DeathAltarItem : ModItem
9
9
{
10
public override string Texture => "DeathMod/Tiles/DeathAltarTile";
11
12
10
public override void SetDefaults()
13
11
{
14
12
Item.DefaultToPlaceableTile(ModContent.TileType<DeathAltarTile>());
二进制文件已变更,无法进行逐行预览。
@@ -14,19 +14,32 @@ namespace DeathMod.Items;
14
14
15
15
public class NormalSickle : ModItem
16
16
{
17
public const int BaseUseTime = 25;
18
public const float BaseKnockback = 4f;
19
public const int BaseHitCooldownFrames = 15;
17
20
public const int MaxDamageLevel = 25;
18
21
public const int MaxProjectileCount = 7;
19
22
public const int MaxArmorPenetrationLevel = 20;
20
23
public const int MaxLifeStealLevel = 5;
24
public const int MaxAttackSpeedLevel = 10;
25
public const int MaxKnockbackLevel = 10;
26
public const int MaxHitCooldownLevel = 10;
21
27
22
28
public int DamageLevel { get; internal set; } = 1;
23
29
public int ProjectileCount { get; internal set; } = 1;
24
30
public int ArmorPenetrationLevel { get; internal set; } = 1;
25
31
public int LifeStealLevel { get; internal set; }
32
public int AttackSpeedLevel { get; internal set; } = 1;
33
public int KnockbackLevel { get; internal set; } = 1;
34
public int HitCooldownLevel { get; internal set; } = 1;
26
35
27
36
public virtual int SickleTier => 0;
28
37
public virtual bool SupportsProjectileCount => Item.shoot > ProjectileID.None;
29
38
public int DamageBonus => (DamageLevel - 1) * 2;
39
public int AttackUseTime => BaseUseTime - (AttackSpeedLevel - 1);
40
public float KnockbackBonus => (KnockbackLevel - 1) * 0.5f;
41
public float TotalBaseKnockback => BaseKnockback + KnockbackBonus;
42
public int HitCooldownFrames => BaseHitCooldownFrames - (HitCooldownLevel - 1);
30
43
31
44
protected override bool CloneNewInstances => true;
32
45
@@ -36,12 +49,12 @@ public class NormalSickle : ModItem
36
49
Item.DamageType = DamageClass.Melee;
37
50
Item.width = 100;
38
51
Item.height = 169;
39
Item.useTime = 25;
40
Item.useAnimation = 25;
52
Item.useTime = BaseUseTime;
53
Item.useAnimation = BaseUseTime;
41
54
Item.useStyle = ItemUseStyleID.Swing;
42
55
Item.crit = 0;
43
56
Item.scale = 0.65f;
44
Item.knockBack = 4f;
57
Item.knockBack = BaseKnockback;
45
58
Item.rare = ItemRarityID.White;
46
59
Item.UseSound = SoundID.Item1;
47
60
Item.autoReuse = true;
@@ -64,6 +77,16 @@ public class NormalSickle : ModItem
64
77
damage.Flat += DamageBonus;
65
78
}
66
79
80
public override float UseSpeedMultiplier(Player player)
81
{
82
return BaseUseTime / (float)AttackUseTime;
83
}
84
85
public override void ModifyWeaponKnockback(Player player, ref StatModifier knockback)
86
{
87
knockback.Flat += KnockbackBonus;
88
}
89
67
90
public override bool Shoot(Player player, EntitySource_ItemUse_WithAmmo source, Vector2 position, Vector2 velocity, int type, int damage, float knockback)
68
91
{
69
92
if (!SupportsProjectileCount || type <= ProjectileID.None)
@@ -94,7 +117,10 @@ public class NormalSickle : ModItem
94
117
DamageBonus,
95
118
projectileText,
96
119
ArmorPenetrationLevel,
97
LifeStealLevel);
120
LifeStealLevel,
121
AttackUseTime,
122
TotalBaseKnockback.ToString("0.0"),
123
HitCooldownFrames);
98
124
99
125
tooltips.Add(new TooltipLine(Mod, "DeathModUpgradeStats", stats)
100
126
{
@@ -106,22 +132,41 @@ public class NormalSickle : ModItem
106
132
});
107
133
}
108
134
109
public int GetUpgradeCost(SickleUpgradeType upgradeType)
135
public SickleUpgradePrice GetUpgradePrice(SickleUpgradeType upgradeType)
110
136
{
111
137
return upgradeType switch
112
138
{
113
SickleUpgradeType.Advancement => GetNextSickleType() == ItemID.None ? -1 : 1 << SickleTier,
114
SickleUpgradeType.Damage => DamageLevel >= MaxDamageLevel ? -1 : 1 + DamageLevel * DamageLevel / 4,
115
SickleUpgradeType.ProjectileCount => !SupportsProjectileCount || ProjectileCount >= MaxProjectileCount ? -1 : 1 << ProjectileCount,
116
SickleUpgradeType.ArmorPenetration => ArmorPenetrationLevel >= MaxArmorPenetrationLevel ? -1 : 1 + ArmorPenetrationLevel / 2,
117
SickleUpgradeType.LifeSteal => LifeStealLevel >= MaxLifeStealLevel ? -1 : (LifeStealLevel + 1) * 3,
118
_ => -1
139
SickleUpgradeType.Advancement => GetNextSickleType() == ItemID.None
140
? SickleUpgradePrice.Unavailable
141
: EssencePrice(1 << SickleTier),
142
SickleUpgradeType.Damage => DamageLevel >= MaxDamageLevel
143
? SickleUpgradePrice.Unavailable
144
: DamageLevel < 6 ? SoulPrice(DamageLevel * 20) : EssencePrice(1 + DamageLevel * DamageLevel / 4),
145
SickleUpgradeType.ProjectileCount => !SupportsProjectileCount || ProjectileCount >= MaxProjectileCount
146
? SickleUpgradePrice.Unavailable
147
: ProjectileCount < 3 ? SoulPrice(ProjectileCount * 50) : EssencePrice(1 << ProjectileCount),
148
SickleUpgradeType.ArmorPenetration => ArmorPenetrationLevel >= MaxArmorPenetrationLevel
149
? SickleUpgradePrice.Unavailable
150
: ArmorPenetrationLevel < 6 ? SoulPrice(ArmorPenetrationLevel * 15) : EssencePrice(1 + ArmorPenetrationLevel / 2),
151
SickleUpgradeType.LifeSteal => LifeStealLevel >= MaxLifeStealLevel
152
? SickleUpgradePrice.Unavailable
153
: LifeStealLevel < 2 ? SoulPrice((LifeStealLevel + 1) * 75) : EssencePrice((LifeStealLevel + 1) * 3),
154
SickleUpgradeType.AttackSpeed => AttackSpeedLevel >= MaxAttackSpeedLevel
155
? SickleUpgradePrice.Unavailable
156
: AttackSpeedLevel < 6 ? SoulPrice(AttackSpeedLevel * 25) : EssencePrice(1 + AttackSpeedLevel / 2),
157
SickleUpgradeType.Knockback => KnockbackLevel >= MaxKnockbackLevel
158
? SickleUpgradePrice.Unavailable
159
: KnockbackLevel < 6 ? SoulPrice(KnockbackLevel * 15) : EssencePrice(1 + KnockbackLevel / 3),
160
SickleUpgradeType.HitCooldown => !SupportsProjectileCount || HitCooldownLevel >= MaxHitCooldownLevel
161
? SickleUpgradePrice.Unavailable
162
: HitCooldownLevel < 6 ? SoulPrice(HitCooldownLevel * 40) : EssencePrice(HitCooldownLevel),
163
_ => SickleUpgradePrice.Unavailable
119
164
};
120
165
}
121
166
122
167
internal bool ApplyUpgrade(SickleUpgradeType upgradeType)
123
168
{
124
if (GetUpgradeCost(upgradeType) < 0)
169
if (!GetUpgradePrice(upgradeType).IsAvailable)
125
170
return false;
126
171
127
172
switch (upgradeType)
@@ -138,6 +183,15 @@ public class NormalSickle : ModItem
138
183
case SickleUpgradeType.LifeSteal:
139
184
LifeStealLevel++;
140
185
break;
186
case SickleUpgradeType.AttackSpeed:
187
AttackSpeedLevel++;
188
break;
189
case SickleUpgradeType.Knockback:
190
KnockbackLevel++;
191
break;
192
case SickleUpgradeType.HitCooldown:
193
HitCooldownLevel++;
194
break;
141
195
default:
142
196
return false;
143
197
}
@@ -156,6 +210,9 @@ public class NormalSickle : ModItem
156
210
int projectileCount = ProjectileCount;
157
211
int armorPenetrationLevel = ArmorPenetrationLevel;
158
212
int lifeStealLevel = LifeStealLevel;
213
int attackSpeedLevel = AttackSpeedLevel;
214
int knockbackLevel = KnockbackLevel;
215
int hitCooldownLevel = HitCooldownLevel;
159
216
int prefix = item.prefix;
160
217
bool favorited = item.favorited;
161
218
@@ -170,6 +227,9 @@ public class NormalSickle : ModItem
170
227
upgraded.ProjectileCount = projectileCount;
171
228
upgraded.ArmorPenetrationLevel = armorPenetrationLevel;
172
229
upgraded.LifeStealLevel = lifeStealLevel;
230
upgraded.AttackSpeedLevel = attackSpeedLevel;
231
upgraded.KnockbackLevel = knockbackLevel;
232
upgraded.HitCooldownLevel = hitCooldownLevel;
173
233
upgraded.ApplyDynamicStats();
174
234
item.favorited = favorited;
175
235
return true;
@@ -193,6 +253,9 @@ public class NormalSickle : ModItem
193
253
tag[nameof(ProjectileCount)] = ProjectileCount;
194
254
tag[nameof(ArmorPenetrationLevel)] = ArmorPenetrationLevel;
195
255
tag[nameof(LifeStealLevel)] = LifeStealLevel;
256
tag[nameof(AttackSpeedLevel)] = AttackSpeedLevel;
257
tag[nameof(KnockbackLevel)] = KnockbackLevel;
258
tag[nameof(HitCooldownLevel)] = HitCooldownLevel;
196
259
}
197
260
198
261
public override void LoadData(TagCompound tag)
@@ -201,6 +264,9 @@ public class NormalSickle : ModItem
201
264
ProjectileCount = Math.Clamp(tag.GetInt(nameof(ProjectileCount)), 1, MaxProjectileCount);
202
265
ArmorPenetrationLevel = Math.Clamp(tag.GetInt(nameof(ArmorPenetrationLevel)), 1, MaxArmorPenetrationLevel);
203
266
LifeStealLevel = Math.Clamp(tag.GetInt(nameof(LifeStealLevel)), 0, MaxLifeStealLevel);
267
AttackSpeedLevel = Math.Clamp(tag.GetInt(nameof(AttackSpeedLevel)), 1, MaxAttackSpeedLevel);
268
KnockbackLevel = Math.Clamp(tag.GetInt(nameof(KnockbackLevel)), 1, MaxKnockbackLevel);
269
HitCooldownLevel = Math.Clamp(tag.GetInt(nameof(HitCooldownLevel)), 1, MaxHitCooldownLevel);
204
270
ApplyDynamicStats();
205
271
}
206
272
@@ -210,6 +276,9 @@ public class NormalSickle : ModItem
210
276
writer.Write((byte)ProjectileCount);
211
277
writer.Write((byte)ArmorPenetrationLevel);
212
278
writer.Write((byte)LifeStealLevel);
279
writer.Write((byte)AttackSpeedLevel);
280
writer.Write((byte)KnockbackLevel);
281
writer.Write((byte)HitCooldownLevel);
213
282
}
214
283
215
284
public override void NetReceive(BinaryReader reader)
@@ -218,6 +287,9 @@ public class NormalSickle : ModItem
218
287
ProjectileCount = Math.Clamp((int)reader.ReadByte(), 1, MaxProjectileCount);
219
288
ArmorPenetrationLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxArmorPenetrationLevel);
220
289
LifeStealLevel = Math.Clamp((int)reader.ReadByte(), 0, MaxLifeStealLevel);
290
AttackSpeedLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxAttackSpeedLevel);
291
KnockbackLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxKnockbackLevel);
292
HitCooldownLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxHitCooldownLevel);
221
293
ApplyDynamicStats();
222
294
}
223
295
@@ -235,6 +307,16 @@ public class NormalSickle : ModItem
235
307
Item.ArmorPenetration = Math.Clamp(ArmorPenetrationLevel, 1, MaxArmorPenetrationLevel);
236
308
}
237
309
310
private static SickleUpgradePrice SoulPrice(int amount)
311
{
312
return new SickleUpgradePrice(amount, SickleUpgradeCurrency.Souls);
313
}
314
315
private static SickleUpgradePrice EssencePrice(int amount)
316
{
317
return new SickleUpgradePrice(amount, SickleUpgradeCurrency.SoulEssence);
318
}
319
238
320
private static Color GetGradientColor()
239
321
{
240
322
double time = Main.GlobalTimeWrappedHourly;
@@ -50,7 +50,7 @@ Mods: {
50
50
51
51
UI: {
52
52
MeleeOnly: Melee form
53
SickleStats: Altar upgrades: Damage Lv.{0} (+{1}) | Projectiles {2} | Armor penetration {3} | Life steal {4}%
53
SickleStats: "Altar upgrades: Damage Lv.{0} (+{1}) | Speed {5}f | Knockback {6}\nProjectiles {2} | Armor penetration {3} | Life steal {4}% | Hit cooldown {7}f"
54
54
AltarHint: Right-click a Death Altar to modify this sickle
55
55
SoulCount: Souls: {0}
56
56
SoulJarHint: Right-click to extract Soul Essence ({0} souls each)
@@ -62,20 +62,31 @@ Mods: {
62
62
UpgradeUnavailableReason: "{0} | {1}"
63
63
Maxed: Maximum level
64
64
65
UpgradeCosts: {
66
Souls: "{0} character souls"
67
SoulEssence: "{0} Soul Essence"
68
}
69
65
70
UpgradeNames: {
66
71
Advancement: Advanced modification
67
72
Damage: Damage modification
73
AttackSpeed: Attack speed
74
Knockback: Knockback
68
75
ProjectileCount: Projectile multiplication
69
76
ArmorPenetration: Armor penetration
77
HitCooldown: Hit cooldown
70
78
LifeSteal: Life steal
71
79
}
72
80
73
81
UpgradeButtons: {
74
Advancement: "{0} → {1} | Costs {2} Soul Essence"
75
Damage: "{0} Lv.{1} → Lv.{2} (total +{3}) | Costs {4} Soul Essence"
76
ProjectileCount: "{0} ×{1} → ×{2} | Costs {3} Soul Essence"
77
ArmorPenetration: "{0} {1} → {2} | Costs {3} Soul Essence"
78
LifeSteal: "{0} {1}% → {2}% | Costs {3} Soul Essence"
82
Advancement: "{0} → {1} | Costs {2}"
83
Damage: "{0} Lv.{1} → Lv.{2} (total +{3}) | Costs {4}"
84
AttackSpeed: "{0} {1}f → {2}f | Costs {3}"
85
Knockback: "{0} {1} → {2} | Costs {3}"
86
ProjectileCount: "{0} ×{1} → ×{2} | Costs {3}"
87
ArmorPenetration: "{0} {1} → {2} | Costs {3}"
88
HitCooldown: "{0} {1}f → {2}f | Costs {3}"
89
LifeSteal: "{0} {1}% → {2}% | Costs {3}"
79
90
}
80
91
}
81
92
@@ -84,6 +95,7 @@ Mods: {
84
95
NotEnoughSouls: Not enough souls: extracting Soul Essence requires 100 souls.
85
96
UpgradeSucceeded: Sickle modification complete.
86
97
NotEnoughEssence: Not enough Soul Essence.
98
NotEnoughUpgradeSouls: Not enough character souls for this low-tier infusion.
87
99
HoldSickle: Hold a DeathMod sickle first.
88
100
TooFarFromAltar: You are too far from the Death Altar, or it has been removed.
89
101
UpgradeMaxed: This modification is unavailable or already at maximum level.
@@ -50,7 +50,7 @@ Mods: {
50
50
51
51
UI: {
52
52
MeleeOnly: 近战形态
53
SickleStats: 祭坛改造:攻击 Lv.{0}(+{1}) | 弹幕 {2} | 穿甲 {3} | 吸血 {4}%
53
SickleStats: "祭坛改造:攻击 Lv.{0}(+{1}) | 攻速 {5}帧 | 击退 {6}\n弹幕 {2} | 穿甲 {3} | 吸血 {4}% | 伤害间隔 {7}帧"
54
54
AltarHint: 在死神祭坛右键打开改造界面
55
55
SoulCount: 灵魂:{0}
56
56
SoulJarHint: 右键提取灵魂精华(每个消耗 {0} 灵魂)
@@ -62,20 +62,31 @@ Mods: {
62
62
UpgradeUnavailableReason: "{0} | {1}"
63
63
Maxed: 已达到上限
64
64
65
UpgradeCosts: {
66
Souls: "{0} 人物灵魂"
67
SoulEssence: "{0} 灵魂精华"
68
}
69
65
70
UpgradeNames: {
66
71
Advancement: 进阶改造
67
72
Damage: 攻击改造
73
AttackSpeed: 攻速改造
74
Knockback: 击退改造
68
75
ProjectileCount: 弹幕增殖
69
76
ArmorPenetration: 穿甲改造
77
HitCooldown: 伤害间隔
70
78
LifeSteal: 吸血改造
71
79
}
72
80
73
81
UpgradeButtons: {
74
Advancement: "{0} → {1} | 消耗 {2} 灵魂精华"
75
Damage: "{0} Lv.{1} → Lv.{2}(总加成 +{3}) | 消耗 {4} 灵魂精华"
76
ProjectileCount: "{0} ×{1} → ×{2} | 消耗 {3} 灵魂精华"
77
ArmorPenetration: "{0} {1} → {2} | 消耗 {3} 灵魂精华"
78
LifeSteal: "{0} {1}% → {2}% | 消耗 {3} 灵魂精华"
82
Advancement: "{0} → {1} | 消耗 {2}"
83
Damage: "{0} Lv.{1} → Lv.{2}(总加成 +{3}) | 消耗 {4}"
84
AttackSpeed: "{0} {1}帧 → {2}帧 | 消耗 {3}"
85
Knockback: "{0} {1} → {2} | 消耗 {3}"
86
ProjectileCount: "{0} ×{1} → ×{2} | 消耗 {3}"
87
ArmorPenetration: "{0} {1} → {2} | 消耗 {3}"
88
HitCooldown: "{0} {1}帧 → {2}帧 | 消耗 {3}"
89
LifeSteal: "{0} {1}% → {2}% | 消耗 {3}"
79
90
}
80
91
}
81
92
@@ -84,6 +95,7 @@ Mods: {
84
95
NotEnoughSouls: 灵魂不足:提取灵魂精华需要100灵魂。
85
96
UpgradeSucceeded: 镰刀改造完成。
86
97
NotEnoughEssence: 灵魂精华不足。
98
NotEnoughUpgradeSouls: 人物灵魂不足,无法完成这次低阶注魂。
87
99
HoldSickle: 请先将一把死神模组镰刀拿在手中。
88
100
TooFarFromAltar: 你距离死神祭坛太远,或祭坛已经被移除。
89
101
UpgradeMaxed: 这项改造尚不可用或已经达到上限。
@@ -14,7 +14,8 @@ Soul Harvest(内部模组名:`DeathMod`)是一款面向 Terraria tModLoade
14
14
- 灵魂保存在人物存档中,并通过屏幕右上角的灵魂罐显示。
15
15
- 每个新人物拥有 100 个初始灵魂。
16
16
- 只有本模组的镰刀能够登记并收割灵魂。
17
- 可在死神祭坛提升攻击、弹幕数量、穿甲和吸血。
17
- 可在死神祭坛提升攻击、攻速、击退、弹幕数量、穿甲、伤害间隔和吸血。
18
- 低等级属性可直接注入人物灵魂,高等级改造与镰刀进阶消耗灵魂精华。
18
19
- 镰刀进阶时会继承已有的祭坛改造和物品前缀。
19
20
- 支持单人和多人游戏;Boss 奖励按参与玩家分别结算。
20
21
@@ -43,7 +44,10 @@ Soul Harvest(内部模组名:`DeathMod`)是一款面向 Terraria tModLoade
43
44
| 镰刀阶段 | 普通 → 骷髅 → 恶魔 → 灵魂 → 死神 |
44
45
| 最大弹幕数量 | 7 |
45
46
| 最大攻击等级 | 25 |
47
| 最大攻速等级 | 10 |
48
| 最大击退等级 | 10 |
46
49
| 最大穿甲等级 | 20 |
50
| 最大伤害间隔等级 | 10 |
47
51
| 最大吸血等级 | 5 |
48
52
49
53
## 安装
@@ -101,6 +101,7 @@ internal class DeathAltarUIState : UIState
101
101
{
102
102
private readonly Dictionary<SickleUpgradeType, AltarUpgradeButton> buttons = [];
103
103
private UIPanel? panel;
104
private UIText? titleText;
104
105
private UIText? itemText;
105
106
private UIText? resourceText;
106
107
private int tileX;
@@ -109,19 +110,19 @@ internal class DeathAltarUIState : UIState
109
110
public override void OnInitialize()
110
111
{
111
112
panel = new UIPanel();
112
panel.Left.Set(-270f, 0.5f);
113
panel.Top.Set(-225f, 0.5f);
114
panel.Width.Set(540f, 0f);
115
panel.Height.Set(450f, 0f);
113
panel.Left.Set(-310f, 0.5f);
114
panel.Top.Set(-275f, 0.5f);
115
panel.Width.Set(620f, 0f);
116
panel.Height.Set(550f, 0f);
116
117
panel.SetPadding(14f);
117
118
panel.BackgroundColor = new Color(22, 17, 38) * 0.96f;
118
119
panel.BorderColor = new Color(112, 65, 150);
119
120
Append(panel);
120
121
121
UIText title = new(Language.GetTextValue("Mods.DeathMod.UI.AltarTitle"), 1.1f, true);
122
title.HAlign = 0.5f;
123
title.Top.Set(4f, 0f);
124
panel.Append(title);
122
titleText = new UIText(string.Empty, 1.1f, true);
123
titleText.HAlign = 0.5f;
124
titleText.Top.Set(4f, 0f);
125
panel.Append(titleText);
125
126
126
127
UITextPanel<string> closeButton = new("×", 1f);
127
128
closeButton.Left.Set(-34f, 1f);
@@ -137,12 +138,12 @@ internal class DeathAltarUIState : UIState
137
138
panel.Append(closeButton);
138
139
139
140
itemText = new UIText(string.Empty, 0.9f);
140
itemText.Top.Set(48f, 0f);
141
itemText.Top.Set(40f, 0f);
141
142
itemText.Width.Set(0f, 1f);
142
143
panel.Append(itemText);
143
144
144
145
resourceText = new UIText(string.Empty, 0.82f);
145
resourceText.Top.Set(74f, 0f);
146
resourceText.Top.Set(64f, 0f);
146
147
resourceText.Width.Set(0f, 1f);
147
148
panel.Append(resourceText);
148
149
@@ -150,8 +151,11 @@ internal class DeathAltarUIState : UIState
150
151
[
151
152
SickleUpgradeType.Advancement,
152
153
SickleUpgradeType.Damage,
154
SickleUpgradeType.AttackSpeed,
155
SickleUpgradeType.Knockback,
153
156
SickleUpgradeType.ProjectileCount,
154
157
SickleUpgradeType.ArmorPenetration,
158
SickleUpgradeType.HitCooldown,
155
159
SickleUpgradeType.LifeSteal
156
160
];
157
161
@@ -160,9 +164,9 @@ internal class DeathAltarUIState : UIState
160
164
SickleUpgradeType type = upgradeTypes[index];
161
165
AltarUpgradeButton button = new(type, this);
162
166
button.Left.Set(0f, 0f);
163
button.Top.Set(108f + index * 61f, 0f);
167
button.Top.Set(94f + index * 55f, 0f);
164
168
button.Width.Set(0f, 1f);
165
button.Height.Set(54f, 0f);
169
button.Height.Set(49f, 0f);
166
170
panel.Append(button);
167
171
buttons[type] = button;
168
172
}
@@ -179,13 +183,14 @@ internal class DeathAltarUIState : UIState
179
183
int essenceCount = player.CountItem(ModContent.ItemType<Soul>());
180
184
int souls = player.GetModPlayer<MyPlayer>().Souls;
181
185
186
titleText?.SetText(Language.GetTextValue("Mods.DeathMod.UI.AltarTitle"));
182
187
itemText?.SetText(sickle is null
183
188
? Language.GetTextValue("Mods.DeathMod.UI.HoldSickle")
184
189
: Language.GetTextValue("Mods.DeathMod.UI.CurrentSickle", player.HeldItem.Name));
185
190
resourceText?.SetText(Language.GetTextValue("Mods.DeathMod.UI.AltarResources", essenceCount, souls));
186
191
187
192
foreach ((SickleUpgradeType type, AltarUpgradeButton button) in buttons)
188
button.Refresh(sickle, essenceCount, BuildButtonText(type, sickle));
193
button.Refresh(sickle, essenceCount, souls, BuildButtonText(type, sickle));
189
194
}
190
195
191
196
public void SetAltarPosition(int x, int y)
@@ -216,50 +221,75 @@ internal class DeathAltarUIState : UIState
216
221
if (sickle is null)
217
222
return Language.GetTextValue("Mods.DeathMod.UI.UpgradeUnavailable", name);
218
223
219
int cost = sickle.GetUpgradeCost(type);
220
if (cost < 0)
224
SickleUpgradePrice price = sickle.GetUpgradePrice(type);
225
if (!price.IsAvailable)
221
226
{
222
string reason = type == SickleUpgradeType.ProjectileCount && !sickle.SupportsProjectileCount
227
string reason = type is SickleUpgradeType.ProjectileCount or SickleUpgradeType.HitCooldown && !sickle.SupportsProjectileCount
223
228
? Language.GetTextValue("Mods.DeathMod.UI.MeleeOnly")
224
229
: Language.GetTextValue("Mods.DeathMod.UI.Maxed");
225
230
return Language.GetTextValue("Mods.DeathMod.UI.UpgradeUnavailableReason", name, reason);
226
231
}
227
232
233
string priceText = FormatPrice(price);
228
234
return type switch
229
235
{
230
236
SickleUpgradeType.Advancement => Language.GetTextValue(
231
237
"Mods.DeathMod.UI.UpgradeButtons.Advancement",
232
238
name,
233
239
Lang.GetItemNameValue(sickle.GetNextSickleType()),
234
cost),
240
priceText),
235
241
SickleUpgradeType.Damage => Language.GetTextValue(
236
242
"Mods.DeathMod.UI.UpgradeButtons.Damage",
237
243
name,
238
244
sickle.DamageLevel,
239
245
sickle.DamageLevel + 1,
240
246
sickle.DamageBonus + 2,
241
cost),
247
priceText),
248
SickleUpgradeType.AttackSpeed => Language.GetTextValue(
249
"Mods.DeathMod.UI.UpgradeButtons.AttackSpeed",
250
name,
251
sickle.AttackUseTime,
252
sickle.AttackUseTime - 1,
253
priceText),
254
SickleUpgradeType.Knockback => Language.GetTextValue(
255
"Mods.DeathMod.UI.UpgradeButtons.Knockback",
256
name,
257
sickle.TotalBaseKnockback.ToString("0.0"),
258
(sickle.TotalBaseKnockback + 0.5f).ToString("0.0"),
259
priceText),
242
260
SickleUpgradeType.ProjectileCount => Language.GetTextValue(
243
261
"Mods.DeathMod.UI.UpgradeButtons.ProjectileCount",
244
262
name,
245
263
sickle.ProjectileCount,
246
264
sickle.ProjectileCount + 1,
247
cost),
265
priceText),
248
266
SickleUpgradeType.ArmorPenetration => Language.GetTextValue(
249
267
"Mods.DeathMod.UI.UpgradeButtons.ArmorPenetration",
250
268
name,
251
269
sickle.ArmorPenetrationLevel,
252
270
sickle.ArmorPenetrationLevel + 1,
253
cost),
271
priceText),
272
SickleUpgradeType.HitCooldown => Language.GetTextValue(
273
"Mods.DeathMod.UI.UpgradeButtons.HitCooldown",
274
name,
275
sickle.HitCooldownFrames,
276
sickle.HitCooldownFrames - 1,
277
priceText),
254
278
SickleUpgradeType.LifeSteal => Language.GetTextValue(
255
279
"Mods.DeathMod.UI.UpgradeButtons.LifeSteal",
256
280
name,
257
281
sickle.LifeStealLevel,
258
282
sickle.LifeStealLevel + 1,
259
cost),
283
priceText),
260
284
_ => name
261
285
};
262
286
}
287
288
private static string FormatPrice(SickleUpgradePrice price)
289
{
290
string currencyKey = price.Currency == SickleUpgradeCurrency.Souls ? "Souls" : "SoulEssence";
291
return Language.GetTextValue($"Mods.DeathMod.UI.UpgradeCosts.{currencyKey}", price.Amount);
292
}
263
293
}
264
294
265
295
internal class AltarUpgradeButton : UITextPanel<string>
@@ -268,22 +298,25 @@ internal class AltarUpgradeButton : UITextPanel<string>
268
298
private readonly DeathAltarUIState owner;
269
299
private bool canRequest;
270
300
271
public AltarUpgradeButton(SickleUpgradeType upgradeType, DeathAltarUIState owner) : base(string.Empty, 0.78f)
301
public AltarUpgradeButton(SickleUpgradeType upgradeType, DeathAltarUIState owner) : base(string.Empty, 0.72f)
272
302
{
273
303
this.upgradeType = upgradeType;
274
304
this.owner = owner;
275
305
TextHAlign = 0f;
276
SetPadding(10f);
306
SetPadding(8f);
277
307
}
278
308
279
public void Refresh(NormalSickle? sickle, int essenceCount, string text)
309
public void Refresh(NormalSickle? sickle, int essenceCount, int souls, string text)
280
310
{
281
311
SetText(text);
282
int cost = sickle?.GetUpgradeCost(upgradeType) ?? -1;
283
canRequest = cost >= 0;
312
SickleUpgradePrice price = sickle?.GetUpgradePrice(upgradeType) ?? SickleUpgradePrice.Unavailable;
313
canRequest = price.IsAvailable;
314
bool canAfford = price.Currency == SickleUpgradeCurrency.Souls
315
? souls >= price.Amount
316
: essenceCount >= price.Amount;
284
317
BackgroundColor = !canRequest
285
318
? new Color(48, 43, 62)
286
: essenceCount >= cost
319
: canAfford
287
320
? new Color(48, 76, 104)
288
321
: new Color(92, 52, 62);
289
322
BorderColor = !canRequest ? new Color(70, 65, 82) : new Color(116, 100, 170);
@@ -44,7 +44,7 @@ Boss 死亡时,每位曾使用本模组镰刀命中过该 Boss 的有效参与
44
44
100 灵魂 = 1 灵魂精华
45
45
```
46
46
47
提取成功后,灵魂精华会进入人物物品栏。灵魂精华是祭坛所有改造的唯一消耗品;它本身不再由 Boss 掉落。
47
提取成功后,灵魂精华会进入人物物品栏。灵魂精华用于镰刀进阶与高等级属性改造;低等级属性则可以在祭坛直接注入人物灵魂。灵魂精华本身不再由 Boss 掉落。
48
48
49
49
新人物的 100 个初始灵魂正好可以提取第一份灵魂精华。这份精华应首先用于制作死神祭坛;之后收割的下一批 100 灵魂可用于首次镰刀进阶。
50
50
@@ -61,7 +61,7 @@ Boss 死亡时,每位曾使用本模组镰刀命中过该 Boss 的有效参与
61
61
62
62
## 六、镰刀进阶路线
63
63
64
“进阶改造”会把当前镰刀变成下一阶段,并保留攻击等级、弹幕数量、穿甲等级、吸血等级、前缀和收藏状态。
64
“进阶改造”会把当前镰刀变成下一阶段,并保留攻击、攻速、击退、弹幕数量、穿甲、伤害间隔、吸血、前缀和收藏状态。
65
65
66
66
| 当前镰刀 | 攻击特征 | 下一阶段 | 进阶费用 |
67
67
| --- | --- | --- | ---: |
@@ -75,47 +75,37 @@ Boss 死亡时,每位曾使用本模组镰刀命中过该 Boss 的有效参与
75
75
76
76
## 七、属性改造
77
77
78
每柄新获得的镰刀默认拥有 1 级攻击、1 个投射物、1 级穿甲和 0 级吸血。通过进阶得到下一把镰刀时,当前改造会完整继承。
78
镰刀默认拥有 1 级攻击、1 级攻速、1 级击退、1 个投射物、1 级穿甲、1 级伤害间隔和 0 级吸血。通过进阶得到下一把镰刀时,当前改造会完整继承。
79
79
80
费用公式中的 `L` 表示当前属性等级,`N` 表示当前投射物数量。公式计算结果均以整数为准。
80
祭坛采用双货币曲线:低等级属性直接扣除人物灵魂,高等级属性自动切换为灵魂精华。按钮会明确显示本次使用哪一种资源。费用公式中的 `L` 表示当前属性等级,`N` 表示当前投射物数量;公式结果向下取整。
81
81
82
| 改造 | 效果 | 上限 | 下一次费用 |
83
| --- | --- | ---: | --- |
84
| 攻击 | 每级增加 2 点基础伤害 | 25 级 | `1 + floor(L² / 4)` |
85
| 弹幕数量 | 每级增加 1 个投射物 | 7 个 | `2^N` |
86
| 穿甲 | 每级增加 1 点穿甲 | 20 级 | `1 + floor(L / 2)` |
87
| 吸血 | 每级增加 1% 吸血系数 | 5 级 | `(L + 1) × 3` |
82
| 改造 | 每级效果 | 上限 | 低等级人物灵魂费用 | 高等级灵魂精华费用 |
83
| --- | --- | ---: | --- | --- |
84
| 攻击 | 增加 2 点基础伤害 | 25 级 | `L < 6` 时消耗 `20L` | `1 + floor(L² / 4)` |
85
| 攻速 | 使用间隔减少 1 帧 | 10 级(16 帧) | `L < 6` 时消耗 `25L` | `1 + floor(L / 2)` |
86
| 击退 | 增加 0.5 击退 | 10 级(8.5) | `L < 6` 时消耗 `15L` | `1 + floor(L / 3)` |
87
| 弹幕数量 | 增加 1 个投射物 | 7 个 | `N < 3` 时消耗 `50N` | `2^N` |
88
| 穿甲 | 增加 1 点穿甲 | 20 级 | `L < 6` 时消耗 `15L` | `1 + floor(L / 2)` |
89
| 伤害间隔 | 同一投射物的命中无敌帧减少 1 帧 | 10 级(6 帧) | `L < 6` 时消耗 `40L` | `L` |
90
| 吸血 | 增加 1% 吸血系数 | 5 级 | `L < 2` 时消耗 `75(L + 1)` | `(L + 1) × 3` |
88
91
89
### 攻击费用示例
92
### 低阶注魂示例
90
93
91
| 升级 | 费用 |
92
| --- | ---: |
93
| 1 → 2 | 1 灵魂精华 |
94
| 2 → 3 | 2 灵魂精华 |
95
| 3 → 4 | 3 灵魂精华 |
96
| 4 → 5 | 5 灵魂精华 |
97
| 5 → 6 | 7 灵魂精华 |
98
99
### 弹幕数量费用
100
101
| 升级 | 费用 |
102
| --- | ---: |
103
| 1 → 2 | 2 灵魂精华 |
104
| 2 → 3 | 4 灵魂精华 |
105
| 3 → 4 | 8 灵魂精华 |
106
| 4 → 5 | 16 灵魂精华 |
107
| 5 → 6 | 32 灵魂精华 |
108
| 6 → 7 | 64 灵魂精华 |
94
- 攻击 `1 → 2` 消耗 20 人物灵魂,`5 → 6` 消耗 100 人物灵魂;从 `6 → 7` 开始改用灵魂精华。
95
- 攻速 `1 → 2` 消耗 25 人物灵魂,使用间隔从 25 帧降至 24 帧。
96
- 击退 `1 → 2` 消耗 15 人物灵魂,基础击退从 4.0 提升到 4.5。
97
- 穿甲 `1 → 2` 消耗 15 人物灵魂。
98
- 吸血 `0 → 1` 消耗 75 人物灵魂,`1 → 2` 消耗 150 人物灵魂,之后改用灵魂精华。
109
99
110
基础镰刀没有投射物,因此无法进行弹幕数量改造。首次进阶为骷髅镰刀后,该选项才会开放。
100
### 投射物相关改造
111
101
112
### 穿甲费用
102
弹幕数量 `1 → 2` 消耗 50 人物灵魂,`2 → 3` 消耗 100 人物灵魂;此后费用依次为 `8、16、32、64` 个灵魂精华。
113
103
114
穿甲从 1 级开始,费用缓慢递增。例如 `1 → 2` 需要 1 个灵魂精华,`2 → 3` 和 `3 → 4` 均需要 2 个灵魂精华。
104
伤害间隔控制每个镰刀投射物再次命中同一敌人前需要等待的局部无敌帧,等级越高,间隔越短。基础镰刀没有投射物,因此弹幕数量和伤害间隔需要首次进阶为骷髅镰刀后才会开放。
115
105
116
### 吸血费用
106
### 吸血限制
117
107
118
五次吸血改造依次需要 `3、6、9、12、15` 个灵魂精华。吸血触发后存在短暂冷却,单次回复量也受到等级上限约束,因此它更适合作为中后期生存属性。
108
吸血触发后存在短暂冷却,单次回复量也受到等级上限约束,因此更适合作为中后期生存属性。
119
109
120
110
## 八、推荐成长顺序
121
111
@@ -130,11 +120,11 @@ Boss 死亡时,每位曾使用本模组镰刀命中过该 Boss 的有效参与
130
120
### 建立第一个祭坛后
131
121
132
122
1. 使用收割所得的第二份灵魂精华完成首次进阶。
133
2. 花费 1 个灵魂精华把攻击从 1 级提升到 2 级。
134
3. 根据敌人防御选择第一次穿甲升级。
135
4. 积攒 2 个灵魂精华,将弹幕数量从 1 提升到 2。
136
5. 优先继续进阶,或根据当前流程补充攻击和穿甲。
137
6. 当输出已经稳定后,再投资费用较高的吸血与多重弹幕。
123
2. 直接注入 20 人物灵魂,把攻击从 1 级提升到 2 级。
124
3. 根据需要花费 15 人物灵魂提升穿甲或击退。
125
4. 花费 50 人物灵魂,将弹幕数量从 1 提升到 2。
126
5. 用人物灵魂补充早期攻速;面对高生命目标时再考虑降低伤害间隔。
127
6. 优先继续进阶,或根据当前流程补充属性;中后期再投资吸血与多重弹幕。
138
128
139
129
如果目标是尽快解锁不同攻击模式,应该优先进阶;如果当前阶段生存和输出压力较大,可以暂缓进阶,先提升攻击与穿甲。
140
130
@@ -143,7 +133,7 @@ Boss 死亡时,每位曾使用本模组镰刀命中过该 Boss 的有效参与
143
133
- 灵魂余额保存在各自的人物存档中。
144
134
- 普通敌人的 1 个灵魂只会授予最近用本模组镰刀命中过它的有效玩家。
145
135
- 每位用本模组镰刀参与 Boss 战的玩家都可获得 150 灵魂。
146
- 祭坛会在服务端检查手持物品、灵魂精华数量、祭坛是否存在以及人物距离。
136
- 祭坛会在服务端检查手持物品、人物灵魂或灵魂精华余额、祭坛是否存在以及人物距离。
147
137
- 灵魂精华必须放在人物物品栏中,放在箱子里的精华不会被祭坛消耗。
148
138
149
139
## 十、常见问题
@@ -170,8 +160,8 @@ Boss 死亡时,每位曾使用本模组镰刀命中过该 Boss 的有效参与
170
160
171
161
### 为什么点击升级没有反应?
172
162
173
确认人物手持本模组镰刀、物品栏中有足够的灵魂精华,并且距离原祭坛不超过约 12 格。祭坛被拆除后,已经打开的界面也不能继续提交升级。
163
确认人物手持本模组镰刀、拥有按钮所示的足够人物灵魂或灵魂精华,并且距离原祭坛不超过约 12 格。祭坛被拆除后,已经打开的界面也不能继续提交升级。
174
164
175
165
### 镰刀进阶后,之前的升级会消失吗?
176
166
177
不会。攻击、弹幕数量、穿甲、吸血、物品前缀和收藏状态都会继承。
167
不会。攻击、攻速、击退、弹幕数量、穿甲、伤害间隔、吸血、物品前缀和收藏状态都会继承。