feat(BT12-Dev): SkillInventoryHUD 시각화 + 사망 원인 디버그 로그 (PD 후속 지시 2건)

PD 직접 발화 2건:
1. PlayerSkillInventory 등록 시각화 (유니티 기본 자원 활용)
2. 스킬 습득 후 사망 버그 수정

작업 내용:
- SkillInventoryHUD.cs 신규 (OnGUI 좌상단·장착 액티브 DisplayName/Lv/CD·패시브 카운트)
- PlayerController.Awake에 HUD 자동 부착
- ProjectileSpawner fallback prefab 시각화 (SpriteRenderer + 16x16 동적 흰색 원 + 속성별 색상)
- Projectile.OnTriggerEnter2D Player 명시 차단 (defensive proxy)
- Health.Decrement·DecrementSilent·Die에 Debug.Log + StackTrace (사망 호출자 추적)

가설 (미검증): BT5-Dev EnemyController patrol → PlayerEnemyCollision Event → player.health.Decrement().
검증 절차: PD Play 테스트 → Console log StackTrace 분석 → 호출자 확정.

pm-auditor Major 1 정정 완료 (PD 지시 로그 2행 등재)·Minor 2 정정 완료.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-05-09 21:23:25 +09:00
parent d53150b5ed
commit e31c34cf24
6 changed files with 161 additions and 3 deletions

View File

