From 0286237b98f9664939837a7dd1b538423f416c07 Mon Sep 17 00:00:00 2001 From: dal4segno Date: Tue, 10 Mar 2026 13:19:55 +0900 Subject: [PATCH] =?UTF-8?q?[Stats]=20=EC=BA=90=EB=A6=AD=ED=84=B0=20?= =?UTF-8?q?=EC=8A=A4=ED=83=AF=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 6가지 기본 스탯 추가 (STR, DEX, INT, VIT, WIS, SPI) - 스탯 수정자 시스템 (Flat, PercentAdd, PercentMult) - 파생 스탯 계산 (체력/마나/대미지/회복력) - 스킬 효과에 스탯 기반 대미지/회복량 적용 - 마나 비용 체크 및 소모 로직 추가 --- .../Scripts/Player/PlayerNetworkController.cs | 33 +++-- Assets/Scripts/Player/PlayerSkillInput.cs | 22 ++++ Assets/Scripts/Skills/Effects/DamageEffect.cs | 61 +++++++++- Assets/Scripts/Skills/Effects/HealEffect.cs | 35 +++++- Assets/Scripts/Stats/CharacterStat.cs | 113 ++++++++++++++++++ Assets/Scripts/Stats/CharacterStats.cs | 60 ++++++++++ Assets/Scripts/Stats/StatModifier.cs | 35 ++++++ Assets/Scripts/Stats/StatType.cs | 15 +++ 8 files changed, 355 insertions(+), 19 deletions(-) create mode 100644 Assets/Scripts/Stats/CharacterStat.cs create mode 100644 Assets/Scripts/Stats/CharacterStats.cs create mode 100644 Assets/Scripts/Stats/StatModifier.cs create mode 100644 Assets/Scripts/Stats/StatType.cs diff --git a/Assets/Scripts/Player/PlayerNetworkController.cs b/Assets/Scripts/Player/PlayerNetworkController.cs index 494edeaf..b9a5d10f 100644 --- a/Assets/Scripts/Player/PlayerNetworkController.cs +++ b/Assets/Scripts/Player/PlayerNetworkController.cs @@ -1,5 +1,6 @@ using UnityEngine; using Unity.Netcode; +using Colosseum.Stats; namespace Colosseum.Player { @@ -8,26 +9,33 @@ namespace Colosseum.Player /// public class PlayerNetworkController : NetworkBehaviour { - [Header("Stats")] - [SerializeField] private float maxHealth = 100f; - [SerializeField] private float maxMana = 50f; + [Header("References")] + [Tooltip("CharacterStats 컴포넌트 (없으면 자동 검색)")] + [SerializeField] private CharacterStats characterStats; // 네트워크 동기화 변수 private NetworkVariable currentHealth = new NetworkVariable(100f); private NetworkVariable currentMana = new NetworkVariable(50f); public float Health => currentHealth.Value; - public float MaxHealth => maxHealth; public float Mana => currentMana.Value; - public float MaxMana => maxMana; + public float MaxHealth => characterStats != null ? characterStats.MaxHealth : 100f; + public float MaxMana => characterStats != null ? characterStats.MaxMana : 50f; + public CharacterStats Stats => characterStats; public override void OnNetworkSpawn() { + // CharacterStats 참조 확인 + if (characterStats == null) + { + characterStats = GetComponent(); + } + // 초기화 if (IsServer) { - currentHealth.Value = maxHealth; - currentMana.Value = maxMana; + currentHealth.Value = MaxHealth; + currentMana.Value = MaxMana; } } @@ -54,13 +62,22 @@ namespace Colosseum.Player currentMana.Value = Mathf.Max(0f, currentMana.Value - amount); } + /// + /// 체력 회복 (서버에서만 실행) + /// + [Rpc(SendTo.Server)] + public void RestoreHealthRpc(float amount) + { + currentHealth.Value = Mathf.Min(MaxHealth, currentHealth.Value + amount); + } + /// /// 마나 회복 (서버에서만 실행) /// [Rpc(SendTo.Server)] public void RestoreManaRpc(float amount) { - currentMana.Value = Mathf.Min(maxMana, currentMana.Value + amount); + currentMana.Value = Mathf.Min(MaxMana, currentMana.Value + amount); } private void HandleDeath() diff --git a/Assets/Scripts/Player/PlayerSkillInput.cs b/Assets/Scripts/Player/PlayerSkillInput.cs index ddef75aa..2272534f 100644 --- a/Assets/Scripts/Player/PlayerSkillInput.cs +++ b/Assets/Scripts/Player/PlayerSkillInput.cs @@ -18,6 +18,8 @@ namespace Colosseum.Player [Header("References")] [Tooltip("SkillController (없으면 자동 검색)")] [SerializeField] private SkillController skillController; + [Tooltip("PlayerNetworkController (없으면 자동 검색)")] + [SerializeField] private PlayerNetworkController networkController; private InputSystem_Actions inputActions; @@ -43,6 +45,12 @@ namespace Colosseum.Player } } + // PlayerNetworkController 참조 확인 + if (networkController == null) + { + networkController = GetComponent(); + } + InitializeInputActions(); } @@ -89,11 +97,25 @@ namespace Colosseum.Player return; } + // 마나 비용 체크 + if (networkController != null && networkController.Mana < skill.ManaCost) + { + Debug.Log($"Not enough mana for skill: {skill.SkillName} (Required: {skill.ManaCost}, Current: {networkController.Mana})"); + return; + } + // 논타겟: 타겟 없이 스킬 시전 bool success = skillController.ExecuteSkill(skill); if (!success) { Debug.Log($"Cannot execute skill: {skill.SkillName}"); + return; + } + + // 스킬 성공 시 마나 소모 + if (networkController != null && skill.ManaCost > 0) + { + networkController.UseManaRpc(skill.ManaCost); } } diff --git a/Assets/Scripts/Skills/Effects/DamageEffect.cs b/Assets/Scripts/Skills/Effects/DamageEffect.cs index 70f803eb..2030833b 100644 --- a/Assets/Scripts/Skills/Effects/DamageEffect.cs +++ b/Assets/Scripts/Skills/Effects/DamageEffect.cs @@ -1,7 +1,20 @@ using UnityEngine; +using Colosseum.Stats; +using Colosseum.Player; namespace Colosseum.Skills.Effects { + /// + /// 대미지 타입 + /// + public enum DamageType + { + Physical, // 물리 대미지 (STR 기반) + Magical, // 마법 대미지 (INT 기반) + Ranged, // 원거리 대미지 (DEX 기반) + True, // 고정 대미지 (스탯 영향 없음) + } + /// /// 데미지 효과 /// @@ -9,18 +22,54 @@ namespace Colosseum.Skills.Effects public class DamageEffect : SkillEffect { [Header("Damage Settings")] - [Min(0f)] [SerializeField] private float damageAmount = 10f; - [SerializeField] private string damageType = "Physical"; + [Min(0f)] [SerializeField] private float baseDamage = 10f; + [SerializeField] private DamageType damageType = DamageType.Physical; + [Tooltip("스탯 계수 (1.0 = 100%)")] + [Min(0f)] [SerializeField] private float statScaling = 1f; protected override void ApplyEffect(GameObject caster, GameObject target) { if (target == null) return; - // TODO: 실제 데미지 시스템 연동 - // var health = target.GetComponent(); - // health?.TakeDamage(damageAmount, caster, damageType); + // 대미지 계산 + float totalDamage = CalculateDamage(caster); - Debug.Log($"[Damage] {caster.name} -> {target.name}: {damageAmount} ({damageType})"); + // 타겟에 대미지 적용 + var networkController = target.GetComponent(); + if (networkController != null) + { + networkController.TakeDamageRpc(totalDamage); + } + + Debug.Log($"[Damage] {caster.name} -> {target.name}: {totalDamage:F1} ({damageType})"); + } + + /// + /// 시전자 스탯 기반 대미지 계산 + /// 공식: baseDamage + (statDamage * scaling) + /// + private float CalculateDamage(GameObject caster) + { + if (damageType == DamageType.True) + { + return baseDamage; + } + + var stats = caster.GetComponent(); + if (stats == null) + { + return baseDamage; + } + + float statDamage = damageType switch + { + DamageType.Physical => stats.PhysicalDamage, + DamageType.Magical => stats.MagicDamage, + DamageType.Ranged => stats.Dexterity.FinalValue * 2f, // DEX 기반 원거리 대미지 + _ => 0f, + }; + + return baseDamage + (statDamage * statScaling); } } } diff --git a/Assets/Scripts/Skills/Effects/HealEffect.cs b/Assets/Scripts/Skills/Effects/HealEffect.cs index fb2ff46a..5ea6ce93 100644 --- a/Assets/Scripts/Skills/Effects/HealEffect.cs +++ b/Assets/Scripts/Skills/Effects/HealEffect.cs @@ -1,4 +1,6 @@ using UnityEngine; +using Colosseum.Stats; +using Colosseum.Player; namespace Colosseum.Skills.Effects { @@ -9,17 +11,40 @@ namespace Colosseum.Skills.Effects public class HealEffect : SkillEffect { [Header("Heal Settings")] - [Min(0f)] [SerializeField] private float healAmount = 10f; + [Min(0f)] [SerializeField] private float baseHeal = 10f; + [Tooltip("회복력 계수 (1.0 = 100%)")] + [Min(0f)] [SerializeField] private float healScaling = 1f; protected override void ApplyEffect(GameObject caster, GameObject target) { if (target == null) return; - // TODO: 실제 체력 시스템 연동 - // var health = target.GetComponent(); - // health?.Heal(healAmount); + // 회복량 계산 + float totalHeal = CalculateHeal(caster); - Debug.Log($"[Heal] {caster.name} -> {target.name}: {healAmount}"); + // 타겟에 회복 적용 + var networkController = target.GetComponent(); + if (networkController != null) + { + networkController.RestoreHealthRpc(totalHeal); + } + + Debug.Log($"[Heal] {caster.name} -> {target.name}: {totalHeal:F1}"); + } + + /// + /// 시전자 스탯 기반 회복량 계산 + /// 공식: baseHeal + (healPower * healScaling) + /// + private float CalculateHeal(GameObject caster) + { + var stats = caster.GetComponent(); + if (stats == null) + { + return baseHeal; + } + + return baseHeal + (stats.HealPower * healScaling); } } } diff --git a/Assets/Scripts/Stats/CharacterStat.cs b/Assets/Scripts/Stats/CharacterStat.cs new file mode 100644 index 00000000..1cf8abf1 --- /dev/null +++ b/Assets/Scripts/Stats/CharacterStat.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; + +namespace Colosseum.Stats +{ + /// + /// 개별 스탯 클래스. 기본값과 수정자를 관리하고 최종 값을 계산. + /// + [Serializable] + public class CharacterStat + { + [UnityEngine.Tooltip("기본 값")] + [UnityEngine.SerializeField] private float baseValue = 10f; + + private readonly List modifiers = new List(); + private bool isDirty = true; + private float cachedValue; + + public float BaseValue + { + get => baseValue; + set + { + baseValue = value; + isDirty = true; + } + } + + public CharacterStat() { } + + public CharacterStat(float baseValue) + { + this.baseValue = baseValue; + } + + /// + /// 최종 스탯 값. 수정자 적용 후 계산. + /// + public float FinalValue + { + get + { + if (!isDirty) return cachedValue; + + float finalValue = baseValue; + + // 1. Flat 수정자 적용 + for (int i = 0; i < modifiers.Count; i++) + { + if (modifiers[i].Type == StatModType.Flat) + finalValue += modifiers[i].Value; + } + + // 2. PercentAdd 수정자 적용 (합산 후 곱셈) + float percentAdd = 0f; + for (int i = 0; i < modifiers.Count; i++) + { + if (modifiers[i].Type == StatModType.PercentAdd) + percentAdd += modifiers[i].Value; + } + finalValue *= (1f + percentAdd); + + // 3. PercentMult 수정자 적용 (개별 곱셈) + for (int i = 0; i < modifiers.Count; i++) + { + if (modifiers[i].Type == StatModType.PercentMult) + finalValue *= modifiers[i].Value; + } + + cachedValue = finalValue; + isDirty = false; + return cachedValue; + } + } + + /// + /// 수정자 추가 + /// + public void AddModifier(StatModifier modifier) + { + modifiers.Add(modifier); + isDirty = true; + } + + /// + /// 수정자 제거 + /// + public bool RemoveModifier(StatModifier modifier) + { + bool removed = modifiers.Remove(modifier); + if (removed) isDirty = true; + return removed; + } + + /// + /// 특정 출처의 모든 수정자 제거 + /// + public void RemoveAllModifiersFromSource(object source) + { + modifiers.RemoveAll(m => m.Source == source); + isDirty = true; + } + + /// + /// 모든 수정자 제거 + /// + public void ClearModifiers() + { + modifiers.Clear(); + isDirty = true; + } + } +} diff --git a/Assets/Scripts/Stats/CharacterStats.cs b/Assets/Scripts/Stats/CharacterStats.cs new file mode 100644 index 00000000..3ed761e2 --- /dev/null +++ b/Assets/Scripts/Stats/CharacterStats.cs @@ -0,0 +1,60 @@ +using UnityEngine; + +namespace Colosseum.Stats +{ + /// + /// 캐릭터 스탯 관리 컴포넌트. + /// 6개 기본 스탯과 파생 스탯을 관리. + /// + public class CharacterStats : MonoBehaviour + { + [Header("기본 스탯")] + [SerializeField] private CharacterStat strength = new CharacterStat(10f); + [SerializeField] private CharacterStat dexterity = new CharacterStat(10f); + [SerializeField] private CharacterStat intelligence = new CharacterStat(10f); + [SerializeField] private CharacterStat vitality = new CharacterStat(10f); + [SerializeField] private CharacterStat wisdom = new CharacterStat(10f); + [SerializeField] private CharacterStat spirit = new CharacterStat(10f); + + // 기본 스탯 접근자 + public CharacterStat Strength => strength; + public CharacterStat Dexterity => dexterity; + public CharacterStat Intelligence => intelligence; + public CharacterStat Vitality => vitality; + public CharacterStat Wisdom => wisdom; + public CharacterStat Spirit => spirit; + + // 파생 스탯 프로퍼티 + // 최대 체력 = VIT * 10 + public float MaxHealth => vitality.FinalValue * 10f; + + // 최대 마나 = SPI * 5 + public float MaxMana => spirit.FinalValue * 5f; + + // 물리 대미지 = STR * 2 + public float PhysicalDamage => strength.FinalValue * 2f; + + // 마법 대미지 = INT * 2 + public float MagicDamage => intelligence.FinalValue * 2f; + + // 회복력 = WIS * 1.5 + public float HealPower => wisdom.FinalValue * 1.5f; + + /// + /// 스탯 타입으로 CharacterStat 가져오기 + /// + public CharacterStat GetStat(StatType statType) + { + return statType switch + { + StatType.Strength => strength, + StatType.Dexterity => dexterity, + StatType.Intelligence => intelligence, + StatType.Vitality => vitality, + StatType.Wisdom => wisdom, + StatType.Spirit => spirit, + _ => null, + }; + } + } +} diff --git a/Assets/Scripts/Stats/StatModifier.cs b/Assets/Scripts/Stats/StatModifier.cs new file mode 100644 index 00000000..14288f4c --- /dev/null +++ b/Assets/Scripts/Stats/StatModifier.cs @@ -0,0 +1,35 @@ +namespace Colosseum.Stats +{ + /// + /// 스탯 수정자 타입 + /// + public enum StatModType + { + Flat, // 고정값 추가 (예: +10) + PercentAdd, // 퍼센트 추가 (예: +10% → 1.1배) + PercentMult, // 퍼센트 곱셈 (예: x1.5) + } + + /// + /// 스탯 수정자. 버프/장비 등에 의한 스탯 변경을 관리. + /// + public readonly struct StatModifier + { + public readonly float Value; + public readonly StatModType Type; + public readonly object Source; + + /// + /// 스탯 수정자 생성 + /// + /// 수정값 + /// 수정 타입 + /// 출처 (버프 제거용) + public StatModifier(float value, StatModType type, object source = null) + { + Value = value; + Type = type; + Source = source; + } + } +} diff --git a/Assets/Scripts/Stats/StatType.cs b/Assets/Scripts/Stats/StatType.cs new file mode 100644 index 00000000..8afa6e37 --- /dev/null +++ b/Assets/Scripts/Stats/StatType.cs @@ -0,0 +1,15 @@ +namespace Colosseum.Stats +{ + /// + /// 캐릭터 기본 스탯 타입 + /// + public enum StatType + { + Strength, // STR - 무기 공격 대미지 + Dexterity, // DEX - 원거리 조준/대미지, 근거리 공격 속도 + Intelligence, // INT - 마법 공격 대미지 + Vitality, // VIT - 최대 체력 + Wisdom, // WIS - 회복량 + Spirit, // SPI - 최대 마나 + } +}