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<DeathAltarUpgradeType> 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<MyPlayer>();
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<TooltipLine> 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<Soul>()
.AddTile(ModContent.TileType<DeathAltarTile>())
.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);
}
}
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<DeathAltarUpgradeType> 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<MyPlayer>();
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<TooltipLine> 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<Soul>()
.AddTile(ModContent.TileType<DeathAltarTile>())
.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);
}
}