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

245 lines
11 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 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 = inventory.GetSpawnAnchor() + data.OffsetXY; // BT12-Dev-Clone (2026-05-15) γ
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;
// PD 지시 2026-05-14 — OffsetDistance 적용 (facing sign · A05·Laser 동일 패턴)
Vector2 facing = Vector2.right;
var pc = inventory.GetComponent<PlayerController>();
facing = inventory.GetSpawnFacing(pc); // BT12-Dev-Clone (2026-05-15) γ
float signX = facing.x < 0f ? -1f : 1f;
Vector2 offset = new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y);
var instance = shieldGo.AddComponent<SpiritFireInstance>();
instance.Init(inventory.transform, duration, boxSize, damage, interval, offset);
}
}
/// <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;
Vector2 _offset; // PD 지시 2026-05-14 — OffsetDistance (facing sign 적용 후·Player 기준)
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, Vector2 offset)
{
_player = player;
_duration = duration;
_boxSize = boxSize;
_damage = damage;
_interval = interval;
_offset = offset;
_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 토글)
// PD 지시 2026-05-14 — OffsetDistance 영역 박스 localPosition 적용 (facing sign 영역 Spawner 영역 사전 처리)
var dbg = new GameObject("SpiritFireHitbox_Debug");
dbg.hideFlags = HideFlags.DontSave;
dbg.transform.SetParent(_player, false); // Player 자식·매 frame 동조
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.localPosition = new Vector3(_offset.x / lpx, _offset.y / lpy, 0f);
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;
}
// PD 지시 2026-05-14 — 판정 center = Player 위치 + OffsetDistance (박스 시각 ↔ 판정 일관)
Vector2 basePos = _player != null ? (Vector2)_player.position : (Vector2)transform.position;
Vector2 center = basePos + _offset;
// 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;
// PD 지시 2026-05-14 — hit 모션 + flash 발화 (A05·Laser·Lightning 동등 패턴·시각 정합)
h.DecrementBypassInvulnWithHit(_damage);
if (!h.IsAlive) Schedule<EnemyDeath>().enemy = e;
}
}
}
}