679 lines
15 KiB
Vue
679 lines
15 KiB
Vue
<template>
|
|
<div class="gamemaster-manager">
|
|
<div class="container">
|
|
<div class="header">
|
|
<router-link to="/" class="back-button"> ← Back Home </router-link>
|
|
<h1>Gamemaster Manager</h1>
|
|
</div>
|
|
<p class="description">
|
|
Fetch the latest Pokemon GO gamemaster data from PokeMiners and break it
|
|
up into separate files for easier processing.
|
|
</p>
|
|
|
|
<!-- Fetch Section -->
|
|
<div class="section">
|
|
<h2>1. Fetch Latest Gamemaster</h2>
|
|
<button
|
|
@click="fetchGamemaster"
|
|
:disabled="loading"
|
|
class="btn btn-primary"
|
|
>
|
|
{{ loading ? 'Fetching...' : 'Fetch from PokeMiners' }}
|
|
</button>
|
|
|
|
<div v-if="error" class="error">
|
|
{{ error }}
|
|
</div>
|
|
|
|
<div v-if="rawGamemaster" class="success">
|
|
✅ Fetched {{ rawGamemaster.length.toLocaleString() }} items from
|
|
gamemaster
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Break Up Section -->
|
|
<div v-if="rawGamemaster" class="section">
|
|
<h2>2. Break Up Gamemaster</h2>
|
|
<button @click="processGamemaster" class="btn btn-primary">
|
|
Process & Break Up Data
|
|
</button>
|
|
|
|
<div v-if="processedData" class="stats-grid">
|
|
<div class="stat-card">
|
|
<h3>Pokemon (Filtered)</h3>
|
|
<p class="stat-number">
|
|
{{ stats.pokemonCount.toLocaleString() }}
|
|
</p>
|
|
<p class="stat-detail">{{ stats.pokemonSize }}</p>
|
|
<p class="stat-info">Base forms + regional variants</p>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<h3>All Forms & Costumes</h3>
|
|
<p class="stat-number">
|
|
{{ stats.allFormsCount.toLocaleString() }}
|
|
</p>
|
|
<p class="stat-detail">{{ stats.allFormsSize }}</p>
|
|
<p class="stat-info">Every variant, costume, form</p>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<h3>Moves</h3>
|
|
<p class="stat-number">{{ stats.movesCount.toLocaleString() }}</p>
|
|
<p class="stat-detail">{{ stats.movesSize }}</p>
|
|
<p class="stat-info">All quick & charged moves</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Save to Server Section -->
|
|
<div v-if="processedData" class="section">
|
|
<h2>3. Save to Server</h2>
|
|
<p class="description">
|
|
Store processed data on server for other apps to access via API
|
|
</p>
|
|
<button
|
|
@click="saveToServer"
|
|
:disabled="saving"
|
|
class="btn btn-primary"
|
|
>
|
|
{{ saving ? 'Saving...' : '💾 Save All Files to Server' }}
|
|
</button>
|
|
<div v-if="saveSuccess" class="success">
|
|
✅ Files saved to server successfully! Other apps can now access the
|
|
data via the Gamemaster API.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Server Status Section -->
|
|
<div v-if="serverStatus" class="section">
|
|
<h2>4. Server Storage Status</h2>
|
|
<div class="status-card">
|
|
<p>
|
|
<strong>Last Update:</strong>
|
|
{{ formatDate(serverStatus.lastUpdate) }}
|
|
</p>
|
|
<p><strong>Files Available:</strong> {{ serverStatus.totalFiles }}</p>
|
|
<div class="file-list">
|
|
<div
|
|
v-for="file in serverStatus.available"
|
|
:key="file.filename"
|
|
class="file-item"
|
|
>
|
|
<button
|
|
@click="downloadFromServer(file.filename)"
|
|
class="btn btn-small btn-success file-download-btn"
|
|
>
|
|
📥
|
|
</button>
|
|
<div class="file-info">
|
|
<span class="file-name">{{ file.filename }}</span>
|
|
<span class="file-size">{{ file.sizeKb }} KB</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="serverStatus.totalFiles > 0"
|
|
class="button-group"
|
|
style="margin-top: 1rem"
|
|
>
|
|
<button @click="downloadAllFromServer" class="btn btn-primary">
|
|
📦 Download All from Server
|
|
</button>
|
|
<router-link to="/gamemaster-explorer" class="btn btn-secondary">
|
|
🔍 Explore in JSON Viewer
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info Section -->
|
|
<div class="section info-section">
|
|
<h2>About This Tool</h2>
|
|
<p>
|
|
This tool fetches the latest Pokemon GO gamemaster data from
|
|
<a
|
|
href="https://github.com/PokeMiners/game_masters"
|
|
target="_blank"
|
|
rel="noopener"
|
|
>PokeMiners GitHub</a
|
|
>
|
|
and processes it into three separate files:
|
|
</p>
|
|
<ul>
|
|
<li>
|
|
<strong>pokemon.json</strong> - Base Pokemon forms plus regional
|
|
variants (Alola, Galarian, Hisuian, Paldea)
|
|
</li>
|
|
<li>
|
|
<strong>pokemon-allFormsCostumes.json</strong> - Complete dataset
|
|
including all costumes, event forms, shadows, etc.
|
|
</li>
|
|
<li>
|
|
<strong>pokemon-moves.json</strong> - All quick and charged moves
|
|
available in Pokemon GO
|
|
</li>
|
|
</ul>
|
|
<p class="note">
|
|
💡 The filtered pokemon.json is ideal for most use cases, while
|
|
allFormsCostumes is comprehensive for complete data analysis.
|
|
</p>
|
|
|
|
<h3 style="margin-top: 1.5rem; color: #667eea">Using the API</h3>
|
|
<p>
|
|
Once data is saved to the server, other apps can access it via the
|
|
Gamemaster API:
|
|
</p>
|
|
<pre
|
|
class="code-block"
|
|
><code>import { GamemasterClient } from './gamemaster-client.js';
|
|
|
|
const gm = new GamemasterClient('/api/gamemaster');
|
|
|
|
// Get filtered pokemon
|
|
const pokemon = await gm.getPokemon();
|
|
|
|
// Get all forms with costumes
|
|
const allForms = await gm.getAllForms();
|
|
|
|
// Get moves
|
|
const moves = await gm.getMoves();
|
|
|
|
// Check what's available
|
|
const status = await gm.getStatus();</code></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import {
|
|
fetchLatestGamemaster,
|
|
breakUpGamemaster,
|
|
downloadJson,
|
|
getGamemasterStats
|
|
} from '../utilities/gamemaster-utils.js';
|
|
|
|
const loading = ref(false);
|
|
const error = ref(null);
|
|
const saving = ref(false);
|
|
const saveSuccess = ref(false);
|
|
const rawGamemaster = ref(null);
|
|
const processedData = ref(null);
|
|
const serverStatus = ref(null);
|
|
|
|
const stats = computed(() => {
|
|
if (!processedData.value) return null;
|
|
return getGamemasterStats(processedData.value);
|
|
});
|
|
|
|
onMounted(async () => {
|
|
// Load server status on component mount
|
|
await loadServerStatus();
|
|
});
|
|
|
|
async function loadServerStatus() {
|
|
try {
|
|
const response = await fetch('/api/gamemaster/status');
|
|
if (response.ok) {
|
|
serverStatus.value = await response.json();
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to load server status:', err);
|
|
}
|
|
}
|
|
|
|
async function fetchGamemaster() {
|
|
loading.value = true;
|
|
error.value = null;
|
|
rawGamemaster.value = null;
|
|
processedData.value = null;
|
|
|
|
try {
|
|
const data = await fetchLatestGamemaster();
|
|
rawGamemaster.value = data;
|
|
} catch (err) {
|
|
error.value = `Failed to fetch gamemaster: ${err.message}`;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function processGamemaster() {
|
|
if (!rawGamemaster.value) return;
|
|
|
|
try {
|
|
processedData.value = breakUpGamemaster(rawGamemaster.value);
|
|
} catch (err) {
|
|
error.value = `Failed to process gamemaster: ${err.message}`;
|
|
}
|
|
}
|
|
|
|
async function saveToServer() {
|
|
if (!processedData.value) return;
|
|
|
|
saving.value = true;
|
|
saveSuccess.value = false;
|
|
error.value = null;
|
|
|
|
try {
|
|
// Call the server endpoint to fetch and process on the server
|
|
const response = await fetch('/api/gamemaster/process', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.json();
|
|
throw new Error(err.error || 'Failed to save to server');
|
|
}
|
|
|
|
const result = await response.json();
|
|
saveSuccess.value = true;
|
|
console.log('✅ Files saved to server:', result);
|
|
|
|
// Reload server status
|
|
await loadServerStatus();
|
|
} catch (err) {
|
|
error.value = `Failed to save to server: ${err.message}`;
|
|
} finally {
|
|
saving.value = false;
|
|
}
|
|
}
|
|
|
|
function downloadPokemon() {
|
|
downloadJson(processedData.value.pokemon, 'pokemon.json');
|
|
}
|
|
|
|
function downloadAllForms() {
|
|
downloadJson(
|
|
processedData.value.pokemonAllForms,
|
|
'pokemon-allFormsCostumes.json'
|
|
);
|
|
}
|
|
|
|
function downloadMoves() {
|
|
downloadJson(processedData.value.moves, 'pokemon-moves.json');
|
|
}
|
|
|
|
function downloadAll() {
|
|
downloadPokemon();
|
|
setTimeout(() => downloadAllForms(), 500);
|
|
setTimeout(() => downloadMoves(), 1000);
|
|
}
|
|
|
|
async function downloadFromServer(filename) {
|
|
try {
|
|
const response = await fetch(`/api/gamemaster/download/${filename}`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to download ${filename}`);
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = filename;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
} catch (err) {
|
|
error.value = `Failed to download ${filename}: ${err.message}`;
|
|
}
|
|
}
|
|
|
|
async function downloadAllFromServer() {
|
|
const filesToDownload = [
|
|
'pokemon.json',
|
|
'pokemon-allFormsCostumes.json',
|
|
'pokemon-moves.json'
|
|
];
|
|
|
|
for (let i = 0; i < filesToDownload.length; i++) {
|
|
setTimeout(() => {
|
|
downloadFromServer(filesToDownload[i]);
|
|
}, i * 500);
|
|
}
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
if (!dateString || dateString === 'Never') return 'Never';
|
|
return new Date(dateString).toLocaleString();
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.gamemaster-manager {
|
|
min-height: 100vh;
|
|
padding: 2rem 1rem;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 2rem;
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-bottom: 1rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.back-button {
|
|
padding: 0.5rem 1rem;
|
|
background: #667eea;
|
|
color: white;
|
|
text-decoration: none;
|
|
border-radius: 6px;
|
|
font-weight: 600;
|
|
transition: all 0.3s ease;
|
|
display: inline-block;
|
|
}
|
|
|
|
.back-button:hover {
|
|
background: #5568d3;
|
|
transform: translateX(-2px);
|
|
}
|
|
|
|
h1 {
|
|
color: #333;
|
|
margin: 0;
|
|
font-size: 2.5rem;
|
|
}
|
|
|
|
.description {
|
|
color: #666;
|
|
font-size: 1.1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.section {
|
|
margin: 2rem 0;
|
|
padding: 1.5rem;
|
|
background: #f8f9fa;
|
|
border-radius: 8px;
|
|
border: 1px solid #e9ecef;
|
|
}
|
|
|
|
h2 {
|
|
color: #495057;
|
|
margin-bottom: 1rem;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.btn {
|
|
padding: 0.75rem 1.5rem;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
margin-right: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #667eea;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: #5568d3;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
|
}
|
|
|
|
.btn-success {
|
|
background: #10b981;
|
|
color: white;
|
|
}
|
|
|
|
.btn-success:hover {
|
|
background: #059669;
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
|
}
|
|
|
|
.button-group {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.error {
|
|
margin-top: 1rem;
|
|
padding: 1rem;
|
|
background: #fee;
|
|
color: #c33;
|
|
border-radius: 6px;
|
|
border-left: 4px solid #c33;
|
|
}
|
|
|
|
.success {
|
|
margin-top: 1rem;
|
|
padding: 1rem;
|
|
background: #d1fae5;
|
|
color: #065f46;
|
|
border-radius: 6px;
|
|
border-left: 4px solid #10b981;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 1rem;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
padding: 1.5rem;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-card h3 {
|
|
font-size: 1rem;
|
|
color: #666;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 2.5rem;
|
|
font-weight: bold;
|
|
color: #667eea;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.stat-detail {
|
|
font-size: 1rem;
|
|
color: #999;
|
|
margin: 0.25rem 0;
|
|
}
|
|
|
|
.stat-info {
|
|
font-size: 0.875rem;
|
|
color: #666;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.info-section {
|
|
background: #e7f3ff;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.info-section h2 {
|
|
color: #667eea;
|
|
}
|
|
|
|
.info-section ul {
|
|
margin: 1rem 0;
|
|
padding-left: 1.5rem;
|
|
}
|
|
|
|
.info-section li {
|
|
margin: 0.5rem 0;
|
|
color: #495057;
|
|
}
|
|
|
|
.info-section a {
|
|
color: #667eea;
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.info-section a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.note {
|
|
margin-top: 1rem;
|
|
padding: 0.75rem;
|
|
background: white;
|
|
border-radius: 6px;
|
|
font-size: 0.95rem;
|
|
color: #495057;
|
|
}
|
|
|
|
.status-card {
|
|
background: white;
|
|
padding: 1.5rem;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.status-card p {
|
|
margin: 0.75rem 0;
|
|
color: #495057;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.status-card strong {
|
|
color: #667eea;
|
|
}
|
|
|
|
.file-list {
|
|
margin-top: 1rem;
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.file-item {
|
|
background: #f8f9fa;
|
|
padding: 0.75rem;
|
|
border-radius: 6px;
|
|
border: 1px solid #e9ecef;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
text-align: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.file-download-btn {
|
|
align-self: center;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.file-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
min-width: 0;
|
|
}
|
|
|
|
.file-name {
|
|
font-weight: 500;
|
|
color: #495057;
|
|
font-size: 0.85rem;
|
|
word-break: break-word;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.file-size {
|
|
color: #999;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
.btn-small {
|
|
padding: 0.5rem 0.75rem;
|
|
font-size: 0.9rem;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background-color: #6c757d;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background-color: #5a6268;
|
|
}
|
|
|
|
.code-block {
|
|
background: #2d3748;
|
|
color: #e2e8f0;
|
|
padding: 1rem;
|
|
border-radius: 6px;
|
|
overflow-x: auto;
|
|
margin: 1rem 0;
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 0.85rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.code-block code {
|
|
color: inherit;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
padding: 1rem;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.button-group {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.btn {
|
|
width: 100%;
|
|
margin-right: 0;
|
|
}
|
|
|
|
.file-list {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|