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

DeathMod

【Terraria】DeathMod (now aka Soul Harvest)

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

XFEstudio/DeathMod

feat: expand Death Altar upgrades

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

代码差异

13 个文件 +281 -101
Modified Common/MyGlobalProjectile.cs +10 -0
@@ -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)
Modified Common/MyPlayer.cs +10 -0
@@ -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)
Modified Common/SickleUpgradeService.cs +16 -4
@@ -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);
Modified Common/SickleUpgradeType.cs +17 -1
@@ -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 }
Modified DeathMod.cs +1 -0
@@ -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
Modified Items/DeathAltarItem.cs +0 -2
@@ -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>());
Added Items/DeathAltarItem.png +0 -0
二进制文件已变更,无法进行逐行预览。
Modified Items/NormalSickle.cs +94 -12
@@ -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;
Modified Localization/en-US.hjson +18 -6
@@ -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.
Modified Localization/zh-Hans.hjson +18 -6
@@ -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: 这项改造尚不可用或已经达到上限。
Modified README.md +5 -1
@@ -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 ## 安装
Modified UI/DeathAltarUISystem.cs +60 -27
@@ -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);
Modified docs/PLAY_GUIDE.md +32 -42
@@ -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 不会。攻击、攻速、击退、弹幕数量、穿甲、伤害间隔、吸血、物品前缀和收藏状态都会继承。