diff --git a/Assets/ModelRenderer/Scripts/SkinnedMesh/SkeletonBuilder.cs b/Assets/ModelRenderer/Scripts/SkinnedMesh/SkeletonBuilder.cs index d97792d050..a0317303e8 100644 --- a/Assets/ModelRenderer/Scripts/SkinnedMesh/SkeletonBuilder.cs +++ b/Assets/ModelRenderer/Scripts/SkinnedMesh/SkeletonBuilder.cs @@ -10,7 +10,6 @@ namespace BrewMonster.Scripts public Transform rootBone; public Dictionary hooks = new Dictionary(); - /// /// This function retrieves an array of transforms for the specified bone names. @@ -67,12 +66,21 @@ namespace BrewMonster.Scripts if (hooks.TryGetValue(hookName, out Transform hook)) return hook; - // Recursive search in child models (Phase 2) - // 在子模型中递归搜索(阶段2) + // Recursive search in child models (if recursive flag is true) + // 在子模型中递归搜索(如果递归标志为true) if (recursive) { - // TODO Phase 2: Search child models - // TODO 阶段2:搜索子模型 + // Search in child SkeletonBuilders (weapons, pets, etc.) + // 在子SkeletonBuilder中搜索(武器、宠物等) + SkeletonBuilder[] childBuilders = GetComponentsInChildren(true); + foreach (SkeletonBuilder childBuilder in childBuilders) + { + if (childBuilder == this) continue; // Skip self + + Transform childHook = childBuilder.GetHook(hookName, false); // Don't recurse again + if (childHook != null) + return childHook; + } } return null; diff --git a/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs b/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs index 390c3d452b..fc7a3faf37 100644 --- a/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs @@ -365,7 +365,8 @@ namespace BrewMonster // 这与C++逻辑匹配:当投射物击中地面(无目标)时使用m_szHitGrndGfx bool bTargetExists = m_bTargetExist && m_nTargetID != 0; GameObject prefab = bTargetExists ? m_pComposer.GetHitGFX() : m_pComposer.GetHitGrdGFX(); - + BMLogger.LogError("HitGfx : " + m_pComposer.hitGfxName); + if (prefab == null) { // Fallback: if ground hit GFX is null but target doesn't exist, try regular hit GFX @@ -465,11 +466,66 @@ namespace BrewMonster { if (bIsGoblinSkill) { - // TODO: Handle goblin skill position - // if (pPlayer->GetGoblinModel()) - // vPos = pPlayer->GetGoblinModel()->GetModel()->GetModelAABB().Center; - // else - // return false; + // Get goblin model from player + // 从玩家获取小精灵模型 + // Note: GetGoblin() method may need to be implemented in CECPlayer + // 注意: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(); + if (pModel != null) + { + // Try common goblin hanger names + // 尝试常见的小精灵挂载者名称 + CECModel goblinModel = pModel.GetChildModel("goblin") ?? + pModel.GetChildModel("pet") ?? + pModel.GetChildModel("_goblin"); + + if (goblinModel != null) + { + // Use hook if specified, otherwise use model center + // 如果指定了挂点则使用挂点,否则使用模型中心 + if (!string.IsNullOrEmpty(szHook)) + { + Transform pHook = goblinModel.GetHook(szHook, true); + if (pHook != null) + { + 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 + return true; + } + } + } + +#if UNITY_EDITOR + BMLogger.LogWarning($"[HOOK_DEBUG] Goblin model not found for player ID {nID}"); +#endif return false; } else @@ -481,33 +537,55 @@ namespace BrewMonster if (string.IsNullOrEmpty(szHook)) break; - // TODO: Get player model and hook position - /*CECModel pModel = pPlayer->GetPlayerModel(); - if (!pModel) - break; - - if (szHanger && bChildHook) - pModel = pModel->GetChildModel(szHanger); - - if (!pModel) - break; - - A3DSkinModel* pSkin = pModel->GetA3DSkinModel(); - A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true); - - if (!pHook) - break; - - if (bRelHook) - vPos = pHook->GetAbsoluteTM() * pOffset; - else + // Get player model and hook position + // 获取玩家模型和挂点位置 + CECModel pModel = pPlayer.GetPlayerModel(); + if (pModel == null) { - vPos = pSkin->GetAbsoluteTM() * pOffset; - vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); +#if UNITY_EDITOR + BMLogger.LogWarning($"[HOOK_DEBUG] Player model not found for ID {nID}, hook '{szHook}'"); +#endif + break; } - return true;*/ - break; + // Handle child model (hanger) if specified + // 如果指定了子模型(挂载者),则处理 + if (!string.IsNullOrEmpty(szHanger) && bChildHook) + { + pModel = pModel.GetChildModel(szHanger); + if (pModel == null) + { +#if UNITY_EDITOR + BMLogger.LogWarning($"[HOOK_DEBUG] Child model '{szHanger}' not found for player ID {nID}, hook '{szHook}'"); +#endif + break; + } + } + + // Get hook Transform (non-recursive search as per C++: GetSkeletonHook(szHook, true)) + // 获取挂点变换(非递归搜索,对应C++:GetSkeletonHook(szHook, true)) + Transform pHook = pModel.GetHook(szHook, false); + if (pHook == null) + { +#if UNITY_EDITOR + BMLogger.LogWarning($"[HOOK_DEBUG] Hook '{szHook}' not found for player ID {nID}, falling back to center position"); +#endif + break; + } + + // Get model transform for absolute offset calculation + // 获取模型变换用于绝对偏移计算 + Transform modelTransform = pModel.transform; + + // Calculate position based on relative/absolute offset + // 根据相对/绝对偏移计算位置 + vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform); + +#if UNITY_EDITOR + BMLogger.Log($"[HOOK_DEBUG] Found hook '{szHook}' for player ID {nID}, position={vPos}, relative={bRelHook}, offset={pOffset}"); +#endif + + return true; } if (HitPos == GfxHitPos.enumHitBottom) @@ -539,20 +617,58 @@ namespace BrewMonster { while (true) { - // TODO: Get NPC hook position - /*A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook); - if (!pHook) + if (string.IsNullOrEmpty(szHook)) break; - A3DSkinModel *pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook); - if (bRelHook) - vPos = pHook->GetAbsoluteTM() * pOffset; - else + + // Get NPC model + // 获取NPC模型 + CECModel pModel = pNPC.GetModel(); + if (pModel == null) { - vPos = pSkin->GetAbsoluteTM() * pOffset; - vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); +#if UNITY_EDITOR + BMLogger.LogWarning($"[HOOK_DEBUG] NPC model not found for ID {nID}, hook '{szHook}'"); +#endif + break; } - return true;*/ - break; + + // Handle child model (hanger) if specified + // 如果指定了子模型(挂载者),则处理 + if (!string.IsNullOrEmpty(szHanger) && bChildHook) + { + pModel = pModel.GetChildModel(szHanger); + if (pModel == null) + { +#if UNITY_EDITOR + BMLogger.LogWarning($"[HOOK_DEBUG] Child model '{szHanger}' not found for NPC ID {nID}, hook '{szHook}'"); +#endif + break; + } + } + + // Get hook Transform (non-recursive search as per C++: GetSkeletonHook(szHook, true)) + // 获取挂点变换(非递归搜索,对应C++:GetSkeletonHook(szHook, true)) + Transform pHook = pModel.GetHook(szHook, false); + if (pHook == null) + { +#if UNITY_EDITOR + BMLogger.LogWarning($"[HOOK_DEBUG] Hook '{szHook}' not found for NPC ID {nID}, falling back to center position"); +#endif + break; + } + + // Get model transform for absolute offset calculation + // 获取模型变换用于绝对偏移计算 + Transform modelTransform = pModel.transform; + + // Calculate position based on relative/absolute offset + // 根据相对/绝对偏移计算位置 + vPos = HookUtils.GetHookWorldPosition(pHook, pOffset, bRelHook, modelTransform); + +#if UNITY_EDITOR + BMLogger.Log($"[HOOK_DEBUG] Found hook '{szHook}' for NPC ID {nID}, position={vPos}, relative={bRelHook}, offset={pOffset}"); +#endif + + return true; } if (HitPos == GfxHitPos.enumHitBottom) diff --git a/Assets/PerfectWorld/Scripts/NPC/CECModel.cs b/Assets/PerfectWorld/Scripts/NPC/CECModel.cs index 4b6a153a6d..270732874e 100644 --- a/Assets/PerfectWorld/Scripts/NPC/CECModel.cs +++ b/Assets/PerfectWorld/Scripts/NPC/CECModel.cs @@ -367,6 +367,7 @@ public class CECModel protected CECModelStaticData m_pMapModel; private SkeletonBuilder m_skeletonBuilder; private Dictionary m_hookCache = new Dictionary(); + private Dictionary m_childModels = new Dictionary(); private Transform m_transform; public void ClearComActFlag(bool bSignalCurrent) { ClearComActFlag(0, bSignalCurrent); } public void ClearComActFlag(int nChannel, bool bSignalCurrent) @@ -532,6 +533,51 @@ public class CECModel m_hookCache.Clear(); } + /// + /// Register a child model (weapon, pet, etc.) + /// 注册子模型(武器、宠物等) + /// + /// Hanger name / 挂载者名称 + /// Child model instance / 子模型实例 + public void RegisterChildModel(string hangerName, CECModel childModel) + { + if (!string.IsNullOrEmpty(hangerName) && childModel != null) + { + m_childModels[hangerName] = childModel; + BMLogger.Log($"[CECModel] Registered child model '{hangerName}'"); + } + } + + /// + /// Unregister a child model + /// 注销子模型 + /// + /// Hanger name / 挂载者名称 + public void UnregisterChildModel(string hangerName) + { + if (!string.IsNullOrEmpty(hangerName)) + { + if (m_childModels.Remove(hangerName)) + { + BMLogger.Log($"[CECModel] Unregistered child model '{hangerName}'"); + } + } + } + + /// + /// Get child model by hanger name + /// 根据挂载者名称获取子模型 + /// + /// Hanger name / 挂载者名称 + /// Child model or null if not found / 子模型,未找到返回null + public CECModel GetChildModel(string hangerName) + { + if (string.IsNullOrEmpty(hangerName)) + return null; + + return m_childModels.TryGetValue(hangerName, out CECModel child) ? child : null; + } + public void PlayGfx(string szPath, string szHook, float fScale, bool bFadeOut, A3DVECTOR3 vOffset, float fPitch, float fYaw, float fRot, bool bUseECMHook, uint dwFadeOutTime) { if (!bFadeOut) @@ -539,16 +585,34 @@ public class CECModel string strKey = szPath; strKey += szHook; + // Apply hook scale factor if using ECM hook + // 如果使用ECM挂点,应用挂点缩放因子 + if (bUseECMHook && !string.IsNullOrEmpty(szHook)) + { + Transform pHook = GetHook(szHook, true); + if (pHook != null) + { + // Apply hook scale factor if available + // 如果可用,应用挂点缩放因子 + // TODO Phase 4: Add hook scale factor support + // TODO 第4阶段:添加挂点缩放因子支持 + // fScale *= pHook->GetScaleFactor(); + +#if UNITY_EDITOR + BMLogger.Log($"[CECModel.PlayGfx] Using hook '{szHook}' for GFX '{szPath}', scale={fScale}"); +#endif + } + else + { +#if UNITY_EDITOR + BMLogger.LogWarning($"[CECModel.PlayGfx] Hook '{szHook}' not found for GFX '{szPath}', using default scale"); +#endif + } + } // PGFX_INFO pInfo; // // CoGfxMap::iterator it = m_CoGfxMap.find(strKey); - // if (bUseECMHook) { - // if (CECModelHook* pHook = GetECMHook(szHook)) - // { - // fScale *= pHook->GetScaleFactor(); - // } - // } // // if (it != m_CoGfxMap.end()) // { diff --git a/HOOK_SYSTEM_FLOW_C++_ANALYSIS.md b/HOOK_SYSTEM_FLOW_C++_ANALYSIS.md new file mode 100644 index 0000000000..0eabdf1b1f --- /dev/null +++ b/HOOK_SYSTEM_FLOW_C++_ANALYSIS.md @@ -0,0 +1,393 @@ +# Hook System Flow - C++ Analysis + +**Date Created:** 2026-02-24 +**Purpose:** Complete flow analysis of how player skill casting spawns GFX and uses hook system to attach GFX to correct bone positions + +--- + +## Complete Flow Diagram + +``` +1. Player Casts Skill + ↓ +2. CECAttacksMan::AddSkillAttack() [EC_ManAttacks.cpp:657] + ↓ +3. CECAttackEvent Created (with timing: m_timeToBeFired, m_timeToDoDamage) + ↓ +4. CECAttackEvent::Tick() [EC_ManAttacks.cpp:458] + - Updates m_timeLived + - When m_timeToBeFired <= 0 → calls DoFire() + ↓ +5. CECAttackEvent::DoFire() [EC_ManAttacks.cpp:111] + - For Player skills (m_idSkill != 0): + ↓ +6. A3DSkillGfxComposerMan::Play() [EC_ManAttacks.cpp:137-139] + - Finds composer by skill ID + ↓ +7. A3DSkillGfxComposer::Play() [A3DSkillGfxComposer2.cpp:453] + - Iterates through all targets + - For each target → calls AddOneTarget() + ↓ +8. A3DSkillGfxComposer::AddOneTarget() [A3DSkillGfxComposer2.cpp:388] + - Determines host/target based on TargetMode + - Calculates scale + - Calls AddSkillGfxEvent() + ↓ +9. A3DSkillGfxMan::AddSkillGfxEvent() [A3DSkillGfxEvent2.cpp:678] + - Creates multiple GFX events (clustering support) + - For each event → calls AddOneSkillGfxEvent() + ↓ +10. A3DSkillGfxMan::AddOneSkillGfxEvent() [A3DSkillGfxEvent2.cpp:603] + - Creates A3DSkillGfxEvent object + - Loads fly GFX and hit GFX + - Pushes event to update list + ↓ +11. CECSkillGfxEvent::Tick() [EC_ManSkillGfx.cpp:307] + - **THIS IS WHERE HOOK SYSTEM IS USED** + - Updates host and target positions using hooks + ↓ +12. _get_pos_by_id() [EC_ManSkillGfx.cpp:10] + - **HOOK LOOKUP AND POSITION CALCULATION** + - Gets hook from skeleton + - Calculates world position using hook transform + ↓ +13. A3DSkillGfxEvent::Tick() [A3DSkillGfxEvent2.cpp:487] + - State machine: Wait → Flying → Hit → Finished + - Updates fly GFX position/rotation each frame + - When hit → spawns hit GFX +``` + +--- + +## Detailed Hook System Usage + +### Step 11: CECSkillGfxEvent::Tick() - Hook Position Updates + +**File:** `EC_ManSkillGfx.cpp:307-373` + +```cpp +void CECSkillGfxEvent::Tick(DWORD dwDeltaTime) +{ + if (A3DSkillGfxComposer* pComposer = GetComposer()) + { + const SGC_POS_INFO *pHostPos, *pTargetPos; + + // Determine which position info to use (reverse mode support) + if (m_pMoveMethod->IsReverse()) + { + pHostPos = &m_pComposer->m_FlyEndPos; // Target becomes host + pTargetPos = &m_pComposer->m_FlyPos; // Host becomes target + } + else + { + pHostPos = &m_pComposer->m_FlyPos; // Normal: host is caster + pTargetPos = &m_pComposer->m_FlyEndPos; // Normal: target is receiver + } + + // UPDATE HOST POSITION USING HOOK + m_bHostExist = _get_pos_by_id( + m_pPlayerMan, + m_pNPCMan, + m_nHostID, + m_vHostPos, // Output: world position + pHostPos->HitPos, // HitPos enum (enumHitCenter, enumHitBottom) + m_bIsGoblinSkill, + pHostPos->szHook, // Hook name (e.g., "hand_r", "weapon") + pHostPos->bRelHook, // Relative offset flag + &pHostPos->vOffset, // Offset vector + pHostPos->szHanger, // Child model name (weapon, pet) + pHostPos->bChildHook); // Use child model flag + + // UPDATE TARGET POSITION USING HOOK + m_bTargetExist = _get_pos_by_id( + m_pPlayerMan, + m_pNPCMan, + m_nTargetID, + m_vTargetPos, // Output: world position + pTargetPos->HitPos, + false, + pTargetPos->szHook, // Hook name for target + pTargetPos->bRelHook, + &pTargetPos->vOffset, + pTargetPos->szHanger, + pTargetPos->bChildHook); + + // Get target direction/up for orientation + m_bTargetDirAndUpExist = _get_dir_and_up_by_id( + m_pPlayerMan, m_pNPCMan, m_nTargetID, + m_vTargetDir, m_vTargetUp); + } + else + { + // Fallback: no composer, use default positions + m_bHostExist = _get_pos_by_id(..., m_pMoveMethod->GetHitPos(), ...); + m_bTargetExist = _get_pos_by_id(..., m_pMoveMethod->GetHitPos()); + } + + // Call base class tick (handles movement and GFX updates) + A3DSkillGfxEvent::Tick(dwDeltaTime); +} +``` + +**Key Points:** +- Hook information comes from `SGC_POS_INFO` structure in composer +- `SGC_POS_INFO` contains: `szHook`, `vOffset`, `bRelHook`, `szHanger`, `bChildHook`, `HitPos` +- Position is updated **every frame** during GFX event lifetime + +--- + +### Step 12: _get_pos_by_id() - Hook Lookup and Position Calculation + +**File:** `EC_ManSkillGfx.cpp:10-122` + +#### Player Branch (Lines 24-87) + +```cpp +if (ISPLAYERID(nID)) +{ + CECPlayer* pPlayer = pPlayerMan->GetPlayer(nID); + if (pPlayer) + { + // Hook lookup loop (only if szHook is provided) + while (1) + { + if (!szHook) + break; // No hook specified, use default position + + // 1. Get player model + CECModel* pModel = pPlayer->GetPlayerModel(); + if (!pModel) + break; + + // 2. Handle child model (weapon/pet) if specified + if (szHanger && bChildHook) + pModel = pModel->GetChildModel(szHanger); + + if (!pModel) + break; + + // 3. Get skeleton from model + A3DSkinModel* pSkin = pModel->GetA3DSkinModel(); + + // 4. LOOKUP HOOK BY NAME (non-recursive) + A3DSkeletonHook* pHook = pSkin->GetSkeletonHook(szHook, true); // true = non-recursive + + if (!pHook) + break; + + // 5. CALCULATE POSITION USING HOOK TRANSFORM + if (bRelHook) + { + // Relative offset: transform offset in hook's local space + // C++: pHook->GetAbsoluteTM() * (*pOffset) + vPos = pHook->GetAbsoluteTM() * (*pOffset); + } + else + { + // Absolute offset: transform offset in model space, then translate to hook + // C++: vPos = pSkin->GetAbsoluteTM() * (*pOffset); + // vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); + vPos = pSkin->GetAbsoluteTM() * (*pOffset); + vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); + } + + return true; // Success: position calculated from hook + } + + // Fallback: No hook or hook lookup failed, use default position + if (HitPos == enumHitBottom) + vPos = pPlayer->GetPos(); + else + { + const A3DAABB& aabb = pPlayer->GetPlayerAABB(); + vPos = aabb.Center; + vPos.y += aabb.Extents.y * .5f; + } + } +} +``` + +#### NPC Branch (Lines 89-118) + +```cpp +else if (ISNPCID(nID)) +{ + CECNPC* pNPC = pNPCMan->GetNPCFromAll(nID); + if (pNPC) + { + while (true) + { + // NPC hook lookup (handles child models internally) + A3DSkeletonHook* pHook = pNPC->GetSgcHook(szHanger, bChildHook, szHook); + if (!pHook) + break; + + A3DSkinModel* pSkin = pNPC->GetSgcSkinModel(szHanger, bChildHook, szHook); + + // Same position calculation as player + if (bRelHook) + vPos = pHook->GetAbsoluteTM() * (*pOffset); + else + { + vPos = pSkin->GetAbsoluteTM() * (*pOffset); + vPos = vPos - pSkin->GetAbsoluteTM().GetRow(3) + pHook->GetAbsoluteTM().GetRow(3); + } + return true; + } + + // Fallback: default position + if (HitPos == enumHitBottom) + vPos = pNPC->GetPos(); + else + { + const A3DAABB& aabb = pNPC->GetPickAABB(); + vPos = aabb.Center; + vPos.y += aabb.Extents.y * .5f; + } + } +} +``` + +**Key Points:** +- Hook lookup is **optional** - if `szHook` is NULL or lookup fails, uses default position +- `bRelHook = true`: Offset is in hook's local space (rotates with hook) +- `bRelHook = false`: Offset is in model space, then translated to hook position +- `bChildHook = true`: Searches in child model (weapon/pet) instead of main model +- `szHanger`: Name of child model to search (e.g., "weapon", "pet") + +--- + +## Hook Data Source: SGC_POS_INFO + +**File:** `A3DSkillGfxComposer2.cpp:43-74` + +The hook information is loaded from skill GFX composer files (`.sgc` files): + +```cpp +struct SGC_POS_INFO +{ + char szHook[64]; // Hook name (e.g., "hand_r", "weapon", "head") + A3DVECTOR3 vOffset; // Offset from hook position + bool bRelHook; // Relative offset flag + char szHanger[64]; // Child model name (weapon, pet) + bool bChildHook; // Use child model flag + GfxHitPos HitPos; // Fallback hit position enum +}; + +// Loaded from .sgc file: +_load_sgc_pos(dwVersion, pFile, szLine, m_FlyPos); // Host position +_load_sgc_pos(dwVersion, pFile, szLine, m_FlyEndPos); // Target position +_load_sgc_pos(dwVersion, pFile, szLine, m_HitPos); // Hit position +``` + +**Example .sgc file content:** +``` +Hook: hand_r # Hook name for fly GFX spawn position +Pos: 0.0, 0.0, 0.0 # Offset from hook +RelHook: 1 # Use relative offset +Hanger: weapon # Child model name (optional) +ChildHook: 1 # Search in child model +HitPos: 0 # Fallback hit position enum +``` + +--- + +## GFX Position Updates During Flight + +**File:** `A3DSkillGfxEvent2.cpp:487-568` + +After hook positions are calculated, the GFX event updates the fly GFX transform: + +```cpp +void A3DSkillGfxEvent::Tick(DWORD dwDeltaTime) +{ + // ... state machine logic ... + + if (m_enumState == enumFlying) + { + // Update movement + if (m_pMoveMethod->TickMove(dwDeltaTime, m_vHostPos, m_vTargetPos)) + HitTarget(GetTargetCenter()); // Target hit + else if (m_pFlyGfx) + { + // UPDATE FLY GFX TRANSFORM + A3DVECTOR3 vDir, vUp; + + if (m_pMoveMethod->GetMode() == enumOnTarget && + m_pMoveMethod->IsReverse() && + GetTargetDirAndUp(vDir, vUp)) + { + // Use target's direction/up for orientation + m_pFlyGfx->SetParentTM(a3d_TransformMatrix(vDir, vUp, m_pMoveMethod->GetPos())); + } + else + { + // Use movement direction for orientation + m_pFlyGfx->SetParentTM(_build_matrix(m_pMoveMethod->GetMoveDir(), m_pMoveMethod->GetPos())); + } + + m_pMoveMethod->UpdateGfxParam(m_pFlyGfx, m_vHostPos, m_vTargetPos); + m_pFlyGfx->TickAnimation(dwDeltaTime); + } + } +} +``` + +**Key Points:** +- `m_vHostPos` and `m_vTargetPos` are updated **every frame** via `CECSkillGfxEvent::Tick()` +- Fly GFX position is calculated by movement method (`m_pMoveMethod->GetPos()`) +- Fly GFX rotation uses movement direction or target's direction/up +- Host position (`m_vHostPos`) comes from hook lookup (if hook specified) + +--- + +## Summary: Hook System Integration Points + +1. **Configuration:** Hook info stored in `.sgc` files (composer files) + - `FlyPos`: Hook for fly GFX spawn position (host) + - `FlyEndPos`: Hook for fly GFX target position (target) + - `HitPos`: Hook for hit GFX spawn position + +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) + - Calculates world position using hook transform matrix + +3. **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. **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) + +--- + +## C++ to C# Conversion Notes + +### Key Functions to Convert: + +1. **`_get_pos_by_id()`** → `HookUtils.GetHookWorldPosition()` + - Already planned in conversion documents + - Needs model transform parameter for absolute offset + +2. **`CECSkillGfxEvent::Tick()`** → `CECSkillGfxEvent.Tick()` + - Update positions every frame using hook system + - Pass composer's `SGC_POS_INFO` to hook lookup + +3. **`A3DSkeletonHook::GetAbsoluteTM()`** → `Transform` (Unity) + - C++ returns matrix, Unity uses Transform component + - Hook transforms are already stored in `SkeletonBuilder` + +### Data Structures: + +- `SGC_POS_INFO` → `SgcPosInfo` (C# struct) +- Hook names come from `.sgc` file parsing +- Hook lookup uses `SkeletonBuilder.GetHook(name, recursive)` + +--- + +**End of Document**