using BrewMonster.Managers; using BrewMonster.Network; using BrewMonster.Scripts; using BrewMonster.Scripts.World; using CSNetwork; using CSNetwork.GPDataType; using System; using UnityEngine; using static BrewMonster.Scripts.CECHPWork; using static CECPlayerWrapper; 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); } } } 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); CECIvtrEquip pEquip = (CECIvtrEquip)m_pEquipPack.GetItem(cEquip); if (pEquip) pEquip.AddCurEndurance(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 = (cmd_be_hurt)Msg.dwParam1; if (pCmd.damage != 0) Damaged(pCmd.damage); } else if (cmd == CommandID.HURT_RESULT) { cmd_hurt_result pCmd = (cmd_hurt_result)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)) { CECElsePlayer 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); } 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); 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); if (((m_pComboSkill != null && !m_pComboSkill.IsStop()) || m_pCurSkill.ChangeToMelee()) && !m_pWorkMan.HasDelayedWork()) { bDoOtherThing = true; idTarget = m_idCurSkillTarget; } 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) { if (m_pComboSkill != null && !m_pComboSkill.IsStop()) { // Continue combo skill // 继续连击技能 if (CECAutoPolicy.GetInstance().IsAutoPolicyEnabled()) EC_ManMessage.PostMessage(EC_MsgDef.MSG_HST_CONTINUECOMBOSKILL, MANAGER_INDEX.MAN_PLAYER, 0, 0, m_pComboSkill.GetGroupIndex()); else 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); } } 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); if (PopupManager.Instance != null) { PopupManager.Instance.OnPlayerDied(); } } 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()) // { // g_pGame.GetGameSession().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); } } }