Compare commits

...

3 Commits

Author SHA1 Message Date
9a010524f2 Merge branch 'main' of http://192.168.10.102:30008/dal4segno/Northbound 2026-02-25 21:13:14 +09:00
34ab12a093 추격 포기로 인한 복귀 시, 복귀하는 동안 무적 상태 + 체력 전체 회복 2026-02-25 21:13:06 +09:00
d066290607 플레이어 체력 자연 회복 기능 추가
전투 상태 감지 기능
player stat으로 관리 가능
2026-02-25 21:09:19 +09:00
6 changed files with 150 additions and 2 deletions

View File

@@ -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

View File

@@ -118,6 +118,12 @@ namespace Northbound
{
var damageable = targetObj.GetComponent<IDamageable>();
damageable?.TakeDamage(_playerStats?.GetDamage() ?? 10, attackerNetworkId);
// 공격자를 전투 상태로 기록 (체력 회복 방지)
if (_networkPlayerController != null)
{
_networkPlayerController.MarkInCombat();
}
}
}

View File

@@ -916,6 +916,16 @@ namespace Northbound
switch (state)
{
case EnemyAIState.Idle:
_agent.isStopped = true;
_agent.ResetPath();
// 즉시 정지를 위해 velocity 초기화
_agent.velocity = Vector3.zero;
// 복귀 완료 - 무적 해제
if (_enemyUnit != null)
{
_enemyUnit.SetInvulnerable(false);
}
break;
case EnemyAIState.Attack:
_agent.isStopped = true;
_agent.ResetPath();
@@ -940,6 +950,12 @@ namespace Northbound
_agent.isStopped = false;
_agent.speed = moveSpeed;
_agent.SetDestination(_originPosition);
// 복귀 시작 - 체력 회복 및 무적
if (_enemyUnit != null)
{
_enemyUnit.HealToFull();
_enemyUnit.SetInvulnerable(true);
}
break;
case EnemyAIState.Dead:
_agent.isStopped = true;

View File

@@ -36,6 +36,12 @@ namespace Northbound
NetworkVariableWritePermission.Server
);
private NetworkVariable<bool> _isInvulnerable = new NetworkVariable<bool>(
false,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
/// <summary>
/// 사망 시 발생하는 이벤트 (매개변수: killerId)
/// </summary>
@@ -106,6 +112,9 @@ namespace Northbound
if (!IsServer) return;
if (_currentHealth.Value <= 0) return;
// 무적 상태면 데미지 무시
if (_isInvulnerable.Value) return;
// 공격자의 팀 확인
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj))
{
@@ -207,6 +216,24 @@ namespace Northbound
#endregion
#region Invulnerability & Healing
public bool IsInvulnerable() => _isInvulnerable.Value;
public void SetInvulnerable(bool value)
{
if (!IsServer) return;
_isInvulnerable.Value = value;
}
public void HealToFull()
{
if (!IsServer) return;
_currentHealth.Value = maxHealth;
}
#endregion
private void OnDrawGizmosSelected()
{
#if UNITY_EDITOR

View File

@@ -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;
/// <summary>
/// 전투 상태 기록 (데미지를 받거나 입힐 때 호출)
/// </summary>
public void MarkInCombat()
{
_lastCombatTime = Time.time;
}
/// <summary>
/// 현재 전투 중인지 확인 (회복 대기 시간이 지났는지)
/// </summary>
public bool IsInCombat()
{
if (_playerStats == null) return false;
float regenDelay = _playerStats.GetHpRegenDelay();
return (Time.time - _lastCombatTime) < regenDelay;
}
/// <summary>
/// 서버에서 체력 자연 회복 처리
/// </summary>
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()

View File

@@ -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;
}
/// <summary>
/// 초당 체력 회복량 반환
/// </summary>
public float GetHpRegen()
{
float bonus = CalculateStatBonus("player_hp_regen");
return baseHpRegen + bonus;
}
/// <summary>
/// 전투 종료 후 회복 시작 대기 시간 반환 (초)
/// </summary>
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)
{