fix(BT12-Dev): FxRotation 박스 미적용 분리 (PD 진단 2026-05-13)

PD 진단: 박스(판정) 은 facing 좌/우 반전만 받아야 하는데
FxRotation 이 박스에도 함께 적용되어 facing flip 과 결합 시
시각상 두 박스가 X 자로 겹쳐 보이는 현상.

방침:
- 박스(판정) = facing 만 반영 · FxRotation 미적용
- 이펙트(시각) = facing + FxRotation 그대로 (현행)

LaserSpawner:
- 박스 rotation = baseAngle (facing) 만, FxRotation 제외
- 박스 forwardDir = facing.normalized (FxRotation 회전 제외)
- OffsetDistance.x 에 facing sign 반영 (좌/우 위치 반전)
- ApplyLaserDamage 도 동일 정합

MeleeAreaSpawner:
- 박스 localRotation = identity (FxRotation 제거)
- OffsetDistance.x 에 facing sign 반영
- DoOverlapBoxFromPlayer rotation 0 · facing sign 반영

LightningStrikeSpawner:
- 박스 rotation 0 · capturedRot 변수 제거
- HitboxDebug.Spawn 후 rotation 별도 부여 라인 제거
- FixedHitDamageCoroutine 호출 rotZ=0

Projectile:
- transform.rotation = facing 만 (FxRotation 제외)
- root 가 BoxCollider2D + 시각 동시 보유 → 박스 회전 금지 의도

검증 (Play 모드 8 case):
- Laser R/L × Fx 0/90 — 박스 rot=0 또는 180 (FxRotation 무반응)
- Melee R/L × Fx 0/45 — 박스 rot=0 (FxRotation 무반응)
- Proj A13 R/L Fx 0 — 박스 rot=0/180 (facing 만)
- Stop 후 Scene 잔존 0건 (HideFlags.DontSave 유지)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-05-13 17:27:49 +09:00
parent 60e28e32ec
commit ea7d32f437
4 changed files with 43 additions and 39 deletions

View File

