Files
pogo-mover/index.html
T
Justin Oros f5be6d53b2 PoGoMover
WebApp for spoofing your location on PoGo (Proof-Of-Concept)
2026-05-25 15:50:52 -07:00

1476 lines
39 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PoGo Mover</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
:root {
--red: #e3350d;
--yellow: #ffcb05;
--blue: #3d7dca;
--dark: #1a1a2e;
--darker: #0f0f1a;
--panel: #16213e;
--border: #0f3460;
--text: #e8f4f8;
--muted: #8899aa;
--green: #4caf50;
--orange: #ff9800;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--darker);
color: var(--text);
font-family: 'Nunito', sans-serif;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
header {
background: var(--red);
padding: 0 20px;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 4px solid #8b0000;
flex-shrink: 0;
position: relative;
z-index: 1000;
}
.logo {
font-family: 'Press Start 2P', monospace;
font-size: 14px;
color: var(--yellow);
text-shadow: 2px 2px 0 #8b6000, -1px -1px 0 #8b6000;
letter-spacing: 1px;
}
.logo span {
color: white;
}
.status-pill {
display: flex;
align-items: center;
gap: 8px;
background: rgba(0,0,0,0.3);
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 700;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #555;
animation: none;
}
.status-dot.connected {
background: var(--green);
animation: pulse 1.5s infinite;
}
.status-dot.error {
background: var(--red);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.main {
display: flex;
flex: 1;
overflow: hidden;
}
#map {
flex: 1;
cursor: crosshair;
}
.sidebar {
width: 280px;
background: var(--panel);
border-left: 3px solid var(--border);
display: flex;
flex-direction: column;
overflow-y: auto;
flex-shrink: 0;
}
.section {
padding: 16px;
border-bottom: 2px solid var(--border);
}
.section-title {
font-family: 'Press Start 2P', monospace;
font-size: 8px;
color: var(--yellow);
margin-bottom: 12px;
letter-spacing: 1px;
}
.dpad-container {
display: flex;
justify-content: center;
margin: 8px 0;
}
.dpad {
display: grid;
grid-template-columns: 52px 52px 52px;
grid-template-rows: 52px 52px 52px;
gap: 4px;
}
.dpad-btn {
background: var(--dark);
border: 2px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 18px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.1s;
user-select: none;
-webkit-user-select: none;
}
.dpad-btn:hover {
background: var(--border);
border-color: var(--blue);
}
.dpad-btn:active, .dpad-btn.pressed {
background: var(--blue);
border-color: var(--yellow);
transform: scale(0.93);
box-shadow: 0 0 10px rgba(61,125,202,0.5);
}
.dpad-center {
background: var(--darker);
border-radius: 50%;
border: 2px solid var(--border);
font-size: 10px;
font-family: 'Press Start 2P', monospace;
color: var(--muted);
display: flex;
align-items: center;
justify-content: center;
cursor: default;
}
.key-hint {
text-align: center;
font-size: 10px;
color: var(--muted);
margin-top: 6px;
}
.key-hint kbd {
background: var(--dark);
border: 1px solid var(--border);
border-radius: 4px;
padding: 1px 5px;
font-family: monospace;
font-size: 10px;
color: var(--yellow);
}
.speed-section { }
.speed-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-weight: 700;
}
.speed-badge {
background: var(--blue);
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 800;
}
.speed-presets {
display: flex;
gap: 6px;
margin-bottom: 10px;
}
.preset-btn {
flex: 1;
background: var(--dark);
border: 2px solid var(--border);
border-radius: 8px;
color: var(--text);
font-family: 'Nunito', sans-serif;
font-size: 11px;
font-weight: 700;
padding: 6px 4px;
cursor: pointer;
transition: all 0.15s;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.preset-btn:hover, .preset-btn.active {
border-color: var(--yellow);
background: var(--border);
}
.preset-btn.active {
background: var(--blue);
}
.preset-icon { font-size: 14px; }
input[type=range] {
width: 100%;
accent-color: var(--yellow);
cursor: pointer;
}
.coords-display {
background: var(--darker);
border: 2px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
font-family: monospace;
font-size: 12px;
line-height: 1.8;
}
.coords-display .label {
color: var(--muted);
font-size: 10px;
}
.coords-display .value {
color: var(--yellow);
font-weight: bold;
}
.click-tip {
background: rgba(61,125,202,0.15);
border: 1px solid rgba(61,125,202,0.4);
border-radius: 8px;
padding: 10px;
font-size: 11px;
color: var(--muted);
line-height: 1.6;
margin-top: 8px;
}
.click-tip strong {
color: var(--text);
}
.server-warning {
background: rgba(227,53,13,0.15);
border: 2px solid rgba(227,53,13,0.5);
border-radius: 8px;
padding: 12px;
font-size: 11px;
line-height: 1.7;
margin: 16px;
}
.server-warning strong {
color: var(--yellow);
display: block;
margin-bottom: 4px;
font-size: 10px;
font-family: 'Press Start 2P', monospace;
}
.move-queue-indicator {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
font-size: 11px;
color: var(--muted);
}
.moving-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--green);
display: none;
}
.moving-dot.active {
display: block;
animation: pulse 0.5s infinite;
}
.character-marker {
width: 36px;
height: 36px;
background: var(--blue);
border: 3px solid var(--yellow);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
box-shadow: 0 0 0 3px rgba(61,125,202,0.3), 0 4px 12px rgba(0,0,0,0.5);
animation: float 2s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.destination-marker {
width: 20px;
height: 20px;
background: var(--yellow);
border: 2px solid var(--red);
border-radius: 50%;
box-shadow: 0 0 8px rgba(255,203,5,0.6);
}
.locate-btn {
width: 44px;
height: 44px;
background: var(--panel);
border: 2px solid var(--border);
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
transition: all 0.2s;
position: relative;
}
.locate-btn:hover {
background: var(--border);
border-color: var(--blue);
box-shadow: 0 0 12px rgba(61,125,202,0.4);
}
.locate-btn:active {
transform: scale(0.93);
}
.locate-btn.locating svg {
animation: locateSpin 1s linear infinite;
}
.locate-btn.located svg #arrow {
fill: var(--blue);
filter: drop-shadow(0 0 4px rgba(61,125,202,0.8));
}
@keyframes locateSpin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.locate-toast {
position: absolute;
bottom: 60px;
left: 0;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 6px 10px;
font-size: 11px;
white-space: nowrap;
color: var(--text);
pointer-events: none;
opacity: 0;
transform: translateY(4px);
transition: opacity 0.2s, transform 0.2s;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
.locate-toast.visible {
opacity: 1;
transform: translateY(0);
}
.leaflet-control-layers {
background: var(--panel) !important;
border: 2px solid var(--border) !important;
border-radius: 10px !important;
box-shadow: 0 2px 10px rgba(0,0,0,0.5) !important;
padding: 2px 4px !important;
margin-bottom: 8px !important;
}
.leaflet-control-layers-list {
display: flex !important;
gap: 4px !important;
}
.leaflet-control-layers label {
display: flex !important;
align-items: center !important;
gap: 5px !important;
padding: 5px 8px !important;
border-radius: 7px !important;
cursor: pointer !important;
font-family: 'Nunito', sans-serif !important;
font-size: 12px !important;
font-weight: 700 !important;
color: var(--muted) !important;
transition: all 0.15s !important;
white-space: nowrap !important;
}
.leaflet-control-layers label:has(input:checked) {
background: var(--blue) !important;
color: white !important;
}
.leaflet-control-layers label:hover {
color: var(--text) !important;
background: var(--border) !important;
}
.leaflet-control-layers input[type=radio] {
display: none !important;
}
.leaflet-control-layers-toggle {
display: none !important;
}
.leaflet-control-layers-separator {
display: none !important;
}
.teleport-btn {
width: 100%;
margin-top: 10px;
padding: 10px;
background: linear-gradient(135deg, #1a0030, #2d0050);
border: 2px solid #7b2fff;
border-radius: 10px;
color: #d4aaff;
font-family: 'Press Start 2P', monospace;
font-size: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
letter-spacing: 1px;
}
.teleport-btn:hover {
background: linear-gradient(135deg, #2d0050, #4a00a0);
border-color: #bf80ff;
color: #fff;
box-shadow: 0 0 16px rgba(123,47,255,0.5);
}
.teleport-btn.active {
background: linear-gradient(135deg, #4a00a0, #7b2fff);
border-color: #ffcb05;
color: #ffcb05;
box-shadow: 0 0 20px rgba(123,47,255,0.7);
}
.teleport-btn .t-icon { font-size: 16px; }
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.75);
z-index: 9999;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
.modal-overlay.visible {
display: flex;
}
.modal {
background: var(--panel);
border: 3px solid #7b2fff;
border-radius: 16px;
padding: 28px 28px 24px;
max-width: 420px;
width: 90%;
box-shadow: 0 0 40px rgba(123,47,255,0.4), 0 20px 60px rgba(0,0,0,0.6);
animation: modalIn 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes modalIn {
from { opacity: 0; transform: scale(0.85) translateY(20px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.modal-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.modal-icon {
font-size: 32px;
line-height: 1;
}
.modal-title {
font-family: 'Press Start 2P', monospace;
font-size: 11px;
color: #bf80ff;
line-height: 1.6;
}
.modal-body {
font-size: 13px;
line-height: 1.8;
color: var(--text);
margin-bottom: 20px;
}
.modal-body .warning-box {
background: rgba(227,53,13,0.12);
border: 2px solid rgba(227,53,13,0.5);
border-radius: 10px;
padding: 12px 14px;
margin-bottom: 14px;
font-weight: 700;
color: #ffb3a7;
}
.modal-body .warning-box .warn-title {
font-family: 'Press Start 2P', monospace;
font-size: 8px;
color: var(--red);
margin-bottom: 8px;
display: block;
}
.modal-body .cooldown-box {
background: rgba(123,47,255,0.1);
border: 1px solid rgba(123,47,255,0.4);
border-radius: 10px;
padding: 12px 14px;
font-size: 12px;
color: #d4aaff;
}
.modal-body .cooldown-box strong {
color: #ffcb05;
}
.modal-dest {
font-family: monospace;
font-size: 11px;
color: var(--muted);
background: var(--darker);
border-radius: 6px;
padding: 6px 10px;
margin-bottom: 16px;
}
.modal-actions {
display: flex;
gap: 10px;
}
.modal-btn {
flex: 1;
padding: 12px;
border-radius: 10px;
border: 2px solid transparent;
font-family: 'Nunito', sans-serif;
font-size: 14px;
font-weight: 800;
cursor: pointer;
transition: all 0.15s;
}
.modal-btn-cancel {
background: var(--dark);
border-color: var(--border);
color: var(--muted);
}
.modal-btn-cancel:hover {
border-color: var(--muted);
color: var(--text);
}
.modal-btn-confirm {
background: linear-gradient(135deg, #4a00a0, #7b2fff);
border-color: #bf80ff;
color: white;
}
.modal-btn-confirm:hover {
background: linear-gradient(135deg, #6200d0, #9b4fff);
box-shadow: 0 0 16px rgba(123,47,255,0.5);
}
.cooldown-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.85);
z-index: 9998;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 20px;
backdrop-filter: blur(6px);
}
.cooldown-overlay.visible {
display: flex;
}
.cooldown-title {
font-family: 'Press Start 2P', monospace;
font-size: 13px;
color: #bf80ff;
text-align: center;
line-height: 1.8;
}
.cooldown-ring {
position: relative;
width: 140px;
height: 140px;
}
.cooldown-ring svg {
transform: rotate(-90deg);
}
.cooldown-ring-track {
fill: none;
stroke: rgba(123,47,255,0.2);
stroke-width: 8;
}
.cooldown-ring-progress {
fill: none;
stroke: #7b2fff;
stroke-width: 8;
stroke-linecap: round;
transition: stroke-dashoffset 1s linear;
}
.cooldown-time {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Press Start 2P', monospace;
font-size: 22px;
color: #ffcb05;
line-height: 1;
}
.cooldown-time .cd-unit {
font-size: 8px;
color: var(--muted);
margin-top: 4px;
}
.cooldown-msg {
font-size: 13px;
color: var(--muted);
text-align: center;
max-width: 300px;
line-height: 1.7;
}
.cooldown-skip {
background: transparent;
border: 2px solid var(--border);
border-radius: 8px;
color: var(--muted);
font-family: 'Nunito', sans-serif;
font-size: 12px;
padding: 8px 20px;
cursor: pointer;
transition: all 0.15s;
margin-top: 8px;
}
.cooldown-skip:hover {
border-color: var(--muted);
color: var(--text);
}
footer {
background: var(--darker);
border-top: 2px solid var(--border);
padding: 6px 20px;
font-size: 10px;
color: var(--muted);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
</style>
</head>
<body>
<header>
<div class="logo">PoGo<span>Mover</span></div>
<div class="status-pill">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Connecting...</span>
</div>
</header>
<div class="main">
<div id="map"></div>
<div class="sidebar">
<div class="section">
<div class="section-title">Movement</div>
<div class="dpad-container">
<div class="dpad">
<div></div>
<button class="dpad-btn" id="btn-up" data-dir="up">▲</button>
<div></div>
<button class="dpad-btn" id="btn-left" data-dir="left">◀</button>
<div class="dpad-center">✦</div>
<button class="dpad-btn" id="btn-right" data-dir="right">▶</button>
<div></div>
<button class="dpad-btn" id="btn-down" data-dir="down">▼</button>
<div></div>
</div>
</div>
<div class="key-hint">
<kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd> or arrow keys
</div>
<div class="move-queue-indicator">
<div class="moving-dot" id="movingDot"></div>
<span id="movingLabel">Ready</span>
</div>
</div>
<div class="section speed-section">
<div class="section-title">Speed</div>
<div class="speed-presets">
<button class="preset-btn active" data-speed="1.4" id="preset-walk">
<span class="preset-icon">🚶</span>Walk
</button>
<button class="preset-btn" data-speed="4" id="preset-jog">
<span class="preset-icon">🏃</span>Jog
</button>
<button class="preset-btn" data-speed="11" id="preset-bike">
<span class="preset-icon">🚲</span>Bike
</button>
<button class="preset-btn" data-speed="28" id="preset-car">
<span class="preset-icon">🚗</span>Drive
</button>
</div>
<button class="teleport-btn" id="teleportToggle">
<span class="t-icon">✨</span> TELEPORT MODE
</button>
<div class="speed-label" id="speedLabelRow" style="margin-top:10px">
<span>Speed</span>
<div class="speed-badge" id="speedBadge">1.4 m/s</div>
</div>
<input type="range" id="speedSlider" min="0.5" max="40" step="0.5" value="1.4">
</div>
<div class="section">
<div class="section-title">Position</div>
<div class="coords-display">
<div class="label">Latitude</div>
<div class="value" id="dispLat">—</div>
<div class="label" style="margin-top:6px">Longitude</div>
<div class="value" id="dispLng">—</div>
</div>
<div class="click-tip" id="clickTip">
<strong>Click the map</strong> to add waypoints. The player moves through them in order. Click multiple times to build a route.
</div>
</div>
<div id="serverWarning" class="server-warning" style="display:none">
<strong>⚠ Server offline</strong>
Start <code>server.py</code> on your Mac and connect your iPhone via USB. See the README for setup steps.
</div>
</div>
</div>
<footer>
<span id="footerMode">🎮 Click the map or use WASD to move</span>
<span id="footerCoords"></span>
</footer>
<div class="modal-overlay" id="teleportModal">
<div class="modal">
<div class="modal-header">
<div class="modal-icon">✨</div>
<div class="modal-title">TELEPORT<br>WARNING</div>
</div>
<div class="modal-body">
<div class="warning-box">
<span class="warn-title">⚠ IMPORTANT — READ BEFORE TELEPORTING</span>
Teleporting instantly moves your character across large distances.
Pokémon GO can detect this and issue a <strong>soft ban</strong>.
</div>
<div class="cooldown-box">
<strong>Before you teleport:</strong><br>
1. Force-close Pokémon GO on your iPhone<br>
2. Confirm the teleport here<br>
3. Wait the recommended cooldown time<br>
4. Then reopen Pokémon GO<br><br>
<strong>Recommended wait time:</strong> ~2 hours for long distances, 30 min for short hops
</div>
</div>
<div class="modal-dest" id="modalDestCoords"></div>
<div class="modal-actions">
<button class="modal-btn modal-btn-cancel" id="teleportCancel">Cancel</button>
<button class="modal-btn modal-btn-confirm" id="teleportConfirm">✨ Teleport &amp; Start Timer</button>
</div>
</div>
</div>
<div class="cooldown-overlay" id="cooldownOverlay">
<div class="cooldown-title">COOLDOWN TIMER<br><span style="color:var(--muted);font-size:9px">Safe to open Pokémon GO when timer ends</span></div>
<div class="cooldown-ring">
<svg width="140" height="140" viewBox="0 0 140 140">
<circle class="cooldown-ring-track" cx="70" cy="70" r="58"/>
<circle class="cooldown-ring-progress" id="cooldownArc" cx="70" cy="70" r="58"
stroke-dasharray="364.4" stroke-dashoffset="0"/>
</svg>
<div class="cooldown-time">
<span id="cooldownDisplay">—</span>
<span class="cd-unit">remaining</span>
</div>
</div>
<div class="cooldown-msg">
Keep Pokémon GO <strong>closed</strong> until this timer reaches zero to avoid a soft ban.
</div>
<button class="cooldown-skip" id="cooldownSkip">Skip timer (not recommended)</button>
</div>
<script>
const SERVER = "http:
const STEP_INTERVAL_MS = 700;
let lat = 37.7749;
let lng = -122.4194;
const _savedSpeed = parseFloat(localStorage.getItem("pm_speed") || "1.4");
let speedMs = _savedSpeed;
let serverOnline = false;
let moveInterval = null;
let clickPath = null;
let clickPathIndex = 0;
let clickAnimating = false;
let keysDown = {};
const _savedZoom = parseInt(localStorage.getItem("pm_zoom") || "16");
const _savedLayer = localStorage.getItem("pm_layer") || "street";
const map = L.map("map", { zoomControl: true }).setView([lat, lng], _savedZoom);
const streetLayer = L.tileLayer("https:
attribution: "© OpenStreetMap contributors",
maxZoom: 19
});
const satelliteLayer = L.tileLayer("https:
attribution: "© Esri, Maxar, Earthstar Geographics",
maxZoom: 19
});
const satelliteLabels = L.tileLayer("https:
attribution: "",
maxZoom: 19,
opacity: 0.8
});
if (_savedLayer === "satellite") {
satelliteLayer.addTo(map);
satelliteLabels.addTo(map);
} else {
streetLayer.addTo(map);
}
L.control.layers(
{ "🗺 Street": streetLayer, "🛰 Satellite": satelliteLayer },
{},
{ position: "bottomleft", collapsed: false }
).addTo(map);
map.on("baselayerchange", e => {
if (e.name === "🛰 Satellite") {
satelliteLabels.addTo(map);
localStorage.setItem("pm_layer", "satellite");
} else {
if (map.hasLayer(satelliteLabels)) map.removeLayer(satelliteLabels);
localStorage.setItem("pm_layer", "street");
}
});
map.on("zoomend", () => {
localStorage.setItem("pm_zoom", map.getZoom());
});
const modeIcons = { walk: "🚶", jog: "🏃", bike: "🚲", car: "🚗", teleport: "✨" };
const _savedMode = localStorage.getItem("pm_mode") || "walk";
let currentMode = _savedMode;
function charIconHtml(mode) {
return `<div class="character-marker">${modeIcons[mode] || "🚶"}</div>`;
}
const charIcon = L.divIcon({
className: "",
html: charIconHtml("walk"),
iconSize: [36, 36],
iconAnchor: [18, 18]
});
function updateCharIcon(mode) {
currentMode = mode;
charMarker.setIcon(L.divIcon({
className: "",
html: charIconHtml(mode),
iconSize: [36, 36],
iconAnchor: [18, 18]
}));
}
const destIcon = L.divIcon({
className: "",
html: '<div class="destination-marker"></div>',
iconSize: [20, 20],
iconAnchor: [10, 10]
});
const charMarker = L.marker([lat, lng], { icon: charIcon }).addTo(map);
const LocateControl = L.Control.extend({
options: { position: "bottomleft" },
onAdd() {
const container = L.DomUtil.create("div");
L.DomEvent.disableClickPropagation(container);
container.innerHTML = `
<button class="locate-btn" id="locateBtn" title="Set player to my real GPS location">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http:
<id id="arrow"/>
<circle cx="12" cy="12" r="3" fill="#ffcb05"/>
<circle cx="12" cy="12" r="6" stroke="#3d7dca" stroke-width="1.5" fill="none" stroke-dasharray="3 2"/>
<polygon id="arrow" points="12,2 15.5,9 12,7.5 8.5,9" fill="#e8f4f8"/>
<line x1="12" y1="12" x2="12" y2="2.5" stroke="#e8f4f8" stroke-width="1.5" stroke-linecap="round"/>
<line x1="12" y1="22" x2="12" y2="18" stroke="#8899aa" stroke-width="1.5" stroke-linecap="round"/>
<line x1="2" y1="12" x2="6" y2="12" stroke="#8899aa" stroke-width="1.5" stroke-linecap="round"/>
<line x1="18" y1="12" x2="22" y2="12" stroke="#8899aa" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<div class="locate-toast" id="locateToast"></div>
</button>`;
return container;
}
});
new LocateControl().addTo(map);
function showLocateToast(msg, color) {
const toast = document.getElementById("locateToast");
toast.textContent = msg;
toast.style.color = color || "var(--text)";
toast.classList.add("visible");
setTimeout(() => toast.classList.remove("visible"), 2500);
}
document.addEventListener("click", e => {
if (!e.target.closest("#locateBtn")) return;
const btn = document.getElementById("locateBtn");
if (!navigator.geolocation) {
showLocateToast("Geolocation not supported", "var(--red)");
return;
}
btn.classList.add("locating");
btn.classList.remove("located");
showLocateToast("Finding your location…", "var(--muted)");
navigator.geolocation.getCurrentPosition(
pos => {
btn.classList.remove("locating");
btn.classList.add("located");
const realLat = pos.coords.latitude;
const realLng = pos.coords.longitude;
stopClickPath();
stopKeyLoop();
keysDown = {};
sendCoords(realLat, realLng);
map.setView([realLat, realLng], map.getZoom());
showLocateToast("📍 Moved to your location!", "var(--green)");
setTimeout(() => btn.classList.remove("located"), 3000);
},
err => {
btn.classList.remove("locating");
const msgs = { 1: "Location permission denied", 2: "Location unavailable", 3: "Request timed out" };
showLocateToast(msgs[err.code] || "Location error", "var(--red)");
},
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
);
});
function metersPerDegLat() { return 111320; }
function metersPerDegLng(latDeg) { return 111320 * Math.cos(latDeg * Math.PI / 180); }
function updateDisplay() {
document.getElementById("dispLat").textContent = lat.toFixed(6);
document.getElementById("dispLng").textContent = lng.toFixed(6);
document.getElementById("footerCoords").textContent = `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
charMarker.setLatLng([lat, lng]);
}
async function sendCoords(newLat, newLng) {
lat = newLat;
lng = newLng;
updateDisplay();
if (!serverOnline) return;
try {
await fetch(`${SERVER}/move`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lat, lng })
});
} catch (e) {}
}
function stepSize() {
return (speedMs * STEP_INTERVAL_MS / 1000);
}
const dirHandlers = {
up: () => sendCoords(lat + stepSize() / metersPerDegLat(), lng),
down: () => sendCoords(lat - stepSize() / metersPerDegLat(), lng),
left: () => sendCoords(lat, lng - stepSize() / metersPerDegLng(lat)),
right: () => sendCoords(lat, lng + stepSize() / metersPerDegLng(lat))
};
function getActiveDirections() {
const dirs = [];
if (keysDown["w"] || keysDown["arrowup"]) dirs.push("up");
if (keysDown["s"] || keysDown["arrowdown"]) dirs.push("down");
if (keysDown["a"] || keysDown["arrowleft"]) dirs.push("left");
if (keysDown["d"] || keysDown["arrowright"]) dirs.push("right");
return dirs;
}
function startKeyLoop() {
if (moveInterval) return;
stopClickPath();
moveInterval = setInterval(() => {
const dirs = getActiveDirections();
if (dirs.length === 0) { stopKeyLoop(); return; }
dirs.forEach(d => dirHandlers[d]());
setMoving(true);
}, STEP_INTERVAL_MS);
}
function stopKeyLoop() {
if (moveInterval) { clearInterval(moveInterval); moveInterval = null; }
if (Object.keys(keysDown).length === 0) setMoving(false);
}
document.addEventListener("keydown", e => {
const key = e.key.toLowerCase();
if (!["w","a","s","d","arrowup","arrowdown","arrowleft","arrowright"].includes(key)) return;
e.preventDefault();
if (keysDown[key]) return;
keysDown[key] = true;
getActiveDirections().forEach(d => {
const btn = document.getElementById(`btn-${d}`);
if (btn) btn.classList.add("pressed");
});
startKeyLoop();
});
document.addEventListener("keyup", e => {
const key = e.key.toLowerCase();
delete keysDown[key];
const dir = { w:"up", a:"left", s:"down", d:"right", arrowup:"up", arrowdown:"down", arrowleft:"left", arrowright:"right" }[key];
if (dir) {
const btn = document.getElementById(`btn-${dir}`);
if (btn) btn.classList.remove("pressed");
}
if (getActiveDirections().length === 0) stopKeyLoop();
});
document.querySelectorAll(".dpad-btn").forEach(btn => {
const dir = btn.dataset.dir;
let interval = null;
const start = () => {
stopClickPath();
dirHandlers[dir]();
setMoving(true);
interval = setInterval(() => dirHandlers[dir](), STEP_INTERVAL_MS);
};
const stop = () => { if (interval) { clearInterval(interval); interval = null; } setMoving(false); };
btn.addEventListener("mousedown", start);
btn.addEventListener("touchstart", e => { e.preventDefault(); start(); });
btn.addEventListener("mouseup", stop);
btn.addEventListener("mouseleave", stop);
btn.addEventListener("touchend", stop);
});
const waypointQueue = [];
let waypointMarkers = [];
let waypointLines = [];
function buildPath(fromLat, fromLng, toLat, toLng) {
const dLat = (toLat - fromLat) * metersPerDegLat();
const dLng = (toLng - fromLng) * metersPerDegLng(fromLat);
const totalMeters = Math.sqrt(dLat * dLat + dLng * dLng);
if (totalMeters < 1) return [];
const steps = Math.ceil(totalMeters / stepSize());
const path = [];
for (let i = 1; i <= steps; i++) {
const t = i / steps;
path.push([fromLat + (toLat - fromLat) * t, fromLng + (toLng - fromLng) * t]);
}
return path;
}
function clearWaypointVisuals() {
waypointMarkers.forEach(m => map.removeLayer(m));
waypointLines.forEach(l => map.removeLayer(l));
waypointMarkers = [];
waypointLines = [];
}
function rebuildWaypointVisuals() {
clearWaypointVisuals();
let prevLat = lat, prevLng = lng;
waypointQueue.forEach((wp, i) => {
const marker = L.marker([wp.lat, wp.lng], { icon: destIcon }).addTo(map);
const line = L.polyline([[prevLat, prevLng], [wp.lat, wp.lng]], {
color: i === 0 ? "#ffcb05" : "#aaaaff",
weight: 2,
dashArray: "6 6",
opacity: 0.7
}).addTo(map);
waypointMarkers.push(marker);
waypointLines.push(line);
prevLat = wp.lat;
prevLng = wp.lng;
});
}
function stopClickPath() {
if (clickPath) { clearInterval(clickPath); clickPath = null; }
clickAnimating = false;
waypointQueue.length = 0;
clearWaypointVisuals();
}
function processNextWaypoint() {
if (waypointQueue.length === 0) {
setMoving(false);
clickAnimating = false;
return;
}
const wp = waypointQueue[0];
const path = buildPath(lat, lng, wp.lat, wp.lng);
if (path.length === 0) {
waypointQueue.shift();
rebuildWaypointVisuals();
processNextWaypoint();
return;
}
let i = 0;
clickAnimating = true;
setMoving(true);
clickPath = setInterval(() => {
if (i >= path.length) {
clearInterval(clickPath);
clickPath = null;
waypointQueue.shift();
rebuildWaypointVisuals();
processNextWaypoint();
return;
}
sendCoords(path[i][0], path[i][1]);
i++;
}, STEP_INTERVAL_MS);
}
function addWaypoint(toLat, toLng) {
waypointQueue.push({ lat: toLat, lng: toLng });
rebuildWaypointVisuals();
if (!clickAnimating) {
processNextWaypoint();
}
}
map.on("click", e => {
const toLat = e.latlng.lat;
const toLng = e.latlng.lng;
if (teleportMode) {
showTeleportModal(toLat, toLng);
return;
}
stopKeyLoop();
keysDown = {};
addWaypoint(toLat, toLng);
});
function setMoving(active) {
const dot = document.getElementById("movingDot");
const label = document.getElementById("movingLabel");
dot.classList.toggle("active", active);
if (active) {
const remaining = waypointQueue.length;
label.textContent = remaining > 1 ? `Moving… (${remaining} waypoints)` : "Moving…";
} else {
label.textContent = "Ready";
}
}
const speedSlider = document.getElementById("speedSlider");
const speedBadge = document.getElementById("speedBadge");
speedSlider.addEventListener("input", () => {
speedMs = parseFloat(speedSlider.value);
speedBadge.textContent = `${speedMs.toFixed(1)} m/s`;
document.querySelectorAll(".preset-btn").forEach(b => b.classList.remove("active"));
localStorage.setItem("pm_speed", speedMs);
if (speedMs <= 2) updateCharIcon("walk");
else if (speedMs <= 6) updateCharIcon("jog");
else if (speedMs <= 18) updateCharIcon("bike");
else updateCharIcon("car");
});
document.querySelectorAll(".preset-btn").forEach(btn => {
btn.addEventListener("click", () => {
speedMs = parseFloat(btn.dataset.speed);
speedSlider.value = speedMs;
speedBadge.textContent = `${speedMs.toFixed(1)} m/s`;
document.querySelectorAll(".preset-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
const modeMap = { "preset-walk": "walk", "preset-jog": "jog", "preset-bike": "bike", "preset-car": "car" };
const mode = modeMap[btn.id] || "walk";
localStorage.setItem("pm_speed", speedMs);
localStorage.setItem("pm_mode", mode);
updateCharIcon(mode);
});
});
function applyInitialSpeed() {
speedSlider.value = speedMs;
speedBadge.textContent = `${speedMs.toFixed(1)} m/s`;
const modeToPreset = { walk: "preset-walk", jog: "preset-jog", bike: "preset-bike", car: "preset-car" };
const presetId = modeToPreset[currentMode];
if (presetId) {
document.querySelectorAll(".preset-btn").forEach(b => b.classList.remove("active"));
const btn = document.getElementById(presetId);
if (btn) btn.classList.add("active");
}
updateCharIcon(currentMode);
}
applyInitialSpeed();
let teleportMode = false;
let pendingTeleportLat = null;
let pendingTeleportLng = null;
let cooldownTimer = null;
const CIRCUMFERENCE = 364.4;
const teleportToggle = document.getElementById("teleportToggle");
const teleportModal = document.getElementById("teleportModal");
const teleportCancel = document.getElementById("teleportCancel");
const teleportConfirm = document.getElementById("teleportConfirm");
const cooldownOverlay = document.getElementById("cooldownOverlay");
const cooldownSkip = document.getElementById("cooldownSkip");
const clickTip = document.getElementById("clickTip");
const speedLabelRow = document.getElementById("speedLabelRow");
const footerMode = document.getElementById("footerMode");
function setTeleportMode(active) {
teleportMode = active;
teleportToggle.classList.toggle("active", active);
document.getElementById("speedSlider").disabled = active;
document.querySelectorAll(".preset-btn").forEach(b => b.disabled = active);
document.querySelectorAll(".dpad-btn").forEach(b => b.disabled = active);
speedLabelRow.style.opacity = active ? "0.35" : "1";
updateCharIcon(active ? "teleport" : currentMode);
if (active) {
clickTip.innerHTML = '<strong style="color:#bf80ff">✨ TELEPORT MODE ON</strong> — Click anywhere on the map to instantly jump there. A cooldown timer will guide you on when to reopen Pokémon GO.';
clickTip.style.background = "rgba(123,47,255,0.12)";
clickTip.style.borderColor = "rgba(123,47,255,0.4)";
footerMode.textContent = "✨ Teleport Mode — click the map to jump";
document.body.style.cursor = "crosshair";
} else {
clickTip.innerHTML = '<strong>Click anywhere on the map</strong> to move your character to that location automatically at the current speed.';
clickTip.style.background = "rgba(61,125,202,0.15)";
clickTip.style.borderColor = "rgba(61,125,202,0.4)";
footerMode.textContent = "🎮 Click the map or use WASD to move";
document.body.style.cursor = "";
}
}
teleportToggle.addEventListener("click", () => setTeleportMode(!teleportMode));
function distanceKm(lat1, lng1, lat2, lng2) {
const R = 6371;
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLng = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLng/2)**2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
function cooldownMinutes(km) {
if (km < 10) return 30;
if (km < 50) return 60;
if (km < 500) return 90;
return 120;
}
function showTeleportModal(toLat, toLng) {
pendingTeleportLat = toLat;
pendingTeleportLng = toLng;
const km = distanceKm(lat, lng, toLat, toLng);
const mins = cooldownMinutes(km);
document.getElementById("modalDestCoords").textContent =
`Destination: ${toLat.toFixed(5)}, ${toLng.toFixed(5)} • ~${km < 1 ? (km*1000).toFixed(0)+"m" : km.toFixed(1)+"km"} away • Suggested cooldown: ${mins} min`;
teleportModal.classList.add("visible");
}
teleportCancel.addEventListener("click", () => {
teleportModal.classList.remove("visible");
pendingTeleportLat = null;
pendingTeleportLng = null;
});
teleportConfirm.addEventListener("click", async () => {
teleportModal.classList.remove("visible");
if (pendingTeleportLat === null) return;
stopClickPath();
stopKeyLoop();
keysDown = {};
const km = distanceKm(lat, lng, pendingTeleportLat, pendingTeleportLng);
const totalSecs = cooldownMinutes(km) * 60;
await sendCoords(pendingTeleportLat, pendingTeleportLng);
clearWaypointVisuals();
map.setView([pendingTeleportLat, pendingTeleportLng], map.getZoom());
pendingTeleportLat = null;
pendingTeleportLng = null;
startCooldown(totalSecs);
});
function startCooldown(totalSecs) {
cooldownOverlay.classList.add("visible");
let remaining = totalSecs;
const arc = document.getElementById("cooldownArc");
function tick() {
if (remaining <= 0) {
clearInterval(cooldownTimer);
cooldownOverlay.classList.remove("visible");
setTeleportMode(false);
return;
}
const mins = Math.floor(remaining / 60);
const secs = remaining % 60;
document.getElementById("cooldownDisplay").textContent =
mins > 0 ? `${mins}:${String(secs).padStart(2,"0")}` : `${secs}s`;
const pct = remaining / totalSecs;
arc.style.strokeDashoffset = CIRCUMFERENCE * (1 - pct);
remaining--;
}
tick();
cooldownTimer = setInterval(tick, 1000);
}
cooldownSkip.addEventListener("click", () => {
if (cooldownTimer) clearInterval(cooldownTimer);
cooldownOverlay.classList.remove("visible");
setTeleportMode(false);
});
let initialViewSet = false;
async function checkServer() {
try {
const r = await fetch(`${SERVER}/status`, { signal: AbortSignal.timeout(2000) });
if (r.ok) {
serverOnline = true;
document.getElementById("statusDot").className = "status-dot connected";
document.getElementById("statusText").textContent = "iPhone Connected";
document.getElementById("serverWarning").style.display = "none";
const coords = await fetch(`${SERVER}/coords`).then(r => r.json());
lat = coords.lat; lng = coords.lng;
if (!initialViewSet) {
map.setView([lat, lng], _savedZoom);
initialViewSet = true;
}
updateDisplay();
return;
}
} catch (e) {}
serverOnline = false;
initialViewSet = false;
document.getElementById("statusDot").className = "status-dot error";
document.getElementById("statusText").textContent = "Server offline";
document.getElementById("serverWarning").style.display = "block";
}
updateDisplay();
checkServer();
setInterval(checkServer, 5000);
</script>
</body>
</html>