using System.Collections.Generic; using UnityEngine; using Platformer.Mechanics; using static Platformer.Core.Simulation; namespace EerieVillage.Skills.Effectors { /// /// A10 분신 인스턴스 (BT12-Dev-Clone · 2026-05-15). /// /// PD 직접 명세 (2026-05-15): /// 1. 플레이어 x좌표 1 뒤쪽 spawn (facing 반대 방향 1유닛) /// 2. 반투명 sprite (alpha 0.5) /// 3. 플레이어와 동일한 스킬 사용 (장착 액티브 미러링) /// 4. 분신 공격력 = 플레이어 공격력의 50% 반감 /// 5. 플레이어보다 0.5초 뒤 사용 (공격 시작 딜레이) /// /// PD 추가 결정 (2026-05-15): /// - BaseCooldown 25초 (PM 1차 30 → PD 조정 25) /// - MinionLifetime 12초 자동 소멸 + Singleton 1기 유지 /// - facing 고정 (spawn 시점 facing 고정·이동 시 분신 위치·방향 불변) /// - 무적 = Collider 미부착 (적 투사체·벽·player·enemy 모두 통과) /// - Lv 업 시 분신 수 증가 X · 추후 지속시간↑+플레이어 참조 데미지 비율(%)↑ /// (balance-designer 후속 수치 확정) /// /// β 단계 의존성 (PlayerSkillInventory 신규 필드 추가 후 컴파일 통과): /// - PlayerSkillInventory.OnPlayerSkillFired (event System.Action<ActiveSkillRuntime>) /// - PlayerSkillInventory.IsCloneFireActive (bool · default false) /// - PlayerSkillInventory.CloneFireOrigin (Vector2) /// - PlayerSkillInventory.CloneFireFacingX (float) /// - PlayerSkillInventory.CLONE_DAMAGE_MULTIPLIER (const 0.5f) /// public class CloneInstance : MonoBehaviour { // ───────────────────────────────────────────────────────────── // PD 결정 상수 (2026-05-15) // ───────────────────────────────────────────────────────────── /// PD 결정 — 분신 자동 소멸 시간 (초). Lv 업 시 추후 증가 (balance-designer). public const float LIFETIME_SECONDS = 12f; /// PD 명세 — 공격 시작 딜레이 (초). public const float FIRE_DELAY_SECONDS = 0.5f; /// PD 명세 — 분신 spawn 위치 (facing 반대 방향 1유닛). public const float SPAWN_OFFSET_X = 1f; /// PD 명세 — 분신 sprite alpha (반투명). public const float SPRITE_ALPHA = 0.5f; // ───────────────────────────────────────────────────────────── // Singleton (PD 결정 — 분신 1기 유지·재발동 시 기존 destroy + 새 spawn) // ───────────────────────────────────────────────────────────── private static CloneInstance _current; /// 현재 활성 분신 인스턴스 (테스트·진단용 · null = 분신 없음). public static CloneInstance Current => _current; // ───────────────────────────────────────────────────────────── // 상태 필드 // ───────────────────────────────────────────────────────────── private PlayerSkillInventory _playerInventory; // spawn 시점 Player facing.x sign (PD 결정 — 분신 facing 고정) private float _spawnFacingX; // spawn 시각 (lifetime 자동 소멸용 · timeScale 무관 unscaledTime) private float _spawnTime; // 지연 큐 — Player Fire 시 0.5초 후 분신 발동 enqueue private readonly Queue _pendingQueue = new Queue(); private struct PendingFire { public float TriggerTime; // Time.unscaledTime + FIRE_DELAY_SECONDS public ActiveSkillRuntime Runtime; } // ───────────────────────────────────────────────────────────── // Spawn / Replace (Singleton 1기 유지) // ───────────────────────────────────────────────────────────── /// /// 분신 spawn 또는 기존 destroy + 새 spawn. /// CloneEffector.Trigger 영역에서 호출 (A10 발동 시점). /// public static void SpawnOrReplace(PlayerSkillInventory playerInventory, ActiveSkillData cloneData) { if (playerInventory == null) return; // 1. 기존 분신 destroy (Singleton 1기 유지) if (_current != null) { Destroy(_current.gameObject); _current = null; } // 2. Player 위치·facing 취득 var pc = playerInventory.GetComponent(); Vector2 facing = pc != null ? pc.Facing : Vector2.right; float signX = facing.x < 0f ? -1f : 1f; Vector2 playerPos = playerInventory.transform.position; // 3. PD 명세 — facing 반대 1유닛 (facing=오른쪽 → 분신 x = player.x - 1) Vector2 spawnPos = playerPos + new Vector2(-signX * SPAWN_OFFSET_X, 0f); // 4. 신규 GameObject — sprite 복제 + alpha 0.5 + collider 미부착 (무적) // PD 지시 2026-05-18: Player 자식 부착 (Player 이동 시 분신 자동 동조) + scale 영역 Player 동일 (크기 정합) var go = new GameObject("Clone_A10"); go.hideFlags = HideFlags.DontSave; // Scene 오염 방지 (BT12-Dev SOT 정합) var playerSr = playerInventory.GetComponentInChildren(); // Player 자식 부착 (worldPositionStays=false) + localPosition 영역 facing 반대 1유닛 go.transform.SetParent(playerInventory.transform, false); go.transform.localPosition = new Vector3(-signX * SPAWN_OFFSET_X, 0f, 0f); // PD 지시 2026-05-18 — 분신 크기 = Player 동일. PlayerSr 자식 영역 localScale 영역 정합 (자식 lossyScale 영역 영역 영역). go.transform.localScale = playerSr != null ? playerSr.transform.localScale : Vector3.one; go.transform.localRotation = Quaternion.identity; if (playerSr != null) { var sr = go.AddComponent(); sr.sprite = playerSr.sprite; sr.flipX = playerSr.flipX; sr.sortingOrder = playerSr.sortingOrder - 1; // Player 뒤쪽 sorting Color c = playerSr.color; c.a = SPRITE_ALPHA; // PD 명세 — 반투명 sr.color = c; } // PD 결정 — Collider 미부착 (무적·무충돌). Rigidbody2D도 미부착 (gravity·물리 차단). // 5. CloneInstance 컴포넌트 부착 + 상태 초기화 var instance = go.AddComponent(); instance._playerInventory = playerInventory; instance._spawnFacingX = signX; instance._spawnTime = Time.unscaledTime; _current = instance; // 6. Player Fire 이벤트 구독 // (β 단계 PlayerSkillInventory.OnPlayerSkillFired 신설 후 컴파일 통과) playerInventory.OnPlayerSkillFired += instance.EnqueuePlayerFire; } // ───────────────────────────────────────────────────────────── // Player Fire hook (0.5초 지연 큐 enqueue) // ───────────────────────────────────────────────────────────── /// /// Player Fire 시점 hook (PlayerSkillInventory.OnPlayerSkillFired 이벤트 핸들러). /// 0.5초 지연 후 분신 위치 anchor로 동일 Effector 재호출. /// public void EnqueuePlayerFire(ActiveSkillRuntime runtime) { if (runtime == null || runtime.ActiveData == null) return; // 무한 재귀 방지 — 분신은 A10 분신을 발동하지 않음 if (runtime.ActiveData.CardId == "A10") return; _pendingQueue.Enqueue(new PendingFire { TriggerTime = Time.unscaledTime + FIRE_DELAY_SECONDS, Runtime = runtime }); } // ───────────────────────────────────────────────────────────── // Update — lifetime 자동 소멸 + 지연 큐 dequeue // ───────────────────────────────────────────────────────────── void Update() { // PD 결정 (2026-05-15) — 12초 자동 소멸 if (Time.unscaledTime - _spawnTime >= LIFETIME_SECONDS) { Destroy(gameObject); return; } // 지연 큐 dequeue — TriggerTime 도달 항목 발동 while (_pendingQueue.Count > 0 && _pendingQueue.Peek().TriggerTime <= Time.unscaledTime) { var pending = _pendingQueue.Dequeue(); FireAtClonePosition(pending.Runtime); } } // ───────────────────────────────────────────────────────────── // 분신 위치 anchor Effector 재호출 (50% damage 분기는 PlayerSkillInventory.IsCloneFireActive 영역) // ───────────────────────────────────────────────────────────── private void FireAtClonePosition(ActiveSkillRuntime runtime) { if (runtime == null || runtime.ActiveData == null || _playerInventory == null) return; // β 단계 — PlayerSkillInventory 신규 필드 set // IsCloneFireActive = true // → γ 단계 6 Effector 영역 anchor·facing·damage 분기 // → ActiveSkillRuntime.CalculateEffectiveDamage 영역 50% multiplier // CloneFireOrigin = transform.position (분신 위치 anchor) // CloneFireFacingX = _spawnFacingX (분신 facing 고정) _playerInventory.IsCloneFireActive = true; _playerInventory.CloneFireOrigin = transform.position; _playerInventory.CloneFireFacingX = _spawnFacingX; // SkillFireEvent.Execute 영역 그대로 재호출 (Effector 분기 영역 IsCloneFireActive 처리) // reset은 γ 단계 SkillFireEvent.Cleanup 영역 IsCloneFireActive=false 일관 reset // (설계 §A10-7 채택안 (가)) var ev = Schedule(); // parent namespace EerieVillage.Skills 자동 인식 ev.Runtime = runtime; ev.Inventory = _playerInventory; } // ───────────────────────────────────────────────────────────── // OnDestroy — Player Inventory hook unsubscribe + Singleton 정리 // ───────────────────────────────────────────────────────────── void OnDestroy() { // Player Inventory hook unsubscribe (β 단계 OnPlayerSkillFired 신설 후 컴파일 통과) if (_playerInventory != null) { _playerInventory.OnPlayerSkillFired -= EnqueuePlayerFire; } if (_current == this) _current = null; } } }