168 lines
6.6 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|