🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
HTML / CSS / JS
<!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>

poke

ポケモン

🤖 使用したAI
Gemini
👁 39 回閲覧
📅 投稿:2026年4月19日 🔄 更新:2026年8月13日
応援スタンプを押してみよう!