2010 lines
89 KiB
C#
2010 lines
89 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Anti-spam gate for <see cref="ApplySkillShortcut"/> (client-side input flood control).
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// Notes:
|
|
/// - Uses <see cref="Time.unscaledTime"/> so throttling remains consistent under timeScale changes.
|
|
/// - This is client-side hygiene only; server cooldown/validation is still authoritative.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// ApplySkillShortcut 的反刷 (客户端输入洪泛控制)
|
|
///
|
|
/// 目的:
|
|
/// - 防止鼠标/触摸狂点、按键连发、UI 重复触发导致的频繁施法请求,避免客户端不稳定。
|
|
///
|
|
/// 设计:
|
|
/// - ANY:对所有技能共用的极短间隔,过滤超高频 burst。
|
|
/// - PER_SKILL:对同一技能的短间隔,过滤同技能连点。
|
|
/// - 例外:充能技能“再次按下立即释放”不会被拦截,保证充能手感。
|
|
///
|
|
/// 备注:
|
|
/// - 使用 Time.unscaledTime,避免 timeScale 变化导致反刷失效。
|
|
/// - 仅客户端防抖/限流,服务器校验与冷却依然为准。
|
|
/// </remarks>
|
|
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<int, float> _applySkillShortcut_lastSkillTime = new Dictionary<int, float>(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<uint>((byte[])Msg.dwParam1);
|
|
int offset = sizeof(uint);
|
|
int skillSize = Marshal.SizeOf<cmd_skill_data.SKILL>();
|
|
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<cmd_skill_data.SKILL>((byte[])Msg.dwParam1, offset);
|
|
offset += skillSize;
|
|
}
|
|
|
|
if (pCmd.skill_list == null)
|
|
{
|
|
BMLogger.LogError("OnMsgHstSkillData: cmd is null");
|
|
return;
|
|
}
|
|
|
|
List<SkillShortCutConfig> skillSCConfigArray1 = new List<SkillShortCutConfig>();
|
|
List<SkillShortCutConfig> skillSCConfigArray2 = new List<SkillShortCutConfig>();
|
|
List<SkillGrpShortCutConfig> skillGrpSCConfigArray1 = new List<SkillGrpShortCutConfig>();
|
|
List<SkillGrpShortCutConfig> skillGrpSCConfigArray2 = new List<SkillGrpShortCutConfig>();
|
|
|
|
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<cmd_learn_skill>((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<cmd_combo_skill_prepare>((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<uint, int> 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<ushort>(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<item_t>(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<cmd_set_cooldown>((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<unsigned int, CECSkill*>&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<SkillGrpShortCutConfig> 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<SkillGrpShortCutConfig> 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<SkillShortCutConfig> 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<SkillShortCutConfig> 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<SkillGrpShortCutConfig> 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<SkillShortCutConfig> 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
#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<int> skillIDs = new List<int>();
|
|
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
|
|
}
|
|
|
|
}
|