- Added hunt/feed duck mechanics (80% hunt, 20% feed) - Implemented persistent scoring system - Added channel control commands (\!stopducks/\!startducks) - Enhanced duck hunt with wrong action penalties - Organized bot structure with botmain.js as main file - Added comprehensive documentation (README.md) - Included 17 plugins with various games and utilities 🦆 Duck Hunt Features: - Hunt ducks with \!shoot/\!bang (80% of spawns) - Feed ducks with \!feed (20% of spawns) - Persistent scores saved to JSON - Channel-specific controls for #bakedbeans - Reaction time tracking and special achievements 🎮 Other Games: - Casino games (slots, coinflip, hi-lo, scratch cards) - Multiplayer games (pigs, zombie dice, quiplash) - Text generation (babble, conspiracy, drunk historian) - Interactive features (story writing, emojify, combos) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1288 lines
No EOL
43 KiB
HTML
1288 lines
No EOL
43 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Pet Battles World Map</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: #333;
|
||
overflow: hidden;
|
||
height: 100vh;
|
||
}
|
||
|
||
.map-container {
|
||
position: relative;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.world-map {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(135deg, #87CEEB 0%, #98FB98 30%, #F4A460 60%, #DDA0DD 100%);
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Biome Areas */
|
||
.biome {
|
||
position: absolute;
|
||
border-radius: 20px;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 2em;
|
||
font-weight: bold;
|
||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||
color: white;
|
||
border: 3px solid rgba(255,255,255,0.3);
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.biome:hover {
|
||
transform: scale(1.05);
|
||
border-color: rgba(255,255,255,0.8);
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.biome-town {
|
||
top: 45%;
|
||
left: 45%;
|
||
width: 10%;
|
||
height: 10%;
|
||
background: linear-gradient(135deg, #8B4513, #A0522D);
|
||
}
|
||
|
||
.biome-forest {
|
||
top: 20%;
|
||
left: 15%;
|
||
width: 25%;
|
||
height: 30%;
|
||
background: linear-gradient(135deg, #228B22, #32CD32);
|
||
}
|
||
|
||
.biome-mountain {
|
||
top: 10%;
|
||
left: 60%;
|
||
width: 30%;
|
||
height: 35%;
|
||
background: linear-gradient(135deg, #696969, #2F4F4F);
|
||
}
|
||
|
||
.biome-ocean {
|
||
top: 65%;
|
||
left: 10%;
|
||
width: 35%;
|
||
height: 25%;
|
||
background: linear-gradient(135deg, #4682B4, #1E90FF);
|
||
}
|
||
|
||
.biome-desert {
|
||
top: 55%;
|
||
left: 65%;
|
||
width: 30%;
|
||
height: 30%;
|
||
background: linear-gradient(135deg, #DAA520, #B8860B);
|
||
}
|
||
|
||
/* Player Icons */
|
||
.player {
|
||
position: absolute;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, #FF6B6B, #4ECDC4);
|
||
border: 3px solid white;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 18px;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||
z-index: 100;
|
||
}
|
||
|
||
.player:hover {
|
||
transform: scale(1.2);
|
||
box-shadow: 0 6px 25px rgba(0,0,0,0.4);
|
||
}
|
||
|
||
.player.exploring {
|
||
background: linear-gradient(135deg, #32CD32, #90EE90);
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.player.encountering {
|
||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||
animation: encounter 1s infinite;
|
||
}
|
||
|
||
.player.battling {
|
||
background: linear-gradient(135deg, #FF4500, #DC143C);
|
||
animation: battle 0.5s infinite alternate;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { transform: scale(1); }
|
||
50% { transform: scale(1.1); }
|
||
}
|
||
|
||
@keyframes encounter {
|
||
0%, 100% { box-shadow: 0 4px 15px rgba(255,215,0,0.5); }
|
||
50% { box-shadow: 0 4px 25px rgba(255,215,0,0.8); }
|
||
}
|
||
|
||
@keyframes battle {
|
||
from { transform: rotate(-2deg); }
|
||
to { transform: rotate(2deg); }
|
||
}
|
||
|
||
/* Player Info Panel */
|
||
.player-info {
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: white;
|
||
border-radius: 15px;
|
||
padding: 30px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||
z-index: 1000;
|
||
max-width: 500px;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
display: none;
|
||
}
|
||
|
||
.player-info.show {
|
||
display: block;
|
||
animation: slideIn 0.3s ease;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
|
||
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||
}
|
||
|
||
.close-btn {
|
||
position: absolute;
|
||
top: 15px;
|
||
right: 20px;
|
||
background: #ff4757;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50%;
|
||
width: 30px;
|
||
height: 30px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.player-header {
|
||
text-align: center;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 15px;
|
||
border-bottom: 2px solid #eee;
|
||
}
|
||
|
||
.player-name {
|
||
font-size: 1.5em;
|
||
font-weight: bold;
|
||
color: #2c3e50;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.player-location {
|
||
color: #7f8c8d;
|
||
font-size: 1.1em;
|
||
}
|
||
|
||
.info-section {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 1.2em;
|
||
font-weight: bold;
|
||
color: #2c3e50;
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.pet-card {
|
||
background: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 8px;
|
||
padding: 10px;
|
||
margin-bottom: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.pet-card.active {
|
||
border-color: #28a745;
|
||
background: #d4edda;
|
||
}
|
||
|
||
.pet-emoji {
|
||
font-size: 1.5em;
|
||
}
|
||
|
||
.pet-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.pet-name {
|
||
font-weight: bold;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.pet-stats {
|
||
font-size: 0.9em;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.hp-bar {
|
||
width: 100%;
|
||
height: 8px;
|
||
background: #dee2e6;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.hp-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #28a745, #20c997);
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.item-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
.item-card {
|
||
background: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 6px;
|
||
padding: 8px;
|
||
text-align: center;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.item-name {
|
||
font-weight: bold;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.item-quantity {
|
||
color: #6c757d;
|
||
font-size: 0.8em;
|
||
}
|
||
|
||
/* UI Controls */
|
||
.controls {
|
||
position: fixed;
|
||
top: 20px;
|
||
left: 20px;
|
||
z-index: 200;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.control-panel {
|
||
background: rgba(255,255,255,0.9);
|
||
backdrop-filter: blur(10px);
|
||
border-radius: 10px;
|
||
padding: 15px;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.refresh-btn {
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
padding: 10px 20px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.refresh-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
.auto-refresh {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.legend {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
background: rgba(255,255,255,0.9);
|
||
backdrop-filter: blur(10px);
|
||
border-radius: 10px;
|
||
padding: 15px;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||
z-index: 200;
|
||
}
|
||
|
||
.legend-title {
|
||
font-weight: bold;
|
||
margin-bottom: 10px;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 5px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.status-indicator {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.status-exploring { background: linear-gradient(135deg, #32CD32, #90EE90); }
|
||
.status-encountering { background: linear-gradient(135deg, #FFD700, #FFA500); }
|
||
.status-battling { background: linear-gradient(135deg, #FF4500, #DC143C); }
|
||
.status-idle { background: linear-gradient(135deg, #FF6B6B, #4ECDC4); }
|
||
|
||
/* Stats Display */
|
||
.stats-panel {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
left: 20px;
|
||
background: rgba(255,255,255,0.9);
|
||
backdrop-filter: blur(10px);
|
||
border-radius: 10px;
|
||
padding: 15px;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||
z-index: 200;
|
||
}
|
||
|
||
.stat-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 5px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.stat-label {
|
||
color: #6c757d;
|
||
}
|
||
|
||
.stat-value {
|
||
font-weight: bold;
|
||
color: #2c3e50;
|
||
}
|
||
|
||
/* Overlay */
|
||
.overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0,0,0,0.5);
|
||
z-index: 999;
|
||
display: none;
|
||
}
|
||
|
||
.overlay.show {
|
||
display: block;
|
||
}
|
||
|
||
/* Responsive Design */
|
||
@media (max-width: 768px) {
|
||
.controls, .legend, .stats-panel {
|
||
position: relative;
|
||
margin: 10px;
|
||
}
|
||
|
||
.player-info {
|
||
max-width: 90vw;
|
||
max-height: 90vh;
|
||
padding: 20px;
|
||
}
|
||
|
||
.biome {
|
||
font-size: 1.5em;
|
||
}
|
||
}
|
||
|
||
/* Loading Animation */
|
||
.loading {
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
z-index: 1001;
|
||
display: none;
|
||
}
|
||
|
||
.loading.show {
|
||
display: block;
|
||
}
|
||
|
||
.spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 4px solid #f3f3f3;
|
||
border-top: 4px solid #667eea;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="map-container">
|
||
<!-- World Map -->
|
||
<div class="world-map" id="worldMap">
|
||
<!-- Biomes -->
|
||
<div class="biome biome-town" data-biome="town" title="Trainer Town">
|
||
🏘️<br><small style="font-size:0.5em;">Town</small>
|
||
</div>
|
||
<div class="biome biome-forest" data-biome="forest" title="Mystic Forest">
|
||
🌲<br><small style="font-size:0.5em;">Forest</small>
|
||
</div>
|
||
<div class="biome biome-mountain" data-biome="mountain" title="Dragon Peaks">
|
||
🏔️<br><small style="font-size:0.5em;">Mountain</small>
|
||
</div>
|
||
<div class="biome biome-ocean" data-biome="ocean" title="Crystal Shores">
|
||
🌊<br><small style="font-size:0.5em;">Ocean</small>
|
||
</div>
|
||
<div class="biome biome-desert" data-biome="desert" title="Thunder Desert">
|
||
🏜️<br><small style="font-size:0.5em;">Desert</small>
|
||
</div>
|
||
|
||
<!-- Players will be dynamically added here -->
|
||
</div>
|
||
|
||
<!-- Controls -->
|
||
<div class="controls">
|
||
<div class="control-panel">
|
||
<button class="refresh-btn" onclick="refreshMap()">🔄 Refresh Map</button>
|
||
<div class="auto-refresh">
|
||
<input type="checkbox" id="autoRefresh" checked>
|
||
<label for="autoRefresh">Auto-refresh (5s)</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Legend -->
|
||
<div class="legend">
|
||
<div class="legend-title">Player Status</div>
|
||
<div class="legend-item">
|
||
<div class="status-indicator status-idle"></div>
|
||
<span>Idle</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="status-indicator status-exploring"></div>
|
||
<span>Exploring</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="status-indicator status-encountering"></div>
|
||
<span>Wild Encounter</span>
|
||
</div>
|
||
<div class="legend-item">
|
||
<div class="status-indicator status-battling"></div>
|
||
<span>In Battle</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Stats Panel -->
|
||
<div class="stats-panel">
|
||
<div class="legend-title">World Stats</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">Active Players:</span>
|
||
<span class="stat-value" id="playerCount">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">Wild Encounters:</span>
|
||
<span class="stat-value" id="encounterCount">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">Active Battles:</span>
|
||
<span class="stat-value" id="battleCount">0</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">Last Update:</span>
|
||
<span class="stat-value" id="lastUpdate">Never</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Player Info Modal -->
|
||
<div class="overlay" id="overlay" onclick="closePlayerInfo()"></div>
|
||
<div class="player-info" id="playerInfo">
|
||
<button class="close-btn" onclick="closePlayerInfo()">×</button>
|
||
<div class="player-header">
|
||
<div class="player-name" id="playerName">Player Name</div>
|
||
<div class="player-location" id="playerLocation">Current Location</div>
|
||
</div>
|
||
|
||
<div class="info-section">
|
||
<div class="section-title">🎒 Active Party</div>
|
||
<div id="playerParty">
|
||
<!-- Party pets will be loaded here -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-section">
|
||
<div class="section-title">📦 Inventory</div>
|
||
<div class="item-grid" id="playerInventory">
|
||
<!-- Items will be loaded here -->
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-section">
|
||
<div class="section-title">📊 Statistics</div>
|
||
<div id="playerStats">
|
||
<!-- Stats will be loaded here -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Loading Indicator -->
|
||
<div class="loading" id="loading">
|
||
<div class="spinner"></div>
|
||
</div>
|
||
|
||
<script>
|
||
// Global variables
|
||
let gameData = null;
|
||
let autoRefreshInterval = null;
|
||
let selectedPlayer = null;
|
||
|
||
// Biome positioning data
|
||
const biomePositions = {
|
||
town: { x: 50, y: 50 },
|
||
forest: { x: 27.5, y: 35 },
|
||
mountain: { x: 75, y: 27.5 },
|
||
ocean: { x: 27.5, y: 77.5 },
|
||
desert: { x: 80, y: 70 }
|
||
};
|
||
|
||
// Pet type emojis
|
||
const petTypeEmojis = {
|
||
normal: '🐱',
|
||
fire: '🔥',
|
||
water: '💧',
|
||
grass: '🌱',
|
||
electric: '⚡',
|
||
fighting: '🥊',
|
||
psychic: '🌟',
|
||
ghost: '👻',
|
||
dragon: '🐉'
|
||
};
|
||
|
||
// Pet species to type mapping
|
||
const petSpeciesTypes = {
|
||
'Whiskers': 'normal',
|
||
'Flame': 'fire',
|
||
'Splash': 'water',
|
||
'Leafy': 'grass',
|
||
'Sparky': 'electric',
|
||
'Boxer': 'fighting',
|
||
'Mystic': 'psychic',
|
||
'Phantom': 'ghost',
|
||
'Draco': 'dragon',
|
||
'Phoenix': 'fire'
|
||
};
|
||
|
||
// Initialize the map
|
||
function initMap() {
|
||
console.log('Initializing Pet Battles World Map...');
|
||
refreshMap();
|
||
startAutoRefresh();
|
||
}
|
||
|
||
// Refresh map data
|
||
async function refreshMap() {
|
||
showLoading(true);
|
||
try {
|
||
const response = await fetch('/api/gamestate');
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
gameData = await response.json();
|
||
updateMap();
|
||
updateStats();
|
||
} catch (error) {
|
||
console.error('Error fetching game data:', error);
|
||
// Show mock data for demonstration
|
||
gameData = generateMockData();
|
||
updateMap();
|
||
updateStats();
|
||
}
|
||
showLoading(false);
|
||
}
|
||
|
||
// Generate mock data for demonstration
|
||
function generateMockData() {
|
||
const mockPlayers = [
|
||
{
|
||
nick: 'TrainerAlex',
|
||
location: { biome: 'forest', x: 25, y: 30 },
|
||
activePet: { nickname: 'Leafy', species_name: 'Leafy', level: 12, hp: 45, max_hp: 60 },
|
||
totalPets: 5,
|
||
status: 'exploring'
|
||
},
|
||
{
|
||
nick: 'FireMaster99',
|
||
location: { biome: 'mountain', x: 70, y: 25 },
|
||
activePet: { nickname: 'Blaze', species_name: 'Phoenix', level: 25, hp: 80, max_hp: 95 },
|
||
totalPets: 8,
|
||
status: 'encountering'
|
||
},
|
||
{
|
||
nick: 'WaterTrainer',
|
||
location: { biome: 'ocean', x: 30, y: 80 },
|
||
activePet: { nickname: 'Splash', species_name: 'Splash', level: 8, hp: 32, max_hp: 40 },
|
||
totalPets: 3,
|
||
status: 'idle'
|
||
},
|
||
{
|
||
nick: 'ElectricAce',
|
||
location: { biome: 'desert', x: 75, y: 65 },
|
||
activePet: { nickname: 'Thunder', species_name: 'Sparky', level: 15, hp: 28, max_hp: 45 },
|
||
totalPets: 6,
|
||
status: 'battling'
|
||
}
|
||
];
|
||
|
||
return {
|
||
players: mockPlayers,
|
||
encounters: [
|
||
{
|
||
player: 'FireMaster99',
|
||
pet: { name: 'Draco', level: 18, rarity: 'rare' },
|
||
location: { biome: 'mountain', x: 70, y: 25 }
|
||
}
|
||
],
|
||
battles: [],
|
||
biomes: {
|
||
town: { name: 'Trainer Town', emoji: '🏘️' },
|
||
forest: { name: 'Mystic Forest', emoji: '🌲' },
|
||
mountain: { name: 'Dragon Peaks', emoji: '🏔️' },
|
||
ocean: { name: 'Crystal Shores', emoji: '🌊' },
|
||
desert: { name: 'Thunder Desert', emoji: '🏜️' }
|
||
}
|
||
};
|
||
}
|
||
|
||
// Update map with current data
|
||
function updateMap() {
|
||
const worldMap = document.getElementById('worldMap');
|
||
|
||
// Remove existing players
|
||
const existingPlayers = worldMap.querySelectorAll('.player');
|
||
existingPlayers.forEach(player => player.remove());
|
||
|
||
// Add current players
|
||
if (gameData && gameData.players) {
|
||
gameData.players.forEach(player => {
|
||
createPlayerElement(player);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Create player element on map
|
||
function createPlayerElement(player) {
|
||
const worldMap = document.getElementById('worldMap');
|
||
const playerElement = document.createElement('div');
|
||
|
||
playerElement.className = `player ${player.status || 'idle'}`;
|
||
playerElement.style.left = `${player.location.x}%`;
|
||
playerElement.style.top = `${player.location.y}%`;
|
||
playerElement.title = `${player.nick} - ${player.location.biome}`;
|
||
playerElement.onclick = () => showPlayerInfo(player);
|
||
|
||
// Add pet emoji if available
|
||
if (player.activePet) {
|
||
const petType = petSpeciesTypes[player.activePet.species_name] || 'normal';
|
||
playerElement.innerHTML = petTypeEmojis[petType] || '🐾';
|
||
} else {
|
||
playerElement.innerHTML = '👤';
|
||
}
|
||
|
||
worldMap.appendChild(playerElement);
|
||
}
|
||
|
||
// Show player information modal
|
||
async function showPlayerInfo(player) {
|
||
selectedPlayer = player;
|
||
|
||
// Update basic info
|
||
document.getElementById('playerName').textContent = player.nick;
|
||
document.getElementById('playerLocation').textContent =
|
||
`${gameData.biomes[player.location.biome]?.emoji || ''} ${gameData.biomes[player.location.biome]?.name || player.location.biome}`;
|
||
|
||
// Load detailed player data
|
||
try {
|
||
showLoading(true);
|
||
const detailedData = await fetchPlayerDetails(player.nick);
|
||
populatePlayerInfo(detailedData);
|
||
} catch (error) {
|
||
console.error('Error loading player details:', error);
|
||
populatePlayerInfo(generateMockPlayerData(player));
|
||
}
|
||
|
||
showLoading(false);
|
||
document.getElementById('overlay').classList.add('show');
|
||
document.getElementById('playerInfo').classList.add('show');
|
||
}
|
||
|
||
// Fetch detailed player information
|
||
async function fetchPlayerDetails(playerNick) {
|
||
const response = await fetch(`/api/player/${playerNick}`);
|
||
if (!response.ok) {
|
||
throw new Error('Player data not available');
|
||
}
|
||
return await response.json();
|
||
}
|
||
|
||
// Generate mock player data
|
||
function generateMockPlayerData(player) {
|
||
const mockParty = [
|
||
{
|
||
nickname: player.activePet?.nickname || 'Starter',
|
||
species_name: player.activePet?.species_name || 'Whiskers',
|
||
level: player.activePet?.level || 5,
|
||
hp: player.activePet?.hp || 30,
|
||
max_hp: player.activePet?.max_hp || 40,
|
||
party_slot: 1
|
||
},
|
||
{
|
||
nickname: 'Buddy',
|
||
species_name: 'Flame',
|
||
level: 8,
|
||
hp: 25,
|
||
max_hp: 35,
|
||
party_slot: 2
|
||
}
|
||
];
|
||
|
||
const mockInventory = [
|
||
{ item_name: 'Potion', quantity: 5 },
|
||
{ item_name: 'Great Ball', quantity: 3 },
|
||
{ item_name: 'Rare Candy', quantity: 2 }
|
||
];
|
||
|
||
return {
|
||
party: mockParty,
|
||
inventory: mockInventory,
|
||
stats: {
|
||
totalPets: player.totalPets || 3,
|
||
wins: Math.floor(Math.random() * 20),
|
||
battles: Math.floor(Math.random() * 30),
|
||
badges: Math.floor(Math.random() * 5)
|
||
}
|
||
};
|
||
}
|
||
|
||
// Populate player info modal
|
||
function populatePlayerInfo(data) {
|
||
// Party
|
||
const partyContainer = document.getElementById('playerParty');
|
||
partyContainer.innerHTML = '';
|
||
|
||
if (data.party && data.party.length > 0) {
|
||
data.party.forEach(pet => {
|
||
const petCard = createPetCard(pet);
|
||
partyContainer.appendChild(petCard);
|
||
});
|
||
} else {
|
||
partyContainer.innerHTML = '<p style="color: #6c757d; text-align: center;">No pets in party</p>';
|
||
}
|
||
|
||
// Inventory
|
||
const inventoryContainer = document.getElementById('playerInventory');
|
||
inventoryContainer.innerHTML = '';
|
||
|
||
if (data.inventory && data.inventory.length > 0) {
|
||
data.inventory.forEach(item => {
|
||
const itemCard = createItemCard(item);
|
||
inventoryContainer.appendChild(itemCard);
|
||
});
|
||
} else {
|
||
inventoryContainer.innerHTML = '<p style="color: #6c757d; text-align: center;">No items</p>';
|
||
}
|
||
|
||
// Stats
|
||
const statsContainer = document.getElementById('playerStats');
|
||
statsContainer.innerHTML = '';
|
||
|
||
if (data.stats) {
|
||
const stats = [
|
||
{ label: 'Total Pets', value: data.stats.totalPets || 0 },
|
||
{ label: 'Battles Won', value: data.stats.wins || 0 },
|
||
{ label: 'Total Battles', value: data.stats.battles || 0 },
|
||
{ label: 'Gym Badges', value: data.stats.badges || 0 }
|
||
];
|
||
|
||
stats.forEach(stat => {
|
||
const statElement = document.createElement('div');
|
||
statElement.className = 'stat-item';
|
||
statElement.innerHTML = `
|
||
<span class="stat-label">${stat.label}:</span>
|
||
<span class="stat-value">${stat.value}</span>
|
||
`;
|
||
statsContainer.appendChild(statElement);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Create pet card element
|
||
function createPetCard(pet) {
|
||
const petCard = document.createElement('div');
|
||
petCard.className = `pet-card ${pet.party_slot === 1 ? 'active' : ''}`;
|
||
|
||
const petType = petSpeciesTypes[pet.species_name] || 'normal';
|
||
const petEmoji = petTypeEmojis[petType] || '🐾';
|
||
const hpPercentage = (pet.hp / pet.max_hp) * 100;
|
||
|
||
petCard.innerHTML = `
|
||
<div class="pet-emoji">${petEmoji}</div>
|
||
<div class="pet-info">
|
||
<div class="pet-name">${pet.nickname}</div>
|
||
<div class="pet-stats">Lv.${pet.level} ${pet.species_name}</div>
|
||
<div class="hp-bar">
|
||
<div class="hp-fill" style="width: ${hpPercentage}%"></div>
|
||
</div>
|
||
<div style="font-size: 0.8em; color: #6c757d; margin-top: 2px;">
|
||
${pet.hp}/${pet.max_hp} HP
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
return petCard;
|
||
}
|
||
|
||
// Create item card element
|
||
function createItemCard(item) {
|
||
const itemCard = document.createElement('div');
|
||
itemCard.className = 'item-card';
|
||
|
||
// Get item emoji based on type
|
||
const itemEmojis = {
|
||
'Potion': '🧪',
|
||
'Super Potion': '🧪',
|
||
'Hyper Potion': '🧪',
|
||
'Rare Candy': '🍬',
|
||
'Lucky Egg': '🥚',
|
||
'Great Ball': '🥎',
|
||
'Ultra Ball': '⚾',
|
||
'Master Ball': '🔮',
|
||
'Power Bracer': '💪',
|
||
'Quick Claw': '⚡'
|
||
};
|
||
|
||
const emoji = itemEmojis[item.item_name] || '📦';
|
||
|
||
itemCard.innerHTML = `
|
||
<div style="font-size: 1.2em; margin-bottom: 4px;">${emoji}</div>
|
||
<div class="item-name">${item.item_name}</div>
|
||
<div class="item-quantity">x${item.quantity}</div>
|
||
`;
|
||
|
||
return itemCard;
|
||
}
|
||
|
||
// Close player info modal
|
||
function closePlayerInfo() {
|
||
document.getElementById('overlay').classList.remove('show');
|
||
document.getElementById('playerInfo').classList.remove('show');
|
||
selectedPlayer = null;
|
||
}
|
||
|
||
// Update world statistics
|
||
function updateStats() {
|
||
if (!gameData) return;
|
||
|
||
const playerCount = gameData.players ? gameData.players.length : 0;
|
||
const encounterCount = gameData.encounters ? gameData.encounters.length : 0;
|
||
const battleCount = gameData.battles ? gameData.battles.length : 0;
|
||
|
||
document.getElementById('playerCount').textContent = playerCount;
|
||
document.getElementById('encounterCount').textContent = encounterCount;
|
||
document.getElementById('battleCount').textContent = battleCount;
|
||
document.getElementById('lastUpdate').textContent = new Date().toLocaleTimeString();
|
||
}
|
||
|
||
// Show/hide loading indicator
|
||
function showLoading(show) {
|
||
const loading = document.getElementById('loading');
|
||
if (show) {
|
||
loading.classList.add('show');
|
||
} else {
|
||
loading.classList.remove('show');
|
||
}
|
||
}
|
||
|
||
// Auto-refresh functionality
|
||
function startAutoRefresh() {
|
||
const checkbox = document.getElementById('autoRefresh');
|
||
|
||
function updateAutoRefresh() {
|
||
if (autoRefreshInterval) {
|
||
clearInterval(autoRefreshInterval);
|
||
autoRefreshInterval = null;
|
||
}
|
||
|
||
if (checkbox.checked) {
|
||
autoRefreshInterval = setInterval(refreshMap, 5000); // 5 seconds
|
||
}
|
||
}
|
||
|
||
checkbox.addEventListener('change', updateAutoRefresh);
|
||
updateAutoRefresh(); // Start immediately if checked
|
||
}
|
||
|
||
// Biome click handlers
|
||
function setupBiomeHandlers() {
|
||
const biomes = document.querySelectorAll('.biome');
|
||
biomes.forEach(biome => {
|
||
biome.addEventListener('click', (e) => {
|
||
const biomeName = e.target.dataset.biome;
|
||
showBiomeInfo(biomeName);
|
||
});
|
||
});
|
||
}
|
||
|
||
// Show biome information
|
||
function showBiomeInfo(biomeName) {
|
||
const biomeData = {
|
||
town: {
|
||
name: 'Trainer Town',
|
||
description: 'The starting area for new trainers. A peaceful place with basic amenities.',
|
||
commonPets: ['Whiskers', 'Boxer'],
|
||
gymLeader: null
|
||
},
|
||
forest: {
|
||
name: 'Mystic Forest',
|
||
description: 'A dense woodland filled with grass and ghost type pets.',
|
||
commonPets: ['Whiskers', 'Leafy', 'Phantom'],
|
||
rarePets: ['Mystic'],
|
||
gymLeader: 'Ranger Oak'
|
||
},
|
||
mountain: {
|
||
name: 'Dragon Peaks',
|
||
description: 'Towering mountains where fire and dragon types thrive.',
|
||
commonPets: ['Flame', 'Boxer'],
|
||
rarePets: ['Draco', 'Phoenix'],
|
||
gymLeader: 'Flame Master Ash'
|
||
},
|
||
ocean: {
|
||
name: 'Crystal Shores',
|
||
description: 'Pristine waters home to water and psychic type pets.',
|
||
commonPets: ['Splash'],
|
||
rarePets: ['Mystic'],
|
||
gymLeader: 'Captain Marina'
|
||
},
|
||
desert: {
|
||
name: 'Thunder Desert',
|
||
description: 'An arid landscape crackling with electric energy.',
|
||
commonPets: ['Sparky', 'Flame'],
|
||
rarePets: ['Phoenix'],
|
||
gymLeader: 'Thunder Sage Volt'
|
||
}
|
||
};
|
||
|
||
const biome = biomeData[biomeName];
|
||
if (!biome) return;
|
||
|
||
const playersInBiome = gameData.players ?
|
||
gameData.players.filter(p => p.location.biome === biomeName) : [];
|
||
|
||
alert(`🏞️ ${biome.name}\n\n${biome.description}\n\nCommon Pets: ${biome.commonPets.join(', ')}\n${biome.rarePets ? `Rare Pets: ${biome.rarePets.join(', ')}\n` : ''}${biome.gymLeader ? `Gym Leader: ${biome.gymLeader}\n` : ''}Players Here: ${playersInBiome.length}`);
|
||
}
|
||
|
||
// Keyboard shortcuts
|
||
function setupKeyboardShortcuts() {
|
||
document.addEventListener('keydown', (e) => {
|
||
switch(e.key) {
|
||
case 'r':
|
||
case 'R':
|
||
if (!e.ctrlKey && !e.metaKey) {
|
||
refreshMap();
|
||
}
|
||
break;
|
||
case 'Escape':
|
||
closePlayerInfo();
|
||
break;
|
||
case ' ':
|
||
e.preventDefault();
|
||
const checkbox = document.getElementById('autoRefresh');
|
||
checkbox.checked = !checkbox.checked;
|
||
checkbox.dispatchEvent(new Event('change'));
|
||
break;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Tooltip system
|
||
function setupTooltips() {
|
||
// Add tooltips to various elements
|
||
const tooltips = {
|
||
'.refresh-btn': 'Refresh map data (R)',
|
||
'.biome': 'Click to view biome details',
|
||
'.player': 'Click to view trainer details',
|
||
'#autoRefresh': 'Toggle auto-refresh (Space)'
|
||
};
|
||
|
||
Object.entries(tooltips).forEach(([selector, text]) => {
|
||
const elements = document.querySelectorAll(selector);
|
||
elements.forEach(el => {
|
||
if (!el.title) {
|
||
el.title = text;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// Enhanced player status detection
|
||
function getPlayerStatusClass(player) {
|
||
if (!gameData) return 'idle';
|
||
|
||
// Check for encounters
|
||
const hasEncounter = gameData.encounters &&
|
||
gameData.encounters.some(enc => enc.player === player.nick);
|
||
if (hasEncounter) return 'encountering';
|
||
|
||
// Check for battles
|
||
const inBattle = gameData.battles &&
|
||
gameData.battles.some(battle =>
|
||
battle.player1 === player.nick || battle.player2 === player.nick);
|
||
if (inBattle) return 'battling';
|
||
|
||
// Check exploration status (recently moved)
|
||
const now = Date.now();
|
||
const lastMoved = player.location.lastMoved || 0;
|
||
const timeSinceMove = now - lastMoved;
|
||
if (timeSinceMove < 30000) { // 30 seconds
|
||
return 'exploring';
|
||
}
|
||
|
||
return 'idle';
|
||
}
|
||
|
||
// Add encounter indicators
|
||
function addEncounterIndicators() {
|
||
if (!gameData.encounters) return;
|
||
|
||
gameData.encounters.forEach(encounter => {
|
||
const encounterElement = document.createElement('div');
|
||
encounterElement.className = 'encounter-indicator';
|
||
encounterElement.style.cssText = `
|
||
position: absolute;
|
||
left: ${encounter.location.x + 2}%;
|
||
top: ${encounter.location.y - 2}%;
|
||
width: 20px;
|
||
height: 20px;
|
||
background: radial-gradient(circle, #FFD700, #FFA500);
|
||
border-radius: 50%;
|
||
animation: pulse 1s infinite;
|
||
z-index: 99;
|
||
pointer-events: none;
|
||
`;
|
||
encounterElement.title = `Wild ${encounter.pet.name} (Lv.${encounter.pet.level})`;
|
||
|
||
document.getElementById('worldMap').appendChild(encounterElement);
|
||
});
|
||
}
|
||
|
||
// Weather effects (cosmetic)
|
||
function addWeatherEffects() {
|
||
const weather = ['sunny', 'rainy', 'cloudy'][Math.floor(Math.random() * 3)];
|
||
const worldMap = document.getElementById('worldMap');
|
||
|
||
// Remove existing weather
|
||
const existingWeather = worldMap.querySelector('.weather-overlay');
|
||
if (existingWeather) {
|
||
existingWeather.remove();
|
||
}
|
||
|
||
if (weather === 'rainy') {
|
||
const rainOverlay = document.createElement('div');
|
||
rainOverlay.className = 'weather-overlay';
|
||
rainOverlay.style.cssText = `
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(transparent, rgba(173, 216, 230, 0.3));
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
`;
|
||
worldMap.appendChild(rainOverlay);
|
||
|
||
// Add rain animation
|
||
for (let i = 0; i < 50; i++) {
|
||
const raindrop = document.createElement('div');
|
||
raindrop.style.cssText = `
|
||
position: absolute;
|
||
width: 2px;
|
||
height: 10px;
|
||
background: linear-gradient(transparent, rgba(173, 216, 230, 0.8));
|
||
left: ${Math.random() * 100}%;
|
||
top: -10px;
|
||
animation: rain ${Math.random() * 2 + 1}s linear infinite;
|
||
animation-delay: ${Math.random() * 2}s;
|
||
`;
|
||
rainOverlay.appendChild(raindrop);
|
||
}
|
||
|
||
// Add rain animation keyframes
|
||
if (!document.querySelector('#rain-style')) {
|
||
const style = document.createElement('style');
|
||
style.id = 'rain-style';
|
||
style.textContent = `
|
||
@keyframes rain {
|
||
to { transform: translateY(100vh); }
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Performance monitoring
|
||
function monitorPerformance() {
|
||
let lastFrameTime = performance.now();
|
||
let frameCount = 0;
|
||
let fps = 0;
|
||
|
||
function updateFPS() {
|
||
const now = performance.now();
|
||
frameCount++;
|
||
|
||
if (now - lastFrameTime >= 1000) {
|
||
fps = Math.round((frameCount * 1000) / (now - lastFrameTime));
|
||
frameCount = 0;
|
||
lastFrameTime = now;
|
||
|
||
// Update FPS display if needed
|
||
console.log(`Map FPS: ${fps}`);
|
||
}
|
||
|
||
requestAnimationFrame(updateFPS);
|
||
}
|
||
|
||
requestAnimationFrame(updateFPS);
|
||
}
|
||
|
||
// Initialize everything when page loads
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
console.log('🗺️ Pet Battles World Map Loading...');
|
||
|
||
initMap();
|
||
setupBiomeHandlers();
|
||
setupKeyboardShortcuts();
|
||
setupTooltips();
|
||
monitorPerformance();
|
||
|
||
// Add weather effects periodically
|
||
setInterval(addWeatherEffects, 60000); // Every minute
|
||
addWeatherEffects(); // Initial weather
|
||
|
||
console.log('✅ Pet Battles World Map Ready!');
|
||
});
|
||
|
||
// Handle window resize
|
||
window.addEventListener('resize', function() {
|
||
// Ensure player positions remain accurate on resize
|
||
setTimeout(updateMap, 100);
|
||
});
|
||
|
||
// Handle visibility change (pause auto-refresh when tab is not visible)
|
||
document.addEventListener('visibilitychange', function() {
|
||
const checkbox = document.getElementById('autoRefresh');
|
||
if (document.hidden) {
|
||
if (autoRefreshInterval) {
|
||
clearInterval(autoRefreshInterval);
|
||
autoRefreshInterval = null;
|
||
}
|
||
} else {
|
||
if (checkbox.checked) {
|
||
autoRefreshInterval = setInterval(refreshMap, 5000);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Add right-click context menu for players
|
||
document.addEventListener('contextmenu', function(e) {
|
||
if (e.target.classList.contains('player')) {
|
||
e.preventDefault();
|
||
|
||
const contextMenu = document.createElement('div');
|
||
contextMenu.style.cssText = `
|
||
position: fixed;
|
||
left: ${e.clientX}px;
|
||
top: ${e.clientY}px;
|
||
background: white;
|
||
border: 1px solid #ccc;
|
||
border-radius: 5px;
|
||
padding: 10px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||
z-index: 1002;
|
||
`;
|
||
|
||
contextMenu.innerHTML = `
|
||
<div style="cursor: pointer; padding: 5px;" onclick="alert('Feature coming soon!')">Send Message</div>
|
||
<div style="cursor: pointer; padding: 5px;" onclick="alert('Feature coming soon!')">Challenge to Battle</div>
|
||
<div style="cursor: pointer; padding: 5px;" onclick="alert('Feature coming soon!')">View Full Profile</div>
|
||
`;
|
||
|
||
document.body.appendChild(contextMenu);
|
||
|
||
// Remove context menu when clicking elsewhere
|
||
setTimeout(() => {
|
||
document.addEventListener('click', function removeMenu() {
|
||
contextMenu.remove();
|
||
document.removeEventListener('click', removeMenu);
|
||
});
|
||
}, 100);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|