using DG.Tweening;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
///
/// 棋盘控件(挂在 ChineseChessMainPanel 下)
///
/// 定位方案(配合自备棋盘底图):
/// 1. 自备棋盘底图:作为本物体的子物体摆好(拖到 img_Board,可选)
/// 2. 建两个空物体作定位点,精确摆到底图交叉点上:
/// point_Start:楚河汉界【下方】的左上角交叉点(红方河沿最左侧,逻辑 (0,4))
/// point_End :楚河汉界【上方】的左下角交叉点(与起始点同列、隔河相邻,逻辑 (0,5))
/// 3. 两点距离 = 网格间距;代码从起始点出发生成全部 90 个定位点
/// 4. 黑方视角:逻辑坐标镜像映射(我方棋子永远在下方),底图不用转
///
/// 棋子:
/// - prefab_Piece:只放 Image + Button 的预制体(不加任何脚本,脚本由代码动态添加)
/// - sprite_RedPieces / sprite_BlackPieces:拖展示图,顺序:将/帅、士/仕、象/相、马、车、炮、兵/卒
/// - 都不配则代码生成默认棋子(米色圆 + 汉字)
///
/// 调试:show_DebugPoints = true 生成 90 个半透明小点,用于校准定位点与底图对齐
///
public class UIChessBoard : MonoBehaviour
{
[Header("棋盘定位(必填)")]
public RectTransform point_Start; // 起始定位点:楚河汉界下方左上角交叉点(红方河沿最左)
public RectTransform point_End; // 间距定位点:楚河汉界上方左下角交叉点(与起始点同列隔河)
public Image img_Board; // 棋盘底图(自摆拖入;不填且 draw_BoardLines 开启时代码画线)
public bool draw_BoardLines = true; // 无底图时代码绘制棋盘线(有底图自动忽略)
[Header("棋子/格子预制体(只放 Image + Button,脚本由代码动态添加)")]
public GameObject prefab_Piece;
public GameObject prefab_Cell; // 可选:格子预制体
[Header("棋子展示图(顺序:将/帅 士/仕 象/相 马 车 炮 兵/卒)")]
public Sprite[] sprite_RedPieces;
public Sprite[] sprite_BlackPieces;
[Range(0.5f, 1.5f)] public float pieceScale = 0.92f; // 棋子直径 = 网格间距 × pieceScale
[Header("上一步历史标记图(仅显示上一位玩家的起点/终点,不填用半透明方块)")]
public Sprite sprite_LastFromMark; // 起点标记图
public Sprite sprite_LastToMark; // 终点标记图
[Range(0.3f, 1.5f)] public float lastMarkScale = 0.85f; // 标记大小 = 格距 × 此值
[Header("选中标记图(点击自己棋子时显示在棋子下面)")]
public Sprite sprite_HintDot; // 点击选中的棋子下面显示的图(沿用原字段名,之前拖的图自动关联)
[Range(0.3f, 1.5f)] public float selectMarkScale = 1.0f; // 标记大小 = 格距 × 此值
[Header("调试")]
public bool show_DebugPoints = false; // 显示 90 个定位点小圆点(校准底图对齐用)
[Header("无展示图时的默认样式")]
public Sprite sprite_PieceBg; // 棋子底图(默认:代码生成的米色圆)
public TMP_FontAsset font_Piece; // 棋子文字字体(默认:TMP 默认字体)
public Color color_RedText = new Color32(0xC6, 0x28, 0x28, 0xFF);
public Color color_BlackText = new Color32(0x21, 0x21, 0x21, 0xFF);
[Header("运行时自动生成(不用拖)")]
public RectTransform tr_Lines; // 棋盘线层(仅无底图时用)
public RectTransform tr_Cells; // 格子层(90 个定位点)
public RectTransform tr_UnderMarks; // 选中标记层(在棋子下面)
public RectTransform tr_Pieces; // 棋子层
public RectTransform tr_Marks; // 提示层(上一步标记/提示文字)
/// 解析后的字体(font_Piece 或 TMP 默认字体)
public TMP_FontAsset ResolvedFont { get; private set; }
// 格子缓存:按逻辑坐标存 [y,x]
private UIChessCell[,] cells = new UIChessCell[ChessConst.BoardHeight, ChessConst.BoardWidth];
// 棋子缓存:pieceId → 控件
private readonly Dictionary pieceMap = new Dictionary();
private ChessGameManager gameManager;
private ChessCamp myCamp;
private bool flipped; // 我方为黑方时,逻辑坐标镜像映射(我方永远在下方)
private UIChessPiece selectedPiece;
private readonly List selectedMoves = new List();
private RectTransform lastFromMark;
private RectTransform lastToMark;
private Image img_SelectMark; // 选中标记(点击棋子时显示在棋子下面)
private TextMeshProUGUI tmp_Toast;
private Tween toastTween;
// ---- 定位计算结果 ----
private bool gridValid; // 定位点是否有效
private Vector2 gridStart; // 起始点在格层本地坐标(逻辑 (0,4))
private Vector2 gridUp; // 逻辑 y+ 方向(指向黑方)
private Vector2 gridRight; // 逻辑 x+ 方向(红方视角从左到右)
private float cellStep; // 网格间距(两定位点距离)
private float gridAngle; // 网格旋转角(底图正摆时为 0)
private float pieceSize; // 棋子直径
private bool interactable = true;
///
/// 初始化棋盘(每次开一局调用一次)
///
public void Init(ChessGameManager manager, ChessCamp camp)
{
Clear();
gameManager = manager;
myCamp = camp;
flipped = camp == ChessCamp.Black;
ResolvedFont = font_Piece != null ? font_Piece : TMP_Settings.defaultFontAsset;
if (ResolvedFont == null)
Debug.LogWarning("[UIChessBoard] 未找到可用 TMP 字体,棋子文字可能无法显示,请在 Inspector 给 font_Piece 拖入中文字体");
BuildContainers();
CalcGridFromPoints(); // ★ 两定位点 → 间距 + 方向
BuildCells(); // ★ 90 个棋盘定位点
if (img_Board == null && draw_BoardLines) BuildBoardLines(); // 无底图时才代码画线
BuildPieces(); // ★ 全部棋子(预制体优先,脚本动态添加)
if (show_DebugPoints) BuildDebugPoints();
BuildToast();
// 订阅数据层事件 → 表现层
gameManager.OnMoveApplied += OnChessMoveApplied;
gameManager.OnUndoApplied += OnChessUndoApplied;
gameManager.OnGameOver += OnChessGameOver;
gameManager.OnTurnChanged += OnTurnChanged;
SetInteractable(true);
if (gridValid) ShowToast(myCamp == ChessCamp.Red ? "红方先行" : "对方先行");
}
/// 清空棋盘(对局结束/面板隐藏时调用,Init 内部也会先调一次保证可重入)
public void Clear()
{
if (gameManager != null)
{
gameManager.OnMoveApplied -= OnChessMoveApplied;
gameManager.OnUndoApplied -= OnChessUndoApplied;
gameManager.OnGameOver -= OnChessGameOver;
gameManager.OnTurnChanged -= OnTurnChanged;
gameManager = null;
}
ClearSelection();
selectedPiece = null;
selectedMoves.Clear();
HideLastMoveMark();
if (tr_Lines != null) { Destroy(tr_Lines.gameObject); tr_Lines = null; }
if (tr_Cells != null) { Destroy(tr_Cells.gameObject); tr_Cells = null; }
if (tr_UnderMarks != null) { Destroy(tr_UnderMarks.gameObject); tr_UnderMarks = null; }
if (tr_Pieces != null) { Destroy(tr_Pieces.gameObject); tr_Pieces = null; }
if (tr_Marks != null) { Destroy(tr_Marks.gameObject); tr_Marks = null; }
lastFromMark = null;
lastToMark = null;
img_SelectMark = null;
tmp_Toast = null;
toastTween?.Kill();
toastTween = null;
pieceMap.Clear();
cells = new UIChessCell[ChessConst.BoardHeight, ChessConst.BoardWidth];
gridValid = false;
cellStep = 0f;
interactable = true;
}
// ================= 资源取用(供棋子控件访问) =================
/// 取某方某类型棋子的展示图(未配置返回 null)
public Sprite GetTypeSprite(ChessCamp camp, ChessPieceType type)
{
var arr = camp == ChessCamp.Red ? sprite_RedPieces : sprite_BlackPieces;
int idx = (int)type - 1; // ChessPieceType:Jiang=1 Shi=2 Xiang=3 Ma=4 Ju=5 Pao=6 Bing=7
if (arr == null || idx < 0 || idx >= arr.Length) return null;
return arr[idx];
}
/// 取某方棋子文字颜色
public Color GetTextColor(ChessCamp camp)
{
return camp == ChessCamp.Red ? color_RedText : color_BlackText;
}
// ================= 定位计算(核心) =================
///
/// 由两个定位点计算网格:间距 = 两点距离,方向 = 起点→终点 为 y+(指向黑方)
///
private void CalcGridFromPoints()
{
if (point_Start == null || point_End == null)
{
Debug.LogError("[UIChessBoard] 未配置定位点 point_Start / point_End!\n" +
"摆法:楚河汉界下方左上角交叉点 = point_Start;上方左下角交叉点 = point_End(两点同列隔河)");
return;
}
Vector2 s = ToBoardLocal(point_Start);
Vector2 e = ToBoardLocal(point_End);
Vector2 delta = e - s;
cellStep = delta.magnitude;
if (cellStep < 1f)
{
Debug.LogError("[UIChessBoard] 两个定位点重合或距离过近(间距=" + cellStep + "),请检查摆放");
return;
}
gridStart = s;
gridUp = delta.normalized; // 逻辑 y+ 方向(起点→终点,指向黑方)
gridRight = new Vector2(gridUp.y, -gridUp.x); // 逻辑 x+ 方向(红方视角从左到右)
gridAngle = Mathf.Atan2(gridUp.y, gridUp.x) * Mathf.Rad2Deg - 90f;
pieceSize = cellStep * pieceScale;
gridValid = true;
Debug.Log($"[UIChessBoard] 定位成功:网格间距={cellStep:F2},起点=({s.x:F1}, {s.y:F1})," +
$"将生成 90 个定位点 + 32 个棋子(棋子直径={pieceSize:F1})");
}
/// 任意 Transform → 格层本地坐标(与格子/棋子的 anchoredPosition 同一坐标系)
private Vector2 ToBoardLocal(Transform t)
{
return (Vector2)tr_Cells.InverseTransformPoint(t.position);
}
/// 物理网格坐标:从起始点(逻辑 0,4)出发按间距推算
private Vector2 PhysicalPos(int x, int y)
{
return gridStart + gridRight * (x * cellStep) + gridUp * ((y - 4) * cellStep);
}
/// 逻辑坐标 → 棋盘位置(黑方视角镜像:我方永远在下方)
private Vector2 GetPos(int x, int y)
{
if (flipped)
{
x = ChessConst.BoardWidth - 1 - x;
y = ChessConst.BoardHeight - 1 - y;
}
return PhysicalPos(x, y);
}
private Vector2 GetPos(Vector2Int p)
{
return GetPos(p.x, p.y);
}
// ================= 父子层级 =================
/// 生成容器层:线 → 格子 → 选中标记 → 棋子 → 提示
private void BuildContainers()
{
tr_Lines = CreateLayer("Lines");
tr_Cells = CreateLayer("Cells");
tr_UnderMarks = CreateLayer("UnderMarks");
tr_Pieces = CreateLayer("Pieces");
tr_Marks = CreateLayer("Marks");
}
private RectTransform CreateLayer(string name)
{
var go = new GameObject(name, typeof(RectTransform));
var rt = go.GetComponent();
rt.SetParent(transform, false);
rt.anchorMin = Vector2.zero;
rt.anchorMax = Vector2.one;
rt.offsetMin = Vector2.zero;
rt.offsetMax = Vector2.zero;
rt.pivot = new Vector2(0.5f, 0.5f);
return rt;
}
// ================= 90 个棋盘定位点 =================
/// 按定位点生成全部格子(透明点击区 + 走子提示),支持拖格子预制体
private void BuildCells()
{
if (!gridValid) return;
for (int y = 0; y < ChessConst.BoardHeight; y++)
{
for (int x = 0; x < ChessConst.BoardWidth; x++)
{
GameObject go;
if (prefab_Cell != null)
{
go = Instantiate(prefab_Cell, tr_Cells);
if (go.GetComponent() == null)
{
Debug.LogError("[UIChessBoard] 格子预制体根节点必须是 UI 物体(带 RectTransform)");
Destroy(go);
continue;
}
}
else
{
go = new GameObject("Cell", typeof(RectTransform), typeof(Image), typeof(Button));
go.GetComponent().color = new Color(1f, 1f, 1f, 0f); // 透明热区
}
var crt = (RectTransform)go.transform;
if (crt.parent != tr_Cells) crt.SetParent(tr_Cells, false);
crt.anchoredPosition = GetPos(x, y);
crt.sizeDelta = new Vector2(cellStep, cellStep);
crt.localEulerAngles = new Vector3(0f, 0f, gridAngle);
var cell = go.GetComponent();
if (cell == null) cell = go.AddComponent();
cell.Init(this, new Vector2Int(x, y), cellStep);
cells[y, x] = cell;
}
}
}
/// 显示 90 个定位点小圆点(校准底图对齐用)
private void BuildDebugPoints()
{
Sprite dot = ChessArtFactory.GetDotSprite();
for (int y = 0; y < ChessConst.BoardHeight; y++)
{
for (int x = 0; x < ChessConst.BoardWidth; x++)
{
var go = new GameObject("DebugPoint_" + x + "_" + y, typeof(RectTransform), typeof(Image));
var rt = (RectTransform)go.transform;
rt.SetParent(tr_Marks, false);
rt.anchoredPosition = PhysicalPos(x, y);
rt.sizeDelta = new Vector2(cellStep * 0.16f, cellStep * 0.16f);
var img = go.GetComponent();
img.sprite = dot;
img.color = new Color(0f, 0.75f, 1f, 0.55f);
img.raycastTarget = false;
}
}
Debug.Log("[UIChessBoard] 已显示 90 个定位点(蓝色小点),确认与底图对齐后关闭 show_DebugPoints");
}
// ================= 无底图时的代码绘线(可忽略) =================
/// 自备底图时不调用;无底图时代码绘制棋盘线(横线/竖线/河界断开/九宫/楚河汉界)
private void BuildBoardLines()
{
if (!gridValid) return;
Color lineColor = new Color32(0x5B, 0x3A, 0x1E, 0xFF);
float thickness = Mathf.Max(2f, cellStep * 0.015f);
// 棋盘底色
var brt = (RectTransform)transform;
var bgGo = new GameObject("BoardBg", typeof(RectTransform), typeof(Image));
var bgRt = (RectTransform)bgGo.transform;
bgRt.SetParent(tr_Lines, false);
bgRt.anchorMin = bgRt.anchorMax = new Vector2(0.5f, 0.5f);
bgRt.anchoredPosition = Vector2.zero;
bgRt.sizeDelta = new Vector2(brt.rect.width, brt.rect.height);
var bgImg = bgGo.GetComponent();
bgImg.color = new Color32(0xE9, 0xC5, 0x8C, 0xFF);
bgImg.raycastTarget = false;
// 10 条横线
for (int y = 0; y < ChessConst.BoardHeight; y++)
DrawLine(PhysicalPos(0, y), PhysicalPos(8, y), thickness, lineColor);
// 9 条竖线(中间 7 列在河界处断开)
for (int x = 0; x < ChessConst.BoardWidth; x++)
{
if (x == 0 || x == 8)
{
DrawLine(PhysicalPos(x, 9), PhysicalPos(x, 0), thickness, lineColor);
}
else
{
DrawLine(PhysicalPos(x, 9), PhysicalPos(x, 5), thickness, lineColor);
DrawLine(PhysicalPos(x, 4), PhysicalPos(x, 0), thickness, lineColor);
}
}
// 九宫斜线
DrawLine(PhysicalPos(3, 0), PhysicalPos(5, 2), thickness, lineColor);
DrawLine(PhysicalPos(5, 0), PhysicalPos(3, 2), thickness, lineColor);
DrawLine(PhysicalPos(3, 7), PhysicalPos(5, 9), thickness, lineColor);
DrawLine(PhysicalPos(5, 7), PhysicalPos(3, 9), thickness, lineColor);
// 楚河 汉界
float riverY = (PhysicalPos(0, 4).y + PhysicalPos(0, 5).y) * 0.5f;
CreateBoardText("楚河", new Vector2(PhysicalPos(2, 0).x, riverY), cellStep * 0.55f, lineColor);
CreateBoardText("汉界", new Vector2(PhysicalPos(6, 0).x, riverY), cellStep * 0.55f, lineColor);
}
private void DrawLine(Vector2 a, Vector2 b, float thickness, Color color)
{
Vector2 dir = b - a;
float len = dir.magnitude;
if (len < 0.001f) return;
float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
var go = new GameObject("Line", typeof(RectTransform), typeof(Image));
var rt = (RectTransform)go.transform;
rt.SetParent(tr_Lines, false);
rt.localEulerAngles = new Vector3(0, 0, angle);
rt.sizeDelta = new Vector2(len + thickness, thickness);
rt.anchoredPosition = (a + b) * 0.5f;
var img = go.GetComponent();
img.color = color;
img.raycastTarget = false;
}
private void CreateBoardText(string text, Vector2 pos, float fontSize, Color color)
{
if (ResolvedFont == null) return;
var go = new GameObject("Text_" + text, typeof(RectTransform));
var rt = (RectTransform)go.transform;
rt.SetParent(tr_Lines, false);
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.anchoredPosition = pos;
rt.sizeDelta = new Vector2(cellStep * 3f, fontSize * 1.8f);
var t = go.AddComponent();
t.font = ResolvedFont;
t.text = text;
t.fontSize = fontSize;
t.alignment = TextAlignmentOptions.Center;
t.color = color;
t.raycastTarget = false;
}
// ================= 棋子生成 =================
///
/// 按棋盘数据生成全部存活棋子:
/// 有 prefab_Piece(只带 Image + Button)→ 实例化后动态加 UIChessPiece;
/// 没有 → 代码生成默认棋子(米色圆 + 汉字)
///
private void BuildPieces()
{
ClearPieces();
if (!gridValid) return;
foreach (var pd in gameManager.board.allPieces)
{
if (!pd.isAlive) continue;
GameObject go;
if (prefab_Piece != null)
{
go = Instantiate(prefab_Piece, tr_Pieces);
if (go.GetComponent() == null)
{
Debug.LogError("[UIChessBoard] 棋子预制体根节点必须是 UI 物体(带 RectTransform)");
Destroy(go);
continue;
}
}
else
{
go = new GameObject("Piece", typeof(RectTransform), typeof(Image), typeof(Button));
}
var prt = (RectTransform)go.transform;
if (prt.parent != tr_Pieces) prt.SetParent(tr_Pieces, false);
prt.sizeDelta = new Vector2(pieceSize, pieceSize);
prt.anchoredPosition = GetPos(pd.pos);
prt.localEulerAngles = new Vector3(0f, 0f, gridAngle);
// 预制体上没有任何脚本,这里动态添加
var piece = go.GetComponent();
if (piece == null) piece = go.AddComponent();
if (prefab_Piece == null) SetupDefaultPieceVisual(piece); // 代码生成时补默认子物体
piece.Init(this, pd);
pieceMap[pd.id] = piece;
}
}
/// 代码生成棋子时的默认子物体(选中圈 + 汉字)
private void SetupDefaultPieceVisual(UIChessPiece piece)
{
var go = piece.gameObject;
// 选中圈
var sgo = new GameObject("SelectRing", typeof(RectTransform), typeof(Image));
var srt = (RectTransform)sgo.transform;
srt.SetParent(go.transform, false);
srt.anchorMin = srt.anchorMax = new Vector2(0.5f, 0.5f);
srt.anchoredPosition = Vector2.zero;
srt.sizeDelta = new Vector2(pieceSize * 1.30f, pieceSize * 1.30f);
var simg = sgo.GetComponent();
simg.sprite = ChessArtFactory.GetRingSprite();
simg.color = new Color(1f, 0.84f, 0.31f, 0.95f);
simg.raycastTarget = false;
sgo.SetActive(false);
piece.img_Select = simg;
// 汉字
if (ResolvedFont != null)
{
var tgo = new GameObject("Label", typeof(RectTransform));
var trt = (RectTransform)tgo.transform;
trt.SetParent(go.transform, false);
trt.anchorMin = Vector2.zero;
trt.anchorMax = Vector2.one;
trt.offsetMin = Vector2.zero;
trt.offsetMax = Vector2.zero;
var t = tgo.AddComponent();
t.font = ResolvedFont;
t.alignment = TextAlignmentOptions.Center;
t.enableAutoSizing = true;
t.fontSizeMin = 8f;
t.fontSizeMax = Mathf.Max(10f, pieceSize * 0.55f);
piece.tmp_Label = t;
}
}
private void ClearPieces()
{
foreach (var kv in pieceMap)
if (kv.Value != null) Destroy(kv.Value.gameObject);
pieceMap.Clear();
}
/// 悔棋后全量刷新棋子(按数据层状态重建,最可靠)
public void RefreshAllPieces()
{
ClearSelection();
selectedPiece = null;
BuildPieces();
HideLastMoveMark();
}
private void BuildToast()
{
var go = new GameObject("Toast", typeof(RectTransform));
var rt = (RectTransform)go.transform;
rt.SetParent(tr_Marks, false);
rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f);
rt.anchoredPosition = new Vector2(0f, cellStep * 2.2f);
rt.sizeDelta = new Vector2(cellStep * 9f, cellStep * 1.2f);
tmp_Toast = go.AddComponent();
if (ResolvedFont != null) tmp_Toast.font = ResolvedFont;
tmp_Toast.alignment = TextAlignmentOptions.Center;
tmp_Toast.fontSize = cellStep * 0.50f;
tmp_Toast.color = new Color(0.80f, 0.16f, 0.12f, 1f);
tmp_Toast.raycastTarget = false;
go.SetActive(false);
}
// ================= 点击交互 =================
public void OnPieceClicked(UIChessPiece piece)
{
if (!interactable || gameManager == null || gameManager.isGameOver) return;
if (piece == null || piece.data == null) return;
if (piece.data.camp == myCamp)
{
// 点自己的子:选中并显示走点
if (!gameManager.IsMyTurn)
{
ShowToast("还没轮到你走棋");
return;
}
if (selectedPiece == piece)
{
ClearSelection();
return;
}
SelectPiece(piece);
}
else
{
// 点对方的子:若它在当前选中子的合法落点里 → 吃子
if (selectedPiece != null && selectedMoves.Contains(piece.data.pos))
{
gameManager.TryLocalMove(selectedPiece.data.pos, piece.data.pos);
}
ClearSelection();
}
}
public void OnCellClicked(UIChessCell cell)
{
if (!interactable || gameManager == null || gameManager.isGameOver) return;
if (selectedPiece == null || cell == null) return;
if (selectedMoves.Contains(cell.gridPos))
{
gameManager.TryLocalMove(selectedPiece.data.pos, cell.gridPos);
}
ClearSelection();
}
private void SelectPiece(UIChessPiece piece)
{
ClearSelection();
selectedPiece = piece;
piece.SetSelected(true);
ShowSelectMark(piece); // 点击选中的棋子下面显示标记图
selectedMoves.AddRange(gameManager.rules.GetLegalMoves(gameManager.board, piece.data));
for (int i = 0; i < selectedMoves.Count; i++)
{
var target = gameManager.board.GetPiece(selectedMoves[i]);
cells[selectedMoves[i].y, selectedMoves[i].x].ShowHint(target != null);
}
}
private void ClearSelection()
{
if (selectedPiece != null) selectedPiece.SetSelected(false);
selectedPiece = null;
selectedMoves.Clear();
HideSelectMark();
for (int y = 0; y < ChessConst.BoardHeight; y++)
for (int x = 0; x < ChessConst.BoardWidth; x++)
if (cells[y, x] != null) cells[y, x].HideHint();
}
/// 选中标记:显示在点击的棋子下面(拖了 sprite_HintDot 才显示)
private void ShowSelectMark(UIChessPiece piece)
{
if (sprite_HintDot == null || piece == null || piece.data == null || !gridValid) return;
if (img_SelectMark == null)
{
var go = new GameObject("SelectMark", typeof(RectTransform), typeof(Image));
var rt = (RectTransform)go.transform;
rt.SetParent(tr_UnderMarks, false);
img_SelectMark = go.GetComponent();
img_SelectMark.raycastTarget = false;
}
img_SelectMark.sprite = sprite_HintDot;
var mrt = (RectTransform)img_SelectMark.transform;
mrt.anchoredPosition = GetPos(piece.data.pos);
float s = cellStep * selectMarkScale;
mrt.sizeDelta = new Vector2(s, s);
mrt.localEulerAngles = new Vector3(0f, 0f, gridAngle);
img_SelectMark.gameObject.SetActive(true);
}
private void HideSelectMark()
{
if (img_SelectMark != null) img_SelectMark.gameObject.SetActive(false);
}
// ================= 数据层事件 → 表现层 =================
/// 走子成功(本地或网络):动画 + 上一步标记 + 将军提示
private void OnChessMoveApplied(ChessMoveEventArgs e)
{
UIChessPiece pieceUI;
if (pieceMap.TryGetValue(e.movePieceId, out pieceUI) && pieceUI != null)
{
var prt = (RectTransform)pieceUI.transform;
prt.DOKill();
prt.DOAnchorPos(GetPos(e.move.toX, e.move.toY), 0.35f).SetEase(Ease.OutQuad);
}
UIChessPiece capUI;
if (e.capturedPieceId >= 0 && pieceMap.TryGetValue(e.capturedPieceId, out capUI) && capUI != null)
{
var crt = (RectTransform)capUI.transform;
crt.DOKill();
crt.DOScale(0f, 0.30f).SetDelay(0.18f).SetEase(Ease.InBack)
.OnComplete(() => { if (capUI != null) capUI.gameObject.SetActive(false); });
}
ShowLastMoveMark(e.move);
if (e.isCheck) ShowToast("将军!");
}
/// 悔棋完成:全量刷新
private void OnChessUndoApplied(int undoCount)
{
RefreshAllPieces();
}
/// 对局结束:锁定交互
private void OnChessGameOver(ChessCamp winner)
{
SetInteractable(false);
ShowToast(winner == myCamp ? "你赢了!" : "你输了");
}
/// 回合切换
private void OnTurnChanged(ChessCamp camp)
{
if (camp == myCamp && gameManager != null && gameManager.moveCount > 0)
ShowToast("轮到你走棋");
}
// ================= 对外接口 =================
/// 显示居中提示文字(将军/轮到你/悔棋等)
public void ShowToast(string msg)
{
if (tmp_Toast == null) return;
tmp_Toast.text = msg;
tmp_Toast.gameObject.SetActive(true);
var rt = tmp_Toast.rectTransform;
toastTween?.Kill();
rt.localScale = Vector3.one * 0.6f;
toastTween = rt.DOScale(1f, 0.18f).SetEase(Ease.OutBack).OnComplete(() =>
{
rt.DOScale(0.6f, 0.35f).SetDelay(1.0f).SetEase(Ease.InBack).OnComplete(() =>
{
if (tmp_Toast != null) tmp_Toast.gameObject.SetActive(false);
});
});
}
/// 锁定/解锁棋盘交互(对局结束时锁定)
public void SetInteractable(bool value)
{
interactable = value;
foreach (var kv in pieceMap)
if (kv.Value != null) kv.Value.SetInteractable(value);
if (!value) ClearSelection();
}
// ================= 内部工具 =================
/// 上一步标记(起点/终点,可用自己的图)
private void ShowLastMoveMark(ChessMoveData move)
{
if (lastFromMark == null || lastToMark == null)
{
lastFromMark = CreateMark("LastFrom", sprite_LastFromMark, new Color(1f, 0.55f, 0f, 0.30f));
lastToMark = CreateMark("LastTo", sprite_LastToMark, new Color(1f, 0.55f, 0f, 0.45f));
}
PlaceMark(lastFromMark, new Vector2Int(move.fromX, move.fromY));
PlaceMark(lastToMark, new Vector2Int(move.toX, move.toY));
}
private void HideLastMoveMark()
{
if (lastFromMark != null) lastFromMark.gameObject.SetActive(false);
if (lastToMark != null) lastToMark.gameObject.SetActive(false);
}
private RectTransform CreateMark(string name, Sprite sprite, Color fallbackColor)
{
var go = new GameObject(name, typeof(RectTransform), typeof(Image));
var rt = (RectTransform)go.transform;
rt.SetParent(tr_Marks, false);
rt.sizeDelta = new Vector2(cellStep * lastMarkScale, cellStep * lastMarkScale);
var img = go.GetComponent();
img.sprite = sprite; // 拖了图用图,没拖保持代码生成的透明方块
img.color = sprite != null ? Color.white : fallbackColor; // 用图时不染色
img.raycastTarget = false;
return rt;
}
private void PlaceMark(RectTransform mark, Vector2Int pos)
{
mark.anchoredPosition = GetPos(pos);
mark.gameObject.SetActive(true);
}
}