diff --git a/Assets/PerfectWorld/Prefab/UI/PlayerOptionPopup.prefab b/Assets/PerfectWorld/Prefab/UI/PlayerOptionPopup.prefab index ba4ca439d2..61f908b24d 100644 --- a/Assets/PerfectWorld/Prefab/UI/PlayerOptionPopup.prefab +++ b/Assets/PerfectWorld/Prefab/UI/PlayerOptionPopup.prefab @@ -1208,7 +1208,7 @@ MonoBehaviour: m_OnCullStateChanged: m_PersistentCalls: m_Calls: [] - m_text: Invite + m_text: "Th\xE1ch \u0111\u1EA5u\n" m_isRightToLeft: 0 m_fontAsset: {fileID: 11400000, guid: 369c2e14814cc9a4b8e3ad4e37769134, type: 2} m_sharedMaterial: {fileID: 9092487103257209053, guid: 369c2e14814cc9a4b8e3ad4e37769134, type: 2} diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkTrace.cs b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkTrace.cs index 652d2799c3..fdd5b8c8f9 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkTrace.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_HPWorkTrace.cs @@ -262,10 +262,14 @@ namespace BrewMonster // 为了保证追踪到目标后能正常攻击,这里在必要时主动选中目标。 // Server select-target ack can arrive later than the trace touch moment, which would // make this block forever. Ensure we select the traced target before attacking. + // In duel: don't overwrite selection if player explicitly chose another target. + bool duelPlayerChoseOther = m_pHost.IsInDuel() && m_pHost.GetDuelOpponentId() == m_iObjectId + && m_pHost.m_idSelTarget != 0 && m_pHost.m_idSelTarget != m_iObjectId; if (m_iObjectId != m_pHost.m_idSelTarget) { UnityGameSession.c2s_CmdSelectTarget(m_iObjectId); - m_pHost.m_idSelTarget = m_iObjectId; + if (!duelPlayerChoseOther) + m_pHost.m_idSelTarget = m_iObjectId; } if (m_pHost.AttackableJudge(m_iObjectId, m_bForceAttack) == 1) { @@ -381,10 +385,14 @@ namespace BrewMonster { // 同 NPC:追踪触碰可能早于服务器选中目标同步,需主动选中以避免攻击不触发。 // Same as NPC: trace touch can happen before server selection ack; proactively select. + // In duel: don't overwrite selection if player explicitly chose another target. + bool duelPlayerChoseOther = m_pHost.IsInDuel() && m_pHost.GetDuelOpponentId() == m_iObjectId + && m_pHost.m_idSelTarget != 0 && m_pHost.m_idSelTarget != m_iObjectId; if (m_iObjectId != m_pHost.m_idSelTarget) { UnityGameSession.c2s_CmdSelectTarget(m_iObjectId); - m_pHost.m_idSelTarget = m_iObjectId; + if (!duelPlayerChoseOther) + m_pHost.m_idSelTarget = m_iObjectId; } // Duel: always send PVP/force mask when target is duel opponent so server accepts the attack bool bUseForceAttack = m_bForceAttack || (m_pHost.IsInDuel() && m_iObjectId == m_pHost.GetDuelOpponentId()); diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_HostInputFilter.cs b/Assets/PerfectWorld/Scripts/Managers/EC_HostInputFilter.cs index 3177d02b80..89884d340e 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_HostInputFilter.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_HostInputFilter.cs @@ -407,8 +407,10 @@ namespace BrewMonster // Trace a object / Trace a object if (iTraceReason == CECHPWorkTrace.Trace_reason.TRACE_ATTACK) { - // So CanDo(CANDO_MELEE) passes: it requires m_idSelTarget set to the attack target - m_idSelTarget = idTraceTarget; + // So CanDo(CANDO_MELEE) passes and target HUD updates (e.g. duel opponent) + SetSelectedTarget(idTraceTarget); + // Sync selected target to server so attack is not rejected (wrong target / too far) + UnityGameSession.c2s_CmdSelectTarget(idTraceTarget); if (!CanDo(ActionCanDo.CANDO_MELEE)) return; // When attacking duel opponent, send force/PVP so server accepts (origin: duel = attack but not kill) @@ -448,6 +450,8 @@ namespace BrewMonster if (idSelTarget != 0 && m_idSelTarget != idSelTarget) { m_idUCSelTarget = idSelTarget; + // Update local selection and HUD first so first click changes target (e.g. in duel); then sync to server + SetSelectedTarget(m_idUCSelTarget); SelectTarget(m_idUCSelTarget); } } diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_ManPlayer.cs b/Assets/PerfectWorld/Scripts/Managers/EC_ManPlayer.cs index 9211f1736a..8c93446808 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_ManPlayer.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_ManPlayer.cs @@ -38,13 +38,12 @@ namespace PerfectWorld.Scripts.Managers if ((int)Msg.dwMsg == EC_MsgDef.MSG_PM_DUELOPT && Convert.ToInt32(Msg.dwParam2) == CommandID.DUEL_RECV_REQUEST && Msg.dwParam1 is byte[] pDataBuf && pDataBuf.Length >= 4) { int inviterId = BitConverter.ToInt32(pDataBuf, 0); - var gameUI = EC_Game.GetGameRun()?.GetUIManager()?.GetInGameUIMan(); - if (gameUI != null) - { - var dlg = gameUI.GetDialog("DlgDuelInvite"); - var showMethod = dlg?.GetType().GetMethod("ShowForDuelInvite", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance, null, new[] { typeof(int) }, null); - showMethod?.Invoke(dlg, new object[] { inviterId }); - } + CECUIManager.Instance?.ShowMessageBox( + title: "", + message: "You have received a duel request. Do you accept?", + messageBoxType: MessageBoxType.BothYesNoButton, + onClickedYes: () => UnityGameSession.c2s_CmdDuelReply(true, inviterId), + onClickedNo: () => UnityGameSession.c2s_CmdDuelReply(false, inviterId)); } CECGameRun.Instance.GetHostPlayer().ProcessMessage(Msg); } diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/C2SCommand/C2SCommandFactory.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/C2SCommand/C2SCommandFactory.cs index f0440f4937..4ed0b279d7 100644 --- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/C2SCommand/C2SCommandFactory.cs +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/C2SCommand/C2SCommandFactory.cs @@ -824,10 +824,14 @@ namespace CSNetwork.C2SCommand return SerializeCommand(CommandID.DUEL_REQUEST, new cmd_team_new_member { idMember = idTarget }); } - /// C2S: accept (true) or reject (false) duel from inviter. + /// C2S: accept (true) or reject (false) duel from inviter. Origin: [cmd][who:4][param:4] — who=inviter id, param=0 accept / non-zero reject. public static Octets CreateDuelReplyCommand(bool accept, int idInviter) { - return SerializeCommand(CommandID.DUEL_REPLY, new cmd_duel_reply { accept = accept ? 1 : 0, idInviter = idInviter }); + var octets = new Octets(); + WriteBasicValue(octets, (ushort)CommandID.DUEL_REPLY); + WriteBasicValue(octets, idInviter); // who + WriteBasicValue(octets, accept ? 0 : 1); // param: 0 = accept, non-zero = reject reason + return octets; } public static Octets CreateTeamKickMemberCommand(int idMember) diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GPDataType.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GPDataType.cs index f7ebb1055e..d661617ad6 100644 --- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GPDataType.cs +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GPDataType.cs @@ -2188,12 +2188,12 @@ namespace CSNetwork.GPDataType public int idMember; } - /// C2S duel reply: accept (1) / reject (0) and inviter character id. + /// C2S duel reply (origin: who = inviter id, param = 0 accept / non-zero reject reason). [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct cmd_duel_reply { - public int accept; // 1 = accept, 0 = reject - public int idInviter; + public int who; // inviter character id (who sent the duel request) + public int param; // 0 = accept (同意), non-zero = reject reason (拒绝原因) } [StructLayout(LayoutKind.Sequential, Pack = 1)] diff --git a/Assets/Scripts/CECHostPlayer.cs b/Assets/Scripts/CECHostPlayer.cs index 325f77becd..3206e612ad 100644 --- a/Assets/Scripts/CECHostPlayer.cs +++ b/Assets/Scripts/CECHostPlayer.cs @@ -1106,6 +1106,9 @@ namespace BrewMonster { var data = (byte[])Msg.dwParam1; cmd_select_target pCmd = GPDataTypeHelper.FromBytes(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; } @@ -1895,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); @@ -2374,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() diff --git a/Assets/Scripts/CECUIManager.cs b/Assets/Scripts/CECUIManager.cs index 9e9715d792..a9c6906992 100644 --- a/Assets/Scripts/CECUIManager.cs +++ b/Assets/Scripts/CECUIManager.cs @@ -112,6 +112,9 @@ public class CECUIManager : MonoSingleton private void ShowUINPC(CECHostPlayer.NPCINFO obj) { + var host = EC_Game.GetGameRun()?.GetHostPlayer(); + if (host != null && host.GetSelectedTarget() != obj.IDNPC) + return; npsUI.gameObject.SetActive(true); string hpText = obj.MaxHealth > 0 ? $"{obj.CurrentHealth}/{obj.MaxHealth}" : "-/-"; npsUI.SetText(hpText, obj.Name ?? "", "");