175 lines
8.3 KiB
C#
175 lines
8.3 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|