using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Platformer.Gameplay;
using static Platformer.Core.Simulation;
using Platformer.Model;
using Platformer.Core;
using UnityEngine.InputSystem;
namespace Platformer.Mechanics
{
///
/// This is the main class used to implement control of the player.
/// It is a superset of the AnimationController class, but is inlined to allow for any kind of customisation.
///
public class PlayerController : KinematicObject
{
public AudioClip jumpAudio;
public AudioClip respawnAudio;
public AudioClip ouchAudio;
///
/// Attack sound effect. 자동 발동 시 PlayerAttack.Execute가 PlayOneShot으로 재생.
/// BT7-Plan 2026-04-24 — VS 순수형 자동 발동 전환 후에도 audio hook 유지.
///
public AudioClip attackAudio;
///
/// Max horizontal speed of the player.
///
public float maxSpeed = 7;
///
/// Initial jump velocity at the start of a jump.
///
public float jumpTakeOffSpeed = 7;
///
/// Attack hitbox component (자동 GetComponent, 없으면 null — PlayerAttack.Execute에서 null 체크).
/// 공격은 PlayerAttackTicker가 주기적으로 Schedule을 발화하여 실행.
///
public AttackHitbox attackHitbox;
public JumpState jumpState = JumpState.Grounded;
private bool stopJump;
/*internal new*/ public Collider2D collider2d;
/*internal new*/ public AudioSource audioSource;
public Health health;
public bool controlEnabled = true;
bool jump;
Vector2 move;
SpriteRenderer spriteRenderer;
internal Animator animator;
readonly PlatformerModel model = Simulation.GetModel();
private InputAction m_MoveAction;
private InputAction m_JumpAction;
// 현재 facing 방향 (마지막 이동 입력 기반, 정지 시 이전 값 유지).
// BT7-Plan 2026-04-24 — PlayerAttackTicker가 자동 발동 시 참조하므로 public 노출.
Vector2 facing = Vector2.right;
/// 현재 플레이어 facing 방향. PlayerAttackTicker가 Schedule 시점에 참조.
public Vector2 Facing => facing;
public Bounds Bounds => collider2d.bounds;
///
/// 마지막 grounded 위치 — 낙사 시 안전 복귀 영역 (PD 지시 2026-05-07).
///
public Vector3 LastGroundedPosition { get; private set; }
void Awake()
{
health = GetComponent();
audioSource = GetComponent();
collider2d = GetComponent();
spriteRenderer = GetComponent();
if (spriteRenderer == null) spriteRenderer = GetComponentInChildren();
animator = GetComponent();
if (animator == null) animator = GetComponentInChildren();
// PD 지시 2026-05-07 — 동반 컴포넌트 자동 부착 (Inspector 부착 불요)
if (GetComponent() == null) gameObject.AddComponent();
if (GetComponent() == null) gameObject.AddComponent();
// BT5-Dev #34 — PlatformDropThrough 폐기 (PlatformEffector2D 표준 패턴 활용)
var oldDrop = GetComponent();
if (oldDrop != null) Destroy(oldDrop);
// PD 지시 2026-05-07 — 사망 시 입력 차단 / 부활 시 입력 복원
if (health != null)
{
health.OnDeathEvent += OnHealthDeath;
health.OnResurrectEvent += OnHealthResurrect;
}
if (attackHitbox == null) attackHitbox = GetComponent();
m_MoveAction = InputSystem.actions.FindAction("Player/Move");
m_JumpAction = InputSystem.actions.FindAction("Player/Jump");
m_MoveAction.Enable();
m_JumpAction.Enable();
LastGroundedPosition = transform.position;
}
void OnDestroy()
{
if (health != null)
{
health.OnDeathEvent -= OnHealthDeath;
health.OnResurrectEvent -= OnHealthResurrect;
}
}
// BT5-Dev #30 — 발판 GameObject 영역 식별 진단 (PD 영역 영역 영역 부딪힘 시 Console 출력)
void OnCollisionEnter2D(Collision2D col)
{
if (col.gameObject == null) return;
int layer = col.gameObject.layer;
string name = col.gameObject.name;
Debug.Log($"[BT30-Collide] name='{name}' layer={layer} (8=JumpThrough, 0=Default)");
}
void OnHealthDeath()
{
controlEnabled = false;
move = Vector2.zero;
}
void OnHealthResurrect()
{
controlEnabled = true;
}
protected override void Update()
{
if (controlEnabled)
{
move.x = m_MoveAction.ReadValue().x;
if (jumpState == JumpState.Grounded && m_JumpAction.WasPressedThisFrame())
jumpState = JumpState.PrepareToJump;
else if (m_JumpAction.WasReleasedThisFrame())
{
stopJump = true;
Schedule().player = this;
}
// 공격은 PlayerAttackTicker가 자동 주기로 Schedule을 발화 (BT7-Plan VS 순수형).
}
else
{
move.x = 0;
}
UpdateJumpState();
base.Update();
// PD 지시 2026-05-07 — 낙사 시 복귀할 안전 위치 추적
if (IsGrounded) LastGroundedPosition = transform.position;
}
// BT5-Dev #39 — 점프 시작 시 Coroutine으로 0.3초 동안 Layer 16 통과 유지 (Physics step 지연 차단)
const int JUMP_THROUGH_LAYER = 16;
bool _jumpThroughActive;
void UpdateJumpState()
{
jump = false;
switch (jumpState)
{
case JumpState.PrepareToJump:
jumpState = JumpState.Jumping;
jump = true;
stopJump = false;
// BT39 — 점프 시작 시 즉시 IgnoreLayerCollision 활성 + Coroutine으로 0.3초 유지
if (!_jumpThroughActive) StartCoroutine(JumpThroughRoutine());
break;
case JumpState.Jumping:
if (!IsGrounded)
{
Schedule().player = this;
jumpState = JumpState.InFlight;
}
break;
case JumpState.InFlight:
if (IsGrounded)
{
Schedule().player = this;
jumpState = JumpState.Landed;
}
break;
case JumpState.Landed:
jumpState = JumpState.Grounded;
break;
}
}
System.Collections.IEnumerator JumpThroughRoutine()
{
_jumpThroughActive = true;
Physics2D.IgnoreLayerCollision(13, JUMP_THROUGH_LAYER, true);
yield return new WaitForSeconds(0.3f);
Physics2D.IgnoreLayerCollision(13, JUMP_THROUGH_LAYER, false);
_jumpThroughActive = false;
}
protected override void ComputeVelocity()
{
if (jump && IsGrounded)
{
velocity.y = jumpTakeOffSpeed * model.jumpModifier;
jump = false;
}
else if (stopJump)
{
stopJump = false;
if (velocity.y > 0)
{
velocity.y = velocity.y * model.jumpDeceleration;
}
}
if (move.x > 0.01f)
{
spriteRenderer.flipX = true;
facing = Vector2.right;
}
else if (move.x < -0.01f)
{
spriteRenderer.flipX = false;
facing = Vector2.left;
}
animator.SetBool("grounded", IsGrounded);
animator.SetFloat("velocityX", Mathf.Abs(velocity.x) / maxSpeed);
targetVelocity = move * maxSpeed;
}
public enum JumpState
{
Grounded,
PrepareToJump,
Jumping,
InFlight,
Landed
}
}
}