739 lines
27 KiB
C#
739 lines
27 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 Image img_DengDaiZhong;
|
||
public ChessGameRoom roomData;
|
||
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());
|
||
ChessGameRoom chessGameRoom = va.arguments[0];
|
||
roomData = chessGameRoom; // 记录房间数据(开象棋对局时要取阵营)
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
public override void Awake()
|
||
{
|
||
base.Awake();
|
||
btn_FanHuiDaTing.onClick.AddListener(() =>
|
||
{
|
||
UIManager.Instance.Close(UIPanel.MainGamePanelZGXQ);
|
||
});
|
||
btn_GuiZe.onClick.AddListener(() =>
|
||
{
|
||
subUIManager.ShowAsync(UIPanel.ZGXQIntroduce);
|
||
});
|
||
}
|
||
protected override void OnShow(object args = null)
|
||
{
|
||
base.OnShow(args);
|
||
// 回到选房间界面时显示等待中(连带父对象)
|
||
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)
|
||
{
|
||
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;
|
||
|
||
// 我的阵营: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);
|
||
|
||
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
|
||
|
||
/// <summary>发送象棋帧同步数据(SyncGameData 通道)</summary>
|
||
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;
|
||
|
||
// 服务器回声/自己发的帧直接忽略
|
||
if (net.userId == GameManager.Instance.user.BaseInfo.number) 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(); // 停走棋倒计时
|
||
if (chessBoard != null) chessBoard.Clear();
|
||
if (autoChessBoardRect != null)
|
||
{
|
||
Destroy(autoChessBoardRect.gameObject);
|
||
autoChessBoardRect = null;
|
||
chessBoard = null;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
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();
|
||
// 清理象棋对局
|
||
ClearChessGame();
|
||
// 退出:播放 Close 动画后禁用 Spine
|
||
PlaySpineAnim(animName_Close, false);
|
||
ActiveSpine(false);
|
||
WebSocketManager.Instance.Close(SocketType.chess);
|
||
Screen.orientation = ScreenOrientation.LandscapeLeft;
|
||
}
|
||
}
|