Merge pull request 'feature/character_info' (#167) from feature/character_info into develop

Reviewed-on: https://git.pthub.vn/Unity/perfect-world-unity/pulls/167
This commit is contained in:
hungdk
2026-02-25 06:41:08 +00:00
8 changed files with 54 additions and 18 deletions
@@ -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}
@@ -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());
@@ -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);
}
}
@@ -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);
}
@@ -824,10 +824,14 @@ namespace CSNetwork.C2SCommand
return SerializeCommand(CommandID.DUEL_REQUEST, new cmd_team_new_member { idMember = idTarget });
}
/// <summary>C2S: accept (true) or reject (false) duel from inviter.</summary>
/// <summary>C2S: accept (true) or reject (false) duel from inviter. Origin: [cmd][who:4][param:4] — who=inviter id, param=0 accept / non-zero reject.</summary>
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)
@@ -2188,12 +2188,12 @@ namespace CSNetwork.GPDataType
public int idMember;
}
/// <summary>C2S duel reply: accept (1) / reject (0) and inviter character id.</summary>
/// <summary>C2S duel reply (origin: who = inviter id, param = 0 accept / non-zero reject reason).</summary>
[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)]
+19 -1
View File
@@ -1106,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;
}
@@ -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()
+3
View File
@@ -112,6 +112,9 @@ public class CECUIManager : MonoSingleton<CECUIManager>
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 ?? "", "");