Files
2026-05-27 17:47:58 +07:00

2039 lines
68 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
using BrewMonster.Scripts.ECModel;
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;
/// <summary>Buff/debuff icon states from server (ICON_STATE_NOTIFY). Same role as CECPlayer.m_aIconStates.</summary>
public List<IconState> m_aIconStates = new List<IconState>();
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<byte> 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<CharacterController>();
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<NPCVisual>();
if (npcVisual == null)
{
BMLogger.LogError("HoangDev npcVisual");
}
m_pNPCModelPolicy.SetNpcVisual(npcVisual);
m_npcUI = GetComponentInChildren<UINPC>(true);
m_npcUI.gameObject.SetActive(true);
// 2) Cắt “đuôi” ngay sau phần cố định info_npc
int fixedSize = System.Runtime.InteropServices.Marshal.SizeOf<info_npc>();
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);
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<IconState>();
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<cmd_update_ext_state>((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<cmd_level_up>((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
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<cmd_object_invisible>((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<cmd_object_start_play_action>((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)
{
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<cmd_object_atk_result>(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;
m_pNPCCECModel = null;
PoolManager.Instance.Despawn(m_modelVisual);
m_modelVisual = null;
/*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));
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;
}
/// <summary>
/// Xoay model NGAY LẬP TỨC theo hướng chỉ định (giữ nguyên trục Y đứng thẳng).
/// </summary>
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);
}
}
}
/// <summary>
/// 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.
/// </summary>
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;
}
if(FLoatingTextManager.Instance == null)
{
return;
}
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;
}
uint bornStampAtRequest = GetBornStamp();
int nidAtRequest = m_NPCInfo.nid;
if (m_modelVisual != null)
{
PoolManager.Instance.Despawn(m_modelVisual);
m_modelVisual = null;
m_pNPCCECModel?.InvalidateHookCache();
}
GameObject loadedVisual = null;
try
{
szModelFile = AFile.NormalizePath(szModelFile.ToLower(), true);
loadedVisual = await NPCBuilder.Instance.GetModelByPath(szModelFile);
if (loadedVisual == null)
{
loadedVisual = GameObject.CreatePrimitive(PrimitiveType.Capsule);
loadedVisual.name = szModelFile;
BMLogger.LogWarning($" CECNPC.QueueLoadNPCModel model == null szModelFile= {szModelFile} ");
}
}
catch
{
loadedVisual = GameObject.CreatePrimitive(PrimitiveType.Capsule);
BMLogger.LogWarning($" CECNPC.QueueLoadNPCModel model == null szModelFile= {szModelFile} ");
}
if (this == null || !gameObject)
return;
if (GetBornStamp() != bornStampAtRequest || m_NPCInfo.nid != nidAtRequest)
{
if (loadedVisual != null)
PoolManager.Instance.Despawn(loadedVisual);
return;
}
m_modelVisual = loadedVisual;
m_modelVisual.transform.SetParent(transform, false);
m_modelVisual.SetActive(true);
InitializeNPCCECModel(m_modelVisual);
var npcVisual = GetComponent<NPCVisual>();
if (npcVisual != null)
{
npcVisual.RefreshNamedAnimancer(m_modelVisual);
npcVisual.InitNPCEventDoneHandler(m_NPCInfo);
}
// UINPC.Start can run before async model instantiate; refresh anchor once SMR hierarchy exists.
// UINPC.Start可能在异步模型实例化之前执行;SMR层次就绪后刷新名牌锚点。
if(m_npcUI != null && m_npcUI.gameObject.activeInHierarchy)
{
m_npcUI?.RefreshWorldNameplatePosition();
}
//QueueECModelForLoad(MTL_ECM_NPC, GetNPCInfo().nid, GetBornStamp(), GetServerPos(), szModelFile, tid);
}
/// <summary>
/// Bind CECModel to the loaded NPC visual (prefab holds NamedAnimancer + CombineActHolder; no SkeletonBuilder).
/// 将CECModel绑定到已加载的NPC视觉对象(预制体含NamedAnimancer与CombineActHolderNPC无SkeletonBuilder)。
/// </summary>
void InitializeNPCCECModel(GameObject modelRoot)
{
if (modelRoot == null)
return;
if (m_pNPCCECModel == null)
m_pNPCCECModel = new CECModel();
m_pNPCCECModel.SetId(m_NPCInfo.nid);
m_pNPCCECModel.SetTransform(modelRoot.transform);
m_pNPCCECModel.InvalidateHookCache();
NamedAnimancerComponent animancer = modelRoot.GetComponentInChildren<NamedAnimancerComponent>(true);
if (animancer != null)
m_pNPCCECModel.SetNamedAnimancerComponent(animancer);
CombineActHolder combineActHolder = modelRoot.GetComponentInChildren<CombineActHolder>(true);
if (combineActHolder != null)
{
try
{
if (combineActHolder.ActionSO != null)
m_pNPCCECModel.SetCombinedAction(combineActHolder.ActionSO);
}
catch (Exception ex)
{
BMLogger.LogWarning($"[CECNPC] Failed to read CombineActHolder.ActionSO on '{modelRoot.name}': {ex.Message}");
}
}
SkeletonBuilder skeletonBuilder = modelRoot.GetComponentInChildren<SkeletonBuilder>(true);
if (skeletonBuilder != null)
{
m_pNPCCECModel.SetSkeletonBuilder(skeletonBuilder);
m_pNPCCECModel.InitializeSkeletonBuilder();
}
}
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)
{
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)
{
int iAction = GetMoveAction(iMoveMode);
if (!m_pNPCModelPolicy.HasAction(iAction))
return false;
return !m_pNPCModelPolicy.IsPlayingAction(iAction);
}
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)
{
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);
}
if(m_pNPCModelPolicy == null)
{
return;
}
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;*/
}
/// <summary>
/// Get NPC's CECModel instance (bound after QueueLoadNPCModel, or lazily from m_modelVisual).
/// 获取NPC的CECModel实例(QueueLoadNPCModel后绑定,或从m_modelVisual延迟绑定)。
/// </summary>
/// <returns>CECModel instance or null / CECModel实例,未找到返回null</returns>
public CECModel GetModel()
{
if (m_pNPCCECModel == null && m_modelVisual != null)
InitializeNPCCECModel(m_modelVisual);
return m_pNPCCECModel;
}
/// <summary>
/// Get hook Transform by name (convenience method)
/// 根据名称获取挂点变换(便捷方法)
/// </summary>
/// <param name="hookName">Hook name / 挂点名称</param>
/// <param name="recursive">Search recursively / 递归搜索</param>
/// <returns>Hook Transform or null / 挂点变换,未找到返回null</returns>
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<byte> _span;
private int _offset;
public ByteReader(ReadOnlySpan<byte> 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;
}
}