<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>筋斗雲 3Dレーシング</title>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #87CEEB; /* Sky blue */
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
touch-action: none;
}
#game-canvas {
display: block;
width: 100vw;
height: 100vh;
}
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.hud {
display: flex;
justify-content: space-between;
padding: 20px;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
font-size: 1.2rem;
font-weight: bold;
}
#level-display {
color: #ffeb3b;
}
#screens {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
pointer-events: auto;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(4px);
transition: opacity 0.3s ease;
}
.screen-content {
background: white;
padding: 30px;
border-radius: 20px;
text-align: center;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
max-width: 500px;
width: 90%;
}
h1 {
color: #ff9800;
margin-top: 0;
font-size: 1.8rem;
text-shadow: 1px 1px 0px #d87b00;
}
.char-selector {
display: flex;
justify-content: center;
gap: 15px;
margin: 20px 0;
}
.char-option {
flex: 1;
padding: 10px;
border: 3px solid #ddd;
border-radius: 15px;
cursor: pointer;
transition: all 0.2s;
background: #f9f9f9;
}
.char-option.selected {
border-color: #ff9800;
background: #fff3e0;
transform: scale(1.05);
}
.char-preview {
width: 40px;
height: 40px;
border-radius: 50%;
margin: 0 auto 10px;
border: 2px solid rgba(0,0,0,0.1);
}
.btn {
background: #ffeb3b;
color: #e65100;
border: 2px solid #ffc107;
padding: 15px 30px;
font-size: 1.2rem;
font-weight: bold;
border-radius: 30px;
cursor: pointer;
margin-top: 10px;
transition: all 0.2s;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.btn:hover {
background: #ffc107;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.2);
}
.hidden {
opacity: 0;
pointer-events: none !important;
}
#lives {
color: #ff5252;
}
#damage-flash {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: red;
opacity: 0;
pointer-events: none;
transition: opacity 0.1s;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
</head>
<body>
<div id="damage-flash"></div>
<div id="ui-layer">
<div class="hud">
<div id="level-display">LEVEL: <span id="level-val">1</span></div>
<div>SCORE: <span id="score-val">0</span></div>
<div id="lives">♥♥♥</div>
<div>SPEED: <span id="speed-val">0.7</span>x</div>
</div>
</div>
<div id="screens">
<div class="screen-content" id="start-screen">
<h1>筋斗雲レーシング</h1>
<p>新しいキャラクターを選んでね!</p>
<div class="char-selector">
<div class="char-option selected" data-char="raijin">
<div class="char-preview" style="background: #333333; border-color: #ffff00;"></div>
<div>雷神</div>
</div>
<div class="char-option" data-char="peach">
<div class="char-preview" style="background: #ffb7c5;"></div>
<div>桃幻</div>
</div>
<div class="char-option" data-char="dark">
<div class="char-preview" style="background: #1a1a1a; border-color: #4b0082;"></div>
<div>漆黒</div>
</div>
</div>
<button class="btn" id="start-btn">ゲームスタート</button>
<p style="font-size: 0.9rem; margin-top: 15px;">操作: 矢印キー / スワイプ</p>
</div>
<div class="screen-content hidden" id="game-over-screen">
<h1 style="color: #f44336;">GAME OVER</h1>
<p>最終スコア: <strong id="final-score" style="font-size:1.5rem;">0</strong></p>
<button class="btn" id="restart-btn">もう一度乗る</button>
</div>
</div>
<script>
// --- State Variables ---
let scene, camera, renderer;
let playerGroup;
let activeObjects = [];
let bgClouds = [];
let gameStarted = false;
let isGameOver = false;
let score = 0;
let lives = 3;
let baseSpeed = 0.7;
let currentSpeed = 0;
let distanceTraveled = 0;
let invulnerableTime = 0;
let currentLevel = 1;
let selectedChar = 'raijin';
const charConfigs = {
raijin: { cloud: 0x333333, body: 0xffff00, head: 0xffe0bd },
peach: { cloud: 0xffb7c5, body: 0xff69b4, head: 0xffe0bd },
dark: { cloud: 0x1a1a1a, body: 0x4b0082, head: 0xffe0bd }
};
let targetBgColor = new THREE.Color(0x87CEEB);
let targetFogColor = new THREE.Color(0x87CEEB);
const keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false, w: false, a: false, s: false, d: false };
const touchInput = { active: false, x: 0, y: 0, startX: 0, startY: 0, playerStartX: 0, playerStartY: 0 };
const BOUNDARY_X = 12;
const BOUNDARY_Y_MAX = 8;
const BOUNDARY_Y_MIN = -2;
const SPAWN_Z = -150;
const DESPAWN_Z = 20;
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.Fog(0x87CEEB, 30, 150);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 250);
camera.position.set(0, 3, 10);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(20, 50, 20);
dirLight.castShadow = true;
scene.add(dirLight);
createPlayer();
for(let i=0; i<10; i++) {
spawnBackgroundCloud(true);
}
// キャラクター選択のリスナー
document.querySelectorAll('.char-option').forEach(opt => {
opt.addEventListener('click', () => {
document.querySelectorAll('.char-option').forEach(o => o.classList.remove('selected'));
opt.classList.add('selected');
selectedChar = opt.dataset.char;
updatePlayerSkin();
});
});
window.addEventListener('resize', onWindowResize);
document.addEventListener('keydown', e => handleKeyDown(e));
document.addEventListener('keyup', e => handleKeyUp(e));
renderer.domElement.addEventListener('touchstart', handleTouchStart, {passive: false});
renderer.domElement.addEventListener('touchmove', handleTouchMove, {passive: false});
renderer.domElement.addEventListener('touchend', handleTouchEnd);
document.getElementById('start-btn').addEventListener('click', startGame);
document.getElementById('restart-btn').addEventListener('click', resetGame);
animate();
}
function createPlayer() {
if(playerGroup) scene.remove(playerGroup);
playerGroup = new THREE.Group();
const config = charConfigs[selectedChar];
const cloudMaterial = new THREE.MeshStandardMaterial({
color: config.cloud,
roughness: 0.8,
flatShading: true
});
const cloudGeometries = [
{ r: 1.0, x: 0, y: 0, z: 0 },
{ r: 0.8, x: -0.9, y: 0.1, z: 0 },
{ r: 0.8, x: 0.9, y: 0.1, z: 0 },
{ r: 0.7, x: -0.5, y: -0.2, z: 0.8 },
{ r: 0.7, x: 0.5, y: -0.2, z: 0.8 },
{ r: 0.9, x: 0, y: 0.2, z: -0.8 },
{ r: 0.6, x: -1.2, y: 0, z: -0.4 },
{ r: 0.6, x: 1.2, y: 0, z: -0.4 }
];
cloudGeometries.forEach(data => {
const geo = new THREE.SphereGeometry(data.r, 16, 16);
const mesh = new THREE.Mesh(geo, cloudMaterial);
mesh.position.set(data.x, data.y, data.z);
mesh.castShadow = true;
mesh.name = "cloud_part";
playerGroup.add(mesh);
});
const riderBodyGeo = new THREE.CylinderGeometry(0.3, 0.4, 1.2, 8);
const riderMat = new THREE.MeshStandardMaterial({ color: config.body });
const riderBody = new THREE.Mesh(riderBodyGeo, riderMat);
riderBody.position.set(0, 1.2, -0.2);
riderBody.rotation.x = 0.2;
riderBody.castShadow = true;
riderBody.name = "rider_body";
playerGroup.add(riderBody);
const riderHeadGeo = new THREE.SphereGeometry(0.35, 16, 16);
const headMat = new THREE.MeshStandardMaterial({ color: config.head });
const riderHead = new THREE.Mesh(riderHeadGeo, headMat);
riderHead.position.set(0, 2.0, -0.4);
riderHead.castShadow = true;
playerGroup.add(riderHead);
scene.add(playerGroup);
}
function updatePlayerSkin() {
const config = charConfigs[selectedChar];
playerGroup.children.forEach(child => {
if(child.name === "cloud_part") {
child.material.color.setHex(config.cloud);
} else if(child.name === "rider_body") {
child.material.color.setHex(config.body);
}
});
}
function spawnBackgroundCloud(randomZ = false) {
const size = Math.random() * 3 + 2;
const geo = new THREE.SphereGeometry(size, 8, 8);
const mat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 });
const cloud = new THREE.Mesh(geo, mat);
const x = (Math.random() - 0.5) * 100;
const y = (Math.random() - 0.5) * 40 - 10;
const z = randomZ ? (Math.random() * -150 - 20) : SPAWN_Z;
if (Math.abs(x) < 20) cloud.position.x += (x > 0 ? 20 : -20);
cloud.position.set(x, y, z);
cloud.scale.set(1, 0.6, 1.2);
scene.add(cloud);
bgClouds.push(cloud);
}
function spawnRing(x, y, z) {
const geo = new THREE.TorusGeometry(2.5, 0.3, 8, 24);
const mat = new THREE.MeshStandardMaterial({ color: 0xffff00, emissive: 0xaa8800, metalness: 0.8, roughness: 0.2 });
const ring = new THREE.Mesh(geo, mat);
ring.position.set(x, y, z);
ring.userData = { type: 'ring', passed: false, rotationSpeed: Math.random() * 0.05 + 0.02 };
scene.add(ring);
activeObjects.push(ring);
}
function spawnObstacle(x, y, z) {
const group = new THREE.Group();
group.position.set(x, y, z);
const mat = new THREE.MeshStandardMaterial({ color: 0x444455, roughness: 0.9, flatShading: true });
for(let i=0; i<5; i++) {
const r = Math.random() * 1.0 + 1.0;
const geo = new THREE.SphereGeometry(r, 8, 8);
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set((Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2, (Math.random() - 0.5) * 2);
group.add(mesh);
}
const boltGeo = new THREE.ConeGeometry(0.2, 1.5, 4);
const boltMat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
for(let i=0; i<3; i++) {
const bolt = new THREE.Mesh(boltGeo, boltMat);
bolt.position.set((Math.random()-0.5)*2, -1, (Math.random()-0.5)*2);
bolt.rotation.x = Math.PI;
group.add(bolt);
}
group.userData = { type: 'obstacle', hit: false, radius: 2.0 };
scene.add(group);
activeObjects.push(group);
}
let spawnTimer = 0;
function updateSpawning(delta) {
spawnTimer += delta * baseSpeed;
const baseInterval = 45;
const spawnInterval = Math.max(18, baseInterval - currentLevel * 7);
if (spawnTimer > spawnInterval) {
spawnTimer = 0;
const pattern = Math.floor(Math.random() * 3);
const startX = (Math.random() - 0.5) * BOUNDARY_X * 1.5;
const startY = Math.random() * (BOUNDARY_Y_MAX - BOUNDARY_Y_MIN) + BOUNDARY_Y_MIN;
if (pattern === 0) {
for(let i=0; i<3; i++) spawnRing(startX, startY, SPAWN_Z - i * 15);
spawnObstacle(startX - 6, startY, SPAWN_Z - 15);
spawnObstacle(startX + 6, startY, SPAWN_Z - 15);
} else if (pattern === 1) {
for(let i=0; i<5; i++) spawnRing(startX + Math.sin(i)*4, startY + Math.cos(i)*2, SPAWN_Z - i * 10);
} else {
spawnObstacle(startX - 5, startY, SPAWN_Z);
spawnRing(startX, startY, SPAWN_Z);
spawnObstacle(startX + 5, startY, SPAWN_Z);
}
const additionalObstacles = currentLevel - 1;
for(let i=0; i < (1 + additionalObstacles); i++) {
const ox = (Math.random() - 0.5) * BOUNDARY_X * 2;
const oy = Math.random() * (BOUNDARY_Y_MAX - BOUNDARY_Y_MIN) + BOUNDARY_Y_MIN;
const oz = SPAWN_Z - Math.random() * 50;
if (Math.abs(ox - startX) > 4 || Math.abs(oy - startY) > 4) spawnObstacle(ox, oy, oz);
}
if (Math.random() < 0.5 + (currentLevel * 0.1)) spawnBackgroundCloud(false);
}
}
function startGame() {
document.getElementById('screens').classList.add('hidden');
score = 0;
lives = 3;
baseSpeed = 0.7;
distanceTraveled = 0;
invulnerableTime = 0;
currentLevel = 1;
targetBgColor.setHex(0x87CEEB);
targetFogColor.setHex(0x87CEEB);
scene.background.setHex(0x87CEEB);
scene.fog.color.setHex(0x87CEEB);
playerGroup.position.set(0, 2, 0);
playerGroup.rotation.set(0, 0, 0);
activeObjects.forEach(obj => scene.remove(obj));
activeObjects = [];
updateHUD();
gameStarted = true;
isGameOver = false;
currentSpeed = 0;
}
function resetGame() {
document.getElementById('game-over-screen').classList.add('hidden');
document.getElementById('start-screen').classList.remove('hidden');
}
function gameOver() {
isGameOver = true;
document.getElementById('screens').classList.remove('hidden');
document.getElementById('game-over-screen').classList.remove('hidden');
document.getElementById('final-score').innerText = Math.floor(score);
}
function takeDamage() {
if (invulnerableTime > 0) return;
lives--;
updateHUD();
const flash = document.getElementById('damage-flash');
flash.style.opacity = 0.5;
setTimeout(() => { flash.style.opacity = 0; }, 100);
baseSpeed = Math.max(0.6, baseSpeed - 0.2);
if (lives <= 0) gameOver();
else invulnerableTime = 90;
}
function updateHUD() {
document.getElementById('level-val').innerText = currentLevel;
document.getElementById('score-val').innerText = Math.floor(score);
document.getElementById('speed-val').innerText = baseSpeed.toFixed(1);
let hearts = "";
for(let i=0; i<lives; i++) hearts += "♥";
document.getElementById('lives').innerText = hearts;
}
function handleKeyDown(e) { if (keys.hasOwnProperty(e.key)) keys[e.key] = true; }
function handleKeyUp(e) { if (keys.hasOwnProperty(e.key)) keys[e.key] = false; }
function handleTouchStart(e) {
e.preventDefault();
if (!gameStarted || isGameOver) return;
const touch = e.touches[0];
touchInput.active = true;
touchInput.startX = touch.clientX;
touchInput.startY = touch.clientY;
touchInput.playerStartX = playerGroup.position.x;
touchInput.playerStartY = playerGroup.position.y;
}
function handleTouchMove(e) {
e.preventDefault();
if (!touchInput.active || isGameOver) return;
const touch = e.touches[0];
const sensitivity = 0.05;
touchInput.targetX = touchInput.playerStartX + (touch.clientX - touchInput.startX) * sensitivity;
touchInput.targetY = touchInput.playerStartY - (touch.clientY - touchInput.startY) * sensitivity;
}
function handleTouchEnd() { touchInput.active = false; }
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const delta = clock.getDelta();
if (gameStarted && !isGameOver) {
scene.background.lerp(targetBgColor, 0.01);
scene.fog.color.lerp(targetFogColor, 0.01);
checkLevelUp();
currentSpeed += (baseSpeed - currentSpeed) * 0.05;
baseSpeed += (0.0001 * (1 + currentLevel * 0.2));
distanceTraveled += currentSpeed;
score += currentSpeed * 0.1;
updatePlayerMovement();
const time = Date.now() * 0.005;
playerGroup.children.forEach((child, index) => {
if (child.name === "cloud_part") {
if (!child.userData.baseY) child.userData.baseY = child.position.y;
child.position.y = child.userData.baseY + Math.sin(time + index) * 0.1;
}
});
if (invulnerableTime > 0) {
invulnerableTime--;
playerGroup.visible = (Math.floor(invulnerableTime / 5) % 2 === 0);
} else playerGroup.visible = true;
updateEnvironment(currentSpeed);
updateSpawning(currentSpeed);
if (Math.floor(distanceTraveled) % 5 === 0) updateHUD();
camera.position.x += (playerGroup.position.x * 0.5 - camera.position.x) * 0.1;
camera.position.y += ((playerGroup.position.y + 3) - camera.position.y) * 0.1;
}
renderer.render(scene, camera);
}
function checkLevelUp() {
let newLevel = 1;
if (score >= 3000) newLevel = 3;
else if (score >= 1000) newLevel = 2;
if (newLevel !== currentLevel) {
const levelDiff = newLevel - currentLevel;
currentLevel = newLevel;
baseSpeed += 0.15 * levelDiff;
updateHUD();
if (currentLevel === 2) { targetBgColor.setHex(0xFF8C00); targetFogColor.setHex(0xFF8C00); }
else if (currentLevel === 3) { targetBgColor.setHex(0x191970); targetFogColor.setHex(0x191970); }
}
}
function updatePlayerMovement() {
let targetX = playerGroup.position.x;
let targetY = playerGroup.position.y;
const moveSpeed = 0.35;
if (keys.ArrowLeft || keys.a) targetX -= moveSpeed;
if (keys.ArrowRight || keys.d) targetX += moveSpeed;
if (keys.ArrowUp || keys.w) targetY += moveSpeed;
if (keys.ArrowDown || keys.s) targetY -= moveSpeed;
if (touchInput.active && touchInput.targetX !== undefined) {
targetX = touchInput.targetX;
targetY = touchInput.targetY;
}
targetX = Math.max(-BOUNDARY_X, Math.min(BOUNDARY_X, targetX));
targetY = Math.max(BOUNDARY_Y_MIN, Math.min(BOUNDARY_Y_MAX, targetY));
playerGroup.position.x += (targetX - playerGroup.position.x) * 0.15;
playerGroup.position.y += (targetY - playerGroup.position.y) * 0.15;
playerGroup.rotation.z = Math.max(-0.4, Math.min(0.4, (playerGroup.position.x - targetX) * 0.1));
playerGroup.rotation.x = Math.max(-0.25, Math.min(0.25, (targetY - playerGroup.position.y) * 0.1));
}
function updateEnvironment(speed) {
const moveSpeed = speed * 1.5;
for (let i = activeObjects.length - 1; i >= 0; i--) {
const obj = activeObjects[i];
obj.position.z += moveSpeed;
if (obj.userData.type === 'ring') obj.rotation.y += obj.userData.rotationSpeed;
if (obj.position.z > -2 && obj.position.z < 2) {
const dx = obj.position.x - playerGroup.position.x;
const dy = obj.position.y - playerGroup.position.y;
const distanceSq = dx * dx + dy * dy;
if (obj.userData.type === 'ring' && !obj.userData.passed) {
if (distanceSq < 3.0 * 3.0) {
obj.userData.passed = true;
score += 100;
obj.material.color.setHex(0xffffff);
obj.material.emissive.setHex(0xaaaaaa);
baseSpeed += 0.02;
updateHUD();
}
} else if (obj.userData.type === 'obstacle' && !obj.userData.hit) {
if (distanceSq < 3.0 * 3.0) {
obj.userData.hit = true;
takeDamage();
}
}
}
if (obj.position.z > DESPAWN_Z) { scene.remove(obj); activeObjects.splice(i, 1); }
}
for (let i = bgClouds.length - 1; i >= 0; i--) {
const cloud = bgClouds[i];
cloud.position.z += moveSpeed * 0.5;
if (cloud.position.z > DESPAWN_Z) { scene.remove(cloud); bgClouds.splice(i, 1); spawnBackgroundCloud(false); }
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
window.onload = init;
</script>
</body>
</html>
金のわっかをくぐると得点GET!高得点を目指そう!