Greasy Fork is available in English.
The 2026 Path Logger Upgrade. Now with duels support, customization, gradients, RDP smoothing, fixed bugs, and more.
当前为
// ==UserScript==
// @name GeoGuessr Path Logger Plus
// @namespace Odinman9847
// @version 1.0.1
// @description The 2026 Path Logger Upgrade. Now with duels support, customization, gradients, RDP smoothing, fixed bugs, and more.
// @author Odinman9847 (Original script by xsanda)
// @copyright 2026, Odinman9847; 2021, xsanda;
// @match https://www.geoguessr.com/*
// @require https://openuserjs.org/src/libs/xsanda/Run_code_as_client.js
// @require https://openuserjs.org/src/libs/xsanda/Google_Maps_Promise.js
// @run-at document-start
// @grant none
// @license MIT
// ==/UserScript==
// --- PART 1: IMMEDIATE EXECUTION (Network & UI) ---
(function() {
'use strict';
runAsClient(() => {
window.__GPL_GAME_ID = null;
window.__GPL_HAS_GUESSED = false;
const checkURL = (url) => {
if (typeof url !== 'string') return;
if (url.includes('/api/lobby/') && url.includes('/join')) {
const match = url.match(/\/api\/lobby\/([0-9a-f]{24})\/join/);
if (match && match[1]) {
window.__GPL_GAME_ID = match[1];
window.__GPL_HAS_GUESSED = false;
}
}
if (url.endsWith('/guess')) {
window.__GPL_HAS_GUESSED = true;
}
};
const originalFetch = window.fetch;
window.fetch = function(...args) {
if (args[0]) checkURL(args[0]);
return originalFetch.apply(this, args);
};
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
checkURL(url);
return originalOpen.apply(this, arguments);
};
});
const SETTINGS_KEY = 'pl_settings_v2';
let state = {
enabled: true,
style: 'gradient',
solidColor: '#ff0000',
gradStart: '#22c55e',
gradMiddle: '#eab308',
gradEnd: '#ef4444',
thickness: 4
};
function loadSettings() {
const saved = localStorage.getItem(SETTINGS_KEY);
if (saved) state = { ...state, ...JSON.parse(saved) };
}
function saveSettings() {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(state));
}
loadSettings();
function uiHexToHsl(hex) {
let r = parseInt(hex.slice(1, 3), 16) / 255, g = parseInt(hex.slice(3, 5), 16) / 255, b = parseInt(hex.slice(5, 7), 16) / 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, l = (max + min) / 2;
if (max === min) h = s = 0; else {
let d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; }
h /= 6;
}
return { h: h * 360, s: s * 100, l: l * 100 };
}
const uiHslToHex = (h, s, l) => {
l /= 100; s /= 100; const a = s * Math.min(l, 1 - l);
const f = n => { const k = (n + h / 30) % 12; const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); return Math.round(255 * color).toString(16).padStart(2, '0'); };
return `#${f(0)}${f(8)}${f(4)}`;
}
const uiInterpolateHSL = (c1, c2, t) => {
const h1 = uiHexToHsl(c1), h2 = uiHexToHsl(c2);
let hue1 = h1.h, hue2 = h2.h;
if (hue2 - hue1 > 180) hue1 += 360; else if (hue2 - hue1 < -180) hue2 += 360;
return uiHslToHex((hue1 + (hue2 - hue1) * t) % 360, h1.s + (h2.s - h1.s) * t, h1.l + (h2.l - h1.l) * t);
}
const presets = [
{ name: 'The Classic', start: '#22c55e', middle: '#eab308', end: '#ef4444' },
{ name: 'The Fire', start: '#fef08a', middle: '#fb923c', end: '#dc2626' },
{ name: 'Ocean', start: '#70e1d4', middle: '#2d568b', end: '#161b5a' },
{ name: 'Rose', start: '#fddbff', middle: '#bc57b4', end: '#3a123b' },
{ name: 'Forest', start: '#aef29c', middle: '#246149', end: '#06280a' },
{ name: 'Peanut', start: '#eae79f', middle: '#ffa500', end: '#171107' }
];
const style = document.createElement('style');
style.innerHTML = `
:root { --pl-bg-modal: #1e1b3a; --pl-bg-accent: #2a2650; --pl-bg-hover: #332d5c; --pl-blue: #3b82f6; --pl-blue-hover: #2563eb; --pl-text: #ffffff; --pl-dim: #9ca3af; --pl-border: #2a2650; }
#pl-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(8px); z-index: 99999; display: none; justify-content: center; align-items: center; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
#pl-modal { background-color: var(--pl-bg-modal); width: 100%; max-width: 550px; max-height: 90vh; border-radius: 20px; border: 1px solid var(--pl-border); color: var(--pl-text); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; animation: pl-fade-in 0.2s ease-out; overflow: hidden; }
@keyframes pl-fade-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
.pl-header { padding: 20px 24px; border-bottom: 1px solid var(--pl-border); flex-shrink: 0; }
.pl-header h2 { margin: 0; font-size: 20px; font-weight: 700; }
.pl-header p { margin: 4px 0 0 0; color: var(--pl-dim); font-size: 13px; }
.pl-content { padding: 20px 24px; display: flex; flex-direction: column; gap: 20px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--pl-bg-accent) transparent; }
.pl-content::-webkit-scrollbar { width: 6px; }
.pl-content::-webkit-scrollbar-thumb { background: var(--pl-bg-accent); border-radius: 10px; }
.pl-section { display: flex; flex-direction: column; }
.pl-title { font-size: 16px; font-weight: 500; color: white; margin-bottom: 10px; }
.pl-sub-label { font-size: 13px; color: var(--pl-dim); margin-bottom: 6px; display: block; }
.pl-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.pl-btn-group { display: flex; gap: 8px; }
.pl-btn-toggle { flex: 1; padding: 10px; border-radius: 10px; border: none; background: var(--pl-bg-accent); color: var(--pl-dim); cursor: pointer; font-weight: 500; transition: 0.2s; font-size: 14px;}
.pl-btn-toggle.active { background: var(--pl-blue); color: white; }
.pl-color-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.pl-swatch { height: 40px; border-radius: 8px; border: 2px solid var(--pl-border); position: relative; cursor: pointer; overflow: hidden; }
.pl-native-picker { position: absolute; opacity: 0; width: 100%; height: 100%; cursor: pointer; }
.pl-preview-bar { height: 48px; border-radius: 10px; border: 2px solid var(--pl-border); margin-top: 4px; }
.pl-preset-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.pl-preset { background: transparent; border: none; padding: 0; cursor: pointer; text-align: center; }
.pl-preset-bar { height: 32px; border-radius: 6px; border: 2px solid var(--pl-border); margin-bottom: 4px; cursor: pointer; }
.pl-preset span { font-size: 10px; color: var(--pl-dim); cursor: pointer; }
.pl-range { -webkit-appearance: none; width: 100%; height: 22px; background: transparent; outline: none; border: none !important; box-shadow: none !important; margin-top: 4px; }
.pl-range::-webkit-slider-runnable-track { width: 100%; height: 6px; cursor: pointer; background: var(--pl-bg-accent); border-radius: 10px; border: none !important; }
.pl-range::-webkit-slider-thumb { -webkit-appearance: none; height: 20px; width: 20px; border-radius: 50%; background: var(--pl-blue); cursor: pointer; border: 2px solid white !important; box-shadow: 0 0 8px rgba(0,0,0,0.4); margin-top: -7px; }
.pl-preview-box { background: #0f0d1f; border-radius: 12px; border: 1px solid var(--pl-border); padding: 15px; height: 80px; display: flex; align-items: center; flex-shrink: 0; }
.pl-footer { padding: 16px 24px; border-top: 1px solid var(--pl-border); display: flex; justify-content: flex-end; gap: 12px; flex-shrink: 0; }
.pl-btn { padding: 10px 20px; border-radius: 10px; border: none; cursor: pointer; font-weight: 600; font-size: 14px; transition: 0.2s; }
.pl-btn-cancel { background: var(--pl-bg-accent); color: var(--pl-dim); }
.pl-btn-save { background: var(--pl-blue); color: white; }
.pl-hidden { display: none; }
.pl-switch { position: relative; width: 50px; height: 26px; cursor: pointer; flex-shrink: 0; }
.pl-switch input { opacity: 0; width: 0; height: 0; }
.pl-slider-round { position: absolute; inset: 0; background: #4b5563; border-radius: 34px; transition: .3s; }
.pl-slider-round:before { position: absolute; content: ""; height: 18px; width: 18px; left: 4px; bottom: 4px; background: white; border-radius: 50%; transition: .3s; }
.pl-switch input:checked + .pl-slider-round { background: var(--pl-blue); }
.pl-switch input:checked + .pl-slider-round:before { transform: translateX(24px); }
`;
document.head.appendChild(style);
const backdrop = document.createElement('div');
backdrop.id = 'pl-backdrop';
backdrop.innerHTML = `
<div id="pl-modal">
<div class="pl-header"><h2>Path Logger Settings</h2><p>Customize your GeoGuessr path visualization</p></div>
<div class="pl-content">
<div class="pl-row">
<div class="pl-section"><span class="pl-title" style="margin:0">Enable Path Logger</span><p style="margin:2px 0 0 0; font-size:13px; color:var(--pl-dim)">Show path on the map</p></div>
<label class="pl-switch"><input type="checkbox" id="pl-enable-toggle" ${state.enabled ? 'checked' : ''}><span class="pl-slider-round"></span></label>
</div>
<div class="pl-section"><h3 class="pl-title">Line Style</h3><div class="pl-btn-group"><button class="pl-btn-toggle" id="pl-style-solid">Solid</button><button class="pl-btn-toggle" id="pl-style-grad">Gradient</button></div></div>
<div id="pl-grad-ui" class="pl-section">
<h3 class="pl-title">Gradient Colors</h3>
<div class="pl-color-grid">
<div class="pl-section"><span class="pl-sub-label">Start</span><div class="pl-swatch" id="pl-swatch-start"><input type="color" class="pl-native-picker" id="pl-pick-start"></div></div>
<div class="pl-section"><span class="pl-sub-label">Middle</span><div class="pl-swatch" id="pl-swatch-mid"><input type="color" class="pl-native-picker" id="pl-pick-mid"></div></div>
<div class="pl-section"><span class="pl-sub-label">End</span><div class="pl-swatch" id="pl-swatch-end"><input type="color" class="pl-native-picker" id="pl-pick-end"></div></div>
</div>
<div style="margin-top:15px"><span class="pl-sub-label">Preview Bar</span><div class="pl-preview-bar" id="pl-grad-bar"></div></div>
<div style="margin-top:15px"><span class="pl-sub-label">Presets</span><div class="pl-preset-grid" id="pl-presets"></div></div>
</div>
<div id="pl-solid-ui" class="pl-section pl-hidden"><h3 class="pl-title">Solid Color</h3><div class="pl-swatch" id="pl-swatch-solid" style="width: 100px"><input type="color" class="pl-native-picker" id="pl-pick-solid"></div></div>
<div class="pl-section"><h3 class="pl-title">Line Thickness: <span id="pl-thick-val" style="color:var(--pl-blue)">${state.thickness}px</span></h3><input type="range" min="1" max="12" value="${state.thickness}" class="pl-range" id="pl-thick-range"></div>
<div class="pl-section"><h3 class="pl-title">Line Preview</h3><div class="pl-preview-box"><svg width="100%" height="50" viewBox="0 0 500 60" preserveAspectRatio="none"><defs><linearGradient id="pl-svg-grad" x1="0%" y1="0%" x2="100%" y2="0%"></linearGradient></defs><path id="pl-svg-path" d="M 20 30 Q 125 0, 250 30 T 480 30" fill="none" stroke-linecap="round" /></svg></div></div>
</div>
<div class="pl-footer"><button class="pl-btn pl-btn-cancel" id="pl-cancel">Close</button><button class="pl-btn pl-btn-save" id="pl-save">Save Settings</button></div>
</div>
`;
function updateUI() {
document.getElementById('pl-solid-ui').classList.toggle('pl-hidden', state.style === 'gradient');
document.getElementById('pl-grad-ui').classList.toggle('pl-hidden', state.style === 'solid');
document.getElementById('pl-style-solid').classList.toggle('active', state.style === 'solid');
document.getElementById('pl-style-grad').classList.toggle('active', state.style === 'gradient');
document.getElementById('pl-swatch-start').style.backgroundColor = state.gradStart;
document.getElementById('pl-swatch-mid').style.backgroundColor = state.gradMiddle;
document.getElementById('pl-swatch-end').style.backgroundColor = state.gradEnd;
document.getElementById('pl-swatch-solid').style.backgroundColor = state.solidColor;
document.getElementById('pl-pick-start').value = state.gradStart;
document.getElementById('pl-pick-mid').value = state.gradMiddle;
document.getElementById('pl-pick-end').value = state.gradEnd;
document.getElementById('pl-pick-solid').value = state.solidColor;
let gradStr = 'linear-gradient(to right, ';
const svgGrad = document.getElementById('pl-svg-grad');
svgGrad.innerHTML = '';
for (let i = 0; i <= 40; i++) {
const t = i / 40;
const color = t < 0.5 ? uiInterpolateHSL(state.gradStart, state.gradMiddle, t * 2) : uiInterpolateHSL(state.gradMiddle, state.gradEnd, (t - 0.5) * 2);
const pos = (t * 100).toFixed(1) + '%';
gradStr += `${color} ${pos}${i < 40 ? ', ' : ')'}`;
const stop = document.createElementNS("http://www.w3.org/2000/svg", "stop");
stop.setAttribute("offset", pos); stop.setAttribute("stop-color", color);
svgGrad.appendChild(stop);
}
document.getElementById('pl-grad-bar').style.background = gradStr;
document.getElementById('pl-thick-val').innerText = state.thickness + 'px';
const path = document.getElementById('pl-svg-path');
path.setAttribute('stroke-width', state.thickness);
path.setAttribute('stroke', state.style === 'solid' ? state.solidColor : 'url(#pl-svg-grad)');
saveSettings();
}
const showModal = () => { backdrop.style.display = 'flex'; updateUI(); };
const hideModal = () => { backdrop.style.display = 'none'; };
const injectUI = () => {
if (!document.getElementById('pl-backdrop')) document.body.appendChild(backdrop);
document.getElementById('pl-presets').innerHTML = presets.map(p => `
<button class="pl-preset" data-s="${p.start}" data-m="${p.middle}" data-e="${p.end}">
<div class="pl-preset-bar" style="background:linear-gradient(to right, ${p.start}, ${p.middle}, ${p.end})"></div>
<span>${p.name}</span>
</button>
`).join('');
document.querySelectorAll('.pl-preset').forEach(b => b.onclick = () => {
state.gradStart = b.dataset.s; state.gradMiddle = b.dataset.m; state.gradEnd = b.dataset.e;
updateUI();
});
document.getElementById('pl-enable-toggle').onchange = (e) => { state.enabled = e.target.checked; saveSettings(); };
document.getElementById('pl-style-solid').onclick = () => { state.style = 'solid'; updateUI(); };
document.getElementById('pl-style-grad').onclick = () => { state.style = 'gradient'; updateUI(); };
document.getElementById('pl-pick-start').oninput = (e) => { state.gradStart = e.target.value; updateUI(); };
document.getElementById('pl-pick-mid').oninput = (e) => { state.gradMiddle = e.target.value; updateUI(); };
document.getElementById('pl-pick-end').oninput = (e) => { state.gradEnd = e.target.value; updateUI(); };
document.getElementById('pl-pick-solid').oninput = (e) => { state.solidColor = e.target.value; updateUI(); };
document.getElementById('pl-thick-range').oninput = (e) => { state.thickness = e.target.value; updateUI(); };
document.getElementById('pl-cancel').onclick = hideModal;
document.getElementById('pl-save').onclick = hideModal;
backdrop.onclick = (e) => { if (e.target === backdrop) hideModal(); };
};
const injectButton = () => {
const headerRight = document.querySelector('[class*=header-desktop_desktopSectionRight__]');
if (headerRight && !document.getElementById('pl-settings-btn')) {
const btn = document.createElement('div');
btn.id = 'pl-settings-btn';
btn.style = 'cursor:pointer; display:flex; align-items:center; margin-right:15px; transition:opacity 0.2s;';
btn.innerHTML = `<img src="https://www.svgrepo.com/show/455171/route-destination.svg" style="width: 22px; height: 22px; filter: brightness(0) invert(1); opacity: 1.0;">`;
btn.onclick = showModal;
headerRight.insertBefore(btn, headerRight.firstChild);
injectUI();
}
};
const uiObserver = new MutationObserver(injectButton);
uiObserver.observe(document.body, { childList: true, subtree: true });
})();
// --- PART 2: MAP LOGIC (DEFERRED) ---
googleMapsPromise.then(() => runAsClient(() => {
const google = window.google;
const SETTINGS_KEY = 'pl_settings_v2';
const RDP_EPSILON = 0.00002;
const TELEPORT_DISTANCE = 120;
const getSettings = () => {
const defaults = { enabled: true, style: 'gradient', solidColor: '#ff0000', gradStart: '#22c55e', gradMiddle: '#eab308', gradEnd: '#ef4444', thickness: 4 };
try { return { ...defaults, ...JSON.parse(localStorage.getItem(SETTINGS_KEY) || "{}") }; } catch(e) { return defaults; }
};
// --- Helpers ---
const hexToHsl = (hex) => {
let r = parseInt(hex.slice(1, 3), 16) / 255, g = parseInt(hex.slice(3, 5), 16) / 255, b = parseInt(hex.slice(5, 7), 16) / 255;
let max = Math.max(r, g, b), min = Math.min(r, g, b), h, s, l = (max + min) / 2;
if (max === min) h = s = 0; else {
let d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; }
h /= 6;
}
return { h: h * 360, s: s * 100, l: l * 100 };
};
const hslToHex = (h, s, l) => {
l /= 100; s /= 100; const a = s * Math.min(l, 1 - l);
const f = n => { const k = (n + h / 30) % 12; const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); return Math.round(255 * color).toString(16).padStart(2, '0'); };
return `#${f(0)}${f(8)}${f(4)}`;
};
const interpolateHSL = (c1, c2, t) => {
const h1 = hexToHsl(c1), h2 = hexToHsl(c2);
let hue1 = h1.h, hue2 = h2.h;
if (hue2 - hue1 > 180) hue1 += 360; else if (hue2 - hue1 < -180) hue2 += 360;
return hslToHex((hue1 + (hue2 - hue1) * t) % 360, h1.s + (h2.s - h1.s) * t, h1.l + (h2.l - h1.l) * t);
};
const getDistMeters = (p1, p2) => {
const R = 6371e3; const dLat = (p2.lat - p1.lat) * Math.PI/180; const dLng = (p2.lng - p1.lng) * Math.PI/180;
const a = Math.sin(dLat/2)**2 + Math.cos(p1.lat * Math.PI/180) * Math.cos(p2.lat * Math.PI/180) * Math.sin(dLng/2)**2;
return R * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)));
};
const findPerpDist = (p, l1, l2) => {
if (l1.lat === l2.lat && l1.lng === l2.lng) return Math.sqrt((p.lat - l1.lat)**2 + (p.lng - l1.lng)**2);
let num = Math.abs((l2.lng - l1.lng) * p.lat - (l2.lat - l1.lat) * p.lng + l2.lat * l1.lng - l2.lng * l1.lat);
let den = Math.sqrt((l2.lng - l1.lng)**2 + (l2.lat - l1.lat)**2);
return num / den;
};
const rdp = (points, epsilon) => {
if (points.length <= 2) return points;
let dmax = 0, index = 0, end = points.length - 1;
for (let i = 1; i < end; i++) {
let d = findPerpDist(points[i], points[0], points[end]);
if (d > dmax) { index = i; dmax = d; }
}
if (dmax > epsilon) {
let res1 = rdp(points.slice(0, index + 1), epsilon);
let res2 = rdp(points.slice(index), epsilon);
return res1.slice(0, res1.length - 1).concat(res2);
} else { return [points[0], points[end]]; }
};
const saveToStorage = (key, value) => {
const val = JSON.stringify(value);
while (JSON.stringify(localStorage).length + val.length > 5242880) {
const ts = JSON.parse(localStorage.timestamps || "{}");
const oldest = Object.entries(ts).sort((a,b) => a[1]-b[1])[0];
if (!oldest) break;
delete ts[oldest[0]]; Object.keys(localStorage).forEach(k => { if(k.startsWith(oldest[0])) localStorage.removeItem(k); });
localStorage.timestamps = JSON.stringify(ts);
}
localStorage.setItem(key, val);
};
// --- State Detection ---
let markers = [], inGame = false, route = [], currentRound = undefined, mapState = 0;
let lastObservedSpawn = null;
const isGamePage = () => {
const path = location.pathname;
return path.includes("/challenge/") || path.includes("/results/") || path.includes("/game/") ||
path.includes("/duels/") || path.includes("/multiplayer") || path.includes("/summary");
};
const resultShown = () => {
if (document.querySelector('[data-qa="result-view-bottom"]')) return true;
if (document.querySelector('[class*="round-score-2_root"]')) return true;
if (location.href.includes('results') || location.href.includes('summary')) return true;
return false;
};
const isGameFinished = () => {
if (location.href.includes('results') || location.href.includes('summary')) return true;
if (document.querySelector('[data-qa="play-again-button"]') || document.querySelector('[class*="play-again-button"]')) return true;
return false;
};
const getGameID = () => {
const urlMatch = location.href.match(/\w{15,}/);
if (urlMatch && !location.pathname.includes('multiplayer')) return urlMatch[0];
if (window.__GPL_GAME_ID) return window.__GPL_GAME_ID;
if (urlMatch) return urlMatch[0];
return "unknown_game";
};
const getRoundNumber = () => {
const spEl = document.querySelector('[data-qa=round-number] :nth-child(2)');
if (spEl) return parseInt(spEl.innerHTML);
const duelEl = document.querySelector('[class*="round-score-2_roundNumber"]');
if (duelEl) return parseInt(duelEl.innerText.replace(/\D/g, ''));
return 0;
};
const onMove = (sv) => {
if (!getSettings().enabled || !isGamePage()) return;
const pos = { lat: sv.position.lat(), lng: sv.position.lng() };
// 1. Result visible? ALWAYS update spawn buffer and reset guess flag.
if (resultShown()) {
lastObservedSpawn = pos;
if (window.__GPL_HAS_GUESSED) window.__GPL_HAS_GUESSED = false;
return;
}
// 2. Spectating? Stop.
if (window.__GPL_HAS_GUESSED) return;
// 3. Start Recording Logic
if (!inGame) {
inGame = true;
// Use buffered spawn point as point 0
route = lastObservedSpawn ? [[lastObservedSpawn]] : [[]];
}
// 4. Teleport Check
const currentSeg = route[route.length - 1];
const last = currentSeg[currentSeg.length - 1];
if (last && getDistMeters(last, pos) > TELEPORT_DISTANCE) {
route.push([]);
}
route[route.length - 1].push(pos);
};
const onMapUpdate = (map) => {
if (!isGamePage() || !google.maps.geometry) return;
// Add Round Number to checksum to handle persistent Duel pages
const newState = (inGame ? 5 : 0) + (resultShown() ? 10 : 0) + (isGameFinished() ? 20 : 0) + getRoundNumber();
if (newState === mapState) return;
mapState = newState;
markers.forEach(m => m.setMap(null));
markers = [];
if (resultShown()) {
const settings = getSettings();
const currentGameID = getGameID();
// SAVE Logic
if (inGame) {
const rNum = getRoundNumber();
const saveID = currentGameID + '-' + rNum;
const simplifiedRoute = route.map(segment => rdp(segment, RDP_EPSILON));
const encoded = simplifiedRoute.map(p => google.maps.geometry.encoding.encodePath(p.map(x => new google.maps.LatLng(x))));
saveToStorage(saveID, encoded);
const ts = JSON.parse(localStorage.timestamps || "{}");
ts[currentGameID] = Date.now();
localStorage.timestamps = JSON.stringify(ts);
inGame = false;
}
// RENDER
if (!settings.enabled) return;
let keysToShow = [];
if (isGameFinished()) {
keysToShow = Object.keys(localStorage).filter(k => k.startsWith(currentGameID) && !k.includes('timestamp'));
} else {
const rNum = getRoundNumber();
keysToShow = [currentGameID + '-' + rNum];
}
keysToShow.forEach(k => {
const raw = localStorage.getItem(k);
if (raw) {
const segs = JSON.parse(raw).map(x => google.maps.geometry.encoding.decodePath(x));
const total = segs.reduce((a, b) => a + b.length, 0);
let count = 0;
segs.forEach(path => {
const step = Math.max(2, Math.ceil(total / 100));
for (let i = 0; i < path.length - 1; i += (step - 1)) {
const chunk = path.slice(i, i + step);
const t = count / (total || 1);
const color = settings.style === 'solid' ? settings.solidColor : (t < 0.5 ? interpolateHSL(settings.gradStart, settings.gradMiddle, t * 2) : interpolateHSL(settings.gradMiddle, settings.gradEnd, (t - 0.5) * 2));
markers.push(new google.maps.Polyline({
path: chunk,
strokeColor: color,
strokeWeight: settings.thickness,
geodesic: true,
zIndex: Math.floor(t * 100),
clickable: false
}));
count += (chunk.length - 1);
}
});
}
});
markers.forEach(m => m.setMap(map));
}
};
const oldSV = google.maps.StreetViewPanorama;
google.maps.StreetViewPanorama = Object.assign(function (...args) {
const res = oldSV.apply(this, args);
this.addListener('position_changed', () => onMove(this));
return res;
}, { prototype: Object.create(oldSV.prototype) });
const oldMap = google.maps.Map;
google.maps.Map = Object.assign(function (...args) {
const res = oldMap.apply(this, args);
this.addListener('idle', () => onMapUpdate(this));
return res;
}, { prototype: Object.create(oldMap.prototype) });
}));