From d066290607b5b8e2153c66ee86fd62b569cd44ce Mon Sep 17 00:00:00 2001 From: dal4segno Date: Wed, 25 Feb 2026 21:09:19 +0900 Subject: [PATCH] =?UTF-8?q?=ED=94=8C=EB=A0=88=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EC=B2=B4=EB=A0=A5=20=EC=9E=90=EC=97=B0=20=ED=9A=8C=EB=B3=B5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 전투 상태 감지 기능 player stat으로 관리 가능 --- Assets/Prefabs/Player/Player.prefab | 3 +- Assets/Scripts/AttackAction.cs | 6 ++ Assets/Scripts/NetworkPlayerController.cs | 69 +++++++++++++++++++++++ Assets/Scripts/PlayerStats.cs | 31 +++++++++- 4 files changed, 107 insertions(+), 2 deletions(-) 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) {