🔬 AI発明ギャラリー kizu ポケモン図鑑
🔒 サンドボックス内で実行中 ⛶ 全画面で遊ぶ
HTML / CSS / JS
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>究極のポケモン図鑑 + バトルモード</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://unpkg.com/lucide@latest"></script>
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700;900&display=swap');
    
    body { font-family: 'Noto Sans JP', sans-serif; }
    
    @keyframes fadeInZoom {
      from { opacity: 0; transform: scale(0.95) translateY(10px); }
      to { opacity: 1; transform: scale(1) translateY(0); }
    }
    .animate-zoom-in { animation: fadeInZoom 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }

    @keyframes shake {
      0%, 100% { transform: translateX(0); }
      25% { transform: translateX(-5px) rotate(-1deg); }
      75% { transform: translateX(5px) rotate(1deg); }
    }
    .animate-shake { animation: shake 0.2s ease-in-out infinite; }

    @keyframes attack-right {
      0% { transform: translateX(0); }
      50% { transform: translateX(50px); }
      100% { transform: translateX(0); }
    }
    .animate-attack-right { animation: attack-right 0.3s ease-in-out; }

    @keyframes attack-left {
      0% { transform: translateX(0); }
      50% { transform: translateX(-50px); }
      100% { transform: translateX(0); }
    }
    .animate-attack-left { animation: attack-left 0.3s ease-in-out; }

    .hp-bar-transition { transition: width 0.5s ease-out; }

    ::-webkit-scrollbar { width: 8px; }
    ::-webkit-scrollbar-track { background: #f1f1f1; }
    ::-webkit-scrollbar-thumb { background: #ef4444; border-radius: 10px; }
  </style>
</head>
<body class="bg-slate-100 text-slate-800 min-h-screen pb-20">

  <!-- ヘッダー -->
  <header class="bg-red-600 text-white p-4 shadow-lg sticky top-0 z-30">
    <div class="max-w-6xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
      <div class="flex items-center gap-3">
        <div class="w-10 h-10 bg-white rounded-full border-4 border-slate-900 flex items-center justify-center relative shadow-inner">
          <div class="w-full h-1.5 bg-slate-900 absolute top-1/2 -translate-y-1/2"></div>
          <div class="w-4 h-4 bg-white rounded-full border-2 border-slate-900 relative z-10"></div>
        </div>
        <h1 class="text-2xl font-black tracking-tighter uppercase italic">PokeBattle</h1>
      </div>
      
      <div class="flex items-center gap-2 w-full sm:w-auto">
        <div class="relative flex-1 sm:w-64 text-slate-800">
          <input type="text" id="search-input" placeholder="名前や番号で検索..." class="w-full pl-10 pr-4 py-2 rounded-xl focus:outline-none shadow-md border-none" />
          <i data-lucide="search" class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 w-4 h-4"></i>
        </div>
        <button id="view-collection" class="bg-slate-900 text-white p-2 rounded-xl shadow-md hover:bg-slate-800 transition-colors relative">
          <i data-lucide="briefcase"></i>
          <span id="collection-count" class="absolute -top-2 -right-2 bg-yellow-400 text-slate-900 text-[10px] font-black w-5 h-5 rounded-full flex items-center justify-center border-2 border-slate-900">0</span>
        </button>
      </div>
    </div>
  </header>

  <!-- メインレイアウト -->
  <main class="max-w-6xl mx-auto p-4 sm:p-8">
    <div id="view-title" class="mb-6 flex justify-between items-center">
        <h2 class="text-xl font-black text-slate-400 uppercase tracking-widest">ポケモンリスト</h2>
        <span id="status-text" class="text-sm font-bold text-slate-400">読み込み中...</span>
    </div>
    
    <div id="pokemon-grid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-6"></div>
    <div id="loading" class="flex flex-col items-center justify-center py-12 gap-4">
      <div class="animate-spin rounded-full h-10 w-10 border-4 border-slate-200 border-t-red-500"></div>
    </div>
    <div id="no-results" class="hidden flex flex-col items-center py-20 opacity-50 text-center w-full">
      <i data-lucide="search-x" class="w-16 h-16 mb-4 mx-auto"></i>
      <p class="text-xl font-medium">見つかりませんでした</p>
    </div>
  </main>

  <!-- ポケモン選択モーダル -->
  <div id="select-modal" class="fixed inset-0 bg-slate-900/90 backdrop-blur-sm flex items-center justify-center p-4 z-[60] hidden">
    <div class="bg-white rounded-3xl p-6 w-full max-w-md shadow-2xl">
      <h3 class="text-xl font-black mb-4 flex items-center gap-2 italic">
        <i data-lucide="swords" class="text-red-500"></i> バトルに出すポケモンを選択
      </h3>
      <div id="my-pokemon-list" class="grid grid-cols-2 gap-4 max-h-80 overflow-y-auto mb-6 p-2"></div>
      <button onclick="document.getElementById('select-modal').classList.add('hidden')" class="w-full py-3 bg-slate-200 rounded-xl font-bold">キャンセル</button>
    </div>
  </div>

  <!-- バトル画面 -->
  <div id="battle-scene" class="fixed inset-0 bg-emerald-900 z-[70] hidden overflow-hidden flex flex-col">
    <div class="absolute inset-0 opacity-20 pointer-events-none">
        <div class="absolute top-0 left-0 w-full h-full bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-white/20 to-transparent"></div>
    </div>
    <div class="flex-1 relative flex items-center justify-end pr-10 sm:pr-20">
      <div id="enemy-platform" class="relative group">
        <div class="absolute -bottom-4 left-1/2 -translate-x-1/2 w-48 h-12 bg-black/20 rounded-[100%] blur-md"></div>
        <img id="enemy-img" src="" class="w-48 h-48 sm:w-64 sm:h-64 object-contain relative z-10" />
        <div class="absolute top-0 -left-64 bg-white/90 backdrop-blur p-4 rounded-br-3xl rounded-tl-3xl shadow-xl w-56 border-l-8 border-red-500">
           <p id="enemy-name" class="font-black text-slate-800 truncate"></p>
           <div class="flex items-center gap-2 mt-2">
              <span class="text-[10px] font-bold text-slate-400">HP</span>
              <div class="flex-1 h-3 bg-slate-200 rounded-full overflow-hidden border border-slate-300">
                 <div id="enemy-hp-bar" class="h-full bg-green-500 hp-bar-transition" style="width: 100%"></div>
              </div>
           </div>
        </div>
      </div>
    </div>
    <div class="flex-1 relative flex items-center justify-start pl-10 sm:pl-20">
      <div id="player-platform" class="relative">
        <div class="absolute -bottom-4 left-1/2 -translate-x-1/2 w-48 h-12 bg-black/20 rounded-[100%] blur-md"></div>
        <img id="player-img" src="" class="w-48 h-48 sm:w-64 sm:h-64 object-contain relative z-10 scale-x-[-1]" />
        <div class="absolute -top-10 -right-64 bg-white/90 backdrop-blur p-4 rounded-bl-3xl rounded-tr-3xl shadow-xl w-56 border-r-8 border-blue-500">
           <p id="player-name" class="font-black text-slate-800 truncate"></p>
           <div class="flex items-center gap-2 mt-2">
              <span class="text-[10px] font-bold text-slate-400">HP</span>
              <div class="flex-1 h-3 bg-slate-200 rounded-full overflow-hidden border border-slate-300">
                 <div id="player-hp-bar" class="h-full bg-green-500 hp-bar-transition" style="width: 100%"></div>
              </div>
           </div>
           <p class="text-right text-[10px] font-bold mt-1 text-slate-400"><span id="player-hp-text">100</span> / 100</p>
        </div>
      </div>
    </div>
    <div class="h-1/3 bg-slate-900/90 border-t-4 border-slate-800 p-4 flex flex-col sm:flex-row gap-4">
      <div id="battle-log" class="flex-1 bg-black/40 rounded-xl p-4 text-white text-sm font-medium overflow-y-auto font-mono">
        <p class="text-emerald-400">バトル開始!</p>
      </div>
      <div id="battle-actions" class="w-full sm:w-72 grid grid-cols-2 gap-2"></div>
    </div>
  </div>

  <!-- 詳細モーダル -->
  <div id="pokemon-modal" class="fixed inset-0 bg-slate-900/80 backdrop-blur-md flex items-center justify-center p-4 z-50 hidden">
    <div class="bg-white rounded-3xl shadow-2xl w-full max-w-lg overflow-hidden relative animate-zoom-in max-h-[90vh] overflow-y-auto">
      <button id="close-modal" class="absolute top-4 right-4 text-white hover:bg-black/20 bg-black/10 rounded-full p-2 z-20">
        <i data-lucide="x"></i>
      </button>
      <div id="modal-loading" class="h-96 flex items-center justify-center hidden">
         <div class="animate-spin rounded-full h-12 w-12 border-4 border-red-500 border-t-transparent"></div>
      </div>
      <div id="modal-content" class="hidden">
        <div id="modal-header" class="pt-12 pb-20 px-8 relative">
           <div class="relative z-10 text-white">
              <span id="modal-id" class="text-sm font-black bg-black/20 px-3 py-1 rounded-full mb-2 inline-block"></span>
              <h2 id="modal-name-jp" class="text-4xl font-black mb-1"></h2>
              <div id="modal-types" class="flex gap-2 mt-4"></div>
           </div>
        </div>
        <div class="bg-white px-8 pb-10 pt-16 relative rounded-t-[2.5rem] -mt-10">
          <div class="absolute -top-32 left-1/2 -translate-x-1/2 w-48 h-48">
             <img id="modal-image" src="" class="w-full h-full object-contain" />
          </div>
          <div class="flex flex-col gap-3 mb-8">
              <button id="start-battle-btn" class="bg-orange-500 hover:bg-orange-600 text-white font-black py-4 px-6 rounded-2xl shadow-lg flex items-center justify-center gap-3">
                  <i data-lucide="swords"></i> バトルを挑む
              </button>
              <button id="catch-btn" class="bg-red-500 hover:bg-red-600 text-white font-black py-4 px-6 rounded-2xl shadow-lg flex items-center justify-center gap-3">
                  <i data-lucide="target"></i> つかまえる
              </button>
              <p id="already-caught-msg" class="hidden text-green-600 font-bold text-center">
                  <i data-lucide="check-circle" class="inline w-4 h-4"></i> つかまえ済み
              </p>
          </div>
          <div id="modal-stats" class="space-y-4"></div>
        </div>
      </div>
    </div>
  </div>

  <script>
    lucide.createIcons();

    const typeColors = {
      normal: 'bg-[#949495]', fire: 'bg-[#ff4422]', water: 'bg-[#3399ff]',
      electric: 'bg-[#ffcc33]', grass: 'bg-[#77cc55]', ice: 'bg-[#66ccff]',
      fighting: 'bg-[#bb5544]', poison: 'bg-[#aa5599]', ground: 'bg-[#ddbb55]',
      flying: 'bg-[#8899ff]', psychic: 'bg-[#ff5599]', bug: 'bg-[#aabb22]',
      rock: 'bg-[#bbaa66]', ghost: 'bg-[#6666bb]', dragon: 'bg-[#7766ee]',
      dark: 'bg-[#775544]', steel: 'bg-[#aaaabb]', fairy: 'bg-[#ee99ee]',
    };

    let jpNameCache = {};
    let allPokemons = [];
    let caughtPokemons = JSON.parse(localStorage.getItem('my_pokemons') || '[]');
    let currentPokemonData = null;
    let battleState = { active: false, player: null, enemy: null, turn: 'player' };

    // 日本語名を取得する関数
    async function fetchJapaneseName(id) {
        if (jpNameCache[id]) return jpNameCache[id];
        try {
            const res = await fetch(`https://pokeapi.co/api/v2/pokemon-species/${id}/`);
            const data = await res.json();
            const jpName = data.names.find(n => n.language.name === 'ja-Hrkt')?.name || data.name;
            jpNameCache[id] = jpName;
            return jpName;
        } catch (e) {
            return "???";
        }
    }

    async function init() {
      updateCollectionCount();
      try {
        const response = await fetch(`https://pokeapi.co/api/v2/pokemon?limit=151`);
        const data = await response.json();
        
        allPokemons = data.results.map((p, i) => ({
          id: String(i + 1), 
          name: p.name,
          image: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${i+1}.png`
        }));

        await renderGrid(allPokemons);
        document.getElementById('loading').classList.add('hidden');
        document.getElementById('status-text').textContent = `151匹 読み込み済み`;
      } catch (e) { console.error(e); }
    }

    function updateCollectionCount() {
        document.getElementById('collection-count').textContent = caughtPokemons.length;
    }

    async function renderGrid(list) {
      const grid = document.getElementById('pokemon-grid');
      grid.innerHTML = '';
      
      for (const p of list) {
        const isCaught = caughtPokemons.some(cp => cp.id === p.id);
        const div = document.createElement('div');
        div.className = "pokemon-card bg-white rounded-3xl p-4 shadow-sm hover:shadow-xl transition-all cursor-pointer border border-slate-200 flex flex-col items-center group relative animate-zoom-in";
        div.onclick = () => showDetail(p.id);
        
        div.innerHTML = `
          <div class="absolute top-3 left-4 flex items-center gap-1">
              <span class="text-[10px] font-black text-slate-300">#${p.id.padStart(3, '0')}</span>
              ${isCaught ? '<div class="w-3 h-3 bg-red-500 rounded-full border border-white"></div>' : ''}
          </div>
          <img src="${p.image}" class="w-24 h-24 object-contain mb-2 group-hover:scale-110 transition-transform" />
          <h2 id="name-${p.id}" class="font-bold text-slate-700 text-sm">読み込み中...</h2>
        `;
        grid.appendChild(div);

        // 非同期で日本語名をセット
        fetchJapaneseName(p.id).then(jpName => {
            const nameEl = document.getElementById(`name-${p.id}`);
            if (nameEl) nameEl.textContent = jpName;
        });
      }
    }

    async function showDetail(id) {
      const modal = document.getElementById('pokemon-modal');
      document.getElementById('modal-loading').classList.remove('hidden');
      document.getElementById('modal-content').classList.add('hidden');
      modal.classList.remove('hidden');

      const pRes = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
      const data = await pRes.json();
      currentPokemonData = data;
      
      const jpName = await fetchJapaneseName(id);
      currentPokemonData.jpName = jpName;

      document.getElementById('modal-header').className = `pt-12 pb-20 px-8 relative ${typeColors[data.types[0].type.name] || 'bg-slate-400'}`;
      document.getElementById('modal-name-jp').textContent = jpName;
      document.getElementById('modal-id').textContent = `#${id}`;
      document.getElementById('modal-image').src = data.sprites.other['official-artwork'].front_default;
      
      const isCaught = caughtPokemons.some(cp => cp.id === String(id));
      document.getElementById('catch-btn').classList.toggle('hidden', isCaught);
      document.getElementById('already-caught-msg').classList.toggle('hidden', !isCaught);

      document.getElementById('modal-types').innerHTML = data.types.map(t => 
        `<span class="bg-white/20 px-3 py-1 rounded-full text-xs font-bold uppercase">${t.type.name}</span>`
      ).join('');

      // ステータス表示
      document.getElementById('modal-stats').innerHTML = data.stats.map(s => `
        <div class="flex items-center gap-4">
            <span class="text-[10px] font-bold text-slate-400 w-24 uppercase">${s.stat.name}</span>
            <div class="flex-1 h-2 bg-slate-100 rounded-full overflow-hidden">
                <div class="h-full bg-slate-800" style="width: ${Math.min(100, s.base_stat)}%"></div>
            </div>
            <span class="text-xs font-black w-8 text-right">${s.base_stat}</span>
        </div>
      `).join('');

      document.getElementById('modal-loading').classList.add('hidden');
      document.getElementById('modal-content').classList.remove('hidden');
    }

    document.getElementById('start-battle-btn').onclick = () => {
        if (caughtPokemons.length === 0) {
            alert("まずは野生のポケモンを1匹つかまえてください!");
            return;
        }
        const list = document.getElementById('my-pokemon-list');
        list.innerHTML = '';
        caughtPokemons.forEach(p => {
            const btn = document.createElement('button');
            btn.className = "flex flex-col items-center bg-slate-50 p-3 rounded-2xl hover:bg-red-50 border-2 border-transparent hover:border-red-200 transition-all";
            btn.onclick = () => startBattle(p);
            btn.innerHTML = `<img src="${p.image}" class="w-16 h-16 object-contain" /><span class="text-xs font-bold mt-2 truncate w-full text-center">${p.jpName}</span>`;
            list.appendChild(btn);
        });
        document.getElementById('select-modal').classList.remove('hidden');
    };

    async function startBattle(playerPoke) {
        document.getElementById('select-modal').classList.add('hidden');
        document.getElementById('pokemon-modal').classList.add('hidden');
        const battleScene = document.getElementById('battle-scene');
        battleScene.classList.remove('hidden');
        document.getElementById('battle-log').innerHTML = '';

        battleState = {
            active: true,
            player: { 
                name: playerPoke.jpName, 
                hp: 100, maxHp: 100, 
                img: playerPoke.image, 
                moves: ['たいあたり', 'でんこうせっか', 'はたく']
            },
            enemy: { 
                name: currentPokemonData.jpName, 
                hp: 100, maxHp: 100, 
                img: currentPokemonData.sprites.other['official-artwork'].front_default,
                id: currentPokemonData.id
            },
            turn: 'player'
        };

        document.getElementById('player-img').src = battleState.player.img;
        document.getElementById('player-name').textContent = battleState.player.name;
        document.getElementById('enemy-img').src = battleState.enemy.img;
        document.getElementById('enemy-name').textContent = battleState.enemy.name;
        
        updateBattleUI();
        logMessage(`野生の ${battleState.enemy.name} が あらわれた!`);
        createActionButtons();
    }

    function updateBattleUI() {
        const pHP = (battleState.player.hp / battleState.player.maxHp) * 100;
        const eHP = (battleState.enemy.hp / battleState.enemy.maxHp) * 100;
        document.getElementById('player-hp-bar').style.width = `${pHP}%`;
        document.getElementById('player-hp-text').textContent = Math.ceil(battleState.player.hp);
        document.getElementById('enemy-hp-bar').style.width = `${eHP}%`;
    }

    function logMessage(msg) {
        const log = document.getElementById('battle-log');
        const p = document.createElement('p');
        p.className = "mb-1";
        p.textContent = msg;
        log.appendChild(p);
        log.scrollTop = log.scrollHeight;
    }

    function createActionButtons() {
        const container = document.getElementById('battle-actions');
        container.innerHTML = '';
        if (battleState.turn !== 'player') return;

        battleState.player.moves.forEach(move => {
            const btn = document.createElement('button');
            btn.className = "bg-white hover:bg-slate-100 text-slate-800 font-bold py-3 px-2 rounded-xl text-sm transition-all border-b-4 border-slate-200 active:translate-y-1 active:border-b-0";
            btn.textContent = move;
            btn.onclick = () => handlePlayerMove(move);
            container.appendChild(btn);
        });
    }

    function handlePlayerMove(move) {
        battleState.turn = 'enemy';
        createActionButtons();
        logMessage(`${battleState.player.name} の ${move}!`);
        
        document.getElementById('player-img').classList.add('animate-attack-right');
        setTimeout(() => document.getElementById('player-img').classList.remove('animate-attack-right'), 300);

        const damage = 20 + Math.random() * 20;
        battleState.enemy.hp = Math.max(0, battleState.enemy.hp - damage);
        
        setTimeout(() => {
            updateBattleUI();
            if (battleState.enemy.hp <= 0) {
                logMessage(`野生の ${battleState.enemy.name} を たおした!`);
                setTimeout(() => {
                    alert("バトルに勝利しました!");
                    document.getElementById('battle-scene').classList.add('hidden');
                    showDetail(battleState.enemy.id);
                }, 1000);
            } else {
                setTimeout(handleEnemyTurn, 1000);
            }
        }, 500);
    }

    function handleEnemyTurn() {
        logMessage(`野生の ${battleState.enemy.name} の こうげき!`);
        document.getElementById('enemy-img').classList.add('animate-attack-left');
        setTimeout(() => document.getElementById('enemy-img').classList.remove('animate-attack-left'), 300);

        const damage = 10 + Math.random() * 15;
        battleState.player.hp = Math.max(0, battleState.player.hp - damage);
        
        setTimeout(() => {
            updateBattleUI();
            if (battleState.player.hp <= 0) {
                logMessage(`${battleState.player.name} は たおれてしまった...`);
                setTimeout(() => {
                    document.getElementById('battle-scene').classList.add('hidden');
                }, 1500);
            } else {
                battleState.turn = 'player';
                createActionButtons();
            }
        }, 500);
    }

    document.getElementById('catch-btn').onclick = async () => {
        if (!currentPokemonData) return;
        const success = Math.random() > 0.3;
        if (success) {
            const newCaught = { 
                id: String(currentPokemonData.id), 
                jpName: currentPokemonData.jpName,
                image: currentPokemonData.sprites.other['official-artwork'].front_default 
            };
            if (!caughtPokemons.find(p => p.id === newCaught.id)) {
                caughtPokemons.push(newCaught);
                localStorage.setItem('my_pokemons', JSON.stringify(caughtPokemons));
                updateCollectionCount();
                alert(`${currentPokemonData.jpName} をつかまえた!`);
                document.getElementById('pokemon-modal').classList.add('hidden');
                renderGrid(allPokemons);
            }
        } else {
            alert("ざんねん!つかまえられなかった...");
        }
    };

    document.getElementById('view-collection').onclick = () => {
        renderGrid(caughtPokemons.map(p => ({
            id: p.id,
            image: p.image,
            name: p.jpName // allPokemonsと同じ形式で渡す
        })));
        document.getElementById('view-title').querySelector('h2').textContent = "つかまえたポケモン";
    };

    document.getElementById('search-input').oninput = (e) => {
        const val = e.target.value.toLowerCase();
        const filtered = allPokemons.filter(p => p.name.includes(val) || p.id === val);
        renderGrid(filtered);
        document.getElementById('view-title').querySelector('h2').textContent = "ポケモンリスト";
    };

    document.getElementById('close-modal').onclick = () => document.getElementById('pokemon-modal').classList.add('hidden');

    init();
  </script>
</body>
</html>

ポケモン図鑑

by kizu

ポケモンを捕まえたり戦ったりするゲーム

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