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

233 lines
10 KiB
C#

using UnityEngine;
using Platformer.Mechanics;
using Platformer.Gameplay;
using static Platformer.Core.Simulation;
namespace EerieVillage.Skills.Effectors
{
/// <summary>
/// A11 정령불 Effector — Category D (Minion).
/// PD 지시 2026-05-13:
/// - 15초마다 (BaseCooldown 15) Player 자식 spawn
/// - 8초 유지 (FX_Rotating shield)
/// - 지속 시간 동안 적 투사체 SelfDestruct·근접 적 매 초 5 피해
/// </summary>
public class SpiritFireSpawner : IEffector
{
public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory)
{
var data = runtime.ActiveData;
Vector2 spawnPos = (Vector2)inventory.transform.position + data.OffsetXY;
GameObject shieldGo;
if (data.OnHitFxPrefab != null)
{
shieldGo = Object.Instantiate(data.OnHitFxPrefab, spawnPos, Quaternion.identity, inventory.transform);
shieldGo.transform.localScale *= data.HitFxScale;
// PD 지시 2026-05-13 — ParticleSystem 명시 Play (playOnAwake 영역 정합·재발 안전망)
foreach (var ps in shieldGo.GetComponentsInChildren<ParticleSystem>(true))
{
var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy;
ps.Play(true);
}
}
else
{
shieldGo = new GameObject("SpiritFire_Fallback");
shieldGo.transform.SetParent(inventory.transform, false);
}
shieldGo.hideFlags = HideFlags.DontSave;
float duration = data.MinionLifetime > 0.1f ? data.MinionLifetime : 8f;
// PD 지시 2026-05-14 — 판정 박스 형태 일관 (HitboxSize 사용·OverlapBox)
Vector2 boxSize = data.HitboxSize.sqrMagnitude > 0.01f ? data.HitboxSize : new Vector2(2.5f, 2.5f);
int damage = data.BaseDamage > 0 ? data.BaseDamage : 5;
// PD 지시 2026-05-14 — 일정 피해 간격 (DotInterval 우선·기본 1초)
float interval = data.DotInterval > 0.01f ? data.DotInterval : 1f;
var instance = shieldGo.AddComponent<SpiritFireInstance>();
instance.Init(inventory.transform, duration, boxSize, damage, interval);
}
}
/// <summary>
/// 정령불 인스턴스 — Player 자식 부착·duration 동안 OverlapCircle 영역 적 투사체 SelfDestruct·근접 적 매 초 5 피해.
/// PD 지시 2026-05-13 — FX_Rotating shield Animator frame 제어 (60fps·intro 1~88·loop 89~105·outro 남은 frame).
/// </summary>
public class SpiritFireInstance : MonoBehaviour
{
// PD 지시 2026-05-13 — FX_Rotating shield.anim 정합 (m_SampleRate 60·m_StopTime 2.8166666 → 169 frame·2.8167s)
const float FPS = 60f;
const int INTRO_END_FRAME = 88;
const int LOOP_END_FRAME = 105;
const float CLIP_LENGTH = 2.8166666f;
static readonly int STATE_HASH = Animator.StringToHash("Base Layer.FX_Rotating shield");
Transform _player;
Animator _animator;
float _spawnTime;
float _duration;
Vector2 _boxSize;
int _damage;
float _interval;
float _lastDamageTime;
Transform _debugBoxTransform;
// PD 지시 2026-05-14 — 소멸 0.5초 전 페이드 (alpha 1→0·scale 1→0.5)
const float FADE_DURATION = 0.5f;
Vector3 _baseScale;
Renderer[] _renderers;
MaterialPropertyBlock _mpb;
float[] _baseAlphas;
public void Init(Transform player, float duration, Vector2 boxSize, int damage, float interval)
{
_player = player;
_duration = duration;
_boxSize = boxSize;
_damage = damage;
_interval = interval;
_spawnTime = Time.unscaledTime;
_animator = GetComponent<Animator>();
if (_animator == null) _animator = GetComponentInChildren<Animator>();
if (_animator != null)
{
_animator.updateMode = AnimatorUpdateMode.UnscaledTime;
_animator.speed = 1f;
}
// PD 지시 2026-05-14 — 판정 박스 시각화 (붉은 반투명 박스·ShowDebugVisuals 토글)
var dbg = new GameObject("SpiritFireHitbox_Debug");
dbg.hideFlags = HideFlags.DontSave;
dbg.transform.SetParent(_player, false); // Player 자식·매 frame 동조
dbg.transform.localPosition = Vector3.zero;
float lpx = _player.lossyScale.x != 0f ? Mathf.Abs(_player.lossyScale.x) : 1f;
float lpy = _player.lossyScale.y != 0f ? Mathf.Abs(_player.lossyScale.y) : 1f;
dbg.transform.localScale = new Vector3(_boxSize.x / lpx, _boxSize.y / lpy, 1f);
var sr = dbg.AddComponent<SpriteRenderer>();
sr.sprite = HitboxDebug.GetWhiteSprite();
sr.color = new Color(1f, 0f, 0f, 0.35f);
sr.sortingOrder = 100;
sr.enabled = HitboxDebug.ShowDebugVisuals;
_debugBoxTransform = dbg.transform;
Destroy(dbg, _duration);
// PD 지시 2026-05-14 — fade 캐싱 (Renderer·MaterialPropertyBlock·기본 alpha)
_baseScale = transform.localScale;
_renderers = GetComponentsInChildren<Renderer>();
_mpb = new MaterialPropertyBlock();
_baseAlphas = new float[_renderers.Length];
for (int i = 0; i < _renderers.Length; i++)
{
var r = _renderers[i];
if (r == null) { _baseAlphas[i] = 1f; continue; }
if (r.sharedMaterial != null && r.sharedMaterial.HasProperty("_TintColor"))
_baseAlphas[i] = r.sharedMaterial.GetColor("_TintColor").a;
else if (r.sharedMaterial != null && r.sharedMaterial.HasProperty("_Color"))
_baseAlphas[i] = r.sharedMaterial.GetColor("_Color").a;
else _baseAlphas[i] = 1f;
}
}
// PD 지시 2026-05-14 — 페이드 보간 적용
void ApplyFadeout(float t)
{
t = Mathf.Clamp01(t);
float alphaMul = 1f - t;
float scaleMul = 1f - 0.5f * t;
transform.localScale = _baseScale * scaleMul;
if (_renderers == null) return;
for (int i = 0; i < _renderers.Length; i++)
{
var r = _renderers[i];
if (r == null) continue;
r.GetPropertyBlock(_mpb);
string propName = (r.sharedMaterial != null && r.sharedMaterial.HasProperty("_TintColor"))
? "_TintColor" : "_Color";
Color baseCol = (r.sharedMaterial != null && r.sharedMaterial.HasProperty(propName))
? r.sharedMaterial.GetColor(propName) : Color.white;
baseCol.a = _baseAlphas[i] * alphaMul;
_mpb.SetColor(propName, baseCol);
r.SetPropertyBlock(_mpb);
}
}
void Update()
{
// PD 지시 2026-05-13 — Animator frame 제어 (intro 0~88f·loop 88~105f·outro 105~clip_end)
if (_animator != null)
{
float introEnd = INTRO_END_FRAME / FPS; // 1.4667s
float loopEnd = LOOP_END_FRAME / FPS; // 1.75s
float outroLength = Mathf.Max(0f, CLIP_LENGTH - loopEnd); // 1.0666s
float outroStart = Mathf.Max(introEnd, _duration - outroLength);
float elapsed = Time.unscaledTime - _spawnTime;
float targetTime;
if (elapsed < introEnd)
{
targetTime = elapsed;
}
else if (elapsed < outroStart)
{
float loopRange = Mathf.Max(0.01f, loopEnd - introEnd);
float loopT = (elapsed - introEnd) % loopRange;
targetTime = introEnd + loopT;
}
else
{
targetTime = loopEnd + (elapsed - outroStart);
}
float normalized = Mathf.Clamp01(targetTime / CLIP_LENGTH);
_animator.Play(STATE_HASH, 0, normalized);
}
float elapsedFromSpawn = Time.unscaledTime - _spawnTime;
if (elapsedFromSpawn >= _duration)
{
Destroy(gameObject);
return;
}
Vector2 center = _player != null ? (Vector2)_player.position : (Vector2)transform.position;
// PD 지시 2026-05-14 — 정령불의 범위에 있는 적은 지속 시간 동안 일정한 피해 간격마다 피해
if (Time.unscaledTime - _lastDamageTime >= _interval)
{
_lastDamageTime = Time.unscaledTime;
ApplyDamageAround(center);
}
// PD 지시 2026-05-14 — 소멸 0.5초 전 페이드 (alpha 감소·scale 50% 축소)
float remaining = _duration - elapsedFromSpawn;
if (remaining <= FADE_DURATION && remaining > 0f)
{
float t = 1f - (remaining / FADE_DURATION); // 0→1 over 0.5s
ApplyFadeout(t);
}
}
void ApplyDamageAround(Vector2 center)
{
// PD 지시 2026-05-14 — OverlapBox 영역 박스 형태 판정 (HitboxSize 사용·박스 시각 ↔ 판정 정합)
var cf = new ContactFilter2D { useTriggers = false };
var results = new Collider2D[32];
int n = Physics2D.OverlapBox(center, _boxSize, 0f, 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.DecrementBypassInvuln(_damage);
if (!h.IsAlive) Schedule<EnemyDeath>().enemy = e;
}
}
}
}