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

140 lines
7.1 KiB
C#
Raw 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 System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Platformer.Mechanics;
using Platformer.Gameplay;
using static Platformer.Core.Simulation;
namespace EerieVillage.Skills.Effectors
{
/// <summary>
/// 레이저 Effector — FX_Dragonfire 레이저.
/// PD 지시 (2026-05-13):
/// - 3초마다 발사 (BaseCooldown 3)
/// - 캐릭터 위치에서 정면 방향 발사
/// - 40프레임 이후부터 이펙트 소멸 시점까지 0.5초 간격 5 데미지
/// - 경로 상 긴 범위 내 적 (HitboxSize.x=길이·HitboxSize.y=폭)
/// </summary>
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<PlayerController>();
if (pc != null) facing = pc.Facing;
float baseAngle = Mathf.Atan2(facing.y, facing.x) * Mathf.Rad2Deg;
float totalAngle = baseAngle + data.FxRotation;
float rad = totalAngle * Mathf.Deg2Rad;
Vector2 forwardDir = new Vector2(Mathf.Cos(rad), Mathf.Sin(rad));
Vector2 playerPos = inventory.transform.position;
// PD 정합 2026-05-13 — OffsetDistance = (X, Y) 절대 오프셋. facing+FxRotation 영역 박스 size·rotation 만 영역.
Vector2 hitboxOrigin = playerPos + data.OffsetDistance;
Vector2 fxPos = playerPos + data.OffsetXY; // 이펙트 위치
// 이펙트 spawn — fxPos 영역·facing 회전 + FxRotation·HitFxScale 적용
GameObject fx = null;
if (data.OnHitFxPrefab != null)
{
fx = Object.Instantiate(data.OnHitFxPrefab, (Vector3)fxPos, Quaternion.Euler(0f, 0f, totalAngle));
fx.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피
fx.transform.SetParent(inventory.transform, true); // 캐릭터 이동 시 함께 이동
fx.transform.localScale *= data.HitFxScale;
}
float fxLifetime = GetFxLifetime(fx);
int damage = Mathf.Max(data.BaseDamage, 1);
float length = Mathf.Max(data.HitboxSize.x, 1f); // 레이저 길이
float width = Mathf.Max(data.HitboxSize.y, 0.5f);
// PD 지시 2026-05-13 — 박스 영역 Player 자식 영역 부착·forwardDir(facing+FxRotation) 정합
var boxGo = new GameObject("LaserHitbox_Debug");
boxGo.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피
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 정합 — 박스 중심 (world) = hitboxOrigin + forwardDir × length/2
// local 좌표 = OffsetDistance + forwardDir × length/2 (parent lossyScale 보정)
float localX = (data.OffsetDistance.x + forwardDir.x * length * 0.5f) / lpx;
float localY = (data.OffsetDistance.y + forwardDir.y * length * 0.5f) / lpy;
boxGo.transform.localPosition = new Vector3(localX, localY, 0f);
boxGo.transform.localRotation = Quaternion.Euler(0f, 0f, totalAngle);
boxGo.transform.localScale = new Vector3(length / lpx, width / lpy, 1f);
var sr = boxGo.AddComponent<SpriteRenderer>();
sr.sprite = HitboxDebug.GetWhiteSprite();
sr.color = new Color(1f, 0f, 0f, 0.35f);
sr.sortingOrder = 100;
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 — OffsetDistanceX = X 절대·OffsetDistance = Y 절대·facing 영역 박스 방향만 영역
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<PlayerController>();
if (pc != null) facing = pc.Facing;
float baseAngle = Mathf.Atan2(facing.y, facing.x) * Mathf.Rad2Deg;
float totalAngle = baseAngle + data.FxRotation;
float rad = totalAngle * Mathf.Deg2Rad;
Vector2 forwardDir = new Vector2(Mathf.Cos(rad), Mathf.Sin(rad));
Vector2 origin = (Vector2)inventory.transform.position + data.OffsetDistance;
var enemies = Object.FindObjectsByType<EnemyController>(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<Health>();
if (h == null || !h.IsAlive) continue;
h.DecrementBypassInvulnWithHit(damage);
hits++;
if (!h.IsAlive) Schedule<EnemyDeath>().enemy = e;
}
}
static float GetFxLifetime(GameObject fxGo)
{
if (fxGo == null) return 2f;
var psList = fxGo.GetComponentsInChildren<ParticleSystem>(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;
}
}
}