<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Pokémon World Adventure - Local Save Fixed</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=DotGothic16&display=swap');
body {
background-color: #0a1a05;
font-family: 'DotGothic16', sans-serif;
touch-action: manipulation;
margin: 0;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.game-container {
width: 100%;
max-width: 450px;
height: 100%;
max-height: 850px;
background: #0d2b11;
display: flex;
flex-direction: column;
position: relative;
box-shadow: 0 0 100px rgba(0,0,0,0.8);
border: 6px solid #2d4a1e;
border-radius: 24px;
}
/* --- Map Tiles --- */
#map-container {
position: relative;
width: 100%;
aspect-ratio: 1 / 1.2;
overflow: hidden;
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(12, 1fr);
border-bottom: 4px solid #1a3311;
}
.tile { width: 100%; height: 100%; position: relative; }
.stage-1 .grass { background: radial-gradient(circle, #3a6d2c 0%, #2d5a27 100%); }
.stage-1 .tree::after { content: '🌲'; }
.stage-1 .path { background: #d2c290; }
.stage-2 .grass { background: radial-gradient(circle, #8d5524 0%, #5d3a1a 100%); }
.stage-2 .tree::after { content: '🍁'; }
.stage-2 .path { background: #b08d57; }
.stage-3 .grass { background: radial-gradient(circle, #e0f7fa 0%, #b2ebf2 100%); }
.stage-3 .tree::after { content: '❄️'; }
.stage-3 .path { background: #90a4ae; }
.stage-4 .grass { background: radial-gradient(circle, #2c3e50 0%, #1a252f 100%); }
.stage-4 .tree::after { content: '💀'; }
.stage-4 .path { background: #4a4a4a; }
.tree { display: flex; align-items: center; justify-content: center; }
.tree::after { font-size: 1.5rem; filter: drop-shadow(0 5px 3px rgba(0,0,0,0.5)); }
#player {
position: absolute;
width: 10%;
height: 8.33%;
z-index: 60;
transition: all 0.1s linear;
display: flex;
align-items: center;
justify-content: center;
}
#player-sprite { width: 140%; filter: drop-shadow(0 4px 6px rgba(0,0,0,0.4)); }
.walking #player-sprite { animation: pika-walk 0.1s infinite alternate; }
@keyframes pika-walk { from { transform: translateY(0); } to { transform: translateY(-4px); } }
/* --- UI Controls --- */
.controller-area { background: #2d4a1e; padding: 20px 15px; border-bottom-left-radius: 18px; border-bottom-right-radius: 18px; }
.d-pad-container { position: relative; width: 140px; height: 140px; margin: 0 auto; user-select: none; }
.d-btn {
position: absolute; background: #1a3311; color: #fff;
display: flex; align-items: center; justify-content: center;
border: 3px solid #0a1a05; font-size: 1.2rem; box-shadow: 0 4px 0 #0a1a05;
cursor: pointer; z-index: 10; -webkit-tap-highlight-color: transparent;
}
.d-btn:active { background: #2d5a27; transform: translateY(2px); box-shadow: 0 2px 0 #0a1a05; }
.d-up { width: 44px; height: 48px; top: 0; left: 48px; border-radius: 8px 8px 0 0; }
.d-down { width: 44px; height: 48px; bottom: 0; left: 48px; border-radius: 0 0 8px 8px; }
.d-left { width: 48px; height: 44px; left: 0; top: 48px; border-radius: 8px 0 0 8px; }
.d-right { width: 48px; height: 44px; right: 0; top: 48px; border-radius: 0 8px 8px 0; }
.d-center { width: 44px; height: 44px; left: 48px; top: 48px; background: #1a3311; border: none; box-shadow: none; z-index: 5; pointer-events: none; }
/* Battle / Scenes */
.battle-scene { height: 100%; width: 100%; position: relative; overflow: hidden; }
.pokemon-enemy { position: absolute; top: 15%; right: 10%; width: 32%; animation: enemy-float 4s infinite ease-in-out; z-index: 5; }
.pokemon-player { position: absolute; bottom: 12%; left: 8%; width: 42%; z-index: 5; animation: player-breathe 2.5s infinite ease-in-out; transform-origin: bottom center; }
@keyframes enemy-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
@keyframes player-breathe { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(0.96) scaleX(1.02); } }
.status-box { background: rgba(255, 254, 240, 0.95); border: 3px solid #333; border-radius: 4px 16px; padding: 10px; position: absolute; width: 170px; z-index: 10; box-shadow: 6px 6px 0 rgba(0,0,0,0.2); }
.enemy-status { top: 10%; left: 5%; }
.player-status { bottom: 28%; right: 5%; }
.hp-bar-container { background: #333; height: 12px; border-radius: 6px; padding: 2px; margin-top: 6px; }
.hp-bar-fill { height: 100%; transition: width 0.6s ease; border-radius: 4px; }
#evolution-view {
position: absolute; inset: 0; background: radial-gradient(circle, #fff 0%, #000 70%);
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 500;
}
.evo-sprite { width: 60%; filter: drop-shadow(0 0 20px #fff); }
.evo-glow { animation: evo-glow-anim 1s infinite alternate; }
@keyframes evo-glow-anim {
0% { filter: brightness(1) contrast(1) drop-shadow(0 0 10px #fff); }
100% { filter: brightness(5) contrast(0) drop-shadow(0 0 50px #fff); }
}
.message-window { background: #fdfdfd; border: 6px double #2d4a1e; padding: 14px; min-height: 90px; margin: 12px; border-radius: 12px; }
.hidden { display: none !important; }
.flash-overlay { position: absolute; inset: 0; background: white; z-index: 200; pointer-events: none; opacity: 0; }
.flash-active { animation: flash-fade 0.8s forwards; }
@keyframes flash-fade { 0% { opacity: 0; } 50% { opacity: 1; } 100% { opacity: 0; } }
.active-card { border-color: #7cfc00 !important; background-color: #f0fff0 !important; }
#audio-toggle { cursor: pointer; padding: 8px; background: rgba(0,0,0,0.3); border-radius: 50%; }
.modal {
position: absolute; inset: 0; background: rgba(0,0,0,0.9);
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 600; padding: 20px; text-align: center; color: white;
}
#user-id-display {
position: absolute; bottom: 2px; right: 10px; font-size: 8px; color: rgba(255,255,255,0.4); pointer-events: none;
}
</style>
</head>
<body>
<div class="game-container">
<!-- Initial Loader Modal -->
<div id="loading-modal" class="modal">
<div class="w-12 h-12 border-4 border-white border-t-transparent rounded-full animate-spin mb-4"></div>
<p id="loading-text">セーブデータを読み込んでいます...</p>
</div>
<!-- Reset Confirm Modal -->
<div id="reset-modal" class="modal hidden">
<div class="bg-[#fdfdfd] border-[6px] border-double border-[#2d4a1e] p-6 rounded-2xl text-center w-4/5 max-w-sm">
<h3 class="text-[#1a3311] text-lg font-bold mb-4">セーブデータを けして<br>さいしょから あそびますか?</h3>
<div class="flex justify-around mt-6 gap-2">
<button onclick="confirmReset(true)" class="bg-red-500 text-white flex-1 py-2 rounded-xl font-bold border-b-4 border-red-800">はい</button>
<button onclick="confirmReset(false)" class="bg-gray-400 text-white flex-1 py-2 rounded-xl font-bold border-b-4 border-gray-600">いいえ</button>
</div>
</div>
</div>
<!-- Starter Selection -->
<div id="start-screen" class="absolute inset-0 bg-[#0a1a05] z-[300] p-10 text-center flex flex-col items-center hidden">
<h2 class="text-2xl font-bold mb-10 text-[#7cfc00]">冒険のパートナーを選ぶ</h2>
<div class="space-y-4 w-full max-w-xs">
<button onclick="pickStarter(1)" class="w-full bg-[#1a3311] p-4 rounded-2xl border-2 border-[#3a6d2c] flex items-center gap-4 text-white">
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1.png" class="w-16">
<span class="font-bold">フシギダネ</span>
</button>
<button onclick="pickStarter(4)" class="w-full bg-[#1a3311] p-4 rounded-2xl border-2 border-[#3a6d2c] flex items-center gap-4 text-white">
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/4.png" class="w-16">
<span class="font-bold">ヒトカゲ</span>
</button>
<button onclick="pickStarter(7)" class="w-full bg-[#1a3311] p-4 rounded-2xl border-2 border-[#3a6d2c] flex items-center gap-4 text-white">
<img src="https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/7.png" class="w-16">
<span class="font-bold">ゼニガメ</span>
</button>
</div>
</div>
<!-- Header -->
<div class="bg-[#1a3311] text-[#7cfc00] px-5 py-3 flex justify-between items-center text-xs border-b border-[#2d4a1e]">
<div class="flex items-center gap-2">
<span class="w-2 h-2 bg-green-500 rounded-full animate-ping"></span>
<span id="stage-name" class="tracking-widest uppercase">Stage 1</span>
</div>
<div class="flex items-center gap-4">
<div id="audio-toggle" onclick="toggleAudio()" class="text-lg">🔈</div>
<div class="font-mono">TEAM: <span id="team-size">1</span></div>
</div>
</div>
<!-- Viewport -->
<div id="game-viewport" class="relative flex-grow overflow-hidden stage-1">
<div id="flash-overlay" class="flash-overlay"></div>
<div id="map-view" class="h-full w-full relative">
<div id="map-container"></div>
<div id="player"><img id="player-sprite" src=""></div>
</div>
<div id="battle-view" class="h-full w-full hidden">
<div class="battle-scene">
<div class="status-box enemy-status">
<div class="flex justify-between items-center"><span id="enemy-name" class="font-bold text-[11px] uppercase">WILD</span><span class="text-[9px]">Lv.<span id="enemy-level">??</span></span></div>
<div class="hp-bar-container"><div id="enemy-hp-fill" class="hp-bar-fill bg-green-500 w-full"></div></div>
</div>
<img id="enemy-img" class="pokemon-enemy" src="">
<div class="status-box player-status">
<div class="flex justify-between items-center"><span id="player-pokemon-name" class="font-bold text-[11px] uppercase">PARTNER</span><span class="text-[9px]">Lv.<span id="player-pokemon-level">5</span></span></div>
<div class="hp-bar-container"><div id="player-hp-fill" class="hp-bar-fill bg-green-500 w-full"></div></div>
<div class="text-right text-[10px] mt-1 font-mono"><span id="player-hp-cur">0</span> / <span id="player-hp-max">0</span></div>
</div>
<img id="player-img" class="pokemon-player" src="">
</div>
</div>
<div id="evolution-view" class="hidden">
<h2 class="text-xl font-bold mb-10 text-white drop-shadow-lg">おや...!?</h2>
<img id="evo-sprite" class="evo-sprite" src="">
<p id="evo-message" class="mt-10 text-white font-bold text-center h-12"></p>
</div>
<div id="pokedex-view" class="h-full w-full bg-[#f8fff2] hidden p-5 overflow-y-auto">
<div class="flex justify-between items-center mb-4 border-b-2 border-[#2d4a1e] pb-2">
<h2 class="text-xl font-bold text-[#1a3311]">マイチーム</h2>
<div class="flex gap-2">
<button onclick="resetGame()" class="bg-red-600 text-white text-[10px] px-3 py-1 rounded shadow-sm">さいしょから</button>
<button onclick="manualSave()" class="bg-blue-600 text-white text-[10px] px-3 py-1 rounded shadow-sm">レポート</button>
</div>
</div>
<div id="pokedex-list" class="grid grid-cols-2 gap-3 pb-10"></div>
</div>
</div>
<!-- Controls -->
<div class="shrink-0 bg-[#2d4a1e] border-t-2 border-[#1a3311] relative">
<div id="user-id-display">Local Save</div>
<div class="message-window">
<p id="game-message" class="text-sm text-[#1a3311] font-bold">ロード中...</p>
</div>
<div id="map-controls" class="controller-area">
<div class="d-pad-container">
<button class="d-btn d-up" onmousedown="startMove(0, -1)" onmouseup="stopMove()" ontouchstart="startMove(0, -1)" ontouchend="stopMove()">▲</button>
<button class="d-btn d-down" onmousedown="startMove(0, 1)" onmouseup="stopMove()" ontouchstart="startMove(0, 1)" ontouchend="stopMove()">▼</button>
<button class="d-btn d-left" onmousedown="startMove(-1, 0)" onmouseup="stopMove()" ontouchstart="startMove(-1, 0)" ontouchend="stopMove()">◀</button>
<button class="d-btn d-right" onmousedown="startMove(1, 0)" onmouseup="stopMove()" ontouchstart="startMove(1, 0)" ontouchend="stopMove()">▶</button>
<div class="d-center"></div>
</div>
<div class="flex justify-around mt-6 gap-1">
<button onclick="switchTab('map')" class="bg-[#7cfc00] text-[#0a1a05] flex-1 py-2 rounded-full font-bold border-2 border-[#0a1a05] shadow-lg text-xs">冒険</button>
<button onclick="switchTab('pokedex')" class="bg-[#1a3311] text-[#7cfc00] flex-1 py-2 rounded-full font-bold border-2 border-[#0a1a05] shadow-lg text-xs">チーム</button>
<button onclick="triggerEvolution()" class="bg-purple-600 text-white flex-1 py-2 rounded-full font-bold border-2 border-[#0a1a05] shadow-lg text-xs">進化</button>
<button onclick="triggerMegaEvolution()" class="bg-gradient-to-r from-cyan-400 to-blue-600 text-white flex-1 py-2 rounded-full font-bold border-2 border-[#0a1a05] shadow-lg text-xs">メガ</button>
</div>
</div>
<div id="battle-controls" class="controller-area hidden">
<div class="grid grid-cols-2 gap-3 p-2">
<button onclick="handleAttack()" class="bg-orange-500 text-white py-4 rounded-2xl font-bold border-b-4 border-orange-800">たたかう</button>
<button onclick="handleCatch()" class="bg-blue-500 text-white py-4 rounded-2xl font-bold border-b-4 border-blue-800">捕まえる</button>
<button onclick="handleHeal()" class="bg-pink-500 text-white py-3 rounded-2xl font-bold col-span-2 border-b-4 border-pink-800">回復する</button>
</div>
</div>
</div>
</div>
<script>
// --- Game State ---
let state = {
playerPos: { x: 5, y: 10 },
map: [],
partner: null,
enemy: null,
collection: [],
mode: 'map',
isBusy: false,
moveInterval: null,
currentStage: 1
};
const $ = id => document.getElementById(id);
// --- Audio Engine ---
const AudioEngine = {
ctx: null, bgmInterval: null, evoInterval: null, isMuted: true,
init() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
},
playTone(freq, type, duration, vol = 0.1) {
if (this.isMuted || !this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type; osc.frequency.setValueAtTime(freq, this.ctx.currentTime);
gain.gain.setValueAtTime(vol, this.ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.0001, this.ctx.currentTime + duration);
osc.connect(gain); gain.connect(this.ctx.destination);
osc.start(); osc.stop(this.ctx.currentTime + duration);
},
playSelect() { this.playTone(600, 'square', 0.1); },
playStep() { this.playTone(150, 'triangle', 0.05, 0.03); },
playHit() { this.playTone(100, 'sawtooth', 0.3, 0.2); },
playCatch() { [440, 554, 659, 880].forEach((f, i) => setTimeout(() => this.playTone(f, 'square', 0.2), i * 100)); },
playEncounter() { for(let i=0; i<10; i++) setTimeout(() => this.playTone(400 + i*100, 'sawtooth', 0.1), i * 50); },
playShiny() { [880, 1046, 1318, 1567].forEach((f, i) => setTimeout(() => this.playTone(f, 'sine', 0.2, 0.15), i * 100)); },
playLevelUp() { [440, 554, 659, 880, 659, 880].forEach((f, i) => setTimeout(() => this.playTone(f, 'square', 0.15, 0.1), i * 150)); },
playEvoDone() { [523, 659, 783, 1046, 523, 659, 783, 1046].forEach((f, i) => setTimeout(() => this.playTone(f, 'square', 0.3, 0.1), i * 150)); },
startEvoBGM() {
this.stopBGM(); if (this.isMuted) return; this.init();
let i = 0;
this.evoInterval = setInterval(() => {
this.playTone(300 + (i%2 === 0 ? 0 : 50), 'triangle', 0.2, 0.05); i++;
}, 200);
},
stopEvoBGM() {
if (this.evoInterval) { clearInterval(this.evoInterval); this.evoInterval = null; }
},
startBGM(type) {
this.stopBGM(); if (this.isMuted) return; this.init();
let notes = type === 'battle' ? [220, 233, 220, 200] : [329, 392, 440, 349];
let tempo = type === 'battle' ? 150 : 400; let i = 0;
this.bgmInterval = setInterval(() => { this.playTone(notes[i % notes.length], type === 'battle' ? 'sawtooth' : 'square', 0.3, 0.02); i++; }, tempo);
},
stopBGM() {
if (this.bgmInterval) { clearInterval(this.bgmInterval); this.bgmInterval = null; }
this.stopEvoBGM();
}
};
// --- LocalStorage Persistence ---
async function saveGameData() {
if (!state.partner) return;
try {
const partnerIndex = state.collection.findIndex(p => p.id === state.partner.id && p.name === state.partner.name);
const dataToSave = {
playerPos: state.playerPos,
collection: state.collection,
partnerIndex: partnerIndex >= 0 ? partnerIndex : 0,
currentStage: state.currentStage,
updatedAt: new Date().getTime()
};
localStorage.setItem('pokemon_adventure_save', JSON.stringify(dataToSave));
} catch (e) {
console.error("Save error:", e);
}
}
async function loadGameData() {
try {
const savedData = localStorage.getItem('pokemon_adventure_save');
if (savedData) {
const data = JSON.parse(savedData);
state.playerPos = data.playerPos || { x: 5, y: 10 };
state.collection = data.collection || [];
state.partner = state.collection[data.partnerIndex || 0];
state.currentStage = data.currentStage || 1;
return true;
}
} catch (e) {
console.error("Load error:", e);
}
return false;
}
// --- Global Methods ---
function getSprites(data, isShiny) {
const fKey = isShiny ? 'front_shiny' : 'front_default';
const bKey = isShiny ? 'back_shiny' : 'back_default';
const showdown = data.sprites?.other?.showdown;
const anim = data.sprites?.versions?.['generation-v']?.['black-white']?.animated;
const frontSprite = showdown?.[fKey] || anim?.[fKey] || data.sprites?.[fKey] || data.sprites?.other?.['official-artwork']?.[fKey] || data.sprites?.front_default;
const backSprite = showdown?.[bKey] || anim?.[bKey] || data.sprites?.[bKey] || frontSprite;
return { frontSprite, backSprite };
}
window.toggleAudio = () => {
AudioEngine.init(); AudioEngine.isMuted = !AudioEngine.isMuted;
$('audio-toggle').innerText = AudioEngine.isMuted ? '🔈' : '🔊';
if (!AudioEngine.isMuted) AudioEngine.startBGM(state.mode === 'battle' ? 'battle' : 'map');
else AudioEngine.stopBGM();
};
window.switchTab = (tab) => {
if (state.mode === 'battle' || state.isBusy) return;
AudioEngine.playSelect();
$('map-view').classList.add('hidden');
$('pokedex-view').classList.add('hidden');
if (tab === 'map') {
$('map-view').classList.remove('hidden');
state.mode = 'map';
} else {
$('pokedex-view').classList.remove('hidden');
state.mode = 'pokedex';
renderPokedex();
}
};
window.pickStarter = async (id) => {
if (state.isBusy) return;
state.isBusy = true;
AudioEngine.playSelect();
try {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
if (!res.ok) throw new Error('Network error');
const data = await res.json();
const isShiny = Math.random() < 0.05; // 5%の確率で色違い
const sprites = getSprites(data, isShiny);
const p = {
id: data.id, name: data.name, hp: 60, maxHp: 60, level: 5, exp: 0,
evoStage: 0, isShiny: isShiny,
frontSprite: sprites.frontSprite,
backSprite: sprites.backSprite
};
state.partner = p;
state.collection = [p];
$('player-sprite').src = p.frontSprite;
$('start-screen').classList.add('hidden');
state.isBusy = false;
initMap();
updateUI();
AudioEngine.startBGM('map');
log(`${p.name.toUpperCase()} と冒険に出よう!`);
saveGameData();
} catch(e) {
console.error(e);
state.isBusy = false;
log("ポケモンの取得に失敗しました。");
}
};
window.setActivePartner = (index) => {
if (state.isBusy) return;
AudioEngine.playSelect();
state.partner = state.collection[index];
log(`${state.partner.name.toUpperCase()} に交代!`);
updateUI();
renderPokedex();
updateStageUI();
saveGameData();
};
window.manualSave = () => {
AudioEngine.playCatch();
saveGameData();
log("レポートに 記録した!");
};
window.resetGame = () => {
AudioEngine.playSelect();
$('reset-modal').classList.remove('hidden');
};
window.confirmReset = (isConfirmed) => {
AudioEngine.playSelect();
$('reset-modal').classList.add('hidden');
if (isConfirmed) {
localStorage.removeItem('pokemon_adventure_save');
location.reload();
}
};
// --- Move Logic ---
window.startMove = (dx, dy) => {
if (state.moveInterval || state.mode !== 'map' || state.isBusy) return;
move(dx, dy);
state.moveInterval = setInterval(() => move(dx, dy), 180);
};
window.stopMove = () => {
if (state.moveInterval) { clearInterval(state.moveInterval); state.moveInterval = null; }
};
function move(dx, dy) {
if (state.mode !== 'map' || state.isBusy) return;
const newX = Math.max(0, Math.min(GRID_W - 1, state.playerPos.x + dx));
const newY = Math.max(0, Math.min(GRID_H - 1, state.playerPos.y + dy));
if (state.map[newY * GRID_W + newX] === 'tree') return;
AudioEngine.playStep();
const sprite = $('player-sprite');
if (dx < 0) sprite.style.transform = 'scaleX(-1)'; else if (dx > 0) sprite.style.transform = 'scaleX(1)';
$('player').classList.add('walking');
state.playerPos.x = newX;
state.playerPos.y = newY;
updatePlayerPos();
setTimeout(() => $('player').classList.remove('walking'), 120);
if (['grass', 'flower'].includes(state.map[newY * GRID_W + newX]) && Math.random() < 0.15) {
stopMove();
triggerEncounter();
}
}
// --- Core Logic ---
const GRID_W = 10, GRID_H = 12;
function initMap() {
const container = $('map-container');
container.innerHTML = '';
state.map = [];
for (let y = 0; y < GRID_H; y++) {
for (let x = 0; x < GRID_W; x++) {
const tile = document.createElement('div');
tile.className = 'tile';
let type = 'path'; const rand = Math.random();
if (rand < 0.08) type = 'tree'; else if (rand < 0.12) type = 'flower'; else if (rand < 0.40) type = 'grass';
if (Math.abs(x - state.playerPos.x) <= 1 && Math.abs(y - state.playerPos.y) <= 1) type = 'path';
tile.classList.add(type);
container.appendChild(tile);
state.map.push(type);
}
}
updatePlayerPos();
}
function updatePlayerPos() {
$('player').style.left = `${state.playerPos.x * 10}%`;
$('player').style.top = `${state.playerPos.y * 8.33}%`;
}
async function triggerEncounter() {
state.isBusy = true; AudioEngine.playEncounter(); AudioEngine.stopBGM();
$('flash-overlay').classList.add('flash-active');
setTimeout(async () => {
$('flash-overlay').classList.remove('flash-active');
state.mode = 'battle'; AudioEngine.startBGM('battle');
const randomId = Math.floor(Math.random() * 151) + 1;
try {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${randomId}`);
if (!res.ok) throw new Error('Network error');
const data = await res.json();
const isShiny = Math.random() < 0.05; // 5%の確率で色違い
const sprites = getSprites(data, isShiny);
state.enemy = {
id: data.id, name: data.name, level: Math.max(1, state.partner.level + (Math.random() > 0.5 ? 2 : -1)),
maxHp: 40 + (state.partner.level * 2),
isShiny: isShiny,
sprite: sprites.frontSprite,
backSprite: sprites.backSprite
};
state.enemy.hp = state.enemy.maxHp;
$('enemy-img').src = state.enemy.sprite; $('enemy-name').innerText = (isShiny ? `✨${state.enemy.name}` : state.enemy.name).toUpperCase();
$('enemy-level').innerText = state.enemy.level; $('player-img').src = state.partner.backSprite;
$('player-pokemon-name').innerText = (state.partner.isShiny ? `✨${state.partner.name}` : state.partner.name).toUpperCase();
$('map-view').classList.add('hidden'); $('map-controls').classList.add('hidden');
$('battle-view').classList.remove('hidden'); $('battle-controls').classList.remove('hidden');
log(`あ!野生の ${isShiny ? '✨色違いの ' : ''}${state.enemy.name.toUpperCase()} だ!`);
if (isShiny) AudioEngine.playShiny(); // 色違い専用の出現音
state.isBusy = false; updateUI();
} catch (e) {
console.error("Encounter error:", e);
log("通信エラーにより出現に失敗しました...");
endBattle();
}
}, 600);
}
window.handleAttack = () => {
if (state.isBusy || !state.enemy) return;
state.isBusy = true; AudioEngine.playHit();
log(`${state.partner.name.toUpperCase()} の攻撃!`);
state.enemy.hp = Math.max(0, state.enemy.hp - (15 + Math.floor(state.partner.level * 1.5)));
$('enemy-img').style.opacity = '0.3';
setTimeout(() => {
$('enemy-img').style.opacity = '1'; updateUI();
if (state.enemy.hp <= 0) { log(`${state.enemy.name.toUpperCase()} を倒した!`); victory(); }
else setTimeout(enemyTurn, 600);
}, 200);
};
function enemyTurn() {
log(`相手の攻撃!`); state.partner.hp = Math.max(0, state.partner.hp - (8 + state.enemy.level));
AudioEngine.playHit(); $('player-img').style.transform = 'translateX(25px)';
setTimeout(() => {
$('player-img').style.transform = 'translateX(0)'; updateUI();
if (state.partner.hp <= 0) { log("パートナーが倒れてしまった..."); setTimeout(endBattle, 1500); }
else state.isBusy = false;
}, 200);
}
window.handleCatch = async () => {
if (state.isBusy || !state.enemy) return;
state.isBusy = true; AudioEngine.playSelect(); log("ボールを投げた!");
setTimeout(async () => {
const success = Math.random() < (0.4 + (1 - state.enemy.hp / state.enemy.maxHp) * 0.5);
if (success) {
AudioEngine.playCatch(); log(`${state.enemy.name.toUpperCase()} をゲットした!`);
state.collection.push({
id: state.enemy.id, name: state.enemy.name, hp: 60 + (state.enemy.level * 2), maxHp: 60 + (state.enemy.level * 2),
level: state.enemy.level, exp: 0, evoStage: 0, isShiny: state.enemy.isShiny,
frontSprite: state.enemy.sprite,
backSprite: state.enemy.backSprite || state.enemy.sprite
});
saveGameData();
setTimeout(endBattle, 1500);
} else { log("逃げ出された!"); setTimeout(enemyTurn, 800); }
}, 800);
};
function victory() {
state.partner.exp += 85;
if (state.partner.exp >= 100) {
state.partner.level++;
state.partner.exp = 0;
state.partner.maxHp += 12;
state.partner.hp = state.partner.maxHp;
log(`レベルアップ! Lv.${state.partner.level}!`);
AudioEngine.playLevelUp();
updateStageUI();
// 進化判定:Lv12以上で1段階目、Lv24以上で2段階目の場合
if ((state.partner.level >= 12 && state.partner.evoStage === 0) ||
(state.partner.level >= 24 && state.partner.evoStage === 1)) {
setTimeout(() => triggerEvolution(), 2500);
return;
}
}
saveGameData();
setTimeout(endBattle, 1200);
}
window.triggerMegaEvolution = async () => {
if (state.isBusy || !state.partner) return;
let currentName = state.partner.name.toLowerCase();
if (currentName.includes('-mega')) {
log(`${state.partner.name.toUpperCase()} は これ以上メガシンカできない!`);
return;
}
state.isBusy = true; AudioEngine.startEvoBGM();
log("キーストーンが 反応している...!");
let targetName = `${currentName}-mega`;
if (currentName === 'charizard') {
targetName = Math.random() < 0.5 ? 'charizard-mega-x' : 'charizard-mega-y';
}
if (currentName === 'mewtwo') {
targetName = Math.random() < 0.5 ? 'mewtwo-mega-x' : 'mewtwo-mega-y';
}
try {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${targetName}`);
if (!res.ok) {
log(`${state.partner.name.toUpperCase()} は メガシンカできないようだ...`);
state.isBusy = false;
AudioEngine.stopEvoBGM();
AudioEngine.startBGM(state.mode === 'battle' ? 'battle' : 'map');
return;
}
const data = await res.json();
$('evo-sprite').src = state.partner.frontSprite;
$('evo-sprite').classList.add('evo-glow');
$('evolution-view').classList.remove('hidden');
setTimeout(() => {
setTimeout(() => {
const oldName = state.partner.name;
state.partner.id = data.id;
state.partner.name = data.name;
state.partner.maxHp += 80;
state.partner.hp = state.partner.maxHp;
const sprites = getSprites(data, state.partner.isShiny);
state.partner.frontSprite = sprites.frontSprite;
state.partner.backSprite = sprites.backSprite;
$('evo-sprite').src = state.partner.frontSprite;
$('evo-message').innerText = `${oldName.toUpperCase()} は\nメガシンカした!`;
AudioEngine.stopEvoBGM();
AudioEngine.playEvoDone();
saveGameData();
setTimeout(() => {
$('evolution-view').classList.add('hidden');
$('evo-sprite').classList.remove('evo-glow');
$('evo-message').innerText = "";
state.isBusy = false; endBattle();
}, 4000);
}, 2500);
}, 500);
} catch (e) {
console.error("Mega Evolution error:", e);
log("メガシンカに 失敗した...");
state.isBusy = false;
AudioEngine.stopEvoBGM();
AudioEngine.startBGM(state.mode === 'battle' ? 'battle' : 'map');
}
};
window.triggerEvolution = async () => {
if (state.isBusy || !state.partner) return;
state.isBusy = true; AudioEngine.startEvoBGM();
log("おや...!? 姿が...!");
try {
const speciesRes = await fetch(`https://pokeapi.co/api/v2/pokemon-species/${state.partner.id}/`);
if (!speciesRes.ok) throw new Error('Species fetch failed');
const speciesData = await speciesRes.json();
const evoRes = await fetch(speciesData.evolution_chain.url);
if (!evoRes.ok) throw new Error('Evo chain fetch failed');
const evoData = await evoRes.json();
let nextEvoName = findNextEvolution(evoData.chain, state.partner.name.toLowerCase());
if (!nextEvoName) {
log(`${state.partner.name.toUpperCase()} は これ以上進化しない!`);
state.isBusy = false;
AudioEngine.stopEvoBGM();
endBattle();
return;
}
$('evo-sprite').src = state.partner.frontSprite;
$('evo-sprite').classList.add('evo-glow');
$('evolution-view').classList.remove('hidden');
setTimeout(() => {
setTimeout(async () => {
try {
const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${nextEvoName.toLowerCase()}/`);
if (!res.ok) throw new Error('Pokemon fetch failed');
const data = await res.json();
const oldName = state.partner.name;
state.partner.id = data.id;
state.partner.name = data.name;
state.partner.maxHp += 45;
state.partner.hp = state.partner.maxHp;
state.partner.evoStage++;
const sprites = getSprites(data, state.partner.isShiny);
state.partner.frontSprite = sprites.frontSprite;
state.partner.backSprite = sprites.backSprite;
$('evo-sprite').src = state.partner.frontSprite;
$('evo-message').innerText = `${oldName.toUpperCase()} は\n${state.partner.name.toUpperCase()} に進化した!`;
AudioEngine.stopEvoBGM();
AudioEngine.playEvoDone();
saveGameData();
setTimeout(() => {
$('evolution-view').classList.add('hidden');
$('evo-sprite').classList.remove('evo-glow');
$('evo-message').innerText = "";
state.isBusy = false; endBattle();
}, 4000);
} catch(e) {
console.error("Evolution sprite error:", e);
$('evolution-view').classList.add('hidden');
state.isBusy = false;
AudioEngine.stopEvoBGM();
endBattle();
}
}, 2500);
}, 500);
} catch (e) {
console.error("Evolution fetch error:", e);
state.isBusy = false;
AudioEngine.stopEvoBGM();
endBattle();
}
}
function findNextEvolution(chain, currentName) {
if (chain.species.name.toLowerCase() === currentName) {
return (chain.evolves_to && chain.evolves_to.length > 0) ? chain.evolves_to[0].species.name : null;
}
for (let next of chain.evolves_to) {
let found = findNextEvolution(next, currentName);
if (found) return found;
}
return null;
}
window.handleHeal = () => {
if (state.isBusy) return;
state.isBusy = true; AudioEngine.playCatch(); log("木陰で回復中...");
setTimeout(() => {
state.collection.forEach(p => p.hp = p.maxHp);
updateUI(); log("みんな元気になった!");
state.isBusy = false; saveGameData();
}, 1000);
};
function endBattle() {
state.enemy = null; state.mode = 'map'; state.isBusy = false;
AudioEngine.stopBGM(); AudioEngine.startBGM('map');
$('battle-view').classList.add('hidden'); $('battle-controls').classList.add('hidden');
$('map-view').classList.remove('hidden'); $('map-controls').classList.remove('hidden');
updateUI(); updateStageUI();
}
function updateStageUI() {
const viewport = $('game-viewport'); const stageName = $('stage-name');
if (!state.partner) return;
let level = state.partner.level;
let stage = 1; if (level >= 20) stage = 4; else if (level >= 15) stage = 3; else if (level >= 10) stage = 2;
if (state.currentStage !== stage) {
state.currentStage = stage; viewport.className = `relative flex-grow overflow-hidden stage-${stage}`;
const names = ["", "Beginning Forest", "Golden Path", "Silent Snowfield", "Abyssal Swamp"];
stageName.innerText = `Stage ${stage}: ${names[stage]}`;
initMap();
log(`エリア「${names[stage]}」に切り替わりました。`);
}
}
function updateUI() {
if (state.partner) {
const p = state.partner; const hpPerc = (p.hp / p.maxHp) * 100;
$('player-hp-fill').style.width = hpPerc + '%'; $('player-hp-cur').innerText = Math.ceil(p.hp);
$('player-hp-max').innerText = p.maxHp; $('player-hp-fill').className = `hp-bar-fill ${hpPerc < 25 ? 'bg-red-500' : 'bg-green-500'}`;
$('player-pokemon-level').innerText = p.level; $('player-sprite').src = p.frontSprite;
$('player-pokemon-name').innerText = (p.isShiny ? `✨${p.name}` : p.name).toUpperCase();
}
if (state.enemy) {
const e = state.enemy; const eHpPerc = Math.max(0, (e.hp / e.maxHp) * 100);
$('enemy-hp-fill').style.width = eHpPerc + '%';
$('enemy-hp-fill').className = `hp-bar-fill ${eHpPerc < 25 ? 'bg-red-500' : 'bg-green-500'}`;
}
$('team-size').innerText = state.collection.length;
}
function renderPokedex() {
const list = $('pokedex-list'); list.innerHTML = '';
state.collection.forEach((p, index) => {
const isActive = state.partner && state.partner.id === p.id && state.partner.name === p.name;
const card = document.createElement('div');
const displayName = p.isShiny ? `✨${p.name}` : p.name;
card.className = `bg-white p-3 rounded-xl border-2 text-center shadow-sm flex flex-col items-center ${isActive ? 'active-card border-[#7cfc00]' : 'border-gray-200'}`;
card.innerHTML = `<img src="${p.frontSprite}" class="w-14 h-14 object-contain"><div class="text-[11px] font-bold uppercase truncate mt-1">${displayName}</div><div class="text-[9px] text-green-600 font-bold">Lv.${p.level}</div>${isActive ? '<div class="mt-2 text-[9px] text-[#2d4a1e] font-bold">PARTNER</div>' : `<button onclick="setActivePartner(${index})" class="mt-2 bg-[#2d4a1e] text-white text-[9px] px-3 py-1 rounded-full">つかう</button>`}`;
list.appendChild(card);
});
}
function log(msg) { $('game-message').innerText = msg; }
// --- Init Sequence ---
async function initGame() {
try {
const hasSave = await loadGameData();
$('loading-modal').classList.add('hidden');
if (hasSave) {
log("レポートから続きを始めます!");
updateUI(); initMap(); updateStageUI(); AudioEngine.startBGM('map');
} else {
$('start-screen').classList.remove('hidden');
log("最初のパートナーを選ぼう!");
}
} catch (e) {
console.error(e);
$('loading-text').innerHTML = `<span class="text-red-500">エラーが発生しました。<br>再読み込みしてください。</span>`;
}
}
window.addEventListener('keydown', e => {
if (state.mode !== 'map' || state.isBusy) return;
let dx=0, dy=0;
if (e.key === 'ArrowUp' || e.key === 'w') dy = -1; if (e.key === 'ArrowDown' || e.key === 's') dy = 1;
if (e.key === 'ArrowLeft' || e.key === 'a') dx = -1; if (e.key === 'ArrowRight' || e.key === 'd') dx = 1;
if (dx !== 0 || dy !== 0) move(dx, dy);
});
initGame();
</script>
</body>
</html>
ポケモン