using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Platformer.Mechanics;
namespace EerieVillage.Skills
{
///
/// Player GameObject에 부착. 장착 스킬 슬롯·Lv·각성 상태 관리 중앙 컴포넌트.
/// BT12-Dev v1 §2-3·§4-3·§4-4 정합.
/// Phase 2-D에서 SkillCardPlaceholder·LevelUpManager와 정식 통합 예정.
///
[RequireComponent(typeof(Health))]
public class PlayerSkillInventory : MonoBehaviour
{
[Header("슬롯 상한 (balance-designer 재확인 — VS 원작 6/6 참고)")]
public int ActiveSlotMax = 6;
public int PassiveSlotMax = 6;
// PD 지시 2026-05-13 — 게임 시작 시 기본 습득 스킬 CardId 배열 (Inspector 조절·다중 지원)
[Header("기본 습득 스킬 (게임 시작 시 자동 장착)")]
[Tooltip("게임 시작 시 자동 습득할 스킬 CardId 배열 (예: A02 파이어볼·중복·잘못된 ID 자동 skip)")]
// PD 지시 2026-05-14 — 기본 공격 파이어볼 발사 X (테스트 영역)
public string[] StartingCardIds = new string[] { };
// 장착 슬롯 (인덱스 = 슬롯 번호)
private readonly List _activeSkills = new List();
private readonly List _passiveSkills = new List();
private readonly List _awakenedSkills = new List();
// 각 카드 ID → 런타임 인스턴스 매핑 (재픽 시 Lv 업용)
private readonly Dictionary _cardIdToRuntime = new Dictionary();
// 적 처치 누적 (OnKill 트리거용)
private int _totalKillCount = 0;
// Health 참조 — OnEnable에서 이벤트 구독
private Health _health;
/// 통합 PlayerStats — 패시브 보정이 여기에 누적 적용
public PlayerStats Stats { get; private set; }
/// 적 처치·피격 이벤트 구독자 (OnKill·OnHit 트리거용)
public event System.Action OnEnemyKilled;
public event System.Action OnPlayerDamaged;
// ─────────────────────────────────────────────────────────────
// BT12-Dev-Clone (2026-05-15) — A10 분신 영역 (CloneInstance·CloneEffector hook)
// ─────────────────────────────────────────────────────────────
/// Player 스킬 발동 시 호출 (CloneInstance.EnqueuePlayerFire hook).
public event System.Action OnPlayerSkillFired;
/// 분신 발동 컨텍스트 분기 플래그. Effector 영역 anchor·facing·damage 분기 적용. (BT12-Dev-Clone 2026-05-18 fix: internal → public · Test assembly 접근 영역)
public bool IsCloneFireActive = false;
/// 분신 발동 시 anchor 위치 (분신 GameObject 위치).
public Vector2 CloneFireOrigin = Vector2.zero;
/// 분신 발동 시 facing sign (-1 = 왼쪽·+1 = 오른쪽). spawn 시점 고정 (PD 결정 2026-05-15).
public float CloneFireFacingX = 1f;
/// PD 결정 (2026-05-15) — 분신 damage multiplier. 50% 반감.
public const float CLONE_DAMAGE_MULTIPLIER = 0.5f;
///
/// OnPlayerSkillFired 외부 invoke 영역 (BT12-Dev-Clone 2026-05-18 fix).
/// C# event 영역 외부 클래스 영역 .Invoke() 직접 호출 불가 (CS0070) 영역 fix.
/// ActiveSkillRuntime.Fire 영역 본 메서드 호출 → 분신 hook 발화.
///
public void RaisePlayerSkillFired(ActiveSkillRuntime runtime)
{
OnPlayerSkillFired?.Invoke(runtime);
}
///
/// 스킬 발동 anchor 위치 반환 (BT12-Dev-Clone 2026-05-15 γ 단계).
/// IsCloneFireActive=true → 분신 위치 (CloneFireOrigin) · false → Player 위치.
/// 6 Effector 영역 일관 사용 — Player·분신 발동 위치 분기 단일 진입점.
///
public Vector2 GetSpawnAnchor()
{
return IsCloneFireActive ? CloneFireOrigin : (Vector2)transform.position;
}
///
/// 스킬 발동 facing 반환 (BT12-Dev-Clone 2026-05-15 γ 단계).
/// IsCloneFireActive=true → 분신 facing 고정 (CloneFireFacingX) · false → Player facing.
/// 6 Effector 영역 일관 사용.
///
public Vector2 GetSpawnFacing(PlayerController pc)
{
if (IsCloneFireActive) return new Vector2(CloneFireFacingX, 0f);
return pc != null ? pc.Facing : Vector2.right;
}
// PD 지시 2026-05-14 — Scene load 직후 정적 cleanup (가장 이른 진입점·Awake 보다 선행)
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.AfterSceneLoad)]
static void OnSceneLoadedStaticCleanup()
{
int removed = DoCleanupStalePooledSpawns(true); // useDestroyImmediate=true (정적·Awake 이전)
Debug.Log("[PlayerSkillInventory] AfterSceneLoad static cleanup removed=" + removed);
}
void Awake()
{
// PD 지시 2026-05-14 — 게임 재실행 시 이전 Play 잔존 spawn (투사체·박스·Range) 강제 cleanup.
// 이중 방어: AfterSceneLoad 정적 cleanup 외 Awake·1 frame 후 (Coroutine) 추가 cleanup.
int removed = DoCleanupStalePooledSpawns(false);
Debug.Log("[PlayerSkillInventory] Awake cleanup removed=" + removed);
StartCoroutine(DelayedCleanupCoroutine());
Stats = new PlayerStats();
PlayerStats.Current = Stats;
_health = GetComponent();
}
// PD 지시 2026-05-14 — 1·3·10 frame 후 추가 cleanup. Awake 시점 미발화 spawn·다른 컴포넌트 Awake/Start spawn 모두 catch.
System.Collections.IEnumerator DelayedCleanupCoroutine()
{
int[] delays = { 1, 3, 10 };
foreach (int d in delays)
{
for (int i = 0; i < d; i++) yield return null;
int removed = DoCleanupStalePooledSpawns(false);
if (removed > 0)
Debug.Log("[PlayerSkillInventory] Delayed cleanup frame=" + d + " removed=" + removed);
}
}
// PD 지시 2026-05-14 — 잔존 풀링 GameObject 강제 cleanup
// 대상: Projectile 및 파생 (HomingProjectile·PiercingProjectile) + 박스·Range·FX (Clone) 시각화 GameObject
static readonly System.Collections.Generic.HashSet StaleSpawnNames =
new System.Collections.Generic.HashSet
{
"Hitbox_Debug",
"ProjectileHitbox_Debug",
"LaserHitbox_Debug",
"MeleeHitbox_Debug",
"MeleeHitbox_Debug2",
"Range_Debug",
// PD 지시 2026-05-14 — A06·A11 판정 박스 시각화 신규
"PoisonSwampHitbox_Debug",
"SpiritFireHitbox_Debug",
};
// PD 지시 2026-05-14 — FX clone 카탈로그 (Projectile component 미부착 — Cast FX·HitFx 등)
static readonly string[] StaleClonePrefixes =
{
"FX_Fireball_Bullet",
"FX_Lightningball",
"FX_Dragonfire",
"FX_Thunder",
"FX_SLASH",
"FX_PinkMagicArrow",
"Projectile_", // CreateFallbackProjectile name 패턴
};
static int DoCleanupStalePooledSpawns(bool useImmediate)
{
int removed = 0;
// PD 지시 2026-05-14 — Scene root 재귀 탐색 (Resources.FindObjectsOfTypeAll 영역 hideFlags hidden GO 누락 우려·정확도 ↑)
var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
if (!scene.IsValid()) return 0;
var roots = scene.GetRootGameObjects();
// 재귀 cleanup 대상 수집 (직접 destroy 시 enumeration 중 collection 변경 회피)
var targets = new System.Collections.Generic.List();
foreach (var root in roots)
{
CollectStaleRecursive(root, targets);
}
foreach (var go in targets)
{
if (go == null) continue;
if (useImmediate) DestroyImmediate(go); else Destroy(go);
removed++;
}
return removed;
}
static void CollectStaleRecursive(GameObject root, System.Collections.Generic.List targets)
{
if (root == null) return;
// Projectile 및 파생 component 부착 → 이 root 단위 destroy
if (root.GetComponent() != null)
{
targets.Add(root);
return; // 자식 재귀 불필요 (root destroy 시 자식 자동 destroy)
}
// Name 매칭 (박스·Range·FX clone)
if (IsStaleByName(root.name))
{
targets.Add(root);
return;
}
// 자식 재귀
foreach (Transform child in root.transform)
{
CollectStaleRecursive(child.gameObject, targets);
}
}
static bool IsStaleByName(string name)
{
if (StaleSpawnNames.Contains(name)) return true;
if (!name.EndsWith("(Clone)")) return name.StartsWith("Projectile_");
foreach (var pre in StaleClonePrefixes)
{
if (name.StartsWith(pre)) return true;
}
return false;
}
// PD 지시 2026-05-13 — Start 시점에 기본 습득 스킬 자동 장착 (Resources 로드 완료 후·Awake 영역 아님)
void Start()
{
if (StartingCardIds == null) return;
foreach (var cardId in StartingCardIds)
{
if (string.IsNullOrEmpty(cardId)) continue;
AddSkillByCardId(cardId);
}
}
void OnEnable()
{
if (_health != null)
{
_health.OnDamagedEvent += OnPlayerDamagedHandler;
}
}
void OnDisable()
{
if (_health != null)
{
_health.OnDamagedEvent -= OnPlayerDamagedHandler;
}
}
void Update()
{
// 액티브 Cooldown 감산·OnTime 발동
foreach (var active in _activeSkills)
{
active.Tick(Time.deltaTime);
}
// 조건부 패시브 타이머 감산 (P17 감로수 등 OnTimer 계열)
foreach (var passive in _passiveSkills)
{
if (!passive.IsAlwaysOn)
{
passive.OnTrigger(new PassiveTriggerContext
{
Kind = PassiveTriggerKind.OnTimer,
TimeElapsed = Time.deltaTime
});
}
}
}
///
/// 픽 UI 선택 결과 반영 — cardId로 SkillDataAsset 조회 후 런타임 생성·장착.
/// Phase 2-D에서 SkillCardPlaceholder·LevelUpManager 통합 hook 연결 예정.
///
public bool AddSkillByCardId(string cardId)
{
if (string.IsNullOrEmpty(cardId)) return false;
// 재픽 = Lv 업
if (_cardIdToRuntime.TryGetValue(cardId, out var existing))
{
existing.Upgrade();
return true;
}
var data = SkillRuntimeFactory.Resolve(cardId);
if (data == null)
{
Debug.LogWarning($"[PlayerSkillInventory] CardId '{cardId}' 에 해당하는 SkillDataAsset을 찾을 수 없습니다.");
return false;
}
var runtime = SkillRuntimeFactory.Create(data);
if (runtime == null) return false;
if (runtime is IActiveSkill active)
{
if (_activeSkills.Count >= ActiveSlotMax)
{
Debug.LogWarning($"[PlayerSkillInventory] 액티브 슬롯 초과 (상한 {ActiveSlotMax}).");
return false;
}
runtime.OnEquip(this);
_activeSkills.Add(active);
_cardIdToRuntime[cardId] = runtime;
return true;
}
if (runtime is IPassiveSkill passive)
{
if (_passiveSkills.Count >= PassiveSlotMax)
{
Debug.LogWarning($"[PlayerSkillInventory] 패시브 슬롯 초과 (상한 {PassiveSlotMax}).");
return false;
}
runtime.OnEquip(this);
passive.ApplyModifier(Stats);
_passiveSkills.Add(passive);
_cardIdToRuntime[cardId] = runtime;
return true;
}
return false;
}
///
/// 적 처치 이벤트 외부 호출 (EnemyController 사망 시 연결 예정 — Phase 2-D).
/// OnKill 트리거 액티브·패시브 발동.
///
public void NotifyEnemyKilled(EnemyKillContext ctx)
{
_totalKillCount++;
OnEnemyKilled?.Invoke(ctx);
foreach (var active in _activeSkills.Where(a => a.Trigger == ActiveTrigger.OnKill))
active.Fire();
foreach (var passive in _passiveSkills.Where(p => !p.IsAlwaysOn))
passive.OnTrigger(new PassiveTriggerContext
{
Kind = PassiveTriggerKind.OnEnemyKilled,
KillCount = _totalKillCount
});
}
// Health.OnDamagedEvent 구독 핸들러
private void OnPlayerDamagedHandler(int damage)
{
OnPlayerDamaged?.Invoke(damage);
foreach (var active in _activeSkills.Where(a => a.Trigger == ActiveTrigger.OnHit))
active.Fire();
foreach (var passive in _passiveSkills.Where(p => !p.IsAlwaysOn))
passive.OnTrigger(new PassiveTriggerContext
{
Kind = PassiveTriggerKind.OnPlayerDamaged,
DamageTaken = damage
});
}
// --- 읽기 전용 접근자 (디버그·UI 용) ---
public IReadOnlyList ActiveSkills => _activeSkills;
public IReadOnlyList PassiveSkills => _passiveSkills;
}
/// 적 처치 컨텍스트 (Phase 2-D에서 EnemyController 참조 추가 예정)
public struct EnemyKillContext
{
public string EnemyId;
public int XPReward;
}
}