2026-05-18 00:39:46 +00:00
|
|
|
|
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;
|
|
|
|
|
|
|
2026-05-18 10:13:30 +00:00
|
|
|
|
/// <summary>PD 명세 — 공격 시작 딜레이 (초). PD 지시 2026-05-18: 0.5 → 0.25 (50% 단축).</summary>
|
|
|
|
|
|
public const float FIRE_DELAY_SECONDS = 0.25f;
|
2026-05-18 00:39:46 +00:00
|
|
|
|
|
|
|
|
|
|
/// <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;
|
|
|
|
|
|
|
2026-05-18 10:05:57 +00:00
|
|
|
|
/// <summary>PD 지시 2026-05-18 — 분신 위치 영역 영역 영역 영역 (Player facing 변경 시 자연 영역 영역·순간 이동 X·걸어서 뒤로 가는 영역).</summary>
|
|
|
|
|
|
public const float MOVE_SPEED = 3f;
|
|
|
|
|
|
|
2026-05-18 00:39:46 +00:00
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
// Singleton (PD 결정 — 분신 1기 유지·재발동 시 기존 destroy + 새 spawn)
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
private static CloneInstance _current;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>현재 활성 분신 인스턴스 (테스트·진단용 · null = 분신 없음).</summary>
|
|
|
|
|
|
public static CloneInstance Current => _current;
|
|
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 상태 필드
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
private PlayerSkillInventory _playerInventory;
|
2026-05-18 10:05:57 +00:00
|
|
|
|
// spawn 시점 Player facing.x sign (PD 결정 — 분신 발동 facing 고정·시각 영역 영역 영역 영역)
|
2026-05-18 00:39:46 +00:00
|
|
|
|
private float _spawnFacingX;
|
|
|
|
|
|
// spawn 시각 (lifetime 자동 소멸용 · timeScale 무관 unscaledTime)
|
|
|
|
|
|
private float _spawnTime;
|
|
|
|
|
|
|
2026-05-18 10:05:57 +00:00
|
|
|
|
// BT12-Dev-Clone (2026-05-18) — 분신 영역 매 frame sprite 영역 + 영역 영역 영역 영역
|
|
|
|
|
|
private SpriteRenderer _cloneSr;
|
|
|
|
|
|
private SpriteRenderer _playerSr;
|
|
|
|
|
|
private Vector3 _targetLocalPos;
|
|
|
|
|
|
|
2026-05-18 00:39:46 +00:00
|
|
|
|
// 지연 큐 — 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 미부착 (무적)
|
2026-05-18 06:56:10 +00:00
|
|
|
|
// PD 지시 2026-05-18: Player 자식 부착 (Player 이동 시 분신 자동 동조) + scale 영역 Player 동일 (크기 정합)
|
2026-05-18 00:39:46 +00:00
|
|
|
|
var go = new GameObject("Clone_A10");
|
|
|
|
|
|
go.hideFlags = HideFlags.DontSave; // Scene 오염 방지 (BT12-Dev SOT 정합)
|
|
|
|
|
|
|
|
|
|
|
|
var playerSr = playerInventory.GetComponentInChildren<SpriteRenderer>();
|
2026-05-18 06:56:10 +00:00
|
|
|
|
|
|
|
|
|
|
// Player 자식 부착 (worldPositionStays=false) + localPosition 영역 facing 반대 1유닛
|
|
|
|
|
|
go.transform.SetParent(playerInventory.transform, false);
|
|
|
|
|
|
go.transform.localPosition = new Vector3(-signX * SPAWN_OFFSET_X, 0f, 0f);
|
2026-05-18 10:20:40 +00:00
|
|
|
|
// PD 지시 2026-05-18 — 분신 크기 = Player 동일.
|
|
|
|
|
|
// PlayerSr 영역 자식 구조 영역 영역 영역 영역 — PlayerSr.lossyScale 영역 동일 영역 영역 localScale 영역 정정.
|
|
|
|
|
|
// 분신 lossyScale = playerInventory.lossyScale * localScale → PlayerSr.lossyScale 동일 영역 = srLossy / playerInvLossy.
|
|
|
|
|
|
if (playerSr != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Vector3 playerInvLossy = playerInventory.transform.lossyScale;
|
|
|
|
|
|
Vector3 srLossy = playerSr.transform.lossyScale;
|
|
|
|
|
|
go.transform.localScale = new Vector3(
|
|
|
|
|
|
Mathf.Abs(playerInvLossy.x) > 0.0001f ? srLossy.x / playerInvLossy.x : 1f,
|
|
|
|
|
|
Mathf.Abs(playerInvLossy.y) > 0.0001f ? srLossy.y / playerInvLossy.y : 1f,
|
|
|
|
|
|
Mathf.Abs(playerInvLossy.z) > 0.0001f ? srLossy.z / playerInvLossy.z : 1f
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
go.transform.localScale = Vector3.one;
|
|
|
|
|
|
}
|
2026-05-18 06:56:10 +00:00
|
|
|
|
go.transform.localRotation = Quaternion.identity;
|
|
|
|
|
|
|
2026-05-18 10:05:57 +00:00
|
|
|
|
SpriteRenderer cloneSr = null;
|
2026-05-18 00:39:46 +00:00
|
|
|
|
if (playerSr != null)
|
|
|
|
|
|
{
|
2026-05-18 10:05:57 +00:00
|
|
|
|
cloneSr = go.AddComponent<SpriteRenderer>();
|
|
|
|
|
|
cloneSr.sprite = playerSr.sprite;
|
|
|
|
|
|
cloneSr.flipX = playerSr.flipX;
|
|
|
|
|
|
cloneSr.sortingOrder = playerSr.sortingOrder - 1; // Player 뒤쪽 sorting
|
2026-05-18 00:39:46 +00:00
|
|
|
|
Color c = playerSr.color;
|
2026-05-18 10:05:57 +00:00
|
|
|
|
c.a = SPRITE_ALPHA; // PD 명세 — 반투명
|
|
|
|
|
|
cloneSr.color = c;
|
2026-05-18 00:39:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PD 결정 — Collider 미부착 (무적·무충돌). Rigidbody2D도 미부착 (gravity·물리 차단).
|
|
|
|
|
|
|
|
|
|
|
|
// 5. CloneInstance 컴포넌트 부착 + 상태 초기화
|
|
|
|
|
|
var instance = go.AddComponent<CloneInstance>();
|
|
|
|
|
|
instance._playerInventory = playerInventory;
|
|
|
|
|
|
instance._spawnFacingX = signX;
|
|
|
|
|
|
instance._spawnTime = Time.unscaledTime;
|
2026-05-18 10:05:57 +00:00
|
|
|
|
// BT12-Dev-Clone (2026-05-18) — sprite 영역 + 영역 영역 영역 영역
|
|
|
|
|
|
instance._cloneSr = cloneSr;
|
|
|
|
|
|
instance._playerSr = playerSr;
|
|
|
|
|
|
instance._targetLocalPos = go.transform.localPosition;
|
2026-05-18 00:39:46 +00:00
|
|
|
|
_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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 10:05:57 +00:00
|
|
|
|
// BT12-Dev-Clone (2026-05-18) — 매 frame Player sprite 동기화 + 위치 영역 영역 영역
|
|
|
|
|
|
SyncSpriteAndPosition();
|
|
|
|
|
|
|
2026-05-18 00:39:46 +00:00
|
|
|
|
// 지연 큐 dequeue — TriggerTime 도달 항목 발동
|
|
|
|
|
|
while (_pendingQueue.Count > 0 && _pendingQueue.Peek().TriggerTime <= Time.unscaledTime)
|
|
|
|
|
|
{
|
|
|
|
|
|
var pending = _pendingQueue.Dequeue();
|
|
|
|
|
|
FireAtClonePosition(pending.Runtime);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 10:05:57 +00:00
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
// BT12-Dev-Clone (2026-05-18) — 매 frame Player 동조
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
private void SyncSpriteAndPosition()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_playerInventory == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. PD 지시 2026-05-18 — sprite 영역 매 frame Player 영역 동조 (Animator frame·flipX 영역 영역)
|
|
|
|
|
|
if (_cloneSr != null && _playerSr != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_cloneSr.sprite = _playerSr.sprite;
|
|
|
|
|
|
_cloneSr.flipX = _playerSr.flipX;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. PD 지시 2026-05-18 — Player facing 변경 시 분신 위치 영역 영역 영역 영역 (순간 이동 X·걸어서 뒤로 가는 영역)
|
|
|
|
|
|
var pc = _playerInventory.GetComponent<PlayerController>();
|
|
|
|
|
|
if (pc != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Vector2 facing = pc.Facing;
|
|
|
|
|
|
if (facing.sqrMagnitude > 0.01f)
|
|
|
|
|
|
{
|
|
|
|
|
|
float signX = facing.x < 0f ? -1f : 1f;
|
|
|
|
|
|
_targetLocalPos = new Vector3(-signX * SPAWN_OFFSET_X, 0f, 0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 영역 영역 영역 영역 영역 영역 영역 (MoveTowards · MOVE_SPEED 영역 영역)
|
|
|
|
|
|
transform.localPosition = Vector3.MoveTowards(transform.localPosition, _targetLocalPos, MOVE_SPEED * Time.deltaTime);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 00:39:46 +00:00
|
|
|
|
// ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
// 분신 위치 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 채택안 (가))
|
2026-05-18 06:13:02 +00:00
|
|
|
|
var ev = Schedule<SkillFireEvent>(); // parent namespace EerieVillage.Skills 자동 인식
|
2026-05-18 00:39:46 +00:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|