using BrewMonster.Managers; using BrewMonster.Network; using BrewMonster.Scripts; using BrewMonster.Scripts.Managers; using BrewMonster.Scripts.World; using CSNetwork; using CSNetwork.GPDataType; using System; using UnityEngine; using static BrewMonster.Scripts.CECHPWork; using static CECPlayerWrapper; using PerfectWorld.Scripts.Managers; namespace BrewMonster { public partial class CECHostPlayer { public void OnMsgHstAttackResult(ECMSG Msg) { byte[] data = Msg.dwParam1 as byte[]; cmd_host_attack_result pCmd = EC_Utility.ByteArrayToStructure(data); int iAttackTime = 0; TurnFaceTo(pCmd.idTarget); PlayAttackEffect(pCmd.idTarget, 0, 0, pCmd.iDamage, (uint)pCmd.attack_flag, pCmd.attack_speed * 50, ref iAttackTime); if (iAttackTime != 0) { if (m_pWorkMan.GetRunningWork(CECHPWork.Host_work_ID.WORK_HACKOBJECT) is CECHPWorkMelee pCurWork) { pCurWork.SetIdleTime(iAttackTime); } } } /// Message MSG_HST_ATTACKONCE handler. One normal attack executed (consumes ammo/endurance). Continue combo if melee and combo active. Mirrors C++ OnMsgHstAttackOnce. void OnMsgHstAttackOnce(ECMSG Msg) { byte[] data = Msg.dwParam1 as byte[]; if (data == null || data.Length < 1) return; cmd_attack_once pCmd = GPDataTypeHelper.FromBytes(data); // Decrease ammo and weapon endurance (mirror C++ OnMsgHstAttackOnce) CECIvtrWeapon pWeapon = (CECIvtrWeapon)m_pEquipPack.GetItem((int)IndexOfIteminEquipmentInventory.EQUIPIVTR_WEAPON); if (pWeapon != null) { if (pCmd.ammo_num != 0) { IVTR_ESSENCE_WEAPON Essence = pWeapon.GetEssence(); if (Essence.weapon_type == (int)WeaponType.WEAPONTYPE_RANGE) m_pEquipPack.RemoveItem((int)IndexOfIteminEquipmentInventory.EQUIPIVTR_PROJECTILE, pCmd.ammo_num); // If ammo will be used out soon, try to equip from package automatically (C++ logic) EC_IvtrItem pItem = m_pEquipPack.GetItem((int)IndexOfIteminEquipmentInventory.EQUIPIVTR_PROJECTILE, false); if (pItem != null && pItem.GetPileLimitInstance() > 0 && (float)pItem.GetCount() / pItem.GetPileLimitInstance() < 0.2f) { int iIndex = m_pPack.FindItem(pItem.GetTemplateID()); if (iIndex >= 0) { EC_IvtrItem pPackItem = m_pPack.GetItem(iIndex, false); if (pPackItem != null && !pPackItem.IsFrozen()) { // TODO: when c2s_CmdMoveItemToEquip is implemented, call it here to auto-equip ammo } } } } pWeapon.AddCurEndurance(GameConstants.WEAPON_RUIN_SPEED); } m_bPrepareFight = false; if (m_bMelee && m_pComboSkill != null && !m_pComboSkill.IsStop()) { if (CECAutoPolicy.GetInstance().IsAutoPolicyEnabled()) EC_ManMessage.PostMessage(EC_MsgDef.MSG_HST_CONTINUECOMBOSKILL, MANAGER_INDEX.MAN_PLAYER, 0, 1, m_pComboSkill.GetGroupIndex()); else m_pComboSkill.Continue(true); } } void OnMsgHstAttacked(ECMSG Msg) { var m_pPlayerMan = EC_ManMessageMono.Instance.EC_ManPlayer; cmd_host_attacked pCmd = GPDataTypeHelper.FromBytes(Msg.dwParam1 as byte[]); if (pCmd.iDamage != 0 && (pCmd.cEquipment & 0x7f) != 0x7f) { char cEquip = (char)(pCmd.cEquipment & 0x7f); EC_IvtrEquip pEquip = (EC_IvtrEquip)m_pEquipPack.GetItem(cEquip); if (pEquip != null) pEquip.AddCurEndurance(GameConstants.ARMOR_RUIN_SPEED); } // The host player is attacked, we should make an effect here if (GPDataTypeHelper.ISPLAYERID(pCmd.idAttacker)) { EC_ElsePlayer pAttacker = m_pPlayerMan.GetElsePlayer(pCmd.idAttacker); if (pAttacker) { if (!pAttacker.IsDead()) { // Face to target pAttacker.TurnFaceTo(GetPlayerInfo().cid); } int useless_attacktime = 0; pAttacker.PlayAttackEffect(GetCharacterID(), 0, 0, pCmd.iDamage, (uint)pCmd.attack_flag, pCmd.speed * 50, ref useless_attacktime); pAttacker.EnterFightState(); } } else if (GPDataTypeHelper.ISNPCID(pCmd.idAttacker)) { CECNPC pAttacker = EC_ManMessageMono.Instance.CECNPCMan.GetNPC(pCmd.idAttacker); if (pAttacker) { pAttacker.OnMsgAttackHostResult(GetCharacterID(), pCmd.iDamage, pCmd.attack_flag, pCmd.speed); } } //CECAutoPolicy::GetInstance().SendEvent_BeHurt(pCmd.idAttacker); } private void OnMsgHstHurtResult(ECMSG Msg) { //BMLogger.LogError("HoangDev : OnMsgHstHurtResult"); int cmd = Convert.ToInt32(Msg.dwParam2); if (cmd == CommandID.BE_HURT) { cmd_be_hurt pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1 ); if (pCmd.damage != 0) Damaged(pCmd.damage); } else if (cmd == CommandID.HURT_RESULT) { cmd_hurt_result pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1 ); if (pCmd.target_id == m_PlayerInfo.cid) return; // Host himself will receive BE_HURT, so ignore this. if (UnityGameSession.Instance.GameSession.ISPLAYERID(pCmd.target_id)) { EC_ElsePlayer pTarget = m_pPlayerMan.GetElsePlayer(pCmd.target_id); if (pTarget) pTarget.Damaged(pCmd.damage); } else if (UnityGameSession.Instance.GameSession.ISNPCID(pCmd.target_id)) { CECNPC pTarget = EC_ManMessageMono.Instance.CECNPCMan.GetNPC(pCmd.target_id); if (pTarget) pTarget.Damaged(pCmd.damage); } } } private void OnMsgHstStartAttack(in ECMSG Msg) { cmd_host_start_attack pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); // ASSERT(pCmd); // test code... // g_pGame.GetRTDebug().OutputNotifyMessage(RTDCOL_WARNING, _AL("start attack !")); // Check whether target is the one that we have selected if (m_idSelTarget != pCmd.idTarget) // g_pGame.RuntimeDebugInfo(RTDCOL_WARNING, _AL("Target has changed !")); // If target turn to be un-attackable, cancel action if (AttackableJudge(pCmd.idTarget, true) == 0) { UnityGameSession.c2s_CmdCancelAction(); // g_pGame.RuntimeDebugInfo(RTDCOL_WARNING, _AL("Cannel attacking !")); return; } // Synchronize ammo amount // CECIvtrItem* pItem = m_pEquipPack.GetItem(EQUIPIVTR_PROJECTILE); // if (pItem) // { // if (!pCmd.ammo_remain) // m_pEquipPack.SetItem(EQUIPIVTR_PROJECTILE, NULL); // else // pItem.SetAmount(pCmd.ammo_remain); // } CECHPWorkMelee pWork = (CECHPWorkMelee)m_pWorkMan.CreateWork(Host_work_ID.WORK_HACKOBJECT); m_pWorkMan.StartWork_p1(pWork); m_bMelee = true; // AP_ActionEvent(AP_EVENT_STARTMELEE); } private void OnMsgHstStopAttack(in ECMSG Msg) { // using namespace S2C; // m_bMelee = false; // // // if there is an attack event currently, we should let it fire now. ClearComActFlagAllRankNodes(true); cmd_host_stop_attack pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); // ASSERT(pCmd); /* If attack stopped for target is leave too far, trace it and continue attacking. Stop reason defined as below: 0x00: attack is canceled or host want to do some other things. 0x01: unable to attack anymore (no ammo, weapon is broken etc.) 0x02: invalid target (target missed or died) 0x04: target is out of range */ if ((pCmd.iReason & 0x04) != 0) { if (!m_pWorkMan.IsMovingToPosition() && !m_pWorkMan.IsTracing()) { if (CmdNormalAttack(false, false, 0, -1)) //m_pComboSkill != NULL { // AP_ActionEvent(AP_EVENT_MELEEOUTOFRANGE, 1); } else { m_pWorkMan.FinishRunningWork(Host_work_ID.WORK_HACKOBJECT); // AP_ActionEvent(AP_EVENT_STOPMELEE); } } else { // AP_ActionEvent(AP_EVENT_STOPMELEE); } } else { m_pWorkMan.FinishRunningWork(Host_work_ID.WORK_HACKOBJECT); // AP_ActionEvent(AP_EVENT_STOPMELEE); } // #ifdef _SHOW_AUTOPOLICY_DEBUG // a_LogOutput(1, "Stop Attack, Reason = %d", pCmd.iReason); // #endif } private void OnMsgHstSkillResult(ECMSG Msg) { cmd_host_skill_attack_result pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); int refFake = 0; PlayAttackEffect(pCmd.idTarget, pCmd.idSkill, 0, pCmd.iDamage, (uint)pCmd.attack_flag, pCmd.attack_speed * 50, ref refFake, pCmd.section); } void OnMsgHstSkillAttacked(ECMSG Msg) { cmd_host_skill_attacked pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); if (pCmd.iDamage != 0 && (pCmd.cEquipment & 0x7f) != 0x7f) { EC_IvtrEquip pEquip = (EC_IvtrEquip)m_pEquipPack.GetItem(pCmd.cEquipment & 0x7f); if (pEquip != null) pEquip.AddCurEndurance(GameConstants.ARMOR_RUIN_SPEED); } // The host player is attacked, we should make an effect here if( GPDataTypeHelper.ISPLAYERID(pCmd.idAttacker) ) { EC_ElsePlayer pAttacker = EC_ManMessageMono.Instance.EC_ManPlayer.GetElsePlayer(pCmd.idAttacker); if (pAttacker) { if( !pAttacker.IsDead() ) { // Face to target pAttacker.TurnFaceTo(GetPlayerInfo().cid); } int refFake = 0; pAttacker.PlayAttackEffect(GetCharacterID(), pCmd.idSkill, 0, pCmd.iDamage, (uint)pCmd.attack_flag, pCmd.speed * 50,ref refFake,pCmd.section); pAttacker.EnterFightState(); } } else if( GPDataTypeHelper.ISNPCID(pCmd.idAttacker) ) { CECNPC pAttacker = EC_ManMessageMono.Instance.CECNPCMan.GetNPC(pCmd.idAttacker); if (pAttacker) { if( !pAttacker.IsDead()) { // Face to target pAttacker.NPCTurnFaceTo(GetPlayerInfo().cid); } pAttacker.PlayAttackEffect(GetCharacterID(), pCmd.idSkill, 0, pCmd.iDamage, (uint)pCmd.attack_flag, pCmd.speed,pCmd.section); } } CECAutoPolicy.GetInstance().SendEvent_BeHurt(pCmd.idAttacker); } private void OnMsgPlayerCastSkill(ECMSG Msg) { bool bDoOtherThing = false; int idTarget = 0; bool bActionStartSkill = false; int iActionTime = 1000; CECPlayerWrapper pWrapper = CECAutoPolicy.GetInstance().GetPlayerWrapper(); switch (Convert.ToInt32(Msg.dwParam2)) { case CommandID.OBJECT_CAST_SKILL: { cmd_object_cast_skill pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); // Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: Received OBJECT_CAST_SKILL, skillID={pCmd.skill}, " + // $"target={pCmd.target}, time={pCmd.time}, m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}, " + // $"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, " + // $"IsSpellingMagic={IsSpellingMagic()}"); if (m_pCurSkill != null) { m_pCurSkill.EndCharging(); } m_pCurSkill = GetPositiveSkillByID(pCmd.skill); if (m_pCurSkill == null) m_pCurSkill = GetEquipSkillByID(pCmd.skill); if (m_pCurSkill == null) m_pCurSkill = CECComboSkillState.Instance.GetInherentSkillByID((uint)pCmd.skill); if (m_pCurSkill == null) { Debug.Assert(m_pCurSkill != null, "Current skill should not be null"); return; } // Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: OBJECT_CAST_SKILL - Skill found, skillID={m_pCurSkill.GetSkillID()}, " + // $"IsChargeable={m_pCurSkill.IsChargeable()}"); if (m_pCurSkill.IsChargeable()) m_pCurSkill.StartCharging(pCmd.time); int iWaitTime = -1; if (m_pCurSkill.GetExecuteTime() >= 0) iWaitTime = pCmd.time + m_pCurSkill.GetExecuteTime(); CECHPWorkSpell pWork = (CECHPWorkSpell)m_pWorkMan.CreateWork(Host_work_ID.WORK_SPELLOBJECT); /*BMLogger.LogError($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: Created CECHPWorkSpell for skillID={m_pCurSkill.GetSkillID()}, " + $"executeTime={m_pCurSkill.GetExecuteTime()}, waitTime={iWaitTime}");*/ pWork.PrepareCast(pCmd.target, m_pCurSkill, iWaitTime); m_pWorkMan.StartWork_p1(pWork); // Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: OBJECT_CAST_SKILL - Created WORK_SPELLOBJECT, " + // $"skillID={m_pCurSkill.GetSkillID()}, target={pCmd.target}, waitTime={iWaitTime}, " + // $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}"); // Start time counter for some type skill // 为某些类型的技能启动时间计数器 if (!m_pCurSkill.IsChargeable()) { int iTime = pCmd.time; if (iTime < 10) iTime = 10; // a_ClampFloor m_IncantCnt.SetPeriod(iTime); m_IncantCnt.Reset(); } else { // make sure the counter is correct shown // 确保计数器正确显示 m_IncantCnt.Reset(true); } m_bSpellDSkill = false; TurnFaceTo(pCmd.target); m_idCurSkillTarget = pCmd.target; PlaySkillCastAction(m_pCurSkill.GetSkillID()); bActionStartSkill = true; iActionTime = iWaitTime; // Special logging for return-to-town skill (167) // 回城技能(167)的特殊日志 if (m_pCurSkill.GetSkillID() == ID_RETURNTOWN_SKILL) { Debug.Log( $"Return-to-town skill (167) cast - State2 should trigger SetReturntown(1) on server"); } break; } case CommandID.SKILL_PERFORM: { // Skill perform // 技能执行 int prepSkillIDBefore = m_pPrepSkill != null ? m_pPrepSkill.GetSkillID() : 0; // Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: Received SKILL_PERFORM, " + // $"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, " + // $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")} (clearing)"); m_pPrepSkill = null; // Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: SKILL_PERFORM cleared m_pPrepSkill (was skillID={prepSkillIDBefore}), now null"); if (m_pCurSkill != null && m_pCurSkill.IsDurative()) m_bSpellDSkill = true; // Special handling for return-to-town skill (167) // 回城技能(167)的特殊处理 // When skill 167 reaches State2, server calls SetReturntown(1) which should trigger MSG_HST_GOTO // 当技能167到达State2时,服务器调用SetReturntown(1),这应该触发MSG_HST_GOTO if (m_pCurSkill != null && m_pCurSkill.GetSkillID() == ID_RETURNTOWN_SKILL) { Debug.Log($"Skill 167 (Return to Town) performed - waiting for MSG_HST_GOTO from server"); } break; } case CommandID.HOST_STOP_SKILL: { m_pPrepSkill = null; CECSkill pSkillToMatch = m_pCurSkill; if (m_pCurSkill != null) { ClearComActFlagAllRankNodes(true); bool comboActive = m_pComboSkill != null && !m_pComboSkill.IsStop(); bool changeToMelee = m_pCurSkill.ChangeToMelee(); bool hasDelayedWork = m_pWorkMan.HasDelayedWork(); if ((comboActive || changeToMelee) && !hasDelayedWork) { bDoOtherThing = true; idTarget = m_idCurSkillTarget; } BMLogger.Log($"[COMBO] HOST_STOP_SKILL comboActive={comboActive} changeToMelee={changeToMelee} hasDelayedWork={hasDelayedWork} => bDoOtherThing={bDoOtherThing} idTarget={idTarget} curSkillID={m_pCurSkill?.GetSkillID()}"); m_pCurSkill.EndCharging(); m_pCurSkill = null; } AP.AP_ActionEvent((int)AP_EVENT.AP_EVENT_STOPSKILL); if (pSkillToMatch != null) { // m_pWorkMan中的当前任何Work为最高优先级或、或 CECHPWorkSpell 处于最高优先级且队列中有Delay时间 // 此时此处无法准确匹配,而导致的 CECHPWorkSpell 执行时将落后于其它 CECHPWorkSpell 执行期间将无法响应 // 在这种方法执行完成后,我们使用回城机制来 // If the current Work in m_pWorkMan is the highest priority or CECHPWorkSpell is at the highest priority and there is Delay time in the queue // At this point, it cannot be accurately matched, causing the CECHPWorkSpell execution to lag behind other CECHPWorkSpell execution and cannot respond // After this method is executed, we use the return to city mechanism m_pWorkMan.FinishWork(new CECHPWorkSpellMatcher(pSkillToMatch)); } StopSkillAttackAction(); m_idCurSkillTarget = 0; break; } case CommandID.SELF_SKILL_INTERRUPTED: { // Skill interrupted // 技能被打断 int skill_id = 0; m_pPrepSkill = null; CECSkill pSkillToMatch = m_pCurSkill; // 保存指针值,用在后面函数调用中 | Save pointer value for later function call if (m_pCurSkill != null) { skill_id = m_pCurSkill.GetSkillID(); ClearComActFlagAllRankNodes(false); if (((m_pComboSkill != null && !m_pComboSkill.IsStop()) || m_pCurSkill.ChangeToMelee()) && !m_pWorkMan.HasDelayedWork()) { bDoOtherThing = true; idTarget = m_idCurSkillTarget; } m_pCurSkill.EndCharging(); m_pCurSkill = null; } m_idCurSkillTarget = 0; if (pSkillToMatch != null) { // 逻辑同 HOST_STOP_SKILL 分支处理 | Logic same as HOST_STOP_SKILL branch m_pWorkMan.FinishWork(new CECHPWorkSpellMatcher(pSkillToMatch)); } StopSkillCastAction(); // Print a notify message // 打印提示消息 //EC_Game.GetGameRun().AddFixedMessage(FIXMSG_SKILLINTERRUPT); AP.AP_ActionEvent((int)AP_EVENT.AP_EVENT_STOPSKILL); // 通知策略技能被打断 | Notify policy that skill is interrupted CECAutoPolicy.GetInstance().SendEvent_SkillInterrupt(skill_id); break; } case CommandID.OBJECT_CAST_INSTANT_SKILL: { // Cast instant skill // 施放即时技能 cmd_object_cast_instant_skill pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); Debug.Assert(pCmd.caster == m_PlayerInfo.cid); CECSkill pSkill = GetPositiveSkillByID(pCmd.skill); if (pSkill == null) pSkill = GetEquipSkillByID(pCmd.skill); if (pSkill == null) { Debug.Assert(pSkill != null, "Skill should not be null"); return; } if (pCmd.target != 0 && pCmd.target != m_PlayerInfo.cid) TurnFaceTo(pCmd.target); PlaySkillCastAction(pSkill.GetSkillID()); bActionStartSkill = true; break; } case CommandID.OBJECT_CAST_POS_SKILL: { // Cast position skill (target position instead of target object) // 施放位置技能(目标位置而不是目标对象) cmd_object_cast_pos_skill pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); Debug.Assert(pCmd.caster == m_PlayerInfo.cid); // Log position BEFORE flashmove processing // 记录闪移处理前的位置 A3DVECTOR3 vHostPosBefore = EC_Utility.ToA3DVECTOR3(transform.position); /* BMLogger.LogError($"[DISTANCE_DEBUG] OBJECT_CAST_POS_SKILL: Received, skillID={pCmd.skill}, " + $"hostPosBefore=({vHostPosBefore.x:F2}, {vHostPosBefore.y:F2}, {vHostPosBefore.z:F2}), " + $"destPos=({pCmd.pos.x:F2}, {pCmd.pos.y:F2}, {pCmd.pos.z:F2}), " + $"target={pCmd.target}, distanceBefore={A3d_Magnitude(pCmd.pos - vHostPosBefore):F2}");*/ // Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: Received OBJECT_CAST_POS_SKILL, skillID={pCmd.skill}, " + // $"target={pCmd.target}, pos=({pCmd.pos.x:F2}, {pCmd.pos.y:F2}, {pCmd.pos.z:F2}), " + // $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}, " + // $"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, " + // $"IsSpellingMagic={IsSpellingMagic()}, IsFlashMoving={IsFlashMoving()}"); CECSkill pSkill = GetNormalSkill(pCmd.skill); if (pSkill == null) pSkill = GetEquipSkillByID(pCmd.skill); if (pSkill == null) { Debug.Assert(pSkill != null, "Skill should not be null"); break; } // Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: OBJECT_CAST_POS_SKILL - Skill found, skillID={pSkill.GetSkillID()}, " + // $"type={pSkill.GetType()}, rangeType={pSkill.GetRangeType()}"); TurnFaceTo(pCmd.target); if (pSkill.GetRangeType() != (int)CECSkill.RangeType.RANGE_SLEF && pSkill.GetRangeType() != (int)CECSkill.RangeType.RANGE_SELFSPHERE && (pSkill.GetSkillID() == 1095 || // 瞬影瞬斩 | Shadow instant slash pSkill.GetSkillID() == 1278 || // 魔·瞬影瞬斩 | Demon Shadow instant slash pSkill.GetSkillID() == 1279 || // 妖瞬影瞬斩 | Monster Shadow instant slash pSkill.GetSkillID() == 2313)) { A3DVECTOR3 vPos = pCmd.pos; if (!IsPosCollideFree(vPos)) { // Add a little height to ensure player's AABB won't embed with building // 添加一点高度以确保玩家的AABB不会嵌入建筑物 vPos += new A3DVECTOR3(0, 1, 0) * 0.1f; } // Ensure we are not under ground // 确保我们不在地下 A3DVECTOR3 vNormal = new A3DVECTOR3(); float vTerrainHeight = CECGameRun.Instance.GetWorld().GetTerrainHeight(vPos, ref vNormal); if (vPos.y < vTerrainHeight) vPos.y = vTerrainHeight; SetPos(EC_Utility.ToVector3(vPos)); m_CDRInfo.vTPNormal = vPos.y <= vTerrainHeight + 0.1f ? vNormal : new A3DVECTOR3(0); m_CDRInfo.fYVel = 0.0f; m_CDRInfo.vAbsVelocity = new A3DVECTOR3(0); ResetJump(); m_MoveCtrl.SetHostLastPos(vPos); m_MoveCtrl.SetLastSevPos(vPos); // Update camera // 更新相机 //UpdateFollowCamera(false, 10); } else { CECHPWorkFMove pWork = (CECHPWorkFMove)m_pWorkMan.CreateWork(CECHPWork.Host_work_ID.WORK_FLASHMOVE); // 检查技能执行时间,防止出现浮点数0,出现错误 | Check skill execute time to prevent float 0 error int nExecuteTime = pSkill.GetExecuteTime(); if (nExecuteTime < 50) nExecuteTime = 50; // a_ClampFloor if (pSkill.GetSkillID() == 1145 || // 刺客的瞬移技能,需要增加地面效果1145:百步穿杨瞬斩 | Assassin teleport skill pSkill.GetSkillID() == 1314 || // 魔·百步穿杨瞬斩 | Demon hundred steps instant slash pSkill.GetSkillID() == 1315 || // 妖·百步穿杨瞬斩 | Monster hundred steps instant slash pSkill.GetSkillID() == 1362 || // 影遁回杨 | Shadow escape return pSkill.GetSkillID() == 1690 || // 影遁魔·回杨 | Shadow escape demon return pSkill.GetSkillID() == 1691 || // 影遁妖回杨 | Shadow escape monster return pSkill.GetSkillID() == 1845 || // 星湖之狐·瞬移技能 | Star Lake Fox teleport pSkill.GetSkillID() == 1844 || // 星湖之狐·瞬移技能 | Star Lake Fox teleport pSkill.GetSkillID() == 1815 || // 妖精 飞 | Fairy fly pSkill.GetSkillID() == 2272 || // 一步登天 | One step to heaven pSkill.GetSkillID() == 2315 || // 百步穿杨瞬斩 | Hundred steps instant slash pSkill.GetSkillID() == 2285 || // 回杨 | Return pSkill.GetSkillID() == 2340 || // 突袭 | Raid pSkill.GetSkillID() == 2341 || // 突袭 | Raid pSkill.GetSkillID() == 2342 || // 突袭 | Raid pSkill.GetSkillID() == 2553 || // 飞箭雨 | Arrow rain pSkill.GetSkillID() == 2740 || // 魔·飞箭雨 | Demon arrow rain pSkill.GetSkillID() == 2741 || // 妖飞箭雨 | Monster arrow rain pSkill.GetSkillID() == 2559 || // 闪电 | Lightning pSkill.GetSkillID() == 2752 || // 魔·闪电 | Demon lightning pSkill.GetSkillID() == 2753) // 妖闪电 | Monster lightning { A3DVECTOR3 vPos = pCmd.pos; if (!IsPosCollideFree(vPos)) { // Add a little height to ensure player's AABB won't embed with building // 添加一点高度以确保玩家的AABB不会嵌入建筑物 vPos += new A3DVECTOR3(0, 1, 0) * 0.1f; } // Ensure we are not under ground // 确保我们不在地下 A3DVECTOR3 vNormal = new A3DVECTOR3(); float vTerrainHeight = CECGameRun.Instance.GetWorld().GetTerrainHeight(vPos, ref vNormal); if (vPos.y < vTerrainHeight) vPos.y = vTerrainHeight; m_CDRInfo.vTPNormal = vPos.y <= vTerrainHeight + 0.1f ? vNormal : new A3DVECTOR3(0); m_CDRInfo.fYVel = 0.0f; m_CDRInfo.vAbsVelocity = new A3DVECTOR3(0); ResetJump(); pWork.PrepareMove(pCmd.pos, nExecuteTime * 0.001f, pSkill.GetSkillID()); } else { if (pSkill.GetRangeType() == (int)CECSkill.RangeType.RANGE_SLEF || pSkill.GetRangeType() == (int)CECSkill.RangeType.RANGE_SELFSPHERE) { m_CDRInfo.vTPNormal = m_MoveCtrl.m_vFlashTPNormal; } pWork.PrepareMove(pCmd.pos, nExecuteTime * 0.001f, 0); } m_pWorkMan.StartWork_p2(pWork); iActionTime = nExecuteTime; // Update position tracking immediately so distance checks use correct position // The work will move the player visually over time, but we need position tracking // updated now so normal attacks can check distance correctly // 立即更新位置跟踪,以便距离检查使用正确的位置 // 工作将在时间上移动玩家,但我们需要现在更新位置跟踪,以便普通攻击可以正确检查距离 m_MoveCtrl.SetHostLastPos(pCmd.pos); m_MoveCtrl.SetLastSevPos(pCmd.pos); // Log position AFTER updating position tracking // 记录更新位置跟踪后的位置 A3DVECTOR3 vHostPosAfter = EC_Utility.ToA3DVECTOR3(transform.position); /* BMLogger.LogError($"[DISTANCE_DEBUG] OBJECT_CAST_POS_SKILL: After position update, skillID={pSkill.GetSkillID()}, " + $"hostPosAfter=({vHostPosAfter.x:F2}, {vHostPosAfter.y:F2}, {vHostPosAfter.z:F2}), " + $"SetHostLastPos=({pCmd.pos.x:F2}, {pCmd.pos.y:F2}, {pCmd.pos.z:F2}), " + $"destPos=({pCmd.pos.x:F2}, {pCmd.pos.y:F2}, {pCmd.pos.z:F2}), " + $"executeTime={nExecuteTime}, positionChange={A3d_Magnitude(pCmd.pos - vHostPosBefore):F2}");*/ // Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: OBJECT_CAST_POS_SKILL - Created WORK_FLASHMOVE, " + // $"skillID={pSkill.GetSkillID()}, executeTime={nExecuteTime}, destPos=({pCmd.pos.x:F2}, {pCmd.pos.y:F2}, {pCmd.pos.z:F2}), " + // $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}"); } bActionStartSkill = true; // Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: OBJECT_CAST_POS_SKILL - Completed, skillID={pCmd.skill}"); break; } case CommandID.PLAYER_CAST_RUNE_SKILL: { // Cast rune skill (item skill) // 施放符文技能(物品技能) cmd_player_cast_rune_skill pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); Debug.Assert(pCmd.caster == m_PlayerInfo.cid); if (m_pTargetItemSkill != null) { // Delete old target item skill m_pTargetItemSkill = null; } m_pTargetItemSkill = new CECSkill(pCmd.skill, pCmd.level); if (m_pTargetItemSkill == null) { Debug.Assert(m_pTargetItemSkill != null, "Target item skill should not be null"); return; } m_pCurSkill = m_pTargetItemSkill; if (m_pCurSkill.IsChargeable()) m_pCurSkill.StartCharging(pCmd.time); int iWaitTime = -1; if (m_pCurSkill.GetExecuteTime() >= 0) iWaitTime = pCmd.time + m_pCurSkill.GetExecuteTime(); CECHPWorkSpell pWork = (CECHPWorkSpell)m_pWorkMan.CreateWork(CECHPWork.Host_work_ID.WORK_SPELLOBJECT); pWork.PrepareCast(pCmd.target, m_pCurSkill, iWaitTime); m_pWorkMan.StartWork_p1(pWork); // Start time counter for some type skill // 为某些类型的技能启动时间计数器 if (!m_pCurSkill.IsChargeable()) { int iTime = pCmd.time; if (iTime < 10) iTime = 10; // a_ClampFloor m_IncantCnt.SetPeriod(iTime); m_IncantCnt.Reset(); } else { // make sure the counter is correct shown // 确保计数器正确显示 m_IncantCnt.Reset(true); } m_bSpellDSkill = false; TurnFaceTo(pCmd.target); m_idCurSkillTarget = pCmd.target; PlaySkillCastAction(m_pCurSkill.GetSkillID()); bActionStartSkill = true; iActionTime = iWaitTime; Debug.Log($"Cast rune skill({m_pCurSkill.GetSkillID()})"); break; } case CommandID.PLAYER_CAST_RUNE_INSTANT_SKILL: { // Cast instant rune skill // 施放即时符文技能 cmd_player_cast_rune_instant_skill pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); Debug.Assert(pCmd.caster == m_PlayerInfo.cid); if (m_pTargetItemSkill != null) { // Delete old target item skill m_pTargetItemSkill = null; } m_pTargetItemSkill = new CECSkill(pCmd.skill, pCmd.level); if (m_pTargetItemSkill == null) { Debug.Assert(m_pTargetItemSkill != null, "Target item skill should not be null"); return; } if (pCmd.target != 0 && pCmd.target != m_PlayerInfo.cid) TurnFaceTo(pCmd.target); PlaySkillCastAction(m_pTargetItemSkill.GetSkillID()); bActionStartSkill = true; break; } case CommandID.ERROR_MESSAGE: { // Log error message from server for debugging distance issues // 记录来自服务器的错误消息,用于调试距离问题 //cmd_error_message pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); //int errorCode = pCmd.message; // Common error codes: // 2 = FIXMSG_NEEDMP (Need MP) // 20 = FIXMSG_NEEDITEM (Need item) // 21 = FIXMSG_TARGETISFAR (Target is too far) // 22 = FIXMSG_TARGETTOOCLOSE (Target too close) A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(transform.position); int idCurrentTarget = m_idSelTarget; CECObject pTarget = idCurrentTarget > 0 ? EC_ManMessageMono.Instance.GetObject(idCurrentTarget, 1) : null; if (pTarget != null) { A3DVECTOR3 vTargetPos = EC_Utility.ToA3DVECTOR3(pTarget.transform.position); float fDistance = A3d_Magnitude(vTargetPos - vHostPos); BMLogger.LogError($"[DISTANCE_DEBUG] ERROR_MESSAGE from server: errorCode=, " + $"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " + $"targetPos=({vTargetPos.x:F2}, {vTargetPos.y:F2}, {vTargetPos.z:F2}), " + $"distance={fDistance:F2}, targetID={idCurrentTarget}, " + $"attackRange={m_ExtProps.ak.AttackRange:F2}, " + $"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, " + $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}"); } else { BMLogger.LogError($"[DISTANCE_DEBUG] ERROR_MESSAGE from server: errorCode=, " + $"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " + $"targetID={idCurrentTarget} (target object is null), " + $"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, " + $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}"); } bDoOtherThing = true; break; } default: Debug.Assert(false, "Unknown message type in OnMsgPlayerCastSkill"); break; } if (bActionStartSkill) AP.AP_ActionEvent((int)AP_EVENT.AP_EVENT_STARTSKILL, iActionTime); if (bDoOtherThing) { bool comboOk = m_pComboSkill != null && !m_pComboSkill.IsStop(); BMLogger.Log($"[COMBO] bDoOtherThing block comboOk={comboOk} m_pComboSkill={m_pComboSkill != null} IsStop={m_pComboSkill?.IsStop()} autoPolicy={CECAutoPolicy.GetInstance().IsAutoPolicyEnabled()}"); if (comboOk) { // Continue combo skill // 继续连击技能 if (CECAutoPolicy.GetInstance().IsAutoPolicyEnabled()) { BMLogger.Log($"[COMBO] Posting MSG_HST_CONTINUECOMBOSKILL group={m_pComboSkill.GetGroupIndex()}"); EC_ManMessage.PostMessage(EC_MsgDef.MSG_HST_CONTINUECOMBOSKILL, MANAGER_INDEX.MAN_PLAYER, 0, 0, m_pComboSkill.GetGroupIndex()); } else { BMLogger.Log($"[COMBO] Calling m_pComboSkill.Continue(false) directly"); m_pComboSkill.Continue(false); } } else { if (idTarget != 0 && idTarget != m_PlayerInfo.cid) NormalAttackObject(idTarget, true); } } } void OnMsgHstTargetIsFar(ECMSG Msg) { // TO DO: fix later //if(CmdNormalAttack(true, m_pComboSkill != null, 0, -1) ) // AP_ActionEvent(AP_EVENT_MELEEOUTOFRANGE, 1); if (CmdNormalAttack(true, false, 0, -1)) { // AP_ActionEvent(AP_EVENT_MELEEOUTOFRANGE, 1); AP.AP_ActionEvent(AP.AP_EVENT_MELEEOUTOFRANGE, 1); } } private void OnMsgHstDied(in ECMSG msg) { // Mark host player as corpse so CECPlayer.IsDead() returns true m_dwStates |= (uint)PlayerNPCState.GP_STATE_CORPSE; EventBus.PublishChannel(GetCharacterID(), new ClearComActFlagAllRankNodesEvent(true)); PlayAction((int)PLAYER_ACTION_TYPE.ACT_GROUNDDIE); PopupManager.NotifyPlayerDied(); } private bool NormalAttackObject(int idTarget, bool bForceAttack, bool bMoreClose = false) { if (idTarget == 0 || idTarget == m_PlayerInfo.cid) { // We should have check target isn't dead return false; } // Log position and distance information for debugging // 记录位置和距离信息用于调试 // Use server-tracked position instead of visual position for distance checks // This ensures distance checks use the correct position immediately after flashmove // 使用服务器跟踪的位置而不是视觉位置进行距离检查 // 这确保在闪移后立即使用正确的位置进行距离检查 A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(m_MoveCtrl.GetLastSevPos()); CECObject pTarget = EC_ManMessageMono.Instance.GetObject(idTarget, 1); if (pTarget != null && pTarget is CECNPC cECNPC) { A3DVECTOR3 vTargetPos = EC_Utility.ToA3DVECTOR3(pTarget.transform.position); float fDistance = A3d_Magnitude(vTargetPos - vHostPos); float fAttackRange = m_ExtProps.ak.AttackRange; bool bCanTouch = CanTouchTarget(vHostPos,vTargetPos, cECNPC.GetTouchRadius(), 1); // 1 = melee BMLogger.Log($"[DISTANCE_DEBUG] NormalAttackObject: Entry, idTarget={idTarget}, " + $"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " + $"targetPos=({vTargetPos.x:F2}, {vTargetPos.y:F2}, {vTargetPos.z:F2}), " + $"distance={fDistance:F2}, attackRange={fAttackRange:F2}, " + $"targetRadius={cECNPC.GetTouchRadius():F2}, CanTouch={bCanTouch}, " + $"bForceAttack={bForceAttack}, bMoreClose={bMoreClose}"); } else { BMLogger.Log($"[DISTANCE_DEBUG] NormalAttackObject: Entry, idTarget={idTarget}, target object is null"); } //if (!EC_Game.GetGameRun().GetWorld().GetObject(idTarget, 1)) // return false; bool bStartNewWork = false; bool bUseAutoPF = false; //CECPlayerWrapper* pWrapper = CECAutoPolicy::GetInstance().GetPlayerWrapper(); //if (CECAutoPolicy::GetInstance().IsAutoPolicyEnabled() && pWrapper.GetAttackError() >= 2) //bUseAutoPF = true; CECHPWorkTrace pWorkTrace = null; CECHPWork pWork = null; if ((pWork = m_pWorkMan.GetWork(CECHPWork.Host_work_ID.WORK_TRACEOBJECT)) != null) { pWorkTrace = pWork as CECHPWorkTrace; } else if ((pWork = m_pWorkMan.GetWork(CECHPWork.Host_work_ID.WORK_HACKOBJECT)) != null) { if ((pWork as CECHPWorkMelee).GetTarget() == idTarget) return false; // Host is attacking the target pWorkTrace = (CECHPWorkTrace)m_pWorkMan.CreateWork(CECHPWork.Host_work_ID.WORK_TRACEOBJECT); bStartNewWork = true; } else if (m_pWorkMan.CanStartWork(CECHPWork.Host_work_ID.WORK_TRACEOBJECT)) { pWorkTrace = (CECHPWorkTrace)m_pWorkMan.CreateWork(CECHPWork.Host_work_ID.WORK_TRACEOBJECT); bStartNewWork = true; } if (pWorkTrace != null) { pWorkTrace.SetTraceTarget( pWorkTrace.CreatTraceTarget(idTarget, CECHPWorkTrace.Trace_reason.TRACE_ATTACK, bForceAttack), bUseAutoPF); pWorkTrace.SetMoveCloseFlag(bMoreClose); if (bStartNewWork) m_pWorkMan.StartWork_p1(pWorkTrace); return true; } return false; } public int AttackableJudge(int idTarget, bool bForceAttack) { if (CannotAttack()) return 0; //if (CDlgAutoHelp::IsAutoHelp()) // return 0; if (idTarget == 0 || idTarget == m_PlayerInfo.cid) return -1; CECObject pObject = EC_ManMessageMono.Instance.GetObject(idTarget, 1); if (!pObject) return -1; // If target is pet, it's attacked possibility depends on it's monster if (GPDataTypeHelper.ISNPCID(idTarget)) { CECNPC pNPC = (CECNPC)pObject; int idMaster = pNPC.GetMasterID(); if (idMaster != 0) { // master¿ÉÄÜÊÇhostplayer if (idMaster == m_PlayerInfo.cid) return 0; //// Follow pet cannot be attacked //if (pNPC.IsPetNPC() && ((CECPet)pNPC).IsFollowPet()) // return 0; idTarget = idMaster; pObject = EC_ManMessageMono.Instance.GetObject(idTarget, 1); if (!pObject) return -1; } } int iRet = 0; if (GPDataTypeHelper.ISNPCID(idTarget)) { CECNPC pNPC = (CECNPC)pObject; // If this npc is host's pet, cannot be attacked if (pNPC.GetMasterID() == m_PlayerInfo.cid) return 0; // If it's a pet and can not be attacked, pet can be attacked only if it's a fighting pet //if (pNPC.IsPetNPC() && !((CECPet)pNPC).CanBeAttacked()) // return 0; if (IsInBattle()) // Host is in battle { if (InSameBattleCamp(pNPC)) iRet = 0; else { if (pNPC.IsMonsterNPC()) iRet = 1; else if (pNPC.IsServerNPC() && (IsInFortress() || pNPC.GetRoleInBattle() == 8)) // ¶Ô·þÎñÐÍNPCµÄ¹¥»÷£¬°ïÅÉ»ùµØ»ò³Çսʱ¿ÉÓà iRet = 1; else iRet = 0; } } else if (pNPC.IsServerNPC()) { // In sanctuary we cannot attack NPCs if (!IsPVPOpen() || m_bInSanctuary || !bForceAttack) iRet = 0; else iRet = 1; } else // Is monster { iRet = 1; } if (iRet == 1 && pNPC.GetOwnerFaction() > 0) { // Õë¶Ô°ïÅÉ PVP Õ½ÕùÖнûÖ¹²¿·Ö¹¥»÷ if (GetFactionID() == pNPC.GetOwnerFaction() || // ²»¹¥»÷ͬ°ï¹Ö pNPC.IsFactionPVPMineCar() && !CanAttackFactionPVPMineCar() || // ÎÞ·¨ÔÙ¹¥»÷Ëû°ï¿ó³µÇé¿ö pNPC.IsFactionPVPMineBase() && !CanAttackFactionPVPMineBase()) { // ÎÞ·¨ÔÙ¹¥»÷Ëû°ï´æ¿óµãÇé¿ö iRet = 0; } } } else if (GPDataTypeHelper.ISPLAYERID(idTarget)) { // Duel: allow attack only on duel opponent while in duel if (IsInDuel() && m_pvp.idDuelOpp == idTarget) return 1; if (m_pvp.iDuelState == Duel_state.DUEL_ST_STOPPING && m_pvp.idDuelOpp == idTarget) return 0; // Other player PVP not implemented here; treat as not attackable return 0; } else { return -1; } return iRet; } public bool CannotAttack() { return (m_dwLIES & (uint)Logic_Influence_Extned_states.LIES_DISABLEFIGHT) != 0; } public bool glb_GetForceAttackFlag(uint pdwParam) { /*bool bForceAttack = false; CECInputCtrl* pInputCtrl = g_pGame.GetGameRun().GetInputCtrl(); if (pdwParam) bForceAttack = pInputCtrl.IsCtrlPressed(*pdwParam); else bForceAttack = pInputCtrl.KeyIsBeingPressed(VK_CONTROL); return bForceAttack;*/ return false; } // Start normal attacking to selected target public bool CmdNormalAttack(bool bMoreClose /* false */, bool bCombo /* false */, int idTarget /* 0 */, int iForceAtk /* -1 */) { // StackChecker::ACTrace(2); // first of all see if we need to cancel sitdown work. if (m_pWorkMan.IsSitting()) { UnityGameSession.c2s_CmdStandUp(); return false; } if (!CanDo(ActionCanDo.CANDO_MELEE)) return false; if (InSlidingState()) return false; if (!bCombo) ClearComboSkill(); if (idTarget <= 0) idTarget = m_idSelTarget; bool bForceAttack; if (iForceAtk < 0) bForceAttack = glb_GetForceAttackFlag(0); else bForceAttack = iForceAtk > 0 ? true : false; // Duel: server accepts attack on opponent only with PVP/force mask if (IsInDuel() && idTarget == m_pvp.idDuelOpp) bForceAttack = true; if (AttackableJudge(idTarget, bForceAttack) != 1) return false; return NormalAttackObject(idTarget, bForceAttack, bMoreClose); } } }