using BrewMonster; using BrewMonster.Managers; using CSNetwork.GPDataType; using CSNetwork.Protocols; using System.Collections.Generic; using System.Threading.Tasks; using BrewMonster.Scripts; using UnityEngine; public class CECNPCModelDefaultPolicy : CECNPCModelPolicy { CECNPC m_pNPC; CECModel NPCModel => m_pNPC?.GetModel(); int m_nBrushes; A3DAABB m_CHAABB; // AABB Updated with m_ppBrushes // number of brush object used in collision // [中文] 状态效果 GFX 对象缓存,键为 (路径 + 挂点名) // [English] Active state-effect GFX objects, keyed by (path + hook name) private Dictionary _stateGfxObjects = new Dictionary(); // [中文] 挂点 Transform 缓存,键为挂点名称 // [English] Hook transform cache, keyed by bone name private Dictionary _hookCache = new Dictionary(); public CECNPCModelDefaultPolicy(CECNPC pNPC) { m_pNPC = pNPC; } static bool IsMoveAction(int iAction) { return iAction == (int)NPCActionIndex.ACT_WALK || iAction == (int)NPCActionIndex.ACT_RUN || iAction == (int)NPCActionIndex.ACT_NPC_WALK || iAction == (int)NPCActionIndex.ACT_NPC_RUN; } public string GetActionName(int iAct, bool bAttackStart = false) { // Tạo builder thay cho static char[128] var sb = new System.Text.StringBuilder(); // Thêm tên base action sb.Append(CECNPC.GetBaseActionName(iAct)); // Nếu là attack action thì thêm hậu tố if (CECNPC.IsAttackAction(iAct)) { sb.Append(bAttackStart ? EC_Utility.FixGBKString("Æð") : EC_Utility.FixGBKString("Âä")); } // Xử lý loại bỏ dấu nháy kép (nếu có) string result = sb.ToString().Replace("\"", ""); // Trả về kết quả đã clean return result; } public override void ClearComActFlag(bool bSignalCurrent) { if (NPCModel != null) NPCModel.ClearComActFlag(bSignalCurrent); } public override bool PlayAttackAction(int nAttackSpeed, CECAttackEvent attackEvent) { int iAction = m_pNPC.IsMonsterOrPet() ? (int)NPCActionIndex.ACT_ATTACK1 + UnityEngine.Random.Range(0, 2) : (int)NPCActionIndex.ACT_NPC_ATTACK; if (!PlayModelAction(iAction, true, attackEvent)) { return false; } return true; } public override void StopChannelAction() { if (NPCModel != null) NPCModel.StopChannelAction(0, true); } public override bool GetCHAABB(ref A3DAABB aabb) { bool bRet = (false); if (HasCHAABB()) { aabb = m_CHAABB; // m_CHAABB ¸ù¾Ý͹°üÉϵĶ¥µãλÖÃÀ´¼ÆËã°üΧºÐ£¬¸ü׼ȷ²¢Óë͹°ü¼ì²â±£³ÖÒ»Ö // ²»ÄÜʹÓà GetPos() À´µ÷Õû m_CHAABB ºó×÷Ϊ¼ÆËã½á¹û·µ»Ø //aabb.Center = GetPos() + A3DVECTOR3(0.0f, m_CHAABB.Extents.y, 0.0f); //aabb.CompleteMinsMaxs(); bRet = true; } return bRet; } bool HasCHAABB() { return NPCModel != null && NPCModel.HasCHAABB() && m_nBrushes > 0; } public override bool PlayModelAction(int iAction, bool bRestart, CECAttackEvent cECAttackEvent) { /* if (m_pNPCModel == null) { return false; } */ bool result = false; bool ignoreRef = false; if (iAction == (int)NPCActionIndex.ACT_WOUNDED) { string szAct = GetActionName(iAction); if (_npcVisual.IsAnimationExist(szAct)) { szAct = GetActionName((int)NPCActionIndex.ACT_WOUNDED2); } result = _npcVisual.TryPlayAction(szAct, cECAttackEvent); } else if (iAction == (int)NPCActionIndex.ACT_ATTACK1 || iAction == (int)NPCActionIndex.ACT_ATTACK2) { //bool bHideFX = !CECOptimize::Instance().GetGFX().CanShowAttack(m_pNPC->GetNPCID(), m_pNPC->GetClassID()); result = _npcVisual.TryPlayAction(GetActionName(iAction, true), cECAttackEvent, true); if (result) { string szAct = GetActionName(iAction, false); string szAct2 = GetActionName((int)NPCActionIndex.ACT_GUARD); if (_npcVisual.IsAnimationExist(szAct)) { NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct, ref ignoreRef, 0, 0, false, false, false); } if (_npcVisual.IsAnimationExist(szAct2)) { NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); } } } else if (iAction == (int)NPCActionIndex.ACT_IDLE) { result = _npcVisual.TryPlayAction(GetActionName(iAction), cECAttackEvent); if (result) { string szAct2 = GetActionName((int)NPCActionIndex.ACT_STAND); if (_npcVisual.IsAnimationExist(szAct2)) { NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); } //m_pNPCModel->QueueAction(GetActionName((int)NPCActionIndex.ACT_STAND)); } } else if (iAction == (int)NPCActionIndex.ACT_NPC_IDLE1 || iAction == (int)NPCActionIndex.ACT_NPC_IDLE2) { result = _npcVisual.TryPlayAction(GetActionName(iAction), cECAttackEvent); if (result) { string szAct2 = GetActionName((int)NPCActionIndex.ACT_NPC_STAND); if (_npcVisual.IsAnimationExist(szAct2)) { NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); } //m_pNPCModel->QueueAction(GetActionName((int)NPCActionIndex.ACT_NPC_STAND)); } } else if (iAction == (int)NPCActionIndex.ACT_NPC_ATTACK) { //bool bHideFX = !CECOptimize::Instance().GetGFX().CanShowAttack(m_pNPC->GetNPCID(), m_pNPC->GetClassID()); result = _npcVisual.TryPlayAction(GetActionName(iAction, true), cECAttackEvent); ; if (result) { string szAct = GetActionName(iAction, false); string szAct2 = GetActionName((int)NPCActionIndex.ACT_NPC_STAND); if (_npcVisual.IsAnimationExist(szAct)) { NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct, ref ignoreRef, 0, 0, false, false, false); } if (_npcVisual.IsAnimationExist(szAct2)) { NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); } } } else if (iAction == (int)NPCActionIndex.ACT_COMMON_BORN) { result = _npcVisual.TryPlayAction(GetActionName(iAction), cECAttackEvent); if (result) { string szAct2 = GetActionName((int)NPCActionIndex.ACT_STAND); if (_npcVisual.IsAnimationExist(szAct2)) { NPCModel?.QueueAction(_npcVisual.GetNPCINFO, szAct2, ref ignoreRef, 300); } //m_pNPCModel->QueueAction(GetActionName((int)NPCActionIndex.ACT_STAND)); } } else { result = _npcVisual.TryPlayAction( GetActionName(iAction), cECAttackEvent, isHit: false, bRestart: bRestart, bLoop: true); } return result; } public override bool IsPlayingAction() { return _npcVisual.IsPlayAnimation(); } public override bool IsPlayingAction(int iAction) { return _npcVisual.IsPlayAnimation(GetActionName(iAction)); } public override bool HasAction(int iAction) { return _npcVisual.IsAnimationExist(GetActionName(iAction)); } public override void SetNpcVisual(NPCVisual npcVisual) { _npcVisual = npcVisual; // [中文] 模型重新加载时清除挂点缓存 // [English] Clear hook cache when the NPC model is reloaded _hookCache.Clear(); } public override void SetDefaultPickAABBExt(A3DVECTOR3 vExt) { } // ----------------------------------------------------------------------- // IsModelLoaded / PlayGfx / RemoveGfx // ----------------------------------------------------------------------- // [中文] 判断 NPC 模型是否已加载并激活(作为状态 GFX 的前置守卫) // [English] Returns true when the NPC model is present and the game object is active public override bool IsModelLoaded() { return m_pNPC != null && m_pNPC.gameObject.activeInHierarchy && NPCModel != null && NPCModel.transform != null; } // [中文] 异步加载并挂载状态效果 GFX 到指定挂点;以 (路径+挂点) 为键去重 // [English] Async-load and attach a state-effect GFX to the given hook; deduplicated by (path+hook) public override async void PlayGfx(string szPath, string szHook) { if (string.IsNullOrEmpty(szPath)) return; string key = szPath + szHook; if (_stateGfxObjects.ContainsKey(key)) return; GameObject prefab = await AddressableManager.Instance.LoadPrefabAsync(szPath); if (prefab == null) { BMLogger.LogWarning($"[NPC GFX] Failed to load prefab: {szPath}"); return; } // [中文] 检查 NPC 是否仍然存在(异步加载期间可能被销毁) // [English] Guard against the NPC being destroyed during async load if (m_pNPC == null) return; // [中文] 查找挂点,找不到则回退到 NPC 根节点 // [English] Locate hook bone; fall back to NPC root if not found Transform parent = FindHookTransform(szHook) ?? m_pNPC.transform; GameObject vfx = Object.Instantiate(prefab, parent); if (vfx == null) return; vfx.transform.localPosition = Vector3.zero; vfx.transform.localRotation = Quaternion.Euler(-180f, -90f, 90f); _stateGfxObjects[key] = vfx; BMLogger.Log($"[NPC GFX] Playing: {szPath}, hook: {szHook}"); } // [中文] 销毁并移除指定状态效果 GFX // [English] Destroy and untrack a state-effect GFX public override void RemoveGfx(string szPath, string szHook) { string key = szPath + szHook; if (_stateGfxObjects.TryGetValue(key, out GameObject vfx) && vfx != null) { Object.Destroy(vfx); _stateGfxObjects.Remove(key); } } // ----------------------------------------------------------------------- // Hook lookup — child-name traversal (no SkeletonBuilder on NPC) // 挂点查找 —— 遍历子节点名称(NPC 无 SkeletonBuilder) // ----------------------------------------------------------------------- // [中文] 在 NPC GameObject 的子节点中按名称递归查找挂点,结果缓存以提升性能 // [English] Recursively find a child transform by name under the NPC; results are cached private Transform FindHookTransform(string hookName) { if (string.IsNullOrEmpty(hookName)) return null; if (_hookCache.TryGetValue(hookName, out Transform cached) && cached != null) return cached; Transform root = m_pNPC?.transform; if (root == null) return null; Transform found = FindChildByName(root, hookName); if (found != null) _hookCache[hookName] = found; return found; } // [中文] 深度优先递归遍历子节点,按名称查找 // [English] Depth-first recursive child search by name private static Transform FindChildByName(Transform parent, string name) { foreach (Transform child in parent) { if (child.name == name) return child; Transform found = FindChildByName(child, name); if (found != null) return found; } return null; } }