fix(BT12-Dev): 스킬 박스·FX Scene 잔존 정정 (PD 지시 2026-05-13)

원인: Scene Assets/Scenes/Ingame.unity 에 이전 Edit Mode 측정 잔존 spawn 6건 영구 저장
- FX_Lightningball(Clone) × 3 (각각 ProjectileHitbox_Debug × 1 자식)
- FX_SLASH(Clone) × 2
- FX_Dragonfire(Clone) × 1

정정 1: Scene 잔존 spawn 6건 일괄 삭제 + Scene 재저장 (Ingame.unity).
정정 2: 모든 runtime spawn GameObject 에 HideFlags.DontSave 부여
       (HitboxDebug · Projectile · LaserSpawner · MeleeAreaSpawner ·
        LightningStrikeSpawner · ProjectileSpawner · EnemyStateComponents)
       → Scene 저장 시 무시 + Play→Stop 자동 cleanup.

검증: Play 모드 1회 발사 시 박스 1개만 spawn (LaserHitbox_Debug 1·
     A13 ProjectileHitbox_Debug 1) · Stop 후 Scene 잔존 0건 확인.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-05-13 17:20:40 +09:00
parent 2ebf3130c2
commit 60e28e32ec
9 changed files with 362 additions and 108 deletions

View File

@ -4341,7 +4341,7 @@ PrefabInstance:
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchorMax.y propertyPath: m_AnchorMax.y
value: 1 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
@ -4351,27 +4351,27 @@ PrefabInstance:
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchorMin.y propertyPath: m_AnchorMin.y
value: 1 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_SizeDelta.x propertyPath: m_SizeDelta.x
value: 480 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_SizeDelta.y propertyPath: m_SizeDelta.y
value: 600 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: 750 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: -300 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 3550758221024711263, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 3550758221024711263, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
@ -4491,7 +4491,7 @@ PrefabInstance:
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchorMax.y propertyPath: m_AnchorMax.y
value: 1 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
@ -4501,27 +4501,27 @@ PrefabInstance:
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchorMin.y propertyPath: m_AnchorMin.y
value: 1 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_SizeDelta.x propertyPath: m_SizeDelta.x
value: 480 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_SizeDelta.y propertyPath: m_SizeDelta.y
value: 600 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: 1260 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: -300 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
@ -4531,7 +4531,7 @@ PrefabInstance:
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchorMax.y propertyPath: m_AnchorMax.y
value: 1 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
@ -4541,27 +4541,27 @@ PrefabInstance:
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchorMin.y propertyPath: m_AnchorMin.y
value: 1 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_SizeDelta.x propertyPath: m_SizeDelta.x
value: 480 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_SizeDelta.y propertyPath: m_SizeDelta.y
value: 600 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchoredPosition.x propertyPath: m_AnchoredPosition.x
value: 240 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50, - target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
type: 3} type: 3}
propertyPath: m_AnchoredPosition.y propertyPath: m_AnchoredPosition.y
value: -300 value: 0
objectReference: {fileID: 0} objectReference: {fileID: 0}
m_RemovedComponents: [] m_RemovedComponents: []
m_RemovedGameObjects: [] m_RemovedGameObjects: []
@ -297788,7 +297788,7 @@ PrefabInstance:
- target: {fileID: 6885218279372954802, guid: 5166868e077e5a345bae2929e402f427, - target: {fileID: 6885218279372954802, guid: 5166868e077e5a345bae2929e402f427,
type: 3} type: 3}
propertyPath: m_LocalPosition.x propertyPath: m_LocalPosition.x
value: -4 value: -5.5
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 6885218279372954802, guid: 5166868e077e5a345bae2929e402f427, - target: {fileID: 6885218279372954802, guid: 5166868e077e5a345bae2929e402f427,
type: 3} type: 3}

View File

