1130 lines
53 KiB
C#
1130 lines
53 KiB
C#
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<cmd_host_attack_result>(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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Message MSG_HST_ATTACKONCE handler. One normal attack executed (consumes ammo/endurance). Continue combo if melee and combo active. Mirrors C++ OnMsgHstAttackOnce.</summary>
|
|
void OnMsgHstAttackOnce(ECMSG Msg)
|
|
{
|
|
byte[] data = Msg.dwParam1 as byte[];
|
|
if (data == null || data.Length < 1)
|
|
return;
|
|
cmd_attack_once pCmd = GPDataTypeHelper.FromBytes<cmd_attack_once>(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<cmd_host_attacked>(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 = (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<cmd_host_start_attack>((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<cmd_host_stop_attack>((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<cmd_host_skill_attack_result>((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<cmd_host_skill_attacked>((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<cmd_object_cast_skill>((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);
|
|
|
|
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<cmd_object_cast_instant_skill>((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<cmd_object_cast_pos_skill>((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<cmd_player_cast_rune_skill>((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<cmd_player_cast_rune_instant_skill>((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<cmd_error_message>((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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|