diff --git a/Assets/Scripts/Weapons.meta b/Assets/Scripts/Weapons.meta
new file mode 100644
index 00000000..4253ca13
--- /dev/null
+++ b/Assets/Scripts/Weapons.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e113408f0cfce6c4aa60725c3005f25c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Weapons/WeaponData.cs b/Assets/Scripts/Weapons/WeaponData.cs
new file mode 100644
index 00000000..f319a139
--- /dev/null
+++ b/Assets/Scripts/Weapons/WeaponData.cs
@@ -0,0 +1,107 @@
+using UnityEngine;
+
+using Colosseum.Stats;
+
+namespace Colosseum.Weapons
+{
+ ///
+ /// 무기 장착 위치
+ ///
+ public enum WeaponSlot
+ {
+ RightHand, // 오른손
+ LeftHand, // 왼손
+ Back, // 등
+ Hip, // 허리
+ TwoHanded, // 양손
+ }
+
+ ///
+ /// 무기 데이터. 무기의 기본 정보, 스탯 보너스, 배율, 외형을 관리합니다.
+ ///
+ [CreateAssetMenu(fileName = "NewWeapon", menuName = "Colosseum/Weapon")]
+ public class WeaponData : ScriptableObject
+ {
+ [Header("기본 정보")]
+ [SerializeField] private string weaponName;
+ [TextArea(2, 4)]
+ [SerializeField] private string description;
+ [SerializeField] private Sprite icon;
+
+ [Header("장착 설정")]
+ [Tooltip("무기가 장착될 슬롯")]
+ [SerializeField] private WeaponSlot weaponSlot = WeaponSlot.RightHand;
+ [Tooltip("무기 프리팹 (메시, 콜라이더 등 포함)")]
+ [SerializeField] private GameObject weaponPrefab;
+ [Tooltip("장착 시 위치 오프셋")]
+ [SerializeField] private Vector3 positionOffset = Vector3.zero;
+ [Tooltip("장착 시 회전 오프셋 (오일러 각도)")]
+ [SerializeField] private Vector3 rotationOffset = Vector3.zero;
+ [Tooltip("장착 시 스케일")]
+ [SerializeField] private Vector3 scale = Vector3.one;
+
+ [Header("스탯 보너스 (Flat)")]
+ [Tooltip("힘 보너스")]
+ [SerializeField] private int strengthBonus = 0;
+ [Tooltip("민첩 보너스")]
+ [SerializeField] private int dexterityBonus = 0;
+ [Tooltip("지능 보너스")]
+ [SerializeField] private int intelligenceBonus = 0;
+ [Tooltip("활력 보너스")]
+ [SerializeField] private int vitalityBonus = 0;
+ [Tooltip("지혜 보너스")]
+ [SerializeField] private int wisdomBonus = 0;
+ [Tooltip("정신 보너스")]
+ [SerializeField] private int spiritBonus = 0;
+
+ [Header("배율")]
+ [Tooltip("데미지 배율 (1.0 = 100%, 1.5 = 150%)")]
+ [Min(0f)] [SerializeField] private float damageMultiplier = 1f;
+ [Tooltip("사거리 배율 (1.0 = 100%, 1.2 = 120%)")]
+ [Min(0f)] [SerializeField] private float rangeMultiplier = 1f;
+ [Tooltip("마나 소모 배율 (1.0 = 100%, 0.8 = 80%)")]
+ [Min(0f)] [SerializeField] private float manaCostMultiplier = 1f;
+
+ // Properties - 기본 정보
+ public string WeaponName => weaponName;
+ public string Description => description;
+ public Sprite Icon => icon;
+
+ // Properties - 장착 설정
+ public WeaponSlot WeaponSlot => weaponSlot;
+ public GameObject WeaponPrefab => weaponPrefab;
+ public Vector3 PositionOffset => positionOffset;
+ public Vector3 RotationOffset => rotationOffset;
+ public Vector3 Scale => scale;
+
+ // Properties - 스탯 보너스
+ public int StrengthBonus => strengthBonus;
+ public int DexterityBonus => dexterityBonus;
+ public int IntelligenceBonus => intelligenceBonus;
+ public int VitalityBonus => vitalityBonus;
+ public int WisdomBonus => wisdomBonus;
+ public int SpiritBonus => spiritBonus;
+
+ // Properties - 배율
+ public float DamageMultiplier => damageMultiplier;
+ public float RangeMultiplier => rangeMultiplier;
+ public float ManaCostMultiplier => manaCostMultiplier;
+
+ ///
+ /// 스탯 타입에 해당하는 보너스 값 반환
+ ///
+ public int GetStatBonus(StatType statType)
+ {
+ return statType switch
+ {
+ StatType.Strength => strengthBonus,
+ StatType.Dexterity => dexterityBonus,
+ StatType.Intelligence => intelligenceBonus,
+ StatType.Vitality => vitalityBonus,
+ StatType.Wisdom => wisdomBonus,
+ StatType.Spirit => spiritBonus,
+ _ => 0,
+ };
+ }
+ }
+}
diff --git a/Assets/Scripts/Weapons/WeaponEquipment.cs b/Assets/Scripts/Weapons/WeaponEquipment.cs
new file mode 100644
index 00000000..80093979
--- /dev/null
+++ b/Assets/Scripts/Weapons/WeaponEquipment.cs
@@ -0,0 +1,401 @@
+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;
+
+ // 캐싱된 소켓 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 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 event Action OnWeaponEquipped;
+ public event Action OnWeaponUnequipped;
+
+ private void Awake()
+ {
+ // CharacterStats 참조 확인
+ if (characterStats == null)
+ {
+ characterStats = GetComponent();
+ }
+
+ // 소켓 자동 검색
+ CacheSockets();
+ }
+
+ public override void OnNetworkSpawn()
+ {
+ // 네트워크 변수 변경 콜백
+ equippedWeaponId.OnValueChanged += HandleEquippedWeaponChanged;
+
+ // 서버에서 시작 무기 장착
+ if (IsServer && startingWeapon != null)
+ {
+ EquipWeapon(startingWeapon);
+ }
+ }
+
+ public override void OnNetworkDespawn()
+ {
+ equippedWeaponId.OnValueChanged -= HandleEquippedWeaponChanged;
+ }
+
+ ///
+ /// 메시 이름으로 소켓 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)
+ {
+ // -1이면 무기 해제, 그 외에는 무기 장착됨
+ // (GetInstanceID()는 음수를 반환할 수 있으므로 >= 0 체크 사용 불가)
+ if (newValue == -1)
+ {
+ UnequipWeaponInternal();
+ }
+ // 클라이언트에서는 서버에서 이미 장착된 무기 정보를 받아야 함
+ // TODO: WeaponDatabase에서 ID로 WeaponData 조회
+ }
+
+
+ ///
+ /// 무기 장착 (서버에서만 호출)
+ ///
+ 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);
+
+ // 네트워크 동기화 (간단한 ID 사용, 실제로는 WeaponDatabase 필요)
+ equippedWeaponId.Value = weapon.GetInstanceID();
+
+ // 이벤트 발생
+ 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}");
+ }
+
+ ///
+ /// 내부 해제 로직 (클라이언트 동기화용)
+ ///
+ private void UnequipWeaponInternal()
+ {
+ if (currentWeapon == null) return;
+
+ WeaponData previousWeapon = currentWeapon;
+ RemoveStatBonuses();
+ DespawnWeaponVisuals();
+ currentWeapon = null;
+
+ 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);
+
+ // 소켓 스케일 보정 (부모 스케일이 작은 경우 무기도 작아지는 문제 해결)
+ Vector3 scaleCompensation = new Vector3(
+ socket.lossyScale.x != 0 ? 1f / socket.lossyScale.x : 1f,
+ socket.lossyScale.y != 0 ? 1f / socket.lossyScale.y : 1f,
+ socket.lossyScale.z != 0 ? 1f / socket.lossyScale.z : 1f
+ );
+ currentWeaponInstance.transform.localScale = Vector3.Scale(weapon.Scale, scaleCompensation);
+ 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] Weapon instantiated - LocalScale: {currentWeaponInstance.transform.localScale}, LossyScale: {currentWeaponInstance.transform.lossyScale}");
+ Debug.Log($"[WeaponEquipment] Socket: {socket.name}, Socket scale: {socket.lossyScale}");
+ Debug.Log($"[WeaponEquipment] Position offset: {weapon.PositionOffset}, Rotation offset: {weapon.RotationOffset}");
+
+ // 네트워크 동기화를 위해 Spawn (서버에서만)
+ if (IsServer && currentWeaponInstance.TryGetComponent(out var networkObject))
+ {
+ networkObject.Spawn(true);
+ }
+
+ Debug.Log($"[WeaponEquipment] Spawned weapon visual: {weapon.WeaponName}");
+ }
+
+ ///
+ /// 무기 외형 제거
+ ///
+ private void DespawnWeaponVisuals()
+ {
+ if (currentWeaponInstance == null) return;
+
+ // 네트워크 Object면 Despawn
+ if (currentWeaponInstance.TryGetComponent(out var networkObject) && networkObject.IsSpawned)
+ {
+ if (IsServer)
+ {
+ networkObject.Despawn(true);
+ }
+ }
+ else
+ {
+ Destroy(currentWeaponInstance);
+ }
+
+ currentWeaponInstance = null;
+ Debug.Log("[WeaponEquipment] Despawned weapon visual");
+ }
+
+ ///
+ /// 슬롯 타입에 맞는 소켓 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();
+ }
+ }
+}