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; [Tooltip("Match CDlgSkillSubList god/evil path: false = base+god ranks, true = base+evil ranks.")] [SerializeField] private bool isEvilSkillPath; [SerializeField] private int skillCatalogLevel = 1; [SerializeField] private LogPanelAnimeScene logPanelAnimeScene; [Header("Channeling (time_type = 2)")] [Tooltip("Stops durative/channeling skill repeat and allows other skills again.")] [SerializeField] private Button channelCancelButton; private Coroutine _channelingCoroutine; private Coroutine _fadeChantGfxCoroutine; private Coroutine _fadeAttackGfxCoroutine; private int _channelingSkillId; private bool _isChanneling; private const float k_FadeChantGfxDelaySecs = 0.5f; private const float k_DefaultAttackGfxDurationSecs = 2f; private const float k_AttackGfxFadeBufferSecs = 0.25f; /// /// 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; if (channelCancelButton != null) channelCancelButton.onClick.AddListener(CancelChanneling); } private void OnDestroy() { if (channelCancelButton != null) channelCancelButton.onClick.RemoveListener(CancelChanneling); StopChanneling(); StopFadeGfxCoroutines(); 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); } /// /// Anim scene host swap: keep panel player reference in sync with bootstrap. /// 动画场景切换角色时同步面板上的主角引用。 /// public void SetHostPlayer(CECHostPlayer host) { if (host == null) return; player = host; RegisterSceneHostWithGameRun(); } /// 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 until CECGameUIMan is ready and the host has skills (server, skill model, or config fallback). /// /// 轮询直到 UI 图集与技能数据就绪(服务端、CECHostSkillModel 或配置回退)。 /// private const float k_RefreshWaitTimeoutSecs = 30f; 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; } ResolveHostPlayerReference(); // Wait for skills: server list, CECHostSkillModel catalog (anim scene), or config fallback. // 等待技能:服务端列表、CECHostSkillModel 目录(动画场景)或配置回退。 while (player != null && !HasSkillCatalogOrPositiveSkills()) { if (TryInjectFromSkillModel()) break; if (elapsed >= k_RefreshWaitTimeoutSecs) { Debug.LogWarning("[SkillTriggerPanel] No skills after " + $"{k_RefreshWaitTimeoutSecs}s — falling back to SkillStub.map injection."); player.InjectDebugSkillsFromConfig(); break; } elapsed += 0.5f; yield return wait; } Refresh(); } private void ResolveHostPlayerReference() { if (player != null) return; player = EC_Game.GetGameRun()?.GetHostPlayer(); } private bool HasSkillCatalogOrPositiveSkills() { if (player != null && player.GetPositiveSkillNum() > 0) return true; List catalog = CECHostSkillModel.Instance?.CollectSkillSubListSkillIds(isEvilSkillPath); return catalog != null && catalog.Count > 0; } private bool TryInjectFromSkillModel() { if (player == null) return false; List catalog = CECHostSkillModel.Instance?.CollectSkillSubListSkillIds(isEvilSkillPath); if (catalog == null || catalog.Count == 0) return false; player.InjectSkillsFromSkillModel(skillCatalogLevel, isEvilSkillPath); return true; } /// /// 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(); } /// /// Rebuild host positive skills from (same catalog as CDlgSkillSubList) and refresh the grid. /// AnimScenePlayerBootstrap calls this after each model / role switch. /// /// 从 CECHostSkillModel 重建主角正向技能并刷新网格(与 CDlgSkillSubList 相同目录)。 /// [ContextMenu("Debug: Refresh From Skill Model")] public void RefreshFromSkillModel(int level = -1) { ResolveHostPlayerReference(); if (player == null) { Debug.LogWarning("[SkillTriggerPanel] RefreshFromSkillModel: no host player."); return; } if (level > 0) skillCatalogLevel = level; player.InjectSkillsFromSkillModel(skillCatalogLevel, isEvilSkillPath); List catalogSkills = player.BuildSkillSubListSkills(skillCatalogLevel, isEvilSkillPath); PopulateSkillGrid(catalogSkills, catalogSkills.Count); } public void SetEvilSkillPath(bool isEvil) { isEvilSkillPath = isEvil; } /// /// Reload from CECHostSkillModel and optionally hide skills that fail weapon/form/env checks. /// 从技能目录重新加载;applyRestrictions 为 true 时按武器/形态/环境过滤。 /// public void ResetSkillsFromSkillModel(bool applyRestrictions, int level = -1) { filterByRestrictions = applyRestrictions; RefreshFromSkillModel(level); } /// /// 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; } var catalogSkills = player.BuildSkillSubListSkills(skillCatalogLevel, isEvilSkillPath); if (catalogSkills.Count > 0) { PopulateSkillGrid(catalogSkills, catalogSkills.Count); return; } 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; } var positiveSkills = new List(total); for (int i = 0; i < total; i++) { CECSkill skill = player.GetPositiveSkillByIndex(i); if (skill != null) positiveSkills.Add(skill); } PopulateSkillGrid(positiveSkills, total); } private void PopulateSkillGrid(IReadOnlyList skills, int totalListed) { foreach (Transform child in skillGridContainer) Destroy(child.gameObject); 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 < skills.Count; i++) { CECSkill skill = skills[i]; if (skill == null) continue; if (filterByRestrictions && !IsSkillUsable(skill)) { filtered++; continue; } GameObject cell = Instantiate(skillButtonPrefab, skillGridContainer); string iconName = skill.GetIconFile(); if (!string.IsNullOrEmpty(iconName)) { Sprite icon = uiMan.GetIcon(iconName, EC_GAMEUI_ICONS.ICONS_SKILL); Image cellImage = GetIconImage(cell); if (cellImage != null && icon != null) cellImage.sprite = icon; } else { Debug.LogWarning("[SkillTriggerPanel] skill.GetIconFile() is empty for skill " + skill.GetSkillID()); } 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()}"; } CECSkill captured = skill; Button btn = cell.GetComponentInChildren