@ -27,8 +27,8 @@ namespace EerieVillage.Skills.Effectors
private float _tickElapsed; private float _tickElapsed;
private GameObject _fxInstance; // BT12-Dev 2026-05-13 — DoT 시각 이펙트 자식 private GameObject _fxInstance; // BT12-Dev 2026-05-13 — DoT 시각 이펙트 자식
// BT12-Dev 2026-05-13 — fxPrefab 영역 자식 Instantiate·ParticleSystem.main.duration 영역 DoT 지속 영역 자동 정합 // PD 지시 2026-05-13 — fxPrefab 영역 자식 Instantiate·DotFxScale Inspector 배율 적용
public void AddDoT(int dmg, float duration, float interval, GameObject fxPrefab = null) public void AddDoT(int dmg, float duration, float interval, GameObject fxPrefab = null, float fxScale = 1f)
{ {
_damagePerTick = dmg; _damagePerTick = dmg;
_interval = interval; _interval = interval;
@ -42,8 +42,9 @@ namespace EerieVillage.Skills.Effectors
if (fxPrefab != null) if (fxPrefab != null)
{ {
_fxInstance = Instantiate(fxPrefab, transform.position, Quaternion.identity, transform); _fxInstance = Instantiate(fxPrefab, transform.position, Quaternion.identity, transform);
_fxInstance.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피
_fxInstance.transform.localPosition = Vector3.zero; _fxInstance.transform.localPosition = Vector3.zero;
_fxInstance.transform.localScale *= 0.5f; // PD 지시 2026-05-13 — 불태우기 이펙트 50% 축소 _fxInstance.transform.localScale *= fxScale; // PD 지시 2026-05-13 — Inspector DotFxScale 배율 (기존 hardcoded 0.5 → 필드 이관)
var ps = _fxInstance.GetComponent<ParticleSystem>(); var ps = _fxInstance.GetComponent<ParticleSystem>();
if (ps == null) ps = _fxInstance.GetComponentInChildren<ParticleSystem>(); if (ps == null) ps = _fxInstance.GetComponentInChildren<ParticleSystem>();
if (ps != null) if (ps != null)
@ -72,8 +73,7 @@ namespace EerieVillage.Skills.Effectors
if (hp != null && hp.IsAlive) if (hp != null && hp.IsAlive)
{ {
hp.DecrementBypassInvuln(_damagePerTick); hp.DecrementBypassInvuln(_damagePerTick);
// PD 지시 2026-05-13 — 도트 피해 시 hit 모션 X·SpriteRenderer 영역 alpha 0.5 + 붉은색 1 frame flash // PD 지시 2026-05-13 — flash 영역 Health 영역 영역 (FlashDotHurt 폐기)
StartCoroutine(FlashDotHurt());
// PD 지시 2026-05-13 — DoT 가 Enemy 를 죽일 때 EnemyDeath Schedule 정합 발화·불태우기 즉시 정리 // PD 지시 2026-05-13 — DoT 가 Enemy 를 죽일 때 EnemyDeath Schedule 정합 발화·불태우기 즉시 정리
if (!hp.IsAlive) if (!hp.IsAlive)

View File

@ -0,0 +1,60 @@
using UnityEngine;
namespace EerieVillage.Skills.Effectors
{
/// <summary>
/// PD 지시 2026-05-13 — 모든 액티브 스킬 판정 영역 시각화 공용 helper.
/// 붉은 반투명 박스 (1×1 white sprite·color 1,0,0,0.35) 를 size 만큼 scale.
/// </summary>
public static class HitboxDebug
{
/// <summary>지정 world 좌표·size 박스 spawn·lifetime 후 destroy. lifetime=0 영역 영구.</summary>
public static GameObject Spawn(Vector2 pos, Vector2 size, float lifetime)
{
var go = new GameObject("Hitbox_Debug");
// PD 지시 2026-05-13 — 런타임 spawn 박스 Scene 저장 회피 (Edit Mode execute 시 잔존 방지)
go.hideFlags = HideFlags.DontSave;
go.transform.position = (Vector3)pos;
go.transform.localScale = new Vector3(size.x, size.y, 1f);
AttachSprite(go);
if (lifetime > 0f) Object.Destroy(go, lifetime);
return go;
}
/// <summary>지정 Transform 의 자식으로 박스 attach (target 이동 시 함께 이동·scale 은 size 그대로 유지).</summary>
public static GameObject AttachToTransform(Transform target, Vector2 size)
{
var go = new GameObject("Hitbox_Debug");
go.hideFlags = HideFlags.DontSave;
go.transform.SetParent(target, false);
go.transform.localPosition = Vector3.zero;
// parent lossyScale 보정 — local scale 환산
float px = target.lossyScale.x != 0f ? Mathf.Abs(target.lossyScale.x) : 1f;
float py = target.lossyScale.y != 0f ? Mathf.Abs(target.lossyScale.y) : 1f;
go.transform.localScale = new Vector3(size.x / px, size.y / py, 1f);
AttachSprite(go);
return go;
}
static void AttachSprite(GameObject go)
{
var sr = go.AddComponent<SpriteRenderer>();
sr.sprite = GetWhiteSprite();
sr.color = new Color(1f, 0f, 0f, 0.35f);
sr.sortingOrder = 100;
}
static Sprite _whiteSprite;
public static Sprite GetWhiteSprite()
{
if (_whiteSprite != null) return _whiteSprite;
var tex = new Texture2D(2, 2, TextureFormat.RGBA32, false);
var pixels = new Color[4];
for (int i = 0; i < 4; i++) pixels[i] = Color.white;
tex.SetPixels(pixels);
tex.Apply();
_whiteSprite = Sprite.Create(tex, new Rect(0, 0, 2, 2), new Vector2(0.5f, 0.5f), 2);
return _whiteSprite;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 13dcd1e4402dd554b9da9d95b0c58f25

View File

@ -20,18 +20,27 @@ namespace EerieVillage.Skills.Effectors
public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory) public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory)
{ {
var data = runtime.ActiveData; var data = runtime.ActiveData;
Vector2 playerPos = inventory.transform.position; // PD 지시 2026-05-13 분리 — OffsetDistance(forwardDir) = 히트박스만·OffsetXY = 이펙트만
Vector2 facing = Vector2.right; Vector2 facing = Vector2.right;
var pc = inventory.GetComponent<PlayerController>(); var pc = inventory.GetComponent<PlayerController>();
if (pc != null) facing = pc.Facing; 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 — 캐릭터 위치·방향 정합 rotation // 이펙트 spawn — fxPos 영역·facing 회전 + FxRotation·HitFxScale 적용
GameObject fx = null; GameObject fx = null;
if (data.OnHitFxPrefab != null) if (data.OnHitFxPrefab != null)
{ {
float angle = Mathf.Atan2(facing.y, facing.x) * Mathf.Rad2Deg; fx = Object.Instantiate(data.OnHitFxPrefab, (Vector3)fxPos, Quaternion.Euler(0f, 0f, totalAngle));
fx = Object.Instantiate(data.OnHitFxPrefab, (Vector3)playerPos, Quaternion.Euler(0f, 0f, angle)); fx.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피
fx.transform.SetParent(inventory.transform, true); // 캐릭터 이동 시 함께 이동 fx.transform.SetParent(inventory.transform, true); // 캐릭터 이동 시 함께 이동
fx.transform.localScale *= data.HitFxScale;
} }
float fxLifetime = GetFxLifetime(fx); float fxLifetime = GetFxLifetime(fx);
@ -39,51 +48,77 @@ namespace EerieVillage.Skills.Effectors
float length = Mathf.Max(data.HitboxSize.x, 1f); // 레이저 길이 float length = Mathf.Max(data.HitboxSize.x, 1f); // 레이저 길이
float width = Mathf.Max(data.HitboxSize.y, 0.5f); float width = Mathf.Max(data.HitboxSize.y, 0.5f);
inventory.StartCoroutine(LaserTickDamage(inventory, fx, fxLifetime, damage, length, width, data.DotInterval)); // 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 LaserTickDamage(PlayerSkillInventory inventory, GameObject fx, float fxLifetime, static IEnumerator LaserFixedHitDamageCoroutine(PlayerSkillInventory inventory, GameObject fx, ActiveSkillData data, int damage, float length, float width)
int damage, float length, float width, float interval)
{ {
// 40 frame 대기 (이펙트 강타 시점) for (int i = 0; i < data.DamageFrameDelay; i++) yield return null;
for (int i = 0; i < 40; i++) yield return null; if (fx == null || inventory == null) yield break;
ApplyLaserDamage(inventory, data, damage, length, width);
// 이펙트 종료 시점까지 interval 간격 데미지 if (data.EnableRepeatDamage)
float elapsed = 40f / 60f; // 대략 0.667s (60fps 기준)
float tickInterval = Mathf.Max(interval, 0.1f);
while (elapsed < fxLifetime && fx != null)
{ {
ApplyLaserDamage(inventory, damage, length, width); int remaining = Mathf.Max(0, data.MaxHitCount - 1);
yield return new WaitForSeconds(tickInterval); int interval = Mathf.Max(1, data.RepeatFrameInterval);
elapsed += tickInterval; 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);
}
} }
} }
static void ApplyLaserDamage(PlayerSkillInventory inventory, int damage, float length, float 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; if (inventory == null) return;
Vector2 origin = inventory.transform.position;
Vector2 facing = Vector2.right; Vector2 facing = Vector2.right;
var pc = inventory.GetComponent<PlayerController>(); var pc = inventory.GetComponent<PlayerController>();
if (pc != null) facing = pc.Facing; 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); var enemies = Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
int hits = 0;
foreach (var e in enemies) foreach (var e in enemies)
{ {
if (e == null) continue; if (e == null) continue;
Vector2 toEnemy = (Vector2)e.transform.position - origin; Vector2 toEnemy = (Vector2)e.transform.position - origin;
float along = Vector2.Dot(toEnemy, facing.normalized); // 레이저 진행 거리 float along = Vector2.Dot(toEnemy, forwardDir);
if (along < 0f || along > length) continue; // 뒤 / 너무 멀리 if (along < 0f || along > length) continue;
Vector2 perpVec = toEnemy - facing.normalized * along; Vector2 perpVec = toEnemy - forwardDir * along;
if (perpVec.magnitude > width * 0.5f) continue; // 폭 밖 if (perpVec.magnitude > width * 0.5f) continue;
var h = e.GetComponent<Health>(); var h = e.GetComponent<Health>();
if (h == null || !h.IsAlive) continue; if (h == null || !h.IsAlive) continue;
h.DecrementBypassInvuln(damage); // 0.5s 간격 누적 — invuln 미갱신 h.DecrementBypassInvulnWithHit(damage);
if (!h.IsAlive) hits++;
{ if (!h.IsAlive) Schedule<EnemyDeath>().enemy = e;
Schedule<EnemyDeath>().enemy = e;
}
} }
} }

View File

@ -47,24 +47,69 @@ namespace EerieVillage.Skills.Effectors
// 2. 임의 1기 선택 (Primary target) // 2. 임의 1기 선택 (Primary target)
var primary = candidates[Random.Range(0, candidates.Count)]; var primary = candidates[Random.Range(0, candidates.Count)];
Vector2 strikePos = primary.transform.position; // PD 정합 2026-05-13 — OffsetDistance = (X, Y) 절대 오프셋·OffsetXY = 이펙트만
Vector2 hitboxPos = (Vector2)primary.transform.position + data.OffsetDistance;
Vector2 fxPos = (Vector2)primary.transform.position + data.OffsetXY;
// 3. 이펙트 spawn (data.OnHitFxPrefab — FX_Thunder) + 총 lifetime 측정 // 3. 이펙트 spawn (data.OnHitFxPrefab — FX_Thunder) + 총 lifetime 측정
float fxTotalLifetime = 1f; float fxTotalLifetime = 1f;
if (data.OnHitFxPrefab != null) if (data.OnHitFxPrefab != null)
{ {
var fx = Object.Instantiate(data.OnHitFxPrefab, strikePos, Quaternion.identity); // PD 정합 — 이펙트는 fxPos·박스는 hitboxPos 분리
// PD 지시 2026-05-13 — 번개 이펙트 크기 20% 축소 var fx = Object.Instantiate(data.OnHitFxPrefab, fxPos, Quaternion.Euler(0f, 0f, data.FxRotation));
fx.transform.localScale *= 0.2f; fx.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피
fx.transform.localScale *= data.HitFxScale;
fxTotalLifetime = GetFxLifetime(fx); fxTotalLifetime = GetFxLifetime(fx);
AutoDestroyFx(fx, fxTotalLifetime); AutoDestroyFx(fx, fxTotalLifetime);
} }
// 4. PD 지시 2026-05-13 — 이펙트 총 lifetime 의 4/5 시점에 데미지 판정 (이펙트 강타 정합) // PD 지시 2026-05-13 — 이펙트 생성 시점 영역 판정 영역 영역 캡처 (적 이동 무관 영역 정적)
int damage = Mathf.Max(runtime.CalculateEffectiveDamage(), data.BaseDamage); Vector2 capturedSize = data.HitboxSize;
float radius = Mathf.Max(data.HitboxSize.x, data.HitboxSize.y); float capturedRot = data.FxRotation;
float damageDelay = fxTotalLifetime * 0.8f; int capturedDamage = Mathf.Max(data.BaseDamage, 1);
inventory.StartCoroutine(DelayedDamage(strikePos, radius, damage, primary, candidates, damageDelay));
// 박스 즉시 spawn (Trigger 시점 hitboxPos 정적·lifetime = BaseCooldown)
var dbgGo = HitboxDebug.Spawn(hitboxPos, capturedSize, Mathf.Max(data.BaseCooldown, 1f));
if (dbgGo != null) dbgGo.transform.rotation = Quaternion.Euler(0f, 0f, capturedRot);
// PD 지시 2026-05-13 — ScriptableObject DamageFrameDelay·반복 피해 영역 정합
inventory.StartCoroutine(FixedHitDamageCoroutine(hitboxPos, capturedSize, capturedRot, capturedDamage, data));
}
// PD 지시 2026-05-13 — 고정 발동형 영역 영역 판정 영역 (DamageFrameDelay·EnableRepeatDamage·MaxHitCount·RepeatFrameInterval)
static System.Collections.IEnumerator FixedHitDamageCoroutine(Vector2 pos, Vector2 size, float rotZ, int damage, ActiveSkillData data)
{
for (int i = 0; i < data.DamageFrameDelay; i++) yield return null;
DoOverlapBoxDamage(pos, size, rotZ, 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;
DoOverlapBoxDamage(pos, size, rotZ, damage);
}
}
}
static void DoOverlapBoxDamage(Vector2 pos, Vector2 size, float rotZ, int damage)
{
var cf = new ContactFilter2D();
cf.useTriggers = false;
var results = new Collider2D[32];
int n = Physics2D.OverlapBox(pos, size, rotZ, 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;
}
} }
// PD 지시 2026-05-13 — FX_Thunder 영역 root PS 영역 wrapper (10s) 이므로 자식 PS 영역 영역 영역 영역 // PD 지시 2026-05-13 — FX_Thunder 영역 root PS 영역 wrapper (10s) 이므로 자식 PS 영역 영역 영역 영역
@ -88,23 +133,6 @@ namespace EerieVillage.Skills.Effectors
return sum / n; return sum / n;
} }
static System.Collections.IEnumerator DelayedDamage(Vector2 strikePos, float radius, int damage, EnemyController primary, List<EnemyController> candidates, float delay)
{
yield return new WaitForSeconds(delay);
foreach (var e in candidates)
{
if (e == null) continue;
if (Vector2.Distance(e.transform.position, strikePos) > radius && e != primary) continue;
var h = e.GetComponent<Health>();
if (h == null || !h.IsAlive) continue;
h.Decrement(damage);
if (!h.IsAlive)
{
Schedule<EnemyDeath>().enemy = e;
}
}
}
static void AutoDestroyFx(GameObject fxGo, float lifetime) static void AutoDestroyFx(GameObject fxGo, float lifetime)
{ {

View File

@ -19,36 +19,98 @@ namespace EerieVillage.Skills.Effectors
public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory) public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory)
{ {
var data = runtime.ActiveData; var data = runtime.ActiveData;
// PD 지시 2026-05-13 — OffsetDistanceX = X 절대·OffsetDistance = Y 절대·OffsetXY = 이펙트만
Vector2 playerPos = inventory.transform.position; Vector2 playerPos = inventory.transform.position;
Vector2 fxPos = playerPos + data.OffsetXY;
// 이펙트 spawn — 플레이어 위치 Vector2 facing = Vector2.right;
var pc = inventory.GetComponent<PlayerController>();
if (pc != null) facing = pc.Facing;
// 이펙트 spawn — fxPos·HitFxScale·FxRotation·facing flip
GameObject fxGo = null;
float fxLifetime = 1f;
if (data.OnHitFxPrefab != null) if (data.OnHitFxPrefab != null)
{ {
var fx = Object.Instantiate(data.OnHitFxPrefab, playerPos, Quaternion.identity); fxGo = Object.Instantiate(data.OnHitFxPrefab, fxPos, Quaternion.Euler(0f, 0f, data.FxRotation));
AutoDestroyFx(fx); 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;
fxLifetime = GetFxLifetime(fxGo);
Object.Destroy(fxGo, fxLifetime + 0.2f);
} }
// 범위 내 적 일괄 피해 // PD 지시 2026-05-13 — 박스 영역 Player 자식 영역 부착·매 frame Player 따라감
Vector2 hitboxSize = data.HitboxSize;
int damage = Mathf.Max(runtime.CalculateEffectiveDamage(), data.BaseDamage); int damage = Mathf.Max(runtime.CalculateEffectiveDamage(), data.BaseDamage);
float radius = Mathf.Max(data.HitboxSize.x, data.HitboxSize.y); float duration = Mathf.Max(data.BaseCooldown, 1f);
var enemies = Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
foreach (var e in enemies) var boxGo = new GameObject("MeleeHitbox_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;
boxGo.transform.localPosition = new Vector3(data.OffsetDistance.x / lpx, data.OffsetDistance.y / lpy, 0f);
boxGo.transform.localRotation = Quaternion.Euler(0f, 0f, data.FxRotation);
boxGo.transform.localScale = new Vector3(hitboxSize.x / lpx, hitboxSize.y / 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, 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)
{ {
if (e == null) continue; int remaining = Mathf.Max(0, data.MaxHitCount - 1);
if (Vector2.Distance(e.transform.position, playerPos) > radius) continue; int interval = Mathf.Max(1, data.RepeatFrameInterval);
var h = e.GetComponent<Health>(); for (int hit = 0; hit < remaining; hit++)
if (h == null || !h.IsAlive) continue;
h.Decrement(damage);
if (!h.IsAlive)
{ {
Schedule<EnemyDeath>().enemy = e; 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;
Vector2 hitboxPos = (Vector2)inventory.transform.position + data.OffsetDistance;
var cf = new ContactFilter2D();
cf.useTriggers = false;
var results = new Collider2D[32];
int n = Physics2D.OverlapBox(hitboxPos, data.HitboxSize, data.FxRotation, 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) static void AutoDestroyFx(GameObject fxGo)
{ {
if (fxGo == null) return; 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); var psList = fxGo.GetComponentsInChildren<ParticleSystem>(true);
float max = 0f; float max = 0f;
foreach (var ps in psList) foreach (var ps in psList)
@ -57,7 +119,7 @@ namespace EerieVillage.Skills.Effectors
float t = main.duration + main.startLifetime.constantMax; float t = main.duration + main.startLifetime.constantMax;
if (t > max) max = t; if (t > max) max = t;
} }
Object.Destroy(fxGo, Mathf.Max(max, 1f) + 0.2f); return Mathf.Max(max, 1f);
} }
} }
} }

