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

168 lines
6.6 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;
float radius = data.AuraRadius > 0.1f ? data.AuraRadius : 2.5f;
int damage = data.BaseDamage > 0 ? data.BaseDamage : 5;
var instance = shieldGo.AddComponent<SpiritFireInstance>();
instance.Init(inventory.transform, duration, radius, damage);
}
}
/// <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;
float _radius;
int _damage;
float _lastDamageTime;
public void Init(Transform player, float duration, float radius, int damage)
{
_player = player;
_duration = duration;
_radius = radius;
_damage = damage;
_spawnTime = Time.unscaledTime;
_animator = GetComponent<Animator>();
if (_animator == null) _animator = GetComponentInChildren<Animator>();
if (_animator != null)
{
_animator.updateMode = AnimatorUpdateMode.UnscaledTime;
_animator.speed = 1f;
}
}
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);
}
if (Time.unscaledTime - _spawnTime >= _duration)
{
Destroy(gameObject);
return;
}
Vector2 center = _player != null ? (Vector2)_player.position : (Vector2)transform.position;
// 적 투사체 SelfDestruct (Projectile 컴포넌트 영역 적 발사·향후 Enemy 측 투사체 구현 시 정합)
var allProjectiles = Object.FindObjectsByType<Projectile>(FindObjectsSortMode.None);
foreach (var p in allProjectiles)
{
if (p == null) continue;
float d = Vector2.Distance(center, p.transform.position);
if (d <= _radius)
{
// PD 명시 — 적 투사체만 SelfDestruct. 현 Projectile = Player 발사 only → 영향 X 영역 (방어 코드).
// 향후 Enemy 측 Projectile 분리 시 friendly check 추가 필요.
}
}
// 매 1초 근접 적 피해
if (Time.unscaledTime - _lastDamageTime >= 1f)
{
_lastDamageTime = Time.unscaledTime;
ApplyDamageAround(center);
}
}
void ApplyDamageAround(Vector2 center)
{
var cf = new ContactFilter2D { useTriggers = false };
var results = new Collider2D[32];
int n = Physics2D.OverlapCircle(center, _radius, 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;
}
}
}
}