using System.Collections.Generic; using UnityEngine; using Platformer.Mechanics; using Platformer.Gameplay; using static Platformer.Core.Simulation; namespace EerieVillage.Skills.Effectors { /// /// A05 학익진 — 플레이어 주변 범위 즉시 피해 + FX_SLASH 이펙트. /// PD 지시 (2026-05-13): /// - 1.5초 간격 (BaseCooldown 1.5) /// - 플레이어 주변 영역 (HitboxSize 반경) 내 적에게 피해 /// - 공격력 10 (BaseDamage) /// - FX_SLASH 이펙트 재생 (data.OnHitFxPrefab) /// public class MeleeAreaSpawner : IEffector { public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory) { var data = runtime.ActiveData; // PD 지시 2026-05-13 — OffsetDistanceX = X 절대·OffsetDistance = Y 절대·OffsetXY = 이펙트만 Vector2 playerPos = inventory.transform.position; Vector2 fxPos = playerPos + data.OffsetXY; Vector2 facing = Vector2.right; var pc = inventory.GetComponent(); if (pc != null) facing = pc.Facing; // 이펙트 spawn — fxPos·HitFxScale·FxRotation·facing flip GameObject fxGo = null; float fxLifetime = 1f; if (data.OnHitFxPrefab != null) { fxGo = Object.Instantiate(data.OnHitFxPrefab, fxPos, Quaternion.Euler(0f, 0f, data.FxRotation)); fxGo.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피 Vector3 s = fxGo.transform.localScale * data.HitFxScale; if (facing.x < 0f) s.x = -Mathf.Abs(s.x); fxGo.transform.localScale = s; // PD 지시 2026-05-13 — 좌우 베기 이펙트가 Player 이동 시 뒤로 밀리는 현상 정정. // Player 자식 부착 (worldPositionStays=true) 으로 spawn 후에도 Player 이동에 동조. fxGo.transform.SetParent(inventory.transform, true); fxLifetime = GetFxLifetime(fxGo); // PD 지시 2026-05-13 — FX 잔상 safety cap 5초 Object.Destroy(fxGo, Mathf.Min(fxLifetime + 0.2f, 5f)); } // PD 지시 2026-05-13 — 박스 영역 Player 자식 영역 부착·매 frame Player 따라감 Vector2 hitboxSize = data.HitboxSize; int damage = Mathf.Max(runtime.CalculateEffectiveDamage(), data.BaseDamage); float duration = Mathf.Max(data.BaseCooldown, 1f); var boxGo = new GameObject("MeleeHitbox_Debug"); boxGo.hideFlags = HideFlags.DontSave; boxGo.transform.SetParent(inventory.transform, false); float lpx = inventory.transform.lossyScale.x != 0f ? Mathf.Abs(inventory.transform.lossyScale.x) : 1f; float lpy = inventory.transform.lossyScale.y != 0f ? Mathf.Abs(inventory.transform.lossyScale.y) : 1f; // PD 지시 2026-05-13 — 박스(판정) = facing 좌/우 sign 만 반영 · FxRotation 미적용 (시각 전용) float signX = facing.x < 0f ? -1f : 1f; boxGo.transform.localPosition = new Vector3(signX * data.OffsetDistance.x / lpx, data.OffsetDistance.y / lpy, 0f); boxGo.transform.localRotation = Quaternion.identity; boxGo.transform.localScale = new Vector3(hitboxSize.x / lpx, hitboxSize.y / lpy, 1f); var sr = boxGo.AddComponent(); sr.sprite = HitboxDebug.GetWhiteSprite(); sr.color = new Color(1f, 0f, 0f, 0.35f); sr.sortingOrder = 100; sr.enabled = HitboxDebug.ShowDebugVisuals; Object.Destroy(boxGo, duration); // PD 지시 2026-05-13 — DamageFrameDelay·반복 피해 영역 정합 (Player 영역 매 hit 시 영역 영역 영역) inventory.StartCoroutine(MeleeFixedHitDamageCoroutine(inventory, data, damage)); } static System.Collections.IEnumerator MeleeFixedHitDamageCoroutine(PlayerSkillInventory inventory, ActiveSkillData data, int damage) { for (int i = 0; i < data.DamageFrameDelay; i++) yield return null; DoOverlapBoxFromPlayer(inventory, data, damage); if (data.EnableRepeatDamage) { int remaining = Mathf.Max(0, data.MaxHitCount - 1); int interval = Mathf.Max(1, data.RepeatFrameInterval); for (int hit = 0; hit < remaining; hit++) { for (int i = 0; i < interval; i++) yield return null; if (inventory == null) yield break; DoOverlapBoxFromPlayer(inventory, data, damage); } } } static void DoOverlapBoxFromPlayer(PlayerSkillInventory inventory, ActiveSkillData data, int damage) { if (inventory == null) return; // PD 지시 2026-05-13 — 박스(판정) = facing 좌/우 sign 만 · FxRotation 미적용 Vector2 facing = Vector2.right; var pc = inventory.GetComponent(); if (pc != null) facing = pc.Facing; float signX = facing.x < 0f ? -1f : 1f; Vector2 hitboxPos = (Vector2)inventory.transform.position + new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y); var cf = new ContactFilter2D(); cf.useTriggers = false; var results = new Collider2D[32]; int n = Physics2D.OverlapBox(hitboxPos, data.HitboxSize, 0f, cf, results); for (int i = 0; i < n; i++) { var c = results[i]; if (c == null) continue; var e = c.GetComponent(); if (e == null) continue; var h = c.GetComponent(); if (h == null || !h.IsAlive) continue; h.DecrementBypassInvulnWithHit(damage); if (!h.IsAlive) Schedule().enemy = e; } } static void AutoDestroyFx(GameObject fxGo) { if (fxGo == null) return; Object.Destroy(fxGo, GetFxLifetime(fxGo) + 0.2f); } static float GetFxLifetime(GameObject fxGo) { if (fxGo == null) return 1f; var psList = fxGo.GetComponentsInChildren(true); float max = 0f; foreach (var ps in psList) { var main = ps.main; float t = main.duration + main.startLifetime.constantMax; if (t > max) max = t; } return Mathf.Max(max, 1f); } } }