Files
test/Assets/PerfectWorld/Scripts/Debug/SkillTriggerPanel.cs
T

550 lines
23 KiB
C#

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
{
/// <summary>
/// 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.
/// </summary>
public class SkillTriggerPanel : MonoBehaviour
{
/// <summary>
/// Sentinel entity ID used to represent the draggable target marker in the GFX system.
/// 用于在特效系统中表示可拖动目标标记的实体 ID。
/// </summary>
/// <summary>Local id for debug NPC marker; encoded with <see cref="EncodeDebugNpcId"/> for GFX lookups.</summary>
public const int DEBUG_TARGET_ID = 1200;
/// <summary>Encode a local index as a server-style NPC id (high bit set).</summary>
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;
/// <summary>
/// World position of the draggable target marker, read by CECSkillGfxMan.get_pos_by_id.
/// 可拖动目标标记的世界坐标,由 CECSkillGfxMan.get_pos_by_id 读取。
/// </summary>
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());
}
/// <summary>
/// Animation test scene: in-scene <see cref="CECHostPlayer"/> is not created via <see cref="CECGameRun.InitCharacter"/>.
/// GFX <c>get_pos_by_id</c> resolves host through <see cref="EC_ManPlayer.GetPlayer"/> → <see cref="CECGameRun.GetHostPlayer"/>.
/// </summary>
private void RegisterSceneHostWithGameRun()
{
if (player == null)
return;
var gameRun = EC_Game.GetGameRun();
if (gameRun == null)
return;
gameRun.RegisterAnimSceneHostPlayer(player);
}
/// <summary>
/// Anim scene host swap: keep panel player reference in sync with bootstrap.
/// 动画场景切换角色时同步面板上的主角引用。
/// </summary>
public void SetHostPlayer(CECHostPlayer host)
{
if (host == null)
return;
player = host;
RegisterSceneHostWithGameRun();
}
/// <summary>Target id for <see cref="LocalCastSkill"/> / GFX composer (self = host cid, else marker NPC id).</summary>
private int ResolveLocalCastTargetId(CECSkill skill)
{
if (skill.GetTargetType() == 0)
return player.GetCharacterID();
if (targetMarker != null)
return targetMarker.GetNPCID();
return EncodeDebugNpcId();
}
/// <summary>
/// Polls until CECGameUIMan is ready and the host has skills (server, skill model, or config fallback).
///
/// 轮询直到 UI 图集与技能数据就绪(服务端、CECHostSkillModel 或配置回退)。
/// </summary>
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<int> catalog = CECHostSkillModel.Instance?.CollectSkillSubListSkillIds(isEvilSkillPath);
return catalog != null && catalog.Count > 0;
}
private bool TryInjectFromSkillModel()
{
if (player == null)
return false;
List<int> catalog = CECHostSkillModel.Instance?.CollectSkillSubListSkillIds(isEvilSkillPath);
if (catalog == null || catalog.Count == 0)
return false;
player.InjectSkillsFromSkillModel(skillCatalogLevel, isEvilSkillPath);
return true;
}
/// <summary>
/// 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(配置文件)中所有技能直接注入玩家技能列表,绕过服务端消息。
/// 适用于无服务端的本地测试场景。
/// </summary>
[ContextMenu("Debug: Inject All Config Skills")]
public void InjectConfigSkills()
{
if (player == null)
{
Debug.LogWarning("[SkillTriggerPanel] player is not assigned.");
return;
}
player.InjectDebugSkillsFromConfig();
Refresh();
}
/// <summary>
/// Rebuild host positive skills from <see cref="CECHostSkillModel"/> (same catalog as CDlgSkillSubList) and refresh the grid.
/// AnimScenePlayerBootstrap calls this after each model / role switch.
///
/// 从 CECHostSkillModel 重建主角正向技能并刷新网格(与 CDlgSkillSubList 相同目录)。
/// </summary>
[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<CECSkill> catalogSkills = player.BuildSkillSubListSkills(skillCatalogLevel, isEvilSkillPath);
PopulateSkillGrid(catalogSkills, catalogSkills.Count);
}
public void SetEvilSkillPath(bool isEvil)
{
isEvilSkillPath = isEvil;
}
/// <summary>
/// Reload from CECHostSkillModel and optionally hide skills that fail weapon/form/env checks.
/// 从技能目录重新加载;applyRestrictions 为 true 时按武器/形态/环境过滤。
/// </summary>
public void ResetSkillsFromSkillModel(bool applyRestrictions, int level = -1)
{
filterByRestrictions = applyRestrictions;
RefreshFromSkillModel(level);
}
/// <summary>
/// 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.
/// 从主角当前正向技能列表清空并重新填充技能格。
/// 在更换武器或变身后再次调用;右键此组件可手动调用。
/// </summary>
[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<CECSkill>(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<CECSkill> 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<Text>();
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<Button>();
if (btn != null)
btn.onClick.AddListener(() => LocalCastSkill(captured));
added++;
}
Debug.Log($"[SkillTriggerPanel] Refreshed: {added} buttons added " +
$"({filtered} filtered by restrictions, {totalListed} catalog/server skills).");
}
/// <summary>
/// Returns true if the skill passes weapon, form, and move-environment restrictions
/// with resources (MP/AP/HP/arrow) treated as unlimited.
/// Relies on the same SkillCore.Condition path as CheckSkillCastCondition but with faked resource values.
/// 当武器、形态、移动环境限制通过(资源视为无限)时返回 true。
/// </summary>
private bool IsSkillUsable(CECSkill skill)
{
if (skill == null || skill.SkillCore == null)
return true;
try
{
// Build real weapon major-type ID / 获取当前装备武器的主类型 ID
int weaponId = 0;
EC_Inventory equipPack = player.IvtrEquipPack;
if (equipPack != null)
{
var weapon = equipPack.GetItem((int)IndexOfIteminEquipmentInventory.EQUIPIVTR_WEAPON) as CECIvtrWeapon;
if (weapon != null && weapon.CurEndurance > 0)
weaponId = (int)weapon.GetDBMajorType().id;
}
// Fake unlimited resources but keep real weapon / form / env values
// 伪造无限资源,但保留真实的武器/形态/环境值
UseRequirement fakeInfo = new UseRequirement
{
mp = int.MaxValue, // ignore mana / 忽略法力
ap = int.MaxValue, // ignore AP / 忽略怒气
hp = int.MaxValue, // ignore HP / 忽略生命
max_hp = int.MaxValue,
freepackage = 999, // ignore inventory space / 忽略背包空间
arrow = 9999, // ignore arrow count / 忽略箭矢数量
weapon = weaponId, // real weapon type / 真实武器类型
form = player.GetOriginalShapeID(), // real shape/form / 真实形态
move_env = player.GetMoveEnv(), // real move env / 真实移动环境
is_combat = true,
combo_state = new ComboSkillState
{
skillid = 0,
arg = new int[ComboSkillState.MAX_COMBO_ARG],
},
};
int result = skill.SkillCore.Condition((uint)skill.GetSkillID(), fakeInfo, skill.GetSkillLevel());
return result == 0;
}
catch (Exception ex)
{
Debug.LogWarning($"[SkillTriggerPanel] IsSkillUsable exception for skill {skill.GetSkillID()}: {ex.Message} — treating as usable.");
return true;
}
}
/// <summary>
/// Fires the skill locally: plays the cast animation and triggers VFX directly.
/// No network packet is sent; cooldown and resources are not consumed.
/// 本地触发技能:播放施法动画并直接触发特效。
/// 不发送网络包,不消耗冷却时间和资源。
/// </summary>
/// <summary>
/// Returns the first Image in the cell hierarchy that is NOT used as a Button's target graphic.
/// Button background Images sit on the same GameObject as a Button component, so we skip those.
/// The icon Image is a leaf child that has no Button sibling.
///
/// 返回层级中第一个不作为 Button 目标图的 Image。
/// Button 背景图与 Button 组件共存于同一 GameObject,故跳过;图标 Image 是没有 Button 兄弟组件的子节点。
/// </summary>
private static Image GetIconImage(GameObject root)
{
return root.GetComponentInChildren<Image>();
}
public void LocalCastSkill(CECSkill skill)
{
if (player == null || skill == null)
return;
logPanelAnimeScene.Reset();
int skillId = skill.GetSkillID();
logPanelAnimeScene.AddCopyTextButton("ID", skillId.ToString());
logPanelAnimeScene.LogSkillCastGfx(player, skillId);
logPanelAnimeScene.LogPlayAttackEffectGfx(player, skillId, 0);
// Self-cast skills target the host; all others target the draggable marker.
// 自身施法技能以主角为目标;其余技能以可拖动标记为目标。
int targetId = ResolveLocalCastTargetId(skill);
// 1. Trigger cast animation (PlaySkillCastAction resolves action name internally)
// 触发施法动画(PlaySkillCastAction 内部解析动作名)
bool animOk = player.PlaySkillCastAction(skillId);
if (!animOk)
Debug.LogWarning($"[SkillTriggerPanel] PlaySkillCastAction returned false for skill {skillId} — animation may not play.");
// // 2. Trigger VFX via the GFX composer — bypasses network entirely
// // 通过特效组合器触发特效,完全绕过网络
// var composerMan = CECAttacksMan.Instance?.GetSkillGfxComposerMan();
// if (composerMan != null)
// {
// var targets = new List<TARGET_DATA>
// {
// new TARGET_DATA { idTarget = targetId, dwModifier = 0, nDamage = 0 }
// };
// composerMan.Play(skillId, player.GetCharacterID(), targetId, targets);
// }
// else
// {
// Debug.LogWarning($"[SkillTriggerPanel] composerMan is null — VFX skipped for skill {skillId}.");
// }
// 3. After 2s, play the release / 施放起+落 attack animation chain (local-only).
// 2 秒后播放施放攻击动画链(仅本地)。
StartCoroutine(DelayedPlaySkillAttackAction(player, skillId));
}
private IEnumerator DelayedPlaySkillAttackAction(CECHostPlayer hostPlayer, int skillId)
{
int attackTime = 0;
yield return new WaitForSeconds(1f);
if (hostPlayer == null)
yield break;
CECAttackEvent pAttack = null;
// first try to find if there is already a skill attack event in attackman
CECAttackerEvents attackerEvents = CECAttacksMan.Instance.FindAttackByAttacker(hostPlayer.GetPlayerInfo().cid);
if (attackerEvents)
{
pAttack = attackerEvents.Find(skillId, 0);
if (pAttack != null)
{
// Ãæ¹¥»÷µÄ·ÇµÚÒ»´ÎÉ˺¦ÏûÏ¢
pAttack.AddTarget(DEBUG_TARGET_ID, 1, 1);
goto EXIT;
}
else
{
attackerEvents.Signal();
}
}
if (ElementSkill.IsGoblinSkill((uint)skillId) &&
ElementSkill.GetType((uint)skillId) == 2)
{
pAttack = CECAttacksMan.Instance.AddSkillAttack(
hostPlayer.GetPlayerInfo().cid, hostPlayer.GetPlayerInfo().cid, hostPlayer.GetPlayerInfo().cid, hostPlayer.GetWeaponID(), skillId, 0, 0x0200, 0);
}
else
{
// begin a skill attack
pAttack = CECAttacksMan.Instance.AddSkillAttack(
hostPlayer.GetPlayerInfo().cid, targetMarker.GetNPCID(), targetMarker.GetNPCID(), hostPlayer.GetWeaponID(), skillId, 0, 0x0200, 0);
}
bool ok = hostPlayer.PlaySkillAttackAction(skillId, 0, ref attackTime,0, pAttack);
if (!ok)
BMLogger.LogWarning($"[SkillTriggerPanel] PlaySkillAttackAction returned false for skill {skillId} after delay — attack anim may not play.");
EXIT:
// // For skill attacking, time is always set to 0
if (attackTime != 0)
attackTime = 0;
}
}
}