using BrewMonster; using BrewMonster.Managers; using BrewMonster.Scripts; using CSNetwork; using CSNetwork.GPDataType; using ModelRenderer.Scripts.Common; using System; using System.Threading.Tasks; using BrewMonster.Scripts.Chat; using System.Collections.Generic; using UnityEngine; using BrewMonster.Scripts.Skills; using BrewMonster.Network; using Animancer; public class CECNPC : CECObject { [SerializeField] protected INFO m_NPCInfo; protected private uint m_dwStates; protected private uint m_dwStates2; protected private A3DVECTOR3 m_vServerPos; protected private int m_iRandomProp; protected private int m_iMoveEnv; protected int m_idMaster; protected string m_strName; protected int m_idOwnerFaction; protected float m_fDistToHost; protected float m_fDistToHostH; protected OtherPlayer_Move_Info m_cdr; protected float m_fTouchRad = 1f; protected A3DVECTOR3 m_vMoveDir; protected int m_iPassiveMove; protected bool m_bStopMove; protected bool m_bStartFight; protected int[] m_aWorks = new int[4]; protected int m_iAction; protected int m_idSelTarget; protected int m_iCurWorkType; protected int m_iCurWork; protected uint m_nPolicyActionIntervalTimer; protected CECCounter m_DisappearCnt = new CECCounter(); protected CECCounter m_TransCnt = new CECCounter(); protected int m_StartDisappearCnt; protected bool m_bAboutToDie; protected A3DVECTOR3 m_vStopDir; protected ROLEEXTPROP m_ExtProps; protected ROLEBASICPROP m_BasicProps; protected CECNPCModelPolicy m_pNPCModelPolicy; protected CECPolicyAction m_pPolicyAction; public int m_iMMIndex; // [中文] 上次从服务器收到的完整扩展状态位图(用于 SetNewExtendStates 中的 Array.Copy) // [English] Last full extended-state bitmask received from server (copied in SetNewExtendStates) protected uint[] m_aExtStates = new uint[(int)OBJECT_EXT_STATE.OBJECT_EXT_STATE_COUNT]; // [中文] 当前已显示的扩展状态位图(与新数据做差分,控制 GFX 增删) // [English] Currently displayed extended-state bitmask (diffed against new data to add/remove GFX) protected uint[] m_aExtStatesShown = new uint[(int)OBJECT_EXT_STATE.OBJECT_EXT_STATE_COUNT]; public int m_idAttackTarget; /// Buff/debuff icon states from server (ICON_STATE_NOTIFY). Same role as CECPlayer.m_aIconStates. public List m_aIconStates = new List(); protected UINPC m_npcUI; private CECModel m_pNPCCECModel; // CECModel instance for hook system / 用于挂点系统的CECModel实例 CECCounter m_IdleCnt = new CECCounter(); protected float m_nFightTimeLeft = 0; // Work Fight time left [SerializeField] protected float m_fMoveSpeed; [SerializeField] protected CharacterController _characterController; [SerializeField] protected bool isDebug; [SerializeField] protected NPCVisual npcVisual; GameObject m_modelVisual = null; protected static CECStringTab m_ActionNames; /* public string NameNPC => m_strName; public string ROLEBASICPROP => m_strName;*/ protected virtual void Awake() { m_DisappearCnt = new CECCounter(); m_IdleCnt = new CECCounter(); m_TransCnt = new CECCounter(); m_aWorks = new int[4]; } public virtual void SetUpCECNPC(CECNPCMan pNPCMan) { base.SetUpCECObject(); m_vServerPos = new A3DVECTOR3(); m_iCID = (int)Class_ID.OCID_NPC; m_DisappearCnt.SetPeriod(5000); m_IdleCnt.SetPeriod(25000); m_IdleCnt.IncCounter(UnityEngine.Random.Range(0, 6000)); m_pNPCModelPolicy = new CECNPCModelDefaultPolicy(this); } public string GetName() { return m_strName; } public virtual bool Init(int tid, in info_npc info, ReadOnlySpan packet, int infoOffset) { m_NPCInfo.nid = info.nid; m_NPCInfo.tid = tid; m_NPCInfo.vis_tid = info.vis_tid; m_dwStates = (uint)info.state; m_dwStates2 = (uint)info.state2; m_vServerPos = info.pos; m_iRandomProp = (info.state & 0x0f00) >> 8; m_pNPCModelPolicy = new CECNPCModelDefaultPolicy(this); m_idSelTarget = 0; m_iCurWorkType = -1; m_fMoveSpeed = 1.0f; m_iCurWork = 0; m_bStartFight = false; m_bAboutToDie = false; m_StartDisappearCnt = 0; m_BasicProps = new ROLEBASICPROP(true); // struct mặc định, các trường số = 0, mảng đã tạo m_ExtProps = new ROLEEXTPROP(true); _characterController = GetComponent(); m_iMoveEnv = (int)((info.state & PlayerNPCState.GP_STATE_NPC_FLY) != 0 ? Move_environment.MOVEENV_AIR : (info.state & PlayerNPCState.GP_STATE_NPC_SWIM) != 0 ? Move_environment.MOVEENV_WATER : Move_environment.MOVEENV_GROUND); npcVisual = GetComponent(); if (npcVisual == null) { BMLogger.LogError("HoangDev npcVisual"); } m_pNPCModelPolicy.SetNpcVisual(npcVisual); m_npcUI = GetComponentInChildren(); // 2) Cắt “đuôi” ngay sau phần cố định info_npc int fixedSize = System.Runtime.InteropServices.Marshal.SizeOf(); var tail = packet.Slice(infoOffset); var r = new ByteReader(tail); // 3) Đọc theo cờ state, giống C++ (pData tăng dần) // EXTEND_PROPERTY var ojexitStateCount = (uint)OBJECT_EXT_STATE.OBJECT_EXT_STATE_COUNT; var ext = new uint[ojexitStateCount]; if ((info.state & PlayerNPCState.GP_STATE_EXTEND_PROPERTY) != 0) r.ReadInto(ext); SetNewExtendStates(0, ext, (int)ojexitStateCount); // PET m_idMaster = 0; if ((info.state & PlayerNPCState.GP_STATE_NPC_PET) != 0) m_idMaster = r.ReadInt32(); // NAME if ((info.state & PlayerNPCState.GP_STATE_NPC_NAME) != 0) { BMLogger.LogError($" PlayerNPCState.GP_STATE_NPC_NAME "); byte len = r.ReadByte(); if (len > 0) { // ACHAR thường là UTF-16LE → len là số byte var nameBytes = r.ReadBytes(len); m_strName = System.Text.Encoding.Unicode.GetString(nameBytes); // BMLogger.LogError($"HoangDev:m_npcUI = {m_npcUI}, NPC Name = " + m_strName); if (m_npcUI != null) { m_npcUI.SetName(m_strName); } } } SetSelectable((info.state & PlayerNPCState.GP_STATE_FORBIDBESELECTED) == 0); // MULTIOBJ_EFFECT if ((info.state & PlayerNPCState.GP_STATE_MULTIOBJ_EFFECT) != 0) { int n = r.ReadInt32(); for (int i = 0; i < n; i++) { int idTarget = r.ReadInt32(); char cType = (char)r.ReadByte(); //AddMultiObjectEffect(idTarget, cType); } } // MAFIA m_idOwnerFaction = 0; if ((info.state & PlayerNPCState.GP_STATE_NPC_MAFIA) != 0) m_idOwnerFaction = r.ReadInt32(); m_cdr.fStepHeight = 0.4f; m_cdr.vVelocity.Clear(); var pHost = CECGameRun.Instance.GetHostPlayer(); if (pHost != null) { m_fDistToHost = Vector3.Distance(EC_Utility.ToVector3(m_vServerPos), pHost.transform.position); m_fDistToHostH = Vector2.Distance( new Vector2(m_vServerPos.x, m_vServerPos.z), new Vector2(pHost.transform.position.x, pHost.transform.position.z)); } return true; } public void ProcessMessage(ECMSG Msg) { switch (Msg.dwMsg) { case long value when value == EC_MsgDef.MSG_NM_NPCATKRESULT: OnMsgNPCAtkResult(Msg); break; case long value when value == EC_MsgDef.MSG_NM_NPCSTARTPLAYACTION: OnMsgNPCStartPlayAction(Msg); break; case long value when value == EC_MsgDef.MSG_NM_NPCEXTSTATE: OnMsgNPCExtState(Msg); break; //case long value when value == EC_MsgDef.MSG_NM_NPCCASTSKILL: OnMsgNPCCastSkill(Msg); break; //case long value when value == EC_MsgDef.MSG_NM_ENCHANTRESULT: OnMsgNPCEnchantResult(Msg); break; //case long value when value == EC_MsgDef.MSG_NM_NPCROOT: OnMsgNPCRoot(Msg); break; //case long value when value == EC_MsgDef.MSG_NM_NPCSKILLRESULT: OnMsgNPCSkillResult(Msg); break; case long value when value == EC_MsgDef.MSG_NM_NPCLEVELUP: OnMsgNPCLevel(Msg); break; case long value when value == EC_MsgDef.MSG_NM_NPCINVISIBLE: OnMsgNPCInvisible(Msg); break; //case long value when value == EC_MsgDef.MSG_NM_NPCSTOPPLAYACTION: OnMsgNPCStopPlayAction(Msg); break; //case long value when value == EC_MsgDef.MSG_NM_MULTIOBJECT_EFFECT: OnMsgNPCMultiObjectEffect(Msg); break; } } private void OnMsgNPCExtState(ECMSG Msg) { if (Convert.ToInt32(Msg.dwParam2) == CommandID.ICON_STATE_NOTIFY) { var cmd = new cmd_icon_state_notify(); if (!cmd.Initialize((byte[])Msg.dwParam1)) return; if (cmd.id != GetNPCID()) return; m_aIconStates = cmd.states ?? new List(); if (m_aIconStates.Count > 1) { m_aIconStates.Sort((a, b) => { if (a.id < b.id) return -1; if (a.id > b.id) return 1; return 0; }); } } if (Convert.ToInt32(Msg.dwParam2) == CommandID.UPDATE_EXT_STATE) { cmd_update_ext_state pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); if (pCmd.id == m_NPCInfo.nid) SetNewExtendStates(0, pCmd.states, (int)OBJECT_EXT_STATE.OBJECT_EXT_STATE_COUNT); } // UPDATE_EXT_STATE for NPC is handled separately from icon strip; extend if NPC state GFX is ported. } private void OnMsgNPCLevel(ECMSG Msg) { cmd_level_up pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); //m_pNPCModelPolicy.PlayGfx(res_GFXFile(RES_GFX_LEVELUP), NULL); } // [中文] 处理服务器发来的扩展状态更新消息,驱动状态 GFX 的添加与移除 // [English] Handle server ext-state update message — drives state GFX add/remove // [中文] 更新扩展状态并刷新 GFX 显示 // [English] Update the ext-state arrays and refresh GFX display public void SetNewExtendStates(int start, uint[] pData, int count) { if (pData == null || start + count > (int)OBJECT_EXT_STATE.OBJECT_EXT_STATE_COUNT) return; ShowExtendStates(start, pData, count); Array.Copy(pData, 0, m_aExtStates, start, count); } // [中文] 清除所有已显示的状态效果 GFX(传入全零位图触发全量移除) // [English] Clear all currently displayed state-effect GFX (pass all-zero bitmap to remove everything) private void ClearShowExtendStates() { ShowExtendStates(0, new uint[(int)OBJECT_EXT_STATE.OBJECT_EXT_STATE_COUNT], (int)OBJECT_EXT_STATE.OBJECT_EXT_STATE_COUNT, true); } // [中文] 对比旧位图与新位图,逐位差分,新增或移除对应状态效果 GFX // [English] Diff old vs new bitmask bit-by-bit and add/remove state-effect GFX accordingly private void ShowExtendStates(int start, uint[] pData, int count, bool bIgnoreOptimize = false) { if (pData == null || start + count > (int)OBJECT_EXT_STATE.OBJECT_EXT_STATE_COUNT) return; // [中文] 模型必须已加载才能挂载 GFX // [English] Model must be loaded before GFX can be attached if (!m_pNPCModelPolicy.IsModelLoaded()) return; // [中文] 策划联入\状态效果\ —— 状态效果 GFX 的基础路径(与 C++ 保持一致) // [English] Designer-linked state-effect GFX base path (matches C++) const string szBasePath = "gfx/策划联入/状态效果/"; const int bitSize = sizeof(uint) * 8; for (int index = 0; index < count; index++) { int idState = index + start; for (int i = 0; i < bitSize; i++) { uint dwMask = 1u << i; uint dwFlag1 = m_aExtStatesShown[idState] & dwMask; // currently shown uint dwFlag2 = pData[index] & dwMask; // incoming // [中文] 两者相同(都激活或都未激活),无需处理 // [English] Both unchanged — nothing to do if ((dwFlag1 == 0 && dwFlag2 == 0) || (dwFlag1 != 0 && dwFlag2 != 0)) continue; // [中文] 查询可见状态定义(NPC 固定使用 profession 127) // [English] Query visible state definition (NPCs always use profession 127) VisibleState pvs = GNET.QueryVisibleState(127, i + idState * bitSize); if (pvs == null) continue; string strEffect = pvs.GetEffect(); if (string.IsNullOrEmpty(strEffect)) continue; string strGFXFile = szBasePath + strEffect; if (dwFlag1 != 0) { // [中文] 移除旧状态效果 GFX // [English] Remove old state GFX m_pNPCModelPolicy.RemoveGfx(strGFXFile, "HH_头顶" /*pvs.GetHH()*/); } else { // [中文] 添加新状态效果 GFX // [English] Add new state GFX BMLogger.Log($"[HoangDev NPC StateGFX] Playing: {strGFXFile}, hook: {pvs.GetHH()}"); m_pNPCModelPolicy.PlayGfx(strGFXFile,"HH_头顶" /*pvs.GetHH()*/); } } } Array.Copy(pData, 0, m_aExtStatesShown, start, count); } private void OnMsgNPCInvisible(ECMSG Msg) { cmd_object_invisible pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); if (pCmd.invisible_degree > 0) { m_dwStates |= PlayerNPCState.GP_STATE_INVISIBLE; } else { m_dwStates &= ~(uint)PlayerNPCState.GP_STATE_INVISIBLE; } } private void OnMsgNPCStartPlayAction(ECMSG Msg) { if (IsInPolicyAction()) CheckStopPolicyAction(); cmd_object_start_play_action cmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); StartWork((int)WorkType.WT_INTERRUPT, (int)WorkID.WORK_POLICYACTION, 0, cmd); } private void CheckStopPolicyAction() { if (!IsInPolicyAction()) return; WorkFinished((int)WorkID.WORK_POLICYACTION); if (m_pPolicyAction != null) { m_pNPCModelPolicy.StopChannelAction(); m_pPolicyAction = null; m_nPolicyActionIntervalTimer = 0; } } private bool IsInPolicyAction() { return m_pPolicyAction != null; } public void OnMsgAttackHostResult(int idHost, int nDamage, int nFlag, int nSpeed) { //BMLogger.LogError("HoangDev: OnMsgAttackHostResultNPC"); if (!IsDead()) { // 🔹 Quay mặt về mục tiêu NPCTurnFaceTo(idHost); m_idAttackTarget = idHost; // 🔹 Nếu là quái vật thì bắt đầu “work” đánh nhau if (IsMonsterNPC()) { var pMonster = this as CECMonster; var pMonsterEssence = (MONSTER_ESSENCE?)pMonster?.GetDBEssence(); if (pMonsterEssence != null) { StartWork( (int)WorkType.WT_NORMAL, (int)WorkID.WORK_FIGHT, (uint)(pMonsterEssence.Value.attack_speed * 1000) ); } } } // 🔹 Phát hiệu ứng tấn công PlayAttackEffect(idHost, 0, 0, nDamage, (uint)nFlag, nSpeed); // 🔹 Nếu là pet thì phát câu thoại chiến đấu /* if (!IsDead() && IsPetNPC()) { OnPetSays(CECPetWords.TW_FIGHT); }*/ } public void OnMsgNPCAtkResult(ECMSG msg) { var pCmd = GPDataTypeHelper.FromBytes(msg.dwParam1 as byte[]); if (!IsDead()) { // Face to target NPCTurnFaceTo(pCmd.target_id); m_idAttackTarget = pCmd.target_id; // Start a fight work if (IsMonsterNPC()) { var pMonster = (CECMonster)this; var pMonsterEssence = pMonster.GetDBEssence(); StartWork((int)WorkType.WT_NORMAL, (int)WorkID.WORK_FIGHT, (uint)(pMonsterEssence.attack_speed * 1000)); } } var pHost = CECGameRun.Instance.GetHostPlayer(); int iDamage = -1; // Attacker is host's pet if (IsPetNPC() && GetMasterID() == pHost.GetCharacterID()) { iDamage = pCmd.damage; } // Attack target is host's pet else if (GPDataTypeHelper.ISNPCID(pCmd.target_id)) { var pTarget = EC_ManMessageMono.Instance.CECNPCMan.GetNPC(pCmd.target_id); if (pTarget != null && pTarget.GetMasterID() == pHost.GetCharacterID()) iDamage = pCmd.damage; } // Common melee attack result, idSkill = 0 PlayAttackEffect( pCmd.target_id, 0, // idSkill 0, // dwUserData iDamage, (uint)pCmd.attack_flag, pCmd.speed * 50 ); /* if (!IsDead() && IsPetNPC()) { OnPetSays(CECPetWords.TW_FIGHT); }*/ } public void PlayAttackEffect( int idTarget, int idSkill, int skillLevel, int nDamage, uint dwModifier, int nAttackSpeed, int nSection = 0) { if (m_pNPCModelPolicy == null) return; m_bStartFight = true; var pAttacksMan = CECAttacksMan.Instance; // --- Melee Attack --- if (idSkill == 0) { int nTimeFly = 10; if (IsMonsterNPC()) { var pData = ((CECMonster)this).GetDBEssence(); if (pData.short_range_mode == 0) // cận chiến nTimeFly = 700; } /* if (IsPetNPC()) { var pData = ((CECPet)this).GetDBEssence() as PET_ESSENCE; if (pData != null && !string.IsNullOrEmpty(pData.file_gfx_short)) nTimeFly = 700; }*/ if (pAttacksMan.FindAttackByAttacker(m_NPCInfo.nid) != null) { // signal early attack event ClearComActFlag(true); } // Melee attack var pAttack = pAttacksMan.AddMeleeAttack( m_NPCInfo.nid, idTarget, 0, dwModifier, nDamage, nTimeFly ); if (!IsDead() && (dwModifier & (uint)MOD.MOD_RETORT) == 0 && (dwModifier & (uint)MOD.MOD_ATTACK_AURA) == 0 && PlayAttackAction(nAttackSpeed, pAttack) && (dwModifier & (uint)MOD.MOD_BEAT_BACK) == 0) { // normal flow } else { pAttack.m_bSignaled = true; } } /* // --- Skill Attack --- else { if (skillLevel == 0) { if (m_pCurSkill != null) skillLevel = m_pCurSkill.GetSkillLevel(); else skillLevel = 1; } // Try to find if there is already a skill attack event in attackman var attackerEvents = pAttacksMan.FindAttackByAttacker(m_NPCInfo.nid); if (attackerEvents != null) { var pAttack = attackerEvents.Find(idSkill, nSection); if (pAttack != null) { // thêm target vào skill attack hiện có pAttack.AddTarget(idTarget, dwModifier, nDamage); return; } else { attackerEvents.Signal(); } } // Begin a new skill attack var pNewAttack = pAttacksMan.AddSkillAttack( m_NPCInfo.nid, m_idCurSkillTarget, idTarget, 0, idSkill, skillLevel, dwModifier, nDamage ); if (pNewAttack != null) { pNewAttack.SetSkillSection(nSection); if (!IsDead() && (dwModifier & CECAttackEvent.MOD_RETORT) == 0 && (dwModifier & CECAttackEvent.MOD_ATTACK_AURA) == 0 && PlaySkillAttackAction(idSkill, nAttackSpeed, nSection, ref pNewAttack.m_bSignaled) && (dwModifier & CECAttackEvent.MOD_BEAT_BACK) == 0) { // normal case } else { pNewAttack.m_bSignaled = true; } } }*/ } private bool PlayAttackAction(int nAttackSpeed, CECAttackEvent attackevent) { return m_pNPCModelPolicy.PlayAttackAction(nAttackSpeed, attackevent); } public void NPCTurnFaceTo(int idTarget, float dwTime = 0.3f) { if (IsDirFixed()) { return; } // tower in war can not turn face to. /* if (IsMonsterNPC()) { int role_in_war = ((CECMonster)this)->GetDBEssence()->role_in_war; if (role_in_war == 2 || role_in_war == 5) return; }*/ TurnFaceTo(idTarget, dwTime); } protected override void Update() { base.Update(); switch (m_iCurWork) { case (int)WorkID.WORK_MOVE: TickWork_Move(Time.deltaTime); break; case (int)WorkID.WORK_STAND: TickWork_Stand(Time.deltaTime); break; case (int)WorkID.WORK_FIGHT: TickWork_Fight(Time.deltaTime); break; case (int)WorkID.WORK_SPELL: TickWork_Spell(Time.deltaTime); break; case (int)WorkID.WORK_DEAD: TickWork_Dead(Time.deltaTime); break; //case (int)WorkID.WORK_POLICYACTION: TickWork_PolicyAction(Time.deltaTime); break; } // Calculate distance to host player CECHostPlayer pHost = EC_ManMessageMono.Instance.EC_ManPlayer.GetHostPlayer(); if (pHost /*&& pHost.IsSkeletonReady()*/) { m_fDistToHost = CalcDist(pHost.GetPos(), true); m_fDistToHostH = CalcDist(pHost.GetPos(), false); if(m_modelVisual != null) { if(m_fDistToHostH < EC_Game.GetSettingViewDistanceNPC().fShow) { m_modelVisual.SetActive(true); } else if(m_fDistToHostH > EC_Game.GetSettingViewDistanceNPC().fHide) { m_modelVisual.SetActive(false); } } } if (IsDisappearing()) { // When m_DisappearCnt passed half length, start changing model's transparence float dwOldCnt = m_DisappearCnt.GetCounter(); m_DisappearCnt.IncCounter(Time.deltaTime * 1000); float dwHalf = m_DisappearCnt.GetPeriod() / 2; if (dwOldCnt < dwHalf && m_DisappearCnt.GetCounter() >= dwHalf) StartAdjustTransparency(-1.0f, 1.0f, dwHalf); } else { StartAdjustTransparency(-1.0f, GetTransparentLimit(), 500); } } private void TickWork_Dead(float deltaTime) { } private void TickWork_Spell(float deltaTime) { } private void TickWork_Fight(float deltaTime) { m_nFightTimeLeft -= deltaTime; if (m_nFightTimeLeft < 0) { m_nFightTimeLeft = 0; WorkFinished((int)WorkID.WORK_FIGHT); return; } NPCTurnFaceTo(m_idAttackTarget, 100); A3DVECTOR3 vDir = m_vServerPos - GetPos(); float fDist = vDir.Normalize(); if (fDist > 0.0001f) { float fMoveDist = 10.0f * deltaTime * 0.001f; if (fMoveDist > fDist) fMoveDist = fDist; SetPos(EC_Utility.ToVector3(GetPos() + vDir * fMoveDist)); } } private void TickWork_Stand(float deltaTime) { if (m_IdleCnt.IncCounter(deltaTime * 1000)) { m_IdleCnt.Reset(); if (IsMonsterOrPet()) { PlayModelAction((int)NPCActionIndex.ACT_IDLE); if (IsPetNPC() && !IsDead() && !IsDisappearing()) { //OnPetSays(CECPetWords::TW_REST); } } else PlayModelAction((int)NPCActionIndex.ACT_NPC_IDLE1 + UnityEngine.Random.Range(0, 2)); } } public void DestroySelf() { PrefabPoolManager.Instance.Despawn(gameObject); //Destroy(gameObject); } public float GetTransparentLimit() { if ((m_dwStates & (uint)PlayerNPCState.GP_STATE_INVISIBLE) != 0) { return 0.7f;//ÒþÉí } else if (!IsSelectable()) { return 0.5f;//ÎÞ·¨Ñ¡ÖÐ } return -1.0f; } bool StartAdjustTransparency(float fCur, float fDest, float dwTime) { // use current value for starting /*if (fCur < 0.0f) { if (!m_pNPCModelPolicy->GetTransparent(fCur)) { fCur = m_fCurTrans; } } if (fDest < 0.f) fDest = 0.f; // ignore the invalid params if (dwTime == 0 || fabs(fDest - m_fDstTrans) < 0.0001f || fabs(fDest - fCur) < 0.0001f) return false; m_fCurTrans = fCur; m_fDstTrans = fDest; m_fTransDelta = (fDest - m_fCurTrans) / dwTime; m_TransCnt.SetPeriod(dwTime); m_TransCnt.Reset();*/ return true; } public void TickWork_Move(float dwDeltaTime) { if (m_bAboutToDie) { WorkFinished((int)WorkID.WORK_MOVE); } else if (MovingTo(dwDeltaTime)) { if (!IsDirFixed()) { SetDestDirAndUp((m_vStopDir), g_vAxisY, 150); } WorkFinished((int)WorkID.WORK_MOVE); // when stopped, we should rebuild the convex brushes for collision detection. //RebuildTraceBrush(); } } public void Release() { // Release current skill if it exists /*if (m_pCurSkill) { delete m_pCurSkill; m_pCurSkill = NULL; }*/ // Clear extend states before model is released /* ClearShowExtendStates(); ::memset(m_aExtStates, 0, sizeof(m_aExtStates)); m_aIconStates.clear();*/ m_aIconStates?.Clear(); // [中文] 模型释放前先移除所有状态效果 GFX,并重置位图 // [English] Remove all state-effect GFX and reset bitmasks before the model is released ClearShowExtendStates(); Array.Clear(m_aExtStates, 0, m_aExtStates.Length); m_pNPCModelPolicy = null; PoolManager.Instance.Despawn(m_modelVisual); /*if (m_pPateName) { delete m_pPateName; m_pPateName = NULL; } if (m_pPateLastWords1) { delete m_pPateLastWords1; m_pPateLastWords1 = NULL; } if (m_pPateLastWords2) { delete m_pPateLastWords2; m_pPateLastWords2 = NULL; } if (m_pBubbleTexts) { delete m_pBubbleTexts; m_pBubbleTexts = NULL; }*/ m_pPolicyAction = null; m_nPolicyActionIntervalTimer = 0; /* for (MOEffectMAP::iterator it = m_mapMOEffect.begin(); it != m_mapMOEffect.end(); ++it) { A3DGFXExMan* pGFXExMan = g_pGame->GetA3DGFXExMan(); pGFXExMan->CacheReleasedGfx(it->second); } m_mapMOEffect.clear();*/ } public bool MovingTo(float deltaTime) { if (IsDisappearing()) return true; bool bRet = false; A3DVECTOR3 vCurPos = EC_Utility.ToA3DVECTOR3(transform.position); if (m_bStopMove) { // Tính hướng đến serverPos A3DVECTOR3 vDir = m_vServerPos - vCurPos; float fDist = vDir.Normalize(); A3DVECTOR3 vPos = vDir * m_fMoveSpeed * deltaTime; A3DVECTOR3 exPPos = vPos + vCurPos; float fMoveDelta = Vector3.Magnitude(EC_Utility.ToVector3(exPPos) - EC_Utility.ToVector3(vCurPos)); BMLogger.LogMono(this, $"HoangDev: m_vServerPos:{m_vServerPos},vCurPos:{vCurPos},vDir:{vDir},fDist:{fDist},vPos:{vPos},fMoveDelta:{fMoveDelta}"); if (fMoveDelta >= fDist) { // Already at destination _characterController.enabled = false; SetPos(EC_Utility.ToVector3(m_vServerPos)); _characterController.enabled = true; bRet = true; } else { _characterController.enabled = false; SetPos(EC_Utility.ToVector3(exPPos)); _characterController.enabled = true; FaceDirectionImmediate(EC_Utility.ToVector3(vPos)); } } else // đang move bình thường { float fDist = Vector3.Magnitude(EC_Utility.ToVector3(m_vServerPos) - EC_Utility.ToVector3(vCurPos)); if (IsLag(fDist)) { // Teleport nếu lag xa _characterController.enabled = false; SetPos(EC_Utility.ToVector3(m_vServerPos)); _characterController.enabled = true; m_vStopDir = EC_Utility.ToA3DVECTOR3(transform.forward); return true; } A3DVECTOR3 dir = m_vMoveDir; dir.Normalize(); var vDir = EC_Utility.ToVector3(dir); Vector3 moveDelta = vDir * m_fMoveSpeed * deltaTime; if (_characterController == null) { BMLogger.LogError("CECNPC.MovingTo _characterController == null " + gameObject.name); } _characterController.Move(moveDelta); // Thêm xoay theo trục Y FaceDirectionImmediate(vDir); } return bRet; } /// /// Xoay model NGAY LẬP TỨC theo hướng chỉ định (giữ nguyên trục Y đứng thẳng). /// public void FaceDirectionImmediate(Vector3 dir) { if (dir.sqrMagnitude > 0.0001f) { Vector3 flatDir = new Vector3(dir.x, 0, dir.z); if (flatDir.sqrMagnitude > 0.0001f) { transform.rotation = Quaternion.LookRotation(flatDir, Vector3.up); } } } /// /// Xoay model TỪ TỪ (mượt) theo hướng chỉ định (giữ nguyên trục Y đứng thẳng). /// rotateSpeed = tốc độ xoay (độ mượt), ví dụ 10f. /// deltaTime = Time.deltaTime trong Update. /// public void FaceDirectionSmooth(Vector3 dir, float rotateSpeed, float deltaTime) { if (dir.sqrMagnitude > 0.0001f) { Vector3 flatDir = new Vector3(dir.x, 0, dir.z); if (flatDir.sqrMagnitude > 0.0001f) { Quaternion targetRot = Quaternion.LookRotation(flatDir, Vector3.up); transform.rotation = Quaternion.Slerp(transform.rotation, targetRot, deltaTime * rotateSpeed); } } } public bool IsAboutToDie() { return m_bAboutToDie; } public void Killed(bool bDelay) { ClearComActFlag(true); m_dwStates |= PlayerNPCState.GP_STATE_CORPSE; // No delay die, enter disappear process immediately if (!bDelay) { Disappear(); } StartWork((int)WorkType.WT_NORMAL, (int)WorkID.WORK_DEAD, m_dwStates); SetUseGroundNormal(true); } public void Disappear() { FadeOut(); m_DisappearCnt.SetCounter(1); PlayModelAction((int)NPCActionIndex.ACT_NPC_DISAPPEAR); } public void ClearComActFlag(bool bSignalCurrent) { EventBus.PublishChannel(m_NPCInfo.nid, new ClearComActFlagEvent(true)); } public void Damaged(int iDamage, uint dwModifier = 0/* 0 */) { if (iDamage == -1 || iDamage == -2) { // when else player hit this npc iDamage is -1, // so if iDamage is -1 we will shoud the wounded animation if (iDamage == -1 && !m_bStartFight) PlayModelAction((int)NPCActionIndex.ACT_WOUNDED); FLoatingTextManager.Instance.ShowText(transform.position, iDamage, Color.red, 1.0f, ImageResType.NUM_IMAGE, this); if ((dwModifier & (uint)MOD.MOD_IMMUNE) != 0 /* && !IsImmuneDisable()*/) BubbleText((int)MOD.MOD_IMMUNE, 0); else if ((dwModifier & (uint)MOD.MOD_NULLITY) != 0) BubbleText((int)MOD.MOD_NULLITY, 0); else if ((dwModifier & (uint)MOD.MOD_ENCHANT_FAILED) != 0) BubbleText((int)MOD.MOD_ENCHANT_FAILED, 0); else if ((dwModifier & (uint)MOD.MOD_SUCCESS) != 0) BubbleText((int)MOD.MOD_SUCCESS, 0); else if ((dwModifier & (uint)MOD.MOD_DODGE_DEBUFF) != 0) BubbleText((int)MOD.MOD_DODGE_DEBUFF, 0); } else { // this message is related to the host, so we should show a pop up message // Popup a damage decal bool bDeadlyStrike = (dwModifier & (uint)MOD.MOD_CRITICAL_STRIKE) != 0; bool bRetort = (dwModifier & (uint)MOD.MOD_RETORT) != 0; if (iDamage > 0) { if (!m_bStartFight) PlayModelAction((int)NPCActionIndex.ACT_WOUNDED); // Damage number + tint/icons: BubbleText → ShowText only (avoid duplicate red plain text + orange BubbleText). // 伤害数字与图标只走 BubbleText,避免先红字再橙字飘两次。 int p1 = 0; if (bDeadlyStrike) p1 |= 0x0001; else if (bRetort) p1 |= 0x0002; if ((dwModifier & (uint)MOD.MOD_REBOUND) != 0) BubbleText((int)BubbleTextType.BUBBLE_REBOUND, iDamage); else if ((dwModifier & (uint)MOD.MOD_BEAT_BACK) != 0) BubbleText((int)BubbleTextType.BUBBLE_BEAT_BACK, iDamage); else BubbleText((int)BubbleTextType.BUBBLE_DAMAGE, iDamage, p1); } else if ((dwModifier & (uint)MOD.MOD_IMMUNE) != 0 /*&& !IsImmuneDisable()*/) BubbleText((int)BubbleTextType.BUBBLE_IMMUNE, 0); else if ((dwModifier & (uint)MOD.MOD_NULLITY) != 0) BubbleText((int)BubbleTextType.BUBBLE_INVALIDHIT, 0); else if ((dwModifier & (uint)MOD.MOD_ENCHANT_FAILED) != 0) BubbleText((int)BubbleTextType.BUBBLE_LOSE, 0); else if ((dwModifier & (uint)MOD.MOD_SUCCESS) != 0) BubbleText((int)BubbleTextType.BUBBLE_SUCCESS, 0); else BubbleText((int)BubbleTextType.BUBBLE_HITMISSED, 0); } } public void BubbleText(int iIndex, int dwNum, int p1 = 0/* 0 */) { //FLoatingTextManager.Instance.ShowText(transform.position, dwNum, Color.red, 1.0f, (uint)iIndex); Color displayColor = new Color(237, 56, 0); ImageResType imageResType = ImageResType.NUM_IMAGE; switch (iIndex) { case (int)BubbleTextType.BUBBLE_DAMAGE: if ((p1 & 0x0001) != 0) imageResType = ImageResType.IMG_DEADLYSTRIKE; else if ((p1 & 0x0002) != 0) imageResType = ImageResType.IMG_RETORT; break; case (int)BubbleTextType.BUBBLE_HITMISSED: imageResType = ImageResType.IMG_HITMISSED; break; case (int)BubbleTextType.BUBBLE_INVALIDHIT: imageResType = ImageResType.IMG_INVALIDHIT; break; case (int)BubbleTextType.BUBBLE_IMMUNE: imageResType = ImageResType.IMG_IMMUNE; break; case (int)BubbleTextType.BUBBLE_HPWARN: imageResType = ImageResType.IMG_HPWARN; break; case (int)BubbleTextType.BUBBLE_LOSE: imageResType = ImageResType.IMG_ATTACKLOSE; break; case (int)BubbleTextType.BUBBLE_SUCCESS: imageResType = ImageResType.IMG_SUCCESS; break; case (int)BubbleTextType.BUBBLE_REBOUND: imageResType = ImageResType.IMG_REBOUND; break; case (int)BubbleTextType.BUBBLE_BEAT_BACK: imageResType = ImageResType.IMG_BEAT_BACK; break; case (int)BubbleTextType.BUBBLE_DODGE_DEBUFF: imageResType = ImageResType.IMG_DODGE_DEBUFF; break; default: imageResType = ImageResType.NUM_IMAGE; break; } FLoatingTextManager.Instance.ShowText(transform.position, dwNum, displayColor, 1.0f, imageResType, this); } public void WorkFinished(int iWorkID) { // Note: below judge can prevent many problems when we attempt to // finish a work but don't assure we are doing this work if (m_iCurWork != iWorkID) return; // ASSERT equivalent if (m_iCurWork <= 0 || m_iCurWorkType < 0) { throw new InvalidOperationException("Invalid work state in WorkFinished"); } // Release current work ReleaseWork(m_iCurWorkType); bool foundNextWork = false; for (int i = m_iCurWorkType - 1; i >= 0; i--) { if (m_aWorks[i] != 0) // giả định m_aWorks là mảng int workIDs { m_iCurWorkType = i; StartWorkByID(m_aWorks[i], 0); foundNextWork = true; break; } } // If no lower priority work found, default to WORK_STAND (idle state) // This matches C++ behavior where NPC should stand when movement finishes if (!foundNextWork && !IsDead()) { StartWork((int)WorkType.WT_NORMAL, (int)WorkID.WORK_STAND, 0); } // clear passive move flag if (iWorkID == (int)WorkID.WORK_MOVE) m_iPassiveMove = 0; } public static bool InitStaticRes() { m_ActionNames = new CECStringTab(); // Load action names from file if (!m_ActionNames.IsInitialized()) m_ActionNames.Init("actions_npc", false); return true; } public void TransformShape(int vis_tid) { if (m_NPCInfo.vis_tid == vis_tid) { return; } m_NPCInfo.vis_tid = vis_tid; QueueLoadNPCModel(); } public async Task QueueLoadNPCModel() { /* if (ShouldUseMasterModel()) { if (GetMaster()) { return; // ÄÜ»ñÈ¡½ÇɫģÐÍʱ£¬µ½Ï¸ö Tick ¼ÓÔØÄ£ÐÍ } // ÎÞ·¨»ñÈ¡½Çɫʱ¡¢ÔÝʱʹÓà NPC Ä£ÐÍ }*/ int tid = 0; string szModelFile = ""; if (!GetVisibleModel(out tid, out szModelFile)) { return; } try { szModelFile = AFile.NormalizePath(szModelFile.ToLower(), true); m_modelVisual = await NPCBuilder.Instance.GetModelByPath(szModelFile); if (m_modelVisual == null) { m_modelVisual = GameObject.CreatePrimitive(PrimitiveType.Capsule); m_modelVisual.name = szModelFile; BMLogger.LogWarning($" CECNPC.QueueLoadNPCModel model == null szModelFile= {szModelFile} "); } } catch { m_modelVisual = GameObject.CreatePrimitive(PrimitiveType.Capsule); BMLogger.LogWarning($" CECNPC.QueueLoadNPCModel model == null szModelFile= {szModelFile} "); } //var monsterModel = Instantiate(model, transform); m_modelVisual.transform.SetParent(transform, false); m_modelVisual.SetActive(true); var npcVisual = GetComponent(); npcVisual?.InitNPCEventDoneHandler(m_NPCInfo); // UINPC.Start can run before async model instantiate; refresh anchor once SMR hierarchy exists. // UINPC.Start可能在异步模型实例化之前执行;SMR层次就绪后刷新名牌锚点。 m_npcUI?.RefreshWorldNameplatePosition(); //QueueECModelForLoad(MTL_ECM_NPC, GetNPCInfo().nid, GetBornStamp(), GetServerPos(), szModelFile, tid); } public ROLEBASICPROP GetBasicProps() { return m_BasicProps; } public ROLEEXTPROP GetExtendProps() { return m_ExtProps; } public void SetSelectedTarget(int id) { m_idSelTarget = id; } public bool GetVisibleModel(out int tid, out string szModelFile) { tid = 0; szModelFile = string.Empty; // nếu vis_tid có model file if (GetModelFile(GetNPCInfo().vis_tid, out szModelFile)) { tid = GetNPCInfo().vis_tid; } // nếu không có thì thử lấy từ tid thường else if (GetModelFile(GetNPCInfo().tid, out szModelFile)) { tid = GetNPCInfo().tid; } return tid > 0; } public bool GetModelFile(int tid, out string szModelFile) { szModelFile = string.Empty; // Lấy database var pDB = ElementDataManProvider.GetElementDataMan(); // g_pGame->GetElementDataMan() DATA_TYPE dataType = default; // Giả định get_data_ptr trả về object (Essence) và out DataType var pDBEssence = pDB.get_data_ptr((uint)tid, ID_SPACE.ID_SPACE_ESSENCE, ref dataType); if (pDBEssence == null) return false; bool ret = true; switch (dataType) { case DATA_TYPE.DT_MONSTER_ESSENCE: { var ess = (MONSTER_ESSENCE)pDBEssence; szModelFile = ByteToStringUtils.ByteArrayToCP936String(ess.file_model); break; } case DATA_TYPE.DT_PET_ESSENCE: { var ess = (PET_ESSENCE)pDBEssence; szModelFile = ByteToStringUtils.ByteArrayToCP936String(ess.file_model); break; } case DATA_TYPE.DT_NPC_ESSENCE: { var ess = (NPC_ESSENCE)pDBEssence; szModelFile = ByteToStringUtils.ByteArrayToCP936String(ess.file_model); break; } default: ret = false; break; } return ret; } public static void ReleaseStaticRes() { m_ActionNames.Release(); } public static string GetBaseActionName(int iAct) { return m_ActionNames.GetANSIString(iAct); } public static bool IsAttackAction(int iAct) { return iAct == (int)NPCActionIndex.ACT_ATTACK1 || iAct == (int)NPCActionIndex.ACT_ATTACK2 || iAct == (int)NPCActionIndex.ACT_NPC_ATTACK; } public void StopMoveTo(cmd_object_stop_move cmd) { BMLogger.LogMono(this, "CECNPC::StopMoveTo"); if (IsDead()) return; BMLogger.LogMono(this, "CECNPC::StopMoveTo not dead"); int iMoveMode = cmd.move_mode & (int)GPMoveMode.GP_MOVE_MASK; cmd.dest = new A3DVECTOR3( (float)Math.Round(cmd.dest.x, 4), (float)Math.Round(cmd.dest.y, 4), (float)Math.Round(cmd.dest.z, 4) ); m_vMoveDir = (cmd.dest) - EC_Utility.ToA3DVECTOR3(transform.position); m_bStopMove = true; m_fMoveSpeed = EC_Utility.FIX8TOFLOAT(cmd.sSpeed); m_vServerPos = cmd.dest; m_vStopDir = EC_Utility.glb_DecompressDirH(cmd.dir); // only store the passive move mode m_iPassiveMove = (iMoveMode == (int)GPMoveMode.GP_MOVE_PUSH || iMoveMode == (int)GPMoveMode.GP_MOVE_PULL || iMoveMode == (int)GPMoveMode.GP_MOVE_BLINK) ? iMoveMode : 0; if (IsDirFixed()) { transform.forward = EC_Utility.ToVector3(m_vStopDir); } float fDist = m_vMoveDir.Normalize(); // Trong các trường hợp dưới thì kéo NPC về đích if (iMoveMode != (int)GPMoveMode.GP_MOVE_RETURN && iMoveMode != (int)GPMoveMode.GP_MOVE_PUSH && iMoveMode != (int)GPMoveMode.GP_MOVE_PULL) { bool bPull = false; if (IsLag(fDist)) { // case 1 bPull = true; } else if (fDist < 1.0f) { // case 2 A3DVECTOR3 vDirH = (m_vMoveDir); vDirH.y = 0.0f; vDirH.Normalize(); if (DotProduct(vDirH, (m_vStopDir)) < 0.7f) bPull = true; } else if (iMoveMode == (int)GPMoveMode.GP_MOVE_BLINK) { // case 3 bPull = true; } if (bPull) { SetPos(EC_Utility.ToVector3(cmd.dest)); if (!IsDirFixed()) { SetDestDirAndUp(m_vStopDir, g_vAxisY, 150); } WorkFinished((int)WorkID.WORK_MOVE); return; } } m_cdr.bTraceGround = true; if ((cmd.move_mode & (int)GPMoveMode.GP_MOVE_AIR) != 0) { m_iMoveEnv = (int)MoveEnvironment.MOVEENV_AIR; m_cdr.bTraceGround = false; } else if ((cmd.move_mode & (int)GPMoveMode.GP_MOVE_WATER) != 0) { m_iMoveEnv = (int)MoveEnvironment.MOVEENV_WATER; m_cdr.bTraceGround = false; } else { m_iMoveEnv = (int)MoveEnvironment.MOVEENV_GROUND; if (iMoveMode == (int)GPMoveMode.GP_MOVE_FALL || iMoveMode == (int)GPMoveMode.GP_MOVE_FLYFALL) m_cdr.bTraceGround = false; } if (!IsDirFixed() && m_iPassiveMove == 0) { A3DVECTOR3 vDir = m_vMoveDir; vDir.y = 0.0f; if (!vDir.IsZero()) { vDir.Normalize(); SetDestDirAndUp(vDir, g_vAxisY, 150); } } if (m_aWorks[(int)WorkType.WT_NORMAL] != (int)WorkID.WORK_MOVE) { StartWork((int)WorkType.WT_NORMAL, (int)WorkID.WORK_MOVE); if (m_iPassiveMove == 0) { PlayMoveAction(iMoveMode); } } } public static float DotProduct(A3DVECTOR3 v1, A3DVECTOR3 v2) { return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; } public bool GetCHAABB(ref A3DAABB aabb) { return m_pNPCModelPolicy.GetCHAABB(ref aabb); } public void SetPos(Vector3 pos) { transform.position = pos; } public void MoveTo(cmd_object_move Cmd) { if (Cmd.use_time == 0) return; var dest = Cmd.dest; m_vServerPos = (dest); m_vMoveDir = dest - EC_Utility.ToA3DVECTOR3(transform.position); float fDist = m_vMoveDir.Magnitude(); // lấy độ dài ban đầu m_vMoveDir.Normalize(); // giả sử Normalize() trả về độ dài trước khi chuẩn hóa // If destination position is too far, forcely pull player if (IsLag(fDist)) { transform.position = EC_Utility.ToVector3(Cmd.dest); return; } int iMoveMode = Cmd.move_mode & (int)GPMoveMode.GP_MOVE_MASK; m_bStopMove = false; if (iMoveMode == (int)GPMoveMode.GP_MOVE_PUSH || iMoveMode == (int)GPMoveMode.GP_MOVE_PULL) { // Push back or pull should occur in stop move command UnityEngine.Debug.Assert(false, "Invalid move mode: push/pull inside MoveTo"); return; } m_cdr.bTraceGround = true; if ((Cmd.move_mode & (int)GPMoveMode.GP_MOVE_AIR) != 0) { m_iMoveEnv = (int)MoveEnvironment.MOVEENV_AIR; m_cdr.bTraceGround = false; } else if ((Cmd.move_mode & (int)GPMoveMode.GP_MOVE_WATER) != 0) { m_iMoveEnv = (int)MoveEnvironment.MOVEENV_WATER; m_cdr.bTraceGround = false; } else { m_iMoveEnv = (int)MoveEnvironment.MOVEENV_GROUND; int iTemp = iMoveMode & (int)GPMoveMode.GP_MOVE_MASK; if (iTemp == (int)GPMoveMode.GP_MOVE_FALL || iTemp == (int)GPMoveMode.GP_MOVE_FLYFALL) m_cdr.bTraceGround = false; } m_fMoveSpeed = fDist / (Cmd.use_time * 0.001f); // Adjust NPC's direction /*if (!IsDirFixed()) { var vDir = m_vMoveDir; vDir.y = 0.0f; if (!vDir.IsZero()) { vDir.Normalize(); SetDestDirAndUp(vDir, g_vAxisY, 150); } } */ if (m_aWorks[(int)WorkType.WT_NORMAL] != (int)WorkID.WORK_MOVE || ShouldPlayNewActionFor(iMoveMode)) { StartWork((int)WorkType.WT_NORMAL, (int)WorkID.WORK_MOVE); // Play run or walk action PlayMoveAction(iMoveMode); } } public bool IsDirFixed() { return (m_dwStates & PlayerNPCState.GP_STATE_NPC_FIXDIR) != 0 ? true : false; } public void ReleaseWork(int iWorkType) { //BrewMonster.BMLogger.LogError("HoangDev : ReleaseWorkl :"+ iWorkType); Debug.Assert(iWorkType >= 0 && iWorkType < (int)WorkType.NUM_WORKTYPE); switch (m_aWorks[iWorkType]) { case (int)WorkID.WORK_STAND: break; case (int)WorkID.WORK_FIGHT: break; case (int)WorkID.WORK_SPELL: break; case (int)WorkID.WORK_DEAD: break; case (int)WorkID.WORK_MOVE: { var pos = EC_Utility.ToVector3(m_vServerPos); var vDelta = pos - transform.position; float fDist = vDelta.magnitude; // Vector3.magnitude trong Unity if (fDist > 0.1f) { transform.position = (pos); /* if (!IsDirFixed()) { SetDestDirAndUp(m_vStopDir, Vector3.up, 150); }*/ //RebuildTraceBrush(); } break; } case (int)WorkID.WORK_POLICYACTION: { /* m_pNPCModelPolicy?.StopChannelAction(); m_pPolicyAction = null; m_nPolicyActionIntervalTimer = 0;*/ break; } } m_aWorks[iWorkType] = 0; if (m_iCurWorkType == iWorkType) m_iCurWork = 0; } public void StartWork(int iWorkType, int iNewWork, uint dwParam = 0, cmd_object_start_play_action cmd_Object_Start_Play_Action = default) { Debug.Assert(iWorkType >= 0 && iWorkType < (int)WorkType.NUM_WORKTYPE); if (iNewWork == (int)WorkID.WORK_DEAD) { // Dead is a special work ReleaseWork((int)WorkType.WT_INTERRUPT); ReleaseWork((int)WorkType.WT_NORMAL); m_aWorks[(int)WorkType.WT_NORMAL] = iNewWork; m_iCurWorkType = (int)WorkType.WT_NORMAL; } else if (iWorkType == (int)WorkType.WT_INTERRUPT) { // Release old work ReleaseWork((int)WorkType.WT_INTERRUPT); m_aWorks[(int)WorkType.WT_INTERRUPT] = iNewWork; if (m_iCurWorkType == (int)WorkType.WT_NORMAL || m_iCurWorkType == (int)WorkType.WT_NOTHING) StopWork(m_iCurWorkType); m_aWorks[(int)WorkType.WT_INTERRUPT] = iNewWork; m_iCurWorkType = (int)WorkType.WT_INTERRUPT; } else if (iWorkType == (int)WorkType.WT_NORMAL) { // Release old work ReleaseWork((int)WorkType.WT_NORMAL); m_aWorks[(int)WorkType.WT_NORMAL] = iNewWork; if (m_iCurWorkType < 0 || m_iCurWorkType == (int)WorkType.WT_NORMAL || m_iCurWorkType == (int)WorkType.WT_NOTHING) { if (m_iCurWorkType == (int)WorkType.WT_NOTHING) StopWork((int)WorkType.WT_NOTHING); m_iCurWorkType = (int)WorkType.WT_NORMAL; } else return; } else // iWorkType == WT_NOTHING { // Release old work ReleaseWork((int)WorkType.WT_NOTHING); m_aWorks[(int)WorkType.WT_NOTHING] = iNewWork; if (m_iCurWorkType < 0 || m_iCurWorkType == (int)WorkType.WT_NOTHING) m_iCurWorkType = (int)WorkType.WT_NOTHING; else return; } StartWorkByID(iNewWork, dwParam, cmd_Object_Start_Play_Action); } public bool ShouldDisappear() { return m_DisappearCnt.IsFull(); } public void StopWork(int iWorkType) { } public void StartWorkByID(int iWorkID, uint dwParam, cmd_object_start_play_action cmd_Object_Start_Play_Action = default) { // Ignore all message if this NPC is dead. // if (IsDead()) // return; switch (iWorkID) { case (int)WorkID.WORK_STAND: StartWork_Stand(dwParam); break; case (int)WorkID.WORK_FIGHT: StartWork_Fight(dwParam); break; case (int)WorkID.WORK_SPELL: StartWork_Spell(dwParam); break; case (int)WorkID.WORK_DEAD: StartWork_Dead(dwParam); break; case (int)WorkID.WORK_MOVE: StartWork_Move(dwParam); break; case (int)WorkID.WORK_POLICYACTION: StartWork_PolicyAction(cmd_Object_Start_Play_Action); break; } // if (iWorkID != WORK_MOVE) m_iPassiveMove = 0; m_iCurWork = iWorkID; } public void StartWork_Stand(uint dwParam) { if (isDebug) { BMLogger.LogError("StartWork_Stand "); } if (!m_bStartFight) { if (IsMonsterOrPet()) PlayModelAction((int)NPCActionIndex.ACT_STAND); else PlayModelAction((int)NPCActionIndex.ACT_NPC_STAND); } } public void StartWork_Fight(uint dwParam) { // dwParam được dùng như “thời gian chiến đấu còn lại” //m_nFightTimeLeft = (int)dwParam; // Không play animation ở đây vì animation được điều khiển bởi message tấn công } public void StartWork_Spell(uint dwParam) { // Trong C++ không có xử lý gì, giữ nguyên } public void StartWork_Dead(uint dwParam) { if (IsMonsterOrPet()) PlayModelAction((int)NPCActionIndex.ACT_DIE); else PlayModelAction((int)NPCActionIndex.ACT_NPC_DIE); } public void StartWork_Move(uint dwParam) { m_bStartFight = false; /* if (m_pNPCModelPolicy != null && m_pNPCModelPolicy.IsModelLoaded()) { ClearComActFlag(true); // Khi NPC đang di chuyển thì bỏ trace brush (không cần va chạm) ReleaseTraceBrush(); }*/ } public void StartWork_PolicyAction(cmd_object_start_play_action cmd_Object_Start_Play_Action) { /* if (m_pPolicyAction == null) m_pPolicyAction = new CECPolicyAction(); m_pPolicyAction.Init((cmd_object_start_play_action)cmd_Object_Start_Play_Action); m_pPolicyAction.Tick(0); m_nPolicyActionIntervalTimer = 0; CheckStartPolicyAction();*/ } public bool ShouldPlayNewActionFor(int iMoveMode) { if (m_pNPCModelPolicy.IsPlayingAction()) { int iAction = GetMoveAction(iMoveMode); return !m_pNPCModelPolicy.IsPlayingAction(iAction) && m_pNPCModelPolicy.HasAction(iAction); } return false; } public int GetMoveAction(int iMoveMode) { if (iMoveMode == (int)GPMoveMode.GP_MOVE_RUN || iMoveMode == (int)GPMoveMode.GP_MOVE_RETURN) { if (IsMonsterOrPet()) return (int)NPCActionIndex.ACT_RUN; else return (int)NPCActionIndex.ACT_NPC_RUN; } else { if (IsMonsterOrPet()) return (int)NPCActionIndex.ACT_WALK; else return (int)NPCActionIndex.ACT_NPC_WALK; } } public bool IsMonsterOrPet() { return IsMonsterNPC() || IsPetNPC(); } public bool IsMonsterNPC() { return (int)Class_ID.OCID_MONSTER == m_iCID; } public bool IsPetNPC() { return (int)Class_ID.OCID_PET == m_iCID; } public bool IsDead() { return (m_dwStates & PlayerNPCState.GP_STATE_CORPSE) != 0; } public void PlayMoveAction(int iMoveMode) { //BrewMonster.BMLogger.LogError($"HoangDev: PlayMoveAction {iMoveMode}"); if (iMoveMode == (int)GPMoveMode.GP_MOVE_RUN || iMoveMode == (int)GPMoveMode.GP_MOVE_RETURN) { if (IsMonsterOrPet()) PlayModelAction((int)NPCActionIndex.ACT_RUN, false); else PlayModelAction((int)NPCActionIndex.ACT_NPC_RUN, false); } else { if (IsMonsterOrPet()) PlayModelAction((int)NPCActionIndex.ACT_WALK, false); else PlayModelAction((int)NPCActionIndex.ACT_NPC_WALK, false); } } public void SetWorldHealthImage(float currenthealth, float maxHealth) { m_npcUI.gameObject.SetActive(true); m_npcUI.SetHealthImage(currenthealth / maxHealth); m_npcUI.SetHealthText($"{currenthealth}/{maxHealth}"); } public void PlayModelAction(int iAction, bool bRestart = false) { m_iAction = iAction; if (IsDead()) { // ¼ì²éËÀÍö״̬£¬ÒÔÆÁ±ÎÆäËü¶¯×÷ // ËÀÍö״̬ÉèÖúó£¬Ö»ÔÊÐí²¥·ÅËÀÍö¶¯×÷ // Íæ¼Ò¹¥»÷NPCʱ£¬»áÊ×ÏȲ¥·ÅÍæ¼ÒµÄ¹¥»÷¶¯×÷£¬Íê³Éºó²¥·Å¹ÖÎïµÄÊÜÉ˶¯×÷ // µ«ÔÚÍæ¼Ò×ÔÉíµÄ¹¥»÷¶¯×÷δÍê³Éʱ£¬¿ÉÄܾÍÊÕµ½NPCËÀÍöµÄÏûÏ¢ // Òò´Ë£¬¿ÉÄÜ»áÏȲ¥·ÅËÀÍö¶¯×÷£¬¶øºóÓÖ²¥·ÅÊÜÉ˶¯×÷£¬µ¼ÖÂËÀÍö¶¯×÷²»ÄÜÕý³£²¥·ÅµÄ±íÏÖ½á¹û // ²»·ûºÏÆÚÍû if (IsMonsterOrPet()) { if (iAction != (int)NPCActionIndex.ACT_DIE) { return; } } else { if (iAction != (int)NPCActionIndex.ACT_NPC_DIE) { return; } } m_npcUI.gameObject.SetActive(false); } if (isDebug) { BMLogger.LogError("PlayModelAction iAction :" + iAction); } m_pNPCModelPolicy.PlayModelAction(iAction, bRestart, null); } bool IsDisappearing() { if(m_DisappearCnt != null) { return m_DisappearCnt.GetCounter() != 0 ? true : false; } return false; } public int GetTemplateID() { return m_NPCInfo.tid; } public float GetTouchRadius() { return m_fTouchRad; } bool IsLag(float fDist) { return m_iPassiveMove == 0 && fDist > MAX_LAGDIST; } public INFO GetNPCInfo() { return m_NPCInfo; } [Serializable] public struct INFO { public int nid; // NPC id public int tid; // Template id public int vis_tid;// template id for shape }; public const float MAX_LAGDIST = 25.0f; // Get NPC's real position on server public A3DVECTOR3 GetServerPos() { return (m_vServerPos); } // Get master id public int GetMasterID() { return m_idMaster; } // Is monster in invader camp in battle ? public virtual bool IsInBattleInvaderCamp() { return false; } // Is monster in defender camp in battle ? public virtual bool IsInBattleDefenderCamp() { return false; } // Get role in battle public virtual int GetRoleInBattle() { return 0; } public int GetOwnerFaction() { return m_idOwnerFaction; } public bool IsFactionPVPMineCar() { //if (const MONSTER_ESSENCE* pMonsterEssence = GetMonsterEssence()){ // return (pMonsterEssence.faction & (1 << 19)) != 0; //} return false; } public bool IsFactionPVPMineBase() { //if (const MONSTER_ESSENCE *pMonsterEssence = GetMonsterEssence()){ // return (pMonsterEssence->faction & (1 << 20)) != 0; //} return false; } // Get NPC ID public int GetNPCID() { return m_NPCInfo.nid; } // Get distance to host player public float GetDistToHost() { return m_fDistToHost; } public float GetDistToHostH() { return m_fDistToHostH; } // Get NPC name color public virtual uint GetNameColor() { return 0xffffff00; } CECPateText m_pPateLastWords1; CECPateText m_pPateLastWords2; public void SetLastSaidWords(string words, int timeShow = -1) { if (m_pPateLastWords1 == null) { m_pPateLastWords1 = new CECPateText(); } if (m_pPateLastWords2 == null) { m_pPateLastWords2 = new CECPateText(); } if (words == null) return; int len1 = m_pPateLastWords1.SetText(words, true, out string strWords, false, null); /* if (len1 < strWords.Length) m_pPateLastWords2.SetText(strWords.Substring(len1), true, true); else m_pPateLastWords2.Clear(); m_iLastSayCnt = timeShow > 0 ? timeShow : 20000;*/ } /// /// Get NPC's CECModel instance /// 获取NPC的CECModel实例 /// /// CECModel instance or null / CECModel实例,未找到返回null public CECModel GetModel() { // Initialize CECModel if not already created // 如果尚未创建,则初始化CECModel if (m_pNPCCECModel == null) { // Find the model GameObject (typically a child of this NPC) // 查找模型GameObject(通常是此NPC的子对象) GameObject modelObject = null; // Try to find model in children (where NPC models are typically placed) // 尝试在子对象中查找模型(NPC模型通常放置在那里) NPCVisual npcVisual = GetComponent(); if (npcVisual != null) { // Find SkeletonBuilder which is typically on the model GameObject // 查找通常在模型GameObject上的SkeletonBuilder SkeletonBuilder skeleton = GetComponentInChildren(true); if (skeleton != null) { modelObject = skeleton.gameObject; } } // If no model found, try to find any child with SkeletonBuilder // 如果未找到模型,尝试查找任何带有SkeletonBuilder的子对象 if (modelObject == null) { SkeletonBuilder skeleton = GetComponentInChildren(true); if (skeleton != null) { modelObject = skeleton.gameObject; } } if (modelObject != null) { // Create and initialize CECModel instance // 创建并初始化CECModel实例 m_pNPCCECModel = new CECModel(); m_pNPCCECModel.SetTransform(modelObject.transform); // Find and set SkeletonBuilder // 查找并设置SkeletonBuilder SkeletonBuilder skeletonBuilder = modelObject.GetComponent(); if (skeletonBuilder == null) { skeletonBuilder = modelObject.GetComponentInChildren(true); } if (skeletonBuilder != null) { m_pNPCCECModel.SetSkeletonBuilder(skeletonBuilder); m_pNPCCECModel.InitializeSkeletonBuilder(); } NamedAnimancerComponent animancer = modelObject.GetComponent(); if (animancer == null) { animancer = modelObject.GetComponentInChildren(true); } if (animancer != null) { m_pNPCCECModel.SetNamedAnimancerComponent(animancer); } } } return m_pNPCCECModel; } /// /// Get hook Transform by name (convenience method) /// 根据名称获取挂点变换(便捷方法) /// /// Hook name / 挂点名称 /// Search recursively / 递归搜索 /// Hook Transform or null / 挂点变换,未找到返回null public Transform GetHook(string hookName, bool recursive = true) { CECModel model = GetModel(); return model?.GetHook(hookName, recursive); } public enum BubbleTextType { BUBBLE_DAMAGE = 0, BUBBLE_HITMISSED, BUBBLE_INVALIDHIT, BUBBLE_IMMUNE, BUBBLE_HPWARN, BUBBLE_LOSE, BUBBLE_SUCCESS, BUBBLE_REBOUND, // ���� BUBBLE_BEAT_BACK, // ���� BUBBLE_DODGE_DEBUFF, }; } public struct ClearComActFlagEvent { public bool BSignalCurrent; public ClearComActFlagEvent(bool bSignalCurrentnid) { BSignalCurrent = bSignalCurrentnid; } } public enum WorkType { WT_NOTHING = 0, // Do thing WT_NORMAL, // Normal type work WT_INTERRUPT, // Interrupt type work NUM_WORKTYPE, }; // Work ID public enum WorkID { WORK_STAND = 1, // Stand and do nothing WORK_FIGHT, // Fighting WORK_SPELL, // Monster is cast skill WORK_DEAD, // Monster is dead WORK_MOVE, // Move to a destination terrain position WORK_POLICYACTION, // Is playing policy action£¬WT_INTERRUPT }; public struct NameNPCGotEvent { public string NpcName; public NameNPCGotEvent(string npcName) { NpcName = npcName; } } public enum NPCActionIndex { ACT_STAND = 0, ACT_IDLE, ACT_GUARD, ACT_SKILL1, ACT_WALK, ACT_ATTACK1, ACT_ATTACK2, ACT_RUN, ACT_DIE, ACT_JUMP_START, ACT_JUMP_LAND, ACT_JUMP_LOOP, ACT_MAGIC1, ACT_WOUNDED, ACT_NPC_CHAT1, ACT_NPC_CHAT2, ACT_NPC_CHATLOOP, ACT_NPC_IDLE1, ACT_NPC_IDLE2, ACT_NPC_STAND, ACT_NPC_WALK, ACT_NPC_RUN, ACT_NPC_ATTACK, ACT_NPC_DIE, ACT_COMMON_BORN, ACT_NPC_DISAPPEAR, ACT_WOUNDED2, ACT_MAX, }; //BUBBLE TEXT TYPE FOR NPC. HOST IS DIFFERENT FROM NPC. public ref struct ByteReader { private ReadOnlySpan _span; private int _offset; public ByteReader(ReadOnlySpan span) { _span = span; _offset = 0; } public bool Eof => _offset >= _span.Length; public byte ReadByte() => _span[_offset++]; public int ReadInt32() { int v = BitConverter.ToInt32(_span.Slice(_offset, 4)); _offset += 4; return v; } public void ReadInto(uint[] dst) { // OBJECT_EXT_STATE_COUNT * sizeof(DWORD) int bytes = dst.Length * 4; var s = _span.Slice(_offset, bytes); for (int i = 0; i < dst.Length; i++) dst[i] = BitConverter.ToUInt32(s.Slice(i * 4, 4)); _offset += bytes; } public byte[] ReadBytes(int len) { var s = _span.Slice(_offset, len).ToArray(); _offset += len; return s; } }