using System.Collections; using System.Collections.Generic; using UnityEngine; using Platformer.Mechanics; using Platformer.Gameplay; using static Platformer.Core.Simulation; namespace EerieVillage.Skills.Effectors { /// /// 레이저 Effector — FX_Dragonfire 레이저. /// PD 지시 (2026-05-13): /// - 3초마다 발사 (BaseCooldown 3) /// - 캐릭터 위치에서 정면 방향 발사 /// - 40프레임 이후부터 이펙트 소멸 시점까지 0.5초 간격 5 데미지 /// - 경로 상 긴 범위 내 적 (HitboxSize.x=길이·HitboxSize.y=폭) /// public class LaserSpawner : IEffector { public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory) { var data = runtime.ActiveData; // PD 지시 2026-05-13 분리 — OffsetDistance(forwardDir) = 히트박스만·OffsetXY = 이펙트만 Vector2 facing = Vector2.right; var pc = inventory.GetComponent(); if (pc != null) facing = pc.Facing; // PD 지시 2026-05-13 — FxRotation 은 이펙트(시각) 전용. 박스(판정) 은 facing 만 반영. float baseAngle = Mathf.Atan2(facing.y, facing.x) * Mathf.Rad2Deg; // 박스 회전 = facing 만 (좌/우) float fxAngle = baseAngle + data.FxRotation; // 이펙트 회전 = facing + FxRotation Vector2 boxForward = facing.normalized; // 박스 진행 방향 = facing (FxRotation 미적용) Vector2 playerPos = inventory.transform.position; Vector2 fxPos = playerPos + data.OffsetXY; // 이펙트 위치 // 이펙트 spawn — fxPos·HitFxScale·facing+FxRotation 적용 GameObject fx = null; if (data.OnHitFxPrefab != null) { fx = Object.Instantiate(data.OnHitFxPrefab, (Vector3)fxPos, Quaternion.Euler(0f, 0f, fxAngle)); fx.hideFlags = HideFlags.DontSave; fx.transform.SetParent(inventory.transform, true); fx.transform.localScale *= data.HitFxScale; } float fxLifetime = GetFxLifetime(fx); // PD 지시 2026-05-13 — fx AutoDestroy 누락 fix·safety cap 5초 (잔상 차단) if (fx != null) Object.Destroy(fx, Mathf.Min(fxLifetime + 0.2f, 5f)); int damage = Mathf.Max(data.BaseDamage, 1); float length = Mathf.Max(data.HitboxSize.x, 1f); float width = Mathf.Max(data.HitboxSize.y, 0.5f); // 박스 = Player 자식 부착·facing 만 회전 (FxRotation 미적용) var boxGo = new GameObject("LaserHitbox_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 — OffsetDistance.x 는 facing sign 반영 (좌/우 반전) float signX = facing.x < 0f ? -1f : 1f; float localX = (signX * data.OffsetDistance.x + boxForward.x * length * 0.5f) / lpx; float localY = (data.OffsetDistance.y + boxForward.y * length * 0.5f) / lpy; boxGo.transform.localPosition = new Vector3(localX, localY, 0f); boxGo.transform.localRotation = Quaternion.Euler(0f, 0f, baseAngle); // facing 만 boxGo.transform.localScale = new Vector3(length / lpx, width / 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, fxLifetime + 0.2f); // PD 지시 2026-05-13 — DamageFrameDelay·반복 피해 영역 정합 (Player 영역 매 hit 시 영역 영역) inventory.StartCoroutine(LaserFixedHitDamageCoroutine(inventory, fx, data, damage, length, width)); } static IEnumerator LaserFixedHitDamageCoroutine(PlayerSkillInventory inventory, GameObject fx, ActiveSkillData data, int damage, float length, float width) { for (int i = 0; i < data.DamageFrameDelay; i++) yield return null; if (fx == null || inventory == null) yield break; ApplyLaserDamage(inventory, data, damage, length, width); 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 (fx == null || inventory == null) yield break; ApplyLaserDamage(inventory, data, damage, length, width); } } } // PD 지시 2026-05-13 — 판정 방향 = facing 만 (FxRotation 은 시각 전용·박스 미적용) static void ApplyLaserDamage(PlayerSkillInventory inventory, ActiveSkillData data, int damage, float length, float width) { if (inventory == null) return; Vector2 facing = Vector2.right; var pc = inventory.GetComponent(); if (pc != null) facing = pc.Facing; Vector2 forwardDir = facing.normalized; // 판정 진행 방향 = facing 만 float signX = facing.x < 0f ? -1f : 1f; // OffsetDistance.x 좌/우 반전 Vector2 origin = (Vector2)inventory.transform.position + new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y); var enemies = Object.FindObjectsByType(FindObjectsSortMode.None); int hits = 0; foreach (var e in enemies) { if (e == null) continue; Vector2 toEnemy = (Vector2)e.transform.position - origin; float along = Vector2.Dot(toEnemy, forwardDir); if (along < 0f || along > length) continue; Vector2 perpVec = toEnemy - forwardDir * along; if (perpVec.magnitude > width * 0.5f) continue; var h = e.GetComponent(); if (h == null || !h.IsAlive) continue; h.DecrementBypassInvulnWithHit(damage); hits++; if (!h.IsAlive) Schedule().enemy = e; } } static float GetFxLifetime(GameObject fxGo) { if (fxGo == null) return 2f; 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 max > 0.1f ? max : 2f; } } }