@ -24,42 +24,40 @@ namespace EerieVillage.Skills.Effectors
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; // PD 지시 2026-05-13 — FxRotation 은 이펙트(시각) 전용. 박스(판정) 은 facing 만 반영.
float totalAngle = baseAngle + data.FxRotation; float baseAngle = Mathf.Atan2(facing.y, facing.x) * Mathf.Rad2Deg; // 박스 회전 = facing 만 (좌/우)
float rad = totalAngle * Mathf.Deg2Rad; float fxAngle = baseAngle + data.FxRotation; // 이펙트 회전 = facing + FxRotation
Vector2 forwardDir = new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)); Vector2 boxForward = facing.normalized; // 박스 진행 방향 = facing (FxRotation 미적용)
Vector2 playerPos = inventory.transform.position; Vector2 playerPos = inventory.transform.position;
// PD 정합 2026-05-13 — OffsetDistance = (X, Y) 절대 오프셋. facing+FxRotation 영역 박스 size·rotation 만 영역. Vector2 fxPos = playerPos + data.OffsetXY; // 이펙트 위치
Vector2 hitboxOrigin = playerPos + data.OffsetDistance;
Vector2 fxPos = playerPos + data.OffsetXY; // 이펙트 위치
// 이펙트 spawn — fxPos 영역·facing 회전 + FxRotation·HitFxScale 적용 // 이펙트 spawn — fxPos·HitFxScale·facing+FxRotation 적용
GameObject fx = null; GameObject fx = null;
if (data.OnHitFxPrefab != null) if (data.OnHitFxPrefab != null)
{ {
fx = Object.Instantiate(data.OnHitFxPrefab, (Vector3)fxPos, Quaternion.Euler(0f, 0f, totalAngle)); fx = Object.Instantiate(data.OnHitFxPrefab, (Vector3)fxPos, Quaternion.Euler(0f, 0f, fxAngle));
fx.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피 fx.hideFlags = HideFlags.DontSave;
fx.transform.SetParent(inventory.transform, true); // 캐릭터 이동 시 함께 이동 fx.transform.SetParent(inventory.transform, true);
fx.transform.localScale *= data.HitFxScale; fx.transform.localScale *= data.HitFxScale;
} }
float fxLifetime = GetFxLifetime(fx); float fxLifetime = GetFxLifetime(fx);
int damage = Mathf.Max(data.BaseDamage, 1); int damage = Mathf.Max(data.BaseDamage, 1);
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);
// PD 지시 2026-05-13 — 박스 영역 Player 자식 영역 부착·forwardDir(facing+FxRotation) 정합 // 박스 = Player 자식 부착·facing 만 회전 (FxRotation 미적용)
var boxGo = new GameObject("LaserHitbox_Debug"); var boxGo = new GameObject("LaserHitbox_Debug");
boxGo.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피 boxGo.hideFlags = HideFlags.DontSave;
boxGo.transform.SetParent(inventory.transform, false); boxGo.transform.SetParent(inventory.transform, false);
float lpx = inventory.transform.lossyScale.x != 0f ? Mathf.Abs(inventory.transform.lossyScale.x) : 1f; float lpx = inventory.transform.lossyScale.x != 0f ? Mathf.Abs(inventory.transform.lossyScale.x) : 1f;
float lpy = inventory.transform.lossyScale.y != 0f ? Mathf.Abs(inventory.transform.lossyScale.y) : 1f; float lpy = inventory.transform.lossyScale.y != 0f ? Mathf.Abs(inventory.transform.lossyScale.y) : 1f;
// PD 정합 — 박스 중심 (world) = hitboxOrigin + forwardDir × length/2 // PD 지시 2026-05-13 — OffsetDistance.x 는 facing sign 반영 (좌/우 반전)
// local 좌표 = OffsetDistance + forwardDir × length/2 (parent lossyScale 보정) float signX = facing.x < 0f ? -1f : 1f;
float localX = (data.OffsetDistance.x + forwardDir.x * length * 0.5f) / lpx; float localX = (signX * data.OffsetDistance.x + boxForward.x * length * 0.5f) / lpx;
float localY = (data.OffsetDistance.y + forwardDir.y * length * 0.5f) / lpy; float localY = (data.OffsetDistance.y + boxForward.y * length * 0.5f) / lpy;
boxGo.transform.localPosition = new Vector3(localX, localY, 0f); boxGo.transform.localPosition = new Vector3(localX, localY, 0f);
boxGo.transform.localRotation = Quaternion.Euler(0f, 0f, totalAngle); boxGo.transform.localRotation = Quaternion.Euler(0f, 0f, baseAngle); // facing 만
boxGo.transform.localScale = new Vector3(length / lpx, width / lpy, 1f); boxGo.transform.localScale = new Vector3(length / lpx, width / lpy, 1f);
var sr = boxGo.AddComponent<SpriteRenderer>(); var sr = boxGo.AddComponent<SpriteRenderer>();
sr.sprite = HitboxDebug.GetWhiteSprite(); sr.sprite = HitboxDebug.GetWhiteSprite();
@ -90,18 +88,17 @@ namespace EerieVillage.Skills.Effectors
} }
} }
// PD 정합 2026-05-13 — OffsetDistanceX = X 절대·OffsetDistance = Y 절대·facing 영역 박스 방향만 영역 // PD 지시 2026-05-13 — 판정 방향 = facing 만 (FxRotation 은 시각 전용·박스 미적용)
static void ApplyLaserDamage(PlayerSkillInventory inventory, ActiveSkillData data, int damage, float length, float width) static void ApplyLaserDamage(PlayerSkillInventory inventory, ActiveSkillData data, int damage, float length, float width)
{ {
if (inventory == null) return; if (inventory == null) return;
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; Vector2 forwardDir = facing.normalized; // 판정 진행 방향 = facing 만
float totalAngle = baseAngle + data.FxRotation; float signX = facing.x < 0f ? -1f : 1f; // OffsetDistance.x 좌/우 반전
float rad = totalAngle * Mathf.Deg2Rad; Vector2 origin = (Vector2)inventory.transform.position
Vector2 forwardDir = new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)); + new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y);
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; int hits = 0;

View File

@ -63,17 +63,16 @@ namespace EerieVillage.Skills.Effectors
AutoDestroyFx(fx, fxTotalLifetime); AutoDestroyFx(fx, fxTotalLifetime);
} }
// PD 지시 2026-05-13 — 이펙트 생성 시점 영역 판정 영역 영역 캡처 (적 이동 무관 영역 정적) // PD 지시 2026-05-13 — 이펙트 생성 시점 판정 영역 정적 캡처 (적 이동 무관)
// FxRotation 은 시각(이펙트) 전용 · 박스(판정) 은 rotation=0 · facing 도 적 위치 기준이라 미적용.
Vector2 capturedSize = data.HitboxSize; Vector2 capturedSize = data.HitboxSize;
float capturedRot = data.FxRotation;
int capturedDamage = Mathf.Max(data.BaseDamage, 1); int capturedDamage = Mathf.Max(data.BaseDamage, 1);
// 박스 즉시 spawn (Trigger 시점 hitboxPos 정적·lifetime = BaseCooldown) // 박스 즉시 spawn (rotation=0 — FxRotation 미적용)
var dbgGo = HitboxDebug.Spawn(hitboxPos, capturedSize, Mathf.Max(data.BaseCooldown, 1f)); 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·반복 피해 영역 정합 // PD 지시 2026-05-13 — ScriptableObject DamageFrameDelay·반복 피해 정합
inventory.StartCoroutine(FixedHitDamageCoroutine(hitboxPos, capturedSize, capturedRot, capturedDamage, data)); inventory.StartCoroutine(FixedHitDamageCoroutine(hitboxPos, capturedSize, 0f, capturedDamage, data));
} }
// PD 지시 2026-05-13 — 고정 발동형 영역 영역 판정 영역 (DamageFrameDelay·EnableRepeatDamage·MaxHitCount·RepeatFrameInterval) // PD 지시 2026-05-13 — 고정 발동형 영역 영역 판정 영역 (DamageFrameDelay·EnableRepeatDamage·MaxHitCount·RepeatFrameInterval)

