using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using SoulHarvest.Common;
using SoulHarvest.Items;
using System;
using System.IO;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
namespace SoulHarvest.Projectiles;
/// <summary>
/// Server-authored, non-damaging form-switch clock. The target form is not
/// committed to character progression until this projectile completes, so its
/// synchronized timer is also the visual and gameplay definition of “assembled”.
/// </summary>
public sealed class ReaperFormAssemblyProjectile : ModProjectile
{
private ReaperFormId targetForm;
private ReaperStage targetStage;
private int duration;
private int timer;
private byte deathRangeLevel;
private bool configured;
private bool serverAuthorized;
private float weaponAngle;
private int facing = 1;
internal ReaperFormId TargetForm => targetForm;
internal ReaperStage TargetStage => targetStage;
internal int Duration => duration;
internal int Elapsed => timer;
internal bool AssemblyComplete => configured && timer >= duration;
public override string Texture => "Terraria/Images/Projectile_0";
internal static bool TryGetActive(Player player,
out ReaperFormAssemblyProjectile? assembly)
{
int type = ModContent.ProjectileType<ReaperFormAssemblyProjectile>();
foreach (Projectile projectile in Main.ActiveProjectiles)
{
if (projectile.owner == player.whoAmI && projectile.type == type
&& projectile.ModProjectile
is ReaperFormAssemblyProjectile candidate)
{
assembly = candidate;
return true;
}
}
assembly = null;
return false;
}
internal static int Spawn(Player player, ReaperFormId form,
ReaperStage stage, int assemblyFrames, int rangeLevel)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
return -1;
int index = Projectile.NewProjectile(
player.GetSource_Misc("SoulHarvest:ReaperFormAssembly"),
player.MountedCenter, Vector2.Zero,
ModContent.ProjectileType<ReaperFormAssemblyProjectile>(),
0, 0f, player.whoAmI);
if (index < 0 || index >= Main.maxProjectiles)
return -1;
Projectile projectile = Main.projectile[index];
if (projectile.ModProjectile is not ReaperFormAssemblyProjectile assembly)
{
projectile.Kill();
return -1;
}
assembly.Configure(form, stage, assemblyFrames, rangeLevel,
authorizeServer: true);
ReaperProjectileHelper.SyncNewProjectile(projectile);
return index;
}
public override void SetDefaults()
{
Projectile.width = 2;
Projectile.height = 2;
Projectile.friendly = false;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = 360;
Projectile.hide = true;
Projectile.netImportant = true;
}
public override bool ShouldUpdatePosition() => false;
public override bool? CanDamage() => false;
public override void AI()
{
if (Main.netMode == NetmodeID.Server && !serverAuthorized)
{
Projectile.Kill();
return;
}
if (!configured || Projectile.owner < 0
|| Projectile.owner >= Main.maxPlayers)
{
return;
}
Player player = Main.player[Projectile.owner];
if (!player.active || player.dead
|| player.HeldItem.ModItem is not NormalSickle)
{
Projectile.Kill();
return;
}
Projectile.Center = player.MountedCenter;
Projectile.timeLeft = 2;
facing = player.direction == 0 ? 1 : player.direction;
float aimAngle = facing > 0 ? 0f : MathHelper.Pi;
weaponAngle = aimAngle - 1.45f * facing;
player.ChangeDir(facing);
player.heldProj = Projectile.whoAmI;
player.itemTime = Math.Max(player.itemTime, 2);
player.itemAnimation = Math.Max(player.itemAnimation, 2);
player.SetCompositeArmFront(true, Player.CompositeArmStretchAmount.Full,
weaponAngle - MathHelper.PiOver2);
player.itemRotation = MathHelper.WrapAngle(weaponAngle);
if (timer < duration)
{
timer++;
if (timer == duration)
Projectile.netUpdate = true;
return;
}
if (Main.netMode == NetmodeID.MultiplayerClient)
{
// The owner client can reach the last visual tick a frame before the
// server. Never let that predicted copy send a projectile kill back
// and cancel the authoritative commit; hold the completed weapon
// until the server changes the form and removes the controller.
timer = duration;
return;
}
bool completed = player.GetModPlayer<MyPlayer>()
.TryCompleteReaperFormAssembly(Projectile.whoAmI, targetForm);
#if DEBUG
Mod.Logger.Info(
$"SOULHARVEST_FORM_ASSEMBLY: complete player={player.whoAmI} target={targetForm} elapsed={timer}/{duration} committed={completed}");
#endif
Projectile.Kill();
}
public override bool PreDraw(ref Color lightColor)
{
if (Main.dedServ || !configured || Projectile.owner < 0
|| Projectile.owner >= Main.maxPlayers)
{
return false;
}
Player player = Main.player[Projectile.owner];
if (!player.active)
return false;
Texture2D texture = ModContent.Request<Texture2D>(
ReaperCombatRegistry.GetTexturePath(targetForm, targetStage)).Value;
Vector2 normalizedAnchor = ReaperCombatRegistry.GetHandleAnchor(
targetForm, targetStage);
Vector2 anchor = new(texture.Width * normalizedAnchor.X,
texture.Height * normalizedAnchor.Y);
SpriteEffects effects = facing < 0
? SpriteEffects.FlipVertically : SpriteEffects.None;
if ((effects & SpriteEffects.FlipVertically) != 0)
anchor.Y = texture.Height - anchor.Y;
int stageNumber = targetForm == ReaperFormId.Base ? 0
: targetForm == ReaperFormId.Death ? 3 : (int)targetStage;
float scale = targetForm == ReaperFormId.Death
? ReaperCombatRegistry.DeathWeaponDrawScale
* ReaperDefinitions.GetDeathRangeMultiplier(deathRangeLevel)
: 0.86f + stageNumber * 0.045f;
float rotation = weaponAngle
+ ReaperCombatRegistry.GetTextureRotationCorrection(targetForm,
targetStage);
DeathSickleAssemblyTextureSystem.Draw(texture, player.MountedCenter,
rotation, anchor, scale, effects, lightColor, timer, duration,
Projectile.identity * 47 + (int)targetForm * 1291,
targetForm == ReaperFormId.Death);
return false;
}
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
writer.Write((byte)targetForm);
writer.Write((byte)targetStage);
writer.Write((short)Math.Clamp(duration, 1, short.MaxValue));
writer.Write((short)Math.Clamp(timer, 0, short.MaxValue));
writer.Write(deathRangeLevel);
}
public override void ReceiveExtraAI(BinaryReader reader)
{
bool incomingConfigured = reader.ReadBoolean();
if (!incomingConfigured)
{
if (Main.netMode != NetmodeID.Server)
configured = false;
return;
}
ReaperFormId incomingForm = (ReaperFormId)reader.ReadByte();
ReaperStage incomingStage = (ReaperStage)reader.ReadByte();
int incomingDuration = Math.Max(1, (int)reader.ReadInt16());
int incomingTimer = Math.Clamp(reader.ReadInt16(), 0,
incomingDuration);
byte incomingRangeLevel = reader.ReadByte();
if (Main.netMode == NetmodeID.Server)
return;
Configure(incomingForm, incomingStage, incomingDuration,
incomingRangeLevel, authorizeServer: false);
timer = incomingTimer;
}
public override void OnKill(int timeLeft)
{
#if DEBUG
if (Main.netMode != NetmodeID.MultiplayerClient)
{
string ownerState = Projectile.owner >= 0
&& Projectile.owner < Main.maxPlayers
? $"active={Main.player[Projectile.owner].active} dead={Main.player[Projectile.owner].dead} held={Main.player[Projectile.owner].HeldItem.type}"
: "owner-invalid";
Mod.Logger.Info(
$"SOULHARVEST_FORM_ASSEMBLY: killed player={Projectile.owner} target={targetForm} elapsed={timer}/{duration} authorized={serverAuthorized} configured={configured} {ownerState}");
}
#endif
if (Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
return;
Player player = Main.player[Projectile.owner];
if (player.heldProj == Projectile.whoAmI)
player.heldProj = -1;
}
private void Configure(ReaperFormId form, ReaperStage stage,
int assemblyFrames, int rangeLevel, bool authorizeServer)
{
targetForm = Enum.IsDefined(form) ? form : ReaperFormId.Base;
targetStage = stage is >= ReaperStage.StageI and <= ReaperStage.StageIII
? stage : ReaperStage.StageI;
duration = Math.Clamp(assemblyFrames, 1, 600);
deathRangeLevel = (byte)Math.Clamp(rangeLevel, 0, byte.MaxValue);
timer = 0;
configured = true;
serverAuthorized = authorizeServer;
// Only the authoritative creator publishes state. Marking the replicated
// owner copy dirty echoes a late spawn packet back after the server has
// already committed and killed it, creating a short-lived ghost.
if (authorizeServer)
Projectile.netUpdate = true;
}
}
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using SoulHarvest.Common;
using SoulHarvest.Items;
using System;
using System.IO;
using Terraria;
using Terraria.ID;
using Terraria.ModLoader;
namespace SoulHarvest.Projectiles;
/// <summary>
/// Server-authored, non-damaging form-switch clock. The target form is not
/// committed to character progression until this projectile completes, so its
/// synchronized timer is also the visual and gameplay definition of “assembled”.
/// </summary>
public sealed class ReaperFormAssemblyProjectile : ModProjectile
{
private ReaperFormId targetForm;
private ReaperStage targetStage;
private int duration;
private int timer;
private byte deathRangeLevel;
private bool configured;
private bool serverAuthorized;
private float weaponAngle;
private int facing = 1;
internal ReaperFormId TargetForm => targetForm;
internal ReaperStage TargetStage => targetStage;
internal int Duration => duration;
internal int Elapsed => timer;
internal bool AssemblyComplete => configured && timer >= duration;
public override string Texture => "Terraria/Images/Projectile_0";
internal static bool TryGetActive(Player player,
out ReaperFormAssemblyProjectile? assembly)
{
int type = ModContent.ProjectileType<ReaperFormAssemblyProjectile>();
foreach (Projectile projectile in Main.ActiveProjectiles)
{
if (projectile.owner == player.whoAmI && projectile.type == type
&& projectile.ModProjectile
is ReaperFormAssemblyProjectile candidate)
{
assembly = candidate;
return true;
}
}
assembly = null;
return false;
}
internal static int Spawn(Player player, ReaperFormId form,
ReaperStage stage, int assemblyFrames, int rangeLevel)
{
if (Main.netMode == NetmodeID.MultiplayerClient)
return -1;
int index = Projectile.NewProjectile(
player.GetSource_Misc("SoulHarvest:ReaperFormAssembly"),
player.MountedCenter, Vector2.Zero,
ModContent.ProjectileType<ReaperFormAssemblyProjectile>(),
0, 0f, player.whoAmI);
if (index < 0 || index >= Main.maxProjectiles)
return -1;
Projectile projectile = Main.projectile[index];
if (projectile.ModProjectile is not ReaperFormAssemblyProjectile assembly)
{
projectile.Kill();
return -1;
}
assembly.Configure(form, stage, assemblyFrames, rangeLevel,
authorizeServer: true);
ReaperProjectileHelper.SyncNewProjectile(projectile);
return index;
}
public override void SetDefaults()
{
Projectile.width = 2;
Projectile.height = 2;
Projectile.friendly = false;
Projectile.hostile = false;
Projectile.tileCollide = false;
Projectile.ignoreWater = true;
Projectile.penetrate = -1;
Projectile.timeLeft = 360;
Projectile.hide = true;
Projectile.netImportant = true;
}
public override bool ShouldUpdatePosition() => false;
public override bool? CanDamage() => false;
public override void AI()
{
if (Main.netMode == NetmodeID.Server && !serverAuthorized)
{
Projectile.Kill();
return;
}
if (!configured || Projectile.owner < 0
|| Projectile.owner >= Main.maxPlayers)
{
return;
}
Player player = Main.player[Projectile.owner];
if (!player.active || player.dead
|| player.HeldItem.ModItem is not NormalSickle)
{
Projectile.Kill();
return;
}
Projectile.Center = player.MountedCenter;
Projectile.timeLeft = 2;
facing = player.direction == 0 ? 1 : player.direction;
float aimAngle = facing > 0 ? 0f : MathHelper.Pi;
weaponAngle = aimAngle - 1.45f * facing;
player.ChangeDir(facing);
player.heldProj = Projectile.whoAmI;
player.itemTime = Math.Max(player.itemTime, 2);
player.itemAnimation = Math.Max(player.itemAnimation, 2);
player.SetCompositeArmFront(true, Player.CompositeArmStretchAmount.Full,
weaponAngle - MathHelper.PiOver2);
player.itemRotation = MathHelper.WrapAngle(weaponAngle);
if (timer < duration)
{
timer++;
if (timer == duration)
Projectile.netUpdate = true;
return;
}
if (Main.netMode == NetmodeID.MultiplayerClient)
{
// The owner client can reach the last visual tick a frame before the
// server. Never let that predicted copy send a projectile kill back
// and cancel the authoritative commit; hold the completed weapon
// until the server changes the form and removes the controller.
timer = duration;
return;
}
bool completed = player.GetModPlayer<MyPlayer>()
.TryCompleteReaperFormAssembly(Projectile.whoAmI, targetForm);
#if DEBUG
Mod.Logger.Info(
$"SOULHARVEST_FORM_ASSEMBLY: complete player={player.whoAmI} target={targetForm} elapsed={timer}/{duration} committed={completed}");
#endif
Projectile.Kill();
}
public override bool PreDraw(ref Color lightColor)
{
if (Main.dedServ || !configured || Projectile.owner < 0
|| Projectile.owner >= Main.maxPlayers)
{
return false;
}
Player player = Main.player[Projectile.owner];
if (!player.active)
return false;
Texture2D texture = ModContent.Request<Texture2D>(
ReaperCombatRegistry.GetTexturePath(targetForm, targetStage)).Value;
Vector2 normalizedAnchor = ReaperCombatRegistry.GetHandleAnchor(
targetForm, targetStage);
Vector2 anchor = new(texture.Width * normalizedAnchor.X,
texture.Height * normalizedAnchor.Y);
SpriteEffects effects = facing < 0
? SpriteEffects.FlipVertically : SpriteEffects.None;
if ((effects & SpriteEffects.FlipVertically) != 0)
anchor.Y = texture.Height - anchor.Y;
int stageNumber = targetForm == ReaperFormId.Base ? 0
: targetForm == ReaperFormId.Death ? 3 : (int)targetStage;
float scale = targetForm == ReaperFormId.Death
? ReaperCombatRegistry.DeathWeaponDrawScale
* ReaperDefinitions.GetDeathRangeMultiplier(deathRangeLevel)
: 0.86f + stageNumber * 0.045f;
float rotation = weaponAngle
+ ReaperCombatRegistry.GetTextureRotationCorrection(targetForm,
targetStage);
DeathSickleAssemblyTextureSystem.Draw(texture, player.MountedCenter,
rotation, anchor, scale, effects, lightColor, timer, duration,
Projectile.identity * 47 + (int)targetForm * 1291,
targetForm == ReaperFormId.Death);
return false;
}
public override void SendExtraAI(BinaryWriter writer)
{
writer.Write(configured);
if (!configured)
return;
writer.Write((byte)targetForm);
writer.Write((byte)targetStage);
writer.Write((short)Math.Clamp(duration, 1, short.MaxValue));
writer.Write((short)Math.Clamp(timer, 0, short.MaxValue));
writer.Write(deathRangeLevel);
}
public override void ReceiveExtraAI(BinaryReader reader)
{
bool incomingConfigured = reader.ReadBoolean();
if (!incomingConfigured)
{
if (Main.netMode != NetmodeID.Server)
configured = false;
return;
}
ReaperFormId incomingForm = (ReaperFormId)reader.ReadByte();
ReaperStage incomingStage = (ReaperStage)reader.ReadByte();
int incomingDuration = Math.Max(1, (int)reader.ReadInt16());
int incomingTimer = Math.Clamp(reader.ReadInt16(), 0,
incomingDuration);
byte incomingRangeLevel = reader.ReadByte();
if (Main.netMode == NetmodeID.Server)
return;
Configure(incomingForm, incomingStage, incomingDuration,
incomingRangeLevel, authorizeServer: false);
timer = incomingTimer;
}
public override void OnKill(int timeLeft)
{
#if DEBUG
if (Main.netMode != NetmodeID.MultiplayerClient)
{
string ownerState = Projectile.owner >= 0
&& Projectile.owner < Main.maxPlayers
? $"active={Main.player[Projectile.owner].active} dead={Main.player[Projectile.owner].dead} held={Main.player[Projectile.owner].HeldItem.type}"
: "owner-invalid";
Mod.Logger.Info(
$"SOULHARVEST_FORM_ASSEMBLY: killed player={Projectile.owner} target={targetForm} elapsed={timer}/{duration} authorized={serverAuthorized} configured={configured} {ownerState}");
}
#endif
if (Projectile.owner < 0 || Projectile.owner >= Main.maxPlayers)
return;
Player player = Main.player[Projectile.owner];
if (player.heldProj == Projectile.whoAmI)
player.heldProj = -1;
}
private void Configure(ReaperFormId form, ReaperStage stage,
int assemblyFrames, int rangeLevel, bool authorizeServer)
{
targetForm = Enum.IsDefined(form) ? form : ReaperFormId.Base;
targetStage = stage is >= ReaperStage.StageI and <= ReaperStage.StageIII
? stage : ReaperStage.StageI;
duration = Math.Clamp(assemblyFrames, 1, 600);
deathRangeLevel = (byte)Math.Clamp(rangeLevel, 0, byte.MaxValue);
timer = 0;
configured = true;
serverAuthorized = authorizeServer;
// Only the authoritative creator publishes state. Marking the replicated
// owner copy dirty echoes a late spawn packet back after the server has
// already committed and killed it, creating a short-lived ghost.
if (authorizeServer)
Projectile.netUpdate = true;
}
}