<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pocket Battle - API Edition</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=DotGothic16&family=Press+Start+2P&display=swap');
.pixel-font { font-family: 'DotGothic16', sans-serif; }
.battle-bg {
background: linear-gradient(to bottom, #87CEEB 0%, #87CEEB 50%, #90EE90 50%, #90EE90 100%);
background-size: 100% 200%;
transition: background-position 1s ease;
}
.title-float { animation: float 3s ease-in-out infinite; }
@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
.attack-player { animation: attack-p 0.5s ease-out; }
@keyframes attack-p { 0% { transform: translateX(0); } 50% { transform: translateX(20px) translateY(-10px); } 100% { transform: translateX(0); } }
.attack-enemy { animation: attack-e 0.5s ease-out; }
@keyframes attack-e { 0% { transform: translateX(0); } 50% { transform: translateX(-20px) translateY(10px); } 100% { transform: translateX(0); } }
.loader { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.pixelated { image-rendering: pixelated; }
.shiny-sparkle { filter: drop-shadow(0 0 5px gold); }
</style>
</head>
<body class="flex flex-col items-center justify-center min-h-screen p-4 bg-gray-900 text-gray-800">
<audio id="bgm" loop preload="auto">
<source src="https://petitacode.web2050.jp/users/original/o-yuto/%E3%81%8D%E3%81%BF%E3%81%AB%E3%81%8D%E3%82%81%E3%81%9F%20(1).mp3" type="audio/mpeg">
</audio>
<div id="game-container" class="max-w-lg w-full bg-white rounded-xl shadow-2xl overflow-hidden border-4 border-gray-800 relative min-h-[500px] flex flex-col">
<div id="bgm-controls" class="hidden absolute top-2 right-2 z-50">
<button onclick="toggleBGM()" id="bgm-btn" class="bg-white/80 hover:bg-white p-2 rounded-full shadow-md text-gray-700 transition-all">
<i id="bgm-icon" class="fas fa-volume-up"></i>
</button>
</div>
<!-- タイトル画面 -->
<div id="title-screen" class="flex-grow p-10 flex flex-col items-center justify-center text-center">
<div class="mb-8 title-float">
<h1 class="text-4xl font-black text-blue-600 pixel-font tracking-tighter italic">POCKET<br><span class="text-red-500">BATTLE</span></h1>
<p class="text-xs font-bold text-gray-400 mt-2">API Edition</p>
</div>
<div class="w-24 h-24 mb-10 opacity-20">
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/poke-ball.png" class="w-full h-full object-contain grayscale">
</div>
<button id="start-btn" class="group relative px-8 py-4 bg-red-600 text-white rounded-full font-bold text-xl shadow-[0_4px_0_rgb(153,27,27)] hover:shadow-none hover:translate-y-1 transition-all pixel-font">
ぼうけんを はじめる
</button>
</div>
<!-- パートナー選択画面 -->
<div id="starter-selection" class="hidden flex-grow p-4">
<h2 class="text-xl font-bold mb-4 text-center pixel-font">はじめのパートナーを えらぼう!</h2>
<div id="selection-loader" class="flex flex-col items-center py-10">
<div class="loader mb-2"></div>
<p class="text-xs text-gray-500">よみこみちゅう...</p>
</div>
<div id="starter-list" class="grid grid-cols-3 gap-2 overflow-y-auto max-h-[350px] p-2"></div>
</div>
<!-- バトル画面 -->
<div id="battle-area" class="hidden flex-grow flex flex-col">
<div id="battle-bg" class="battle-bg h-48 relative overflow-hidden border-b-2 border-gray-800">
<!-- 敵情報 -->
<div class="absolute top-4 right-8 text-right w-40 bg-white/80 p-1 rounded-lg border border-gray-400">
<div id="enemy-name" class="text-[10px] font-bold truncate">---</div>
<div class="w-full bg-gray-300 h-2 rounded-full mt-1">
<div id="enemy-hp-fill" class="h-full bg-green-500 rounded-full transition-all duration-500" style="width: 100%"></div>
</div>
<div id="enemy-hp-text" class="text-[8px] text-right font-mono mt-0.5">0 / 0</div>
</div>
<img id="enemy-img" src="" class="absolute top-8 right-12 w-24 h-24 object-contain pixelated">
<!-- プレイヤー情報 -->
<div class="absolute bottom-4 left-8 w-40 bg-white/80 p-1 rounded-lg border border-gray-400">
<div id="player-name" class="text-[10px] font-bold truncate">---</div>
<div class="w-full bg-gray-300 h-2 rounded-full mt-1">
<div id="player-hp-fill" class="h-full bg-green-500 rounded-full transition-all duration-500" style="width: 100%"></div>
</div>
<div id="player-hp-text" class="text-[8px] text-right font-mono mt-0.5">0 / 0</div>
</div>
<img id="player-img" src="" class="absolute bottom-2 left-12 w-32 h-32 object-contain pixelated z-10">
</div>
<!-- メッセージとアクション -->
<div class="p-3 bg-gray-100 flex-grow border-b-2 border-gray-800">
<div id="message-text" class="pixel-font text-sm h-12 overflow-hidden leading-tight">ぼうけんが はじまる!</div>
</div>
<div id="action-controls" class="grid grid-cols-2 gap-2 p-2 bg-gray-200">
<div id="move-buttons" class="grid grid-cols-2 gap-1 col-span-2"></div>
<div class="grid grid-cols-2 gap-1 col-span-2 mt-1">
<button onclick="toggleBallMenu()" class="bg-orange-500 text-white font-bold py-2 rounded text-[10px] shadow-md border-b-2 border-orange-700 active:border-0 active:translate-y-0.5 transition-all">
<i class="fas fa-suitcase mr-1"></i>バッグ
</button>
<button onclick="healPlayer()" class="bg-green-600 text-white font-bold py-2 rounded text-[10px] shadow-md border-b-2 border-green-800 active:border-0 active:translate-y-0.5 transition-all">
<i class="fas fa-heart mr-1"></i>かいふく
</button>
<button onclick="changeEnemyPokemon()" class="bg-blue-500 text-white font-bold py-2 rounded text-[10px] shadow-md border-b-2 border-blue-700 active:border-0 active:translate-y-0.5 transition-all">
<i class="fas fa-search mr-1"></i>別のポケモンをさがす
</button>
<button onclick="changeEnemyPokemon()" class="bg-gray-500 text-white font-bold py-2 rounded text-[10px] shadow-md border-b-2 border-gray-700 active:border-0 active:translate-y-0.5 transition-all">
<i class="fas fa-running mr-1"></i>にげる
</button>
</div>
</div>
<!-- ボールメニュー -->
<div id="ball-selection-menu" class="hidden grid grid-cols-3 gap-2 p-2 bg-gray-300">
<button onclick="catchWithBall('common')" class="flex flex-col items-center bg-white p-2 rounded border border-gray-400">
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/poke-ball.png" class="w-8 h-8">
<span class="text-[8px] font-bold mt-1">モンスター</span>
</button>
<button onclick="catchWithBall('great')" class="flex flex-col items-center bg-white p-2 rounded border border-gray-400">
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/great-ball.png" class="w-8 h-8">
<span class="text-[8px] font-bold mt-1">スーパー</span>
</button>
<button onclick="catchWithBall('ultra')" class="flex flex-col items-center bg-white p-2 rounded border border-gray-400">
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/ultra-ball.png" class="w-8 h-8">
<span class="text-[8px] font-bold mt-1">ハイパー</span>
</button>
<button onclick="toggleBallMenu()" class="col-span-3 text-[10px] font-bold text-gray-600 py-1 text-center">もどる</button>
</div>
</div>
<!-- フッター -->
<div id="pokedex-button-container" class="hidden p-2 bg-gray-800 flex justify-between items-center">
<button onclick="openPokedex()" class="bg-red-500 text-white px-4 py-1 rounded-full text-xs font-bold shadow-lg flex items-center">
<i class="fas fa-book-open mr-2"></i> ポケモンずかん
</button>
<div id="save-status" class="text-[8px] text-gray-400 font-bold"></div>
</div>
<div id="main-loader" class="p-8 hidden flex flex-col items-center justify-center flex-grow">
<div class="loader mb-4"></div>
<p class="text-gray-600 font-bold">よみこみちゅう...</p>
</div>
</div>
<!-- 所持ポケモン -->
<div id="collection-area" class="max-w-lg w-full mt-4 p-4 bg-white/10 rounded-xl border-2 border-white/20 hidden">
<div class="flex justify-between items-center mb-2">
<h3 class="text-white font-bold text-sm"><i class="fas fa-box-open mr-2"></i> てもちのポケモン</h3>
<button onclick="manualSave()" class="bg-blue-600 hover:bg-blue-500 text-white text-[9px] px-2 py-0.5 rounded font-bold">セーブ</button>
</div>
<div id="collection-list" class="flex flex-wrap gap-2"></div>
</div>
<!-- 図鑑モーダル -->
<div id="pokedex-modal" class="fixed inset-0 bg-black/80 z-[100] hidden flex flex-col items-center justify-center p-4">
<div class="bg-red-600 w-full max-w-md rounded-2xl border-4 border-gray-900 overflow-hidden flex flex-col max-h-[90vh]">
<div class="p-4 flex justify-between items-center bg-red-700 border-b-4 border-gray-900">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-blue-400 rounded-full border-4 border-white shadow-inner"></div>
<h2 class="text-white font-black italic tracking-tighter text-xl">POKEDEX</h2>
</div>
<button onclick="closePokedex()" class="text-white text-2xl"><i class="fas fa-times"></i></button>
</div>
<div id="pokedex-list" class="flex-grow overflow-y-auto grid grid-cols-4 gap-2 p-4 bg-white"></div>
</div>
</div>
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, doc, getDoc, setDoc, onSnapshot } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
// 環境変数の安全なチェック
const safeEnv = (key) => {
try { return eval(key); } catch (e) { return null; }
};
const configRaw = safeEnv('__firebase_config');
const firebaseConfig = configRaw ? JSON.parse(configRaw) : null;
const appId = safeEnv('__app_id') || 'pokemon-battle-default';
const initialToken = safeEnv('__initial_auth_token');
let app, db, auth;
if (firebaseConfig) {
app = initializeApp(firebaseConfig);
db = getFirestore(app);
auth = getAuth(app);
}
let userId = null;
let isFirstLoad = true;
const API_URL = "https://pokeapi.co/api/v2/pokemon/";
const SHINY_CHANCE = 0.4;
const LEGENDARY_CHANCE = 0.15;
const LEGENDARY_IDS = [144, 145, 146, 150, 151, 243, 244, 245, 249, 250, 251, 382, 383, 384, 483, 484, 487, 643, 644, 646, 716, 717, 718, 888, 889, 890];
window.player = null;
window.enemy = null;
window.isProcessing = false;
window.capturedPokemons = [];
window.pokedex = {};
window.hasBgmStarted = false;
const ballData = { common: { name: "モンスターボール", rate: 1.0 }, great: { name: "スーパーボール", rate: 1.5 }, ultra: { name: "ハイパーボール", rate: 2.5 } };
const bgm = document.getElementById('bgm');
let isPlaying = false;
async function initStarterSelection() {
const starters = [
1, 4, 7, // 第1世代
152, 155, 158, // 第2世代
252, 255, 258, // 第3世代
387, 390, 393, // 第4世代
722, 725, 728, // 第7世代
810, 813, 816, // 第8世代 (ガラル)
25, 133, 443 // ピカチュウ, イーブイ, フカマル
];
const starterList = document.getElementById('starter-list');
if(!starterList) return;
starterList.innerHTML = '';
for (const id of starters) {
const pokemon = await fetchPokemon(id);
if(!pokemon) continue;
const card = document.createElement('div');
card.className = `p-2 rounded-lg text-center border-2 cursor-pointer flex flex-col items-center bg-white border-gray-200 hover:border-blue-500`;
card.onclick = () => {
addCollection(pokemon);
updatePokedex(pokemon, 'caught');
switchActivePokemon(pokemon.id);
document.getElementById('starter-selection').classList.add('hidden');
showBattleUI();
startNewBattle();
};
card.innerHTML = `<img src="${pokemon.staticSprite}" class="w-12 h-12 pixelated"><p class="text-[8px] font-bold mt-1">${pokemon.name}</p>`;
starterList.appendChild(card);
}
document.getElementById('selection-loader').classList.add('hidden');
}
async function fetchPokemon(idOrName) {
const isShiny = Math.random() < SHINY_CHANCE;
try {
const pokeRes = await fetch(`${API_URL}${idOrName}`);
const data = await pokeRes.json();
const speciesRes = await fetch(data.species.url);
const speciesData = await speciesRes.json();
const ja = speciesData.names.find(n => n.language.name === 'ja-Hrkt' || n.language.name === 'ja');
let japaneseName = ja ? ja.name : data.name;
let moves = await Promise.all(data.moves.slice(0, 4).map(async (m) => {
const moveRes = await fetch(m.move.url);
const moveData = await moveRes.json();
const mJa = moveData.names.find(n => n.language.name === 'ja-Hrkt' || n.language.name === 'ja');
return { name: mJa ? mJa.name : m.move.name, power: moveData.power || 50 };
}));
const sprites = data.sprites.versions['generation-v']?.['black-white']?.animated || data.sprites;
return {
baseId: data.id >= 10000 ? speciesData.id : data.id,
id: data.id,
name: japaneseName,
hp: data.stats[0].base_stat * 2,
maxHp: data.stats[0].base_stat * 2,
attack: data.stats[1].base_stat,
defense: data.stats[2].base_stat,
sprite: isShiny ? (sprites.front_shiny || data.sprites.front_shiny) : (sprites.front_default || data.sprites.front_default),
backSprite: isShiny ? (sprites.back_shiny || data.sprites.back_shiny) : (sprites.back_default || data.sprites.back_default),
staticSprite: isShiny ? data.sprites.front_shiny : data.sprites.front_default,
moves: moves,
isShiny: isShiny
};
} catch(e) { return null; }
}
window.startGame = function() {
startBGM();
document.getElementById('title-screen').classList.add('hidden');
if (window.capturedPokemons.length === 0) {
document.getElementById('starter-selection').classList.remove('hidden');
initStarterSelection();
} else {
showBattleUI();
startNewBattle();
}
};
window.startNewBattle = async function() {
if (window.isProcessing) return;
window.isProcessing = true;
document.getElementById('main-loader').classList.remove('hidden');
document.getElementById('battle-area').classList.add('hidden');
// 自分のポケモンがひんし状態なら全回復させる
if (window.player && window.player.hp <= 0) {
window.player.hp = window.player.maxHp;
}
try {
let randomId = Math.random() < LEGENDARY_CHANCE ? LEGENDARY_IDS[Math.floor(Math.random() * LEGENDARY_IDS.length)] : Math.floor(Math.random() * 800) + 1;
window.enemy = await fetchPokemon(randomId);
updatePokedex(window.enemy, 'seen');
updateUI();
renderMoves();
document.getElementById('main-loader').classList.add('hidden');
document.getElementById('battle-area').classList.remove('hidden');
updateMessage(`野生の ${window.enemy.name} が とびだしてきた!`);
window.isProcessing = false;
} catch (e) {
window.isProcessing = false;
setTimeout(startNewBattle, 1000);
}
}
async function handleAttack(move) {
if (window.isProcessing) return;
window.isProcessing = true;
await performMove(window.player, window.enemy, move, 'player');
if (window.enemy.hp <= 0) {
updateMessage(`${window.enemy.name} を たおした!`);
setTimeout(() => {
window.isProcessing = false;
startNewBattle();
}, 2000);
return;
}
await enemyTurn();
}
async function enemyTurn() {
await new Promise(r => setTimeout(r, 800));
const move = window.enemy.moves[Math.floor(Math.random() * window.enemy.moves.length)];
await performMove(window.enemy, window.player, move, 'enemy');
if (window.player.hp <= 0) {
updateMessage(`${window.player.name} は たおれてしまった...`);
setTimeout(() => {
updateMessage("近くのポケモンセンターで 回復した!");
setTimeout(() => {
window.isProcessing = false;
startNewBattle();
}, 1500);
}, 2000);
return;
}
window.isProcessing = false;
}
async function performMove(attacker, defender, move, role) {
updateMessage(`${attacker.name} の ${move.name}!`);
const img = document.getElementById(`${role}-img`);
img.classList.add(role === 'player' ? 'attack-player' : 'attack-enemy');
await new Promise(r => setTimeout(r, 500));
img.classList.remove(role === 'player' ? 'attack-player' : 'attack-enemy');
let damage = (((2 * 50 / 5 + 2) * move.power * (attacker.attack / defender.defense)) / 50) + 2;
defender.hp = Math.max(0, defender.hp - damage);
updateHPBar(role === 'player' ? 'enemy' : 'player', defender.hp, defender.maxHp);
}
function updateUI() {
if (!window.player || !window.enemy) return;
document.getElementById('player-name').innerText = (window.player.isShiny ? '★' : '') + window.player.name;
document.getElementById('player-img').src = window.player.backSprite || window.player.sprite;
updateHPBar('player', window.player.hp, window.player.maxHp);
document.getElementById('enemy-name').innerText = (window.enemy.isShiny ? '★' : '') + window.enemy.name;
document.getElementById('enemy-img').src = window.enemy.sprite;
updateHPBar('enemy', window.enemy.hp, window.enemy.maxHp);
}
function updateHPBar(who, cur, max) {
const pct = Math.max(0, (cur / max) * 100);
document.getElementById(`${who}-hp-fill`).style.width = `${pct}%`;
document.getElementById(`${who}-hp-text`).innerText = `${Math.ceil(cur)} / ${max}`;
}
function updateMessage(text) { document.getElementById('message-text').innerText = text; }
function renderMoves() {
const container = document.getElementById('move-buttons');
container.innerHTML = '';
window.player.moves.forEach(m => {
const b = document.createElement('button');
b.className = `border-2 font-bold py-2 rounded text-[10px] bg-white border-gray-400 active:bg-gray-100`;
b.innerText = m.name;
b.onclick = () => handleAttack(m);
container.appendChild(b);
});
}
window.toggleBallMenu = () => {
document.getElementById('action-controls').classList.toggle('hidden');
document.getElementById('ball-selection-menu').classList.toggle('hidden');
};
window.catchWithBall = async (type) => {
toggleBallMenu();
window.isProcessing = true;
updateMessage(`${ballData[type].name} を なげた!`);
await new Promise(r => setTimeout(r, 1000));
if (Math.random() < (0.4 * ballData[type].rate)) {
updateMessage(`${window.enemy.name} を つかまえた!`);
updatePokedex(window.enemy, 'caught');
addCollection(window.enemy);
setTimeout(() => {
window.isProcessing = false;
startNewBattle();
}, 2000);
} else { updateMessage("だめだ! もれだしてしまった!"); await enemyTurn(); }
};
window.healPlayer = () => {
if (window.isProcessing) return;
window.player.hp = Math.min(window.player.maxHp, window.player.hp + 60);
updateHPBar('player', window.player.hp, window.player.maxHp);
updateMessage(`${window.player.name} は かいふくした!`);
enemyTurn();
};
window.changeEnemyPokemon = () => window.startNewBattle();
function addCollection(p) {
if (!window.capturedPokemons.find(x => x.id === p.id)) {
window.capturedPokemons.push(p);
renderCollection();
saveToCloud();
}
}
function switchActivePokemon(id) {
const target = window.capturedPokemons.find(p => p.id === id);
if (!target) return;
window.player = JSON.parse(JSON.stringify(target));
updateUI(); renderMoves(); renderCollection();
}
function renderCollection() {
const list = document.getElementById('collection-list');
list.innerHTML = '';
window.capturedPokemons.forEach(p => {
const item = document.createElement('div');
const isActive = window.player && p.id === window.player.id;
item.className = `w-10 h-10 bg-white rounded border-2 cursor-pointer flex items-center justify-center ${isActive ? 'border-blue-500' : 'border-gray-300'}`;
item.innerHTML = `<img src="${p.staticSprite}" class="w-full h-full object-contain pixelated ${p.isShiny ? 'shiny-sparkle' : ''}">`;
item.onclick = () => !window.isProcessing && switchActivePokemon(p.id);
list.appendChild(item);
});
}
function updatePokedex(p, status) {
const id = p.baseId || p.id;
window.pokedex[id] = { status, name: p.name, sprite: p.staticSprite, id };
saveToCloud();
}
window.openPokedex = () => {
const m = document.getElementById('pokedex-modal');
const l = document.getElementById('pokedex-list');
m.classList.remove('hidden');
l.innerHTML = '';
Object.keys(window.pokedex).sort((a,b)=>a-b).forEach(id => {
const e = window.pokedex[id];
const c = document.createElement('div');
c.className = "p-1 border bg-white flex flex-col items-center";
c.innerHTML = `<img src="${e.sprite}" class="w-8 h-8 pixelated ${e.status === 'seen' ? 'brightness-0' : ''}"><p class="text-[6px]">${e.status === 'caught' ? e.name : '???'}</p>`;
l.appendChild(c);
});
};
window.closePokedex = () => document.getElementById('pokedex-modal').classList.add('hidden');
function startBGM() {
if (!window.hasBgmStarted) {
bgm.play().then(() => {
isPlaying = true;
window.hasBgmStarted = true;
document.getElementById('bgm-controls').classList.remove('hidden');
}).catch(() => {});
}
}
window.toggleBGM = () => {
const icon = document.getElementById('bgm-icon');
if (isPlaying) { bgm.pause(); icon.className = 'fas fa-volume-mute'; }
else { bgm.play().catch(() => {}); icon.className = 'fas fa-volume-up'; }
isPlaying = !isPlaying;
};
async function saveToCloud() {
if (!userId || !db) return;
try {
const userDoc = doc(db, 'artifacts', appId, 'users', userId, 'gameData', 'saveSlot');
await setDoc(userDoc, { capturedPokemons: window.capturedPokemons, pokedex: window.pokedex });
} catch (e) {}
}
function syncData() {
if (!userId || !db) return;
const userDoc = doc(db, 'artifacts', appId, 'users', userId, 'gameData', 'saveSlot');
onSnapshot(userDoc, (docSnap) => {
if (docSnap.exists() && isFirstLoad) {
const d = docSnap.data();
window.pokedex = d.pokedex || {};
window.capturedPokemons = d.capturedPokemons || [];
if (window.capturedPokemons.length > 0) switchActivePokemon(window.capturedPokemons[0].id);
isFirstLoad = false;
}
});
}
window.manualSave = () => saveToCloud();
function showBattleUI() {
document.getElementById('collection-area').classList.remove('hidden');
document.getElementById('pokedex-button-container').classList.remove('hidden');
document.getElementById('battle-area').classList.remove('hidden');
}
window.onload = () => {
const startBtn = document.getElementById('start-btn');
if (startBtn) startBtn.onclick = window.startGame;
if (auth) {
onAuthStateChanged(auth, (user) => {
if (user) { userId = user.uid; syncData(); }
});
if (initialToken) signInWithCustomToken(auth, initialToken).catch(()=>signInAnonymously(auth));
else signInAnonymously(auth);
}
};
</script>
</body>
</html>
ポケモンバトルゲーム