@ -126,7 +126,10 @@ namespace Platformer.Mechanics
return;
}
int beforeHP = currentHP;
currentHP = Mathf.Clamp(currentHP - damage, 0, maxHP);
// PD 지시 2026-05-09 — 사망 원인 추적 (스킬 습득 후 갑자기 사망 버그)
Debug.Log($"[Health@{name}] Decrement(damage={damage}) hp {beforeHP}→{currentHP} t={Time.time:F2}\n{System.Environment.StackTrace}");
// 피격 성공 시 무적 시간 활성화 (다음 피격 대비)
if (invulnerableDuration > 0f)
@ -197,7 +200,9 @@ namespace Platformer.Mechanics
if (damage <= 0) return;
if (Time.time < invulnerableUntil) return;
int beforeHP = currentHP;
currentHP = Mathf.Clamp(currentHP - damage, 0, maxHP);
Debug.Log($"[Health@{name}] DecrementSilent(damage={damage}) hp {beforeHP}→{currentHP} t={Time.time:F2}");
if (invulnerableDuration > 0f) invulnerableUntil = Time.time + invulnerableDuration;
if (currentHP > 0)
@ -249,6 +254,7 @@ namespace Platformer.Mechanics
/// </summary>
public void Die()
{
Debug.Log($"[Health@{name}] Die() called t={Time.time:F2}\n{System.Environment.StackTrace}");
invulnerableUntil = -1f; // i-frame 무효화
if (currentHP > 0)
{

View File

@ -96,10 +96,14 @@ namespace Platformer.Mechanics
if (GetComponent<EerieVillage.Progression.PlayerProgression>() == null)
gameObject.AddComponent<EerieVillage.Progression.PlayerProgression>();
// Phase 2-D 신규 (2026-05-09) — PlayerSkillInventory 자동 부착 (스킬 인벤토리 영역)
// Phase 2-D 신규 (2026-05-09) — PlayerSkillInventory 자동 부착 (스킬 인벤토리)
if (GetComponent<EerieVillage.Skills.PlayerSkillInventory>() == null)
gameObject.AddComponent<EerieVillage.Skills.PlayerSkillInventory>();
// BT12-Dev 후속 (2026-05-09) — SkillInventoryHUD 자동 부착 (PD 시각화 지시)
if (GetComponent<EerieVillage.MyUI.SkillInventoryHUD>() == null)
gameObject.AddComponent<EerieVillage.MyUI.SkillInventoryHUD>();
// 사망 시 입력 차단 / 부활 시 입력 복원
if (health != null)
{

View File

@ -0,0 +1,84 @@
using UnityEngine;
using EerieVillage.Skills;
namespace EerieVillage.MyUI
{
/// <summary>
/// BT12-Dev 시각화 HUD — PlayerSkillInventory 등록 상태를 좌상단에 표시.
/// PD 지시 2026-05-09 — "PlayerSkillInventory 등록이 되었는지 어떻게 판단해야하지?
/// 시각적인 변화가 없으니 확인이 불가능해. 유니티 기본 제공 리소스를 활용해도 좋으니 보이게 해줘."
///
/// 표시 내용:
/// - 장착 액티브 스킬 목록 (DisplayName · Lv · CooldownRemaining/EffectiveCooldown)
/// - 패시브 슬롯 카운트
///
/// PlayerController.Awake에서 자동 부착되는 PlayerSkillInventory와 함께 부착.
/// </summary>
[RequireComponent(typeof(PlayerSkillInventory))]
public class SkillInventoryHUD : MonoBehaviour
{
PlayerSkillInventory _inventory;
GUIStyle _boxStyle;
void Awake()
{
_inventory = GetComponent<PlayerSkillInventory>();
}
void OnGUI()
{
if (_inventory == null) return;
if (_boxStyle == null)
{
_boxStyle = new GUIStyle(GUI.skin.box);
_boxStyle.alignment = TextAnchor.UpperLeft;
_boxStyle.fontSize = 14;
_boxStyle.normal.textColor = Color.white;
_boxStyle.padding = new RectOffset(10, 10, 6, 6);
_boxStyle.richText = true;
}
var sb = new System.Text.StringBuilder();
sb.AppendLine("<b><color=#ffd86b>장착 스킬</color></b>");
int activeCount = 0;
foreach (var active in _inventory.ActiveSkills)
{
if (active == null) continue;
var data = active.Data;
if (data == null) continue;
activeCount++;
float remaining = 0f;
float total = 0f;
if (active is ActiveSkillRuntime rt)
{
remaining = Mathf.Max(0f, rt.CooldownRemaining);
total = rt.EffectiveCooldown;
}
sb.Append($"{activeCount}. {data.DisplayName} <color=#88c8ff>Lv.{active.StackLevel}</color>");
if (total > 0f)
{
sb.Append($" <color=#bbbbbb>CD {remaining:F1}s/{total:F1}s</color>");
}
sb.AppendLine();
}
if (activeCount == 0)
{
sb.AppendLine("<color=#888888>(없음)</color>");
}
int passiveCount = _inventory.PassiveSkills.Count;
sb.AppendLine();
sb.AppendLine($"<b><color=#a0d8a0>패시브: {passiveCount}장</color></b>");
// 가변 높이 (라인 수 기반)
int lines = activeCount + 4 + (activeCount == 0 ? 1 : 0);
float height = lines * 20f + 16f;
GUI.Box(new Rect(10f, 10f, 320f, height), sb.ToString(), _boxStyle);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bdee77dfcafe4087a759feafb02f6b3a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -45,6 +45,9 @@ namespace EerieVillage.Skills.Effectors
{
if (_hitTargets.Contains(other)) return;
// PD 지시 2026-05-09 후속 방어 — 자기(Player) hit·자기 자신·hit 방어.
if (other.GetComponent<PlayerController>() != null) return;
// Enemy 레이어 한정.
// Phase 2-D fallback (2026-05-09): TagManager에 "Enemy" 레이어 미등재 시 LayerMask.NameToLayer 반환값 = -1.
// 레이어 매칭 실패 시 EnemyController 컴포넌트 존재 여부로 대체 판정.

View File

@ -1,5 +1,6 @@
using UnityEngine;
using Platformer.Mechanics;
using EerieVillage.Skills;
namespace EerieVillage.Skills.Effectors
{
@ -60,21 +61,70 @@ namespace EerieVillage.Skills.Effectors
/// 투사체 프리팹 로드.
/// Phase 2-C 에서 data.projectilePrefab 필드를 추가하면 해당 경로 우선 사용.
/// 현재는 Resources/Skills/Projectiles/Default 폴백.
/// 폴백 실패 시 Collider2D 부착 빈 오브젝트를 반환한다.
/// 폴백 실패 시 Collider2D + SpriteRenderer 부착 빈 오브젝트를 반환한다.
/// PD 지시 2026-05-09 — 시각화: 흰색 원 sprite (Unity 기본 whiteTexture 사용).
/// </summary>
private static GameObject LoadProjectilePrefab(ActiveSkillData data)
{
var prefab = Resources.Load<GameObject>("Skills/Projectiles/Default");
if (prefab != null) return prefab;
// 폴백 — 빈 오브젝트 + CircleCollider2D (시각 없음, 판정만)
// 폴백 — Collider + SpriteRenderer (시각 표시·판정 동시)
var go = new GameObject($"Projectile_{data.CardId}");
var col = go.AddComponent<CircleCollider2D>();
col.isTrigger = true;
col.radius = 0.2f;
var sr = go.AddComponent<SpriteRenderer>();
sr.sprite = GetOrCreateFallbackSprite();
sr.color = GetColorByAttribute(data.AttributeTags);
sr.sortingOrder = 50; // Player·Enemy 위
// 0.4 unit 직경 정도의 작은 원
go.transform.localScale = new Vector3(0.4f, 0.4f, 1f);
return go;
}
// 캐시: Sprite 매번 생성하지 않도록 정적 보존
static Sprite _fallbackSprite;
static Sprite GetOrCreateFallbackSprite()
{
if (_fallbackSprite != null) return _fallbackSprite;
// 16×16 원형 알파 텍스처 (둥근 형태)
const int size = 16;
var tex = new Texture2D(size, size, TextureFormat.RGBA32, false);
tex.wrapMode = TextureWrapMode.Clamp;
tex.filterMode = FilterMode.Bilinear;
float r = size * 0.5f;
for (int y = 0; y < size; y++)
{
for (int x = 0; x < size; x++)
{
float dx = x - r + 0.5f;
float dy = y - r + 0.5f;
float dist = Mathf.Sqrt(dx * dx + dy * dy);
float alpha = Mathf.Clamp01(1f - (dist - (r - 1.5f)));
tex.SetPixel(x, y, new Color(1f, 1f, 1f, alpha));
}
}
tex.Apply();
_fallbackSprite = Sprite.Create(tex, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), size);
return _fallbackSprite;
}
static Color GetColorByAttribute(AttributeTag attr)
{
// 속성별 색상 (시각 구분 영역)
if ((attr & AttributeTag.Fire) != 0) return new Color(1f, 0.5f, 0.2f);
if ((attr & AttributeTag.Frost) != 0) return new Color(0.5f, 0.85f, 1f);
if ((attr & AttributeTag.Dark) != 0) return new Color(0.6f, 0.3f, 0.85f);
if ((attr & AttributeTag.Lightning) != 0) return new Color(1f, 1f, 0.4f);
if ((attr & AttributeTag.Physical) != 0) return new Color(0.95f, 0.95f, 0.95f);
return Color.white;
}
private static Vector2 RotateVector(Vector2 v, float degrees)
{
float rad = degrees * Mathf.Deg2Rad;