您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows the route your ship will fly.
当前为
// ==UserScript== // @name Pardus navigation highlight a.k.a. Paze // @namespace leaumar // @version 1 // @description Shows the route your ship will fly. // @author [email protected] // @match https://*.pardus.at/main.php // @icon https://icons.duckduckgo.com/ip2/pardus.at.ico // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/ramda.min.js // @grant unsafeWindow // @license MPL-2.0 // ==/UserScript== // convention: nav grid is width * height tiles with origin 0,0 top-left, just like pixels in css // a tile is a <td> // a point is an {x, y} coordinate const classes = { map: { // set by the game impassable: "navImpassable", // i.e. solid energy npc: "navNpc", nodata: "navNoData", tile: "paze-tile", // every tile in the grid passable: "paze-passable", // all clickable tiles }, route: { reachable: "paze-reachable", // a route can be flown to this destination unreachable: "paze-unreachable", // this destination has no route to it step: "paze-step", // ship will fly through here deadEnd: "paze-dead-end", // route calculation gets stuck here, or runs into a monster }, }; const style = (() => { const style = document.createElement("style"); style.textContent = ` #navareatransition img[class^=nf] { /* for some reason these tiles have position:relative but they aren't offset this obscures the outline on the parent td unsetting the position does not break the flying animation */ position: unset !important; } .${classes.map.tile}.${classes.map.impassable} img { cursor: not-allowed !important; } .${classes.map.tile}.${classes.map.passable} { /* outline doesn't affect box sizing nor image scaling, unlike border */ outline: 1px dotted #fff3; outline-offset: -1px; } .${classes.map.tile}.${classes.route.unreachable} img { cursor: no-drop !important; } .${classes.map.tile}.${classes.route.step} { outline-color: yellow; } .${classes.map.tile}.${classes.route.reachable} { outline: 1px solid green; } .${classes.map.tile}.${classes.route.deadEnd} { outline: 1px solid red; } `; return { attach: () => document.head.appendChild(style), }; })(); function makeMap(navArea) { const height = navArea.getElementsByTagName("tr").length; const tiles = [...navArea.getElementsByTagName("td")]; const width = tiles.length / height; const size = tiles[0].getBoundingClientRect().width; console.info( `found a ${width}x${height} ${size}px nav grid with ${tiles.length} tiles` ); function findCoordinateOfTile(tile) { const index = R.findIndex((it) => it.id === tile.id, tiles); return index === -1 ? null : { x: index % width, y: Math.floor(index / width), }; } function isInBounds({ x, y }) { return x < width && y < height; } function findTileByCoordinate(point) { return isInBounds(point) ? tiles[point.y * width + point.x] : null; } function acceptsTraffic(tile) { return ( !tile.classList.contains(classes.map.impassable) && !tile.classList.contains(classes.map.nodata) ); } function isMonster(tile) { return tile.classList.contains(classes.map.npc); } function showGrid() { tiles.forEach((tile) => { tile.classList.add(classes.map.tile); if (acceptsTraffic(tile)) { tile.classList.add(classes.map.passable); } }); } function getCenterPoint() { return { x: Math.floor(width / 2), y: Math.floor(height / 2), }; } const getTileType = (() => { // url(//static.pardus.at/img/stdhq/96/backgrounds/space3.png) const bgRegex = /\/backgrounds\/([a-z_]+)(\d+)\.png/; return (tile) => { const tileBg = tile.style.backgroundImage; const imageUrl = tileBg == null || tileBg === "" ? tile.getElementsByTagName("img")[0].src : tileBg; const [match, name, number] = bgRegex.exec(imageUrl); // TODO is this correct? return name === "viral_cloud" ? parseInt(number) < 23 ? "space" : "energy" : name; }; })(); return { height, tiles, width, size, findCoordinateOfTile, findTileByCoordinate, showGrid, acceptsTraffic, getCenterPoint, getTileType, isMonster, }; } const navigation = (() => { // diagonal first, then straight line // obstacle avoidance: try left+right or up+down neighboring tiles // vertical movement first if diagonally sidestepping, down/right first if orthogonally function* pardusWayfind(destinationPoint, map) { let position = map.getCenterPoint(); yield position; while ( position.x !== destinationPoint.x || position.y !== destinationPoint.y ) { if (map.isMonster(map.findTileByCoordinate(position))) { yield "stuck"; return; } const delta = { x: destinationPoint.x - position.x, y: destinationPoint.y - position.y, }; // if delta is 0, sign is 0 so no move const nextMovePoint = { x: position.x + 1 * Math.sign(delta.x), y: position.y + 1 * Math.sign(delta.y), }; const nextMoveTile = map.findTileByCoordinate(nextMovePoint); if (nextMoveTile != null && map.acceptsTraffic(nextMoveTile)) { yield (position = nextMovePoint); continue; } const isHorizontal = delta.x !== 0; const isVertical = delta.y !== 0; const sidestepPoints = (() => { if (isHorizontal && isVertical) { return [ { x: position.x, y: nextMovePoint.y, }, { x: nextMovePoint.x, y: position.y, }, ]; } if (isHorizontal) { return [ { x: nextMovePoint.x, y: position.y + 1, }, { x: nextMovePoint.x, y: position.y - 1, }, ]; } if (isVertical) { return [ { x: position.x + 1, y: nextMovePoint.y, }, { x: position.x - 1, y: nextMovePoint.y, }, ]; } })(); const sidestepPoint = sidestepPoints.find((point) => { const sidestepTile = map.findTileByCoordinate(point); return sidestepTile != null && map.acceptsTraffic(sidestepTile); }); if (sidestepPoint == null) { // autopilot failure yield "stuck"; return; } yield (position = sidestepPoint); } } function wayfind(destinationPoint, map) { const points = [...pardusWayfind(destinationPoint, map)]; const stuck = points.at(-1) === "stuck"; return { points: stuck ? points.slice(0, -1) : points, stuck, }; } return { wayfind, }; })(); function makeCanvas(map) { const canvas = document.createElement("canvas"); Object.assign(canvas, { width: map.size * map.width, height: map.size * map.height, }); Object.assign(canvas.style, { position: "absolute", top: "0", left: "0", pointerEvents: "none", }); const ctx = canvas.getContext("2d"); function drawDashes(points, { gap, width, length, color }) { ctx.setLineDash([length, gap]); ctx.lineWidth = width; ctx.strokeStyle = color; ctx.beginPath(); ctx.moveTo((map.width * map.size) / 2, (map.height * map.size) / 2); points.forEach(({ x, y }) => { ctx.lineTo(x * map.size + map.size / 2, y * map.size + map.size / 2); }); ctx.stroke(); } function drawCosts(points, movementCost) { ctx.fillStyle = "white"; ctx.strokeStyle = "black"; ctx.lineWidth = 2; ctx.font = "12px sans-serif"; points.slice(1).forEach((point, i) => { const previousTile = map.findTileByCoordinate(points[i]); const apCost = `${movementCost.getCostOfMovingFrom(previousTile)}`; const x = point.x * map.size + 10; const y = point.y * map.size + 16; ctx.strokeText(apCost, x, y); ctx.fillText(apCost, x, y); }); } function drawRectangle(point, color) { ctx.fillStyle = color; ctx.fillRect(point.x * map.size, point.y * map.size, map.size, map.size); } return { appendTo: (positionedParent) => { positionedParent.appendChild(canvas); }, clear: () => { ctx.clearRect(0, 0, canvas.width, canvas.height); }, plot: (way, destinationPoint, movementCost) => { drawRectangle(destinationPoint, way.stuck ? "#f001" : "#fff1"); // they start at the same pixel so the outline is actually misaligned by 1 border width drawDashes(way.points, { gap: 6, width: 4, length: 9, color: "black" }); drawDashes(way.points, { gap: 7, width: 2, length: 8, color: way.stuck ? "red" : "green", }); drawCosts(way.points, movementCost); }, }; } function makeRoute(map) { const routeClassNames = R.values(classes.route); function erase() { map.tiles.forEach((tile) => { tile.classList.remove(...routeClassNames); }); } function plot(way, destinationTile) { way.points.slice(1).forEach((stepPoint) => { const stepTile = map.findTileByCoordinate(stepPoint); stepTile.classList.add(classes.route.step); }); destinationTile.classList.add( way.stuck ? classes.route.unreachable : classes.route.reachable ); if (way.stuck) { const stuckTile = map.findTileByCoordinate(way.points.at(-1)); stuckTile.classList.add(classes.route.deadEnd); } } return { erase, plot, }; } const makeMovementCost = (() => { const baseCost = { asteroids: 24, energy: 19, exotic_matter: 35, nebula: 15, space: 10, }; let calculatedMovementCost = null; return (map) => { const effectiveCost = calculatedMovementCost ?? (calculatedMovementCost = (() => { const currentCost = parseInt( document.getElementById("tdStatusMove").textContent.trim() ); const currentType = map.getTileType( map.findTileByCoordinate(map.getCenterPoint()) ); const efficiency = baseCost[currentType] - currentCost; return R.map((cost) => cost - efficiency, baseCost); })()); function getCostOfMovingFrom(tile) { return effectiveCost[map.getTileType(tile)]; } return { getCostOfMovingFrom, }; }; })(); function gps(navArea) { const map = makeMap(navArea); map.showGrid(); const canvas = makeCanvas(map); canvas.appendTo(navArea.parentNode); const route = makeRoute(map); const movementCost = makeMovementCost(map); function clearRoute() { route.erase(); canvas.clear(); } function showRoute(event) { if (event.target.tagName === "TD") { const destinationTile = event.target; clearRoute(); if (map.acceptsTraffic(destinationTile)) { const destinationPoint = map.findCoordinateOfTile(destinationTile); if (destinationPoint == null) { throw new Error("unexpected tile", destinationTile, destinationPoint); } if (R.equals(destinationPoint, map.getCenterPoint())) { return; } const way = navigation.wayfind(destinationPoint, map); // TODO hint at more efficient route canvas.plot(way, destinationPoint, movementCost); route.plot(way, destinationTile); } } } // capture phase is required: https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseenter_event#behavior_of_mouseenter_events navArea.addEventListener("mouseenter", showRoute, true); navArea.addEventListener("mouseleave", clearRoute); return function cleanUp() { clearRoute(); navArea.removeEventListener("mouseenter", showRoute, true); navArea.removeEventListener("mouseleave", clearRoute); }; } style.attach(); let cleanUp = gps(document.getElementById("navarea")); unsafeWindow.addUserFunction(() => { cleanUp(); // TODO route to tile under mouse after flight should be shown without needing mouseenter event cleanUp = gps(document.getElementById("navareatransition")); });