using System; using System.Collections.Generic; using System.IO; using Newtonsoft.Json; using AibisDream.Utility; using AibisDream.Framework; using UnityEngine; using Yarn.Unity; namespace AibisDream { public static class BlockPuzzleKit { /// /// 保存单个 BlockShapeData 到文件,确保 Key 唯一(如果 Key 已存在则更新,不存在则添加) /// /// 要保存的数据 /// 数据的键(用于字典存储) /// 保存路径,如果为空则使用默认路径 public static void SaveBlockShapeData(BlockShapeData data, string key, string path = null) { if (string.IsNullOrEmpty(path)) { path = ConstRef.BlockShapeDataPath; } // 确保目录存在 string directory = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } // 读取现有字典(如果文件存在) Dictionary dataDict; if (File.Exists(path)) { try { dataDict = JsonUtil.ReadBeanDict(path) ?? new Dictionary(); } catch (Exception e) { Debug.LogWarning($"读取现有 BlockShapeData 文件失败,将创建新文件: {e.Message}"); dataDict = new Dictionary(); } } else { dataDict = new Dictionary(); } // 更新或添加数据(确保 Key 唯一) dataDict[key] = data; // 保存整个字典 JsonUtil.SaveBean(dataDict, path); } /// /// 从文件加载单个 BlockShapeData /// /// 数据的键 /// 文件路径,如果为空则使用默认路径 /// 加载的数据 /// 是否加载成功 public static bool TryLoadBlockShapeData(string key, out BlockShapeData data, string path = null) { if (string.IsNullOrEmpty(path)) { path = ConstRef.BlockShapeDataPath; } try { if (!File.Exists(path)) { data = default; return false; } var dict = JsonUtil.ReadBeanDict(path); var infoDict = JsonUtil.ReadBeanDict(ConstRef.BlockShapeInfoPath); if (dict != null && dict.ContainsKey(key)) { data = dict[key]; data.blockShapeInfo = infoDict[key]; return true; } data = default; return false; } catch (Exception e) { Debug.LogError($"加载 BlockShapeData 失败: {e.Message}"); data = default; return false; } } /// /// 从文件加载单个 BlockPuzzleData /// /// 数据的键 /// 加载的数据 /// 是否加载成功 public static bool TryLoadBlockPuzzleData(string key, out BlockPuzzleData data) { var path = ConstRef.BlockPuzzleDataPath; // 加载配置 try { if (!File.Exists(path)) { data = default; return false; } var dict = JsonUtil.ReadBeanDict(path); if (dict != null && dict.ContainsKey(key)) { data = dict[key]; return true; } data = default; return false; } catch (Exception e) { Debug.LogError($"加载 BlockPuzzleData 失败: {e.Message}"); data = default; return false; } } /// /// 保存单个 BlockPuzzleData 到文件,确保 Key 唯一(如果 Key 已存在则更新,不存在则添加) /// /// 要保存的数据 /// 数据的键(用于字典存储) /// 保存路径,如果为空则使用默认路径 public static void SaveBlockPuzzleData(BlockPuzzleData data, string key, string path = null) { if (string.IsNullOrEmpty(path)) { path = ConstRef.BlockPuzzleDataPath; } // 确保目录存在 string directory = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } // 读取现有字典(如果文件存在) Dictionary dataDict; if (File.Exists(path)) { try { dataDict = JsonUtil.ReadBeanDict(path) ?? new Dictionary(); } catch (Exception e) { Debug.LogWarning($"读取现有 BlockPuzzleData 文件失败,将创建新文件: {e.Message}"); dataDict = new Dictionary(); } } else { dataDict = new Dictionary(); } // 更新或添加数据(确保 Key 唯一) dataDict[key] = data; // 保存整个字典 JsonUtil.SaveBean(dataDict, path); } // ---------------------- 碰撞箱计算 ---------------------- /// /// 将网格数组转换为多边形顶点数组 /// /// 二维数组,值为1的格子表示填充区域 /// 按顺序排列的多边形顶点数组,可以围成闭合多边形 public static Vector2[] GridToPolygonVertices(int[,] grid) { if (grid == null || grid.GetLength(0) == 0 || grid.GetLength(1) == 0) return new Vector2[0]; int width = grid.GetLength(0); int height = grid.GetLength(1); // 查找起始边界点 Vector2Int? startPoint = FindStartBoundaryPoint(grid, width, height); if (!startPoint.HasValue) return new Vector2[0]; // 边界追踪 List vertices = TraceBoundary(grid, width, height, startPoint.Value); if (vertices.Count == 0) return new Vector2[0]; // 顶点优化:移除共线点 vertices = OptimizeVertices(vertices); return vertices.ToArray(); } /// /// 查找起始边界点 /// 找到第一个值为1的格子,确定其边界上的起始追踪点 /// private static Vector2Int? FindStartBoundaryPoint(int[,] grid, int width, int height) { // 从左下角开始,按行扫描,找到第一个值为1的格子 for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { if (grid[i, j] == 1) { // 找到第一个填充格子,检查其下边界(最可能的外边界) // 如果下边界是外边界,返回左下角点 if (j == 0 || (j > 0 && grid[i, j - 1] == 0)) { // 返回格子(i,j)的左下角点坐标 return new Vector2Int(i, j); } // 否则检查左边界 else if (i == 0 || (i > 0 && grid[i - 1, j] == 0)) { return new Vector2Int(i, j); } } } } return null; } /// /// 边界追踪:从起始点开始,沿着边界顺时针追踪,记录所有边界顶点 /// 使用边界边收集和连接的方法 /// 边长固定为2,所有坐标使用整数 /// private static List TraceBoundary(int[,] grid, int width, int height, Vector2Int startCell) { // 收集所有边界边:每条边用起点和终点表示(整数坐标) List<(Vector2Int start, Vector2Int end)> boundaryEdges = CollectBoundaryEdges(grid, width, height); if (boundaryEdges.Count == 0) return new List(); // 构建顶点邻接表(直接使用整数坐标) Dictionary> adjacency = BuildAdjacencyList(boundaryEdges); if (adjacency.Count == 0) return new List(); // 找到起始顶点(最左下角的顶点) Vector2Int startVertex = FindStartVertex(adjacency); // 沿着邻接表追踪,形成有序顶点序列 List vertices = TraceVertices(adjacency, startVertex); return vertices; } /// /// 收集所有边界边 /// 坐标系统:格子(i,j)的中心为(i*2, j*2),边长为2 /// 格子(i,j)的四个角点为:(i*2-1, j*2-1), (i*2+1, j*2-1), (i*2+1, j*2+1), (i*2-1, j*2+1) /// private static List<(Vector2Int start, Vector2Int end)> CollectBoundaryEdges(int[,] grid, int width, int height) { List<(Vector2Int, Vector2Int)> edges = new List<(Vector2Int, Vector2Int)>(); for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { if (grid[i, j] != 1) continue; // 计算格子(i,j)的四个角点坐标(整数) int left = i * 2 - 1; int right = i * 2 + 1; int bottom = j * 2 - 1; int top = j * 2 + 1; // 检查四个方向的边界 // 下边界(从左下角到右下角) if (j == 0 || (j > 0 && grid[i, j - 1] == 0)) { edges.Add((new Vector2Int(left, bottom), new Vector2Int(right, bottom))); } // 右边界(从右下角到右上角) if (i == width - 1 || (i < width - 1 && grid[i + 1, j] == 0)) { edges.Add((new Vector2Int(right, bottom), new Vector2Int(right, top))); } // 上边界(从右上角到左上角) if (j == height - 1 || (j < height - 1 && grid[i, j + 1] == 0)) { edges.Add((new Vector2Int(right, top), new Vector2Int(left, top))); } // 左边界(从左上角到左下角) if (i == 0 || (i > 0 && grid[i - 1, j] == 0)) { edges.Add((new Vector2Int(left, top), new Vector2Int(left, bottom))); } } } return edges; } /// /// 构建顶点邻接表 /// 直接使用整数坐标,无需量化 /// private static Dictionary> BuildAdjacencyList(List<(Vector2Int start, Vector2Int end)> edges) { Dictionary> adjacency = new Dictionary>(); foreach (var edge in edges) { Vector2Int key1 = edge.start; Vector2Int key2 = edge.end; if (!adjacency.ContainsKey(key1)) adjacency[key1] = new List(); if (!adjacency.ContainsKey(key2)) adjacency[key2] = new List(); if (!adjacency[key1].Contains(key2)) adjacency[key1].Add(key2); if (!adjacency[key2].Contains(key1)) adjacency[key2].Add(key1); } return adjacency; } /// /// 找到起始顶点(最左下角的顶点) /// private static Vector2Int FindStartVertex(Dictionary> adjacency) { Vector2Int start = new Vector2Int(int.MaxValue, int.MaxValue); foreach (var key in adjacency.Keys) { if (key.y < start.y || (key.y == start.y && key.x < start.x)) { start = key; } } return start; } /// /// 沿着邻接表追踪顶点,形成有序序列(顺时针) /// 直接使用整数坐标,最后转换为Vector2返回 /// private static List TraceVertices(Dictionary> adjacency, Vector2Int start) { List vertices = new List(); Vector2Int current = start; Vector2Int? previous = null; do { // 将整数坐标转换为Vector2 Vector2 currentPos = new Vector2(current.x, current.y); // 避免重复添加相同顶点(除非是起点) if (vertices.Count == 0 || vertices[vertices.Count - 1] != currentPos) { vertices.Add(currentPos); } // 找到下一个顶点:选择使方向变化最小的邻居(顺时针) List neighbors = adjacency[current]; Vector2Int? next = null; if (neighbors.Count == 1) { // 只有一个邻居 next = neighbors[0]; } else if (neighbors.Count == 2) { // 有两个邻居,选择不是前一个的那个 foreach (var neighbor in neighbors) { if (!previous.HasValue || neighbor != previous.Value) { next = neighbor; break; } } } else { // 多个邻居,选择角度最小的(顺时针方向) Vector2 currentVec = new Vector2(current.x, current.y); Vector2 direction; if (previous.HasValue) { Vector2 prevVec = new Vector2(previous.Value.x, previous.Value.y); direction = (currentVec - prevVec).normalized; } else { // 起始点,选择最下方的邻居(顺时针起始方向) direction = new Vector2(1, 0); // 向右 } float bestAngle = float.MaxValue; foreach (var neighbor in neighbors) { if (previous.HasValue && neighbor == previous.Value) continue; Vector2 neighborVec = new Vector2(neighbor.x, neighbor.y); Vector2 toNeighbor = (neighborVec - currentVec).normalized; // 计算从当前方向到邻居方向的角度(顺时针) float angle = GetClockwiseAngle(direction, toNeighbor); if (angle < bestAngle) { bestAngle = angle; next = neighbor; } } } if (!next.HasValue) break; previous = current; current = next.Value; } while (current != start && vertices.Count < adjacency.Count * 2); return vertices; } /// /// 计算从向量a到向量b的顺时针角度(0到2π) /// private static float GetClockwiseAngle(Vector2 a, Vector2 b) { float angleA = Mathf.Atan2(a.y, a.x); float angleB = Mathf.Atan2(b.y, b.x); float angle = angleB - angleA; // 转换为0到2π范围 if (angle < 0) angle += 2 * Mathf.PI; return angle; } /// /// 优化顶点:移除共线的顶点,保留关键转折点 /// private static List OptimizeVertices(List vertices) { if (vertices.Count <= 3) return vertices; List optimized = new List(); const float epsilon = 0.0001f; // 浮点数比较阈值 for (int i = 0; i < vertices.Count; i++) { Vector2 prev = vertices[(i - 1 + vertices.Count) % vertices.Count]; Vector2 current = vertices[i]; Vector2 next = vertices[(i + 1) % vertices.Count]; // 计算两个向量 Vector2 v1 = current - prev; Vector2 v2 = next - current; // 检查是否共线(叉积接近0)且方向相同(点积大于0) float crossProduct = v1.x * v2.y - v1.y * v2.x; float dotProduct = Vector2.Dot(v1.normalized, v2.normalized); // 如果共线且方向相同,跳过当前点 if (Mathf.Abs(crossProduct) < epsilon && dotProduct > 0.99f) { continue; } optimized.Add(current); } return optimized; } } public static class BlockPuzzleYarnCommand { [YarnCommand("init_block_puzzle")] public static void InitBlockPuzzle(string puzzleName) { if (BlockPuzzleKit.TryLoadBlockPuzzleData(puzzleName, out var blockPuzzleData)) { BlockPuzzleSystem.Instance.Initialize(blockPuzzleData); } else { Debug.LogError($"加载 BlockPuzzleData 失败: {puzzleName}"); } } [YarnCommand("clear_block_puzzle")] public static void ClearBlockPuzzle() { BlockPuzzleSystem.Instance.ClearGame(); } // [YarnCommand("reset_block_puzzle")] // public static void ResetBlockPuzzle() // { // BlockPuzzleSystem.Instance.ResetGame(); // } } [Serializable] public struct BlockPuzzleData { public string puzzleName; public int gridWidth; public int gridHeight; public float cellSize; // 网格内Shape public GridShapeData[] gridShapeDataInfos; // 验证器 public string validatorName; } [Serializable] public struct GridShapeData { public string shapeKey; public Vector2Int rootCell; public RotationAngle rotationAngle; public bool isDraggable; } [Serializable] public struct BlockShapeData { public int shapeId; public string shapeName; public int width; public int height; public float cellSize; public string originalPatternStr; public string spritePath; [Header("额外限制")] public bool extraLimits; public Vector2Int rootLimit; public RotationAngle rotationAngleLimit; [JsonIgnore] [HideInInspector] public BlockShapeInfo blockShapeInfo; public Sprite GetSprite() { return ResourceKit.LoadAssetSync(spritePath); } } [Serializable] public struct BlockShapeInfo { public string shapeName; public BlockPuzzleParamValue paramValue; public string shapeDescription; public string[] states; public string buttonImagePath; public Sprite GetButtonImage() { return ResourceKit.LoadAssetSync(buttonImagePath); } } [Serializable] public struct BlockPuzzleParamValue { public int power; public int noise; public int heat; } }