BoardView.ts 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020
  1. import { _decorator, Component, Node, SpriteFrame, EventTouch, Graphics, Vec2, Vec3, tween, Sprite, Layout, UITransform, Size, view, Color, Prefab, instantiate } from 'cc';
  2. import { CellState, ObstacleType, PatternType } from '../../util/Enum';
  3. import { Board } from './Board';
  4. import { aa } from 'db://assets/scripts/aa';
  5. import { UI_Win } from '../../ui/UIDialog/win/UI_Win';
  6. import { ContainerView } from '../container/ContainerView';
  7. import { Container } from '../container/Container';
  8. const { ccclass, property } = _decorator;
  9. // 线条数据结构
  10. interface Line {
  11. points: Vec2[]; // 线条经过的所有点
  12. cells: { x: number, y: number }[]; // 线条经过的所有格子坐标
  13. color?: Color;
  14. width?: number;
  15. startCell: { x: number, y: number }; // 起始格子坐标
  16. endCell?: { x: number, y: number }; // 结束格子坐标
  17. patternType?: PatternType; // 图案类型
  18. linkNodes?: Node[]; // 该线条对应的所有链接节点
  19. }
  20. @ccclass('BoardView')
  21. export class BoardView extends Component {
  22. board: Board
  23. @property(Node)
  24. grid: Node = null;
  25. @property(Node)
  26. gridfather: Node = null;
  27. @property([SpriteFrame])
  28. emptyCell: SpriteFrame[] = [];
  29. @property(Prefab) // 地图格子预制体
  30. cellPrefab: Prefab = null;
  31. @property([Color]) // 颜色属性数组
  32. patternColors: Color[] = [];
  33. @property(Prefab)
  34. Knitt: Prefab = null;//毛线预制体
  35. @property(Prefab)
  36. knittPrefab: Prefab = null;//毛线球预制体
  37. @property(Node)
  38. KnittFlow: Node = null;//毛线节点
  39. @property(Prefab)
  40. KnittStart: Prefab = null;//毛线起点预制体
  41. @property(Prefab)
  42. KnittLink: Prefab = null;//毛线线条预制体
  43. @property(Prefab)
  44. KnittTurn: Prefab = null;//毛线拐弯预制体
  45. @property(Node)
  46. KnittLinkNode: Node = null;//毛线线条节点
  47. // 地图边界属性Node
  48. private mapBounds: { left: number, right: number, top: number, bottom: number } | null = null;
  49. // 数据模型
  50. private boardModel: Board = new Board();
  51. private containerView: ContainerView = null;
  52. // 视图相关属性
  53. private isDrawing: boolean = false;
  54. private currentLine: Line = null;
  55. private historyLines: Line[] = [];
  56. private cellSize: number = 80;
  57. private mapOffset: Vec2 = new Vec2();
  58. private startCell: { x: number, y: number } = null;
  59. private currentCell: { x: number, y: number } = null;
  60. private visitedCells: { x: number, y: number }[] = [];
  61. private startPatternType: PatternType | null = null;
  62. private cellNodes: Node[][] = [];
  63. private linkNodes: Node[] = []; // 存储所有Link节点
  64. private currentLinkNode: Node = null; // 当前正在绘制的Link节点
  65. private pathGroups: { path: number[], groupIndex: number }[] = []; // 存储路径组信息
  66. private isReversed: boolean = false; // 标记是否反向绘制
  67. private knittPositions: Map<PatternType, { x: number, y: number }[]> = new Map(); // 存储每个图案类型的Knitt位置
  68. init(board: Board) {
  69. this.board = board;
  70. this.boardModel.boardSize = this.boardModel.boardRows * this.boardModel.boardCols;
  71. this.adjustMapSize();
  72. this.boardModel.initBoard(this.boardModel.boardRows, this.boardModel.boardCols);
  73. this.calculateMapOffset();
  74. this.initCellNodes();
  75. this.calculateMapBounds();
  76. this.updateBoardView();
  77. this.setKnittConfig();
  78. }
  79. // 初始化格子节点
  80. private initCellNodes() {
  81. this.cellNodes = [];
  82. for (let y = 0; y < this.boardModel.boardRows; y++) {
  83. this.cellNodes[y] = [];
  84. for (let x = 0; x < this.boardModel.boardCols; x++) {
  85. const cellNode = instantiate(this.cellPrefab);
  86. cellNode.name = `cell_${x}_${y}`;
  87. cellNode.setParent(this.grid);
  88. const posX = x * this.cellSize - this.mapOffset.x;
  89. const posY = -y * this.cellSize + this.mapOffset.y;
  90. cellNode.setPosition(posX, posY);
  91. this.cellNodes[y][x] = cellNode;
  92. const contentNode = cellNode.getChildByName('cottent');
  93. if (contentNode) {
  94. contentNode.active = false;
  95. }
  96. }
  97. }
  98. }
  99. private calculateMapOffset() {
  100. if (!this.grid) return;
  101. const transform = this.grid.getComponent(UITransform);
  102. if (!transform) return;
  103. // 计算居中偏移量
  104. const mapWidth = this.boardModel.boardCols * this.cellSize;
  105. const mapHeight = this.boardModel.boardRows * this.cellSize;
  106. this.mapOffset.x = mapWidth / 2 - this.cellSize / 2;
  107. this.mapOffset.y = mapHeight / 2 - this.cellSize / 2;
  108. }
  109. private calculateMapBounds() {
  110. if (!this.grid) return;
  111. const transform = this.grid.getComponent(UITransform);
  112. if (!transform) return;
  113. const worldPos = this.grid.worldPosition;
  114. const width = transform.width;
  115. const height = transform.height;
  116. this.mapBounds = {
  117. left: worldPos.x - width / 2,
  118. right: worldPos.x + width / 2,
  119. top: worldPos.y + height / 2,
  120. bottom: worldPos.y - height / 2
  121. };
  122. }
  123. private adjustMapSize() {
  124. if (!this.gridfather) return;
  125. if (!this.grid) return;
  126. const gridFatherTransform = this.gridfather.getComponent(UITransform);
  127. if (gridFatherTransform) {
  128. const idealWidth = (this.boardModel.boardCols * this.cellSize) + 60;
  129. const idealHeight = (this.boardModel.boardRows * this.cellSize) + 60;
  130. gridFatherTransform.setContentSize(new Size(idealWidth, idealHeight));
  131. }
  132. const gridTransform = this.grid.getComponent(UITransform);
  133. if (gridTransform) {
  134. const idealWidth = this.boardModel.boardCols * this.cellSize;
  135. const idealHeight = this.boardModel.boardRows * this.cellSize;
  136. gridTransform.setContentSize(new Size(idealWidth, idealHeight));
  137. }
  138. }
  139. updateBoardView() {
  140. // 更新棋盘视图逻辑
  141. }
  142. private setKnittConfig() {
  143. const container = new Container();
  144. container.setPathConfig("9,8,15,22,29,36;18,25,24,31;40,39,32;13,6,5,4,3,2,1,0,7,14,21,28,35,42,43,44,45,46,47,48,41;20,27,34;33,26,19,12,11,10,17,16,23,30,37,38;");
  145. this.pathGroups = container.pathGroups;
  146. this.containerView = this.node.addComponent(ContainerView);
  147. this.containerView.init(container);
  148. // this.containerView.setPatternColors(this.patternColors);
  149. this.containerView.setemptyCell(this.emptyCell);
  150. this.containerView.Knitt = this.knittPrefab;
  151. this.containerView.KnittLink = this.KnittLink;
  152. this.containerView.KnittFlow = this.KnittFlow;
  153. // 设置触摸事件回调
  154. this.containerView.setTouchCallbacks(
  155. this.onKnittTouchStart.bind(this),
  156. this.onKnittTouchMove.bind(this),
  157. this.onKnittTouchEnd.bind(this)
  158. );
  159. this.containerView.setupKnittPaths(this.cellNodes, this.boardModel.boardCols, this.boardModel.boardRows);
  160. }
  161. // Knitt触摸开始回调
  162. private onKnittTouchStart(cellIndex: number, groupIndex: number, event: EventTouch) {
  163. console.log(`Knitt触摸开始: 格子索引=${cellIndex}, 组索引=${groupIndex}`);
  164. const { row, col } = this.containerView.getRowColFromIndex(cellIndex, this.boardModel.boardCols);
  165. // 检查是否是路径组的起点或终点
  166. const pathGroup = this.pathGroups.find(group => group.groupIndex === groupIndex);
  167. if (!pathGroup) return;
  168. const startIndex = pathGroup.path[0];
  169. const endIndex = pathGroup.path[pathGroup.path.length - 1];
  170. const { row: startRow, col: startCol } = this.containerView.getRowColFromIndex(startIndex, this.boardModel.boardCols);
  171. const { row: endRow, col: endCol } = this.containerView.getRowColFromIndex(endIndex, this.boardModel.boardCols);
  172. // 检查是否点击的是起点或终点
  173. const isStartPoint = (col === startCol && row === startRow);
  174. const isEndPoint = (col === endCol && row === endRow);
  175. if (!isStartPoint && !isEndPoint) {
  176. console.log(`点击的不是起点或终点,不允许开始绘制`);
  177. return;
  178. }
  179. // 检查是否已经完成了该路径组的绘制
  180. const existingLineIndex = this.historyLines.findIndex(line => line.patternType === groupIndex);
  181. if (existingLineIndex !== -1) {
  182. // 如果路径已完成,清除该路径的所有链接节点
  183. this.clearPathGroup(groupIndex);
  184. return; // 清除后直接返回,不开始新的绘制
  185. }
  186. // 确定绘制方向
  187. this.isReversed = isEndPoint; // 如果点击的是终点,则反向绘制
  188. this.startDrawing(col, row, groupIndex);
  189. }
  190. // 清除指定路径组的所有链接节点
  191. private clearPathGroup(patternType: PatternType) {
  192. // 从historyLines中移除该路径组的线条
  193. const lineIndex = this.historyLines.findIndex(line => line.patternType === patternType);
  194. if (lineIndex !== -1) {
  195. const line = this.historyLines[lineIndex];
  196. // 清除该线条对应的所有链接节点
  197. if (line.linkNodes) {
  198. line.linkNodes.forEach(linkNode => {
  199. if (linkNode && linkNode.isValid) {
  200. linkNode.destroy();
  201. }
  202. });
  203. // 从总链接节点数组中移除
  204. this.linkNodes = this.linkNodes.filter(linkNode =>
  205. line.linkNodes.indexOf(linkNode) === -1
  206. );
  207. }
  208. this.historyLines.splice(lineIndex, 1);
  209. }
  210. console.log(`清除路径组${patternType}的绘制`);
  211. }
  212. // Knitt触摸移动回调
  213. private onKnittTouchMove(cellIndex: number, groupIndex: number, event: EventTouch) {
  214. if (!this.isDrawing) return;
  215. const pos = event.getUILocation();
  216. const touchPoint = new Vec2(pos.x, pos.y);
  217. this.updateDrawing(touchPoint, groupIndex);
  218. }
  219. // Knitt触摸结束回调
  220. private onKnittTouchEnd(cellIndex: number, groupIndex: number, event: EventTouch) {
  221. if (!this.isDrawing) return;
  222. const { row, col } = this.containerView.getRowColFromIndex(cellIndex, this.boardModel.boardCols);
  223. this.endDrawing(col, row, groupIndex);
  224. }
  225. // ... existing code ...
  226. // 开始绘制
  227. private startDrawing(x: number, y: number, patternType: PatternType) {
  228. // 重置之前的绘制状态(如果有)
  229. this.resetDrawingState();
  230. this.isDrawing = true;
  231. this.startCell = { x, y };
  232. this.startPatternType = patternType;
  233. this.currentCell = { x, y };
  234. this.visitedCells = [{ x, y }];
  235. this.currentLine = {
  236. points: [this.getCellCenterPoint(x, y)],
  237. cells: [{ x, y }],
  238. color: this.patternColors[patternType],
  239. width: 40,
  240. startCell: { x, y },
  241. patternType: patternType,
  242. linkNodes: [] // 初始化链接节点数组
  243. };
  244. // 创建起始点(使用Knitt作为头部)
  245. const startPos = this.getCellCenterPoint(x, y);
  246. const startNode = instantiate(this.Knitt); // 使用Knitt作为头部
  247. startNode.setParent(this.KnittLinkNode);
  248. // 设置起始点位置
  249. startNode.setPosition(startPos.x, startPos.y);
  250. // 设置颜色
  251. this.setupLinkColor(startNode, patternType);
  252. // 设置初始方向(默认朝右,可根据需要调整)
  253. startNode.setRotationFromEuler(0, 0, 0);
  254. // 保存链接节点引用
  255. this.linkNodes.push(startNode);
  256. this.currentLine.linkNodes.push(startNode);
  257. console.log(`开始绘制路径: 起点(${x},${y}), 图案类型${patternType}, 反向:${this.isReversed}`);
  258. }
  259. // ... existing code ...
  260. // ... existing code ...
  261. // 更新绘制
  262. private updateDrawing(touchPoint: Vec2, patternType: PatternType) {
  263. const nearestCell = this.findNearestCell(touchPoint);
  264. if (!nearestCell) return;
  265. const { x, y } = nearestCell;
  266. // 检查是否可以连接
  267. if (this.canConnectToCell(x, y, patternType)) {
  268. // 检查路径是否与其他已完成路径交叉
  269. const crossedLine = this.getLineCrossedByPath(this.currentCell.x, this.currentCell.y, x, y);
  270. if (crossedLine) {
  271. // 如果交叉,清除被交叉的路径
  272. this.clearPathGroup(crossedLine.patternType);
  273. }
  274. // 检查是否是回退操作(回到已访问的单元格)
  275. const existingIndex = this.visitedCells.findIndex(cell => cell.x === x && cell.y === y);
  276. if (existingIndex !== -1) {
  277. // 回退操作 - 移除回退路径上的链接段
  278. this.handlePathBacktrack(existingIndex);
  279. } else {
  280. // 前进操作 - 创建新的链接段
  281. this.createLinkSegment(this.currentCell.x, this.currentCell.y, x, y, patternType);
  282. // 如果这是第一次移动,设置起始点的方向
  283. if (this.currentLine.cells.length === 1) {
  284. this.updateStartNodeDirection(this.currentCell.x, this.currentCell.y, x, y);
  285. }
  286. }
  287. this.currentCell = { x, y };
  288. // 更新访问过的单元格列表
  289. if (existingIndex !== -1) {
  290. // 如果是回退,截断访问列表
  291. this.visitedCells = this.visitedCells.slice(0, existingIndex + 1);
  292. // 同时截断线条点列表
  293. this.currentLine.points = this.currentLine.points.slice(0, existingIndex + 1);
  294. this.currentLine.cells = this.currentLine.cells.slice(0, existingIndex + 1);
  295. // 同时截断链接节点列表
  296. if (this.currentLine.linkNodes) {
  297. const removeCount = this.currentLine.linkNodes.length - existingIndex;
  298. for (let i = 0; i < removeCount; i++) {
  299. const linkNode = this.currentLine.linkNodes.pop();
  300. if (linkNode && linkNode.isValid) {
  301. linkNode.destroy();
  302. }
  303. }
  304. }
  305. } else {
  306. // 如果是前进,添加新单元格到访问列表
  307. this.visitedCells.push({ x, y });
  308. this.currentLine.points.push(this.getCellCenterPoint(x, y));
  309. this.currentLine.cells.push({ x, y });
  310. }
  311. // 检查是否连接到终点
  312. if (this.isEndPoint(x, y, patternType)) {
  313. this.currentLine.endCell = { x, y };
  314. this.completeDrawing(patternType);
  315. }
  316. }
  317. }
  318. // ... existing code ...
  319. // ... existing code ...
  320. // 更新起始节点方向
  321. private updateStartNodeDirection(fromX: number, fromY: number, toX: number, toY: number) {
  322. if (this.currentLine.linkNodes.length > 0) {
  323. const startNode = this.currentLine.linkNodes[0]; // 起始节点是第一个节点
  324. if (startNode && startNode.isValid) {
  325. // 计算方向
  326. const dx = toX - fromX;
  327. const dy = toY - fromY;
  328. const angle = Math.atan2(dy, dx) * 180 / Math.PI;
  329. // 设置起始节点方向
  330. startNode.setRotationFromEuler(0, 0, angle);
  331. }
  332. }
  333. }
  334. // ... existing code ...
  335. // 处理路径回退
  336. private handlePathBacktrack(backIndex: number) {
  337. // 计算需要移除的链接段数量
  338. const removeCount = this.visitedCells.length - backIndex - 1;
  339. // 从linkNodes中移除对应的链接段
  340. for (let i = 0; i < removeCount; i++) {
  341. if (this.linkNodes.length > 0) {
  342. const linkNode = this.linkNodes.pop();
  343. if (linkNode && linkNode.isValid) {
  344. linkNode.destroy();
  345. }
  346. }
  347. }
  348. console.log(`路径回退: 移除${removeCount}个链接段`);
  349. }
  350. // ... existing code ...
  351. // 创建链接段
  352. private createLinkSegment(fromX: number, fromY: number, toX: number, toY: number, patternType: PatternType) {
  353. // 创建从起点到终点的线段
  354. const fromPos = this.getCellCenterPoint(fromX, fromY);
  355. const toPos = this.getCellCenterPoint(toX, toY);
  356. // 先将前一个"线头"转换为"线身"
  357. if (this.currentLine.linkNodes.length > 0) {
  358. const prevHeadNode = this.currentLine.linkNodes[this.currentLine.linkNodes.length - 1];
  359. if (prevHeadNode && prevHeadNode.isValid) {
  360. // 销毁旧的线头节点
  361. prevHeadNode.destroy();
  362. // 从数组中移除
  363. this.currentLine.linkNodes.pop();
  364. this.linkNodes.pop();
  365. }
  366. }
  367. // 检查是否是拐角(需要至少3个点才能判断是否拐角)
  368. let isTurn = false;
  369. if (this.currentLine.cells.length >= 2) {
  370. const prevCell = this.currentLine.cells[this.currentLine.cells.length - 2];
  371. const currentCell = this.currentLine.cells[this.currentLine.cells.length - 1];
  372. // 判断是否拐角:前一个方向和当前方向不同
  373. const prevDirX = currentCell.x - prevCell.x;
  374. const prevDirY = currentCell.y - prevCell.y;
  375. const currentDirX = toX - currentCell.x;
  376. const currentDirY = toY - currentCell.y;
  377. // 如果方向改变(叉积不为0),则是拐角
  378. if (prevDirX * currentDirY - prevDirY * currentDirX !== 0) {
  379. isTurn = true;
  380. }
  381. }
  382. // 根据是否拐角选择不同的预制体
  383. const bodyPrefab = isTurn ? this.KnittTurn : this.KnittLink;
  384. if (!bodyPrefab) return;
  385. // 创建新的线身
  386. const bodyNode = instantiate(bodyPrefab);
  387. bodyNode.setParent(this.KnittLinkNode);
  388. if (isTurn) {
  389. // 处理拐点方向
  390. this.updateTurnTransform(bodyNode, fromX, fromY, toX, toY);
  391. } else {
  392. // 处理普通线段
  393. this.updateLinkTransform(bodyNode, fromPos, toPos);
  394. }
  395. this.setupLinkColor(bodyNode, patternType);
  396. this.linkNodes.push(bodyNode);
  397. this.currentLine.linkNodes.push(bodyNode);
  398. // 创建新的线头(使用Knitt预制体)
  399. const headNode = instantiate(this.Knitt);
  400. headNode.setParent(this.KnittLinkNode);
  401. headNode.setPosition(toPos.x, toPos.y);
  402. this.setupLinkColor(headNode, patternType);
  403. // 设置线头方向,使其面向移动方向(从fromPos到toPos)
  404. const dx = toPos.x - fromPos.x;
  405. const dy = toPos.y - fromPos.y;
  406. const angle = Math.atan2(dy, dx) * 180 / Math.PI;
  407. // 因为线头的右边是头,所以不需要额外调整角度
  408. headNode.setRotationFromEuler(0, 0, angle);
  409. this.linkNodes.push(headNode);
  410. this.currentLine.linkNodes.push(headNode);
  411. this.currentLinkNode = headNode;
  412. console.log(`创建链接段: 从(${fromX},${fromY})到(${toX},${toY}), 拐角: ${isTurn}`);
  413. }
  414. // 更新拐点的变换
  415. private updateTurnTransform(turnNode: Node, fromX: number, fromY: number, toX: number, toY: number) {
  416. // 获取拐点前一个点
  417. if (this.currentLine.cells.length < 2) return;
  418. const prevCell = this.currentLine.cells[this.currentLine.cells.length - 2];
  419. const currentCell = { x: fromX, y: fromY }; // 当前点
  420. const nextCell = { x: toX, y: toY }; // 下一个点
  421. // 计算拐点位置(当前格子中心)
  422. const turnPos = this.getCellCenterPoint(currentCell.x, currentCell.y);
  423. turnNode.setPosition(turnPos.x, turnPos.y);
  424. // 设置拐点的缩放为1,1,1
  425. turnNode.setScale(1, 1, 1);
  426. // 计算进入方向和离开方向
  427. const inDirX = currentCell.x - prevCell.x;
  428. const inDirY = currentCell.y - prevCell.y;
  429. const outDirX = nextCell.x - currentCell.x;
  430. const outDirY = nextCell.y - currentCell.y;
  431. // 根据进入和离开方向计算旋转角度
  432. let angle = 0;
  433. // 判断拐角类型并设置正确的角度
  434. if (inDirX === 1 && inDirY === 0) { // 从右进入
  435. if (outDirX === 0 && outDirY === 1) { // 向下离开
  436. angle = 0; // 右下拐角,需要旋转180度
  437. } else if (outDirX === 0 && outDirY === -1) { // 向上离开
  438. angle = -90; // 右上拐角,需要顺时针旋转90度
  439. }
  440. } else if (inDirX === -1 && inDirY === 0) { // 从左进入
  441. if (outDirX === 0 && outDirY === 1) { // 向下离开
  442. angle = 90; // 左下拐角,与默认方向一致
  443. } else if (outDirX === 0 && outDirY === -1) { // 向上离开
  444. angle = 180; // 左上拐角,需要逆时针旋转90度
  445. }
  446. } else if (inDirX === 0 && inDirY === 1) { // 从下进入
  447. if (outDirX === 1 && outDirY === 0) { // 向右离开
  448. angle = 180; // 下右拐角,需要逆时针旋转90度
  449. } else if (outDirX === -1 && outDirY === 0) { // 向左离开
  450. angle = -90; // 下左拐角,需要旋转180度
  451. }
  452. } else if (inDirX === 0 && inDirY === -1) { // 从上进入
  453. if (outDirX === 1 && outDirY === 0) { // 向右离开
  454. angle = 90; // 上右拐角,需要顺时针旋转90度
  455. } else if (outDirX === -1 && outDirY === 0) { // 向左离开
  456. angle = 0; // 上左拐角,需要顺时针旋转90度
  457. }
  458. }
  459. turnNode.setRotationFromEuler(0, 0, angle);
  460. }
  461. // ... existing code ...
  462. // 创建平滑曲线连接
  463. private createSmoothCurve(fromPos: Vec2, toPos: Vec2, patternType: PatternType) {
  464. // 创建多个节点来模拟曲线
  465. const segments = 8; // 曲线分段数
  466. const controlPoints = this.calculateControlPoints(fromPos, toPos);
  467. for (let i = 0; i < segments; i++) {
  468. const t1 = i / segments;
  469. const t2 = (i + 1) / segments;
  470. // 计算贝塞尔曲线上的两点
  471. const point1 = this.calculateBezierPoint(t1, fromPos, controlPoints.control1, controlPoints.control2, toPos);
  472. const point2 = this.calculateBezierPoint(t2, fromPos, controlPoints.control1, controlPoints.control2, toPos);
  473. // 创建线段
  474. const linkNode = instantiate(this.Knitt);
  475. linkNode.setParent(this.KnittLinkNode);
  476. // 设置线段位置和旋转
  477. this.updateLinkTransform(linkNode, point1, point2);
  478. // 设置颜色
  479. this.setupLinkColor(linkNode, patternType);
  480. // 保存链接节点引用
  481. this.linkNodes.push(linkNode);
  482. if (this.currentLine.linkNodes) {
  483. this.currentLine.linkNodes.push(linkNode);
  484. }
  485. }
  486. }
  487. // 计算控制点以创建自然的曲线效果
  488. private calculateControlPoints(fromPos: Vec2, toPos: Vec2): { control1: Vec2, control2: Vec2 } {
  489. const dx = toPos.x - fromPos.x;
  490. const dy = toPos.y - fromPos.y;
  491. // 控制点距离基于方向差异
  492. const controlDistance = Math.sqrt(dx * dx + dy * dy) * 0.5;
  493. // 创建控制点,使线条呈现S形弯曲效果
  494. const control1 = new Vec2(
  495. fromPos.x + dx * 0.25 + (-dy * 0.2),
  496. fromPos.y + dy * 0.25 + (dx * 0.2)
  497. );
  498. const control2 = new Vec2(
  499. fromPos.x + dx * 0.75 + (dy * 0.2),
  500. fromPos.y + dy * 0.75 + (-dx * 0.2)
  501. );
  502. return { control1, control2 };
  503. }
  504. // 计算贝塞尔曲线上的点
  505. private calculateBezierPoint(t: number, p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2): Vec2 {
  506. const u = 1 - t;
  507. const tt = t * t;
  508. const uu = u * u;
  509. const uuu = uu * u;
  510. const ttt = tt * t;
  511. const p = new Vec2();
  512. p.x = uuu * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + ttt * p3.x;
  513. p.y = uuu * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + ttt * p3.y;
  514. return p;
  515. }
  516. // ... existing code ...
  517. // 更新Link的变换
  518. private updateLinkTransform(linkNode: Node, fromPos: Vec2, toPos: Vec2) {
  519. // 计算中心点
  520. const centerX = (fromPos.x + toPos.x) / 2;
  521. const centerY = (fromPos.y + toPos.y) / 2;
  522. linkNode.setPosition(centerX, centerY);
  523. // 计算长度和旋转
  524. const dx = toPos.x - fromPos.x;
  525. const dy = toPos.y - fromPos.y;
  526. const distance = Math.sqrt(dx * dx + dy * dy);
  527. const angle = Math.atan2(dy, dx) * 180 / Math.PI;
  528. // 获取Link节点的实际宽度以正确缩放
  529. const linkUITransform = linkNode.getComponent(UITransform);
  530. const linkWidth = linkUITransform ? linkUITransform.width : 100;
  531. // 设置缩放(长度),使用实际宽度而不是硬编码的100
  532. linkNode.setScale(distance / linkWidth, 1, 1);
  533. // 设置旋转
  534. linkNode.setRotationFromEuler(0, 0, angle);
  535. }
  536. // ... existing code ...
  537. // 结束绘制
  538. private endDrawing(x: number, y: number, patternType: PatternType) {
  539. // 检查是否连接到另一个端点
  540. if (this.isValidEndPoint(x, y, patternType)) {
  541. this.currentLine.endCell = { x, y };
  542. this.completeDrawing(patternType);
  543. } else {
  544. // 如果不是有效终点,取消绘制
  545. this.cancelDrawing();
  546. }
  547. }
  548. // 检查是否是有效的终点(另一个端点)
  549. private isValidEndPoint(x: number, y: number, patternType: PatternType): boolean {
  550. // 查找对应路径组的起点和终点
  551. const pathGroup = this.pathGroups.find(group => group.groupIndex === patternType);
  552. if (!pathGroup) return false;
  553. const startIndex = pathGroup.path[0];
  554. const endIndex = pathGroup.path[pathGroup.path.length - 1];
  555. const { row: startRow, col: startCol } = this.containerView.getRowColFromIndex(startIndex, this.boardModel.boardCols);
  556. const { row: endRow, col: endCol } = this.containerView.getRowColFromIndex(endIndex, this.boardModel.boardCols);
  557. // 根据绘制方向确定终点
  558. let targetPoint;
  559. if (this.isReversed) {
  560. // 反向绘制:终点应该是起点
  561. targetPoint = { x: startCol, y: startRow };
  562. } else {
  563. // 正向绘制:终点应该是终点
  564. targetPoint = { x: endCol, y: endRow };
  565. }
  566. // 检查当前格子是否是目标终点格子
  567. const isEndPoint = (x === targetPoint.x && y === targetPoint.y);
  568. if (isEndPoint) {
  569. console.log(`成功连接到终点: 图案类型${patternType}, 终点(${x},${y})`);
  570. return true;
  571. }
  572. //console.log(`无效终点: 当前(${x},${y})不是图案类型${patternType}的目标终点`);
  573. return false;
  574. }
  575. // ... existing code ...
  576. // 完成绘制
  577. private completeDrawing(patternType: PatternType) {
  578. // 将当前线条添加到历史记录
  579. this.historyLines.push(this.currentLine);
  580. // 检查是否完成所有路径
  581. if (this.checkAllPathsCompleted()) {
  582. console.log("所有路径完成,游戏胜利!");
  583. this.showWinEffect();
  584. }
  585. this.resetDrawingState();
  586. console.log(`图案类型${patternType}的路径绘制完成`);
  587. }
  588. // 取消绘制
  589. private cancelDrawing() {
  590. // 删除当前绘制过程中的链接段
  591. if (this.currentLine && this.currentLine.linkNodes) {
  592. this.currentLine.linkNodes.forEach(linkNode => {
  593. if (linkNode && linkNode.isValid) {
  594. linkNode.destroy();
  595. }
  596. });
  597. // 从总链接节点数组中移除
  598. this.linkNodes = this.linkNodes.filter(linkNode =>
  599. this.currentLine.linkNodes.indexOf(linkNode) === -1
  600. );
  601. }
  602. this.resetDrawingState();
  603. console.log("绘制取消");
  604. }
  605. // ... existing code ...
  606. // 重置绘制状态
  607. private resetDrawingState() {
  608. this.isDrawing = false;
  609. this.currentLine = null;
  610. this.startCell = null;
  611. this.startPatternType = null;
  612. this.currentCell = null;
  613. this.visitedCells = [];
  614. this.currentLinkNode = null;
  615. this.isReversed = false;
  616. }
  617. // 设置Link颜色
  618. private setupLinkColor(linkNode: Node, patternType: PatternType) {
  619. const color = this.patternColors[patternType];
  620. const sprite = linkNode.getComponent(Sprite);
  621. if (sprite) {
  622. sprite.color = color;
  623. }
  624. // 递归设置子节点颜色
  625. this.setChildSpritesColor(linkNode, color);
  626. }
  627. // 递归设置子节点Sprite颜色
  628. private setChildSpritesColor(node: Node, color: Color) {
  629. const sprite = node.getComponent(Sprite);
  630. if (sprite) {
  631. sprite.color = color;
  632. }
  633. node.children.forEach(child => this.setChildSpritesColor(child, color));
  634. }
  635. // 查找最近的格子
  636. private findNearestCell(point: Vec2): { x: number, y: number } | null {
  637. let minDistance = Infinity;
  638. let nearestCell = null;
  639. for (let y = 0; y < this.boardModel.boardRows; y++) {
  640. for (let x = 0; x < this.boardModel.boardCols; x++) {
  641. const cellCenter = this.getCellCenterPoint(x, y);
  642. const distance = Vec2.distance(point, cellCenter);
  643. const threshold = this.cellSize * 0.6;
  644. if (distance < minDistance && distance < threshold) {
  645. minDistance = distance;
  646. nearestCell = { x, y };
  647. }
  648. }
  649. }
  650. return nearestCell;
  651. }
  652. // 检查是否可以连接到格子
  653. private canConnectToCell(x: number, y: number, patternType: PatternType): boolean {
  654. // 检查是否相邻
  655. if (!this.isNeighbor(this.currentCell.x, this.currentCell.y, x, y)) {
  656. return false;
  657. }
  658. // 检查是否已经在路径上(允许回退)
  659. const isOnPath = this.visitedCells.some(cell => cell.x === x && cell.y === y);
  660. if (isOnPath) {
  661. return true;
  662. }
  663. // 检查格子是否被其他路径占用
  664. if (this.isCellOccupied(x, y)) {
  665. return false;
  666. }
  667. // 新增:检查目标格子是否有其他路径组的Knitt
  668. if (this.hasKnittOfOtherGroup(x, y, patternType)) {
  669. console.log(`目标格子(${x},${y})有其他路径组的Knitt,不允许连接`);
  670. return false;
  671. }
  672. return true;
  673. }
  674. // 检查两个格子是否相邻
  675. private isNeighbor(x1: number, y1: number, x2: number, y2: number): boolean {
  676. const dx = Math.abs(x1 - x2);
  677. const dy = Math.abs(y1 - y2);
  678. return (dx === 1 && dy === 0) || (dx === 0 && dy === 1);
  679. }
  680. // 检查格子是否被占用
  681. private isCellOccupied(x: number, y: number): boolean {
  682. return this.historyLines.some(line =>
  683. line.cells.some(cell => cell.x === x && cell.y === y)
  684. );
  685. }
  686. // 检查目标格子是否有其他路径组的Knitt
  687. private hasKnittOfOtherGroup(x: number, y: number, currentPatternType: PatternType): boolean {
  688. // 计算当前格子的索引
  689. const cellIndex = y * this.boardModel.boardCols + x;
  690. // 检查这个格子是否属于任何路径组
  691. for (const pathGroup of this.pathGroups) {
  692. // 如果格子属于某个路径组,但不是当前路径组
  693. if (pathGroup.path.indexOf(cellIndex) !== -1 && pathGroup.groupIndex !== currentPatternType) {
  694. // 检查这个格子是否是起点或终点
  695. const isStartOrEnd =
  696. cellIndex === pathGroup.path[0] ||
  697. cellIndex === pathGroup.path[pathGroup.path.length - 1];
  698. if (isStartOrEnd) {
  699. console.log(`格子(${x},${y})是路径组${pathGroup.groupIndex}的起点或终点,不允许连接`);
  700. return true;
  701. }
  702. }
  703. }
  704. return false;
  705. }
  706. // 检查路径段是否与其他已完成路径交叉
  707. private getLineCrossedByPath(fromX: number, fromY: number, toX: number, toY: number): Line | null {
  708. // 检查从(fromX, fromY)到(toX, toY)的线段是否与任何历史线段交叉
  709. for (const line of this.historyLines) {
  710. // 检查相邻格子之间的连线是否与历史路径交叉
  711. for (let i = 0; i < line.cells.length - 1; i++) {
  712. const cell1 = line.cells[i];
  713. const cell2 = line.cells[i + 1];
  714. // 检查是否是相邻格子
  715. if (this.isNeighbor(cell1.x, cell1.y, cell2.x, cell2.y)) {
  716. // 检查两条线段是否交叉
  717. if (this.isLineSegmentCrossing(fromX, fromY, toX, toY, cell1.x, cell1.y, cell2.x, cell2.y)) {
  718. return line;
  719. }
  720. }
  721. }
  722. }
  723. return null;
  724. }
  725. // 检查两条线段是否交叉
  726. private isLineSegmentCrossing(x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, x4: number, y4: number): boolean {
  727. // 使用向量叉积方法检测线段是否相交
  728. function cross(o: { x: number, y: number }, a: { x: number, y: number }, b: { x: number, y: number }) {
  729. return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);
  730. }
  731. const p1 = { x: x1, y: y1 };
  732. const p2 = { x: x2, y: y2 };
  733. const p3 = { x: x3, y: y3 };
  734. const p4 = { x: x4, y: y4 };
  735. // 快速排斥试验
  736. if (Math.max(p1.x, p2.x) < Math.min(p3.x, p4.x) ||
  737. Math.max(p3.x, p4.x) < Math.min(p1.x, p2.x) ||
  738. Math.max(p1.y, p2.y) < Math.min(p3.y, p4.y) ||
  739. Math.max(p3.y, p4.y) < Math.min(p1.y, p2.y)) {
  740. return false;
  741. }
  742. // 跨立试验
  743. if (cross(p1, p2, p3) * cross(p1, p2, p4) <= 0 &&
  744. cross(p3, p4, p1) * cross(p3, p4, p2) <= 0) {
  745. // 排除端点重合的情况
  746. if ((p1.x === p3.x && p1.y === p3.y) ||
  747. (p1.x === p4.x && p1.y === p4.y) ||
  748. (p2.x === p3.x && p2.y === p3.y) ||
  749. (p2.x === p4.x && p2.y === p4.y)) {
  750. return false;
  751. }
  752. return true;
  753. }
  754. return false;
  755. }
  756. // 检查是否是终点(兼容旧代码)
  757. private isEndPoint(x: number, y: number, patternType: PatternType): boolean {
  758. return this.isValidEndPoint(x, y, patternType);
  759. }
  760. // 检查所有路径是否完成(简化版)
  761. private checkAllPathsCompleted(): boolean {
  762. // 1. 检查每个路径组是否都有对应的完成线条
  763. for (const pathGroup of this.pathGroups) {
  764. const line = this.historyLines.find(line =>
  765. line.patternType === pathGroup.groupIndex &&
  766. line.endCell !== undefined
  767. );
  768. if (!line) {
  769. console.log(`路径组${pathGroup.groupIndex}未完成:未找到线条`);
  770. return false;
  771. }
  772. // 2. 检查线条是否覆盖了路径组的所有格子
  773. const pathGroupCells = new Set(pathGroup.path);
  774. const lineCells = new Set(line.cells.map(cell =>
  775. cell.y * this.boardModel.boardCols + cell.x
  776. ));
  777. // 检查路径组的所有格子是否都被线条覆盖
  778. for (const cellIndex of pathGroup.path) {
  779. if (!lineCells.has(cellIndex)) {
  780. console.log(`路径组${pathGroup.groupIndex}未完成:格子${cellIndex}未被覆盖`);
  781. return false;
  782. }
  783. }
  784. }
  785. // 3. 检查地图上是否还有空格子(可选,根据你的需求)
  786. if (this.hasEmptyCells()) {
  787. console.log("还有空格子未连接");
  788. return false;
  789. }
  790. console.log("所有路径组均已完成");
  791. return true;
  792. }
  793. // 检查是否有空格子
  794. private hasEmptyCells(): boolean {
  795. const allPathCells = new Set();
  796. // 收集所有路径组定义的格子
  797. for (const pathGroup of this.pathGroups) {
  798. pathGroup.path.forEach(index => allPathCells.add(index));
  799. }
  800. // 收集所有已连接格子的索引
  801. const connectedCells = new Set();
  802. for (const line of this.historyLines) {
  803. line.cells.forEach(cell => {
  804. const index = cell.y * this.boardModel.boardCols + cell.x;
  805. connectedCells.add(index);
  806. });
  807. }
  808. // 如果有路径组的格子没有被连接,就认为有空格子
  809. for (const cellIndex of allPathCells) {
  810. if (!connectedCells.has(cellIndex)) {
  811. return true;
  812. }
  813. }
  814. return false;
  815. }
  816. // 获取格子中心点
  817. getCellCenterPoint(x: number, y: number): Vec2 {
  818. const cellNode = this.cellNodes[y][x];
  819. if (!cellNode) return new Vec2(0, 0);
  820. // 获取格子的世界坐标
  821. const worldPos = cellNode.worldPosition;
  822. // 将世界坐标转换为KnittLinkNode的本地坐标(如果KnittLinkNode存在的话)
  823. if (this.KnittLinkNode) {
  824. const localPos = new Vec3();
  825. this.KnittLinkNode.inverseTransformPoint(localPos, new Vec3(worldPos.x, worldPos.y, 0));
  826. return new Vec2(localPos.x, localPos.y);
  827. }
  828. // 如果KnittLinkNode不存在,则使用grid作为备选
  829. if (this.grid) {
  830. const localPos = new Vec3();
  831. this.grid.inverseTransformPoint(localPos, new Vec3(worldPos.x, worldPos.y, 0));
  832. return new Vec2(localPos.x, localPos.y);
  833. }
  834. // 如果都没有,则返回世界坐标
  835. return new Vec2(worldPos.x, worldPos.y);
  836. }
  837. // 显示胜利效果
  838. private showWinEffect() {
  839. console.log("恭喜你赢了!");
  840. aa.uIMgr.showUI(UI_Win);
  841. }
  842. }