Compare commits
3 Commits
1c77c5d9cd
...
9a010524f2
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a010524f2 | |||
| 34ab12a093 | |||
| d066290607 |
@@ -93,7 +93,6 @@ MonoBehaviour:
|
|||||||
deathEffectPrefab: {fileID: 5642766282230003982, guid: b5c8ca7ed10b61e499cce8ec3b6e2e4c, type: 3}
|
deathEffectPrefab: {fileID: 5642766282230003982, guid: b5c8ca7ed10b61e499cce8ec3b6e2e4c, type: 3}
|
||||||
resourcePickupPrefab: {fileID: 1627676033990080135, guid: 8c45964a69bf8fa4ba461ed217bc052f, type: 3}
|
resourcePickupPrefab: {fileID: 1627676033990080135, guid: 8c45964a69bf8fa4ba461ed217bc052f, type: 3}
|
||||||
respawnDelay: 10
|
respawnDelay: 10
|
||||||
respawnPanelPrefab: {fileID: 5112555873318329611, guid: 9257920ba4a6256499ad89eeb7d7098a, type: 3}
|
|
||||||
showHealthBar: 1
|
showHealthBar: 1
|
||||||
healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3}
|
healthBarPrefab: {fileID: 100000, guid: 8e7a5b12c9f8a4a5ba3c8d1f2e5a7b9c, type: 3}
|
||||||
--- !u!143 &3007098678582223509
|
--- !u!143 &3007098678582223509
|
||||||
@@ -423,6 +422,8 @@ MonoBehaviour:
|
|||||||
baseMoveSpeed: 5
|
baseMoveSpeed: 5
|
||||||
baseSight: 10
|
baseSight: 10
|
||||||
baseAttackRange: 2
|
baseAttackRange: 2
|
||||||
|
baseHpRegen: 1
|
||||||
|
baseHpRegenDelay: 5
|
||||||
--- !u!1 &1862223349553492570
|
--- !u!1 &1862223349553492570
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
@@ -118,6 +118,12 @@ namespace Northbound
|
|||||||
{
|
{
|
||||||
var damageable = targetObj.GetComponent<IDamageable>();
|
var damageable = targetObj.GetComponent<IDamageable>();
|
||||||
damageable?.TakeDamage(_playerStats?.GetDamage() ?? 10, attackerNetworkId);
|
damageable?.TakeDamage(_playerStats?.GetDamage() ?? 10, attackerNetworkId);
|
||||||
|
|
||||||
|
// 공격자를 전투 상태로 기록 (체력 회복 방지)
|
||||||
|
if (_networkPlayerController != null)
|
||||||
|
{
|
||||||
|
_networkPlayerController.MarkInCombat();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -916,6 +916,16 @@ namespace Northbound
|
|||||||
switch (state)
|
switch (state)
|
||||||
{
|
{
|
||||||
case EnemyAIState.Idle:
|
case EnemyAIState.Idle:
|
||||||
|
_agent.isStopped = true;
|
||||||
|
_agent.ResetPath();
|
||||||
|
// 즉시 정지를 위해 velocity 초기화
|
||||||
|
_agent.velocity = Vector3.zero;
|
||||||
|
// 복귀 완료 - 무적 해제
|
||||||
|
if (_enemyUnit != null)
|
||||||
|
{
|
||||||
|
_enemyUnit.SetInvulnerable(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case EnemyAIState.Attack:
|
case EnemyAIState.Attack:
|
||||||
_agent.isStopped = true;
|
_agent.isStopped = true;
|
||||||
_agent.ResetPath();
|
_agent.ResetPath();
|
||||||
@@ -940,6 +950,12 @@ namespace Northbound
|
|||||||
_agent.isStopped = false;
|
_agent.isStopped = false;
|
||||||
_agent.speed = moveSpeed;
|
_agent.speed = moveSpeed;
|
||||||
_agent.SetDestination(_originPosition);
|
_agent.SetDestination(_originPosition);
|
||||||
|
// 복귀 시작 - 체력 회복 및 무적
|
||||||
|
if (_enemyUnit != null)
|
||||||
|
{
|
||||||
|
_enemyUnit.HealToFull();
|
||||||
|
_enemyUnit.SetInvulnerable(true);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case EnemyAIState.Dead:
|
case EnemyAIState.Dead:
|
||||||
_agent.isStopped = true;
|
_agent.isStopped = true;
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ namespace Northbound
|
|||||||
NetworkVariableWritePermission.Server
|
NetworkVariableWritePermission.Server
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private NetworkVariable<bool> _isInvulnerable = new NetworkVariable<bool>(
|
||||||
|
false,
|
||||||
|
NetworkVariableReadPermission.Everyone,
|
||||||
|
NetworkVariableWritePermission.Server
|
||||||
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 사망 시 발생하는 이벤트 (매개변수: killerId)
|
/// 사망 시 발생하는 이벤트 (매개변수: killerId)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -106,6 +112,9 @@ namespace Northbound
|
|||||||
if (!IsServer) return;
|
if (!IsServer) return;
|
||||||
if (_currentHealth.Value <= 0) return;
|
if (_currentHealth.Value <= 0) return;
|
||||||
|
|
||||||
|
// 무적 상태면 데미지 무시
|
||||||
|
if (_isInvulnerable.Value) return;
|
||||||
|
|
||||||
// 공격자의 팀 확인
|
// 공격자의 팀 확인
|
||||||
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj))
|
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj))
|
||||||
{
|
{
|
||||||
@@ -207,6 +216,24 @@ namespace Northbound
|
|||||||
|
|
||||||
#endregion
|
#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()
|
private void OnDrawGizmosSelected()
|
||||||
{
|
{
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
|
|||||||
private UnitHealthBar _healthBar;
|
private UnitHealthBar _healthBar;
|
||||||
private RespawnCountdownUI _respawnCountdownUI;
|
private RespawnCountdownUI _respawnCountdownUI;
|
||||||
|
|
||||||
|
// 체력 자연 회복
|
||||||
|
private float _lastCombatTime;
|
||||||
|
private float _hpRegenAccumulator;
|
||||||
|
|
||||||
// 이 플레이어가 로컬 플레이어인지 확인
|
// 이 플레이어가 로컬 플레이어인지 확인
|
||||||
|
|
||||||
|
|
||||||
@@ -163,6 +167,12 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
|
|||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
|
// 서버에서 체력 자연 회복 처리
|
||||||
|
if (IsServer)
|
||||||
|
{
|
||||||
|
UpdateHealthRegeneration();
|
||||||
|
}
|
||||||
|
|
||||||
// 로컬 플레이어만 입력 처리
|
// 로컬 플레이어만 입력 처리
|
||||||
if (!IsLocalPlayer) return;
|
if (!IsLocalPlayer) return;
|
||||||
|
|
||||||
@@ -257,6 +267,9 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
|
|||||||
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
|
||||||
_currentHealth.Value -= actualDamage;
|
_currentHealth.Value -= actualDamage;
|
||||||
|
|
||||||
|
// 전투 상태 기록 (회복 방지)
|
||||||
|
MarkInCombat();
|
||||||
|
|
||||||
// 데미지 이펙트
|
// 데미지 이펙트
|
||||||
ShowDamageEffectClientRpc();
|
ShowDamageEffectClientRpc();
|
||||||
|
|
||||||
@@ -478,6 +491,62 @@ public class NetworkPlayerController : NetworkBehaviour, ITeamMember, IDamageabl
|
|||||||
|
|
||||||
public int GetCurrentHealth() => _currentHealth.Value;
|
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 int GetMaxHealth() => _playerStats?.GetMaxHp() ?? 100;
|
||||||
|
|
||||||
public float GetHealthPercentage()
|
public float GetHealthPercentage()
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ namespace Northbound
|
|||||||
[SerializeField] private float baseSight = 10f;
|
[SerializeField] private float baseSight = 10f;
|
||||||
[SerializeField] private float baseAttackRange = 2f;
|
[SerializeField] private float baseAttackRange = 2f;
|
||||||
|
|
||||||
|
[Header("Health Regeneration")]
|
||||||
|
[Tooltip("초당 체력 회복량")]
|
||||||
|
[SerializeField] private float baseHpRegen = 2f;
|
||||||
|
[Tooltip("전투 종료 후 회복 시작까지의 대기 시간 (초)")]
|
||||||
|
[SerializeField] private float baseHpRegenDelay = 5f;
|
||||||
|
|
||||||
private PlayerUpgradeManager _upgradeManager;
|
private PlayerUpgradeManager _upgradeManager;
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
@@ -129,6 +135,25 @@ namespace Northbound
|
|||||||
return baseAttackRange + bonus;
|
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 (에디터/초기화용)
|
#region Base Stat Setters (에디터/초기화용)
|
||||||
|
|
||||||
public void SetBaseMaxHp(int value) => baseMaxHp = value;
|
public void SetBaseMaxHp(int value) => baseMaxHp = value;
|
||||||
@@ -138,6 +163,8 @@ namespace Northbound
|
|||||||
public void SetBaseMoveSpeed(float value) => baseMoveSpeed = value;
|
public void SetBaseMoveSpeed(float value) => baseMoveSpeed = value;
|
||||||
public void SetBaseSight(float value) => baseSight = value;
|
public void SetBaseSight(float value) => baseSight = value;
|
||||||
public void SetBaseAttackRange(float value) => baseAttackRange = value;
|
public void SetBaseAttackRange(float value) => baseAttackRange = value;
|
||||||
|
public void SetBaseHpRegen(float value) => baseHpRegen = value;
|
||||||
|
public void SetBaseHpRegenDelay(float value) => baseHpRegenDelay = value;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -153,7 +180,9 @@ namespace Northbound
|
|||||||
$" Manpower: {GetManpower()} (기본: {baseManpower})\n" +
|
$" Manpower: {GetManpower()} (기본: {baseManpower})\n" +
|
||||||
$" Move Speed: {GetMoveSpeed()} (기본: {baseMoveSpeed})\n" +
|
$" Move Speed: {GetMoveSpeed()} (기본: {baseMoveSpeed})\n" +
|
||||||
$" Sight: {GetSight()} (기본: {baseSight})\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)
|
if (_upgradeManager != null)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user