using System; using UnityEngine; using Unity.Netcode; using Colosseum.Stats; namespace Colosseum.Weapons { /// /// 무기 장착을 관리하는 컴포넌트. /// 무기 장착 시 스탯 보너스를 적용하고 배율을 제공하며, 무기 외형을 표시합니다. /// 메시 이름으로 소켓을 자동 검색합니다. /// public class WeaponEquipment : NetworkBehaviour { [Header("References")] [Tooltip("CharacterStats 컴포넌트 (없으면 자동 검색)")] [SerializeField] private CharacterStats characterStats; [Header("Socket Names (메시 이름)")] [Tooltip("오른손 메시 이름")] [SerializeField] private string rightHandName = "Hand_R"; [Tooltip("왼손 메시 이름")] [SerializeField] private string leftHandName = "Hand_L"; [Tooltip("등 메시 이름")] [SerializeField] private string backName = "Spine"; [Tooltip("허리 메시 이름")] [SerializeField] private string hipName = "Hip"; [Tooltip("양손 메시 이름 (기본값: 오른손 사용)")] [SerializeField] private string twoHandedName = ""; [Header("Starting Weapon")] [Tooltip("시작 무기 (선택)")] [SerializeField] private WeaponData startingWeapon; [Header("Starting Offhand")] [Tooltip("시작 보조 무기 (선택)")] [SerializeField] private WeaponData startingOffhandWeapon; [Header("네트워크 동기화")] [Tooltip("이 장착 시스템이 사용하는 모든 WeaponData 목록. 서버→클라이언트 무기 동기화에 사용됩니다.")] [SerializeField] private System.Collections.Generic.List registeredWeapons = new(); [Header("네트워크 동기화 (보조손)")] [Tooltip("보조손 WeaponData 목록. 서버→클라이언트 동기화에 사용됩니다.")] [SerializeField] private System.Collections.Generic.List registeredOffhands = new(); // 캐싱된 소켓 Transform들 private Transform rightHandSocket; private Transform leftHandSocket; private Transform backSocket; private Transform hipSocket; private Transform twoHandedSocket; // 현재 장착 중인 무기 private WeaponData currentWeapon; // 현재 생성된 무기 인스턴스 private GameObject currentWeaponInstance; // 현재 적용된 스탯 수정자들 (해제 시 제거용) private readonly System.Collections.Generic.Dictionary activeModifiers = new System.Collections.Generic.Dictionary(); // 보조손 무기 private WeaponData currentOffhandWeapon; private GameObject currentOffhandWeaponInstance; private readonly System.Collections.Generic.Dictionary activeOffhandModifiers = new System.Collections.Generic.Dictionary(); // 보조손 네트워크 동기화 (registeredOffhands 인덱스, -1 = 없음) private NetworkVariable equippedOffhandId = new NetworkVariable(-1); // 무기 장착 상태 동기화 (registeredWeapons 인덱스, -1 = 없음) private NetworkVariable equippedWeaponId = new NetworkVariable(-1); public WeaponData CurrentWeapon => currentWeapon; public bool HasWeaponEquipped => currentWeapon != null; public GameObject CurrentWeaponInstance => currentWeaponInstance; // 배율 프로퍼티 (무기 없으면 기본값 1.0) public float DamageMultiplier => currentWeapon != null ? currentWeapon.DamageMultiplier : 1f; public float RangeMultiplier => currentWeapon != null ? currentWeapon.RangeMultiplier : 1f; public float ManaCostMultiplier => currentWeapon != null ? currentWeapon.ManaCostMultiplier : 1f; // 보조손 프로퍼티 public WeaponData CurrentOffhandWeapon => currentOffhandWeapon; public bool HasOffhandEquipped => currentOffhandWeapon != null; /// /// 현재 장착된 무기(메인+보조)의 특성 합산 /// public WeaponTrait EquippedWeaponTraits { get { WeaponTrait traits = currentWeapon != null ? currentWeapon.WeaponTrait : WeaponTrait.None; if (currentOffhandWeapon != null) traits |= currentOffhandWeapon.WeaponTrait; return traits; } } // 이벤트 public event Action OnWeaponEquipped; public event Action OnWeaponUnequipped; public event Action OnOffhandWeaponEquipped; public event Action OnOffhandWeaponUnequipped; private void Awake() { // CharacterStats 참조 확인 if (characterStats == null) { characterStats = GetComponent(); } // 소켓 자동 검색 CacheSockets(); } public override void OnNetworkSpawn() { equippedWeaponId.OnValueChanged += HandleEquippedWeaponChanged; equippedOffhandId.OnValueChanged += HandleEquippedOffhandChanged; if (IsServer && startingWeapon != null) { EquipWeapon(startingWeapon); } else if (!IsServer && equippedWeaponId.Value >= 0) { // 늦게 접속한 클라이언트: 현재 장착된 무기 시각화 SpawnWeaponVisualsLocal(equippedWeaponId.Value); } if (IsServer && startingOffhandWeapon != null) { EquipOffhandWeapon(startingOffhandWeapon); } else if (!IsServer && equippedOffhandId.Value >= 0) { SpawnOffhandVisualsLocal(equippedOffhandId.Value); } } public override void OnNetworkDespawn() { equippedWeaponId.OnValueChanged -= HandleEquippedWeaponChanged; equippedOffhandId.OnValueChanged -= HandleEquippedOffhandChanged; } /// /// 메시 이름으로 소켓 Transform 캐싱 /// private void CacheSockets() { rightHandSocket = FindDeepChild(rightHandName); leftHandSocket = FindDeepChild(leftHandName); backSocket = FindDeepChild(backName); hipSocket = FindDeepChild(hipName); // 양손은 별도 이름 없으면 오른손 사용 if (!string.IsNullOrEmpty(twoHandedName)) { twoHandedSocket = FindDeepChild(twoHandedName); } else { twoHandedSocket = rightHandSocket; } Debug.Log($"[WeaponEquipment] Sockets cached - R:{rightHandSocket != null}, L:{leftHandSocket != null}, Back:{backSocket != null}, Hip:{hipSocket != null}"); } /// /// 이름으로 자식 Transform 재귀 검색 /// private Transform FindDeepChild(string name) { if (string.IsNullOrEmpty(name)) return null; // BFS로 검색 var queue = new System.Collections.Generic.Queue(); queue.Enqueue(transform); while (queue.Count > 0) { Transform current = queue.Dequeue(); if (current.name == name) { return current; } foreach (Transform child in current) { queue.Enqueue(child); } } return null; } private void HandleEquippedWeaponChanged(int oldValue, int newValue) { if (IsServer) return; // 서버는 EquipWeapon/UnequipWeapon에서 직접 처리 if (newValue == -1) UnequipWeaponInternal(); else SpawnWeaponVisualsLocal(newValue); } /// /// 무기 장착 (서버에서만 호출) /// public void EquipWeapon(WeaponData weapon) { if (weapon == null) { Debug.LogWarning("[WeaponEquipment] EquipWeapon called with null weapon"); return; } if (!IsServer) { Debug.LogWarning("[WeaponEquipment] EquipWeapon can only be called on server"); return; } // 기존 무기 해제 if (currentWeapon != null) { UnequipWeapon(); } currentWeapon = weapon; // 스탯 보너스 적용 ApplyStatBonuses(weapon); // 무기 외형 생성 및 부착 SpawnWeaponVisuals(weapon); // registeredWeapons 인덱스로 동기화 equippedWeaponId.Value = registeredWeapons.IndexOf(weapon); if (equippedWeaponId.Value < 0) Debug.LogWarning($"[WeaponEquipment] '{weapon.WeaponName}' is not in registeredWeapons. Add it to sync to clients."); // 이벤트 발생 OnWeaponEquipped?.Invoke(weapon); Debug.Log($"[WeaponEquipment] Equipped: {weapon.WeaponName} at {weapon.WeaponSlot}"); } /// /// 무기 해제 (서버에서만 호출) /// public void UnequipWeapon() { if (currentWeapon == null) return; if (!IsServer) { Debug.LogWarning("[WeaponEquipment] UnequipWeapon can only be called on server"); return; } WeaponData previousWeapon = currentWeapon; // 스탯 보너스 제거 RemoveStatBonuses(); // 무기 외형 제거 DespawnWeaponVisuals(); currentWeapon = null; equippedWeaponId.Value = -1; // 이벤트 발생 OnWeaponUnequipped?.Invoke(previousWeapon); Debug.Log($"[WeaponEquipment] Unequipped: {previousWeapon.WeaponName}"); } #region 보조손 private void HandleEquippedOffhandChanged(int oldValue, int newValue) { if (IsServer) return; if (newValue == -1) UnequipOffhandWeaponInternal(); else SpawnOffhandVisualsLocal(newValue); } /// /// 보조손 무기 장착 (서버에서만 호출) /// public void EquipOffhandWeapon(WeaponData weapon) { if (weapon == null) { Debug.LogWarning("[WeaponEquipment] EquipOffhandWeapon called with null weapon"); return; } if (!IsServer) { Debug.LogWarning("[WeaponEquipment] EquipOffhandWeapon can only be called on server"); return; } if (currentOffhandWeapon != null) { UnequipOffhandWeapon(); } currentOffhandWeapon = weapon; ApplyOffhandStatBonuses(weapon); SpawnOffhandVisuals(weapon); equippedOffhandId.Value = registeredOffhands.IndexOf(weapon); if (equippedOffhandId.Value < 0) Debug.LogWarning($"[WeaponEquipment] '{weapon.WeaponName}' is not in registeredOffhands. Add it to sync to clients."); OnOffhandWeaponEquipped?.Invoke(weapon); Debug.Log($"[WeaponEquipment] Equipped offhand: {weapon.WeaponName}"); } /// /// 보조손 무기 해제 (서버에서만 호출) /// public void UnequipOffhandWeapon() { if (currentOffhandWeapon == null) return; if (!IsServer) { Debug.LogWarning("[WeaponEquipment] UnequipOffhandWeapon can only be called on server"); return; } WeaponData previousWeapon = currentOffhandWeapon; RemoveOffhandStatBonuses(); DespawnOffhandVisuals(); currentOffhandWeapon = null; equippedOffhandId.Value = -1; OnOffhandWeaponUnequipped?.Invoke(previousWeapon); Debug.Log($"[WeaponEquipment] Unequipped offhand: {previousWeapon.WeaponName}"); } /// /// 보조손 내부 해제 로직 (클라이언트 동기화용) /// private void UnequipOffhandWeaponInternal() { if (currentOffhandWeaponInstance == null && currentOffhandWeapon == null) return; WeaponData previousWeapon = currentOffhandWeapon; DespawnOffhandVisualsLocal(); OnOffhandWeaponUnequipped?.Invoke(previousWeapon); } /// /// 보조손 무기의 스탯 보너스 적용 /// private void ApplyOffhandStatBonuses(WeaponData weapon) { if (characterStats == null) return; foreach (StatType statType in System.Enum.GetValues(typeof(StatType))) { int bonus = weapon.GetStatBonus(statType); if (bonus != 0) { var stat = characterStats.GetStat(statType); if (stat != null) { var modifier = new StatModifier(bonus, StatModType.Flat, weapon); stat.AddModifier(modifier); activeOffhandModifiers[statType] = modifier; Debug.Log($"[WeaponEquipment] Applied offhand {statType} +{bonus}"); } } } } /// /// 보조손 무기의 스탯 보너스 제거 /// private void RemoveOffhandStatBonuses() { if (characterStats == null) return; foreach (StatType statType in System.Enum.GetValues(typeof(StatType))) { var stat = characterStats.GetStat(statType); if (stat != null && activeOffhandModifiers.TryGetValue(statType, out StatModifier modifier)) { stat.RemoveModifier(modifier); Debug.Log($"[WeaponEquipment] Removed offhand {statType} modifier"); } } activeOffhandModifiers.Clear(); } /// /// 보조손 무기 외형 생성 및 부착 (서버). 항상 왼손에 장착합니다. /// private void SpawnOffhandVisuals(WeaponData weapon) { if (weapon == null || weapon.WeaponPrefab == null) return; if (leftHandSocket == null) { Debug.LogWarning("[WeaponEquipment] No left hand socket found for offhand weapon"); return; } currentOffhandWeaponInstance = Instantiate(weapon.WeaponPrefab, leftHandSocket); currentOffhandWeaponInstance.transform.localPosition = weapon.PositionOffset; currentOffhandWeaponInstance.transform.localRotation = Quaternion.Euler(weapon.RotationOffset); currentOffhandWeaponInstance.transform.localScale = weapon.Scale; Debug.Log($"[WeaponEquipment] Spawned offhand visual: {weapon.WeaponName}"); } /// /// 클라이언트: registeredOffhands 인덱스로 보조손 무기 외형 생성 /// private void SpawnOffhandVisualsLocal(int weaponIndex) { if (weaponIndex < 0 || weaponIndex >= registeredOffhands.Count || registeredOffhands[weaponIndex] == null) { Debug.LogWarning($"[WeaponEquipment] Offhand weapon index {weaponIndex} not found in registeredOffhands."); return; } var weapon = registeredOffhands[weaponIndex]; if (weapon.WeaponPrefab == null) return; DespawnOffhandVisualsLocal(); if (leftHandSocket == null) { Debug.LogWarning("[WeaponEquipment] No left hand socket found for offhand weapon"); return; } currentOffhandWeaponInstance = Instantiate(weapon.WeaponPrefab, leftHandSocket); currentOffhandWeaponInstance.transform.localPosition = weapon.PositionOffset; currentOffhandWeaponInstance.transform.localRotation = Quaternion.Euler(weapon.RotationOffset); currentOffhandWeaponInstance.transform.localScale = weapon.Scale; currentOffhandWeapon = weapon; } /// /// 보조손 무기 외형 제거 (서버) /// private void DespawnOffhandVisuals() { if (currentOffhandWeaponInstance == null) return; Destroy(currentOffhandWeaponInstance); currentOffhandWeaponInstance = null; } /// /// 보조손 무기 외형 제거 (클라이언트) /// private void DespawnOffhandVisualsLocal() { if (currentOffhandWeaponInstance == null) return; Destroy(currentOffhandWeaponInstance); currentOffhandWeaponInstance = null; currentOffhandWeapon = null; } #endregion /// /// 내부 해제 로직 (클라이언트 동기화용) /// private void UnequipWeaponInternal() { if (currentWeaponInstance == null && currentWeapon == null) return; WeaponData previousWeapon = currentWeapon; DespawnWeaponVisualsLocal(); OnWeaponUnequipped?.Invoke(previousWeapon); } /// /// 무기의 스탯 보너스 적용 /// private void ApplyStatBonuses(WeaponData weapon) { if (characterStats == null) return; // 모든 스탯 타입에 대해 보너스 적용 foreach (StatType statType in System.Enum.GetValues(typeof(StatType))) { int bonus = weapon.GetStatBonus(statType); if (bonus != 0) { var stat = characterStats.GetStat(statType); if (stat != null) { var modifier = new StatModifier(bonus, StatModType.Flat, weapon); stat.AddModifier(modifier); activeModifiers[statType] = modifier; Debug.Log($"[WeaponEquipment] Applied {statType} +{bonus}"); } } } } /// /// 무기의 스탯 보너스 제거 /// private void RemoveStatBonuses() { if (characterStats == null) return; // 각 스탯에서 무기로부터 추가된 수정자 제거 foreach (StatType statType in System.Enum.GetValues(typeof(StatType))) { var stat = characterStats.GetStat(statType); if (stat != null && activeModifiers.TryGetValue(statType, out StatModifier modifier)) { stat.RemoveModifier(modifier); Debug.Log($"[WeaponEquipment] Removed {statType} modifier"); } } activeModifiers.Clear(); } /// /// 무기 외형 생성 및 부착 (서버) /// private void SpawnWeaponVisuals(WeaponData weapon) { if (weapon == null || weapon.WeaponPrefab == null) return; Transform socket = GetSocketForSlot(weapon.WeaponSlot); if (socket == null) { Debug.LogWarning($"[WeaponEquipment] No socket found for slot: {weapon.WeaponSlot}"); return; } currentWeaponInstance = Instantiate(weapon.WeaponPrefab, socket); currentWeaponInstance.transform.localPosition = weapon.PositionOffset; currentWeaponInstance.transform.localRotation = Quaternion.Euler(weapon.RotationOffset); currentWeaponInstance.transform.localScale = weapon.Scale; Debug.Log($"[WeaponEquipment] Spawned weapon visual: {weapon.WeaponName}"); } /// /// 클라이언트: registeredWeapons 인덱스로 무기 외형 생성 /// private void SpawnWeaponVisualsLocal(int weaponIndex) { if (weaponIndex < 0 || weaponIndex >= registeredWeapons.Count || registeredWeapons[weaponIndex] == null) { Debug.LogWarning($"[WeaponEquipment] Weapon index {weaponIndex} not found in registeredWeapons."); return; } var weapon = registeredWeapons[weaponIndex]; if (weapon.WeaponPrefab == null) return; DespawnWeaponVisualsLocal(); Transform socket = GetSocketForSlot(weapon.WeaponSlot); if (socket == null) { Debug.LogWarning($"[WeaponEquipment] No socket found for slot: {weapon.WeaponSlot}"); return; } currentWeaponInstance = Instantiate(weapon.WeaponPrefab, socket); currentWeaponInstance.transform.localPosition = weapon.PositionOffset; currentWeaponInstance.transform.localRotation = Quaternion.Euler(weapon.RotationOffset); currentWeaponInstance.transform.localScale = weapon.Scale; currentWeapon = weapon; } /// /// 무기 외형 제거 (서버) /// private void DespawnWeaponVisuals() { if (currentWeaponInstance == null) return; Destroy(currentWeaponInstance); currentWeaponInstance = null; } /// /// 무기 외형 제거 (클라이언트) /// private void DespawnWeaponVisualsLocal() { if (currentWeaponInstance == null) return; Destroy(currentWeaponInstance); currentWeaponInstance = null; currentWeapon = null; } /// /// 슬롯 타입에 맞는 소켓 Transform 반환 /// private Transform GetSocketForSlot(WeaponSlot slot) { return slot switch { WeaponSlot.RightHand => rightHandSocket, WeaponSlot.LeftHand => leftHandSocket, WeaponSlot.Back => backSocket, WeaponSlot.Hip => hipSocket, WeaponSlot.TwoHanded => twoHandedSocket != null ? twoHandedSocket : rightHandSocket, _ => rightHandSocket, }; } /// /// 서버에 무기 장착 요청 /// [Rpc(SendTo.Server)] public void RequestEquipWeaponRpc(int weaponInstanceId) { // TODO: WeaponDatabase에서 ID로 WeaponData 조회 후 EquipWeapon 호출 Debug.Log($"[WeaponEquipment] Client requested weapon equip: {weaponInstanceId}"); } /// /// 서버에 무기 해제 요청 /// [Rpc(SendTo.Server)] public void RequestUnequipWeaponRpc() { UnequipWeapon(); } } }