From 2a7f9602dbedf0f62d9f8cf5fc685a01d8e23214 Mon Sep 17 00:00:00 2001 From: Tran Hai Nam Date: Tue, 19 May 2026 10:46:26 +0700 Subject: [PATCH 1/4] Add anim test scene --- .../Scripts/AnimScenePlayerBootstrap.cs | 313 +++++++++++++ .../DataProcess/ElementDataManProvider.cs | 9 +- .../Scripts/Debug/SkillTriggerPanel.cs | 437 ++++++++++++++++++ .../Scripts/Debug/SkillTriggerPanel.cs.meta | 2 + .../Editor/AnimScenePlayerBootstrapEditor.cs | 155 +++++++ .../AnimScenePlayerBootstrapEditor.cs.meta | 2 + .../Scripts/Managers/CECNPCMan.cs | 6 +- .../Scripts/Managers/InGameGraphicOption.cs | 67 ++- .../Scripts/Move/CECPlayer.AnimSceneWeapon.cs | 153 ++++++ .../Move/CECPlayer.AnimSceneWeapon.cs.meta | 2 + .../Scripts/NPC/CECMonsterTest.cs | 31 ++ .../Scripts/NPC/CECMonsterTest.cs.meta | 2 + Assets/PerfectWorld/Scripts/NPC/CECNPC.cs | 8 + Assets/PerfectWorld/Scripts/UI/AUIManager.cs | 5 + .../Scripts/UI/Dialogs/CDlgSkillAction.cs | 10 + Assets/Scripts/CECHostPlayer.Skill.cs | 51 ++ Assets/Scripts/CECUIManager.cs | 10 + Assets/Scripts/EC_GameRun.cs | 14 + 18 files changed, 1272 insertions(+), 5 deletions(-) create mode 100644 Assets/PerfectWorld/Scripts/AnimScenePlayerBootstrap.cs create mode 100644 Assets/PerfectWorld/Scripts/Debug/SkillTriggerPanel.cs create mode 100644 Assets/PerfectWorld/Scripts/Debug/SkillTriggerPanel.cs.meta create mode 100644 Assets/PerfectWorld/Scripts/Editor/AnimScenePlayerBootstrapEditor.cs create mode 100644 Assets/PerfectWorld/Scripts/Editor/AnimScenePlayerBootstrapEditor.cs.meta create mode 100644 Assets/PerfectWorld/Scripts/Move/CECPlayer.AnimSceneWeapon.cs create mode 100644 Assets/PerfectWorld/Scripts/Move/CECPlayer.AnimSceneWeapon.cs.meta create mode 100644 Assets/PerfectWorld/Scripts/NPC/CECMonsterTest.cs create mode 100644 Assets/PerfectWorld/Scripts/NPC/CECMonsterTest.cs.meta 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