diff --git a/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md b/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md
new file mode 100644
index 0000000000..82a8271619
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md
@@ -0,0 +1,1106 @@
+# Perfect World Navigation System - Complete Guide
+
+## Table of Contents
+1. [System Overview](#system-overview)
+2. [Architecture](#architecture)
+3. [Complete Flow: UI Click to Movement](#complete-flow-ui-click-to-movement)
+4. [Force Navigate System (Bezier-Based)](#force-navigate-system-bezier-based)
+5. [Normal Auto-Move System](#normal-auto-move-system)
+6. [Map Loading from StreamingAssets](#map-loading-from-streamingassets)
+7. [Bezier Curve Loading (navigate.dat)](#bezier-curve-loading-navigatedat)
+8. [Coordinate Resolution System](#coordinate-resolution-system)
+9. [Data Files Structure](#data-files-structure)
+10. [Key Components Deep Dive](#key-components-deep-dive)
+11. [Troubleshooting](#troubleshooting)
+
+---
+
+## System Overview
+
+The Perfect World navigation system provides **two distinct movement modes**:
+
+1. **Force Navigate (Bezier-Based)**: Predefined cinematic paths using Bezier curves for scripted sequences
+2. **Normal Auto-Move**: Pathfinding-based movement for general navigation
+
+### When Each Mode is Used
+
+- **Force Navigate**: Tasks with `enumTMSimpleClientTaskForceNavi` flag OR entries in `force_navigate.txt`
+- **Normal Auto-Move**: All other cases (NPCs, monsters, items, general coordinates)
+
+---
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ User Interface Layer │
+│ DlgTask (Task UI) → OnEventLButtonDown_Txt_QuestItem │
+└──────────────────────┬──────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ Coordinate Resolution Layer │
+│ CECUIHelper.FollowCoord() → GetTaskObjectCoordinates() │
+│ - Live objects (NPCs/Players/Matters) │
+│ - Template ID lookup (spawned NPCs) │
+│ - task_npc.data table │
+│ - coord_data.txt │
+│ - Task regions (fallback) │
+└──────────────────────┬──────────────────────────────────────┘
+ │
+ ┌─────────────┴─────────────┐
+ │ │
+ ▼ ▼
+┌──────────────────┐ ┌──────────────────┐
+│ Force Navigate │ │ Normal Auto-Move │
+│ (Bezier Path) │ │ (Pathfinding) │
+└──────────────────┘ └──────────────────┘
+```
+
+---
+
+## Complete Flow: UI Click to Movement
+
+### Step 1: User Clicks NPC/Monster Name in Task UI
+
+**File**: `Assets/PerfectWorld/Scripts/Task/UI/DlgTask.cs`
+
+When a user clicks a link in the task description (e.g., NPC name, monster name, item name):
+
+```csharp
+private void OnEventLButtonDown_Txt_QuestItem(PointerEventData eventData)
+{
+ // 1. Detect clicked link using TextMeshPro
+ int linkIndex = TMP_TextUtilities.FindIntersectingLink(
+ m_pTxt_QuestItem, eventData.position, camera);
+
+ // 2. Extract entity ID from link (format: "coord_{entityID}")
+ string linkID = m_pTxt_QuestItem.textInfo.linkInfo[linkIndex].GetLinkID();
+ int entityID = int.Parse(linkID.Substring(6)); // Remove "coord_" prefix
+
+ // 3. Trigger navigation
+ CECUIHelper.FollowCoord(entityID, currentTaskID);
+}
+```
+
+**Key Points**:
+- Links are formatted as `{name}` in rich text
+- `TMP_TextUtilities.FindIntersectingLink` detects which link was clicked
+- Camera must be `null` for `ScreenSpaceOverlay` canvases
+
+### Step 2: Coordinate Resolution
+
+**File**: `Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs`
+
+The system attempts to find coordinates in this **priority order**:
+
+```csharp
+public static A3DVECTOR3 GetTaskObjectCoordinates(int id, ref bool in_table)
+{
+ // Priority 1: Live runtime objects (spawned NPCs, players, matters)
+ var world = EC_Game.GetGameRun()?.GetWorld();
+ if (world != null && isLikelyRuntimeObjectId)
+ {
+ var obj = world.GetObject(id, 0);
+ if (obj != null)
+ return EC_Utility.ToA3DVECTOR3(obj.transform.position);
+ }
+
+ // Priority 2: Live NPC/Monster by template ID
+ var npcMan = EC_ManMessageMono.Instance?.CECNPCMan;
+ if (npcMan != null)
+ {
+ var npc = npcMan.FindNPCByTemplateID(id);
+ if (npc != null)
+ return EC_Utility.ToA3DVECTOR3(npc.transform.position);
+ }
+
+ // Priority 3: task_npc.data table
+ ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
+ if (pMan != null && pMan.TryGetTaskNPCInfo((uint)id, out NPC_INFO info))
+ {
+ return new A3DVECTOR3(info.x, info.z, info.y); // Note: y/z swap
+ }
+
+ // Priority 4: coord_data.txt
+ if (EC_Game.TryGetFirstObjectCoord(id.ToString(), out var pos, out var mapName))
+ {
+ return new A3DVECTOR3(pos.x, pos.y, pos.z);
+ }
+
+ // Priority 5: Task regions (fallback)
+ // Uses delivery zone, reach-site, enter/leave regions
+ return new A3DVECTOR3(0);
+}
+```
+
+### Step 3: Navigation Mode Decision
+
+**File**: `Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs`
+
+```csharp
+public static bool FollowCoord(int id, int taskId)
+{
+ // Check if task uses Force Navigate
+ bool shouldForceNavigate = false;
+
+ // Method 1: Task template flag
+ var templ = taskMan.GetTaskTemplByID((uint)taskId);
+ if (templ != null &&
+ templ.m_FixedData.m_enumMethod ==
+ (uint)TaskCompletionMethod.enumTMSimpleClientTaskForceNavi)
+ {
+ shouldForceNavigate = true;
+ }
+
+ // Method 2: force_navigate.txt mapping
+ if (!shouldForceNavigate)
+ {
+ var ctrl = hostPlayer.GetNavigatePlayer()?.GetNavigateCtrl();
+ if (ctrl != null && ctrl.GetNavigateInfo(taskId, ref tmp))
+ {
+ shouldForceNavigate = true;
+ }
+ }
+
+ if (shouldForceNavigate)
+ {
+ // Use Bezier-based Force Navigate
+ hostPlayer.OnNaviageEvent(taskId, EM_PREPARE);
+ hostPlayer.OnNaviageEvent(taskId, EM_BEGIN);
+ return true;
+ }
+
+ // Otherwise: Use normal auto-move with pathfinding
+ CECHPWorkMove work = wm.CreateWork(WORK_MOVETOPOS);
+ work.SetDestination(DEST_AUTOPF, vPos);
+ wm.StartWork_p2(work);
+ return true;
+}
+```
+
+---
+
+## Force Navigate System (Bezier-Based)
+
+### Configuration File: `force_navigate.txt`
+
+**Location**: `Assets/Addressable/force_navigate.txt`
+
+**Format**: CSV with 6 fields per line
+```
+taskID,bezierID,speed,angle,dir,modelPath
+32220,-1001,30,30.0,1,Models\NPC\xxx.ecm
+```
+
+**Fields**:
+- `taskID`: Task ID that triggers this navigation
+- `bezierID`: Global ID of Bezier curve in `navigate.dat` (negative = force navigate)
+- `speed`: Movement speed (units per second)
+- `angle`: Camera angle adjustment (degrees)
+- `dir`: `1` = use Bezier direction, `0` = fixed horizontal
+- `modelPath`: Model file path for navigation (optional)
+
+**Loading**:
+```csharp
+// Auto-loaded when CECNavigateCtrl is created
+public CECNavigateCtrl(CECHostPlayer pHost)
+{
+ EnsureDefaultConfigLoaded(); // Loads from Addressables
+}
+```
+
+### Phase 1: Prepare (`EM_PREPARE`)
+
+**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs`
+
+```csharp
+public void OnPrepareNavigate(int task)
+{
+ m_bForceNavigateState = true;
+ m_taskID = task;
+
+ // Load navigation config for this task
+ INFO naviInfo = new INFO();
+ if (GetNavigateInfo(task, ref naviInfo))
+ {
+ m_curNavigateInfo = naviInfo;
+
+ // Set up navigate player with model file
+ CECHostNavigatePlayer player = m_pHost.GetNavigatePlayer();
+ if (player != null)
+ {
+ player.SetNavigateModelFile(naviInfo.strModelPath);
+ player.Init();
+ }
+ }
+ else
+ {
+ // No config = give up task
+ m_pHost.GetTaskInterface().GiveUpTask((uint)m_taskID);
+ }
+}
+```
+
+**What Happens**:
+- Sets force navigate state flag
+- Loads navigation config from `force_navigate.txt`
+- Initializes navigate player with model file
+- **If config missing → gives up task** (strict C++ behavior)
+
+### Phase 2: Begin (`EM_BEGIN`)
+
+```csharp
+public void OnBeginNavigate()
+{
+ // Load Bezier curve from navigate.dat by globalID
+ CECBezier pBezier = CECBezierNavigateLoader.GetBezierObjectByGlobalID(
+ m_curNavigateInfo.bezierID);
+
+ if (pBezier == null)
+ {
+ // No Bezier = give up task (strict C++ behavior)
+ m_pHost.GetTaskInterface().GiveUpTask((uint)m_taskID);
+ return;
+ }
+
+ // Create Bezier walker
+ if (m_pBezierWalker == null)
+ m_pBezierWalker = new CECBezierWalker();
+
+ // Bind Bezier curve and configure
+ m_pBezierWalker.BindBezier(pBezier);
+ m_pBezierWalker.SetSpeed(m_curNavigateInfo.speed);
+ m_pBezierWalker.StartWalk(false, true); // no loop, forward
+
+ // Create work system entry
+ CECHPWorkNavigate pWork = m_pHost.GetWorkMan()
+ .CreateWork(WORK_FORCENAVIGATEMOVE) as CECHPWorkNavigate;
+ pWork.BeginNavigate();
+ m_pHost.GetWorkMan().StartWork_p2(pWork);
+}
+```
+
+**What Happens**:
+- Loads Bezier curve from `navigate.dat` using `bezierID`
+- Creates `CECBezierWalker` to follow the curve
+- Sets speed and starts walking
+- Creates `CECHPWorkNavigate` work to drive movement each frame
+
+### Phase 3: Movement Loop (`Tick`)
+
+**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs`
+
+Each frame, `CECHPWorkNavigate.Tick()` is called:
+
+```csharp
+public override bool Tick(float dwDeltaTime)
+{
+ CECBezierWalker pBezierWalker = /* get from navigate ctrl */;
+
+ if (!m_bMove || pBezierWalker == null) return true;
+
+ // Get frame delta time in milliseconds
+ int dwRealTime = (int)EC_Game.GetRealTickTime();
+
+ if (pBezierWalker.IsWalking())
+ {
+ // Update Bezier walker position along curve
+ pBezierWalker.Tick(dwRealTime);
+
+ // Get position and direction from Bezier curve
+ A3DVECTOR3 vCurPos = pBezierWalker.GetPos();
+ A3DVECTOR3 vDir = pBezierWalker.GetDir();
+ vDir.Normalize();
+
+ // Calculate rotation vectors
+ A3DVECTOR3 vRight = A3DVECTOR3.CrossProduct(g_vAxisY, vDir);
+ A3DVECTOR3 vUp = A3DVECTOR3.CrossProduct(vDir, vRight);
+ A3DVECTOR3 vDirH = A3DVECTOR3.CrossProduct(vRight, g_vAxisY);
+ vUp.Normalize();
+
+ // Apply to player
+ CECHostNavigatePlayer pClone = m_pHost.GetNavigatePlayer();
+ pClone.SetPos(vCurPos);
+
+ // Use Bezier direction or fixed horizontal based on config
+ if (naviInfo.bezierDir)
+ pClone.ChangeModelMoveDirAndUp(vDir, vUp);
+ else
+ pClone.ChangeModelMoveDirAndUp(vDirH, g_vAxisY);
+ }
+ else
+ {
+ // Bezier walker finished
+ m_bMove = false;
+ m_pHost.GetTaskInterface().SetForceNavigateFinishFlag(true);
+ }
+
+ return true;
+}
+```
+
+**Key Points**:
+- `GetRealTickTime()` returns **delta milliseconds** (not absolute time)
+- Bezier walker accumulates time and calculates position along curve
+- Position/direction come from Bezier curve calculations
+- Rotation can follow Bezier tangent or stay horizontal
+
+### Phase 4: End (`EM_END`)
+
+```csharp
+public void OnEndNavigate()
+{
+ if (!m_bForceNavigateState) return;
+
+ m_taskID = 0;
+
+ // Finish work
+ m_pHost.GetWorkMan()
+ .FinishRunningWork(WORK_FORCENAVIGATEMOVE);
+
+ m_bForceNavigateState = false;
+}
+```
+
+---
+
+## Normal Auto-Move System
+
+When **not** using Force Navigate, the system uses pathfinding:
+
+```csharp
+// In CECUIHelper.FollowCoord()
+CECHPWorkMove work = wm.CreateWork(WORK_MOVETOPOS);
+work.SetDestination(CECHPWorkMove.DestTypes.DEST_AUTOPF, vPos);
+
+// Store task NPC info for trace switching
+if (taskId > 0)
+{
+ work.SetTaskNPCInfo(id, taskId);
+}
+
+wm.StartWork_p2(work);
+```
+
+**What Happens**:
+- Creates `CECHPWorkMove` work
+- Uses **AutoPF** (intelligent pathfinding) instead of straight-line
+- Can switch to `WORK_TRACEOBJECT` when near NPCs
+- Uses pathfinding maps from `StreamingAssets/maps/{mapName}/movemap/`
+
+---
+
+## Map Loading from StreamingAssets
+
+### Directory Structure
+
+```
+Assets/StreamingAssets/maps/
+├── a61/ # Map name (from CECInstance.GetPath())
+│ ├── movemap/ # Pathfinding data
+│ │ ├── r1_1-c1_2-l0.cfg
+│ │ ├── r1_1-c1_2-l0.prmap
+│ │ ├── r1_1-c1_2-l0.pdhmap
+│ │ └── r1_1-c1_2-l0.mlu
+│ └── navigate.dat # Bezier curves for force navigate (C++ loads this)
+└── world/
+ └── ...
+```
+
+### How Maps Are Loaded
+
+**File**: `Assets/PerfectWorld/Scripts/World/EC_Instance.cs`
+
+```csharp
+public class CECInstance
+{
+ string m_strPath; // e.g., "a61"
+ int m_iRowNum = 3; // Number of map rows
+ int m_iColNum = 4; // Number of map columns
+ List m_routeFiles; // e.g., ["r1_1-c1_2-l0"]
+
+ public string GetPath() { return m_strPath; }
+ public List GetRouteFiles() { return m_routeFiles; }
+}
+```
+
+**File**: `Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs`
+
+When the world instance changes, pathfinding maps are loaded:
+
+```csharp
+public void ChangeWorldInstance(int idInstance)
+{
+ CECInstance pInst = EC_Game.GetGameRun()?.GetInstance(idInstance);
+ if (pInst == null) return;
+
+ // Get route files from instance (e.g., ["r1_1-c1_2-l0"])
+ List files = pInst.GetRouteFiles();
+
+ foreach (var fileBase in files)
+ {
+ // Parse range: "r1_1-c1_2-l0" → rowFrom=1, rowTo=1, colFrom=1, colTo=2
+ int rowFrom, rowTo, colFrom, colTo;
+ TryParseRouteFileRange(fileBase, out rowFrom, out rowTo,
+ out colFrom, out colTo);
+
+ // Build path: "maps/a61/movemap/r1_1-c1_2-l0"
+ string basePath = $"maps/{pInst.GetPath()}/movemap/{fileBase}";
+
+ // Load pathfinding agent
+ var agent = new CMoveAgent();
+ agent.Load(basePath, ResolveStreamingBytes, origin);
+
+ m_moveAgents.Add(new RangedMoveAgent { agent = agent, ... });
+ }
+}
+
+private static byte[] ResolveStreamingBytes(string key)
+{
+ // Resolves "maps/a61/movemap/r1_1-c1_2-l0.cfg" to:
+ // Application.streamingAssetsPath + "/maps/a61/movemap/r1_1-c1_2-l0.cfg"
+ string full = Path.Combine(
+ Application.streamingAssetsPath,
+ key.Replace('/', Path.DirectorySeparatorChar));
+
+ return File.Exists(full) ? File.ReadAllBytes(full) : null;
+}
+```
+
+**Key Points**:
+- Maps are loaded from `StreamingAssets/maps/{mapName}/movemap/`
+- Route files follow pattern: `r{rowFrom}_{rowTo}-c{colFrom}_{colTo}-l0`
+- Files: `.cfg`, `.prmap`, `.pdhmap`, `.mlu`
+- Loaded via `CMoveAgent.Load()` which reads from `StreamingAssets`
+
+---
+
+## Bezier Curve Loading (navigate.dat)
+
+### File Location
+
+**Original C++**: `maps/{mapName}/navigate.dat` (loaded per-map)
+**Unity Version**: `Assets/Addressable/navigate.dat` (single global file)
+
+**Note**: The Unity version currently loads a single global `navigate.dat` from Addressables. In the original C++, each map has its own `navigate.dat` file loaded when the map loads.
+
+### Binary File Format
+
+**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs`
+
+```csharp
+public static class CECBezierNavigateLoader
+{
+ private struct NAVIGATEBEZIERFILEHEADER
+ {
+ public int iVersion; // File version
+ public int iNumBezier; // Number of Bezier curves
+ }
+
+ public static bool LoadBezierNavigate(string address)
+ {
+ // Load from Addressables
+ var ta = Addressables.LoadAssetAsync(address)
+ .WaitForCompletion();
+
+ using var ms = new MemoryStream(ta.bytes, false);
+ using var br = new BinaryReader(ms);
+
+ // Read header
+ NAVIGATEBEZIERFILEHEADER header;
+ header.iVersion = br.ReadInt32();
+ header.iNumBezier = br.ReadInt32();
+
+ // Read each Bezier curve
+ for (int i = 0; i < header.iNumBezier; i++)
+ {
+ var pBezier = new CECBezier();
+ pBezier.Load(br); // Loads curve data
+
+ // Store by GlobalID
+ s_beziers[pBezier.GetGlobalID()] = pBezier;
+ }
+ }
+}
+```
+
+### Bezier Curve Binary Format
+
+**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs`
+
+```csharp
+public bool Load(BinaryReader br)
+{
+ // Header
+ uint dwVersion = br.ReadUInt32(); // BEZIER_FILE_VERSION (3)
+ m_nObjectID = br.ReadInt32();
+
+ // Version 2+: Global IDs
+ if (dwVersion >= 2)
+ {
+ m_iGlobalID = br.ReadInt32(); // Used for lookup
+ m_iNextGlobalID = br.ReadInt32(); // Next curve in chain
+ }
+
+ // Version 3+: Action name
+ if (dwVersion >= 3)
+ {
+ int len = br.ReadInt32();
+ if (len > 0)
+ {
+ byte[] buf = br.ReadBytes(len);
+ m_strActName = Encoding.GetEncoding(936).GetString(buf);
+ }
+ }
+
+ // Points array
+ int ptNum = br.ReadInt32();
+ m_pListPoint = new CECBezierPoint[ptNum];
+ for (int i = 0; i < ptNum; i++)
+ {
+ A3DVECTOR3 vPos = new A3DVECTOR3(
+ br.ReadSingle(), br.ReadSingle(), br.ReadSingle());
+ A3DVECTOR3 vDir = new A3DVECTOR3(
+ br.ReadSingle(), br.ReadSingle(), br.ReadSingle());
+ m_pListPoint[i] = new CECBezierPoint();
+ m_pListPoint[i].SetPos(vPos);
+ m_pListPoint[i].SetDir(vDir);
+ }
+
+ // Segments array
+ int segNum = br.ReadInt32();
+ m_pListSeg = new CECBezierSeg[segNum];
+ for (int i = 0; i < segNum; i++)
+ {
+ A3DVECTOR3 vAnchorHead = new A3DVECTOR3(
+ br.ReadSingle(), br.ReadSingle(), br.ReadSingle());
+ A3DVECTOR3 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;
+}
+```
+
+**Binary Structure**:
+```
+[Header]
+ int32: version (3)
+ int32: objectID
+
+[Version 2+]
+ int32: globalID
+ int32: nextGlobalID
+
+[Version 3+]
+ int32: actionNameLength
+ byte[]: actionName (CP936 encoding)
+
+[Points Array]
+ int32: pointCount
+ for each point:
+ float32[3]: position (x, y, z)
+ float32[3]: direction (x, y, z)
+
+[Segments Array]
+ int32: segmentCount
+ for each segment:
+ float32[3]: anchorHead (x, y, z)
+ float32[3]: anchorTail (x, y, z)
+ int32: headPointIndex
+ int32: tailPointIndex
+ float32: segmentLength
+```
+
+### How Bezier Curves Are Looked Up
+
+```csharp
+// When OnBeginNavigate() is called:
+CECBezier pBezier = CECBezierNavigateLoader.GetBezierObjectByGlobalID(
+ m_curNavigateInfo.bezierID);
+
+// GetBezierObjectByGlobalID:
+public static CECBezier GetBezierObjectByGlobalID(int iGlobalID)
+{
+ if (!s_loaded)
+ {
+ LoadBezierNavigate("Assets/Addressable/navigate.dat");
+ }
+
+ return s_beziers.TryGetValue(iGlobalID, out var bezier)
+ ? bezier : null;
+}
+```
+
+**Key Points**:
+- Bezier curves are stored in a `Dictionary` keyed by `GlobalID`
+- `bezierID` from `force_navigate.txt` is the `GlobalID` used for lookup
+- Negative `bezierID` values (e.g., `-1001`) indicate force navigate routes
+- Loaded **once** on first access (lazy loading)
+
+---
+
+## Coordinate Resolution System
+
+### Priority Order
+
+**File**: `Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs`
+
+```csharp
+public static A3DVECTOR3 GetTaskObjectCoordinates(int id, ref bool in_table)
+{
+ // Priority 1: Live runtime objects
+ // Check if ID is a runtime object ID (NPC, Player, Matter)
+ bool isLikelyRuntimeObjectId =
+ GPDataTypeHelper.ISNPCID(id) ||
+ GPDataTypeHelper.ISMATTERID(id) ||
+ (id > 100000000); // Player IDs are huge
+
+ if (isLikelyRuntimeObjectId)
+ {
+ var obj = world.GetObject(id, 0);
+ if (obj != null)
+ return EC_Utility.ToA3DVECTOR3(obj.transform.position);
+ }
+
+ // Priority 2: Live NPC/Monster by template ID
+ var npc = npcMan.FindNPCByTemplateID(id);
+ if (npc != null)
+ return EC_Utility.ToA3DVECTOR3(npc.transform.position);
+
+ // Priority 3: task_npc.data table
+ if (pMan.TryGetTaskNPCInfo((uint)id, out NPC_INFO info))
+ {
+ return new A3DVECTOR3(info.x, info.z, info.y); // Note: y/z swap
+ }
+
+ // Priority 4: coord_data.txt
+ if (EC_Game.TryGetFirstObjectCoord(id.ToString(), out var pos, out var mapName))
+ {
+ return new A3DVECTOR3(pos.x, pos.y, pos.z);
+ }
+
+ // Priority 5: Task regions (fallback)
+ // Uses delivery zone, reach-site, enter/leave regions
+ return new A3DVECTOR3(0);
+}
+```
+
+### Data Sources
+
+#### 1. Live Runtime Objects
+
+**Source**: `CECWorld.GetObject(id, 0)`
+
+- Spawned NPCs, players, matters in the current world
+- Uses runtime object IDs (not template IDs)
+- **Fastest** - direct world lookup
+
+#### 2. Live NPCs by Template ID
+
+**Source**: `CECNPCMan.FindNPCByTemplateID(id)`
+
+- Searches spawned NPCs/monsters matching template ID
+- Checks both active NPCs and disappeared NPCs (may still have valid positions)
+- **Good for** clicking NPC names that are currently spawned
+
+#### 3. task_npc.data Table
+
+**Source**: `ATaskTemplMan.TryGetTaskNPCInfo(uint id, out NPC_INFO info)`
+
+- Static NPC coordinates from task data
+- Loaded from binary `task_npc.data` file
+- **Note**: Coordinates use `(x, z, y)` mapping (y/z swapped)
+
+#### 4. coord_data.txt
+
+**Source**: `EC_Game.TryGetFirstObjectCoord(string targetId, out Vector3 pos, out string mapName)`
+
+**File**: `Assets/Addressable/coord_data.txt`
+
+**Format**:
+```
+ID MapName X Y Z
+44393 a61 33.37 25.08 244.49
+44618 a61 -440.90 21.00 102.72
+```
+
+**Loading**:
+```csharp
+// In EC_Game.Init()
+LoadObjectCoord(); // Loads coord_data.txt from Addressables
+
+// Parsing:
+private static void ParseCoordDataText(string text)
+{
+ // Skip header line
+ // Parse: key mapName x y z
+ string key = parts[0]; // e.g., "44393"
+ string map = parts[1]; // e.g., "a61"
+ float x = float.Parse(parts[2]);
+ float y = float.Parse(parts[3]);
+ float z = float.Parse(parts[4]);
+
+ m_CoordTab[key] = new List {
+ new OBJECT_COORD { strMap = map, vPos = new Vector3(x, y, z) }
+ };
+}
+```
+
+**Lookup**:
+```csharp
+public static bool TryGetFirstObjectCoord(string targetId,
+ out Vector3 pos, out string map)
+{
+ 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;
+}
+```
+
+#### 5. Task Regions (Fallback)
+
+**Source**: `ATaskTempl` region data
+
+If no specific coordinates found, uses task region centers:
+
+- **Delivery zone**: `m_pDelvRegion` (where quest giver is)
+- **Reach-site**: `m_pReachSite` (for reach-site tasks)
+- **Enter region**: `m_pEnterRegion` (where objectives happen)
+- **Leave region**: `m_pLeaveRegion` (exit zones)
+
+```csharp
+// Calculate region center
+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;
+```
+
+---
+
+## Data Files Structure
+
+### 1. force_navigate.txt
+
+**Location**: `Assets/Addressable/force_navigate.txt`
+
+**Format**: CSV (comma-separated, may be quoted)
+```
+"32220,-1001,30,30.0,1,Models\NPC\xxx.ecm"
+"32221,-1002,25,45.0,0,Models\NPC\yyy.ecm"
+```
+
+**Fields**:
+- `taskID`: Task ID
+- `bezierID`: Global ID in `navigate.dat` (negative = force navigate)
+- `speed`: Movement speed
+- `angle`: Camera angle (degrees)
+- `dir`: `1` = Bezier dir, `0` = fixed horizontal
+- `modelPath`: Model file path
+
+**Loading**: Auto-loaded when `CECNavigateCtrl` is created
+
+### 2. navigate.dat
+
+**Location**: `Assets/Addressable/navigate.dat` (Unity)
+**Original**: `maps/{mapName}/navigate.dat` (C++)
+
+**Format**: Binary file with Bezier curves
+
+**Structure**:
+```
+[Header: 8 bytes]
+ int32: version (3)
+ int32: numBezier
+
+[For each Bezier curve:]
+ [Bezier Header]
+ uint32: version
+ int32: objectID
+ int32: globalID (version >= 2)
+ int32: nextGlobalID (version >= 2)
+ int32: actionNameLength (version >= 3)
+ byte[]: actionName (CP936, if length > 0)
+
+ [Points Array]
+ int32: pointCount
+ for each point:
+ float32[3]: position
+ float32[3]: direction
+
+ [Segments Array]
+ int32: segmentCount
+ for each segment:
+ float32[3]: anchorHead
+ float32[3]: anchorTail
+ int32: headPointIndex
+ int32: tailPointIndex
+ float32: segmentLength
+```
+
+**Loading**: Lazy-loaded on first `GetBezierObjectByGlobalID()` call
+
+### 3. coord_data.txt
+
+**Location**: `Assets/Addressable/coord_data.txt`
+
+**Format**: Tab/space-separated text
+```
+ID MapName X Y Z
+44393 a61 33.37 25.08 244.49
+44618 a61 -440.90 21.00 102.72
+```
+
+**Loading**: Loaded in `EC_Game.Init()` via `LoadObjectCoord()`
+
+**Lookup**: `EC_Game.TryGetFirstObjectCoord(string id, ...)`
+
+### 4. task_npc.data
+
+**Location**: Binary file loaded by `ATaskTemplMan`
+
+**Format**: Binary (NPC_INFO structs)
+
+**Lookup**: `ATaskTemplMan.TryGetTaskNPCInfo(uint id, out NPC_INFO info)`
+
+**Note**: Coordinates use `(x, z, y)` mapping (y/z swapped from Unity)
+
+### 5. Map Pathfinding Files
+
+**Location**: `Assets/StreamingAssets/maps/{mapName}/movemap/`
+
+**Files**:
+- `r{rowFrom}_{rowTo}-c{colFrom}_{colTo}-l0.cfg` - Configuration
+- `r{rowFrom}_{rowTo}-c{colFrom}_{colTo}-l0.prmap` - Pathfinding map
+- `r{rowFrom}_{rowTo}-c{colFrom}_{colTo}-l0.pdhmap` - Height map
+- `r{rowFrom}_{rowTo}-c{colFrom}_{colTo}-l0.mlu` - Movement lookup
+
+**Loading**: Loaded by `CECIntelligentRoute.ChangeWorldInstance()` when map changes
+
+---
+
+## Key Components Deep Dive
+
+### CECNavigateCtrl
+
+**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs`
+
+**Responsibilities**:
+- Loads `force_navigate.txt` config
+- Manages force navigate state
+- Coordinates navigation events (PREPARE, BEGIN, END)
+- Creates and manages `CECBezierWalker`
+
+**Key Methods**:
+- `LoadConfig()` / `LoadConfigAddressable()` - Load config file
+- `GetNavigateInfo()` - Lookup config by task ID
+- `OnPrepareNavigate()` - Phase 1: Setup
+- `OnBeginNavigate()` - Phase 2: Start movement
+- `OnEndNavigate()` - Phase 3: Cleanup
+
+### CECBezierWalker
+
+**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs`
+
+**Responsibilities**:
+- Follows Bezier curves over time
+- Calculates position/direction along curve
+- Handles segment transitions and looping
+
+**Key Methods**:
+- `BindBezier()` - Attach Bezier curve
+- `SetSpeed()` - Set movement speed
+- `StartWalk()` - Begin walking along curve
+- `Tick()` - Update position based on time
+- `GetPos()` - Get current position on curve
+- `GetDir()` - Get current direction on curve
+
+**Time Calculation**:
+```csharp
+// Time accumulates in milliseconds
+m_iTimeCnt += iDeltaTime;
+
+// Calculate parameter u [0,1] for current segment
+float u = (float)(m_iTimeCnt - m_iPassSegTime) / m_iCurSegTime;
+
+// Get position from Bezier curve
+A3DVECTOR3 pos = segment.Bezier(u, bForward);
+```
+
+### CECHPWorkNavigate
+
+**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs`
+
+**Responsibilities**:
+- Work system entry point for force navigate
+- Ticks each frame to update player position
+- Applies Bezier walker results to player
+
+**Key Methods**:
+- `BeginNavigate()` - Start movement flag
+- `OnFirstTick()` - Initial camera setup
+- `Tick()` - Main movement loop
+
+### CECHostNavigatePlayer
+
+**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs`
+
+**Responsibilities**:
+- Wrapper that applies position/rotation to host player
+- Manages navigate control instance
+
+**Key Methods**:
+- `SetPos()` - Apply position to host player
+- `ChangeModelMoveDirAndUp()` - Apply rotation to host player
+- `GetNavigateCtrl()` - Access navigation control
+
+### CECBezier / CECBezierSeg / CECBezierPoint
+
+**File**: `Assets/PerfectWorld/Scripts/Managers/EC_HPWorkNavigate.cs`
+
+**Bezier Curve Structure**:
+- `CECBezier`: Container for multiple segments
+- `CECBezierSeg`: Single cubic Bezier segment
+- `CECBezierPoint`: Control point with position and direction
+
+**Bezier Calculation**:
+```csharp
+// Cubic Bezier: B(u) = (1-u)³P₀ + 3(1-u)²uP₁ + 3(1-u)u²P₂ + u³P₃
+public A3DVECTOR3 Bezier(float u, bool bForward)
+{
+ 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
+ {
+ // Reverse direction
+ pt1 = m_pListPoint[m_nTailPoint].GetPos();
+ control1 = m_vAnchorTail;
+ control2 = m_vAnchorHead;
+ pt2 = m_pListPoint[m_nHeadPoint].GetPos();
+ }
+
+ // Calculate cubic Bezier
+ float u2 = u * u;
+ float u3 = u2 * u;
+ float oneMinusU = 1.0f - u;
+ float oneMinusU2 = oneMinusU * oneMinusU;
+ float oneMinusU3 = oneMinusU2 * oneMinusU;
+
+ A3DVECTOR3 pos;
+ pos.x = oneMinusU3 * pt1.x + 3.0f * oneMinusU2 * u * control1.x +
+ 3.0f * oneMinusU * u2 * control2.x + u3 * pt2.x;
+ // ... (y, z similar)
+
+ return pos;
+}
+```
+
+### CECUIHelper
+
+**File**: `Assets/PerfectWorld/Scripts/UI/EC_UIHelper.cs`
+
+**Responsibilities**:
+- Entry point for navigation
+- Coordinate resolution
+- Navigation mode decision
+
+**Key Methods**:
+- `FollowCoord()` - Main entry point
+- `GetTaskObjectCoordinates()` - Multi-source coordinate lookup
+
+---
+
+## Troubleshooting
+
+### Player Not Moving
+
+**Check**:
+1. Is coordinate resolution working? Check logs for `GetTaskObjectCoordinates` results
+2. Is Force Navigate config loaded? Check `force_navigate.txt` parsing
+3. Is Bezier curve found? Check `GetBezierObjectByGlobalID` returns non-null
+4. Is work system running? Check `CECHPWorkNavigate.Tick()` is being called
+
+### Bezier Curve Not Found
+
+**Check**:
+1. Is `navigate.dat` loaded? Check `CECBezierNavigateLoader.s_loaded`
+2. Does `bezierID` match `GlobalID` in `navigate.dat`?
+3. Is `bezierID` negative? (Force navigate routes use negative IDs)
+
+### Coordinates Not Found
+
+**Check**:
+1. Is entity spawned? Check live object lookup
+2. Is template ID correct? Check `FindNPCByTemplateID`
+3. Is entry in `task_npc.data`? Check `TryGetTaskNPCInfo`
+4. Is entry in `coord_data.txt`? Check `TryGetFirstObjectCoord`
+5. Are task regions available? Check fallback region lookup
+
+### Link Click Not Detected
+
+**Check**:
+1. Is `raycastTarget` enabled on `TMP_Text`?
+2. Is camera correct? `ScreenSpaceOverlay` needs `camera = null`
+3. Are links formatted correctly? Must be `...`
+
+### Time Calculation Issues
+
+**Check**:
+1. Is `GetRealTickTime()` returning delta milliseconds? (not absolute seconds)
+2. Is time accumulating correctly? Check `m_iTimeCnt` in `CECBezierWalker`
+3. Are segment times calculated correctly? `segmentLength * (1000.0f / speed)`
+
+---
+
+## Summary
+
+The navigation system provides two movement modes:
+
+1. **Force Navigate**: Bezier-based cinematic movement for scripted sequences
+ - Configured in `force_navigate.txt`
+ - Uses Bezier curves from `navigate.dat`
+ - Time-based progression along curves
+
+2. **Normal Auto-Move**: Pathfinding-based movement for general navigation
+ - Uses AutoPF pathfinding
+ - Reads pathfinding maps from `StreamingAssets/maps/{mapName}/movemap/`
+ - Can switch to trace mode near NPCs
+
+**Coordinate Resolution** uses multiple fallback sources:
+1. Live runtime objects
+2. Spawned NPCs by template ID
+3. `task_npc.data` table
+4. `coord_data.txt` lookup
+5. Task region centers
+
+**Map Loading**:
+- Maps loaded from `StreamingAssets/maps/{mapName}/`
+- Pathfinding maps: `movemap/r{row}_{row}-c{col}_{col}-l0.*`
+- Bezier curves: `navigate.dat` (currently global, originally per-map)
+
+The system is designed to match the original C++ behavior exactly, with strict error handling (missing config = give up task) and time-based Bezier progression.
diff --git a/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md.meta b/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md.meta
new file mode 100644
index 0000000000..6304c64005
--- /dev/null
+++ b/Assets/PerfectWorld/Scripts/Managers/NAVIGATION_SYSTEM_GUIDE.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 2d08d7d0debb5e44e8bd42943353c125
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant: