2547 lines
95 KiB
C#
2547 lines
95 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Runtime.InteropServices;
|
||
using System.Text.RegularExpressions;
|
||
using BrewMonster.Managers;
|
||
using BrewMonster.Scripts.Managers;
|
||
using BrewMonster.Network;
|
||
using BrewMonster.Scripts.Task;
|
||
using BrewMonster.Scripts.UI;
|
||
using BrewMonster.UI;
|
||
using CSNetwork.GPDataType;
|
||
using ModelRenderer.Scripts.Common;
|
||
using ModelRenderer.Scripts.GameData;
|
||
using NUnit.Framework;
|
||
using PerfectWorld.Scripts.Task;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using UnityEngine.EventSystems;
|
||
using TMPro;
|
||
|
||
namespace BrewMonster.Scripts.Task.UI
|
||
{
|
||
/// <summary>
|
||
/// This is DlgTask.cpp
|
||
/// </summary>
|
||
public class DlgTask : AUIDialog
|
||
{
|
||
#if UNITY_EDITOR
|
||
[ContextMenu("Generate Tasks")]
|
||
public void TestUpdateTask()
|
||
{
|
||
UpdateTask(-1);
|
||
}
|
||
#endif
|
||
|
||
// Keep original macro as constant for array sizing
|
||
public const int CDLGTASK_AWARDITEM_MAX = 8;
|
||
public const int TickRate = 1000;
|
||
// ===== Nested structs (converted from C++), keep naming, public with explicit layout =====
|
||
|
||
// [中文] 任务目标位置
|
||
// [English] Task object position
|
||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||
public struct TASK_OBJECT_POS
|
||
{
|
||
public int x;
|
||
public int y;
|
||
public int z;
|
||
public int mapid;
|
||
}
|
||
|
||
|
||
// [中文] 任务完成时间
|
||
// [English] Task finished time
|
||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||
public struct TASK_FINISHED_TIME
|
||
{
|
||
public int iTaskID;
|
||
public uint dwTime; // DWORD -> uint
|
||
}
|
||
|
||
// [中文] ������������
|
||
// [English] Type grouping node
|
||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||
public struct TypeNode
|
||
{
|
||
public uint type; // DWORD -> uint
|
||
public GameObject item; // P_AUITREEVIEW_ITEM -> GameObject
|
||
}
|
||
|
||
// [中文] ��������ȼ�����
|
||
// [English] Level priority node
|
||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||
public struct LevelNode
|
||
{
|
||
public int level;
|
||
public GameObject item; // P_AUITREEVIEW_ITEM -> GameObject
|
||
}
|
||
|
||
// ===== Converted member variables (keep original naming) =====
|
||
|
||
// protected:
|
||
protected int m_idLastTask;
|
||
protected int m_idSelTask;
|
||
|
||
protected bool m_bTraceNew;
|
||
protected bool m_bShowTrace;
|
||
|
||
protected int m_iType;
|
||
|
||
[SerializeField] protected TMP_Text _nameTaskText;
|
||
[SerializeField] protected TMP_Text m_pTxt_QuestNO; // PAUILABEL -> TMP_Text
|
||
[SerializeField] protected TaskTreeView m_pTv_Quest; // PAUITREEVIEW -> GameObject container
|
||
[SerializeField] protected TMP_Text m_pTxt_Content; // PAUITEXTAREA -> TMP_Text
|
||
[SerializeField] protected TMP_Text m_pTxt_QuestItem; // PAUITEXTAREA -> TMP_Text
|
||
[SerializeField] protected Button m_pBtn_Abandon; // PAUISTILLIMAGEBUTTON -> Button
|
||
[SerializeField] protected Button m_pBtn_MainQuest; // PAUISTILLIMAGEBUTTON -> Button
|
||
[SerializeField] protected Button m_pBtn_NormalQuest; // PAUISTILLIMAGEBUTTON -> Button
|
||
[SerializeField] protected Button m_pBtn_SearchQuest; // PAUISTILLIMAGEBUTTON -> Button
|
||
[SerializeField] protected Button m_pBtn_HaveQuest; // PAUISTILLIMAGEBUTTON -> Button
|
||
[SerializeField] protected Toggle m_pTog_bShowTrace; // PAUISTILLIMAGEBUTTON -> Button
|
||
[SerializeField] protected Button m_pBtn_FinishTask; // PAUISTILLIMAGEBUTTON -> Button
|
||
[SerializeField] protected TMP_Text m_pTxt_BaseAward; // PAUILABEL -> TMP_Text
|
||
|
||
|
||
[Space(10)]
|
||
[SerializeField] protected Button Btn_TreasureMap;
|
||
[SerializeField] protected Button Btn_Focus;
|
||
[SerializeField] protected GameObject Lab_QuestNO; // the title label of m_pTxt_QuestNO
|
||
|
||
// PAUIIMAGEPICTURE m_pImg_Item[CDLGTASK_AWARDITEM_MAX];
|
||
// Use fixed-size array semantics via initialization length
|
||
// [中文] 奖励物品图片数组
|
||
// [English] Award item images array
|
||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = CDLGTASK_AWARDITEM_MAX)]
|
||
[SerializeField] protected Image[] m_pImg_Item = new Image[CDLGTASK_AWARDITEM_MAX];
|
||
protected uint m_ImgCount => (uint)m_pImg_Item.Length; // unsigned int -> uint
|
||
|
||
[SerializeField] protected Button m_pBtn_GotoNPC; // PAUISTILLIMAGEBUTTON -> Button
|
||
[SerializeField] protected GameObject m_pQuickBuyTrigger; // CECQuickBuyPopActivityTrigger* -> GameObject
|
||
|
||
// private:
|
||
private static List<int> m_vecTasksUnFinish = new List<int>();
|
||
private static List<int> m_vecTasksCanFinish = new List<int>();
|
||
private static List<int> m_vecTasksCanGet = new List<int>();
|
||
|
||
// [中文] 目标坐标集合
|
||
// [English] Target coordinates collection
|
||
private static List<OBJECT_COORD> m_TargetCoord = new List<OBJECT_COORD>();
|
||
|
||
private static string m_strTraceName = string.Empty; // ACString -> string
|
||
|
||
// [中文] 任务相关矿点映射
|
||
// [English] Mine map related to tasks
|
||
private static Dictionary<int, object> m_TaskMines = new Dictionary<int, object>(); // MINE_ESSENCE* -> object
|
||
|
||
// [中文] 任务跟踪计时器
|
||
// [English] Task trace counter
|
||
private CECCounter m_TaskTraceCounter = new (); // CECCounter -> object placeholder
|
||
|
||
// ===== Time-gated task UI refresh (search list) =====
|
||
// Timetable/time-window tasks become available/unavailable as server time moves.
|
||
// The original C++ client periodically re-evaluates prerequisites; in this port we refresh the search list
|
||
// at a low frequency while the Search view is open so players can see time-gated tasks appear/disappear.
|
||
private uint _lastSearchRefreshMinuteKey = uint.MaxValue;
|
||
private uint _pendingReselectTaskId = 0;
|
||
|
||
// Active-task timer refresh (wait-time / time-limit / protect-time)
|
||
private float _nextActiveTimerUiRefreshAt = 0f;
|
||
|
||
#region Unity METHODS
|
||
|
||
private new void OnEnable()
|
||
{
|
||
OnShowDialog();
|
||
OnCommand_havequest();
|
||
}
|
||
private new void OnDisable()
|
||
{
|
||
OnHideDialog();
|
||
}
|
||
|
||
private new void Awake()
|
||
{
|
||
EventBus.Subscribe<TaskItemClickEvent>(evt =>
|
||
{
|
||
OnEventLButtonDown_Tv_Quest(evt.Data);
|
||
});
|
||
|
||
m_pBtn_HaveQuest.onClick.AddListener(OnCommand_havequest);
|
||
m_pBtn_SearchQuest.onClick.AddListener(OnCommand_searchquest);
|
||
m_pBtn_Abandon.onClick.AddListener(OnCommand_abandon);
|
||
Btn_Focus.onClick.AddListener(() => OnCommand_focus());
|
||
if (m_pTog_bShowTrace)
|
||
{
|
||
m_pTog_bShowTrace.onValueChanged.AddListener((state) => OnCommand_showtrace(null,state));
|
||
}
|
||
|
||
// 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<EventTrigger>();
|
||
if (trigger == null)
|
||
{
|
||
trigger = m_pTxt_QuestItem.gameObject.AddComponent<EventTrigger>();
|
||
}
|
||
|
||
// 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);
|
||
|
||
}
|
||
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();
|
||
|
||
// Debug: verify TMP parsed <link> 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<Canvas>();
|
||
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
|
||
}
|
||
public static void SetTracePosition(List<OBJECT_COORD> targetPos, string targetName)
|
||
{
|
||
CECUIHelper.FollowCoord(targetPos, targetName);
|
||
}
|
||
public static void SetTraceNpc(int entityID, int taskID)
|
||
{
|
||
CECUIHelper.FollowCoord(entityID, taskID);
|
||
}
|
||
|
||
|
||
#endregion
|
||
|
||
#region PUBLIC METHODS
|
||
|
||
public void OnCommand_searchquest()
|
||
{
|
||
// if (m_szName != "Win_Quest") return;
|
||
m_iType = 1;
|
||
// TODO
|
||
// PAUIOBJECT pObj = GetDlgItem("Img_New");
|
||
// if (pObj && IsShow()) pObj->Show(false);
|
||
// if(GetDlgItem("Lab_Trace")) GetDlgItem("Lab_Trace")->Show(false);
|
||
m_pBtn_Abandon.gameObject.SetActive(false);
|
||
Btn_Focus.gameObject.SetActive(false);
|
||
Lab_QuestNO.gameObject.SetActive(false);
|
||
m_pTxt_QuestNO.gameObject.SetActive(false);
|
||
m_pBtn_SearchQuest.interactable = false;
|
||
m_pBtn_HaveQuest.interactable = true;
|
||
SearchForTask();
|
||
}
|
||
public void OnCommand_havequest()
|
||
{
|
||
m_iType = 0;
|
||
// TODO: if(GetDlgItem("Lab_Trace")) GetDlgItem("Lab_Trace")->Show(true);
|
||
m_pBtn_Abandon.gameObject.SetActive(true);
|
||
Btn_Focus.gameObject.SetActive(true);
|
||
Lab_QuestNO.gameObject.SetActive(true);
|
||
m_pTxt_QuestNO.gameObject.SetActive(true);
|
||
m_pBtn_SearchQuest.interactable = true;
|
||
m_pBtn_HaveQuest.interactable = false;
|
||
UpdateTask();
|
||
RefreshVecTasksCanGet();
|
||
}
|
||
public void OnCommand_showtrace(string szCommand,bool state) {
|
||
m_bShowTrace = state;
|
||
Debug.Log($"[DlgTask] OnCommand_showtrace: state={m_bShowTrace}");
|
||
if (state)
|
||
RebuildAcceptableQuestCache();
|
||
ShowTraceDialog();
|
||
}
|
||
public void TraceTask(uint idTask)
|
||
{
|
||
if (IsPQTaskOrSubTask((int)idTask))
|
||
return;
|
||
m_bShowTrace = true;
|
||
CECTaskInterface pTask = GetHostPlayer().GetTaskInterface();
|
||
bool bFinishedTask = pTask.CanFinishTask(idTask);
|
||
List<int> vec_task = bFinishedTask ? m_vecTasksCanFinish : m_vecTasksUnFinish;
|
||
if (IsTaskTraceable(idTask))
|
||
{
|
||
if (!vec_task.Contains((int)idTask))
|
||
vec_task.Add((int)idTask);
|
||
else
|
||
{
|
||
vec_task.Remove((int)idTask);
|
||
}
|
||
}
|
||
Debug.Log($"[DlgTask] TraceTask: idTask={idTask}, task name={pTask.GetTaskTemplMan().GetTaskTemplByID((uint)idTask).GetName()}");
|
||
//get the position of idTask in pLst
|
||
int position = pTask.GetFirstSubTaskPosition(idTask);
|
||
while(position != -1)
|
||
{
|
||
int idSub = pTask.GetNextSub(ref position);
|
||
TraceTask((uint)idSub);
|
||
}
|
||
ShowTraceDialog();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Add task to trace lists if traceable (no toggle). Used when seeding trace after TASK_DATA init; mirrors TraceTask minus remove/toggle.
|
||
/// 若可追踪则加入追踪列表(无切换)。在 TASK_DATA 初始化后填充追踪时用;逻辑对齐 TraceTask 但不移除/切换。
|
||
/// </summary>
|
||
private void AddTaskToTraceListsNonToggle(uint idTask)
|
||
{
|
||
if (IsPQTaskOrSubTask((int)idTask))
|
||
return;
|
||
CECTaskInterface pTask = GetHostPlayer()?.GetTaskInterface();
|
||
if (pTask == null)
|
||
return;
|
||
bool bFinishedTask = pTask.CanFinishTask(idTask);
|
||
List<int> vec_task = bFinishedTask ? m_vecTasksCanFinish : m_vecTasksUnFinish;
|
||
if (IsTaskTraceable(idTask))
|
||
{
|
||
if (!vec_task.Contains((int)idTask))
|
||
vec_task.Add((int)idTask);
|
||
}
|
||
int position = pTask.GetFirstSubTaskPosition(idTask);
|
||
while (position != -1)
|
||
{
|
||
int idSub = pTask.GetNextSub(ref position);
|
||
AddTaskToTraceListsNonToggle((uint)idSub);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// After server task pack and templates are loaded (CECTaskInterface.Init finished), apply trace like USER_LAYOUT from server:
|
||
/// SyncTrace with a full dwTraceMask for the first 32 top-level slots, then add any further top-level tasks (mask is only 32 bits).
|
||
/// 服务端任务包与模板加载完成后调用:用满掩码对前 32 个顶层槽位 SyncTrace,再为更多顶层任务补充追踪(掩码仅 32 位)。
|
||
/// </summary>
|
||
public void SyncTraceAfterTaskDataInit(CECTaskInterface pTask)
|
||
{
|
||
if (pTask == null || GetHostPlayer()?.GetTaskInterface() != pTask)
|
||
return;
|
||
|
||
var ul = new USER_LAYOUT { bTraceAll = true };
|
||
int n = (int)pTask.GetTaskCount();
|
||
if (n > 0)
|
||
{
|
||
int bits = Mathf.Min(n, 32);
|
||
ul.dwTraceMask = bits >= 32 ? uint.MaxValue : ((1u << bits) - 1u);
|
||
}
|
||
|
||
SyncTrace(ul, true);
|
||
|
||
for (int i = 32; i < n; i++)
|
||
AddTaskToTraceListsNonToggle(pTask.GetTaskId((uint)i));
|
||
|
||
RefreshVecTasksCanGet(pTask);
|
||
RefreshTaskTrace(ul.bTraceAll);
|
||
}
|
||
|
||
public void ShowTraceDialog()
|
||
{
|
||
Debug.Log($"[DlgTask] ShowTraceDialog: m_bShowTrace={m_bShowTrace}");
|
||
if(m_bShowTrace)
|
||
{
|
||
EventBus.Publish(new UIEvent(UIEventType.ShowTrace));
|
||
}
|
||
else
|
||
{
|
||
EventBus.Publish(new UIEvent(UIEventType.HideTrace));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// TASK_SVR_NOTIFY_COMPLETE (reason 2): re-expand the quest trace from the top task (add-only, same subchain walk as TraceTask).
|
||
/// Using TraceTask() here would toggle entries off/on incorrectly; this re-seeds the active subtask chain after server advance.
|
||
/// 服务端 reason=2 任务推进/完成后,从顶层任务重新挂接追踪子链(与 TraceTask 同 walk,但只添加不切换)。
|
||
/// </summary>
|
||
public void RetraceAfterTaskServerNotifyComplete(uint notifyTaskId, bool bShowTrace)
|
||
{
|
||
var pIface = GetHostPlayer()?.GetTaskInterface();
|
||
if (pIface == null)
|
||
return;
|
||
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
|
||
ATaskTempl t = pMan?.GetTaskTemplByID(notifyTaskId);
|
||
if (t == null)
|
||
return;
|
||
uint rootId = t.GetTopTask().GetID();
|
||
if (rootId == 0)
|
||
return;
|
||
if (pIface.HasTask(rootId))
|
||
AddTaskToTraceListsNonToggle(rootId);
|
||
RefreshTaskTrace(bShowTrace);
|
||
}
|
||
public void OnCommand_focus(string szCommand="") {
|
||
var pTree = m_pTv_Quest;
|
||
var pItem = pTree?.GetSelectedItem();
|
||
if (pItem == null)
|
||
{
|
||
BMLogger.LogWarning("Cannot focus task: No task is currently selected");
|
||
return;
|
||
}
|
||
var idTask = pTree?.GetItemData(pItem);
|
||
if (idTask == 0)
|
||
{
|
||
BMLogger.LogWarning("Cannot focus task: Selected item has no valid task ID");
|
||
return;
|
||
}
|
||
m_pTog_bShowTrace.onValueChanged.Invoke(true);
|
||
TraceTask((uint)idTask);
|
||
//SwitchTaskTrace((int)idTask);
|
||
}
|
||
bool IsPQTaskOrSubTask(int idTask)
|
||
{
|
||
var pMan = EC_Game.GetTaskTemplateMan();
|
||
var pTemp = pMan?.GetTaskTemplByID((uint)idTask);
|
||
return (pTemp!=null && (pTemp.m_FixedData.m_bPQTask || pTemp.m_FixedData.m_bPQSubTask));
|
||
}
|
||
public void OnCommand_abandon()
|
||
{
|
||
// [中文] 放弃任务:发送通知到服务器 // [English] Abandon task: send notification to server
|
||
// Get the currently selected task from the tree view
|
||
var pTree = m_pTv_Quest;
|
||
var pSelectedItem = pTree?.GetSelectedItem();
|
||
if (pSelectedItem == null)
|
||
{
|
||
BMLogger.LogWarning("Cannot abandon task: No task is currently selected");
|
||
return;
|
||
}
|
||
|
||
// Get the task ID from the selected item
|
||
uint selectedTaskId = pTree.GetItemData(pSelectedItem);
|
||
if (selectedTaskId == 0)
|
||
{
|
||
BMLogger.LogWarning("Cannot abandon task: Selected item has no task ID");
|
||
return;
|
||
}
|
||
|
||
CECTaskInterface pTask = GetHostPlayer()?.GetTaskInterface();
|
||
if (pTask == null)
|
||
{
|
||
BMLogger.LogError("Cannot abandon task: TaskInterface is null");
|
||
return;
|
||
}
|
||
|
||
// Check if the task is actually active before allowing abandon
|
||
// This prevents trying to abandon a task that's already been abandoned
|
||
if (!pTask.HasTask(selectedTaskId))
|
||
{
|
||
BMLogger.LogWarning($"Cannot abandon task: Task {selectedTaskId} is not active");
|
||
return;
|
||
}
|
||
|
||
// Get the task template to find the top-level task
|
||
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
|
||
ATaskTempl pTempl = pMan?.GetTaskTemplByID(selectedTaskId);
|
||
if (pTempl == null)
|
||
{
|
||
BMLogger.LogError($"Cannot abandon task: Task template {selectedTaskId} not found");
|
||
return;
|
||
}
|
||
|
||
// Get the top-level task ID (tasks can have subtasks)
|
||
ATaskTempl pTopTask = pTempl.GetTopTask();
|
||
uint topTaskId = pTopTask != null ? pTopTask.GetID() : selectedTaskId;
|
||
|
||
// Verify the top-level task is also active
|
||
if (!pTask.HasTask(topTaskId))
|
||
{
|
||
BMLogger.LogWarning($"Cannot abandon task: Top-level task {topTaskId} is not active");
|
||
return;
|
||
}
|
||
|
||
// Remove task from trace lists immediately (optimistic update)
|
||
// This prevents the abandoned task from showing in the task trace UI
|
||
int taskIdInt = (int)topTaskId;
|
||
m_vecTasksCanFinish.Remove(taskIdInt);
|
||
m_vecTasksUnFinish.Remove(taskIdInt);
|
||
RefreshVecTasksCanGet();
|
||
|
||
// Send notification to server to abandon the currently selected task
|
||
TaskClient._notify_svr(pTask, (byte)ClientNotificationConstants.TASK_CLT_NOTIFY_CHECK_GIVEUP, (ushort)topTaskId);
|
||
|
||
// Refresh UI immediately to reflect the change
|
||
// The server confirmation will trigger another refresh, but this gives immediate feedback
|
||
RefreshTaskTrace(true);
|
||
|
||
// Clear selection if the abandoned task was selected
|
||
if (m_idSelTask == (int)topTaskId)
|
||
{
|
||
m_idSelTask = 0;
|
||
}
|
||
}
|
||
public void OnCommand_CANCEL(string szCommand) {}
|
||
public void OnCommand_TreasureMap(string szCommand) {}
|
||
public void OnCommand_FinishTask(string szCommand) {}
|
||
public void OnCommand_GotoNPC(string szCommand) {}
|
||
|
||
public void OnEventLButtonDown_Tv_Quest(uint itemData)
|
||
{
|
||
// POINT ptPos = pObj->GetPos();
|
||
// A3DVIEWPORTPARAM *p = m_pA3DEngine->GetActiveViewport()->GetParam();
|
||
// int x = GET_X_LPARAM(lParam) - ptPos.x - p->X;
|
||
// int y = GET_Y_LPARAM(lParam) - ptPos.y - p->Y;
|
||
// PAUITREEVIEW pTree = (PAUITREEVIEW)pObj;
|
||
//
|
||
// if( AUI_PRESS(VK_SHIFT) && m_iType == 0)
|
||
// {
|
||
// P_AUITREEVIEW_ITEM pItem = pTree->HitTest(x, y);
|
||
//
|
||
// if( pItem ) OnCommand_focus("focus");
|
||
// }
|
||
// P_AUITREEVIEW_ITEM pItem = pTree->GetSelectedItem();
|
||
// var pItem = m_pTv_Quest.GetItemByData(itemData);
|
||
int idTask = (int)itemData;
|
||
// int idTask(0);
|
||
// if( pItem && pTree->GetParentItem(pItem) != pTree->GetRootItem())
|
||
// idTask = pTree->GetItemData(pItem);
|
||
//
|
||
if (idTask == 0) return;
|
||
//
|
||
// if (m_szName == "Win_Quest" && CDlgAutoHelp::IsAutoHelp())
|
||
// {
|
||
// if(pTree->GetHitArea(x,y) == AUITREEVIEW_RECT_FRAME)
|
||
// CDlgWikiShortcut::PopQuestWiki(GetGameUIMan(),idTask);
|
||
// }
|
||
|
||
m_idSelTask = idTask;
|
||
}
|
||
// void OnEventMouseMove_Txt_QuestItem(WPARAM wParam, LPARAM lParam, AUIObject *pObj);
|
||
// void OnEventLButtonDown_Txt_QuestItem(WPARAM wParam, LPARAM lParam, AUIObject *pObj);
|
||
// void OnEventLButtonDown_Award_Item(WPARAM wParam, LPARAM lParam, AUIObject *pObj);
|
||
//
|
||
// void GetItemLinkItemOn(int x, int y, PAUIOBJECT pObj, AUITEXTAREA_EDITBOX_ITEM *pLink);
|
||
//
|
||
// // get formatted data
|
||
// static ACString FormatTaskText(const ACHAR* szText, A3DCOLOR background);
|
||
Color GetTaskColor(int idType)
|
||
{
|
||
// TODO: Map task type to color. Default white.
|
||
if (idType < (int)ENUM_TASK_TYPE.enumTTDaily || idType >= (int)ENUM_TASK_TYPE.enumTTEnd) {
|
||
// ASSERT(false && "wrong task type");
|
||
return Color.white;
|
||
}
|
||
Color result;
|
||
if (!EC_Utility.STRING_TO_A3DCOLOR(GetStringFromTable(idType - (int)ENUM_TASK_TYPE.enumTTDaily + 3121), out result))
|
||
{
|
||
// 解析颜色失败,返回白色
|
||
// [English] Failed to parse color, return white
|
||
result = Color.white;
|
||
}
|
||
return result;
|
||
}
|
||
// static A3DCOLOR GetTaskColor(const ATaskTempl *pTempl);
|
||
// static ACString FormatTime(int nSec, const ACString& desc, int timeLimit);
|
||
private string GetTaskNameWithColor(ATaskTempl pTempl)
|
||
{
|
||
if (pTempl == null) return string.Empty;
|
||
var type = (ENUM_TASK_TYPE)pTempl.m_FixedData.m_ulType;
|
||
string rawName = ModelRenderer.Scripts.Common.ByteToStringUtils.UshortArrayToUnicodeString(pTempl.m_FixedData.m_szName);
|
||
|
||
if (type == ENUM_TASK_TYPE.enumTTQiShaList && !string.IsNullOrEmpty(rawName) && rawName[0] == '^')
|
||
{
|
||
// 如果是七杀榜任务且已经加了颜色,则颜色不变 // If QiShaList task already has color, keep it
|
||
return rawName;
|
||
}
|
||
string strTaskName = GetTaskNameWithOutColor(pTempl);
|
||
string strColorPreFix = A3DColorToString(GetTaskColor(pTempl));
|
||
return strColorPreFix + strTaskName;
|
||
}
|
||
private string GetTaskNameWithColor(ATaskTempl pTempl, out Color color)
|
||
{
|
||
if (pTempl == null) {
|
||
color = Color.white;
|
||
return string.Empty;
|
||
}
|
||
var type = (ENUM_TASK_TYPE)pTempl.m_FixedData.m_ulType;
|
||
string rawName = ModelRenderer.Scripts.Common.ByteToStringUtils.UshortArrayToUnicodeString(pTempl.m_FixedData.m_szName);
|
||
|
||
if (type == ENUM_TASK_TYPE.enumTTQiShaList && !string.IsNullOrEmpty(rawName) && rawName[0] == '^')
|
||
{
|
||
// 如果是七杀榜任务且已经加了颜色,则颜色不变 // If QiShaList task already has color, keep it
|
||
color = GetTaskColor(pTempl);
|
||
return rawName;
|
||
}
|
||
string strTaskName = GetTaskNameWithOutColor(pTempl);
|
||
color = GetTaskColor(pTempl);
|
||
return strTaskName;
|
||
}
|
||
// static ACString GetTaskNameWithOutColor(const ATaskTempl* pTempl);
|
||
private static string GetTaskNameWithOutColor(ATaskTempl pTempl)
|
||
{
|
||
if (pTempl == null) return string.Empty;
|
||
string name = ModelRenderer.Scripts.Common.ByteToStringUtils.UshortArrayToUnicodeString(pTempl.m_FixedData.m_szName);
|
||
if (!string.IsNullOrEmpty(name) && name[0] == '^')
|
||
{
|
||
// 去掉颜色前缀(假设格式为 ^RRGGBB) // Strip color prefix (assume ^RRGGBB)
|
||
if (name.Length > 7) return name.Substring(7);
|
||
return string.Empty;
|
||
}
|
||
return name;
|
||
}
|
||
|
||
private Color GetTaskColor(ATaskTempl pTempl)
|
||
{
|
||
// TODO: Map task type/flags to color. Default white.
|
||
// [English] Map task type/flags to color. Default white.
|
||
if (pTempl == null) return UnityEngine.Color.white;
|
||
int idType = (int)(ENUM_TASK_TYPE)pTempl.m_FixedData.m_ulType;
|
||
if (idType < (int)ENUM_TASK_TYPE.enumTTDaily || idType >= (int)ENUM_TASK_TYPE.enumTTEnd) {
|
||
// ASSERT(false && "wrong task type")
|
||
// [English] wrong task type
|
||
return UnityEngine.Color.white;
|
||
}
|
||
Color result;
|
||
EC_Utility.STRING_TO_A3DCOLOR(GetStringFromTable(idType - (int)ENUM_TASK_TYPE.enumTTDaily + 3121), out result);
|
||
return result;
|
||
// return UnityEngine.Color.white;
|
||
}
|
||
|
||
private static string A3DColorToString(UnityEngine.Color c)
|
||
{
|
||
// 原代码将颜色转换为字符串前缀,这里返回空前缀以保持UI简洁 // Return empty prefix for TMP rich text compatibility
|
||
return string.Empty;
|
||
}
|
||
//
|
||
public bool Tick()
|
||
{
|
||
//RefreshTaskTrace();
|
||
// Time-window task refresh: while in Search view, refresh the list when server time crosses a minute boundary.
|
||
// This is throttled to avoid rebuilding large task lists every frame.
|
||
if (m_iType == 1)
|
||
{
|
||
var host = GetHostPlayer();
|
||
var task = host != null ? host.GetTaskInterface() : null;
|
||
if (task != null)
|
||
{
|
||
uint now = task.GetCurTime();
|
||
uint minuteKey = now / 60u;
|
||
if (minuteKey != _lastSearchRefreshMinuteKey)
|
||
{
|
||
_lastSearchRefreshMinuteKey = minuteKey;
|
||
|
||
// Preserve current selection if any, so refreshing doesn't feel disruptive.
|
||
var curItem = m_pTv_Quest != null ? m_pTv_Quest.GetSelectedItem() : null;
|
||
_pendingReselectTaskId = (curItem != null) ? m_pTv_Quest.GetItemData(curItem) : 0u;
|
||
|
||
// Rebuild available task list according to current time-based prerequisites.
|
||
SearchForTask(-1);
|
||
// Restore selection best-effort (TaskTreeView selection is driven by EventBus).
|
||
if (_pendingReselectTaskId != 0u)
|
||
{
|
||
EventBus.Publish(new TaskItemClickEvent { Data = _pendingReselectTaskId });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Active view: refresh selected task detail periodically so countdown UI updates in real time.
|
||
// (Wait-time tasks depend on m_ulTimePassed which changes with server time; without this, UI looks "stuck".)
|
||
if (m_iType == 0 && Time.unscaledTime >= _nextActiveTimerUiRefreshAt)
|
||
{
|
||
_nextActiveTimerUiRefreshAt = Time.unscaledTime + 0.5f; // 2 Hz is plenty for countdown text
|
||
|
||
var pTree = m_pTv_Quest;
|
||
var pItem = pTree != null ? pTree.GetSelectedItem() : null;
|
||
if (pItem != null && pTree.transform != pItem.transform.parent)
|
||
{
|
||
uint selectedTaskId = pTree.GetItemData(pItem);
|
||
if (selectedTaskId > 0)
|
||
{
|
||
var task = GetHostPlayer()?.GetTaskInterface();
|
||
if (task != null)
|
||
{
|
||
Task_State_info tsi = default;
|
||
task.GetTaskStateInfo(selectedTaskId, ref tsi, true);
|
||
if (tsi.m_ulWaitTime > 0 || tsi.m_ulTimeLimit > 0 || tsi.m_ulProtectTime > 0)
|
||
{
|
||
UpdateTask((int)selectedTaskId);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// if( m_szName == "Win_Quest" && IsShow() )
|
||
{
|
||
var pTree = m_pTv_Quest;
|
||
var pItem = pTree.GetSelectedItem();
|
||
|
||
if( pItem )
|
||
{
|
||
for( int i = 0; i < m_ImgCount; i++ )
|
||
m_pImg_Item[i].gameObject.SetActive(false);
|
||
m_pTxt_BaseAward.gameObject.SetActive(false);
|
||
|
||
// if( pTree->GetParentItem(pItem) != pTree->GetRootItem() )
|
||
if( pTree.transform != pItem.transform.parent )
|
||
{
|
||
if (m_iType == 0)
|
||
{
|
||
UpdateTask((int)pTree.GetItemData(pItem));
|
||
}
|
||
else if (m_iType == 1)
|
||
{
|
||
SearchForTask((int)pTree.GetItemData(pItem));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
m_idLastTask = -2;
|
||
m_pTxt_Content.SetText("");
|
||
m_pTxt_QuestItem.SetText("");
|
||
_nameTaskText.SetText("");
|
||
m_pBtn_Abandon.interactable = false;
|
||
UpdateTaskConfirm(0, false);
|
||
Btn_TreasureMap.interactable = false;
|
||
}
|
||
}
|
||
|
||
// TODO
|
||
// UpdateGotoNPC();
|
||
}
|
||
|
||
// return CDlgBase::Tick();
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
public void RefreshTaskTrace(bool bShowTrace)
|
||
{
|
||
m_bShowTrace = bShowTrace;
|
||
// Get AUIManager, fallback to GetGameUIMan() if m_pAUIManager is not set yet
|
||
// 获取AUIManager,如果m_pAUIManager尚未设置则回退到GetGameUIMan()
|
||
AUIManager auiManager = GetAUIManager();
|
||
if (auiManager == null) {
|
||
CECGameUIMan gameUIMan = GetGameUIMan();
|
||
if (gameUIMan == null) {
|
||
return;
|
||
}
|
||
auiManager = gameUIMan;
|
||
}
|
||
|
||
DlgTaskTrace pDlgTaskTrace = auiManager.GetDialog("Win_QuestMinion") as DlgTaskTrace;
|
||
if (pDlgTaskTrace == null)
|
||
return;
|
||
if (!m_bShowTrace)
|
||
{
|
||
pDlgTaskTrace.Show(false);
|
||
return;
|
||
}
|
||
ShowType showType = pDlgTaskTrace.GetShowType();
|
||
// bool bShow = showType == ShowType.ST_TRACED || showType == ShowType.ST_TITLE;
|
||
bool bShow =true;//for test
|
||
if (bShow)
|
||
{
|
||
CECHostPlayer host = GetHostPlayer();
|
||
CECTaskInterface pTask = host.GetTaskInterface();
|
||
ShowType2 showType2 = pDlgTaskTrace.GetShowType2();
|
||
List<int> tasks = new List<int>();
|
||
if(showType2 == ShowType2.ST_RECIEVED)
|
||
{
|
||
if( m_vecTasksCanFinish.Count > 0 )
|
||
{
|
||
for(int i = 0; i < m_vecTasksCanFinish.Count; i++ )
|
||
{
|
||
int idTask = m_vecTasksCanFinish[i];
|
||
bool bCanFinish = pTask.CanFinishTask((uint)idTask);
|
||
bool bHasTask = pTask.HasTask((uint)idTask);
|
||
if(!bHasTask || !bCanFinish)
|
||
{
|
||
// 任务未完成,状态变化要删除
|
||
m_vecTasksCanFinish.RemoveAt(i);
|
||
i--;
|
||
if (bHasTask && !bCanFinish) {
|
||
if (!m_vecTasksUnFinish.Contains(idTask))
|
||
m_vecTasksUnFinish.Add(idTask);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if( m_vecTasksUnFinish.Count > 0 )
|
||
{
|
||
for(int i = 0; i < m_vecTasksUnFinish.Count; i++ )
|
||
{
|
||
int idTask = m_vecTasksUnFinish[i];
|
||
bool bCanFinish = pTask.CanFinishTask((uint)idTask);
|
||
bool bHasTask = pTask.HasTask((uint)idTask);
|
||
if(!bHasTask || bCanFinish)
|
||
{
|
||
// �������û���ˣ�����״̬�仯Ҫ�Ƴ�����
|
||
m_vecTasksUnFinish.RemoveAt(i);
|
||
i--;
|
||
if (bHasTask && bCanFinish) {
|
||
if (!m_vecTasksCanFinish.Contains(idTask))
|
||
m_vecTasksCanFinish.Add(idTask);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
tasks.Capacity = m_vecTasksCanFinish.Count + m_vecTasksUnFinish.Count;
|
||
tasks.AddRange(m_vecTasksCanFinish);
|
||
tasks.AddRange(m_vecTasksUnFinish);
|
||
}
|
||
else if(showType2 == ShowType2.ST_NOT_RECIEVED)
|
||
{
|
||
// Can-get list is refreshed only on level-up, quest state change (UpdateQuestView), trace toggle on, or minimion "可接" tab — not every tick.
|
||
// 可接列表仅在升级、任务变更、打开追踪、小窗切到可接时刷新,避免每帧扫全表拖垮 FPS。
|
||
tasks.Capacity = m_vecTasksCanGet.Count;
|
||
tasks.AddRange(m_vecTasksCanGet);
|
||
}
|
||
|
||
List<ATaskTempl> titlle_list = new List<ATaskTempl>();
|
||
List<int> titletask_list = new List<int>();
|
||
//Dictionary<int, int> title_map = new Dictionary<int, int>();
|
||
if (pDlgTaskTrace.GetShowType() == ShowType.ST_TITLE)
|
||
{
|
||
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
|
||
//pMan.GetTitleTasks(pTask, titlle_list);
|
||
for (int i = 0; i < titlle_list.Count; i++) {
|
||
if (IsTaskTraceable(titlle_list[i].GetID()))
|
||
titletask_list.Add((int)titlle_list[i].GetID());
|
||
}
|
||
}
|
||
// Always refresh task trace, even when lists are empty, to clear/update the display
|
||
if (!(GetHostPlayer().IsDead() || GetHostPlayer().IsTrading() || 0 != GetHostPlayer().GetBoothState()))
|
||
{
|
||
pDlgTaskTrace.RefreshTaskTrace(tasks.ToArray(), tasks.Count, titletask_list.ToArray(), titletask_list.Count, true);
|
||
}
|
||
} else if (showType == ShowType.ST_CONTRIBUTION)
|
||
{
|
||
pDlgTaskTrace.UpdateContributionTask();
|
||
}
|
||
}
|
||
public bool IsTaskTraceable(uint idTask)
|
||
{
|
||
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
|
||
ATaskTempl pTemp = pMan.GetTaskTemplByID(idTask);
|
||
|
||
// ����������
|
||
if (IsQuestionTask(pTemp)) {
|
||
return false;
|
||
}
|
||
|
||
Task_State_info tsi = new Task_State_info();
|
||
CECTaskInterface pTask = GetHostPlayer().GetTaskInterface();
|
||
pTask.GetTaskStateInfo(idTask, ref tsi);
|
||
|
||
bool bTrace = tsi.m_ulTimeLimit > 0 ||
|
||
tsi.m_ulProtectTime > 0 ||
|
||
tsi.m_ulNPCToProtect > 0 ||
|
||
tsi.m_MonsterWanted[0].m_ulMonstersToKill > 0 ||
|
||
tsi.m_PlayerWanted[0].m_ulPlayersToKill > 0 ||
|
||
tsi.m_ItemsWanted[0].m_ulItemId > 0 ||
|
||
tsi.m_ulWaitTime > 0 ||
|
||
tsi.m_TaskCharArr._finish > 0 ||
|
||
tsi.m_ulReachLevel > 0 ||
|
||
tsi.m_ulReachRealm > 0 ||
|
||
tsi.m_ulReachReincarnation > 0;
|
||
|
||
// check the condition from template
|
||
if(!bTrace)
|
||
{
|
||
if (pTemp.m_FixedData.m_ulReachSiteCnt > 0 ||
|
||
(pTemp.m_FixedData.m_ulAwardNPC > 0 && pTask.CanFinishTask((uint)idTask))){
|
||
bTrace = true;
|
||
}
|
||
}
|
||
|
||
return bTrace;
|
||
}
|
||
|
||
public bool IsQuestionTask(ATaskTempl pTemp){
|
||
return pTemp.m_FixedData.m_ulType == (uint)ENUM_TASK_TYPE.enumTTQuestion;
|
||
}
|
||
|
||
public bool UpdateTask(int idTask = -1)
|
||
{
|
||
// Only rebuild the list if viewing "Have Quest" (m_iType == 0)
|
||
// But always allow updating specific task details regardless of view type
|
||
if (idTask < 0 && m_iType != 0)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// ATaskTemplMan *pMan = GetGame()->GetTaskTemplateMan();
|
||
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
|
||
|
||
// Host player can be null during early UI bootstrap (before InitCharacter/self-info).
|
||
// In that case just no-op; quest UI will populate once the player exists and the dialog is reopened/refreshed.
|
||
var host = EC_Game.GetGameRun()?.GetHostPlayer();
|
||
if (host == null) return true;
|
||
|
||
CECTaskInterface pTask = host.GetTaskInterface();
|
||
|
||
if (pTask == null)
|
||
{
|
||
BMLogger.LogError("No CECTaskInterface found !!!");
|
||
return false;
|
||
}
|
||
|
||
// PAUITEXTAREA pTextDesc = m_pTxt_Content;
|
||
var pTextDesc = m_pTxt_Content;
|
||
// PAUITEXTAREA pTextItem = m_pTxt_QuestItem;
|
||
var pTextItem = m_pTxt_QuestItem;
|
||
|
||
string strNewTextItem = "";
|
||
string strNewHintItem = "";
|
||
bool bLastTaskChanged = false;
|
||
|
||
// PAUIOBJECT pObj = GetDlgItem("Txt_Contribution");
|
||
// if (pObj) {
|
||
// ACString strText;
|
||
// strText.Format(_AL("%d"), GetHostPlayer()->GetWorldContribution());
|
||
// pObj->SetText(strText);
|
||
// }
|
||
|
||
if ( idTask >= 0)
|
||
{
|
||
ATaskTempl pTemp = pMan.GetTaskTemplByID((uint)idTask);
|
||
if (pTemp != null)
|
||
{
|
||
// Only update description and name if task changed
|
||
if( idTask != m_idLastTask )
|
||
{
|
||
_nameTaskText.SetText(EC_Utility.FormatForTextMeshPro(CECUIHelper.FormatCoordText(GetTaskNameWithColor(pTemp))));
|
||
//pTextDesc->SetText(FormatTaskText(pTemp->GetDescription(), pTextDesc->GetColor()));
|
||
pTextDesc.SetText(EC_Utility.FormatForTextMeshPro(CECUIHelper.FormatCoordText(pTemp.GetDescription())));
|
||
m_idLastTask = idTask;
|
||
bLastTaskChanged = true;
|
||
}
|
||
|
||
m_pBtn_Abandon.interactable = pMan.CanGiveUpTask((uint)idTask);
|
||
|
||
// Always refresh task state info to get latest progress data
|
||
// This ensures real-time updates when task progress changes
|
||
// When viewing "Have Quest" (m_iType == 0), tasks are active, so pass true to read kill counts
|
||
Task_State_info tsi = new Task_State_info();
|
||
pTask.GetTaskStateInfo((uint)idTask, ref tsi, m_iType == 0);
|
||
|
||
// Clear first
|
||
strNewTextItem = "";
|
||
|
||
// Base desc
|
||
UpdateTaskBaseDesc(ref strNewTextItem, tsi);
|
||
|
||
// Award NPC
|
||
int nANPC = (int)pTemp.GetAwardNPC();
|
||
UpdateAwardNPC(ref strNewTextItem, nANPC, idTask);
|
||
|
||
// Complete condition - always refresh to show updated progress
|
||
UpdateCompleteCondition(ref strNewTextItem, ref strNewHintItem, tsi);
|
||
|
||
// Wanted Item - always refresh to show updated item counts
|
||
UpdateItemWanted(ref strNewTextItem, tsi, idTask);
|
||
|
||
// Treasure Map
|
||
UpdateTreasureMap(ref strNewTextItem);
|
||
|
||
// Task Confirm - always refresh to update button state
|
||
UpdateTaskConfirm(idTask, pTemp.m_FixedData.m_enumFinishType == (uint)TaskFinishType.enumTFTConfirm);
|
||
|
||
// Award - always refresh to show updated award preview
|
||
Task_Award_Preview award = default;
|
||
pTask.GetTaskAwardPreview((uint)idTask, ref award);
|
||
|
||
UpdateBaseAward(award);
|
||
|
||
UpdateItemAward(award);
|
||
|
||
// GameObject pObj = GetDlgItem<GameObject>("Btn_TreasureMap");
|
||
if (Btn_TreasureMap != null)
|
||
{
|
||
Btn_TreasureMap.gameObject.
|
||
SetActive(pTemp.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMReachTreasureZone);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError($"Task {idTask} not found ATaskTempl !!!");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ClearContent(true);
|
||
|
||
if (m_pBtn_FinishTask)
|
||
m_pBtn_FinishTask.gameObject.SetActive(false);
|
||
|
||
for (int i = 0; i < pTask.GetTaskCount(); i++)
|
||
{
|
||
int id = (int)pTask.GetTaskId((uint)i);
|
||
AddTaskNode(id);
|
||
}
|
||
SortTaskNodeByType();
|
||
// Resolve all prefabs after quest loading is complete (after SetLastItem() calls are done)
|
||
// 在任务加载完成后解析所有预制体(在SetLastItem()调用完成后)
|
||
m_pTv_Quest.ResolveAllPrefabs();
|
||
string strTemp;
|
||
int iMaxTaskCount = TaskInterfaceConstants.TASK_ACTIVE_LIST_MAX_LEN;
|
||
strTemp = $"{pTask.GetTaskCount()}/{iMaxTaskCount}";
|
||
if (m_pTxt_QuestNO != null) m_pTxt_QuestNO.text = strTemp;
|
||
}
|
||
|
||
// GetGameUIMan()->ReplaceColor(&strNewTextItem, A3DCOLORRGB(255, 255, 255), pTextItem->GetColor());
|
||
SetTextItemText(strNewTextItem, pMan.GetTaskTemplByID((uint)idTask) != null && !bLastTaskChanged, strNewHintItem);
|
||
|
||
return true;
|
||
}
|
||
//
|
||
// Guard: only handle search list when current UI type is 1 (search)
|
||
public bool SearchForTask(int idTask = -1)
|
||
{
|
||
// Only process search list when in search view (m_iType == 1)
|
||
// This prevents clearing the wrong list when updating
|
||
if (m_iType != 1)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
// Setup managers and UI references
|
||
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
|
||
CECTaskInterface pTask = GetHostPlayer()?.GetTaskInterface();
|
||
var pTextDesc = m_pTxt_Content;
|
||
var pTextItem = m_pTxt_QuestItem;
|
||
|
||
// Track composed text buffers and change flag
|
||
string strNewTextItem = "";
|
||
string strNewHintItem = "";
|
||
bool bLastTaskChanged = false;
|
||
|
||
// Simplified: assume we are in quest UI context if tree exists
|
||
bool bQuestUI = m_pTv_Quest != null;
|
||
|
||
// When a concrete task id is provided
|
||
if (idTask >= 0)
|
||
{
|
||
ATaskTempl pTemp = pMan != null ? pMan.GetTaskTemplByID((uint)idTask) : null;
|
||
if (pTemp != null)
|
||
{
|
||
// Update description when the selected task changes
|
||
if (idTask != m_idLastTask)
|
||
{
|
||
_nameTaskText.SetText(EC_Utility.FormatForTextMeshPro(CECUIHelper.FormatCoordText(GetTaskNameWithColor(pTemp))));
|
||
Debug.Log("[DlgTask] SearchForTask name task: " + _nameTaskText.text);
|
||
if (pTextDesc != null) pTextDesc.SetText(EC_Utility.FormatForTextMeshPro(CECUIHelper.FormatCoordText(pTemp.GetDescription())));
|
||
m_idLastTask = idTask;
|
||
bLastTaskChanged = true;
|
||
}
|
||
|
||
// Optional: update tree item text if needed (skip if API not available)
|
||
if (bQuestUI)
|
||
{
|
||
var pItem = m_pTv_Quest.GetSelectedItem();
|
||
if (pItem != null)
|
||
{
|
||
uint id = m_pTv_Quest.GetItemData(pItem);
|
||
// NOTE: ATaskTemplMan.IsTaskToPush may be conditionally compiled; avoid hard dependency
|
||
// If needed, uncomment when method is available:
|
||
// if (pMan.IsTaskToPush((int)id)) m_pTv_Quest.SetItemText(pItem, GetTaskNameWithColor(pTemp));
|
||
}
|
||
}
|
||
|
||
// Get task state info
|
||
Task_State_info tsi = default;
|
||
if (pTask != null) pTask.GetTaskStateInfo((uint)idTask, ref tsi, false);
|
||
|
||
// Reset composed text buffer
|
||
strNewTextItem = "";
|
||
|
||
// Append: base description
|
||
UpdateTaskBaseDesc(ref strNewTextItem, tsi);
|
||
|
||
// Append: deliver NPC
|
||
UpdateDeliverNPC(ref strNewTextItem, (int)pTemp.GetDeliverNPC(), idTask);
|
||
|
||
// Append: award NPC
|
||
int nANPC = (int)pTemp.GetAwardNPC();
|
||
UpdateAwardNPC(ref strNewTextItem, nANPC, idTask);
|
||
|
||
// Append: completion conditions
|
||
UpdateCompleteCondition(ref strNewTextItem, ref strNewHintItem, tsi);
|
||
|
||
// Append: wanted items
|
||
UpdateItemWanted(ref strNewTextItem, tsi, idTask);
|
||
|
||
// Preview and show awards
|
||
Task_Award_Preview award = default;
|
||
if (pTask != null) pTask.GetTaskAwardPreview((uint)idTask, ref award);
|
||
UpdateBaseAward(award);
|
||
UpdateItemAward(award);
|
||
}
|
||
else
|
||
{
|
||
// No template found for id: clear content
|
||
m_idLastTask = -2;
|
||
if (m_pTxt_Content != null) m_pTxt_Content.SetText("");
|
||
if (m_pTxt_QuestItem != null) m_pTxt_QuestItem.SetText("");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// zhangyitian 20140521 先将可接任务列表清空,再判断是否有可接任务
|
||
// zhangyitian 20140521 First clear the available tasks list, then check if there are available tasks
|
||
// 修正了原先没有可接任务时,可接任务列表显示已接任务的问题
|
||
// Fix: prevent accepted tasks from showing when there are no available tasks
|
||
ClearContent(false);
|
||
|
||
// TaskTemplLst ttl;
|
||
List<ATaskTempl> ttl = new List<ATaskTempl>(); // TaskTemplLst -> List<ATaskTempl>
|
||
pMan.GetAvailableTasks(pTask, ttl);
|
||
// Mirror available-to-accept IDs for trace / other systems (same set as tree nodes).
|
||
// 与可接任务树节点一致,填充可接任务 ID 列表供追踪等系统使用。
|
||
m_vecTasksCanGet.Clear();
|
||
for (int j = 0; j < ttl.Count; j++)
|
||
m_vecTasksCanGet.Add((int)ttl[j].GetID());
|
||
if( ttl.Count <= 0 ) return true;
|
||
|
||
for(int i = 0; i < ttl.Count; i++ )
|
||
{
|
||
int id = (int)ttl[i].GetID();
|
||
AddTaskNode(id);
|
||
}
|
||
|
||
SortTaskNodeByType();
|
||
// Resolve all prefabs after quest loading is complete (after SetLastItem() calls are done)
|
||
// 在任务加载完成后解析所有预制体(在SetLastItem()调用完成后)
|
||
m_pTv_Quest.ResolveAllPrefabs();
|
||
|
||
// string strTemp;
|
||
// ActiveTaskList pLst = (ActiveTaskList)pTask.GetActiveTaskList();
|
||
// int iMaxTaskCount = pLst->GetMaxSimultaneousCount();
|
||
// strTemp.Format(_AL("%d/%d"), pTask->GetTaskCount(), iMaxTaskCount);
|
||
// m_pTxt_QuestNO->SetText(strTemp);
|
||
}
|
||
|
||
// Apply colors and set composed text into UI
|
||
bool hasTempl = idTask >= 0 && pMan != null && pMan.GetTaskTemplByID((uint)idTask) != null;
|
||
SetTextItemText(strNewTextItem, hasTempl && !bLastTaskChanged, strNewHintItem);
|
||
|
||
// Done
|
||
return true;
|
||
}
|
||
//
|
||
// //�������������б��������ɽ�������ѽ�������ѽ����� zhangyitian
|
||
// When task updates, the available task list also needs to be updated, otherwise the available task list won't update
|
||
public bool UpdateQuestView()
|
||
{
|
||
// Refresh the list for the current view type
|
||
// This ensures that when tasks are taken/completed/abandoned, the visible list is updated
|
||
bool result = true;
|
||
|
||
if (m_iType == 0)
|
||
{
|
||
// Refresh "Have Quest" list (taken tasks)
|
||
result = UpdateTask(-1);
|
||
}
|
||
else if (m_iType == 1)
|
||
{
|
||
// Refresh "Search Quest" list (available tasks)
|
||
result = SearchForTask(-1);
|
||
}
|
||
|
||
// Refresh the currently selected task details if one is selected
|
||
// This ensures task progress updates are reflected in real-time
|
||
var pTree = m_pTv_Quest;
|
||
var pItem = pTree?.GetSelectedItem();
|
||
|
||
if (pItem != null && pTree.transform != pItem.transform.parent)
|
||
{
|
||
uint selectedTaskId = pTree.GetItemData(pItem);
|
||
if (selectedTaskId > 0)
|
||
{
|
||
if (m_iType == 0)
|
||
{
|
||
// Refresh the selected task's details to show updated progress
|
||
UpdateTask((int)selectedTaskId);
|
||
}
|
||
else if (m_iType == 1)
|
||
{
|
||
// For search view, refresh the selected task
|
||
SearchForTask((int)selectedTaskId);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Have-quest tab: rebuild can-get cache for trace minimion. Search tab: SearchForTask(-1) already filled m_vecTasksCanGet.
|
||
// 已接任务页:重建可接缓存。搜索页:SearchForTask 已写 m_vecTasksCanGet,勿二次全表扫描。
|
||
if (m_iType == 0)
|
||
RefreshVecTasksCanGet();
|
||
|
||
return result;
|
||
}
|
||
//
|
||
// bool IsPQTaskOrSubTask(int idTask);
|
||
// bool IsTreasureMapTask(int idTask);
|
||
//
|
||
// bool TraceTask(int idTask);
|
||
|
||
public void SyncTrace(object pData, bool fromServer)
|
||
{
|
||
USER_LAYOUT pul = (USER_LAYOUT)pData;
|
||
if(fromServer)
|
||
{
|
||
m_vecTasksUnFinish.Clear();
|
||
m_vecTasksCanFinish.Clear();
|
||
|
||
uint dwTraceMask = pul.dwTraceMask;
|
||
CECTaskInterface pTask = GetHostPlayer().GetTaskInterface();
|
||
for(int i = 0; i < (int)pTask.GetTaskCount(); i++ )
|
||
{
|
||
if( (dwTraceMask & (1 << i)) != 0 )
|
||
{
|
||
TraceTask(pTask.GetTaskId((uint)i));
|
||
}
|
||
}
|
||
|
||
// store m_bShowTrace flag instead of bTraceAll flag
|
||
m_bShowTrace = pul.bTraceAll;
|
||
RefreshVecTasksCanGet();
|
||
}
|
||
else
|
||
{
|
||
pul.bTraceAll = m_bShowTrace;
|
||
|
||
uint dwTraceMask = 0;
|
||
int i = 0;
|
||
for(i=0;i<m_vecTasksCanFinish.Count;i++)
|
||
{
|
||
int index = GetTaskIndex(m_vecTasksCanFinish[i]);
|
||
if (index >= 0)
|
||
{
|
||
dwTraceMask |= (uint)(1 << index);
|
||
}
|
||
}
|
||
for(i=0;i<m_vecTasksUnFinish.Count;i++)
|
||
{
|
||
int index = GetTaskIndex(m_vecTasksUnFinish[i]);
|
||
if (index >= 0)
|
||
{
|
||
dwTraceMask |= (uint)(1 << index);
|
||
}
|
||
}
|
||
|
||
pul.dwTraceMask = dwTraceMask;
|
||
}
|
||
}
|
||
|
||
// bool IsShowTrace(){return m_bShowTrace;}
|
||
//
|
||
// typedef CECGame::ObjectCoords ObjectCoords;
|
||
// static const ObjectCoords& GetObjectCoords() { return m_TargetCoord; }
|
||
// static const ACString& GetTraceName() { return m_strTraceName; }
|
||
// static void SetTraceObjects(const ObjectCoords& objs, const ACString& name);
|
||
// static const MINE_ESSENCE* SearchTaskMine(int idTask);
|
||
//
|
||
// ACString GetKillPlayerRequirements(const Task_State_info& tsi,int iIndex);
|
||
//
|
||
// void SwitchTaskTrace(int idTask);
|
||
// void OnTaskPush(); // ���µĿɽ�����
|
||
// void OnTaskProcessUpdated(int idTask); // �ѽ�����������Ҫ��ǰ��ʾ��
|
||
// void OnTaskItemGained(int idItem);
|
||
public static MINE_ESSENCE SearchTaskMine(int idTask, out bool found)
|
||
{
|
||
if(idTask <= 0)
|
||
{
|
||
found = false;
|
||
return default;
|
||
}
|
||
|
||
if (m_TaskMines.Count == 0)
|
||
{
|
||
var pDataMan = ElementDataManProvider.GetElementDataMan();
|
||
|
||
foreach (var kv in pDataMan.essence_id_data_type_map)
|
||
{
|
||
if (kv.Value == DATA_TYPE.DT_MINE_ESSENCE && kv.Key != 0)
|
||
{
|
||
DATA_TYPE dt = DATA_TYPE.DT_INVALID;
|
||
var pMine = (MINE_ESSENCE)pDataMan.get_data_ptr(kv.Key, ID_SPACE.ID_SPACE_ESSENCE, ref dt);
|
||
m_TaskMines[(int)pMine.task_in] = pMine;
|
||
}
|
||
}
|
||
|
||
// avoid duplicated init
|
||
m_TaskMines[0] = null;
|
||
}
|
||
|
||
var itr = m_TaskMines.TryGetValue(idTask, out var value);
|
||
found =itr;
|
||
return itr ? (MINE_ESSENCE)value : default;
|
||
}
|
||
#endregion
|
||
|
||
#region PRIVATE METHODS
|
||
|
||
/// <summary>
|
||
/// Rebuild m_vecTasksCanGet from ATaskTemplMan.GetAvailableTasks (same source as Search quest tree).
|
||
/// 用 GetAvailableTasks 重建可接任务 ID 列表,与可接任务搜索树数据源一致。
|
||
/// </summary>
|
||
private void RefreshVecTasksCanGet(CECTaskInterface pTaskIfKnown = null)
|
||
{
|
||
RebuildAcceptableQuestCache(pTaskIfKnown);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Rebuilds the cached list of quests the host may accept (trace minimion "可接", etc.).
|
||
/// Safe without a DlgTask instance — call from UI manager on level-up and similar.
|
||
/// 重建玩家当前可接任务 ID 缓存;可无对话框实例,由 UI 管理器在升级等时机调用。
|
||
/// </summary>
|
||
public static void RebuildAcceptableQuestCache(CECTaskInterface pTaskIfKnown = null)
|
||
{
|
||
m_vecTasksCanGet.Clear();
|
||
CECTaskInterface pTask = pTaskIfKnown;
|
||
if (pTask == null)
|
||
{
|
||
var run = EC_Game.GetGameRun();
|
||
pTask = run?.GetHostPlayer()?.GetTaskInterface();
|
||
}
|
||
if (pTask == null) return;
|
||
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
|
||
if (pMan == null) return;
|
||
List<ATaskTempl> ttl = new List<ATaskTempl>();
|
||
pMan.GetAvailableTasks(pTask, ttl);
|
||
for (int i = 0; i < ttl.Count; i++)
|
||
m_vecTasksCanGet.Add((int)ttl[i].GetID());
|
||
}
|
||
|
||
private bool OnInitDialog()
|
||
{
|
||
// m_pTxt_QuestNO = (PAUILABEL)GetDlgItem("Txt_QuestNO");
|
||
// m_pTv_Quest = (PAUITREEVIEW)GetDlgItem("Tv_Quest");
|
||
// m_pTxt_Content = dynamic_cast<PAUITEXTAREA>(GetDlgItem("Txt_Content"));
|
||
// m_pTxt_QuestItem = dynamic_cast<PAUITEXTAREA>(GetDlgItem("Txt_QuestItem"));
|
||
// m_pBtn_Abandon = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_Abandon");
|
||
// m_pTxt_BaseAward = (PAUILABEL)GetDlgItem("Txt_BaseAward");
|
||
// m_pBtn_SearchQuest = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_SearchQuest");
|
||
// m_pBtn_HaveQuest = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_HaveQuest");
|
||
// m_pBtn_bShowTrace = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_ShowTrace");
|
||
// m_pBtn_FinishTask = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_FinishTask");
|
||
// m_pBtn_GotoNPC = (PAUISTILLIMAGEBUTTON)GetDlgItem("Btn_GotoNPC");
|
||
if (m_pBtn_FinishTask) m_pBtn_FinishTask.gameObject.SetActive(false);
|
||
if (m_pBtn_GotoNPC) m_pBtn_GotoNPC.gameObject.SetActive(false);
|
||
|
||
// TODO: Set button pushed state
|
||
// if (m_pBtn_HaveQuest != null) { /* set pushed state if needed */ }
|
||
|
||
GameObject pObj = GetDlgItem<GameObject>("Btn_TreasureMap");
|
||
if (pObj != null)
|
||
{
|
||
pObj.SetActive(false);
|
||
}
|
||
|
||
pObj = GetDlgItem<GameObject>("Img_New");
|
||
if (pObj != null)
|
||
{
|
||
pObj.SetActive(false);
|
||
}
|
||
|
||
if (m_pTxt_QuestNO != null)
|
||
{
|
||
m_pTxt_QuestNO.text = "0";
|
||
}
|
||
|
||
m_TaskTraceCounter.SetPeriod(950);
|
||
return true;
|
||
}
|
||
|
||
private T GetDlgItem<T>(string name)
|
||
{
|
||
var t = transform.Find(name);
|
||
if (t != null) return t.GetComponent<T>();
|
||
return default(T);
|
||
}
|
||
|
||
public new CECHostPlayer GetHostPlayer()
|
||
{
|
||
if(EC_Game.GetGameRun() == null)
|
||
{
|
||
BMLogger.LogError("EC_Game.GetGameRun() is null !!!");
|
||
return null;
|
||
}
|
||
|
||
if (EC_Game.GetGameRun().GetHostPlayer() == null)
|
||
{
|
||
BMLogger.LogError("EC_Game.GetHostPlayer() is null !!!");
|
||
return null;
|
||
}
|
||
|
||
return EC_Game.GetGameRun().GetHostPlayer();
|
||
}
|
||
//
|
||
// virtual bool OnInitDialog();
|
||
void OnShowDialog()
|
||
{
|
||
AUIManager auiManager = GetAUIManager();
|
||
|
||
if (m_idSelTask != 0)
|
||
UpdateTask();
|
||
if(auiManager != null)
|
||
{
|
||
m_pTog_bShowTrace.isOn = auiManager.IsDialogShow("Win_QuestMinion");
|
||
}
|
||
}
|
||
void OnHideDialog()
|
||
{
|
||
if (m_szName != "Win_Quest") return;
|
||
var pTree = m_pTv_Quest;
|
||
var pItem = pTree?.GetSelectedItem();
|
||
if( pItem )
|
||
m_idSelTask = (int)pTree.GetItemData(pItem);
|
||
else m_idSelTask = 0;
|
||
}
|
||
// virtual void OnHideDialog();
|
||
// virtual bool OnChangeLayout(PAUIOBJECT pMine, PAUIOBJECT pTheir);
|
||
// virtual void OnChangeLayoutEnd(bool bAllDone);
|
||
//
|
||
private void InsertTaskChildren(TaskTreeViewHolder pRoot, uint idTask, bool bExpand, bool bKey)
|
||
{
|
||
var pTreeTask = m_pTv_Quest;
|
||
var pMan = EC_Game.GetTaskTemplateMan();
|
||
var pTask = GetHostPlayer()?.GetTaskInterface();
|
||
if (pTreeTask == null || pMan == null || pTask == null) return;
|
||
|
||
ATaskTempl parentTempl = pMan.GetTaskTemplByID(idTask);
|
||
if (parentTempl == null) return;
|
||
|
||
ATaskTempl child = parentTempl.m_pFirstChild;
|
||
while (child != null)
|
||
{
|
||
uint id = child.m_FixedData.m_ID;
|
||
Color disPlayColor=Color.white;
|
||
string text = GetTaskNameWithColor(child,out disPlayColor);
|
||
var pItem = pTreeTask.InsertItem(text, pRoot, null);
|
||
pItem.SetItemTextColor(disPlayColor);
|
||
if (pItem != null)
|
||
{
|
||
pTreeTask.SetItemData(pItem, id);
|
||
if ((int)id == m_idSelTask)
|
||
{
|
||
if (m_pBtn_Abandon != null) m_pBtn_Abandon.interactable = true;
|
||
UpdateTask((int)id);
|
||
}
|
||
// Optional: colorize key tasks if UI supports it
|
||
}
|
||
|
||
InsertTaskChildren(pItem, id, bExpand, bKey);
|
||
child = child.m_pNextSibling;
|
||
}
|
||
|
||
}
|
||
|
||
// [中文] 仅插入“已接任务(Active)”中的子任务(基于 ActiveTaskList 的 Child/NextSbl 索引)
|
||
// [English] Insert only active subtasks (based on ActiveTaskList Child/NextSbl indices)
|
||
private void InsertActiveTaskChildren(TaskTreeViewHolder pRoot, uint idTask)
|
||
{
|
||
var pTreeTask = m_pTv_Quest;
|
||
var pMan = EC_Game.GetTaskTemplateMan();
|
||
var pTask = GetHostPlayer()?.GetTaskInterface();
|
||
if (pTreeTask == null || pMan == null || pTask == null) return;
|
||
|
||
ActiveTaskList pList = pTask.GetActiveTaskList();
|
||
if (pList == null) return;
|
||
|
||
ActiveTaskEntry parentEntry = pList.GetEntry(idTask);
|
||
if (parentEntry == null) return;
|
||
|
||
char idx = parentEntry.m_ChildIndex;
|
||
while (idx != (char)0xff)
|
||
{
|
||
int childIndex = (byte)idx;
|
||
if (childIndex < 0 || childIndex >= TaskInterfaceConstants.TASK_ACTIVE_LIST_MAX_LEN) break;
|
||
|
||
ActiveTaskEntry childEntry = pList.m_TaskEntries[childIndex];
|
||
if (childEntry == null || childEntry.m_ID == 0) break;
|
||
|
||
uint childId = childEntry.m_ID;
|
||
ATaskTempl childTempl = pMan.GetTaskTemplByID(childId);
|
||
Color disPlayColor=Color.white;
|
||
string text = childTempl != null ? GetTaskNameWithColor(childTempl,out disPlayColor) : $"Task {childId}";
|
||
var pItem = pTreeTask.InsertItem(text, pRoot, null);
|
||
pItem.SetItemTextColor(disPlayColor);
|
||
if (pItem != null)
|
||
{
|
||
pTreeTask.SetItemData(pItem, childId);
|
||
if ((int)childId == m_idSelTask)
|
||
{
|
||
if (m_pBtn_Abandon != null) m_pBtn_Abandon.interactable = true;
|
||
UpdateTask((int)childId);
|
||
}
|
||
|
||
// recurse into active children
|
||
InsertActiveTaskChildren(pItem, childId);
|
||
}
|
||
|
||
idx = childEntry.m_NextSblIndex;
|
||
}
|
||
}
|
||
|
||
private void SetTextItemText(string strNewTextItem, bool keepScrollPos, string strNewHintItem)
|
||
{
|
||
var pTextItem = m_pTxt_QuestItem;
|
||
if (pTextItem == null) return;
|
||
|
||
// Preserve scroll position if inside a ScrollRect
|
||
// UnityEngine.UI.ScrollRect scrollRect = pTextItem.GetComponentInParent<UnityEngine.UI.ScrollRect>();
|
||
// float oldNormPos = scrollRect != null ? scrollRect.verticalNormalizedPosition : 0f;
|
||
|
||
string formatted = EC_Utility.FormatForTextMeshPro(CECUIHelper.FormatCoordText(strNewTextItem ?? string.Empty));
|
||
if (!string.Equals(formatted, pTextItem.text))
|
||
{
|
||
pTextItem.text = formatted;
|
||
// TODO: apply hint to a tooltip UI if available (strNewHintItem)
|
||
}
|
||
|
||
// if (keepScrollPos && scrollRect != null)
|
||
// {
|
||
// // Restore previous scroll position
|
||
// scrollRect.verticalNormalizedPosition = oldNormPos;
|
||
// }
|
||
}
|
||
|
||
// void SetTaskText(PAUIOBJECT pObj, ACString* pStr);
|
||
|
||
int GetTaskIndex(int idTask)
|
||
{
|
||
// find the task index in task list
|
||
if(idTask <= 0)
|
||
{
|
||
return -1;
|
||
}
|
||
|
||
CECTaskInterface pTask = GetHostPlayer().GetTaskInterface();
|
||
for(int i = 0; i < (int)pTask.GetTaskCount(); i++ )
|
||
{
|
||
int curTask = pTask.GetTaskId(i);
|
||
if(idTask == curTask || pTask.CheckParent((uint)curTask, (uint)idTask))
|
||
return i;
|
||
}
|
||
|
||
return -1;
|
||
}
|
||
|
||
//
|
||
// bool IsQuest()const;
|
||
// bool IsShowHaveQuest()const;
|
||
// int GetSelectedTaskFromUI();
|
||
// bool IsTreasureMapSelected();
|
||
//
|
||
// // update task content in dialog
|
||
// update task content in dialog (converted from C++)
|
||
private void UpdateBaseAward(Task_Award_Preview award)
|
||
{
|
||
// Ported from original C++: strTemp.Format(GetStringFromTable(3201), award.m_ulGold);
|
||
var sb = new System.Text.StringBuilder();
|
||
int colCount = 0;
|
||
const int col = 3;
|
||
|
||
string strTemp;
|
||
if (award.m_ulGold > 0)
|
||
{
|
||
strTemp = Format(GetStringFromTable(3201), award.m_ulGold);
|
||
sb.Append(strTemp);
|
||
if ((++colCount) % col == 0) sb.Append("\n");
|
||
}
|
||
if (award.m_ulExp > 0)
|
||
{
|
||
strTemp = Format(GetStringFromTable(3202), award.m_ulExp);
|
||
sb.Append(strTemp);
|
||
if ((++colCount) % col == 0) sb.Append("\n");
|
||
}
|
||
if (award.m_ulSP > 0)
|
||
{
|
||
strTemp = Format(GetStringFromTable(3203), award.m_ulSP);
|
||
sb.Append(strTemp);
|
||
if ((++colCount) % col == 0) sb.Append("\n");
|
||
}
|
||
if (award.m_ulRealmExp > 0)
|
||
{
|
||
strTemp = Format(GetStringFromTable(3207), award.m_ulRealmExp);
|
||
sb.Append(strTemp);
|
||
if ((++colCount) % col == 0) sb.Append("\n");
|
||
}
|
||
if (award.m_iForceContrib > 0)
|
||
{
|
||
strTemp = Format(GetStringFromTable(3205), award.m_iForceContrib);
|
||
sb.Append(strTemp);
|
||
if ((++colCount) % col == 0) sb.Append("\n");
|
||
}
|
||
if (award.m_iForceRepu > 0)
|
||
{
|
||
strTemp = Format(GetStringFromTable(3206), award.m_iForceRepu);
|
||
sb.Append(strTemp);
|
||
if ((++colCount) % col == 0) sb.Append("\n");
|
||
}
|
||
|
||
if (sb.Length > 0 && m_pTxt_BaseAward != null)
|
||
{
|
||
m_pTxt_BaseAward.text = EC_Utility.FormatForTextMeshPro(sb.ToString());
|
||
m_pTxt_BaseAward.gameObject.SetActive(true);
|
||
}
|
||
}
|
||
|
||
private void UpdateItemAward(Task_Award_Preview award)
|
||
{
|
||
bool bShowItem = false;
|
||
if (award.m_bHasItem)
|
||
{
|
||
if (!award.m_bItemKnown)
|
||
{
|
||
if (m_pTxt_BaseAward != null && m_pTxt_BaseAward.gameObject.activeSelf)
|
||
{
|
||
string strAward = (GetStringFromTable(3204) ?? string.Empty) + "\n" + (m_pTxt_BaseAward.text ?? string.Empty);
|
||
m_pTxt_BaseAward.text = EC_Utility.FormatForTextMeshPro(strAward);
|
||
}
|
||
else if (m_pTxt_BaseAward != null)
|
||
{
|
||
m_pTxt_BaseAward.text = EC_Utility.FormatForTextMeshPro(GetStringFromTable(3204) ?? string.Empty);
|
||
m_pTxt_BaseAward.gameObject.SetActive(true);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
int max = m_pImg_Item != null ? m_pImg_Item.Length : 0;
|
||
for (int i = 0; i < max; i++)
|
||
{
|
||
if (i < award.m_ulItemTypes)
|
||
{
|
||
var img = m_pImg_Item[i];
|
||
if (img == null) continue;
|
||
var sprite = EC_IvtrItemUtils.Instance.ResolveItemIconSprite((int)award.m_ItemsId[i]);
|
||
if (sprite != null) img.sprite = sprite;
|
||
img.color = Color.white;
|
||
img.gameObject.SetActive(true);
|
||
var countLabel = img.GetComponentInChildren<TMP_Text>(true);
|
||
if (countLabel != null) countLabel.text = award.m_ItemsNum[i].ToString();
|
||
bShowItem = true;
|
||
}
|
||
else if (m_pImg_Item[i] != null)
|
||
{
|
||
m_pImg_Item[i].gameObject.SetActive(false);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// adjust the label position relative to item icons (approximate)
|
||
if (m_pTxt_BaseAward != null && m_pImg_Item != null && m_pImg_Item.Length > 0 && m_pImg_Item[0] != null)
|
||
{
|
||
var txtRT = m_pTxt_BaseAward.rectTransform;
|
||
var imgRT = m_pImg_Item[0].rectTransform;
|
||
var pos = imgRT.anchoredPosition;
|
||
var sz = imgRT.sizeDelta;
|
||
float margin = 2f;
|
||
if (bShowItem) txtRT.anchoredPosition = new Vector2(pos.x, pos.y - (sz.y + margin));
|
||
else txtRT.anchoredPosition = pos;
|
||
}
|
||
}
|
||
private void UpdateTaskBaseDesc(ref string strText, Task_State_info tsi)
|
||
{
|
||
// Build the base description text from task state
|
||
var sb = new System.Text.StringBuilder();
|
||
// NOTE: Original appended each entry in tsi.m_TaskCharArr (vector<wchar_t*>)
|
||
// In C#, this array is not directly available; content is already localized elsewhere.
|
||
|
||
// Append error message if any
|
||
if (tsi.m_ulErrCode != 0)
|
||
{
|
||
string szMsg = GetFixedMsg(tsi.m_ulErrCode);
|
||
if (!string.IsNullOrEmpty(szMsg))
|
||
{
|
||
sb.Append("<color=#ff0000>");
|
||
sb.Append(szMsg);
|
||
string strTemp;
|
||
if (tsi.m_ulErrCode == TaskInterfaceConstants.TASK_AWARD_FAIL_LEVEL_CHECK)
|
||
strTemp = Format(GetStringFromTable(7637), tsi.m_ulPremLevelMin);
|
||
else
|
||
strTemp = GetStringFromTable(807);
|
||
sb.Append(strTemp);
|
||
sb.AppendLine("</color>");
|
||
}
|
||
}
|
||
|
||
// Time limit and remaining time
|
||
if (tsi.m_ulTimeLimit > 0)
|
||
{
|
||
int nSec = (int)tsi.m_ulTimeLimit;
|
||
sb.Append(FormatTime(nSec, GetStringFromTable(245), 0));
|
||
int remain = System.Math.Max(0, (int)tsi.m_ulTimeLimit - (int)tsi.m_ulTimePassed);
|
||
sb.Append(FormatTime(remain, GetStringFromTable(246), 0));
|
||
}
|
||
|
||
// Wait time
|
||
if (tsi.m_ulWaitTime > 0)
|
||
{
|
||
int nSec = System.Math.Max(0, (int)tsi.m_ulWaitTime - (int)tsi.m_ulTimePassed);
|
||
sb.Append(FormatTime(nSec, GetStringFromTable(199), 0));
|
||
}
|
||
|
||
// Protect NPC info and timers
|
||
if (tsi.m_ulNPCToProtect > 0)
|
||
{
|
||
// Fallback text with NPC id; detailed name lookup omitted
|
||
sb.Append(Format(GetStringFromTable(257) ?? "Protect NPC: %d", tsi.m_ulNPCToProtect));
|
||
sb.Append(FormatTime((int)tsi.m_ulProtectTime, GetStringFromTable(258), 0));
|
||
int remain = System.Math.Max(0, (int)tsi.m_ulProtectTime - (int)tsi.m_ulTimePassed);
|
||
sb.Append(FormatTime(remain, GetStringFromTable(259), 0));
|
||
}
|
||
|
||
// Apply to content text
|
||
// if (m_pTxt_Content != null)
|
||
// {
|
||
// m_pTxt_Content.text += sb.ToString();
|
||
// }
|
||
|
||
strText += sb.ToString();
|
||
}
|
||
|
||
public static string FormatTime(int nSec, string desc, int timeLimit)
|
||
{
|
||
int total = System.Math.Max(0, nSec);
|
||
int h = total / 3600;
|
||
int m = (total % 3600) / 60;
|
||
int s = total % 60;
|
||
// Matches table printf "%d:%02d:%02d" (hours, zero-padded min/sec); works for durations over 24h.
|
||
string timeStr = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}:{1:D2}:{2:D2}", h, m, s);
|
||
if (string.IsNullOrEmpty(desc))
|
||
return timeStr;
|
||
const string kPrintfTime = "%d:%02d:%02d";
|
||
if (desc.IndexOf(kPrintfTime, System.StringComparison.Ordinal) >= 0)
|
||
return desc.Replace(kPrintfTime, timeStr);
|
||
return desc + timeStr;
|
||
}
|
||
|
||
// private static string GetStringFromTable(int id)
|
||
// {
|
||
// // TODO: return AUIManager.GetStringFromTable(id);
|
||
//
|
||
// // HARD CODED STRINGS
|
||
// switch (id)
|
||
// {
|
||
// case 3101:
|
||
// return "Hàng ngày";
|
||
// case 3102:
|
||
// return "Tu chân";
|
||
// case 3103:
|
||
// return "Chủ tuyến";
|
||
// case 3104:
|
||
// return "Phụ tuyến";
|
||
// case 3105:
|
||
// return "Event";
|
||
// case 3106:
|
||
// return "7 Killer List";
|
||
// case 3107:
|
||
// return "Bang hội";
|
||
// case 3108:
|
||
// return "Management";
|
||
// case 3109:
|
||
// return "Huyền thoại";
|
||
// case 3110:
|
||
// return "Câu hỏi";
|
||
// case 3201:
|
||
// return "Gold:";
|
||
// case 3202:
|
||
// return "EXP:";
|
||
// case 3203:
|
||
// return "SP:";
|
||
// case 3207:
|
||
// return "Realm EXP:";
|
||
// case 3205:
|
||
// return "Contribution:";
|
||
// case 3206:
|
||
// return "Reputation:";
|
||
// case 7621:
|
||
// return "NPC giao nhiệm vụ: ";
|
||
// default:
|
||
// return $"UnKnown_{id} ";
|
||
// }
|
||
// }
|
||
|
||
private string GetStringFromTable(uint id)
|
||
{
|
||
// return AUIManager.GetStringFromTable(id);
|
||
return GetStringFromTable((int)id);
|
||
}
|
||
|
||
private static string GetFixedMsg(uint id)
|
||
{
|
||
return BrewMonster.Network.EC_Game.GetFixedMsgs()?.GetWideString((int)id) ?? string.Empty;
|
||
}
|
||
|
||
|
||
private void UpdateDeliverNPC(ref string strText, int nDNPC, int idTask = 0)
|
||
{
|
||
// [中文] 交付NPC
|
||
// [English] Deliver NPC
|
||
if (nDNPC == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// [中文] 从元素数据中查找NPC
|
||
// [English] Lookup NPC from element data
|
||
string npcName = string.Empty;
|
||
var edm = BrewMonster.ElementDataManProvider.GetElementDataMan();
|
||
if (edm != null)
|
||
{
|
||
if (edm.essence_id_data_type_map.TryGetValue((uint)nDNPC, out var dtype)
|
||
&& dtype == DATA_TYPE.DT_NPC_ESSENCE
|
||
&& edm.essence_id_data_map.TryGetValue((uint)nDNPC, out var obj)
|
||
&& obj is NPC_ESSENCE npc)
|
||
{
|
||
npcName = npc.Name;
|
||
}
|
||
}
|
||
if (string.IsNullOrEmpty(npcName)) npcName = nDNPC.ToString();
|
||
|
||
// [中文] 追加到内容文本,如果找到坐标则添加可点击链接(如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));
|
||
|
||
// Always create clickable link (even if coords aren't known yet).
|
||
// 总是创建可点击链接(即使暂时不知道坐标)。
|
||
sb.Append($"<link=\"coord_{nDNPC}\"><color=#00FF00>{npcName}</color></link>");
|
||
|
||
sb.Append("\n");
|
||
|
||
strText += sb.ToString();
|
||
}
|
||
|
||
private A3DVECTOR3 UpdateAwardNPC(ref string strText, int nANPC, int idTask = 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, 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($"<link=\"coord_{nANPC}\"><color=#00FF00>{npcName}</color></link>");
|
||
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)
|
||
{
|
||
// Setup host reference
|
||
// 设置宿主引用
|
||
var pHost = GetHostPlayer();
|
||
|
||
// Monster kill requirements
|
||
// 怪物击杀条件
|
||
for (int i = 0; i < TaskInterfaceConstants.MAX_MONSTER_WANTED; i++)
|
||
{
|
||
if (tsi.m_MonsterWanted[i].m_ulMonsterId == 0) break;
|
||
|
||
uint id = tsi.m_MonsterWanted[i].m_ulMonsterId;
|
||
if (tsi.m_MonsterWanted[i].m_ulMonstersKilled > 0 || tsi.m_MonsterWanted[i].m_ulMonstersToKill > 0)
|
||
{
|
||
// Resolve monster name
|
||
// 解析怪物名称
|
||
string strName = "<color=#00FF00>????<color=#FFFFFF>";
|
||
var edm = BrewMonster.ElementDataManProvider.GetElementDataMan();
|
||
if (edm != null && edm.essence_id_data_type_map.TryGetValue(id, out var dtype)
|
||
&& dtype == DATA_TYPE.DT_MONSTER_ESSENCE
|
||
&& edm.essence_id_data_map.TryGetValue(id, out var obj)
|
||
&& obj is MONSTER_ESSENCE pMonster)
|
||
{
|
||
// strName = ModelRenderer.Scripts.Common.ByteToStringUtils.UshortArrayToUnicodeString(me.name);
|
||
|
||
// Always create clickable link (even if coords aren't known yet).
|
||
// 总是创建可点击链接(即使暂时不知道坐标)。
|
||
string monsterName = ByteToStringUtils.UshortArrayToUnicodeString(pMonster.name);
|
||
strName = $"<link=\"coord_{id}\"><color=#00FF00>{monsterName}</color></link>";
|
||
}
|
||
|
||
// Build description for this monster requirement
|
||
// 构建该怪物需求描述
|
||
string strTemp = "";
|
||
if (tsi.m_MonsterWanted[i].m_ulMonstersToKill > 0)
|
||
{
|
||
strTemp += Format(GetStringFromTable(7624), strName,
|
||
tsi.m_MonsterWanted[i].m_ulMonstersKilled, tsi.m_MonsterWanted[i].m_ulMonstersToKill);
|
||
}
|
||
else
|
||
{
|
||
strTemp += Format(GetStringFromTable(256), tsi.m_MonsterWanted[i].m_ulMonstersKilled);
|
||
}
|
||
|
||
// Prefix label for first/next item
|
||
// 首项/后续项的前缀标签
|
||
strText += (i == 0) ? GetStringFromTable(7622) : GetStringFromTable(7626);
|
||
strText += strTemp;
|
||
}
|
||
}
|
||
|
||
// Player kill requirements
|
||
// 击杀玩家条件
|
||
for (int i = 0; i < TaskInterfaceConstants.MAX_PLAYER_WANTED; i++)
|
||
{
|
||
if (tsi.m_PlayerWanted[i].m_ulPlayersToKill == 0) break;
|
||
if (tsi.m_ItemsWanted[i].m_ulItemId > 0) continue;
|
||
|
||
strText += (i == 0) ? GetStringFromTable(7630) : GetStringFromTable(7626);
|
||
strText += GetKillPlayerRequirements(tsi, i);
|
||
}
|
||
|
||
// Gold requirement
|
||
// 金币需求
|
||
if (tsi.m_ulGoldWanted != 0)
|
||
{
|
||
string strTemp = Format(GetStringFromTable(7636), tsi.m_ulGoldWanted);
|
||
strText += strTemp;
|
||
}
|
||
|
||
// Reincarnation requirement
|
||
// 转生次数需求
|
||
if (tsi.m_ulReachReincarnation != 0)
|
||
{
|
||
int iLevel = GetReincarnationCount(pHost);
|
||
string strColor = (iLevel < (int)tsi.m_ulReachReincarnation) ? "<color=#ff0000>" : "<color=#00ff00>";
|
||
if (iLevel < (int)tsi.m_ulReachReincarnation)
|
||
{
|
||
strHint += Format(GetStringFromTable(11144), iLevel);
|
||
}
|
||
strText += strColor;
|
||
strText += Format(GetStringFromTable(11141), tsi.m_ulReachReincarnation);
|
||
}
|
||
|
||
// Level requirement
|
||
// 等级需求
|
||
if (tsi.m_ulReachLevel != 0 && pHost != null)
|
||
{
|
||
int iLevel = pHost.GetBasicProps().iLevel;
|
||
string strColor = (iLevel < (int)tsi.m_ulReachLevel) ? "<color=#ff0000>" : "<color=#00ff00>";
|
||
if (iLevel < (int)tsi.m_ulReachLevel)
|
||
{
|
||
strHint += Format(GetStringFromTable(11143), iLevel);
|
||
}
|
||
strText += strColor;
|
||
strText += Format(GetStringFromTable(11140), tsi.m_ulReachLevel);
|
||
}
|
||
|
||
// Realm requirement
|
||
// 境界等级需求
|
||
if (tsi.m_ulReachRealm != 0 && pHost != null)
|
||
{
|
||
int iLevel = GetRealmLevel(pHost);
|
||
string strColor = (iLevel < (int)tsi.m_ulReachRealm) ? "<color=#ff0000>" : "<color=#00ff00>";
|
||
if (iLevel < (int)tsi.m_ulReachRealm)
|
||
{
|
||
strHint += Format(GetStringFromTable(11145), iLevel);
|
||
}
|
||
strText += strColor;
|
||
strText += Format(GetStringFromTable(11142), (int)tsi.m_ulReachRealm);
|
||
}
|
||
}
|
||
|
||
// Build text for player kill requirements
|
||
// 构建击杀玩家需求的文本
|
||
private string GetKillPlayerRequirements(Task_State_info tsi, int index)
|
||
{
|
||
uint killed = tsi.m_PlayerWanted[index].m_ulPlayersKilled;
|
||
uint toKill = tsi.m_PlayerWanted[index].m_ulPlayersToKill;
|
||
return $" {killed}/{toKill}\n";
|
||
}
|
||
|
||
// Get host reincarnation count (fallback implementation)
|
||
// 获取宿主转生次数(回退实现)
|
||
private int GetReincarnationCount(CECHostPlayer host)
|
||
{
|
||
return 0; // TODO: Replace with actual value when available
|
||
}
|
||
|
||
// Get host realm level via basic props level2
|
||
// 通过二级等级获取宿主境界等级
|
||
private int GetRealmLevel(CECHostPlayer host)
|
||
{
|
||
return host.GetBasicProps().iLevel2;
|
||
}
|
||
// Update wanted items section
|
||
// 更新需要的物品部分
|
||
private void UpdateItemWanted(ref string strText, Task_State_info tsi, int idTask)
|
||
{
|
||
// Resolve task template
|
||
// 获取任务模板
|
||
var pMan = EC_Game.GetTaskTemplateMan();
|
||
var pTempl = pMan != null ? pMan.GetTaskTemplByID((uint)idTask) : null;
|
||
if (pTempl == null) return;
|
||
|
||
// Iterate wanted items
|
||
// 遍历需要的物品
|
||
for (int i = 0; i < TaskInterfaceConstants.MAX_ITEM_WANTED; i++)
|
||
{
|
||
if (tsi.m_ItemsWanted[i].m_ulItemId == 0) break;
|
||
|
||
// Resolve item name
|
||
// 解析物品名称
|
||
int itemTid = unchecked((int)tsi.m_ItemsWanted[i].m_ulItemId);
|
||
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 = $"<link=\"coord_{search_id}\"><color=#00FF00>{itemName}</color></link>";
|
||
}
|
||
|
||
// Compose line: name and progress (gained/toGet)
|
||
// 组合文本:名称与进度(已获得/所需)
|
||
string strTemp = Format(GetStringFromTable(7625), displayName,
|
||
tsi.m_ItemsWanted[i].m_ulItemsGained,
|
||
tsi.m_ItemsWanted[i].m_ulItemsToGet);
|
||
|
||
// Prefix for first or subsequent entries
|
||
// 首项或后续项前缀
|
||
strText += (i == 0) ? GetStringFromTable(7623) : GetStringFromTable(7626);
|
||
strText += strTemp;
|
||
|
||
// If task is KillPlayer, also add player requirements line
|
||
// 若为击杀玩家任务,同时追加玩家需求行
|
||
if (pTempl.m_FixedData.m_enumMethod == (uint)TaskCompletionMethod.enumTMKillPlayer && i < TaskInterfaceConstants.MAX_PLAYER_WANTED)
|
||
{
|
||
strText += GetKillPlayerRequirements(tsi, i);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void UpdateTreasureMap(ref string strText)
|
||
{
|
||
// if (IsTreasureMapSelected())
|
||
// {
|
||
// EditBoxItemBase item(enumEICoord);
|
||
// item.SetName(GetStringFromTable(7629));
|
||
// item.SetInfo(GetStringFromTable(7629));
|
||
// item.SetColor(A3DCOLORRGB(0, 255, 0));
|
||
//
|
||
// strText += (ACHAR)AUICOMMON_ITEM_CODE_START + item.Serialize();
|
||
// }
|
||
}
|
||
|
||
private void UpdateTaskConfirm(int idTask, bool bFinishType)
|
||
{
|
||
CECTaskInterface pTask = GetHostPlayer()?.GetTaskInterface();
|
||
if (m_pBtn_FinishTask != null && pTask != null && bFinishType)
|
||
{
|
||
m_pBtn_FinishTask.gameObject.SetActive(true);
|
||
// TODO: Enable/disable based on task readiness
|
||
// m_pBtn_FinishTask->Enable(pTask->IsTaskReadyToConfirm(idTask));
|
||
m_pBtn_FinishTask.interactable = pTask.IsTaskReadyToConfirm(idTask);
|
||
|
||
}
|
||
else m_pBtn_FinishTask.gameObject.SetActive(false);
|
||
}
|
||
|
||
// void UpdateGotoNPC();
|
||
// void ClearGotoNPC();
|
||
//
|
||
// clear the task content in dialog
|
||
void ClearContent(bool clearNPC)
|
||
{
|
||
m_idLastTask = -2;
|
||
|
||
m_pTxt_Content.SetText("");
|
||
m_pTxt_BaseAward.gameObject.SetActive(false);
|
||
|
||
for( int j = 0; j < m_ImgCount; j++ )
|
||
{
|
||
m_pImg_Item[j].gameObject.SetActive(false);
|
||
// TODO: Clear image data
|
||
// m_pImg_Item[j]->SetData(0);
|
||
}
|
||
|
||
// TODO: Clear Tree quest view
|
||
m_pTv_Quest.DeleteAllItems();
|
||
}
|
||
// // add node to task tree
|
||
void AddTaskNode(int id)
|
||
{
|
||
var pTreeTask = m_pTv_Quest;
|
||
|
||
ATaskTemplMan pMan = EC_Game.GetTaskTemplateMan();
|
||
ATaskTempl pTemp = pMan.GetTaskTemplByID((uint)id);
|
||
if( pTemp == null )
|
||
{
|
||
return;
|
||
}
|
||
|
||
uint nTaskType = pTemp.m_FixedData.m_ulType;
|
||
if (pTemp.m_FixedData.m_DynTaskType != 0) nTaskType = (uint)ENUM_TASK_TYPE.enumTTEvent;
|
||
uint nAfterType = 0;
|
||
TaskTreeViewHolder pAfter = null, pParent = null;
|
||
// P_AUITREEVIEW_ITEM pItem = pTreeTask->GetFirstChildItem(pTreeTask->GetRootItem());
|
||
// var pItem = pTreeTask.transform.parent.GetChild(0).GetComponent<TaskTreeViewItem>();
|
||
var pItem = pTreeTask.GetFirstChild();
|
||
while( pItem )
|
||
{
|
||
uint nType = pItem.GetItemData();
|
||
if( nType == nTaskType ){
|
||
pParent = pItem;
|
||
break;
|
||
}
|
||
else if (nType < nTaskType && nType > nAfterType){
|
||
nAfterType = nType;
|
||
pAfter = pItem;
|
||
}
|
||
pItem = pTreeTask.GetNextSiblingItem(pItem);
|
||
}
|
||
|
||
// add Biggest node if not exist
|
||
string strItem = GetTaskNameWithColor(pTemp, out Color color);
|
||
if(pParent ==null)
|
||
{
|
||
pParent = pTreeTask.InsertItem(GetStringFromTable(3101 + nTaskType - 100), null, pAfter);
|
||
pParent.SetItemTextColor(color);
|
||
// TODO: Expand tree node
|
||
// pTreeTask.Expand(pParent, AUITREEVIEW_EXPAND_EXPAND);
|
||
pTreeTask.SetItemData(pParent, nTaskType);
|
||
|
||
//if(nTaskType == enumTTLevel2)
|
||
// pTreeTask.SetItemTextColor(pParent, GetTaskColor(pTemp));
|
||
}
|
||
|
||
CECTaskInterface pTask = GetHostPlayer()?.GetTaskInterface();
|
||
if (pTask == null) return;
|
||
bool bTaskPushed = pMan.IsTaskToPush(id) && !pTask.HasTask((uint)id);
|
||
if (bTaskPushed) {
|
||
strItem += GetStringFromTable(3100);
|
||
}
|
||
pItem = pTreeTask.InsertItem(strItem, pParent, null);
|
||
pItem.SetItemTextColor(color);
|
||
if( pTemp.IsKeyTask() )
|
||
{
|
||
pItem.SetItemTextColor(GetTaskColor((int)ENUM_TASK_TYPE.enumTTLevel2));
|
||
}
|
||
pTreeTask.SetItemData(pItem, (uint)id);
|
||
// HaveQuest view: children should reflect ActiveTaskList, not template tree (otherwise they never disappear on completion)
|
||
if (m_iType == 0)
|
||
InsertActiveTaskChildren(pItem, (uint)id);
|
||
else
|
||
InsertTaskChildren(pItem, (uint)id, true, pTemp.IsKeyTask());
|
||
|
||
if( (int)id == m_idSelTask )
|
||
{
|
||
// TODO : select the item in UI
|
||
// pTreeTask.SelectItem(pItem);
|
||
// m_pBtn_Abandon->Enable(true);
|
||
UpdateTask(id);
|
||
}
|
||
}
|
||
|
||
private void SortTaskNodeByType()
|
||
{
|
||
var pTreeTask = m_pTv_Quest;
|
||
if (pTreeTask == null) return;
|
||
|
||
// Collect direct children under the tree root (this component's transform)
|
||
int childCount = pTreeTask.transform.childCount;
|
||
var items = new List<(uint type, TaskTreeViewHolder item)>(childCount);
|
||
for (int i = 0; i < childCount; i++)
|
||
{
|
||
var child = pTreeTask.transform.GetChild(i).GetComponent<TaskTreeViewHolder>();
|
||
if (child == null) continue;
|
||
uint nType = pTreeTask.GetItemData(child);
|
||
items.Add((nType, child));
|
||
}
|
||
|
||
// Sort by type ascending
|
||
items.Sort((a, b) => a.type.CompareTo(b.type));
|
||
|
||
// Reorder siblings to match sorted order
|
||
for (int i = 0; i < items.Count; i++)
|
||
{
|
||
items[i].item.transform.SetSiblingIndex(i);
|
||
}
|
||
}
|
||
// // whether the task can be traced
|
||
// bool IsTaskTraceable(int idTask);
|
||
|
||
string Format( string formatStr, params object[] args )
|
||
{
|
||
// Ported from original C++ ACString::Format() which uses vsprintf/vswprintf
|
||
// Original implementation: AString& AString::Format(const char* szFormat, ...) { vsprintf(m_pStr, szFormat, argList); }
|
||
// This converts printf-style format specifiers to C# format specifiers
|
||
if (string.IsNullOrEmpty(formatStr))
|
||
return formatStr;
|
||
|
||
// Process format string character by character to convert %d, %s, etc. to {0}, {1}, etc.
|
||
// This matches the original vsprintf behavior exactly
|
||
var sb = new System.Text.StringBuilder();
|
||
int paramIndex = 0;
|
||
|
||
for (int i = 0; i < formatStr.Length; i++)
|
||
{
|
||
if (formatStr[i] == '%' && i + 1 < formatStr.Length)
|
||
{
|
||
// Check for %% (literal %)
|
||
if (formatStr[i + 1] == '%')
|
||
{
|
||
sb.Append('%');
|
||
i++; // Skip the second %
|
||
continue;
|
||
}
|
||
|
||
// Parse format specifier: %[flags][width][.precision][length]type
|
||
int startPos = i;
|
||
i++; // Skip the %
|
||
|
||
// Track flags
|
||
bool hasMinus = false;
|
||
bool hasPlus = false;
|
||
bool hasZero = false;
|
||
|
||
// Parse flags: +, -, 0, space
|
||
while (i < formatStr.Length && (formatStr[i] == '+' || formatStr[i] == '-' || formatStr[i] == '0' || formatStr[i] == ' '))
|
||
{
|
||
if (formatStr[i] == '-') hasMinus = true;
|
||
if (formatStr[i] == '+') hasPlus = true;
|
||
if (formatStr[i] == '0') hasZero = true;
|
||
i++;
|
||
}
|
||
|
||
// Parse width: digits
|
||
int width = 0;
|
||
int widthStart = i;
|
||
while (i < formatStr.Length && char.IsDigit(formatStr[i]))
|
||
{
|
||
i++;
|
||
}
|
||
if (i > widthStart)
|
||
{
|
||
int.TryParse(formatStr.Substring(widthStart, i - widthStart), out width);
|
||
}
|
||
|
||
// Skip precision: .digits
|
||
if (i < formatStr.Length && formatStr[i] == '.')
|
||
{
|
||
i++;
|
||
while (i < formatStr.Length && char.IsDigit(formatStr[i]))
|
||
{
|
||
i++;
|
||
}
|
||
}
|
||
|
||
// Skip length modifier: h, l, L, etc.
|
||
while (i < formatStr.Length && (formatStr[i] == 'h' || formatStr[i] == 'l' || formatStr[i] == 'L'))
|
||
{
|
||
i++;
|
||
}
|
||
|
||
// Get type specifier
|
||
if (i < formatStr.Length)
|
||
{
|
||
char typeChar = formatStr[i];
|
||
// Common types: d, i, u, o, x, X, f, e, E, g, G, c, s, p, n
|
||
if ("diouxXeEfFgGaAcspn".IndexOf(typeChar) >= 0)
|
||
{
|
||
// Extract the full format specifier for special handling
|
||
string fullSpec = formatStr.Substring(startPos, i - startPos + 1);
|
||
|
||
// Check for special formats
|
||
string csharpFormatSpec = "";
|
||
|
||
// Handle %-10d, %-5d, etc. (left-aligned with width) - check this FIRST
|
||
// Also handle %-d (left-aligned without explicit width, but width might be 0)
|
||
if (hasMinus && (typeChar == 'd' || typeChar == 'i' || typeChar == 'u' || typeChar == 's' || typeChar == 'c'))
|
||
{
|
||
if (width > 0)
|
||
{
|
||
// C# left alignment with width: {0,-10} (comma for alignment, negative for left-align)
|
||
csharpFormatSpec = $",-{width}";
|
||
}
|
||
else
|
||
{
|
||
// Just left alignment flag, no width specified - use default
|
||
csharpFormatSpec = "";
|
||
}
|
||
}
|
||
// Handle %02d, %03d, etc. (zero-padded integers) - check before regular width
|
||
else if (hasZero && width > 0 && (typeChar == 'd' || typeChar == 'i' || typeChar == 'u'))
|
||
{
|
||
csharpFormatSpec = $":D{width}";
|
||
}
|
||
// Handle %10d, %5d, etc. (right-aligned with width, no zero-padding)
|
||
else if (width > 0 && (typeChar == 'd' || typeChar == 'i' || typeChar == 'u' || typeChar == 's' || typeChar == 'c'))
|
||
{
|
||
// C# right alignment: {0,10} (comma for alignment)
|
||
csharpFormatSpec = $",{width}";
|
||
}
|
||
// Handle %+d (signed format)
|
||
else if (hasPlus && (typeChar == 'd' || typeChar == 'i'))
|
||
{
|
||
csharpFormatSpec = ":+0;-0";
|
||
}
|
||
// Handle %.2f, %.3f, etc. (float precision)
|
||
else if (fullSpec.Contains(".") && (typeChar == 'f' || typeChar == 'F' || typeChar == 'e' || typeChar == 'E' || typeChar == 'g' || typeChar == 'G'))
|
||
{
|
||
int dotPos = fullSpec.IndexOf('.');
|
||
if (dotPos >= 0 && dotPos + 1 < fullSpec.Length)
|
||
{
|
||
int precStart = dotPos + 1;
|
||
int precEnd = precStart;
|
||
while (precEnd < fullSpec.Length - 1 && char.IsDigit(fullSpec[precEnd]))
|
||
{
|
||
precEnd++;
|
||
}
|
||
if (precEnd > precStart && int.TryParse(fullSpec.Substring(precStart, precEnd - precStart), out int precNum))
|
||
{
|
||
if (typeChar == 'f' || typeChar == 'F')
|
||
{
|
||
csharpFormatSpec = $":F{precNum}";
|
||
}
|
||
else if (typeChar == 'e' || typeChar == 'E')
|
||
{
|
||
csharpFormatSpec = $":E{precNum}";
|
||
}
|
||
else if (typeChar == 'g' || typeChar == 'G')
|
||
{
|
||
csharpFormatSpec = $":G{precNum}";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Build replacement - always replace recognized format specifiers
|
||
// C# format: {index,alignment} for width, {index:format} for format specifiers
|
||
if (csharpFormatSpec.Length > 0)
|
||
{
|
||
// If format spec starts with comma (alignment) or colon (format), use it directly
|
||
// Otherwise, assume it's a format specifier and add colon
|
||
if (csharpFormatSpec.StartsWith(",") || csharpFormatSpec.StartsWith(":"))
|
||
{
|
||
sb.Append($"{{{paramIndex}{csharpFormatSpec}}}");
|
||
}
|
||
else
|
||
{
|
||
sb.Append($"{{{paramIndex}:{csharpFormatSpec}}}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
sb.Append($"{{{paramIndex}}}");
|
||
}
|
||
|
||
paramIndex++;
|
||
// Note: i currently points to the type character (e.g., 'd' in "%-10d")
|
||
// The for loop will increment i, so we've consumed the entire format specifier
|
||
}
|
||
else
|
||
{
|
||
// Not a recognized format specifier, keep as-is
|
||
sb.Append(formatStr[startPos]);
|
||
i = startPos; // Reset to process next character
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Incomplete format specifier, keep the %
|
||
sb.Append('%');
|
||
i = startPos; // Reset to process next character
|
||
}
|
||
}
|
||
else
|
||
{
|
||
sb.Append(formatStr[i]);
|
||
}
|
||
}
|
||
|
||
// Use C#'s string.Format (equivalent to original vsprintf)
|
||
// Handle case where we might have more format specifiers than arguments
|
||
try
|
||
{
|
||
return string.Format(sb.ToString(), args);
|
||
}
|
||
catch (System.FormatException)
|
||
{
|
||
// If format fails (e.g., not enough arguments), return the original format string
|
||
// This can happen if GetStringFromTable returns an unexpected format
|
||
BMLogger.LogWarning($"Format failed for string: {formatStr}, expected {paramIndex} args, got {args.Length}");
|
||
return formatStr;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
}
|
||
} |