Files
test/Assets/Scripts/CECHostPlayer.Combat.cs
T

1154 lines
54 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 = GPDataTypeHelper.FromBytes<cmd_be_hurt>((byte[])Msg.dwParam1 );
if (pCmd.damage != 0)
Damaged(pCmd.damage);
}
else if (cmd == CommandID.HURT_RESULT)
{
cmd_hurt_result pCmd = GPDataTypeHelper.FromBytes<cmd_hurt_result>((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<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);
/*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<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);
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;
// Mobile joystick keeps input while held; stop move work immediately (mirror HOST_NOTIFY_ROOT).
StopHostMovementOnDeath();
EventBus.PublishChannel(GetCharacterID(), new ClearComActFlagAllRankNodesEvent(true));
PlayAction((int)PLAYER_ACTION_TYPE.ACT_GROUNDDIE);
PopupManager.NotifyPlayerDied();
}
/// <summary>
/// Cancel active locomotion when host dies. Push-move reads joystick directly in GetPushDir,
/// so finishing WORK_MOVETOPOS alone is not enough until m_dwMoveRelDir is cleared.
/// </summary>
private void StopHostMovementOnDeath()
{
m_dwMoveRelDir = 0;
if (m_pWorkMan == null)
return;
if (m_pWorkMan.IsFollowing())
m_pWorkMan.FinishRunningWork(Host_work_ID.WORK_FOLLOW);
if (m_pWorkMan.IsMovingToPosition())
m_pWorkMan.FinishRunningWork(Host_work_ID.WORK_MOVETOPOS);
if (m_pWorkMan.IsTracing())
m_pWorkMan.FinishRunningWork(Host_work_ID.WORK_TRACEOBJECT);
}
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);
}
}
}