diff --git a/Assets/PerfectWorld/Scripts/AnimScenePlayerBootstrap.cs b/Assets/PerfectWorld/Scripts/AnimScenePlayerBootstrap.cs
new file mode 100644
index 0000000000..9e71c4732b
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/AnimScenePlayerBootstrap.cs
@@ -0,0 +1,313 @@
+using BrewMonster;
+using BrewMonster.Network;
+using BrewMonster.Scripts;
+using UnityEngine;
+
+namespace PerfectWorld.Scripts
+{
+ ///
+ /// One row per WEAPON_SUB_TYPE.action_type / suffix index (0..14).
+ /// 每个武器 action_type / 动作后缀索引一行(0..14)。
+ ///
+ [System.Serializable]
+ public struct AnimSceneWeaponSlot
+ {
+ public GameObject rightHandModelPrefab;
+ public GameObject leftHandModelPrefab;
+ }
+
+ ///
+ /// Test scene only: bootstraps model load. After each successful ,
+ /// optionally applies serialized weapon prefabs via (sets m_uAttackType like equip data).
+ /// After each load, destroys the previous major model instance so it does not linger in the hierarchy.
+ /// Optional: discard the whole host instance and instantiate a fresh prefab when switching role (see ).
+ ///
+ public class AnimScenePlayerBootstrap : MonoBehaviour
+ {
+ [Header("Player Reference")]
+ [Tooltip("Scene host player. Do not put this bootstrap component on the same GameObject as the host if you enable Replace host on role switch.")]
+ [SerializeField]
+ private CECHostPlayer player;
+
+ [Header("Initial Role")]
+ [SerializeField]
+ private Profession profession = Profession.PROF_WARRIOR;
+
+ [SerializeField]
+ private Gender gender = Gender.GENDER_MALE;
+
+ [Header("Animation scene — weapon visuals")]
+ [Tooltip("Prefabs per weapon action_type index (0–14). Empty slot clears weapons and resets m_uAttackType when applied.")]
+ [SerializeField]
+ private AnimSceneWeaponSlot[] weaponByActionType = new AnimSceneWeaponSlot[15];
+
+ [Tooltip("Which weaponByActionType row to apply after load / when using Re-apply in the custom inspector.")]
+ [SerializeField]
+ [Range(0, 14)]
+ private int activeWeaponActionTypeIndex;
+
+ [SerializeField]
+ private bool applyWeaponAfterModelLoad = true;
+
+ [Header("Animation scene — element data compat")]
+ [Tooltip(
+ "If true, sets m_iFashionWeaponType = -1 on the host so PlayAction / GetShowingWeaponType do not call IsFashionWeaponTypeFit with missing DT_FASHION_WEAPON_CONFIG (avoids null action_mask NRE). Disable only if you load that config row.")]
+ [SerializeField]
+ private bool animSceneAvoidFashionWeaponConfigPath = true;
+
+ [Header("Animation scene — PlayerVisual")]
+ [Tooltip(
+ "After SetPlayerModel, call RefreshNamedAnimancer on the loaded .ecm then InitPlayerEventDoneHandler. " +
+ "Without this, the animation test scene never runs InitCharacter, so PlayerVisual never subscribes to PlayActionEvent.")]
+ [SerializeField]
+ private bool bindPlayerVisualAfterModelLoad = true;
+
+ [Header("Test-only — cleanup & host swap")]
+ [Tooltip("After SetPlayerModel, Destroy the previous major .ecm instance (frees scene objects left by re-load).")]
+ [SerializeField]
+ private bool destroyPreviousModelAfterSwitch = true;
+
+ [Tooltip("When switching role (not on first Start), Destroy the current CECHostPlayer GameObject and Instantiate hostPlayerPrefab before SetPlayerModel. Requires a sibling bootstrap object.")]
+ [SerializeField]
+ private bool replaceHostOnRoleSwitch;
+
+ [SerializeField]
+ private CECHostPlayer hostPlayerPrefab;
+
+ private bool _initStaticResDone;
+ private bool _busy;
+
+ private async void Start()
+ {
+ if (player == null)
+ {
+ Debug.LogError(
+ "[AnimSceneBootstrap] AnimScenePlayerBootstrap: player reference is null — assign CECHostPlayer in Inspector.");
+ return;
+ }
+
+ await BootstrapAsync((byte)profession, (byte)gender, fromInitialStart: true);
+ }
+
+ /// Swap profession/gender at runtime (async load inside).
+ public void SwitchRole(Profession newProfession, Gender newGender)
+ {
+ if (_busy)
+ {
+ Debug.LogWarning("[AnimSceneBootstrap] SwitchRole called while busy — ignoring.");
+ return;
+ }
+
+ profession = newProfession;
+ gender = newGender;
+ _ = BootstrapAsync((byte)newProfession, (byte)newGender, fromInitialStart: false);
+ }
+
+ /// profIndex: 0..-1; genderIndex: 0 = male, 1 = female.
+ public void SwitchRole(int profIndex, int genderIndex)
+ {
+ SwitchRole((Profession)profIndex, (Gender)genderIndex);
+ }
+
+ ///
+ /// Custom inspector: call before when is this bootstrap's .
+ ///
+ public void AnimSceneEnsureFashionPathSafeBeforePlayAction(CECHostPlayer host)
+ {
+ if (!animSceneAvoidFashionWeaponConfigPath || host == null || player == null || host != player)
+ {
+ return;
+ }
+
+ host.AnimSceneSanitizeFashionWeaponStateForElementDataCompat();
+ }
+
+ private void AnimSceneApplyFashionPathCompatIfEnabled()
+ {
+ if (!animSceneAvoidFashionWeaponConfigPath || player == null)
+ {
+ return;
+ }
+
+ player.AnimSceneSanitizeFashionWeaponStateForElementDataCompat();
+ }
+
+ ///
+ /// Login calls from InitCharacter; animation scenes skip that — bind here.
+ ///
+ private void AnimSceneBindPlayerVisualIfEnabled()
+ {
+ if (!bindPlayerVisualAfterModelLoad || player == null)
+ {
+ return;
+ }
+
+ PlayerVisual pv = player.GetComponentInChildren();
+ if (pv == null)
+ {
+ return;
+ }
+
+ GameObject modelRoot = player.m_pPlayerCECModel != null ? player.m_pPlayerCECModel.m_pPlayerModel : null;
+ if (modelRoot != null)
+ {
+ pv.RefreshNamedAnimancer(modelRoot);
+ }
+
+ pv.InitPlayerEventDoneHandler();
+ }
+
+ ///
+ /// Apply [] on the host (Play Mode).
+ /// 在主机上应用当前选中的武器槽(运行模式)。
+ ///
+ public void ApplyWeaponForActiveSlot()
+ {
+ if (player == null)
+ {
+ Debug.LogWarning("[AnimSceneBootstrap] ApplyWeaponForActiveSlot: player is null.");
+ return;
+ }
+
+ if (weaponByActionType == null || weaponByActionType.Length == 0)
+ {
+ return;
+ }
+
+ int idx = Mathf.Clamp(activeWeaponActionTypeIndex, 0, weaponByActionType.Length - 1);
+ AnimSceneWeaponSlot slot = weaponByActionType[idx];
+ bool empty = slot.rightHandModelPrefab == null && slot.leftHandModelPrefab == null;
+ if (empty)
+ {
+ player.AnimSceneAttachWeaponPrefabs(null, null, CECPlayer.DEFAULT_ACTION_TYPE);
+ }
+ else
+ {
+ player.AnimSceneAttachWeaponPrefabs(
+ slot.rightHandModelPrefab,
+ slot.leftHandModelPrefab,
+ (uint)idx);
+ }
+ }
+
+ private void ReplaceHostPlayerInstance()
+ {
+ if (hostPlayerPrefab == null)
+ {
+ Debug.LogWarning("[AnimSceneBootstrap] replaceHostOnRoleSwitch is set but hostPlayerPrefab is null — skipping host replace.");
+ return;
+ }
+
+ if (player == null)
+ {
+ return;
+ }
+
+ if (player.gameObject == gameObject)
+ {
+ Debug.LogError(
+ "[AnimSceneBootstrap] Cannot replace host: this bootstrap is on the same GameObject as CECHostPlayer. Move AnimScenePlayerBootstrap to another object (e.g. a parent or sibling).");
+ return;
+ }
+
+ Vector3 pos = player.transform.position;
+ Quaternion rot = player.transform.rotation;
+ Transform parent = player.transform.parent;
+
+ Object.Destroy(player.gameObject);
+
+ GameObject instance = Object.Instantiate(hostPlayerPrefab.gameObject, pos, rot, parent);
+ player = instance.GetComponent();
+ if (player == null)
+ {
+ Debug.LogError("[AnimSceneBootstrap] hostPlayerPrefab has no CECHostPlayer component.");
+ }
+ }
+
+ private async System.Threading.Tasks.Task BootstrapAsync(byte prof, byte gen, bool fromInitialStart)
+ {
+ _busy = true;
+ try
+ {
+ Debug.Log("[AnimSceneBootstrap] AnimScenePlayerBootstrap — waiting for ElementDataManProvider...");
+ int waitedFrames = 0;
+ while (!ElementDataManProvider.IsDataLoaded)
+ {
+ await System.Threading.Tasks.Task.Yield();
+ waitedFrames++;
+ if (waitedFrames > 3000)
+ {
+ Debug.LogError(
+ "[AnimSceneBootstrap] ElementDataManProvider never became ready — aborting (check Addressables / load_data).");
+ return;
+ }
+ }
+
+ Debug.Log($"[AnimSceneBootstrap] ElementDataManProvider ready after ~{waitedFrames} yields.");
+
+ if (!fromInitialStart && replaceHostOnRoleSwitch)
+ {
+ ReplaceHostPlayerInstance();
+ if (player == null)
+ {
+ Debug.LogError("[AnimSceneBootstrap] No CECHostPlayer after host replace — aborting.");
+ return;
+ }
+ }
+
+ if (!_initStaticResDone)
+ {
+ CECPlayer.InitStaticRes();
+ _initStaticResDone = true;
+ }
+
+ if (player != null)
+ {
+ player.AnimSceneRebindPlayerActions();
+ AnimSceneApplyFashionPathCompatIfEnabled();
+ }
+
+ GameObject previousModel = null;
+ if (player != null && player.m_pPlayerCECModel != null)
+ {
+ previousModel = player.m_pPlayerCECModel.m_pPlayerModel;
+ }
+
+ Debug.Log($"[AnimSceneBootstrap] Loading model profession={prof} gender={gen}...");
+ await player.SetPlayerModel(prof, gen);
+ Debug.Log("[AnimSceneBootstrap] SetPlayerModel pipeline finished.");
+
+ if (player != null)
+ {
+ EC_Game.GetGameRun()?.RegisterAnimSceneHostPlayer(player);
+ }
+
+ if (applyWeaponAfterModelLoad && player != null)
+ {
+ ApplyWeaponForActiveSlot();
+ }
+
+ AnimSceneBindPlayerVisualIfEnabled();
+
+ if (destroyPreviousModelAfterSwitch && previousModel != null && player != null &&
+ player.m_pPlayerCECModel != null)
+ {
+ GameObject current = player.m_pPlayerCECModel.m_pPlayerModel;
+ if (previousModel != current)
+ {
+ Object.Destroy(previousModel);
+ Debug.Log($"[AnimSceneBootstrap] Destroyed previous major model: {previousModel.name}");
+ }
+ }
+ }
+ catch (System.Exception ex)
+ {
+ Debug.LogError($"[AnimSceneBootstrap] Bootstrap failed: {ex}");
+ }
+ finally
+ {
+ _busy = false;
+ }
+ }
+ }
+}
diff --git a/Assets/PerfectWorld/Scripts/Common/DataProcess/ElementDataManProvider.cs b/Assets/PerfectWorld/Scripts/Common/DataProcess/ElementDataManProvider.cs
index 905b313812..9ccb5ab1fd 100644
--- a/Assets/PerfectWorld/Scripts/Common/DataProcess/ElementDataManProvider.cs
+++ b/Assets/PerfectWorld/Scripts/Common/DataProcess/ElementDataManProvider.cs
@@ -11,6 +11,11 @@ namespace BrewMonster
private static ElementDataManProvider _instance;
private elementdataman _elementDataMan;
+ ///
+ /// True after succeeds. Bootstrap scripts can poll this before / .
+ ///
+ public static bool IsDataLoaded { get; private set; }
+
public static elementdataman GetElementDataMan()
{
return _instance._elementDataMan;
@@ -25,7 +30,7 @@ namespace BrewMonster
{
_elementDataMan = new();
_instance = this;
-
+ IsDataLoaded = false;
try
{
while (!AddressableManager.Instance.IsInitialized())
@@ -41,6 +46,7 @@ namespace BrewMonster
}
else
{
+ IsDataLoaded = true;
BMLogger.Log("ElementDataManProvider: Successfully loaded element data");
// Build suite equip tab now that data is loaded
// 数据加载完成后构建套装装备表
@@ -55,6 +61,7 @@ namespace BrewMonster
public void Dispose()
{
+ IsDataLoaded = false;
_elementDataMan = null;
_instance = null;
}
diff --git a/Assets/PerfectWorld/Scripts/Debug/SkillTriggerPanel.cs b/Assets/PerfectWorld/Scripts/Debug/SkillTriggerPanel.cs
new file mode 100644
index 0000000000..ad24ef7b4b
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Debug/SkillTriggerPanel.cs
@@ -0,0 +1,437 @@
+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;
+
+ ///
+ /// 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;
+ }
+
+ private void OnDestroy()
+ {
+ 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);
+ }
+
+ /// 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 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
+ /// seconds, injects all config skills directly from SkillStub.map so the panel works offline.
+ ///
+ /// 每 0.5 秒轮询,直到主角已有技能且 CECGameUIMan 初始化完毕,再调用一次 Refresh()。
+ /// 若 k_InjectFallbackSecs 秒内未收到服务端技能数据,则直接从 SkillStub.map 注入所有配置技能,以便离线使用。
+ ///
+ 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();
+ }
+
+ ///
+ /// 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();
+ }
+
+ ///
+ /// 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;
+ }
+
+ // 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();
+ 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