using SoulHarvest.Common; using Microsoft.Xna.Framework; using System; using System.IO; using Terraria; using Terraria.DataStructures; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Projectiles; /// /// A tiny, zero-damage server-authored presentation event. Skill mechanics stay /// authoritative on the server while every receiving client gets the same skill, /// level, target point and deterministic seed for its local storyboard. /// public sealed class ReaperSkillCueProjectile : ModProjectile { private ReaperFormId form; private ReaperStage stage; private byte skillIndex; private byte skillLevel; private byte variant; private int actionId; private int seed; private Vector2 direction; private bool configured; private bool presented; public override string Texture => "Terraria/Images/Projectile_0"; public static int Spawn( IEntitySource source, int owner, in SickleCombatSnapshot snapshot, int skillIndex, Vector2 center, Vector2 direction, int variant = 0, int actionId = -1) { return Spawn(source, owner, snapshot.Form, snapshot.Stage, skillIndex, snapshot.GetSkillLevel(skillIndex), center, direction, variant, actionId); } public static int Spawn( IEntitySource source, int owner, ReaperFormId form, ReaperStage stage, int skillIndex, int skillLevel, Vector2 center, Vector2 direction, int variant = 0, int actionId = -1) { if (Main.netMode == NetmodeID.MultiplayerClient || owner < 0 || owner >= Main.maxPlayers || !ReaperDefinitions.IsBranchForm(form) || skillIndex is < 0 or > 2 || skillLevel <= 0) { return -1; } direction = direction.SafeNormalize(Vector2.UnitY * -1f); int projectileIndex = Projectile.NewProjectile(source, center, direction, ModContent.ProjectileType(), 0, 0f, owner); if (projectileIndex < 0 || projectileIndex >= Main.maxProjectiles) return -1; Projectile projectile = Main.projectile[projectileIndex]; if (projectile.ModProjectile is not ReaperSkillCueProjectile cue) { projectile.Kill(); return -1; } cue.form = form; cue.stage = stage; cue.skillIndex = (byte)skillIndex; cue.skillLevel = (byte)Math.Clamp(skillLevel, 1, 3); cue.variant = (byte)Math.Clamp(variant, 0, byte.MaxValue); cue.actionId = actionId >= 0 ? actionId : projectile.identity; cue.seed = StableSeed(owner, projectile.identity, cue.actionId, (int)form, skillIndex); cue.direction = direction; cue.configured = true; 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( Projectile.owner, form, stage, ReaperVisualActionKind.Skill, skillIndex, skillLevel, actionId, Target: -1, Origin: Projectile.Center, Direction: direction, Seed: seed, TimelinePhase: variant, 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)form); writer.Write((byte)stage); writer.Write(skillIndex); writer.Write(skillLevel); writer.Write(variant); writer.Write(actionId); writer.Write(seed); writer.WriteVector2(direction); } 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(); byte incomingSkillIndex = reader.ReadByte(); byte incomingSkillLevel = reader.ReadByte(); byte incomingVariant = reader.ReadByte(); int incomingActionId = reader.ReadInt32(); int incomingSeed = reader.ReadInt32(); Vector2 incomingDirection = reader.ReadVector2().SafeNormalize(Vector2.UnitY * -1f); if (Main.netMode == NetmodeID.Server) return; if (!ReaperDefinitions.IsBranchForm(incomingForm) || incomingStage is < ReaperStage.StageI or > ReaperStage.StageIII || incomingSkillIndex > 2 || incomingSkillLevel is < 1 or > 3) { configured = false; return; } form = incomingForm; stage = incomingStage; skillIndex = incomingSkillIndex; skillLevel = incomingSkillLevel; variant = incomingVariant; actionId = incomingActionId; seed = incomingSeed; direction = incomingDirection; configured = true; } internal static int StableSeed(int first, int second, int third, int fourth, int fifth = 0) { unchecked { uint hash = 2166136261u; hash = (hash ^ (uint)first) * 16777619u; hash = (hash ^ (uint)second) * 16777619u; hash = (hash ^ (uint)third) * 16777619u; hash = (hash ^ (uint)fourth) * 16777619u; hash = (hash ^ (uint)fifth) * 16777619u; return (int)hash; } } }