116 lines
4.8 KiB
C#
116 lines
4.8 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);
|
||
|
|
}
|
||
|
|
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<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 System.Collections.IEnumerator DelayedDamage(Vector2 strikePos, float radius, int damage, EnemyController primary, List<EnemyController> 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<Health>();
|
||
|
|
if (h == null || !h.IsAlive) continue;
|
||
|
|
h.Decrement(damage);
|
||
|
|
if (!h.IsAlive)
|
||
|
|
{
|
||
|
|
Schedule<EnemyDeath>().enemy = e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static void AutoDestroyFx(GameObject fxGo, float lifetime)
|
||
|
|
{
|
||
|
|
if (fxGo == null) return;
|
||
|
|
Object.Destroy(fxGo, lifetime + 0.2f);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|