using SoulHarvest.Common;
using SoulHarvest.Projectiles;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.DataStructures;
using Terraria.ID;
using Terraria.Localization;
using Terraria.ModLoader;
using Terraria.ModLoader.IO;
namespace SoulHarvest.Items;
public class NormalSickle : ModItem
{
public const int BaseUseTime = 25;
public const float BaseKnockback = 4f;
public const int BaseHitCooldownFrames = 15;
public const int MaxDamageLevel = 40;
public const int MaxProjectileCount = 7;
public const int MaxProjectileDurationLevel = 10;
public const int MaxArmorPenetrationLevel = 20;
public const int MaxLifeStealLevel = 5;
public const int MaxAttackSpeedLevel = 10;
public const int MaxKnockbackLevel = 10;
public const int MaxHitCooldownLevel = 10;
public const int MaxFatedStackLevel = 10;
public const int MaxFatedStackCapacity = 1 + (MaxFatedStackLevel - 1) * 3;
public const int MaxFatedStacksPerHitLevel = 5;
public const int MaxFatedDurationLevel = 10;
public const int BaseFatedDurationFrames = 60 * 3;
private static readonly DeathAltarUpgradeType[] LinearUpgradeTypes =
[
DeathAltarUpgradeType.Advancement,
DeathAltarUpgradeType.Damage,
DeathAltarUpgradeType.AttackSpeed,
DeathAltarUpgradeType.Knockback,
DeathAltarUpgradeType.ProjectileCount,
DeathAltarUpgradeType.ProjectileDuration,
DeathAltarUpgradeType.ArmorPenetration,
DeathAltarUpgradeType.HitCooldown,
DeathAltarUpgradeType.LifeSteal,
DeathAltarUpgradeType.FatedUnlock,
DeathAltarUpgradeType.FatedMaxStacks,
DeathAltarUpgradeType.FatedStacksPerHit,
DeathAltarUpgradeType.FatedDuration,
DeathAltarUpgradeType.Regression
];
private static readonly DeathAltarUpgradeType[] BranchingUpgradeTypes =
[
DeathAltarUpgradeType.Advancement,
DeathAltarUpgradeType.AlternateAdvancement,
DeathAltarUpgradeType.Damage,
DeathAltarUpgradeType.AttackSpeed,
DeathAltarUpgradeType.Knockback,
DeathAltarUpgradeType.ProjectileCount,
DeathAltarUpgradeType.ProjectileDuration,
DeathAltarUpgradeType.ArmorPenetration,
DeathAltarUpgradeType.HitCooldown,
DeathAltarUpgradeType.LifeSteal,
DeathAltarUpgradeType.FatedUnlock,
DeathAltarUpgradeType.FatedMaxStacks,
DeathAltarUpgradeType.FatedStacksPerHit,
DeathAltarUpgradeType.FatedDuration,
DeathAltarUpgradeType.Regression
];
public int DamageLevel { get; internal set; } = 1;
public int ProjectileCount { get; internal set; } = 1;
public int ProjectileDurationLevel { get; internal set; } = 1;
public int ArmorPenetrationLevel { get; internal set; } = 1;
public int LifeStealLevel { get; internal set; }
public int AttackSpeedLevel { get; internal set; } = 1;
public int KnockbackLevel { get; internal set; } = 1;
public int HitCooldownLevel { get; internal set; } = 1;
public bool FatedUnlocked { get; internal set; }
public int FatedMaximumStacksLevel { get; internal set; } = 1;
public int FatedStacksPerHitLevel { get; internal set; } = 1;
public int FatedDurationLevel { get; internal set; } = 1;
private int previousTierOneType;
private int previousTierTwoType;
private int previousTierThreeType;
public virtual int SickleTier => 0;
public virtual int BaseSickleDamage => 14;
public virtual int AttackProjectileType => ProjectileID.None;
public virtual float AttackProjectileSpeed => 8f;
public virtual float ProjectileDamageFactor => 0.55f;
public virtual int PrimaryNextSickleType => ItemID.None;
public virtual int AlternateNextSickleType => ItemID.None;
public virtual bool UsesModernSwing => true;
public virtual SickleSwingStyle SwingStyle => SickleSwingStyle.Diagonal;
public virtual SickleAlternateAttackStyle AlternateAttackStyle => SickleAlternateAttackStyle.None;
public virtual int MaximumChargeFrames => 55 + SickleTier * 8;
public virtual int SickleReach => 74 + SickleTier * 10;
public virtual float SickleCollisionWidth => 18f + SickleTier * 3f;
public virtual Color SickleColor => ReaperCombatRegistry.GetPrimaryColor(ResolvePresentationForm());
public virtual Color SickleSecondaryColor => ReaperCombatRegistry.GetSecondaryColor(ResolvePresentationForm());
public virtual int SickleDustType => DustID.AncientLight;
public virtual float TrailWidth => 9f;
public virtual Vector2 HandleAnchor => new(0.06f, 0.95f);
public virtual float TextureRotationCorrection => 0.98f;
public virtual float BladeTipLength => 157.4f;
public virtual int AlternateCooldownFrames => 45;
public virtual int OnHitDebuffType => 0;
public virtual int OnHitDebuffDuration => 0;
public bool SupportsProjectileCount => AttackProjectileType > ProjectileID.None;
public bool SupportsProjectileDuration => AttackProjectileType > ProjectileID.None;
public bool HasAlternateAttack => AlternateAttackStyle != SickleAlternateAttackStyle.None;
public bool SupportsCharge => AlternateAttackStyle is SickleAlternateAttackStyle.BloodCharge
or SickleAlternateAttackStyle.FrostCharge
or SickleAlternateAttackStyle.SoulCharge;
public double DamageMultiplier => ScaledBaseDamage / (double)Math.Max(1, BaseSickleDamage);
public int ScaledBaseDamage => GetScaledDamage(DamageLevel);
public int AttackUseTime => BaseUseTime - (AttackSpeedLevel - 1);
public float KnockbackBonus => (KnockbackLevel - 1) * 0.5f;
public float TotalBaseKnockback => BaseKnockback + KnockbackBonus;
public float ArmorPenetrationPercent => ArmorPenetrationLevel / 100f;
public float ProjectileLifetimeMultiplier => 1f + (ProjectileDurationLevel - 1) * 0.12f;
public int HitCooldownFrames => BaseHitCooldownFrames - (HitCooldownLevel - 1);
public int FatedMaximumStacks => FatedUnlocked ? GetFatedMaximumStacks(FatedMaximumStacksLevel) : 0;
public int FatedStacksPerHit => FatedUnlocked ? FatedStacksPerHitLevel : 0;
public int FatedDurationFrames => FatedUnlocked ? BaseFatedDurationFrames + (FatedDurationLevel - 1) * 60 : 0;
public bool AllNonAdvancementUpgradesMaxed => DamageLevel >= MaxDamageLevel
&& (!SupportsProjectileCount || ProjectileCount >= MaxProjectileCount)
&& (!SupportsProjectileDuration || ProjectileDurationLevel >= MaxProjectileDurationLevel)
&& ArmorPenetrationLevel >= MaxArmorPenetrationLevel
&& LifeStealLevel >= MaxLifeStealLevel
&& AttackSpeedLevel >= MaxAttackSpeedLevel
&& KnockbackLevel >= MaxKnockbackLevel
&& (!SupportsProjectileCount || HitCooldownLevel >= MaxHitCooldownLevel)
&& FatedUnlocked
&& FatedMaximumStacksLevel >= MaxFatedStackLevel
&& FatedStacksPerHitLevel >= MaxFatedStacksPerHitLevel
&& FatedDurationLevel >= MaxFatedDurationLevel;
public IReadOnlyList<DeathAltarUpgradeType> AltarUpgradeTypes => Array.Empty<DeathAltarUpgradeType>();
protected override bool CloneNewInstances => true;
public override void PreReforge()
{
DeathAltarReforgePreservation.Capture(this);
}
public override void PostReforge()
{
DeathAltarReforgePreservation.Restore(this);
ApplyDynamicStats();
}
public override void SetDefaults()
{
Item.damage = BaseSickleDamage;
Item.DamageType = DamageClass.Melee;
Item.width = 72;
Item.height = 96;
Item.useTime = BaseUseTime;
Item.useAnimation = BaseUseTime;
Item.useStyle = UsesModernSwing ? ItemUseStyleID.Shoot : ItemUseStyleID.Swing;
Item.crit = 0;
Item.scale = 1f;
Item.knockBack = BaseKnockback;
Item.rare = ItemRarityID.White;
Item.UseSound = UsesModernSwing ? null : SoundID.Item1;
Item.autoReuse = true;
Item.noUseGraphic = UsesModernSwing;
Item.noMelee = UsesModernSwing;
Item.channel = SupportsCharge;
Item.shoot = UsesModernSwing ? ModContent.ProjectileType<SickleSwingProjectile>() : AttackProjectileType;
Item.shootSpeed = AttackProjectileSpeed;
ApplyDynamicStats();
}
public override void UpdateInventory(Player player) => ApplyDynamicStats();
public override void HoldItem(Player player) => ApplyDynamicStats();
public override bool PreDrawInInventory(
SpriteBatch spriteBatch,
Vector2 position,
Rectangle frame,
Color drawColor,
Color itemColor,
Vector2 origin,
float scale)
{
ReaperFormId form = ResolvePresentationForm();
ReaperStage stage = ResolvePresentationStage(form);
Texture2D texture = ModContent.Request<Texture2D>(
ReaperCombatRegistry.GetTexturePath(form, stage)).Value;
Rectangle presentationFrame = texture.Frame();
// Preserve the footprint Terraria calculated for the physical Harvest
// Sickle shell while substituting the character's active form artwork.
// This keeps large late-game scythes inside hotbar and inventory slots.
float calculatedExtent = Math.Max(frame.Width, frame.Height) * scale;
float presentationScale = calculatedExtent / Math.Max(presentationFrame.Width, presentationFrame.Height);
spriteBatch.Draw(
texture,
position,
presentationFrame,
drawColor,
0f,
presentationFrame.Size() * 0.5f,
presentationScale,
SpriteEffects.None,
0f);
return false;
}
public override bool PreDrawInWorld(
SpriteBatch spriteBatch,
Color lightColor,
Color alphaColor,
ref float rotation,
ref float scale,
int whoAmI)
{
ReaperFormId form = ResolvePresentationForm();
ReaperStage stage = ResolvePresentationStage(form);
Texture2D texture = ModContent.Request<Texture2D>(
ReaperCombatRegistry.GetTexturePath(form, stage)).Value;
Rectangle presentationFrame = texture.Frame();
Vector2 position = Item.position - Main.screenPosition
+ new Vector2(Item.width, Item.height) * 0.5f;
float calculatedExtent = Math.Max(Item.width, Item.height) * scale;
float presentationScale = calculatedExtent
/ Math.Max(presentationFrame.Width, presentationFrame.Height);
spriteBatch.Draw(texture, position, presentationFrame, lightColor, rotation,
presentationFrame.Size() * 0.5f, presentationScale, SpriteEffects.None, 0f);
return false;
}
public override void ModifyWeaponDamage(Player player, ref StatModifier damage)
{
ReaperProgressionState state = player.GetModPlayer<MyPlayer>().ReaperProgression;
ReaperFormId form = ReaperCombatRegistry.ResolveUsableForm(state);
ReaperStage stage = ReaperDefinitions.IsBranchForm(form) ? state.GetStage(form) : ReaperStage.StageIII;
float multiplier = ReaperCombatRegistry.GetBaseDamage(form, stage) / (float)BaseSickleDamage;
multiplier *= 1f + state.GetCommonNodeLevel(ReaperCommonNode.Damage) * 0.03f;
if (form == ReaperFormId.Death)
multiplier *= 1f + state.GetCommonNodeLevel(ReaperCommonNode.DeathDamage) * 0.03f;
if (form == ReaperFormId.Blood && player.GetModPlayer<ReaperCombatPlayer>().BloodFrenzyActive)
{
int level = state.GetSkillLevel(ReaperFormId.Blood, 1);
multiplier *= 1f + (level switch { 2 => 0.12f, >= 3 => 0.16f, _ => level > 0 ? 0.08f : 0f });
}
damage *= multiplier;
}
public override float UseSpeedMultiplier(Player player)
{
ReaperProgressionState state = player.GetModPlayer<MyPlayer>().ReaperProgression;
ReaperFormId form = ReaperCombatRegistry.ResolveUsableForm(state);
ReaperStage stage = ReaperDefinitions.IsBranchForm(form) ? state.GetStage(form) : ReaperStage.StageIII;
float speed = BaseUseTime / (float)ReaperCombatRegistry.GetUseTime(form, stage);
speed *= ReaperDefinitions.GetAttackSpeedMultiplier(
state.GetCommonNodeLevel(ReaperCommonNode.AttackSpeed));
if (form == ReaperFormId.Blood && player.GetModPlayer<ReaperCombatPlayer>().BloodFrenzyActive)
{
int level = state.GetSkillLevel(ReaperFormId.Blood, 1);
speed *= 1f + (level switch { 2 => 0.08f, >= 3 => 0.11f, _ => level > 0 ? 0.05f : 0f });
}
if (form == ReaperFormId.Death)
speed *= ReaperCombatService.GetDeathPrimaryTempo(player);
return speed;
}
public override void ModifyWeaponCrit(Player player, ref float crit)
{
crit += player.GetModPlayer<MyPlayer>().ReaperProgression.GetCommonNodeLevel(ReaperCommonNode.Crit) * 10f;
}
public override void ModifyWeaponKnockback(Player player, ref StatModifier knockback)
{
// Legacy per-item knockback remains serialized only so migration can value
// old investment. Modern combat is entirely character-owned.
}
public override bool AltFunctionUse(Player player) => false;
public override bool CanUseItem(Player player)
{
if (!UsesModernSwing)
return true;
if (SickleSwingProjectile.HasBlockingPrimarySwing(player))
return false;
if (player.ownedProjectileCounts[ModContent.ProjectileType<ReaperActionControllerProjectile>()] > 0)
return false;
if (player.GetModPlayer<MyPlayer>().IsReaperFormAssemblyActive())
return false;
Item.channel = false;
Item.useTime = BaseUseTime;
Item.useAnimation = BaseUseTime;
return true;
}
public override bool Shoot(Player player, EntitySource_ItemUse_WithAmmo source, Vector2 position, Vector2 velocity, int type, int damage, float knockback)
{
if (!UsesModernSwing)
{
SpawnLegacyProjectiles(source, position, velocity, type, damage, knockback, player.whoAmI);
return false;
}
SickleCombatSnapshot snapshot = SickleCombatSnapshot.Capture(player, damage, knockback);
bool requireDeathAssembly = snapshot.Form == ReaperFormId.Death
&& ReaperCombatService.RequiresDeathPrimaryAssembly(player);
int phase = ReaperCombatService.TakeNextPrimaryPhase(player, snapshot,
out float deathTempo);
int swingDirection = player.GetModPlayer<MyPlayer>().TakeNextSwingDirection(player.direction);
Vector2 aim = velocity.SafeNormalize(Vector2.UnitX * player.direction);
int projectileIndex = Projectile.NewProjectile(
source,
player.MountedCenter,
aim,
ModContent.ProjectileType<SickleSwingProjectile>(),
damage,
knockback,
player.whoAmI,
swingDirection,
0f,
Item.type);
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
return false;
if (Main.projectile[projectileIndex].ModProjectile is SickleSwingProjectile swing)
swing.Configure(snapshot, phase, deathTempo,
requireDeathAssembly: requireDeathAssembly);
return false;
}
public override void OnHitNPC(Player player, NPC target, NPC.HitInfo hit, int damageDone)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
{
if (player.whoAmI != Main.myPlayer)
return;
int healedLife = player.GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
LifeStealVisuals.Spawn(target.Center, player.whoAmI, LifeStealLevel, healedLife);
return;
}
if (Main.netMode == NetmodeID.Server)
return;
int localHealedLife = player.GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
LifeStealVisuals.Spawn(target.Center, player.whoAmI, LifeStealLevel, localHealedLife);
MyGlobalProjectile.ApplyStatusEffects(target, player.whoAmI, LifeStealLevel, OnHitDebuffType, OnHitDebuffDuration, FatedStacksPerHit, FatedMaximumStacks, FatedDurationFrames);
}
public override void ModifyTooltips(List<TooltipLine> tooltips)
{
ReaperFormId form = ResolvePresentationForm();
string formName = Language.GetTextValue($"Mods.SoulHarvest.UI.ReaperTree.Forms.{form}.Name");
tooltips.Add(new TooltipLine(Mod, "SoulHarvestReaperForm", formName)
{
OverrideColor = ReaperCombatRegistry.GetPrimaryColor(form)
});
string special = Language.GetTextValue("Mods.SoulHarvest.Keybinds.SpecialAttack.DisplayName");
string ultimate = Language.GetTextValue("Mods.SoulHarvest.Keybinds.Ultimate.DisplayName");
string wheel = Language.GetTextValue("Mods.SoulHarvest.Keybinds.FormWheel.DisplayName");
tooltips.Add(new TooltipLine(Mod, "SoulHarvestReaperControls", $"{special} · {ultimate} · {wheel}")
{
OverrideColor = new Color(160, 205, 235)
});
}
public DeathAltarPrice GetUpgradePrice(DeathAltarUpgradeType upgradeType)
{
return upgradeType switch
{
DeathAltarUpgradeType.Advancement => PrimaryNextSickleType == ItemID.None
|| SickleTier == 3 && !AllNonAdvancementUpgradesMaxed
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(GetAdvancementEssenceCost()),
DeathAltarUpgradeType.AlternateAdvancement => AlternateNextSickleType == ItemID.None
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(GetAdvancementEssenceCost()),
DeathAltarUpgradeType.Regression => SickleTier <= 0
? DeathAltarPrice.Unavailable
: DeathAltarPrice.RefundEssence(GetRegressionEssenceRefund()),
DeathAltarUpgradeType.Damage => DamageLevel >= MaxDamageLevel
? DeathAltarPrice.Unavailable
: DamageLevel < 20
? DeathAltarPrice.Souls(DamageLevel * 2)
: DeathAltarPrice.Essence(DamageLevel < 30
? 1 + (DamageLevel - 20) / 2
: 6 + DamageLevel - 30),
DeathAltarUpgradeType.ProjectileCount => !SupportsProjectileCount || ProjectileCount >= MaxProjectileCount
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 << (ProjectileCount - 1)),
DeathAltarUpgradeType.ProjectileDuration => !SupportsProjectileDuration || ProjectileDurationLevel >= MaxProjectileDurationLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 + (ProjectileDurationLevel - 1) / 2),
DeathAltarUpgradeType.ArmorPenetration => ArmorPenetrationLevel >= MaxArmorPenetrationLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 + (ArmorPenetrationLevel - 1) / 3),
DeathAltarUpgradeType.LifeSteal => LifeStealLevel >= MaxLifeStealLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 + LifeStealLevel * 2),
DeathAltarUpgradeType.AttackSpeed => AttackSpeedLevel >= MaxAttackSpeedLevel
? DeathAltarPrice.Unavailable
: AttackSpeedLevel < 6 ? DeathAltarPrice.Souls(AttackSpeedLevel * 25) : DeathAltarPrice.Essence(1 + AttackSpeedLevel / 2),
DeathAltarUpgradeType.Knockback => KnockbackLevel >= MaxKnockbackLevel
? DeathAltarPrice.Unavailable
: KnockbackLevel < 6 ? DeathAltarPrice.Souls(KnockbackLevel * 15) : DeathAltarPrice.Essence(1 + KnockbackLevel / 3),
DeathAltarUpgradeType.HitCooldown => !SupportsProjectileCount || HitCooldownLevel >= MaxHitCooldownLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 + (HitCooldownLevel - 1) / 2),
DeathAltarUpgradeType.FatedUnlock => FatedUnlocked
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1),
DeathAltarUpgradeType.FatedMaxStacks => !FatedUnlocked || FatedMaximumStacksLevel >= MaxFatedStackLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(FatedMaximumStacksLevel),
DeathAltarUpgradeType.FatedStacksPerHit => !FatedUnlocked || FatedStacksPerHitLevel >= MaxFatedStacksPerHitLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(FatedStacksPerHitLevel * 2),
DeathAltarUpgradeType.FatedDuration => !FatedUnlocked || FatedDurationLevel >= MaxFatedDurationLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 + (FatedDurationLevel - 1) / 2),
_ => DeathAltarPrice.Unavailable
};
}
public bool ApplyAltarUpgrade(Item item, DeathAltarUpgradeType upgradeType)
{
return upgradeType switch
{
DeathAltarUpgradeType.Advancement or DeathAltarUpgradeType.AlternateAdvancement => Advance(item, upgradeType),
DeathAltarUpgradeType.Regression => Regress(item),
_ => ApplyUpgrade(upgradeType)
};
}
public string GetCurrentUpgradeValue(DeathAltarUpgradeType upgradeType)
{
return upgradeType switch
{
DeathAltarUpgradeType.Advancement or DeathAltarUpgradeType.AlternateAdvancement or DeathAltarUpgradeType.Regression => Item.Name,
DeathAltarUpgradeType.Damage => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Damage", DamageLevel, ScaledBaseDamage),
DeathAltarUpgradeType.AttackSpeed => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Frames", AttackUseTime),
DeathAltarUpgradeType.Knockback => TotalBaseKnockback.ToString("0.0"),
DeathAltarUpgradeType.ProjectileCount => $"×{ProjectileCount}",
DeathAltarUpgradeType.ProjectileDuration => $"{ProjectileLifetimeMultiplier:P0}",
DeathAltarUpgradeType.ArmorPenetration => $"{ArmorPenetrationLevel}%",
DeathAltarUpgradeType.HitCooldown => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Frames", HitCooldownFrames),
DeathAltarUpgradeType.LifeSteal => $"{LifeStealLevel}%",
DeathAltarUpgradeType.FatedUnlock => Language.GetTextValue(FatedUnlocked ? "Mods.SoulHarvest.UI.Enabled" : "Mods.SoulHarvest.UI.Disabled"),
DeathAltarUpgradeType.FatedMaxStacks => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Stacks", FatedMaximumStacks),
DeathAltarUpgradeType.FatedStacksPerHit => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.StacksPerHit", FatedStacksPerHit),
DeathAltarUpgradeType.FatedDuration => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Seconds", FatedDurationFrames / 60f),
_ => string.Empty
};
}
public string GetNextUpgradeValue(DeathAltarUpgradeType upgradeType)
{
return upgradeType switch
{
DeathAltarUpgradeType.Advancement => Lang.GetItemNameValue(PrimaryNextSickleType),
DeathAltarUpgradeType.AlternateAdvancement => Lang.GetItemNameValue(AlternateNextSickleType),
DeathAltarUpgradeType.Regression => Lang.GetItemNameValue(GetPreviousSickleType()),
DeathAltarUpgradeType.Damage => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Damage", DamageLevel + 1, GetScaledDamage(DamageLevel + 1)),
DeathAltarUpgradeType.AttackSpeed => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Frames", AttackUseTime - 1),
DeathAltarUpgradeType.Knockback => (TotalBaseKnockback + 0.5f).ToString("0.0"),
DeathAltarUpgradeType.ProjectileCount => $"×{ProjectileCount + 1}",
DeathAltarUpgradeType.ProjectileDuration => $"{1f + ProjectileDurationLevel * 0.12f:P0}",
DeathAltarUpgradeType.ArmorPenetration => $"{ArmorPenetrationLevel + 1}%",
DeathAltarUpgradeType.HitCooldown => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Frames", HitCooldownFrames - 1),
DeathAltarUpgradeType.LifeSteal => $"{LifeStealLevel + 1}%",
DeathAltarUpgradeType.FatedUnlock => Language.GetTextValue("Mods.SoulHarvest.UI.Enabled"),
DeathAltarUpgradeType.FatedMaxStacks => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Stacks", GetFatedMaximumStacks(FatedMaximumStacksLevel + 1)),
DeathAltarUpgradeType.FatedStacksPerHit => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.StacksPerHit", FatedStacksPerHitLevel + 1),
DeathAltarUpgradeType.FatedDuration => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Seconds", (BaseFatedDurationFrames + FatedDurationLevel * 60) / 60f),
_ => string.Empty
};
}
public DeathAltarUnavailableReason GetUnavailableReason(DeathAltarUpgradeType upgradeType)
{
if (upgradeType == DeathAltarUpgradeType.Advancement
&& PrimaryNextSickleType != ItemID.None
&& SickleTier == 3
&& !AllNonAdvancementUpgradesMaxed)
{
return DeathAltarUnavailableReason.RequiresAllSickleUpgrades;
}
if (upgradeType == DeathAltarUpgradeType.Regression && SickleTier <= 0)
return DeathAltarUnavailableReason.RequiresHigherSickleForm;
if ((upgradeType is DeathAltarUpgradeType.ProjectileCount or DeathAltarUpgradeType.ProjectileDuration or DeathAltarUpgradeType.HitCooldown) && !SupportsProjectileCount)
return DeathAltarUnavailableReason.RequiresProjectile;
if ((upgradeType is DeathAltarUpgradeType.FatedMaxStacks or DeathAltarUpgradeType.FatedStacksPerHit or DeathAltarUpgradeType.FatedDuration) && !FatedUnlocked)
return DeathAltarUnavailableReason.RequiresFatedUnlock;
return DeathAltarUnavailableReason.Maxed;
}
internal void SpawnAttackProjectiles(Player player, Projectile swingProjectile, float charge, bool heavySlash)
{
if (!SupportsProjectileCount || AttackProjectileType <= ProjectileID.None)
return;
int count = ProjectileCount;
if (heavySlash)
{
float projectileMultiplier = SupportsCharge
? MathHelper.Lerp(1f, 2f, MathHelper.Clamp(charge, 0f, 1f))
: 1.5f;
count = Math.Max(1, (int)Math.Ceiling(ProjectileCount * projectileMultiplier));
}
count = Math.Clamp(count, 1, MaxProjectileCount * 2);
if (heavySlash && AlternateAttackStyle == SickleAlternateAttackStyle.BoneBarrage)
count = Math.Max(3, count);
else if (heavySlash && AlternateAttackStyle == SickleAlternateAttackStyle.VoidRitual)
count = Math.Max(4, count);
float spread = MathHelper.ToRadians(heavySlash && AlternateAttackStyle == SickleAlternateAttackStyle.BoneBarrage ? 18f : heavySlash ? 12f : 7f);
float speed = AttackProjectileSpeed * (heavySlash ? MathHelper.Lerp(1.1f, 1.35f, charge) : 1f);
int damage = Math.Max(1, (int)(swingProjectile.damage * ProjectileDamageFactor * (heavySlash ? MathHelper.Lerp(1.25f, 1.9f, charge) : 1f)));
Vector2 aim = swingProjectile.velocity.SafeNormalize(Vector2.UnitX * player.direction);
IEntitySource source = swingProjectile.GetSource_FromThis();
for (int index = 0; index < count; index++)
{
float spreadSlot = GetForwardBiasedSpreadSlot(index, (swingProjectile.identity & 1) == 0 ? 1f : -1f);
Vector2 projectileDirection = heavySlash && AlternateAttackStyle == SickleAlternateAttackStyle.VoidRitual
? aim.RotatedBy(index * MathHelper.TwoPi / count)
: aim.RotatedBy(spreadSlot * spread);
Vector2 projectileVelocity = projectileDirection * speed;
Projectile.NewProjectile(
source,
player.MountedCenter + projectileDirection * 28f,
projectileVelocity,
AttackProjectileType,
damage,
swingProjectile.knockBack,
player.whoAmI,
heavySlash ? 1f : 0f,
charge,
spreadSlot);
}
}
private bool ApplyUpgrade(DeathAltarUpgradeType upgradeType)
{
if (!GetUpgradePrice(upgradeType).IsAvailable)
return false;
switch (upgradeType)
{
case DeathAltarUpgradeType.Damage:
DamageLevel++;
break;
case DeathAltarUpgradeType.ProjectileCount:
ProjectileCount++;
break;
case DeathAltarUpgradeType.ProjectileDuration:
ProjectileDurationLevel++;
break;
case DeathAltarUpgradeType.ArmorPenetration:
ArmorPenetrationLevel++;
break;
case DeathAltarUpgradeType.LifeSteal:
LifeStealLevel++;
break;
case DeathAltarUpgradeType.AttackSpeed:
AttackSpeedLevel++;
break;
case DeathAltarUpgradeType.Knockback:
KnockbackLevel++;
break;
case DeathAltarUpgradeType.HitCooldown:
HitCooldownLevel++;
break;
case DeathAltarUpgradeType.FatedUnlock:
FatedUnlocked = true;
break;
case DeathAltarUpgradeType.FatedMaxStacks:
FatedMaximumStacksLevel++;
break;
case DeathAltarUpgradeType.FatedStacksPerHit:
FatedStacksPerHitLevel++;
break;
case DeathAltarUpgradeType.FatedDuration:
FatedDurationLevel++;
break;
default:
return false;
}
ApplyDynamicStats();
return true;
}
internal bool Advance(Item item, DeathAltarUpgradeType advancementType)
{
int nextType = advancementType == DeathAltarUpgradeType.AlternateAdvancement
? AlternateNextSickleType
: PrimaryNextSickleType;
return TransformSickle(item, nextType, recordCurrentForm: true);
}
private bool Regress(Item item)
{
if (SickleTier <= 0)
return false;
return TransformSickle(item, GetPreviousSickleType(), recordCurrentForm: false);
}
private bool TransformSickle(Item item, int targetType, bool recordCurrentForm)
{
if (targetType <= ItemID.None || ItemLoader.GetItem(targetType) is not NormalSickle targetTemplate)
return false;
int expectedTier = SickleTier + (recordCurrentForm ? 1 : -1);
if (targetTemplate.SickleTier != expectedTier)
return false;
int damageLevel = DamageLevel;
int projectileCount = ProjectileCount;
int projectileDurationLevel = ProjectileDurationLevel;
int armorPenetrationLevel = ArmorPenetrationLevel;
int lifeStealLevel = LifeStealLevel;
int attackSpeedLevel = AttackSpeedLevel;
int knockbackLevel = KnockbackLevel;
int hitCooldownLevel = HitCooldownLevel;
bool fatedUnlocked = FatedUnlocked;
int fatedMaximumStacksLevel = FatedMaximumStacksLevel;
int fatedStacksPerHitLevel = FatedStacksPerHitLevel;
int fatedDurationLevel = FatedDurationLevel;
int tierOneHistory = previousTierOneType;
int tierTwoHistory = previousTierTwoType;
int tierThreeHistory = previousTierThreeType;
int currentType = item.type;
if (recordCurrentForm)
{
switch (SickleTier)
{
case 1:
tierOneHistory = currentType;
break;
case 2:
tierTwoHistory = currentType;
break;
case 3:
tierThreeHistory = currentType;
break;
}
}
int prefix = item.prefix;
bool favorited = item.favorited;
item.SetDefaults(targetType);
if (prefix > 0)
item.Prefix(prefix);
if (item.ModItem is not NormalSickle upgraded)
return false;
upgraded.DamageLevel = damageLevel;
upgraded.ProjectileCount = projectileCount;
upgraded.ProjectileDurationLevel = projectileDurationLevel;
upgraded.ArmorPenetrationLevel = armorPenetrationLevel;
upgraded.LifeStealLevel = lifeStealLevel;
upgraded.AttackSpeedLevel = attackSpeedLevel;
upgraded.KnockbackLevel = knockbackLevel;
upgraded.HitCooldownLevel = hitCooldownLevel;
upgraded.FatedUnlocked = fatedUnlocked;
upgraded.FatedMaximumStacksLevel = fatedMaximumStacksLevel;
upgraded.FatedStacksPerHitLevel = fatedStacksPerHitLevel;
upgraded.FatedDurationLevel = fatedDurationLevel;
upgraded.previousTierOneType = tierOneHistory;
upgraded.previousTierTwoType = tierTwoHistory;
upgraded.previousTierThreeType = tierThreeHistory;
upgraded.ApplyDynamicStats();
item.favorited = favorited;
return true;
}
private int GetPreviousSickleType()
{
int storedType = SickleTier switch
{
2 => previousTierOneType,
3 => previousTierTwoType,
4 => previousTierThreeType,
_ => ItemID.None
};
if (storedType > ItemID.None
&& ItemLoader.GetItem(storedType) is NormalSickle storedSickle
&& storedSickle.SickleTier == SickleTier - 1)
{
return storedType;
}
// Items created before regression history existed use a deterministic
// canonical predecessor. Newly advanced items always return along the exact
// branch the player originally chose.
return this switch
{
SkullSickle or BloodSickle => ModContent.ItemType<NormalSickle>(),
FrostSickle => ModContent.ItemType<BloodSickle>(),
DevilSickle => ModContent.ItemType<SkullSickle>(),
SoulSickle => ModContent.ItemType<DevilSickle>(),
VoidSickle => ModContent.ItemType<FrostSickle>(),
DeathSickle => ModContent.ItemType<SoulSickle>(),
_ => ItemID.None
};
}
public override void SaveData(TagCompound tag)
{
tag[nameof(DamageLevel)] = DamageLevel;
tag[nameof(ProjectileCount)] = ProjectileCount;
tag[nameof(ProjectileDurationLevel)] = ProjectileDurationLevel;
tag[nameof(ArmorPenetrationLevel)] = ArmorPenetrationLevel;
tag[nameof(LifeStealLevel)] = LifeStealLevel;
tag[nameof(AttackSpeedLevel)] = AttackSpeedLevel;
tag[nameof(KnockbackLevel)] = KnockbackLevel;
tag[nameof(HitCooldownLevel)] = HitCooldownLevel;
tag[nameof(FatedUnlocked)] = FatedUnlocked;
tag[nameof(FatedMaximumStacksLevel)] = FatedMaximumStacksLevel;
tag[nameof(FatedStacksPerHitLevel)] = FatedStacksPerHitLevel;
tag[nameof(FatedDurationLevel)] = FatedDurationLevel;
tag[nameof(previousTierOneType)] = previousTierOneType;
tag[nameof(previousTierTwoType)] = previousTierTwoType;
tag[nameof(previousTierThreeType)] = previousTierThreeType;
}
public override void LoadData(TagCompound tag)
{
DamageLevel = Math.Clamp(tag.GetInt(nameof(DamageLevel)), 1, MaxDamageLevel);
ProjectileCount = Math.Clamp(tag.GetInt(nameof(ProjectileCount)), 1, MaxProjectileCount);
ProjectileDurationLevel = Math.Clamp(tag.GetInt(nameof(ProjectileDurationLevel)), 1, MaxProjectileDurationLevel);
ArmorPenetrationLevel = Math.Clamp(tag.GetInt(nameof(ArmorPenetrationLevel)), 1, MaxArmorPenetrationLevel);
LifeStealLevel = Math.Clamp(tag.GetInt(nameof(LifeStealLevel)), 0, MaxLifeStealLevel);
AttackSpeedLevel = Math.Clamp(tag.GetInt(nameof(AttackSpeedLevel)), 1, MaxAttackSpeedLevel);
KnockbackLevel = Math.Clamp(tag.GetInt(nameof(KnockbackLevel)), 1, MaxKnockbackLevel);
HitCooldownLevel = Math.Clamp(tag.GetInt(nameof(HitCooldownLevel)), 1, MaxHitCooldownLevel);
FatedUnlocked = tag.GetBool(nameof(FatedUnlocked));
FatedMaximumStacksLevel = Math.Clamp(tag.GetInt(nameof(FatedMaximumStacksLevel)), 1, MaxFatedStackLevel);
FatedStacksPerHitLevel = Math.Clamp(tag.GetInt(nameof(FatedStacksPerHitLevel)), 1, MaxFatedStacksPerHitLevel);
FatedDurationLevel = Math.Clamp(tag.GetInt(nameof(FatedDurationLevel)), 1, MaxFatedDurationLevel);
previousTierOneType = tag.GetInt(nameof(previousTierOneType));
previousTierTwoType = tag.GetInt(nameof(previousTierTwoType));
previousTierThreeType = tag.GetInt(nameof(previousTierThreeType));
ApplyDynamicStats();
}
public override void NetSend(BinaryWriter writer)
{
writer.Write((byte)DamageLevel);
writer.Write((byte)ProjectileCount);
writer.Write((byte)ProjectileDurationLevel);
writer.Write((byte)ArmorPenetrationLevel);
writer.Write((byte)LifeStealLevel);
writer.Write((byte)AttackSpeedLevel);
writer.Write((byte)KnockbackLevel);
writer.Write((byte)HitCooldownLevel);
writer.Write(FatedUnlocked);
writer.Write((byte)FatedMaximumStacksLevel);
writer.Write((byte)FatedStacksPerHitLevel);
writer.Write((byte)FatedDurationLevel);
writer.Write(previousTierOneType);
writer.Write(previousTierTwoType);
writer.Write(previousTierThreeType);
}
public override void NetReceive(BinaryReader reader)
{
DamageLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxDamageLevel);
ProjectileCount = Math.Clamp((int)reader.ReadByte(), 1, MaxProjectileCount);
ProjectileDurationLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxProjectileDurationLevel);
ArmorPenetrationLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxArmorPenetrationLevel);
LifeStealLevel = Math.Clamp((int)reader.ReadByte(), 0, MaxLifeStealLevel);
AttackSpeedLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxAttackSpeedLevel);
KnockbackLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxKnockbackLevel);
HitCooldownLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxHitCooldownLevel);
FatedUnlocked = reader.ReadBoolean();
FatedMaximumStacksLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxFatedStackLevel);
FatedStacksPerHitLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxFatedStacksPerHitLevel);
FatedDurationLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxFatedDurationLevel);
previousTierOneType = reader.ReadInt32();
previousTierTwoType = reader.ReadInt32();
previousTierThreeType = reader.ReadInt32();
ApplyDynamicStats();
}
public override void AddRecipes()
{
CreateRecipe()
.AddRecipeGroup(RecipeGroupID.Wood, 10)
.AddRecipeGroup(RecipeGroupID.IronBar, 5)
.AddTile(TileID.Anvils)
.Register();
}
internal void ApplyDynamicStats()
{
// Percentage penetration is applied through NPC.HitModifiers so this must not
// also contribute vanilla's flat armor penetration.
Item.ArmorPenetration = 0;
}
private static int GetFatedMaximumStacks(int level)
{
return 1 + (Math.Clamp(level, 1, MaxFatedStackLevel) - 1) * 3;
}
private int GetScaledDamage(int level)
{
level = Math.Clamp(level, 1, MaxDamageLevel);
if (level == 1)
return Math.Max(1, BaseSickleDamage);
const int lateGameStartLevel = 29;
double earlyMidGrowth = GetUnscaledDamageGrowth(Math.Min(level, lateGameStartLevel - 1)) * 0.25d;
double lateGameGrowth = level < lateGameStartLevel
? 0d
: (GetUnscaledDamageGrowth(level) - GetUnscaledDamageGrowth(lateGameStartLevel - 1)) * 0.70d;
return Math.Max(1, (int)Math.Round(BaseSickleDamage + earlyMidGrowth + lateGameGrowth));
}
private double GetUnscaledDamageGrowth(int level)
{
double progress = (level - 1d) / (MaxDamageLevel - 1d);
double targetDamage = 1_200d + SickleTier * 550d;
double guaranteedGrowth = level - 1d;
double curvedGrowth = Math.Max(0d, targetDamage - BaseSickleDamage - (MaxDamageLevel - 1d)) * Math.Pow(progress, 2.3d);
return guaranteedGrowth + curvedGrowth;
}
private int GetAdvancementEssenceCost()
{
// The price is based on the target form: form 2 is the introduction, while
// forms 3-5 become deliberate long-term milestones.
return SickleTier switch
{
0 => 1,
1 => 16,
2 => 32,
3 => 99,
_ => 999
};
}
private int GetRegressionEssenceRefund()
{
// Refund exactly the cost paid to enter the current form.
return SickleTier switch
{
1 => 1,
2 => 16,
3 => 32,
4 => 99,
_ => 0
};
}
private void SpawnLegacyProjectiles(IEntitySource source, Vector2 position, Vector2 velocity, int type, int damage, float knockback, int owner)
{
if (!SupportsProjectileCount || type <= ProjectileID.None)
return;
int count = Math.Clamp(ProjectileCount, 1, MaxProjectileCount);
float spread = MathHelper.ToRadians(7f);
for (int index = 0; index < count; index++)
{
float offset = GetForwardBiasedSpreadSlot(index, 1f) * spread;
Projectile.NewProjectile(source, position, velocity.RotatedBy(offset), type, damage, knockback, owner);
}
}
private static float GetForwardBiasedSpreadSlot(int index, float firstSide)
{
if (index <= 0)
return 0f;
int distance = (index + 1) / 2;
float side = index % 2 == 1 ? firstSide : -firstSide;
return distance * side;
}
private Color GetGradientColor()
{
float pulse = (float)Math.Sin(Main.GlobalTimeWrappedHourly * 2.4f) * 0.16f + 0.84f;
return SickleColor * pulse;
}
private static ReaperFormId ResolvePresentationForm()
{
if (Main.gameMenu || Main.LocalPlayer is null || !Main.LocalPlayer.active)
return ReaperFormId.Base;
return ReaperCombatRegistry.ResolveUsableForm(Main.LocalPlayer.GetModPlayer<MyPlayer>().ReaperProgression);
}
private static ReaperStage ResolvePresentationStage(ReaperFormId form)
{
if (form == ReaperFormId.Death)
return ReaperStage.StageIII;
if (!ReaperDefinitions.IsBranchForm(form)
|| Main.gameMenu
|| Main.LocalPlayer is null
|| !Main.LocalPlayer.active)
{
return ReaperStage.StageI;
}
ReaperStage stage = Main.LocalPlayer.GetModPlayer<MyPlayer>().ReaperProgression.GetStage(form);
return stage is >= ReaperStage.StageI and <= ReaperStage.StageIII
? stage
: ReaperStage.StageI;
}
}
using SoulHarvest.Common;
using SoulHarvest.Projectiles;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.IO;
using Terraria;
using Terraria.DataStructures;
using Terraria.ID;
using Terraria.Localization;
using Terraria.ModLoader;
using Terraria.ModLoader.IO;
namespace SoulHarvest.Items;
public class NormalSickle : ModItem
{
public const int BaseUseTime = 25;
public const float BaseKnockback = 4f;
public const int BaseHitCooldownFrames = 15;
public const int MaxDamageLevel = 40;
public const int MaxProjectileCount = 7;
public const int MaxProjectileDurationLevel = 10;
public const int MaxArmorPenetrationLevel = 20;
public const int MaxLifeStealLevel = 5;
public const int MaxAttackSpeedLevel = 10;
public const int MaxKnockbackLevel = 10;
public const int MaxHitCooldownLevel = 10;
public const int MaxFatedStackLevel = 10;
public const int MaxFatedStackCapacity = 1 + (MaxFatedStackLevel - 1) * 3;
public const int MaxFatedStacksPerHitLevel = 5;
public const int MaxFatedDurationLevel = 10;
public const int BaseFatedDurationFrames = 60 * 3;
private static readonly DeathAltarUpgradeType[] LinearUpgradeTypes =
[
DeathAltarUpgradeType.Advancement,
DeathAltarUpgradeType.Damage,
DeathAltarUpgradeType.AttackSpeed,
DeathAltarUpgradeType.Knockback,
DeathAltarUpgradeType.ProjectileCount,
DeathAltarUpgradeType.ProjectileDuration,
DeathAltarUpgradeType.ArmorPenetration,
DeathAltarUpgradeType.HitCooldown,
DeathAltarUpgradeType.LifeSteal,
DeathAltarUpgradeType.FatedUnlock,
DeathAltarUpgradeType.FatedMaxStacks,
DeathAltarUpgradeType.FatedStacksPerHit,
DeathAltarUpgradeType.FatedDuration,
DeathAltarUpgradeType.Regression
];
private static readonly DeathAltarUpgradeType[] BranchingUpgradeTypes =
[
DeathAltarUpgradeType.Advancement,
DeathAltarUpgradeType.AlternateAdvancement,
DeathAltarUpgradeType.Damage,
DeathAltarUpgradeType.AttackSpeed,
DeathAltarUpgradeType.Knockback,
DeathAltarUpgradeType.ProjectileCount,
DeathAltarUpgradeType.ProjectileDuration,
DeathAltarUpgradeType.ArmorPenetration,
DeathAltarUpgradeType.HitCooldown,
DeathAltarUpgradeType.LifeSteal,
DeathAltarUpgradeType.FatedUnlock,
DeathAltarUpgradeType.FatedMaxStacks,
DeathAltarUpgradeType.FatedStacksPerHit,
DeathAltarUpgradeType.FatedDuration,
DeathAltarUpgradeType.Regression
];
public int DamageLevel { get; internal set; } = 1;
public int ProjectileCount { get; internal set; } = 1;
public int ProjectileDurationLevel { get; internal set; } = 1;
public int ArmorPenetrationLevel { get; internal set; } = 1;
public int LifeStealLevel { get; internal set; }
public int AttackSpeedLevel { get; internal set; } = 1;
public int KnockbackLevel { get; internal set; } = 1;
public int HitCooldownLevel { get; internal set; } = 1;
public bool FatedUnlocked { get; internal set; }
public int FatedMaximumStacksLevel { get; internal set; } = 1;
public int FatedStacksPerHitLevel { get; internal set; } = 1;
public int FatedDurationLevel { get; internal set; } = 1;
private int previousTierOneType;
private int previousTierTwoType;
private int previousTierThreeType;
public virtual int SickleTier => 0;
public virtual int BaseSickleDamage => 14;
public virtual int AttackProjectileType => ProjectileID.None;
public virtual float AttackProjectileSpeed => 8f;
public virtual float ProjectileDamageFactor => 0.55f;
public virtual int PrimaryNextSickleType => ItemID.None;
public virtual int AlternateNextSickleType => ItemID.None;
public virtual bool UsesModernSwing => true;
public virtual SickleSwingStyle SwingStyle => SickleSwingStyle.Diagonal;
public virtual SickleAlternateAttackStyle AlternateAttackStyle => SickleAlternateAttackStyle.None;
public virtual int MaximumChargeFrames => 55 + SickleTier * 8;
public virtual int SickleReach => 74 + SickleTier * 10;
public virtual float SickleCollisionWidth => 18f + SickleTier * 3f;
public virtual Color SickleColor => ReaperCombatRegistry.GetPrimaryColor(ResolvePresentationForm());
public virtual Color SickleSecondaryColor => ReaperCombatRegistry.GetSecondaryColor(ResolvePresentationForm());
public virtual int SickleDustType => DustID.AncientLight;
public virtual float TrailWidth => 9f;
public virtual Vector2 HandleAnchor => new(0.06f, 0.95f);
public virtual float TextureRotationCorrection => 0.98f;
public virtual float BladeTipLength => 157.4f;
public virtual int AlternateCooldownFrames => 45;
public virtual int OnHitDebuffType => 0;
public virtual int OnHitDebuffDuration => 0;
public bool SupportsProjectileCount => AttackProjectileType > ProjectileID.None;
public bool SupportsProjectileDuration => AttackProjectileType > ProjectileID.None;
public bool HasAlternateAttack => AlternateAttackStyle != SickleAlternateAttackStyle.None;
public bool SupportsCharge => AlternateAttackStyle is SickleAlternateAttackStyle.BloodCharge
or SickleAlternateAttackStyle.FrostCharge
or SickleAlternateAttackStyle.SoulCharge;
public double DamageMultiplier => ScaledBaseDamage / (double)Math.Max(1, BaseSickleDamage);
public int ScaledBaseDamage => GetScaledDamage(DamageLevel);
public int AttackUseTime => BaseUseTime - (AttackSpeedLevel - 1);
public float KnockbackBonus => (KnockbackLevel - 1) * 0.5f;
public float TotalBaseKnockback => BaseKnockback + KnockbackBonus;
public float ArmorPenetrationPercent => ArmorPenetrationLevel / 100f;
public float ProjectileLifetimeMultiplier => 1f + (ProjectileDurationLevel - 1) * 0.12f;
public int HitCooldownFrames => BaseHitCooldownFrames - (HitCooldownLevel - 1);
public int FatedMaximumStacks => FatedUnlocked ? GetFatedMaximumStacks(FatedMaximumStacksLevel) : 0;
public int FatedStacksPerHit => FatedUnlocked ? FatedStacksPerHitLevel : 0;
public int FatedDurationFrames => FatedUnlocked ? BaseFatedDurationFrames + (FatedDurationLevel - 1) * 60 : 0;
public bool AllNonAdvancementUpgradesMaxed => DamageLevel >= MaxDamageLevel
&& (!SupportsProjectileCount || ProjectileCount >= MaxProjectileCount)
&& (!SupportsProjectileDuration || ProjectileDurationLevel >= MaxProjectileDurationLevel)
&& ArmorPenetrationLevel >= MaxArmorPenetrationLevel
&& LifeStealLevel >= MaxLifeStealLevel
&& AttackSpeedLevel >= MaxAttackSpeedLevel
&& KnockbackLevel >= MaxKnockbackLevel
&& (!SupportsProjectileCount || HitCooldownLevel >= MaxHitCooldownLevel)
&& FatedUnlocked
&& FatedMaximumStacksLevel >= MaxFatedStackLevel
&& FatedStacksPerHitLevel >= MaxFatedStacksPerHitLevel
&& FatedDurationLevel >= MaxFatedDurationLevel;
public IReadOnlyList<DeathAltarUpgradeType> AltarUpgradeTypes => Array.Empty<DeathAltarUpgradeType>();
protected override bool CloneNewInstances => true;
public override void PreReforge()
{
DeathAltarReforgePreservation.Capture(this);
}
public override void PostReforge()
{
DeathAltarReforgePreservation.Restore(this);
ApplyDynamicStats();
}
public override void SetDefaults()
{
Item.damage = BaseSickleDamage;
Item.DamageType = DamageClass.Melee;
Item.width = 72;
Item.height = 96;
Item.useTime = BaseUseTime;
Item.useAnimation = BaseUseTime;
Item.useStyle = UsesModernSwing ? ItemUseStyleID.Shoot : ItemUseStyleID.Swing;
Item.crit = 0;
Item.scale = 1f;
Item.knockBack = BaseKnockback;
Item.rare = ItemRarityID.White;
Item.UseSound = UsesModernSwing ? null : SoundID.Item1;
Item.autoReuse = true;
Item.noUseGraphic = UsesModernSwing;
Item.noMelee = UsesModernSwing;
Item.channel = SupportsCharge;
Item.shoot = UsesModernSwing ? ModContent.ProjectileType<SickleSwingProjectile>() : AttackProjectileType;
Item.shootSpeed = AttackProjectileSpeed;
ApplyDynamicStats();
}
public override void UpdateInventory(Player player) => ApplyDynamicStats();
public override void HoldItem(Player player) => ApplyDynamicStats();
public override bool PreDrawInInventory(
SpriteBatch spriteBatch,
Vector2 position,
Rectangle frame,
Color drawColor,
Color itemColor,
Vector2 origin,
float scale)
{
ReaperFormId form = ResolvePresentationForm();
ReaperStage stage = ResolvePresentationStage(form);
Texture2D texture = ModContent.Request<Texture2D>(
ReaperCombatRegistry.GetTexturePath(form, stage)).Value;
Rectangle presentationFrame = texture.Frame();
// Preserve the footprint Terraria calculated for the physical Harvest
// Sickle shell while substituting the character's active form artwork.
// This keeps large late-game scythes inside hotbar and inventory slots.
float calculatedExtent = Math.Max(frame.Width, frame.Height) * scale;
float presentationScale = calculatedExtent / Math.Max(presentationFrame.Width, presentationFrame.Height);
spriteBatch.Draw(
texture,
position,
presentationFrame,
drawColor,
0f,
presentationFrame.Size() * 0.5f,
presentationScale,
SpriteEffects.None,
0f);
return false;
}
public override bool PreDrawInWorld(
SpriteBatch spriteBatch,
Color lightColor,
Color alphaColor,
ref float rotation,
ref float scale,
int whoAmI)
{
ReaperFormId form = ResolvePresentationForm();
ReaperStage stage = ResolvePresentationStage(form);
Texture2D texture = ModContent.Request<Texture2D>(
ReaperCombatRegistry.GetTexturePath(form, stage)).Value;
Rectangle presentationFrame = texture.Frame();
Vector2 position = Item.position - Main.screenPosition
+ new Vector2(Item.width, Item.height) * 0.5f;
float calculatedExtent = Math.Max(Item.width, Item.height) * scale;
float presentationScale = calculatedExtent
/ Math.Max(presentationFrame.Width, presentationFrame.Height);
spriteBatch.Draw(texture, position, presentationFrame, lightColor, rotation,
presentationFrame.Size() * 0.5f, presentationScale, SpriteEffects.None, 0f);
return false;
}
public override void ModifyWeaponDamage(Player player, ref StatModifier damage)
{
ReaperProgressionState state = player.GetModPlayer<MyPlayer>().ReaperProgression;
ReaperFormId form = ReaperCombatRegistry.ResolveUsableForm(state);
ReaperStage stage = ReaperDefinitions.IsBranchForm(form) ? state.GetStage(form) : ReaperStage.StageIII;
float multiplier = ReaperCombatRegistry.GetBaseDamage(form, stage) / (float)BaseSickleDamage;
multiplier *= 1f + state.GetCommonNodeLevel(ReaperCommonNode.Damage) * 0.03f;
if (form == ReaperFormId.Death)
multiplier *= 1f + state.GetCommonNodeLevel(ReaperCommonNode.DeathDamage) * 0.03f;
if (form == ReaperFormId.Blood && player.GetModPlayer<ReaperCombatPlayer>().BloodFrenzyActive)
{
int level = state.GetSkillLevel(ReaperFormId.Blood, 1);
multiplier *= 1f + (level switch { 2 => 0.12f, >= 3 => 0.16f, _ => level > 0 ? 0.08f : 0f });
}
damage *= multiplier;
}
public override float UseSpeedMultiplier(Player player)
{
ReaperProgressionState state = player.GetModPlayer<MyPlayer>().ReaperProgression;
ReaperFormId form = ReaperCombatRegistry.ResolveUsableForm(state);
ReaperStage stage = ReaperDefinitions.IsBranchForm(form) ? state.GetStage(form) : ReaperStage.StageIII;
float speed = BaseUseTime / (float)ReaperCombatRegistry.GetUseTime(form, stage);
speed *= ReaperDefinitions.GetAttackSpeedMultiplier(
state.GetCommonNodeLevel(ReaperCommonNode.AttackSpeed));
if (form == ReaperFormId.Blood && player.GetModPlayer<ReaperCombatPlayer>().BloodFrenzyActive)
{
int level = state.GetSkillLevel(ReaperFormId.Blood, 1);
speed *= 1f + (level switch { 2 => 0.08f, >= 3 => 0.11f, _ => level > 0 ? 0.05f : 0f });
}
if (form == ReaperFormId.Death)
speed *= ReaperCombatService.GetDeathPrimaryTempo(player);
return speed;
}
public override void ModifyWeaponCrit(Player player, ref float crit)
{
crit += player.GetModPlayer<MyPlayer>().ReaperProgression.GetCommonNodeLevel(ReaperCommonNode.Crit) * 10f;
}
public override void ModifyWeaponKnockback(Player player, ref StatModifier knockback)
{
// Legacy per-item knockback remains serialized only so migration can value
// old investment. Modern combat is entirely character-owned.
}
public override bool AltFunctionUse(Player player) => false;
public override bool CanUseItem(Player player)
{
if (!UsesModernSwing)
return true;
if (SickleSwingProjectile.HasBlockingPrimarySwing(player))
return false;
if (player.ownedProjectileCounts[ModContent.ProjectileType<ReaperActionControllerProjectile>()] > 0)
return false;
if (player.GetModPlayer<MyPlayer>().IsReaperFormAssemblyActive())
return false;
Item.channel = false;
Item.useTime = BaseUseTime;
Item.useAnimation = BaseUseTime;
return true;
}
public override bool Shoot(Player player, EntitySource_ItemUse_WithAmmo source, Vector2 position, Vector2 velocity, int type, int damage, float knockback)
{
if (!UsesModernSwing)
{
SpawnLegacyProjectiles(source, position, velocity, type, damage, knockback, player.whoAmI);
return false;
}
SickleCombatSnapshot snapshot = SickleCombatSnapshot.Capture(player, damage, knockback);
bool requireDeathAssembly = snapshot.Form == ReaperFormId.Death
&& ReaperCombatService.RequiresDeathPrimaryAssembly(player);
int phase = ReaperCombatService.TakeNextPrimaryPhase(player, snapshot,
out float deathTempo);
int swingDirection = player.GetModPlayer<MyPlayer>().TakeNextSwingDirection(player.direction);
Vector2 aim = velocity.SafeNormalize(Vector2.UnitX * player.direction);
int projectileIndex = Projectile.NewProjectile(
source,
player.MountedCenter,
aim,
ModContent.ProjectileType<SickleSwingProjectile>(),
damage,
knockback,
player.whoAmI,
swingDirection,
0f,
Item.type);
if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles)
return false;
if (Main.projectile[projectileIndex].ModProjectile is SickleSwingProjectile swing)
swing.Configure(snapshot, phase, deathTempo,
requireDeathAssembly: requireDeathAssembly);
return false;
}
public override void OnHitNPC(Player player, NPC target, NPC.HitInfo hit, int damageDone)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
{
if (player.whoAmI != Main.myPlayer)
return;
int healedLife = player.GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
LifeStealVisuals.Spawn(target.Center, player.whoAmI, LifeStealLevel, healedLife);
return;
}
if (Main.netMode == NetmodeID.Server)
return;
int localHealedLife = player.GetModPlayer<MyPlayer>().TryLifeSteal(damageDone, LifeStealLevel);
LifeStealVisuals.Spawn(target.Center, player.whoAmI, LifeStealLevel, localHealedLife);
MyGlobalProjectile.ApplyStatusEffects(target, player.whoAmI, LifeStealLevel, OnHitDebuffType, OnHitDebuffDuration, FatedStacksPerHit, FatedMaximumStacks, FatedDurationFrames);
}
public override void ModifyTooltips(List<TooltipLine> tooltips)
{
ReaperFormId form = ResolvePresentationForm();
string formName = Language.GetTextValue($"Mods.SoulHarvest.UI.ReaperTree.Forms.{form}.Name");
tooltips.Add(new TooltipLine(Mod, "SoulHarvestReaperForm", formName)
{
OverrideColor = ReaperCombatRegistry.GetPrimaryColor(form)
});
string special = Language.GetTextValue("Mods.SoulHarvest.Keybinds.SpecialAttack.DisplayName");
string ultimate = Language.GetTextValue("Mods.SoulHarvest.Keybinds.Ultimate.DisplayName");
string wheel = Language.GetTextValue("Mods.SoulHarvest.Keybinds.FormWheel.DisplayName");
tooltips.Add(new TooltipLine(Mod, "SoulHarvestReaperControls", $"{special} · {ultimate} · {wheel}")
{
OverrideColor = new Color(160, 205, 235)
});
}
public DeathAltarPrice GetUpgradePrice(DeathAltarUpgradeType upgradeType)
{
return upgradeType switch
{
DeathAltarUpgradeType.Advancement => PrimaryNextSickleType == ItemID.None
|| SickleTier == 3 && !AllNonAdvancementUpgradesMaxed
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(GetAdvancementEssenceCost()),
DeathAltarUpgradeType.AlternateAdvancement => AlternateNextSickleType == ItemID.None
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(GetAdvancementEssenceCost()),
DeathAltarUpgradeType.Regression => SickleTier <= 0
? DeathAltarPrice.Unavailable
: DeathAltarPrice.RefundEssence(GetRegressionEssenceRefund()),
DeathAltarUpgradeType.Damage => DamageLevel >= MaxDamageLevel
? DeathAltarPrice.Unavailable
: DamageLevel < 20
? DeathAltarPrice.Souls(DamageLevel * 2)
: DeathAltarPrice.Essence(DamageLevel < 30
? 1 + (DamageLevel - 20) / 2
: 6 + DamageLevel - 30),
DeathAltarUpgradeType.ProjectileCount => !SupportsProjectileCount || ProjectileCount >= MaxProjectileCount
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 << (ProjectileCount - 1)),
DeathAltarUpgradeType.ProjectileDuration => !SupportsProjectileDuration || ProjectileDurationLevel >= MaxProjectileDurationLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 + (ProjectileDurationLevel - 1) / 2),
DeathAltarUpgradeType.ArmorPenetration => ArmorPenetrationLevel >= MaxArmorPenetrationLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 + (ArmorPenetrationLevel - 1) / 3),
DeathAltarUpgradeType.LifeSteal => LifeStealLevel >= MaxLifeStealLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 + LifeStealLevel * 2),
DeathAltarUpgradeType.AttackSpeed => AttackSpeedLevel >= MaxAttackSpeedLevel
? DeathAltarPrice.Unavailable
: AttackSpeedLevel < 6 ? DeathAltarPrice.Souls(AttackSpeedLevel * 25) : DeathAltarPrice.Essence(1 + AttackSpeedLevel / 2),
DeathAltarUpgradeType.Knockback => KnockbackLevel >= MaxKnockbackLevel
? DeathAltarPrice.Unavailable
: KnockbackLevel < 6 ? DeathAltarPrice.Souls(KnockbackLevel * 15) : DeathAltarPrice.Essence(1 + KnockbackLevel / 3),
DeathAltarUpgradeType.HitCooldown => !SupportsProjectileCount || HitCooldownLevel >= MaxHitCooldownLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 + (HitCooldownLevel - 1) / 2),
DeathAltarUpgradeType.FatedUnlock => FatedUnlocked
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1),
DeathAltarUpgradeType.FatedMaxStacks => !FatedUnlocked || FatedMaximumStacksLevel >= MaxFatedStackLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(FatedMaximumStacksLevel),
DeathAltarUpgradeType.FatedStacksPerHit => !FatedUnlocked || FatedStacksPerHitLevel >= MaxFatedStacksPerHitLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(FatedStacksPerHitLevel * 2),
DeathAltarUpgradeType.FatedDuration => !FatedUnlocked || FatedDurationLevel >= MaxFatedDurationLevel
? DeathAltarPrice.Unavailable
: DeathAltarPrice.Essence(1 + (FatedDurationLevel - 1) / 2),
_ => DeathAltarPrice.Unavailable
};
}
public bool ApplyAltarUpgrade(Item item, DeathAltarUpgradeType upgradeType)
{
return upgradeType switch
{
DeathAltarUpgradeType.Advancement or DeathAltarUpgradeType.AlternateAdvancement => Advance(item, upgradeType),
DeathAltarUpgradeType.Regression => Regress(item),
_ => ApplyUpgrade(upgradeType)
};
}
public string GetCurrentUpgradeValue(DeathAltarUpgradeType upgradeType)
{
return upgradeType switch
{
DeathAltarUpgradeType.Advancement or DeathAltarUpgradeType.AlternateAdvancement or DeathAltarUpgradeType.Regression => Item.Name,
DeathAltarUpgradeType.Damage => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Damage", DamageLevel, ScaledBaseDamage),
DeathAltarUpgradeType.AttackSpeed => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Frames", AttackUseTime),
DeathAltarUpgradeType.Knockback => TotalBaseKnockback.ToString("0.0"),
DeathAltarUpgradeType.ProjectileCount => $"×{ProjectileCount}",
DeathAltarUpgradeType.ProjectileDuration => $"{ProjectileLifetimeMultiplier:P0}",
DeathAltarUpgradeType.ArmorPenetration => $"{ArmorPenetrationLevel}%",
DeathAltarUpgradeType.HitCooldown => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Frames", HitCooldownFrames),
DeathAltarUpgradeType.LifeSteal => $"{LifeStealLevel}%",
DeathAltarUpgradeType.FatedUnlock => Language.GetTextValue(FatedUnlocked ? "Mods.SoulHarvest.UI.Enabled" : "Mods.SoulHarvest.UI.Disabled"),
DeathAltarUpgradeType.FatedMaxStacks => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Stacks", FatedMaximumStacks),
DeathAltarUpgradeType.FatedStacksPerHit => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.StacksPerHit", FatedStacksPerHit),
DeathAltarUpgradeType.FatedDuration => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Seconds", FatedDurationFrames / 60f),
_ => string.Empty
};
}
public string GetNextUpgradeValue(DeathAltarUpgradeType upgradeType)
{
return upgradeType switch
{
DeathAltarUpgradeType.Advancement => Lang.GetItemNameValue(PrimaryNextSickleType),
DeathAltarUpgradeType.AlternateAdvancement => Lang.GetItemNameValue(AlternateNextSickleType),
DeathAltarUpgradeType.Regression => Lang.GetItemNameValue(GetPreviousSickleType()),
DeathAltarUpgradeType.Damage => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Damage", DamageLevel + 1, GetScaledDamage(DamageLevel + 1)),
DeathAltarUpgradeType.AttackSpeed => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Frames", AttackUseTime - 1),
DeathAltarUpgradeType.Knockback => (TotalBaseKnockback + 0.5f).ToString("0.0"),
DeathAltarUpgradeType.ProjectileCount => $"×{ProjectileCount + 1}",
DeathAltarUpgradeType.ProjectileDuration => $"{1f + ProjectileDurationLevel * 0.12f:P0}",
DeathAltarUpgradeType.ArmorPenetration => $"{ArmorPenetrationLevel + 1}%",
DeathAltarUpgradeType.HitCooldown => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Frames", HitCooldownFrames - 1),
DeathAltarUpgradeType.LifeSteal => $"{LifeStealLevel + 1}%",
DeathAltarUpgradeType.FatedUnlock => Language.GetTextValue("Mods.SoulHarvest.UI.Enabled"),
DeathAltarUpgradeType.FatedMaxStacks => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Stacks", GetFatedMaximumStacks(FatedMaximumStacksLevel + 1)),
DeathAltarUpgradeType.FatedStacksPerHit => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.StacksPerHit", FatedStacksPerHitLevel + 1),
DeathAltarUpgradeType.FatedDuration => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Seconds", (BaseFatedDurationFrames + FatedDurationLevel * 60) / 60f),
_ => string.Empty
};
}
public DeathAltarUnavailableReason GetUnavailableReason(DeathAltarUpgradeType upgradeType)
{
if (upgradeType == DeathAltarUpgradeType.Advancement
&& PrimaryNextSickleType != ItemID.None
&& SickleTier == 3
&& !AllNonAdvancementUpgradesMaxed)
{
return DeathAltarUnavailableReason.RequiresAllSickleUpgrades;
}
if (upgradeType == DeathAltarUpgradeType.Regression && SickleTier <= 0)
return DeathAltarUnavailableReason.RequiresHigherSickleForm;
if ((upgradeType is DeathAltarUpgradeType.ProjectileCount or DeathAltarUpgradeType.ProjectileDuration or DeathAltarUpgradeType.HitCooldown) && !SupportsProjectileCount)
return DeathAltarUnavailableReason.RequiresProjectile;
if ((upgradeType is DeathAltarUpgradeType.FatedMaxStacks or DeathAltarUpgradeType.FatedStacksPerHit or DeathAltarUpgradeType.FatedDuration) && !FatedUnlocked)
return DeathAltarUnavailableReason.RequiresFatedUnlock;
return DeathAltarUnavailableReason.Maxed;
}
internal void SpawnAttackProjectiles(Player player, Projectile swingProjectile, float charge, bool heavySlash)
{
if (!SupportsProjectileCount || AttackProjectileType <= ProjectileID.None)
return;
int count = ProjectileCount;
if (heavySlash)
{
float projectileMultiplier = SupportsCharge
? MathHelper.Lerp(1f, 2f, MathHelper.Clamp(charge, 0f, 1f))
: 1.5f;
count = Math.Max(1, (int)Math.Ceiling(ProjectileCount * projectileMultiplier));
}
count = Math.Clamp(count, 1, MaxProjectileCount * 2);
if (heavySlash && AlternateAttackStyle == SickleAlternateAttackStyle.BoneBarrage)
count = Math.Max(3, count);
else if (heavySlash && AlternateAttackStyle == SickleAlternateAttackStyle.VoidRitual)
count = Math.Max(4, count);
float spread = MathHelper.ToRadians(heavySlash && AlternateAttackStyle == SickleAlternateAttackStyle.BoneBarrage ? 18f : heavySlash ? 12f : 7f);
float speed = AttackProjectileSpeed * (heavySlash ? MathHelper.Lerp(1.1f, 1.35f, charge) : 1f);
int damage = Math.Max(1, (int)(swingProjectile.damage * ProjectileDamageFactor * (heavySlash ? MathHelper.Lerp(1.25f, 1.9f, charge) : 1f)));
Vector2 aim = swingProjectile.velocity.SafeNormalize(Vector2.UnitX * player.direction);
IEntitySource source = swingProjectile.GetSource_FromThis();
for (int index = 0; index < count; index++)
{
float spreadSlot = GetForwardBiasedSpreadSlot(index, (swingProjectile.identity & 1) == 0 ? 1f : -1f);
Vector2 projectileDirection = heavySlash && AlternateAttackStyle == SickleAlternateAttackStyle.VoidRitual
? aim.RotatedBy(index * MathHelper.TwoPi / count)
: aim.RotatedBy(spreadSlot * spread);
Vector2 projectileVelocity = projectileDirection * speed;
Projectile.NewProjectile(
source,
player.MountedCenter + projectileDirection * 28f,
projectileVelocity,
AttackProjectileType,
damage,
swingProjectile.knockBack,
player.whoAmI,
heavySlash ? 1f : 0f,
charge,
spreadSlot);
}
}
private bool ApplyUpgrade(DeathAltarUpgradeType upgradeType)
{
if (!GetUpgradePrice(upgradeType).IsAvailable)
return false;
switch (upgradeType)
{
case DeathAltarUpgradeType.Damage:
DamageLevel++;
break;
case DeathAltarUpgradeType.ProjectileCount:
ProjectileCount++;
break;
case DeathAltarUpgradeType.ProjectileDuration:
ProjectileDurationLevel++;
break;
case DeathAltarUpgradeType.ArmorPenetration:
ArmorPenetrationLevel++;
break;
case DeathAltarUpgradeType.LifeSteal:
LifeStealLevel++;
break;
case DeathAltarUpgradeType.AttackSpeed:
AttackSpeedLevel++;
break;
case DeathAltarUpgradeType.Knockback:
KnockbackLevel++;
break;
case DeathAltarUpgradeType.HitCooldown:
HitCooldownLevel++;
break;
case DeathAltarUpgradeType.FatedUnlock:
FatedUnlocked = true;
break;
case DeathAltarUpgradeType.FatedMaxStacks:
FatedMaximumStacksLevel++;
break;
case DeathAltarUpgradeType.FatedStacksPerHit:
FatedStacksPerHitLevel++;
break;
case DeathAltarUpgradeType.FatedDuration:
FatedDurationLevel++;
break;
default:
return false;
}
ApplyDynamicStats();
return true;
}
internal bool Advance(Item item, DeathAltarUpgradeType advancementType)
{
int nextType = advancementType == DeathAltarUpgradeType.AlternateAdvancement
? AlternateNextSickleType
: PrimaryNextSickleType;
return TransformSickle(item, nextType, recordCurrentForm: true);
}
private bool Regress(Item item)
{
if (SickleTier <= 0)
return false;
return TransformSickle(item, GetPreviousSickleType(), recordCurrentForm: false);
}
private bool TransformSickle(Item item, int targetType, bool recordCurrentForm)
{
if (targetType <= ItemID.None || ItemLoader.GetItem(targetType) is not NormalSickle targetTemplate)
return false;
int expectedTier = SickleTier + (recordCurrentForm ? 1 : -1);
if (targetTemplate.SickleTier != expectedTier)
return false;
int damageLevel = DamageLevel;
int projectileCount = ProjectileCount;
int projectileDurationLevel = ProjectileDurationLevel;
int armorPenetrationLevel = ArmorPenetrationLevel;
int lifeStealLevel = LifeStealLevel;
int attackSpeedLevel = AttackSpeedLevel;
int knockbackLevel = KnockbackLevel;
int hitCooldownLevel = HitCooldownLevel;
bool fatedUnlocked = FatedUnlocked;
int fatedMaximumStacksLevel = FatedMaximumStacksLevel;
int fatedStacksPerHitLevel = FatedStacksPerHitLevel;
int fatedDurationLevel = FatedDurationLevel;
int tierOneHistory = previousTierOneType;
int tierTwoHistory = previousTierTwoType;
int tierThreeHistory = previousTierThreeType;
int currentType = item.type;
if (recordCurrentForm)
{
switch (SickleTier)
{
case 1:
tierOneHistory = currentType;
break;
case 2:
tierTwoHistory = currentType;
break;
case 3:
tierThreeHistory = currentType;
break;
}
}
int prefix = item.prefix;
bool favorited = item.favorited;
item.SetDefaults(targetType);
if (prefix > 0)
item.Prefix(prefix);
if (item.ModItem is not NormalSickle upgraded)
return false;
upgraded.DamageLevel = damageLevel;
upgraded.ProjectileCount = projectileCount;
upgraded.ProjectileDurationLevel = projectileDurationLevel;
upgraded.ArmorPenetrationLevel = armorPenetrationLevel;
upgraded.LifeStealLevel = lifeStealLevel;
upgraded.AttackSpeedLevel = attackSpeedLevel;
upgraded.KnockbackLevel = knockbackLevel;
upgraded.HitCooldownLevel = hitCooldownLevel;
upgraded.FatedUnlocked = fatedUnlocked;
upgraded.FatedMaximumStacksLevel = fatedMaximumStacksLevel;
upgraded.FatedStacksPerHitLevel = fatedStacksPerHitLevel;
upgraded.FatedDurationLevel = fatedDurationLevel;
upgraded.previousTierOneType = tierOneHistory;
upgraded.previousTierTwoType = tierTwoHistory;
upgraded.previousTierThreeType = tierThreeHistory;
upgraded.ApplyDynamicStats();
item.favorited = favorited;
return true;
}
private int GetPreviousSickleType()
{
int storedType = SickleTier switch
{
2 => previousTierOneType,
3 => previousTierTwoType,
4 => previousTierThreeType,
_ => ItemID.None
};
if (storedType > ItemID.None
&& ItemLoader.GetItem(storedType) is NormalSickle storedSickle
&& storedSickle.SickleTier == SickleTier - 1)
{
return storedType;
}
// Items created before regression history existed use a deterministic
// canonical predecessor. Newly advanced items always return along the exact
// branch the player originally chose.
return this switch
{
SkullSickle or BloodSickle => ModContent.ItemType<NormalSickle>(),
FrostSickle => ModContent.ItemType<BloodSickle>(),
DevilSickle => ModContent.ItemType<SkullSickle>(),
SoulSickle => ModContent.ItemType<DevilSickle>(),
VoidSickle => ModContent.ItemType<FrostSickle>(),
DeathSickle => ModContent.ItemType<SoulSickle>(),
_ => ItemID.None
};
}
public override void SaveData(TagCompound tag)
{
tag[nameof(DamageLevel)] = DamageLevel;
tag[nameof(ProjectileCount)] = ProjectileCount;
tag[nameof(ProjectileDurationLevel)] = ProjectileDurationLevel;
tag[nameof(ArmorPenetrationLevel)] = ArmorPenetrationLevel;
tag[nameof(LifeStealLevel)] = LifeStealLevel;
tag[nameof(AttackSpeedLevel)] = AttackSpeedLevel;
tag[nameof(KnockbackLevel)] = KnockbackLevel;
tag[nameof(HitCooldownLevel)] = HitCooldownLevel;
tag[nameof(FatedUnlocked)] = FatedUnlocked;
tag[nameof(FatedMaximumStacksLevel)] = FatedMaximumStacksLevel;
tag[nameof(FatedStacksPerHitLevel)] = FatedStacksPerHitLevel;
tag[nameof(FatedDurationLevel)] = FatedDurationLevel;
tag[nameof(previousTierOneType)] = previousTierOneType;
tag[nameof(previousTierTwoType)] = previousTierTwoType;
tag[nameof(previousTierThreeType)] = previousTierThreeType;
}
public override void LoadData(TagCompound tag)
{
DamageLevel = Math.Clamp(tag.GetInt(nameof(DamageLevel)), 1, MaxDamageLevel);
ProjectileCount = Math.Clamp(tag.GetInt(nameof(ProjectileCount)), 1, MaxProjectileCount);
ProjectileDurationLevel = Math.Clamp(tag.GetInt(nameof(ProjectileDurationLevel)), 1, MaxProjectileDurationLevel);
ArmorPenetrationLevel = Math.Clamp(tag.GetInt(nameof(ArmorPenetrationLevel)), 1, MaxArmorPenetrationLevel);
LifeStealLevel = Math.Clamp(tag.GetInt(nameof(LifeStealLevel)), 0, MaxLifeStealLevel);
AttackSpeedLevel = Math.Clamp(tag.GetInt(nameof(AttackSpeedLevel)), 1, MaxAttackSpeedLevel);
KnockbackLevel = Math.Clamp(tag.GetInt(nameof(KnockbackLevel)), 1, MaxKnockbackLevel);
HitCooldownLevel = Math.Clamp(tag.GetInt(nameof(HitCooldownLevel)), 1, MaxHitCooldownLevel);
FatedUnlocked = tag.GetBool(nameof(FatedUnlocked));
FatedMaximumStacksLevel = Math.Clamp(tag.GetInt(nameof(FatedMaximumStacksLevel)), 1, MaxFatedStackLevel);
FatedStacksPerHitLevel = Math.Clamp(tag.GetInt(nameof(FatedStacksPerHitLevel)), 1, MaxFatedStacksPerHitLevel);
FatedDurationLevel = Math.Clamp(tag.GetInt(nameof(FatedDurationLevel)), 1, MaxFatedDurationLevel);
previousTierOneType = tag.GetInt(nameof(previousTierOneType));
previousTierTwoType = tag.GetInt(nameof(previousTierTwoType));
previousTierThreeType = tag.GetInt(nameof(previousTierThreeType));
ApplyDynamicStats();
}
public override void NetSend(BinaryWriter writer)
{
writer.Write((byte)DamageLevel);
writer.Write((byte)ProjectileCount);
writer.Write((byte)ProjectileDurationLevel);
writer.Write((byte)ArmorPenetrationLevel);
writer.Write((byte)LifeStealLevel);
writer.Write((byte)AttackSpeedLevel);
writer.Write((byte)KnockbackLevel);
writer.Write((byte)HitCooldownLevel);
writer.Write(FatedUnlocked);
writer.Write((byte)FatedMaximumStacksLevel);
writer.Write((byte)FatedStacksPerHitLevel);
writer.Write((byte)FatedDurationLevel);
writer.Write(previousTierOneType);
writer.Write(previousTierTwoType);
writer.Write(previousTierThreeType);
}
public override void NetReceive(BinaryReader reader)
{
DamageLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxDamageLevel);
ProjectileCount = Math.Clamp((int)reader.ReadByte(), 1, MaxProjectileCount);
ProjectileDurationLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxProjectileDurationLevel);
ArmorPenetrationLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxArmorPenetrationLevel);
LifeStealLevel = Math.Clamp((int)reader.ReadByte(), 0, MaxLifeStealLevel);
AttackSpeedLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxAttackSpeedLevel);
KnockbackLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxKnockbackLevel);
HitCooldownLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxHitCooldownLevel);
FatedUnlocked = reader.ReadBoolean();
FatedMaximumStacksLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxFatedStackLevel);
FatedStacksPerHitLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxFatedStacksPerHitLevel);
FatedDurationLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxFatedDurationLevel);
previousTierOneType = reader.ReadInt32();
previousTierTwoType = reader.ReadInt32();
previousTierThreeType = reader.ReadInt32();
ApplyDynamicStats();
}
public override void AddRecipes()
{
CreateRecipe()
.AddRecipeGroup(RecipeGroupID.Wood, 10)
.AddRecipeGroup(RecipeGroupID.IronBar, 5)
.AddTile(TileID.Anvils)
.Register();
}
internal void ApplyDynamicStats()
{
// Percentage penetration is applied through NPC.HitModifiers so this must not
// also contribute vanilla's flat armor penetration.
Item.ArmorPenetration = 0;
}
private static int GetFatedMaximumStacks(int level)
{
return 1 + (Math.Clamp(level, 1, MaxFatedStackLevel) - 1) * 3;
}
private int GetScaledDamage(int level)
{
level = Math.Clamp(level, 1, MaxDamageLevel);
if (level == 1)
return Math.Max(1, BaseSickleDamage);
const int lateGameStartLevel = 29;
double earlyMidGrowth = GetUnscaledDamageGrowth(Math.Min(level, lateGameStartLevel - 1)) * 0.25d;
double lateGameGrowth = level < lateGameStartLevel
? 0d
: (GetUnscaledDamageGrowth(level) - GetUnscaledDamageGrowth(lateGameStartLevel - 1)) * 0.70d;
return Math.Max(1, (int)Math.Round(BaseSickleDamage + earlyMidGrowth + lateGameGrowth));
}
private double GetUnscaledDamageGrowth(int level)
{
double progress = (level - 1d) / (MaxDamageLevel - 1d);
double targetDamage = 1_200d + SickleTier * 550d;
double guaranteedGrowth = level - 1d;
double curvedGrowth = Math.Max(0d, targetDamage - BaseSickleDamage - (MaxDamageLevel - 1d)) * Math.Pow(progress, 2.3d);
return guaranteedGrowth + curvedGrowth;
}
private int GetAdvancementEssenceCost()
{
// The price is based on the target form: form 2 is the introduction, while
// forms 3-5 become deliberate long-term milestones.
return SickleTier switch
{
0 => 1,
1 => 16,
2 => 32,
3 => 99,
_ => 999
};
}
private int GetRegressionEssenceRefund()
{
// Refund exactly the cost paid to enter the current form.
return SickleTier switch
{
1 => 1,
2 => 16,
3 => 32,
4 => 99,
_ => 0
};
}
private void SpawnLegacyProjectiles(IEntitySource source, Vector2 position, Vector2 velocity, int type, int damage, float knockback, int owner)
{
if (!SupportsProjectileCount || type <= ProjectileID.None)
return;
int count = Math.Clamp(ProjectileCount, 1, MaxProjectileCount);
float spread = MathHelper.ToRadians(7f);
for (int index = 0; index < count; index++)
{
float offset = GetForwardBiasedSpreadSlot(index, 1f) * spread;
Projectile.NewProjectile(source, position, velocity.RotatedBy(offset), type, damage, knockback, owner);
}
}
private static float GetForwardBiasedSpreadSlot(int index, float firstSide)
{
if (index <= 0)
return 0f;
int distance = (index + 1) / 2;
float side = index % 2 == 1 ? firstSide : -firstSide;
return distance * side;
}
private Color GetGradientColor()
{
float pulse = (float)Math.Sin(Main.GlobalTimeWrappedHourly * 2.4f) * 0.16f + 0.84f;
return SickleColor * pulse;
}
private static ReaperFormId ResolvePresentationForm()
{
if (Main.gameMenu || Main.LocalPlayer is null || !Main.LocalPlayer.active)
return ReaperFormId.Base;
return ReaperCombatRegistry.ResolveUsableForm(Main.LocalPlayer.GetModPlayer<MyPlayer>().ReaperProgression);
}
private static ReaperStage ResolvePresentationStage(ReaperFormId form)
{
if (form == ReaperFormId.Death)
return ReaperStage.StageIII;
if (!ReaperDefinitions.IsBranchForm(form)
|| Main.gameMenu
|| Main.LocalPlayer is null
|| !Main.LocalPlayer.active)
{
return ReaperStage.StageI;
}
ReaperStage stage = Main.LocalPlayer.GetModPlayer<MyPlayer>().ReaperProgression.GetStage(form);
return stage is >= ReaperStage.StageI and <= ReaperStage.StageIII
? stage
: ReaperStage.StageI;
}
}