[Stats] 캐릭터 스탯 시스템 구현
- 6가지 기본 스탯 추가 (STR, DEX, INT, VIT, WIS, SPI) - 스탯 수정자 시스템 (Flat, PercentAdd, PercentMult) - 파생 스탯 계산 (체력/마나/대미지/회복력) - 스킬 효과에 스탯 기반 대미지/회복량 적용 - 마나 비용 체크 및 소모 로직 추가
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
using Colosseum.Stats;
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
@@ -8,26 +9,33 @@ namespace Colosseum.Player
|
||||
/// </summary>
|
||||
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<float> currentHealth = new NetworkVariable<float>(100f);
|
||||
private NetworkVariable<float> currentMana = new NetworkVariable<float>(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<CharacterStats>();
|
||||
}
|
||||
|
||||
// 초기화
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 체력 회복 (서버에서만 실행)
|
||||
/// </summary>
|
||||
[Rpc(SendTo.Server)]
|
||||
public void RestoreHealthRpc(float amount)
|
||||
{
|
||||
currentHealth.Value = Mathf.Min(MaxHealth, currentHealth.Value + amount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마나 회복 (서버에서만 실행)
|
||||
/// </summary>
|
||||
[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()
|
||||
|
||||
@@ -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<PlayerNetworkController>();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
using UnityEngine;
|
||||
using Colosseum.Stats;
|
||||
using Colosseum.Player;
|
||||
|
||||
namespace Colosseum.Skills.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// 대미지 타입
|
||||
/// </summary>
|
||||
public enum DamageType
|
||||
{
|
||||
Physical, // 물리 대미지 (STR 기반)
|
||||
Magical, // 마법 대미지 (INT 기반)
|
||||
Ranged, // 원거리 대미지 (DEX 기반)
|
||||
True, // 고정 대미지 (스탯 영향 없음)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 데미지 효과
|
||||
/// </summary>
|
||||
@@ -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>();
|
||||
// health?.TakeDamage(damageAmount, caster, damageType);
|
||||
// 대미지 계산
|
||||
float totalDamage = CalculateDamage(caster);
|
||||
|
||||
Debug.Log($"[Damage] {caster.name} -> {target.name}: {damageAmount} ({damageType})");
|
||||
// 타겟에 대미지 적용
|
||||
var networkController = target.GetComponent<PlayerNetworkController>();
|
||||
if (networkController != null)
|
||||
{
|
||||
networkController.TakeDamageRpc(totalDamage);
|
||||
}
|
||||
|
||||
Debug.Log($"[Damage] {caster.name} -> {target.name}: {totalDamage:F1} ({damageType})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시전자 스탯 기반 대미지 계산
|
||||
/// 공식: baseDamage + (statDamage * scaling)
|
||||
/// </summary>
|
||||
private float CalculateDamage(GameObject caster)
|
||||
{
|
||||
if (damageType == DamageType.True)
|
||||
{
|
||||
return baseDamage;
|
||||
}
|
||||
|
||||
var stats = caster.GetComponent<CharacterStats>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
// health?.Heal(healAmount);
|
||||
// 회복량 계산
|
||||
float totalHeal = CalculateHeal(caster);
|
||||
|
||||
Debug.Log($"[Heal] {caster.name} -> {target.name}: {healAmount}");
|
||||
// 타겟에 회복 적용
|
||||
var networkController = target.GetComponent<PlayerNetworkController>();
|
||||
if (networkController != null)
|
||||
{
|
||||
networkController.RestoreHealthRpc(totalHeal);
|
||||
}
|
||||
|
||||
Debug.Log($"[Heal] {caster.name} -> {target.name}: {totalHeal:F1}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시전자 스탯 기반 회복량 계산
|
||||
/// 공식: baseHeal + (healPower * healScaling)
|
||||
/// </summary>
|
||||
private float CalculateHeal(GameObject caster)
|
||||
{
|
||||
var stats = caster.GetComponent<CharacterStats>();
|
||||
if (stats == null)
|
||||
{
|
||||
return baseHeal;
|
||||
}
|
||||
|
||||
return baseHeal + (stats.HealPower * healScaling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
113
Assets/Scripts/Stats/CharacterStat.cs
Normal file
113
Assets/Scripts/Stats/CharacterStat.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Colosseum.Stats
|
||||
{
|
||||
/// <summary>
|
||||
/// 개별 스탯 클래스. 기본값과 수정자를 관리하고 최종 값을 계산.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class CharacterStat
|
||||
{
|
||||
[UnityEngine.Tooltip("기본 값")]
|
||||
[UnityEngine.SerializeField] private float baseValue = 10f;
|
||||
|
||||
private readonly List<StatModifier> modifiers = new List<StatModifier>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 최종 스탯 값. 수정자 적용 후 계산.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 수정자 추가
|
||||
/// </summary>
|
||||
public void AddModifier(StatModifier modifier)
|
||||
{
|
||||
modifiers.Add(modifier);
|
||||
isDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 수정자 제거
|
||||
/// </summary>
|
||||
public bool RemoveModifier(StatModifier modifier)
|
||||
{
|
||||
bool removed = modifiers.Remove(modifier);
|
||||
if (removed) isDirty = true;
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 출처의 모든 수정자 제거
|
||||
/// </summary>
|
||||
public void RemoveAllModifiersFromSource(object source)
|
||||
{
|
||||
modifiers.RemoveAll(m => m.Source == source);
|
||||
isDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 수정자 제거
|
||||
/// </summary>
|
||||
public void ClearModifiers()
|
||||
{
|
||||
modifiers.Clear();
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
Assets/Scripts/Stats/CharacterStats.cs
Normal file
60
Assets/Scripts/Stats/CharacterStats.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Stats
|
||||
{
|
||||
/// <summary>
|
||||
/// 캐릭터 스탯 관리 컴포넌트.
|
||||
/// 6개 기본 스탯과 파생 스탯을 관리.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 스탯 타입으로 CharacterStat 가져오기
|
||||
/// </summary>
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Assets/Scripts/Stats/StatModifier.cs
Normal file
35
Assets/Scripts/Stats/StatModifier.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace Colosseum.Stats
|
||||
{
|
||||
/// <summary>
|
||||
/// 스탯 수정자 타입
|
||||
/// </summary>
|
||||
public enum StatModType
|
||||
{
|
||||
Flat, // 고정값 추가 (예: +10)
|
||||
PercentAdd, // 퍼센트 추가 (예: +10% → 1.1배)
|
||||
PercentMult, // 퍼센트 곱셈 (예: x1.5)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 스탯 수정자. 버프/장비 등에 의한 스탯 변경을 관리.
|
||||
/// </summary>
|
||||
public readonly struct StatModifier
|
||||
{
|
||||
public readonly float Value;
|
||||
public readonly StatModType Type;
|
||||
public readonly object Source;
|
||||
|
||||
/// <summary>
|
||||
/// 스탯 수정자 생성
|
||||
/// </summary>
|
||||
/// <param name="value">수정값</param>
|
||||
/// <param name="type">수정 타입</param>
|
||||
/// <param name="source">출처 (버프 제거용)</param>
|
||||
public StatModifier(float value, StatModType type, object source = null)
|
||||
{
|
||||
Value = value;
|
||||
Type = type;
|
||||
Source = source;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
Assets/Scripts/Stats/StatType.cs
Normal file
15
Assets/Scripts/Stats/StatType.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Colosseum.Stats
|
||||
{
|
||||
/// <summary>
|
||||
/// 캐릭터 기본 스탯 타입
|
||||
/// </summary>
|
||||
public enum StatType
|
||||
{
|
||||
Strength, // STR - 무기 공격 대미지
|
||||
Dexterity, // DEX - 원거리 조준/대미지, 근거리 공격 속도
|
||||
Intelligence, // INT - 마법 공격 대미지
|
||||
Vitality, // VIT - 최대 체력
|
||||
Wisdom, // WIS - 회복량
|
||||
Spirit, // SPI - 최대 마나
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user