using System; using System.Collections; using System.Collections.Generic; using BrewMonster.Scripts; using BrewMonster.Scripts.Skills; using BrewMonster.UI; using UnityEngine; using UnityEngine.UI; using BrewMonster.Managers; using BrewMonster.Network; namespace BrewMonster { /// /// Debug skill trigger panel. /// 调试技能触发面板。 /// /// Populates a UI GridLayoutGroup with one button per usable skill on the host player. /// Clicking a button fires the skill locally: animation + VFX only, no network, no cooldown, no resource check. /// 用主角可用技能填充 UI GridLayoutGroup,每个技能一个按钮。 /// 点击按钮本地触发技能:仅播放动画 + 特效,无网络、无冷却、无资源消耗。 /// /// Scene setup: /// 1. Assign Player (CECHostPlayer reference). /// 2. Assign Target Marker (a Transform ~5 units in front of the player — used for projectile/fly-VFX skills). /// 3. Assign Skill Grid Container (RectTransform with GridLayoutGroup). /// 4. Assign Skill Button Prefab (GameObject with a Button root, child Image for icon, child Text for label). /// 5. Enter Play Mode; the grid is populated automatically. Call Refresh() again after role/weapon changes. /// public class SkillTriggerPanel : MonoBehaviour { /// /// Sentinel entity ID used to represent the draggable target marker in the GFX system. /// 用于在特效系统中表示可拖动目标标记的实体 ID。 /// /// Local id for debug NPC marker; encoded with for GFX lookups. public const int DEBUG_TARGET_ID = 1200; /// Encode a local index as a server-style NPC id (high bit set). public static int EncodeDebugNpcId(int localId = DEBUG_TARGET_ID) => unchecked((int)(0x80000000u | (uint)localId)); public static SkillTriggerPanel Instance { get; private set; } [Header("References")] [Tooltip("The host player to read skills from and trigger cast on.")] [SerializeField] private CECHostPlayer player; [Tooltip("World-space Transform used as target for projectile (non-self) skills. Place a Sphere ~5 units in front of the player.")] [SerializeField] public CECMonsterTest targetMarker; [Tooltip("RectTransform that has a GridLayoutGroup component. Buttons are instantiated here.")] [SerializeField] private RectTransform skillGridContainer; [Tooltip("Prefab with: root Button, child Image (icon), child Text (skill name/ID).")] [SerializeField] private GameObject skillButtonPrefab; [Header("Filter")] [Tooltip("When enabled, hides skills that fail weapon/form/move-env restrictions. Disable to show ALL positive skills regardless of restrictions.")] [SerializeField] private bool filterByRestrictions = false; /// /// World position of the draggable target marker, read by CECSkillGfxMan.get_pos_by_id. /// 可拖动目标标记的世界坐标,由 CECSkillGfxMan.get_pos_by_id 读取。 /// public static Vector3 DebugTargetPosition => Instance != null && Instance.targetMarker != null ? Instance.targetMarker.transform.position : Vector3.zero; private void Awake() { Instance = this; } private void OnDestroy() { if (Instance == this) Instance = null; } private void Start() { RegisterSceneHostWithGameRun(); StartCoroutine(RefreshWhenReady()); } /// /// Animation test scene: in-scene is not created via . /// GFX get_pos_by_id resolves host through . /// private void RegisterSceneHostWithGameRun() { if (player == null) return; var gameRun = EC_Game.GetGameRun(); if (gameRun == null) return; gameRun.RegisterAnimSceneHostPlayer(player); } /// Target id for / GFX composer (self = host cid, else marker NPC id). private int ResolveLocalCastTargetId(CECSkill skill) { if (skill.GetTargetType() == 0) return player.GetCharacterID(); if (targetMarker != null) return targetMarker.GetNPCID(); return EncodeDebugNpcId(); } /// /// Polls every 0.5 s until the host player has skills and CECGameUIMan is initialized, /// then calls Refresh() once. If no server skill data arrives within /// seconds, injects all config skills directly from SkillStub.map so the panel works offline. /// /// 每 0.5 秒轮询,直到主角已有技能且 CECGameUIMan 初始化完毕,再调用一次 Refresh()。 /// 若 k_InjectFallbackSecs 秒内未收到服务端技能数据,则直接从 SkillStub.map 注入所有配置技能,以便离线使用。 /// private const float k_InjectFallbackSecs = 2f; private IEnumerator RefreshWhenReady() { var wait = new WaitForSeconds(0.5f); float elapsed = 0f; // Wait for UI atlas first — required for icons regardless of skill source. // 先等待 UI 图集加载完毕,图标渲染必须。 while (CECUIManager.Instance?.GetInGameUIMan() == null) { elapsed += 0.5f; yield return wait; } // Wait for skills, but fall back to config injection after timeout. // 等待技能数据,超时后回退到配置注入。 while (player != null && player.GetPositiveSkillNum() == 0) { if (elapsed >= k_InjectFallbackSecs) { Debug.Log("[SkillTriggerPanel] No server skills after " + $"{k_InjectFallbackSecs}s — injecting all config skills for offline debug."); player.InjectDebugSkillsFromConfig(); break; } elapsed += 0.5f; yield return wait; } Refresh(); } /// /// Loads every skill defined in SkillStub.map (config) directly into the player's skill list, /// bypassing the server skill message. Use this when testing locally without a server. /// 将 SkillStub.map(配置文件)中所有技能直接注入玩家技能列表,绕过服务端消息。 /// 适用于无服务端的本地测试场景。 /// [ContextMenu("Debug: Inject All Config Skills")] public void InjectConfigSkills() { if (player == null) { Debug.LogWarning("[SkillTriggerPanel] player is not assigned."); return; } player.InjectDebugSkillsFromConfig(); Refresh(); } /// /// Clears and repopulates the skill grid from the host player's current positive skill list. /// Call again after equipping a different weapon or changing shape/form. /// Right-click this component in the Inspector to call Refresh manually. /// 从主角当前正向技能列表清空并重新填充技能格。 /// 在更换武器或变身后再次调用;右键此组件可手动调用。 /// [ContextMenu("Refresh")] public void Refresh() { if (player == null) { Debug.LogWarning("[SkillTriggerPanel] player is not assigned."); return; } if (skillGridContainer == null) { Debug.LogWarning("[SkillTriggerPanel] skillGridContainer is not assigned."); return; } if (skillButtonPrefab == null) { Debug.LogWarning("[SkillTriggerPanel] skillButtonPrefab is not assigned."); return; } // Clear existing buttons / 清空已有按钮 foreach (Transform child in skillGridContainer) Destroy(child.gameObject); int total = player.GetPositiveSkillNum(); if (total == 0) { Debug.LogWarning("[SkillTriggerPanel] GetPositiveSkillNum() = 0. " + "Skills are loaded via OnMsgHstSkillData (server message). " + "Make sure the host player has received skill data before calling Refresh."); return; } CECGameUIMan uiMan = CECUIManager.Instance?.GetInGameUIMan(); if (uiMan == null) { Debug.LogWarning("[SkillTriggerPanel] CECGameUIMan not ready — icon atlas not loaded. " + "Call Refresh() after the game UI finishes initializing."); return; } int added = 0; int filtered = 0; for (int i = 0; i < total; i++) { CECSkill skill = player.GetPositiveSkillByIndex(i); if (skill == null) continue; if (filterByRestrictions && !IsSkillUsable(skill)) { filtered++; continue; } GameObject cell = Instantiate(skillButtonPrefab, skillGridContainer); // --- Icon --- string iconName = skill.GetIconFile(); if (!string.IsNullOrEmpty(iconName)) { Sprite icon = uiMan.GetIcon(iconName, EC_GAMEUI_ICONS.ICONS_SKILL); Image cellImage = GetIconImage(cell); Debug.Log("[SkillTriggerPanel] icon = " + (icon == null) + "cellImage = " + (cellImage == null)); if (cellImage != null && icon != null) cellImage.sprite = icon; } else{ Debug.LogWarning("[SkillTriggerPanel] skill.GetIconFile() is empty for skill " + skill.GetSkillID()); } // --- Label --- Text cellText = cell.GetComponentInChildren(); if (cellText != null) { string skillName = skill.GetName(); cellText.text = string.IsNullOrEmpty(skillName) ? $"[{skill.GetSkillID()}]\nLv{skill.GetSkillLevel()}" : $"{skillName}\nLv{skill.GetSkillLevel()}"; } // --- Button --- CECSkill captured = skill; Button btn = cell.GetComponentInChildren