From 60e28e32ecf2d80ddbbea5336d87d53918686472 Mon Sep 17 00:00:00 2001 From: swrring Date: Wed, 13 May 2026 17:20:40 +0900 Subject: [PATCH] =?UTF-8?q?fix(BT12-Dev):=20=EC=8A=A4=ED=82=AC=20=EB=B0=95?= =?UTF-8?q?=EC=8A=A4=C2=B7FX=20Scene=20=EC=9E=94=EC=A1=B4=20=EC=A0=95?= =?UTF-8?q?=EC=A0=95=20(PD=20=EC=A7=80=EC=8B=9C=202026-05-13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 원인: 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) --- Assets/Scenes/Ingame.unity | 38 ++++---- .../Skills/Effectors/EnemyStateComponents.cs | 10 +- .../Scripts/Skills/Effectors/HitboxDebug.cs | 60 ++++++++++++ .../Skills/Effectors/HitboxDebug.cs.meta | 2 + .../Scripts/Skills/Effectors/LaserSpawner.cs | 89 ++++++++++++------ .../Effectors/LightningStrikeSpawner.cs | 80 ++++++++++------ .../Skills/Effectors/MeleeAreaSpawner.cs | 92 ++++++++++++++++--- Assets/Scripts/Skills/Effectors/Projectile.cs | 72 +++++++++++++-- .../Skills/Effectors/ProjectileSpawner.cs | 27 ++++-- 9 files changed, 362 insertions(+), 108 deletions(-) create mode 100644 Assets/Scripts/Skills/Effectors/HitboxDebug.cs create mode 100644 Assets/Scripts/Skills/Effectors/HitboxDebug.cs.meta diff --git a/Assets/Scenes/Ingame.unity b/Assets/Scenes/Ingame.unity index cfb18b3..a9b1739 100644 --- a/Assets/Scenes/Ingame.unity +++ b/Assets/Scenes/Ingame.unity @@ -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} diff --git a/Assets/Scripts/Skills/Effectors/EnemyStateComponents.cs b/Assets/Scripts/Skills/Effectors/EnemyStateComponents.cs index 31bf65c..72df74f 100644 --- a/Assets/Scripts/Skills/Effectors/EnemyStateComponents.cs +++ b/Assets/Scripts/Skills/Effectors/EnemyStateComponents.cs @@ -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(); if (ps == null) ps = _fxInstance.GetComponentInChildren(); 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) diff --git a/Assets/Scripts/Skills/Effectors/HitboxDebug.cs b/Assets/Scripts/Skills/Effectors/HitboxDebug.cs new file mode 100644 index 0000000..ccb8c7f --- /dev/null +++ b/Assets/Scripts/Skills/Effectors/HitboxDebug.cs @@ -0,0 +1,60 @@ +using UnityEngine; + +namespace EerieVillage.Skills.Effectors +{ + /// + /// PD 지시 2026-05-13 — 모든 액티브 스킬 판정 영역 시각화 공용 helper. + /// 붉은 반투명 박스 (1×1 white sprite·color 1,0,0,0.35) 를 size 만큼 scale. + /// + public static class HitboxDebug + { + /// 지정 world 좌표·size 박스 spawn·lifetime 후 destroy. lifetime=0 영역 영구. + 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; + } + + /// 지정 Transform 의 자식으로 박스 attach (target 이동 시 함께 이동·scale 은 size 그대로 유지). + 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(); + 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; + } + } +} diff --git a/Assets/Scripts/Skills/Effectors/HitboxDebug.cs.meta b/Assets/Scripts/Skills/Effectors/HitboxDebug.cs.meta new file mode 100644 index 0000000..2120075 --- /dev/null +++ b/Assets/Scripts/Skills/Effectors/HitboxDebug.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 13dcd1e4402dd554b9da9d95b0c58f25 \ No newline at end of file diff --git a/Assets/Scripts/Skills/Effectors/LaserSpawner.cs b/Assets/Scripts/Skills/Effectors/LaserSpawner.cs index bee7be0..721f6ef 100644 --- a/Assets/Scripts/Skills/Effectors/LaserSpawner.cs +++ b/Assets/Scripts/Skills/Effectors/LaserSpawner.cs @@ -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(); 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(); + 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(); 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(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(); if (h == null || !h.IsAlive) continue; - h.DecrementBypassInvuln(damage); // 0.5s 간격 누적 — invuln 미갱신 - if (!h.IsAlive) - { - Schedule().enemy = e; - } + h.DecrementBypassInvulnWithHit(damage); + hits++; + if (!h.IsAlive) Schedule().enemy = e; } } diff --git a/Assets/Scripts/Skills/Effectors/LightningStrikeSpawner.cs b/Assets/Scripts/Skills/Effectors/LightningStrikeSpawner.cs index a92775d..095df09 100644 --- a/Assets/Scripts/Skills/Effectors/LightningStrikeSpawner.cs +++ b/Assets/Scripts/Skills/Effectors/LightningStrikeSpawner.cs @@ -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(); + if (e == null) continue; + var h = c.GetComponent(); + if (h == null || !h.IsAlive) continue; + h.DecrementBypassInvulnWithHit(damage); + if (!h.IsAlive) Schedule().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 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(); - if (h == null || !h.IsAlive) continue; - h.Decrement(damage); - if (!h.IsAlive) - { - Schedule().enemy = e; - } - } - } static void AutoDestroyFx(GameObject fxGo, float lifetime) { diff --git a/Assets/Scripts/Skills/Effectors/MeleeAreaSpawner.cs b/Assets/Scripts/Skills/Effectors/MeleeAreaSpawner.cs index cf95450..cbf883b 100644 --- a/Assets/Scripts/Skills/Effectors/MeleeAreaSpawner.cs +++ b/Assets/Scripts/Skills/Effectors/MeleeAreaSpawner.cs @@ -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(); + 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(FindObjectsSortMode.None); - foreach (var e in enemies) + 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(); + 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; - if (Vector2.Distance(e.transform.position, playerPos) > radius) continue; - var h = e.GetComponent(); - if (h == null || !h.IsAlive) continue; - h.Decrement(damage); - if (!h.IsAlive) + int remaining = Mathf.Max(0, data.MaxHitCount - 1); + int interval = Mathf.Max(1, data.RepeatFrameInterval); + for (int hit = 0; hit < remaining; hit++) { - Schedule().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(); + if (e == null) continue; + var h = c.GetComponent(); + if (h == null || !h.IsAlive) continue; + h.DecrementBypassInvulnWithHit(damage); + if (!h.IsAlive) Schedule().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(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); } } } diff --git a/Assets/Scripts/Skills/Effectors/Projectile.cs b/Assets/Scripts/Skills/Effectors/Projectile.cs index 73a1d1e..988db37 100644 --- a/Assets/Scripts/Skills/Effectors/Projectile.cs +++ b/Assets/Scripts/Skills/Effectors/Projectile.cs @@ -29,7 +29,8 @@ namespace EerieVillage.Skills.Effectors protected readonly HashSet _hitTargets = new HashSet(); // 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(); _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(); + 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(); + 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(); + 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) { diff --git a/Assets/Scripts/Skills/Effectors/ProjectileSpawner.cs b/Assets/Scripts/Skills/Effectors/ProjectileSpawner.cs index 0bb3dd6..2b2a2b0 100644 --- a/Assets/Scripts/Skills/Effectors/ProjectileSpawner.cs +++ b/Assets/Scripts/Skills/Effectors/ProjectileSpawner.cs @@ -26,8 +26,12 @@ namespace EerieVillage.Skills.Effectors var pc = inventory.GetComponent(); 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() == null) { - var cc = go.AddComponent(); - 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(); + BoxCollider2D box; + if (existingCol is BoxCollider2D) { + box = (BoxCollider2D)existingCol; + } else { + if (existingCol != null) Object.Destroy(existingCol); + box = go.AddComponent(); } + box.isTrigger = true; + box.size = data.HitboxSize; + box.offset = Vector2.zero; Projectile proj; if (data.Trajectory == ProjectileTrajectory.Homing)