fix cast skill while flashing move

This commit is contained in:
VDH
2026-03-05 17:39:11 +07:00
parent 52ac4d4b39
commit 3df4ce90de
14 changed files with 1093 additions and 239 deletions
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:57bb52418b44eb7e3e333915973057be091b0826983b641ceb05263b4dffe2e4
size 287382
oid sha256:20cec4d7a902084d6e87ff1de8301f79123189bb3c8f06210396aab2d1b1d7ac
size 291045
@@ -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();
}
/// <summary>
/// Toggle auto-add wrath feature (calls OnBtnAddWrathClicked every 3 seconds)
/// 切换自动添加怒气功能(每3秒调用OnBtnAddWrathClicked
/// </summary>
public void ToggleAutoAddWrath()
{
_isAutoAddWrathEnabled = !_isAutoAddWrathEnabled;
@@ -116,10 +113,6 @@ namespace BrewMonster.Scripts
}
}
/// <summary>
/// Coroutine that calls OnBtnAddWrathClicked every 3 seconds
/// 每3秒调用OnBtnAddWrathClicked的协程
/// </summary>
private IEnumerator AutoAddWrathCoroutine()
{
while (_isAutoAddWrathEnabled)
@@ -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)
{
@@ -1520,3 +1520,8 @@ public enum GfxSkillValType
@@ -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
/// </summary>
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);
}
/// <summary>
@@ -333,6 +348,27 @@ namespace BrewMonster
m_flyGfxInstance.transform.rotation = Quaternion.LookRotation(dir);
}
/// <summary>
/// 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++逻辑匹配:命中特效位置每帧更新以跟随目标
/// </summary>
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
// 如果目标不存在,命中特效保持在最后已知位置
}
/// <summary>
/// Destroy fly GFX instance
/// 销毁飞行特效实例
@@ -352,13 +388,13 @@ namespace BrewMonster
/// </summary>
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);
}
/// <summary>
@@ -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
@@ -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
@@ -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
+182 -9
View File
@@ -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<int> m_aCurEffects = new List<int>(); // 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; // 扛旗者IDbFlagCarrier为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<ScoreRankEntry> OffenseRanks; // Offense ranks / 攻击方排行
public List<ScoreRankEntry> DefenceRanks; // Defence ranks / 防守方排行
// Death containers (using List instead of std::vector) / 死亡容器(使用List代替std::vector
public List<DeathEntry> OffenseDeaths; // Offense deaths / 攻击方死亡
public List<DeathEntry> 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<ScoreRankEntry>();
if (DefenceRanks == null)
DefenceRanks = new List<ScoreRankEntry>();
if (OffenseDeaths == null)
OffenseDeaths = new List<DeathEntry>();
if (DefenceDeaths == null)
DefenceDeaths = new List<DeathEntry>();
}
// 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
{
@@ -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);
+4
View File
@@ -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())
+201 -171
View File
@@ -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<CSNetwork.GPDataType.OBJECT_COORD>;
@@ -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;
}
View File
+162 -7
View File
@@ -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<A3DSkeletonHook*>)
- 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<A3DSkeletonHook*> 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
+434
View File
@@ -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<CECModel>();
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**