1476 lines
39 KiB
HTML
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 & 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>
|