using Unity.Netcode; using UnityEngine; namespace Northbound { public class WorkerSpawner : NetworkBehaviour, IInteractable, IVisionProvider, ITeamMember { [Header("Vision Settings")] [Tooltip("워커 홀이 제공하는 시야 범위")] public float visionRange = 10f; [Header("Team")] [Tooltip("건물의 팀")] public TeamType initialTeam = TeamType.Player; [Header("Spawner Settings")] public GameObject workerPrefab; public Transform spawnPoint; public float spawnRadius = 2f; public int maxWorkers = 5; public float spawnCooldown = 5f; public int recruitmentCost = 20; [Header("Interaction")] public string interactionAnimationTrigger = "Build"; public string interactionPrompt = "[E] 워커 생성"; [Header("Visual")] public GameObject spawnEffectPrefab; private NetworkVariable _workerCount = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private float _lastSpawnTime; public int WorkerCount => _workerCount.Value; public override void OnNetworkSpawn() { base.OnNetworkSpawn(); _workerCount.OnValueChanged += OnWorkerCountChanged; if (IsServer) { // 시야 제공자로 등록 FogOfWarSystem.Instance?.RegisterVisionProvider(this); } } public override void OnNetworkDespawn() { _workerCount.OnValueChanged -= OnWorkerCountChanged; if (IsServer) { // 시야 제공자 등록 해제 FogOfWarSystem.Instance?.UnregisterVisionProvider(this); } base.OnNetworkDespawn(); } private void OnWorkerCountChanged(int previousValue, int newValue) { } #region IInteractable Implementation public bool CanInteract(ulong playerId) { if (_workerCount.Value >= maxWorkers) return false; if (Time.time - _lastSpawnTime < spawnCooldown) return false; if (workerPrefab == null) return false; var coreResourceManager = CoreResourceManager.Instance; if (coreResourceManager != null && !coreResourceManager.CanAfford(recruitmentCost)) return false; return true; } public void Interact(ulong playerId) { if (!CanInteract(playerId)) return; SpawnWorkerServerRpc(playerId); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void SpawnWorkerServerRpc(ulong playerId) { if (!CanInteract(playerId)) return; if (_workerCount.Value >= maxWorkers) { Debug.LogWarning("[WorkerSpawner] 최대 워커 수에 도달함"); return; } var coreResourceManager = CoreResourceManager.Instance; if (coreResourceManager == null) { Debug.LogWarning("[WorkerSpawner] CoreResourceManager 인스턴스를 찾을 수 없습니다."); return; } if (!coreResourceManager.CanAfford(recruitmentCost)) { Debug.LogWarning($"[WorkerSpawner] 코어 자원이 부족합니다. 필요: {recruitmentCost}"); return; } coreResourceManager.SpendResources(recruitmentCost); Vector3 spawnPosition = spawnPoint != null ? spawnPoint.position : transform.position; float randomAngle = Random.Range(0f, 360f); Vector3 offset = new Vector3( Mathf.Cos(randomAngle * Mathf.Deg2Rad) * spawnRadius, 0f, Mathf.Sin(randomAngle * Mathf.Deg2Rad) * spawnRadius ); spawnPosition += offset; GameObject workerObj = Instantiate(workerPrefab, spawnPosition, Quaternion.identity); NetworkObject workerNetObj = workerObj.GetComponent(); if (workerNetObj == null) { Debug.LogError("[WorkerSpawner] Worker prefab must have NetworkObject component"); Destroy(workerObj); return; } // IMPORTANT: Spawn as server-owned so server can modify worker's NetworkVariables workerNetObj.Spawn(); _lastSpawnTime = Time.time; _workerCount.Value++; ShowSpawnEffectClientRpc(spawnPosition); ScheduleWorkerAssignment(playerId, workerNetObj.NetworkObjectId); } private void ScheduleWorkerAssignment(ulong playerId, ulong workerNetObjectId) { StartCoroutine(AssignWorkerAfterFrame(playerId, workerNetObjectId)); } private System.Collections.IEnumerator AssignWorkerAfterFrame(ulong playerId, ulong workerNetObjectId) { yield return new WaitForEndOfFrame(); if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(workerNetObjectId, out var workerObj)) { var worker = workerObj.GetComponent(); if (worker != null) { SetPlayerAssignedWorkerClientRpc(playerId, workerNetObjectId); } } } [Rpc(SendTo.ClientsAndHost)] private void SetPlayerAssignedWorkerClientRpc(ulong playerId, ulong workerNetObjectId) { var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects; NetworkObject playerObj = null; foreach (var kvp in spawnedObjects) { if (kvp.Value != null && kvp.Value.OwnerClientId == playerId) { playerObj = kvp.Value; break; } } if (playerObj != null) { var playerInteraction = playerObj.GetComponent(); if (playerInteraction != null && playerInteraction.IsOwner) { if (spawnedObjects.TryGetValue(workerNetObjectId, out var workerObj)) { var worker = workerObj.GetComponent(); if (worker != null) { playerInteraction.assignedWorker = worker; } } } } } [Rpc(SendTo.ClientsAndHost)] private void ShowSpawnEffectClientRpc(Vector3 position) { if (spawnEffectPrefab != null) { GameObject effect = Instantiate(spawnEffectPrefab, position, Quaternion.identity); Destroy(effect, 2f); } } public string GetInteractionPrompt() { if (_workerCount.Value >= maxWorkers) return $"Worker ({_workerCount.Value}/{maxWorkers})"; float cooldownRemaining = Mathf.Max(0, spawnCooldown - (Time.time - _lastSpawnTime)); if (cooldownRemaining > 0) return $"Waiting Worker... ({cooldownRemaining:F1}s)"; var coreResourceManager = CoreResourceManager.Instance; if (coreResourceManager != null && !coreResourceManager.CanAfford(recruitmentCost)) return $"Resource Required: {recruitmentCost})"; return $"{interactionPrompt} ({_workerCount.Value}/{maxWorkers}) - 비용: {recruitmentCost}"; } public string GetInteractionAnimation() { return interactionAnimationTrigger; } public EquipmentData GetEquipmentData() { return null; } public Transform GetTransform() { return transform; } #endregion public void OnWorkerDestroyed() { if (IsServer) { _workerCount.Value = Mathf.Max(0, _workerCount.Value - 1); } } private void OnDrawGizmosSelected() { #if UNITY_EDITOR Gizmos.color = Color.green; Vector3 spawnCenter = spawnPoint != null ? spawnPoint.position : transform.position; Gizmos.DrawWireSphere(spawnCenter, spawnRadius); UnityEditor.Handles.Label(spawnCenter + Vector3.up * 2f, $"Worker Spawner\nWorkers: {_workerCount.Value}/{maxWorkers}"); #endif } #region IVisionProvider Implementation public ulong GetOwnerId() => 0; // 워커 홀은 모든 플레이어에게 시야 제공 public float GetVisionRange() => visionRange; Transform IVisionProvider.GetTransform() => transform; public bool IsActive() => IsSpawned; TeamType IVisionProvider.GetTeam() => initialTeam; #endregion #region ITeamMember Implementation public TeamType GetTeam() => initialTeam; public bool IsDead() => false; // 워커 홀은 파괴되지 않음 public void SetTeam(TeamType team) { // 워커 홀의 팀은 변경할 수 없음 Debug.LogWarning("[WorkerSpawner] 워커 홀의 팀은 변경할 수 없습니다."); } #endregion } }