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 int _channelingSkillId;
private bool _isChanneling;
///
/// 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;
<<<<<<< HEAD
SkillStubs.Init();
=======
if (channelCancelButton != null)
channelCancelButton.onClick.AddListener(CancelChanneling);
>>>>>>> 6fde1d1583 (Add requirement for play skill)
}
private void OnDestroy()
{
if (channelCancelButton != null)
channelCancelButton.onClick.RemoveListener(CancelChanneling);
StopChanneling();
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