您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Replace teleport-like jumps on the neal.fun/internet-roadtrip minimap with portal markers
// ==UserScript== // @name Internet Roadtrip - Minimap Teleport Markers // @description Replace teleport-like jumps on the neal.fun/internet-roadtrip minimap with portal markers // @namespace me.netux.site/user-scripts/internet-roadtrip/minimap-teleport-markers // @version 0.2.0 // @author netux // @license MIT // @match https://neal.fun/* // @icon https://cloudy.netux.site/neal_internet_roadtrip/Minimap%20Teleport%20Markers%20logo.png // @grant GM_addStyle // @grant GM.setValue // @grant GM.getValue // @run-at document-start // @require https://cdn.jsdelivr.net/npm/[email protected] // ==/UserScript== /* globals IRF */ (async () => { const MOD_NAME = GM.info.script.name.replace('Internet Roadtrip -', '').trim(); const MOD_PREFIX = 'mtpm-'; const PORTAL_ICONS_SOURCE_NAME = `${MOD_PREFIX}portal-icons`; const PORTAL_ICONS_LAYER_NAME = `${MOD_PREFIX}portal-icons`; // const TELEPORT_LINES_SOURCE_NAME = `${MOD_PREFIX}teleport-lines`; const TELEPORT_LINES_LAYER_NAME = `${MOD_PREFIX}teleport-lines`; //const PORTAL_ENTRANCE_IMAGE_URL = 'https://cloudy.netux.site/neal_internet_roadtrip/teleport-markers/portal-entrance.svg'; const PORTAL_ENTRANCE_IMAGE_URL = 'https://cloudy.netux.site/neal_internet_roadtrip/teleport-markers/portal-entrance.png'; const PORTAL_ENTRANCE_IMAGE_NAME = 'portal-entrance'; const PORTAL_ENTRANCE_COLOR = 'blue'; //const PORTAL_EXIT_IMAGE_URL = 'https://cloudy.netux.site/neal_internet_roadtrip/teleport-markers/portal-exit.svg'; const PORTAL_EXIT_IMAGE_URL = 'https://cloudy.netux.site/neal_internet_roadtrip/teleport-markers/portal-exit.png'; const PORTAL_EXIT_IMAGE_NAME = 'portal-exit'; const PORTAL_EXIT_COLOR = 'orange'; const TELEPORT_LINES_DASH_SEGMENT_1_LENGTH = 10; const TELEPORT_LINES_DASH_SEGMENT_2_LENGTH = 10; const cssClass = (... names) => names.map((name) => MOD_PREFIX + name).join(' '); const cssProp = (name) => `--${MOD_PREFIX}${name}`; const RAD_TO_DEG = 180 / Math.PI; const DEG_TO_RAD = 1 / RAD_TO_DEG; const util = { // Yoinked from Chris. // Thanks Chris! haversineDistance(lat1, lon1, lat2, lon2) { const R = 6371e3; // Earth radius in meters const phi1 = lat1 * DEG_TO_RAD; const phi2 = lat2 * DEG_TO_RAD; const deltaPhi = (lat2 - lat1) * DEG_TO_RAD; const deltaLambda = (lon2 - lon1) * DEG_TO_RAD; const a = Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) * Math.sin(deltaLambda / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; }, // Yoinked from Jakub. Thanks Jakub! // Helper functions for working with tiles // Based on https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames gcsToTileCoordinates(x, y, z) { const n = 2**z const lon_deg = x / n * 360.0 - 180.0 const lat_rad = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n))) const lat_deg = lat_rad * 180.0 / Math.PI return [lat_deg, lon_deg] }, tileToGcsCoordinates([lat, lng], z) { const n = 2 ** z; const x = n * ((lng + 180) / 360); const latRad = lat / 180 * Math.PI; const y = n * (1 - (Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI)) / 2; return [Math.floor(x), Math.floor(y), Math.floor(z)]; } }; // Thank you Perplexity.ai! class Maplibre2dCanvasLayer { type = 'custom'; renderingMode = '2d'; variableLocations = {}; alpha = 1; constructor({ id, canvas, rerender }) { this.id = id; this.canvas = canvas; this.rerenderCanvas = rerender; } onAdd(map, gl) { this.gl = gl; this.map = map; const vertexSource = ` attribute vec2 a_pos; varying vec2 v_texcoord; void main() { v_texcoord = vec2((a_pos.x + 1.0) / 2.0, 1.0 - (a_pos.y + 1.0) / 2.0); gl_Position = vec4(a_pos, 0.0, 1.0); } `; const fragmentSource = ` precision mediump float; uniform sampler2D u_texture; uniform float u_alpha; varying vec2 v_texcoord; void main() { vec4 tex_color = texture2D(u_texture, v_texcoord); tex_color.a = tex_color.a * u_alpha; gl_FragColor = tex_color; } `; this.vertexShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(this.vertexShader, vertexSource); gl.compileShader(this.vertexShader); this.fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(this.fragmentShader, fragmentSource); gl.compileShader(this.fragmentShader); this.program = gl.createProgram(); gl.attachShader(this.program, this.vertexShader); gl.attachShader(this.program, this.fragmentShader); gl.linkProgram(this.program); this.buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([ // Fullscreen quad: (-1, -1) to (+1, +1) -1, -1, +1, -1, -1, +1, +1, +1, ]), gl.STATIC_DRAW ); this.texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); this.variableLocations = { a_pos: gl.getAttribLocation(this.program, "a_pos"), u_alpha: gl.getUniformLocation(this.program, "u_alpha"), u_texture: gl.getUniformLocation(this.program, "u_texture"), } } onRemove() { this.gl.deleteShader(this.vertexShader); this.gl.deleteShader(this.fragmentShader); this.gl.deleteBuffer(this.buffer); this.gl.deleteProgram(this.program); this.map = null; } render(gl, _args) { const mapContainerEl = this.map.getContainer(); if (this.canvas.width !== mapContainerEl.clientWidth) { this.canvas.width = mapContainerEl.clientWidth; } if (this.canvas.height !== mapContainerEl.clientHeight) { this.canvas.height = mapContainerEl.clientHeight; } this.rerenderCanvas(); gl.useProgram(this.program); gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); gl.enableVertexAttribArray(this.variableLocations.a_pos); gl.vertexAttribPointer(this.variableLocations.a_pos, 2, gl.FLOAT, false, 0, 0); gl.uniform1f(this.variableLocations.u_alpha, this.alpha); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.canvas ); gl.uniform1i(this.variableLocations.u_texture, 0); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } } const settings = { teleportMetersThreshold: 5_000, portalIcons: { layer: { show: true, opacity: 1, }, }, teleportLines: { layer: { show: true, opacity: 0.5, dashed: false }, } }; for (const key in settings) { settings[key] = await GM.getValue(key, settings[key]); } async function saveSettings() { for (const key in settings) { await GM.setValue(key, settings[key]); } } const mapVDOM = await IRF.vdom.map; const map = mapVDOM.data.map; let isMapLoaded = map.loaded(); const waitForMapToLoad = new Promise((resolve) => { map.once('load', () => { isMapLoaded = true; resolve(); }); }); function updateUiFromSettings() { map.setLayoutProperty(PORTAL_ICONS_LAYER_NAME, 'visibility', settings.portalIcons.layer.show ? 'visible' : 'none'); map.setPaintProperty(PORTAL_ICONS_LAYER_NAME, 'icon-opacity', settings.portalIcons.layer.opacity); map.setLayoutProperty(TELEPORT_LINES_LAYER_NAME, 'visibility', settings.teleportLines.layer.show ? 'visible' : 'none'); map.getLayer(TELEPORT_LINES_LAYER_NAME).implementation.alpha = settings.teleportLines.layer.opacity; } const portalIconsSourceGeojsonData = { type: 'FeatureCollection', features: [] }; const teleportLinesSourceGeojsonData = { type: 'Feature', geometry: { type: 'MultiLineString', coordinates: [] } }; const teleportLinesSourceCanvasCtx = document.createElement('canvas').getContext('2d'); function drawTeleportLinesIntoSourceCanvas() { function clampCoords(x1, y1, x2, y2, offset = 0) { const { width, height } = teleportLinesSourceCanvasCtx.canvas; const m = (y2 - y1) / (x2 - x1); const b = y1 - m * x1; function clampX() { if (x1 < 0) { x1 = offset; y1 = x1 * m + b; return true; } else if (x1 > width) { x1 = width - offset; y1 = x1 * m + b; return true; } return false; } function clampY() { if (y1 < 0) { y1 = offset; x1 = (y1 - b) / m; return true; } else if (y1 > height) { y1 = height - offset; x1 = (y1 - b) / m; return true; } return false; } if (clampX()) { clampY(); } else if (clampY()) { clampX(); } return { x: x1, y: y1 }; } const { width, height } = teleportLinesSourceCanvasCtx.canvas; teleportLinesSourceCanvasCtx.reset(); teleportLinesSourceCanvasCtx.clearRect( 0, 0, width, height ); // const DEBUG__fillGradient = teleportLinesSourceCanvasCtx.createConicGradient(0, width / 2, height / 2); // DEBUG__fillGradient.addColorStop(0, 'rgba(100% 0 0 / 50%)'); // DEBUG__fillGradient.addColorStop(0.25, 'rgba(100% 50% 0 / 50%)'); // DEBUG__fillGradient.addColorStop(0.5, 'rgba(100% 100% 0 / 50%)'); // DEBUG__fillGradient.addColorStop(0.75, 'rgba(0 100% 0 / 50%)'); // DEBUG__fillGradient.addColorStop(1, 'rgba(0 0 100% / 50%)'); // teleportLinesSourceCanvasCtx.fillStyle = DEBUG__fillGradient; // teleportLinesSourceCanvasCtx.fillRect(0, 0, width, height); for (const [fromLngLat, toLngLat] of teleportLinesSourceGeojsonData.geometry.coordinates) { const fromPx = map.project(fromLngLat); const toPx = map.project(toLngLat); const gradient = teleportLinesSourceCanvasCtx.createLinearGradient( fromPx.x, fromPx.y, toPx.x, toPx.y ); gradient.addColorStop(0, PORTAL_ENTRANCE_COLOR); gradient.addColorStop(1, PORTAL_EXIT_COLOR); const dashSegment1Length = TELEPORT_LINES_DASH_SEGMENT_1_LENGTH; const dashSegment2Length = TELEPORT_LINES_DASH_SEGMENT_2_LENGTH; const clampOffset = -1 * (dashSegment1Length + dashSegment2Length) / 2; const fromPxClamped = clampCoords(fromPx.x, fromPx.y, toPx.x, toPx.y, clampOffset); const toPxClamped = clampCoords(toPx.x, toPx.y, fromPx.x, fromPx.y, clampOffset); teleportLinesSourceCanvasCtx.strokeStyle = gradient; teleportLinesSourceCanvasCtx.lineWidth = 3; teleportLinesSourceCanvasCtx.beginPath(); if (settings.teleportLines.layer.dashed) { teleportLinesSourceCanvasCtx.setLineDash([dashSegment1Length, dashSegment2Length]); teleportLinesSourceCanvasCtx.dashOffset = Math.abs(Math.sqrt(fromPx.x ** 2 + fromPx.y ** 2) - Math.sqrt(fromPxClamped.x ** 2 + fromPxClamped.y ** 2)); } teleportLinesSourceCanvasCtx.moveTo( fromPxClamped.x, fromPxClamped.y ); teleportLinesSourceCanvasCtx.lineTo( toPxClamped.x, toPxClamped.y ); teleportLinesSourceCanvasCtx.stroke(); } } waitForMapToLoad.then(() => { map.loadImage(PORTAL_ENTRANCE_IMAGE_URL) .then((image) => map.addImage(PORTAL_ENTRANCE_IMAGE_NAME, image.data)) .catch((error) => { console.error(`[${MOD_NAME}] Could not load portal entrance image:`, error) }); map.loadImage(PORTAL_EXIT_IMAGE_URL) .then((image) => map.addImage(PORTAL_EXIT_IMAGE_NAME, image.data)) .catch((error) => { console.error(`[${MOD_NAME}] Could not load portal exit image:`, error) }); map.addSource(PORTAL_ICONS_SOURCE_NAME, { type: 'geojson', data: portalIconsSourceGeojsonData }); map.addLayer({ id: PORTAL_ICONS_LAYER_NAME, source: PORTAL_ICONS_SOURCE_NAME, type: 'symbol', minzoom: 5, layout: { 'icon-image': ['get', 'icon'], 'icon-rotate': ['get', 'angle'], 'icon-size': [ 'interpolate', ['linear'], ['zoom'], /* map zoom, icon-size */ 5, 0.02, 18, 0.1, ], 'icon-allow-overlap': true } }); map.addLayer(new Maplibre2dCanvasLayer({ id: TELEPORT_LINES_LAYER_NAME, canvas: teleportLinesSourceCanvasCtx.canvas, rerender: drawTeleportLinesIntoSourceCanvas })); updateUiFromSettings(); }); //map.on('zoom', () => console.debug(map.getZoom())); const isTeleport = ([prevLat, prevLng], [thisLat, thisLng]) => util.haversineDistance(prevLat, prevLng, thisLat, thisLng) > settings.teleportMetersThreshold; function addTeleportToMap([prevLat, prevLng], [thisLat, thisLng]) { // Add teleport line to its own layer teleportLinesSourceGeojsonData.geometry.coordinates.push([ [prevLng, prevLat], [thisLng, thisLat] ]); // Create portal icons const angle = Math.atan2(thisLat - prevLat, thisLng - prevLng) * RAD_TO_DEG; portalIconsSourceGeojsonData.features.push({ type: 'Feature', properties: { 'icon': PORTAL_ENTRANCE_IMAGE_NAME, 'angle': angle, 'portal-type': 'entrance' }, geometry: { type: 'Point', coordinates: [prevLng, prevLat] } }); portalIconsSourceGeojsonData.features.push({ type: 'Feature', properties: { 'icon': PORTAL_EXIT_IMAGE_NAME, 'angle': -angle % 360, 'portal-type': 'exit' }, geometry: { type: 'Point', coordinates: [thisLng, thisLat] } }); } function updateModLayerSources() { map.getSource(PORTAL_ICONS_SOURCE_NAME).setData(portalIconsSourceGeojsonData); // map.getSource(TELEPORT_LINES_SOURCE_NAME).setData(teleportLinesSourceGeojsonData); } let lastOldRouteCoord = null; async function resolveTeleportsInOldRoute() { const oldRouteLayer = map.getLayer('old-route-layer'); oldRouteLayer.setPaintProperty('line-gradient', 'red'); map.moveLayer('old-route-layer', /* before: */ PORTAL_ICONS_LAYER_NAME); const oldRouteSource = map.getSource('old-route'); const oldRouteData = await oldRouteSource.getData(); const customOldRouteGeojsonData = { type: 'Feature', geometry: { type: 'MultiLineString', coordinates: [] } }; function newLineStringArray() { const lineStringArray = []; customOldRouteGeojsonData.geometry.coordinates.push(lineStringArray); return lineStringArray; }; let currentLineStringArray = newLineStringArray(); for (let i = 0; i < oldRouteData.geometry.coordinates.length; i++) { const currentCoords = oldRouteData.geometry.coordinates[i]; currentLineStringArray.push(currentCoords); if (i >= 1) { const [prevLng, prevLat] = oldRouteData.geometry.coordinates[i - 1]; const [thisLng, thisLat] = currentCoords; if (isTeleport([prevLat, prevLng], [thisLat, thisLng])) { // Remove teleport line from route, and // setup new line for the rest of the route. currentLineStringArray.pop(); currentLineStringArray = newLineStringArray(); currentLineStringArray.push(currentCoords); addTeleportToMap([prevLat, prevLng], [thisLat, thisLng]); } } lastOldRouteCoord = currentCoords; } oldRouteSource.setData(customOldRouteGeojsonData); updateModLayerSources(); } async function patchRouteSourceAndLayer() { const routeSource = map.getSource('route'); const customRouteGeojsonData = { type: 'Feature', geometry: { type: 'MultiLineString', coordinates: [ [] ] } }; const ogSourceSetData = routeSource.setData; routeSource.setData = new Proxy(routeSource.setData, { apply(ogSetData, thisArg, args) { const [incomingData, ... restArgs] = args; const incomingCoordinates = incomingData.geometry.coordinates; const prevCoords = incomingCoordinates.length > 1 ? incomingCoordinates[incomingCoordinates.length - 2] : lastOldRouteCoord; const currentCoords = incomingCoordinates[incomingCoordinates.length - 1]; const [prevLng, prevLat] = prevCoords; const [thisLng, thisLat] = currentCoords; if (isTeleport([prevLat, prevLng], [thisLat, thisLng])) { const newCoordinatesArray = []; customRouteGeojsonData.geometry.coordinates.push(newCoordinatesArray); newCoordinatesArray.push([thisLng, thisLat]); addTeleportToMap([prevLat, prevLng], [thisLat, thisLng]); updateModLayerSources(); } else { const lastCoordinatesArray = customRouteGeojsonData.geometry.coordinates[customRouteGeojsonData.geometry.coordinates.length - 1]; lastCoordinatesArray.push([thisLng, thisLat]); } return ogSetData.apply(thisArg, [customRouteGeojsonData, ... restArgs]) } }) } mapVDOM.state.getInitialData = new Proxy(mapVDOM.state.getInitialData, { apply(ogGetInitialData, thisArg, args) { const promise = ogGetInitialData.apply(thisArg, args); promise.then(() => { resolveTeleportsInOldRoute(); patchRouteSourceAndLayer(); }); return promise; } }); { const tab = IRF.ui.panel.createTabFor( { ... GM.info, script: { ... GM.info.script, name: MOD_NAME } }, { tabName: MOD_NAME, style: ` .${cssClass('tab-content')} { & *, *::before, *::after { box-sizing: border-box; } & .${cssClass('field-group')} { margin-left: calc(var(${cssProp('field-group-indent')}, 0) * 1rem); margin-block: 1rem; gap: 0.25rem; display: flex; align-items: center; justify-content: space-between; & label > small { color: lightgray; display: block; } & input:is(:not([type]), [type="text"], [type="number"]) { --padding-inline: 0.5rem; width: calc(100% - 2 * var(--padding-inline)); min-height: 1.5rem; margin: 0; padding-inline: var(--padding-inline); color: white; background: transparent; border: 1px solid #848e95; font-size: 100%; border-radius: 5rem; } & .${cssClass('field-group__label-container')}, & .${cssClass('field-group__input-container')} { width: 100%; display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; gap: 1ch; } & .${cssClass('field-group__input-container')} { justify-content: end; white-space: nowrap; } } } `, className: cssClass('tab-content') } ); function makeFieldGroup({ id, label, labelSubtext = null, indent = 0 }, renderInput) { const fieldGroupEl = document.createElement('div'); fieldGroupEl.className = cssClass('field-group'); if (indent > 0) { fieldGroupEl.style.setProperty(cssProp('field-group-indent'), indent); } const labelContainerEl = document.createElement('div'); labelContainerEl.className = cssClass('field-group__label-container'); fieldGroupEl.append(labelContainerEl); const labelEl = document.createElement('label'); labelEl.textContent = label; labelContainerEl.append(labelEl); if (labelSubtext != null) { const labelSubtextEl = document.createElement('small'); labelSubtextEl.textContent = labelSubtext; labelEl.append(labelSubtextEl); } const inputContainerEl = document.createElement('div'); inputContainerEl.className = cssClass('field-group__input-container'); fieldGroupEl.append(inputContainerEl); const renderInputOutput = renderInput({ id }); inputContainerEl.append(... (Array.isArray(renderInputOutput) ? renderInputOutput : [renderInputOutput])); return fieldGroupEl; } tab.container.append( makeFieldGroup( { id: `${MOD_PREFIX}teleport-meters-threshold`, label: 'Teleport Threshold', labelSubtext: 'A reload is required to update the minimap' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.type = 'number'; inputEl.style.width = '15ch'; inputEl.value = settings.teleportMetersThreshold; inputEl.addEventListener('change', async () => { let numberValue = Number.parseFloat(inputEl.value); if (isNaN(numberValue)) { return; } numberValue = Math.max(0, numberValue); settings.teleportMetersThreshold = numberValue; await saveSettings(); updateUiFromSettings(); }); return [ inputEl, document.createTextNode(' meters') ]; } ), makeFieldGroup( { id: `${MOD_PREFIX}portal-icons-show-layer`, label: 'Portal Icons' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.type = 'checkbox'; inputEl.className = IRF.ui.panel.styles.toggle; inputEl.checked = settings.portalIcons.layer.show; inputEl.addEventListener('change', async () => { settings.portalIcons.layer.show = inputEl.checked; await saveSettings(); updateUiFromSettings(); }); return inputEl; } ), makeFieldGroup( { id: `${MOD_PREFIX}portal-icons-layer-opacity`, indent: 1, label: 'Opacity' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.type = 'range'; inputEl.className = IRF.ui.panel.styles.slider; inputEl.min = 0; inputEl.max = 1; inputEl.step = 0.05; inputEl.value = settings.portalIcons.layer.opacity; inputEl.addEventListener('input', async () => { let numberValue = Number.parseFloat(inputEl.value); if (Number.isNaN(numberValue)) { return; } numberValue = Math.min(Math.max(parseFloat(inputEl.min), numberValue), parseFloat(inputEl.max)); settings.portalIcons.layer.opacity = numberValue; await saveSettings(); updateUiFromSettings(); }); return inputEl; } ), makeFieldGroup( { id: `${MOD_PREFIX}teleport-lines-show-layer`, label: 'Teleport Lines' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.type = 'checkbox'; inputEl.className = IRF.ui.panel.styles.toggle; inputEl.checked = settings.teleportLines.layer.show; inputEl.addEventListener('change', async () => { settings.teleportLines.layer.show = inputEl.checked; await saveSettings(); updateUiFromSettings(); }); return inputEl; } ), makeFieldGroup( { id: `${MOD_PREFIX}teleport-lines-layer-opacity`, indent: 1, label: 'Opacity' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.type = 'range'; inputEl.className = IRF.ui.panel.styles.slider; inputEl.min = 0; inputEl.max = 1; inputEl.step = 0.05; inputEl.value = settings.teleportLines.layer.opacity; inputEl.addEventListener('input', async () => { let numberValue = Number.parseFloat(inputEl.value); if (Number.isNaN(numberValue)) { return; } numberValue = Math.min(Math.max(parseFloat(inputEl.min), numberValue), parseFloat(inputEl.max)); settings.teleportLines.layer.opacity = numberValue; await saveSettings(); updateUiFromSettings(); }); return inputEl; } ), makeFieldGroup( { id: `${MOD_PREFIX}teleport-lines-layer-dashed`, indent: 1, label: 'Dashed' }, ({ id }) => { const inputEl = document.createElement('input'); inputEl.type = 'checkbox'; inputEl.className = IRF.ui.panel.styles.toggle; inputEl.checked = settings.teleportLines.layer.dashed; inputEl.addEventListener('input', async () => { settings.teleportLines.layer.dashed = inputEl.checked; await saveSettings(); updateUiFromSettings(); }); return inputEl; } ), ) } if (typeof unsafeWindow !== 'undefined') { unsafeWindow.mtpm = { get map() { return map }, get portalIconsLayer() { return map.getLayer(PORTAL_ICONS_LAYER_NAME); }, get teleportLinesLayer() { return map.getLayer(TELEPORT_LINES_LAYER_NAME); }, debug: { mockJumpToCoordsOnMap([lat, lng]) { mapVDOM.state.setMarkerPosition(lat, lng); } } }; } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址