<!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>City Smasher 3D - Easy Mode</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
body { margin: 0; overflow: hidden; background-color: #1a1a1a; touch-action: none; }
#game-canvas { display: block; width: 100vw; height: 100vh; }
.ui-panel { pointer-events: none; }
.interactive { pointer-events: auto; }
</style>
</head>
<body>
<canvas id="game-canvas"></canvas>
<!-- UI Layer -->
<div class="absolute inset-0 ui-panel flex flex-col justify-between p-6">
<div class="flex justify-between items-start">
<div class="bg-black/60 text-white p-4 rounded-xl border-2 border-orange-500 shadow-lg">
<div class="text-xs uppercase tracking-widest text-orange-400">破壊スコア</div>
<div id="score" class="text-3xl font-black">0</div>
</div>
<div class="bg-black/60 text-white p-4 rounded-xl border-2 border-blue-500 shadow-lg">
<div class="text-xs uppercase tracking-widest text-blue-400">残り時間</div>
<div id="timer" class="text-3xl font-black text-center">60</div>
</div>
</div>
<!-- Start Screen -->
<div id="start-screen" class="absolute inset-0 flex items-center justify-center bg-black/80 backdrop-blur-md interactive z-50">
<div class="text-center p-8 bg-gray-900 border-4 border-orange-600 rounded-3xl shadow-2xl max-w-sm">
<h1 class="text-5xl font-black text-orange-500 mb-2 italic">CITY SMASH</h1>
<p class="text-gray-400 mb-8 uppercase tracking-tighter">かんたん操作モード</p>
<div class="space-y-4 mb-8 text-left text-sm text-gray-300 bg-black/40 p-4 rounded-lg">
<p>🕹️ <b>操作:</b> マウス・指を左右に動かすだけ!</p>
<p>🚀 <b>自動移動:</b> キャラクターは自動で進みます</p>
</div>
<button id="start-btn" class="w-full bg-orange-600 hover:bg-orange-500 text-white font-bold py-4 rounded-xl text-xl transition-all transform hover:scale-105 active:scale-95 shadow-lg">
破壊を開始する
</button>
</div>
</div>
<!-- Game Over Screen -->
<div id="game-over-screen" class="absolute inset-0 flex items-center justify-center bg-red-900/90 backdrop-blur-lg hidden interactive z-50">
<div class="text-center text-white">
<h2 class="text-6xl font-black mb-2">TIME UP!</h2>
<p class="text-2xl mb-8">最終破壊スコア: <span id="final-score" class="text-yellow-400">0</span></p>
<button id="restart-btn" class="bg-white text-red-600 font-bold py-4 px-12 rounded-full text-xl hover:bg-gray-200 transition-all">
もう一度暴れる
</button>
</div>
</div>
</div>
<script>
let scene, camera, renderer, monster, buildings = [], debris = [];
let score = 0, timeLeft = 60, isPlaying = false;
let targetX = 0;
let monsterAutoSpeed = 0.2; // 自動前進速度
let clock = new THREE.Clock();
const config = {
citySize: 150,
buildingCount: 100,
monsterLerp: 0.1 // 左右移動のスムーズさ
};
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a0a0f);
scene.fog = new THREE.Fog(0x0a0a0f, 30, 100);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// カメラを少し高く、遠くに配置して見やすく
camera.position.set(0, 20, 30);
renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('game-canvas'), antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
// Lights
const ambient = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xffaa00, 1.2);
sun.position.set(50, 100, 50);
sun.castShadow = true;
sun.shadow.mapSize.width = 1024;
sun.shadow.mapSize.height = 1024;
scene.add(sun);
// Ground
const groundGeo = new THREE.PlaneGeometry(1000, 1000);
const groundMat = new THREE.MeshPhongMaterial({ color: 0x111111 });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// Grid
const grid = new THREE.GridHelper(1000, 100, 0x4444ff, 0x222222);
scene.add(grid);
createMonster();
createCity();
window.addEventListener('mousemove', onInputMove);
window.addEventListener('touchmove', onInputMove);
window.addEventListener('resize', onWindowResize);
document.getElementById('start-btn').onclick = startGame;
document.getElementById('restart-btn').onclick = startGame;
animate();
}
function createMonster() {
const group = new THREE.Group();
// 本体
const geo = new THREE.SphereGeometry(2.5, 32, 32);
const mat = new THREE.MeshPhongMaterial({
color: 0xff4400,
emissive: 0xff2200,
emissiveIntensity: 0.6
});
const body = new THREE.Mesh(geo, mat);
body.castShadow = true;
group.add(body);
// 目の光
const eyeGeo = new THREE.SphereGeometry(0.5, 16, 16);
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const eyeL = new THREE.Mesh(eyeGeo, eyeMat);
eyeL.position.set(1, 1, -1.5);
group.add(eyeL);
const eyeR = eyeL.clone();
eyeR.position.x = -1;
group.add(eyeR);
monster = group;
monster.position.y = 2.5;
scene.add(monster);
}
function createCity() {
const colors = [0x555555, 0x777777, 0x444466, 0x333333];
for (let i = 0; i < config.buildingCount; i++) {
const w = 4 + Math.random() * 4;
const h = 10 + Math.random() * 20;
const d = 4 + Math.random() * 4;
const group = new THREE.Group();
// プレイヤーの進行方向(Zマイナス)に沿って配置
group.position.x = (Math.random() - 0.5) * 80;
group.position.z = -Math.random() * 500 - 20;
const floors = Math.max(2, Math.floor(h / 3));
const floorHeight = h / floors;
const buildingColor = colors[Math.floor(Math.random() * colors.length)];
for(let j = 0; j < floors; j++) {
const floorGeo = new THREE.BoxGeometry(w, floorHeight - 0.2, d);
const floorMat = new THREE.MeshPhongMaterial({ color: buildingColor });
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.position.y = (j * floorHeight) + floorHeight/2;
floor.castShadow = true;
floor.receiveShadow = true;
// 窓
if (Math.random() > 0.3) {
const winGeo = new THREE.BoxGeometry(w + 0.1, 0.4, d + 0.1);
const winMat = new THREE.MeshBasicMaterial({ color: 0xffffaa });
const win = new THREE.Mesh(winGeo, winMat);
floor.add(win);
}
group.add(floor);
}
scene.add(group);
buildings.push({
mesh: group,
active: true,
radius: (w + d) / 4 + 1.5, // 判定用の半径
floors: group.children.length
});
}
}
function startGame() {
score = 0;
timeLeft = 60;
isPlaying = true;
monsterAutoSpeed = 0.2;
document.getElementById('score').innerText = score;
document.getElementById('start-screen').classList.add('hidden');
document.getElementById('game-over-screen').classList.add('hidden');
buildings.forEach(b => scene.remove(b.mesh));
debris.forEach(d => scene.remove(d.mesh));
buildings = [];
debris = [];
createCity();
monster.position.set(0, 2.5, 0);
targetX = 0;
const timerInt = setInterval(() => {
if (!isPlaying) {
clearInterval(timerInt);
return;
}
timeLeft--;
document.getElementById('timer').innerText = timeLeft;
// 徐々にスピードアップして爽快感を出す
monsterAutoSpeed += 0.005;
if (timeLeft <= 0) {
endGame();
clearInterval(timerInt);
}
}, 1000);
}
function endGame() {
isPlaying = false;
document.getElementById('game-over-screen').classList.remove('hidden');
document.getElementById('final-score').innerText = score;
}
function onInputMove(e) {
if (!isPlaying) return;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const x = (clientX / window.innerWidth) * 2 - 1;
targetX = x * 45; // 左右移動範囲
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function updatePhysics() {
if (!isPlaying) return;
// 自動前進 + 左右移動
monster.position.z -= monsterAutoSpeed;
monster.position.x = THREE.MathUtils.lerp(monster.position.x, targetX, config.monsterLerp);
// 怪獣を進行方向に少し傾ける
monster.rotation.z = (monster.position.x - targetX) * 0.05;
monster.rotation.x = -0.1;
// カメラの追従
camera.position.x = monster.position.x * 0.5;
camera.position.z = monster.position.z + 35;
camera.lookAt(monster.position.x, 2, monster.position.z - 10);
// 衝突判定
for (let i = buildings.length - 1; i >= 0; i--) {
const b = buildings[i];
const dx = monster.position.x - b.mesh.position.x;
const dz = monster.position.z - b.mesh.position.z;
const distSq = dx*dx + dz*dz;
// 半径に基づいた円形判定(処理が速い)
if (distSq < b.radius * b.radius + 10) {
explodeBuilding(b);
buildings.splice(i, 1);
score += b.floors * 10;
document.getElementById('score').innerText = score;
}
// 通り過ぎたビルを再配置(無限ループ用)
if (b.mesh.position.z > monster.position.z + 20) {
b.mesh.position.z -= 500;
b.mesh.position.x = (Math.random() - 0.5) * 80;
}
}
// デブリ
for (let i = debris.length - 1; i >= 0; i--) {
const d = debris[i];
d.mesh.position.add(d.velocity);
d.velocity.y -= 0.02;
d.mesh.rotation.x += d.rot.x;
if (d.mesh.position.y < 0.5) {
d.mesh.position.y = 0.5;
d.velocity.set(0,0,0);
d.life--;
if(d.life < 0) {
scene.remove(d.mesh);
debris.splice(i, 1);
}
}
}
}
function explodeBuilding(building) {
const children = [...building.mesh.children];
children.forEach((child, idx) => {
const worldPos = new THREE.Vector3();
child.getWorldPosition(worldPos);
scene.add(child);
child.position.copy(worldPos);
debris.push({
mesh: child,
velocity: new THREE.Vector3(
(Math.random() - 0.5) * 0.8,
Math.random() * 0.6 + 0.2,
-Math.random() * 0.5
),
rot: new THREE.Vector3(Math.random()*0.2, Math.random()*0.2, 0),
life: 100
});
});
scene.remove(building.mesh);
}
function animate() {
requestAnimationFrame(animate);
updatePhysics();
renderer.render(scene, camera);
}
init();
</script>
</body>
</html>