Greasy Fork 支持简体中文。

Highlight squares

14/04/2025, 16:51:53

  1. // ==UserScript==
  2. // @name Highlight squares
  3. // @namespace Violentmonkey Scripts
  4. // @match https://chess.ytdraws.win/*
  5. // @grant none
  6. // @version 1.0
  7. // @author -
  8. // @description 14/04/2025, 16:51:53
  9. // @license CC-NC-SA
  10. // ==/UserScript==
  11. // == Pathfinding & Attack Visualization Script ==
  12.  
  13. (function() { // Encapsulate to avoid polluting global scope too much
  14.  
  15. // --- Configuration ---
  16. const ATTACKED_SQUARE_COLOR = 'rgba(255, 0, 0, 0.25)'; // Red, semi-transparent
  17. const PATH_COLOR = 'rgba(0, 0, 255, 0.3)'; // Blue, semi-transparent
  18. const PATH_STEP_INTERVAL_MS = 50; // How often to check if we can take the next step (doesn't override game cooldown)
  19. const MOVE_COOLDOWN_THRESHOLD = 220; // From original game code, ms
  20.  
  21. // --- State Variables ---
  22. let visualizeAttacksEnabled = false;
  23. let attackedSquares = new Set(); // Stores "x,y" strings of attacked squares
  24. let pathfindingData = {
  25. active: false,
  26. pieceType: 0,
  27. startX: -1,
  28. startY: -1,
  29. targetX: -1,
  30. targetY: -1,
  31. currentPath: [], // Array of [x, y] steps
  32. pathIndex: 0,
  33. intervalId: null
  34. };
  35.  
  36. // --- Attack Calculation ---
  37.  
  38. /**
  39. * Calculates all squares attacked by enemy pieces.
  40. * Assumes generateLegalMoves works correctly for all piece types.
  41. * @returns {Set<string>} A set of "x,y" strings representing attacked squares.
  42. */
  43. function calculateAllEnemyAttacks() {
  44. const attacked = new Set();
  45. if (typeof board === 'undefined' || typeof teams === 'undefined' || typeof selfId === 'undefined' || typeof generateLegalMoves !== 'function') {
  46. return attacked;
  47. }
  48.  
  49. for (let x = 0; x < boardW; x++) {
  50. for (let y = 0; y < boardH; y++) {
  51. const team = teams[x]?.[y];
  52. const piece = board[x]?.[y];
  53. // Check if it's an enemy piece (not ours, not neutral, not empty)
  54. if (piece && team && team !== selfId && team !== 0) {
  55. try {
  56. const moves = generateLegalMoves(x, y, board, teams);
  57. for (const move of moves) {
  58. attacked.add(`${move[0]},${move[1]}`);
  59. }
  60. } catch (e) {
  61. // console.warn(`Error generating moves for enemy at ${x},${y}:`, e);
  62. }
  63. }
  64. }
  65. }
  66. return attacked;
  67. }
  68.  
  69. // --- Pathfinding (BFS) ---
  70.  
  71. /**
  72. * Finds the shortest path using Breadth-First Search.
  73. * @param {number} startX
  74. * @param {number} startY
  75. * @param {number} targetX
  76. * @param {number} targetY
  77. * @param {number} pieceType The type of piece trying to move (for generateLegalMoves)
  78. * @returns {Array<Array<number>> | null} Path as array of [x,y] steps, or null if no path.
  79. */
  80. function findPathBFS(startX, startY, targetX, targetY, pieceType) {
  81. console.log(`Pathfinding: Type ${pieceType} from ${startX},${startY} to ${targetX},${targetY}`);
  82. if (typeof board === 'undefined' || typeof teams === 'undefined' || typeof selfId === 'undefined' || typeof generateLegalMoves !== 'function') {
  83. console.error("Pathfinding failed: Missing game variables or functions.");
  84. return null;
  85. }
  86.  
  87. const queue = [[startX, startY]];
  88. const visited = new Set([`${startX},${startY}`]);
  89. const parentMap = {}; // Store "childX,childY": [parentX, parentY]
  90.  
  91. while (queue.length > 0) {
  92. const [currX, currY] = queue.shift();
  93.  
  94. // Target found?
  95. if (currX === targetX && currY === targetY) {
  96. // Reconstruct path
  97. const path = [];
  98. let step = [targetX, targetY];
  99. while (step[0] !== startX || step[1] !== startY) {
  100. path.push(step);
  101. const parentKey = `${step[0]},${step[1]}`;
  102. if (!parentMap[parentKey]) break; // Should not happen if target found
  103. step = parentMap[parentKey];
  104. }
  105. path.push([startX, startY]); // Add start
  106. console.log("Path found:", path.reverse()); // Reverse to get start -> end
  107. return path;
  108. }
  109.  
  110. // Get legal moves *as if* the piece were at currX, currY
  111. // We need a way to call generateLegalMoves assuming the piece *type*
  112. // is at currX, currY, temporarily ignoring the actual board state there
  113. // for planning purposes, *except* for blocking friendly pieces.
  114. // This requires modifying generateLegalMoves or having a variant.
  115. // --- SIMPLIFICATION for this example: ---
  116. // We'll call generateLegalMoves normally. This means it might fail if
  117. // the intermediate square is occupied inappropriately for the real piece.
  118. // A more robust solution needs a planning-specific move generator.
  119. // We also *simulate* the piece being there to check moves.
  120. const originalPiece = board[currX]?.[currY];
  121. const originalTeam = teams[currX]?.[currY];
  122. board[currX][currY] = pieceType; // Temporarily place piece for move generation
  123. teams[currX][currY] = selfId;
  124. let legalMoves = [];
  125. try {
  126. legalMoves = generateLegalMoves(currX, currY, board, teams);
  127. } catch (e) { /* handle error */ }
  128. // Restore original board state
  129. board[currX][currY] = originalPiece === undefined ? 0 : originalPiece;
  130. teams[currX][currY] = originalTeam === undefined ? 0 : originalTeam;
  131. // --- End Simplification ---
  132.  
  133.  
  134. for (const move of legalMoves) {
  135. const [nextX, nextY] = move;
  136. const nextKey = `${nextX},${nextY}`;
  137.  
  138. // Check bounds and if already visited
  139. if (nextX < 0 || nextX >= boardW || nextY < 0 || nextY >= boardH || visited.has(nextKey)) {
  140. continue;
  141. }
  142.  
  143. // Check if destination is blocked by a friendly piece (crucial!)
  144. // Allow moving *to* the target square even if occupied (capture/overwrite)
  145. if (teams[nextX]?.[nextY] === selfId && !(nextX === targetX && nextY === targetY)) {
  146. continue;
  147. }
  148.  
  149. // Mark visited, store parent, enqueue
  150. visited.add(nextKey);
  151. parentMap[nextKey] = [currX, currY];
  152. queue.push([nextX, nextY]);
  153. }
  154. }
  155.  
  156. console.log("Pathfinding failed: No path found.");
  157. return null; // No path found
  158. }
  159.  
  160. // --- Path Execution ---
  161.  
  162. function executePathStep() {
  163. if (!pathfindingData.active || typeof send !== 'function' || typeof curMoveCooldown === 'undefined') {
  164. stopPathfinding();
  165. return;
  166. }
  167.  
  168. // Check game cooldown
  169. if (curMoveCooldown > MOVE_COOLDOWN_THRESHOLD) {
  170. return; // Wait for cooldown
  171. }
  172.  
  173. // Check if path is complete
  174. if (pathfindingData.pathIndex >= pathfindingData.currentPath.length - 1) {
  175. console.log("Path complete.");
  176. stopPathfinding();
  177. return;
  178. }
  179.  
  180. const currentStep = pathfindingData.currentPath[pathfindingData.pathIndex];
  181. const nextStep = pathfindingData.currentPath[pathfindingData.pathIndex + 1];
  182.  
  183. console.log(`Executing path step: [${currentStep[0]}, ${currentStep[1]}] -> [${nextStep[0]}, ${nextStep[1]}]`);
  184.  
  185. try {
  186. // Send the move for the *next* step
  187. const buf = new Uint16Array(4);
  188. buf[0] = currentStep[0]; // From
  189. buf[1] = currentStep[1];
  190. buf[2] = nextStep[0]; // To
  191. buf[3] = nextStep[1];
  192. send(buf);
  193.  
  194. // Advance path index (will be used in next interval check)
  195. pathfindingData.pathIndex++;
  196. // We assume the 'send' action triggers the game's cooldown mechanism.
  197. // Our check of `curMoveCooldown` at the start handles waiting.
  198.  
  199. } catch (e) {
  200. console.error("Error sending path step move:", e);
  201. stopPathfinding();
  202. }
  203. }
  204.  
  205. function startPathfinding(targetX, targetY) {
  206. stopPathfinding(); // Stop any previous path
  207.  
  208. if (typeof selectedSquareX === 'undefined' || typeof selectedSquareY === 'undefined') {
  209. console.log("Pathfinding: No piece selected.");
  210. return;
  211. }
  212. if (selectedSquareX === targetX && selectedSquareY === targetY) {
  213. console.log("Pathfinding: Target is the same as start.");
  214. return;
  215. }
  216. const pieceType = board[selectedSquareX]?.[selectedSquareY];
  217. if (!pieceType) {
  218. console.error("Pathfinding: Cannot determine selected piece type.");
  219. return;
  220. }
  221.  
  222.  
  223. const path = findPathBFS(selectedSquareX, selectedSquareY, targetX, targetY, pieceType);
  224.  
  225. if (path && path.length > 1) { // Need at least start and one step
  226. pathfindingData = {
  227. active: true,
  228. pieceType: pieceType,
  229. startX: selectedSquareX,
  230. startY: selectedSquareY,
  231. targetX: targetX,
  232. targetY: targetY,
  233. currentPath: path,
  234. pathIndex: 0, // Start at the beginning of the path
  235. intervalId: setInterval(executePathStep, PATH_STEP_INTERVAL_MS)
  236. };
  237. console.log("Pathfinding started.");
  238. // Deselect piece visually maybe? Or keep selected? User choice.
  239. // selectedSquareX = selectedSquareY = undefined; // Optional deselect
  240. } else {
  241. console.log("Pathfinding: No valid path found or path too short.");
  242. // Optionally provide user feedback (e.g., flash screen red)
  243. }
  244. }
  245.  
  246. function stopPathfinding() {
  247. if (pathfindingData.intervalId) {
  248. clearInterval(pathfindingData.intervalId);
  249. }
  250. pathfindingData = { active: false, intervalId: null, currentPath: [], pathIndex: 0 }; // Reset state
  251. console.log("Pathfinding stopped.");
  252. }
  253.  
  254. // --- Drawing ---
  255.  
  256. function drawAttackedSquares() {
  257. if (!visualizeAttacksEnabled || typeof ctx === 'undefined' || typeof camera === 'undefined') return;
  258.  
  259. // Recalculate attacks (can be optimized)
  260. attackedSquares = calculateAllEnemyAttacks();
  261.  
  262. if (attackedSquares.size === 0) return;
  263.  
  264. const originalTransform = ctx.getTransform(); // Save original transform
  265. ctx.translate(canvas.width / 2, canvas.height / 2);
  266. ctx.scale(camera.scale, camera.scale);
  267. ctx.translate(camera.x, camera.y);
  268.  
  269. ctx.fillStyle = ATTACKED_SQUARE_COLOR;
  270.  
  271. // Get visible bounds (using canvasPos - slightly adapted)
  272. const topLeftView = canvasPos ? canvasPos({ x: 0, y: 0 }) : { x: -camera.x * camera.scale, y: -camera.y * camera.scale }; // Fallback if canvasPos missing
  273. const bottomRightView = canvasPos ? canvasPos({ x: innerWidth, y: innerHeight }) : { x: (innerWidth / camera.scale) - camera.x, y: (innerHeight / camera.scale) - camera.y }; // Fallback
  274.  
  275. const startCol = Math.max(0, Math.floor(topLeftView.x / squareSize) - 1);
  276. const endCol = Math.min(boardW, Math.ceil(bottomRightView.x / squareSize) + 1);
  277. const startRow = Math.max(0, Math.floor(topLeftView.y / squareSize) - 1);
  278. const endRow = Math.min(boardH, Math.ceil(bottomRightView.y / squareSize) + 1);
  279.  
  280.  
  281. attackedSquares.forEach(key => {
  282. const [x, y] = key.split(',').map(Number);
  283. // Only draw if within rough view bounds
  284. if (x >= startCol && x < endCol && y >= startRow && y < endRow) {
  285. ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
  286. }
  287. });
  288.  
  289. ctx.setTransform(originalTransform); // Restore original transform
  290. }
  291.  
  292. function drawCurrentPath() {
  293. if (!pathfindingData.active || pathfindingData.currentPath.length === 0 || typeof ctx === 'undefined' || typeof camera === 'undefined') return;
  294.  
  295. const originalTransform = ctx.getTransform();
  296. ctx.translate(canvas.width / 2, canvas.height / 2);
  297. ctx.scale(camera.scale, camera.scale);
  298. ctx.translate(camera.x, camera.y);
  299.  
  300. ctx.fillStyle = PATH_COLOR;
  301. ctx.strokeStyle = 'rgba(0, 0, 150, 0.5)';
  302. ctx.lineWidth = 3 / camera.scale; // Make line width scale invariant
  303.  
  304. // Draw path lines/squares
  305. for (let i = pathfindingData.pathIndex; i < pathfindingData.currentPath.length; i++) {
  306. const [x, y] = pathfindingData.currentPath[i];
  307. // Draw square highlight
  308. ctx.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
  309.  
  310. // Draw line to next segment (optional)
  311. if (i < pathfindingData.currentPath.length - 1) {
  312. const [nextX, nextY] = pathfindingData.currentPath[i+1];
  313. ctx.beginPath();
  314. ctx.moveTo(x * squareSize + squareSize / 2, y * squareSize + squareSize / 2);
  315. ctx.lineTo(nextX * squareSize + squareSize / 2, nextY * squareSize + squareSize / 2);
  316. ctx.stroke();
  317. }
  318. }
  319. ctx.setTransform(originalTransform);
  320. }
  321.  
  322.  
  323. // --- Integration with Game Loop & Events ---
  324.  
  325. // Monkey-patch the render function (use MutationObserver if render is complex/obfuscated)
  326. if (typeof render === 'function') {
  327. const originalRender = render;
  328. window.render = function(...args) {
  329. originalRender.apply(this, args); // Call original render first
  330. // Add our drawing functions
  331. try {
  332. drawAttackedSquares();
  333. drawCurrentPath();
  334. } catch (e) {
  335. console.error("Error in visualization drawing:", e);
  336. // Stop visuals if they error out?
  337. visualizeAttacksEnabled = false;
  338. stopPathfinding();
  339. }
  340. }
  341. console.log("Visualization hooks added to render function.");
  342. } else {
  343. console.error("Could not find global 'render' function to hook into.");
  344. }
  345.  
  346. // Monkey-patch mousedown (or add event listener if preferred)
  347. if (typeof onmousedown === 'function') {
  348. const originalMouseDown = onmousedown;
  349. window.onmousedown = function(e) {
  350. // Stop any ongoing pathfinding if user clicks anywhere
  351. if (pathfindingData.active) {
  352. console.log("User click interrupted pathfinding.");
  353. stopPathfinding();
  354. // Let the original handler decide if a new selection/move happens
  355. }
  356.  
  357. // Calculate potential target square BEFORE calling original handler
  358. let targetX, targetY;
  359. let mousePosDown; // Store mouse pos for pathfinding check
  360. try {
  361. const t = ctx.getTransform(); // Need context for transform
  362. ctx.translate(canvas.width/2, canvas.height/2);
  363. ctx.scale(camera.scale, camera.scale);
  364. ctx.translate(camera.x, camera.y);
  365. mousePosDown = canvasPos({ x: e.clientX, y: e.clientY }); // Use clientX/Y
  366. ctx.setTransform(t); // Restore immediately
  367. targetX = Math.floor(mousePosDown.x / squareSize);
  368. targetY = Math.floor(mousePosDown.y / squareSize);
  369. } catch (err) {
  370. // console.error("Could not calculate target square on mousedown:", err);
  371. originalMouseDown.call(this, e); // Still call original
  372. return;
  373. }
  374.  
  375. // --- Pathfinding Check ---
  376. let isImmediateLegalMove = false;
  377. if (typeof selectedSquareX !== 'undefined' && typeof selectedSquareY !== 'undefined' && typeof legalMoves !== 'undefined' && Array.isArray(legalMoves)) {
  378. for (let i = 0; i < legalMoves.length; i++) {
  379. if (legalMoves[i][0] === targetX && legalMoves[i][1] === targetY) {
  380. isImmediateLegalMove = true;
  381. break;
  382. }
  383. }
  384. }
  385.  
  386. // Call the original handler *first* to handle selection/deselection/immediate moves
  387. originalMouseDown.call(this, e);
  388.  
  389. // --- Initiate Pathfinding AFTER original handler ---
  390. // Check if:
  391. // 1. A piece *is still* selected (original handler didn't deselect or move successfully).
  392. // 2. The click was NOT an immediate legal move for the originally selected piece.
  393. // 3. The click is within board bounds.
  394. // 4. Pathfinding isn't already active (should have been stopped above, but double check).
  395. if (typeof selectedSquareX !== 'undefined' && typeof selectedSquareY !== 'undefined' &&
  396. !isImmediateLegalMove &&
  397. targetX >= 0 && targetX < boardW && targetY >= 0 && targetY < boardH &&
  398. !pathfindingData.active)
  399. {
  400. // Check cooldown just before starting pathfinding
  401. if (curMoveCooldown <= MOVE_COOLDOWN_THRESHOLD) {
  402. console.log("Initiating pathfinding to", targetX, targetY);
  403. startPathfinding(targetX, targetY);
  404. } else {
  405. console.log("Cannot start pathfinding, move cooldown active.");
  406. }
  407. }
  408. }
  409. console.log("Pathfinding hook added to onmousedown function.");
  410. } else {
  411. console.error("Could not find global 'onmousedown' function to hook into.");
  412. }
  413.  
  414.  
  415. // --- Global Control Functions ---
  416. window.toggleAttackVisualization = function(enable = !visualizeAttacksEnabled) {
  417. visualizeAttacksEnabled = enable;
  418. console.log("Attack Visualization " + (visualizeAttacksEnabled ? "Enabled" : "Disabled"));
  419. if (!visualizeAttacksEnabled) {
  420. attackedSquares.clear(); // Clear stored squares when disabled
  421. // Request a redraw if possible (difficult without direct access to game loop flags)
  422. }
  423. }
  424.  
  425. window.cancelCurrentPath = function() {
  426. stopPathfinding();
  427. }
  428.  
  429. console.log("Pathfinding and Attack Visualization script loaded.");
  430. console.log("Use toggleAttackVisualization() to turn red squares on/off.");
  431. console.log("Click an invalid square after selecting a piece to pathfind.");
  432. console.log("Use cancelCurrentPath() to stop active pathfinding.");
  433. toggleAttackVisualization()
  434.  
  435. })(); // End IIFE