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:
parent
2ebf3130c2
commit
60e28e32ec
|
|
@ -4341,7 +4341,7 @@ PrefabInstance:
|
|||
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchorMax.y
|
||||
value: 1
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
|
|
@ -4351,27 +4351,27 @@ PrefabInstance:
|
|||
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchorMin.y
|
||||
value: 1
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_SizeDelta.x
|
||||
value: 480
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_SizeDelta.y
|
||||
value: 600
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchoredPosition.x
|
||||
value: 750
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 1561733016117246437, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: -300
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 3550758221024711263, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
|
|
@ -4491,7 +4491,7 @@ PrefabInstance:
|
|||
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchorMax.y
|
||||
value: 1
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
|
|
@ -4501,27 +4501,27 @@ PrefabInstance:
|
|||
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchorMin.y
|
||||
value: 1
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_SizeDelta.x
|
||||
value: 480
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_SizeDelta.y
|
||||
value: 600
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchoredPosition.x
|
||||
value: 1260
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6974954132386231314, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: -300
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
|
|
@ -4531,7 +4531,7 @@ PrefabInstance:
|
|||
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchorMax.y
|
||||
value: 1
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
|
|
@ -4541,27 +4541,27 @@ PrefabInstance:
|
|||
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchorMin.y
|
||||
value: 1
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_SizeDelta.x
|
||||
value: 480
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_SizeDelta.y
|
||||
value: 600
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchoredPosition.x
|
||||
value: 240
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 9212598073689065413, guid: 9071d6ddb5d4f854185629ee1970af50,
|
||||
type: 3}
|
||||
propertyPath: m_AnchoredPosition.y
|
||||
value: -300
|
||||
value: 0
|
||||
objectReference: {fileID: 0}
|
||||
m_RemovedComponents: []
|
||||
m_RemovedGameObjects: []
|
||||
|
|
@ -297788,7 +297788,7 @@ PrefabInstance:
|
|||
- target: {fileID: 6885218279372954802, guid: 5166868e077e5a345bae2929e402f427,
|
||||
type: 3}
|
||||
propertyPath: m_LocalPosition.x
|
||||
value: -4
|
||||
value: -5.5
|
||||
objectReference: {fileID: 0}
|
||||
- target: {fileID: 6885218279372954802, guid: 5166868e077e5a345bae2929e402f427,
|
||||
type: 3}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ namespace EerieVillage.Skills.Effectors
|
|||
private float _tickElapsed;
|
||||
private GameObject _fxInstance; // BT12-Dev 2026-05-13 — DoT 시각 이펙트 자식
|
||||
|
||||
// BT12-Dev 2026-05-13 — fxPrefab 영역 자식 Instantiate·ParticleSystem.main.duration 영역 DoT 지속 영역 자동 정합
|
||||
public void AddDoT(int dmg, float duration, float interval, GameObject fxPrefab = null)
|
||||
// PD 지시 2026-05-13 — fxPrefab 영역 자식 Instantiate·DotFxScale Inspector 배율 적용
|
||||
public void AddDoT(int dmg, float duration, float interval, GameObject fxPrefab = null, float fxScale = 1f)
|
||||
{
|
||||
_damagePerTick = dmg;
|
||||
_interval = interval;
|
||||
|
|
@ -42,8 +42,9 @@ namespace EerieVillage.Skills.Effectors
|
|||
if (fxPrefab != null)
|
||||
{
|
||||
_fxInstance = Instantiate(fxPrefab, transform.position, Quaternion.identity, transform);
|
||||
_fxInstance.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피
|
||||
_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>();
|
||||
if (ps == null) ps = _fxInstance.GetComponentInChildren<ParticleSystem>();
|
||||
if (ps != null)
|
||||
|
|
@ -72,8 +73,7 @@ namespace EerieVillage.Skills.Effectors
|
|||
if (hp != null && hp.IsAlive)
|
||||
{
|
||||
hp.DecrementBypassInvuln(_damagePerTick);
|
||||
// PD 지시 2026-05-13 — 도트 피해 시 hit 모션 X·SpriteRenderer 영역 alpha 0.5 + 붉은색 1 frame flash
|
||||
StartCoroutine(FlashDotHurt());
|
||||
// PD 지시 2026-05-13 — flash 영역 Health 영역 영역 (FlashDotHurt 폐기)
|
||||
|
||||
// PD 지시 2026-05-13 — DoT 가 Enemy 를 죽일 때 EnemyDeath Schedule 정합 발화·불태우기 즉시 정리
|
||||
if (!hp.IsAlive)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 13dcd1e4402dd554b9da9d95b0c58f25
|
||||
|
|
@ -20,18 +20,27 @@ namespace EerieVillage.Skills.Effectors
|
|||
public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory)
|
||||
{
|
||||
var data = runtime.ActiveData;
|
||||
Vector2 playerPos = inventory.transform.position;
|
||||
// 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 — 캐릭터 위치·방향 정합 rotation
|
||||
// 이펙트 spawn — fxPos 영역·facing 회전 + FxRotation·HitFxScale 적용
|
||||
GameObject fx = null;
|
||||
if (data.OnHitFxPrefab != null)
|
||||
{
|
||||
float angle = Mathf.Atan2(facing.y, facing.x) * Mathf.Rad2Deg;
|
||||
fx = Object.Instantiate(data.OnHitFxPrefab, (Vector3)playerPos, Quaternion.Euler(0f, 0f, angle));
|
||||
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);
|
||||
|
|
@ -39,51 +48,77 @@ namespace EerieVillage.Skills.Effectors
|
|||
float length = Mathf.Max(data.HitboxSize.x, 1f); // 레이저 길이
|
||||
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,
|
||||
int damage, float length, float width, float interval)
|
||||
static IEnumerator LaserFixedHitDamageCoroutine(PlayerSkillInventory inventory, GameObject fx, ActiveSkillData data, int damage, float length, float width)
|
||||
{
|
||||
// 40 frame 대기 (이펙트 강타 시점)
|
||||
for (int i = 0; i < 40; i++) yield return null;
|
||||
for (int i = 0; i < data.DamageFrameDelay; i++) yield return null;
|
||||
if (fx == null || inventory == null) yield break;
|
||||
ApplyLaserDamage(inventory, data, damage, length, width);
|
||||
|
||||
// 이펙트 종료 시점까지 interval 간격 데미지
|
||||
float elapsed = 40f / 60f; // 대략 0.667s (60fps 기준)
|
||||
float tickInterval = Mathf.Max(interval, 0.1f);
|
||||
while (elapsed < fxLifetime && fx != null)
|
||||
if (data.EnableRepeatDamage)
|
||||
{
|
||||
ApplyLaserDamage(inventory, damage, length, width);
|
||||
yield return new WaitForSeconds(tickInterval);
|
||||
elapsed += tickInterval;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
Vector2 origin = inventory.transform.position;
|
||||
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, facing.normalized); // 레이저 진행 거리
|
||||
if (along < 0f || along > length) continue; // 뒤 / 너무 멀리
|
||||
Vector2 perpVec = toEnemy - facing.normalized * along;
|
||||
if (perpVec.magnitude > width * 0.5f) continue; // 폭 밖
|
||||
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.DecrementBypassInvuln(damage); // 0.5s 간격 누적 — invuln 미갱신
|
||||
if (!h.IsAlive)
|
||||
{
|
||||
Schedule<EnemyDeath>().enemy = e;
|
||||
}
|
||||
h.DecrementBypassInvulnWithHit(damage);
|
||||
hits++;
|
||||
if (!h.IsAlive) Schedule<EnemyDeath>().enemy = e;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,24 +47,69 @@ namespace EerieVillage.Skills.Effectors
|
|||
|
||||
// 2. 임의 1기 선택 (Primary target)
|
||||
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 측정
|
||||
float fxTotalLifetime = 1f;
|
||||
if (data.OnHitFxPrefab != null)
|
||||
{
|
||||
var fx = Object.Instantiate(data.OnHitFxPrefab, strikePos, Quaternion.identity);
|
||||
// PD 지시 2026-05-13 — 번개 이펙트 크기 20% 축소
|
||||
fx.transform.localScale *= 0.2f;
|
||||
// PD 정합 — 이펙트는 fxPos·박스는 hitboxPos 분리
|
||||
var fx = Object.Instantiate(data.OnHitFxPrefab, fxPos, Quaternion.Euler(0f, 0f, data.FxRotation));
|
||||
fx.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피
|
||||
fx.transform.localScale *= data.HitFxScale;
|
||||
fxTotalLifetime = GetFxLifetime(fx);
|
||||
AutoDestroyFx(fx, fxTotalLifetime);
|
||||
}
|
||||
|
||||
// 4. PD 지시 2026-05-13 — 이펙트 총 lifetime 의 4/5 시점에 데미지 판정 (이펙트 강타 정합)
|
||||
int damage = Mathf.Max(runtime.CalculateEffectiveDamage(), data.BaseDamage);
|
||||
float radius = Mathf.Max(data.HitboxSize.x, data.HitboxSize.y);
|
||||
float damageDelay = fxTotalLifetime * 0.8f;
|
||||
inventory.StartCoroutine(DelayedDamage(strikePos, radius, damage, primary, candidates, damageDelay));
|
||||
// PD 지시 2026-05-13 — 이펙트 생성 시점 영역 판정 영역 영역 캡처 (적 이동 무관 영역 정적)
|
||||
Vector2 capturedSize = data.HitboxSize;
|
||||
float capturedRot = data.FxRotation;
|
||||
int capturedDamage = Mathf.Max(data.BaseDamage, 1);
|
||||
|
||||
// 박스 즉시 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 영역 영역 영역 영역
|
||||
|
|
@ -88,23 +133,6 @@ namespace EerieVillage.Skills.Effectors
|
|||
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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,36 +19,98 @@ namespace EerieVillage.Skills.Effectors
|
|||
public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory)
|
||||
{
|
||||
var data = runtime.ActiveData;
|
||||
// PD 지시 2026-05-13 — OffsetDistanceX = X 절대·OffsetDistance = Y 절대·OffsetXY = 이펙트만
|
||||
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)
|
||||
{
|
||||
var fx = Object.Instantiate(data.OnHitFxPrefab, playerPos, Quaternion.identity);
|
||||
AutoDestroyFx(fx);
|
||||
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;
|
||||
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);
|
||||
float radius = Mathf.Max(data.HitboxSize.x, data.HitboxSize.y);
|
||||
var enemies = Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
|
||||
foreach (var e in enemies)
|
||||
{
|
||||
if (e == null) continue;
|
||||
if (Vector2.Distance(e.transform.position, playerPos) > radius) continue;
|
||||
var h = e.GetComponent<Health>();
|
||||
if (h == null || !h.IsAlive) continue;
|
||||
h.Decrement(damage);
|
||||
if (!h.IsAlive)
|
||||
{
|
||||
Schedule<EnemyDeath>().enemy = e;
|
||||
float duration = Mathf.Max(data.BaseCooldown, 1f);
|
||||
|
||||
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)
|
||||
{
|
||||
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;
|
||||
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)
|
||||
{
|
||||
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)
|
||||
|
|
@ -57,7 +119,7 @@ namespace EerieVillage.Skills.Effectors
|
|||
float t = main.duration + main.startLifetime.constantMax;
|
||||
if (t > max) max = t;
|
||||
}
|
||||
Object.Destroy(fxGo, Mathf.Max(max, 1f) + 0.2f);
|
||||
return Mathf.Max(max, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ namespace EerieVillage.Skills.Effectors
|
|||
protected readonly HashSet<Collider2D> _hitTargets = new HashSet<Collider2D>();
|
||||
|
||||
// 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 MaterialPropertyBlock _mpb;
|
||||
protected float[] _baseAlphas;
|
||||
|
|
@ -46,8 +47,8 @@ namespace EerieVillage.Skills.Effectors
|
|||
_direction = direction.normalized;
|
||||
_hitTargets.Clear();
|
||||
|
||||
// PD 지시 2026-05-13 — 투사체 방향 정렬 (좌·우·각도)
|
||||
float angle = Mathf.Atan2(_direction.y, _direction.x) * Mathf.Rad2Deg;
|
||||
// PD 지시 2026-05-13 — 투사체 방향 정렬 + FxRotation 추가
|
||||
float angle = Mathf.Atan2(_direction.y, _direction.x) * Mathf.Rad2Deg + _data.FxRotation;
|
||||
transform.rotation = Quaternion.Euler(0f, 0f, angle);
|
||||
|
||||
// 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);
|
||||
_maxRange = camWidth * mults[idx];
|
||||
|
||||
// BT12-Dev 2026-05-13 — 기본 크기 50% 축소 + 페이드 시작 scale 저장 (PD 지시)
|
||||
transform.localScale *= 0.5f;
|
||||
// PD 정합 2026-05-13 — Inspector ProjFxScale 매 frame 영역 영역 _originalScale 저장
|
||||
_originalScale = transform.localScale;
|
||||
transform.localScale *= _data.ProjectileFxScale;
|
||||
_baseScale = transform.localScale;
|
||||
|
||||
// PD 지시 2026-05-13 — 판정 영역 시각화 (자체 Collider2D bounds 영역 자식 박스·이동·페이드 정합)
|
||||
SpawnHitboxDebugChild();
|
||||
|
||||
// Renderer·MaterialPropertyBlock 캐싱 + 기본 alpha 저장
|
||||
_renderers = GetComponentsInChildren<Renderer>();
|
||||
_mpb = new MaterialPropertyBlock();
|
||||
|
|
@ -126,6 +131,9 @@ namespace EerieVillage.Skills.Effectors
|
|||
|
||||
protected virtual void Update()
|
||||
{
|
||||
// PD 지시 2026-05-13 — Inspector HitboxSize 변경 즉시 반영
|
||||
SyncHitboxToData();
|
||||
|
||||
transform.position += (Vector3)(_direction * _speed * Time.deltaTime);
|
||||
|
||||
// BT12-Dev 2026-05-10 (PD #1) — 거리 제한 영역 영역 SelfDestruct
|
||||
|
|
@ -185,12 +193,12 @@ namespace EerieVillage.Skills.Effectors
|
|||
// 피해 적용
|
||||
health.Decrement(damage);
|
||||
|
||||
// BT12-Dev 2026-05-13 — 피격 이펙트 spawn (OnHitFxPrefab 영역·적 위치·자동 destroy)
|
||||
// PD 지시 2026-05-13 — 피격 이펙트 크기 50% 축소
|
||||
// PD 지시 2026-05-13 — 피격 이펙트 spawn·HitFxScale·FxRotation 적용
|
||||
if (_data != null && _data.OnHitFxPrefab != null)
|
||||
{
|
||||
var fx = Object.Instantiate(_data.OnHitFxPrefab, other.transform.position, Quaternion.identity);
|
||||
fx.transform.localScale *= 0.5f;
|
||||
var fx = Object.Instantiate(_data.OnHitFxPrefab, other.transform.position, Quaternion.Euler(0f, 0f, _data.FxRotation));
|
||||
fx.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피
|
||||
fx.transform.localScale *= _data.HitFxScale;
|
||||
AutoDestroyOnParticleEnd(fx);
|
||||
}
|
||||
|
||||
|
|
@ -229,6 +237,52 @@ namespace EerieVillage.Skills.Effectors
|
|||
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.
|
||||
protected static void AutoDestroyOnParticleEnd(GameObject fxGo)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -26,8 +26,12 @@ namespace EerieVillage.Skills.Effectors
|
|||
var pc = inventory.GetComponent<PlayerController>();
|
||||
if (pc != null) facing = pc.Facing;
|
||||
|
||||
// BT12-Dev 2026-05-10 회귀 정정 — OffsetDistance 영역 적용 (Player 영역 영역 영역 spawn → Player 영역 OverlapPoint hit·즉시 SelfDestruct 회피)
|
||||
Vector2 spawnPos = (Vector2)playerTransform.position + facing * data.OffsetDistance;
|
||||
// PD 정합 2026-05-13 — OffsetDistance.x = facing 방향 거리·OffsetDistance.y = 직각 거리·OffsetXY = 이펙트 절대
|
||||
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)
|
||||
GameObject prefab = LoadProjectilePrefab(data);
|
||||
|
|
@ -48,13 +52,22 @@ namespace EerieVillage.Skills.Effectors
|
|||
GameObject go = prefab != null
|
||||
? Object.Instantiate(prefab, (Vector3)spawnPos, Quaternion.identity)
|
||||
: 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).
|
||||
if (go.GetComponent<Collider2D>() == null) {
|
||||
var cc = go.AddComponent<CircleCollider2D>();
|
||||
cc.isTrigger = true;
|
||||
cc.radius = 0.3f;
|
||||
// PD 지시 2026-05-13 — 시각화 ↔ 판정 정합 — BoxCollider2D size = HitboxSize·isTrigger=true.
|
||||
// FX prefab 영역 기존 Collider2D 있으면 size 만 정합·없으면 신규 BoxCollider2D 부착.
|
||||
var existingCol = go.GetComponent<Collider2D>();
|
||||
BoxCollider2D box;
|
||||
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;
|
||||
if (data.Trajectory == ProjectileTrajectory.Homing)
|
||||
|
|
|
|||
Loading…
Reference in New Issue