2026-05-13 14:06:59 +00:00
|
|
|
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;
|
2026-05-13 14:10:32 +00:00
|
|
|
// PD 지시 2026-05-13 — ParticleSystem 명시 Play (playOnAwake 영역 정합·재발 안전망)
|
|
|
|
|
foreach (var ps in shieldGo.GetComponentsInChildren<ParticleSystem>(true))
|
|
|
|
|
{
|
2026-05-13 14:58:39 +00:00
|
|
|
var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy;
|
2026-05-13 14:10:32 +00:00
|
|
|
ps.Play(true);
|
|
|
|
|
}
|
2026-05-13 14:06:59 +00:00
|
|
|
}
|
|
|
|
|
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;
|
2026-05-14 14:14:05 +00:00
|
|
|
// PD 지시 2026-05-14 — 판정 박스 형태 일관 (HitboxSize 사용·OverlapBox)
|
|
|
|
|
Vector2 boxSize = data.HitboxSize.sqrMagnitude > 0.01f ? data.HitboxSize : new Vector2(2.5f, 2.5f);
|
2026-05-13 14:06:59 +00:00
|
|
|
int damage = data.BaseDamage > 0 ? data.BaseDamage : 5;
|
2026-05-14 14:14:05 +00:00
|
|
|
// PD 지시 2026-05-14 — 일정 피해 간격 (DotInterval 우선·기본 1초)
|
|
|
|
|
float interval = data.DotInterval > 0.01f ? data.DotInterval : 1f;
|
2026-05-14 14:28:13 +00:00
|
|
|
// PD 지시 2026-05-14 — OffsetDistance 적용 (facing sign · A05·Laser 동일 패턴)
|
|
|
|
|
Vector2 facing = Vector2.right;
|
|
|
|
|
var pc = inventory.GetComponent<PlayerController>();
|
|
|
|
|
if (pc != null) facing = pc.Facing;
|
|
|
|
|
float signX = facing.x < 0f ? -1f : 1f;
|
|
|
|
|
Vector2 offset = new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y);
|
2026-05-13 14:06:59 +00:00
|
|
|
|
|
|
|
|
var instance = shieldGo.AddComponent<SpiritFireInstance>();
|
2026-05-14 14:28:13 +00:00
|
|
|
instance.Init(inventory.transform, duration, boxSize, damage, interval, offset);
|
2026-05-13 14:06:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 정령불 인스턴스 — Player 자식 부착·duration 동안 OverlapCircle 영역 적 투사체 SelfDestruct·근접 적 매 초 5 피해.
|
2026-05-13 14:18:45 +00:00
|
|
|
/// PD 지시 2026-05-13 — FX_Rotating shield Animator frame 제어 (60fps·intro 1~88·loop 89~105·outro 남은 frame).
|
2026-05-13 14:06:59 +00:00
|
|
|
/// </summary>
|
|
|
|
|
public class SpiritFireInstance : MonoBehaviour
|
|
|
|
|
{
|
2026-05-13 14:18:45 +00:00
|
|
|
// 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");
|
|
|
|
|
|
2026-05-13 14:06:59 +00:00
|
|
|
Transform _player;
|
2026-05-13 14:18:45 +00:00
|
|
|
Animator _animator;
|
2026-05-13 14:06:59 +00:00
|
|
|
float _spawnTime;
|
|
|
|
|
float _duration;
|
2026-05-14 14:14:05 +00:00
|
|
|
Vector2 _boxSize;
|
2026-05-14 14:28:13 +00:00
|
|
|
Vector2 _offset; // PD 지시 2026-05-14 — OffsetDistance (facing sign 적용 후·Player 기준)
|
2026-05-13 14:06:59 +00:00
|
|
|
int _damage;
|
2026-05-14 14:14:05 +00:00
|
|
|
float _interval;
|
2026-05-13 14:06:59 +00:00
|
|
|
float _lastDamageTime;
|
2026-05-14 14:14:05 +00:00
|
|
|
Transform _debugBoxTransform;
|
2026-05-13 14:06:59 +00:00
|
|
|
|
2026-05-14 14:14:05 +00:00
|
|
|
// 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;
|
|
|
|
|
|
2026-05-14 14:28:13 +00:00
|
|
|
public void Init(Transform player, float duration, Vector2 boxSize, int damage, float interval, Vector2 offset)
|
2026-05-13 14:06:59 +00:00
|
|
|
{
|
|
|
|
|
_player = player;
|
|
|
|
|
_duration = duration;
|
2026-05-14 14:14:05 +00:00
|
|
|
_boxSize = boxSize;
|
2026-05-13 14:06:59 +00:00
|
|
|
_damage = damage;
|
2026-05-14 14:14:05 +00:00
|
|
|
_interval = interval;
|
2026-05-14 14:28:13 +00:00
|
|
|
_offset = offset;
|
2026-05-13 14:06:59 +00:00
|
|
|
_spawnTime = Time.unscaledTime;
|
2026-05-13 14:18:45 +00:00
|
|
|
|
|
|
|
|
_animator = GetComponent<Animator>();
|
|
|
|
|
if (_animator == null) _animator = GetComponentInChildren<Animator>();
|
|
|
|
|
if (_animator != null)
|
|
|
|
|
{
|
|
|
|
|
_animator.updateMode = AnimatorUpdateMode.UnscaledTime;
|
|
|
|
|
_animator.speed = 1f;
|
|
|
|
|
}
|
2026-05-14 14:14:05 +00:00
|
|
|
|
|
|
|
|
// PD 지시 2026-05-14 — 판정 박스 시각화 (붉은 반투명 박스·ShowDebugVisuals 토글)
|
2026-05-14 14:28:13 +00:00
|
|
|
// PD 지시 2026-05-14 — OffsetDistance 영역 박스 localPosition 적용 (facing sign 영역 Spawner 영역 사전 처리)
|
2026-05-14 14:14:05 +00:00
|
|
|
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;
|
2026-05-14 14:28:13 +00:00
|
|
|
dbg.transform.localPosition = new Vector3(_offset.x / lpx, _offset.y / lpy, 0f);
|
2026-05-14 14:14:05 +00:00
|
|
|
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);
|
|
|
|
|
}
|
2026-05-13 14:06:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void Update()
|
|
|
|
|
{
|
2026-05-13 14:18:45 +00:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 14:14:05 +00:00
|
|
|
float elapsedFromSpawn = Time.unscaledTime - _spawnTime;
|
|
|
|
|
if (elapsedFromSpawn >= _duration)
|
2026-05-13 14:06:59 +00:00
|
|
|
{
|
|
|
|
|
Destroy(gameObject);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 14:28:13 +00:00
|
|
|
// PD 지시 2026-05-14 — 판정 center = Player 위치 + OffsetDistance (박스 시각 ↔ 판정 일관)
|
|
|
|
|
Vector2 basePos = _player != null ? (Vector2)_player.position : (Vector2)transform.position;
|
|
|
|
|
Vector2 center = basePos + _offset;
|
2026-05-13 14:06:59 +00:00
|
|
|
|
2026-05-14 14:14:05 +00:00
|
|
|
// PD 지시 2026-05-14 — 정령불의 범위에 있는 적은 지속 시간 동안 일정한 피해 간격마다 피해
|
|
|
|
|
if (Time.unscaledTime - _lastDamageTime >= _interval)
|
2026-05-13 14:06:59 +00:00
|
|
|
{
|
2026-05-14 14:14:05 +00:00
|
|
|
_lastDamageTime = Time.unscaledTime;
|
|
|
|
|
ApplyDamageAround(center);
|
2026-05-13 14:06:59 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-14 14:14:05 +00:00
|
|
|
// PD 지시 2026-05-14 — 소멸 0.5초 전 페이드 (alpha 감소·scale 50% 축소)
|
|
|
|
|
float remaining = _duration - elapsedFromSpawn;
|
|
|
|
|
if (remaining <= FADE_DURATION && remaining > 0f)
|
2026-05-13 14:06:59 +00:00
|
|
|
{
|
2026-05-14 14:14:05 +00:00
|
|
|
float t = 1f - (remaining / FADE_DURATION); // 0→1 over 0.5s
|
|
|
|
|
ApplyFadeout(t);
|
2026-05-13 14:06:59 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ApplyDamageAround(Vector2 center)
|
|
|
|
|
{
|
2026-05-14 14:14:05 +00:00
|
|
|
// PD 지시 2026-05-14 — OverlapBox 영역 박스 형태 판정 (HitboxSize 사용·박스 시각 ↔ 판정 정합)
|
2026-05-13 14:06:59 +00:00
|
|
|
var cf = new ContactFilter2D { useTriggers = false };
|
|
|
|
|
var results = new Collider2D[32];
|
2026-05-14 14:14:05 +00:00
|
|
|
int n = Physics2D.OverlapBox(center, _boxSize, 0f, cf, results);
|
2026-05-13 14:06:59 +00:00
|
|
|
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;
|
2026-05-14 14:58:51 +00:00
|
|
|
// PD 지시 2026-05-14 — hit 모션 + flash 발화 (A05·Laser·Lightning 동등 패턴·시각 정합)
|
|
|
|
|
h.DecrementBypassInvulnWithHit(_damage);
|
2026-05-13 14:06:59 +00:00
|
|
|
if (!h.IsAlive) Schedule<EnemyDeath>().enemy = e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|