diff --git a/Assets/Prefabs/Player/Player.prefab b/Assets/Prefabs/Player/Player.prefab index e89e154..9fbbb2b 100644 --- a/Assets/Prefabs/Player/Player.prefab +++ b/Assets/Prefabs/Player/Player.prefab @@ -93,7 +93,6 @@ MonoBehaviour: deathEffectPrefab: {fileID: 5642766282230003982, guid: b5c8ca7ed10b61e499cce8ec3b6e2e4c, type: 3} resourcePickupPrefab: {fileID: 1627676033990080135, guid: 8c45964a69bf8fa4ba461ed217bc052f, type: 3} respawnDelay: 10 - respawnPanelPrefab: {fileID: 5112555873318329611, guid: 9257920ba4a6256499ad89eeb7d7098a, type: 3} showHealthBar: 1 healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3} --- !u!143 &3007098678582223509 @@ -423,6 +422,8 @@ MonoBehaviour: baseMoveSpeed: 5 baseSight: 10 baseAttackRange: 2 + baseHpRegen: 1 + baseHpRegenDelay: 5 --- !u!1 &1862223349553492570 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/AttackAction.cs b/Assets/Scripts/AttackAction.cs index 7e5668e..15ce99d 100644 --- a/Assets/Scripts/AttackAction.cs +++ b/Assets/Scripts/AttackAction.cs @@ -118,6 +118,12 @@ namespace Northbound { var damageable = targetObj.GetComponent(); damageable?.TakeDamage(_playerStats?.GetDamage() ?? 10, attackerNetworkId); + + // 공격자를 전투 상태로 기록 (체력 회복 방지) + if (_networkPlayerController != null) + { + _networkPlayerController.MarkInCombat(); + } } } diff --git a/Assets/Scripts/NetworkPlayerController.cs b/Assets/Scripts/NetworkPlayerController.cs index 5996edf..e019099 100644 --- a/Assets/Scripts/NetworkPlayerController.cs +++ b/Assets/Scripts/NetworkPlayerController.cs @@ -54,6 +54,10 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl private UnitHealthBar _healthBar; private RespawnCountdownUI _respawnCountdownUI; + // 체력 자연 회복 + private float _lastCombatTime; + private float _hpRegenAccumulator; + // 이 플레이어가 로컬 플레이어인지 확인 @@ -163,6 +167,12 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl void Update() { + // 서버에서 체력 자연 회복 처리 + if (IsServer) + { + UpdateHealthRegeneration(); + } + // 로컬 플레이어만 입력 처리 if (!IsLocalPlayer) return; @@ -257,6 +267,9 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl int actualDamage = Mathf.Min(damage, _currentHealth.Value); _currentHealth.Value -= actualDamage; + // 전투 상태 기록 (회복 방지) + MarkInCombat(); + // 데미지 이펙트 ShowDamageEffectClientRpc(); @@ -478,6 +491,62 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl public int GetCurrentHealth() => _currentHealth.Value; + /// + /// 전투 상태 기록 (데미지를 받거나 입힐 때 호출) + /// + public void MarkInCombat() + { + _lastCombatTime = Time.time; + } + + /// + /// 현재 전투 중인지 확인 (회복 대기 시간이 지났는지) + /// + public bool IsInCombat() + { + if (_playerStats == null) return false; + float regenDelay = _playerStats.GetHpRegenDelay(); + return (Time.time - _lastCombatTime) < regenDelay; + } + + /// + /// 서버에서 체력 자연 회복 처리 + /// + private void UpdateHealthRegeneration() + { + // 죽었으면 회복하지 않음 + if (_currentHealth.Value <= 0) return; + + // 이미 최대 체력이면 회복하지 않음 + int maxHealth = GetMaxHealth(); + if (_currentHealth.Value >= maxHealth) + { + _hpRegenAccumulator = 0f; + return; + } + + // 전투 중이면 회복하지 않음 + if (IsInCombat()) return; + + // 회복량 계산 + float hpRegen = _playerStats?.GetHpRegen() ?? 0f; + if (hpRegen <= 0f) return; + + // 프레임당 회복량 누적 + _hpRegenAccumulator += hpRegen * Time.deltaTime; + + // 1 이상 누적되면 정수만큼 회복 + if (_hpRegenAccumulator >= 1f) + { + int healAmount = Mathf.FloorToInt(_hpRegenAccumulator); + _hpRegenAccumulator -= healAmount; + + // 실제 회복 + int actualHeal = Mathf.Min(healAmount, maxHealth - _currentHealth.Value); + _currentHealth.Value += actualHeal; + } + } + public int GetMaxHealth() => _playerStats?.GetMaxHp() ?? 100; public float GetHealthPercentage() diff --git a/Assets/Scripts/PlayerStats.cs b/Assets/Scripts/PlayerStats.cs index 2e354a5..57de1df 100644 --- a/Assets/Scripts/PlayerStats.cs +++ b/Assets/Scripts/PlayerStats.cs @@ -19,6 +19,12 @@ namespace Northbound [SerializeField] private float baseSight = 10f; [SerializeField] private float baseAttackRange = 2f; + [Header("Health Regeneration")] + [Tooltip("초당 체력 회복량")] + [SerializeField] private float baseHpRegen = 2f; + [Tooltip("전투 종료 후 회복 시작까지의 대기 시간 (초)")] + [SerializeField] private float baseHpRegenDelay = 5f; + private PlayerUpgradeManager _upgradeManager; private void Awake() @@ -129,6 +135,25 @@ namespace Northbound return baseAttackRange + bonus; } + /// + /// 초당 체력 회복량 반환 + /// + public float GetHpRegen() + { + float bonus = CalculateStatBonus("player_hp_regen"); + return baseHpRegen + bonus; + } + + /// + /// 전투 종료 후 회복 시작 대기 시간 반환 (초) + /// + public float GetHpRegenDelay() + { + float bonus = CalculateStatBonus("player_hp_regen_delay"); + // delay는 보통 줄어드는 것이 좋으므로 음수 bonus는 감소로 처리 + return Mathf.Max(1f, baseHpRegenDelay + bonus); + } + #region Base Stat Setters (에디터/초기화용) public void SetBaseMaxHp(int value) => baseMaxHp = value; @@ -138,6 +163,8 @@ namespace Northbound public void SetBaseMoveSpeed(float value) => baseMoveSpeed = value; public void SetBaseSight(float value) => baseSight = value; public void SetBaseAttackRange(float value) => baseAttackRange = value; + public void SetBaseHpRegen(float value) => baseHpRegen = value; + public void SetBaseHpRegenDelay(float value) => baseHpRegenDelay = value; #endregion @@ -153,7 +180,9 @@ namespace Northbound $" Manpower: {GetManpower()} (기본: {baseManpower})\n" + $" Move Speed: {GetMoveSpeed()} (기본: {baseMoveSpeed})\n" + $" Sight: {GetSight()} (기본: {baseSight})\n" + - $" Attack Range: {GetAttackRange()} (기본: {baseAttackRange})"); + $" Attack Range: {GetAttackRange()} (기본: {baseAttackRange})\n" + + $" HP Regen: {GetHpRegen():F1}/s (기본: {baseHpRegen})\n" + + $" HP Regen Delay: {GetHpRegenDelay():F1}s (기본: {baseHpRegenDelay})"); if (_upgradeManager != null) {