1176 lines
46 KiB
C#
1176 lines
46 KiB
C#
using Assets.MyGame.Scripts.Net;
|
||
using Newtonsoft.Json;
|
||
using Spine.Unity;
|
||
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using TMPro;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
using static Assets.MyGame.Scripts.Net.WebResponses.ChineseChess.ChineseChessGameClassList;
|
||
|
||
public class HintData
|
||
{
|
||
public ChessGameRoom roomData;
|
||
public int hintType; //1求和2认输
|
||
}
|
||
|
||
/// <summary>
|
||
/// GoldConfigRequired Socket 推送数据
|
||
/// </summary>
|
||
[Serializable]
|
||
public class GoldConfigRequiredData
|
||
{
|
||
public string targetUserId;
|
||
public ChessGameRoom room;
|
||
}
|
||
|
||
public enum CountdownType
|
||
{
|
||
WaitPlayer, // 等待玩家进入,超时 → 退出到选房
|
||
WaitHostStart, // 等待房主开始游戏,超时 → 退出到选房
|
||
WaitSetCoin, // 等待设置金额,超时 → 退出到选房
|
||
WaitOpponent, // 等待对手操作,超时 → 自动处理
|
||
}
|
||
|
||
public class ChineseChessMainPanel : GameMainPanel_Base
|
||
{
|
||
public Button btn_QiuHe;
|
||
public Button btn_RenShu;
|
||
public Button btn_HuiQi;
|
||
public Button btn_SheZhi;
|
||
public Button btn_GuiZe;
|
||
public Button btn_StartGame;
|
||
public Button btn_TiRen; // 踢人按钮(拖拽:房主 + 开局前 + 双方在房间时自动显示,其余情况自动隐藏)
|
||
public Image img_DengDaiZhong;
|
||
public ChessGameRoom roomData;
|
||
/// <summary>4.4 已退出的房间 ID:退房后忽略该房间的后续推送(重新进房时由 MarkJoiningRoom 解除)</summary>
|
||
private string lastExitedRoomId;
|
||
public static Dictionary<string,ChessGameRoom> dic_RoomData=new Dictionary<string, ChessGameRoom>();
|
||
public Button btn_FanHuiDaTing;
|
||
[Header("Bottom")]
|
||
public TextMeshProUGUI tmp_Name_Bottom;
|
||
public TextMeshProUGUI tmp_CoinCount_Bottom;
|
||
public TextMeshProUGUI tmp_WaitTime_Bottom;
|
||
public Image img_Title_Bottom;
|
||
[Header("Top")]
|
||
public TextMeshProUGUI tmp_Name_Top;
|
||
public TextMeshProUGUI tmp_CoinCount_Top;
|
||
public TextMeshProUGUI tmp_WaitTime_Top;
|
||
public Image img_Title_Top;
|
||
|
||
[Header("ChessGame 象棋对局")]
|
||
public UIChessBoard chessBoard; // 棋盘控件(Inspector 手动拖入;留空则运行时自动生成)
|
||
|
||
private Coroutine waitCountdown;
|
||
|
||
public SkeletonGraphic skeletonGraphic;
|
||
public string skineName_JinXingZhong;
|
||
public string skineName_KaiShi;
|
||
public string skineName_PiPeiChengGong;
|
||
public string skineName_ZhengZaiPiPei;
|
||
|
||
public string animName_Close;
|
||
public string animName_Open;
|
||
public string animName_Loop;
|
||
|
||
#region Spine 动画辅助
|
||
|
||
/// <summary>激活/禁用 Spine(不用时禁用节省性能,用时激活)</summary>
|
||
public void ActiveSpine(bool active)
|
||
{
|
||
if (skeletonGraphic == null) return;
|
||
if (skeletonGraphic.gameObject.activeSelf != active)
|
||
skeletonGraphic.gameObject.SetActive(active);
|
||
}
|
||
|
||
/// <summary>设置 Spine skin(切换皮肤并重置 slot)</summary>
|
||
public void SetSpineSkin(string skinName)
|
||
{
|
||
if (skeletonGraphic == null || string.IsNullOrEmpty(skinName)) return;
|
||
ActiveSpine(true);
|
||
var skel = skeletonGraphic.Skeleton;
|
||
if (skel == null) return;
|
||
skel.SetSkin(skinName);
|
||
skel.SetSlotsToSetupPose();
|
||
skeletonGraphic.LateUpdate();
|
||
}
|
||
|
||
/// <summary>播放 Spine 动画</summary>
|
||
public void PlaySpineAnim(string animName, bool loop = true)
|
||
{
|
||
if (skeletonGraphic == null || string.IsNullOrEmpty(animName)) return;
|
||
ActiveSpine(true);
|
||
skeletonGraphic.AnimationState.SetAnimation(0, animName, loop);
|
||
}
|
||
|
||
/// <summary>切换到"正在匹配"状态</summary>
|
||
public void Spine_ZhengZaiPiPei()
|
||
{
|
||
SetSpineSkin(skineName_ZhengZaiPiPei);
|
||
PlaySpineAnim(animName_Loop, true);
|
||
}
|
||
|
||
/// <summary>切换到"匹配成功"状态:Open → 完成后播 Close → 禁用</summary>
|
||
public void Spine_PiPeiChengGong()
|
||
{
|
||
SetSpineSkin(skineName_PiPeiChengGong);
|
||
PlaySpineAnim(animName_Open, false);
|
||
// Open 播完后播 Close,然后禁用 Spine
|
||
StartCoroutine(DelayedSpineClose());
|
||
}
|
||
|
||
/// <summary>Open 动画播完后播放 Close 动画并禁用 Spine</summary>
|
||
private IEnumerator DelayedSpineClose()
|
||
{
|
||
// 等 Open 动画播完
|
||
yield return new WaitForSeconds(0.5f);
|
||
PlaySpineAnim(animName_Close, false);
|
||
// Close 播完后禁用 Spine
|
||
yield return new WaitForSeconds(0.5f);
|
||
ActiveSpine(false);
|
||
}
|
||
|
||
/// <summary>切换到"开始游戏"状态(短暂过渡)</summary>
|
||
public void Spine_KaiShi()
|
||
{
|
||
SetSpineSkin(skineName_KaiShi);
|
||
PlaySpineAnim(animName_Open, false);
|
||
}
|
||
|
||
/// <summary>切换到"进行中"状态</summary>
|
||
public void Spine_JinXingZhong()
|
||
{
|
||
SetSpineSkin(skineName_JinXingZhong);
|
||
PlaySpineAnim(animName_Loop, true);
|
||
}
|
||
|
||
/// <summary>延迟切换到"进行中"状态(等 KaiShi 过渡动画播完)</summary>
|
||
private IEnumerator DelayedSpine_JinXingZhong()
|
||
{
|
||
// 等待开始过渡动画播放完(默认 0.5s)
|
||
yield return new WaitForSeconds(0.5f);
|
||
Spine_JinXingZhong();
|
||
}
|
||
|
||
/// <summary>KaiShi 动画流程:Open 播完后切 Loop 播一小会,再播 Close 关闭并禁用</summary>
|
||
private IEnumerator DelayedKaiShiLoopToClose()
|
||
{
|
||
// 等 Open 动画播完
|
||
yield return new WaitForSeconds(0.5f);
|
||
// 切 Loop 动画播一小会
|
||
PlaySpineAnim(animName_Loop, true);
|
||
yield return new WaitForSeconds(1f);
|
||
// 播 Close 动画并禁用 Spine
|
||
PlaySpineAnim(animName_Close, false);
|
||
yield return new WaitForSeconds(0.5f);
|
||
ActiveSpine(false);
|
||
}
|
||
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// 加载头像精灵(优先使用缓存,否则异步下载)
|
||
/// </summary>
|
||
public static void LoadAvatar(Image target, string avatarUrl, System.Action<Sprite> onLoaded = null)
|
||
{
|
||
if (target == null) return;
|
||
var sprite = GameManager.Instance.GetAvatarSprite(avatarUrl);
|
||
if (sprite != null)
|
||
{
|
||
target.sprite = sprite;
|
||
onLoaded?.Invoke(sprite);
|
||
return;
|
||
}
|
||
// 缓存未命中,通过 DownSprite 协程异步加载
|
||
GameManager.Instance.StartCoroutine(LoadAvatarCoroutine(target, avatarUrl, onLoaded));
|
||
}
|
||
|
||
private static IEnumerator LoadAvatarCoroutine(Image target, string avatarUrl, System.Action<Sprite> onLoaded)
|
||
{
|
||
// 构造一个临时的 UserBaseInfo 对象来复用 DownSprite 逻辑
|
||
var tempInfo = new UserBaseInfo { avatarUrl = avatarUrl };
|
||
yield return tempInfo.DownSprite(sprite =>
|
||
{
|
||
if (sprite != null && target != null)
|
||
target.sprite = sprite;
|
||
onLoaded?.Invoke(sprite);
|
||
});
|
||
}
|
||
|
||
public static string FormatGold(long gold)
|
||
{
|
||
if (gold >= 100000000)
|
||
{
|
||
// 截取两位小数,不四舍五入
|
||
long yi = gold / 100000000;
|
||
long remainder = gold % 100000000;
|
||
long decimals = remainder * 100 / 100000000;
|
||
if (decimals > 0)
|
||
return $"{yi}.{decimals:D2}亿";
|
||
return $"{yi}亿";
|
||
}
|
||
if (gold >= 10000)
|
||
{
|
||
// 截取两位小数,不四舍五入
|
||
long wan = gold / 10000;
|
||
long remainder = gold % 10000;
|
||
long decimals = remainder * 100 / 10000; // 两位小数
|
||
if (decimals > 0)
|
||
return $"{wan}.{decimals:D2}万";
|
||
return $"{wan}万";
|
||
}
|
||
return gold.ToString();
|
||
}
|
||
|
||
public void StartGame(string msg)
|
||
{
|
||
var va = JsonConvert.DeserializeObject<WebSocketResponseBase<ChessGameRoom>>(msg.ToString());
|
||
StartGameRoom(va.arguments[0]);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 4.3 房间状态完整快照:BeginGame / RoomStateNotice / GetRoomState 返回统一走这里。
|
||
/// 用服务端房间对象整体覆盖本地状态(users/state/gold/createUserId/isGameEnded/timeoutSeconds/pendingWinUserId),
|
||
/// 不做"人数 ±1"式增量更新。
|
||
/// </summary>
|
||
public void ApplyRoomSnapshot(ChessGameRoom room)
|
||
{
|
||
if (room == null || string.IsNullOrEmpty(room.roomId)) return;
|
||
|
||
// 4.4 防串房:忽略已退出的旧房间推送
|
||
if (!string.IsNullOrEmpty(lastExitedRoomId) && room.roomId == lastExitedRoomId)
|
||
{
|
||
Debug.Log($"[Chess] 忽略已退出房间的快照: {room.roomId}");
|
||
return;
|
||
}
|
||
// 4.4 防串房:本地在 A 房间时忽略 B 房间的推送
|
||
if (roomData != null && !string.IsNullOrEmpty(roomData.roomId) && roomData.roomId != room.roomId)
|
||
{
|
||
Debug.Log($"[Chess] 忽略其他房间快照: {room.roomId}(当前: {roomData.roomId})");
|
||
return;
|
||
}
|
||
|
||
bool wasActive = IsChessGameActive;
|
||
roomData = room; // 4.3 整体覆盖
|
||
|
||
if (wasActive && room.state == 2 && !room.isGameEnded)
|
||
{
|
||
// 4.1 断线重连/取消挂起的恢复同步:只刷新双方信息,不重置正在进行的棋局
|
||
UpdateUserData(room);
|
||
return;
|
||
}
|
||
|
||
if (wasActive)
|
||
{
|
||
// 4.1 房间已被服务端重置(双断线)或已结束:清除本地对局,按快照回到对应阶段
|
||
ClearChessGame();
|
||
}
|
||
|
||
subUIManager.CloseAll();
|
||
StartGameRoom(room);
|
||
}
|
||
|
||
/// <summary>重新进入指定房间前调用:解除对该房间的"旧房间推送过滤"(建房/进房面板发送请求时用)</summary>
|
||
public void MarkJoiningRoom(string roomId)
|
||
{
|
||
if (!string.IsNullOrEmpty(roomId) && roomId == lastExitedRoomId)
|
||
lastExitedRoomId = null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 4.4 清空本地房间状态:gameId、棋盘、双方玩家、准备状态、结算状态
|
||
/// </summary>
|
||
public void ClearRoomLocalState()
|
||
{
|
||
if (roomData != null) lastExitedRoomId = roomData.roomId; // 记录,忽略该房间后续推送
|
||
ClearChessGame();
|
||
roomData = null;
|
||
RefreshKickButtonVisible(); // 已退房 → 隐藏踢人按钮
|
||
}
|
||
|
||
private void StartGameRoom(ChessGameRoom chessGameRoom)
|
||
{
|
||
roomData = chessGameRoom; // 记录房间数据(开象棋对局时要取阵营)
|
||
StartHeartbeat(); // 房间心跳(更新活跃时间 + 断线超时检查)
|
||
UpdateUserData(chessGameRoom);
|
||
|
||
switch (chessGameRoom.users.Count)
|
||
{
|
||
case 1:
|
||
{
|
||
tmp_Name_Top.text = "等待玩家进入";
|
||
tmp_CoinCount_Top.text = "";
|
||
img_Title_Top.gameObject.SetActive(false);
|
||
if (tmp_WaitTime_Bottom != null) tmp_WaitTime_Bottom.text = "等待玩家进入02:00";
|
||
if (tmp_WaitTime_Top != null) tmp_WaitTime_Top.text = "等待玩家进入02:00";
|
||
StartWaitCountdown(120, CountdownType.WaitPlayer, "等待玩家进入");
|
||
// 正在匹配
|
||
Spine_ZhengZaiPiPei();
|
||
}
|
||
break;
|
||
case 2:
|
||
{
|
||
StopWaitCountdown();
|
||
img_Title_Top.gameObject.SetActive(true);
|
||
|
||
// 判断是否为房主
|
||
bool isRoomOwner = chessGameRoom.users.Count > 0
|
||
&& chessGameRoom.users[0].userId == GameManager.Instance.user.BaseInfo.number;
|
||
|
||
if (isRoomOwner)
|
||
{
|
||
//btn_StartGame.gameObject.SetActive(true);
|
||
btn_StartGame.onClick.RemoveAllListeners();
|
||
btn_StartGame.onClick.AddListener(() =>
|
||
{
|
||
// 房主点击开始 → 发送 StartGame Socket 请求
|
||
btn_StartGame.gameObject.SetActive(false);
|
||
StopWaitCountdown();
|
||
WebSocketSendList.StartGameChineseChess(chessGameRoom.roomId, GameManager.Instance.user.BaseInfo.number);
|
||
});
|
||
}
|
||
else
|
||
{
|
||
btn_StartGame.gameObject.SetActive(false);
|
||
}
|
||
|
||
StartWaitCountdown(60, CountdownType.WaitHostStart, "等待房主开始游戏");
|
||
// 匹配成功
|
||
Spine_PiPeiChengGong();
|
||
}
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 服务端推送 GoldConfigRequired → 显示金币设置面板(红方配置)
|
||
/// data: { targetUserId, room }
|
||
/// </summary>
|
||
public void OnGoldConfigRequired(string msg)
|
||
{
|
||
StopWaitCountdown();
|
||
var result = JsonConvert.DeserializeObject<WebSocketResponseBase<GoldConfigRequiredData>>(msg);
|
||
var data = result.arguments[0];
|
||
|
||
// 更新房间数据
|
||
if (data.room != null)
|
||
{
|
||
roomData = data.room;
|
||
UpdateUserData(data.room);
|
||
}
|
||
|
||
// 判断当前用户是否为红方(targetUserId)
|
||
bool isRedSide = data.targetUserId == GameManager.Instance.user.BaseInfo.number;
|
||
if (isRedSide)
|
||
{
|
||
// 红方显示金币设置面板
|
||
subUIManager.ShowAsync(UIPanel.ZGXQSelectCoin, roomData);
|
||
StartWaitCountdown(60, CountdownType.WaitSetCoin, "等待设置金额");
|
||
}
|
||
else
|
||
{
|
||
// 黑方等待红方设置金币
|
||
StartWaitCountdown(60, CountdownType.WaitSetCoin, "等待对方设置金额");
|
||
}
|
||
}
|
||
|
||
public void UpdateUserData(ChessGameRoom chessGameRoom)
|
||
{
|
||
foreach (var item in chessGameRoom.users)
|
||
{
|
||
if (item.userId == GameManager.Instance.user.BaseInfo.number)
|
||
{
|
||
tmp_Name_Bottom.text ="ID:" + item.userId;
|
||
tmp_CoinCount_Bottom.text = FormatGold(item.gold);
|
||
LoadAvatar(img_Title_Bottom, item.userAvatar);
|
||
}
|
||
else
|
||
{
|
||
tmp_Name_Top.text ="ID:" + item.userId;
|
||
tmp_CoinCount_Top.text = FormatGold(item.gold);
|
||
LoadAvatar(img_Title_Top, item.userAvatar);
|
||
}
|
||
}
|
||
RefreshKickButtonVisible(); // 房间数据刷新 → 同步踢人按钮显隐
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用户数据变化回调(结算/领奖/邮件等触发 LoginHub AssetUpdated → GetUser → 此处)
|
||
/// 用账户金币实时校准房间内自己的金币显示(账户金币是权威数据)
|
||
/// </summary>
|
||
private void OnUserDataChangedHandler()
|
||
{
|
||
if (roomData?.users == null) return;
|
||
var assets = GameManager.Instance?.user?.assets;
|
||
if (assets == null) return;
|
||
string myId = GameManager.Instance.user.BaseInfo.number;
|
||
var mine = roomData.users.Find(u => u.userId == myId);
|
||
if (mine == null || mine.gold == assets.gold) return;
|
||
mine.gold = assets.gold;
|
||
if (tmp_CoinCount_Bottom != null) tmp_CoinCount_Bottom.text = FormatGold(mine.gold);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 结算后实时刷新房间双方金币:按服务端结果本地预更新(赢家 +底分,输家 -底分)
|
||
/// (对方的账户变化客户端收不到推送,只能按底分本地算;
|
||
/// 自己的金币随后由 AssetUpdated → OnUserDataChangedHandler 用账户数据校准)
|
||
/// winUserId:赢家 userId;空 = 和棋(金币不变)
|
||
/// </summary>
|
||
public void ApplySettlementGold(string winUserId)
|
||
{
|
||
if (string.IsNullOrEmpty(winUserId)) return; // 和棋不动金币
|
||
if (roomData?.users == null || roomData.users.Count < 2) return;
|
||
|
||
long bet = roomData.gold;
|
||
for (int i = 0; i < roomData.users.Count; i++)
|
||
{
|
||
var u = roomData.users[i];
|
||
if (u.userId == winUserId) u.gold += bet;
|
||
else u.gold = u.gold > bet ? u.gold - bet : 0;
|
||
}
|
||
UpdateUserData(roomData);
|
||
}
|
||
|
||
public override void Awake()
|
||
{
|
||
base.Awake();
|
||
btn_FanHuiDaTing.onClick.AddListener(() =>
|
||
{
|
||
if (roomData != null)
|
||
{
|
||
// 4.4 在房间中:必须主动调用退出房间,不能只断开连接
|
||
WebSocketSendList.ExitRoomChineseChess(roomData.roomId, MyChessUserId);
|
||
ClearRoomLocalState(); // 立即清 gameId/棋盘/双方/准备/结算状态
|
||
subUIManager.CloseAll(); // 关结算等子面板
|
||
WebSocketSendList.GetRoomListChineseChess(0); // 回大厅重拉列表(响应自动刷新选房界面,0=重新加入大厅通知组)
|
||
}
|
||
else
|
||
{
|
||
// 4.4 已在选房界面:关闭象棋面板(OnHide 兜底再退一次房)
|
||
UIManager.Instance.Close(UIPanel.MainGamePanelZGXQ);
|
||
}
|
||
});
|
||
btn_GuiZe.onClick.AddListener(() =>
|
||
{
|
||
subUIManager.ShowAsync(UIPanel.ZGXQIntroduce);
|
||
});
|
||
// 4.5 踢人按钮:房主 + 开局前 + 对方在房间时才显示(显隐由 RefreshKickButtonVisible 控制)
|
||
if (btn_TiRen != null)
|
||
{
|
||
btn_TiRen.onClick.RemoveAllListeners();
|
||
btn_TiRen.onClick.AddListener(OnKickButtonClicked);
|
||
btn_TiRen.gameObject.SetActive(false); // 默认隐藏
|
||
}
|
||
}
|
||
protected override void OnShow(object args = null)
|
||
{
|
||
base.OnShow(args);
|
||
// 订阅用户数据变化:结算/领奖后 LoginHub AssetUpdated 会触发,实时刷新房间金币
|
||
GameManager.Instance.OnUserDataChanged -= OnUserDataChangedHandler;
|
||
GameManager.Instance.OnUserDataChanged += OnUserDataChangedHandler;
|
||
RefreshKickButtonVisible(); // 进面板默认隐藏踢人按钮(防上次残留)
|
||
// 回到选房间界面时显示等待中(连带父对象)
|
||
SetDengDaiZhongActive(true);
|
||
subUIManager.ShowAsync(UIPanel.ZGXQDistrict, args);
|
||
// 选房阶段不使用 Spine,禁用
|
||
ActiveSpine(false);
|
||
}
|
||
/// <summary>
|
||
/// 显示/隐藏等待中(连带控制父对象链)
|
||
/// </summary>
|
||
public void SetDengDaiZhongActive(bool active)
|
||
{
|
||
if (img_DengDaiZhong != null)
|
||
{
|
||
img_DengDaiZhong.gameObject.SetActive(active);
|
||
}
|
||
}
|
||
public void IsShowHint(string id,int type)
|
||
{
|
||
// 4.4 只处理当前对手发起的操作请求(旧房间/陌生 userId 忽略)
|
||
if (roomData == null || id != OpponentChessUserId) return;
|
||
if (GameManager.Instance.user.BaseInfo.number!=id)
|
||
{
|
||
// 悔棋回执:直接处理,不弹窗
|
||
if (type == 4) // 对方同意悔棋 → 本地悔一步
|
||
{
|
||
OnUndoAgreed();
|
||
return;
|
||
}
|
||
if (type == 5) // 对方拒绝悔棋
|
||
{
|
||
if (chessBoard != null) chessBoard.ShowToast("对方拒绝悔棋");
|
||
return;
|
||
}
|
||
// 1求和 2认输 3悔棋请求 → 走之前的确认弹窗
|
||
subUIManager.ShowAsync(UIPanel.ZGXQHint, new HintData
|
||
{
|
||
roomData = roomData,
|
||
hintType = type
|
||
});
|
||
}
|
||
}
|
||
public void InitRoomData()
|
||
{
|
||
// 游戏已开始,停止所有倒计时
|
||
StopWaitCountdown();
|
||
|
||
// 关闭游戏说明面板(双方都准备完毕,进入进行中)
|
||
subUIManager.Close(UIPanel.ZGXQGameExplain);
|
||
|
||
// 双方都准备完毕:播放 KaiShi Spine 动画 Open → Loop 一小会 → Close
|
||
Spine_KaiShi();
|
||
StartCoroutine(DelayedKaiShiLoopToClose());
|
||
|
||
// 初始化象棋对局(普通管理类统一在此控制)
|
||
StartChessGame();
|
||
|
||
btn_QiuHe.onClick.RemoveAllListeners();
|
||
btn_RenShu.onClick.RemoveAllListeners();
|
||
btn_HuiQi.onClick.RemoveAllListeners();
|
||
btn_HuiQi.onClick.AddListener(RequestUndoChess);
|
||
btn_QiuHe.onClick.AddListener(() =>
|
||
{
|
||
WebSocketSendList.GameOperationChineseChess(roomData.roomId,1,GameManager.Instance.user.BaseInfo.number);
|
||
|
||
});
|
||
btn_RenShu.onClick.AddListener(() =>
|
||
{
|
||
//WebSocketSendList.GameOperationChineseChess(roomData.roomId, 2, GameManager.Instance.user.BaseInfo.number);
|
||
|
||
subUIManager.ShowAsync(UIPanel.ZGXQHint, new HintData
|
||
{
|
||
roomData = roomData,
|
||
hintType = 2
|
||
});
|
||
});
|
||
}
|
||
|
||
#region 象棋对局(普通管理类统一在面板控制)
|
||
|
||
/// <summary>象棋对局总管理器(普通类)</summary>
|
||
private ChessGameManager chessGameManager;
|
||
|
||
/// <summary>运行时自动生成的棋盘(用于退出时销毁)</summary>
|
||
private RectTransform autoChessBoardRect;
|
||
|
||
/// <summary>
|
||
/// 开始一局象棋(双方准备完毕,InitRoomData 里调用)
|
||
/// 所有规则/走法本地运算(帧同步),后端只透传数据与结算胜负
|
||
/// </summary>
|
||
private void StartChessGame()
|
||
{
|
||
if (roomData == null || roomData.users == null) return;
|
||
|
||
// 新一局开始:清挂起状态
|
||
isMyAfk = false;
|
||
isOpponentAfk = false;
|
||
StopAfkMonitor();
|
||
|
||
// 我的阵营:RedisGameUser.type 1 黑方 2 红方
|
||
var me = roomData.users.Find(u => u.userId == GameManager.Instance.user.BaseInfo.number);
|
||
ChessCamp myCamp = me != null ? (ChessCamp)me.type : ChessCamp.Red;
|
||
|
||
chessGameManager = new ChessGameManager();
|
||
chessGameManager.OnMoveApplied += OnChessMoveApplied;
|
||
chessGameManager.OnGameOver += OnChessGameOver;
|
||
chessGameManager.OnTurnChanged += OnChessTurnChanged; // 要在 StartGame 前订阅(开局红先会触发一次)
|
||
chessGameManager.StartGame(myCamp);
|
||
RefreshKickButtonVisible(); // 对局开始 → 隐藏踢人按钮
|
||
|
||
if (chessBoard == null) CreateDefaultChessBoard();
|
||
chessBoard.Init(chessGameManager, myCamp);
|
||
}
|
||
|
||
/// <summary>面板上没拖棋盘控件时,自动生成一个(不依赖任何预制体/资源,定位点也自动建好)</summary>
|
||
private void CreateDefaultChessBoard()
|
||
{
|
||
var go = new GameObject("ChessBoard_Auto", typeof(RectTransform));
|
||
var rt = go.GetComponent<RectTransform>();
|
||
rt.SetParent(transform, false);
|
||
|
||
var prt = (RectTransform)transform;
|
||
float pw = prt.rect.width > 10f ? prt.rect.width : 1080f;
|
||
float ph = prt.rect.height > 10f ? prt.rect.height : 1920f;
|
||
float step = Mathf.Min(pw * 0.96f / 9f, ph * 0.88f / 10.1f);
|
||
|
||
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
|
||
rt.anchoredPosition = new Vector2(0f, -step * 0.4f); // 稍微下移,避开上方对手信息
|
||
rt.sizeDelta = new Vector2(step * 9f, step * 10.1f);
|
||
|
||
chessBoard = go.AddComponent<UIChessBoard>();
|
||
|
||
// 自动生成时:代码自建两个定位点(逻辑 (0,4) 与 (0,5),网格铺满棋盘区域)
|
||
// 逻辑 (0,4) = (-4*step, -0.5*step),逻辑 (0,5) = (-4*step, +0.5*step)(棋盘中心为原点)
|
||
var ps = new GameObject("Point_Start", typeof(RectTransform)).GetComponent<RectTransform>();
|
||
ps.SetParent(rt, false);
|
||
ps.anchoredPosition = new Vector2(-4f * step, -0.5f * step);
|
||
var pe = new GameObject("Point_End", typeof(RectTransform)).GetComponent<RectTransform>();
|
||
pe.SetParent(rt, false);
|
||
pe.anchoredPosition = new Vector2(-4f * step, 0.5f * step);
|
||
chessBoard.point_Start = ps;
|
||
chessBoard.point_End = pe;
|
||
|
||
autoChessBoardRect = rt;
|
||
}
|
||
|
||
/// <summary>本地走子成功 → 帧同步给对方(走 SyncGameData 通道,后端只透传)</summary>
|
||
private void OnChessMoveApplied(ChessMoveEventArgs e)
|
||
{
|
||
if (!e.isLocal) return; // 对方同步过来的不回发
|
||
SendChessNetData(new ChessNetData { msgType = 1, move = e.move });
|
||
}
|
||
|
||
/// <summary>对局结束(本地规则判定)→ 上报后端结算 + 弹结果面板</summary>
|
||
private void OnChessGameOver(ChessCamp winner)
|
||
{
|
||
StopWaitCountdown(); // 对局结束,停走棋倒计时
|
||
bool iWin = winner == chessGameManager.myCamp;
|
||
if (iWin)
|
||
{
|
||
// 只有赢家上报(避免双端重复),EndGame 第二个参数为胜者 userId
|
||
WebSocketSendList.EndGameChineseChess(roomData.roomId, GameManager.Instance.user.BaseInfo.number);
|
||
subUIManager.ShowAsync(UIPanel.ZGXQWin);
|
||
}
|
||
else
|
||
{
|
||
subUIManager.ShowAsync(UIPanel.ZGXQDefeated);
|
||
}
|
||
}
|
||
|
||
/// <summary>悔棋按钮 → 走之前写好的 GameOperation 通道(1求和 2认输 3悔棋),对方确认后双方各悔一步</summary>
|
||
private void RequestUndoChess()
|
||
{
|
||
if (chessGameManager == null || chessGameManager.isGameOver) return;
|
||
if (chessGameManager.recorder.Count == 0)
|
||
{
|
||
if (chessBoard != null) chessBoard.ShowToast("暂无可悔的棋步");
|
||
return;
|
||
}
|
||
WebSocketSendList.GameOperationChineseChess(roomData.roomId, 3, GameManager.Instance.user.BaseInfo.number);
|
||
if (chessBoard != null) chessBoard.ShowToast("已发送悔棋请求");
|
||
}
|
||
|
||
/// <summary>同意对方的悔棋请求(UIChineseChessHint 的同意按钮调用):回执已由弹窗发送,这里本地悔一步</summary>
|
||
public void AgreeUndoChess()
|
||
{
|
||
if (chessGameManager == null) return;
|
||
chessGameManager.UndoOneStep();
|
||
}
|
||
|
||
/// <summary>收到对方同意悔棋的回执(IsShowHint type=4):本地悔一步</summary>
|
||
private void OnUndoAgreed()
|
||
{
|
||
if (chessGameManager == null) return;
|
||
chessGameManager.UndoOneStep();
|
||
if (chessBoard != null) chessBoard.ShowToast("对方同意悔棋");
|
||
}
|
||
|
||
#region 对局走棋倒计时(复用原有倒计时字段:waitCountdown / StopWaitCountdown / tmp_WaitTime_Bottom、Top)
|
||
|
||
/// <summary>每回合倒计时秒数(用服务端房间配置 timeoutSeconds,没配默认 60)</summary>
|
||
private int ChessTurnSeconds => roomData != null && roomData.timeoutSeconds > 0 ? roomData.timeoutSeconds : 60;
|
||
|
||
/// <summary>回合切换 → 重新开始走棋倒计时(当前回合方的信息栏显示)</summary>
|
||
private void OnChessTurnChanged(ChessCamp camp)
|
||
{
|
||
if (chessGameManager == null || chessGameManager.isGameOver) return;
|
||
bool myTurn = camp == chessGameManager.myCamp;
|
||
if (waitCountdown != null) StopCoroutine(waitCountdown);
|
||
waitCountdown = StartCoroutine(TurnCountdownRoutine(ChessTurnSeconds, myTurn));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 走棋倒计时:轮到我 → 显示在下方(超时 AI 自动走一步,不判负不结算);
|
||
/// 轮到对方 → 显示在上方(对方客户端超时同样由它的 AI 自动走)
|
||
/// </summary>
|
||
private IEnumerator TurnCountdownRoutine(int totalSeconds, bool myTurn)
|
||
{
|
||
int remaining = totalSeconds;
|
||
while (remaining >= 0)
|
||
{
|
||
string timeStr = $"{remaining / 60:D2}:{remaining % 60:D2}";
|
||
if (myTurn)
|
||
{
|
||
if (tmp_WaitTime_Bottom != null) tmp_WaitTime_Bottom.text = $"请走棋 {timeStr}";
|
||
if (tmp_WaitTime_Top != null) tmp_WaitTime_Top.text = "";
|
||
}
|
||
else
|
||
{
|
||
if (tmp_WaitTime_Top != null) tmp_WaitTime_Top.text = $"对方走棋 {timeStr}";
|
||
if (tmp_WaitTime_Bottom != null) tmp_WaitTime_Bottom.text = "";
|
||
}
|
||
yield return new WaitForSeconds(1f);
|
||
remaining--;
|
||
}
|
||
|
||
if (myTurn)
|
||
{
|
||
// 我方超时 → AI 自动走一步(走完会触发回合切换,倒计时自动重开)
|
||
if (chessGameManager != null && !chessGameManager.isGameOver)
|
||
{
|
||
bool moved = chessGameManager.AutoMove();
|
||
if (moved && chessBoard != null) chessBoard.ShowToast("超时,已自动走子");
|
||
}
|
||
}
|
||
// 对方回合超时:对方客户端同样 AI 自动走一步,我方等它的走子帧
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 玩家挂起(退到后台)/ 断线 / 房间心跳
|
||
|
||
/// <summary>挂起判负秒数:挂起最多 120s 超时判输;双方都挂起时,先挂起的判输</summary>
|
||
private const float AfkTimeoutSeconds = 120f;
|
||
/// <summary>挂起判负毫秒数(北京时间戳比较用)</summary>
|
||
private const long AfkTimeoutMs = (long)(AfkTimeoutSeconds * 1000f);
|
||
|
||
/// <summary>当前北京时间(毫秒,服务端同步)</summary>
|
||
private static long NowMs => BeijingTimeManager.Instance.CurrentTimestampMs;
|
||
|
||
private bool isMyAfk; // 我是否挂起中(退到后台)
|
||
private long myAfkStartTimeMs; // 我挂起开始时间(北京时间毫秒)
|
||
private bool isOpponentAfk; // 对方是否挂起中
|
||
private long opponentAfkStartTimeMs; // 对方挂起开始时间(北京时间毫秒)
|
||
private Coroutine afkMonitorCoroutine;
|
||
private Coroutine heartbeatCoroutine;
|
||
private Coroutine disconnectCountdownCoroutine; // 对方断线 60s 离线倒计时
|
||
|
||
/// <summary>是否处于对局中(挂起/断线/重连处理只对进行中的对局生效)</summary>
|
||
public bool IsChessGameActive => chessGameManager != null && !chessGameManager.isGameOver;
|
||
|
||
private string MyChessUserId => GameManager.Instance.user.BaseInfo.number;
|
||
|
||
private string OpponentChessUserId
|
||
{
|
||
get
|
||
{
|
||
if (roomData?.users == null) return null;
|
||
var op = roomData.users.Find(u => u.userId != MyChessUserId);
|
||
return op?.userId;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用切后台/回前台(iOS/Android 都会触发 OnApplicationPause):
|
||
/// 退到后台 = 挂起(发 SetPlayerAfk);回前台 = 取消挂起(发 CancelPlayerAfk,本人会收到 BeginGame 恢复同步)
|
||
/// </summary>
|
||
private void OnApplicationPause(bool pause)
|
||
{
|
||
if (roomData == null || !IsChessGameActive) return; // 只对进行中的对局处理
|
||
|
||
if (pause)
|
||
{
|
||
isMyAfk = true;
|
||
myAfkStartTimeMs = NowMs;
|
||
StopWaitCountdown(); // 后台期间停走棋倒计时,回前台重新计
|
||
WebSocketSendList.SetPlayerAfkChineseChess(roomData.roomId, MyChessUserId);
|
||
}
|
||
else if (isMyAfk)
|
||
{
|
||
isMyAfk = false;
|
||
WebSocketSendList.CancelPlayerAfkChineseChess(roomData.roomId, MyChessUserId);
|
||
|
||
// ===== 挂起判定(北京时间,回前台必判,弹对应结果面板):
|
||
// 谁挂起满 120s 谁输;双方都超时 → 先挂起的输 =====
|
||
long now = NowMs;
|
||
bool iTimedOut = now - myAfkStartTimeMs >= AfkTimeoutMs;
|
||
bool oppTimedOut = isOpponentAfk && now - opponentAfkStartTimeMs >= AfkTimeoutMs;
|
||
ChessCamp oppCamp = chessGameManager.myCamp == ChessCamp.Red ? ChessCamp.Black : ChessCamp.Red;
|
||
|
||
if (oppTimedOut && (!iTimedOut || opponentAfkStartTimeMs <= myAfkStartTimeMs))
|
||
{
|
||
JudgeAfkLose(chessGameManager.myCamp); // 对方超时(或双方都超时但对方先挂起)→ 对方输
|
||
return;
|
||
}
|
||
if (iTimedOut)
|
||
{
|
||
JudgeAfkLose(oppCamp); // 我挂起超时 → 我输(本地立即弹失败面板)
|
||
return;
|
||
}
|
||
// 都没超时 → 继续对局(对方挂起监控继续跑)
|
||
|
||
// 4.1 兜底:主动查询房间真实状态,防止服务端已判负/重置但推送在后台丢失
|
||
// (快照分流:还在对局 → 只刷新数据;已判负/重置 → 自动清本地回对应界面)
|
||
if (roomData != null) WebSocketSendList.GetRoomStateChineseChess(roomData.roomId);
|
||
|
||
// 恢复走棋倒计时
|
||
OnChessTurnChanged(chessGameManager.currentTurn);
|
||
}
|
||
}
|
||
|
||
/// <summary>PlayerAfkNotice:对方退到后台 → 提示 + 开始 120s 判负监控</summary>
|
||
public void OnPlayerAfkNotice(string userId)
|
||
{
|
||
if (!IsChessGameActive || userId != OpponentChessUserId) return;
|
||
isOpponentAfk = true;
|
||
opponentAfkStartTimeMs = NowMs;
|
||
if (chessBoard != null) chessBoard.ShowToast($"对方已挂起,{AfkTimeoutSeconds:0}秒内未恢复将判负");
|
||
StartAfkMonitor();
|
||
}
|
||
|
||
/// <summary>PlayerCancelAfkNotice:对方回前台 → 清除挂起,继续对局</summary>
|
||
public void OnPlayerCancelAfkNotice(string userId)
|
||
{
|
||
if (userId != OpponentChessUserId) return;
|
||
isOpponentAfk = false;
|
||
StopAfkMonitor();
|
||
if (chessBoard != null) chessBoard.ShowToast("对方已恢复");
|
||
}
|
||
|
||
/// <summary>对方挂起监控:挂起满 120s → 对方判输(我在前台时运行;我挂起时冻结,回前台再判)</summary>
|
||
private IEnumerator AfkMonitorRoutine()
|
||
{
|
||
while (IsChessGameActive && isOpponentAfk)
|
||
{
|
||
if (NowMs - opponentAfkStartTimeMs >= AfkTimeoutMs)
|
||
{
|
||
JudgeAfkLose(chessGameManager.myCamp); // 对方挂起超时 → 对方输
|
||
yield break;
|
||
}
|
||
yield return new WaitForSeconds(1f);
|
||
}
|
||
}
|
||
|
||
private void StartAfkMonitor()
|
||
{
|
||
StopAfkMonitor();
|
||
if (IsChessGameActive && isActiveAndEnabled)
|
||
afkMonitorCoroutine = StartCoroutine(AfkMonitorRoutine());
|
||
}
|
||
|
||
private void StopAfkMonitor()
|
||
{
|
||
if (afkMonitorCoroutine != null)
|
||
{
|
||
StopCoroutine(afkMonitorCoroutine);
|
||
afkMonitorCoroutine = null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 挂起判负结算:由判定方直接上报 EndGame(对方可能还在后台收不到,不能只靠赢家上报)
|
||
/// </summary>
|
||
private void JudgeAfkLose(ChessCamp winnerCamp)
|
||
{
|
||
if (!IsChessGameActive) return;
|
||
StopAfkMonitor();
|
||
isOpponentAfk = false;
|
||
isMyAfk = false;
|
||
StopWaitCountdown();
|
||
|
||
bool iWin = winnerCamp == chessGameManager.myCamp;
|
||
string winnerId = iWin ? MyChessUserId : OpponentChessUserId;
|
||
if (roomData != null && !string.IsNullOrEmpty(winnerId))
|
||
WebSocketSendList.EndGameChineseChess(roomData.roomId, winnerId); // EndGame 第二参 = 胜者 userId
|
||
|
||
// 本地对局置结束(静默:不走 OnChessGameOver 的重复上报,上面已上报)
|
||
chessGameManager.ForceGameOver(winnerCamp, silent: true);
|
||
subUIManager.ShowAsync(iWin ? UIPanel.ZGXQWin : UIPanel.ZGXQDefeated);
|
||
}
|
||
|
||
/// <summary>Disconnect:对方断线 → 停走棋倒计时 + 上方显示离线倒计时(60s 内未重连由服务端判负,超时后主动查房间状态兜底)</summary>
|
||
public void OnOpponentDisconnected(string userId, int timeoutSeconds)
|
||
{
|
||
if (!IsChessGameActive || userId != OpponentChessUserId) return;
|
||
StopWaitCountdown();
|
||
if (disconnectCountdownCoroutine != null) StopCoroutine(disconnectCountdownCoroutine);
|
||
disconnectCountdownCoroutine = StartCoroutine(DisconnectCountdownRoutine(Mathf.Max(1, timeoutSeconds)));
|
||
}
|
||
|
||
/// <summary>对方断线倒计时:显示离线状态和剩余秒数;倒计时归零后主动 GetRoomState 校准(房间被重置/已判负时快照分流自动处理)</summary>
|
||
private IEnumerator DisconnectCountdownRoutine(int totalSeconds)
|
||
{
|
||
int remaining = totalSeconds;
|
||
while (remaining >= 0)
|
||
{
|
||
if (tmp_WaitTime_Top != null) tmp_WaitTime_Top.text = $"对方断线 {remaining / 60:D2}:{remaining % 60:D2}";
|
||
if (tmp_WaitTime_Bottom != null) tmp_WaitTime_Bottom.text = "";
|
||
yield return new WaitForSeconds(1f);
|
||
remaining--;
|
||
}
|
||
disconnectCountdownCoroutine = null;
|
||
if (tmp_WaitTime_Top != null) tmp_WaitTime_Top.text = "";
|
||
|
||
// 4.1 超过重连时间:主动查询房间真实状态(服务端超时会推 GameEnd;房间已释放/重置则 RoomStateNotice 快照分流带回大厅)
|
||
if (IsChessGameActive && roomData != null)
|
||
WebSocketSendList.GetRoomStateChineseChess(roomData.roomId);
|
||
}
|
||
|
||
/// <summary>Reconnect:对方重连 → 停断线倒计时 + 恢复走棋倒计时</summary>
|
||
public void OnOpponentReconnected(string userId)
|
||
{
|
||
if (!IsChessGameActive || userId != OpponentChessUserId) return;
|
||
if (disconnectCountdownCoroutine != null)
|
||
{
|
||
StopCoroutine(disconnectCountdownCoroutine);
|
||
disconnectCountdownCoroutine = null;
|
||
if (tmp_WaitTime_Top != null) tmp_WaitTime_Top.text = "";
|
||
}
|
||
if (chessBoard != null) chessBoard.ShowToast("对方已重连");
|
||
OnChessTurnChanged(chessGameManager.currentTurn);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 踢人(4.5:房主开局前可踢人,被踢端完整处理)
|
||
|
||
/// <summary>4.5 被踢出房间 KickedFromRoom({ gameId, userId, reason?, source? }):只处理自己被踢</summary>
|
||
public void OnKickedFromRoom(string userId, string reason)
|
||
{
|
||
if (userId != MyChessUserId) return;
|
||
|
||
// 停止计时/棋盘/输入 → 清空房间本地状态 → 回大厅重拉列表
|
||
ClearRoomLocalState();
|
||
subUIManager.CloseAll();
|
||
WebSocketSendList.GetRoomListChineseChess(0);
|
||
|
||
// 优先显示服务端 reason,为空显示默认文案
|
||
UIManager.Instance.ShowBoardError(string.IsNullOrEmpty(reason) ? "已被移出房间" : reason);
|
||
}
|
||
|
||
/// <summary>房主点击踢人按钮(开局前):3 秒内二次点击确认踢出(4.5 KickPlayer)</summary>
|
||
private float lastKickClickTime;
|
||
|
||
private void OnKickButtonClicked()
|
||
{
|
||
// 仅房主 + 开局前 + 对方在房间内时可踢
|
||
if (roomData == null || roomData.users == null || roomData.users.Count < 2) return;
|
||
if (IsChessGameActive || roomData.state == 2) return; // 对局中/服务端已开局不可踢
|
||
if (roomData.users[0].userId != MyChessUserId) return; // 第一个进房的是房主,只有房主能踢
|
||
string oppId = OpponentChessUserId;
|
||
if (string.IsNullOrEmpty(oppId)) return;
|
||
|
||
if (Time.realtimeSinceStartup - lastKickClickTime > 3f)
|
||
{
|
||
lastKickClickTime = Time.realtimeSinceStartup;
|
||
UIManager.Instance.ShowBoardError("再次点击踢人按钮确认踢出对方");
|
||
return;
|
||
}
|
||
|
||
WebSocketSendList.KickPlayerChineseChess(roomData.roomId, oppId);
|
||
}
|
||
|
||
/// <summary>踢人按钮显隐:房主(第一个进房)+ 开局前 + 双方在房间时显示,其余隐藏(房间数据每次刷新时调用)</summary>
|
||
private void RefreshKickButtonVisible()
|
||
{
|
||
if (btn_TiRen == null) return;
|
||
bool show = roomData != null && roomData.users != null
|
||
&& roomData.users.Count >= 2 // 对方在房间
|
||
&& roomData.users[0].userId == MyChessUserId // 第一个进房的是房主
|
||
&& roomData.state != 2 // 服务端未开局
|
||
&& !IsChessGameActive; // 本地对局未开始
|
||
btn_TiRen.gameObject.SetActive(show);
|
||
}
|
||
|
||
#endregion
|
||
|
||
/// <summary>服务端结算通知(EndGameSuccess/GameEnd)→ 本地对局同步置结束,防止后台方回前台后对局还在跑</summary>
|
||
public void MarkChessGameOverByServer()
|
||
{
|
||
if (chessGameManager != null && !chessGameManager.isGameOver)
|
||
chessGameManager.ForceGameOver(chessGameManager.myCamp, silent: true);
|
||
}
|
||
|
||
/// <summary>房间心跳:更新房间活跃时间 + 触发断线超时检查(协议 2.11 / 2.12,每 30s)</summary>
|
||
private IEnumerator HeartbeatRoutine()
|
||
{
|
||
var wait = new WaitForSeconds(30f);
|
||
while (true)
|
||
{
|
||
yield return wait;
|
||
if (roomData != null && !string.IsNullOrEmpty(roomData.roomId))
|
||
{
|
||
WebSocketSendList.HeartbeatChineseChess(roomData.roomId);
|
||
WebSocketSendList.CheckTimeoutChineseChess(roomData.roomId);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void StartHeartbeat()
|
||
{
|
||
StopHeartbeat();
|
||
if (isActiveAndEnabled) heartbeatCoroutine = StartCoroutine(HeartbeatRoutine());
|
||
}
|
||
|
||
private void StopHeartbeat()
|
||
{
|
||
if (heartbeatCoroutine != null)
|
||
{
|
||
StopCoroutine(heartbeatCoroutine);
|
||
heartbeatCoroutine = null;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
private void SendChessNetData(ChessNetData data)
|
||
{
|
||
if (roomData == null || chessGameManager == null || data == null) return;
|
||
data.userId = GameManager.Instance.user.BaseInfo.number;
|
||
WebSocketSendList.SyncGameDataChineseChess(roomData.roomId, JsonConvert.SerializeObject(data));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 收到 SyncGameData 推送(由 WebSocketManager_Message 的 SyncGameData 分支调用)
|
||
/// 兼容 arguments[0] = {"data":"..."} 与 arguments = [gameId, data] 两种回包格式
|
||
/// </summary>
|
||
public void OnChessNetData(string rawMsg)
|
||
{
|
||
if (chessGameManager == null) return;
|
||
try
|
||
{
|
||
var resp = JsonConvert.DeserializeObject<WebSocketResponseBase<Newtonsoft.Json.Linq.JToken>>(rawMsg);
|
||
if (resp == null || resp.arguments == null) return;
|
||
foreach (var arg in resp.arguments)
|
||
{
|
||
string dataStr = null;
|
||
if (arg.Type == Newtonsoft.Json.Linq.JTokenType.String
|
||
&& arg.ToString().TrimStart().StartsWith("{"))
|
||
{
|
||
dataStr = arg.ToString();
|
||
}
|
||
else if (arg.Type == Newtonsoft.Json.Linq.JTokenType.Object && arg["data"] != null)
|
||
{
|
||
dataStr = arg["data"].ToString();
|
||
}
|
||
if (!string.IsNullOrEmpty(dataStr)) HandleChessNetData(dataStr);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Debug.LogWarning("[ChineseChess] SyncGameData 解析失败: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>处理对方发来的走棋帧(悔棋/认输/求和走 GameOperation 通道,不经过这里)</summary>
|
||
private void HandleChessNetData(string dataStr)
|
||
{
|
||
ChessNetData net;
|
||
try { net = JsonConvert.DeserializeObject<ChessNetData>(dataStr); }
|
||
catch { return; }
|
||
if (net == null || string.IsNullOrEmpty(net.userId)) return;
|
||
|
||
// 4.4 只处理当前对手的走子帧(自己的回声、旧房间、陌生 userId 一并忽略)
|
||
if (net.userId != OpponentChessUserId) return;
|
||
|
||
if (net.msgType == 1) // 对方走棋
|
||
{
|
||
chessGameManager.ApplyRemoteMove(net.move);
|
||
}
|
||
}
|
||
|
||
/// <summary>清理对局(面板隐藏时调用)</summary>
|
||
private void ClearChessGame()
|
||
{
|
||
if (chessGameManager != null)
|
||
{
|
||
chessGameManager.OnMoveApplied -= OnChessMoveApplied;
|
||
chessGameManager.OnGameOver -= OnChessGameOver;
|
||
chessGameManager.OnTurnChanged -= OnChessTurnChanged;
|
||
chessGameManager = null;
|
||
}
|
||
StopWaitCountdown(); // 停走棋倒计时
|
||
StopAfkMonitor(); // 停挂起监控
|
||
StopHeartbeat(); // 停房间心跳
|
||
if (disconnectCountdownCoroutine != null) // 停对方断线倒计时
|
||
{
|
||
StopCoroutine(disconnectCountdownCoroutine);
|
||
disconnectCountdownCoroutine = null;
|
||
}
|
||
isMyAfk = false;
|
||
isOpponentAfk = false;
|
||
if (chessBoard != null) chessBoard.Clear();
|
||
if (autoChessBoardRect != null)
|
||
{
|
||
Destroy(autoChessBoardRect.gameObject);
|
||
autoChessBoardRect = null;
|
||
chessBoard = null;
|
||
}
|
||
}
|
||
|
||
|
||
public void Start()
|
||
{
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// 开始等待倒计时
|
||
/// </summary>
|
||
/// <param name="seconds">总秒数</param>
|
||
/// <param name="type">倒计时类型</param>
|
||
/// <param name="waitMsg">等待时显示的文字,如"等待玩家进入"</param>
|
||
public void StartWaitCountdown(int seconds, CountdownType type = CountdownType.WaitPlayer, string waitMsg = null)
|
||
{
|
||
if (waitCountdown != null) StopCoroutine(waitCountdown);
|
||
waitCountdown = StartCoroutine(WaitCountdownRoutine(seconds, type, waitMsg));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止等待倒计时
|
||
/// </summary>
|
||
public void StopWaitCountdown()
|
||
{
|
||
if (waitCountdown != null)
|
||
{
|
||
StopCoroutine(waitCountdown);
|
||
waitCountdown = null;
|
||
}
|
||
if (tmp_WaitTime_Bottom != null) tmp_WaitTime_Bottom.text = "";
|
||
if (tmp_WaitTime_Top != null) tmp_WaitTime_Top.text = "";
|
||
}
|
||
|
||
private IEnumerator WaitCountdownRoutine(int totalSeconds, CountdownType type, string waitMsg)
|
||
{
|
||
int remaining = totalSeconds;
|
||
while (remaining >= 0)
|
||
{
|
||
string timeStr = $"{remaining / 60:D2}:{remaining % 60:D2}";
|
||
switch (type)
|
||
{
|
||
default:
|
||
string displayText = string.IsNullOrEmpty(waitMsg) ? timeStr : $"{waitMsg}{timeStr}";
|
||
if (tmp_WaitTime_Bottom != null)
|
||
tmp_WaitTime_Bottom.text = displayText;
|
||
if (tmp_WaitTime_Top != null)
|
||
tmp_WaitTime_Top.text = displayText;
|
||
break;
|
||
}
|
||
|
||
yield return new WaitForSeconds(1f);
|
||
remaining--;
|
||
}
|
||
switch (type)
|
||
{
|
||
default:
|
||
// 超时:返回选房间界面
|
||
subUIManager.CloseAll();
|
||
WebSocketSendList.GetRoomListChineseChess(1);
|
||
break;
|
||
}
|
||
}
|
||
|
||
protected override void OnHide()
|
||
{
|
||
base.OnHide();
|
||
// 4.4 任何关闭路径都确保已退房(连接尚未关闭,能发出去;已退过则 roomData 为空跳过)
|
||
if (roomData != null)
|
||
{
|
||
WebSocketSendList.ExitRoomChineseChess(roomData.roomId, MyChessUserId);
|
||
ClearRoomLocalState();
|
||
}
|
||
// 退订用户数据变化
|
||
GameManager.Instance.OnUserDataChanged -= OnUserDataChangedHandler;
|
||
// 清理象棋对局
|
||
ClearChessGame();
|
||
// 退出:播放 Close 动画后禁用 Spine
|
||
PlaySpineAnim(animName_Close, false);
|
||
ActiveSpine(false);
|
||
WebSocketManager.Instance.Close(SocketType.chess);
|
||
Screen.orientation = ScreenOrientation.LandscapeLeft;
|
||
}
|
||
}
|