Merge branch 'develop' into feature/skill-set-shortcut

# Conflicts:
#	Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs
#	Assets/PerfectWorld/Scripts/DebugCommandMenu.meta
This commit is contained in:
VDH
2026-02-26 15:30:49 +07:00
7531 changed files with 5273512 additions and 16261 deletions
+7
View File
@@ -205,6 +205,13 @@ public partial class CECGameRun
CECPlayer.InitStaticRes();
hostPlayer = ObjectSpawner.Instance.InstantiateObject(_playerPrefab, setThisAsParent: true).AddComponent<CECHostPlayer>();
hostPlayer.InitCharacter(info);
if (hostPlayer != null)
{
var t = Type.GetType("BrewMonster.UI.SelectedTargetHUDController, Assembly-CSharp");
if (t != null && hostPlayer.GetComponent(t) == null)
hostPlayer.gameObject.AddComponent(t);
}
}
public CECMonster GetMonster()
{
+15 -70
View File
@@ -1,4 +1,4 @@
using BrewMonster.Managers;
using BrewMonster.Managers;
using BrewMonster.Network;
using BrewMonster.Scripts;
using BrewMonster.Scripts.World;
@@ -828,75 +828,16 @@ namespace BrewMonster
}
}
}
// TO DO: fix later
//else if (GPDataTypeHelper.ISPLAYERID(idTarget))
//{
// // Check duel at first
// if (m_pvp.iDuelState == Duel_state.DUEL_ST_INDUEL && m_pvp.idDuelOpp == idTarget)
// return 1;
// else if (m_pvp.iDuelState == Duel_state.DUEL_ST_STOPPING && m_pvp.idDuelOpp == idTarget)
// return 0;
// // In sanctuary we cannot attack other players
// if (m_bInSanctuary)
// return 0;
// //ASSERT(pObject.GetClassID() == CECObject::OCID_ELSEPLAYER);
// EC_ElsePlayer pPlayer = (EC_ElsePlayer)pObject;
// ROLEBASICPROP bp = pPlayer.GetBasicProps();
// EC_GAME_SETTING gs = g_pGame.GetConfigs().GetGameSettings();
// if (m_pvp.bFreePVP)
// {
// if (IsTeamMember(idTarget))
// return 0;
// // In free pvp mode, for example, host is in arena.
// if (bForceAttack)
// iRet = 1;
// else if (gs.bAtk_NoMafia && IsFactionMember(pPlayer.GetFactionID()))
// iRet = 0;
// else if (gs.bAtk_NoWhite && !pPlayer.IsInvader() && !pPlayer.IsPariah())
// iRet = 0;
// else if (gs.bAtk_NoAlliance && g_pGame.GetFactionMan().IsFactionAlliance(pPlayer.GetFactionID()))
// iRet = 0;
// else if (gs.bAtk_NoForce && GetForce() > 0 && GetForce() == pPlayer.GetForce())
// iRet = 0;
// else
// iRet = 1;
// }
// else if (m_iBattleCamp != GP_BATTLE_CAMP_NONE)
// {
// // Host is in battle
// int iCamp = pPlayer.GetBattleCamp();
// if (iCamp != GP_BATTLE_CAMP_NONE && iCamp != m_iBattleCamp)
// iRet = 1;
// else
// iRet = 0;
// }
// else // Normal mode
// {
// if (IsTeamMember(idTarget))
// return 0;
// if (!IsPVPOpen() || !pPlayer.IsPVPOpen() || m_BasicProps.iLevel < EC_MAXNOPKLEVEL || bp.iLevel < EC_MAXNOPKLEVEL)
// iRet = 0;
// else if (bForceAttack)
// iRet = 1;
// else if (!gs.bAtk_Player)
// iRet = 0;
// else if (gs.bAtk_NoMafia && IsFactionMember(pPlayer.GetFactionID()))
// iRet = 0;
// else if (gs.bAtk_NoWhite && !pPlayer.IsInvader() && !pPlayer.IsPariah())
// iRet = 0;
// else if (gs.bAtk_NoAlliance && g_pGame.GetFactionMan().IsFactionAlliance(pPlayer.GetFactionID()))
// iRet = 0;
// else if (gs.bAtk_NoForce && GetForce() > 0 && GetForce() == pPlayer.GetForce())
// iRet = 0;
// else
// iRet = 1;
// }
//}
else if (GPDataTypeHelper.ISPLAYERID(idTarget))
{
// Duel: allow attack only on duel opponent while in duel
if (IsInDuel() && m_pvp.idDuelOpp == idTarget)
return 1;
if (m_pvp.iDuelState == Duel_state.DUEL_ST_STOPPING && m_pvp.idDuelOpp == idTarget)
return 0;
// Other player PVP not implemented here; treat as not attackable
return 0;
}
else
{
return -1;
@@ -952,6 +893,10 @@ namespace BrewMonster
else
bForceAttack = iForceAtk > 0 ? true : false;
// Duel: server accepts attack on opponent only with PVP/force mask
if (IsInDuel() && idTarget == m_pvp.idDuelOpp)
bForceAttack = true;
if (AttackableJudge(idTarget, bForceAttack) != 1)
return false;
+14
View File
@@ -705,6 +705,20 @@ namespace BrewMonster
PlayAttackEffect(pCmd.target, pCmd.skill, pCmd.level, nDamage,
dwModifier & mask, 0, ref attackTime, pCmd.section);
}
public void OnMsgHstClearTessera(ECMSG Msg)
{
cmd_clear_tessera pCmd = GPDataTypeHelper.FromBytes<cmd_clear_tessera>((byte[])Msg.dwParam1);
AddMoneyAmount(-(int)pCmd.cost);
// Refresh equip's data
UnityGameSession.c2s_CmdGetItemInfo(Inventory_type.IVTRTYPE_PACK, (byte)pCmd.equip_idx);
}
// Add money amount
private int AddMoneyAmount(int iAmount)
{
m_iMoneyCnt += (uint)iAmount;
return (int)m_iMoneyCnt;
}
public bool HaveHealthStones()
{
+139 -25
View File
@@ -19,6 +19,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
@@ -579,7 +580,6 @@ namespace BrewMonster
case EC_MsgDef.MSG_HST_SELTARGET:
OnMsgHstSelTarget(Msg); break;
case EC_MsgDef.MSG_HST_USEITEM:
OnMsgHstUseItem(Msg);
break;
case EC_MsgDef.MSG_HST_ATKRESULT: OnMsgHstAttackResult(Msg); break;
case EC_MsgDef.MSG_HST_ATTACKED: OnMsgHstAttacked(Msg); break;
@@ -600,10 +600,7 @@ namespace BrewMonster
case EC_MsgDef.MSG_HST_TARGETISFAR: OnMsgHstTargetIsFar(Msg); break;
case EC_MsgDef.MSG_PM_PLAYERGATHER: OnMsgPlayerGather(Msg); break;
case EC_MsgDef.MSG_HST_COOLTIMEDATA: OnMsgHstCoolTimeData(Msg); break;
case EC_MsgDef.MSG_HST_SETCOOLTIME: OnMsgHstSetCoolTime(Msg); break;
case EC_MsgDef.MSG_HST_PRESSCANCEL: OnMsgHstPressCancel(Msg); break;
case EC_MsgDef.MSG_HST_LEARNSKILL: OnMsgHstLearnSkill(Msg); break;
case EC_MsgDef.MSG_HST_COMBO_SKILL_PREPARE: OnMsgComboSkillPrepare(Msg); break;
case EC_MsgDef.MSG_PM_PLAYERFLY: OnMsgPlayerFly(Msg); break;
case EC_MsgDef.MSG_HST_PETOPT: OnMsgHstPetOpt(Msg); break;
case EC_MsgDef.MSG_HST_SETPLAYERLIMIT: OnMsgHstSetPlayerLimit(Msg); break;
@@ -613,7 +610,8 @@ namespace BrewMonster
case EC_MsgDef.MSG_HST_LEAVETEAM: OnMsgHstLeaveTeam(Msg); break;
case EC_MsgDef.MSG_HST_NEWTEAMMEM: OnMsgHstNewTeamMem(Msg); break;
case EC_MsgDef.MSG_HST_TEAMMEMBERDATA: OnMsgHstTeamMemberData(Msg); break;
case EC_MsgDef.MSG_HST_CONTINUECOMBOSKILL: OnMsgContinueComboSkill(Msg); break;
case EC_MsgDef.MSG_PM_DUELOPT: OnMsgHstDuelOpt(Msg); break;
case EC_MsgDef.MSG_HST_CLEARTESSERA: OnMsgHstClearTessera(Msg); break;
}
/*if (bActionStartSkill)
@@ -645,6 +643,85 @@ namespace BrewMonster
}
catch { }
}
/// <summary>Update host duel state from S2C duel commands (MSG_PM_DUELOPT).</summary>
private void OnMsgHstDuelOpt(ECMSG Msg)
{
int cmdId = Convert.ToInt32(Msg.dwParam2);
byte[] data = Msg.dwParam1 as byte[];
int idOpp = 0;
if (data != null && data.Length >= 4)
idOpp = BitConverter.ToInt32(data, 0);
switch (cmdId)
{
case CommandID.DUEL_PREPARE:
m_pvp.iDuelState = Duel_state.DUEL_ST_PREPARE;
m_pvp.idDuelOpp = idOpp;
m_pvp.iDuelTimeCnt = data != null && data.Length >= 8 ? BitConverter.ToInt32(data, 4) : 3000;
break;
case CommandID.HOST_DUEL_START:
m_pvp.iDuelState = Duel_state.DUEL_ST_INDUEL;
m_pvp.idDuelOpp = idOpp;
m_pvp.iDuelTimeCnt = 0;
// Origin: server must be notified of force-attack (PVP) so it accepts attacks on the duel opponent
NotifyServerForceAttack(true);
break;
case CommandID.DUEL_STOP:
case CommandID.DUEL_RESULT:
m_pvp.iDuelState = Duel_state.DUEL_ST_STOPPING;
m_pvp.iDuelTimeCnt = data != null && data.Length >= 8 ? BitConverter.ToInt32(data, 4) : 3000;
if (data != null && data.Length >= 12)
m_pvp.iDuelRlt = BitConverter.ToInt32(data, 8);
NotifyServerForceAttack(false);
break;
case CommandID.DUEL_CANCEL:
case CommandID.DUEL_REJECT_REQUEST:
m_pvp.iDuelState = Duel_state.DUEL_ST_NONE;
m_pvp.idDuelOpp = 0;
m_pvp.iDuelTimeCnt = 0;
NotifyServerForceAttack(false);
break;
}
}
/// <summary>Called when MSG_PM_PLAYERDUELOPT (229) is received; server may send duel start to both participants. If host is one of the two ids, set duel state.</summary>
public void OnMsgPlayerDuelStart(byte[] data)
{
if (data == null || data.Length < 8) return;
int id1 = BitConverter.ToInt32(data, 0);
int id2 = BitConverter.ToInt32(data, 4);
int cid = m_PlayerInfo.cid;
if (cid == id1)
{
m_pvp.iDuelState = Duel_state.DUEL_ST_INDUEL;
m_pvp.idDuelOpp = id2;
m_pvp.iDuelTimeCnt = 0;
NotifyServerForceAttack(true);
}
else if (cid == id2)
{
m_pvp.iDuelState = Duel_state.DUEL_ST_INDUEL;
m_pvp.idDuelOpp = id1;
m_pvp.iDuelTimeCnt = 0;
NotifyServerForceAttack(true);
}
}
/// <summary>Origin: notify server of force-attack (PVP) state so it accepts/rejects attacks on players. Call when duel starts (true) or ends (false).</summary>
private void NotifyServerForceAttack(bool bForceAttack)
{
byte refuseBless = EC_Utility.glb_BuildRefuseBLSMask();
UnityGameSession.c2s_SendCmdNotifyForceAttack(glb_BuildPVPMask(bForceAttack), refuseBless);
}
#if UNITY_EDITOR
/// <summary>
/// Cycles through learned skills by removing all shortcuts and adding 2 new skills to slots 0 and 1.
/// If m_startingSkillID is set (>0), uses that specific skill ID and the next one (ID+1).
/// Otherwise, cycles through all learned skills in pairs.
/// </summary>
#endif
public bool HostIsReady()
{
return m_bEnterGame;
@@ -1029,6 +1106,9 @@ namespace BrewMonster
{
var data = (byte[])Msg.dwParam1;
cmd_select_target pCmd = GPDataTypeHelper.FromBytes<cmd_select_target>(data);
// In duel: don't let server force selection back to duel opponent when player chose another target
if (IsInDuel() && pCmd.idTarget == m_pvp.idDuelOpp && m_idSelTarget != 0 && m_idSelTarget != m_pvp.idDuelOpp)
return;
m_idSelTarget = pCmd.idTarget;
m_idUCSelTarget = 0;
}
@@ -1041,6 +1121,7 @@ namespace BrewMonster
public override void SetUpPlayer()
{
base.SetUpPlayer();
m_iCID = (int)CECObject.Class_ID.OCID_HOSTPLAYER;
m_IncantCnt = new CECCounter();
m_IncantCnt.SetPeriod(1000);
@@ -1256,6 +1337,12 @@ namespace BrewMonster
// SetPlayerModel();
//Debug.LogError("Pos Character = " + pos);
}
/// <summary>Use host's m_pvp (we update it from S2C duel packets). Base IsInDuel() reads CECPlayer.m_pvp which is never set.</summary>
public new bool IsInDuel() { return m_pvp.iDuelState == Duel_state.DUEL_ST_INDUEL; }
/// <summary>Duel opponent character id when in duel; 0 otherwise. Used by HPWork so trace/melee send correct PVP mask.</summary>
public int GetDuelOpponentId() { return m_pvp.idDuelOpp; }
public void ClearAnimation()
{
@@ -1527,6 +1614,7 @@ namespace BrewMonster
return fSpeedSev;
}
public void PrepareNPCService(int idSev)
{
@@ -1706,7 +1794,6 @@ namespace BrewMonster
return true;
}
public A3DVECTOR3 GetDir()
{
// Return forward direction from transform
@@ -1811,7 +1898,6 @@ namespace BrewMonster
public bool SelectTarget(int idTarget)
{
//BMLogger.LogError("HoangDev: HostPlayer SelectTarget");
bool bRet = false;
bool canDo = CanDo(ActionCanDo.CANDO_CHANGESELECT);
bool canselect = CanSelectTarget(idTarget);
@@ -1822,11 +1908,15 @@ namespace BrewMonster
{
//BMLogger.LogError("HoangDev: HostPlayer Unsetlect npc");
UnityGameSession.c2s_CmdUnselect();
m_idSelTarget = 0;
m_idUCSelTarget = 0;
}
else
{
//BMLogger.LogError("HoangDev: HostPlayer setlect npc");
UnityGameSession.c2s_CmdSelectTarget(idTarget);
m_idSelTarget = idTarget;
m_idUCSelTarget = idTarget;
}
}
@@ -1864,6 +1954,10 @@ namespace BrewMonster
return true;
}
// Duel: always allow selecting the duel opponent so we can attack/cast (distance checked when trace runs)
if (IsInDuel() && idTarget == m_pvp.idDuelOpp)
return true;
CECObject pTarget = null;
if (GPDataTypeHelper.ISPLAYERID(idTarget))
{
@@ -2282,7 +2376,23 @@ namespace BrewMonster
}
public void SetSelectedTarget(int id)
{
if (m_idSelTarget != id)
EventBus.Publish(new CECHostPlayer.TargetHUDClearEvent());
m_idSelTarget = id;
// In duel, when player selects a different target, cancel trace work so it doesn't overwrite selection on touch
if (IsInDuel() && id != 0 && id != m_pvp.idDuelOpp)
m_pWorkMan?.FinishRunningWork(CECHPWork.Host_work_ID.WORK_TRACEOBJECT);
// When selecting another player, publish NPCINFO so target HUD shows (server doesn't resend base info on select)
if (id != 0 && id != GetCharacterID() && GPDataTypeHelper.ISPLAYERID(id))
{
var elsePlayer = EC_ManMessageMono.Instance?.GetECManPlayer?.GetPlayer(id) as EC_ElsePlayer;
if (elsePlayer != null && elsePlayer.m_bBaseInfoReady)
{
string name = elsePlayer.GetName();
if (!string.IsNullOrEmpty(name))
EventBus.Publish(new CECHostPlayer.NPCINFO(name, 100, 100, id));
}
}
}
public new int GetSelectedTarget()
@@ -3209,7 +3319,10 @@ namespace BrewMonster
MaxHealth = maxHealth;
IDNPC = idnpc;
}
};
}
/// <summary>Fired when selected target HUD should be hidden (deselect or switch).</summary>
public struct TargetHUDClearEvent { }
public struct GNDINFO
{
@@ -3540,21 +3653,19 @@ namespace BrewMonster
public CECCounter GetIncantCnt() { return m_IncantCnt; }
// Get key object(NPC..) coordinates
public A3DVECTOR3 GetObjectCoordinates(int idTarget, ref ObjectCoords TargetCoord, ref bool bInTable)
public A3DVECTOR3 GetObjectCoordinates(int idTarget, out List<OBJECT_COORD> TargetCoord, ref bool bInTable)
{
if (TargetCoord == null)
{
TargetCoord = new ObjectCoords();
}
TargetCoord.Clear();
TargetCoord = new List<OBJECT_COORD>();
A3DVECTOR3 vDestPos = new A3DVECTOR3(0, 0, 0);
bInTable = false;
// Get object coordinates from CECGame::m_CoordTab
string szText = idTarget.ToString();
List<OBJECT_COORD> tempCoord = new List<OBJECT_COORD>();
int iCount = EC_Game.GetObjectCoord(szText, ref tempCoord);
List<OBJECT_COORD> originalCoords = new List<OBJECT_COORD>();
int iCount = EC_Game.GetObjectCoord(szText, out originalCoords);
if (iCount == 0)
{
return vDestPos;
@@ -3567,11 +3678,12 @@ namespace BrewMonster
CECInstance pInstance = CECGameRun.Instance.GetInstance(idInstance);
string strCurMap = pInstance.GetPath();
// ȼͬһͼǷҪҵƷ
bool bHasObjectInCurrentInstance = tempCoord.Any(coord => coord.strMap == strCurMap);
bool bHasObjectInCurrentInstance = originalCoords.Any(coord => coord.strMap == strCurMap);
// Iterate over original list and build filtered TargetCoord list
for (int i = 0; i < iCount; i++)
{
OBJECT_COORD objCoord = tempCoord[i];
OBJECT_COORD objCoord = originalCoords[i];
if (strCurMap == objCoord.strMap)
{
TargetCoord.Add(objCoord);
@@ -3583,22 +3695,24 @@ namespace BrewMonster
fMinDist = tempDist;
bInTable = true;
vDestPos = objCoord.vPos;
}
}
}
// ǰͼûĿĻĿڵͼڵǰͼ
else if (!bHasObjectInCurrentInstance)
{
// find the entrance of instance
List<OBJECT_COORD> instCoord = new List<OBJECT_COORD>();
int iCount2 = EC_Game.GetObjectCoord(objCoord.strMap, ref instCoord);
for (int j = 0; j < iCount2; ++j)
int iCount2 = EC_Game.GetObjectCoord(objCoord.strMap, out instCoord);
for (int j = 0; j < iCount2; ++j)
{
if (instCoord[j].strMap == strCurMap)
{
TargetCoord.Add(instCoord[j]);
// Check if this is the nearest target
float tempDist = (instCoord[i].vPos - GetPos()).Magnitude();
float tempDist = (instCoord[j].vPos - GetPos()).Magnitude();
if (tempDist < fMinDist)
{
fMinDist = tempDist;
@@ -3616,7 +3730,7 @@ namespace BrewMonster
public int GetRealmSubLevel() { return m_RealmLevel % 100; }
public static int GetRealmLayer(int realmLevel) { return realmLevel > 0 ? (realmLevel + 9) / 10 : 0; }
public static int GetRealmSubLevel(int realmLevel) { return realmLevel > 0 ? (realmLevel % 10 > 0 ? realmLevel % 10 : 10) : 0; }
// <summary>
// Calculate distance to an object and optionally retrieve the object reference
// 计算到对象的距离,并可选地获取对象引用
+65 -2
View File
@@ -22,6 +22,7 @@ public class CECUIManager : MonoSingleton<CECUIManager>
[SerializeField] private int currentTargetNPCID;
CECGameUIMan gameUI;
AUIManager aUIManager;
AUIDialog _dlgPlayerOptions;
[SerializeField] private DialogScriptTableObject dialogResouce;
[SerializeField] private Canvas canvasDlg;
@@ -33,11 +34,16 @@ public class CECUIManager : MonoSingleton<CECUIManager>
Sprite[] m_iconlistIvtr;
private CDlgInfoTooltip m_pDlgSkillTooltip;
// Task update timer / 任务更新计时器
private float _nextTaskUpdateTime = 0f;
private const float TASK_UPDATE_INTERVAL = .1f;
protected override void Awake()
{
base.Awake();
EventBus.Subscribe<CECHostPlayer.NPCINFO>(ShowUINPC);
EventBus.Subscribe<CECHostPlayer.TargetHUDClearEvent>(OnTargetHUDClear);
EventBus.Subscribe<NPCDiedEvent>(TryHideUINPC);
EventBus.Subscribe<MessageBoxEvent>(OnMessageBox);
@@ -58,22 +64,69 @@ public class CECUIManager : MonoSingleton<CECUIManager>
// _fpsText.text = $"{Mathf.RoundToInt(1f / Time.deltaTime)}";
if (m_pDlgQuickBar1 != null)
{ m_pDlgQuickBar1.UpdateShortcuts(); }
// Periodically update task UI (DlgTask and DlgTaskTrace) / 定期更新任务UIDlgTask和DlgTaskTrace
if (Time.unscaledTime >= _nextTaskUpdateTime)
{
_nextTaskUpdateTime = Time.unscaledTime + TASK_UPDATE_INTERVAL;
UpdateTaskUI();
}
}
/// <summary>
/// Update task UI from DlgTask and DlgTaskTrace / 从DlgTask和DlgTaskTrace更新任务UI
/// </summary>
private void UpdateTaskUI()
{
try
{
var inGameUIMan = GetInGameUIMan();
if (inGameUIMan == null) return;
var dlgTaskDialog = inGameUIMan.GetDialog("Win_Quest");
var dlgTaskTraceDialog = inGameUIMan.GetDialog("Win_QuestMinion");
if (dlgTaskDialog != null && dlgTaskDialog is BrewMonster.Scripts.Task.UI.DlgTask dlgTaskForTrace)
{
if (dlgTaskTraceDialog != null && dlgTaskTraceDialog.IsShow())
{
dlgTaskForTrace.RefreshTaskTrace();
}
dlgTaskForTrace.Tick();
}
}
catch (System.Exception)
{
// Silently handle errors to avoid spamming logs / 静默处理错误以避免日志刷屏
// Task UI update errors are handled silently / 任务UI更新错误被静默处理
}
}
private void OnDestroy()
{
EventBus.Unsubscribe<CECHostPlayer.NPCINFO>(ShowUINPC);
EventBus.Unsubscribe<CECHostPlayer.TargetHUDClearEvent>(OnTargetHUDClear);
EventBus.Unsubscribe<NPCDiedEvent>(TryHideUINPC);
EventBus.Unsubscribe<MessageBoxEvent>(OnMessageBox);
}
private void ShowUINPC(CECHostPlayer.NPCINFO obj)
{
var host = EC_Game.GetGameRun()?.GetHostPlayer();
if (host != null && host.GetSelectedTarget() != obj.IDNPC)
return;
npsUI.gameObject.SetActive(true);
npsUI.SetText($"{obj.CurrentHealth}/{obj.MaxHealth}", obj.Name, "");
npsUI.SetHealthImage((float)obj.CurrentHealth / (float)obj.MaxHealth);
string hpText = obj.MaxHealth > 0 ? $"{obj.CurrentHealth}/{obj.MaxHealth}" : "-/-";
npsUI.SetText(hpText, obj.Name ?? "", "");
float fill = obj.MaxHealth > 0 ? (float)obj.CurrentHealth / (float)obj.MaxHealth : 1f;
npsUI.SetHealthImage(fill);
currentTargetNPCID = obj.IDNPC;
}
private void OnTargetHUDClear(CECHostPlayer.TargetHUDClearEvent _)
{
npsUI.gameObject.SetActive(false);
}
public CDlgQuickBar GetCDlgQuickBar()
{
return m_pDlgQuickBar1;
@@ -408,6 +461,16 @@ public class CECUIManager : MonoSingleton<CECUIManager>
return gameUI;
}
/// <summary>Shows the player options menu for the given character. Requires a prefab with id "DlgPlayerOptions" in DialogScriptTableObject.</summary>
public void ShowPlayerOptionsDialog(int characterId)
{
if (characterId == 0 || canvasDlg == null) return;
var gui = GetInGameUIMan();
if (_dlgPlayerOptions == null)
_dlgPlayerOptions = gui.GetDialog("DlgPlayerOptions");
_dlgPlayerOptions?.ShowForPlayer(characterId);
}
/// <summary>
/// Get the current target NPC ID (same as stored from NPCINFO event)
/// </summary>