using SoulHarvest.Common; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.IO; using Terraria; using Terraria.DataStructures; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Projectiles; /// /// Zero-damage, server-authored presentation cue for a Void Reaper direct hit. /// It only transports the authoritative action snapshot and impact location; /// clients own all drawing and the cue can never create damage or move an NPC. /// public sealed class ReaperVoidHitCueProjectile : ModProjectile { private const int SeedSalt = 0x564F4944; private const int MaximumActiveCues = 96; private int ownerIndex = -1; private ReaperStage stage; private int actionId; private int targetIndex = -1; private Vector2 origin; private Vector2 direction; private int seed; private bool configured; private bool presented; public override string Texture => "Terraria/Images/Projectile_0"; public static int Spawn( IEntitySource source, int owner, ReaperStage stage, int actionId, int targetIndex, Vector2 origin, Vector2 direction) { if (Main.netMode == NetmodeID.MultiplayerClient || owner < 0 || owner >= Main.maxPlayers || stage is < ReaperStage.StageI or > ReaperStage.StageIII || targetIndex is < -1 || targetIndex >= Main.maxNPCs || !IsFinite(origin) || !IsFinite(direction)) { return -1; } direction = direction.SafeNormalize(Vector2.UnitX); int projectileType = ModContent.ProjectileType(); if (!HasTransportCapacity(projectileType) || !ReaperVoidHitCueReservationSystem.TryReserve(owner, actionId, targetIndex)) { return -1; } // The server owns the transport projectile. Using the releasing player's // owner slot here would let the server's local identity collide with a // different player-owned projectile identity on that player's client. int transportOwner = Main.netMode == NetmodeID.Server ? Main.maxPlayers : owner; int projectileIndex = Projectile.NewProjectile(source, origin, Vector2.Zero, projectileType, 0, 0f, transportOwner); if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles) { ReaperVoidHitCueReservationSystem.Release(owner, actionId, targetIndex); return -1; } Projectile projectile = Main.projectile[projectileIndex]; if (projectile.ModProjectile is not ReaperVoidHitCueProjectile cue) { ReaperVoidHitCueReservationSystem.Release(owner, actionId, targetIndex); projectile.Kill(); return -1; } cue.ownerIndex = owner; cue.stage = stage; cue.actionId = actionId; cue.targetIndex = targetIndex; cue.origin = origin; cue.direction = direction; cue.seed = ReaperSkillCueProjectile.StableSeed( owner, actionId, targetIndex, (int)stage, SeedSalt); cue.configured = true; projectile.Center = origin; projectile.timeLeft = 30; ReaperProjectileHelper.SyncNewProjectile(projectile); return projectileIndex; } 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 = 30; Projectile.hide = true; Projectile.netImportant = true; } public override bool ShouldUpdatePosition() => false; public override bool? CanDamage() => false; public override void AI() { if (!configured || presented || Main.netMode == NetmodeID.Server) return; presented = true; ReaperVfxDirector.Submit(new ReaperVisualEvent( ownerIndex, ReaperFormId.Void, stage, ReaperVisualActionKind.NormalAttack, Skill: -1, SkillLevel: 0, actionId, Target: targetIndex, Origin: origin, Direction: direction, Seed: seed, TimelinePhase: 0, NormalizedTime: 0f)); } public override bool PreDraw(ref Color lightColor) => false; public override void SendExtraAI(BinaryWriter writer) { writer.Write(configured); if (!configured) return; writer.Write((byte)ownerIndex); writer.Write((byte)stage); writer.Write(actionId); writer.Write(targetIndex); writer.WriteVector2(origin); writer.WriteVector2(direction); writer.Write(seed); } public override void ReceiveExtraAI(BinaryReader reader) { bool incomingConfigured = reader.ReadBoolean(); if (!incomingConfigured) { if (Main.netMode != NetmodeID.Server) configured = false; return; } int incomingOwner = reader.ReadByte(); ReaperStage incomingStage = (ReaperStage)reader.ReadByte(); int incomingActionId = reader.ReadInt32(); int incomingTargetIndex = reader.ReadInt32(); Vector2 incomingOrigin = reader.ReadVector2(); Vector2 incomingDirection = reader.ReadVector2(); int incomingSeed = reader.ReadInt32(); if (Main.netMode == NetmodeID.Server) return; if (incomingOwner < 0 || incomingOwner >= Main.maxPlayers || incomingStage is < ReaperStage.StageI or > ReaperStage.StageIII || incomingTargetIndex is < -1 || incomingTargetIndex >= Main.maxNPCs || !IsFinite(incomingOrigin) || !IsFinite(incomingDirection)) { configured = false; return; } ownerIndex = incomingOwner; stage = incomingStage; actionId = incomingActionId; targetIndex = incomingTargetIndex; origin = incomingOrigin; direction = incomingDirection.SafeNormalize(Vector2.UnitX); seed = incomingSeed; configured = true; Projectile.Center = origin; } private static bool IsFinite(Vector2 value) => float.IsFinite(value.X) && float.IsFinite(value.Y); private static bool HasTransportCapacity(int projectileType) { bool hasFreeSlot = false; int activeCues = 0; for (int index = 0; index < Main.maxProjectiles; index++) { Projectile projectile = Main.projectile[index]; if (!projectile.active) { hasFreeSlot = true; continue; } if (projectile.type == projectileType && ++activeCues >= MaximumActiveCues) return false; } return hasFreeSlot; } }