2026-05-25 15:50:52 -07:00
<!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 ; }
2026-05-25 16:05:20 -07:00
body { background : var ( -- darker ); color : var ( -- text ); font-family : 'Nunito' , sans-serif ; height : 100 vh ; display : flex ; flex-direction : column ; overflow : hidden ; }
header { background : var ( -- red ); padding : 0 20 px ; height : 56 px ; display : flex ; align-items : center ; justify-content : space-between ; border-bottom : 4 px solid #8b0000 ; flex-shrink : 0 ; position : relative ; z-index : 1000 ; }
. logo { font-family : 'Press Start 2P' , monospace ; font-size : 14 px ; color : var ( -- yellow ); text-shadow : 2 px 2 px 0 #8b6000 , -1 px -1 px 0 #8b6000 ; letter-spacing : 1 px ; }
. logo span { color : white ; }
. status-pill { display : flex ; align-items : center ; gap : 8 px ; background : rgba ( 0 , 0 , 0 , 0.3 ); padding : 6 px 14 px ; border-radius : 20 px ; font-size : 12 px ; font-weight : 700 ; }
. status-dot { width : 10 px ; height : 10 px ; border-radius : 50 % ; background : #555 ; }
. status-dot . connected { background : var ( -- green ); animation : pulse 1.5 s 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 : 280 px ; background : var ( -- panel ); border-left : 3 px solid var ( -- border ); display : flex ; flex-direction : column ; overflow-y : auto ; flex-shrink : 0 ; }
. section { padding : 16 px ; border-bottom : 2 px solid var ( -- border ); }
. section-title { font-family : 'Press Start 2P' , monospace ; font-size : 8 px ; color : var ( -- yellow ); margin-bottom : 12 px ; letter-spacing : 1 px ; }
. dpad-container { display : flex ; justify-content : center ; margin : 8 px 0 ; }
. dpad { display : grid ; grid-template-columns : 52 px 52 px 52 px ; grid-template-rows : 52 px 52 px 52 px ; gap : 4 px ; }
. dpad-btn { background : var ( -- dark ); border : 2 px solid var ( -- border ); border-radius : 8 px ; color : var ( -- text ); font-size : 18 px ; cursor : pointer ; display : flex ; align-items : center ; justify-content : center ; transition : all 0.1 s ; 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 10 px rgba ( 61 , 125 , 202 , 0.5 ); }
. dpad-center { background : var ( -- darker ); border-radius : 50 % ; border : 2 px solid var ( -- border ); font-size : 10 px ; 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 : 10 px ; color : var ( -- muted ); margin-top : 6 px ; }
. key-hint kbd { background : var ( -- dark ); border : 1 px solid var ( -- border ); border-radius : 4 px ; padding : 1 px 5 px ; font-family : monospace ; font-size : 10 px ; color : var ( -- yellow ); }
. speed-label { display : flex ; justify-content : space-between ; align-items : center ; margin-bottom : 10 px ; font-weight : 700 ; }
. speed-badge { background : var ( -- blue ); padding : 3 px 10 px ; border-radius : 12 px ; font-size : 12 px ; font-weight : 800 ; }
. speed-presets { display : flex ; gap : 6 px ; margin-bottom : 10 px ; }
. preset-btn { flex : 1 ; background : var ( -- dark ); border : 2 px solid var ( -- border ); border-radius : 8 px ; color : var ( -- text ); font-family : 'Nunito' , sans-serif ; font-size : 11 px ; font-weight : 700 ; padding : 6 px 4 px ; cursor : pointer ; transition : all 0.15 s ; display : flex ; flex-direction : column ; align-items : center ; gap : 2 px ; }
. preset-btn : hover , . preset-btn . active { border-color : var ( -- yellow ); background : var ( -- border ); }
. preset-btn . active { background : var ( -- blue ); }
2026-05-25 15:50:52 -07:00
. preset-icon { font-size : 14 px ; }
2026-05-25 16:05:20 -07:00
input [ type = range ] { width : 100 % ; accent-color : var ( -- yellow ); cursor : pointer ; }
. coords-display { background : var ( -- darker ); border : 2 px solid var ( -- border ); border-radius : 8 px ; padding : 10 px 12 px ; font-family : monospace ; font-size : 12 px ; line-height : 1.8 ; }
. coords-display . label { color : var ( -- muted ); font-size : 10 px ; }
. coords-display . value { color : var ( -- yellow ); font-weight : bold ; }
. click-tip { background : rgba ( 61 , 125 , 202 , 0.15 ); border : 1 px solid rgba ( 61 , 125 , 202 , 0.4 ); border-radius : 8 px ; padding : 10 px ; font-size : 11 px ; color : var ( -- muted ); line-height : 1.6 ; margin-top : 8 px ; }
. click-tip strong { color : var ( -- text ); }
. server-warning { background : rgba ( 227 , 53 , 13 , 0.15 ); border : 2 px solid rgba ( 227 , 53 , 13 , 0.5 ); border-radius : 8 px ; padding : 12 px ; font-size : 11 px ; line-height : 1.7 ; margin : 16 px ; }
. server-warning strong { color : var ( -- yellow ); display : block ; margin-bottom : 4 px ; font-size : 10 px ; font-family : 'Press Start 2P' , monospace ; }
. move-queue-indicator { display : flex ; align-items : center ; gap : 8 px ; margin-top : 8 px ; font-size : 11 px ; color : var ( -- muted ); }
. moving-dot { width : 8 px ; height : 8 px ; border-radius : 50 % ; background : var ( -- green ); display : none ; }
. moving-dot . active { display : block ; animation : pulse 0.5 s infinite ; }
. character-marker { width : 36 px ; height : 36 px ; background : var ( -- blue ); border : 3 px solid var ( -- yellow ); border-radius : 50 % ; display : flex ; align-items : center ; justify-content : center ; font-size : 18 px ; box-shadow : 0 0 0 3 px rgba ( 61 , 125 , 202 , 0.3 ), 0 4 px 12 px rgba ( 0 , 0 , 0 , 0.5 ); animation : float 2 s ease-in-out infinite ; }
@ keyframes float { 0 %, 100 % { transform : translateY ( 0 ); } 50 % { transform : translateY ( -4 px ); } }
. destination-marker { width : 20 px ; height : 20 px ; background : var ( -- yellow ); border : 2 px solid var ( -- red ); border-radius : 50 % ; box-shadow : 0 0 8 px rgba ( 255 , 203 , 5 , 0.6 ); }
. teleport-btn { width : 100 % ; margin-top : 10 px ; padding : 10 px ; background : linear-gradient ( 135 deg , #1a0030 , #2d0050 ); border : 2 px solid #7b2fff ; border-radius : 10 px ; color : #d4aaff ; font-family : 'Press Start 2P' , monospace ; font-size : 8 px ; cursor : pointer ; display : flex ; align-items : center ; justify-content : center ; gap : 8 px ; transition : all 0.2 s ; letter-spacing : 1 px ; }
. teleport-btn : hover { background : linear-gradient ( 135 deg , #2d0050 , #4a00a0 ); border-color : #bf80ff ; color : #fff ; box-shadow : 0 0 16 px rgba ( 123 , 47 , 255 , 0.5 ); }
. teleport-btn . active { background : linear-gradient ( 135 deg , #4a00a0 , #7b2fff ); border-color : #ffcb05 ; color : #ffcb05 ; box-shadow : 0 0 20 px rgba ( 123 , 47 , 255 , 0.7 ); }
2026-05-25 15:50:52 -07:00
. teleport-btn . t-icon { font-size : 16 px ; }
2026-05-25 16:05:20 -07:00
. 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 ( 4 px ); }
. modal-overlay . visible { display : flex ; }
. modal { background : var ( -- panel ); border : 3 px solid #7b2fff ; border-radius : 16 px ; padding : 28 px 28 px 24 px ; max-width : 420 px ; width : 90 % ; box-shadow : 0 0 40 px rgba ( 123 , 47 , 255 , 0.4 ), 0 20 px 60 px rgba ( 0 , 0 , 0 , 0.6 ); animation : modalIn 0.25 s cubic-bezier ( 0.34 , 1.56 , 0.64 , 1 ); }
@ keyframes modalIn { from { opacity : 0 ; transform : scale ( 0.85 ) translateY ( 20 px ); } to { opacity : 1 ; transform : scale ( 1 ) translateY ( 0 ); } }
. modal-header { display : flex ; align-items : center ; gap : 12 px ; margin-bottom : 16 px ; }
. modal-icon { font-size : 32 px ; line-height : 1 ; }
. modal-title { font-family : 'Press Start 2P' , monospace ; font-size : 11 px ; color : #bf80ff ; line-height : 1.6 ; }
. modal-body { font-size : 13 px ; line-height : 1.8 ; color : var ( -- text ); margin-bottom : 20 px ; }
. modal-body . warning-box { background : rgba ( 227 , 53 , 13 , 0.12 ); border : 2 px solid rgba ( 227 , 53 , 13 , 0.5 ); border-radius : 10 px ; padding : 12 px 14 px ; margin-bottom : 14 px ; font-weight : 700 ; color : #ffb3a7 ; }
. modal-body . warning-box . warn-title { font-family : 'Press Start 2P' , monospace ; font-size : 8 px ; color : var ( -- red ); margin-bottom : 8 px ; display : block ; }
. modal-body . cooldown-box { background : rgba ( 123 , 47 , 255 , 0.1 ); border : 1 px solid rgba ( 123 , 47 , 255 , 0.4 ); border-radius : 10 px ; padding : 12 px 14 px ; font-size : 12 px ; color : #d4aaff ; }
. modal-body . cooldown-box strong { color : #ffcb05 ; }
. modal-dest { font-family : monospace ; font-size : 11 px ; color : var ( -- muted ); background : var ( -- darker ); border-radius : 6 px ; padding : 6 px 10 px ; margin-bottom : 16 px ; }
. modal-actions { display : flex ; gap : 10 px ; }
. modal-btn { flex : 1 ; padding : 12 px ; border-radius : 10 px ; border : 2 px solid transparent ; font-family : 'Nunito' , sans-serif ; font-size : 14 px ; font-weight : 800 ; cursor : pointer ; transition : all 0.15 s ; }
. 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 ( 135 deg , #4a00a0 , #7b2fff ); border-color : #bf80ff ; color : white ; }
. modal-btn-confirm : hover { background : linear-gradient ( 135 deg , #6200d0 , #9b4fff ); box-shadow : 0 0 16 px 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 : 20 px ; backdrop-filter : blur ( 6 px ); }
. cooldown-overlay . visible { display : flex ; }
. cooldown-title { font-family : 'Press Start 2P' , monospace ; font-size : 13 px ; color : #bf80ff ; text-align : center ; line-height : 1.8 ; }
. cooldown-ring { position : relative ; width : 140 px ; height : 140 px ; }
. cooldown-ring svg { transform : rotate ( -90 deg ); }
. 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 1 s 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 : 22 px ; color : #ffcb05 ; line-height : 1 ; }
. cooldown-time . cd-unit { font-size : 8 px ; color : var ( -- muted ); margin-top : 4 px ; }
. cooldown-msg { font-size : 13 px ; color : var ( -- muted ); text-align : center ; max-width : 300 px ; line-height : 1.7 ; }
. cooldown-skip { background : transparent ; border : 2 px solid var ( -- border ); border-radius : 8 px ; color : var ( -- muted ); font-family : 'Nunito' , sans-serif ; font-size : 12 px ; padding : 8 px 20 px ; cursor : pointer ; transition : all 0.15 s ; margin-top : 8 px ; }
. cooldown-skip : hover { border-color : var ( -- muted ); color : var ( -- text ); }
. locate-btn { width : 44 px ; height : 44 px ; background : var ( -- panel ); border : 2 px solid var ( -- border ); border-radius : 10 px ; cursor : pointer ; display : flex ; align-items : center ; justify-content : center ; box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.4 ); transition : all 0.2 s ; position : relative ; }
. locate-btn : hover { background : var ( -- border ); border-color : var ( -- blue ); box-shadow : 0 0 12 px rgba ( 61 , 125 , 202 , 0.4 ); }
. locate-btn : active { transform : scale ( 0.93 ); }
. locate-btn . locating svg { animation : locateSpin 1 s linear infinite ; }
. locate-btn . located svg # arrow { fill : var ( -- blue ); filter : drop-shadow ( 0 0 4 px rgba ( 61 , 125 , 202 , 0.8 )); }
@ keyframes locateSpin { from { transform : rotate ( 0 deg ); } to { transform : rotate ( 360 deg ); } }
. locate-toast { position : absolute ; bottom : 60 px ; left : 0 ; background : var ( -- panel ); border : 1 px solid var ( -- border ); border-radius : 8 px ; padding : 6 px 10 px ; font-size : 11 px ; white-space : nowrap ; color : var ( -- text ); pointer-events : none ; opacity : 0 ; transform : translateY ( 4 px ); transition : opacity 0.2 s , transform 0.2 s ; box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.4 ); }
. locate-toast . visible { opacity : 1 ; transform : translateY ( 0 ); }
. leaflet-control-layers { background : var ( -- panel ) !important ; border : 2 px solid var ( -- border ) !important ; border-radius : 10 px !important ; box-shadow : 0 2 px 10 px rgba ( 0 , 0 , 0 , 0.5 ) !important ; padding : 2 px 4 px !important ; margin-bottom : 8 px !important ; }
. leaflet-control-layers-list { display : flex !important ; gap : 4 px !important ; }
. leaflet-control-layers label { display : flex !important ; align-items : center !important ; gap : 5 px !important ; padding : 5 px 8 px !important ; border-radius : 7 px !important ; cursor : pointer !important ; font-family : 'Nunito' , sans-serif !important ; font-size : 12 px !important ; font-weight : 700 !important ; color : var ( -- muted ) !important ; transition : all 0.15 s !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 ; }
footer { background : var ( -- darker ); border-top : 2 px solid var ( -- border ); padding : 6 px 20 px ; font-size : 10 px ; color : var ( -- muted ); display : flex ; justify-content : space-between ; align-items : center ; flex-shrink : 0 ; }
2026-05-25 15:50:52 -07:00
</ 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 >
2026-05-25 16:05:20 -07:00
< div class = "key-hint" >< kbd > W</ kbd >< kbd > A</ kbd >< kbd > S</ kbd >< kbd > D</ kbd > or arrow keys</ div >
2026-05-25 15:50:52 -07:00
< div class = "move-queue-indicator" >
< div class = "moving-dot" id = "movingDot" ></ div >
< span id = "movingLabel" > Ready</ span >
</ div >
</ div >
2026-05-25 16:05:20 -07:00
< div class = "section" >
2026-05-25 15:50:52 -07:00
< div class = "section-title" > Speed</ div >
< div class = "speed-presets" >
2026-05-25 16:05:20 -07:00
< 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 >
2026-05-25 15:50:52 -07:00
</ div >
2026-05-25 16:05:20 -07:00
< button class = "teleport-btn" id = "teleportToggle" >< span class = "t-icon" > ✨</ span > TELEPORT MODE</ button >
2026-05-25 15:50:52 -07:00
< 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 >
2026-05-25 16:05:20 -07:00
Start < code > start.py</ code > on your Mac and connect your iPhone via USB.
2026-05-25 15:50:52 -07:00
</ 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" >
2026-05-25 16:05:20 -07:00
< div class = "modal-header" >< div class = "modal-icon" > ✨</ div >< div class = "modal-title" > TELEPORT< br > WARNING</ div ></ div >
2026-05-25 15:50:52 -07:00
< div class = "modal-body" >
2026-05-25 16:05:20 -07:00
< 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 >
2026-05-25 15:50:52 -07:00
</ 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 & 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" />
2026-05-25 16:05:20 -07:00
< circle class = "cooldown-ring-progress" id = "cooldownArc" cx = "70" cy = "70" r = "58" stroke-dasharray = "364.4" stroke-dashoffset = "0" />
2026-05-25 15:50:52 -07:00
</ svg >
2026-05-25 16:05:20 -07:00
< div class = "cooldown-time" >< span id = "cooldownDisplay" > —</ span >< span class = "cd-unit" > remaining</ span ></ div >
2026-05-25 15:50:52 -07:00
</ div >
2026-05-25 16:05:20 -07:00
< div class = "cooldown-msg" > Keep Pokémon GO < strong > closed</ strong > until this timer reaches zero to avoid a soft ban.</ div >
2026-05-25 15:50:52 -07:00
< button class = "cooldown-skip" id = "cooldownSkip" > Skip timer (not recommended)</ button >
</ div >
< script >
2026-05-25 16:05:20 -07:00
const SERVER = "http://localhost:7777" ;
2026-05-25 15:50:52 -07:00
const STEP_INTERVAL_MS = 700 ;
const _savedSpeed = parseFloat ( localStorage . getItem ( "pm_speed" ) || "1.4" );
2026-05-25 16:05:20 -07:00
const _savedMode = localStorage . getItem ( "pm_mode" ) || "walk" ;
2026-05-25 15:50:52 -07:00
let speedMs = _savedSpeed ;
2026-05-25 16:05:20 -07:00
let lat = 33.4484 ;
let lng = - 112.0740 ;
2026-05-25 15:50:52 -07:00
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 );
2026-05-25 16:05:20 -07:00
const streetLayer = L . tileLayer ( "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" , {
2026-05-25 15:50:52 -07:00
attribution : "© OpenStreetMap contributors" ,
maxZoom : 19
});
2026-05-25 16:05:20 -07:00
const satelliteLayer = L . tileLayer ( "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}" , {
2026-05-25 15:50:52 -07:00
attribution : "© Esri, Maxar, Earthstar Geographics" ,
maxZoom : 19
});
2026-05-25 16:05:20 -07:00
const satelliteLabels = L . tileLayer ( "https://server.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}" , {
2026-05-25 15:50:52 -07:00
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" );
}
});
2026-05-25 16:05:20 -07:00
map . on ( "zoomend" , () => { localStorage . setItem ( "pm_zoom" , map . getZoom ()); });
2026-05-25 15:50:52 -07:00
const modeIcons = { walk : "🚶" , jog : "🏃" , bike : "🚲" , car : "🚗" , teleport : "✨" };
let currentMode = _savedMode ;
2026-05-25 16:05:20 -07:00
function charIconHtml ( mode ) { return `<div class="character-marker"> ${ modeIcons [ mode ] || "🚶" } </div>` ; }
2026-05-25 15:50:52 -07:00
2026-05-25 16:05:20 -07:00
const charIcon = L . divIcon ({ className : "" , html : charIconHtml ( _savedMode ), iconSize : [ 36 , 36 ], iconAnchor : [ 18 , 18 ] });
2026-05-25 15:50:52 -07:00
function updateCharIcon ( mode ) {
currentMode = mode ;
2026-05-25 16:05:20 -07:00
charMarker . setIcon ( L . divIcon ({ className : "" , html : charIconHtml ( mode ), iconSize : [ 36 , 36 ], iconAnchor : [ 18 , 18 ] }));
2026-05-25 15:50:52 -07:00
}
2026-05-25 16:05:20 -07:00
const destIcon = L . divIcon ({ className : "" , html : '<div class="destination-marker"></div>' , iconSize : [ 20 , 20 ], iconAnchor : [ 10 , 10 ] });
2026-05-25 15:50:52 -07:00
const LocateControl = L . Control . extend ({
options : { position : "bottomleft" },
onAdd () {
const container = L . DomUtil . create ( "div" );
L . DomEvent . disableClickPropagation ( container );
2026-05-25 16:05:20 -07:00
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://www.w3.org/2000/svg">
<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>` ;
2026-05-25 15:50:52 -07:00
return container ;
}
});
new LocateControl (). addTo ( map );
2026-05-25 16:05:20 -07:00
const charMarker = L . marker ([ lat , lng ], { icon : charIcon }). addTo ( map );
2026-05-25 15:50:52 -07:00
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" );
2026-05-25 16:05:20 -07:00
if ( ! navigator . geolocation ) { showLocateToast ( "Geolocation not supported" , "var(--red)" ); return ; }
2026-05-25 15:50:52 -07:00
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 ) {}
}
2026-05-25 16:05:20 -07:00
function stepSize () { return ( speedMs * STEP_INTERVAL_MS / 1000 ); }
2026-05-25 15:50:52 -07:00
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 ;
2026-05-25 16:05:20 -07:00
getActiveDirections (). forEach ( d => { const btn = document . getElementById ( `btn- ${ d } ` ); if ( btn ) btn . classList . add ( "pressed" ); });
2026-05-25 15:50:52 -07:00
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 ];
2026-05-25 16:05:20 -07:00
if ( dir ) { const btn = document . getElementById ( `btn- ${ dir } ` ); if ( btn ) btn . classList . remove ( "pressed" ); }
2026-05-25 15:50:52 -07:00
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 () {
2026-05-25 16:05:20 -07:00
if ( waypointQueue . length === 0 ) { setMoving ( false ); clickAnimating = false ; return ; }
2026-05-25 15:50:52 -07:00
const wp = waypointQueue [ 0 ];
const path = buildPath ( lat , lng , wp . lat , wp . lng );
2026-05-25 16:05:20 -07:00
if ( path . length === 0 ) { waypointQueue . shift (); rebuildWaypointVisuals (); processNextWaypoint (); return ; }
2026-05-25 15:50:52 -07:00
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 ();
2026-05-25 16:05:20 -07:00
if ( ! clickAnimating ) processNextWaypoint ();
2026-05-25 15:50:52 -07:00
}
map . on ( "click" , e => {
const toLat = e . latlng . lat ;
const toLng = e . latlng . lng ;
2026-05-25 16:05:20 -07:00
if ( teleportMode ) { showTeleportModal ( toLat , toLng ); return ; }
2026-05-25 15:50:52 -07:00
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" ;
} else {
2026-05-25 16:05:20 -07:00
clickTip . innerHTML = '<strong>Click the map</strong> to add waypoints. The player moves through them in order. Click multiple times to build a route.' ;
2026-05-25 15:50:52 -07:00
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" ;
}
}
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 () {
2026-05-25 16:05:20 -07:00
if ( remaining <= 0 ) { clearInterval ( cooldownTimer ); cooldownOverlay . classList . remove ( "visible" ); setTeleportMode ( false ); return ; }
2026-05-25 15:50:52 -07:00
const mins = Math . floor ( remaining / 60 );
const secs = remaining % 60 ;
2026-05-25 16:05:20 -07:00
document . getElementById ( "cooldownDisplay" ). textContent = mins > 0 ? ` ${ mins } : ${ String ( secs ). padStart ( 2 , "0" ) } ` : ` ${ secs } s` ;
2026-05-25 15:50:52 -07:00
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 >