diff --git a/Assets/PerfectWorld/Scripts/MainFiles/EC_Game.cs b/Assets/PerfectWorld/Scripts/MainFiles/EC_Game.cs index adcbc3d2d3..409d1d3335 100644 --- a/Assets/PerfectWorld/Scripts/MainFiles/EC_Game.cs +++ b/Assets/PerfectWorld/Scripts/MainFiles/EC_Game.cs @@ -4,6 +4,7 @@ using CSNetwork; using ModelRenderer.Scripts.GameData; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using UnityEngine; using UnityEngine.AddressableAssets; @@ -114,6 +115,10 @@ namespace BrewMonster.Network GetGameRun().Init(); InitializeStringTables(); + // Load coord_data.txt (C++: Configs/Coord_data.txt) for clickable task links auto-move. + // 加载 coord_data.txt(C++:Configs/Coord_data.txt)用于任务可点击链接的自动移动。 + LoadObjectCoord(); + return true; } public static CECConfigs GetConfigs() { return m_pConfigs; } @@ -262,6 +267,140 @@ namespace BrewMonster.Network long unixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); return (int)unixTime + m_iTimeError; } + + //////////////////////////////////////////////////////////////////////////////// + // + // Coord_data.txt support (C++: Configs/Coord_data.txt, CECGame::LoadObjectCoord/GetObjectCoord) + // + //////////////////////////////////////////////////////////////////////////////// + + private struct OBJECT_COORD + { + public string strMap; + public Vector3 vPos; + } + + private static readonly Dictionary> m_CoordTab = + new Dictionary>(StringComparer.OrdinalIgnoreCase); + + private static bool m_bCoordLoaded = false; + + public static bool LoadObjectCoord() + { + if (m_bCoordLoaded) + { + return true; + } + + try + { + Addressables.InitializeAsync().WaitForCompletion(); + var ta = Addressables.LoadAssetAsync("Assets/Addressable/coord_data.txt").WaitForCompletion(); + if (ta == null) + { + Debug.LogError("[EC_Game] LoadObjectCoord: failed to load Addressable 'Assets/Addressable/coord_data.txt'"); + return false; + } + + ParseCoordDataText(ta.text); + m_bCoordLoaded = true; + Debug.Log($"[EC_Game] LoadObjectCoord: loaded {m_CoordTab.Count} coord keys from coord_data.txt"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"[EC_Game] LoadObjectCoord exception: {ex}"); + return false; + } + } + + private static void ParseCoordDataText(string text) + { + m_CoordTab.Clear(); + if (string.IsNullOrEmpty(text)) + { + return; + } + + using var sr = new StringReader(text); + string line; + int lineNo = 0; + + while ((line = sr.ReadLine()) != null) + { + lineNo++; + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + line = line.Trim(); + if (line.StartsWith("#", StringComparison.Ordinal) || line.StartsWith("//", StringComparison.Ordinal)) + { + continue; + } + + if (lineNo == 1 && line.StartsWith("ID", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string[] parts = line.Split((char[])null, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 5) + { + continue; + } + + string key = parts[0]; + string map = parts[1]; + + if (!float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out float x) || + !float.TryParse(parts[3], NumberStyles.Float, CultureInfo.InvariantCulture, out float y) || + !float.TryParse(parts[4], NumberStyles.Float, CultureInfo.InvariantCulture, out float z)) + { + continue; + } + + var coord = new OBJECT_COORD + { + strMap = map, + vPos = new Vector3(x, y, z), + }; + + if (!m_CoordTab.TryGetValue(key, out var list)) + { + list = new List(1); + m_CoordTab[key] = list; + } + + list.Add(coord); + } + } + + public static bool TryGetFirstObjectCoord(string targetId, out Vector3 pos, out string map) + { + pos = default; + map = null; + + if (string.IsNullOrWhiteSpace(targetId)) + { + return false; + } + + if (!m_bCoordLoaded) + { + LoadObjectCoord(); + } + + if (!m_CoordTab.TryGetValue(targetId, out var list) || list == null || list.Count == 0) + { + return false; + } + + pos = list[0].vPos; + map = list[0].strMap; + return true; + } #endregion } } \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs b/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs index 01f82e888a..4539c8ad88 100644 --- a/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs +++ b/Assets/PerfectWorld/Scripts/Managers/CECNPCMan.cs @@ -502,6 +502,25 @@ public class CECNPCMan : IMsgHandler return npc; } + + // Find first NPC/Monster by template id (tid). Used by UI auto-move coordinate resolving. + // 通过模板ID(tid)查找第一个NPC/怪物。用于UI自动寻路坐标解析。 + public CECNPC FindNPCByTemplateID(int tid) + { + if (tid == 0) return null; + + foreach (var npc in m_NPCTab.Values) + { + if (!npc) continue; + var info = npc.GetNPCInfo(); + if (info.tid == tid || info.vis_tid == tid) + { + return npc; + } + } + + return null; + } public CECNPC GetNPCFromAll(int nid) { CECNPC pNPC = GetNPC(nid); diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_HPWork.cs b/Assets/PerfectWorld/Scripts/Managers/EC_HPWork.cs index 367f7042a7..56541b4296 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_HPWork.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_HPWork.cs @@ -586,7 +586,7 @@ namespace BrewMonster.Scripts case CECHPWork.Host_work_ID.WORK_PASSIVEMOVE: pWork = new CECHPWorkPassiveMove(this); break; //case CECHPWork.Host_work_ID.WORK_CONGREGATE: pWork = new CECHPWorkCongregate(this); break; //case CECHPWork.Host_work_ID.WORK_SKILLSTATEACT: pWork = new CECHPWorkSkillStateAction(this); break; - //case CECHPWork.Host_work_ID.WORK_FORCENAVIGATEMOVE: pWork = new CECHPWorkNavigate(this); break; + case CECHPWork.Host_work_ID.WORK_FORCENAVIGATEMOVE: pWork = new CECHPWorkNavigate(this); break; default: return null; } diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs new file mode 100644 index 0000000000..fe67b7a2f5 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs @@ -0,0 +1,1385 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using UnityEngine; +using CSNetwork.GPDataType; +using BrewMonster.Scripts.Task; +using BrewMonster.Network; +using UnityEngine.AddressableAssets; + +namespace BrewMonster.Scripts +{ + ////////////////////////////////////////////////////////////////////////// + // + // Helper functions for vector/yaw calculations + // + ////////////////////////////////////////////////////////////////////////// + + // Helper class for navigation calculations // 导航计算辅助类 + public static class NavigateHelper + { + // Convert vector to yaw angle // 将向量转换为偏航角 + public static float glb_VectorToYaw(A3DVECTOR3 v) + { + A3DVECTOR3 v1 = v; + v1.y = 0.0f; + v1.Normalize(); + return Mathf.Rad2Deg * (float)Mathf.Atan2(v1.z, v1.x); + } + + // Convert yaw to vector // 将偏航角转换为向量 + public static A3DVECTOR3 glb_YawToVector(float fYaw) + { + float fRad = Mathf.Deg2Rad * fYaw; + + A3DVECTOR3 v; + v.x = 10.0f * Mathf.Cos(fRad); + v.z = 10.0f * Mathf.Sin(fRad); + v.y = 0.0f; + v.Normalize(); + + return v; + } + + // Clamp yaw to [-180.0f, 180.0f] // 将偏航角限制在 [-180.0f, 180.0f] 范围内 + public static void glb_ClampYaw(ref float fYaw) + { + if (fYaw >= -180.0f && fYaw <= 180.0f) + { + // Below clamp calculation will bring little error to original number, this will + // cause problem in yaw's equal comparing. So if original yaw has been in valid + // range, return directly. + // 下面的限制计算会给原始数字带来小的误差,这会在偏航角的相等比较中造成问题。 + // 所以如果原始偏航角已经在有效范围内,直接返回。 + return; + } + + float fInv = 1.0f / 360.0f; + + fYaw += 180.0f; + if (fYaw < 0.0f) + { + int n = (int)(-fYaw * fInv) + 1; + fYaw = fYaw + 360.0f * n; + } + else + { + int n = (int)(fYaw * fInv); + fYaw = fYaw - 360.0f * n; + } + + fYaw -= 180.0f; + UnityEngine.Debug.Assert(fYaw >= -180.0f && fYaw <= 180.0f); + } + + // Get pitch from vector // 从向量获取俯仰角 + public static float glb_GetPitch(A3DVECTOR3 v) + { + return Mathf.Rad2Deg * Mathf.Acos(v.y); + } + + // Rotate position around axis // 围绕轴旋转位置 + public static A3DVECTOR3 a3d_RotatePosAroundAxis(A3DVECTOR3 vPos, A3DVECTOR3 vAxis, float fRad) + { + // C++: a3d_RotatePosAroundAxis uses A3DMATRIX4 RotateAxis(axis, rad) then matrix * v + // 原版:a3d_RotatePosAroundAxis 使用 A3DMATRIX4 RotateAxis(axis, rad) 然后 matrix * v + Vector3 axis = new Vector3(vAxis.x, vAxis.y, vAxis.z); + if (axis.sqrMagnitude < 1e-12f) + { + return vPos; + } + + Quaternion rotation = Quaternion.AngleAxis(Mathf.Rad2Deg * fRad, axis.normalized); + Vector3 result = rotation * new Vector3(vPos.x, vPos.y, vPos.z); + return new A3DVECTOR3(result.x, result.y, result.z); + } + } + + ////////////////////////////////////////////////////////////////////////// + // + // CECNavigateCtrl class + // + ////////////////////////////////////////////////////////////////////////// + + public class CECNavigateCtrl + { + public enum NavigateEvent + { + EM_PREPARE = 0, + EM_BEGIN = 1, + EM_END = 2, + } + + // Navigation info structure // 导航信息结构 + public struct INFO + { + public int taskID; + public int bezierID; + public float speed; + public float angleWithH; + public bool bezierDir; // false: fix, true: bezier // false: 固定, true: 贝塞尔 + public string strModelPath; + } + + // Config info list, vector type // 配置信息列表,vector类型 + private List m_configInfo = new List(); + private CECHostPlayer m_pHost; + + // Bezier walker // 贝塞尔行走器 + private CECBezierWalker m_pBezierWalker; + // Force navigate state // 强制导航状态 + private bool m_bForceNavigateState; + // Current force navigate info // 当前强制导航信息 + private INFO m_curNavigateInfo; + // Corresponding task ID // 对应的任务ID + private int m_taskID; + + public CECNavigateCtrl(CECHostPlayer pHost) + { + m_pHost = pHost; + m_pBezierWalker = null; + m_bForceNavigateState = false; + m_taskID = 0; + } + + // Load configuration // 加载配置 + public bool LoadConfig(string szFile) + { + try + { + string filePath = szFile; + + // Handle Unity asset paths (relative to Assets folder) // 处理Unity资源路径(相对于Assets文件夹) + if (filePath.StartsWith("Assets/")) + { + // Convert to absolute path // 转换为绝对路径 + string projectPath = Application.dataPath.Replace("/Assets", ""); + filePath = Path.Combine(projectPath, filePath); + } + // Handle relative paths from Application.dataPath // 处理相对于Application.dataPath的相对路径 + else if (!Path.IsPathRooted(filePath)) + { + filePath = Path.Combine(Application.dataPath, filePath); + } + + if (!File.Exists(filePath)) + { + Debug.LogError($"CECNavigateCtrl::LoadConfig, failed to open file {filePath} (original: {szFile})"); + return false; + } + + m_configInfo.Clear(); + int lineNumber = 0; + using (StreamReader reader = new StreamReader(filePath)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + lineNumber++; + // Skip empty lines and comments + line = line.Trim(); + if (string.IsNullOrEmpty(line) || line.StartsWith("//")) + continue; + + // C++: CSplit(sf.m_szToken).Split(",") and require >= 6 tokens + // 原版:CSplit(sf.m_szToken).Split(",") 并要求 >= 6 个字段 + string[] parts = line.Split(','); + if (parts.Length < 6) + { + // C++: ASSERT(0); sf.Close(); return false; + // 原版:ASSERT(0); 关闭文件并返回 false + return false; + } + + INFO navi = new INFO(); + navi.taskID = int.Parse(parts[0]); + navi.bezierID = int.Parse(parts[1]); + navi.speed = float.Parse(parts[2]); + navi.angleWithH = float.Parse(parts[3]); + navi.bezierDir = (int.Parse(parts[4]) == 1); + navi.strModelPath = parts[5]; + m_configInfo.Add(navi); + } + } + + return true; + } + catch (Exception) + { + return false; + } + } + + // Get navigation info // 获取导航信息 + public bool GetNavigateInfo(int task, ref INFO info) + { + if (task <= 0) return false; + for (int i = 0; i < m_configInfo.Count; i++) + { + if (m_configInfo[i].taskID == task) + { + info = m_configInfo[i]; + return true; + } + } + return false; + } + + // Prepare navigation // 准备导航 + public void OnPrepareNavigate(int task) + { + m_bForceNavigateState = true; + m_taskID = task; + + // TODO: Implement CECUIHelper.GetGameUIMan().SetShowAllPanlesFlag(false); + // CECUIHelper::GetGameUIMan()->SetShowAllPanlesFlag(false); + + INFO naviInfo = new INFO(); + if (GetNavigateInfo(task, ref naviInfo)) + { + m_curNavigateInfo = naviInfo; + + // Set navigate model file // 设置导航模型文件 + CECHostNavigatePlayer player = m_pHost != null ? m_pHost.GetNavigatePlayer() : null; + if (player != null) + { + player.SetNavigateModelFile(m_curNavigateInfo.strModelPath); + player.Init(); + } + } + else + { + // Give up task if no navigation info found // 如果找不到导航信息则放弃任务 + if (m_pHost != null && m_pHost.GetTaskInterface() != null) + { + m_pHost.GetTaskInterface().GiveUpTask((uint)m_taskID); + } + } + } + + // Begin navigation // 开始导航 + public void OnBeginNavigate() + { + // C++: + // CECScene* pScene = g_pGame->GetGameRun()->GetWorld()->GetScene(); + // CECBezier* pBezier = dynamic_cast(pScene->GetBezierObjectByGlobalID(m_curNavigateInfo.bezierID)); + // if (!pBezier) { GiveUpTask; return; } + // + // 原版: + // 从Scene按GlobalID取Bezier;取不到则放弃任务并返回 + CECBezier pBezier = CECBezierNavigateLoader.GetBezierObjectByGlobalID(m_curNavigateInfo.bezierID); + if (pBezier == null) + { + if (m_pHost != null && m_pHost.GetTaskInterface() != null) + { + m_pHost.GetTaskInterface().GiveUpTask((uint)m_taskID); + } + return; + } + + if (m_pBezierWalker == null) + { + m_pBezierWalker = new CECBezierWalker(); + } + + // Bind bezier and start walking // 绑定贝塞尔曲线并开始行走 + m_pBezierWalker.BindBezier(pBezier); + m_pBezierWalker.SetSpeed(m_curNavigateInfo.speed); + m_pBezierWalker.StartWalk(false, true); + + // Create and start work // 创建并开始工作 + if (m_pHost != null && m_pHost.GetWorkMan() != null) + { + CECHPWorkNavigate pWork = m_pHost.GetWorkMan().CreateWork(CECHPWork.Host_work_ID.WORK_FORCENAVIGATEMOVE) as CECHPWorkNavigate; + if (pWork != null) + { + pWork.BeginNavigate(); + m_pHost.GetWorkMan().StartWork_p2(pWork); + } + } + } + + // End navigation // 结束导航 + public void OnEndNavigate() + { + if (!m_bForceNavigateState) return; + m_taskID = 0; + // Finish running work // 完成运行中的工作 + if (m_pHost != null && m_pHost.GetWorkMan() != null) + { + m_pHost.GetWorkMan().FinishRunningWork(CECHPWork.Host_work_ID.WORK_FORCENAVIGATEMOVE); + } + // TODO: Implement UI helper + // CECUIHelper::GetGameUIMan()->SetShowAllPanlesFlag(true); + m_bForceNavigateState = false; + } + + // Check if in force navigate state // 检查是否在强制导航状态 + public bool IsInForceNavigateState() + { + return m_bForceNavigateState; + } + + // Get bezier walker // 获取贝塞尔行走器 + public CECBezierWalker GetBezierWalker() + { + return m_pBezierWalker; + } + + // Get current navigate info // 获取当前导航信息 + public INFO GetCurrentNavigateInfo() + { + return m_curNavigateInfo; + } + } + + ////////////////////////////////////////////////////////////////////////// + // + // CECHPWorkNavigate class + // + ////////////////////////////////////////////////////////////////////////// + + public class CECHPWorkNavigate : CECHPWork + { + // Move flag // 移动标志 + private bool m_bMove; + // Move speed // 移动速度 + private float m_fSpeed; + + public CECHPWorkNavigate(CECHPWorkMan pWorkMan) : base(Host_work_ID.WORK_FORCENAVIGATEMOVE, pWorkMan) + { + m_dwMask = Work_mask.MASK_FORCENAVIGATE; + m_dwTransMask = Work_mask.MASK_STAND; + Reset(); + } + + // Reset work // 重置工作 + public override void Reset() + { + base.Reset(); + m_bMove = false; + } + + // Copy work data // 复制工作数据 + public override bool CopyData(CECHPWork pWork) + { + if (!base.CopyData(pWork)) + return false; + + CECHPWorkNavigate pSrc = pWork as CECHPWorkNavigate; + if (pSrc == null) + return false; + + m_fSpeed = pSrc.m_fSpeed; + m_bMove = pSrc.m_bMove; + + return true; + } + + // Begin navigation // 开始导航 + public void BeginNavigate() + { + Debug.Log($"[CECHPWorkNavigate] BeginNavigate: Setting m_bMove=true"); + m_bMove = true; + } + + // Stop move // 停止移动 + public void Finish() + { + Reset(); + } + + // On first tick // 第一次tick时 + protected override void OnFirstTick() + { + // TODO: Implement GetNavigatePlayer + // A3DVECTOR3 dir = m_pHost->GetNavigatePlayer()->GetDir(); + // dir.Normalize(); + + CECHostNavigatePlayer pNavigatePlayer = m_pHost.GetNavigatePlayer(); + if (pNavigatePlayer == null) + { + return; + } + + A3DVECTOR3 dir = pNavigatePlayer.GetDir(); + dir.Normalize(); + + // TODO: Implement GetNavigateCtrl + // CECNavigateCtrl* pNaviCtrl = m_pHost->GetNavigatePlayer()->GetNavigateCtrl(); + + CECNavigateCtrl pNaviCtrl = pNavigatePlayer.GetNavigateCtrl(); + if (pNaviCtrl == null) + { + return; + } + + CECNavigateCtrl.INFO naviInfo = pNaviCtrl.GetCurrentNavigateInfo(); + + A3DVECTOR3 vRight = A3DVECTOR3.CrossProduct(GPDataTypeHelper.g_vAxisY, dir); + dir = NavigateHelper.a3d_RotatePosAroundAxis(dir, vRight, Mathf.Deg2Rad * naviInfo.angleWithH); + + float fDesYaw = NavigateHelper.glb_VectorToYaw(dir); + NavigateHelper.glb_ClampYaw(ref fDesYaw); + + // TODO: Implement camera control + // m_pHost->GetCameraCtrl()->SetDgree(90.f-fDesYaw); + + float fDesPitch = NavigateHelper.glb_GetPitch(dir); + // TODO: Implement camera control + // m_pHost->GetCameraCtrl()->SetPitch(90.f-fDesPitch); + } + + // Tick routine // Tick例程 + public override bool Tick(float dwDeltaTime) + { + // TODO: Implement GetNavigateCtrl + // CECNavigateCtrl* pNaviCtrl = m_pHost->GetNavigatePlayer()->GetNavigateCtrl(); + // CECBezierWalker* pBezierWalker = pNaviCtrl->GetBezierWalker(); + + CECHostNavigatePlayer pNavigatePlayer = m_pHost.GetNavigatePlayer(); + CECNavigateCtrl pNaviCtrl = pNavigatePlayer != null ? pNavigatePlayer.GetNavigateCtrl() : null; + CECBezierWalker pBezierWalker = pNaviCtrl != null ? pNaviCtrl.GetBezierWalker() : null; + + if (!m_bMove || pBezierWalker == null) return true; + + // C++: DWORD dwRealTime = g_pGame->GetRealTickTime(); // ms delta + // 原版:DWORD dwRealTime = g_pGame->GetRealTickTime(); // 帧间毫秒差 + int dwRealTime = (int)EC_Game.GetRealTickTime(); + + // TODO: Implement IsWalking + // if (pBezierWalker->IsWalking()) { + if (pBezierWalker.IsWalking()) + { + pBezierWalker.Tick(dwRealTime); + + // Get updated position from bezier walker // 从贝塞尔行走器获取更新的位置 + A3DVECTOR3 vCurPos = pBezierWalker.GetPos(); + + // Get direction from bezier walker // 从贝塞尔行走器获取方向 + A3DVECTOR3 vDir = pBezierWalker.GetDir(); + vDir.Normalize(); + + A3DVECTOR3 vRight = A3DVECTOR3.CrossProduct(GPDataTypeHelper.g_vAxisY, vDir); + A3DVECTOR3 vUp = A3DVECTOR3.CrossProduct(vDir, vRight); + + A3DVECTOR3 vDirH = A3DVECTOR3.CrossProduct(vRight, GPDataTypeHelper.g_vAxisY); + vUp.Normalize(); + + // TODO: Implement GetNavigatePlayer + // CECHostNavigatePlayer* pClone = m_pHost->GetNavigatePlayer(); + CECHostNavigatePlayer pClone = m_pHost.GetNavigatePlayer(); + if (pClone != null) + { + pClone.SetPos(vCurPos); + + if(pNaviCtrl.GetCurrentNavigateInfo().bezierDir) + { + pClone.ChangeModelMoveDirAndUp(vDir, vUp); + } + else + { + pClone.ChangeModelMoveDirAndUp(vDirH, GPDataTypeHelper.g_vAxisY); + } + } + } + else + { + if (m_bMove) + { + m_bMove = false; + } + else + { + UnityEngine.Debug.Assert(false); + } + + // Set finish flag // 设置完成标志 + if (m_pHost != null && m_pHost.GetTaskInterface() != null) + { + m_pHost.GetTaskInterface().SetForceNavigateFinishFlag(true); + } + } + + base.Tick(dwDeltaTime); + + return true; + } + } + + ////////////////////////////////////////////////////////////////////////// + // + // Forward declarations / Stub classes for dependencies + // + ////////////////////////////////////////////////////////////////////////// + + // CECBezierWalker class - Converted from C++ // CECBezierWalker类 - 从C++转换 + public class CECBezierWalker + { + private CECBezier m_pBezier; // Bezier route data // 贝塞尔路径数据 + private bool m_bForward; // Forward flag // 前进标志 + private float m_fSpeed; // Moving speed // 移动速度 + private int m_iTotalTime; // Total time of whole bezier route // 整个贝塞尔路径的总时间 + private int m_iTimeCnt; // Time counter // 时间计数器 + private int m_iCurSeg; // Current segment // 当前段 + private int m_iCurSegTime; // Total time of current segment // 当前段的总时间 + private int m_iPassSegTime; // Total time of passed segments // 已通过段的总时间 + private bool m_bLoop; // Loop flag // 循环标志 + private bool m_bWalking; // true, is walking // true,正在行走 + private bool m_bPause; // Pause flag // 暂停标志 + private bool m_bForwardStop; // Stop flag // 停止标志 + public CECBezierWalker() + { + m_pBezier = null; + m_fSpeed = 1.0f; + m_iTotalTime = 0; + m_iTimeCnt = 0; + m_bForward = true; + m_iCurSeg = 0; + m_iCurSegTime = 0; + m_iPassSegTime = 0; + m_bLoop = true; + m_bWalking = false; + m_bPause = false; + m_bForwardStop = false; + } + + // Bind bezier route // 绑定贝塞尔路径 + public bool BindBezier(CECBezier pBezier) + { + m_pBezier = pBezier; + + // Stop at default position // 在默认位置停止 + m_iTimeCnt = 0; + m_iCurSeg = 0; + m_iPassSegTime = 0; + m_bForwardStop = false; + m_bWalking = false; + m_bPause = false; + + return true; + } + + // Start walk // 开始行走 + public bool StartWalk(bool bLoop, bool bForward) + { + if (m_pBezier == null) + { + return false; + } + + m_bLoop = bLoop; + m_bForward = bForward; + m_iTimeCnt = 0; + m_iCurSeg = bForward ? 0 : m_pBezier.GetSegmentNum() - 1; + + CECBezierSeg pSeg = m_pBezier.GetSegment(m_iCurSeg); + if (pSeg != null) + { + float fInvSpeed = 1000.0f / m_fSpeed; + m_iCurSegTime = (int)(pSeg.GetSegLength() * fInvSpeed); + } + else + { + m_iCurSegTime = 0; + } + + m_iPassSegTime = 0; + m_bWalking = true; + m_bPause = false; + + return true; + } + + // Pause walk // 暂停行走 + public void Pause(bool bPause) + { + if (bPause) + { + if (m_bWalking) + { + m_bPause = true; + m_bWalking = false; + } + } + else + { + if (m_bPause) + { + m_bPause = false; + m_bWalking = true; + } + } + } + + // Move speed // 移动速度 + public void SetSpeed(float fSpeed) + { + if (fSpeed <= 0.0f) + { + UnityEngine.Debug.Assert(false); + return; + } + + m_fSpeed = fSpeed; + + // Calculate total time // 计算总时间 + if (m_pBezier != null) + { + int iNumSeg = m_pBezier.GetSegmentNum(); + float fInvSpeed = 1000.0f / m_fSpeed; + + m_iTotalTime = 0; + for (int i = 0; i < iNumSeg; i++) + { + CECBezierSeg pSeg = m_pBezier.GetSegment(i); + if (pSeg != null) + { + m_iTotalTime += (int)(pSeg.GetSegLength() * fInvSpeed); + } + } + } + + } + + public void SetForwardFlag(bool bForward) + { + if (!m_bWalking && !m_bPause) + { + m_bForward = bForward; + return; + } + + int iDeltaTime = m_iPassSegTime + m_iCurSegTime - m_iTimeCnt; + m_iPassSegTime = m_iTotalTime - (m_iPassSegTime + m_iCurSegTime); + m_iTimeCnt = m_iPassSegTime + iDeltaTime; + + m_bForward = bForward; + } + + // Tick routine // Tick例程 + public bool Tick(int iDeltaTime) + { + if (m_pBezier == null || !m_bWalking) + { + return true; + } + + int iNumSeg = m_pBezier.GetSegmentNum(); + if (iNumSeg == 0) + { + return true; + } + + float fInvSpeed = 1000.0f / m_fSpeed; + + if (m_iTotalTime > 0) + { + iDeltaTime = iDeltaTime % m_iTotalTime; + } + m_iTimeCnt += iDeltaTime; + + if (m_bForward) + { + while (m_iTimeCnt >= m_iPassSegTime + m_iCurSegTime) + { + if (m_iCurSeg + 1 >= iNumSeg) + { + if (m_bLoop) + { + m_iCurSeg = 0; + m_iPassSegTime = 0; + m_iTimeCnt -= m_iTotalTime; + + if (m_iTimeCnt < 0) + m_iTimeCnt = 0; + } + else + { + m_iTimeCnt = m_iPassSegTime + m_iCurSegTime; + m_bWalking = false; + m_bForwardStop = true; + break; + } + } + else + { + m_iCurSeg++; + m_iPassSegTime += m_iCurSegTime; + } + + CECBezierSeg pSeg = m_pBezier.GetSegment(m_iCurSeg); + if (pSeg != null) + { + m_iCurSegTime = (int)(pSeg.GetSegLength() * fInvSpeed); + } + else + { + m_iCurSegTime = 0; + break; + } + } + } + else + { + while (m_iTimeCnt >= m_iPassSegTime + m_iCurSegTime) + { + if (m_iCurSeg - 1 < 0) + { + if (m_bLoop) + { + m_iCurSeg = iNumSeg - 1; + m_iPassSegTime = 0; + m_iTimeCnt -= m_iTotalTime; + + if (m_iTimeCnt < 0) + m_iTimeCnt = 0; + } + else + { + m_iTimeCnt = m_iPassSegTime + m_iCurSegTime; + m_bWalking = false; + m_bForwardStop = false; + break; + } + } + else + { + m_iCurSeg--; + m_iPassSegTime += m_iCurSegTime; + } + + CECBezierSeg pSeg = m_pBezier.GetSegment(m_iCurSeg); + if (pSeg != null) + { + m_iCurSegTime = (int)(pSeg.GetSegLength() * fInvSpeed); + } + else + { + m_iCurSegTime = 0; + break; + } + } + } + + return true; + } + + // Get current position // 获取当前位置 + public A3DVECTOR3 GetPos() + { + if (m_pBezier == null) + { + UnityEngine.Debug.Assert(false); + return new A3DVECTOR3(0, 0, 0); + } + + int iSeg; + float f; + bool bForward; + + if (m_bWalking || m_bPause) + { + if (m_iCurSegTime > 0) + { + f = (float)(m_iTimeCnt - m_iPassSegTime) / m_iCurSegTime; + } + else + { + f = 0.0f; + } + bForward = m_bForward; + iSeg = m_iCurSeg; + } + else + { + bForward = true; + + if (m_bForwardStop) + { + f = 1.0f; + iSeg = m_pBezier.GetSegmentNum() - 1; + } + else + { + f = 0.0f; + iSeg = 0; + } + } + + CECBezierSeg pSeg = m_pBezier.GetSegment(iSeg); + if (pSeg != null) + { + return pSeg.Bezier(f, bForward); + } + + return new A3DVECTOR3(0, 0, 0); + } + + // Get current direction // 获取当前方向 + public A3DVECTOR3 GetDir() + { + if (m_pBezier == null) + { + UnityEngine.Debug.Assert(false); + return new A3DVECTOR3(0, 0, 1); + } + + int iSeg; + float f; + bool bForward; + + if (m_bWalking || m_bPause) + { + if (m_iCurSegTime > 0) + { + f = (float)(m_iTimeCnt - m_iPassSegTime) / m_iCurSegTime; + } + else + { + f = 0.0f; + } + bForward = m_bForward; + iSeg = m_iCurSeg; + } + else + { + bForward = true; + + if (m_bForwardStop) + { + f = 1.0f; + iSeg = m_pBezier.GetSegmentNum() - 1; + } + else + { + f = 0.0f; + iSeg = 0; + } + } + + CECBezierSeg pSeg = m_pBezier.GetSegment(iSeg); + if (pSeg != null) + { + return pSeg.Vector(f, bForward); + } + + return new A3DVECTOR3(0, 0, 1); + } + + public bool IsWalking() { return m_bWalking; } + public bool IsPause() { return m_bPause; } + public bool GetForwardFlag() { return m_bForward; } + public float GetSpeed() { return m_fSpeed; } + public int GetTotalTime() { return m_iTotalTime; } + public void SetLoopFlag(bool bLoop) { m_bLoop = bLoop; } + public bool GetLoopFlag() { return m_bLoop; } + } + + // CECHostNavigatePlayer class - Basic implementation for navigation player // CECHostNavigatePlayer类 - 导航玩家的基本实现 + public class CECHostNavigatePlayer + { + private CECNavigateCtrl m_pNavigateCtrl; // Force navigate // 强制导航 + private CECHostPlayer m_pHost = null; // Reference to host player // 对宿主玩家的引用 + + public CECHostNavigatePlayer(CECHostPlayer pHost) + { + // Initialize navigate control // 初始化导航控制 + m_pHost = pHost; + m_pNavigateCtrl = new CECNavigateCtrl(pHost); + } + + public A3DVECTOR3 GetDir() + { + if (m_pHost != null) + { + // Get direction from host's transform // 从宿主的变换获取方向 + Transform hostTransform = m_pHost.transform; + if (hostTransform != null) + { + Vector3 forward = hostTransform.forward; + return new A3DVECTOR3(forward.x, forward.y, forward.z); + } + } + return new A3DVECTOR3(0, 0, 1); + } + + public void SetPos(A3DVECTOR3 vPos) + { + if (m_pHost != null) + { + // Actually move the host player // 实际移动宿主玩家 + Vector3 newPos = new Vector3(vPos.x, vPos.y, vPos.z); + m_pHost.SetPos(newPos); + } + } + + public void ChangeModelMoveDirAndUp(A3DVECTOR3 vDir, A3DVECTOR3 vUp) + { + if (m_pHost != null && m_pHost.transform != null) + { + // Update host's rotation based on direction // 根据方向更新宿主的旋转 + Vector3 dir = new Vector3(vDir.x, vDir.y, vDir.z); + Vector3 up = new Vector3(vUp.x, vUp.y, vUp.z); + + if (dir.magnitude > 0.01f) + { + Quaternion rotation = Quaternion.LookRotation(dir, up); + m_pHost.transform.rotation = rotation; + } + } + } + public void SetNavigateModelFile(string szFile) { } // Stub + public bool Init() { return true; } // Stub + public CECNavigateCtrl GetNavigateCtrl() { return m_pNavigateCtrl; } + + // Handle navigation event // 处理导航事件 + public void OnNavigateEvent(int task, int e) + { + if (m_pNavigateCtrl == null) + { + return; + } + + if (e == (int)CECNavigateCtrl.NavigateEvent.EM_PREPARE) + { + m_pNavigateCtrl.OnPrepareNavigate(task); + } + else if (e == (int)CECNavigateCtrl.NavigateEvent.EM_BEGIN) + { + m_pNavigateCtrl.OnBeginNavigate(); + } + else if (e == (int)CECNavigateCtrl.NavigateEvent.EM_END) + { + m_pNavigateCtrl.OnEndNavigate(); + } + } + } + + ////////////////////////////////////////////////////////////////////////// + // + // CECBezierPoint class - Bezier curve point // 贝塞尔曲线点类 + // + ////////////////////////////////////////////////////////////////////////// + + public class CECBezierPoint + { + private A3DVECTOR3 m_vPos; // Position // 位置 + private A3DVECTOR3 m_vDir; // Direction // 方向 + + public CECBezierPoint() + { + m_vPos = new A3DVECTOR3(0, 0, 0); + m_vDir = new A3DVECTOR3(0, 0, 1); + } + + public A3DVECTOR3 GetPos() { return m_vPos; } + public A3DVECTOR3 GetDir() { return m_vDir; } + public void SetPos(A3DVECTOR3 pos) { m_vPos = pos; } + public void SetDir(A3DVECTOR3 dir) { m_vDir = dir; } + } + + ////////////////////////////////////////////////////////////////////////// + // + // CECBezierSeg class - Bezier curve segment // 贝塞尔曲线段类 + // + ////////////////////////////////////////////////////////////////////////// + + public class CECBezierSeg + { + private A3DVECTOR3 m_vAnchorHead; // Head anchor point // 头部锚点 + private A3DVECTOR3 m_vAnchorTail; // Tail anchor point // 尾部锚点 + private int m_nHeadPoint; // Head point index // 头部点索引 + private int m_nTailPoint; // Tail point index // 尾部点索引 + private float m_fLength; // Segment length // 段长度 + private CECBezierPoint[] m_pListPoint; // Reference to point list // 点列表的引用 + + public CECBezierSeg() + { + m_vAnchorHead = new A3DVECTOR3(0, 0, 0); + m_vAnchorTail = new A3DVECTOR3(0, 0, 0); + m_nHeadPoint = 0; + m_nTailPoint = 0; + m_fLength = 0.0f; + m_pListPoint = null; + } + + public void Init(CECBezierPoint[] pListPt) + { + m_pListPoint = pListPt; + } + + // Calculate Bezier curve position at parameter u [0,1] // 计算参数u [0,1]处的贝塞尔曲线位置 + public A3DVECTOR3 Bezier(float u, bool bForward) + { + // C++: use a,b,c arrays form + // 原版:使用 a,b,c 数组形式计算 + A3DVECTOR3 pt1, pt2, control1, control2; + + if (bForward) + { + pt1 = m_pListPoint[m_nHeadPoint].GetPos(); + control1 = m_vAnchorHead; + control2 = m_vAnchorTail; + pt2 = m_pListPoint[m_nTailPoint].GetPos(); + } + else + { + pt1 = m_pListPoint[m_nTailPoint].GetPos(); + control1 = m_vAnchorTail; + control2 = m_vAnchorHead; + pt2 = m_pListPoint[m_nHeadPoint].GetPos(); + } + + float[] a = new float[3]; + float[] b = new float[3]; + float[] c = new float[3]; + + float[] p0 = { pt1.x, pt1.y, pt1.z }; + float[] p1 = { control1.x, control1.y, control1.z }; + float[] p2 = { control2.x, control2.y, control2.z }; + float[] p3 = { pt2.x, pt2.y, pt2.z }; + + for (int i = 0; i < 3; i++) + { + c[i] = 3.0f * (p1[i] - p0[i]); + b[i] = 3.0f * (p2[i] - p1[i]) - c[i]; + a[i] = p3[i] - p0[i] - c[i] - b[i]; + } + + A3DVECTOR3 pos = new A3DVECTOR3(); + pos.x = (a[0] * u * u * u) + (b[0] * u * u) + (c[0] * u) + p0[0]; + pos.y = (a[1] * u * u * u) + (b[1] * u * u) + (c[1] * u) + p0[1]; + pos.z = (a[2] * u * u * u) + (b[2] * u * u) + (c[2] * u) + p0[2]; + return pos; + } + + // Calculate direction vector at parameter u [0,1] // 计算参数u [0,1]处的方向向量 + public A3DVECTOR3 Vector(float u, bool bForward) + { + // C++ implementation + // 原版实现 + A3DVECTOR3 headspot, tailspot; + + if (bForward) + { + headspot = m_pListPoint[m_nHeadPoint].GetDir(); + tailspot = m_pListPoint[m_nTailPoint].GetDir(); + } + else + { + headspot = m_pListPoint[m_nTailPoint].GetDir(); + tailspot = m_pListPoint[m_nHeadPoint].GetDir(); + } + + if (u <= 0.0f) return headspot; + if (u >= 1.0f) return tailspot; + + float m1 = headspot.Magnitude(); + float m2 = tailspot.Magnitude(); + + A3DVECTOR3 hN, tN; + A3DVECTOR3.Normalize(headspot, out hN); + A3DVECTOR3.Normalize(tailspot, out tN); + + float dot = A3DVECTOR3.DotProduct(hN, tN); + if (dot > 1.0f) return headspot; + if (dot < -1.0f) dot = -1.0f; + + float r = Mathf.Acos(dot); + r = r * u; + + A3DVECTOR3 axis = A3DVECTOR3.CrossProduct(headspot, tailspot); + // matrix * headspot (use quaternion equivalent) + A3DVECTOR3 rotated = NavigateHelper.a3d_RotatePosAroundAxis(headspot, axis, r); + A3DVECTOR3.Normalize(rotated, out rotated); + + float len = m1 + (m2 - m1) * u; + rotated = rotated * len; + + if (bForward) return rotated; + return -rotated; + } + + public A3DVECTOR3 GetAnchorHead() { return m_vAnchorHead; } + public A3DVECTOR3 GetAnchorTail() { return m_vAnchorTail; } + public int GetHeadPoint() { return m_nHeadPoint; } + public int GetTailPoint() { return m_nTailPoint; } + public float GetSegLength() { return m_fLength; } + + public void SetAnchorHead(A3DVECTOR3 v) { m_vAnchorHead = v; } + public void SetAnchorTail(A3DVECTOR3 v) { m_vAnchorTail = v; } + public void SetHeadPoint(int index) { m_nHeadPoint = index; } + public void SetTailPoint(int index) { m_nTailPoint = index; } + public void SetSegLength(float length) { m_fLength = length; } + } + + ////////////////////////////////////////////////////////////////////////// + // + // CECBezier class - Bezier curve route // 贝塞尔曲线路径类 + // + ////////////////////////////////////////////////////////////////////////// + + public class CECBezier + { + private int m_nObjectID; // Object ID // 对象ID + private int m_iGlobalID; // Global ID used to link bezier routes // 用于链接贝塞尔路径的全局ID + private int m_iNextGlobalID; // Next bezier route's global ID // 下一个贝塞尔路径的全局ID + private int m_nNumPoint; // Number of points // 点数 + private int m_nNumSeg; // Number of segments // 段数 + private string m_strActName; // Action name // 动作名称 + private CECBezierPoint[] m_pListPoint; // Point list // 点列表 + private CECBezierSeg[] m_pListSeg; // Segment list // 段列表 + + public CECBezier() + { + m_nObjectID = 0; + m_iGlobalID = -1; + m_iNextGlobalID = -1; + m_nNumPoint = 0; + m_nNumSeg = 0; + m_strActName = ""; + m_pListPoint = null; + m_pListSeg = null; + } + + ~CECBezier() + { + Release(); + } + + public void Release() + { + m_pListPoint = null; + m_pListSeg = null; + m_nNumPoint = 0; + m_nNumSeg = 0; + } + + // Load bezier from binary stream (navigate.dat) // 从二进制流加载Bezier(navigate.dat) + public bool Load(BinaryReader br) + { + // C++: DWORD version; int objectId; (version>=2: globalID,nextGlobalID) (version>=3: ReadString actName) + // 原版:DWORD version; int objectId;(version>=2:globalID,nextGlobalID)(version>=3:ReadString 动作名) + uint dwVersion = br.ReadUInt32(); + m_nObjectID = br.ReadInt32(); + + if (dwVersion >= 2) + { + m_iGlobalID = br.ReadInt32(); + m_iNextGlobalID = br.ReadInt32(); + } + + if (dwVersion >= 3) + { + int len = br.ReadInt32(); + if (len > 0) + { + byte[] buf = br.ReadBytes(len); + m_strActName = Encoding.GetEncoding(936).GetString(buf); + } + else + { + m_strActName = string.Empty; + } + } + + int ptNum = br.ReadInt32(); + m_pListPoint = new CECBezierPoint[ptNum]; + m_nNumPoint = ptNum; + for (int i = 0; i < ptNum; i++) + { + var vPos = new A3DVECTOR3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()); + var vDir = new A3DVECTOR3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()); + m_pListPoint[i] = new CECBezierPoint(); + m_pListPoint[i].SetPos(vPos); + m_pListPoint[i].SetDir(vDir); + } + + int segNum = br.ReadInt32(); + m_nNumSeg = segNum; + if (segNum > 0) + { + m_pListSeg = new CECBezierSeg[segNum]; + for (int i = 0; i < segNum; i++) + { + var vAnchorHead = new A3DVECTOR3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()); + var vAnchorTail = new A3DVECTOR3(br.ReadSingle(), br.ReadSingle(), br.ReadSingle()); + int headIndex = br.ReadInt32(); + int tailIndex = br.ReadInt32(); + float length = br.ReadSingle(); + + m_pListSeg[i] = new CECBezierSeg(); + m_pListSeg[i].Init(m_pListPoint); + m_pListSeg[i].SetAnchorHead(vAnchorHead); + m_pListSeg[i].SetAnchorTail(vAnchorTail); + m_pListSeg[i].SetHeadPoint(headIndex); + m_pListSeg[i].SetTailPoint(tailIndex); + m_pListSeg[i].SetSegLength(length); + } + } + + return true; + } + + public int GetObjectID() { return m_nObjectID; } + public void SetObjectID(int id) { m_nObjectID = id; } + + public void SetGlobalID(int iID) { m_iGlobalID = iID; } + public int GetGlobalID() { return m_iGlobalID; } + + public void SetNextGlobalID(int iID) { m_iNextGlobalID = iID; } + public int GetNextGlobalID() { return m_iNextGlobalID; } + + public int GetSegmentNum() { return m_nNumSeg; } + public CECBezierSeg GetSegment(int n) + { + if (n >= 0 && n < m_nNumSeg && m_pListSeg != null) + { + return m_pListSeg[n]; + } + return null; + } + + // Assign points and segments (for loading/creation) // 分配点和段(用于加载/创建) + public void Assign(int nNumPt, int nNumSeg) + { + Release(); + m_nNumPoint = nNumPt; + m_nNumSeg = nNumSeg; + + if (nNumPt > 0) + { + m_pListPoint = new CECBezierPoint[nNumPt]; + } + if (nNumSeg > 0) + { + m_pListSeg = new CECBezierSeg[nNumSeg]; + // Initialize segments with point list reference // 使用点列表引用初始化段 + for (int i = 0; i < nNumSeg; i++) + { + m_pListSeg[i] = new CECBezierSeg(); + if (m_pListPoint != null) + { + m_pListSeg[i].Init(m_pListPoint); + } + } + } + } + + public void AddBezierPoint(CECBezierPoint pt, int i) + { + if (i >= 0 && i < m_nNumPoint && m_pListPoint != null) + { + m_pListPoint[i].SetPos(pt.GetPos()); + m_pListPoint[i].SetDir(pt.GetDir()); + } + } + + public void AddBezierSeg(CECBezierSeg seg, int i) + { + if (i >= 0 && i < m_nNumSeg && m_pListSeg != null) + { + m_pListSeg[i].Init(m_pListPoint); + m_pListSeg[i].SetAnchorHead(seg.GetAnchorHead()); + m_pListSeg[i].SetAnchorTail(seg.GetAnchorTail()); + m_pListSeg[i].SetHeadPoint(seg.GetHeadPoint()); + m_pListSeg[i].SetTailPoint(seg.GetTailPoint()); + m_pListSeg[i].SetSegLength(seg.GetSegLength()); + } + } + + public void SetOffset(A3DVECTOR3 vOffset) + { + if (m_pListPoint != null) + { + for (int i = 0; i < m_nNumPoint; i++) + { + A3DVECTOR3 pos = m_pListPoint[i].GetPos(); + pos.x += vOffset.x; + pos.y += vOffset.y; + pos.z += vOffset.z; + m_pListPoint[i].SetPos(pos); + } + } + } + } + + ////////////////////////////////////////////////////////////////////////// + // + // navigate.dat loader (equivalent to CECScene::LoadBezierNavigate + GetBezierObjectByGlobalID) + // navigate.dat加载器(等价于 CECScene::LoadBezierNavigate + GetBezierObjectByGlobalID) + // + ////////////////////////////////////////////////////////////////////////// + public static class CECBezierNavigateLoader + { + // NAVIGATEBEZIERFILEHEADER + // 导航Bezier文件头 + private struct NAVIGATEBEZIERFILEHEADER + { + public int iVersion; // Version // 版本 + public int iNumBezier; // Number of bezier route // Bezier数量 + } + + private static bool s_loaded; + private static readonly Dictionary s_beziers = new Dictionary(); + + public static CECBezier GetBezierObjectByGlobalID(int iGlobalID) + { + if (!s_loaded) + { + LoadBezierNavigate("Assets/Addressable/navigate.dat"); + } + + return s_beziers.TryGetValue(iGlobalID, out var bezier) ? bezier : null; + } + + public static bool LoadBezierNavigate(string address) + { + if (s_loaded) + { + return true; + } + + try + { + Addressables.InitializeAsync().WaitForCompletion(); + var ta = Addressables.LoadAssetAsync(address).WaitForCompletion(); + if (ta == null) + { + return false; + } + + using var ms = new MemoryStream(ta.bytes, false); + using var br = new BinaryReader(ms); + + NAVIGATEBEZIERFILEHEADER header; + header.iVersion = br.ReadInt32(); + header.iNumBezier = br.ReadInt32(); + + s_beziers.Clear(); + for (int i = 0; i < header.iNumBezier; i++) + { + var pBezier = new CECBezier(); + if (!pBezier.Load(br)) + { + return false; + } + + // C++: m_BezierForceNavigateTab.put((int)pBezier->GetGlobalID(), pBezier); + // 原版:按GlobalID存入表 + s_beziers[pBezier.GetGlobalID()] = pBezier; + } + + s_loaded = true; + return true; + } + catch + { + return false; + } + } + } +} + diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs.meta b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs.meta new file mode 100644 index 0000000000..973cf93a12 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c3491aaddacef1a418865bbcd31fb975 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Task/ATaskTemplMan.cs b/Assets/PerfectWorld/Scripts/Task/ATaskTemplMan.cs index e97e84be44..74099bc7cd 100644 --- a/Assets/PerfectWorld/Scripts/Task/ATaskTemplMan.cs +++ b/Assets/PerfectWorld/Scripts/Task/ATaskTemplMan.cs @@ -68,6 +68,13 @@ namespace BrewMonster.Scripts.Task uint m_ulNPCInfoTimeMark; private Dictionary m_NPCInfoMap = new(); + // Lookup NPC/task object coordinates info by template id (loaded from task_npc pack) + // 通过模板ID查找NPC/任务对象坐标信息(从task_npc包加载) + public bool TryGetTaskNPCInfo(uint id, out NPC_INFO info) + { + return m_NPCInfoMap.TryGetValue(id, out info); + } + private TaskTemplContainerSO _taskTemplContainerSO; #if _TASK_CLIENT diff --git a/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs b/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs index 0940a3fda1..49dd15cca7 100644 --- a/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs +++ b/Assets/PerfectWorld/Scripts/Task/CECTaskInterface.cs @@ -11,6 +11,7 @@ using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; using System; +using BrewMonster.Scripts; // For CECNavigateCtrl namespace BrewMonster.Scripts.Task { @@ -1625,19 +1626,25 @@ namespace BrewMonster.Scripts.Task { return m_pFinishedCountListBuf; } - void SetForceNavigateFinishFlag(bool bFinish) { m_bForceNavigateFinish = bFinish;} // - public void OnNewTask(int iTaskID) + public void SetForceNavigateFinishFlag(bool bFinish) { m_bForceNavigateFinish = bFinish;} // Set force navigate finish flag // 设置强制导航完成标志 + public void OnNewTask(int iTaskID) + { + UnityEngine.Debug.Log($"[CECTaskInterface] OnNewTask: TaskID={iTaskID}"); + ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID); + if (pTempl != null + && pTempl.m_FixedData.m_enumMethod== (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi) { - ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID); - if (pTempl != null - && pTempl.m_FixedData.m_enumMethod== (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi) - { - SetForceNavigateFinishFlag(false); - - // TODO: trigger navigation event - // m_pHost.OnNaviageEvent(iTaskID,CECNavigateCtrl::EM_PREPARE); - } + UnityEngine.Debug.Log($"[CECTaskInterface] OnNewTask: Task {iTaskID} is force navigate task, triggering EM_PREPARE"); + SetForceNavigateFinishFlag(false); + + // Trigger navigation event // 触发导航事件 + m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_PREPARE); } + else + { + UnityEngine.Debug.Log($"[CECTaskInterface] OnNewTask: Task {iTaskID} is not a force navigate task (pTempl={pTempl != null}, method={pTempl?.m_FixedData.m_enumMethod})"); + } + } public void OnTaskConfirmUpdate() { @@ -1653,34 +1660,99 @@ namespace BrewMonster.Scripts.Task OnTaskConfirmUpdate(); } - public void OnCompleteTask(int iTaskID) + public void OnCompleteTask(int iTaskID) + { + UnityEngine.Debug.Log($"[CECTaskInterface] OnCompleteTask: TaskID={iTaskID}"); + ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID); + if (pTempl != null && + pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi) { - ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID); - if (pTempl != null && - pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi) - { - //TODO: trigger navigation end event - // m_pHost.OnNaviageEvent(iTaskID,CECNavigateCtrl::EM_END); - SetForceNavigateFinishFlag(false); - } + UnityEngine.Debug.Log($"[CECTaskInterface] OnCompleteTask: Task {iTaskID} is force navigate task, triggering EM_END"); + // Trigger navigation end event // 触发导航结束事件 + m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_END); + SetForceNavigateFinishFlag(false); } + else + { + UnityEngine.Debug.Log($"[CECTaskInterface] OnCompleteTask: Task {iTaskID} is not a force navigate task"); + } + } public void TakeAwayCommonItem(uint ulTemplId, uint ulNum) {} public void TakeAwayTaskItem(uint ulTemplId, uint ulNum) {} public void TakeAwayGold(uint ulNum) {} public void TakeAwayFactionConsumeContrib(int ulNum){} - public void OnGiveupTask(int iTaskID) + public void OnGiveupTask(int iTaskID) + { + UnityEngine.Debug.Log($"[CECTaskInterface] OnGiveupTask: TaskID={iTaskID}"); + ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID); + if (pTempl != null && + pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi) { - ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID); - if (pTempl != null && - pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi) + UnityEngine.Debug.Log($"[CECTaskInterface] OnGiveupTask: Task {iTaskID} is force navigate task, triggering EM_END"); + // Trigger navigation end event // 触发导航结束事件 + m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_END); + SetForceNavigateFinishFlag(false); + } + else + { + UnityEngine.Debug.Log($"[CECTaskInterface] OnGiveupTask: Task {iTaskID} is not a force navigate task"); + } + } + + // Handle task text click in UI - trigger navigation if it's a force navigate task // 处理任务UI文本点击 - 如果是强制导航任务则触发导航 + // This is called when user clicks on task name/link in the task UI // 当用户在任务UI中点击任务名称/链接时调用 + public void OnTaskTextClick(int iTaskID) + { + UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: TaskID={iTaskID}"); + + // Check if task exists and is a force navigate task // 检查任务是否存在且为强制导航任务 + ATaskTempl pTempl = GetTaskTemplMan().GetTaskTemplByID((uint)iTaskID); + if (pTempl != null && + pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi) + { + UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: Task {iTaskID} is force navigate task, triggering navigation"); + + // Check if navigation is already prepared // 检查导航是否已准备 + CECHostNavigatePlayer pNavigatePlayer = m_pHost.GetNavigatePlayer(); + if (pNavigatePlayer != null && pNavigatePlayer.GetNavigateCtrl() != null) { - // TODO: trigger navigation end event - // m_pHost.OnNaviageEvent(iTaskID,CECNavigateCtrl::EM_END); - SetForceNavigateFinishFlag(false); + // If already prepared, trigger begin // 如果已准备,触发开始 + if (pNavigatePlayer.GetNavigateCtrl().IsInForceNavigateState()) + { + UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: Navigation already prepared, triggering EM_BEGIN"); + m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_BEGIN); + } + else + { + // Prepare first, then begin // 先准备,然后开始 + UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: Preparing navigation first, then beginning"); + m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_PREPARE); + // Note: EM_BEGIN will be triggered after preparation is complete + // 注意:EM_BEGIN 将在准备完成后触发 + // For now, trigger it immediately after a short delay + // 目前,在短暂延迟后立即触发 + // TODO: Implement proper async handling if needed + // TODO: 如果需要,实现适当的异步处理 + m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_BEGIN); + } + } + else + { + // Prepare and begin navigation // 准备并开始导航 + UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: Preparing and beginning navigation"); + m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_PREPARE); + m_pHost.OnNaviageEvent(iTaskID, (int)CECNavigateCtrl.NavigateEvent.EM_BEGIN); } } + else + { + UnityEngine.Debug.Log($"[CECTaskInterface] OnTaskTextClick: Task {iTaskID} is not a force navigate task, will use normal auto-move"); + // TODO: Implement normal auto-move behavior here + // TODO: 在此处实现正常的自动移动行为 + } + } public void UpdateTaskUI(uint idTask, int reason) { diff --git a/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs b/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs index 6a2f887065..9a64ab3266 100644 --- a/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs +++ b/Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs @@ -15,6 +15,7 @@ using NUnit.Framework; using PerfectWorld.Scripts.Task; using UnityEngine; using UnityEngine.UI; +using UnityEngine.EventSystems; using TMPro; using Unity.VisualScripting; @@ -166,10 +167,171 @@ namespace BrewMonster.Scripts.Task.UI m_pBtn_SearchQuest.onClick.AddListener(OnCommand_searchquest); m_pBtn_Abandon.onClick.AddListener(OnCommand_abandon); + // Convert exactly like C++ OnEventLButtonDown_Txt_QuestItem // 完全按照C++ OnEventLButtonDown_Txt_QuestItem转换 + // C++ uses WM_LBUTTONDOWN event, we use EventTrigger PointerClick // C++使用WM_LBUTTONDOWN事件,我们使用EventTrigger PointerClick + if (m_pTxt_QuestItem != null) + { + // Enable raycast for TMP link detection (like C++ GetItemLinkItemOn needs link info) // 为TMP链接检测启用射线投射(如C++ GetItemLinkItemOn需要链接信息) + m_pTxt_QuestItem.raycastTarget = true; + + // Add EventTrigger for PointerClick (like WM_LBUTTONDOWN in C++) // 为PointerClick添加EventTrigger(如C++中的WM_LBUTTONDOWN) + EventTrigger trigger = m_pTxt_QuestItem.GetComponent(); + if (trigger == null) + { + trigger = m_pTxt_QuestItem.gameObject.AddComponent(); + } + + // Clear existing triggers // 清除现有触发器 + trigger.triggers.Clear(); + + // Add PointerClick event (like WM_LBUTTONDOWN) // 添加PointerClick事件(如WM_LBUTTONDOWN) + EventTrigger.Entry entry = new EventTrigger.Entry(); + entry.eventID = EventTriggerType.PointerClick; + entry.callback.AddListener((data) => { + PointerEventData pointerData = (PointerEventData)data; + OnEventLButtonDown_Txt_QuestItem(pointerData); + }); + trigger.triggers.Add(entry); + + UnityEngine.Debug.Log($"[DlgTask] Awake: Added EventTrigger for PointerClick to m_pTxt_QuestItem (like C++ WM_LBUTTONDOWN)"); + } + OnInitDialog(); } + + // Convert exactly like C++ OnEventLButtonDown_Txt_QuestItem // 完全按照C++ OnEventLButtonDown_Txt_QuestItem转换 + // C++: void CDlgTask::OnEventLButtonDown_Txt_QuestItem(WPARAM wParam, LPARAM lParam, AUIObject *pObj) + private void OnEventLButtonDown_Txt_QuestItem(PointerEventData eventData) + { + if (m_pTxt_QuestItem == null) return; + + const string LINK_CLICK_VER = "DlgTaskLinkClickCamFix_v2"; + UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: {LINK_CLICK_VER} pressEventCamera={(eventData.pressEventCamera != null ? eventData.pressEventCamera.name : "null")}"); + + // C++: int x = GET_X_LPARAM(lParam); int y = GET_Y_LPARAM(lParam); // C++: int x = GET_X_LPARAM(lParam); int y = GET_Y_LPARAM(lParam); + Vector2 localPoint; + RectTransformUtility.ScreenPointToLocalPointInRectangle( + m_pTxt_QuestItem.rectTransform, + eventData.position, + eventData.pressEventCamera, + out localPoint); + + UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: Click at localPoint={localPoint}, screenPos={eventData.position}"); + + // C++: GetItemLinkItemOn(x, y, pObj, &Item); // C++: GetItemLinkItemOn(x, y, pObj, &Item); + // Find which link was clicked (like C++ GetItemLinkItemOn checks vecItemLink[i].rc.PtInRect) // 查找点击了哪个链接(如C++ GetItemLinkItemOn检查vecItemLink[i].rc.PtInRect) + m_pTxt_QuestItem.ForceMeshUpdate(); - private void Update() + // Debug: verify TMP parsed tags and we have linkInfo + int linkCount = m_pTxt_QuestItem.textInfo?.linkCount ?? -1; + UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: TMP linkCount={linkCount}, raycastTarget={m_pTxt_QuestItem.raycastTarget}, richText={m_pTxt_QuestItem.richText}"); + if (linkCount <= 0) + { + string text = m_pTxt_QuestItem.text ?? string.Empty; + string preview = text.Substring(0, Mathf.Min(300, text.Length)); + UnityEngine.Debug.LogWarning($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: No links parsed. Text preview: {preview}"); + return; + } + + // Get camera for link detection // 获取用于链接检测的相机 + Camera camera = null; + Canvas canvas = m_pTxt_QuestItem.GetComponentInParent(); + if (canvas != null) + { + // IMPORTANT: TMP_TextUtilities.FindIntersectingLink expects camera=null for ScreenSpaceOverlay. + // 重要:ScreenSpaceOverlay 必须传 camera=null,否则会算错导致 linkIndex = -1。 + UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: Canvas renderMode={canvas.renderMode}, worldCamera={(canvas.worldCamera != null ? canvas.worldCamera.name : "null")}, pressEventCamera={(eventData.pressEventCamera != null ? eventData.pressEventCamera.name : "null")}"); + if (canvas.renderMode == RenderMode.ScreenSpaceOverlay) + { + camera = null; + } + else + { + // ScreenSpaceCamera / WorldSpace + camera = eventData.pressEventCamera != null ? eventData.pressEventCamera : canvas.worldCamera; + } + } + else + { + UnityEngine.Debug.LogWarning($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: {LINK_CLICK_VER} Canvas not found in parents of m_pTxt_QuestItem!"); + // No canvas found; fall back to pressEventCamera (may be null) then main + camera = eventData.pressEventCamera != null ? eventData.pressEventCamera : Camera.main; + } + + // Find intersecting link (like C++ checks vecItemLink[i].rc.PtInRect(x, y)) // 查找相交的链接(如C++检查vecItemLink[i].rc.PtInRect(x, y)) + int linkIndexCam = TMP_TextUtilities.FindIntersectingLink(m_pTxt_QuestItem, eventData.position, camera); + int linkIndexNull = TMP_TextUtilities.FindIntersectingLink(m_pTxt_QuestItem, eventData.position, null); + UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: {LINK_CLICK_VER} FindIntersectingLink(cam={(camera != null ? camera.name : "null")})={linkIndexCam}, FindIntersectingLink(null)={linkIndexNull}, screenPos={eventData.position}"); + + int linkIndex = linkIndexCam != -1 ? linkIndexCam : linkIndexNull; + UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: FindIntersectingLink => linkIndex={linkIndex}, camera={(camera != null ? camera.name : "null")}, screenPos={eventData.position}"); + + // Dump all links for debugging + for (int i = 0; i < m_pTxt_QuestItem.textInfo.linkCount; i++) + { + TMP_LinkInfo li = m_pTxt_QuestItem.textInfo.linkInfo[i]; + UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: Link[{i}] id={li.GetLinkID()} text={li.GetLinkText()}"); + } + + // C++: if( Item.m_pItem != NULL ) // C++: if( Item.m_pItem != NULL ) + if (linkIndex != -1 && linkIndex < m_pTxt_QuestItem.textInfo.linkCount) + { + TMP_LinkInfo linkInfo = m_pTxt_QuestItem.textInfo.linkInfo[linkIndex]; + string linkID = linkInfo.GetLinkID(); + + UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: Found link - linkID={linkID}"); + + // C++: if( Item.m_pItem->GetType() == enumEICoord ) // C++: if( Item.m_pItem->GetType() == enumEICoord ) + // Check if this is a coordinate link (NPC, monster, item, target - all use enumEICoord in C++) // 检查这是否是坐标链接(NPC、怪物、物品、目标 - 在C++中都使用enumEICoord) + if (linkID.StartsWith("coord_")) + { + // C++: if (IsTreasureMapSelected()){ OnCommand_TreasureMap(NULL); } // C++: if (IsTreasureMapSelected()){ OnCommand_TreasureMap(NULL); } + // TODO: Implement IsTreasureMapSelected check if needed // TODO: 如果需要,实现IsTreasureMapSelected检查 + + // C++: else { CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask); } // C++: else { CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask); } + // Extract entity ID from link (NPC, monster, item, target - all handled the same) // 从链接中提取实体ID(NPC、怪物、物品、目标 - 处理方式相同) + string entityIdStr = linkID.Substring(6); // Remove "coord_" prefix + if (int.TryParse(entityIdStr, out int entityID)) + { + // Use currently selected task ID (like m_idSelTask in C++ FollowCoord call) // 使用当前选中的任务ID(如C++ FollowCoord调用中的m_idSelTask) + int currentTaskID = m_idSelTask; + + UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: Coordinate link clicked, EntityID={entityID} (NPC/Monster/Item/Target), task={currentTaskID}"); + + // C++: CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask); // C++: CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask); + // FollowCoord triggers auto-move to the coordinates (for NPC, monster, item, target) // FollowCoord触发自动移动到坐标(适用于NPC、怪物、物品、目标) + // This works for all entity types - NPC names, monster names, items, targets // 这适用于所有实体类型 - NPC名称、怪物名称、物品、目标 + if (currentTaskID > 0) + { + CECHostPlayer hostPlayer = GetHostPlayer(); + if (hostPlayer != null) + { + CECTaskInterface taskInterface = hostPlayer.GetTaskInterface(); + if (taskInterface != null) + { + // This matches C++: CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask) + // 这匹配C++:CECUIHelper::FollowCoord(Item.m_pItem, m_idSelTask) + bool ok = CECUIHelper.FollowCoord(entityID, currentTaskID); + UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: FollowCoord(entity={entityID}, task={currentTaskID}) => {ok}"); + } + } + } + else + { + // Even without a task, still follow coord + // 即使没有任务,也照样跟随坐标 + bool ok = CECUIHelper.FollowCoord(entityID, 0); + UnityEngine.Debug.Log($"[DlgTask] OnEventLButtonDown_Txt_QuestItem: FollowCoord(entity={entityID}, task=0) => {ok}"); + } + } + } + } + + // C++: ChangeFocus(NULL); // C++: ChangeFocus(NULL); + // TODO: Implement ChangeFocus if needed // TODO: 如果需要,实现ChangeFocus + } + + private new void Update() { Tick(); } @@ -545,7 +707,7 @@ namespace BrewMonster.Scripts.Task.UI // Award NPC int nANPC = (int)pTemp.GetAwardNPC(); - UpdateAwardNPC(ref strNewTextItem, nANPC); + UpdateAwardNPC(ref strNewTextItem, nANPC, idTask); // Complete condition - always refresh to show updated progress UpdateCompleteCondition(ref strNewTextItem, ref strNewHintItem, tsi); @@ -668,11 +830,11 @@ namespace BrewMonster.Scripts.Task.UI UpdateTaskBaseDesc(ref strNewTextItem, tsi); // Append: deliver NPC - UpdateDeliverNPC(ref strNewTextItem, (int)pTemp.GetDeliverNPC()); + UpdateDeliverNPC(ref strNewTextItem, (int)pTemp.GetDeliverNPC(), idTask); // Append: award NPC int nANPC = (int)pTemp.GetAwardNPC(); - UpdateAwardNPC(ref strNewTextItem, nANPC); + UpdateAwardNPC(ref strNewTextItem, nANPC, idTask); // Append: completion conditions UpdateCompleteCondition(ref strNewTextItem, ref strNewHintItem, tsi); @@ -1216,7 +1378,7 @@ namespace BrewMonster.Scripts.Task.UI } - private void UpdateDeliverNPC(ref string strText, int nDNPC) + private void UpdateDeliverNPC(ref string strText, int nDNPC, int idTask = 0) { // [中文] 交付NPC // [English] Deliver NPC @@ -1241,60 +1403,59 @@ namespace BrewMonster.Scripts.Task.UI } if (string.IsNullOrEmpty(npcName)) npcName = nDNPC.ToString(); - // [中文] 追加到内容文本 - // [English] Append to content text + // [中文] 追加到内容文本,如果找到坐标则添加可点击链接(如C++中的enumEICoord) + // [English] Append to content text, add clickable link if coordinates found (like enumEICoord in C++) var sb = new System.Text.StringBuilder(); sb.Append(GetStringFromTable(7620)); - sb.Append(npcName); + + // Always create clickable link (even if coords aren't known yet). + // 总是创建可点击链接(即使暂时不知道坐标)。 + sb.Append($"{npcName}"); sb.Append("\n"); - // if (m_pTxt_QuestItem != null) - // { - // Debug.Log($"UpdateDeliverNPC: {sb.ToString()}"); - // m_pTxt_QuestItem.text += sb.ToString(); - // } strText += sb.ToString(); } - private A3DVECTOR3 UpdateAwardNPC(ref string strText, int nANPC) + private A3DVECTOR3 UpdateAwardNPC(ref string strText, int nANPC, int idTask = 0) + { + A3DVECTOR3 ret = new A3DVECTOR3(0f); + // Award NPC + if (nANPC == 0) { - A3DVECTOR3 ret = new A3DVECTOR3(0f); - // Award NPC - if (nANPC == 0) - { - return ret; - } - - // Lookup NPC name from element data - string npcName = string.Empty; - var edm = BrewMonster.ElementDataManProvider.GetElementDataMan(); - if (edm != null) - { - if (edm.essence_id_data_type_map.TryGetValue((uint)nANPC, out var dtype) - && dtype == DATA_TYPE.DT_NPC_ESSENCE - && edm.essence_id_data_map.TryGetValue((uint)nANPC, out var obj) - && obj is NPC_ESSENCE npc) - { - npcName = npc.Name; - } - } - if (string.IsNullOrEmpty(npcName)) npcName = nANPC.ToString(); - - // Append to content - var sb = new System.Text.StringBuilder(); - sb.Append(GetStringFromTable(7621)); - sb.Append(npcName); - sb.Append("\n"); - // if (m_pTxt_Content != null) - // { - // Debug.Log($"Award NPC: {sb.ToString()}"); - // m_pTxt_QuestItem.text += sb.ToString(); - // } - - strText += sb.ToString(); - return ret; } + + // Lookup NPC name from element data + string npcName = string.Empty; + var edm = BrewMonster.ElementDataManProvider.GetElementDataMan(); + if (edm != null) + { + if (edm.essence_id_data_type_map.TryGetValue((uint)nANPC, out var dtype) + && dtype == DATA_TYPE.DT_NPC_ESSENCE + && edm.essence_id_data_map.TryGetValue((uint)nANPC, out var obj) + && obj is NPC_ESSENCE npc) + { + npcName = npc.Name; + } + } + if (string.IsNullOrEmpty(npcName)) npcName = nANPC.ToString(); + + // Append to content, add clickable link if coordinates found (like enumEICoord in C++) // 追加到内容,如果找到坐标则添加可点击链接(如C++中的enumEICoord) + var sb = new System.Text.StringBuilder(); + sb.Append(GetStringFromTable(7621)); + + // Add NPC name as clickable link if coordinates found (like enumEICoord in C++) // 如果找到坐标,将NPC名称添加为可点击链接(如C++中的enumEICoord) + // In C++, it always creates a clickable link if coordinates are found // 在C++中,如果找到坐标,它总是创建一个可点击链接 + // For force navigate tasks, we'll trigger navigation when clicked // 对于强制导航任务,点击时将触发导航 + // Always create clickable link (even if coords aren't known yet). + // 总是创建可点击链接(即使暂时不知道坐标)。 + sb.Append($"{npcName}"); + sb.Append("\n"); + + strText += sb.ToString(); + + return ret; + } // Update completion conditions (monsters, players, gold, level/reincarnation/realm) private void UpdateCompleteCondition(ref string strText, ref string strHint, Task_State_info tsi) { @@ -1322,24 +1483,10 @@ namespace BrewMonster.Scripts.Task.UI { // strName = ModelRenderer.Scripts.Common.ByteToStringUtils.UshortArrayToUnicodeString(me.name); - bool bFind = false; - A3DVECTOR3 vPos = CECUIHelper.GetTaskObjectCoordinates((int)id, ref bFind); - - // TODO: serialize position info if found - // ACHAR szPos[100]; - // EditBoxItemBase item(enumEICoord); - // item.SetName(pMonster->name); - if (bFind) - { - // a_sprintf(szPos, _AL("%f %f %f %d"), vPos.x, vPos.y, vPos.z, id); - // item.SetInfo(szPos); - // item.SetColor(A3DCOLORRGB(0, 255, 0)); - // strName = (ACHAR)AUICOMMON_ITEM_CODE_START + item.Serialize(); - } - else - { - strName = ByteToStringUtils.UshortArrayToUnicodeString(pMonster.name); - } + // Always create clickable link (even if coords aren't known yet). + // 总是创建可点击链接(即使暂时不知道坐标)。 + string monsterName = ByteToStringUtils.UshortArrayToUnicodeString(pMonster.name); + strName = $"{monsterName}"; } // Build description for this monster requirement @@ -1468,9 +1615,34 @@ namespace BrewMonster.Scripts.Task.UI string itemName = EC_IvtrItemUtils.Instance.ResolveItemName(itemTid); if (string.IsNullOrEmpty(itemName)) itemName = $"Item {itemTid}"; + // Find coordinates for item (like C++ GetTaskObjectCoordinates) // 查找物品的坐标(如C++ GetTaskObjectCoordinates) + int search_id = 0; + if (pTempl.m_FixedData.m_enumMethod != (uint)TaskCompletionMethod.enumTMKillPlayer) + { + int id = (int)tsi.m_ItemsWanted[i].m_ulMonsterId; + if (id > 0) + { + search_id = id; + } + else + { + // TODO: Search for mine essence if needed // TODO: 如果需要,搜索矿点精华 + // const MINE_ESSENCE* pMine = SearchTaskMine(idTask); + // if(pMine) search_id = pMine->id; + } + } + + // Always create clickable link for the target (search_id). + // 总是为目标(search_id)创建可点击链接。 + string displayName = itemName; + if (search_id > 0) + { + displayName = $"{itemName}"; + } + // Compose line: name and progress (gained/toGet) // 组合文本:名称与进度(已获得/所需) - string strTemp = Format(GetStringFromTable(7625), itemName, + string strTemp = Format(GetStringFromTable(7625), displayName, tsi.m_ItemsWanted[i].m_ulItemsGained, tsi.m_ItemsWanted[i].m_ulItemsToGet); diff --git a/Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs b/Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs index 4d8fb15f3d..82e6be353a 100644 --- a/Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs +++ b/Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs @@ -1,4 +1,7 @@ +using BrewMonster.Network; +using BrewMonster.Managers; using BrewMonster.Scripts.Task; +using BrewMonster.Scripts; using CSNetwork.GPDataType; namespace BrewMonster.Scripts.UI @@ -10,7 +13,65 @@ namespace BrewMonster.Scripts.UI public static A3DVECTOR3 GetTaskObjectCoordinates(int id, ref bool in_table) { in_table = false; - return new A3DVECTOR3(0); + + // 0) If the id is actually a runtime object id (npc/player/matter), fetch it from world directly. + // IMPORTANT: GPDataTypeHelper.ISPLAYERID returns true for ANY positive int, so we must not use it to + // decide whether something is an "object id". Use a heuristic instead. + // 0) 如果这个id其实是运行时对象ID(NPC/玩家/物品),直接从World取坐标。 + // 重要:ISPLAYERID 对任何正整数都为 true,不能用它判断“是否对象ID”,这里用启发式判断。 + bool isLikelyRuntimeObjectId = + GPDataTypeHelper.ISNPCID(id) || + GPDataTypeHelper.ISMATTERID(id) || + (id > 100000000); // player ids are typically huge; template ids are usually small + + var world = EC_Game.GetGameRun()?.GetWorld(); + if (world != null && isLikelyRuntimeObjectId) + { + var obj = world.GetObject(id, 0); + if (obj != null) + { + var objPos = obj.transform.position; + in_table = true; + return new A3DVECTOR3(objPos.x, objPos.y, objPos.z); + } + } + + // 1) Try live NPC/Monster in scene by template id (best match to "click name -> go to that entity") + // 1) 先尝试在场景中按模板ID查找活体NPC/怪物(最符合“点名字就去找它”) + var npcMan = EC_ManMessageMono.Instance != null ? EC_ManMessageMono.Instance.CECNPCMan : null; + if (npcMan != null) + { + var npc = npcMan.FindNPCByTemplateID(id); + if (npc != null) + { + UnityEngine.Vector3 npcPos = npc.transform.position; + in_table = true; + return new A3DVECTOR3(npcPos.x, npcPos.y, npcPos.z); + } + } + + // Fallback to task_npc table (C++: ATaskTemplMan::GetTaskNPCInfo) + // 回退到task_npc表(C++:ATaskTemplMan::GetTaskNPCInfo) + ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan(); + if (pMan != null && pMan.TryGetTaskNPCInfo((uint)id, out NPC_INFO info)) + { + // NOTE: Keep original PW coordinate mapping: ret.Set(x, z, y) + // 注意:保持原版坐标映射:ret.Set(x, z, y) + in_table = true; + return new A3DVECTOR3(info.x, info.z, info.y); + } + + // Fallback to coord_data.txt (C++: Configs/Coord_data.txt via CECGame::GetObjectCoord) + // 回退到 coord_data.txt(C++:Configs/Coord_data.txt,通过 CECGame::GetObjectCoord) + if (BrewMonster.Network.EC_Game.TryGetFirstObjectCoord(id.ToString(), out var coordPos, out var mapName)) + { + UnityEngine.Debug.Log($"[CECUIHelper] GetTaskObjectCoordinates: Resolved id={id} via coord_data.txt map={mapName} pos=({coordPos.x:F2},{coordPos.y:F2},{coordPos.z:F2})"); + in_table = true; + return new A3DVECTOR3(coordPos.x, coordPos.y, coordPos.z); + } + + UnityEngine.Debug.LogWarning($"[CECUIHelper] GetTaskObjectCoordinates: Not found for id={id} (isLikelyRuntimeObjectId={isLikelyRuntimeObjectId})."); + return new A3DVECTOR3(0); // TODO: Implement this method properly // A3DVECTOR3 ret(0.f); @@ -28,5 +89,101 @@ namespace BrewMonster.Scripts.UI // } // return ret; } + + // Follow coord like C++ CECUIHelper::FollowCoord(enumEICoord, taskId) + // 像C++的CECUIHelper::FollowCoord(enumEICoord, taskId)一样跟随坐标 + public static bool FollowCoord(int id, int taskId) + { + // Resolve coordinates + bool inTable = false; + A3DVECTOR3 vPos = GetTaskObjectCoordinates(id, ref inTable); + if (!inTable) + { + // Fallback: use task regions if available (move to the center of the first region) + // 回退:如果任务有区域信息,则移动到第一个区域的中心 + if (taskId > 0) + { + var taskMan = EC_Game.GetTaskTemplateMan(); + var templ = taskMan != null ? taskMan.GetTaskTemplByID((uint)taskId) : null; + var world = EC_Game.GetGameRun()?.GetWorld(); + int curWorldId = world != null ? world.GetInstanceID() : 0; + + if (templ != null) + { + // Helper local function: pick first region center if in current world + bool TryUseRegion(uint worldId, uint cnt, BrewMonster.Scripts.Task.Task_Region[] regions, string tag, out A3DVECTOR3 pos) + { + pos = new A3DVECTOR3(0); + if (cnt == 0 || regions == null || regions.Length == 0) return false; + if (curWorldId != 0 && worldId != 0 && worldId != (uint)curWorldId) return false; + + var r = regions[0]; + float cx = (r.zvMin.x + r.zvMax.x) * 0.5f; + float cy = (r.zvMin.y + r.zvMax.y) * 0.5f; + float cz = (r.zvMin.z + r.zvMax.z) * 0.5f; + pos = new A3DVECTOR3(cx, cy, cz); + UnityEngine.Debug.Log($"[CECUIHelper] FollowCoord: Fallback {tag} region center=({cx:F2},{cy:F2},{cz:F2}) worldId={worldId} curWorldId={curWorldId}"); + return true; + } + + // 1) Deliver zone (often where quest giver is) + if (templ.m_FixedData.m_bDelvInZone && + TryUseRegion(templ.m_FixedData.m_ulDelvWorld, templ.m_FixedData.m_ulDelvRegionCnt, templ.m_FixedData.m_pDelvRegion, "DelvInZone", out vPos)) + { + inTable = true; + } + // 2) Reach-site regions (for reach-site tasks / also can be used as guidance) + else if (TryUseRegion(templ.m_FixedData.m_ulReachSiteId, templ.m_FixedData.m_ulReachSiteCnt, templ.m_FixedData.m_pReachSite, "ReachSite", out vPos)) + { + inTable = true; + } + // 3) Enter region / leave region zones (some tasks use these to define where objectives happen) + else if (TryUseRegion(templ.m_FixedData.m_ulEnterRegionWorld, templ.m_FixedData.m_ulEnterRegionCnt, templ.m_FixedData.m_pEnterRegion, "EnterRegion", out vPos)) + { + inTable = true; + } + else if (TryUseRegion(templ.m_FixedData.m_ulLeaveRegionWorld, templ.m_FixedData.m_ulLeaveRegionCnt, templ.m_FixedData.m_pLeaveRegion, "LeaveRegion", out vPos)) + { + inTable = true; + } + } + } + + if (!inTable) + { + UnityEngine.Debug.LogWarning($"[CECUIHelper] FollowCoord: No coordinates for id={id}, taskId={taskId} (will not move)"); + return false; + } + } + + // Start auto-move work to destination (this is what actually moves the player in this project) + // 启动自动移动工作(这才是本项目中真正驱动角色移动的系统) + CECHostPlayer host = EC_Game.GetGameRun()?.GetHostPlayer(); + if (host == null) + { + UnityEngine.Debug.LogError($"[CECUIHelper] FollowCoord: Host player is null"); + return false; + } + + CECHPWorkMan wm = host.GetWorkMan(); + if (wm == null) + { + UnityEngine.Debug.LogError($"[CECUIHelper] FollowCoord: WorkMan is null"); + return false; + } + + CECHPWorkMove work = wm.CreateWork(CECHPWork.Host_work_ID.WORK_MOVETOPOS) as CECHPWorkMove; + if (work == null) + { + UnityEngine.Debug.LogError($"[CECUIHelper] FollowCoord: Failed to create WORK_MOVETOPOS"); + return false; + } + + work.SetDestination(CECHPWorkMove.DestTypes.DEST_2D, vPos); + wm.StartWork_p2(work); + + UnityEngine.Debug.Log($"[CECUIHelper] FollowCoord: Started auto-move to ({vPos.x},{vPos.y},{vPos.z}) for id={id}, taskId={taskId}"); + return true; + } } } \ No newline at end of file diff --git a/Assets/Scripts/CECHostPlayer.cs b/Assets/Scripts/CECHostPlayer.cs index 14eb17e8ef..d3ad61433a 100644 --- a/Assets/Scripts/CECHostPlayer.cs +++ b/Assets/Scripts/CECHostPlayer.cs @@ -191,6 +191,51 @@ namespace BrewMonster return m_taskInventory; } + // Get work manager // 获取工作管理器 + public CECHPWorkMan GetWorkMan() + { + return m_pWorkMan; + } + + // Get navigate player // 获取导航玩家 + private CECHostNavigatePlayer m_pNavigatePlayer = null; + public CECHostNavigatePlayer GetNavigatePlayer() + { + if (m_pNavigatePlayer == null) + { + // TODO: Implement proper creation of navigate player + // m_pNavigatePlayer = CreateNavigatePlayer(); + } + return m_pNavigatePlayer; + } + + // Check if in force navigate state // 检查是否在强制导航状态 + public bool IsInForceNavigateState() + { + CECHostNavigatePlayer pNavigatePlayer = GetNavigatePlayer(); + if (pNavigatePlayer != null && pNavigatePlayer.GetNavigateCtrl() != null) + { + return pNavigatePlayer.GetNavigateCtrl().IsInForceNavigateState(); + } + return false; + } + + // Handle navigation event // 处理导航事件 + public void OnNaviageEvent(int task, int e) + { + UnityEngine.Debug.Log($"[CECHostPlayer] OnNaviageEvent: Task={task}, Event={e} ({(BrewMonster.Scripts.CECNavigateCtrl.NavigateEvent)e})"); + CECHostNavigatePlayer pNavigatePlayer = GetNavigatePlayer(); + if (pNavigatePlayer != null) + { + UnityEngine.Debug.Log($"[CECHostPlayer] OnNaviageEvent: Forwarding to NavigatePlayer"); + pNavigatePlayer.OnNavigateEvent(task, e); + } + else + { + UnityEngine.Debug.LogWarning($"[CECHostPlayer] OnNaviageEvent: NavigatePlayer is null"); + } + } + public EC_Inventory GetInventory(byte byPackage) { switch (byPackage)