using System.Collections.Generic; using UnityEngine; using Platformer.Mechanics; using Platformer.Gameplay; using static Platformer.Core.Simulation; namespace EerieVillage.Skills.Effectors { /// /// A04 번개 충격 Effector — IEffector 구현. /// PD 지시 (2026-05-13): /// - 2.5초 주기 (BaseCooldown 2.5) /// - 화면 내 보이는 임의의 몬스터 1기 타격 /// - BaseDamage 15 /// - 타격 지점 영역 좁은 범위 (HitboxSize) 내 다른 적도 함께 피해 /// - 이펙트: data.OnHitFxPrefab (FX_Thunder) /// /// 화면 내 검출 — Camera.main 영역 OrthographicSize·aspect 기반 좌·우·상·하 영역 측정. /// public class LightningStrikeSpawner : IEffector { public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory) { var data = runtime.ActiveData; // 1. 화면 내 Enemy 후보 수집 var cam = Camera.main; if (cam == null) return; float halfH = cam.orthographicSize; float halfW = halfH * cam.aspect; Vector2 camPos = cam.transform.position; float xMin = camPos.x - halfW, xMax = camPos.x + halfW; float yMin = camPos.y - halfH, yMax = camPos.y + halfH; var enemies = Object.FindObjectsByType(FindObjectsSortMode.None); var candidates = new List(); foreach (var e in enemies) { if (e == null) continue; var h = e.GetComponent(); if (h == null || !h.IsAlive) continue; var p = e.transform.position; if (p.x < xMin || p.x > xMax || p.y < yMin || p.y > yMax) continue; candidates.Add(e); } // PD 지시 2026-05-13 — 적 유무 관계 없이 일정 쿨타임 자동 발동. candidates 영역 0 시 Player 위치 영역 fallback. Vector2 primaryPos; if (candidates.Count > 0) { primaryPos = candidates[Random.Range(0, candidates.Count)].transform.position; } else { primaryPos = (Vector2)inventory.transform.position; } // PD 정합 2026-05-13 — OffsetDistance = (X, Y) 절대 오프셋·OffsetXY = 이펙트만 Vector2 hitboxPos = primaryPos + data.OffsetDistance; Vector2 fxPos = primaryPos + data.OffsetXY; // 3. 이펙트 spawn (data.OnHitFxPrefab — FX_Thunder) + 총 lifetime 측정 float fxTotalLifetime = 1f; if (data.OnHitFxPrefab != null) { // PD 정합 — 이펙트는 fxPos·박스는 hitboxPos 분리 var fx = Object.Instantiate(data.OnHitFxPrefab, fxPos, Quaternion.Euler(0f, 0f, data.FxRotation)); fx.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피 fx.transform.localScale *= data.HitFxScale; fxTotalLifetime = GetFxLifetime(fx); AutoDestroyFx(fx, fxTotalLifetime); } // PD 지시 2026-05-13 — 이펙트 생성 시점 판정 영역 정적 캡처 (적 이동 무관) // FxRotation 은 시각(이펙트) 전용 · 박스(판정) 은 rotation=0 · facing 도 적 위치 기준이라 미적용. Vector2 capturedSize = data.HitboxSize; int capturedDamage = Mathf.Max(data.BaseDamage, 1); // 박스 즉시 spawn (rotation=0 — FxRotation 미적용) HitboxDebug.Spawn(hitboxPos, capturedSize, Mathf.Max(data.BaseCooldown, 1f)); // PD 지시 2026-05-13 — ExtraHitFxPrefab 0.6초 후 spawn·y -0.5 오프셋 (판정 시점 무관·비주얼 전용) if (data.ExtraHitFxPrefab != null) { Vector2 extraFxPos = fxPos + new Vector2(0f, -0.5f); inventory.StartCoroutine(DelayedExtraHitFx(data, extraFxPos, 0.6f)); } // PD 지시 2026-05-13 — ScriptableObject DamageFrameDelay·반복 피해 정합 inventory.StartCoroutine(FixedHitDamageCoroutine(hitboxPos, capturedSize, 0f, capturedDamage, data)); } // PD 지시 2026-05-13 — 고정 발동형 영역 영역 판정 영역 (DamageFrameDelay·EnableRepeatDamage·MaxHitCount·RepeatFrameInterval) static System.Collections.IEnumerator FixedHitDamageCoroutine(Vector2 pos, Vector2 size, float rotZ, int damage, ActiveSkillData data) { for (int i = 0; i < data.DamageFrameDelay; i++) yield return null; DoOverlapBoxDamage(pos, size, rotZ, damage); if (data.EnableRepeatDamage) { int remaining = Mathf.Max(0, data.MaxHitCount - 1); int interval = Mathf.Max(1, data.RepeatFrameInterval); for (int hit = 0; hit < remaining; hit++) { for (int i = 0; i < interval; i++) yield return null; DoOverlapBoxDamage(pos, size, rotZ, damage); } } } // PD 지시 2026-05-13 — ExtraHitFxPrefab 지연 spawn (비주얼 전용·판정 무관·WaitForSecondsRealtime 영역 Time.timeScale=0 영역 정합) static System.Collections.IEnumerator DelayedExtraHitFx(ActiveSkillData data, Vector2 fxPos, float delaySeconds) { yield return new WaitForSecondsRealtime(delaySeconds); if (data == null || data.ExtraHitFxPrefab == null) yield break; var extraFx = Object.Instantiate(data.ExtraHitFxPrefab, fxPos, Quaternion.Euler(0f, 0f, data.FxRotation)); extraFx.hideFlags = HideFlags.DontSave; extraFx.transform.localScale *= data.HitFxScale; AutoDestroyFx(extraFx, GetFxLifetime(extraFx)); } static void DoOverlapBoxDamage(Vector2 pos, Vector2 size, float rotZ, int damage) { var cf = new ContactFilter2D(); cf.useTriggers = false; var results = new Collider2D[32]; int n = Physics2D.OverlapBox(pos, size, rotZ, cf, results); for (int i = 0; i < n; i++) { var c = results[i]; if (c == null) continue; var e = c.GetComponent(); if (e == null) continue; var h = c.GetComponent(); if (h == null || !h.IsAlive) continue; h.DecrementBypassInvulnWithHit(damage); if (!h.IsAlive) Schedule().enemy = e; } } // PD 지시 2026-05-13 — FX_Thunder 영역 root PS 영역 wrapper (10s) 이므로 자식 PS 영역 영역 영역 영역 // 자식 PS 평균 lifetime 영역 시각 정합 (강타 자식 영역 약 1초 내외). static float GetFxLifetime(GameObject fxGo) { if (fxGo == null) return 1f; var psList = fxGo.GetComponentsInChildren(true); float sum = 0f; int n = 0; foreach (var ps in psList) { // root PS 제외 (wrapper) if (ps.transform == fxGo.transform) continue; var main = ps.main; float t = main.duration + main.startLifetime.constantMax; sum += t; n++; } if (n == 0) return 1f; return sum / n; } static void AutoDestroyFx(GameObject fxGo, float lifetime) { if (fxGo == null) return; // PD 지시 2026-05-13 — unscaledTime cap (Time.timeScale=0 영역 잔존 차단) FxAutoDestroyUnscaled.Attach(fxGo, Mathf.Min(lifetime + 0.2f, 5f)); } } }