<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>High-Visibility Lugia Maze</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
body {
margin: 0;
padding: 0;
background-color: #87ceeb;
color: white;
font-family: 'Press Start 2P', cursive;
overflow: hidden;
touch-action: none;
}
#game-canvas {
width: 100vw;
height: 100vh;
display: block;
}
.ui-layer {
position: fixed;
pointer-events: none;
z-index: 10;
width: 100%;
height: 100%;
}
.interactive {
pointer-events: auto;
}
#loading-screen {
background: #ffffff;
z-index: 100;
color: #003366;
}
.d-pad-btn {
background: rgba(0, 51, 102, 0.6);
backdrop-filter: blur(8px);
border: 3px solid #ffffff;
width: 70px;
height: 70px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 24px;
color: #ffffff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.d-pad-btn:active {
background: rgba(0, 51, 102, 0.9);
transform: scale(0.9);
}
.hud-panel {
background: rgba(0, 51, 102, 0.85);
border: 3px solid #ffffff;
color: #ffffff;
border-radius: 12px;
}
@keyframes title-pulse {
0%, 100% { color: #003366; text-shadow: 0 0 5px #ffffff; }
50% { color: #ffffff; text-shadow: 0 0 15px #003366; }
}
.title-anim {
animation: title-pulse 2s ease-in-out infinite;
}
</style>
</head>
<body>
<!-- Loading Screen -->
<div id="loading-screen" class="fixed inset-0 flex flex-col items-center justify-center">
<div class="mb-8 text-2xl font-bold title-anim">POKEMON MAZE</div>
<div id="loading-text" class="mb-4 text-[10px] tracking-widest uppercase">Visualizing Ocean...</div>
<div class="w-64 h-3 bg-gray-200 rounded-full overflow-hidden">
<div id="loading-bar" class="w-0 h-full bg-blue-600 transition-all duration-300"></div>
</div>
</div>
<!-- Start Screen -->
<div id="start-screen" class="fixed inset-0 flex flex-col items-center justify-center bg-blue-900 bg-opacity-80 z-50 text-center px-4 hidden">
<div class="bg-white p-8 rounded-3xl shadow-2xl max-w-lg w-full border-8 border-blue-400">
<h1 class="text-xl md:text-3xl mb-8 text-blue-900 tracking-tighter">LUGIA SEARCH</h1>
<p class="text-[10px] mb-4 text-blue-600">CHOOSE YOUR PARTNER</p>
<div class="mb-8 flex space-x-4 overflow-x-auto p-4 interactive bg-blue-50 rounded-xl" id="character-selector"></div>
<button id="start-btn" class="interactive bg-yellow-400 hover:bg-yellow-300 text-blue-900 px-10 py-4 rounded-full text-lg font-bold shadow-lg transition-all transform active:scale-95 border-b-4 border-yellow-600">
START GAME
</button>
</div>
</div>
<!-- Game UI Layer -->
<div id="game-ui" class="ui-layer hidden p-6 flex flex-col justify-between">
<div class="flex justify-between items-start">
<div class="hud-panel p-4 shadow-xl">
<div class="text-[8px] opacity-80 mb-1">AREA</div>
<div id="level-val" class="text-2xl font-bold">01</div>
</div>
<div class="hud-panel p-4 shadow-xl text-right">
<div class="text-[8px] opacity-80 mb-1">TIME</div>
<div id="timer-val" class="text-2xl font-bold">00:00</div>
</div>
</div>
<!-- Mobile Controls -->
<div class="flex justify-center mb-10 md:hidden">
<div class="grid grid-cols-3 gap-4 interactive">
<div></div>
<button class="d-pad-btn" id="btn-up">↑</button>
<div></div>
<button class="d-pad-btn" id="btn-left">←</button>
<button class="d-pad-btn" id="btn-down">↓</button>
<button class="d-pad-btn" id="btn-right">→</button>
</div>
</div>
</div>
<!-- Victory Overlay -->
<div id="victory-screen" class="fixed inset-0 bg-blue-900 bg-opacity-95 flex flex-col items-center justify-center hidden z-50">
<h2 class="text-3xl text-white mb-8">LUGIA FOUND!</h2>
<div id="next-poke-preview" class="relative w-64 h-64 mb-10">
<div class="absolute inset-0 bg-white opacity-20 blur-3xl rounded-full"></div>
<div id="victory-sprite-container" class="relative z-10 w-full h-full flex items-center justify-center bg-white bg-opacity-10 rounded-full border-4 border-white shadow-2xl"></div>
</div>
<button id="next-btn" class="bg-yellow-400 text-blue-900 px-12 py-5 rounded-full text-xl font-bold interactive shadow-2xl hover:bg-white transition-all">
NEXT STAGE
</button>
</div>
<canvas id="game-canvas"></canvas>
<script>
const API_URL = 'https://pokeapi.co/api/v2/pokemon/';
const LUGIA_ID = 249;
const GRID_SIZE_START = 11;
const MAX_GRID_SIZE = 21;
let state = {
level: 1,
gridSize: GRID_SIZE_START,
maze: [],
playerPos: { x: 1, y: 1 },
goalPos: { x: 0, y: 0 },
playerSpriteUrl: '',
goalSpriteUrl: '',
characters: [],
timer: 0,
timerInterval: null,
isGameActive: false
};
let scene, camera, renderer, playerGroup, goalGroup, mazeGroup;
let ambientLight, sunLight, hemiLight;
let moveTarget = new THREE.Vector3(1, 0.5, 1);
let currentPos = new THREE.Vector3(1, 0.5, 1);
let keys = {};
const textureLoader = new THREE.TextureLoader();
async function init() {
initThree();
try {
updateLoading(20, "Calling Starters...");
const starterIds = [1, 4, 7, 25, 133, 152, 155, 158];
const promises = starterIds.map(id => fetch(API_URL + id).then(res => res.json()));
state.characters = await Promise.all(promises);
const lugiaData = await fetch(API_URL + LUGIA_ID).then(res => res.json());
state.goalSpriteUrl = lugiaData.sprites.other['official-artwork'].front_default || lugiaData.sprites.front_default;
updateLoading(70, "Calibrating Visibility...");
renderCharacterSelector();
updateLoading(100, "Ready!");
setTimeout(() => {
document.getElementById('loading-screen').classList.add('hidden');
document.getElementById('start-screen').classList.remove('hidden');
}, 800);
} catch (err) {
console.error("Initialization error:", err);
document.getElementById('loading-text').innerText = "CONNECTION ERROR";
}
}
function initThree() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0xadd8e6); // Light Sky Blue for contrast
scene.fog = new THREE.Fog(0xadd8e6, 5, 40);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('game-canvas'), antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
// Balanced Lighting for High Visibility
ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
hemiLight = new THREE.HemisphereLight(0xffffff, 0x003366, 0.5);
scene.add(hemiLight);
sunLight = new THREE.DirectionalLight(0xffffff, 0.8);
sunLight.position.set(10, 20, 10);
sunLight.castShadow = true;
sunLight.shadow.camera.left = -20;
sunLight.shadow.camera.right = 20;
sunLight.shadow.camera.top = 20;
sunLight.shadow.camera.bottom = -20;
scene.add(sunLight);
window.addEventListener('resize', onWindowResize);
animate();
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function updateLoading(progress, text) {
document.getElementById('loading-bar').style.width = progress + '%';
document.getElementById('loading-text').innerText = text;
}
function renderCharacterSelector() {
const container = document.getElementById('character-selector');
state.characters.forEach((poke, index) => {
const div = document.createElement('div');
div.className = `cursor-pointer p-2 border-4 ${index === 0 ? 'border-blue-600' : 'border-transparent'} hover:border-blue-300 bg-white rounded-2xl flex-shrink-0 transition-all transform hover:scale-110 shadow-md`;
div.innerHTML = `<img src="${poke.sprites.front_default}" class="w-16 h-16" style="image-rendering:pixelated">`;
div.onclick = () => {
document.querySelectorAll('#character-selector div').forEach(d => d.classList.replace('border-blue-600', 'border-transparent'));
div.classList.replace('border-transparent', 'border-blue-600');
state.playerSpriteUrl = poke.sprites.front_default;
};
container.appendChild(div);
if(index === 0) state.playerSpriteUrl = poke.sprites.front_default;
});
}
function generateMaze(size) {
const maze = Array(size).fill().map(() => Array(size).fill(1));
function walk(x, y) {
maze[y][x] = 0;
const dirs = [[0,-2],[0,2],[-2,0],[2,0]].sort(() => Math.random() - 0.5);
for (let [dx, dy] of dirs) {
const nx = x+dx, ny = y+dy;
if (nx>0 && nx<size-1 && ny>0 && ny<size-1 && maze[ny][nx] === 1) {
maze[y+dy/2][x+dx/2] = 0;
walk(nx, ny);
}
}
}
walk(1, 1);
return maze;
}
async function setupLevel() {
state.isGameActive = false;
if (mazeGroup) scene.remove(mazeGroup);
mazeGroup = new THREE.Group();
state.maze = generateMaze(state.gridSize);
state.playerPos = { x: 1, y: 1 };
state.goalPos = { x: state.gridSize - 2, y: state.gridSize - 2 };
state.maze[state.goalPos.y][state.goalPos.x] = 0;
// High Contrast Walls
const wallMat = new THREE.MeshStandardMaterial({ color: 0x003366, roughness: 0.5, metalness: 0.1 });
const wallGeo = new THREE.BoxGeometry(1, 1.5, 1);
// High Contrast Floor (Sand Color)
const floorGeo = new THREE.PlaneGeometry(state.gridSize * 3, state.gridSize * 3);
const floorMat = new THREE.MeshStandardMaterial({ color: 0xd2b48c }); // Tan/Sand color
const floorMesh = new THREE.Mesh(floorGeo, floorMat);
floorMesh.rotation.x = -Math.PI / 2;
floorMesh.position.set(state.gridSize/2, 0, state.gridSize/2);
floorMesh.receiveShadow = true;
mazeGroup.add(floorMesh);
for (let y = 0; y < state.gridSize; y++) {
for (let x = 0; x < state.gridSize; x++) {
if (state.maze[y][x] === 1) {
const wall = new THREE.Mesh(wallGeo, wallMat);
wall.position.set(x, 0.75, y);
wall.castShadow = true;
wall.receiveShadow = true;
mazeGroup.add(wall);
}
}
}
// Player Group
playerGroup = new THREE.Group();
const pSprite = createPokemonSprite(state.playerSpriteUrl);
playerGroup.add(pSprite);
playerGroup.position.set(1, 0.5, 1);
mazeGroup.add(playerGroup);
// Goal Group
goalGroup = new THREE.Group();
const gSprite = createPokemonSprite(state.goalSpriteUrl, true);
goalGroup.add(gSprite);
goalGroup.position.set(state.goalPos.x, 0.8, state.goalPos.y);
mazeGroup.add(goalGroup);
scene.add(mazeGroup);
currentPos.set(1, 0.5, 1);
moveTarget.copy(currentPos);
document.getElementById('level-val').innerText = state.level.toString().padStart(2, '0');
startTimer();
state.isGameActive = true;
}
function createPokemonSprite(url, isGoal = false) {
const texture = textureLoader.load(url);
texture.magFilter = THREE.NearestFilter;
// alphaTest solves the transparent sorting issue in 3D
const mat = new THREE.SpriteMaterial({ map: texture, transparent: true, alphaTest: 0.5 });
const sprite = new THREE.Sprite(mat);
sprite.scale.set(1.5, 1.5, 1);
if (isGoal) sprite.scale.set(4, 4, 1);
return sprite;
}
function animate() {
requestAnimationFrame(animate);
const time = Date.now() * 0.001;
if (state.isGameActive && playerGroup) {
handleInput();
currentPos.lerp(moveTarget, 0.15);
playerGroup.position.copy(currentPos);
// Clear Visibility Camera
const offset = new THREE.Vector3(0, 7, 5);
const camPos = currentPos.clone().add(offset);
camera.position.lerp(camPos, 0.1);
camera.lookAt(currentPos.x, 0, currentPos.z - 2);
if (goalGroup) {
goalGroup.position.y = 1.0 + Math.sin(time * 2.5) * 0.5;
}
if (currentPos.distanceTo(new THREE.Vector3(state.goalPos.x, 0.5, state.goalPos.y)) < 1.0) {
state.isGameActive = false;
winLevel();
}
}
renderer.render(scene, camera);
}
function handleInput() {
if (currentPos.distanceTo(moveTarget) > 0.05) return;
let dx = 0, dz = 0;
const isUp = keys['arrowup'] || keys['w'] || keys['btn-up'];
const isDown = keys['arrowdown'] || keys['s'] || keys['btn-down'];
const isLeft = keys['arrowleft'] || keys['a'] || keys['btn-left'];
const isRight = keys['arrowright'] || keys['d'] || keys['btn-right'];
if (isUp) dz = -1;
else if (isDown) dz = 1;
else if (isLeft) dx = -1;
else if (isRight) dx = 1;
if (dx !== 0 || dz !== 0) {
const nx = state.playerPos.x + dx;
const nz = state.playerPos.y + dz;
if (nx >= 0 && nx < state.gridSize && nz >= 0 && nz < state.gridSize && state.maze[nz][nx] === 0) {
state.playerPos.x = nx;
state.playerPos.y = nz;
moveTarget.set(nx, 0.5, nz);
}
}
}
function winLevel() {
clearInterval(state.timerInterval);
const overlay = document.getElementById('victory-screen');
const container = document.getElementById('victory-sprite-container');
container.innerHTML = `<img src="${state.goalSpriteUrl}" class="w-52 h-52">`;
overlay.classList.remove('hidden');
}
async function nextLevel() {
state.level++;
if (state.level % 2 === 0 && state.gridSize < MAX_GRID_SIZE) state.gridSize += 2;
document.getElementById('victory-screen').classList.add('hidden');
await setupLevel();
}
function startTimer() {
state.timer = 0;
clearInterval(state.timerInterval);
state.timerInterval = setInterval(() => {
state.timer++;
const mins = Math.floor(state.timer / 60).toString().padStart(2, '0');
const secs = (state.timer % 60).toString().padStart(2, '0');
document.getElementById('timer-val').innerText = `${mins}:${secs}`;
}, 1000);
}
window.addEventListener('keydown', (e) => keys[e.key.toLowerCase()] = true);
window.addEventListener('keyup', (e) => keys[e.key.toLowerCase()] = false);
['up', 'down', 'left', 'right'].forEach(dir => {
const btn = document.getElementById(`btn-${dir}`);
btn.addEventListener('pointerdown', (e) => { e.preventDefault(); keys[`btn-${dir}`] = true; });
btn.addEventListener('pointerup', () => keys[`btn-${dir}`] = false);
btn.addEventListener('pointerleave', () => keys[`btn-${dir}`] = false);
});
document.getElementById('start-btn').onclick = async () => {
document.getElementById('start-screen').classList.add('hidden');
document.getElementById('game-ui').classList.remove('hidden');
await setupLevel();
};
document.getElementById('next-btn').onclick = nextLevel;
window.onload = init;
</script>
</body>
</html>
ポケモンでぼうけん!