using System; using System.Collections.Generic; using Fusion; using UnityEngine; namespace TPSBR { public sealed class LootBoxSpawner : NetworkBehaviour { [Header("Prefab")] [SerializeField] private NetworkPrefabRef _itemBoxPrefab; [Header("Placement")] [Min(1)] [SerializeField] private int _count = 10; [Min(0f)] [SerializeField] private float _radius = 40f; [Min(0f)] [SerializeField] private float _minSpacing = 3f; [SerializeField] private LayerMask _groundMask = ~0; [SerializeField] private float _raycastUp = 60f; [SerializeField] private float _raycastDown = 120f; [SerializeField] private uint _seed = 12345; [SerializeField] private bool _randomYaw = true; // Keep references to despawn/respawn later if you extend it private readonly List _spawned = new(); public override void Spawned() { if (!HasStateAuthority) return; SpawnAll(); } private void SpawnAll() { // If this is re-entered (e.g., soft reset), clean previous foreach (var no in _spawned) if (no && no.IsValid) Runner.Despawn(no); _spawned.Clear(); var rnd = new System.Random(unchecked((int)_seed)); var positions = SamplePositions(transform.position, _radius, _count, _minSpacing, rnd); foreach (var p in positions) { var rot = _randomYaw ? Quaternion.Euler(0f, (float)rnd.NextDouble() * 360f, 0f) : Quaternion.identity; // Spawn server-side; replicated to all clients var obj = Runner.Spawn(_itemBoxPrefab, p, rot); _spawned.Add(obj); } } // Deterministic sampling inside a circle with ground projection + spacing private List SamplePositions(Vector3 center, float radius, int count, float minSpacing, System.Random rnd) { var results = new List(count); var maxTrials = 2500; // generous to pack in cluttered areas int trials = 0; while (results.Count < count && trials++ < maxTrials) { // pick point in disc (sqrt for uniform) float r = radius * Mathf.Sqrt((float)rnd.NextDouble()); float ang = Mathf.PI * 2f * (float)rnd.NextDouble(); Vector3 local = new Vector3(Mathf.Cos(ang) * r, 0f, Mathf.Sin(ang) * r); Vector3 rayFrom = center + local + Vector3.up * _raycastUp; if (Physics.Raycast(rayFrom, Vector3.down, out var hit, _raycastUp + _raycastDown, _groundMask, QueryTriggerInteraction.Ignore)) { var pos = hit.point; // spacing test bool ok = true; if (minSpacing > 0f) { for (int i = 0; i < results.Count; i++) { if (Vector3.SqrMagnitude(results[i] - pos) < minSpacing * minSpacing) { ok = false; break; } } } if (ok) results.Add(pos); } } if (results.Count < count) Debug.LogWarning($"[LootBoxSpawner] Only placed {results.Count}/{count}. Increase radius or lower spacing."); return results; } // Scene gizmo previews the exact deterministic layout you’ll get at runtime private void OnDrawGizmosSelected() { Gizmos.color = new Color(0.1f, 0.8f, 1f, 0.25f); Gizmos.DrawWireSphere(transform.position, _radius); // preview sample using same seed var rnd = new System.Random(unchecked((int)_seed)); var preview = SamplePositions(transform.position, _radius, _count, _minSpacing, rnd); Gizmos.color = new Color(0.1f, 0.8f, 1f, 0.7f); foreach (var p in preview) { Gizmos.DrawSphere(p + Vector3.up * 0.15f, 0.25f); Gizmos.DrawLine(p + Vector3.up * 3f, p); // drop line } } } }