using NUnit.Framework; using UnityEngine; using EerieVillage.Skills; using EerieVillage.Skills.Effectors; namespace EerieVillage.Skills.Tests.Editor { /// /// A10 분신 스킬 EditMode 테스트 (BT12-Dev-Clone · 2026-05-15 δ 단계). /// /// PD 명세 5항목 + PD 결정 4건 정합 검증. /// PlayMode 의존 (Time.unscaledTime·Update) 영역은 본 EditMode에서 제외 — PD Play 검증 영역. /// public class CloneSkillTests { // ───────────────────────────────────────────────────────────── // 1. PD 결정 상수 검증 (BaseCooldown 25·MinionLifetime 12·딜레이 0.5·offset 1·alpha 0.5) // ───────────────────────────────────────────────────────────── [Test] public void T01_CloneInstance_PdConstantsMatch() { Assert.AreEqual(12f, CloneInstance.LIFETIME_SECONDS, "PD 결정 (2026-05-15) — 분신 lifetime 12초."); Assert.AreEqual(0.5f, CloneInstance.FIRE_DELAY_SECONDS, "PD 명세 — 공격 시작 딜레이 0.5초."); Assert.AreEqual(1f, CloneInstance.SPAWN_OFFSET_X, "PD 명세 — facing 반대 방향 1유닛."); Assert.AreEqual(0.5f, CloneInstance.SPRITE_ALPHA, "PD 명세 — 반투명 alpha 0.5."); } [Test] public void T02_PlayerSkillInventory_CloneDamageMultiplierIsHalf() { // PD 명세 — 분신 공격력 50% 반감 (= 0.5f multiplier) Assert.AreEqual(0.5f, PlayerSkillInventory.CLONE_DAMAGE_MULTIPLIER, "PD 명세 4번 — 분신 damage 50% 반감."); } // ───────────────────────────────────────────────────────────── // 2. helper 메서드 동작 검증 (γ 단계 anchor·facing 분기) // ───────────────────────────────────────────────────────────── [Test] public void T03_GetSpawnAnchor_ReturnsPlayerPositionWhenNotCloneFire() { var go = new GameObject("PlayerStub"); go.AddComponent(); var inv = go.AddComponent(); go.transform.position = new Vector3(5f, 3f, 0f); inv.IsCloneFireActive = false; Vector2 anchor = inv.GetSpawnAnchor(); Assert.AreEqual(5f, anchor.x, "기본 영역 — Player x 위치 반환."); Assert.AreEqual(3f, anchor.y, "기본 영역 — Player y 위치 반환."); Object.DestroyImmediate(go); } [Test] public void T04_GetSpawnAnchor_ReturnsCloneOriginWhenCloneFireActive() { var go = new GameObject("PlayerStub"); go.AddComponent(); var inv = go.AddComponent(); go.transform.position = new Vector3(5f, 3f, 0f); inv.IsCloneFireActive = true; inv.CloneFireOrigin = new Vector2(-2f, 7f); Vector2 anchor = inv.GetSpawnAnchor(); Assert.AreEqual(-2f, anchor.x, "분신 발동 영역 — 분신 위치 반환."); Assert.AreEqual(7f, anchor.y, "분신 발동 영역 — 분신 위치 반환."); Object.DestroyImmediate(go); } [Test] public void T05_GetSpawnFacing_ReturnsCloneFacingWhenCloneFireActive() { var go = new GameObject("PlayerStub"); go.AddComponent(); var inv = go.AddComponent(); inv.IsCloneFireActive = true; inv.CloneFireFacingX = -1f; // 분신 왼쪽 facing 고정 Vector2 facing = inv.GetSpawnFacing(null); Assert.AreEqual(-1f, facing.x, "분신 facing 고정 (x sign)."); Assert.AreEqual(0f, facing.y, "분신 facing y = 0."); Object.DestroyImmediate(go); } // ───────────────────────────────────────────────────────────── // 3. CloneInstance Singleton·재귀 skip 검증 // ───────────────────────────────────────────────────────────── [Test] public void T06_CloneInstance_CurrentIsNullInitially() { // 정적 Singleton 초기 영역 null (또는 이전 테스트 잔존 정리) if (CloneInstance.Current != null) Object.DestroyImmediate(CloneInstance.Current.gameObject); Assert.IsNull(CloneInstance.Current, "분신 미spawn 영역 Current = null."); } [Test] public void T07_EnqueuePlayerFire_IgnoresA10RecursionPrep() { // 분신이 A10 발동 hook 영역 무한 재귀 방지 영역 — null·CardId == "A10" 즉시 return // 본 테스트는 hook 영역 진입 자체가 null safe 영역 확증. var go = new GameObject("CloneStub"); var instance = go.AddComponent(); // null runtime 영역 즉시 return — Exception X Assert.DoesNotThrow(() => instance.EnqueuePlayerFire(null), "null runtime hook 영역 NullReferenceException X."); Object.DestroyImmediate(go); } } }