Files
test/Assets/PerfectWorld/Scripts/Debug/SkillTriggerPanel.cs
T
2026-05-19 10:46:26 +07:00

438 lines
19 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;
/// <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>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 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 <see cref="k_InjectFallbackSecs"/>
/// seconds, injects all config skills directly from SkillStub.map so the panel works offline.
///
/// 每 0.5 秒轮询,直到主角已有技能且 CECGameUIMan 初始化完毕,再调用一次 Refresh()。
/// 若 k_InjectFallbackSecs 秒内未收到服务端技能数据,则直接从 SkillStub.map 注入所有配置技能,以便离线使用。
/// </summary>
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();
}
/// <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>
/// 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;
}
// 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<Text>();
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<Button>();
if (btn != null)
btn.onClick.AddListener(() => LocalCastSkill(captured));
added++;
}
Debug.Log($"[SkillTriggerPanel] Refreshed: {added} buttons added " +
$"({filtered} filtered by restrictions, {total} total positive 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;
int skillId = skill.GetSkillID();
// Self-cast skills target the host; all others target the draggable marker.
// 自身施法技能以主角为目标;其余技能以可拖动标记为目标。
int targetId = ResolveLocalCastTargetId(skill);
BMLogger.Log($"[SkillTriggerPanel] LocalCastSkill: skillID={skillId}, targetId={targetId}, hostCid={player.GetCharacterID()}");
// 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, targetMarker.GetNPCID(), 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;
}
}
}