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**