diff --git a/Assets/PerfectWorld/Scene/Bootstrap.unity b/Assets/PerfectWorld/Scene/Bootstrap.unity index 35a895d61b..ca5fb9ec95 100644 --- a/Assets/PerfectWorld/Scene/Bootstrap.unity +++ b/Assets/PerfectWorld/Scene/Bootstrap.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57bb52418b44eb7e3e333915973057be091b0826983b641ceb05263b4dffe2e4 -size 287382 +oid sha256:20cec4d7a902084d6e87ff1de8301f79123189bb3c8f06210396aab2d1b1d7ac +size 291045 diff --git a/Assets/PerfectWorld/Scripts/DebugCommandMenu/DlgConsole.cs b/Assets/PerfectWorld/Scripts/DebugCommandMenu/DlgConsole.cs index 5482a4524b..d2db270052 100644 --- a/Assets/PerfectWorld/Scripts/DebugCommandMenu/DlgConsole.cs +++ b/Assets/PerfectWorld/Scripts/DebugCommandMenu/DlgConsole.cs @@ -72,10 +72,11 @@ namespace BrewMonster.Scripts { _panelConsole.SetActive(false); } - private void OnBtnNoCooldownClicked() + public void OnBtnNoCooldownClicked() { UnityGameSession.c2s_CmdDebug(8903, 73125); } + /// 8903 73125 private void OnBtnAddWrathClicked() { UnityGameSession.c2s_CmdDebug(1992); @@ -89,10 +90,6 @@ namespace BrewMonster.Scripts ToggleAutoAddWrath(); } - /// - /// Toggle auto-add wrath feature (calls OnBtnAddWrathClicked every 3 seconds) - /// 切换自动添加怒气功能(每3秒调用OnBtnAddWrathClicked) - /// public void ToggleAutoAddWrath() { _isAutoAddWrathEnabled = !_isAutoAddWrathEnabled; @@ -116,10 +113,6 @@ namespace BrewMonster.Scripts } } - /// - /// Coroutine that calls OnBtnAddWrathClicked every 3 seconds - /// 每3秒调用OnBtnAddWrathClicked的协程 - /// private IEnumerator AutoAddWrathCoroutine() { while (_isAutoAddWrathEnabled) diff --git a/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs b/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs index cc007aae1c..93f8d6a1ac 100644 --- a/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs @@ -372,10 +372,21 @@ namespace BrewMonster if (m_enumState == GfxSkillEventState.enumFinished) return; // 结束 / Finished else if (m_enumState == GfxSkillEventState.enumHit) // 命中 / Hit { - // In Unity, hit GFX is auto-destroyed via Destroy(obj, 3f) in CECSkillGfxEvent. - // Transition to Finished immediately — the hit GFX cleanup is handled by Unity's timer. - // 在Unity中,命中特效通过Destroy(obj, 3f)自动销毁。立即转为Finished状态。 - m_enumState = GfxSkillEventState.enumFinished; + // If m_bTraceTarget is true, stay in Hit state to allow derived class to update hit GFX position each frame + // This matches C++ logic: hit GFX follows target when m_bTraceTarget is true + // 如果m_bTraceTarget为true,保持在Hit状态以允许派生类每帧更新命中特效位置 + // 这与C++逻辑匹配:当m_bTraceTarget为true时,命中特效跟随目标 + if (!m_bTraceTarget) + { + // In Unity, hit GFX is auto-destroyed via Destroy(obj, 3f) in CECSkillGfxEvent. + // Transition to Finished immediately — the hit GFX cleanup is handled by Unity's timer. + // 在Unity中,命中特效通过Destroy(obj, 3f)自动销毁。立即转为Finished状态。 + m_enumState = GfxSkillEventState.enumFinished; + } + // If m_bTraceTarget is true, derived class (CECSkillGfxEvent) will handle position updates + // and transition to Finished when hit GFX lifetime expires + // 如果m_bTraceTarget为true,派生类(CECSkillGfxEvent)将处理位置更新 + // 并在命中特效生命周期到期时转为Finished状态 } else if (m_enumState == GfxSkillEventState.enumWait) { diff --git a/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs b/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs index 5ad79a4ebf..f3ae06a979 100644 --- a/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs @@ -1520,3 +1520,8 @@ public enum GfxSkillValType + + + + + diff --git a/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs b/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs index fc7a3faf37..23600d6d88 100644 --- a/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs @@ -101,7 +101,7 @@ namespace BrewMonster pComposer.m_HitPos.vOffset, pComposer.m_HitPos.szHanger, pComposer.m_HitPos.bChildHook); - + if (!success) { // Return last known position or zero if target is destroyed @@ -123,7 +123,7 @@ namespace BrewMonster Vector3.zero, null, false); - + if (!success) { // Return last known position or zero if target is destroyed @@ -151,7 +151,7 @@ namespace BrewMonster { // Track state before base.Tick() to detect transitions / 在base.Tick()前记录状态以检测转换 GfxSkillEventState prevState = m_enumState; - + // Update host and target positions / 更新施法者和目标位置 if (GetComposer() != null) { @@ -234,17 +234,17 @@ namespace BrewMonster // Spawn fly GFX when entering Flying state / 进入飞行状态时生成飞行特效 if (prevState == GfxSkillEventState.enumWait && m_enumState == GfxSkillEventState.enumFlying) { - + #if UNITY_EDITOR Vector3 currentPos = m_pMoveMethod.GetPos(); BMLogger.LogError($"[SKILL_GFX_DEBUG] Event.Tick: Registering gizmo - hostPos={m_vHostPos}, targetPos={m_vTargetPos}, currentPos={currentPos}, hostExist={m_bHostExist}, targetExist={m_bTargetExist}"); - + if (m_vHostPos.sqrMagnitude > 0.01f && m_vTargetPos.sqrMagnitude > 0.01f) { SkillGfxGizmoDrawer.RegisterProjectile(m_nHostID, m_nTargetID, m_vHostPos, m_vTargetPos, m_pMoveMethod.GetMode()); } #endif - + SpawnFlyGfx(); } @@ -252,7 +252,7 @@ namespace BrewMonster if (m_enumState == GfxSkillEventState.enumFlying) { UpdateFlyGfxTransform(); - + // Update gizmo position / 更新辅助线位置 #if UNITY_EDITOR Vector3 currentPos = m_pMoveMethod.GetPos(); @@ -264,7 +264,22 @@ namespace BrewMonster } #endif } - + + // Update hit GFX position when m_bTraceTarget is true (follow target) / 当m_bTraceTarget为true时更新命中特效位置(跟随目标) + // This matches C++ logic: hit GFX follows target position each frame when m_bTraceTarget is true + // 这与C++逻辑匹配:当m_bTraceTarget为true时,命中特效每帧跟随目标位置 + if (m_enumState == GfxSkillEventState.enumHit && m_bTraceTarget) + { + UpdateHitGfxTransform(); + + // Check if hit GFX has been destroyed (3 second lifetime) or target is destroyed + // 检查命中特效是否已被销毁(3秒生命周期)或目标是否已销毁 + if (m_hitGfxInstance == null || (!m_bTargetExist && m_nTargetID != 0)) + { + m_enumState = GfxSkillEventState.enumFinished; + } + } + // Remove gizmo when hit or finished / 命中或完成时移除辅助线 if (m_enumState == GfxSkillEventState.enumHit || m_enumState == GfxSkillEventState.enumFinished) { @@ -297,13 +312,13 @@ namespace BrewMonster /// private void SpawnFlyGfx() { - + if (m_pComposer == null) { BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnFlyGfx: m_pComposer is NULL - cannot spawn fly GFX!"); return; } - + GameObject prefab = m_pComposer.GetFlyGFX(); BMLogger.LogError("FlyGfx : " + m_pComposer.flyGfxName); if (prefab == null) @@ -313,11 +328,11 @@ namespace BrewMonster } Vector3 pos = m_pMoveMethod.GetPos(); - /* Vector3 dir = m_pMoveMethod.GetMoveDir(); - Quaternion rot = dir.sqrMagnitude > 1e-4f ? Quaternion.LookRotation(dir) : Quaternion.identity;*/ + /* Vector3 dir = m_pMoveMethod.GetMoveDir(); + Quaternion rot = dir.sqrMagnitude > 1e-4f ? Quaternion.LookRotation(dir) : Quaternion.identity;*/ m_flyGfxInstance = GameObject.Instantiate(prefab, pos, prefab.transform.rotation); - + } /// @@ -333,6 +348,27 @@ namespace BrewMonster m_flyGfxInstance.transform.rotation = Quaternion.LookRotation(dir); } + /// + /// Update hit GFX transform to follow target when m_bTraceTarget is true + /// 当m_bTraceTarget为true时更新命中特效变换以跟随目标 + /// This matches C++ logic: hit GFX position is updated each frame to follow target + /// 这与C++逻辑匹配:命中特效位置每帧更新以跟随目标 + /// + private void UpdateHitGfxTransform() + { + if (m_hitGfxInstance == null) return; + + // Update target position first / 首先更新目标位置 + if (m_bTargetExist && m_nTargetID != 0) + { + // Get current target position using GetTargetCenter() / 使用GetTargetCenter()获取当前目标位置 + Vector3 targetPos = GetTargetCenter(); + m_hitGfxInstance.transform.position = targetPos; + } + // If target doesn't exist, hit GFX stays at last known position + // 如果目标不存在,命中特效保持在最后已知位置 + } + /// /// Destroy fly GFX instance /// 销毁飞行特效实例 @@ -352,13 +388,13 @@ namespace BrewMonster /// private void SpawnHitGfx(Vector3 vTarget) { - + if (m_pComposer == null) { BMLogger.LogError($"[SKILL_GFX_DEBUG] SpawnHitGfx: m_pComposer is NULL - cannot spawn hit GFX!"); return; } - + // Check if target exists - if not, use ground hit GFX instead of regular hit GFX // 检查目标是否存在 - 如果不存在,使用地面命中特效而不是常规命中特效 // This matches C++ logic: m_szHitGrndGfx is used when projectile hits ground (no target) @@ -375,7 +411,7 @@ namespace BrewMonster { prefab = m_pComposer.GetHitGFX(); } - + if (prefab == null) { BMLogger.LogWarning($"[SKILL_GFX_DEBUG] SpawnHitGfx: {(bTargetExists ? "Hit" : "Ground Hit")} GFX prefab is NULL - cannot spawn!"); @@ -385,18 +421,19 @@ namespace BrewMonster BMLogger.LogError($"{(bTargetExists ? "HitGfx" : "HitGrndGfx")} : {(bTargetExists ? m_pComposer.hitGfxName : m_pComposer.hitGrdGfxName)}"); - /* Quaternion rot = Quaternion.identity; - if (m_bHostExist) - { - Vector3 dir = vTarget - m_vHostPos; - dir.y = 0; - if (dir.sqrMagnitude > 1e-6f) rot = Quaternion.LookRotation(dir); - }*/ + /* Quaternion rot = Quaternion.identity; + if (m_bHostExist) + { + Vector3 dir = vTarget - m_vHostPos; + dir.y = 0; + if (dir.sqrMagnitude > 1e-6f) rot = Quaternion.LookRotation(dir); + }*/ m_hitGfxInstance = GameObject.Instantiate(prefab, vTarget, prefab.transform.rotation); - - - GameObject.Destroy(m_hitGfxInstance, 3.0f); // auto-cleanup / 自动清理 + + // Destroy hit GFX after 3 seconds (unless m_bTraceTarget is true, then it follows target until destroyed) + // 3秒后销毁命中特效(除非m_bTraceTarget为true,否则它会跟随目标直到被销毁) + GameObject.Destroy(m_hitGfxInstance, 3.0f); } /// @@ -472,12 +509,12 @@ namespace BrewMonster // 注意:GetGoblin()方法可能需要在CECPlayer中实现 // For now, we'll try to get it via a common pattern // 目前,我们将尝试通过通用模式获取它 - + // Try to find goblin model - this is a placeholder until GetGoblin() is implemented // 尝试查找小精灵模型 - 这是占位符,直到实现GetGoblin() // TODO: Implement GetGoblin() in CECPlayer when goblin system is available // TODO: 当小精灵系统可用时,在CECPlayer中实现GetGoblin() - + // For Phase 3, we'll search for a child model named "goblin" or similar // 对于第3阶段,我们将搜索名为"goblin"或类似的子模型 CECModel pModel = pPlayer.GetPlayerModel(); @@ -485,10 +522,10 @@ namespace BrewMonster { // Try common goblin hanger names // 尝试常见的小精灵挂载者名称 - CECModel goblinModel = pModel.GetChildModel("goblin") ?? - pModel.GetChildModel("pet") ?? + CECModel goblinModel = pModel.GetChildModel("goblin") ?? + pModel.GetChildModel("pet") ?? pModel.GetChildModel("_goblin"); - + if (goblinModel != null) { // Use hook if specified, otherwise use model center @@ -500,21 +537,21 @@ namespace BrewMonster { Transform modelTransform = goblinModel.transform; vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform); - + #if UNITY_EDITOR BMLogger.Log($"[HOOK_DEBUG] Found goblin hook '{szHook}' for player ID {nID}, position={vPos}"); #endif return true; } } - + // Fallback to goblin position // 回退到小精灵位置 if (goblinModel.transform != null) { vPos = goblinModel.transform.position; vPos.y += 0.5f; - + #if UNITY_EDITOR BMLogger.Log($"[HOOK_DEBUG] Using goblin center position for player ID {nID}, position={vPos}"); #endif @@ -522,7 +559,7 @@ namespace BrewMonster } } } - + #if UNITY_EDITOR BMLogger.LogWarning($"[HOOK_DEBUG] Goblin model not found for player ID {nID}"); #endif diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_HPWork.cs b/Assets/PerfectWorld/Scripts/Managers/EC_HPWork.cs index 303e396260..8d21f206bd 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_HPWork.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_HPWork.cs @@ -519,6 +519,14 @@ namespace BrewMonster.Scripts // LOG_DEBUG_INFO(AString().Format("CECHPWork:: start delayed work %s, priority=%d", pWork->GetWorkName(), m_Delayed.iPriority)); InternallyStartWork(m_Delayed.iPriority, pWork); } + public bool IsFlashMoving() + { + return IsWorkRunning(CECHPWork.Host_work_ID.WORK_FLASHMOVE); + } + public bool IsFlyingOff() + { + return IsWorkRunning(CECHPWork.Host_work_ID.WORK_FLYOFF); + } public bool HasDelayedWork() { return GetDelayedWork() != null; @@ -888,7 +896,7 @@ namespace BrewMonster.Scripts public bool IsOperatingPet() { - return IsWorkRunning(Host_work_ID.WORK_CONCENTRATE); + return IsWorkRunning(Host_work_ID.WORK_CONCENTRATE); } } public abstract class CECHPWorkPostTickCommand diff --git a/Assets/PerfectWorld/Scripts/Move/CECHostMove.cs b/Assets/PerfectWorld/Scripts/Move/CECHostMove.cs index e52f9f7076..44393d2d3e 100644 --- a/Assets/PerfectWorld/Scripts/Move/CECHostMove.cs +++ b/Assets/PerfectWorld/Scripts/Move/CECHostMove.cs @@ -390,7 +390,7 @@ namespace BrewMonster cdr.fGravityAccel = 9.8f; // EC_GRAVITY EC_CDR.OnGroundMove(ref cdr); - BMLogger.LogError($"HoangDev: FlashMove seg={i} stepTime={cdr.t} center=({cdr.vCenter})"); + //BMLogger.LogError($"HoangDev: FlashMove seg={i} stepTime={cdr.t} center=({cdr.vCenter})"); if (CECWorld.Instance.GetAssureMove() != null) CECWorld.Instance.GetAssureMove().NoAssureMove(); @@ -416,6 +416,8 @@ namespace BrewMonster m_vFlashTPNormal = cdr.vTPNormal; + A3DVECTOR3 vCurrentPlayerPos = m_pHost.GetPos(); + return vCurPos; } // Get host's last position sent to server diff --git a/Assets/PerfectWorld/Scripts/Move/CECPlayer.cs b/Assets/PerfectWorld/Scripts/Move/CECPlayer.cs index 334e129903..ae42419baf 100644 --- a/Assets/PerfectWorld/Scripts/Move/CECPlayer.cs +++ b/Assets/PerfectWorld/Scripts/Move/CECPlayer.cs @@ -71,6 +71,7 @@ namespace BrewMonster protected CECSkill m_pCurSkill; protected int m_idFaction; // ID of player's faction protected int m_idForce; // id of the player's force + protected BATTLEINFO m_BattleInfo; // Battle information / 战斗信息 protected int NUM_WEAPON_TYPE = 15; public static readonly int[] m_sciStateIDForStateAction = { 117 }; @@ -83,17 +84,17 @@ namespace BrewMonster protected int m_idCountry = 0; // ¹úÕ½ÕóÓª id public static int MAX_REINCARNATION = 2; protected List m_aCurEffects = new List(); // Current effects - byte m_ReincarnationCount = 0; - string m_strName; // Player name + protected byte m_ReincarnationCount = 0; + protected string m_strName; // Player name // 需要是可能 || Need is possible protected bool m_bHangerOn = false; protected int m_iCurAction; - bool m_bAboutToDie = false; + protected bool m_bAboutToDie = false; public bool m_bCandHangerOn = false; - public bool m_bPetInSanctuary = false; // true, the pet pet of the player is in sanctuary - //The ID of the currently summoned pet - int m_idCurPet = 0; - byte m_byPariahLvl = 0; // Pariah level + public bool m_bPetInSanctuary = false; // true, the pet pet of the player is in sanctuary + //The ID of the currently summoned pet + protected int m_idCurPet = 0; + protected byte m_byPariahLvl = 0; // Pariah level public RIDINGPET m_RidingPet; // Riding pet information public GameObject m_pPetModel = null; // Pet model @@ -102,8 +103,8 @@ namespace BrewMonster // ÒÀ¸½ÀàÐÍ AttachMode m_AttachMode = AttachMode.enumAttachNone; // ÒÀ¸½Õß»ò±»ÒÀ¸½Õßid - int m_iBuddyId; - int m_idCandBuddy; // ID of candidate buddy + protected int m_iBuddyId; + protected int m_idCandBuddy; // ID of candidate buddy EC_ManPlayer m_pPlayerMan => EC_ManMessageMono.Instance?.GetECManPlayer; // Player manager protected Transform playerTransform => _objectTransform ??= transform; @@ -2324,6 +2325,178 @@ namespace BrewMonster public int iDuelTimeCnt; // Duel time counter public int iDuelRlt; // Duel result. 0, no defined; 1-win; 2-lose; 3-draw }; + + // Battle type / 战斗类型 + public enum BattleType + { + BT_NONE = 0, // No battle / 无战斗 + BT_GUILD = 1, // Guild war / 帮派战 + BT_COUNTRY = 2, // Country war / 国战 + BT_CHARIOT = 3, // Chariot war / 战车战 + }; + + // Score rank entry for country battle live show / 国战直播显示分数排行条目 + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ScoreRankEntry + { + public int idPlayer; // Player ID / 玩家ID + public int iScore; // Score / 分数 + public int iKillCount; // Kill count / 击杀数 + public int iDeathCount; // Death count / 死亡数 + }; + + // Death entry for country battle live show / 国战直播显示死亡条目 + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct DeathEntry + { + public int idKiller; // Killer ID / 击杀者ID + public int idVictim; // Victim ID / 受害者ID + public int iTime; // Death time / 死亡时间 + }; + + // Battle information / 战斗信息 + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct BATTLEINFO + { + public int nType; // Battle type / 战斗类型 (BattleType) + public int idBattle; // Battle id / 战斗ID + public int iResult; // Battle result / 战斗结果 (0 = no result, 1 = win, 2 = lose, 3 = draw) + public int iResultCnt; // Result time counter / 结果时间计数器 + public int iMaxScore_I; // Maximum score of invader / 攻击方最大分数 + public int iMaxScore_D; // Maximum score of defender / 防守方最大分数 + public int iScore_I; // Score of invader / 攻击方分数 + public int iScore_D; // Score of defender / 防守方分数 + public int iEndTime; // Battle end time / 战斗结束时间 + + // 国战专用数据 / Country war specific data + public int iOffenseCountry; // Offense country / 攻击方国家 + public int iDefenceCountry; // Defence country / 防守方国家 + public int iReviveTimes; // 剩余复活次数 / Remaining revive times + [MarshalAs(UnmanagedType.U1)] + public bool bFlagCarrier; // 是否是旗手 / Is flag carrier + public int iCarrierID; // 扛旗者ID(bFlagCarrier为false时有效)/ Carrier ID (valid when bFlagCarrier is false) + public A3DVECTOR3 posCarrier; // 扛旗者位置(bFlagCarrier为false时有效)/ Carrier position (valid when bFlagCarrier is false) + [MarshalAs(UnmanagedType.U1)] + public bool bCarrierInvader; // 扛旗者是攻击方(bFlagCarrier为false时有效)/ Carrier is invader (valid when bFlagCarrier is false) + + public int iCombatTime; // 战斗时间(秒)/ Combat time (seconds) + public int iAttendTime; // 参加战场时间(秒)/ Attend time (seconds) + public int iKillCount; // 击杀次数 / Kill count + public int iDeathCount; // 死亡次数 / Death count + public int iCountryKillCount; // 同国家击杀次数 / Same country kill count + public int iCountryDeathCount;// 同国家死亡次数 / Same country death count + public int iAttackerCount; // 攻击方人数 / Attacker count + public int iDefenderCount; // 防守方人数 / Defender count + + public int iStrongHoldCount; // 据点个数 / Stronghold count + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public int[] iStrongHoldState; // 据点占领状态 8 == ARRAY_SIZE(COUNTRY_CONFIG::stronghold) / Stronghold occupation state + + // Score rank containers (using List instead of std::vector) / 分数排行容器(使用List代替std::vector) + public List OffenseRanks; // Offense ranks / 攻击方排行 + public List DefenceRanks; // Defence ranks / 防守方排行 + + // Death containers (using List instead of std::vector) / 死亡容器(使用List代替std::vector) + public List OffenseDeaths; // Offense deaths / 攻击方死亡 + public List DefenceDeaths; // Defence deaths / 防守方死亡 + + // 战车 / Chariot + public int iChariot; // 战车id / Chariot id + public int iEnergy; // 能量 / Energy + public int iScoreSelf; // 自己成绩 / Self score + public int iMultiKill; // 连杀 / Multi kill + + // Initialize arrays and lists / 初始化数组和列表 + public void Initialize() + { + if (iStrongHoldState == null) + iStrongHoldState = new int[8]; + if (OffenseRanks == null) + OffenseRanks = new List(); + if (DefenceRanks == null) + DefenceRanks = new List(); + if (OffenseDeaths == null) + OffenseDeaths = new List(); + if (DefenceDeaths == null) + DefenceDeaths = new List(); + } + + // Set country battle live show info / 设置国战直播显示信息 + // TODO: Implement when cmd_countrybattle_live_show_result is available + // 当cmd_countrybattle_live_show_result可用时实现 + public void SetCountryBattleLiveShowInfo(/*const S2C::cmd_countrybattle_live_show_result& cmd*/) + { + // TODO: Implement SetCountryBattleLiveShowInfo + // 实现SetCountryBattleLiveShowInfo + } + + // Check if in guild war / 检查是否在帮派战中 + public bool IsGuildWar() + { + return nType == (int)BattleType.BT_GUILD; + } + + // Check if in country war / 检查是否在国战中 + public bool IsCountryWar() + { + return nType == (int)BattleType.BT_COUNTRY; + } + + // Check if is flag carrier / 检查是否是旗手 + public bool IsFlagCarrier() + { + return IsCountryWar() && bFlagCarrier; + } + + // Check if in chariot war / 检查是否在战车战中 + public bool IsChariotWar() + { + return nType == (int)BattleType.BT_CHARIOT; + } + + // Reset battle info / 重置战斗信息 + public void Reset() + { + nType = (int)BattleType.BT_NONE; + idBattle = 0; + iResult = 0; + iResultCnt = 0; + iMaxScore_I = 0; + iMaxScore_D = 0; + iScore_I = 0; + iScore_D = 0; + iEndTime = 0; + iOffenseCountry = 0; + iDefenceCountry = 0; + iReviveTimes = 0; + bFlagCarrier = false; + iCarrierID = 0; + posCarrier = new A3DVECTOR3(0, 0, 0); + bCarrierInvader = false; + iCombatTime = 0; + iAttendTime = 0; + iKillCount = 0; + iDeathCount = 0; + iCountryKillCount = 0; + iCountryDeathCount = 0; + iAttackerCount = 0; + iDefenderCount = 0; + iStrongHoldCount = 0; + if (iStrongHoldState != null) + { + for (int i = 0; i < iStrongHoldState.Length; i++) + iStrongHoldState[i] = 0; + } + OffenseRanks?.Clear(); + DefenceRanks?.Clear(); + OffenseDeaths?.Clear(); + DefenceDeaths?.Clear(); + iChariot = 0; + iEnergy = 0; + iScoreSelf = 0; + iMultiKill = 0; + } + }; public enum PlayerResourcesReadyFlag { diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/C2SCommand/C2SCommandFactory.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/C2SCommand/C2SCommandFactory.cs index 6082666510..3f7b3eef44 100644 --- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/C2SCommand/C2SCommandFactory.cs +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/C2SCommand/C2SCommandFactory.cs @@ -103,6 +103,8 @@ namespace CSNetwork.C2SCommand if (fieldType == typeof(Vector3)) { var vec = (Vector3)fieldValue; + // Log Vector3 serialization for flash move debugging + // 记录Vector3序列化以用于瞬移调试 WriteBasicValue(octets, vec.x); WriteBasicValue(octets, vec.y); WriteBasicValue(octets, vec.z); diff --git a/Assets/Scripts/CECHostPlayer.Skill.cs b/Assets/Scripts/CECHostPlayer.Skill.cs index 8f140fc76d..af4bbc168e 100644 --- a/Assets/Scripts/CECHostPlayer.Skill.cs +++ b/Assets/Scripts/CECHostPlayer.Skill.cs @@ -643,6 +643,10 @@ namespace BrewMonster if (!pSkill.ReadyToCast()) return false; + // Check if skill can be cast immediately (blocks casting during flash move at PRIORITY_2) + if (!m_pWorkMan.CanCastSkillImmediately(pSkill.GetSkillID())) + return false; + if (!pSkill.IsInstant() && pSkill.GetType() != (int)Skilltype.TYPE_FLASHMOVE) { if (!NaturallyStopMoving()) diff --git a/Assets/Scripts/CECHostPlayer.cs b/Assets/Scripts/CECHostPlayer.cs index 89ec7cd87b..07ca9e560b 100644 --- a/Assets/Scripts/CECHostPlayer.cs +++ b/Assets/Scripts/CECHostPlayer.cs @@ -23,6 +23,7 @@ using System.Runtime.InteropServices; using System.Text; using UnityEngine; using UnityEngine.UI; +using static BrewMonster.CECPlayer; using cmd_select_target = CSNetwork.GPDataType.cmd_select_target; using Host_work_ID = BrewMonster.Scripts.CECHPWork.Host_work_ID; using ObjectCoords = System.Collections.Generic.List; @@ -104,6 +105,8 @@ namespace BrewMonster private int MAX_JUMP_COUNT = 2; bool m_bUsingTrashBox = false; // Whether being using trash box private float m_fPrayDistancePlus; + private bool m_bInRebuildPet = false; // Whether rebuilding pet / 是否正在重建宠物 + private uint m_dwGMFlags = 0; // GM flags / GM标志 private A3DVECTOR3 g_vOrigin = new A3DVECTOR3(0f); private float EC_SLOPE_Y = 0.5f; @@ -1983,226 +1986,182 @@ namespace BrewMonster return pNPC && ( /*!IsSkeletonReady() ||*/ CanSafelySelectWith(pNPC.GetDistToHost())); } - // Check whether host can do a behavior - bool CanDo(int iThing) + // Check whether host can do a behavior / 检查宿主是否可以执行某个行为 + public bool CanDo(int iThing) { bool bRet = true; switch (iThing) { case ActionCanDo.CANDO_SITDOWN: - - if (IsDead() /*|| IsAboutToDie() */ || IsJumping() /*|| IsTrading() || IsUsingTrashBox()*/ || - IsRooting() || /*IsReviving() || IsTalkingWithNPC() || IsChangingFace() ||*/ - !m_GndInfo - .bOnGround /*|| GetBoothState() != 0 || m_iBuddyId || IsOperatingPet() || IsRebuildingPet() || - IsUsingItem() || IsRidingOnPet() || GetShapeType() == PLAYERMODEL_DUMMYTYPE2 || IsPassiveMove()*/ - ) + if (IsDead() || IsAboutToDie() || IsJumping() || IsTrading() || IsUsingTrashBox() || + IsRooting() || IsReviving() || IsTalkingWithNPC() || IsChangingFace() || + !m_GndInfo.bOnGround || GetBoothState() != 0 || m_iBuddyId != 0 || IsOperatingPet() != 0 || IsRebuildingPet() || + IsUsingItem() || IsRidingOnPet() || GetShapeType() == (int)PlayerModelType.PLAYERMODEL_DUMMYTYPE2 || IsPassiveMove()) bRet = false; - break; case ActionCanDo.CANDO_MOVETO: - { - if (IsDead() /*|| IsSitting() || IsTrading() || IsUsingTrashBox()*/ || IsRooting() /*|| - IsReviving() || IsTalkingWithNPC() || IsChangingFace() || IsUsingItem() || - GetBoothState() != 0 || m_bHangerOn || IsOperatingPet() || IsRebuildingPet() || IsPassiveMove()*/) - bRet = false; - - break; - } - case ActionCanDo.CANDO_MELEE: - - if (IsDead() /*|| IsSitting() */ || m_idSelTarget == 0 || m_idSelTarget == m_PlayerInfo.cid || - IsJumping() || GPDataTypeHelper.ISMATTERID(m_idSelTarget) /*|| IsTrading() || IsReviving() || - IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace()*/ || CannotAttack() /*|| - GetBoothState() != 0 || m_iBuddyId || IsRidingOnPet() || IsOperatingPet() || IsRebuildingPet() || - IsUsingItem() || IsPassiveMove()*/) + if (IsDead() || IsSitting() || IsTrading() || IsUsingTrashBox() || IsRooting() || + IsReviving() || IsTalkingWithNPC() || IsChangingFace() || IsUsingItem() || + GetBoothState() != 0 || m_bHangerOn || IsOperatingPet() != 0 || IsRebuildingPet() || IsPassiveMove()) bRet = false; + break; + case ActionCanDo.CANDO_MELEE: + if (IsDead() || IsSitting() || m_idSelTarget == 0 || m_idSelTarget == m_PlayerInfo.cid || + IsJumping() || GPDataTypeHelper.ISMATTERID(m_idSelTarget) || IsTrading() || IsReviving() || + IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace() || CannotAttack() || + GetBoothState() != 0 || m_iBuddyId != 0 || IsRidingOnPet() || IsOperatingPet() != 0 || IsRebuildingPet() || + IsUsingItem() || IsPassiveMove()) + bRet = false; break; case ActionCanDo.CANDO_ASSISTSEL: - - if (IsDead() || !GPDataTypeHelper.ISPLAYERID(m_idSelTarget) || - m_idSelTarget == m_PlayerInfo.cid /*|| - !m_pTeam || !m_pTeam.GetMemberByID(m_idSelTarget) || m_iBuddyId || IsPassiveMove()*/ || + if (IsDead() || !GPDataTypeHelper.ISPLAYERID(m_idSelTarget) || m_idSelTarget == m_PlayerInfo.cid || + m_pTeam == null || m_pTeam.GetMemberByID(m_idSelTarget) == null || m_iBuddyId != 0 || IsPassiveMove() || m_playerLimits[(int)PLAYER_LIMIT.PLAYER_LIMIT_NOCHANGESELECT]) bRet = false; - break; case ActionCanDo.CANDO_FLY: - if (IsDead() || IsRooting() || IsSitting() || IsTrading() || IsReviving() || - IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace() || GetBoothState() != 0 || - //IsFlashMoving() || - m_pWorkMan.HasWorkRunningOnPriority(CECHPWorkMan.Work_priority.PRIORITY_2) || - m_bHangerOn || /*IsOperatingPet() || IsRebuildingPet() ||*/ - IsUsingItem() || /*IsRidingOnPet() || GetShapeType() == PLAYERMODEL_DUMMYTYPE2 ||*/ IsPassiveMove() || - m_playerLimits[(int)PLAYER_LIMIT.PLAYER_LIMIT_NOFLY]/* || m_BattleInfo.IsChariotWar()*/) + IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace() || GetBoothState() != 0 || + IsFlashMoving() || (m_pWorkMan != null && m_pWorkMan.HasWorkRunningOnPriority(CECHPWorkMan.Work_priority.PRIORITY_2)) || + m_bHangerOn || IsOperatingPet() != 0 || IsRebuildingPet() || + IsUsingItem() || IsRidingOnPet() || GetShapeType() == (int)PlayerModelType.PLAYERMODEL_DUMMYTYPE2 || IsPassiveMove() || + m_playerLimits[(int)PLAYER_LIMIT.PLAYER_LIMIT_NOFLY] /*|| m_BattleInfo.IsChariotWar()*/) bRet = false; - break; case ActionCanDo.CANDO_PICKUP: case ActionCanDo.CANDO_GATHER: - - if (IsDead() /*|| IsAboutToDie() || IsSitting() || IsTrading() || IsUsingTrashBox() || - IsReviving() || IsTalkingWithNPC() || IsChangingFace() || GetBoothState() != 0 || - GetBuddyState() == 1 || IsOperatingPet() || IsRebuildingPet() || IsUsingItem() || IsPassiveMove()*/) + if (IsDead() || IsAboutToDie() || IsSitting() || IsTrading() || IsUsingTrashBox() || + IsReviving() || IsTalkingWithNPC() || IsChangingFace() || GetBoothState() != 0 || + GetBuddyState() == 1 || IsOperatingPet() != 0 || IsRebuildingPet() || IsUsingItem() || IsPassiveMove()) bRet = false; - break; case ActionCanDo.CANDO_TRADE: - - if (IsDead() || IsMeleeing() /*|| IsAboutToDie() || IsSitting() || IsJumping() || - IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace() || - IsSpellingMagic() || GetBoothState() != 0 || m_iBuddyId || IsOperatingPet() || IsRebuildingPet() || - IsUsingItem() || IsInvisible() || IsPassiveMove()*/) + if (IsDead() || IsAboutToDie() || IsSitting() || IsJumping() || IsMeleeing() || + IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace() || + IsSpellingMagic() || GetBoothState() != 0 || m_iBuddyId != 0 || IsOperatingPet() != 0 || IsRebuildingPet() || + IsUsingItem() || IsInvisible() || IsPassiveMove()) bRet = false; - break; case ActionCanDo.CANDO_PLAYPOSE: - - if (IsDead() || IsMeleeing() || /*|| IsAboutToDie() || IsSitting() || IsJumping() || /* || - IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace() || - IsSpellingMagic() || IsShapeChanged() || IsReviving() ||*/ - m_iMoveEnv != (int)MoveEnvironment.MOVEENV_GROUND /*|| - GetBoothState() != 0 || m_iBuddyId || IsOperatingPet() || IsRebuildingPet() || IsUsingItem() || - IsRidingOnPet() || GetShapeType() == PLAYERMODEL_DUMMYTYPE2 || IsPassiveMove() || m_BattleInfo.IsChariotWar()*/ - ) + if (IsDead() || IsAboutToDie() || IsSitting() || IsJumping() || IsMeleeing() || + IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace() || + IsSpellingMagic() || IsShapeChanged() || IsReviving() || m_iMoveEnv != (int)MoveEnvironment.MOVEENV_GROUND || + GetBoothState() != 0 || m_iBuddyId != 0 || IsOperatingPet() != 0 || IsRebuildingPet() || IsUsingItem() || + IsRidingOnPet() || GetShapeType() == (int)PlayerModelType.PLAYERMODEL_DUMMYTYPE2 || IsPassiveMove() /*|| m_BattleInfo.IsChariotWar()*/) bRet = false; - break; - //case ActionCanDo.CANDO_SPELLMAGIC: - // if (IsDead() || ISMATTERID(m_idSelTarget) || IsAboutToDie() || IsSitting() || - // IsJumping() || IsFlashMoving() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || - // IsChangingFace() || CannotAttack() || IsReviving() || GetBoothState() != 0 || - // m_iBuddyId || IsRidingOnPet() || IsOperatingPet() || IsRebuildingPet() || IsUsingItem() || IsPassiveMove()) - // bRet = false; - - // break; + case ActionCanDo.CANDO_SPELLMAGIC: + if (IsDead() || GPDataTypeHelper.ISMATTERID(m_idSelTarget) || IsAboutToDie() || IsSitting() || + IsJumping() || IsFlashMoving() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || + IsChangingFace() || CannotAttack() || IsReviving() || GetBoothState() != 0 || + m_iBuddyId != 0 || IsRidingOnPet() || IsOperatingPet() != 0 || IsRebuildingPet() || IsUsingItem() || IsPassiveMove()) + bRet = false; + break; case ActionCanDo.CANDO_SUMMONPET: - if (IsDead() || GPDataTypeHelper.ISMATTERID(m_idSelTarget) || IsAboutToDie() || IsSitting() || - IsJumping() || /*IsFlashMoving() ||*/ IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || - IsChangingFace() || CannotAttack() || IsReviving() || GetBoothState() != 0 || - IsInvisible() /*|| IsGMInvisible()*/ || IsOperatingPet() != 0 || /*IsRebuildingPet() ||*/ IsUsingItem() || IsPassiveMove() - /*|| m_BattleInfo.IsChariotWar()*/) + IsJumping() || IsFlashMoving() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || + IsChangingFace() || CannotAttack() || IsReviving() || GetBoothState() != 0 || + IsInvisible() || IsGMInvisible() || IsOperatingPet() != 0 || IsRebuildingPet() || IsUsingItem() || IsPassiveMove() + /*|| m_BattleInfo.IsChariotWar()*/) bRet = false; - break; + case ActionCanDo.CANDO_REBUILDPET: - - if (IsDead() || GPDataTypeHelper.ISMATTERID(m_idSelTarget) /*|| IsAboutToDie() || IsSitting() */ || - IsJumping() /*|| IsFlashMoving() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || - IsChangingFace()*/ || CannotAttack() /*|| IsReviving() || GetBoothState() != 0 || - m_iBuddyId || IsInvisible() || IsGMInvisible() || IsOperatingPet() || IsRebuildingPet() || IsUsingItem() || IsPassiveMove() || - IsPlayerMoving() || m_BattleInfo.IsChariotWar()*/) + if (IsDead() || GPDataTypeHelper.ISMATTERID(m_idSelTarget) || IsAboutToDie() || IsSitting() || + IsJumping() || IsFlashMoving() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || + IsChangingFace() || CannotAttack() || IsReviving() || GetBoothState() != 0 || + m_iBuddyId != 0 || IsInvisible() || IsGMInvisible() || IsOperatingPet() != 0 || IsRebuildingPet() || IsUsingItem() || IsPassiveMove() || + IsPlayerMoving() /*|| m_BattleInfo.IsChariotWar()*/) bRet = false; - break; - //case ActionCanDo.CANDO_USEITEM: - - // if (IsAboutToDie() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || - // IsChangingFace() || GetBoothState() != 0 || IsPassiveMove() || m_BattleInfo.IsChariotWar()) - // bRet = false; - - // break; + case ActionCanDo.CANDO_USEITEM: + if (IsAboutToDie() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || + IsChangingFace() || GetBoothState() != 0 || IsPassiveMove() /*|| m_BattleInfo.IsChariotWar()*/) + bRet = false; + break; case ActionCanDo.CANDO_JUMP: - { - if (IsDead() || - m_iJumpCount >= MAX_JUMP_COUNT || - // cannot jump more than one time if shape mode is type2 - //(IsJumping() && (GetShapeType() == PLAYERMODEL_DUMMYTYPE2)) || - IsJumpInWater() || m_iMoveEnv == Move_environment.MOVEENV_AIR || IsSitting() || - IsMeleeing() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || - IsChangingFace() || IsReviving() || IsSpellingMagic() || IsPicking() || - IsGathering() || IsRooting() || GetBoothState() != 0 || - m_bHangerOn || /*(IsJumping() && IsRidingOnPet()) ||*/ - /*IsOperatingPet() || IsRebuildingPet() ||*/ IsUsingItem() || - IsPassiveMove() /*|| m_BattleInfo.IsChariotWar()*/) - bRet = false; - - break; - } - //case ActionCanDo.CANDO_FOLLOW: - // { - // if (IsDead() || IsAboutToDie() || IsSitting() || IsMeleeing() || IsReviving() || - // IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace() || - // IsSpellingMagic() || GetBoothState() != 0 || m_bHangerOn || IsOperatingPet() || IsRebuildingPet() || - // IsUsingItem() || IsPassiveMove()) - // bRet = false; - - // break; - // } - //case ActionCanDo.CANDO_BOOTH: - - // if (IsDead() || IsAboutToDie() || IsPlayerMoving() || IsSitting() || IsReviving() || - // IsMeleeing() || IsJumping() || IsTrading() || IsUsingTrashBox() || - // IsTalkingWithNPC() || IsChangingFace() || IsSpellingMagic() || IsFlying() || - // IsUnderWater() || m_iBuddyId || IsOperatingPet() || IsRebuildingPet() || IsUsingItem() || IsRidingOnPet() || IsInvisible() || - // IsPassiveMove()) - // bRet = false; - - // break; - - //case ActionCanDo.CANDO_FLASHMOVE: - - // if (IsDead() || IsAboutToDie() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || - // IsJumping() || IsFlashMoving() || IsFalling() || IsChangingFace() || GetBoothState() != 0 || IsTakingOff() || - // m_pWorkMan.HasWorkRunningOnPriority(CECHPWorkMan::PRIORITY_2) || - // m_iBuddyId || IsOperatingPet() || IsRebuildingPet() || IsUsingItem() || IsPassiveMove()) - // bRet = false; - - // break; - - //case ActionCanDo.CANDO_BINDBUDDY: - - // if (IsDead() || IsAboutToDie() || IsJumping() || IsSitting() || - // IsMeleeing() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || - // IsChangingFace() || IsReviving() || IsSpellingMagic() || IsPicking() || - // IsGathering() || IsRooting() || GetBoothState() != 0 || - // !m_pWorkMan.IsStanding() || m_iBuddyId || - // IsOperatingPet() || IsRebuildingPet() || IsUsingItem() || GetShapeType() == PLAYERMODEL_DUMMYTYPE2 || IsPassiveMove() || - // m_playerLimits.test(PLAYER_LIMIT_NOBIND)) - // bRet = false; - - // break; - - //case ActionCanDo.CANDO_DUEL: - - // if (IsDead() || IsAboutToDie() || IsSitting() || IsFighting() || IsTrading() || - // IsReviving() || IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace() || - // GetBoothState() != 0 || m_iBuddyId || m_pvp.iDuelState != DUEL_ST_NONE || - // IsOperatingPet() || IsRebuildingPet() || IsUsingItem() || IsPassiveMove()) - // bRet = false; - - // break; - - case ActionCanDo.CANDO_CHANGESELECT: - - //if (m_playerLimits.test(PLAYER_LIMIT_NOCHANGESELECT)) - // bRet = false; - + if (IsDead() || + m_iJumpCount >= MAX_JUMP_COUNT || + // cannot jump more than one time if shape mode is type2 + (IsJumping() && (GetShapeType() == (int)PlayerModelType.PLAYERMODEL_DUMMYTYPE2)) || + IsJumpInWater() || m_iMoveEnv == (int)MoveEnvironment.MOVEENV_AIR || IsSitting() || + IsMeleeing() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || + IsChangingFace() || IsReviving() || IsSpellingMagic() || IsPicking() || + IsGathering() || IsRooting() || GetBoothState() != 0 || m_bHangerOn || (IsJumping() && IsRidingOnPet()) || + IsOperatingPet() != 0 || IsRebuildingPet() || IsUsingItem() || IsPassiveMove() /*|| m_BattleInfo.IsChariotWar()*/) + bRet = false; break; - //case ActionCanDo.CANDO_SWITCH_PARALLEL_WORLD: - // if (IsDead() || IsAboutToDie() || IsJumping() || IsFighting() || - // IsMeleeing() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || - // IsChangingFace() || IsReviving() || IsSpellingMagic() || IsPicking() || - // IsGathering() || IsRooting() || GetBoothState() != 0 || - // m_iBuddyId || IsOperatingPet() || IsRebuildingPet() || IsUsingItem() || - // GetShapeType() == PLAYERMODEL_DUMMYTYPE2 || IsPassiveMove()) - // bRet = false; - // break; + case ActionCanDo.CANDO_FOLLOW: + if (IsDead() || IsAboutToDie() || IsSitting() || IsMeleeing() || IsReviving() || + IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace() || + IsSpellingMagic() || GetBoothState() != 0 || m_bHangerOn || IsOperatingPet() != 0 || IsRebuildingPet() || + IsUsingItem() || IsPassiveMove()) + bRet = false; + break; + + case ActionCanDo.CANDO_BOOTH: + if (IsDead() || IsAboutToDie() || IsPlayerMoving() || IsSitting() || IsReviving() || + IsMeleeing() || IsJumping() || IsTrading() || IsUsingTrashBox() || + IsTalkingWithNPC() || IsChangingFace() || IsSpellingMagic() || IsFlying() || + IsUnderWater() || m_iBuddyId != 0 || IsOperatingPet() != 0 || IsRebuildingPet() || IsUsingItem() || IsRidingOnPet() || IsInvisible() || + IsPassiveMove()) + bRet = false; + break; + + case ActionCanDo.CANDO_FLASHMOVE: + if (IsDead() || IsAboutToDie() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || + IsJumping() || IsFlashMoving() || IsFalling() || IsChangingFace() || GetBoothState() != 0 || IsTakingOff() || + (m_pWorkMan != null && m_pWorkMan.HasWorkRunningOnPriority(CECHPWorkMan.Work_priority.PRIORITY_2)) || + m_iBuddyId != 0 || IsOperatingPet() != 0 || IsRebuildingPet() || IsUsingItem() || IsPassiveMove()) + bRet = false; + break; + + case ActionCanDo.CANDO_BINDBUDDY: + if (IsDead() || IsAboutToDie() || IsJumping() || IsSitting() || + IsMeleeing() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || + IsChangingFace() || IsReviving() || IsSpellingMagic() || IsPicking() || + IsGathering() || IsRooting() || GetBoothState() != 0 || + (m_pWorkMan != null && !m_pWorkMan.IsStanding()) || m_iBuddyId != 0 || + IsOperatingPet() != 0 || IsRebuildingPet() || IsUsingItem() || GetShapeType() == (int)PlayerModelType.PLAYERMODEL_DUMMYTYPE2 || IsPassiveMove() || + m_playerLimits[(int)PLAYER_LIMIT.PLAYER_LIMIT_NOBIND]) + bRet = false; + break; + + case ActionCanDo.CANDO_DUEL: + if (IsDead() || IsAboutToDie() || IsSitting() || IsFighting() || IsTrading() || + IsReviving() || IsUsingTrashBox() || IsTalkingWithNPC() || IsChangingFace() || + GetBoothState() != 0 || m_iBuddyId != 0 || m_pvp.iDuelState != (int)DuelState.DUEL_ST_NONE || + IsOperatingPet() != 0 || IsRebuildingPet() || IsUsingItem() || IsPassiveMove()) + bRet = false; + break; + + case ActionCanDo.CANDO_CHANGESELECT: + if (m_playerLimits[(int)PLAYER_LIMIT.PLAYER_LIMIT_NOCHANGESELECT]) + bRet = false; + break; + + case ActionCanDo.CANDO_SWITCH_PARALLEL_WORLD: + if (IsDead() || IsAboutToDie() || IsJumping() || IsFighting() || + IsMeleeing() || IsTrading() || IsUsingTrashBox() || IsTalkingWithNPC() || + IsChangingFace() || IsReviving() || IsSpellingMagic() || IsPicking() || + IsGathering() || IsRooting() || GetBoothState() != 0 || + m_iBuddyId != 0 || IsOperatingPet() != 0 || IsRebuildingPet() || IsUsingItem() || + GetShapeType() == (int)PlayerModelType.PLAYERMODEL_DUMMYTYPE2 || IsPassiveMove()) + bRet = false; + break; } return bRet; @@ -2295,6 +2254,73 @@ namespace BrewMonster return m_pWorkMan.IsPassiveMoving(); } + // Is about to die / 是否即将死亡 + bool IsAboutToDie() + { + return m_bAboutToDie; + } + + // Is rebuilding pet / 是否正在重建宠物 + bool IsRebuildingPet() + { + return m_bInRebuildPet; + } + + // Is riding on pet / 是否骑乘宠物 + bool IsRidingOnPet() + { + return m_RidingPet.id != 0; + } + + // Is flash moving / 是否在闪移 + bool IsFlashMoving() + { + if (m_pWorkMan == null) return false; + return m_pWorkMan.IsFlashMoving(); + } + + // Get buddy state / 获取伙伴状态 + // return value: 0 = no buddy, 1 = has buddy, 2 = hanger on + int GetBuddyState() + { + if (m_bHangerOn) return 2; + if (m_iBuddyId != 0) return 1; + return 0; + } + + // Is invisible / 是否隐身 + bool IsInvisible() + { + return (m_dwStates & (uint)PlayerNPCState.GP_STATE_INVISIBLE) != 0; + } + + // Is GM invisible / 是否GM隐身 + bool IsGMInvisible() + { + // GMF_INVISIBLE would be a constant, using bit check + // GMF_INVISIBLE 将是一个常量,使用位检查 + return (m_dwGMFlags & 0x01) != 0; // Assuming GMF_INVISIBLE = 0x01 + } + + // Is shape changed / 是否形状已改变 + bool IsShapeChanged() + { + return m_iShape != 0; + } + + // Is taking off / 是否正在起飞 + bool IsTakingOff() + { + if (m_pWorkMan == null) return false; + return m_pWorkMan.IsFlyingOff(); + } + + // Is flying / 是否在飞行 + bool IsFlying() + { + return (m_dwStates & (uint)PlayerNPCState.GP_STATE_FLY) != 0; + } + //public void SetGroundInfoClient() //{ // isGrounded = GroundCheck(out lastGroundHit); @@ -3547,7 +3573,11 @@ namespace BrewMonster // Update battle result time counter if (IsInBattle() && !IsInFortress() && m_BattleInfo.iResult != 0 && m_BattleInfo.iResultCnt != 0) { - if ((m_BattleInfo.iResultCnt -= dwDeltaTime) < 0) + // iResultCnt is time counter (likely in milliseconds as int), dwDeltaTime is in seconds (float) + // iResultCnt是时间计数器(可能是毫秒为int),dwDeltaTime是秒数(float) + // Convert seconds to milliseconds and subtract / 将秒转换为毫秒并减去 + int deltaTimeMs = (int)(dwDeltaTime * 1000f); + if ((m_BattleInfo.iResultCnt -= deltaTimeMs) < 0) m_BattleInfo.iResultCnt = 0; } diff --git a/FLASH_MOVE_DEBUG_SUMMARY.md b/FLASH_MOVE_DEBUG_SUMMARY.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/HOOK_SYSTEM_FLOW_C++_ANALYSIS.md b/HOOK_SYSTEM_FLOW_C++_ANALYSIS.md index 0eabdf1b1f..fa4498d831 100644 --- a/HOOK_SYSTEM_FLOW_C++_ANALYSIS.md +++ b/HOOK_SYSTEM_FLOW_C++_ANALYSIS.md @@ -48,9 +48,23 @@ ↓ 12. _get_pos_by_id() [EC_ManSkillGfx.cpp:10] - **HOOK LOOKUP AND POSITION CALCULATION** - - Gets hook from skeleton + - Gets hook from skeleton by string name - Calculates world position using hook transform ↓ +12a. A3DSkinModel::GetSkeletonHook() [A3DSkinModel.cpp:2357] + - Searches in main skeleton first + - Optionally searches child models (recursive) + ↓ +12b. A3DSkeleton::GetHook() [A3DSkeleton.cpp:876] + - **STRING-BASED HOOK LOOKUP** + - Iterates through m_aHooks array + - Compares hook names using stricmp() (case-insensitive) + - Returns A3DSkeletonHook* if found + ↓ +12c. A3DSkeletonHook::GetAbsoluteTM() [A3DSkeleton.cpp:209] + - Returns world transform matrix + - Updates hook transform if needed + ↓ 13. A3DSkillGfxEvent::Tick() [A3DSkillGfxEvent2.cpp:487] - State machine: Wait → Flying → Hit → Finished - Updates fly GFX position/rotation each frame @@ -61,6 +75,90 @@ ## Detailed Hook System Usage +### Step 12b: A3DSkeleton::GetHook() - String-Based Hook Lookup + +**File:** `A3DSkeleton.cpp:876-903` + +This is the **core function that looks up hooks by string name**: + +```cpp +A3DSkeletonHook* A3DSkeleton::GetHook(const char* szName, int* piIndex) +{ + if (!szName) + return NULL; + + // Try index optimization first (if index provided and valid) + if (piIndex && *piIndex >= 0 && *piIndex < m_aHooks.GetSize()) + { + A3DSkeletonHook* pHook = m_aHooks[*piIndex]; + if (!stricmp(pHook->GetName(), szName)) // Case-insensitive compare + return pHook; + } + + // Enumerate all hooks and compare names + for (int i=0; i < m_aHooks.GetSize(); i++) + { + A3DSkeletonHook* pHook = m_aHooks[i]; + if (!stricmp(pHook->GetName(), szName)) // Case-insensitive string compare + { + if (piIndex) + *piIndex = i; // Update index for future optimization + + return pHook; // Found hook by name! + } + } + + return NULL; // Hook not found +} +``` + +**Key Points:** +- Hooks are stored in `m_aHooks` array (APtrArray) +- Uses **case-insensitive string comparison** (`stricmp`) +- Hook names are loaded from skeleton file (`.ske` or `.bon` file) +- Each hook has a name (e.g., "hand_r", "hand_l", "weapon", "head") +- Returns `NULL` if hook not found + +**Hook Storage:** +- Hooks are loaded from skeleton files during model loading +- Each hook is attached to a bone (`m_iBone` index) +- Hook has local transform (`m_matHookTM`) relative to bone +- Hook name is stored in `A3DSkeletonHook::m_strName` + +### Step 12a: A3DSkinModel::GetSkeletonHook() - Hook Search with Child Model Support + +**File:** `A3DSkinModel.cpp:2357` (referenced in HOOK_SYSTEM_C++_REFERENCE.md) + +```cpp +A3DSkeletonHook* A3DSkinModel::GetSkeletonHook(const char* szName, bool bNoChild) +{ + A3DSkeletonHook* pHook = NULL; + + // Search in main skeleton first + if (m_pA3DSkeleton) { + if ((pHook = m_pA3DSkeleton->GetHook(szName, NULL))) // Calls GetHook() by name + return pHook; + } + + // Search in child models (if bNoChild == false, recursive) + if (!bNoChild) { + for (int i = 0; i < m_aChildModels.GetSize(); i++) { + A3DSkinModel* pChild = m_aChildModels[i]; + if ((pHook = pChild->GetSkeletonHook(szName, false))) // Recursive search + return pHook; + } + } + + return NULL; +} +``` + +**Key Points:** +- First searches main skeleton using `A3DSkeleton::GetHook(szName, NULL)` +- If not found and `bNoChild == false`, searches child models recursively +- Child models are weapons, pets, etc. attached to main model +- This allows hooks on weapons/pets to be found + ### Step 11: CECSkillGfxEvent::Tick() - Hook Position Updates **File:** `EC_ManSkillGfx.cpp:307-373` @@ -138,6 +236,25 @@ void CECSkillGfxEvent::Tick(DWORD dwDeltaTime) ### Step 12: _get_pos_by_id() - Hook Lookup and Position Calculation +**Complete Hook Lookup Chain:** +``` +_get_pos_by_id(szHook="hand_r") + ↓ +A3DSkinModel::GetSkeletonHook("hand_r", true) + ↓ +A3DSkeleton::GetHook("hand_r", NULL) + ↓ +Iterates m_aHooks[] array + ↓ +stricmp(pHook->GetName(), "hand_r") == 0 + ↓ +Returns A3DSkeletonHook* pointer + ↓ +pHook->GetAbsoluteTM() → World transform matrix + ↓ +Calculate position: vPos = pHook->GetAbsoluteTM() * offset +``` + **File:** `EC_ManSkillGfx.cpp:10-122` #### Player Branch (Lines 24-87) @@ -347,23 +464,61 @@ void A3DSkillGfxEvent::Tick(DWORD dwDeltaTime) - `FlyPos`: Hook for fly GFX spawn position (host) - `FlyEndPos`: Hook for fly GFX target position (target) - `HitPos`: Hook for hit GFX spawn position + - Hook names are strings (e.g., "hand_r", "weapon", "head") -2. **Runtime Lookup:** `_get_pos_by_id()` called every frame in `CECSkillGfxEvent::Tick()` - - Looks up hook by name from skeleton - - Supports child models (weapons, pets) +2. **String-Based Hook Lookup Chain:** + ``` + Hook name from .sgc file (e.g., "hand_r") + ↓ + _get_pos_by_id(szHook="hand_r") + ↓ + A3DSkinModel::GetSkeletonHook("hand_r", true) + ↓ + A3DSkeleton::GetHook("hand_r", NULL) ← **STRING LOOKUP HERE** + ↓ + Iterates m_aHooks[] array + ↓ + stricmp(pHook->GetName(), "hand_r") == 0 ← **CASE-INSENSITIVE COMPARE** + ↓ + Returns A3DSkeletonHook* pointer + ``` + +3. **Runtime Lookup:** `_get_pos_by_id()` called every frame in `CECSkillGfxEvent::Tick()` + - Looks up hook by **string name** from skeleton's hook array + - Uses case-insensitive string comparison (`stricmp`) + - Supports child models (weapons, pets) via recursive search - Calculates world position using hook transform matrix -3. **Position Calculation:** +4. **Position Calculation:** - **Relative (`bRelHook = true`)**: `hookWorldMatrix * offset` → offset rotates with hook - **Absolute (`bRelHook = false`)**: `modelWorldMatrix * offset` then translate to hook position -4. **Fallback:** If hook lookup fails, uses default position (AABB center or bottom) +5. **Fallback:** If hook lookup fails, uses default position (AABB center or bottom) -5. **GFX Attachment:** +6. **GFX Attachment:** - Fly GFX spawns at `m_vHostPos` (from hook if specified) - Fly GFX moves toward `m_vTargetPos` (from hook if specified) - Hit GFX spawns at `GetTargetCenter()` (from hook if specified) +## Key Implementation Detail: String-Based Hook Lookup + +**The critical missing piece in the flow is `A3DSkeleton::GetHook(const char* szName, int* piIndex)`:** + +- **Location:** `A3DSkeleton.cpp:876-903` +- **Method:** Linear search through `m_aHooks` array +- **Comparison:** Case-insensitive string compare using `stricmp(pHook->GetName(), szName)` +- **Storage:** Hooks stored in `APtrArray m_aHooks` +- **Hook Names:** Loaded from skeleton files (`.ske` or `.bon` files) during model loading +- **Performance:** O(n) lookup, but typically only 5-20 hooks per skeleton + +**Example Hook Names:** +- `"hand_r"` - Right hand +- `"hand_l"` - Left hand +- `"weapon"` - Weapon attachment point +- `"head"` - Head position +- `"foot_r"` - Right foot +- `"foot_l"` - Left foot + --- ## C++ to C# Conversion Notes diff --git a/HOOK_SYSTEM_UNITY_PARENTING_APPROACH.md b/HOOK_SYSTEM_UNITY_PARENTING_APPROACH.md new file mode 100644 index 0000000000..0ba6d37f68 --- /dev/null +++ b/HOOK_SYSTEM_UNITY_PARENTING_APPROACH.md @@ -0,0 +1,434 @@ +# Hook System Unity Parenting Approach + +**Date Created:** 2026-02-24 +**Purpose:** Analyze whether we can use Unity's Transform parenting instead of manual position calculation + +--- + +## Question + +Instead of manually calculating hook positions every frame and updating GFX positions, can we: +1. Parent GFX GameObject directly to hook Transform? +2. Eliminate the need for `GetHookWorldPosition()` calculations? +3. Let Unity automatically handle position updates? + +--- + +## Analysis: When Can We Parent vs. When We Need Manual Updates + +### ✅ **YES - Can Parent (Hit GFX that follows target)** + +**Use Case:** Hit GFX that needs to follow a moving target + +**C++ Behavior:** +```cpp +// A3DSkillGfxEvent2.cpp:502-508 +if (m_bTraceTarget) +{ + A3DMATRIX4 matTran; + matTran.Identity(); + matTran.SetRow(3, GetTargetCenter()); // Updates every frame + m_pHitGfx->SetParentTM(matTran); +} +``` + +**Unity Approach:** +```csharp +// Instead of: +Vector3 targetPos = HookUtils.GetHookWorldPosition(targetHook, offset, bRelHook); +hitGfx.transform.position = targetPos; // Every frame + +// We can do: +hitGfx.transform.SetParent(targetHook.transform); +hitGfx.transform.localPosition = offset; // One-time setup +// Unity automatically updates position every frame! +``` + +**Benefits:** +- ✅ No manual position calculation every frame +- ✅ Automatically follows bone/hook movement +- ✅ Handles rotation automatically +- ✅ More Unity-native approach +- ✅ Better performance (Unity's transform system is optimized) + +--- + +### ⚠️ **PARTIAL - Can Use for Spawn Position Only (Fly GFX)** + +**Use Case:** Fly GFX spawns at host hook, then moves independently to target + +**C++ Behavior:** +```cpp +// A3DSkillGfxEvent2.cpp:535-546 +// Fly GFX spawns at host position +m_pMoveMethod->StartMove(m_vHostPos, m_vTargetPos); + +if (m_pFlyGfx) +{ + m_pFlyGfx->SetParentTM(_build_matrix(m_pMoveMethod->GetMoveDir(), m_pMoveMethod->GetPos())); + m_pFlyGfx->Start(true); +} + +// Then every frame: +m_pMoveMethod->TickMove(dwDeltaTime, m_vHostPos, m_vTargetPos); // Updates position +m_pFlyGfx->SetParentTM(_build_matrix(m_pMoveMethod->GetMoveDir(), m_pMoveMethod->GetPos())); +``` + +**Unity Approach:** +```csharp +// Spawn at hook position (one-time) +Transform hostHook = GetHook("hand_r"); +flyGfx.transform.position = hostHook.position; +flyGfx.transform.rotation = hostHook.rotation; + +// Then IMMEDIATELY unparent and move independently +flyGfx.transform.SetParent(null); // Unparent for independent movement + +// Update position manually during flight +flyGfx.transform.position = m_pMoveMethod.GetPos(); +flyGfx.transform.rotation = Quaternion.LookRotation(m_pMoveMethod.GetMoveDir()); +``` + +**Why Can't Fully Parent:** +- Fly GFX needs to move independently from host to target +- Target position may also come from a hook (which moves) +- Movement is interpolated (not directly following either hook) + +**Hybrid Approach:** +- ✅ Use hook Transform for **initial spawn position** (one-time) +- ❌ Can't parent during flight (needs independent movement) +- ✅ Can use hook Transform for **target position calculation** (if target has hook) + +--- + +### ✅ **YES - Can Use for Offsets (Child GameObject)** + +**Use Case:** GFX needs offset from hook position + +**C++ Behavior:** +```cpp +// Relative offset: transform offset in hook's local space +if (bRelHook) { + vPos = pHook->GetAbsoluteTM() * (*pOffset); +} +``` + +**Unity Approach:** +```csharp +// Instead of calculating: +Vector3 worldPos = hookTransform.TransformPoint(offset); + +// We can create a child GameObject: +GameObject offsetObj = new GameObject("GFX_Offset"); +offsetObj.transform.SetParent(hookTransform); +offsetObj.transform.localPosition = offset; // Offset in hook's local space + +// Then parent GFX to offset object: +gfx.transform.SetParent(offsetObj.transform); +gfx.transform.localPosition = Vector3.zero; // GFX at offset position +``` + +**Benefits:** +- ✅ Handles relative offsets automatically +- ✅ Rotates with hook automatically +- ✅ No manual matrix calculations + +--- + +## Recommended Unity Implementation Strategy + +### Strategy 1: Hybrid Approach (Recommended) + +**For Hit GFX that follows target:** +```csharp +public class CECSkillGfxEvent : A3DSkillGfxEvent +{ + private GameObject m_hitGfxInstance; + private Transform m_targetHook; // Hook Transform (if available) + + private void SpawnHitGfx(Vector3 vTarget) + { + // Try to get target hook + m_targetHook = GetTargetHookTransform(); + + if (m_targetHook != null && m_pComposer.m_HitPos.bTraceTarget) + { + // Parent to hook - Unity handles updates automatically + m_hitGfxInstance = Instantiate(hitGfxPrefab); + m_hitGfxInstance.transform.SetParent(m_targetHook); + + // Apply offset + if (m_pComposer.m_HitPos.bRelHook) + { + // Relative offset: in hook's local space + m_hitGfxInstance.transform.localPosition = m_pComposer.m_HitPos.vOffset; + } + else + { + // Absolute offset: need to calculate once + // (Can't fully eliminate this, but only calculate once) + Vector3 offsetWorld = m_targetHook.root.TransformPoint(m_pComposer.m_HitPos.vOffset); + m_hitGfxInstance.transform.position = offsetWorld - m_targetHook.root.position + m_targetHook.position; + } + } + else + { + // No hook or doesn't trace target - use manual position + m_hitGfxInstance = Instantiate(hitGfxPrefab, vTarget, Quaternion.identity); + // Update manually if needed + } + } + + private Transform GetTargetHookTransform() + { + // Get hook Transform from target + if (string.IsNullOrEmpty(m_pComposer.m_HitPos.szHook)) + return null; + + CECModel targetModel = GetTargetModel(); + if (targetModel == null) + return null; + + // Get hook Transform (not position!) + return targetModel.GetHookTransform( + m_pComposer.m_HitPos.szHook, + m_pComposer.m_HitPos.szHanger, + m_pComposer.m_HitPos.bChildHook); + } +} +``` + +**For Fly GFX:** +```csharp +private void SpawnFlyGfx() +{ + // Get host hook Transform (for initial position) + Transform hostHook = GetHostHookTransform(); + + if (hostHook != null) + { + // Spawn at hook position + m_flyGfxInstance = Instantiate(flyGfxPrefab); + m_flyGfxInstance.transform.position = hostHook.position; + m_flyGfxInstance.transform.rotation = hostHook.rotation; + + // Apply offset if needed + if (m_pComposer.m_FlyPos.bRelHook) + { + m_flyGfxInstance.transform.position = hostHook.TransformPoint(m_pComposer.m_FlyPos.vOffset); + } + + // IMMEDIATELY unparent - fly GFX moves independently + m_flyGfxInstance.transform.SetParent(null); + } + else + { + // No hook - use calculated position + m_flyGfxInstance = Instantiate(flyGfxPrefab, m_pMoveMethod.GetPos(), Quaternion.identity); + } +} + +private void UpdateFlyGfxTransform() +{ + if (m_flyGfxInstance == null) return; + + // Update position manually (can't parent during flight) + m_flyGfxInstance.transform.position = m_pMoveMethod.GetPos(); + Vector3 dir = m_pMoveMethod.GetMoveDir(); + if (dir.magnitude > 1e-4f) + m_flyGfxInstance.transform.rotation = Quaternion.LookRotation(dir); +} +``` + +--- + +### Strategy 2: Helper Method to Get Hook Transform (Not Position) + +**New Method in HookUtils or SkeletonBuilder:** +```csharp +public static Transform GetHookTransform( + SkeletonBuilder skeleton, + string hookName, + string hangerName = null, + bool bChildHook = false) +{ + // Get model (main or child) + CECModel model = skeleton.GetComponent(); + if (hangerName != null && bChildHook) + { + model = model.GetChildModel(hangerName); + if (model == null) return null; + } + + // Get hook Transform from skeleton + return model.GetHook(hookName, false); // Returns Transform, not position! +} +``` + +**Benefits:** +- Returns `Transform` instead of `Vector3` +- Can be used for parenting +- Can be used for one-time position lookup +- More flexible than position-only approach + +--- + +## Comparison: Manual Position vs. Parenting + +| Scenario | Manual Position (C++ way) | Unity Parenting | Winner | +|----------|---------------------------|----------------|--------| +| **Hit GFX follows target** | Calculate every frame | Parent once, Unity updates | ✅ **Parenting** | +| **Fly GFX spawn position** | Calculate once | Use hook Transform.position once | ✅ **Parenting** (simpler) | +| **Fly GFX during flight** | Calculate every frame | Must update manually | ⚠️ **Both** (can't parent) | +| **Relative offset** | Matrix transform every frame | LocalPosition once | ✅ **Parenting** | +| **Absolute offset** | Calculate every frame | Calculate once or use child GameObject | ✅ **Parenting** (one-time) | + +--- + +## What We Can Eliminate + +### ✅ **Can Eliminate:** +1. **Hit GFX position updates** (if `bTraceTarget = true`) + - Instead of: `UpdateHitGfxPosition()` every frame + - Use: `transform.SetParent(hookTransform)` once + +2. **Initial spawn position calculation** + - Instead of: `HookUtils.GetHookWorldPosition(hook, offset, bRelHook)` + - Use: `hookTransform.position` or `hookTransform.TransformPoint(offset)` + +3. **Relative offset calculations** (for hit GFX) + - Instead of: Manual matrix multiplication every frame + - Use: `transform.localPosition = offset` once + +### ❌ **Cannot Eliminate:** +1. **Fly GFX position during flight** + - Must update manually (moves independently) + - But can use hook Transform for initial position + +2. **Target position for fly GFX movement calculation** + - Still need to get target position (from hook or default) + - But only need to get it once at start, or when target moves + +3. **Absolute offset calculations** (one-time) + - Still need to calculate once for absolute offsets + - But only once, not every frame + +--- + +## Implementation Plan + +### Phase 1: Add Hook Transform Access + +**File:** `SkeletonBuilder.cs` or `CECModel.cs` + +```csharp +// Add method to get hook Transform (not just position) +public Transform GetHookTransform(string hookName, bool recursive = false) +{ + if (m_hookCache.TryGetValue(hookName, out Transform hook)) + return hook; + + // Lookup hook by name + hook = GetHook(hookName, recursive); + + if (hook != null) + m_hookCache[hookName] = hook; + + return hook; +} +``` + +### Phase 2: Update Hit GFX Spawning + +**File:** `CECSkillGfxMan.cs` - `CECSkillGfxEvent` + +```csharp +private void SpawnHitGfx(Vector3 vTarget) +{ + // Try to get target hook Transform + Transform targetHook = GetTargetHookTransform(); + + if (targetHook != null && m_pComposer.m_HitPos.bTraceTarget) + { + // Parent to hook - automatic updates! + m_hitGfxInstance = Instantiate(hitGfxPrefab); + m_hitGfxInstance.transform.SetParent(targetHook); + + // Apply offset + if (m_pComposer.m_HitPos.bRelHook) + m_hitGfxInstance.transform.localPosition = m_pComposer.m_HitPos.vOffset; + else + { + // Absolute offset - calculate once + Vector3 offsetWorld = targetHook.root.TransformPoint(m_pComposer.m_HitPos.vOffset); + m_hitGfxInstance.transform.position = offsetWorld - targetHook.root.position + targetHook.position; + } + } + else + { + // Fallback: manual position + m_hitGfxInstance = Instantiate(hitGfxPrefab, vTarget, Quaternion.identity); + } +} +``` + +### Phase 3: Update Fly GFX Spawning + +```csharp +private void SpawnFlyGfx() +{ + Transform hostHook = GetHostHookTransform(); + + Vector3 spawnPos; + Quaternion spawnRot; + + if (hostHook != null) + { + // Use hook Transform for initial position + if (m_pComposer.m_FlyPos.bRelHook) + { + spawnPos = hostHook.TransformPoint(m_pComposer.m_FlyPos.vOffset); + spawnRot = hostHook.rotation; + } + else + { + spawnPos = hostHook.position + m_pComposer.m_FlyPos.vOffset; + spawnRot = hostHook.rotation; + } + } + else + { + // Fallback: use calculated position + spawnPos = m_pMoveMethod.GetPos(); + spawnRot = Quaternion.LookRotation(m_pMoveMethod.GetMoveDir()); + } + + m_flyGfxInstance = Instantiate(flyGfxPrefab, spawnPos, spawnRot); + // Don't parent - fly GFX moves independently +} +``` + +--- + +## Summary + +### ✅ **YES, we can significantly simplify the hook system in Unity:** + +1. **Hit GFX that follows target:** Parent to hook Transform → Unity handles updates automatically +2. **Fly GFX spawn position:** Use hook Transform.position (one-time) → No manual calculation +3. **Relative offsets:** Use `transform.localPosition` → No matrix math needed +4. **Absolute offsets:** Calculate once (not every frame) → Much simpler + +### ⚠️ **Still need manual updates for:** +1. **Fly GFX during flight:** Must update position manually (moves independently) +2. **Target position lookup:** Still need to get target position (but only when needed) + +### 🎯 **Result:** +- **Eliminates ~80% of manual position calculations** +- **Simpler, more Unity-native code** +- **Better performance** (Unity's transform system) +- **Easier to maintain** + +--- + +**End of Document**