using BrewMonster.Scripts;
using BrewMonster.Scripts.Skills;
using CSNetwork;
using CSNetwork.GPDataType;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using BrewMonster.Assets.PerfectWorld.Scripts.Players;
using BrewMonster.Managers;
using BrewMonster.Network;
using BrewMonster.Scripts.Managers;
using BrewMonster.Scripts.World;
using PerfectWorld.Scripts.Managers;
using UnityEngine;
namespace BrewMonster
{
public partial class CECHostPlayer
{
///
/// Anti-spam gate for (client-side input flood control).
///
/// Purpose:
/// - Prevents rapid repeated shortcut-trigger calls (mouse/touch spam, key repeat, UI double-fire)
/// from spamming trace/cast requests and causing unstable client behavior.
///
///
/// Design:
/// - A small global minimum interval (ANY) blocks ultra-high-frequency bursts across all skills.
/// - A per-skill minimum interval (PER_SKILL) blocks repeatedly pressing the same skill rapidly.
/// - Exception: "press again to release a charging skill immediately" is allowed (not blocked),
/// so charge mechanics remain responsive.
///
///
/// Notes:
/// - Uses so throttling remains consistent under timeScale changes.
/// - This is client-side hygiene only; server cooldown/validation is still authoritative.
///
///
///
/// ApplySkillShortcut 的反刷 (客户端输入洪泛控制)
///
/// 目的:
/// - 防止鼠标/触摸狂点、按键连发、UI 重复触发导致的频繁施法请求,避免客户端不稳定。
///
/// 设计:
/// - ANY:对所有技能共用的极短间隔,过滤超高频 burst。
/// - PER_SKILL:对同一技能的短间隔,过滤同技能连点。
/// - 例外:充能技能“再次按下立即释放”不会被拦截,保证充能手感。
///
/// 备注:
/// - 使用 Time.unscaledTime,避免 timeScale 变化导致反刷失效。
/// - 仅客户端防抖/限流,服务器校验与冷却依然为准。
///
private const float APPLY_SKILL_SHORTCUT_MIN_INTERVAL_ANY = 0.05f;
private const float APPLY_SKILL_SHORTCUT_MIN_INTERVAL_PER_SKILL = 0.12f;
private float _applySkillShortcut_lastAnyTime = -999f;
private readonly Dictionary _applySkillShortcut_lastSkillTime = new Dictionary(64);
private bool ShouldBlockApplySkillShortcutSpam(int idSkill, bool allowChargeRelease)
{
// Use unscaled time so spam protection still works during slow-mo/timeScale changes.
// 使用 unscaledTime,避免 timeScale 变化导致反刷失效
float now = Time.unscaledTime;
if (now - _applySkillShortcut_lastAnyTime < APPLY_SKILL_SHORTCUT_MIN_INTERVAL_ANY)
return true;
if (!allowChargeRelease)
{
if (_applySkillShortcut_lastSkillTime.TryGetValue(idSkill, out float lastSkillTime))
{
if (now - lastSkillTime < APPLY_SKILL_SHORTCUT_MIN_INTERVAL_PER_SKILL)
return true;
}
}
_applySkillShortcut_lastAnyTime = now;
_applySkillShortcut_lastSkillTime[idSkill] = now;
return false;
}
public struct SkillShortCutConfig
{
public int setNum;
public int slotNum;
public int skillId;
};
public struct SkillGrpShortCutConfig
{
public int setNum;
public int slotNum;
public int groupIndex;
};
public CECSkill GetNormalSkill(int id, bool bSenior = false /* false */)
{
CECSkill pSkill = null;
if (ElementSkill.GetType((uint)id) == (byte)CECSkill.SkillType.TYPE_PASSIVE ||
ElementSkill.GetType((uint)id) == (byte)CECSkill.SkillType.TYPE_PRODUCE ||
ElementSkill.GetType((uint)id) == (byte)CECSkill.SkillType.TYPE_LIVE)
pSkill = GetPassiveSkillByID(id, bSenior);
else
pSkill = GetPositiveSkillByID(id, bSenior);
if (pSkill == null) // && m_pGoblin)
{
// This is a goblin skill
for (int i = 0; i < m_aGoblinSkills.Count; i++)
{
if (m_aGoblinSkills[i].GetSkillID() == id)
return m_aGoblinSkills[i];
}
}
if (pSkill == null) // may be target item skill
{
if (m_pTargetItemSkill != null && m_pTargetItemSkill.GetSkillID() == id)
pSkill = m_pTargetItemSkill;
}
return pSkill;
}
public CECComboSkill GetComboSkill()
{
return m_pComboSkill;
}
private void OnMsgHstSkillData(ECMSG Msg)
{
cmd_skill_data pCmd = default;
pCmd.skill_count = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1);
int offset = sizeof(uint);
int skillSize = Marshal.SizeOf();
pCmd.skill_list = new cmd_skill_data.SKILL[pCmd.skill_count];
// BMLogger.LogError("OnMsgHstSkillData: skill_count= " + pCmd.skill_count);
for (int i = 0; i < pCmd.skill_count; i++)
{
pCmd.skill_list[i] = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1, offset);
offset += skillSize;
}
if (pCmd.skill_list == null)
{
BMLogger.LogError("OnMsgHstSkillData: cmd is null");
return;
}
List skillSCConfigArray1 = new List();
List skillSCConfigArray2 = new List();
List skillGrpSCConfigArray1 = new List();
List skillGrpSCConfigArray2 = new List();
if (HostIsReady())
{
m_pWorkMan.FinishWork(new CECHPTraceSpellMatcher());
m_pPrepSkill = null;
m_pCurSkill = null;
ClearComboSkill();
SaveSkillShortcut(skillSCConfigArray1, m_aSCSets1, (int)Shortcut.NUM_HOSTSCSETS1);
SaveSkillShortcut(skillSCConfigArray2, m_aSCSets2, (int)Shortcut.NUM_HOSTSCSETS2);
SaveSkillGrpShortcut(skillGrpSCConfigArray1, m_aSCSets1, (int)Shortcut.NUM_HOSTSCSETS1);
SaveSkillGrpShortcut(skillGrpSCConfigArray2, m_aSCSets2, (int)Shortcut.NUM_HOSTSCSETS2);
/*
for (int i = 0; i < HostConstants.NUM_HOSTSCSETS1; i++)
{
if (hostPlayer.m_aSCSets1[i] != null)
{
hostPlayer.m_aSCSets1[i].RemoveSkillShortcuts();
}
}
qqqqqqaaaaaaaaw
for (int i = 0; i < HostConstants.NUM_HOSTSCSETS2; i++)
{
if (hostPlayer.m_aSCSets2[i] != null)
{
hostPlayer.m_aSCSets2[i].RemoveSkillShortcuts();
}
}*/
// Release passive skills
m_aSCSets1 = new CECShortcutSet[HostCfgConstants.NUM_HOSTSCSETS1];
m_aSCSets2 = new CECShortcutSet[HostCfgConstants.NUM_HOSTSCSETS2];
m_aPtSkills.Clear();
m_aPsSkills.Clear();
}
// Load skill data from command
// C++: GNET::ElementSkill::LoadSkillData(pCmd);
ElementSkill.LoadSkillData(pCmd);
// Create skill objects from command data
for (int i = 0; i < pCmd.skill_count; i++)
{
cmd_skill_data.SKILL data = pCmd.skill_list[i];
CECSkill skill = new CECSkill(data.id_skill, data.level);
// Categorize skills into positive and passive
int skillType = skill.GetType();
if (skillType != (int)CECSkill.SkillType.TYPE_PASSIVE &&
skillType != (int)CECSkill.SkillType.TYPE_PRODUCE &&
skillType != (int)CECSkill.SkillType.TYPE_LIVE)
{
m_aPtSkills.Add(skill);
}
else
{
m_aPsSkills.Add(skill);
}
}
// Restore and convert shortcuts after loading new skills
if (HostIsReady())
{
ConvertSkillShortcut(skillSCConfigArray1);
AssignSkillShortcut(skillSCConfigArray1, m_aSCSets1);
ConvertSkillShortcut(skillSCConfigArray2);
AssignSkillShortcut(skillSCConfigArray2, m_aSCSets2);
ConvertComboSkill();
ValidateSkillGrpShortcut(skillGrpSCConfigArray1);
AssignSkillGrpShortcut(skillGrpSCConfigArray1, m_aSCSets1);
ValidateSkillGrpShortcut(skillGrpSCConfigArray2);
AssignSkillGrpShortcut(skillGrpSCConfigArray2, m_aSCSets2);
}
if (HostIsReady())
{
// Update UI when profession changes, save all shortcut configurations
// to remove effects from intermediate skills (invalid pointers)
// C++: CECGameUIMan *pGameUIMan = g_pGame.GetGameRun().GetUIManager().GetInGameUIMan();
// pGameUIMan.UpdateSkillRelatedUI();
CECUIManager.Instance.UpdateSkillRelatedUI();
}
}
private void OnMsgHstLearnSkill(ECMSG Msg)
{
cmd_learn_skill pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1);
CECSkill pSkill = GetNormalSkill(pCmd.skill_id);
if (pSkill != null)
{
if (pCmd.skill_level > 0)
{
pSkill.LevelUp();
ElementSkill.SetLevel((uint)pCmd.skill_id, pCmd.skill_level);
}
else
{
RemoveNormalSkill(pCmd.skill_id);
}
}
else if (pCmd.skill_level > 0)
{
pSkill = new CECSkill(pCmd.skill_id, pCmd.skill_level);
if (pSkill == null)
{
Debug.Assert(pSkill != null);
return;
}
if (!pSkill.GetJunior().Empty())
{
ReplaceJuniorSkill(pSkill);
}
else
{
if (pSkill.GetType() != (int)CECSkill.SkillType.TYPE_PASSIVE &&
pSkill.GetType() != (int)CECSkill.SkillType.TYPE_PRODUCE &&
pSkill.GetType() != (int)CECSkill.SkillType.TYPE_LIVE)
m_aPtSkills.Add(pSkill);
else
m_aPsSkills.Add(pSkill);
}
ElementSkill.SetLevel((uint)pCmd.skill_id, pCmd.skill_level);
}
CECHostSkillModel.Instance.OnLearnSkill(pCmd.skill_id, pCmd.skill_level);
}
private void OnMsgComboSkillPrepare(ECMSG Msg)
{
cmd_combo_skill_prepare cmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1);
uint skillID = (uint)cmd.skill_id;
ComboSkillState comboSkillState = new()
{
skillid = skillID,
arg = new int[ComboSkillState.MAX_COMBO_ARG]
};
comboSkillState.arg = cmd.args;
/* if (cmd.args != null)
{
Array.Copy(cmd.args, comboSkillState.arg, Math.Min(cmd.args.Length, ComboSkillState.MAX_COMBO_ARG));
}*/
Dictionary comboSkillList = ElementSkill.GetComboSkActivated(comboSkillState);
CECComboSkillState.Instance.SetComboSkillState(comboSkillList, ref comboSkillState);
}
private void OnMsgContinueComboSkill(ECMSG Msg)
{
bool bMeleeing = Msg.dwParam1 != null && Convert.ToInt32(Msg.dwParam1) == 1;
if (bMeleeing != m_bMelee) bMeleeing = m_bMelee;
int iGroupID = Msg.dwParam2 != null ? Convert.ToInt32(Msg.dwParam2) : -1;
bool hasCombo = m_pComboSkill != null;
bool groupMatch = hasCombo && m_pComboSkill.GetGroupIndex() == iGroupID;
bool notStop = hasCombo && !m_pComboSkill.IsStop();
BMLogger.Log($"[COMBO] OnMsgContinueComboSkill received param1={Msg.dwParam1} param2={Msg.dwParam2} => bMeleeing={bMeleeing} iGroupID={iGroupID} hasCombo={hasCombo} groupMatch={groupMatch} notStop={notStop}");
if (hasCombo && groupMatch && notStop)
{
BMLogger.Log($"[COMBO] OnMsgContinueComboSkill calling Continue(bMeleeing={bMeleeing})");
m_pComboSkill.Continue(bMeleeing);
}
}
private void OnMsgHstCoolTimeData(ECMSG Msg)
{
cmd_cooltime_data pCmd = default;
var data = (byte[])Msg.dwParam1;
pCmd.count = GPDataTypeHelper.FromBytes(data, 0);
long offset = Marshal.SizeOf(typeof(ushort));
pCmd.list = new item_t[pCmd.count];
for (int i = 0; i < pCmd.count; i++)
{
pCmd.list[i] = GPDataTypeHelper.FromBytes(data, ref offset);
}
m_skillCoolTime.Clear();
for (int i = 0; i < pCmd.count; i++)
{
item_t item = pCmd.list[i];
if (item.idx > (int)CoolTimeIndex.GP_CT_SKILL_START)
{
// Is skill cool time
int idSkill = item.idx - (int)CoolTimeIndex.GP_CT_SKILL_START;
COOLTIME ct = default;
ct.iCurTime = item.cooldown;
ct.iMaxTime = item.max_cooltime;
Mathf.Clamp(ct.iCurTime, 0, ct.iMaxTime);
CECSkill pSkill = GetNormalSkill(idSkill);
if (pSkill != null)
{
pSkill.StartCooling(item.max_cooltime, item.cooldown);
}
/* else if (pSkill = CECComboSkillState::Instance().GetInherentSkillByID(idSkill))
{
pSkill.StartCooling(item.max_cooltime, item.cooldown);
}*/
else if (GetEquipSkillByID(idSkill) == null)
{
// Add to goblin skill list
pSkill = new CECSkill(idSkill, 1);
pSkill.StartCooling(item.max_cooltime, item.cooldown);
m_aGoblinSkills.Add(pSkill);
}
m_skillCoolTime[idSkill] = ct;
}
else if (item.idx >= 0 && item.idx < (int)CoolTimeIndex.GP_CT_MAX)
{
// Other cool time
COOLTIME ct = m_aCoolTimes[item.idx];
ct.iCurTime = item.cooldown;
ct.iMaxTime = item.max_cooltime;
Mathf.Clamp(ct.iCurTime, 0, ct.iMaxTime);
if (item.idx >= (int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN0 &&
item.idx <= (int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN4)
{
// other player skills should be set public cool down too.
uint mask = (uint)(1 << (item.idx - (int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN0));
for (int y = 0; y < GetPositiveSkillNum(); y++)
{
CECSkill pSkill = GetPositiveSkillByIndex(y);
if (pSkill != null && ((pSkill.GetCommonCoolDown() & mask) != 0))
pSkill.StartCooling(item.max_cooltime, item.cooldown);
}
}
}
else
{
BMLogger.LogError("item.idx >= 0: " + (item.idx >= 0));
}
}
UpdateEquipSkillCoolDown();
}
private void OnMsgHstSetCoolTime(ECMSG Msg)
{
cmd_set_cooldown pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1);
if (pCmd.cooldown_index < 0)
{
BMLogger.LogError("pCmd.cooldown_index >= 0 :" + (pCmd.cooldown_index >= 0));
return;
}
if (pCmd.cooldown_index < (int)CoolTimeIndex.GP_CT_MAX)
{
COOLTIME ct = m_aCoolTimes[pCmd.cooldown_index];
ct.iCurTime = pCmd.cooldown_time;
ct.iMaxTime = pCmd.cooldown_time;
Math.Min(ct.iCurTime, ct.iMaxTime);
m_aCoolTimes[pCmd.cooldown_index] = ct;
if (pCmd.cooldown_index == (int)CoolTimeIndex.GP_CT_CAST_ELF_SKILL)
{
int i;
for (i = 0; i < m_aGoblinSkills.Count; i++)
{
if (m_aGoblinSkills[i] != null && m_aGoblinSkills[i].GetCoolingCnt() == 0)
{
int fakeRef = 0;
int coolTime = GetCoolTime((int)CoolTimeIndex.GP_CT_CAST_ELF_SKILL, out fakeRef);
m_aGoblinSkills[i].StartCooling(coolTime, coolTime);
}
}
for (i = 0; i < m_aPsSkills.Count; i++)
{
CECSkill pSkill = GetPassiveSkillByIndex(i);
if (pSkill != null && (pSkill.GetCommonCoolDown() &
(1 << (pCmd.cooldown_index -
(int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN0))) != 0)
{
int fakeRef = 0;
int coolTime = GetCoolTime(pCmd.cooldown_time, out fakeRef);
pSkill.StartCooling(coolTime, coolTime);
}
}
}
if (pCmd.cooldown_index >= (int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN0 &&
pCmd.cooldown_index <= (int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN4)
{
// other player skills should be set public cool down too.
uint mask = (uint)(1 << (pCmd.cooldown_index - (int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN0));
for (int i = 0; i < GetPositiveSkillNum(); i++)
{
CECSkill pSkill = GetPositiveSkillByIndex(i);
int fakeRef = 0;
if (pSkill != null && (pSkill.GetCommonCoolDown() & mask) != 0)
{
int coolTime = GetCoolTime(pCmd.cooldown_index, out fakeRef);
pSkill.StartCooling(coolTime, coolTime);
//pSkill.StartCooling(GetCoolTime(pCmd.cooldown_index, out fakeRef), GetCoolTime(pCmd.cooldown_index, out fakeRef));
}
}
/*const std::map&inherentSkillMap = CECComboSkillState::Instance().GetInherentSkillMap();
std::map < unsigned int, CECSkill*>::const_iterator it;
for (it = inherentSkillMap.begin(); it != inherentSkillMap.end(); ++it)
{
it.second.StartCooling(GetCoolTime(pCmd.cooldown_index), GetCoolTime(pCmd.cooldown_index));
}*/
}
}
else if (pCmd.cooldown_index > (int)CoolTimeIndex.GP_CT_SKILL_START)
{
int idSkill = pCmd.cooldown_index - (int)CoolTimeIndex.GP_CT_SKILL_START;
COOLTIME ct;
if (!m_skillCoolTime.TryGetValue(idSkill, out ct))
{
// Key doesn't exist, create new entry
ct = new COOLTIME();
m_skillCoolTime[idSkill] = ct;
}
ct.iCurTime = pCmd.cooldown_time;
ct.iMaxTime = pCmd.cooldown_time;
ct.iCurTime = Math.Clamp(ct.iCurTime, 0, ct.iMaxTime);
m_skillCoolTime[idSkill] = ct;
//Math.Clamp(ct.iCurTime, 0, ct.iMaxTime);
CECSkill pSkill = GetNormalSkill(idSkill);
if (pSkill != null)
{
pSkill.StartCooling(pCmd.cooldown_time, pCmd.cooldown_time);
}
/* else if (pSkill = CECComboSkillState::Instance().GetInherentSkillByID(idSkill))
{
pSkill.StartCooling(pCmd.cooldown_time, pCmd.cooldown_time);
}*/
else if (GetEquipSkillByID(idSkill) == null)
{
BMLogger.LogError("HoangDev: pSkill " + pSkill);
}
else
{
pSkill = CECComboSkillState.Instance.GetInherentSkillByID((uint)idSkill);
if (pSkill != null)
{
pSkill.StartCooling(pCmd.cooldown_time, pCmd.cooldown_time);
}
else if (GetEquipSkillByID(idSkill) == null)
{
Debug.LogWarning($"OnMsgHstSetCoolTime: Skill {idSkill} not found in nomal/equip skills");
}
}
}
else
{
// This is a annoying assert and mean nothing, so we ignore it.
// ASSERT(0);
}
UpdateEquipSkillCoolDown(pCmd.cooldown_index);
}
public bool ApplySkillShortcut(int idSkill, bool bCombo = false /* false */,
int idSelTarget = 0 /* 0 */, int iForceAtk = -1 /* -1 */)
{
// Debug.LogError($"ApplySkillShortcut: Skill 167 detected, calling idSkill :"+ idSkill);
//StackChecker::ACTrace(4);
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: Entry, skillID={idSkill}, bCombo={bCombo}, idSelTarget={idSelTarget}, iForceAtk={iForceAtk}, " +
// $"IsSpellingMagic={IsSpellingMagic()}, m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}, " +
// $"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, IsFlashMoving={IsFlashMoving()}");
if (m_pActionSwitcher != null)
m_pActionSwitcher.PostMessge((int)EMsgActionSwitcher.MSG_CASTSKILL);
// Return-town skill is very special, handle it separately
// 回城技能非常特殊,单独处理
if (idSkill == ID_RETURNTOWN_SKILL)
{
Debug.Log($"ApplySkillShortcut: Skill 167 detected, calling ReturnToTargetTown");
return ReturnToTargetTown(0, bCombo);
}
//if (idSkill == ID_SUMMONPLAYER_SKILL)
// return SummonPlayer(idSelTarget, bCombo);
if (!CanDo(ActionCanDo.CANDO_SPELLMAGIC))
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - CanDo(CANDO_SPELLMAGIC) returned false, skillID={idSkill}");
return false;
}
if (InSlidingState())
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - InSlidingState() returned true, skillID={idSkill}");
return false;
}
if (!bCombo)
ClearComboSkill();
if (idSelTarget == 0)
idSelTarget = m_idSelTarget;
CECSkill pSkill = GetPositiveSkillByID(idSkill);
if (pSkill == null) pSkill = GetEquipSkillByID(idSkill);
if (pSkill == null) pSkill = CECComboSkillState.Instance.GetInherentSkillByID((uint)idSkill);
if (pSkill == null)
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - Skill {idSkill} not found");
return false;
}
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: Skill found, skillID={pSkill.GetSkillID()}, type={pSkill.GetType()}, " +
// $"ReadyToCast={pSkill.ReadyToCast()}, IsInstant={pSkill.IsInstant()}, IsFlashMove={pSkill.GetType() == (int)Skilltype.TYPE_FLASHMOVE}");
//// If we press a chargeable skill again when it's being charged,
//// we cast it out at once
bool allowChargeRelease = IsSpellingMagic() && m_pCurSkill != null && m_pCurSkill.IsCharging() &&
m_pCurSkill.GetSkillID() == pSkill.GetSkillID();
// Anti-spam: block rapid-fire shortcut calls (except charge-release press)
// 反刷:阻止短时间内狂点快捷键(充能释放第二次按下除外)
if (ShouldBlockApplySkillShortcutSpam(idSkill, allowChargeRelease))
return false;
if (allowChargeRelease)
{
m_pCurSkill.EndCharging();
UnityGameSession.c2s_SendCmdContinueAction();
return true;
}
int iCon = CheckSkillCastCondition(pSkill);
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: CheckSkillCastCondition returned {iCon} for skillID={idSkill}");
if (iCon != 0)
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - CheckSkillCastCondition returned error {iCon}, skillID={idSkill}");
ProcessSkillCondition(iCon);
return false;
}
//// Get force attack flag
bool bForceAttack = false;
if (iForceAtk < 0)
bForceAttack = glb_GetForceAttackFlag(0);
else
bForceAttack = iForceAtk > 0 ? true : false;
//// Check negative effect skill
if (pSkill.GetType() == (int)skill_type.TYPE_ATTACK || pSkill.GetType() == (int)skill_type.TYPE_CURSE)
{
if (idSelTarget == m_PlayerInfo.cid)
{
// Host cannot spell negative effect magic to himself.
//EC_Game.GetGameRun().AddFixedChannelMsg(FIXMSG_TARGETWRONG, GP_CHAT_FIGHT);
return false;
}
else if (idSelTarget != 0)
{
if (AttackableJudge(idSelTarget, bForceAttack) != 1)
return false;
}
}
//// Check whether target type match
int idCastTarget = idSelTarget;
int iTargetType = pSkill.GetTargetType();
if (pSkill.GetType() == (int)skill_type.TYPE_BLESS ||
pSkill.GetType() == (int)skill_type.TYPE_NEUTRALBLESS)
{
BMLogger.LogError($"ApplySkillShortcut: Skill {pSkill.GetSkillID()} is a bless skill, applying bless skill target rules");
if (iTargetType == 0 || !GPDataTypeHelper.ISPLAYERID(idSelTarget))
idCastTarget = m_PlayerInfo.cid;
// In some case, we shouldn't add bless effect to other players
if (GPDataTypeHelper.ISPLAYERID(idCastTarget) && idCastTarget != m_PlayerInfo.cid)
{
// If host has set bless skill filter only to himself, bless skill couldn't add to other players
byte byBLSMask = EC_Utility.glb_BuildBLSMask();
if (pSkill.GetRangeType() == (int)range_type.RANGE_POINT)
{
if (!IsTeamMember(idCastTarget))
{
if ((byBLSMask & (byte)PVPMask.GP_BLSMASK_SELF) != 0)
idCastTarget = m_PlayerInfo.cid;
else
{
EC_ElsePlayer pPlayer = (EC_ElsePlayer)EC_ManMessageMono.Instance.GetECManPlayer.GetPlayer(idCastTarget);
if (pPlayer == null)
{
// Ä¿±êÏûʧ
return false;
}
if (pPlayer.IsInvader() || pPlayer.IsPariah())
{
if ((byBLSMask & (byte)PVPMask.GP_BLSMASK_NORED) != 0)
idCastTarget = m_PlayerInfo.cid;
}
if (!IsFactionMember(pPlayer.GetFactionID()))
{
if ((byBLSMask & (byte)PVPMask.GP_BLSMASK_NOMAFIA) != 0)
idCastTarget = m_PlayerInfo.cid;
}
if (!IsFactionAllianceMember(pPlayer.GetFactionID()))
{
if ((byBLSMask & (byte)PVPMask.GP_BLSMASK_NOALLIANCE) != 0)
idCastTarget = m_PlayerInfo.cid;
}
if (GetForce() != pPlayer.GetForce())
{
if ((byBLSMask & (byte)PVPMask.GP_BLSMASK_NOFORCE) != 0)
idCastTarget = m_PlayerInfo.cid;
}
}
}
}
// If host is in duel, bless skill couldn't add to opponent
if (IsInDuel() && idSelTarget == m_pvp.idDuelOpp)
idCastTarget = m_PlayerInfo.cid;
// If host is in battle, bless skill couldn't add to enemies
if (IsInBattle())
{
EC_ElsePlayer pPlayer = EC_ManMessageMono.Instance.GetECManPlayer.GetElsePlayer(idCastTarget);
if (!InSameBattleCamp(pPlayer))
idCastTarget = m_PlayerInfo.cid;
}
}
}
/*else if (pSkill.GetType() == (int)skill_type.TYPE_BLESSPET)
{
// Get pet by target ID
// Note: In C++, this uses GetNPCMan()->GetPetByID()
// For C#, we try to get the object and check if it's the host's active pet
CECObject pPetObject = null;
int activePetNPCID = m_pPetCorral.GetActivePetNPCID();
if (idSelTarget != 0)
{
pPetObject = CECWorld.Instance.GetObject(idSelTarget, 0);
}
// If no pet found or target is host's active pet, cast on host's active pet
if (pPetObject == null || idSelTarget == activePetNPCID)
{
// Spell skill on host's pet
CECPetData pPetData = m_pPetCorral.GetActivePet();
if (pPetData == null ||
(pPetData.GetClass() != (int)GP_PET_TYPE.GP_PET_CLASS_COMBAT &&
pPetData.GetClass() != (int)GP_PET_TYPE.GP_PET_CLASS_SUMMON &&
pPetData.GetClass() != (int)GP_PET_TYPE.GP_PET_CLASS_EVOLUTION))
return false;
idCastTarget = activePetNPCID;
}
// Only fighting pet can be blessed
// Check if pet can be attacked (is in combat state)
// Note: CanBeAttacked() check is commented out as it may not be implemented yet
// Server will validate this
if (pPetObject != null)
{
// TODO: Implement CanBeAttacked check when CECPet/CECNPC has this method
// C++ code: if (pPet && !pPet->CanBeAttacked()) return false;
// For now, we'll allow the cast - server will validate
}
}*/
else
{
if (iTargetType != 0 && idCastTarget == 0)
return false;
}
// iTargetType == 4 means target must be pet. The problem is that pet will
// disappear from world after it died, so GetWorld().GetObject() will return
// NULL when host spells revive-pet skill on his dead pet. So, the target
// type of revive-pet skill should be 0
if (iTargetType != 0)
{
// Target shoundn't be a corpse ?
int iAliveFlag = 0;
if (iTargetType == 1)
iAliveFlag = 1;
else if (iTargetType == 2)
iAliveFlag = 2;
/* CECObject pObject = EC_Game.GetGameRun().GetWorld().GetObject(idCastTarget, iAliveFlag);
if (!pObject)
return false;*/
}
if (!IsMeleeing() && !IsSpellingMagic() &&
(iTargetType == 0 || idCastTarget == m_PlayerInfo.cid))
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: Entering main casting path, skillID={idSkill}, IsMeleeing={IsMeleeing()}, " +
// $"IsSpellingMagic={IsSpellingMagic()}, iTargetType={iTargetType}, idCastTarget={idCastTarget}");
// Cast this skill need't checking cast distance
if (!pSkill.ReadyToCast())
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - ReadyToCast() returned false, skillID={idSkill}");
return false;
}
// Check if skill can be cast immediately (blocks casting during flash move at PRIORITY_2)
// 检查技能是否可以立即施放(阻止在PRIORITY_2的闪移工作期间施放)
// Also check IsFlashMoving() as an additional safeguard since client-side work might finish
// before server processes the flashmove
// 同时检查 IsFlashMoving() 作为额外的保护,因为客户端工作可能在服务器处理闪移之前完成
if (IsFlashMoving() || !m_pWorkMan.CanCastSkillImmediately(pSkill.GetSkillID()))
{
bool hasWorkOnPriority2 = m_pWorkMan.HasWorkRunningOnPriority(CECHPWorkMan.Work_priority.PRIORITY_2);
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - IsFlashMoving={IsFlashMoving()}, CanCastSkillImmediately={m_pWorkMan.CanCastSkillImmediately(pSkill.GetSkillID())}, skillID={idSkill}, " +
// $"IsSpellingMagic={IsSpellingMagic()}, HasWorkOnPriority2={hasWorkOnPriority2}");
return false;
}
if (!pSkill.IsInstant() && pSkill.GetType() != (int)Skilltype.TYPE_FLASHMOVE)
{
if (!NaturallyStopMoving())
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - NaturallyStopMoving() returned false, skillID={idSkill}");
return false; // Couldn't stop naturally, so cancel casting skill
}
}
else if (pSkill.GetType() == (int)Skilltype.TYPE_FLASHMOVE)
{
if (!CanDo(ActionCanDo.CANDO_FLASHMOVE))
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - CanDo(CANDO_FLASHMOVE) returned false, skillID={idSkill}");
return false;
}
}
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: Setting prep skill and calling CastSkill, skillID={idSkill}, " +
// $"m_pPrepSkill before={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}");
m_pPrepSkill = pSkill;
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: m_pPrepSkill set to skillID={idSkill}, calling CastSkill");
CastSkill(m_PlayerInfo.cid, bForceAttack);
}
else if (IsSpellingMagic() && m_pCurSkill == pSkill)
{
// If we are casting the same skill and it's in cooling time
return false;
}
else // Have to trace selected object before cast skill
{
if (!pSkill.ReadyToCast())
return false;
if (CECCastSkillWhenMove.Instance.IsSkillSupported(pSkill.GetSkillID(), this) &&
m_pWorkMan.IsMovingToPosition() &&
m_pWorkMan.CanCastSkillImmediately(pSkill.GetSkillID()))
{
m_pPrepSkill = pSkill;
return CastSkill(idCastTarget, bForceAttack);
}
else
{
bool bTraceOK = false;
bool bUseAutoPF = false;
CECPlayerWrapper pWrapper = CECAutoPolicy.GetInstance().GetPlayerWrapper();
if (CECAutoPolicy.GetInstance().IsAutoPolicyEnabled() && pWrapper.GetAttackError() >= 2)
bUseAutoPF = true;
if (idCastTarget == 0)
{
idCastTarget =
GetCharacterID(); // ±ÜÃâË²ÒÆµÈ¼¼ÄÜʱ idCastTarget Ϊ0µ¼Ö CECWorkTrace::CreateTraceTarget ·µ»Ø¿Õ
}
CECHPWork pWork = m_pWorkMan.GetWork(CECHPWork.Host_work_ID.WORK_TRACEOBJECT);
if (pWork != null)
{
CECHPWorkTrace pWorkTrace = (CECHPWorkTrace)(pWork);
if (pWorkTrace.GetTraceReason() == CECHPWorkTrace.Trace_reason.TRACE_SPELL &&
pWorkTrace.GetTarget() == idCastTarget &&
pWorkTrace.GetPrepSkill() == pSkill)
return false; // We are just doing the same thing
pWorkTrace.SetTraceTarget(
pWorkTrace.CreatTraceTarget(idCastTarget, CECHPWorkTrace.Trace_reason.TRACE_SPELL,
bForceAttack),
bUseAutoPF);
pWorkTrace.SetPrepSkill(pSkill);
bTraceOK = true;
}
else if (m_pWorkMan.CanStartWork(CECHPWork.Host_work_ID.WORK_TRACEOBJECT))
{
CECHPWorkTrace pWork2 =
(CECHPWorkTrace)m_pWorkMan.CreateWork(CECHPWork.Host_work_ID.WORK_TRACEOBJECT);
pWork2.SetTraceTarget(
pWork2.CreatTraceTarget(idCastTarget, CECHPWorkTrace.Trace_reason.TRACE_SPELL,
bForceAttack), bUseAutoPF);
pWork2.SetPrepSkill(pSkill);
m_pWorkMan.StartWork_p1(pWork2);
bTraceOK = true;
}
if (!bTraceOK) return false;
// }
//}
}
}
return true;
}
public CECSkill GetPrepSkill()
{
return m_pPrepSkill;
}
public bool CastSkill(int idTarget, bool bForceAttack, CECObject pTarget = null)
{
int prepSkillID = m_pPrepSkill != null ? m_pPrepSkill.GetSkillID() : 0;
bool readyToCast = m_pPrepSkill != null ? m_pPrepSkill.ReadyToCast() : false;
bool isSpelling = IsSpellingMagic();
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Entry, prepSkillID={prepSkillID}, idTarget={idTarget}, bForceAttack={bForceAttack}, " +
// $"ReadyToCast={readyToCast}, IsSpellingMagic={isSpelling}, IsFlashMoving={IsFlashMoving()}");
// Check if prep skill is valid, ready to cast, and not currently spelling magic
if (m_pPrepSkill == null || !m_pPrepSkill.ReadyToCast() || IsSpellingMagic())
{
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: BLOCKED - Prep skill invalid or not ready, prepSkillID={prepSkillID}, " +
// $"ReadyToCast={readyToCast}, IsSpellingMagic={isSpelling}");
// Check if skill can change to melee attack
if (m_pPrepSkill != null && m_pPrepSkill.ChangeToMelee())
{
bool bFlag = m_pPrepSkill.ReadyToCast();
// Finish any tracing work
if (m_pWorkMan.IsTracing())
m_pWorkMan.FinishRunningWork(CECHPWork.Host_work_ID.WORK_TRACEOBJECT);
// Handle combo skill or normal attack
if (m_pComboSkill != null)
m_pComboSkill.Continue(false);
else
{
// Perform normal attack instead
NormalAttackObject(idTarget, true);
}
}
m_pPrepSkill = null;
return false;
}
if (m_pPrepSkill.GetType() == (int)CECSkill.SkillType.TYPE_ATTACK ||
m_pPrepSkill.GetType() == (int)CECSkill.SkillType.TYPE_CURSE)
{
if (idTarget != 0 && AttackableJudge(idTarget, bForceAttack) != 1)
{
m_pPrepSkill = null;
return false;
}
}
//TODO: Check cast condition - method not yet implemented
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: About to check conditions, m_pPrepSkill skillID={prepSkillID}, " +
// $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}");
int iRet = CheckSkillCastCondition(m_pPrepSkill);
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: CheckSkillCastCondition returned {iRet} for skillID={prepSkillID}, " +
// $"checked skillID={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}");
if (iRet != 0)
{
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: BLOCKED - CheckSkillCastCondition returned error {iRet} (2=NeedMP, 8=NeedAP, 10=PackFull, 20=NeedItem, 12=HPUnsatisfied), skillID={prepSkillID}");
switch (iRet)
{
case 2: // Need MP
// g_pGame.GetGameRun().AddFixedMessage(FIXMSG_NEEDMP);
break;
case 8: // Need AP
// g_pGame.GetGameRun().AddFixedMessage(FIXMSG_NEEDAP);
break;
case 10: // Pack full
// g_pGame.GetGameRun().AddFixedMessage(FIXMSG_PACKFULL1);
break;
case 20: // Need item
// Debug.LogError($"[SKILL_CAST_DEBUG] CastSkill: ERROR 20 - Need item for skillID={prepSkillID}, " +
// $"ItemCost={m_pPrepSkill.SkillCore?.GetItemCost() ?? 0}, " +
// $"ItemTotalNum={(m_pPrepSkill.SkillCore != null ? GetPack().GetItemTotalNum(m_pPrepSkill.SkillCore.GetItemCost()) : 0)}");
// g_pGame.GetGameRun().AddFixedMessage(FIXMSG_NEEDITEM);
break;
case 12: // HP unsatisfied
// g_pGame.GetGameRun().AddFixedMessage(FIXMSG_HP_UNSATISFIED);
break;
}
m_pPrepSkill = null;
return false;
}
byte byPVPMask = glb_BuildPVPMask(bForceAttack);
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Sending skill to server, skillID={prepSkillID}, idTarget={idTarget}, " +
// $"byPVPMask={byPVPMask}, IsInstant={m_pPrepSkill.IsInstant()}, IsFlashMove={m_pPrepSkill.GetType() == (int)CECSkill.SkillType.TYPE_FLASHMOVE}");
// Handle instant skills
if (m_pPrepSkill.IsInstant())
{
int countTarget = 1;
targetsCastSkill = new int[countTarget];
targetsCastSkill[0] = idTarget;
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Sending c2s_CmdCastInstantSkill, skillID={prepSkillID}, target={idTarget}, count={countTarget}");
UnityGameSession.c2s_CmdCastInstantSkill(m_pPrepSkill.GetSkillID(), byPVPMask, countTarget,
targetsCastSkill);
m_pPrepSkill = null;
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Instant skill sent, m_pPrepSkill cleared");
}
// Handle flash move skills (瞬移技能)
else if (m_pPrepSkill.GetType() == (int)CECSkill.SkillType.TYPE_FLASHMOVE)
{
// Self or self-sphere range types
if (m_pPrepSkill.GetRangeType() == (int)CECSkill.RangeType.RANGE_SLEF ||
m_pPrepSkill.GetRangeType() == (int)CECSkill.RangeType.RANGE_SELFSPHERE)
{
A3DVECTOR3 vDir = GetDir();
float fDist = m_pPrepSkill.GetCastRange(m_ExtProps.ak.AttackRange, GetPrayDistancePlus());
// 左侧之翼,左跳 (Left wing skill - jump left)
if (m_pPrepSkill.GetSkillID() == 1844)
{
vDir = A3d_RotatePosAroundY(-vDir, Mathf.PI / 2);
}
// 右侧之翼,右跳 (Right wing skill - jump right)
else if (m_pPrepSkill.GetSkillID() == 1845)
{
vDir = A3d_RotatePosAroundY(vDir, Mathf.PI / 2);
}
// 范围小于0则后跳 (If range < 0, jump backward)
else if (fDist < 0.0f)
{
vDir = -vDir;
}
fDist = Mathf.Abs(fDist);
A3DVECTOR3 vDest = m_MoveCtrl.FlashMove(vDir, 100.0f, fDist);
UnityEngine.Vector3 vDestVec3 = EC_Utility.ToVector3(vDest);
// Log position information before sending flashmove
// 在发送闪移前记录位置信息
A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(transform.position);
BMLogger.Log($"[DISTANCE_DEBUG] CastSkill: Before sending c2s_CmdCastPosSkill (flashmove self), skillID={prepSkillID}, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"destPos=({vDestVec3.x:F2}, {vDestVec3.y:F2}, {vDestVec3.z:F2}), " +
$"flashDistance={fDist:F2}, byPVPMask={byPVPMask}");
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Sending c2s_CmdCastPosSkill (flashmove self), skillID={prepSkillID}, " +
// $"pos=({vDestVec3.x:F2}, {vDestVec3.y:F2}, {vDestVec3.z:F2}), byPVPMask={byPVPMask}");
UnityGameSession.c2s_CmdCastPosSkill(m_pPrepSkill.GetSkillID(), vDestVec3, byPVPMask, 0, 0);
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Flashmove (self) sent, clearing m_pPrepSkill (was skillID={prepSkillID})");
m_pPrepSkill = null;
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Flashmove (self) complete, m_pPrepSkill is now null");
}
else
{
// 刺客如影随行类技能 (Assassin shadow-following skills)
bool bSuccess = false;
while (true)
{
// Break if no target or self-target
if (idTarget == 0 || idTarget == GetCharacterID())
break;
// Get target object
CECObject pObject = CECGameRun.Instance.GetWorld().GetObject(idTarget, 0);
if (pObject == null) break;
A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(transform.position); // GetPos()
A3DVECTOR3 vTargetPos = EC_Utility.ToA3DVECTOR3(pObject.transform.position); // pObject.GetPos()
// 判断技能释放距离限制是否满足 (Check if skill cast distance is satisfied)
float fTouchRadius = 0.0f;
if (GPDataTypeHelper.ISNPCID(idTarget))
{
CECNPC pNPC = pObject as CECNPC;
if (pNPC != null)
fTouchRadius = pNPC.GetTouchRadius();
else
break;
}
else if (GPDataTypeHelper.ISPLAYERID(idTarget))
{
EC_ElsePlayer pElsePlayer = pObject as EC_ElsePlayer;
if (pElsePlayer != null)
fTouchRadius = pElsePlayer.GetTouchRadius();
else
break;
}
else
break;
if (!CanTouchTarget(vTargetPos, fTouchRadius, 2))
{
// Target is far - show message
// g_pGame.GetGameRun().AddFixedMessage(FIXMSG_TARGETISFAR);
Debug.Log("Target is too far");
break;
}
A3DVECTOR3 vMoveDir = vTargetPos - vHostPos;
float fDist = EC_Utility.ToVector3(vMoveDir).magnitude;
// 距离目标太近,不处理 (Too close to target, don't process)
float fNearDist = 0.0f;
// TODO: Implement IsTooNear
if (IsTooNear(vTargetPos, ref fNearDist))
{
Debug.Log("Target is too near");
break;
}
// 计算要移往的目标位置(默认值) (Calculate target position to move to)
vMoveDir.Normalize();
A3DVECTOR3 vMovePos = vHostPos + vMoveDir * (fDist - fNearDist);
// TODO: Implement ClampAboveGround
float fClampedHeight = ClampAboveGround(vMovePos);
if (Mathf.Abs(fClampedHeight - vMovePos.y) >= 5.0f)
{
Debug.Log("Would stuck or so");
break;
}
vMovePos.y = fClampedHeight;
bool bPosVerified = false;
// 目标为带凸包的 NPC 时,单独处理 (Special handling for NPCs with collision)
if (GPDataTypeHelper.ISNPCID(idTarget))
{
// TODO: Implement CalcCollideFreePos for NPC AABB
CECNPC pNPC = pObject as CECNPC;
A3DAABB aabbNPC = new A3DAABB();
if (pNPC.GetCHAABB(ref aabbNPC))
{
A3DVECTOR3 vTestPos;
if (CalcCollideFreePos(aabbNPC, out vTestPos))
{
vMovePos = vTestPos;
bPosVerified = true;
}
else
{
Debug.Log("Would stuck or so");
break;
}
}
}
// TODO: Implement collision checking
if (!bPosVerified && !IsPosCollideFree(vMovePos))
{
A3DVECTOR3 vTestPos2;
if (!CalcVerticalCollideFreePos(vMovePos, out vTestPos2))
{
Debug.Log("Would stuck or so");
break;
}
vMovePos = vTestPos2;
bPosVerified = true;
}
//TODO: Implement IsTooNear check for final position
float reffake = 0;
if (IsTooNear(vMovePos, ref reffake))
{
Debug.Log("Target is too near");
break;
}
// 发送协议 (Send protocol)
UnityEngine.Vector3 vMovePosVec3 = EC_Utility.ToVector3(vMovePos);
// Log position information before sending flashmove
// 在发送闪移前记录位置信息
A3DVECTOR3 vHostPos2 = EC_Utility.ToA3DVECTOR3(transform.position);
CECObject pTarget2 = idTarget > 0 ? EC_ManMessageMono.Instance.GetObject(idTarget, 1) : null;
if (pTarget2 != null)
{
A3DVECTOR3 vTargetPos2 = EC_Utility.ToA3DVECTOR3(pTarget2.transform.position);
float fDistance2 = A3d_Magnitude(vTargetPos2 - vHostPos2);
BMLogger.Log($"[DISTANCE_DEBUG] CastSkill: Before sending c2s_CmdCastPosSkill (flashmove target), skillID={prepSkillID}, " +
$"hostPos=({vHostPos2.x:F2}, {vHostPos2.y:F2}, {vHostPos2.z:F2}), " +
$"targetPos=({vTargetPos2.x:F2}, {vTargetPos2.y:F2}, {vTargetPos2.z:F2}), " +
$"destPos=({vMovePosVec3.x:F2}, {vMovePosVec3.y:F2}, {vMovePosVec3.z:F2}), " +
$"distance={fDistance2:F2}, target={idTarget}, byPVPMask={byPVPMask}");
}
else
{
BMLogger.Log($"[DISTANCE_DEBUG] CastSkill: Before sending c2s_CmdCastPosSkill (flashmove target), skillID={prepSkillID}, " +
$"hostPos=({vHostPos2.x:F2}, {vHostPos2.y:F2}, {vHostPos2.z:F2}), " +
$"destPos=({vMovePosVec3.x:F2}, {vMovePosVec3.y:F2}, {vMovePosVec3.z:F2}), " +
$"target={idTarget} (target object is null), byPVPMask={byPVPMask}");
}
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Sending c2s_CmdCastPosSkill (flashmove target), skillID={prepSkillID}, " +
// $"pos=({vMovePosVec3.x:F2}, {vMovePosVec3.y:F2}, {vMovePosVec3.z:F2}), target={idTarget}, byPVPMask={byPVPMask}");
UnityGameSession.c2s_CmdCastPosSkill(m_pPrepSkill.GetSkillID(), vMovePosVec3, byPVPMask, 1, idTarget);
bSuccess = true;
}
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Flashmove (target) sent, clearing m_pPrepSkill (was skillID={prepSkillID}), bSuccess={bSuccess}");
m_pPrepSkill = null;
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Flashmove (target) complete, m_pPrepSkill is now null");
return bSuccess;
}
}
else
{
// Regular skill casting
byte byPVPMask2 = glb_BuildPVPMask(bForceAttack);
int targets = 1;
targetsCastSkill = new int[targets];
targetsCastSkill[0] = idTarget;
// Log position and distance information before sending skill cast
// Use server-tracked position instead of visual position for accurate distance checks
// 在发送技能施放前记录位置和距离信息
// 使用服务器跟踪的位置而不是视觉位置进行准确的距离检查
A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(m_MoveCtrl.GetLastSevPos());
CECObject pTarget1 = idTarget > 0 ? EC_ManMessageMono.Instance.GetObject(idTarget, 1) : null;
if (pTarget != null && pTarget1 is CECNPC cECNPC)
{
A3DVECTOR3 vTargetPos = EC_Utility.ToA3DVECTOR3(pTarget.transform.position);
float fDistance = A3d_Magnitude(vTargetPos - vHostPos);
float fSkillRange = m_pPrepSkill.GetCastRange(m_ExtProps.ak.AttackRange, GetPrayDistancePlus());
bool bCanTouch = CanTouchTarget(vTargetPos, cECNPC.GetTouchRadius(), 2); // 2 = skill
/* BMLogger.Log($"[DISTANCE_DEBUG] CastSkill: Before sending c2s_CmdCastSkill (regular), skillID={prepSkillID}, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"targetPos=({vTargetPos.x:F2}, {vTargetPos.y:F2}, {vTargetPos.z:F2}), " +
$"distance={fDistance:F2}, skillRange={fSkillRange:F2}, " +
$"targetRadius={cECNPC.GetTouchRadius():F2}, CanTouch={bCanTouch}, " +
$"target={idTarget}, byPVPMask={byPVPMask2}");*/
}
/* else
{
BMLogger.Log($"[DISTANCE_DEBUG] CastSkill: Before sending c2s_CmdCastSkill (regular), skillID={prepSkillID}, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"target={idTarget} (target object is null), byPVPMask={byPVPMask2}");
}*/
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Sending c2s_CmdCastSkill (regular), skillID={prepSkillID}, " +
// $"target={idTarget}, count={targets}, byPVPMask={byPVPMask2}");
UnityGameSession.c2s_CmdCastSkill(m_pPrepSkill.GetSkillID(), byPVPMask2, targets, targetsCastSkill);
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Regular skill sent, m_pPrepSkill still set (will be cleared on server response)");
}
return true;
}
// Return to a target town through skill
// 通过技能返回目标城镇
private bool ReturnToTargetTown(int idTarget, bool bCombo = false)
{
if (!CanDo(ActionCanDo.CANDO_SPELLMAGIC))
{
return false;
}
int idSkill = ID_RETURNTOWN_SKILL;
CECSkill pSkill = GetPositiveSkillByID(idSkill);
if (pSkill == null) pSkill = GetEquipSkillByID(idSkill);
if (pSkill == null)
{
Debug.LogError("ReturnToTargetTown: Skill 167 not found");
return false;
}
if (!bCombo)
{
// ClearComboSkill(); // Uncomment if ClearComboSkill exists
}
// Check skill cast condition (commented out in ApplySkillShortcut, so skip for now)
// int iCon = CheckSkillCastCondition(pSkill);
// if (iCon)
// {
// ProcessSkillCondition(iCon);
// return false;
// }
// If this skill is in cooling time or we are casting other skill, return
// 如果此技能在冷却时间或我们正在施放其他技能,返回
if (!pSkill.ReadyToCast() ||
!m_pWorkMan.CanCastSkillImmediately(pSkill.GetSkillID()))
{
// If the current Work in m_pWorkMan is CECHPWorkSpell or CECHPWorkFly, it should be executed first
// Otherwise, when receiving OBJECT_CAST_SKILL protocol, CECHPWorkSpell cannot be executed
// This causes CECHostPlayer::IsSpellingMagic() to return false, causing the client to send c2s_CmdCancelAction
// When this CECHPWorkSpell executes, it cannot respond
// After this method is executed, we use the return to city mechanism
// 如果 m_pWorkMan 中的当前 Work 是 CECHPWorkSpell 或 CECHPWorkFly,则应先执行
// 否则,当收到 OBJECT_CAST_SKILL 协议时,CECHPWorkSpell 无法执行
// 这会导致 CECHostPlayer::IsSpellingMagic() 返回 false,导致客户端发送 c2s_CmdCancelAction
// 当此 CECHPWorkSpell 执行时,它无法响应
// 在此方法执行完成后,我们使用回城机制
Debug.LogError(
$"ReturnToTargetTown: Skill not ready - ReadyToCast={pSkill.ReadyToCast()}, CanCastSkillImmediately={m_pWorkMan.CanCastSkillImmediately(pSkill.GetSkillID())}");
return false;
}
m_pPrepSkill = pSkill;
byte byPVPMask = glb_BuildPVPMask(false);
// Call c2s_CmdCastSkill with target parameter
// 使用目标参数调用 c2s_CmdCastSkill
int targets = 1;
int[] targetsCastSkill = new int[targets];
targetsCastSkill[0] = idTarget;
UnityGameSession.c2s_CmdCastSkill(idSkill, byPVPMask, targets, targetsCastSkill);
return true;
}
public int GetPositiveSkillNum()
{
return m_aPtSkills.Count;
}
public CECSkill GetPositiveSkillByIndex(int n)
{
return m_aPtSkills[n];
}
public int GetPassiveSkillNum()
{
return m_aPsSkills.Count;
}
CECSkill GetPassiveSkillByID(int id, bool bSenior /* false */)
{
CECSkill pSenior = null;
for (int i = 0; i < m_aPsSkills.Count; i++)
{
if (m_aPsSkills[i].GetSkillID() == id)
return m_aPsSkills[i];
else if (m_aPsSkills[i].GetJunior().Find((uint)id))
pSenior = m_aPsSkills[i];
}
if (bSenior && pSenior != null)
return pSenior;
return null;
}
public CECSkill GetPositiveSkillByID(int id, bool bSenior = false)
{
CECSkill pSenior = null;
for (int i = 0; i < m_aPtSkills.Count; i++)
{
if (m_aPtSkills[i].GetSkillID() == id)
return m_aPtSkills[i];
else if (m_aPtSkills[i].GetJunior().Find((uint)id))
pSenior = m_aPtSkills[i];
}
if (bSenior && pSenior != null)
return pSenior;
return null;
}
// C# conversion of CECHostPlayer::GetEquipSkillByID
// Assumes: GetEquipSkillNum() returns the count of equipment skills
// GetEquipSkillByIndex(int) returns a CECSkill at the given index
public CECSkill GetEquipSkillByID(int id)
{
CECSkill pRet = null;
for (int i = 0; i < GetEquipSkillNum(); i++)
{
CECSkill pSkill = GetEquipSkillByIndex(i);
if (pSkill != null && pSkill.GetSkillID() == id)
{
pRet = pSkill;
break;
}
}
return pRet;
}
public int GetEquipSkillNum()
{
return m_aEquipSkills.Count;
}
public CECSkill GetEquipSkillByIndex(int n)
{
return m_aEquipSkills[n];
}
// Check skill cast condition
// Returns: 0 if success, error code otherwise
// Error codes: 1=invalid weapon, 2=need mp, 3=invalid state, 6=target wrong, 7=invalid state,
// 8=need ap, 9=not enough ammo, 10=pack full, 11=invalid env, 12=hp unsatisfied,
// 13=combo skill not active, 20=need item
public int CheckSkillCastCondition(CECSkill pSkill)
{
int skillID = pSkill != null ? pSkill.GetSkillID() : 0;
// Debug.Log($"[SKILL_CAST_DEBUG] CheckSkillCastCondition: Entry, skillID={skillID}, SkillCore={(pSkill.SkillCore != null ? "not null" : "null")}");
// Check if skill requires an item
if (pSkill.SkillCore != null)
{
int idItem = pSkill.SkillCore.GetItemCost();
int itemTotalNum = GetPack().GetItemTotalNum(idItem);
// Debug.Log($"[SKILL_CAST_DEBUG] CheckSkillCastCondition: Item check, skillID={skillID}, idItem={idItem}, itemTotalNum={itemTotalNum}");
if (idItem > 0 && itemTotalNum <= 0)
{
// Debug.LogError($"[SKILL_CAST_DEBUG] CheckSkillCastCondition: ERROR 20 - Need item, skillID={skillID}, requiredItem={idItem}, have={itemTotalNum}");
return 20; // Need item
}
}
if (pSkill.GetComboSkPreSkill() != 0)
{
if (!CECComboSkillState.Instance.IsActiveComboSkill((uint)pSkill.GetSkillID()))
{
return 13; // Combo skill not active
}
}
// Build UseRequirement info
UseRequirement Info = new UseRequirement();
Info.mp = m_BasicProps.iCurMP;
Info.ap = m_BasicProps.iCurAP;
Info.form = m_iShape; // Different from PW, no need to mask
Info.freepackage = m_pPack.GetEmptySlotNum();
Info.move_env = GetMoveEnv();
Info.is_combat = IsFighting();
Info.hp = m_BasicProps.iCurHP;
Info.max_hp = m_ExtProps.bs.max_hp;
Info.combo_state = CECComboSkillState.Instance.GetComboSkillState(); // TODO: Implement GetComboSkillState
// Get weapon's major class ID
int iReason = 0;
CECIvtrWeapon pWeapon =
(CECIvtrWeapon)m_pEquipPack.GetItem((int)IndexOfIteminEquipmentInventory.EQUIPIVTR_WEAPON);
if (pWeapon == null || pWeapon.CurEndurance == 0)
{
//BMLogger.LogError(GetName() + " CheckSkillCastCondition: Weapon major type ID = " + Info.weapon);
Info.weapon = 0;
}
else if (!CanUseEquipment(pWeapon, ref iReason))
{
Info.weapon = (iReason == 5) ? (int)pWeapon.GetDBMajorType().id : 0; }
else
{
Info.weapon = (int)pWeapon.GetDBMajorType().id; }
//BMLogger.LogError(GetName() + " CheckSkillCastCondition: Weapon major type ID = " + Info.weapon);
// Get remaining arrow number
CECIvtrArrow pArrow =
(CECIvtrArrow)m_pEquipPack.GetItem((int)IndexOfIteminEquipmentInventory.EQUIPIVTR_PROJECTILE);
if (pArrow != null && CanUseProjectile(pArrow))
{
Info.arrow = pArrow.GetCount();
}
if (pSkill.SkillCore != null)
{
int conditionResult = pSkill.SkillCore.Condition((uint)pSkill.GetSkillID(), Info, pSkill.GetSkillLevel());
// Debug.Log($"[SKILL_CAST_DEBUG] CheckSkillCastCondition: SkillCore.Condition returned {conditionResult} for skillID={skillID}, " +
// $"MP={Info.mp}, AP={Info.ap}, HP={Info.hp}/{Info.max_hp}, weapon={Info.weapon}, arrow={Info.arrow}, " +
// $"is_combat={Info.is_combat}, move_env={Info.move_env}");
return conditionResult;
}
// Debug.Log($"[SKILL_CAST_DEBUG] CheckSkillCastCondition: Success (no SkillCore), skillID={skillID}");
return 0; // Success
}
// Process skill condition error - display appropriate message
// Returns: true if message was displayed, false otherwise
public bool ProcessSkillCondition(int iCon)
{
int iMsg = -1;
switch (iCon)
{
case 1: iMsg = (int)FixedMsg.FIXMSG_INVALIDWEAPON; break;
case 2: iMsg = (int)FixedMsg.FIXMSG_NEEDMP; break;
case 6: iMsg = (int)FixedMsg.FIXMSG_TARGETWRONG; break;
case 3: iMsg = (int)FixedMsg.FIXMSG_SKILL_INVALIDSTATE; break;
case 7: iMsg = (int)FixedMsg.FIXMSG_SKILL_INVALIDSTATE; break;
case 8: iMsg = (int)FixedMsg.FIXMSG_NEEDAP; break;
case 9: iMsg = (int)FixedMsg.FIXMSG_NOTENOUGHAMMO; break;
case 10: iMsg = (int)FixedMsg.FIXMSG_PACKFULL1; break;
case 11: iMsg = (int)FixedMsg.FIXMSG_SKILL_INVALIDENV; break;
case 20: iMsg = (int)FixedMsg.FIXMSG_NEEDITEM; break;
case 12: iMsg = (int)FixedMsg.FIXMSG_HP_UNSATISFIED; break;
}
if (iMsg >= 0)
{
// TODO: Implement AddFixedChannelMsg or use existing message system
// EC_Game.GetGameRun().AddFixedChannelMsg(iMsg, GP_CHAT_FIGHT);
Debug.LogWarning($"Skill condition failed: {iMsg}");
}
return iMsg >= 0;
}
// Remove skill reference / 删除技能引用
void RemoveSkillReference(int idSkill)
{
if (idSkill <= 0) return;
// Remove reference to self skill / 删除对技能的引用
if (m_pPrepSkill != null && m_pPrepSkill.GetSkillID() == idSkill)
m_pPrepSkill = null;
if (m_pCurSkill != null && m_pCurSkill.GetSkillID() == idSkill)
m_pCurSkill = null;
if (m_pComboSkill != null && m_pComboSkill.FindSkillID(idSkill))
ClearComboSkill();
if (m_pWorkMan != null)
{
CECHPWork pWork = m_pWorkMan.GetWork(CECHPWork.Host_work_ID.WORK_TRACEOBJECT);
if (pWork != null)
{
CECHPWorkTrace pWorkTrace = pWork as CECHPWorkTrace;
if (pWorkTrace != null &&
pWorkTrace.GetTraceReason() == CECHPWorkTrace.Trace_reason.TRACE_SPELL &&
pWorkTrace.GetPrepSkill() != null &&
pWorkTrace.GetPrepSkill().GetSkillID() == idSkill)
pWorkTrace.Reset();
}
}
int i;
for (i = 0; i < HostCfgConstants.NUM_HOSTSCSETS1; i++)
{
if (m_aSCSets1[i] != null)
m_aSCSets1[i].RemoveSkillShortcut(idSkill);
}
for (i = 0; i < HostCfgConstants.NUM_HOSTSCSETS2; i++)
{
if (m_aSCSets2[i] != null)
m_aSCSets2[i].RemoveSkillShortcut(idSkill);
}
}
// Remove skill / 删除技能
void RemoveNormalSkill(int idSkill)
{
// Delete equipped skills / 删除装备技能
if (idSkill <= 0) return;
RemoveSkillReference(idSkill);
// Delete skill list pointer / 删除技能列表指针
int i;
for (i = 0; i < m_aPtSkills.Count; i++)
{
if (m_aPtSkills[i].GetSkillID() == idSkill)
{
m_aPtSkills.RemoveAt(i);
return;
}
}
for (i = 0; i < m_aPsSkills.Count; i++)
{
if (m_aPsSkills[i].GetSkillID() == idSkill)
{
m_aPsSkills.RemoveAt(i);
return;
}
}
for (i = 0; i < m_aGoblinSkills.Count; i++)
{
if (m_aGoblinSkills[i].GetSkillID() == idSkill)
{
m_aGoblinSkills.RemoveAt(i);
return;
}
}
}
// Clear combo skill / 清除连击技能
public bool ApplyComboSkill(int iGroup, bool bIgnoreAtkLoop = false, int iForceAtk = -1)
{
ClearComboSkill();
CECComboSkill comboSkill = new();
bool bForceAttack = iForceAtk < 0 ? glb_GetForceAttackFlag(0) : iForceAtk > 0;
if (!comboSkill.Init(this, iGroup, m_idSelTarget, bForceAttack, bIgnoreAtkLoop))
{
return false;
}
m_pComboSkill = comboSkill;
BMLogger.Log($"[COMBO] ApplyComboSkill group={iGroup} target={m_idSelTarget} calling first Continue(m_bMelee={m_bMelee})");
m_pComboSkill.Continue(m_bMelee);
return true;
}
public void ClearComboSkill()
{
if (m_pComboSkill != null)
{
m_pComboSkill = null;
}
}
// Replace specified skill with it's senior skill / 用高级技能替换指定的低级技能
void ReplaceJuniorSkill(CECSkill pSeniorSkill)
{
if (pSeniorSkill == null)
{
Debug.Assert(pSeniorSkill != null);
return;
}
SkillArrayWrapper juniorArray = pSeniorSkill.GetJunior();
if (juniorArray.Empty())
{
Debug.Assert(false);
return;
}
int i;
// Update shortcuts ... / 更新快捷方式...
for (i = 0; i < HostCfgConstants.NUM_HOSTSCSETS1; i++)
{
if (m_aSCSets1[i] != null)
m_aSCSets1[i].ReplaceSkillID(juniorArray, pSeniorSkill);
}
for (i = 0; i < HostCfgConstants.NUM_HOSTSCSETS2; i++)
{
if (m_aSCSets2[i] != null)
m_aSCSets2[i].ReplaceSkillID(juniorArray, pSeniorSkill);
}
// Update skill groups ... / 更新技能组...
// Note: Combo skill update logic may need to be added here
// Original C++ code had EC_VIDEO_SETTING and combo skill group logic
}
public int CheckSkillLearnCondition(int idSkill, bool bCheckBook)
{
int iLevel = 1;
CECSkill pSkill = GetNormalSkill(idSkill);
if (pSkill != null)
iLevel = pSkill.GetSkillLevel() + 1;
if (iLevel == 1 && bCheckBook)
{
// Do we have the skill book ?
int idBook = ElementSkill.GetRequiredBook((uint)idSkill, iLevel);
if (idBook != 0 && m_pPack.FindItem(idBook) < 0)
return 8;
}
// Build player information
LearnRequirement Info;
Info.level = GetMaxLevelSofar();
Info.sp = m_BasicProps.iSP;
Info.money = (int)m_iMoneyCnt;
Info.profession = m_iProfession;
Info.rank = m_BasicProps.iLevel2;
Info.realm_level = GetRealmLevel();
return ElementSkill.LearnCondition((uint)idSkill, Info, iLevel);
}
public bool GetSkillCoolTime(int idSkill, out COOLTIME ct)
{
// 获取技能的非公共冷却时间
bool bFound = false;
ct = new COOLTIME();
if (m_skillCoolTime.TryGetValue(idSkill, out ct))
{
bFound = true;
}
return bFound;
}
// Update equipment skill cool down
// 更新装备技能冷却
public void UpdateEquipSkillCoolDown(int cooldown_index = -1)
{
if (cooldown_index < 0)
{
for (int i = 0; i < GetEquipSkillNum(); ++i)
{
CECSkill pSkill = GetEquipSkillByIndex(i);
// 检查技能冷却
// Check skill cooldown
COOLTIME temp;
if (GetSkillCoolTime(pSkill.GetSkillID(), out temp))
{
pSkill.StartCooling(temp.iMaxTime, temp.iCurTime);
continue;
}
// 检查公共冷却
// Check common cooldown
int ccd = pSkill.GetCommonCoolDown();
if (ccd == 0) continue;
for (int j = 0; j < 5; ++j)
{
if ((ccd & (1 << j)) != 0)
{
COOLTIME ct = m_aCoolTimes[(int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN0 + j];
pSkill.StartCooling(ct.iMaxTime, ct.iCurTime);
break;
}
}
}
}
else
{
if (cooldown_index > (int)CoolTimeIndex.GP_CT_SKILL_START)
{
int idSkill = cooldown_index - (int)CoolTimeIndex.GP_CT_SKILL_START;
CECSkill pSkill = GetEquipSkillByID(idSkill);
COOLTIME temp;
if (pSkill != null && GetSkillCoolTime(idSkill, out temp))
pSkill.StartCooling(temp.iMaxTime, temp.iCurTime);
}
else if (cooldown_index >= (int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN0 &&
cooldown_index <= (int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN4)
{
int index = cooldown_index - (int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN0;
COOLTIME ct = m_aCoolTimes[(int)CoolTimeIndex.GP_CT_SKILLCOMMONCOOLDOWN0 + index];
uint mask = (uint)(1 << index);
for (int i = 0; i < GetEquipSkillNum(); ++i)
{
CECSkill pSkill = GetEquipSkillByIndex(i);
int ccd = pSkill.GetCommonCoolDown();
if ((ccd & mask) != 0)
pSkill.StartCooling(ct.iMaxTime, ct.iCurTime);
}
}
}
}
public CECSkill GetPassiveSkillByIndex(int n)
{
return m_aPsSkills[n];
}
public void AssignSkillGrpShortcut(List skillGrpSCConfigArray, CECShortcutSet[] aSCSets)
{
for (int i = 0; i < skillGrpSCConfigArray.Count; i++)
{
SkillGrpShortCutConfig cfg = skillGrpSCConfigArray[i];
if (cfg.groupIndex != -1)
{
// C++ không check null; nếu muốn an toàn hơn thì check aSCSets[cfg.setNum] != null
aSCSets[cfg.setNum].CreateSkillGroupShortcut(cfg.slotNum, cfg.groupIndex);
}
}
}
public void ValidateSkillGrpShortcut(List skillGrpSCConfigArray)
{
for (int i = 0; i < skillGrpSCConfigArray.Count; i++)
{
// C++ đang lấy VideoSettings trong mỗi vòng lặp (giữ nguyên hành vi)
EC_VIDEO_SETTING vs = EC_Game.GetConfigs().GetVideoSettings();
SkillGrpShortCutConfig cfg = skillGrpSCConfigArray[i];
if (vs.comboSkill[cfg.groupIndex].nIcon == 0)
cfg.groupIndex = -1; // -1 biểu thị shortcut combo-skill không hợp lệ
skillGrpSCConfigArray[i] = cfg; // cần nếu SkillGrpShortCutConfig là struct
}
}
public void ConvertComboSkill()
{
EC_VIDEO_SETTING vs = EC_Game.GetConfigs().GetVideoSettings();
for (int i = 0; i < EC_ConfigConstants.EC_COMBOSKILL_NUM; i++)
{
if (vs.comboSkill[i].nIcon == 0)
continue;
for (int j = 0; j < EC_ConfigConstants.EC_COMBOSKILL_LEN; j++)
{
int oldSkillId = vs.comboSkill[i].idSkill[j];
if (oldSkillId == 0)
continue;
int convertedSkillId = CECSkillConvert.Instance.GetConvertSkill(oldSkillId);
int newSkillId = (convertedSkillId == 0) ? oldSkillId : convertedSkillId;
// C++: nếu skill tồn tại, hoặc là -1/-2 (loop / normal attack shortcut) thì giữ
if (GetNormalSkill(newSkillId) != null || newSkillId == -1 || newSkillId == -2)
{
vs.comboSkill[i].idSkill[j] = (short)newSkillId;
}
else
{
// Không hợp lệ -> clear combo theo oldSkillId, icon về 0, thoát vòng lặp j
vs.comboSkill[i].Clear(oldSkillId);
vs.comboSkill[i].nIcon = 0;
break;
}
}
}
EC_Game.GetConfigs().SetVideoSettings(vs);
}
public void AssignSkillShortcut(List skillSCConfigArray, CECShortcutSet[] aSCSets)
{
for (int i = 0; i < skillSCConfigArray.Count; i++)
{
SkillShortCutConfig cfg = skillSCConfigArray[i];
if (cfg.skillId == 0)
{
BMLogger.LogError("AssignSkillShortcut: skillId is zero");
}
CECSkill convertSkill = GetNormalSkill(cfg.skillId);
if (convertSkill != null)
{
// C++ không check null set => nếu muốn y hệt thì bỏ check này
if (aSCSets[cfg.setNum] != null)
{
aSCSets[cfg.setNum].CreateSkillShortcut(cfg.slotNum, convertSkill);
}
}
}
}
public void ConvertSkillShortcut(List skillSCConfigArray)
{
for (int i = 0; i < skillSCConfigArray.Count; i++)
{
if (skillSCConfigArray[i].skillId == 0)
{
BMLogger.LogError("ConvertSkillShortcut: skillId is zero");
}
int oldSkillId = skillSCConfigArray[i].skillId;
int convertSkillId = CECSkillConvert.Instance.GetConvertSkill(oldSkillId);
// it->skillId = (convertSkillId == 0) ? it->skillId : convertSkillId;
SkillShortCutConfig cfg = skillSCConfigArray[i];
cfg.skillId = (convertSkillId == 0) ? oldSkillId : convertSkillId;
skillSCConfigArray[i] = cfg; // needed if SkillShortCutConfig is a struct
}
}
public void SaveSkillGrpShortcut(
List skillGrpSCConfigArray,
CECShortcutSet[] aSCSets,
int count)
{
for (int i = 0; i < count; i++)
{
if (aSCSets[i] == null) continue;
for (int j = 0; j < aSCSets[i].GetShortcutNum(); j++)
{
CECShortcut pSC = aSCSets[i].GetShortcut(j);
if (pSC == null || pSC.GetType() != (int)ShortcutType.SCT_SKILLGRP)
continue;
// C-style cast -> safe cast
if (pSC is not CECSCSkillGrp pSkillGrpSC)
continue;
SkillGrpShortCutConfig skillGrpSCConfig = new SkillGrpShortCutConfig();
skillGrpSCConfig.setNum = i;
skillGrpSCConfig.slotNum = j;
skillGrpSCConfig.groupIndex = pSkillGrpSC.GetGroupIndex();
skillGrpSCConfigArray.Add(skillGrpSCConfig);
}
}
}
public void SaveSkillShortcut(
List skillSCConfigArray,
CECShortcutSet[] aSCSets,
int count)
{
for (int i = 0; i < count; i++)
{
if (aSCSets[i] == null) continue;
for (int j = 0; j < aSCSets[i].GetShortcutNum(); j++)
{
CECShortcut pSC = aSCSets[i].GetShortcut(j);
if (pSC == null || pSC.GetType() != (int)ShortcutType.SCT_SKILL)
continue;
if (pSC is not CECSCSkill pSkillSC)
continue;
int iOldSkillId = pSkillSC.GetSkill()?.GetSkillID() ?? 0;
if (iOldSkillId == 0)
continue;
SkillShortCutConfig skillSCConfig = new SkillShortCutConfig();
skillSCConfig.setNum = i;
skillSCConfig.slotNum = j;
skillSCConfig.skillId = iOldSkillId;
skillSCConfigArray.Add(skillSCConfig);
}
}
}
///
/// Cycles through learned skills by removing all shortcuts and adding 8 new skills to slots 0-7.
/// If m_startingSkillID is set (>0), uses that specific skill ID and the next 7 (ID+1 through ID+7).
/// Otherwise, cycles through all learned skills in groups of 8.
/// Only work with first shortcut set, and not work with combo
///
#if UNITY_EDITOR
public void CycleSkillShortcuts()
{
// Get shortcut set 1
CECShortcutSet pSCS = GetShortcutSet1(0);
if (pSCS == null)
{
Debug.LogWarning("CycleSkillShortcuts: Shortcut Set 1 is null");
return;
}
// Remove all shortcuts
pSCS.RemoveAllShortcuts();
// If starting skill ID is configured, cycle through pairs starting from that ID
if (m_startingSkillID > 0)
{
// Calculate the current skill IDs based on cycle index
// First press: startingID + 0, startingID + 1 (e.g., 5, 6)
// Second press: startingID + 2, startingID + 3 (e.g., 7, 8)
// Third press: startingID + 4, startingID + 5 (e.g., 9, 10)
int firstIndex = m_startingSkillID + (m_currentSkillCycleIndex * 8);
List skillIDs = new List();
for (int i = 0; i < 8; i++)
{
skillIDs.Add(firstIndex + i);
}
string skillIDsString = string.Join(", ", skillIDs);
BMLogger.LogError($"CycleSkillShortcuts: Trying to add skills {skillIDsString}");
for (int i = 0; i < skillIDs.Count; i++)
{
int skillID = skillIDs[i];
CECSkill pSkill = GetPositiveSkillByID(skillID);
if (pSkill != null)
{
pSCS.CreateSkillShortcut(i, pSkill);
}
else
{
Debug.LogError($"CycleSkillShortcuts: Skill with ID {skillID} not found in learned skills");
break;
}
}
// Increment cycle index for next press
++m_currentSkillCycleIndex;
// Update UI
CDlgQuickBar cDlgQuickBar2 = CECUIManager.Instance?.GetCDlgQuickBar();
cDlgQuickBar2?.UpdateShortcuts();
return;
}
// Original cycling behavior
// Get the list of learned skills
int skillCount = GetPositiveSkillNum();
// If no skills learned, just clear shortcuts (already done above)
if (skillCount == 0)
{
// Update UI
CDlgQuickBar cDlgQuickBar1 = CECUIManager.Instance?.GetCDlgQuickBar();
cDlgQuickBar1?.UpdateShortcuts();
return;
}
// Calculate how many groups of 8 we can make
int maxGroups = (skillCount + 7) / 8; // Round up division
// Wrap around if we've reached the end
if (m_currentSkillCycleIndex >= maxGroups)
{
m_currentSkillCycleIndex = 0;
}
// Calculate starting skill index for this cycle
int startSkillIndex = m_currentSkillCycleIndex * 8;
// Fill all 8 shortcut slots
for (int slotIndex = 0; slotIndex < 8; slotIndex++)
{
int skillIndex = startSkillIndex + slotIndex;
if (skillIndex < skillCount)
{
CECSkill pSkill = GetPositiveSkillByIndex(skillIndex);
if (pSkill != null)
{
pSCS.CreateSkillShortcut(slotIndex, pSkill);
}
}
}
// Increment cycle index for next time
m_currentSkillCycleIndex++;
// Update UI
CDlgQuickBar cDlgQuickBar = CECUIManager.Instance?.GetCDlgQuickBar();
cDlgQuickBar?.UpdateShortcuts();
}
#endif
}
}