수정본
카드 ui, 대쉬 , 플레이어 공격 범위 .
This commit is contained in:
parent
4164e01d6a
commit
620b0e47e9
File diff suppressed because it is too large
Load Diff
|
|
@ -107,13 +107,6 @@ ModelImporter:
|
|||
bodyMask: 01000000010000000100000001000000010000000100000001000000010000000100000001000000010000000100000001000000
|
||||
curves: []
|
||||
events:
|
||||
- time: 0
|
||||
functionName: StartWeaponCollision
|
||||
data:
|
||||
objectReferenceParameter: {instanceID: 0}
|
||||
floatParameter: 0
|
||||
intParameter: 0
|
||||
messageOptions: 0
|
||||
- time: 0.44848183
|
||||
functionName: StartWeaponCollision
|
||||
data:
|
||||
|
|
|
|||
|
|
@ -39,24 +39,21 @@ public class PlayerAttack : MonoBehaviour
|
|||
if (Time.time < _lastAttackTime + attackCooldown) return;
|
||||
|
||||
_isAttacking = true;
|
||||
_comboCount = (_comboCount % 3) + 1;
|
||||
_comboCount = (_comboCount % 3) + 1; // 1 -> 2 -> 3 순환
|
||||
|
||||
pAnim.TriggerAttack();
|
||||
_lastAttackTime = Time.time;
|
||||
}
|
||||
|
||||
// ⭐ [수정] 휘두를 때 데미지를 실어서 판정을 켭니다.
|
||||
public void StartWeaponCollision()
|
||||
{
|
||||
if (weaponHitBox != null)
|
||||
{
|
||||
Debug.Log("<color=yellow>[Attack]</color> 낫 공격 판정 ON!");
|
||||
// stats에 설정된 공격력을 가져와서 전달하면 더 좋습니다.
|
||||
weaponHitBox.EnableHitBox(20f);
|
||||
}
|
||||
}
|
||||
|
||||
// ⭐ [수정] 휘두르기가 끝나면 확실하게 판정을 끕니다.
|
||||
public void StopWeaponCollision()
|
||||
{
|
||||
if (weaponHitBox != null)
|
||||
|
|
@ -66,26 +63,25 @@ public class PlayerAttack : MonoBehaviour
|
|||
}
|
||||
}
|
||||
|
||||
// ⭐ [수정 완료] 오직 3타 막타일 때만 카메라 연출을 실행합니다.
|
||||
public void OnAttackShake()
|
||||
{
|
||||
if (CinemachineShake.Instance == null) return;
|
||||
|
||||
// 3번째 콤보일 때만 묵직하게 흔들어줍니다.
|
||||
if (_comboCount == 3)
|
||||
{
|
||||
CinemachineShake.Instance.HitSlow(0.2f, 0.05f);
|
||||
CinemachineShake.Instance.CameraKick(10f);
|
||||
CinemachineShake.Instance.ShakeAttack();
|
||||
}
|
||||
else
|
||||
{
|
||||
CinemachineShake.Instance.HitSlow(0.1f, 0.2f);
|
||||
CinemachineShake.Instance.ShakeAttack();
|
||||
Debug.Log("<color=orange>[Shake]</color> 3타 막타 카메라 연출 실행!");
|
||||
CinemachineShake.Instance.HitSlow(0.2f, 0.05f); // 히트 슬로우
|
||||
CinemachineShake.Instance.CameraKick(10f); // 카메라 킥
|
||||
CinemachineShake.Instance.ShakeAttack(); // 진동
|
||||
}
|
||||
// 🚫 else 구문을 삭제하여 1, 2타 시에는 아무것도 하지 않습니다.
|
||||
}
|
||||
|
||||
public void OnAttackEnd()
|
||||
{
|
||||
StopWeaponCollision(); // 🚫 안전장치: 공격이 끝나면 무조건 판정을 끕니다.
|
||||
StopWeaponCollision();
|
||||
if (_comboCount == 3) { StartCoroutine(PostComboRecovery()); }
|
||||
else { _isAttacking = false; }
|
||||
}
|
||||
|
|
@ -129,7 +125,6 @@ public class PlayerAttack : MonoBehaviour
|
|||
|
||||
if (targetDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(targetDirection);
|
||||
|
||||
// 🛠️ 무기 독립 로직 (자식에서 떼어내기)
|
||||
GameObject weaponObj = interaction.CurrentWeapon.gameObject;
|
||||
weaponObj.transform.SetParent(null);
|
||||
|
||||
|
|
@ -162,9 +157,10 @@ public class PlayerAttack : MonoBehaviour
|
|||
pAnim.SetCharging(true);
|
||||
if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true);
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
Gizmos.color = Color.red; // 공격 범위는 빨간색으로 표시
|
||||
Gizmos.color = Color.red;
|
||||
if (TryGetComponent<BoxCollider>(out var box))
|
||||
{
|
||||
Gizmos.matrix = transform.localToWorldMatrix;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using UnityEngine;
|
||||
using System;
|
||||
using System.Collections; // ⭐ IEnumerator 사용을 위해 추가
|
||||
|
||||
public class PlayerHealth : MonoBehaviour, IDamageable
|
||||
{
|
||||
|
|
@ -8,44 +9,41 @@ public class PlayerHealth : MonoBehaviour, IDamageable
|
|||
|
||||
public bool IsDead { get; private set; }
|
||||
public bool isHit { get; private set; }
|
||||
public bool isInvincible; // 대시 중 무적 플래그
|
||||
|
||||
public event Action OnHit, OnDead;
|
||||
public event Action<float, float> OnHealthChanged;
|
||||
|
||||
private float _currentHealth;
|
||||
|
||||
private void Start()
|
||||
// ⭐ [수정] Start를 코루틴으로 변경하여 실행 순서 문제를 해결합니다.
|
||||
private IEnumerator Start()
|
||||
{
|
||||
// 모든 오브젝트가 깨어날 때까지 한 프레임 기다립니다.
|
||||
yield return null;
|
||||
|
||||
if (stats != null)
|
||||
{
|
||||
_currentHealth = stats.MaxHealth;
|
||||
// 이제 UI가 확실히 준비되었으므로 수치를 전달합니다.
|
||||
OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth);
|
||||
Debug.Log($"<color=cyan>[UI Sync]</color> 초기 체력 설정 완료: {_currentHealth}/{stats.MaxHealth}");
|
||||
}
|
||||
if (animator == null) animator = GetComponent<Animator>();
|
||||
}
|
||||
|
||||
// ⭐ [최종 수정] 최대 체력 변동 시 호출: 피는 안 채우되, 그릇(MaxHealth)을 넘치는 피는 깎습니다.
|
||||
public void RefreshHealthUI()
|
||||
{
|
||||
if (stats != null)
|
||||
{
|
||||
// 1. [핵심] 현재 체력이 최대 체력을 넘지 못하도록 고정 (Clamp)
|
||||
// 최대 체력이 늘어나면 기존 피 유지, 줄어들어서 피가 넘치면 최대치로 깎음
|
||||
_currentHealth = Mathf.Min(_currentHealth, stats.MaxHealth);
|
||||
|
||||
// 2. 즉시 UI에 바뀐 수치를 보냅니다.
|
||||
if (OnHealthChanged != null)
|
||||
{
|
||||
OnHealthChanged.Invoke(_currentHealth, stats.MaxHealth);
|
||||
Debug.Log($"[Health Sync] UI 갱신 완료: {_currentHealth} / {stats.MaxHealth}");
|
||||
}
|
||||
OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth);
|
||||
}
|
||||
}
|
||||
|
||||
// ... (TakeDamage 등 나머지 기존 코드 유지)
|
||||
public void TakeDamage(float amount)
|
||||
{
|
||||
if (IsDead) return;
|
||||
if (isInvincible || IsDead) return;
|
||||
_currentHealth = Mathf.Max(0, _currentHealth - amount);
|
||||
OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth);
|
||||
OnHit?.Invoke();
|
||||
|
|
@ -58,24 +56,10 @@ public class PlayerHealth : MonoBehaviour, IDamageable
|
|||
isHit = true;
|
||||
if (animator != null) animator.Play("HitAnime", 0, 0f);
|
||||
CancelInvoke(nameof(OnHitEnd));
|
||||
Invoke(nameof(OnHitEnd), 0.05f);
|
||||
Invoke(nameof(OnHitEnd), 0.25f);
|
||||
}
|
||||
public void OnHitEnd() { isHit = false; }
|
||||
private void Die()
|
||||
{
|
||||
IsDead = true;
|
||||
Cursor.visible = true;
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
OnDead?.Invoke();
|
||||
}
|
||||
public void Heal(float amount)
|
||||
{
|
||||
if (IsDead) return;
|
||||
|
||||
// 현재 체력에 회복량을 더하되, Stats의 MaxHealth를 넘지 못하게 합니다.
|
||||
_currentHealth = Mathf.Min(_currentHealth + amount, stats.MaxHealth);
|
||||
|
||||
// UI 갱신
|
||||
OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth);
|
||||
}
|
||||
private void Die() { IsDead = true; Cursor.visible = true; Cursor.lockState = CursorLockMode.None; OnDead?.Invoke(); }
|
||||
public void Heal(float amount) { if (IsDead) return; _currentHealth = Mathf.Min(_currentHealth + amount, stats.MaxHealth); OnHealthChanged?.Invoke(_currentHealth, stats.MaxHealth); }
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
using UnityEngine;
|
||||
using TMPro; // TextMeshPro를 위해 필수
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
public class PlayerStatsUI : MonoBehaviour
|
||||
{
|
||||
[Header("--- 데이터 연결 ---")]
|
||||
[Header("--- 데이터 연결 ---")]
|
||||
[SerializeField] private Stats playerStats;
|
||||
|
||||
[Header("--- UI 오브젝트 ---")]
|
||||
[SerializeField] private GameObject statWindowPanel; // C키로 껐다 켤 부모 판넬
|
||||
[Header("--- UI 오브젝트 ---")]
|
||||
[SerializeField] private GameObject statWindowPanel;
|
||||
|
||||
[Header("--- 텍스트 UI ---")]
|
||||
[Header("--- 텍스트 UI ---")]
|
||||
[SerializeField] private TextMeshProUGUI maxHealthText;
|
||||
[SerializeField] private TextMeshProUGUI strengthText;
|
||||
[SerializeField] private TextMeshProUGUI damageText;
|
||||
|
|
@ -17,35 +17,30 @@ public class PlayerStatsUI : MonoBehaviour
|
|||
|
||||
private void Start()
|
||||
{
|
||||
// 시작할 때는 창을 닫아둡니다.
|
||||
if (statWindowPanel != null)
|
||||
statWindowPanel.SetActive(false);
|
||||
if (statWindowPanel != null) statWindowPanel.SetActive(false);
|
||||
}
|
||||
|
||||
// 창을 열거나 닫는 함수
|
||||
public void ToggleWindow()
|
||||
{
|
||||
if (statWindowPanel == null) return;
|
||||
|
||||
bool isActive = !statWindowPanel.activeSelf;
|
||||
statWindowPanel.SetActive(isActive);
|
||||
|
||||
// 창이 켜질 때만 최신 정보로 글자를 갱신합니다.
|
||||
if (isActive)
|
||||
{
|
||||
UpdateStatTexts();
|
||||
}
|
||||
if (isActive) UpdateStatTexts();
|
||||
}
|
||||
|
||||
private void UpdateStatTexts()
|
||||
public void UpdateStatTexts()
|
||||
{
|
||||
if (playerStats == null) return;
|
||||
|
||||
// Stats.cs의 실제 로직용 프로퍼티에서 데이터를 가져옵니다.
|
||||
maxHealthText.text = $"MaxHP: {playerStats.MaxHealth}";
|
||||
strengthText.text = $"Strength: {playerStats.Strength}";
|
||||
damageText.text = $"Damage: {playerStats.BaseAttackDamage}";
|
||||
// 스피드는 소수점 한 자리까지 깔끔하게 표시
|
||||
speedText.text = $"Speed: {playerStats.CurrentMoveSpeed:F1}";
|
||||
|
||||
// ⭐ 데미지: 최종합 (+무기보너스)
|
||||
damageText.text = $"Damage: {playerStats.TotalAttackDamage} (+{playerStats.weaponDamage})";
|
||||
|
||||
// ⭐ [수정] 스피드: 현재속도 (-무게페널티) 형태로 표기합니다.
|
||||
// 페널티가 0보다 클 때만 빨간색 느낌으로 괄호 수치를 띄워줍니다.
|
||||
string penaltyText = playerStats.WeightPenalty > 0 ? $" (-{playerStats.WeightPenalty:F1})" : "";
|
||||
speedText.text = $"Speed: {playerStats.CurrentMoveSpeed:F1}{penaltyText}";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using UnityEngine;
|
||||
|
||||
public class PlayerInput : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private PlayerHealth health;
|
||||
|
|
@ -6,22 +7,26 @@ public class PlayerInput : MonoBehaviour
|
|||
[SerializeField] private PlayerAim aim;
|
||||
[SerializeField] private PlayerInteraction interaction;
|
||||
[SerializeField] private PlayerAttack attack;
|
||||
[SerializeField] private PlayerStatsUI statsUI; // ⭐ 추가: UI 매니저 연결
|
||||
[SerializeField] private PlayerStatsUI statsUI;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (health != null && health.IsDead) return;
|
||||
|
||||
// ⭐ [추가] C키를 누르면 상태창 토글
|
||||
if (Input.GetKeyDown(KeyCode.C) && statsUI != null)
|
||||
{
|
||||
statsUI.ToggleWindow();
|
||||
}
|
||||
if (Input.GetKeyDown(KeyCode.C) && statsUI != null) statsUI.ToggleWindow();
|
||||
|
||||
float h = Input.GetAxisRaw("Horizontal");
|
||||
float v = Input.GetAxisRaw("Vertical");
|
||||
bool sprint = Input.GetKey(KeyCode.LeftShift);
|
||||
if (movement != null) movement.SetMoveInput(new Vector3(h, 0, v).normalized, sprint);
|
||||
|
||||
if (movement != null)
|
||||
{
|
||||
movement.SetMoveInput(new Vector3(h, 0, v).normalized, sprint);
|
||||
|
||||
// ⭐ [추가] Space 키를 누르면 대시 실행
|
||||
if (Input.GetKeyDown(KeyCode.Space)) movement.AttemptDash();
|
||||
}
|
||||
|
||||
if (aim != null) aim.RotateTowardsMouse();
|
||||
if (Input.GetKeyDown(KeyCode.F) && interaction != null) interaction.TryInteract();
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,8 @@ public class PlayerInteraction : MonoBehaviour
|
|||
public void TryInteract()
|
||||
{
|
||||
Collider[] hits = Physics.OverlapSphere(transform.position, interactRange, itemLayer);
|
||||
|
||||
foreach (var hit in hits)
|
||||
{
|
||||
// 1. 무기 상호작용
|
||||
if (hit.TryGetComponent<EquippableItem>(out var item))
|
||||
{
|
||||
if (playerStats.Strength >= item.Config.RequiredStrength)
|
||||
|
|
@ -25,28 +23,10 @@ public class PlayerInteraction : MonoBehaviour
|
|||
EquipWeapon(item);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// ⭐ [핵심 수정] 힘이 부족하면 카메라를 흔듭니다!
|
||||
CinemachineShake.Instance?.ShakeNoNo();
|
||||
Debug.Log($"힘 부족! 필요: {item.Config.RequiredStrength}, 현재: {playerStats.Strength}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 포션 상호작용
|
||||
if (hit.TryGetComponent<HealthPotion>(out var potion))
|
||||
{
|
||||
potion.Use(GetComponent<PlayerHealth>());
|
||||
break;
|
||||
}
|
||||
|
||||
// 3. 제단 상호작용
|
||||
if (hit.TryGetComponent<HealthAltar>(out var altar))
|
||||
{
|
||||
altar.Use(GetComponent<PlayerHealth>());
|
||||
break;
|
||||
else { CinemachineShake.Instance?.ShakeNoNo(); continue; }
|
||||
}
|
||||
if (hit.TryGetComponent<HealthPotion>(out var potion)) { potion.Use(GetComponent<PlayerHealth>()); break; }
|
||||
if (hit.TryGetComponent<HealthAltar>(out var altar)) { altar.Use(GetComponent<PlayerHealth>()); break; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,8 +35,27 @@ public class PlayerInteraction : MonoBehaviour
|
|||
if (_currentWeapon != null) _currentWeapon.OnDropped(transform.forward);
|
||||
_currentWeapon = item;
|
||||
_currentWeapon.OnPickedUp(handSlot);
|
||||
|
||||
if (playerStats != null)
|
||||
{
|
||||
playerStats.weaponDamage = item.Config.BaseDamage;
|
||||
playerStats.UpdateWeaponWeight(item.Config.RequiredStrength);
|
||||
}
|
||||
|
||||
public void ClearCurrentWeapon() => _currentWeapon = null;
|
||||
// ⭐ 무기 장착 즉시 UI 갱신
|
||||
FindObjectOfType<PlayerStatsUI>()?.UpdateStatTexts();
|
||||
}
|
||||
|
||||
public void ClearCurrentWeapon()
|
||||
{
|
||||
_currentWeapon = null;
|
||||
if (playerStats != null)
|
||||
{
|
||||
playerStats.weaponDamage = 0;
|
||||
playerStats.ResetWeight();
|
||||
|
||||
// ⭐ 무기 제거 즉시 UI 갱신
|
||||
FindObjectOfType<PlayerStatsUI>()?.UpdateStatTexts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
public class PlayerMovement : MonoBehaviour
|
||||
{
|
||||
|
|
@ -8,32 +9,55 @@ public class PlayerMovement : MonoBehaviour
|
|||
[SerializeField] private PlayerAnimator pAnim;
|
||||
[SerializeField] private PlayerAttack attackScript;
|
||||
|
||||
[Header("--- 대시 설정 ---")]
|
||||
// ⭐ 유저님 이미지(image_42ccbd.png) 수치를 기본값으로 적용했습니다.
|
||||
[SerializeField] private float dashDistance = 3f;
|
||||
[SerializeField] private float dashDuration = 0.08f;
|
||||
[SerializeField] private float dashCooldown = 1.5f;
|
||||
|
||||
[Header("--- 차징 감속 설정 ---")]
|
||||
[Range(0.1f, 1f)]
|
||||
[SerializeField] private float minSpeedMultiplier = 0.3f;
|
||||
|
||||
private Vector3 _moveDir;
|
||||
private bool _isSprinting;
|
||||
private bool _isDashing;
|
||||
private float _lastDashTime;
|
||||
|
||||
// ⭐ PlayerInput에서 호출하는 입력 세팅
|
||||
public void SetMoveInput(Vector3 dir, bool sprint) { _moveDir = dir; _isSprinting = sprint; }
|
||||
|
||||
// ⭐ PlayerInput에서 Space를 누를 때 호출하는 대시 시도 함수
|
||||
public void AttemptDash()
|
||||
{
|
||||
if (CanDash()) StartCoroutine(DashRoutine());
|
||||
}
|
||||
|
||||
private bool CanDash()
|
||||
{
|
||||
// 쿨타임 체크 + 대시 중 아님 + 사망 아님
|
||||
bool isCooldownOver = Time.time >= _lastDashTime + dashCooldown;
|
||||
bool isAlive = health != null && !health.IsDead;
|
||||
return isCooldownOver && !_isDashing && isAlive;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 1. 💀 사망 시 혹은 ⚔️ 공격 중일 때 이동 차단
|
||||
if (health != null && health.IsDead)
|
||||
// 1. 💀 사망, ⚡ 대시 중, 🤕 혹은 피격(isHit) 중일 때 이동 차단
|
||||
if (health != null && (health.IsDead || health.isHit || _isDashing))
|
||||
{
|
||||
if (pAnim != null && !health.IsDead) pAnim.UpdateMove(0f);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. ⚔️ 공격 중(콤보 및 후딜레이)일 때 이동 차단
|
||||
if (attackScript != null && attackScript.IsAttacking)
|
||||
{
|
||||
if (pAnim != null) pAnim.UpdateMove(0f);
|
||||
return;
|
||||
}
|
||||
|
||||
// ⭐ [추가] 공격 중(콤보 및 후딜레이 포함)에는 이동 입력 무시
|
||||
if (attackScript != null && attackScript.IsAttacking)
|
||||
{
|
||||
if (pAnim != null) pAnim.UpdateMove(0f); // 제자리 대기 애니메이션 유도
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 이동 속도 계산 및 차징 감속 적용
|
||||
// 3. 이동 속도 계산 및 차징 감속 적용
|
||||
float speed = _isSprinting ? stats.CurrentRunSpeed : stats.CurrentMoveSpeed;
|
||||
|
||||
if (attackScript != null && attackScript.IsCharging)
|
||||
|
|
@ -42,10 +66,10 @@ public class PlayerMovement : MonoBehaviour
|
|||
speed *= speedReduction;
|
||||
}
|
||||
|
||||
// 3. 실제 이동 처리
|
||||
// 4. 실제 이동 처리
|
||||
transform.Translate(_moveDir * speed * Time.deltaTime, Space.World);
|
||||
|
||||
// 4. 애니메이션 연동
|
||||
// 5. 애니메이션 연동
|
||||
if (pAnim != null)
|
||||
{
|
||||
float animVal = _moveDir.magnitude > 0.1f ? (_isSprinting ? 1.0f : 0.5f) : 0f;
|
||||
|
|
@ -53,4 +77,30 @@ public class PlayerMovement : MonoBehaviour
|
|||
pAnim.UpdateMove(animVal);
|
||||
}
|
||||
}
|
||||
|
||||
// ⭐ [핵심] 방향성 대시 및 무적 처리 로직
|
||||
private IEnumerator DashRoutine()
|
||||
{
|
||||
_isDashing = true;
|
||||
_lastDashTime = Time.time;
|
||||
|
||||
// 입력 방향이 있으면 그쪽으로, 없으면 캐릭터의 정면이 아닌 후방(회피)으로 대시
|
||||
Vector3 dashDir = _moveDir.sqrMagnitude > 0.001f ? _moveDir : -transform.forward;
|
||||
|
||||
// 🛡️ [무적] 대시 시작 시 무적 상태 활성화
|
||||
if (health != null) health.isInvincible = true;
|
||||
|
||||
float startTime = Time.time;
|
||||
while (Time.time < startTime + dashDuration)
|
||||
{
|
||||
// 속도 = 거리 / 시간
|
||||
float speed = dashDistance / dashDuration;
|
||||
transform.Translate(dashDir * speed * Time.deltaTime, Space.World);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// 🛡️ [무적] 대시 종료 시 무적 상태 해제
|
||||
if (health != null) health.isInvincible = false;
|
||||
_isDashing = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,39 +16,37 @@ public class Stats : MonoBehaviour
|
|||
public float bonusStrength;
|
||||
public float bonusAttackDamage;
|
||||
|
||||
[Header("--- 장착 장비 ---")]
|
||||
public float weaponDamage; // ⭐ 추가: 무기의 순수 공격력
|
||||
|
||||
[Header("--- 밸런스 설정 ---")]
|
||||
[SerializeField] private float weightToSpeedPenalty = 0.1f;
|
||||
[SerializeField] private float runSpeedMultiplier = 1.5f;
|
||||
private float _weightPenalty = 0f;
|
||||
|
||||
// ⭐ [추가] 현재 적용 중인 무게 페널티 수치를 외부(UI)에 공개합니다.
|
||||
public float WeightPenalty => _weightPenalty;
|
||||
|
||||
/* =========================
|
||||
* 실제 게임 로직용 프로퍼티
|
||||
* ========================= */
|
||||
public float MaxHealth => baseMaxHealth + bonusMaxHealth;
|
||||
public float Strength => baseStrength + bonusStrength;
|
||||
public float BaseAttackDamage => baseAttackDamage + bonusAttackDamage;
|
||||
public float TotalAttackDamage => BaseAttackDamage + weaponDamage;
|
||||
|
||||
// ⭐ [에러 해결] PlayerMovement.cs에서 사용하는 속도 프로퍼티
|
||||
public float CurrentMoveSpeed => Mathf.Max(1f, baseMoveSpeed + bonusMoveSpeed - _weightPenalty);
|
||||
public float CurrentRunSpeed => CurrentMoveSpeed * runSpeedMultiplier;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 인스펙터 실시간 표시용 (Read Only)
|
||||
finalMaxHealth = MaxHealth;
|
||||
finalMoveSpeed = CurrentMoveSpeed;
|
||||
finalStrength = Strength;
|
||||
finalAttackDamage = BaseAttackDamage;
|
||||
}
|
||||
|
||||
// ⭐ 레벨업 시 기초 능력치 영구 상승
|
||||
public void AddBaseLevelUpStats(float hpAdd, float strAdd)
|
||||
{
|
||||
baseMaxHealth += hpAdd;
|
||||
baseStrength += strAdd;
|
||||
Debug.Log($"[Stats] 기초 스탯 상승 완료! 최대 체력: {MaxHealth}");
|
||||
finalAttackDamage = TotalAttackDamage;
|
||||
}
|
||||
|
||||
public void AddBaseLevelUpStats(float hpAdd, float strAdd) { baseMaxHealth += hpAdd; baseStrength += strAdd; }
|
||||
public void AddMaxHealth(float value) => bonusMaxHealth += value;
|
||||
public void AddMoveSpeed(float value) => bonusMoveSpeed += value;
|
||||
public void AddStrength(float value) => bonusStrength += value;
|
||||
|
|
|
|||
|
|
@ -4,42 +4,37 @@ using System.Collections.Generic;
|
|||
public class WeaponHitBox : MonoBehaviour
|
||||
{
|
||||
private float _damage;
|
||||
private bool _isActive = false; // ⭐ 기본은 꺼져 있어야 합니다!
|
||||
private bool _isActive = false;
|
||||
private List<IDamageable> _hitTargets = new List<IDamageable>();
|
||||
|
||||
public void EnableHitBox(float damage)
|
||||
{
|
||||
_damage = damage;
|
||||
_isActive = true; // 이제부터 공격 가능
|
||||
_isActive = true;
|
||||
_hitTargets.Clear();
|
||||
gameObject.SetActive(true); // 오브젝트도 함께 켜줍니다.
|
||||
gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void DisableHitBox()
|
||||
{
|
||||
_isActive = false; // 공격 불가능
|
||||
gameObject.SetActive(false); // 오브젝트도 꺼줍니다.
|
||||
_isActive = false;
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
// 1. 공격 활성화 상태가 아니면 무시 (근접 킬 방지)
|
||||
if (!_isActive) return;
|
||||
if (!_isActive || other.CompareTag("Player")) return;
|
||||
|
||||
// 2. ⭐ [자해 방지] 부딪힌 대상이 나(Player)라면 무시합니다!
|
||||
if (other.CompareTag("Player")) return;
|
||||
// ⭐ [핵심] 몬스터의 감지 영역(Trigger)은 무시하고 '진짜 몸통'만 타격!
|
||||
if (other.isTrigger) return;
|
||||
|
||||
// 3. 적(IDamageable)에게 데미지 입히기
|
||||
if (other.TryGetComponent<IDamageable>(out var target))
|
||||
{
|
||||
if (!_hitTargets.Contains(target))
|
||||
{
|
||||
target.TakeDamage(_damage);
|
||||
_hitTargets.Add(target);
|
||||
Debug.Log($"<color=green>[Hit]</color> {other.name}에게 {_damage} 데미지!");
|
||||
|
||||
// 타격 시 효과 (카메라 쉐이크 등 호출)
|
||||
SendMessageUpwards("OnAttackShake", SendMessageOptions.DontRequireReceiver);
|
||||
Debug.Log($"<color=green>[Hit]</color> {other.name}의 몸통을 정확히 타격!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
using UnityEngine;
|
||||
using TMPro;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.UI; //
|
||||
|
||||
public class CardUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TextMeshProUGUI effectText;
|
||||
[SerializeField] private Image iconImage;
|
||||
[SerializeField] private Outline selectionOutline; // ⭐ 인스펙터에서 테두리 컴포넌트 연결
|
||||
|
||||
private CardData cardData;
|
||||
private LevelUpUIManager uiManager;
|
||||
|
|
@ -18,56 +19,41 @@ public class CardUI : MonoBehaviour
|
|||
{
|
||||
cardData = data;
|
||||
uiManager = manager;
|
||||
if (selectionOutline != null) selectionOutline.enabled = false; // 처음엔 테두리 끔
|
||||
|
||||
// (랜덤 스탯 카드 데이터 처리 로직 - 기존 코드 유지)
|
||||
RandomStatCardData randomData = cardData as RandomStatCardData;
|
||||
if (randomData != null)
|
||||
{
|
||||
isRandomCard = true;
|
||||
|
||||
// 스탯 2개 랜덤 선택 (중복 방지)
|
||||
s1 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)];
|
||||
do
|
||||
{
|
||||
s2 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)];
|
||||
}
|
||||
while (s1 == s2);
|
||||
|
||||
// ✅ v1 = 무조건 양수
|
||||
int positiveMin = Mathf.Max(1, randomData.minValue);
|
||||
int positiveMax = Mathf.Max(1, randomData.maxValue);
|
||||
v1 = Random.Range(positiveMin, positiveMax + 1);
|
||||
|
||||
// ✅ v2 = 무조건 음수
|
||||
int negativeMin = Mathf.Min(-1, randomData.minValue);
|
||||
int negativeMax = -1;
|
||||
v2 = Random.Range(negativeMin, negativeMax + 1);
|
||||
|
||||
do { s2 = randomData.possibleStats[Random.Range(0, randomData.possibleStats.Length)]; } while (s1 == s2);
|
||||
v1 = Random.Range(Mathf.Max(1, randomData.minValue), Mathf.Max(1, randomData.maxValue) + 1);
|
||||
v2 = Random.Range(Mathf.Min(-1, randomData.minValue), -1 + 1);
|
||||
effectText.text = $"{s1} +{v1}\n{s2} {v2}";
|
||||
}
|
||||
else
|
||||
{
|
||||
effectText.text = cardData.GetText();
|
||||
}
|
||||
else { effectText.text = cardData.GetText(); }
|
||||
}
|
||||
|
||||
public void OnClick()
|
||||
// 카드 클릭 시 매니저에게 알림
|
||||
public void OnClick() { uiManager.OnCardClick(this); }
|
||||
|
||||
// 선택 시 하이라이트 연출
|
||||
public void SetSelected(bool isSelected)
|
||||
{
|
||||
if (selectionOutline != null) selectionOutline.enabled = isSelected;
|
||||
transform.localScale = isSelected ? Vector3.one * 1.05f : Vector3.one;
|
||||
}
|
||||
|
||||
// APPLY 버튼 누를 때 최종 실행될 효과
|
||||
public void ApplyCurrentEffect()
|
||||
{
|
||||
if (isRandomCard)
|
||||
{
|
||||
Debug.Log($"[CardClick] clicked = {gameObject.name}");
|
||||
RandomStatCardData randomData = cardData as RandomStatCardData;
|
||||
randomData.ApplyToPlayer(s1, v1);
|
||||
randomData.ApplyToPlayer(s2, v2);
|
||||
RandomStatCardData rd = cardData as RandomStatCardData;
|
||||
rd.ApplyToPlayer(s1, v1); rd.ApplyToPlayer(s2, v2);
|
||||
}
|
||||
else
|
||||
{
|
||||
cardData.Execute();
|
||||
}
|
||||
|
||||
// ⭐ [추가] 카드 선택 직후 플레이어의 체력 UI를 강제로 동기화합니다.
|
||||
// 씬에서 PlayerHealth 스크립트를 찾아 갱신 함수를 실행합니다.
|
||||
else { cardData.Execute(); }
|
||||
FindObjectOfType<PlayerHealth>()?.RefreshHealthUI();
|
||||
|
||||
uiManager.Close();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI; // ⭐ 버튼 제어를 위해 필수!
|
||||
|
||||
public class LevelUpUIManager : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private GameObject panel;
|
||||
|
|
@ -8,14 +10,23 @@ public class LevelUpUIManager : MonoBehaviour
|
|||
[SerializeField] private Transform cardParent;
|
||||
[SerializeField] private List<CardData> cardPool;
|
||||
|
||||
[Header("--- 확정 버튼 설정 ---")]
|
||||
// ⭐ 이 변수가 있어야 인스펙터에 Apply Button 칸이 보입니다!
|
||||
[SerializeField] private Button applyButton;
|
||||
|
||||
private CardUI _selectedCardUI; // 현재 클릭된 카드 저장용
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
PlayerLevelSystem.OnLevelUp += Show;
|
||||
// ⭐ 버튼 클릭 시 OnApplyButtonClick 실행하도록 연결
|
||||
if (applyButton != null) applyButton.onClick.AddListener(OnApplyButtonClick);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
PlayerLevelSystem.OnLevelUp -= Show;
|
||||
if (applyButton != null) applyButton.onClick.RemoveListener(OnApplyButtonClick);
|
||||
}
|
||||
|
||||
void Show()
|
||||
|
|
@ -23,67 +34,60 @@ public class LevelUpUIManager : MonoBehaviour
|
|||
panel.SetActive(true);
|
||||
Time.timeScale = 0f;
|
||||
|
||||
_selectedCardUI = null;
|
||||
if (applyButton != null) applyButton.interactable = false; // 시작 땐 버튼 비활성
|
||||
|
||||
foreach (Transform child in cardParent)
|
||||
Destroy(child.gameObject);
|
||||
|
||||
int slotCount = 2;
|
||||
|
||||
// ===============================
|
||||
// 🔒 방어 코드
|
||||
// ===============================
|
||||
if (cardPool.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("cardPool이 비어 있습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cardPrefabs.Length < slotCount)
|
||||
{
|
||||
Debug.LogError("cardPrefabs 개수가 slotCount보다 적습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// 카드 데이터 선택
|
||||
// ===============================
|
||||
// ⭐ [에러 해결] selectedCards를 여기서 확실히 선언합니다!
|
||||
List<CardData> selectedCards = new List<CardData>();
|
||||
|
||||
if (cardPool.Count >= slotCount)
|
||||
{
|
||||
List<CardData> tempCardPool = new List<CardData>(cardPool);
|
||||
|
||||
List<CardData> tempPool = new List<CardData>(cardPool);
|
||||
for (int i = 0; i < slotCount; i++)
|
||||
{
|
||||
int index = Random.Range(0, tempCardPool.Count);
|
||||
selectedCards.Add(tempCardPool[index]);
|
||||
tempCardPool.RemoveAt(index);
|
||||
int idx = Random.Range(0, tempPool.Count);
|
||||
selectedCards.Add(tempPool[idx]);
|
||||
tempPool.RemoveAt(idx);
|
||||
}
|
||||
}
|
||||
else
|
||||
else // 카드 부족 시 순환
|
||||
{
|
||||
// ⭐ 카드 부족하면 순환
|
||||
for (int i = 0; i < slotCount; i++)
|
||||
{
|
||||
selectedCards.Add(cardPool[i % cardPool.Count]);
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// ⭐ 카드 프리팹 중복 제거
|
||||
// ===============================
|
||||
List<CardUI> tempPrefabs = new List<CardUI>(cardPrefabs);
|
||||
|
||||
// 카드 생성 및 셋업
|
||||
for (int i = 0; i < slotCount; i++)
|
||||
{
|
||||
int prefabIndex = Random.Range(0, tempPrefabs.Count);
|
||||
CardUI prefab = tempPrefabs[prefabIndex];
|
||||
tempPrefabs.RemoveAt(prefabIndex);
|
||||
|
||||
CardUI ui = Instantiate(prefab, cardParent);
|
||||
ui.Setup(selectedCards[i], this);
|
||||
// 프리팹 랜덤 선택 후 생성
|
||||
CardUI ui = Instantiate(cardPrefabs[Random.Range(0, cardPrefabs.Length)], cardParent);
|
||||
ui.Setup(selectedCards[i], this); // ⭐ 이제 selectedCards 에러가 뜨지 않습니다!
|
||||
}
|
||||
}
|
||||
|
||||
// 카드를 클릭했을 때 호출 (선택 상태 표시)
|
||||
public void OnCardClick(CardUI clickedUI)
|
||||
{
|
||||
if (_selectedCardUI != null) _selectedCardUI.SetSelected(false);
|
||||
_selectedCardUI = clickedUI;
|
||||
_selectedCardUI.SetSelected(true);
|
||||
|
||||
if (applyButton != null) applyButton.interactable = true; // ⭐ 드디어 버튼 활성화!
|
||||
}
|
||||
|
||||
// APPLY 버튼을 눌렀을 때만 최종 적용!
|
||||
private void OnApplyButtonClick()
|
||||
{
|
||||
if (_selectedCardUI == null) return;
|
||||
_selectedCardUI.ApplyCurrentEffect();
|
||||
Close();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Time.timeScale = 1f;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user