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

176 lines
8.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = inventory.GetSpawnAnchor(); // BT12-Dev-Clone (2026-05-15) γ
}
// 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)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; ps.Play(true); }
// PD 지시 2026-05-14 — 피격 이펙트 상위 sortingOrder
foreach (var r in fx.GetComponentsInChildren<Renderer>(true)) r.sortingOrder += 100;
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)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; ps.Play(true); }
// PD 지시 2026-05-14 — 피격 이펙트 상위 sortingOrder
foreach (var r in extraFx.GetComponentsInChildren<Renderer>(true)) r.sortingOrder += 100;
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));
}
}
}