View File

@ -29,7 +29,8 @@ namespace EerieVillage.Skills.Effectors
protected readonly HashSet<Collider2D> _hitTargets = new HashSet<Collider2D>(); protected readonly HashSet<Collider2D> _hitTargets = new HashSet<Collider2D>();
// BT12-Dev 2026-05-13 — 페이드아웃·축소 (PD 지시: 사거리 80%~100% 영역 alpha 0·scale 추가 50% 축소) // BT12-Dev 2026-05-13 — 페이드아웃·축소 (PD 지시: 사거리 80%~100% 영역 alpha 0·scale 추가 50% 축소)
protected Vector3 _baseScale; // 50% 축소 후 시작 scale (페이드 보간 기준) protected Vector3 _baseScale; // 페이드 보간 기준 (= _originalScale × ProjectileFxScale·매 frame 갱신)
protected Vector3 _originalScale; // PD 정합 2026-05-13 — Inspector ProjFxScale 변경 영역 매 frame 영역 영역 base
protected Renderer[] _renderers; protected Renderer[] _renderers;
protected MaterialPropertyBlock _mpb; protected MaterialPropertyBlock _mpb;
protected float[] _baseAlphas; protected float[] _baseAlphas;
@ -46,8 +47,8 @@ namespace EerieVillage.Skills.Effectors
_direction = direction.normalized; _direction = direction.normalized;
_hitTargets.Clear(); _hitTargets.Clear();
// PD 지시 2026-05-13 — 투사체 방향 정렬 (좌·우·각도) // PD 지시 2026-05-13 — 투사체 방향 정렬 + FxRotation 추가
float angle = Mathf.Atan2(_direction.y, _direction.x) * Mathf.Rad2Deg; float angle = Mathf.Atan2(_direction.y, _direction.x) * Mathf.Rad2Deg + _data.FxRotation;
transform.rotation = Quaternion.Euler(0f, 0f, angle); transform.rotation = Quaternion.Euler(0f, 0f, angle);
// BT12-Dev 2026-05-10 (PD #1) — 거리 제한 영역 영역 spawn 위치 저장 // BT12-Dev 2026-05-10 (PD #1) — 거리 제한 영역 영역 spawn 위치 저장
@ -68,10 +69,14 @@ namespace EerieVillage.Skills.Effectors
int idx = Mathf.Clamp((int)_data.Range, 0, mults.Length - 1); int idx = Mathf.Clamp((int)_data.Range, 0, mults.Length - 1);
_maxRange = camWidth * mults[idx]; _maxRange = camWidth * mults[idx];
// BT12-Dev 2026-05-13 — 기본 크기 50% 축소 + 페이드 시작 scale 저장 (PD 지시) // PD 정합 2026-05-13 — Inspector ProjFxScale 매 frame 영역 영역 _originalScale 저장
transform.localScale *= 0.5f; _originalScale = transform.localScale;
transform.localScale *= _data.ProjectileFxScale;
_baseScale = transform.localScale; _baseScale = transform.localScale;
// PD 지시 2026-05-13 — 판정 영역 시각화 (자체 Collider2D bounds 영역 자식 박스·이동·페이드 정합)
SpawnHitboxDebugChild();
// Renderer·MaterialPropertyBlock 캐싱 + 기본 alpha 저장 // Renderer·MaterialPropertyBlock 캐싱 + 기본 alpha 저장
_renderers = GetComponentsInChildren<Renderer>(); _renderers = GetComponentsInChildren<Renderer>();
_mpb = new MaterialPropertyBlock(); _mpb = new MaterialPropertyBlock();
@ -126,6 +131,9 @@ namespace EerieVillage.Skills.Effectors
protected virtual void Update() protected virtual void Update()
{ {
// PD 지시 2026-05-13 — Inspector HitboxSize 변경 즉시 반영
SyncHitboxToData();
transform.position += (Vector3)(_direction * _speed * Time.deltaTime); transform.position += (Vector3)(_direction * _speed * Time.deltaTime);
// BT12-Dev 2026-05-10 (PD #1) — 거리 제한 영역 영역 SelfDestruct // BT12-Dev 2026-05-10 (PD #1) — 거리 제한 영역 영역 SelfDestruct
@ -185,12 +193,12 @@ namespace EerieVillage.Skills.Effectors
// 피해 적용 // 피해 적용
health.Decrement(damage); health.Decrement(damage);
// BT12-Dev 2026-05-13 — 피격 이펙트 spawn (OnHitFxPrefab 영역·적 위치·자동 destroy) // PD 지시 2026-05-13 — 피격 이펙트 spawn·HitFxScale·FxRotation 적용
// PD 지시 2026-05-13 — 피격 이펙트 크기 50% 축소
if (_data != null && _data.OnHitFxPrefab != null) if (_data != null && _data.OnHitFxPrefab != null)
{ {
var fx = Object.Instantiate(_data.OnHitFxPrefab, other.transform.position, Quaternion.identity); var fx = Object.Instantiate(_data.OnHitFxPrefab, other.transform.position, Quaternion.Euler(0f, 0f, _data.FxRotation));
fx.transform.localScale *= 0.5f; fx.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피
fx.transform.localScale *= _data.HitFxScale;
AutoDestroyOnParticleEnd(fx); AutoDestroyOnParticleEnd(fx);
} }
@ -229,6 +237,52 @@ namespace EerieVillage.Skills.Effectors
Destroy(gameObject); Destroy(gameObject);
} }
// PD 지시 2026-05-13 — 시각화 박스 자식 reference (Update 영역 매 frame Inspector 정합 갱신용)
protected Transform _debugBoxTransform;
// PD 지시 2026-05-13 — Projectile 자체 Collider2D bounds 영역 자식 박스 부착 (이동·페이드 정합·HitboxSize 정합)
void SpawnHitboxDebugChild()
{
var col = GetComponent<Collider2D>();
if (col == null) return;
Vector2 size = (col is BoxCollider2D box) ? box.size : new Vector2(col.bounds.size.x / Mathf.Max(0.01f, Mathf.Abs(transform.lossyScale.x)),
col.bounds.size.y / Mathf.Max(0.01f, Mathf.Abs(transform.lossyScale.y)));
Vector2 offset = (col is BoxCollider2D box2) ? box2.offset : (col is CircleCollider2D cc ? cc.offset : Vector2.zero);
var go = new GameObject("ProjectileHitbox_Debug");
// PD 지시 2026-05-13 — 런타임 spawn 박스 Scene 저장 회피
go.hideFlags = HideFlags.DontSave;
go.transform.SetParent(transform, false);
go.transform.localPosition = new Vector3(offset.x, offset.y, 0f);
go.transform.localScale = new Vector3(size.x, size.y, 1f);
var sr = go.AddComponent<SpriteRenderer>();
sr.sprite = HitboxDebug.GetWhiteSprite();
sr.color = new Color(1f, 0f, 0f, 0.35f);
sr.sortingOrder = 100;
_debugBoxTransform = go.transform;
}
// PD 지시 2026-05-13 — Inspector 영역 HitboxSize·ProjFxScale 변경 시 발사 중인 Projectile 도 즉시 반영.
protected void SyncHitboxToData()
{
if (_data == null) return;
// HitboxSize 영역 BoxCollider2D + 자식 박스 정합
var box = GetComponent<BoxCollider2D>();
if (box != null && box.size != _data.HitboxSize)
{
box.size = _data.HitboxSize;
}
if (_debugBoxTransform != null)
{
var s = _debugBoxTransform.localScale;
if (Mathf.Abs(s.x - _data.HitboxSize.x) > 0.001f || Mathf.Abs(s.y - _data.HitboxSize.y) > 0.001f)
{
_debugBoxTransform.localScale = new Vector3(_data.HitboxSize.x, _data.HitboxSize.y, 1f);
}
}
// PD 정합 — ProjectileFxScale 영역 _baseScale 영역 매 frame 영역 (Inspector 변경 영역 즉시)
_baseScale = _originalScale * _data.ProjectileFxScale;
}
// BT12-Dev 2026-05-13 — ParticleSystem 영역 자동 destroy. main.duration + startLifetime.constantMax 영역 영역 후 Destroy. // BT12-Dev 2026-05-13 — ParticleSystem 영역 자동 destroy. main.duration + startLifetime.constantMax 영역 영역 후 Destroy.
protected static void AutoDestroyOnParticleEnd(GameObject fxGo) protected static void AutoDestroyOnParticleEnd(GameObject fxGo)
{ {

View File

@ -26,8 +26,12 @@ namespace EerieVillage.Skills.Effectors
var pc = inventory.GetComponent<PlayerController>(); var pc = inventory.GetComponent<PlayerController>();
if (pc != null) facing = pc.Facing; if (pc != null) facing = pc.Facing;
// BT12-Dev 2026-05-10 회귀 정정 — OffsetDistance 영역 적용 (Player 영역 영역 영역 spawn → Player 영역 OverlapPoint hit·즉시 SelfDestruct 회피) // PD 정합 2026-05-13 — OffsetDistance.x = facing 방향 거리·OffsetDistance.y = 직각 거리·OffsetXY = 이펙트 절대
Vector2 spawnPos = (Vector2)playerTransform.position + facing * data.OffsetDistance; Vector2 perpDir = new Vector2(-facing.y, facing.x);
Vector2 spawnPos = (Vector2)playerTransform.position
+ facing * data.OffsetDistance.x
+ perpDir * data.OffsetDistance.y
+ data.OffsetXY;
// 프리팹 로드 (data.ProjectilePrefab 우선·없으면 fallback) // 프리팹 로드 (data.ProjectilePrefab 우선·없으면 fallback)
GameObject prefab = LoadProjectilePrefab(data); GameObject prefab = LoadProjectilePrefab(data);
@ -48,13 +52,22 @@ namespace EerieVillage.Skills.Effectors
GameObject go = prefab != null GameObject go = prefab != null
? Object.Instantiate(prefab, (Vector3)spawnPos, Quaternion.identity) ? Object.Instantiate(prefab, (Vector3)spawnPos, Quaternion.identity)
: CreateFallbackProjectile(data, (Vector3)spawnPos); : CreateFallbackProjectile(data, (Vector3)spawnPos);
// PD 지시 2026-05-13 — 런타임 spawn 투사체 Scene 저장 회피 (Edit Mode execute 시 잔존 방지)
go.hideFlags = HideFlags.DontSave;
// BT12-Dev 2026-05-13 — 외부 FX prefab 영역 Collider2D 보장 (FX_Fireball_Bullet 등 ParticleSystem prefab 부재 시 OnTriggerEnter2D 발화 X). // PD 지시 2026-05-13 — 시각화 ↔ 판정 정합 — BoxCollider2D size = HitboxSize·isTrigger=true.
if (go.GetComponent<Collider2D>() == null) { // FX prefab 영역 기존 Collider2D 있으면 size 만 정합·없으면 신규 BoxCollider2D 부착.
var cc = go.AddComponent<CircleCollider2D>(); var existingCol = go.GetComponent<Collider2D>();
cc.isTrigger = true; BoxCollider2D box;
cc.radius = 0.3f; if (existingCol is BoxCollider2D) {
box = (BoxCollider2D)existingCol;
} else {
if (existingCol != null) Object.Destroy(existingCol);
box = go.AddComponent<BoxCollider2D>();
} }
box.isTrigger = true;
box.size = data.HitboxSize;
box.offset = Vector2.zero;
Projectile proj; Projectile proj;
if (data.Trajectory == ProjectileTrajectory.Homing) if (data.Trajectory == ProjectileTrajectory.Homing)