EerieVillage/Assets/Scripts/Skills/Effectors/LightningStrikeSpawner.cs

172 lines
8.1 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using Platformer.Mechanics;
using Platformer.Gameplay;
using static Platformer.Core.Simulation;
namespace EerieVillage.Skills.Effectors
{
/// <summary>
/// A04 번개 충격 Effector — IEffector 구현.
/// PD 지시 (2026-05-13):
/// - 2.5초 주기 (BaseCooldown 2.5)
/// - 화면 내 보이는 임의의 몬스터 1기 타격
/// - BaseDamage 15
/// - 타격 지점 영역 좁은 범위 (HitboxSize) 내 다른 적도 함께 피해
/// - 이펙트: data.OnHitFxPrefab (FX_Thunder)
///
/// 화면 내 검출 — Camera.main 영역 OrthographicSize·aspect 기반 좌·우·상·하 영역 측정.
/// </summary>
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<EnemyController>(FindObjectsSortMode.None);
var candidates = new List<EnemyController>();
foreach (var e in enemies)
{
if (e == null) continue;
var h = e.GetComponent<Health>();
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;
// PD 지시 2026-05-13 — ParticleSystem 명시 Play
foreach (var ps in fx.GetComponentsInChildren<ParticleSystem>(true)) ps.Play(true);
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;
// PD 지시 2026-05-13 — ParticleSystem 명시 Play
foreach (var ps in extraFx.GetComponentsInChildren<ParticleSystem>(true)) ps.Play(true);
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<EnemyController>();
if (e == null) continue;
var h = c.GetComponent<Health>();
if (h == null || !h.IsAlive) continue;
h.DecrementBypassInvulnWithHit(damage);
if (!h.IsAlive) Schedule<EnemyDeath>().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<ParticleSystem>(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));
}
}
}