438 lines
19 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|