diff --git a/Assets/Scripts/Mechanics/Health.cs b/Assets/Scripts/Mechanics/Health.cs index f33c43f..b554eb0 100644 --- a/Assets/Scripts/Mechanics/Health.cs +++ b/Assets/Scripts/Mechanics/Health.cs @@ -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 /// public void Die() { + Debug.Log($"[Health@{name}] Die() called t={Time.time:F2}\n{System.Environment.StackTrace}"); invulnerableUntil = -1f; // i-frame 무효화 if (currentHP > 0) { diff --git a/Assets/Scripts/Mechanics/PlayerController.cs b/Assets/Scripts/Mechanics/PlayerController.cs index 216805c..f6f351f 100644 --- a/Assets/Scripts/Mechanics/PlayerController.cs +++ b/Assets/Scripts/Mechanics/PlayerController.cs @@ -96,10 +96,14 @@ namespace Platformer.Mechanics if (GetComponent() == null) gameObject.AddComponent(); - // Phase 2-D 신규 (2026-05-09) — PlayerSkillInventory 자동 부착 (스킬 인벤토리 영역) + // Phase 2-D 신규 (2026-05-09) — PlayerSkillInventory 자동 부착 (스킬 인벤토리) if (GetComponent() == null) gameObject.AddComponent(); + // BT12-Dev 후속 (2026-05-09) — SkillInventoryHUD 자동 부착 (PD 시각화 지시) + if (GetComponent() == null) + gameObject.AddComponent(); + // 사망 시 입력 차단 / 부활 시 입력 복원 if (health != null) { diff --git a/Assets/Scripts/MyUI/SkillInventoryHUD.cs b/Assets/Scripts/MyUI/SkillInventoryHUD.cs new file mode 100644 index 0000000..3d90bc8 --- /dev/null +++ b/Assets/Scripts/MyUI/SkillInventoryHUD.cs @@ -0,0 +1,84 @@ +using UnityEngine; +using EerieVillage.Skills; + +namespace EerieVillage.MyUI +{ + /// + /// BT12-Dev 시각화 HUD — PlayerSkillInventory 등록 상태를 좌상단에 표시. + /// PD 지시 2026-05-09 — "PlayerSkillInventory 등록이 되었는지 어떻게 판단해야하지? + /// 시각적인 변화가 없으니 확인이 불가능해. 유니티 기본 제공 리소스를 활용해도 좋으니 보이게 해줘." + /// + /// 표시 내용: + /// - 장착 액티브 스킬 목록 (DisplayName · Lv · CooldownRemaining/EffectiveCooldown) + /// - 패시브 슬롯 카운트 + /// + /// PlayerController.Awake에서 자동 부착되는 PlayerSkillInventory와 함께 부착. + /// + [RequireComponent(typeof(PlayerSkillInventory))] + public class SkillInventoryHUD : MonoBehaviour + { + PlayerSkillInventory _inventory; + GUIStyle _boxStyle; + + void Awake() + { + _inventory = GetComponent(); + } + + 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("장착 스킬"); + + 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} Lv.{active.StackLevel}"); + if (total > 0f) + { + sb.Append($" CD {remaining:F1}s/{total:F1}s"); + } + sb.AppendLine(); + } + + if (activeCount == 0) + { + sb.AppendLine("(없음)"); + } + + int passiveCount = _inventory.PassiveSkills.Count; + sb.AppendLine(); + sb.AppendLine($"패시브: {passiveCount}장"); + + // 가변 높이 (라인 수 기반) + int lines = activeCount + 4 + (activeCount == 0 ? 1 : 0); + float height = lines * 20f + 16f; + GUI.Box(new Rect(10f, 10f, 320f, height), sb.ToString(), _boxStyle); + } + } +} diff --git a/Assets/Scripts/MyUI/SkillInventoryHUD.cs.meta b/Assets/Scripts/MyUI/SkillInventoryHUD.cs.meta new file mode 100644 index 0000000..51abc7d --- /dev/null +++ b/Assets/Scripts/MyUI/SkillInventoryHUD.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bdee77dfcafe4087a759feafb02f6b3a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Skills/Effectors/Projectile.cs b/Assets/Scripts/Skills/Effectors/Projectile.cs index d42ac76..bc0ef54 100644 --- a/Assets/Scripts/Skills/Effectors/Projectile.cs +++ b/Assets/Scripts/Skills/Effectors/Projectile.cs @@ -45,6 +45,9 @@ namespace EerieVillage.Skills.Effectors { if (_hitTargets.Contains(other)) return; + // PD 지시 2026-05-09 후속 방어 — 자기(Player) hit·자기 자신·hit 방어. + if (other.GetComponent() != null) return; + // Enemy 레이어 한정. // Phase 2-D fallback (2026-05-09): TagManager에 "Enemy" 레이어 미등재 시 LayerMask.NameToLayer 반환값 = -1. // 레이어 매칭 실패 시 EnemyController 컴포넌트 존재 여부로 대체 판정. diff --git a/Assets/Scripts/Skills/Effectors/ProjectileSpawner.cs b/Assets/Scripts/Skills/Effectors/ProjectileSpawner.cs index c5523e1..88ac6d0 100644 --- a/Assets/Scripts/Skills/Effectors/ProjectileSpawner.cs +++ b/Assets/Scripts/Skills/Effectors/ProjectileSpawner.cs @@ -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 사용). /// private static GameObject LoadProjectilePrefab(ActiveSkillData data) { var prefab = Resources.Load("Skills/Projectiles/Default"); if (prefab != null) return prefab; - // 폴백 — 빈 오브젝트 + CircleCollider2D (시각 없음, 판정만) + // 폴백 — Collider + SpriteRenderer (시각 표시·판정 동시) var go = new GameObject($"Projectile_{data.CardId}"); var col = go.AddComponent(); col.isTrigger = true; col.radius = 0.2f; + + var sr = go.AddComponent(); + 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;