View File

@ -47,12 +47,14 @@ namespace EerieVillage.Skills.Effectors
float duration = Mathf.Max(data.BaseCooldown, 1f); float duration = Mathf.Max(data.BaseCooldown, 1f);
var boxGo = new GameObject("MeleeHitbox_Debug"); var boxGo = new GameObject("MeleeHitbox_Debug");
boxGo.hideFlags = HideFlags.DontSave; // PD 지시 2026-05-13 — Scene 저장 회피 boxGo.hideFlags = HideFlags.DontSave;
boxGo.transform.SetParent(inventory.transform, false); boxGo.transform.SetParent(inventory.transform, false);
float lpx = inventory.transform.lossyScale.x != 0f ? Mathf.Abs(inventory.transform.lossyScale.x) : 1f; float lpx = inventory.transform.lossyScale.x != 0f ? Mathf.Abs(inventory.transform.lossyScale.x) : 1f;
float lpy = inventory.transform.lossyScale.y != 0f ? Mathf.Abs(inventory.transform.lossyScale.y) : 1f; float 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); // PD 지시 2026-05-13 — 박스(판정) = facing 좌/우 sign 만 반영 · FxRotation 미적용 (시각 전용)
boxGo.transform.localRotation = Quaternion.Euler(0f, 0f, data.FxRotation); float signX = facing.x < 0f ? -1f : 1f;
boxGo.transform.localPosition = new Vector3(signX * data.OffsetDistance.x / lpx, data.OffsetDistance.y / lpy, 0f);
boxGo.transform.localRotation = Quaternion.identity;
boxGo.transform.localScale = new Vector3(hitboxSize.x / lpx, hitboxSize.y / lpy, 1f); boxGo.transform.localScale = new Vector3(hitboxSize.x / lpx, hitboxSize.y / lpy, 1f);
var sr = boxGo.AddComponent<SpriteRenderer>(); var sr = boxGo.AddComponent<SpriteRenderer>();
sr.sprite = HitboxDebug.GetWhiteSprite(); sr.sprite = HitboxDebug.GetWhiteSprite();
@ -84,11 +86,17 @@ namespace EerieVillage.Skills.Effectors
static void DoOverlapBoxFromPlayer(PlayerSkillInventory inventory, ActiveSkillData data, int damage) static void DoOverlapBoxFromPlayer(PlayerSkillInventory inventory, ActiveSkillData data, int damage)
{ {
if (inventory == null) return; if (inventory == null) return;
Vector2 hitboxPos = (Vector2)inventory.transform.position + data.OffsetDistance; // PD 지시 2026-05-13 — 박스(판정) = facing 좌/우 sign 만 · FxRotation 미적용
Vector2 facing = Vector2.right;
var pc = inventory.GetComponent<PlayerController>();
if (pc != null) facing = pc.Facing;
float signX = facing.x < 0f ? -1f : 1f;
Vector2 hitboxPos = (Vector2)inventory.transform.position
+ new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y);
var cf = new ContactFilter2D(); var cf = new ContactFilter2D();
cf.useTriggers = false; cf.useTriggers = false;
var results = new Collider2D[32]; var results = new Collider2D[32];
int n = Physics2D.OverlapBox(hitboxPos, data.HitboxSize, data.FxRotation, cf, results); int n = Physics2D.OverlapBox(hitboxPos, data.HitboxSize, 0f, cf, results);
for (int i = 0; i < n; i++) for (int i = 0; i < n; i++)
{ {
var c = results[i]; var c = results[i];

View File

@ -47,8 +47,8 @@ namespace EerieVillage.Skills.Effectors
_direction = direction.normalized; _direction = direction.normalized;
_hitTargets.Clear(); _hitTargets.Clear();
// PD 지시 2026-05-13 — 투사체 방향 정렬 + FxRotation 추가 // PD 지시 2026-05-13 — 투사체 root = 박스(판정) 정합. FxRotation 미적용 (시각 전용·박스 회전 금지).
float angle = Mathf.Atan2(_direction.y, _direction.x) * Mathf.Rad2Deg + _data.FxRotation; float angle = Mathf.Atan2(_direction.y, _direction.x) * Mathf.Rad2Deg;
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 위치 저장