#if UNITY_EDITOR || DEVELOPMENT_BUILD using System; using System.Linq; using Components; using CustomEvent; using DataTable; using Definition.DataStruct; using Entity; using CustomUtility; using Procedure; using UnityEngine; using UnityGameFramework.Runtime; #if ENABLE_INPUT_SYSTEM using UnityEngine.InputSystem; #endif namespace CustomComponent { public class RuntimeDebugPanelComponent : GameFrameworkComponent { private const float MinSpawnRate = 0.1f; private const float CornerTapWindow = 0.6f; private const int RequiredCornerTapCount = 3; private const int DebugHealAmount = 200; [Header("Window Content")] [SerializeField] private bool _showBuffSection = true; [SerializeField] private bool _showBattleOverview = true; [SerializeField] private bool _showCollisionStats = true; [SerializeField] private bool _showSpawnControls = true; [SerializeField] private bool _showBattleDurationControls = true; [SerializeField] private bool _showSeparationSolverControls = true; [SerializeField] private bool _showPlayerWeaponControls = true; [SerializeField] private bool _showPlayerHealthControls = true; [SerializeField] private bool _showTips = true; private Rect _windowRect = new Rect(20f, 60f, 460f, 800f); private bool _isPanelVisible; private int _windowId; private string _searchText = string.Empty; private int _selectedIndex; private int _addCount = 1; private Vector2 _buffScroll; private float _spawnRateScaleInput = 1f; private float _extendDurationSeconds = 30f; private DRProp[] _allProps = Array.Empty(); private DRProp[] _filteredProps = Array.Empty(); private string[] _displayNames = Array.Empty(); private int _cornerTapCount; private float _lastCornerTapTime = -10f; private bool _lockPlayerHealthToMax; protected override void Awake() { base.Awake(); _windowId = GetInstanceID(); } private void Update() { if (IsTogglePressed()) { _isPanelVisible = !_isPanelVisible; } HandleCornerTapGesture(); if (_lockPlayerHealthToMax) { KeepPlayerHealthAtMax(); } } private void OnGUI() { DrawToggleButton(); if (!_isPanelVisible) return; _windowRect = GUI.Window(_windowId, _windowRect, DrawWindow, "Runtime Debug Panel"); } private void DrawToggleButton() { const float width = 64f; const float height = 30f; Rect buttonRect = new Rect(Screen.width - width - 12f, 10f, width, height); if (GUI.Button(buttonRect, "DEBUG")) { _isPanelVisible = !_isPanelVisible; } } private void DrawWindow(int windowId) { if (_showBuffSection) { EnsurePropList(); } GUILayout.BeginVertical(); bool hasPreviousSection = false; if (_showBuffSection) { DrawBuffSection(); hasPreviousSection = true; } if (HasVisibleBattleSection()) { if (hasPreviousSection) { GUILayout.Space(8f); GUILayout.Label(string.Empty, GUI.skin.horizontalSlider); GUILayout.Space(8f); } DrawBattleSection(); hasPreviousSection = true; } if (_showTips) { if (hasPreviousSection) { GUILayout.Space(8f); } GUILayout.Label("Tips: press `F8` or tap top-left corner 3 times to toggle.", GUILayout.Height(20f)); } GUILayout.EndVertical(); GUI.DragWindow(new Rect(0, 0, 10000, 22)); } private void DrawBuffSection() { GUILayout.Label("Buff Debug"); GUILayout.BeginHorizontal(); GUILayout.Label("Search", GUILayout.Width(52f)); _searchText = GUILayout.TextField(_searchText); if (GUILayout.Button("Refresh", GUILayout.Width(80f))) { EnsurePropList(true); } GUILayout.EndHorizontal(); ApplyFilter(_searchText); if (_displayNames.Length == 0) { GUILayout.Label("No Buff data. Enter battle and try again."); return; } _selectedIndex = Mathf.Clamp(_selectedIndex, 0, _displayNames.Length - 1); _buffScroll = GUILayout.BeginScrollView(_buffScroll, GUILayout.Height(120f)); _selectedIndex = GUILayout.SelectionGrid(_selectedIndex, _displayNames, 1); GUILayout.EndScrollView(); DRProp selectedProp = GetSelectedProp(); if (selectedProp == null) return; GUILayout.Label($"Selected: {selectedProp.Title} ({selectedProp.Rarity})"); GUILayout.Label(ItemDescUtility.CreatePropDescription(selectedProp.Modifiers), GUILayout.Height(40f)); GUILayout.BeginHorizontal(); GUILayout.Label("Count", GUILayout.Width(52f)); string addCountText = GUILayout.TextField(_addCount.ToString(), GUILayout.Width(60f)); if (!int.TryParse(addCountText, out _addCount)) _addCount = 1; _addCount = Mathf.Clamp(_addCount, 1, 99); if (GUILayout.Button("Add Buff To Player", GUILayout.Height(24f))) { AddSelectedBuffToPlayer(selectedProp, _addCount); } GUILayout.EndHorizontal(); } private void DrawBattleSection() { GUILayout.Label("Battle Debug"); ProcedureGame procedure = GameEntry.Procedure.CurrentProcedure as ProcedureGame; EnemyManagerComponent enemyManager = GameEntry.EnemyManager; Player player = FindPlayer(); HealthComponent playerHealth = player != null ? player.GetComponent() : null; if (enemyManager == null) { GUILayout.Label("EnemyManager unavailable."); return; } if (procedure == null) { GUILayout.Label("ProcedureGame unavailable."); return; } if (_showBattleOverview) { GUILayout.Label($"Spawn Rate: {enemyManager.SpawnRateScale:F2}"); GUILayout.Label($"Battle Time: {enemyManager.ElapsedBattleTime:F1}s / {enemyManager.BattleDuration:F1}s"); GUILayout.Label($"Enemy Count: {enemyManager.CurrentEnemyCount}"); } Simulation.SimulationWorld simulationWorld = GameEntry.SimulationWorld; if (_showCollisionStats && simulationWorld != null) { GUILayout.Space(4f); GUILayout.Label( $"Collision Queries: total {simulationWorld.LastCollisionQueryCount} (Projectile {simulationWorld.LastProjectileCollisionQueryCount} / Area {simulationWorld.LastAreaCollisionQueryCount})"); GUILayout.Label( $"Collision Candidates: total {simulationWorld.LastCollisionCandidateCount} (Projectile {simulationWorld.LastProjectileCollisionCandidateCount} / Area {simulationWorld.LastAreaCollisionCandidateCount})"); GUILayout.Label( $"Area Resolve: hits {simulationWorld.LastResolvedAreaHitCount}"); GUILayout.Label( $"Broad Phase: cell {simulationWorld.LastCollisionCellSize:F2}, hasEnemyTargets {(simulationWorld.LastCollisionHasEnemyTargets ? "Yes" : "No")}"); if (simulationWorld.LastCollisionCandidateCount != 0) { Log.Info($"LastCollisionCandidateCount:{simulationWorld.LastCollisionCandidateCount}"); } if (simulationWorld.LastResolvedAreaHitCount != 0) { Log.Info($"LastResolvedAreaHitCount:{simulationWorld.LastResolvedAreaHitCount}"); } } if (_showSpawnControls) { GUILayout.BeginHorizontal(); GUILayout.Label("Rate", GUILayout.Width(52f)); string rateText = GUILayout.TextField(_spawnRateScaleInput.ToString("F2"), GUILayout.Width(60f)); if (float.TryParse(rateText, out float parsedRate)) { _spawnRateScaleInput = Mathf.Clamp(parsedRate, MinSpawnRate, 50f); } if (GUILayout.Button("Apply", GUILayout.Width(70f))) { enemyManager.SetSpawnRateScale(_spawnRateScaleInput); } if (GUILayout.Button("x0.5", GUILayout.Width(60f))) { _spawnRateScaleInput = Mathf.Max(MinSpawnRate, enemyManager.SpawnRateScale * 0.5f); enemyManager.SetSpawnRateScale(_spawnRateScaleInput); } if (GUILayout.Button("x2", GUILayout.Width(60f))) { _spawnRateScaleInput = enemyManager.SpawnRateScale * 2f; enemyManager.SetSpawnRateScale(_spawnRateScaleInput); } GUILayout.EndHorizontal(); } if (_showBattleDurationControls) { GUILayout.BeginHorizontal(); GUILayout.Label("Add Sec", GUILayout.Width(52f)); string durationText = GUILayout.TextField(_extendDurationSeconds.ToString("F0"), GUILayout.Width(60f)); if (float.TryParse(durationText, out float parsedDuration)) { _extendDurationSeconds = Mathf.Clamp(parsedDuration, 1f, 3600f); } if (GUILayout.Button("Extend Battle", GUILayout.Height(24f))) { if (procedure.CurrentGameState is GameStateBattle gameState) { gameState.AddBattleDuration(_extendDurationSeconds); } } GUILayout.EndHorizontal(); } if (_showSeparationSolverControls) { GUILayout.Space(4f); GUILayout.Label($"Enemy Separation Solver: {EnemySeparationSolverProvider.CurrentSolverName}"); GUILayout.BeginHorizontal(); if (GUILayout.Button("Use Naive O(N^2)", GUILayout.Height(24f))) { EnemySeparationSolverProvider.UseNaiveSolver(); } if (GUILayout.Button("Use Grid Bucket", GUILayout.Height(24f))) { EnemySeparationSolverProvider.UseGridBucketSolver(); } GUILayout.EndHorizontal(); } if (_showPlayerWeaponControls) { GUILayout.Label( $"Player Weapon: {(player == null ? "Player not found" : (player.WeaponEnabled ? "Enabled" : "Disabled"))}"); GUILayout.BeginHorizontal(); GUI.enabled = player != null; if (GUILayout.Button("Disable Weapons", GUILayout.Height(24f))) { player.SetWeaponEnabled(false); } if (GUILayout.Button("Enable Weapons", GUILayout.Height(24f))) { player.SetWeaponEnabled(true); } GUI.enabled = true; GUILayout.EndHorizontal(); } if (_showPlayerHealthControls) { GUILayout.Space(4f); GUILayout.Label( $"Player HP: {(playerHealth == null ? "Unavailable" : $"{playerHealth.CurrentHealth}/{playerHealth.MaxHealth}")}"); GUILayout.BeginHorizontal(); GUI.enabled = playerHealth != null; if (GUILayout.Button($"+{DebugHealAmount} HP", GUILayout.Height(24f))) { AddPlayerHealth(playerHealth, DebugHealAmount); } if (GUILayout.Button(_lockPlayerHealthToMax ? "GodMode: ON" : "GodMode: OFF", GUILayout.Height(24f))) { _lockPlayerHealthToMax = !_lockPlayerHealthToMax; if (_lockPlayerHealthToMax) { RestorePlayerHealthToMax(playerHealth); } } GUI.enabled = true; GUILayout.EndHorizontal(); } } private bool HasVisibleBattleSection() { return _showBattleOverview || _showCollisionStats || _showSpawnControls || _showBattleDurationControls || _showSeparationSolverControls || _showPlayerWeaponControls || _showPlayerHealthControls; } private void EnsurePropList(bool force = false) { if (!force && _allProps != null && _allProps.Length > 0) return; if (GameEntry.DataTable == null) { _allProps = Array.Empty(); _filteredProps = Array.Empty(); _displayNames = Array.Empty(); return; } var table = GameEntry.DataTable.GetDataTable(); _allProps = table != null ? table.ToArray() : Array.Empty(); _selectedIndex = Mathf.Clamp(_selectedIndex, 0, Mathf.Max(0, _allProps.Length - 1)); ApplyFilter(_searchText); } private void ApplyFilter(string keyword) { if (_allProps == null || _allProps.Length == 0) { _filteredProps = Array.Empty(); _displayNames = Array.Empty(); return; } if (string.IsNullOrWhiteSpace(keyword)) { _filteredProps = _allProps; } else { string search = keyword.Trim(); _filteredProps = _allProps.Where(p => p != null && (p.Title?.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0 || p.Id.ToString().Contains(search))).ToArray(); } _displayNames = _filteredProps.Select(p => $"[{p.Id}] {p.Title} ({p.Rarity})").ToArray(); if (_displayNames.Length == 0) _selectedIndex = 0; else _selectedIndex = Mathf.Clamp(_selectedIndex, 0, _displayNames.Length - 1); } private DRProp GetSelectedProp() { if (_filteredProps == null || _filteredProps.Length == 0) return null; _selectedIndex = Mathf.Clamp(_selectedIndex, 0, _filteredProps.Length - 1); return _filteredProps[_selectedIndex]; } private static Player FindPlayer() { return UnityEngine.Object.FindObjectOfType(); } private void KeepPlayerHealthAtMax() { Player player = FindPlayer(); if (player == null) return; HealthComponent playerHealth = player.GetComponent(); if (playerHealth == null) return; RestorePlayerHealthToMax(playerHealth); } private static void AddPlayerHealth(HealthComponent playerHealth, int amount) { if (playerHealth == null || amount <= 0) return; if (playerHealth.CurrentHealth <= 0) return; int maxHealth = playerHealth.MaxHealth; if (maxHealth <= 0) return; int nextHealth = Mathf.Clamp(playerHealth.CurrentHealth + amount, 0, maxHealth); if (nextHealth == playerHealth.CurrentHealth) return; playerHealth.CurrentHealth = nextHealth; PublishPlayerHealthChanged(playerHealth); } private static void RestorePlayerHealthToMax(HealthComponent playerHealth) { if (playerHealth == null) return; if (playerHealth.CurrentHealth <= 0) return; int maxHealth = playerHealth.MaxHealth; if (maxHealth <= 0 || playerHealth.CurrentHealth >= maxHealth) return; playerHealth.CurrentHealth = maxHealth; PublishPlayerHealthChanged(playerHealth); } private static void PublishPlayerHealthChanged(HealthComponent playerHealth) { if (playerHealth == null || GameEntry.Event == null) return; GameEntry.Event.Fire(null, PlayerHealthChangeEventArgs.Create(0, playerHealth.CurrentHealth, playerHealth.MaxHealth)); } private static void AddSelectedBuffToPlayer(DRProp prop, int count) { Player player = FindPlayer(); if (player == null || prop == null || prop.Modifiers == null) return; int applyCount = Mathf.Clamp(count, 1, 99); for (int i = 0; i < applyCount; i++) { player.AddProp(new PropItem(prop.Modifiers, prop.Rarity, prop.Title, prop.IconAssetName)); } } private void HandleCornerTapGesture() { if (!TryGetTouchReleased(out Vector2 touchPosition)) return; if (touchPosition.x > 80f || touchPosition.y < Screen.height - 80f) return; float now = Time.unscaledTime; if (now - _lastCornerTapTime > CornerTapWindow) { _cornerTapCount = 0; } _lastCornerTapTime = now; _cornerTapCount++; if (_cornerTapCount >= RequiredCornerTapCount) { _cornerTapCount = 0; _isPanelVisible = !_isPanelVisible; } } private static bool IsTogglePressed() { #if ENABLE_INPUT_SYSTEM Keyboard keyboard = Keyboard.current; if (keyboard == null) return false; return keyboard.backquoteKey.wasPressedThisFrame || keyboard.f8Key.wasPressedThisFrame; #else return Input.GetKeyDown(KeyCode.BackQuote) || Input.GetKeyDown(KeyCode.F8); #endif } private static bool TryGetTouchReleased(out Vector2 touchPosition) { #if ENABLE_INPUT_SYSTEM Touchscreen touchscreen = Touchscreen.current; if (touchscreen == null) { touchPosition = default; return false; } var touch = touchscreen.primaryTouch; if (!touch.press.wasReleasedThisFrame) { touchPosition = default; return false; } touchPosition = touch.position.ReadValue(); return true; #else if (Input.touchCount <= 0) { touchPosition = default; return false; } Touch touch = Input.GetTouch(0); if (touch.phase != TouchPhase.Ended) { touchPosition = default; return false; } touchPosition = touch.position; return true; #endif } } } #endif