using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using Terraria; using Terraria.ID; using Terraria.ModLoader; namespace SoulHarvest.Common; internal sealed class DeathDomainLootAttractionSystem : ModSystem { private const float ArrivalDistance = 44f; private const float MinimumPullStep = 18f; private const float MaximumPullStep = 160f; private const int NetworkSyncInterval = 5; private static readonly List ActiveCollectors = []; public override void OnWorldUnload() { ActiveCollectors.Clear(); } public override void PostUpdateWorld() { // World item positions are authoritative on the server. Clients only replay // the synchronized flight so a domain cannot collect an item client-side. if (Main.netMode == NetmodeID.MultiplayerClient) return; RefreshActiveCollectors(); if (ActiveCollectors.Count == 0) return; for (int itemIndex = 0; itemIndex < Main.maxItems; itemIndex++) { Item item = Main.item[itemIndex]; if (!item.active || item.IsAir || item.stack <= 0) continue; Player? collector = FindNearestDomainOwner(item.Center); if (collector is null) continue; int absorbAllLevel = collector.GetModPlayer() .GetEquippedDeathNecklace()?.AbsorbAllLevel ?? 1; float pullScale = 0.65f + absorbAllLevel * 0.08f; Vector2 toCollector = collector.MountedCenter - item.Center; float distance = toCollector.Length(); if (distance <= 0.001f) continue; item.noGrabDelay = 0; Vector2 direction = toCollector / distance; bool arrived = distance <= ArrivalDistance; if (arrived) { item.Center = collector.MountedCenter; item.velocity = Vector2.Zero; } else { // Move the world entity itself so the global pull is not stopped by // terrain. Velocity is retained for smooth interpolation on clients. float step = MathHelper.Clamp(distance * 0.075f * pullScale, MinimumPullStep * pullScale, MaximumPullStep * pullScale); item.position += direction * Math.Min(distance - ArrivalDistance, step); item.velocity = Vector2.Lerp(item.velocity, direction * MathHelper.Clamp(step * 0.22f, 6f, 32f * pullScale), 0.58f); } if (Main.netMode == NetmodeID.Server && (Main.GameUpdateCount + (ulong)itemIndex) % NetworkSyncInterval == 0) { NetMessage.SendData(MessageID.SyncItem, -1, -1, null, itemIndex); } } } private static Player? FindNearestDomainOwner(Vector2 itemCenter) { Player? nearest = null; float nearestDistanceSquared = float.MaxValue; foreach (Player player in ActiveCollectors) { float distanceSquared = Vector2.DistanceSquared(itemCenter, player.MountedCenter); if (distanceSquared >= nearestDistanceSquared) continue; nearestDistanceSquared = distanceSquared; nearest = player; } return nearest; } private static void RefreshActiveCollectors() { ActiveCollectors.Clear(); for (int playerIndex = 0; playerIndex < Main.maxPlayers; playerIndex++) { Player player = Main.player[playerIndex]; if (!player.active || player.dead) continue; MyPlayer modPlayer = player.GetModPlayer(); if (modPlayer.DeathDomainEnabled && modPlayer.GetEquippedDeathNecklace() is not null) { ActiveCollectors.Add(player); } } } }