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(); } } /// /// DEBUG ONLY — bypasses the server skill message by loading skills from SkillStub.map /// (populated at startup from config) that belong to the player's current profession /// (or are general skills, cls == 255). Safe to call before server data arrives. /// /// 仅调试用 — 从 SkillStub.map 注入属于当前职业(或通用职业 cls==255)的技能, /// 绕过服务端消息,可在服务端数据到达前调用。 /// public void InjectDebugSkillsFromConfig(int level = 1) { m_aPtSkills.Clear(); m_aPsSkills.Clear(); var stubMap = SkillStub.GetMap(); if (stubMap.Count == 0) { BMLogger.LogWarning("InjectDebugSkillsFromConfig: SkillStub.map is empty — config not loaded yet."); return; } int playerCls = m_iProfession; // current role's profession ID int injected = 0; foreach (var kvp in stubMap) { int stubCls = kvp.Value.cls; // Keep only skills for this profession or universal skills (cls 255). // Same rule used by EC_HostSkillModel when listing learnable skills. // 仅保留当前职业技能或通用技能(cls==255),与 EC_HostSkillModel 的过滤规则相同。 if (stubCls != playerCls && stubCls != 255) continue; uint skillId = kvp.Key; CECSkill skill = new CECSkill((int)skillId, level); if (skill.SkillCore == null) continue; int type = skill.GetType(); if (type != (int)CECSkill.SkillType.TYPE_PASSIVE && type != (int)CECSkill.SkillType.TYPE_PRODUCE && type != (int)CECSkill.SkillType.TYPE_LIVE) m_aPtSkills.Add(skill); else m_aPsSkills.Add(skill); injected++; } BMLogger.Log($"InjectDebugSkillsFromConfig: profession={playerCls}, injected {injected} skills " + $"({m_aPtSkills.Count} active, {m_aPsSkills.Count} passive) at level {level}."); } /// /// Animation test / offline: populate skill lists from catalog /// (same source and filters as CDlgSkillSubList). /// Call before this so the catalog matches the current profession. /// /// 动画测试 / 离线:从 CECHostSkillModel 目录填充技能列表(与 CDlgSkillSubList 相同来源与过滤)。 /// public void InjectSkillsFromSkillModel(int level = 1, bool isEvil = false) { m_aPtSkills.Clear(); m_aPsSkills.Clear(); List skillIds = CECHostSkillModel.Instance?.CollectSkillSubListSkillIds(isEvil); if (skillIds == null || skillIds.Count == 0) { BMLogger.LogWarning( "InjectSkillsFromSkillModel: CECHostSkillModel catalog is empty — call CECHostSkillModel.Initialize() first."); return; } int injected = 0; foreach (int skillId in skillIds) { CECSkill skill = new CECSkill(skillId, level); int type = (int)ElementSkill.GetType((uint)skillId); if (skill.SkillCore != null) type = skill.GetType(); if (type != (int)CECSkill.SkillType.TYPE_PASSIVE && type != (int)CECSkill.SkillType.TYPE_PRODUCE && type != (int)CECSkill.SkillType.TYPE_LIVE) m_aPtSkills.Add(skill); else m_aPsSkills.Add(skill); injected++; } BMLogger.Log($"InjectSkillsFromSkillModel: profession={m_iProfession}, isEvil={isEvil}, " + $"catalog={skillIds.Count}, injected={injected} " + $"({m_aPtSkills.Count} active, {m_aPsSkills.Count} passive) at level {level}."); } /// /// Build instances for every ID returned by /// — includes passive skills shown in the skill tree UI. /// 构建与 CDlgSkillSubList 完全一致的技能列表(含被动)。 /// public List BuildSkillSubListSkills(int level = 1, bool isEvil = false) { var skills = new List(); List skillIds = CECHostSkillModel.Instance?.CollectSkillSubListSkillIds(isEvil); if (skillIds == null) return skills; foreach (int skillId in skillIds) skills.Add(new CECSkill(skillId, level)); return skills; } 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) { } 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 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 } }