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

175 lines
8.3 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 System.Collections.Generic;
using UnityEngine;
using Platformer.Mechanics;
using Platformer.Gameplay;
using static Platformer.Core.Simulation;
namespace EerieVillage.Skills.Effectors
{
/// <summary>
/// A05 학익진 — 플레이어 주변 범위 즉시 피해 + FX_SLASH 이펙트.
/// PD 지시 (2026-05-13):
/// - 1.5초 간격 (BaseCooldown 1.5)
/// - 플레이어 주변 영역 (HitboxSize 반경) 내 적에게 피해
/// - 공격력 10 (BaseDamage)
/// - FX_SLASH 이펙트 재생 (data.OnHitFxPrefab)
/// </summary>
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.GetSpawnAnchor();
Vector2 fxPos = playerPos + data.OffsetXY;
Vector2 facing = Vector2.right;
var pc = inventory.GetComponent<PlayerController>();
facing = inventory.GetSpawnFacing(pc); // BT12-Dev-Clone (2026-05-15) γ
// 이펙트 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);
// PD 지시 2026-05-13 — ParticleSystem 명시 Play
foreach (var ps in fxGo.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; ps.Play(true); }
// PD 지시 2026-05-14 — 피격 이펙트 상위 sortingOrder
foreach (var r in fxGo.GetComponentsInChildren<Renderer>(true)) r.sortingOrder += 100;
fxLifetime = GetFxLifetime(fxGo);
// PD 지시 2026-05-13 — unscaledTime cap (Time.timeScale=0 영역 잔존 차단)
FxAutoDestroyUnscaled.Attach(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);
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;
float signX = facing.x < 0f ? -1f : 1f;
// 1차 판정 박스
SpawnHitboxVisual(inventory, "MeleeHitbox_Debug",
signX * data.OffsetDistance.x, data.OffsetDistance.y,
hitboxSize, lpx, lpy, duration);
// PD 지시 2026-05-14 — 2차 판정 박스 (A12 정화의 빛 등 양방향 판정)
if (data.EnableSecondHitbox)
{
SpawnHitboxVisual(inventory, "MeleeHitbox_Debug2",
signX * data.SecondOffsetDistance.x, data.SecondOffsetDistance.y,
data.SecondHitboxSize, lpx, lpy, duration);
}
// PD 지시 2026-05-13 — DamageFrameDelay·반복 피해 영역 정합 (Player 영역 매 hit 시 영역 영역 영역)
inventory.StartCoroutine(MeleeFixedHitDamageCoroutine(inventory, data, damage));
}
// PD 지시 2026-05-14 — 박스 시각화 공통 helper (1차 + 2차 박스 일관 정합)
static void SpawnHitboxVisual(PlayerSkillInventory inventory, string goName,
float localX, float localY, Vector2 size, float lpx, float lpy, float duration)
{
var boxGo = new GameObject(goName);
boxGo.hideFlags = HideFlags.DontSave;
boxGo.transform.SetParent(inventory.transform, false);
boxGo.transform.localPosition = new Vector3(localX / lpx, localY / lpy, 0f);
boxGo.transform.localRotation = Quaternion.identity;
boxGo.transform.localScale = new Vector3(size.x / lpx, size.y / lpy, 1f);
var sr = boxGo.AddComponent<SpriteRenderer>();
sr.sprite = HitboxDebug.GetWhiteSprite();
sr.color = new Color(1f, 0f, 0f, 0.35f);
sr.sortingOrder = 100;
sr.enabled = HitboxDebug.ShowDebugVisuals;
Object.Destroy(boxGo, duration);
}
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<PlayerController>();
facing = inventory.GetSpawnFacing(pc); // BT12-Dev-Clone (2026-05-15) γ
float signX = facing.x < 0f ? -1f : 1f;
Vector2 playerPos = inventory.GetSpawnAnchor();
// 1차 판정
DoOverlapBoxAt(playerPos + new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y),
data.HitboxSize, damage);
// PD 지시 2026-05-14 — 2차 판정 박스
if (data.EnableSecondHitbox)
{
DoOverlapBoxAt(playerPos + new Vector2(signX * data.SecondOffsetDistance.x, data.SecondOffsetDistance.y),
data.SecondHitboxSize, damage);
}
}
static void DoOverlapBoxAt(Vector2 pos, Vector2 size, int damage)
{
var cf = new ContactFilter2D();
cf.useTriggers = false;
var results = new Collider2D[32];
int n = Physics2D.OverlapBox(pos, size, 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;
h.DecrementBypassInvulnWithHit(damage);
if (!h.IsAlive) Schedule<EnemyDeath>().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<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 Mathf.Max(max, 1f);
}
}
}