EerieVillage/Assets/Scripts/Skills/Effectors/CloneInstance.cs

303 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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&lt;ActiveSkillRuntime&gt;)
/// - 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 명세 — 공격 시작 딜레이 (초). PD 지시 2026-05-18: 0.5 → 0.25 (50% 단축).</summary>
public const float FIRE_DELAY_SECONDS = 0.25f;
/// <summary>PD 명세 — 분신 spawn 위치 (facing 반대 방향). PD 지시 2026-05-18: 1.0 → 0.5 (50% 단축).</summary>
public const float SPAWN_OFFSET_X = 0.5f;
/// <summary>PD 명세 — 분신 sprite alpha (반투명).</summary>
public const float SPRITE_ALPHA = 0.5f;
/// <summary>PD 지시 2026-05-18 — 분신 위치 영역 영역 영역 영역 (Player facing 변경 시 자연 영역 영역·순간 이동 X·걸어서 뒤로 가는 영역).</summary>
public const float MOVE_SPEED = 3f;
// ─────────────────────────────────────────────────────────────
// 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;
// BT12-Dev-Clone (2026-05-18) — 분신 영역 매 frame sprite 영역 + 영역 영역 영역 영역
private SpriteRenderer _cloneSr;
private SpriteRenderer _playerSr;
private Vector3 _targetLocalPos;
// 지연 큐 — 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 미부착 (무적)
// PD 지시 2026-05-18: SetParent X · 독립 GameObject (Player 영역 영역 영역 영역 영역 X)
// 영역 영역 영역 = 매 frame Player worldPos + offset 영역 영역 (SyncSpriteAndPosition).
var go = new GameObject("Clone_A10");
go.hideFlags = HideFlags.DontSave; // Scene 오염 방지 (BT12-Dev SOT 정합)
var playerSr = playerInventory.GetComponentInChildren<SpriteRenderer>();
// SetParent X · 독립 GameObject (Player.localScale 영역 영역 영역 영역 영역 영역 분신 영역 영역 영역 영역)
go.transform.position = playerPos + new Vector2(-signX * SPAWN_OFFSET_X, 0f);
// PD 지시 2026-05-18 — 분신 크기 = Player 동일 (PlayerSr lossyScale 영역 동일·SetParent X 영역 lossyScale = localScale).
go.transform.localScale = playerSr != null ? playerSr.transform.lossyScale : Vector3.one;
go.transform.rotation = Quaternion.identity;
SpriteRenderer cloneSr = null;
if (playerSr != null)
{
cloneSr = go.AddComponent<SpriteRenderer>();
cloneSr.sprite = playerSr.sprite;
cloneSr.flipX = playerSr.flipX;
cloneSr.sortingOrder = playerSr.sortingOrder - 1; // Player 뒤쪽 sorting
Color c = playerSr.color;
c.a = SPRITE_ALPHA; // PD 명세 — 반투명
cloneSr.color = c;
}
// PD 결정 — Collider 미부착 (무적·무충돌). Rigidbody2D도 미부착 (gravity·물리 차단).
// 5. CloneInstance 컴포넌트 부착 + 상태 초기화
var instance = go.AddComponent<CloneInstance>();
instance._playerInventory = playerInventory;
instance._spawnFacingX = signX;
instance._spawnTime = Time.unscaledTime;
// BT12-Dev-Clone (2026-05-18) — sprite 영역 + 영역 영역 영역 영역 (worldPos 기반)
instance._cloneSr = cloneSr;
instance._playerSr = playerSr;
instance._targetLocalPos = go.transform.position; // worldPos 영역 영역
_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;
}
// PD 지시 2026-05-18 — Player 영역 영역 영역 영역 분신 즉시 destroy (영역 영역 영역 X)
if (_playerInventory == null)
{
Destroy(gameObject);
return;
}
var playerHealth = _playerInventory.GetComponent<Health>();
if (playerHealth != null && !playerHealth.IsAlive)
{
Destroy(gameObject);
return;
}
// BT12-Dev-Clone (2026-05-18) — 매 frame Player sprite 동기화 + 위치 영역 영역 영역
SyncSpriteAndPosition();
// 지연 큐 dequeue — TriggerTime 도달 항목 발동
while (_pendingQueue.Count > 0 && _pendingQueue.Peek().TriggerTime <= Time.unscaledTime)
{
var pending = _pendingQueue.Dequeue();
FireAtClonePosition(pending.Runtime);
}
}
// ─────────────────────────────────────────────────────────────
// 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 영역·달리는 영역 영역 영역 영역).
if (_cloneSr != null && _playerSr != null)
{
_cloneSr.sprite = _playerSr.sprite;
}
// 2. PD 지시 2026-05-18 — 목표 위치 영역 = Player worldPos + facing 반대 0.5 영역 (50% 영역)
// SetParent X · 독립 GameObject 영역 — Player.localScale 영역 영역 영역 영역 영역 영역 영역.
// PD 지시 2026-05-18 — _spawnFacingX 매 frame 영역 영역 (Player facing 영역) → 분신 발동 시 투사체 영역 정합 영역.
Vector3 playerWorldPos = _playerInventory.transform.position;
var pc = _playerInventory.GetComponent<PlayerController>();
if (pc != null)
{
Vector2 facing = pc.Facing;
if (facing.sqrMagnitude > 0.01f)
_spawnFacingX = facing.x < 0f ? -1f : 1f; // 매 frame Player facing 영역 영역 갱신
}
float signX = _spawnFacingX;
_targetLocalPos = playerWorldPos + new Vector3(-signX * SPAWN_OFFSET_X, 0f, 0f); // worldPos target
// 3. 영역 영역 영역 영역 영역 영역 영역 (MoveTowards · worldPos 기반)
Vector3 currentPos = transform.position;
Vector3 newPos = Vector3.MoveTowards(currentPos, _targetLocalPos, MOVE_SPEED * Time.deltaTime);
// 4. PD 지시 2026-05-18 — 분신 flipX 영역 = Player.flipX 영역 즉시 동조 (deltaX 영역 영역 폐기·영역 영역 영역 영역 영역 영역 영역)
// PlayerController 영역 영역 영역 (L306·311) = spriteRenderer.flipX 변경 영역 영역.
// 분신 sprite + flipX 영역 영역 Player 영역 영역 동조 → 분신 영역 영역 영역 영역 영역 영역 영역 영역 영역.
// 자연 영역 영역 = 분신 영역 영역 영역 (MoveTowards) 영역 영역 영역.
if (_cloneSr != null && _playerSr != null)
{
_cloneSr.flipX = _playerSr.flipX;
}
transform.position = newPos;
}
// ─────────────────────────────────────────────────────────────
// 분신 위치 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;
}
}
}