Files
Colosseum/Assets/_Game/Scripts/Player/PlayerAbnormalityDebugHUD.cs

447 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;
using Unity.Netcode;
using Colosseum.Abnormalities;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Colosseum.Player
{
/// <summary>
/// 로컬 플레이어가 자신에게 이상상태를 적용/해제할 수 있는 디버그 HUD.
/// 이상상태 이름, 에셋 이름, 인덱스로 검색해 적용할 수 있습니다.
/// </summary>
[DisallowMultipleComponent]
public class PlayerAbnormalityDebugHUD : NetworkBehaviour
{
[Header("References")]
[Tooltip("이상상태 관리자")]
[SerializeField] private AbnormalityManager abnormalityManager;
[Tooltip("플레이어 네트워크 상태")]
[SerializeField] private PlayerNetworkController networkController;
[Header("Display")]
[Tooltip("시작 시 HUD 표시 여부")]
[SerializeField] private bool showOnStart = false;
[Tooltip("로그 출력 여부")]
[SerializeField] private bool debugLogs = true;
[Header("Catalog")]
[Tooltip("디버그 HUD에서 검색 가능한 이상상태 목록")]
[SerializeField] private List<AbnormalityData> abnormalityCatalog = new List<AbnormalityData>();
private Rect windowRect = new Rect(20f, 20f, 420f, 520f);
private Vector2 catalogScroll;
private string abnormalityInput = string.Empty;
private string statusMessage = "이상상태 이름, 에셋 이름, 인덱스를 입력하세요.";
private bool isVisible;
private InputSystem_Actions inputActions;
private void Awake()
{
if (abnormalityManager == null)
abnormalityManager = GetComponent<AbnormalityManager>();
if (networkController == null)
networkController = GetComponent<PlayerNetworkController>();
}
public override void OnNetworkSpawn()
{
if (!IsOwner || !ShouldEnableDebugHud())
{
enabled = false;
return;
}
isVisible = showOnStart;
RefreshCatalog();
InitializeInputActions();
if (debugLogs)
{
Debug.Log("[AbnormalityDebugHUD] DebugHUD 액션으로 HUD를 열고, 이상상태 이름/에셋명/인덱스로 자신에게 적용할 수 있습니다.");
}
}
public override void OnNetworkDespawn()
{
CleanupInputActions();
}
private void OnGUI()
{
if (!IsOwner || !isVisible || !ShouldEnableDebugHud())
return;
windowRect = GUI.Window(GetInstanceID(), windowRect, DrawWindow, "Abnormality Debug HUD");
}
private void DrawWindow(int windowId)
{
GUILayout.BeginVertical();
GUILayout.Label($"사망 상태: {(networkController != null && networkController.IsDead ? "Dead" : "Alive")}");
if (TryGetComponent<PlayerActionState>(out var actionState))
{
GUILayout.Label($"무적 상태: {(actionState.IsDamageImmune ? "Immune" : "Normal")} / 이동:{actionState.CanMove} / 스킬:{actionState.CanUseSkills}");
}
GUILayout.Label("입력 예시: 기절 / Data_Abnormality_Player_Stun / 0");
GUI.SetNextControlName("AbnormalityInputField");
abnormalityInput = GUILayout.TextField(abnormalityInput ?? string.Empty);
GUILayout.BeginHorizontal();
if (GUILayout.Button("적용", GUILayout.Height(28f)))
{
ApplyFromInput();
}
if (GUILayout.Button("해제", GUILayout.Height(28f)))
{
RemoveFromInput();
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
if (GUILayout.Button("모두 해제", GUILayout.Height(24f)))
{
if (abnormalityManager != null)
{
abnormalityManager.RemoveAllAbnormalities();
SetStatus("활성 이상상태를 모두 해제했습니다.");
}
}
if (GUILayout.Button("즉사", GUILayout.Height(24f)))
{
KillSelf();
}
if (GUILayout.Button("리스폰", GUILayout.Height(24f)))
{
RequestRespawnRpc();
}
GUILayout.EndHorizontal();
GUILayout.Space(6f);
GUILayout.Label($"상태: {statusMessage}");
GUILayout.Space(6f);
GUILayout.Label("활성 이상상태");
DrawActiveAbnormalities();
GUILayout.Space(6f);
GUILayout.Label("카탈로그");
catalogScroll = GUILayout.BeginScrollView(catalogScroll, GUILayout.Height(220f));
for (int i = 0; i < abnormalityCatalog.Count; i++)
{
AbnormalityData data = abnormalityCatalog[i];
if (data == null)
continue;
GUILayout.BeginHorizontal();
GUILayout.Label($"[{i}] {data.abnormalityName} ({data.name})", GUILayout.Width(280f));
if (GUILayout.Button("적용", GUILayout.Width(50f)))
{
ApplyAbnormality(data);
}
if (GUILayout.Button("해제", GUILayout.Width(50f)))
{
RemoveAbnormality(data);
}
GUILayout.EndHorizontal();
}
GUILayout.EndScrollView();
GUILayout.EndVertical();
GUI.DragWindow(new Rect(0f, 0f, 10000f, 20f));
}
private void OnEnable()
{
if (IsOwner && inputActions != null)
{
inputActions.Player.Enable();
}
}
private void OnDisable()
{
CleanupInputActions();
}
private void InitializeInputActions()
{
if (inputActions == null)
{
inputActions = new InputSystem_Actions();
inputActions.Player.DebugHUD.performed += OnDebugHudPerformed;
}
inputActions.Player.Enable();
}
private void CleanupInputActions()
{
if (inputActions != null)
{
inputActions.Player.Disable();
}
}
private void OnDebugHudPerformed(InputAction.CallbackContext context)
{
if (!IsOwner)
return;
isVisible = !isVisible;
}
private void DrawActiveAbnormalities()
{
if (abnormalityManager == null || abnormalityManager.ActiveAbnormalities.Count == 0)
{
GUILayout.Label("- 없음");
return;
}
for (int i = 0; i < abnormalityManager.ActiveAbnormalities.Count; i++)
{
var active = abnormalityManager.ActiveAbnormalities[i];
if (active == null || active.Data == null)
continue;
string durationText = active.Data.IsPermanent ? "영구" : $"{active.RemainingDuration:F1}s";
GUILayout.Label($"- {active.Data.abnormalityName} / {durationText}");
}
}
private void ApplyFromInput()
{
if (!TryResolveAbnormality(abnormalityInput, out AbnormalityData data, out string message))
{
SetStatus(message);
return;
}
ApplyAbnormality(data);
}
private void RemoveFromInput()
{
if (!TryResolveAbnormality(abnormalityInput, out AbnormalityData data, out string message))
{
SetStatus(message);
return;
}
RemoveAbnormality(data);
}
private void ApplyAbnormality(AbnormalityData data)
{
if (data == null)
{
SetStatus("적용할 이상상태를 찾지 못했습니다.");
return;
}
if (abnormalityManager == null)
{
SetStatus("AbnormalityManager 참조가 없습니다.");
return;
}
abnormalityManager.ApplyAbnormality(data, gameObject);
SetStatus($"'{data.abnormalityName}' 적용 요청을 보냈습니다.");
}
private void RemoveAbnormality(AbnormalityData data)
{
if (data == null)
{
SetStatus("해제할 이상상태를 찾지 못했습니다.");
return;
}
if (abnormalityManager == null)
{
SetStatus("AbnormalityManager 참조가 없습니다.");
return;
}
abnormalityManager.RemoveAbnormality(data);
SetStatus($"'{data.abnormalityName}' 해제 요청을 보냈습니다.");
}
private bool TryResolveAbnormality(string input, out AbnormalityData resolved, out string message)
{
resolved = null;
message = string.Empty;
string normalizedInput = NormalizeIdentifier(input);
if (string.IsNullOrWhiteSpace(normalizedInput))
{
message = "이상상태 이름, 에셋 이름, 인덱스를 입력하세요.";
return false;
}
if (int.TryParse(normalizedInput, out int index))
{
if (index >= 0 && index < abnormalityCatalog.Count && abnormalityCatalog[index] != null)
{
resolved = abnormalityCatalog[index];
return true;
}
message = $"인덱스 {index} 에 해당하는 이상상태가 없습니다.";
return false;
}
List<AbnormalityData> partialMatches = new List<AbnormalityData>();
for (int i = 0; i < abnormalityCatalog.Count; i++)
{
AbnormalityData candidate = abnormalityCatalog[i];
if (candidate == null)
continue;
string normalizedName = NormalizeIdentifier(candidate.abnormalityName);
string normalizedAssetName = NormalizeIdentifier(candidate.name);
if (normalizedInput == normalizedName || normalizedInput == normalizedAssetName)
{
resolved = candidate;
return true;
}
if (normalizedName.Contains(normalizedInput) || normalizedAssetName.Contains(normalizedInput))
{
partialMatches.Add(candidate);
}
}
if (partialMatches.Count == 1)
{
resolved = partialMatches[0];
return true;
}
if (partialMatches.Count > 1)
{
message = BuildAmbiguousMessage(partialMatches);
return false;
}
message = $"'{input}' 에 해당하는 이상상태를 찾지 못했습니다.";
return false;
}
private string BuildAmbiguousMessage(List<AbnormalityData> matches)
{
int previewCount = Mathf.Min(3, matches.Count);
List<string> previewNames = new List<string>(previewCount);
for (int i = 0; i < previewCount; i++)
{
AbnormalityData match = matches[i];
previewNames.Add(match != null ? match.abnormalityName : "null");
}
string preview = string.Join(", ", previewNames);
if (matches.Count > previewCount)
{
preview += ", ...";
}
return $"여러 후보가 있습니다: {preview}";
}
private string NormalizeIdentifier(string value)
{
return string.IsNullOrWhiteSpace(value)
? string.Empty
: value.Trim().Replace(" ", string.Empty).ToLowerInvariant();
}
private void SetStatus(string message)
{
statusMessage = message;
if (debugLogs)
{
Debug.Log($"[AbnormalityDebugHUD] {message}");
}
}
private void KillSelf()
{
if (networkController == null)
{
SetStatus("PlayerNetworkController 참조가 없습니다.");
return;
}
if (networkController.IsDead)
{
SetStatus("이미 사망한 상태입니다.");
return;
}
networkController.TakeDamageRpc(networkController.Health + 1f);
SetStatus("즉사 요청을 보냈습니다.");
}
[Rpc(SendTo.Server)]
private void RequestRespawnRpc()
{
if (networkController == null)
return;
networkController.Respawn();
}
private bool ShouldEnableDebugHud()
{
#if UNITY_EDITOR
return true;
#else
return Debug.isDebugBuild;
#endif
}
private void RefreshCatalog()
{
abnormalityCatalog.RemoveAll(data => data == null);
#if UNITY_EDITOR
string[] guids = AssetDatabase.FindAssets("t:AbnormalityData", new[] { "Assets/_Game/Data/Abnormalities" });
List<AbnormalityData> loadedAssets = new List<AbnormalityData>(guids.Length);
for (int i = 0; i < guids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
AbnormalityData data = AssetDatabase.LoadAssetAtPath<AbnormalityData>(path);
if (data != null)
{
loadedAssets.Add(data);
}
}
loadedAssets.Sort((left, right) =>
string.Compare(left != null ? left.name : string.Empty, right != null ? right.name : string.Empty, StringComparison.OrdinalIgnoreCase));
abnormalityCatalog = loadedAssets;
#endif
}
}
}