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); } if (candidates.Count == 0) return; // 2. 임의 1기 선택 (Primary target) var primary = candidates[Random.Range(0, candidates.Count)]; Vector2 strikePos = primary.transform.position; // 3. 이펙트 spawn (data.OnHitFxPrefab — FX_Thunder) + 총 lifetime 측정 float fxTotalLifetime = 1f; if (data.OnHitFxPrefab != null) { var fx = Object.Instantiate(data.OnHitFxPrefab, strikePos, Quaternion.identity); // PD 지시 2026-05-13 — 번개 이펙트 크기 20% 축소 fx.transform.localScale *= 0.2f; fxTotalLifetime = GetFxLifetime(fx); AutoDestroyFx(fx, fxTotalLifetime); } // 4. PD 지시 2026-05-13 — 이펙트 총 lifetime 의 4/5 시점에 데미지 판정 (이펙트 강타 정합) int damage = Mathf.Max(runtime.CalculateEffectiveDamage(), data.BaseDamage); float radius = Mathf.Max(data.HitboxSize.x, data.HitboxSize.y); float damageDelay = fxTotalLifetime * 0.8f; inventory.StartCoroutine(DelayedDamage(strikePos, radius, damage, primary, candidates, damageDelay)); } // 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 System.Collections.IEnumerator DelayedDamage(Vector2 strikePos, float radius, int damage, EnemyController primary, List candidates, float delay) { yield return new WaitForSeconds(delay); foreach (var e in candidates) { if (e == null) continue; if (Vector2.Distance(e.transform.position, strikePos) > radius && e != primary) continue; var h = e.GetComponent(); if (h == null || !h.IsAlive) continue; h.Decrement(damage); if (!h.IsAlive) { Schedule().enemy = e; } } } static void AutoDestroyFx(GameObject fxGo, float lifetime) { if (fxGo == null) return; Object.Destroy(fxGo, lifetime + 0.2f); } } }