using SoulHarvest.Common; using SoulHarvest.Tiles; using Microsoft.Xna.Framework; 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; [AutoloadEquip(EquipType.Wings)] public class DeathWings : ModItem, IDeathAltarUpgradeable { public const int MaxWingLevel = 10; private static readonly DeathAltarUpgradeType[] UpgradeTypes = [ DeathAltarUpgradeType.WingFlightTime, DeathAltarUpgradeType.WingHorizontalSpeed, DeathAltarUpgradeType.WingAcceleration, DeathAltarUpgradeType.WingVerticalPower, DeathAltarUpgradeType.WingInfiniteFlight ]; public int FlightTimeLevel { get; private set; } = 1; public int HorizontalSpeedLevel { get; private set; } = 1; public int AccelerationLevel { get; private set; } = 1; public int VerticalPowerLevel { get; private set; } = 1; public bool InfiniteFlightUnlocked { get; private set; } public int FlightTimeTicks => 60 + (FlightTimeLevel - 1) * 120 / 9; public float HorizontalSpeed => MathHelper.Lerp(6f, 25f, LevelProgress(HorizontalSpeedLevel)); public float HorizontalAcceleration => MathHelper.Lerp(0.15f, 1.2f, LevelProgress(AccelerationLevel)); public float VerticalPower => MathHelper.Lerp(1f, 8f, LevelProgress(VerticalPowerLevel)); public IReadOnlyList AltarUpgradeTypes => UpgradeTypes; protected override bool CloneNewInstances => true; public override void PreReforge() { DeathAltarReforgePreservation.Capture(this); } public override void PostReforge() { DeathAltarReforgePreservation.Restore(this); } public override void SetStaticDefaults() { ArmorIDs.Wing.Sets.Stats[Item.wingSlot] = new WingStats(60, 6f, 1f); } public override void SetDefaults() { Item.width = 22; Item.height = 22; Item.accessory = true; Item.rare = ItemRarityID.Red; Item.value = Item.sellPrice(gold: 10); } public override void UpdateAccessory(Player player, bool hideVisual) { player.wingTimeMax = Math.Max(player.wingTimeMax, FlightTimeTicks); } public override void VerticalWingSpeeds( Player player, ref float ascentWhenFalling, ref float ascentWhenRising, ref float maxCanAscendMultiplier, ref float maxAscentMultiplier, ref float constantAscend) { DeathWings equippedWings = FindEquippedWings(player, player.wingsLogic) ?? this; if (equippedWings.InfiniteFlightUnlocked) player.wingTime = player.wingTimeMax; float progress = LevelProgress(equippedWings.VerticalPowerLevel); ascentWhenFalling = MathHelper.Lerp(0.85f, 8f, progress); ascentWhenRising = MathHelper.Lerp(0.15f, 8f, progress); maxAscentMultiplier = equippedWings.VerticalPower; } public override void HorizontalWingSpeeds(Player player, ref float speed, ref float acceleration) { DeathWings equippedWings = FindEquippedWings(player, player.wingsLogic) ?? this; if (equippedWings.InfiniteFlightUnlocked) player.wingTime = player.wingTimeMax; speed = Math.Max(speed, equippedWings.HorizontalSpeed); acceleration = Math.Max(acceleration, equippedWings.HorizontalAcceleration); } public override bool WingUpdate(Player player, bool inUse) { if (!player.active || player.dead) return false; DeathWings visibleWings = FindEquippedWings(player, player.wings) ?? this; float mastery = visibleWings.VisualMastery; MyPlayer modPlayer = player.GetModPlayer(); bool gliding = !inUse && player.controlJump && Math.Abs(player.velocity.Y) > 0.2f; bool justStartedFlying = inUse && !modPlayer.DeathWingsActiveLastTick && !modPlayer.DeathWingsActiveThisTick; modPlayer.DeathWingsActiveThisTick = inUse || gliding; float flightSpeed = player.velocity.Length(); if ((inUse || gliding) && Main.netMode != NetmodeID.MultiplayerClient) { modPlayer.RecordDeathWingDomainTrail(mastery); } if (Main.netMode == NetmodeID.Server) return false; if ((inUse || gliding) && flightSpeed > 1.25f) { // Let Terraria render the entire character at its recorded shadow // positions. The body-width cut remains behind these silhouettes, so // rapid movement reads as one layered deathly afterimage instead of // a detached wing-tip effect. player.armorEffectDrawShadow = true; if (visibleWings.InfiniteFlightUnlocked && flightSpeed > 10f) player.armorEffectDrawShadowLokis = true; } if (justStartedFlying) SpawnTakeoffBurst(player, mastery); if (inUse) { GetWingTips(player, mastery, out Vector2 upperTip, out Vector2 lowerTip, out _); modPlayer.RecordDeathWingTrail(upperTip, lowerTip, mastery); SpawnFlightEffects(player, mastery, visibleWings.InfiniteFlightUnlocked); } else if (gliding) { GetWingTips(player, mastery, out Vector2 upperTip, out Vector2 lowerTip, out _); modPlayer.RecordDeathWingTrail(upperTip, lowerTip, mastery); SpawnGlideEffects(player, mastery); } return false; } public override void ModifyTooltips(List tooltips) { string stats = Language.GetTextValue( "Mods.SoulHarvest.UI.WingStats", FlightTimeTicks, HorizontalSpeed.ToString("0.00"), HorizontalAcceleration.ToString("0.00"), VerticalPower.ToString("0.00"), Language.GetTextValue(InfiniteFlightUnlocked ? "Mods.SoulHarvest.UI.Enabled" : "Mods.SoulHarvest.UI.Disabled")); tooltips.Add(new TooltipLine(Mod, "DeathWingStats", stats) { OverrideColor = new Color(235, 65, 85) }); tooltips.Add(new TooltipLine(Mod, "SoulHarvestAltarHint", Language.GetTextValue("Mods.SoulHarvest.UI.AltarHint"))); } public DeathAltarPrice GetUpgradePrice(DeathAltarUpgradeType upgradeType) { return upgradeType switch { DeathAltarUpgradeType.WingFlightTime => LevelPrice(FlightTimeLevel, 30), DeathAltarUpgradeType.WingHorizontalSpeed => LevelPrice(HorizontalSpeedLevel, 40), DeathAltarUpgradeType.WingAcceleration => LevelPrice(AccelerationLevel, 35), DeathAltarUpgradeType.WingVerticalPower => LevelPrice(VerticalPowerLevel, 45), DeathAltarUpgradeType.WingInfiniteFlight => InfiniteFlightUnlocked || !AllFlightStatsMaxed ? DeathAltarPrice.Unavailable : DeathAltarPrice.Essence(HalvedCost(25)), _ => DeathAltarPrice.Unavailable }; } public bool ApplyAltarUpgrade(Item item, DeathAltarUpgradeType upgradeType) { if (!GetUpgradePrice(upgradeType).IsAvailable) return false; switch (upgradeType) { case DeathAltarUpgradeType.WingFlightTime: FlightTimeLevel++; break; case DeathAltarUpgradeType.WingHorizontalSpeed: HorizontalSpeedLevel++; break; case DeathAltarUpgradeType.WingAcceleration: AccelerationLevel++; break; case DeathAltarUpgradeType.WingVerticalPower: VerticalPowerLevel++; break; case DeathAltarUpgradeType.WingInfiniteFlight: InfiniteFlightUnlocked = true; break; default: return false; } return true; } public string GetCurrentUpgradeValue(DeathAltarUpgradeType upgradeType) { return upgradeType switch { DeathAltarUpgradeType.WingFlightTime => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Frames", FlightTimeTicks), DeathAltarUpgradeType.WingHorizontalSpeed => HorizontalSpeed.ToString("0.00"), DeathAltarUpgradeType.WingAcceleration => HorizontalAcceleration.ToString("0.00"), DeathAltarUpgradeType.WingVerticalPower => VerticalPower.ToString("0.00"), DeathAltarUpgradeType.WingInfiniteFlight => Language.GetTextValue(InfiniteFlightUnlocked ? "Mods.SoulHarvest.UI.Enabled" : "Mods.SoulHarvest.UI.Disabled"), _ => string.Empty }; } public string GetNextUpgradeValue(DeathAltarUpgradeType upgradeType) { return upgradeType switch { DeathAltarUpgradeType.WingFlightTime => Language.GetTextValue("Mods.SoulHarvest.UI.UpgradeValues.Frames", 60 + FlightTimeLevel * 120 / 9), DeathAltarUpgradeType.WingHorizontalSpeed => MathHelper.Lerp(6f, 25f, LevelProgress(HorizontalSpeedLevel + 1)).ToString("0.00"), DeathAltarUpgradeType.WingAcceleration => MathHelper.Lerp(0.15f, 1.2f, LevelProgress(AccelerationLevel + 1)).ToString("0.00"), DeathAltarUpgradeType.WingVerticalPower => MathHelper.Lerp(1f, 8f, LevelProgress(VerticalPowerLevel + 1)).ToString("0.00"), DeathAltarUpgradeType.WingInfiniteFlight => Language.GetTextValue("Mods.SoulHarvest.UI.Enabled"), _ => string.Empty }; } public DeathAltarUnavailableReason GetUnavailableReason(DeathAltarUpgradeType upgradeType) { return upgradeType == DeathAltarUpgradeType.WingInfiniteFlight && !InfiniteFlightUnlocked && !AllFlightStatsMaxed ? DeathAltarUnavailableReason.RequiresWingMastery : DeathAltarUnavailableReason.Maxed; } public override void SaveData(TagCompound tag) { tag[nameof(FlightTimeLevel)] = FlightTimeLevel; tag[nameof(HorizontalSpeedLevel)] = HorizontalSpeedLevel; tag[nameof(AccelerationLevel)] = AccelerationLevel; tag[nameof(VerticalPowerLevel)] = VerticalPowerLevel; tag[nameof(InfiniteFlightUnlocked)] = InfiniteFlightUnlocked; } public override void LoadData(TagCompound tag) { FlightTimeLevel = Math.Clamp(tag.GetInt(nameof(FlightTimeLevel)), 1, MaxWingLevel); HorizontalSpeedLevel = Math.Clamp(tag.GetInt(nameof(HorizontalSpeedLevel)), 1, MaxWingLevel); AccelerationLevel = Math.Clamp(tag.GetInt(nameof(AccelerationLevel)), 1, MaxWingLevel); VerticalPowerLevel = Math.Clamp(tag.GetInt(nameof(VerticalPowerLevel)), 1, MaxWingLevel); InfiniteFlightUnlocked = tag.GetBool(nameof(InfiniteFlightUnlocked)); } public override void NetSend(BinaryWriter writer) { writer.Write((byte)FlightTimeLevel); writer.Write((byte)HorizontalSpeedLevel); writer.Write((byte)AccelerationLevel); writer.Write((byte)VerticalPowerLevel); writer.Write(InfiniteFlightUnlocked); } public override void NetReceive(BinaryReader reader) { FlightTimeLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxWingLevel); HorizontalSpeedLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxWingLevel); AccelerationLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxWingLevel); VerticalPowerLevel = Math.Clamp((int)reader.ReadByte(), 1, MaxWingLevel); InfiniteFlightUnlocked = reader.ReadBoolean(); } public override void AddRecipes() { CreateRecipe() .AddIngredient(ItemID.SoulofFlight, 20) .AddIngredient() .AddTile(ModContent.TileType()) .Register(); } private bool AllFlightStatsMaxed => FlightTimeLevel >= MaxWingLevel && HorizontalSpeedLevel >= MaxWingLevel && AccelerationLevel >= MaxWingLevel && VerticalPowerLevel >= MaxWingLevel; private float VisualMastery => MathHelper.Clamp( (LevelProgress(FlightTimeLevel) + LevelProgress(HorizontalSpeedLevel) + LevelProgress(AccelerationLevel) + LevelProgress(VerticalPowerLevel)) / 4f + (InfiniteFlightUnlocked ? 0.15f : 0f), 0f, 1f); private static DeathAltarPrice LevelPrice(int level, int soulMultiplier) { if (level >= MaxWingLevel) return DeathAltarPrice.Unavailable; return level < 6 ? DeathAltarPrice.Souls(HalvedCost(level * soulMultiplier)) : DeathAltarPrice.Essence(HalvedCost(level)); } private static int HalvedCost(int cost) => Math.Max(1, (cost + 1) / 2); private static float LevelProgress(int level) { return MathHelper.Clamp((level - 1) / 9f, 0f, 1f); } private static DeathWings? FindEquippedWings(Player player, int wingSlot) { DeathWings? bestMatch = null; float bestMastery = -1f; foreach (Item item in player.armor) { if (item.wingSlot != wingSlot || item.ModItem is not DeathWings candidate) continue; float mastery = candidate.VisualMastery; if (mastery > bestMastery) { bestMatch = candidate; bestMastery = mastery; } } return bestMatch; } private static void SpawnFlightEffects(Player player, float mastery, bool infiniteFlight) { GetWingTips(player, mastery, out Vector2 upperTip, out Vector2 lowerTip, out Vector2 wingRoot); Color emberColor = Color.Lerp(new Color(82, 3, 16), new Color(235, 24, 92), mastery); Color soulColor = Color.Lerp(new Color(48, 4, 68), new Color(218, 92, 255), mastery); int interval = mastery >= 0.35f ? 1 : 2; if ((Main.GameUpdateCount + (ulong)player.whoAmI) % (ulong)interval == 0) { SpawnWingTipEmber(player, upperTip, emberColor, mastery); SpawnWingTipEmber(player, lowerTip, emberColor, mastery); } if (Main.rand.NextFloat() < 0.16f + mastery * 0.34f) { Vector2 position = Vector2.Lerp(upperTip, lowerTip, Main.rand.NextFloat()) + Main.rand.NextVector2Circular(3f, 3f); Vector2 velocity = -player.velocity * 0.12f + Main.rand.NextVector2Circular(0.55f, 0.55f); Dust soul = Dust.NewDustPerfect(position, DustID.Smoke, velocity, 65, soulColor, 0.55f + mastery * 0.55f); soul.noGravity = true; soul.fadeIn = 0.75f + mastery * 0.25f; } float speed = player.velocity.Length(); if (mastery >= 0.25f && speed > 6f && (Main.GameUpdateCount + (ulong)player.whoAmI) % 2UL == 0) { Vector2 movementDirection = player.velocity.SafeNormalize(Vector2.UnitY); int streakCount = mastery >= 0.8f ? 2 : 1; for (int index = 0; index < streakCount; index++) { Vector2 position = player.Center - movementDirection * Main.rand.NextFloat(14f, 30f + speed * 1.5f) + Main.rand.NextVector2Circular(8f, 10f); int dustType = mastery >= 0.45f ? DustID.RainbowMk2 : DustID.Shadowflame; Dust streak = Dust.NewDustPerfect(position, dustType, -player.velocity * 0.06f, 70, Color.Lerp(emberColor, soulColor, mastery * 0.38f), 0.4f + mastery * 0.35f); streak.noGravity = true; streak.fadeIn = 0.65f; } } if (mastery >= 0.7f && Main.rand.NextBool(18)) { Dust bat = Dust.NewDustPerfect( wingRoot + Main.rand.NextVector2Circular(7f, 10f), DustID.BatScepter, -player.velocity * 0.08f + Main.rand.NextVector2Circular(1.4f, 1.1f), 85, emberColor, 0.75f + mastery * 0.3f); bat.noGravity = true; } if (infiniteFlight && (Main.GameUpdateCount + (ulong)player.whoAmI) % 3UL == 0) { float angle = Main.GlobalTimeWrappedHourly * 5.5f + player.whoAmI * 0.73f; Vector2 orbit = new Vector2((float)Math.Cos(angle) * 20f, (float)Math.Sin(angle) * 11f); Dust orbitingSoul = Dust.NewDustPerfect(wingRoot + orbit, DustID.DungeonSpirit, -player.velocity * 0.05f, 50, soulColor, 0.75f); orbitingSoul.noGravity = true; } Lighting.AddLight(wingRoot, new Vector3(0.55f + mastery * 0.35f, 0.035f + mastery * 0.04f, 0.08f + mastery * 0.18f)); } private static void SpawnGlideEffects(Player player, float mastery) { if ((Main.GameUpdateCount + (ulong)player.whoAmI) % 4UL != 0) return; GetWingTips(player, mastery, out Vector2 upperTip, out Vector2 lowerTip, out _); Color color = Color.Lerp(new Color(70, 2, 14), new Color(205, 20, 78), mastery); Vector2 position = Main.rand.NextBool() ? upperTip : lowerTip; Dust ember = Dust.NewDustPerfect( position + Main.rand.NextVector2Circular(2f, 2f), DustID.Smoke, -player.velocity * 0.09f + Main.rand.NextVector2Circular(0.35f, 0.35f), 100, color, 0.55f + mastery * 0.25f); ember.noGravity = true; } private static void SpawnTakeoffBurst(Player player, float mastery) { GetWingTips(player, mastery, out _, out _, out Vector2 wingRoot); Color color = Color.Lerp(new Color(215, 20, 35), new Color(255, 90, 165), mastery); int particleCount = 8 + (int)(mastery * 10f); for (int index = 0; index < particleCount; index++) { Vector2 velocity = Main.rand.NextVector2CircularEdge(2.2f + mastery * 2.1f, 1.7f + mastery * 1.4f) - player.velocity * 0.08f; int dustType = index % 4 == 0 ? DustID.Shadowflame : DustID.RainbowMk2; Dust burst = Dust.NewDustPerfect( wingRoot + Main.rand.NextVector2Circular(7f, 10f), dustType, velocity, 65, color, 0.65f + Main.rand.NextFloat(0.25f) + mastery * 0.25f); burst.noGravity = true; burst.fadeIn = 0.7f; } } private static void SpawnWingTipEmber(Player player, Vector2 position, Color color, float mastery) { Vector2 velocity = -player.velocity * MathHelper.Lerp(0.08f, 0.18f, mastery) + new Vector2(-player.direction * Main.rand.NextFloat(0.15f, 0.8f), Main.rand.NextFloat(-0.35f, 0.35f)); int dustType = mastery >= 0.55f && Main.rand.NextBool(4) ? DustID.RainbowMk2 : DustID.Smoke; Dust ember = Dust.NewDustPerfect( position + Main.rand.NextVector2Circular(2f + mastery * 1.5f, 2f + mastery * 1.5f), dustType, velocity, 65, color, 0.55f + mastery * 0.45f); ember.noGravity = true; ember.fadeIn = 0.7f + mastery * 0.25f; } private static void GetWingTips(Player player, float mastery, out Vector2 upperTip, out Vector2 lowerTip, out Vector2 wingRoot) { float flap = (float)Math.Sin(Main.GlobalTimeWrappedHourly * 13f + player.whoAmI * 0.61f); float reach = 15f + mastery * 6f; wingRoot = player.Center + new Vector2(-player.direction * 7f, -2f); upperTip = wingRoot + new Vector2(-player.direction * (reach + flap * 2.5f), -12f - flap * 3.5f); lowerTip = wingRoot + new Vector2(-player.direction * (reach - flap * 1.8f), 12f + flap * 3f); } }