Compare commits
3 Commits
392ef3a11f
...
618525d05b
| Author | SHA1 | Date |
|---|---|---|
|
|
618525d05b | |
|
|
70c98dd3a8 | |
|
|
171506e16d |
|
|
@ -0,0 +1,70 @@
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 69566f3f65e99394d8a0ccd0b395ac77, type: 3}
|
||||||
|
m_Name: A10_bunsin
|
||||||
|
m_EditorClassIdentifier: Assembly-CSharp::EerieVillage.Skills.ActiveSkillData
|
||||||
|
CardId: A10
|
||||||
|
DisplayName: "분신"
|
||||||
|
EnglishName: Clone
|
||||||
|
Icon: {fileID: 0}
|
||||||
|
Description: "25초마다 12초간 반투명 분신
|
||||||
|
1기 소환. 플레이어와 동일한 스킬을
|
||||||
|
0.5초 뒤 50% 데미지로 사용."
|
||||||
|
AttributeTags: 0
|
||||||
|
TypeTags: 0
|
||||||
|
maxLevel: 5
|
||||||
|
Category: 3
|
||||||
|
Trigger: 0
|
||||||
|
BaseCooldown: 25
|
||||||
|
BaseDamage: 0
|
||||||
|
HitboxSize: {x: 0, y: 0}
|
||||||
|
OffsetDistance: {x: 0, y: 0}
|
||||||
|
EnableSecondHitbox: 0
|
||||||
|
SecondHitboxSize: {x: 0, y: 0}
|
||||||
|
SecondOffsetDistance: {x: 0, y: 0}
|
||||||
|
Trajectory: 0
|
||||||
|
MinionPrefab: {fileID: 0}
|
||||||
|
ChainCount: 0
|
||||||
|
DotDuration: 0
|
||||||
|
DotInterval: 0.5
|
||||||
|
StunDuration: 0
|
||||||
|
SlowDuration: 0
|
||||||
|
SlowMultiplier: 0.5
|
||||||
|
KnockbackForce: 0
|
||||||
|
MaxConcurrent: 1
|
||||||
|
MinionLifetime: 12
|
||||||
|
AuraTickInterval: 1
|
||||||
|
AuraRadius: 0
|
||||||
|
CritDamageMultiplier: 2
|
||||||
|
IFrameDuration: 0
|
||||||
|
DebuffStackLimit: 0
|
||||||
|
FireProbability: 1
|
||||||
|
Range: 0
|
||||||
|
MaxRange: 10
|
||||||
|
ProjectileSpeed: 6
|
||||||
|
ProjectilePrefab: {fileID: 0}
|
||||||
|
OnHitFxPrefab: {fileID: 0}
|
||||||
|
ExtraHitFxPrefab: {fileID: 0}
|
||||||
|
CastFxPrefab: {fileID: 0}
|
||||||
|
ProjectileAngleOffset: 0
|
||||||
|
TargetEnemyOnFire: 0
|
||||||
|
OnDotFxPrefab: {fileID: 0}
|
||||||
|
DotDamageMultiplier: 0.5
|
||||||
|
ProjectileFxScale: 1
|
||||||
|
HitFxScale: 1
|
||||||
|
DotFxScale: 1
|
||||||
|
FxRotation: 0
|
||||||
|
OffsetXY: {x: 0, y: 0}
|
||||||
|
DamageFrameDelay: 0
|
||||||
|
EnableRepeatDamage: 0
|
||||||
|
MaxHitCount: 1
|
||||||
|
RepeatFrameInterval: 30
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a10b5c1ec5a72ba84b126e02c4d8f156
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 0
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
namespace EerieVillage.Skills.Effectors
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A10 분신 Effector (BT12-Dev-Clone · 2026-05-15).
|
||||||
|
///
|
||||||
|
/// Category D (Minion) 영역 CardId == "A10" 분기에서 호출.
|
||||||
|
/// γ 단계 SkillFireEvent.Execute 영역 Minion case 분기:
|
||||||
|
/// if (data.CardId == "A10") effector = new CloneEffector();
|
||||||
|
/// else effector = new SpiritFireSpawner(); // 기존 A11
|
||||||
|
///
|
||||||
|
/// 본 Effector는 분신 spawn (또는 Singleton 1기 교체)만 담당.
|
||||||
|
/// 분신 자체 발동은 CloneInstance.EnqueuePlayerFire (Player Fire hook 시점).
|
||||||
|
/// </summary>
|
||||||
|
public class CloneEffector : IEffector
|
||||||
|
{
|
||||||
|
public void Trigger(ActiveSkillRuntime runtime, PlayerSkillInventory inventory)
|
||||||
|
{
|
||||||
|
if (runtime == null || inventory == null) return;
|
||||||
|
|
||||||
|
// 분신 spawn 또는 교체 (Singleton 1기 유지·PD 결정)
|
||||||
|
CloneInstance.SpawnOrReplace(inventory, runtime.ActiveData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: eeea85fc8d91d75468cd46107e1caf27
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using Platformer.Mechanics;
|
||||||
|
using static Platformer.Core.Simulation;
|
||||||
|
|
||||||
|
namespace EerieVillage.Skills.Effectors
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A10 분신 인스턴스 (BT12-Dev-Clone · 2026-05-15).
|
||||||
|
///
|
||||||
|
/// PD 직접 명세 (2026-05-15):
|
||||||
|
/// 1. 플레이어 x좌표 1 뒤쪽 spawn (facing 반대 방향 1유닛)
|
||||||
|
/// 2. 반투명 sprite (alpha 0.5)
|
||||||
|
/// 3. 플레이어와 동일한 스킬 사용 (장착 액티브 미러링)
|
||||||
|
/// 4. 분신 공격력 = 플레이어 공격력의 50% 반감
|
||||||
|
/// 5. 플레이어보다 0.5초 뒤 사용 (공격 시작 딜레이)
|
||||||
|
///
|
||||||
|
/// PD 추가 결정 (2026-05-15):
|
||||||
|
/// - BaseCooldown 25초 (PM 1차 30 → PD 조정 25)
|
||||||
|
/// - MinionLifetime 12초 자동 소멸 + Singleton 1기 유지
|
||||||
|
/// - facing 고정 (spawn 시점 facing 고정·이동 시 분신 위치·방향 불변)
|
||||||
|
/// - 무적 = Collider 미부착 (적 투사체·벽·player·enemy 모두 통과)
|
||||||
|
/// - Lv 업 시 분신 수 증가 X · 추후 지속시간↑+플레이어 참조 데미지 비율(%)↑
|
||||||
|
/// (balance-designer 후속 수치 확정)
|
||||||
|
///
|
||||||
|
/// β 단계 의존성 (PlayerSkillInventory 신규 필드 추가 후 컴파일 통과):
|
||||||
|
/// - PlayerSkillInventory.OnPlayerSkillFired (event System.Action<ActiveSkillRuntime>)
|
||||||
|
/// - PlayerSkillInventory.IsCloneFireActive (bool · default false)
|
||||||
|
/// - PlayerSkillInventory.CloneFireOrigin (Vector2)
|
||||||
|
/// - PlayerSkillInventory.CloneFireFacingX (float)
|
||||||
|
/// - PlayerSkillInventory.CLONE_DAMAGE_MULTIPLIER (const 0.5f)
|
||||||
|
/// </summary>
|
||||||
|
public class CloneInstance : MonoBehaviour
|
||||||
|
{
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// PD 결정 상수 (2026-05-15)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>PD 결정 — 분신 자동 소멸 시간 (초). Lv 업 시 추후 증가 (balance-designer).</summary>
|
||||||
|
public const float LIFETIME_SECONDS = 12f;
|
||||||
|
|
||||||
|
/// <summary>PD 명세 — 공격 시작 딜레이 (초).</summary>
|
||||||
|
public const float FIRE_DELAY_SECONDS = 0.5f;
|
||||||
|
|
||||||
|
/// <summary>PD 명세 — 분신 spawn 위치 (facing 반대 방향 1유닛).</summary>
|
||||||
|
public const float SPAWN_OFFSET_X = 1f;
|
||||||
|
|
||||||
|
/// <summary>PD 명세 — 분신 sprite alpha (반투명).</summary>
|
||||||
|
public const float SPRITE_ALPHA = 0.5f;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Singleton (PD 결정 — 분신 1기 유지·재발동 시 기존 destroy + 새 spawn)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static CloneInstance _current;
|
||||||
|
|
||||||
|
/// <summary>현재 활성 분신 인스턴스 (테스트·진단용 · null = 분신 없음).</summary>
|
||||||
|
public static CloneInstance Current => _current;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 상태 필드
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private PlayerSkillInventory _playerInventory;
|
||||||
|
// spawn 시점 Player facing.x sign (PD 결정 — 분신 facing 고정)
|
||||||
|
private float _spawnFacingX;
|
||||||
|
// spawn 시각 (lifetime 자동 소멸용 · timeScale 무관 unscaledTime)
|
||||||
|
private float _spawnTime;
|
||||||
|
|
||||||
|
// 지연 큐 — Player Fire 시 0.5초 후 분신 발동 enqueue
|
||||||
|
private readonly Queue<PendingFire> _pendingQueue = new Queue<PendingFire>();
|
||||||
|
|
||||||
|
private struct PendingFire
|
||||||
|
{
|
||||||
|
public float TriggerTime; // Time.unscaledTime + FIRE_DELAY_SECONDS
|
||||||
|
public ActiveSkillRuntime Runtime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Spawn / Replace (Singleton 1기 유지)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 분신 spawn 또는 기존 destroy + 새 spawn.
|
||||||
|
/// CloneEffector.Trigger 영역에서 호출 (A10 발동 시점).
|
||||||
|
/// </summary>
|
||||||
|
public static void SpawnOrReplace(PlayerSkillInventory playerInventory, ActiveSkillData cloneData)
|
||||||
|
{
|
||||||
|
if (playerInventory == null) return;
|
||||||
|
|
||||||
|
// 1. 기존 분신 destroy (Singleton 1기 유지)
|
||||||
|
if (_current != null)
|
||||||
|
{
|
||||||
|
Destroy(_current.gameObject);
|
||||||
|
_current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Player 위치·facing 취득
|
||||||
|
var pc = playerInventory.GetComponent<PlayerController>();
|
||||||
|
Vector2 facing = pc != null ? pc.Facing : Vector2.right;
|
||||||
|
float signX = facing.x < 0f ? -1f : 1f;
|
||||||
|
Vector2 playerPos = playerInventory.transform.position;
|
||||||
|
|
||||||
|
// 3. PD 명세 — facing 반대 1유닛 (facing=오른쪽 → 분신 x = player.x - 1)
|
||||||
|
Vector2 spawnPos = playerPos + new Vector2(-signX * SPAWN_OFFSET_X, 0f);
|
||||||
|
|
||||||
|
// 4. 신규 GameObject — sprite 복제 + alpha 0.5 + collider 미부착 (무적)
|
||||||
|
var go = new GameObject("Clone_A10");
|
||||||
|
go.hideFlags = HideFlags.DontSave; // Scene 오염 방지 (BT12-Dev SOT 정합)
|
||||||
|
go.transform.position = spawnPos;
|
||||||
|
|
||||||
|
var playerSr = playerInventory.GetComponentInChildren<SpriteRenderer>();
|
||||||
|
if (playerSr != null)
|
||||||
|
{
|
||||||
|
var sr = go.AddComponent<SpriteRenderer>();
|
||||||
|
sr.sprite = playerSr.sprite;
|
||||||
|
sr.flipX = playerSr.flipX;
|
||||||
|
sr.sortingOrder = playerSr.sortingOrder - 1; // Player 뒤쪽 sorting
|
||||||
|
Color c = playerSr.color;
|
||||||
|
c.a = SPRITE_ALPHA; // PD 명세 — 반투명
|
||||||
|
sr.color = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PD 결정 — Collider 미부착 (무적·무충돌). Rigidbody2D도 미부착 (gravity·물리 차단).
|
||||||
|
|
||||||
|
// 5. CloneInstance 컴포넌트 부착 + 상태 초기화
|
||||||
|
var instance = go.AddComponent<CloneInstance>();
|
||||||
|
instance._playerInventory = playerInventory;
|
||||||
|
instance._spawnFacingX = signX;
|
||||||
|
instance._spawnTime = Time.unscaledTime;
|
||||||
|
_current = instance;
|
||||||
|
|
||||||
|
// 6. Player Fire 이벤트 구독
|
||||||
|
// (β 단계 PlayerSkillInventory.OnPlayerSkillFired 신설 후 컴파일 통과)
|
||||||
|
playerInventory.OnPlayerSkillFired += instance.EnqueuePlayerFire;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Player Fire hook (0.5초 지연 큐 enqueue)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Player Fire 시점 hook (PlayerSkillInventory.OnPlayerSkillFired 이벤트 핸들러).
|
||||||
|
/// 0.5초 지연 후 분신 위치 anchor로 동일 Effector 재호출.
|
||||||
|
/// </summary>
|
||||||
|
public void EnqueuePlayerFire(ActiveSkillRuntime runtime)
|
||||||
|
{
|
||||||
|
if (runtime == null || runtime.ActiveData == null) return;
|
||||||
|
|
||||||
|
// 무한 재귀 방지 — 분신은 A10 분신을 발동하지 않음
|
||||||
|
if (runtime.ActiveData.CardId == "A10") return;
|
||||||
|
|
||||||
|
_pendingQueue.Enqueue(new PendingFire
|
||||||
|
{
|
||||||
|
TriggerTime = Time.unscaledTime + FIRE_DELAY_SECONDS,
|
||||||
|
Runtime = runtime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Update — lifetime 자동 소멸 + 지연 큐 dequeue
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
// PD 결정 (2026-05-15) — 12초 자동 소멸
|
||||||
|
if (Time.unscaledTime - _spawnTime >= LIFETIME_SECONDS)
|
||||||
|
{
|
||||||
|
Destroy(gameObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 지연 큐 dequeue — TriggerTime 도달 항목 발동
|
||||||
|
while (_pendingQueue.Count > 0 && _pendingQueue.Peek().TriggerTime <= Time.unscaledTime)
|
||||||
|
{
|
||||||
|
var pending = _pendingQueue.Dequeue();
|
||||||
|
FireAtClonePosition(pending.Runtime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// 분신 위치 anchor Effector 재호출 (50% damage 분기는 PlayerSkillInventory.IsCloneFireActive 영역)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void FireAtClonePosition(ActiveSkillRuntime runtime)
|
||||||
|
{
|
||||||
|
if (runtime == null || runtime.ActiveData == null || _playerInventory == null) return;
|
||||||
|
|
||||||
|
// β 단계 — PlayerSkillInventory 신규 필드 set
|
||||||
|
// IsCloneFireActive = true
|
||||||
|
// → γ 단계 6 Effector 영역 anchor·facing·damage 분기
|
||||||
|
// → ActiveSkillRuntime.CalculateEffectiveDamage 영역 50% multiplier
|
||||||
|
// CloneFireOrigin = transform.position (분신 위치 anchor)
|
||||||
|
// CloneFireFacingX = _spawnFacingX (분신 facing 고정)
|
||||||
|
_playerInventory.IsCloneFireActive = true;
|
||||||
|
_playerInventory.CloneFireOrigin = transform.position;
|
||||||
|
_playerInventory.CloneFireFacingX = _spawnFacingX;
|
||||||
|
|
||||||
|
// SkillFireEvent.Execute 영역 그대로 재호출 (Effector 분기 영역 IsCloneFireActive 처리)
|
||||||
|
// reset은 γ 단계 SkillFireEvent.Cleanup 영역 IsCloneFireActive=false 일관 reset
|
||||||
|
// (설계 §A10-7 채택안 (가))
|
||||||
|
var ev = Schedule<SkillFireEvent>(); // parent namespace EerieVillage.Skills 자동 인식
|
||||||
|
ev.Runtime = runtime;
|
||||||
|
ev.Inventory = _playerInventory;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// OnDestroy — Player Inventory hook unsubscribe + Singleton 정리
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void OnDestroy()
|
||||||
|
{
|
||||||
|
// Player Inventory hook unsubscribe (β 단계 OnPlayerSkillFired 신설 후 컴파일 통과)
|
||||||
|
if (_playerInventory != null)
|
||||||
|
{
|
||||||
|
_playerInventory.OnPlayerSkillFired -= EnqueuePlayerFire;
|
||||||
|
}
|
||||||
|
if (_current == this) _current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a3721d2092927044e85f3467e225015c
|
||||||
|
|
@ -23,12 +23,12 @@ namespace EerieVillage.Skills.Effectors
|
||||||
// PD 지시 2026-05-13 분리 — OffsetDistance(forwardDir) = 히트박스만·OffsetXY = 이펙트만
|
// 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;
|
facing = inventory.GetSpawnFacing(pc); // BT12-Dev-Clone (2026-05-15) γ
|
||||||
// PD 지시 2026-05-13 — FxRotation 은 이펙트(시각) 전용. 박스(판정) 은 facing 만 반영.
|
// PD 지시 2026-05-13 — FxRotation 은 이펙트(시각) 전용. 박스(판정) 은 facing 만 반영.
|
||||||
float baseAngle = Mathf.Atan2(facing.y, facing.x) * Mathf.Rad2Deg; // 박스 회전 = facing 만 (좌/우)
|
float baseAngle = Mathf.Atan2(facing.y, facing.x) * Mathf.Rad2Deg; // 박스 회전 = facing 만 (좌/우)
|
||||||
float fxAngle = baseAngle + data.FxRotation; // 이펙트 회전 = facing + FxRotation
|
float fxAngle = baseAngle + data.FxRotation; // 이펙트 회전 = facing + FxRotation
|
||||||
Vector2 boxForward = facing.normalized; // 박스 진행 방향 = facing (FxRotation 미적용)
|
Vector2 boxForward = facing.normalized; // 박스 진행 방향 = facing (FxRotation 미적용)
|
||||||
Vector2 playerPos = inventory.transform.position;
|
Vector2 playerPos = inventory.GetSpawnAnchor();
|
||||||
Vector2 fxPos = playerPos + data.OffsetXY; // 이펙트 위치
|
Vector2 fxPos = playerPos + data.OffsetXY; // 이펙트 위치
|
||||||
|
|
||||||
// 이펙트 spawn — fxPos·HitFxScale·facing+FxRotation 적용
|
// 이펙트 spawn — fxPos·HitFxScale·facing+FxRotation 적용
|
||||||
|
|
@ -101,10 +101,10 @@ namespace EerieVillage.Skills.Effectors
|
||||||
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;
|
facing = inventory.GetSpawnFacing(pc); // BT12-Dev-Clone (2026-05-15) γ
|
||||||
Vector2 forwardDir = facing.normalized; // 판정 진행 방향 = facing 만
|
Vector2 forwardDir = facing.normalized; // 판정 진행 방향 = facing 만
|
||||||
float signX = facing.x < 0f ? -1f : 1f; // OffsetDistance.x 좌/우 반전
|
float signX = facing.x < 0f ? -1f : 1f; // OffsetDistance.x 좌/우 반전
|
||||||
Vector2 origin = (Vector2)inventory.transform.position
|
Vector2 origin = (Vector2)inventory.GetSpawnAnchor()
|
||||||
+ new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y);
|
+ new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y);
|
||||||
|
|
||||||
var enemies = Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
|
var enemies = Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ namespace EerieVillage.Skills.Effectors
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
primaryPos = (Vector2)inventory.transform.position;
|
primaryPos = inventory.GetSpawnAnchor(); // BT12-Dev-Clone (2026-05-15) γ
|
||||||
}
|
}
|
||||||
// PD 정합 2026-05-13 — OffsetDistance = (X, Y) 절대 오프셋·OffsetXY = 이펙트만
|
// PD 정합 2026-05-13 — OffsetDistance = (X, Y) 절대 오프셋·OffsetXY = 이펙트만
|
||||||
Vector2 hitboxPos = primaryPos + data.OffsetDistance;
|
Vector2 hitboxPos = primaryPos + data.OffsetDistance;
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,12 @@ namespace EerieVillage.Skills.Effectors
|
||||||
{
|
{
|
||||||
var data = runtime.ActiveData;
|
var data = runtime.ActiveData;
|
||||||
// PD 지시 2026-05-13 — OffsetDistanceX = X 절대·OffsetDistance = Y 절대·OffsetXY = 이펙트만
|
// PD 지시 2026-05-13 — OffsetDistanceX = X 절대·OffsetDistance = Y 절대·OffsetXY = 이펙트만
|
||||||
Vector2 playerPos = inventory.transform.position;
|
Vector2 playerPos = inventory.GetSpawnAnchor();
|
||||||
Vector2 fxPos = playerPos + data.OffsetXY;
|
Vector2 fxPos = playerPos + data.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;
|
facing = inventory.GetSpawnFacing(pc); // BT12-Dev-Clone (2026-05-15) γ
|
||||||
|
|
||||||
// 이펙트 spawn — fxPos·HitFxScale·FxRotation·facing flip
|
// 이펙트 spawn — fxPos·HitFxScale·FxRotation·facing flip
|
||||||
GameObject fxGo = null;
|
GameObject fxGo = null;
|
||||||
|
|
@ -116,9 +116,9 @@ namespace EerieVillage.Skills.Effectors
|
||||||
// PD 지시 2026-05-13 — 박스(판정) = facing 좌/우 sign 만 · FxRotation 미적용
|
// PD 지시 2026-05-13 — 박스(판정) = facing 좌/우 sign 만 · FxRotation 미적용
|
||||||
Vector2 facing = Vector2.right;
|
Vector2 facing = Vector2.right;
|
||||||
var pc = inventory.GetComponent<PlayerController>();
|
var pc = inventory.GetComponent<PlayerController>();
|
||||||
if (pc != null) facing = pc.Facing;
|
facing = inventory.GetSpawnFacing(pc); // BT12-Dev-Clone (2026-05-15) γ
|
||||||
float signX = facing.x < 0f ? -1f : 1f;
|
float signX = facing.x < 0f ? -1f : 1f;
|
||||||
Vector2 playerPos = inventory.transform.position;
|
Vector2 playerPos = inventory.GetSpawnAnchor();
|
||||||
|
|
||||||
// 1차 판정
|
// 1차 판정
|
||||||
DoOverlapBoxAt(playerPos + new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y),
|
DoOverlapBoxAt(playerPos + new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y),
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ namespace EerieVillage.Skills.Effectors
|
||||||
|
|
||||||
EnemyController nearest = null;
|
EnemyController nearest = null;
|
||||||
float minDist = float.MaxValue;
|
float minDist = float.MaxValue;
|
||||||
Vector2 playerPos = inventory.transform.position;
|
Vector2 playerPos = inventory.GetSpawnAnchor(); // BT12-Dev-Clone (2026-05-15) γ
|
||||||
var enemies = Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
|
var enemies = Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
|
||||||
foreach (var e in enemies)
|
foreach (var e in enemies)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -20,18 +20,20 @@ namespace EerieVillage.Skills.Effectors
|
||||||
|
|
||||||
// 플레이어 위치·방향 취득
|
// 플레이어 위치·방향 취득
|
||||||
Transform playerTransform = inventory.transform;
|
Transform playerTransform = inventory.transform;
|
||||||
|
// BT12-Dev-Clone (2026-05-15) — A10 분신 anchor (γ 단계 helper)
|
||||||
|
Vector2 anchorPos = inventory.GetSpawnAnchor();
|
||||||
|
|
||||||
// PlayerController.Facing 참조
|
// PlayerController.Facing 참조
|
||||||
Vector2 facing = Vector2.right;
|
Vector2 facing = Vector2.right;
|
||||||
var pc = inventory.GetComponent<PlayerController>();
|
var pc = inventory.GetComponent<PlayerController>();
|
||||||
if (pc != null) facing = pc.Facing;
|
facing = inventory.GetSpawnFacing(pc); // BT12-Dev-Clone (2026-05-15) γ
|
||||||
// PD 지시 2026-05-13 — 게임 시작 시 Player.Facing=(0,0) 영역 fallback (정지·잔존 투사체 차단)
|
// PD 지시 2026-05-13 — 게임 시작 시 Player.Facing=(0,0) 영역 fallback (정지·잔존 투사체 차단)
|
||||||
if (facing.sqrMagnitude < 0.01f) facing = Vector2.right;
|
if (facing.sqrMagnitude < 0.01f) facing = Vector2.right;
|
||||||
|
|
||||||
// PD 지시 2026-05-14 — TargetEnemyOnFire 영역 가장 가까운 적 방향 영역 발사 (A08 저주의 화살 등)
|
// PD 지시 2026-05-14 — TargetEnemyOnFire 영역 가장 가까운 적 방향 영역 발사 (A08 저주의 화살 등)
|
||||||
if (data.TargetEnemyOnFire)
|
if (data.TargetEnemyOnFire)
|
||||||
{
|
{
|
||||||
Vector2 playerPos2 = playerTransform.position;
|
Vector2 playerPos2 = anchorPos;
|
||||||
EnemyController nearest = null;
|
EnemyController nearest = null;
|
||||||
float minDist = float.MaxValue;
|
float minDist = float.MaxValue;
|
||||||
var enemies = Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
|
var enemies = Object.FindObjectsByType<EnemyController>(FindObjectsSortMode.None);
|
||||||
|
|
@ -54,7 +56,7 @@ namespace EerieVillage.Skills.Effectors
|
||||||
|
|
||||||
// PD 정합 2026-05-13 — OffsetDistance.x = facing 방향 거리·OffsetDistance.y = 직각 거리·OffsetXY = 이펙트 절대
|
// PD 정합 2026-05-13 — OffsetDistance.x = facing 방향 거리·OffsetDistance.y = 직각 거리·OffsetXY = 이펙트 절대
|
||||||
Vector2 perpDir = new Vector2(-facing.y, facing.x);
|
Vector2 perpDir = new Vector2(-facing.y, facing.x);
|
||||||
Vector2 spawnPos = (Vector2)playerTransform.position
|
Vector2 spawnPos = (Vector2)anchorPos
|
||||||
+ facing * data.OffsetDistance.x
|
+ facing * data.OffsetDistance.x
|
||||||
+ perpDir * data.OffsetDistance.y
|
+ perpDir * data.OffsetDistance.y
|
||||||
+ data.OffsetXY;
|
+ data.OffsetXY;
|
||||||
|
|
@ -64,7 +66,7 @@ namespace EerieVillage.Skills.Effectors
|
||||||
if (data.CastFxPrefab != null)
|
if (data.CastFxPrefab != null)
|
||||||
{
|
{
|
||||||
float castAngle = Mathf.Atan2(facing.y, facing.x) * Mathf.Rad2Deg + data.ProjectileAngleOffset + data.FxRotation;
|
float castAngle = Mathf.Atan2(facing.y, facing.x) * Mathf.Rad2Deg + data.ProjectileAngleOffset + data.FxRotation;
|
||||||
var castFx = Object.Instantiate(data.CastFxPrefab, playerTransform.position, Quaternion.Euler(0f, 0f, castAngle));
|
var castFx = Object.Instantiate(data.CastFxPrefab, anchorPos, Quaternion.Euler(0f, 0f, castAngle));
|
||||||
castFx.hideFlags = HideFlags.DontSave;
|
castFx.hideFlags = HideFlags.DontSave;
|
||||||
castFx.transform.localScale *= data.HitFxScale;
|
castFx.transform.localScale *= data.HitFxScale;
|
||||||
foreach (var ps in castFx.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; ps.Play(true); }
|
foreach (var ps in castFx.GetComponentsInChildren<ParticleSystem>(true)) { var m = ps.main; m.scalingMode = ParticleSystemScalingMode.Hierarchy; ps.Play(true); }
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ 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 spawnPos = (Vector2)inventory.transform.position + data.OffsetXY;
|
Vector2 spawnPos = inventory.GetSpawnAnchor() + data.OffsetXY; // BT12-Dev-Clone (2026-05-15) γ
|
||||||
|
|
||||||
GameObject shieldGo;
|
GameObject shieldGo;
|
||||||
if (data.OnHitFxPrefab != null)
|
if (data.OnHitFxPrefab != null)
|
||||||
|
|
@ -47,7 +47,7 @@ namespace EerieVillage.Skills.Effectors
|
||||||
// PD 지시 2026-05-14 — OffsetDistance 적용 (facing sign · A05·Laser 동일 패턴)
|
// PD 지시 2026-05-14 — OffsetDistance 적용 (facing sign · A05·Laser 동일 패턴)
|
||||||
Vector2 facing = Vector2.right;
|
Vector2 facing = Vector2.right;
|
||||||
var pc = inventory.GetComponent<PlayerController>();
|
var pc = inventory.GetComponent<PlayerController>();
|
||||||
if (pc != null) facing = pc.Facing;
|
facing = inventory.GetSpawnFacing(pc); // BT12-Dev-Clone (2026-05-15) γ
|
||||||
float signX = facing.x < 0f ? -1f : 1f;
|
float signX = facing.x < 0f ? -1f : 1f;
|
||||||
Vector2 offset = new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y);
|
Vector2 offset = new Vector2(signX * data.OffsetDistance.x, data.OffsetDistance.y);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,9 @@ namespace EerieVillage.Skills
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ActiveCategory.Minion:
|
case ActiveCategory.Minion:
|
||||||
effector = new SpiritFireSpawner();
|
// BT12-Dev-Clone (2026-05-15) — A10 분신 CardId 분기 (설계 §A10-7)
|
||||||
|
if (data.CardId == "A10") effector = new CloneEffector();
|
||||||
|
else effector = new SpiritFireSpawner();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Phase 2-C~ 예정: Debuff·SpecialJudge
|
// Phase 2-C~ 예정: Debuff·SpecialJudge
|
||||||
|
|
@ -63,6 +65,10 @@ namespace EerieVillage.Skills
|
||||||
|
|
||||||
internal override void Cleanup()
|
internal override void Cleanup()
|
||||||
{
|
{
|
||||||
|
// BT12-Dev-Clone (2026-05-15) — 분신 발동 컨텍스트 reset (설계 §A10-7 채택안 (가))
|
||||||
|
// Effector.Trigger 완료 후 IsCloneFireActive=false 일관 reset
|
||||||
|
// (Simulation Event 일관 시점 — 다음 frame race 회피)
|
||||||
|
if (Inventory != null) Inventory.IsCloneFireActive = false;
|
||||||
Runtime = null;
|
Runtime = null;
|
||||||
Inventory = null;
|
Inventory = null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,11 @@ namespace EerieVillage.Skills
|
||||||
var ev = Simulation.Schedule<SkillFireEvent>();
|
var ev = Simulation.Schedule<SkillFireEvent>();
|
||||||
ev.Runtime = this;
|
ev.Runtime = this;
|
||||||
ev.Inventory = _inventory;
|
ev.Inventory = _inventory;
|
||||||
|
|
||||||
|
// BT12-Dev-Clone (2026-05-15) — A10 분신 hook 발화
|
||||||
|
// IsCloneFireActive 분기 — 분신 발동 시 OnPlayerSkillFired 발화 X (무한 재귀 방지)
|
||||||
|
if (_inventory != null && !_inventory.IsCloneFireActive)
|
||||||
|
_inventory.OnPlayerSkillFired?.Invoke(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -129,12 +134,16 @@ namespace EerieVillage.Skills
|
||||||
attrMult = v;
|
attrMult = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Mathf.RoundToInt(
|
float effective = ActiveData.BaseDamage *
|
||||||
ActiveData.BaseDamage *
|
|
||||||
stats.DamageMultiplier *
|
stats.DamageMultiplier *
|
||||||
StackLevelFactor(StackLevel) *
|
StackLevelFactor(StackLevel) *
|
||||||
attrMult
|
attrMult;
|
||||||
);
|
|
||||||
|
// BT12-Dev-Clone (2026-05-15) — A10 분신 발동 시 damage 50% 반감 (PD 명세 4번)
|
||||||
|
if (_inventory.IsCloneFireActive)
|
||||||
|
effective *= PlayerSkillInventory.CLONE_DAMAGE_MULTIPLIER;
|
||||||
|
|
||||||
|
return Mathf.RoundToInt(effective);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,46 @@ namespace EerieVillage.Skills
|
||||||
public event System.Action<EnemyKillContext> OnEnemyKilled;
|
public event System.Action<EnemyKillContext> OnEnemyKilled;
|
||||||
public event System.Action<float> OnPlayerDamaged;
|
public event System.Action<float> OnPlayerDamaged;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// BT12-Dev-Clone (2026-05-15) — A10 분신 영역 (CloneInstance·CloneEffector hook)
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>Player 스킬 발동 시 호출 (CloneInstance.EnqueuePlayerFire hook).</summary>
|
||||||
|
public event System.Action<ActiveSkillRuntime> OnPlayerSkillFired;
|
||||||
|
|
||||||
|
/// <summary>분신 발동 컨텍스트 분기 플래그. Effector 영역 anchor·facing·damage 분기 적용.</summary>
|
||||||
|
internal bool IsCloneFireActive = false;
|
||||||
|
|
||||||
|
/// <summary>분신 발동 시 anchor 위치 (분신 GameObject 위치).</summary>
|
||||||
|
internal Vector2 CloneFireOrigin = Vector2.zero;
|
||||||
|
|
||||||
|
/// <summary>분신 발동 시 facing sign (-1 = 왼쪽·+1 = 오른쪽). spawn 시점 고정 (PD 결정 2026-05-15).</summary>
|
||||||
|
internal float CloneFireFacingX = 1f;
|
||||||
|
|
||||||
|
/// <summary>PD 결정 (2026-05-15) — 분신 damage multiplier. 50% 반감.</summary>
|
||||||
|
internal const float CLONE_DAMAGE_MULTIPLIER = 0.5f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 스킬 발동 anchor 위치 반환 (BT12-Dev-Clone 2026-05-15 γ 단계).
|
||||||
|
/// IsCloneFireActive=true → 분신 위치 (CloneFireOrigin) · false → Player 위치.
|
||||||
|
/// 6 Effector 영역 일관 사용 — Player·분신 발동 위치 분기 단일 진입점.
|
||||||
|
/// </summary>
|
||||||
|
public Vector2 GetSpawnAnchor()
|
||||||
|
{
|
||||||
|
return IsCloneFireActive ? CloneFireOrigin : (Vector2)transform.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 스킬 발동 facing 반환 (BT12-Dev-Clone 2026-05-15 γ 단계).
|
||||||
|
/// IsCloneFireActive=true → 분신 facing 고정 (CloneFireFacingX) · false → Player facing.
|
||||||
|
/// 6 Effector 영역 일관 사용.
|
||||||
|
/// </summary>
|
||||||
|
public Vector2 GetSpawnFacing(PlayerController pc)
|
||||||
|
{
|
||||||
|
if (IsCloneFireActive) return new Vector2(CloneFireFacingX, 0f);
|
||||||
|
return pc != null ? pc.Facing : Vector2.right;
|
||||||
|
}
|
||||||
|
|
||||||
// PD 지시 2026-05-14 — Scene load 직후 정적 cleanup (가장 이른 진입점·Awake 보다 선행)
|
// PD 지시 2026-05-14 — Scene load 직후 정적 cleanup (가장 이른 진입점·Awake 보다 선행)
|
||||||
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.AfterSceneLoad)]
|
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||||
static void OnSceneLoadedStaticCleanup()
|
static void OnSceneLoadedStaticCleanup()
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,9 @@ namespace EerieVillage.Skills
|
||||||
// PD 지시 2026-05-13 Phase A — A08 저주의 화살·A12 정화의 빛 신규 추가
|
// PD 지시 2026-05-13 Phase A — A08 저주의 화살·A12 정화의 빛 신규 추가
|
||||||
"A08", "A12",
|
"A08", "A12",
|
||||||
// PD 지시 2026-05-13 Phase B — A06 독 늪·A11 정령불 신규 추가
|
// PD 지시 2026-05-13 Phase B — A06 독 늪·A11 정령불 신규 추가
|
||||||
"A06", "A11"
|
"A06", "A11",
|
||||||
|
// BT12-Dev-Clone (2026-05-15) — A10 분신 신규
|
||||||
|
"A10"
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
using NUnit.Framework;
|
||||||
|
using UnityEngine;
|
||||||
|
using EerieVillage.Skills;
|
||||||
|
using EerieVillage.Skills.Effectors;
|
||||||
|
|
||||||
|
namespace EerieVillage.Skills.Tests.Editor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A10 분신 스킬 EditMode 테스트 (BT12-Dev-Clone · 2026-05-15 δ 단계).
|
||||||
|
///
|
||||||
|
/// PD 명세 5항목 + PD 결정 4건 정합 검증.
|
||||||
|
/// PlayMode 의존 (Time.unscaledTime·Update) 영역은 본 EditMode에서 제외 — PD Play 검증 영역.
|
||||||
|
/// </summary>
|
||||||
|
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<Platformer.Mechanics.Health>();
|
||||||
|
var inv = go.AddComponent<PlayerSkillInventory>();
|
||||||
|
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<Platformer.Mechanics.Health>();
|
||||||
|
var inv = go.AddComponent<PlayerSkillInventory>();
|
||||||
|
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<Platformer.Mechanics.Health>();
|
||||||
|
var inv = go.AddComponent<PlayerSkillInventory>();
|
||||||
|
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<CloneInstance>();
|
||||||
|
|
||||||
|
// null runtime 영역 즉시 return — Exception X
|
||||||
|
Assert.DoesNotThrow(() => instance.EnqueuePlayerFire(null),
|
||||||
|
"null runtime hook 영역 NullReferenceException X.");
|
||||||
|
|
||||||
|
Object.DestroyImmediate(go);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c10e7e575c0e15a72ba8d8f156b126e0
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -10,11 +10,9 @@
|
||||||
],
|
],
|
||||||
"excludePlatforms": [],
|
"excludePlatforms": [],
|
||||||
"allowUnsafeCode": false,
|
"allowUnsafeCode": false,
|
||||||
"overrideReferences": true,
|
"overrideReferences": false,
|
||||||
"precompiledReferences": [
|
"precompiledReferences": [],
|
||||||
"nunit.framework.dll"
|
"autoReferenced": true,
|
||||||
],
|
|
||||||
"autoReferenced": false,
|
|
||||||
"defineConstraints": [
|
"defineConstraints": [
|
||||||
"UNITY_INCLUDE_TESTS"
|
"UNITY_INCLUDE_TESTS"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue