<!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>ライダー - Kaiju Battle Edition</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: #000; touch-action: none; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
#game-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
#ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; flex-direction: column; justify-content: center; align-items: center; }
.pointer-events-auto { pointer-events: auto; }
.neon-text {
color: #fff;
text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 20px #0ff, 0 0 40px #0ff, 0 0 80px #0ff;
}
.neon-text-pink {
color: #fff;
text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 20px #f0f, 0 0 40px #f0f, 0 0 80px #f0f;
}
/* AI通信ウィンドウ */
#ai-comms {
position: absolute;
top: 20px;
right: 20px;
width: 300px;
background: rgba(0, 20, 40, 0.85);
border: 2px solid #00ffff;
border-radius: 10px;
padding: 15px;
color: #00ffff;
font-size: 14px;
transform: translateX(350px);
transition: transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
pointer-events: auto;
box-shadow: 0 0 15px rgba(0, 255, 255, 0.3);
z-index: 40;
}
#ai-comms.active { transform: translateX(0); }
.glitch-border { animation: border-flicker 2s infinite; }
@keyframes border-flicker {
0%, 100% { border-color: #00ffff; }
50% { border-color: #008888; }
}
/* アフターアクション・レポート */
#report-modal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 500px;
background: rgba(5, 5, 20, 0.95);
border: 2px solid #6366f1;
padding: 20px;
border-radius: 15px;
color: white;
display: none;
z-index: 100;
pointer-events: auto;
}
/* HUD効果 */
#hud-overlay {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
border: 40px solid transparent;
border-image: radial-gradient(circle, transparent 60%, rgba(0, 255, 255, 0.1) 100%) 1;
pointer-events: none;
display: none;
}
.hud-crosshair {
position: absolute;
top: 50%; left: 50%;
width: 40px; height: 40px;
transform: translate(-50%, -50%);
}
.hud-crosshair::before, .hud-crosshair::after {
content: ''; position: absolute; background: rgba(0, 255, 255, 0.6);
}
.hud-crosshair::before { top: 19px; left: 0; width: 40px; height: 2px; }
.hud-crosshair::after { top: 0; left: 19px; width: 2px; height: 40px; }
.hud-circle {
position: absolute;
top: 50%; left: 50%;
width: 300px; height: 300px;
transform: translate(-50%, -50%);
border: 2px dashed rgba(0, 255, 255, 0.3);
border-radius: 50%;
}
</style>
</head>
<body>
<div id="game-container"></div>
<div id="hud-overlay" class="playing-only">
<div class="hud-circle"></div>
<div class="hud-crosshair"></div>
<!-- 左右の計器風テキスト -->
<div class="absolute top-1/4 left-10 text-cyan-400 font-mono text-sm opacity-80 drop-shadow-[0_0_5px_rgba(0,255,255,0.8)]">
ALT: <span id="hud-alt">0000</span><br>
SPD: <span id="hud-spd">0000</span>
</div>
<div class="absolute top-1/4 right-10 text-cyan-400 font-mono text-sm opacity-80 drop-shadow-[0_0_5px_rgba(0,255,255,0.8)] text-right">
TGT: AUTO<br>
WPN: LASER
</div>
</div>
<div id="ui-layer">
<div id="score-display" class="absolute top-4 left-4 text-2xl font-bold text-cyan-400 hidden">
スコア: <span id="score-value">0</span>
<div id="sector-name" class="text-xs text-indigo-300 italic">分析中...</div>
</div>
<button id="ai-mission-btn" class="absolute top-24 left-4 px-4 py-2 bg-indigo-900 border border-indigo-400 text-indigo-100 rounded-lg pointer-events-auto hidden hover:bg-indigo-800 transition-colors shadow-lg">
✨ 通信開始
</button>
<!-- 視点切り替えボタン -->
<button id="view-toggle-btn" class="absolute top-4 right-4 px-4 py-2 bg-emerald-600 border border-emerald-400 text-white font-bold rounded-lg pointer-events-auto hidden hover:bg-emerald-500 transition-colors shadow-[0_0_15px_rgba(16,185,129,0.5)] z-50">
🎥 コックピットへ
</button>
<div id="ai-comms" class="glitch-border">
<div class="flex items-center mb-2">
<div class="w-3 h-3 bg-red-500 rounded-full animate-pulse mr-2"></div>
<span class="font-bold tracking-widest uppercase text-xs">Incoming Transmission</span>
</div>
<div id="ai-message" class="leading-relaxed min-h-[60px]">指示を待機中...</div>
<div id="ai-loading" class="hidden mt-2 text-xs italic opacity-70">Gemini司令官と接続中...</div>
</div>
<div id="start-screen" class="flex flex-col items-center pointer-events-auto bg-black bg-opacity-70 p-8 rounded-2xl border border-cyan-500 shadow-[0_0_20px_rgba(0,255,255,0.5)] transition-opacity duration-300">
<h1 class="text-5xl font-extrabold mb-6 neon-text tracking-wider">ライダー</h1>
<p class="text-white text-lg mb-8 text-center leading-relaxed">
マウス/スワイプ または 十字キー(WASD)で上下左右に移動<br>
スペースで射撃 | Bキーで爆弾<br>
<span class="text-indigo-300 font-bold">✨ 味方機と協力し、超大型怪獣を撃破せよ!</span>
</p>
<button id="start-btn" class="px-10 py-4 bg-cyan-600 hover:bg-cyan-500 text-white font-bold rounded-full text-2xl transition-all transform hover:scale-105 shadow-[0_0_20px_rgba(0,255,255,0.8)]">
出撃 (LAUNCH)
</button>
</div>
<div id="game-over-screen" class="flex flex-col items-center pointer-events-auto bg-black bg-opacity-80 p-8 rounded-2xl border border-pink-500 shadow-[0_0_20px_rgba(255,0,255,0.5)] hidden transition-opacity duration-300">
<h1 class="text-5xl font-extrabold mb-4 neon-text-pink tracking-wider">CRASHED!</h1>
<p id="final-score-text" class="text-white text-2xl mb-2">最終スコア</p>
<p id="final-score" class="text-6xl font-bold text-cyan-400 mb-4 neon-text">0</p>
<button id="ai-report-btn" class="mb-4 px-6 py-2 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-lg transition-all shadow-[0_0_15px_rgba(99,102,241,0.5)]">
✨ ミッションレポート生成
</button>
<button id="restart-btn" class="px-8 py-3 bg-pink-600 hover:bg-pink-500 text-white font-bold rounded-full text-xl transition-all transform hover:scale-105">
帰還 (リトライ)
</button>
</div>
<button id="bomb-btn" class="absolute bottom-8 right-8 w-20 h-20 bg-red-600 rounded-full border-4 border-red-800 text-white font-bold text-xl shadow-[0_0_20px_rgba(255,0,0,0.8)] flex flex-col items-center justify-center transition-all duration-300 hidden select-none touch-manipulation pointer-events-auto z-50">
<span class="text-3xl">💣</span>
<span id="bomb-cooldown-text" class="text-xs">READY</span>
</button>
<!-- カウントダウン表示 -->
<div id="countdown-display" class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-[10rem] font-extrabold text-red-500 hidden z-50 animate-pulse" style="text-shadow: 0 0 30px #ff0000, 0 0 60px #ff0000;">
10
</div>
<!-- AAR モーダル -->
<div id="report-modal">
<h2 class="text-xl font-bold mb-3 border-b border-indigo-500 pb-2 text-indigo-300">アフターアクション・レポート</h2>
<div id="report-img-container" class="w-full aspect-video bg-gray-800 rounded mb-4 flex items-center justify-center overflow-hidden">
<div id="report-img-loading" class="text-sm italic opacity-50">✨ 画像生成中...</div>
</div>
<div id="report-text" class="text-sm leading-relaxed mb-6 italic">分析データをロード中...</div>
<button id="close-report-btn" class="w-full py-2 bg-indigo-600 rounded font-bold hover:bg-indigo-500">了解</button>
</div>
</div>
<script>
const apiKey = "";
async function fetchWithRetry(url, options, retries = 5, backoff = 1000) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error('API request failed');
return await response.json();
} catch (err) {
if (retries > 0) {
await new Promise(res => setTimeout(res, backoff));
return fetchWithRetry(url, options, retries - 1, backoff * 2);
}
throw err;
}
}
let scene, camera, renderer;
let player, wings, ground, roadLines = [], buildings = [], enemies = [], bullets = [], enemyBullets = [], bombs = [], bigExplosions = [];
let allies = [], allyBullets = []; // 仲間機と仲間の弾
let kaijus = [], kaijuBullets = []; // 怪獣と怪獣の弾
let hq; // 格納庫(本部)
let gameState = 'start'; // 'start', 'countdown', 'launching', 'playing', 'gameover'
let score = 0, currentSpeed = 1.0, lastShootTime = 0, lastBombTime = 0;
const baseSpeed = 1.0, shootInterval = 150, bombCooldown = 3000, playerMaxX = 12;
let keys = {}, targetX = 0, targetY = 100;
let currentSectorTheme = { name: "セクター 0", color: 0x87CEEB };
let isCockpitView = false; // 視点の状態管理
let interiorScreens = []; // 内装の光るモニター管理
let audioCtx; // Web Audio API
const uiElements = {
start: document.getElementById('start-screen'),
gameOver: document.getElementById('game-over-screen'),
score: document.getElementById('score-display'),
scoreVal: document.getElementById('score-value'),
finalScore: document.getElementById('final-score'),
bombBtn: document.getElementById('bomb-btn'),
bombCD: document.getElementById('bomb-cooldown-text'),
aiBtn: document.getElementById('ai-mission-btn'),
viewBtn: document.getElementById('view-toggle-btn'), // 視点切り替えボタン
aiComms: document.getElementById('ai-comms'),
aiMsg: document.getElementById('ai-message'),
aiLoad: document.getElementById('ai-loading'),
sectorName: document.getElementById('sector-name'),
reportBtn: document.getElementById('ai-report-btn'),
reportModal: document.getElementById('report-modal'),
reportText: document.getElementById('report-text'),
reportImgContainer: document.getElementById('report-img-container'),
reportImgLoading: document.getElementById('report-img-loading'),
hud: document.getElementById('hud-overlay')
};
window.onload = function() {
initThreeJS();
setupEvents();
animate();
};
function initThreeJS() {
scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x87CEEB, 20, 300);
scene.background = new THREE.Color(0x87CEEB);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 800);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('game-container').appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 0.7));
const sun = new THREE.DirectionalLight(0xffffff, 0.9);
sun.position.set(10, 50, 10);
scene.add(sun);
// 地面(プレイ中の眼下の街用)
const groundGeo = new THREE.PlaneGeometry(300, 1000);
const groundMat = new THREE.MeshStandardMaterial({ color: 0x2a2a2a });
ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -1;
scene.add(ground);
for(let i=0; i<30; i++) {
const line = new THREE.Mesh(new THREE.PlaneGeometry(0.5, 4), new THREE.MeshBasicMaterial({ color: 0xdddddd }));
line.rotation.x = -Math.PI / 2;
line.position.set(0, -0.98, -i * 15);
scene.add(line);
roadLines.push(line);
}
createPlayer();
createHeadquarters(); // 本部(格納庫)の作成
}
// --- 音響処理 (Web Audio API) ---
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playShootSound(isEnemy = false, isHeavy = false) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
osc.type = isHeavy ? 'square' : 'sawtooth';
if (isHeavy) {
osc.frequency.setValueAtTime(200, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(50, audioCtx.currentTime + 0.3);
gainNode.gain.setValueAtTime(0.2, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3);
osc.start(); osc.stop(audioCtx.currentTime + 0.3);
} else {
osc.frequency.setValueAtTime(isEnemy ? 440 : 880, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(isEnemy ? 110 : 220, audioCtx.currentTime + 0.1);
gainNode.gain.setValueAtTime(0.05, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.1);
osc.start(); osc.stop(audioCtx.currentTime + 0.1);
}
osc.connect(gainNode); gainNode.connect(audioCtx.destination);
}
function playExplosionSound() {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(150, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(20, audioCtx.currentTime + 0.8);
gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.8);
osc.connect(gainNode); gainNode.connect(audioCtx.destination);
osc.start(); osc.stop(audioCtx.currentTime + 0.8);
}
function playKaijuRoar() {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(100, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(40, audioCtx.currentTime + 2.0);
gainNode.gain.setValueAtTime(0.5, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 2.0);
osc.connect(gainNode); gainNode.connect(audioCtx.destination);
osc.start(); osc.stop(audioCtx.currentTime + 2.0);
}
// --- 本部(格納庫)の作成 ---
function createHeadquarters() {
hq = new THREE.Group();
const floorMat = new THREE.MeshStandardMaterial({ color: 0x111115, roughness: 0.8, metalness: 0.6 });
const floor = new THREE.Mesh(new THREE.PlaneGeometry(60, 800), floorMat);
floor.rotation.x = -Math.PI / 2; floor.position.y = -0.5; hq.add(floor);
const lineMat = new THREE.MeshBasicMaterial({ color: 0x00ffff });
for(let i=-5; i<25; i++) {
const lineL = new THREE.Mesh(new THREE.PlaneGeometry(0.5, 15), lineMat);
lineL.rotation.x = -Math.PI / 2; lineL.position.set(-6, -0.4, -i * 30); hq.add(lineL);
const lineR = new THREE.Mesh(new THREE.PlaneGeometry(0.5, 15), lineMat);
lineR.rotation.x = -Math.PI / 2; lineR.position.set(6, -0.4, -i * 30); hq.add(lineR);
}
const archMat = new THREE.MeshStandardMaterial({ color: 0x222233, metalness: 0.8, roughness: 0.5 });
const neonMat = new THREE.MeshBasicMaterial({ color: 0x00ffff });
for(let i=-5; i<25; i++) {
const archGroup = new THREE.Group();
const leftWall = new THREE.Mesh(new THREE.BoxGeometry(2, 20, 5), archMat); leftWall.position.set(-15, 9.5, 0); archGroup.add(leftWall);
const rightWall = new THREE.Mesh(new THREE.BoxGeometry(2, 20, 5), archMat); rightWall.position.set(15, 9.5, 0); archGroup.add(rightWall);
const ceiling = new THREE.Mesh(new THREE.BoxGeometry(32, 2, 5), archMat); ceiling.position.set(0, 20, 0); archGroup.add(ceiling);
const neon = new THREE.Mesh(new THREE.BoxGeometry(30, 0.5, 0.5), neonMat); neon.position.set(0, 19, 2.5); archGroup.add(neon);
archGroup.position.z = -i * 30; hq.add(archGroup);
}
scene.add(hq);
}
function createPlayer() {
player = new THREE.Group();
const metalMat = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x004444, metalness: 0.8, roughness: 0.2 });
const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x003366, transparent: true, opacity: 0.4, side: THREE.DoubleSide });
const fuselage = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.6, 3.5), metalMat); player.add(fuselage);
const nose = new THREE.Mesh(new THREE.ConeGeometry(0.4, 1.2, 4), metalMat); nose.rotation.x = Math.PI / 2; nose.position.z = -2.2; player.add(nose);
const cockpit = new THREE.Mesh(new THREE.SphereGeometry(0.35, 16, 16), cockpitMat); cockpit.scale.set(1.2, 0.8, 1.8); cockpit.position.set(0, 0.4, -0.2); player.add(cockpit);
const tail = new THREE.Mesh(new THREE.BoxGeometry(0.05, 1.0, 0.8), metalMat); tail.position.set(0, 0.6, 1.4); player.add(tail);
const leftStab = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.05, 0.6), metalMat); leftStab.position.set(-0.8, 0, 1.2); leftStab.rotation.y = -Math.PI / 8; player.add(leftStab);
const rightStab = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.05, 0.6), metalMat); rightStab.position.set(0.8, 0, 1.2); rightStab.rotation.y = Math.PI / 8; player.add(rightStab);
wings = new THREE.Group();
const wingShape = new THREE.BoxGeometry(4, 0.05, 1.5);
const lw = new THREE.Mesh(wingShape, metalMat); lw.position.set(-2, 0, -0.5); lw.rotation.y = -Math.PI / 5;
const rw = new THREE.Mesh(wingShape, metalMat); rw.position.set(2, 0, -0.5); rw.rotation.y = Math.PI / 5;
wings.add(lw); wings.add(rw); wings.scale.set(0,0,0); player.add(wings);
const glow = new THREE.Mesh(new THREE.SphereGeometry(0.4, 8, 8), new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.9 }));
glow.position.set(0, 0, 1.7); glow.scale.z = 2.5; player.add(glow);
// コックピット内装
const interior = new THREE.Group(); interior.position.set(0, 0.4, 0); interiorScreens = [];
const dashMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9 });
const dash = new THREE.Mesh(new THREE.BoxGeometry(0.7, 0.3, 0.5), dashMat); dash.position.set(0, -0.2, -0.3); interior.add(dash);
const screenMat1 = new THREE.MeshBasicMaterial({ color: 0x00ffcc }); const screen1 = new THREE.Mesh(new THREE.PlaneGeometry(0.25, 0.15), screenMat1); screen1.position.set(0, -0.04, -0.54); screen1.rotation.x = -Math.PI / 6; interior.add(screen1); interiorScreens.push(screenMat1);
const screenMat2 = new THREE.MeshBasicMaterial({ color: 0xff3366 }); const screen2 = new THREE.Mesh(new THREE.PlaneGeometry(0.12, 0.1), screenMat2); screen2.position.set(-0.2, -0.06, -0.5); screen2.rotation.x = -Math.PI / 6; screen2.rotation.y = Math.PI / 8; interior.add(screen2); interiorScreens.push(screenMat2);
const screenMat3 = new THREE.MeshBasicMaterial({ color: 0x3366ff }); const screen3 = new THREE.Mesh(new THREE.PlaneGeometry(0.12, 0.1), screenMat3); screen3.position.set(0.2, -0.06, -0.5); screen3.rotation.x = -Math.PI / 6; screen3.rotation.y = -Math.PI / 8; interior.add(screen3); interiorScreens.push(screenMat3);
const frameMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.6, roughness: 0.4 });
const leftFrame = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.7, 0.02), frameMat); leftFrame.position.set(-0.35, 0.1, -0.2); leftFrame.rotation.x = -Math.PI / 6; leftFrame.rotation.z = Math.PI / 6; interior.add(leftFrame);
const rightFrame = new THREE.Mesh(new THREE.BoxGeometry(0.02, 0.7, 0.02), frameMat); rightFrame.position.set(0.35, 0.1, -0.2); rightFrame.rotation.x = -Math.PI / 6; rightFrame.rotation.z = -Math.PI / 6; interior.add(rightFrame);
player.add(interior);
scene.add(player);
}
// --- 仲間機 (Ally Jet) ---
function createAllyJet(offsetX, offsetY, offsetZ) {
const ally = new THREE.Group();
const metalMat = new THREE.MeshStandardMaterial({ color: 0x2288ff, emissive: 0x002266, metalness: 0.8, roughness: 0.2 });
const fuselage = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.4, 2.5), metalMat); ally.add(fuselage);
const nose = new THREE.Mesh(new THREE.ConeGeometry(0.3, 1.0, 4), metalMat); nose.rotation.x = Math.PI/2; nose.position.z = -1.7; ally.add(nose);
const wingShape = new THREE.BoxGeometry(3, 0.05, 1.0);
const allyWings = new THREE.Mesh(wingShape, metalMat); ally.add(allyWings);
const glow = new THREE.Mesh(new THREE.SphereGeometry(0.3, 8, 8), new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.9 }));
glow.position.set(0, 0, 1.3); glow.scale.z = 2.0; ally.add(glow);
ally.position.set(offsetX, 100 + offsetY, offsetZ);
ally.userData = {
baseOffsetX: offsetX,
baseOffsetY: offsetY,
baseOffsetZ: offsetZ,
phaseX: Math.random() * Math.PI * 2,
phaseY: Math.random() * Math.PI * 2,
speedX: Math.random() * 0.02 + 0.01,
speedY: Math.random() * 0.02 + 0.01,
followSpeed: Math.random() * 0.03 + 0.02,
lastShoot: 0
};
scene.add(ally);
allies.push(ally);
}
// --- 怪獣 (Kaiju) ---
function createKaiju() {
const kaiju = new THREE.Group();
const skinMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.9, metalness: 0.1 }); // ゴジラ風の黒っぽい皮膚
const spineMat = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x0088ff }); // シアン色の発光背びれ
// 胴体 (直立姿勢)
const body = new THREE.Mesh(new THREE.BoxGeometry(16, 50, 24), skinMat);
body.position.set(0, 35, 0);
body.rotation.x = Math.PI / 12; // 少し前傾姿勢
kaiju.add(body);
// 頭
const head = new THREE.Mesh(new THREE.BoxGeometry(10, 12, 16), skinMat);
head.position.set(0, 65, -12);
kaiju.add(head);
// 光る目
const eyeMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const eyeL = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), eyeMat); eyeL.position.set(-3.5, 66, -19.5); kaiju.add(eyeL);
const eyeR = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), eyeMat); eyeR.position.set(3.5, 66, -19.5); kaiju.add(eyeR);
// 足(太くがっしりと)
const legL = new THREE.Mesh(new THREE.BoxGeometry(10, 25, 12), skinMat); legL.position.set(-12, 12.5, 5); kaiju.add(legL);
const legR = new THREE.Mesh(new THREE.BoxGeometry(10, 25, 12), skinMat); legR.position.set(12, 12.5, 5); kaiju.add(legR);
// 腕(小さめ)
const armL = new THREE.Mesh(new THREE.BoxGeometry(4, 16, 6), skinMat); armL.position.set(-10, 40, -8); armL.rotation.x = -Math.PI / 4; kaiju.add(armL);
const armR = new THREE.Mesh(new THREE.BoxGeometry(4, 16, 6), skinMat); armR.position.set(10, 40, -8); armR.rotation.x = -Math.PI / 4; kaiju.add(armR);
// 尻尾(長く太く)
const tail = new THREE.Mesh(new THREE.ConeGeometry(8, 60, 4), skinMat);
tail.rotation.x = Math.PI / 2.2; tail.position.set(0, 10, 35); kaiju.add(tail);
// 背びれ(巨大なクリスタル)
for(let i=0; i<6; i++){
const spine = new THREE.Mesh(new THREE.ConeGeometry(3 + Math.sin(i)*2, 12 + Math.sin(i)*6, 4), spineMat);
spine.position.set(0, 55 - i*7, -2 + i*7);
spine.rotation.x = Math.PI / 6;
kaiju.add(spine);
}
kaiju.position.set(0, 0, -800); // 地上(y=0)の遠方に配置
kaiju.userData = { hp: 150, maxHp: 150, lastShoot: 0, phase: 0 }; // HPを大幅アップ
scene.add(kaiju);
kaijus.push(kaiju);
}
function toggleView() {
isCockpitView = !isCockpitView;
if (isCockpitView) {
uiElements.viewBtn.innerText = '🎥 後方視点へ';
player.add(camera);
camera.position.set(0, 0.45, 0.05);
camera.rotation.set(0, 0, 0);
camera.fov = 85;
uiElements.hud.style.display = 'block';
} else {
uiElements.viewBtn.innerText = '🎥 コックピットへ';
scene.add(camera);
camera.position.set(0, player.position.y + 4, player.position.z + 8);
camera.rotation.set(0, 0, 0);
camera.fov = 75;
uiElements.hud.style.display = 'none';
}
camera.updateProjectionMatrix();
}
async function generateSectorTheme() {
try {
const prompt = "SFフライトゲームの作戦エリアの名前を1つ考えてください(例:ネオン・トウキョウ、アークティック・ベース)。また、そのエリアの空と霧の色を16進数コード(例:#ff0000)で指定し、JSON形式で返してください。properties: { name: string, color: string }";
const response = await fetchWithRetry(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { responseMimeType: "application/json", responseSchema: { type: "OBJECT", properties: { name: { type: "STRING" }, color: { type: "STRING" } } } }
})
});
const data = JSON.parse(response.candidates[0].content.parts[0].text);
const colorVal = parseInt(data.color.replace('#', '0x'), 16);
currentSectorTheme = { name: data.name, color: colorVal };
uiElements.sectorName.innerText = `作戦エリア: ${currentSectorTheme.name}`;
scene.background = new THREE.Color(colorVal);
scene.fog.color = new THREE.Color(colorVal);
} catch (e) {
uiElements.sectorName.innerText = "作戦エリア: 未確認セクター";
}
}
async function generatePostFlightReport() {
uiElements.reportModal.style.display = 'block';
uiElements.reportText.innerText = "データを分析中...";
uiElements.reportImgLoading.style.display = 'block';
const oldImg = uiElements.reportImgContainer.querySelector('img');
if (oldImg) oldImg.remove();
try {
const textPrompt = `SFアクションゲーム『ライダー』で味方機と協力し怪獣と戦いました。スコアは${Math.floor(score)}点、セクターは「${currentSectorTheme.name}」です。AI司令官として、この飛行を総括する短いレポート(80文字以内)を生成してください。`;
const textResult = await fetchWithRetry(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contents: [{ parts: [{ text: textPrompt }] }] })
});
uiElements.reportText.innerText = textResult.candidates?.[0]?.content?.parts?.[0]?.text || "通信エラー。勇敢な飛行であった。";
const imgPrompt = `A heroic fighter jet flying alongside ally jets, fighting a giant Godzilla-like Kaiju walking on the ground in a sci-fi city with the theme of ${currentSectorTheme.name}. Cinematic lighting, 8k quality.`;
const imgResult = await fetchWithRetry(`https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ instances: { prompt: imgPrompt }, parameters: { sampleCount: 1 } })
});
const imageUrl = `data:image/png;base64,${imgResult.predictions[0].bytesBase64Encoded}`;
const img = document.createElement('img'); img.src = imageUrl; img.className = "w-full h-full object-cover";
uiElements.reportImgLoading.style.display = 'none';
uiElements.reportImgContainer.appendChild(img);
} catch (e) {
uiElements.reportText.innerText = "レポート生成に失敗しました。";
uiElements.reportImgLoading.innerText = "画像生成エラー";
}
}
async function triggerAiMission() {
if (uiElements.aiLoad.classList.contains('active')) return;
uiElements.aiLoad.classList.remove('hidden'); uiElements.aiComms.classList.add('active'); uiElements.aiMsg.innerText = "...";
try {
const prompt = `あなたはSFアクションゲーム『ライダー』の司令官です。スコアは${Math.floor(score)}点。エリアは「${currentSectorTheme.name}」です。戦闘機パイロットへ短い指示を。`;
const textResult = await fetchWithRetry(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] })
});
const missionText = textResult.candidates?.[0]?.content?.parts?.[0]?.text || "前方注意。";
uiElements.aiMsg.innerText = missionText; uiElements.aiLoad.classList.add('hidden');
const ttsResult = await fetchWithRetry(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: `Say commandingly: ${missionText}` }] }],
generationConfig: { responseModalities: ["AUDIO"], speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: "Puck" } } } }
})
});
const audioData = ttsResult.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
if (audioData) {
const bytes = new Uint8Array(atob(audioData).split("").map(c => c.charCodeAt(0)));
const blob = new Blob([pcmToWav(bytes, 24000)], { type: 'audio/wav' });
new Audio(URL.createObjectURL(blob)).play();
}
setTimeout(() => { if (gameState === 'playing') uiElements.aiComms.classList.remove('active'); }, 8000);
} catch (error) {
uiElements.aiMsg.innerText = "通信障害発生。各自の判断で続行せよ。"; uiElements.aiLoad.classList.add('hidden');
}
}
function setupEvents() {
window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });
document.addEventListener('mousemove', (e) => {
if (gameState === 'playing') { targetX = ((e.clientX / window.innerWidth) * 2 - 1) * playerMaxX; targetY = 100 - ((e.clientY / window.innerHeight) * 2 - 1) * 20; }
});
document.addEventListener('touchmove', (e) => {
if (gameState === 'playing') { targetX = ((e.touches[0].clientX / window.innerWidth) * 2 - 1) * playerMaxX; targetY = 100 - ((e.touches[0].clientY / window.innerHeight) * 2 - 1) * 20; }
}, { passive: false });
document.addEventListener('keydown', (e) => {
if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].indexOf(e.code) > -1) e.preventDefault();
keys[e.code] = true;
if (gameState === 'playing' && e.code === 'KeyB') tryDropBomb();
}, { passive: false });
document.addEventListener('keyup', (e) => keys[e.code] = false);
document.addEventListener('touchstart', () => { if (gameState === 'playing') keys['Space'] = true; });
document.addEventListener('touchend', () => keys['Space'] = false);
uiElements.viewBtn.onclick = toggleView;
uiElements.bombBtn.onclick = tryDropBomb;
uiElements.aiBtn.onclick = triggerAiMission;
uiElements.reportBtn.onclick = generatePostFlightReport;
uiElements.start.querySelector('button').onclick = startGame;
document.getElementById('restart-btn').onclick = startGame;
document.getElementById('close-report-btn').onclick = () => uiElements.reportModal.style.display = 'none';
}
function startGame() {
uiElements.start.classList.add('hidden'); uiElements.gameOver.classList.add('hidden');
uiElements.score.classList.remove('hidden'); uiElements.bombBtn.classList.remove('hidden');
uiElements.aiBtn.classList.remove('hidden'); uiElements.aiComms.classList.remove('active');
uiElements.viewBtn.classList.remove('hidden');
uiElements.reportModal.style.display = 'none';
score = 0; currentSpeed = baseSpeed; targetX = 0; targetY = 100; lastShootTime = 0; lastBombTime = 0; keys = {};
gameState = 'countdown';
player.visible = true; player.position.set(0, 1, 0); player.rotation.set(0, 0, 0); wings.scale.set(0,0,0);
hq.position.z = 0; hq.visible = true;
if (!isCockpitView) {
scene.add(camera); camera.position.set(0, 3, 6); camera.rotation.set(0, 0, 0); camera.fov = 75; camera.updateProjectionMatrix();
}
enemies.forEach(e => scene.remove(e)); enemies = [];
buildings.forEach(b => scene.remove(b)); buildings = [];
bullets.forEach(b => scene.remove(b)); bullets = [];
enemyBullets.forEach(b => scene.remove(b)); enemyBullets = [];
bombs.forEach(b => scene.remove(b)); bombs = [];
bigExplosions.forEach(e => scene.remove(e.mesh)); bigExplosions = [];
allies.forEach(a => scene.remove(a)); allies = [];
allyBullets.forEach(b => scene.remove(b)); allyBullets = [];
kaijus.forEach(k => scene.remove(k)); kaijus = [];
kaijuBullets.forEach(b => scene.remove(b)); kaijuBullets = [];
// 仲間機の配置(5機編成に変更し、立体的に配置)
createAllyJet(-8, 2, -6);
createAllyJet(-4, -1, -3);
createAllyJet(4, -1, -3);
createAllyJet(8, 2, -6);
createAllyJet(0, 4, -8);
const cdEl = document.getElementById('countdown-display');
cdEl.classList.remove('hidden');
window.countdownStartTime = Date.now();
window.currentCountValue = 10;
cdEl.innerText = "10";
if (window.countdownTimer) clearInterval(window.countdownTimer);
initAudio();
}
function createCityBuilding() {
const h = 100 + Math.random() * 150;
const mat = new THREE.MeshStandardMaterial({ color: Math.random()>0.5?currentSectorTheme.color:0x666666, metalness: 0.5 });
const mesh = new THREE.Mesh(new THREE.BoxGeometry(10+Math.random()*10, h, 10+Math.random()*10), mat);
mesh.position.set((Math.random()>0.5?1:-1)*(20+Math.random()*40), h/2-1, -250);
scene.add(mesh); buildings.push(mesh);
}
function createEnemyJet() {
const enemy = new THREE.Group();
const metalMat = new THREE.MeshStandardMaterial({ color: 0xff3333, emissive: 0x550000, metalness: 0.7, roughness: 0.3 });
const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x111111, transparent: true, opacity: 0.8 });
const fuselage = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.5, 2.5), metalMat); enemy.add(fuselage);
const nose = new THREE.Mesh(new THREE.ConeGeometry(0.3, 1.0, 4), metalMat); nose.rotation.x = Math.PI / 2; nose.position.z = 1.7; enemy.add(nose);
const cockpit = new THREE.Mesh(new THREE.SphereGeometry(0.25, 16, 16), cockpitMat); cockpit.scale.set(1.0, 0.6, 1.5); cockpit.position.set(0, 0.3, 0.5); enemy.add(cockpit);
const wings = new THREE.Mesh(new THREE.BoxGeometry(3.5, 0.05, 1.0), metalMat); enemy.add(wings);
const tail = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.7, 0.7), metalMat); tail.position.set(0, 0.4, -0.8); enemy.add(tail);
const glow = new THREE.Mesh(new THREE.SphereGeometry(0.3, 8, 8), new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, opacity: 0.9 }));
glow.position.set(0, 0, -1.3); glow.scale.z = 2.0; enemy.add(glow);
enemy.position.set((Math.random()-0.5)*playerMaxX*2.5, 80 + Math.random()*40, -350);
enemy.userData = { baseX: enemy.position.x, phase: Math.random() * Math.PI * 2, speedX: Math.random() * 0.02 + 0.01 };
scene.add(enemy); enemies.push(enemy);
}
function shootEnemyBullet(enemyPos) {
const b = new THREE.Mesh(new THREE.CylinderGeometry(0.1,0.1,2,8), new THREE.MeshBasicMaterial({color:0xff0000}));
b.rotateX(Math.PI/2); b.position.copy(enemyPos); b.position.z += 2.5;
scene.add(b); enemyBullets.push(b);
playShootSound(true);
}
function shootKaijuBullet(kaiju) {
const b = new THREE.Mesh(new THREE.SphereGeometry(4, 16, 16), new THREE.MeshBasicMaterial({color:0x00ffff})); // 巨大な熱線(シアン)
b.position.set(kaiju.position.x, kaiju.position.y + 65, kaiju.position.z - 15); // 口元(頭)から発射
const dir = new THREE.Vector3().subVectors(player.position, b.position).normalize();
b.userData.velocity = dir.multiplyScalar(3.0); // 弾速を少し速く
scene.add(b); kaijuBullets.push(b);
playShootSound(true, true); // 重い発射音
}
function shootAllyBullet(ally) {
const b = new THREE.Mesh(new THREE.CylinderGeometry(0.1,0.1,2,8), new THREE.MeshBasicMaterial({color:0x00ffff}));
b.rotateX(Math.PI/2); b.position.copy(ally.position); b.position.z -= 2.5;
scene.add(b); allyBullets.push(b);
}
function shootBullet() {
const b = new THREE.Mesh(new THREE.CylinderGeometry(0.1,0.1,2,8), new THREE.MeshBasicMaterial({color:0xffff00}));
b.rotateX(Math.PI/2); b.position.copy(player.position); b.position.z -= 2.5;
scene.add(b); bullets.push(b);
playShootSound();
}
function tryDropBomb() {
if (Date.now() - lastBombTime > bombCooldown) {
const b = new THREE.Mesh(new THREE.SphereGeometry(1.5,16,16), new THREE.MeshStandardMaterial({color:0x222222}));
b.position.copy(player.position); b.userData.velocity = new THREE.Vector3(0,-1,-3);
scene.add(b); bombs.push(b); lastBombTime = Date.now();
}
}
function createBigExplosion(pos) {
const m = new THREE.Mesh(new THREE.SphereGeometry(2,32,32), new THREE.MeshBasicMaterial({color:0xff8800, transparent:true, opacity:1}));
m.position.copy(pos); scene.add(m); bigExplosions.push({mesh:m, life:1.0});
playExplosionSound();
}
// 怪獣との衝突・ダメージ処理の共通化
function checkHitKaiju(b, damage) {
let hit = false;
for (let j = kaijus.length - 1; j >= 0; j--) {
const k = kaijus[j];
if (new THREE.Box3().setFromObject(b).intersectsBox(new THREE.Box3().setFromObject(k))) {
k.userData.hp -= damage;
// 被弾エフェクト (胴体を白く点滅)
if(k.children[0]) {
k.children[0].material.emissive.setHex(0xffffff);
setTimeout(() => { if(k.children[0]) k.children[0].material.emissive.setHex(0x000000); }, 100);
}
if (k.userData.hp <= 0) {
createBigExplosion(k.position);
createBigExplosion(new THREE.Vector3(k.position.x + 10, k.position.y, k.position.z));
createBigExplosion(new THREE.Vector3(k.position.x - 10, k.position.y, k.position.z));
scene.remove(k); kaijus.splice(j, 1);
score += 5000;
uiElements.aiMsg.innerText = "ターゲット沈黙!見事な腕前だ!";
uiElements.aiComms.classList.add('active');
setTimeout(() => { uiElements.aiComms.classList.remove('active'); }, 4000);
}
hit = true; break;
}
}
return hit;
}
function animate() {
requestAnimationFrame(animate);
const now = Date.now();
if (interiorScreens.length > 0) {
interiorScreens[0].color.setHSL(0.45, 1.0, 0.5 + Math.sin(now * 0.01) * 0.2);
interiorScreens[1].color.setHSL(0.95, 1.0, 0.5 + Math.sin(now * 0.02) * 0.2);
interiorScreens[2].color.setHSL(0.6, 1.0, 0.5 + Math.sin(now * 0.015) * 0.2);
}
if (gameState === 'start') {
uiElements.hud.style.display = 'none'; scene.add(camera);
camera.position.x = Math.sin(now * 0.0005) * 10; camera.position.z = Math.cos(now * 0.0005) * 10 - 2; camera.position.y = 3; camera.lookAt(0, 1, 0);
player.position.set(0, 1, 0); player.rotation.set(0, 0, 0); player.position.y = Math.sin(now * 0.003) * 0.05 + 1;
hq.position.z = 0; hq.visible = true;
} else if (gameState === 'countdown') {
const elapsed = now - window.countdownStartTime;
const remaining = 10 - Math.floor(elapsed / 1000);
if (remaining > 0 && remaining < window.currentCountValue) {
window.currentCountValue = remaining; document.getElementById('countdown-display').innerText = remaining;
} else if (remaining <= 0 && window.currentCountValue > 0) {
window.currentCountValue = 0;
const cdEl = document.getElementById('countdown-display');
cdEl.innerText = "LAUNCH!"; setTimeout(() => cdEl.classList.add('hidden'), 1000);
gameState = 'launching'; generateSectorTheme();
}
uiElements.hud.style.display = isCockpitView ? 'block' : 'none';
player.position.set(0, 1, 0); player.rotation.set(0, 0, 0); player.position.y = Math.sin(now * 0.003) * 0.05 + 1;
hq.position.z = 0; hq.visible = true;
if (!isCockpitView) { camera.position.x = 0; camera.position.y = 3; camera.position.z = 6; camera.lookAt(0, 1, -20); }
if (isCockpitView) {
const elAlt = document.getElementById('hud-alt'); const elSpd = document.getElementById('hud-spd');
if(elAlt) elAlt.innerText = '0000'; if(elSpd) elSpd.innerText = '0000';
}
} else if (gameState === 'launching') {
currentSpeed += 0.05; hq.position.z += currentSpeed * 1.5;
if (!isCockpitView) {
camera.position.x = 0; camera.position.y += (player.position.y + 3 - camera.position.y) * 0.2; camera.position.z = player.position.z + 6; camera.lookAt(0, player.position.y, player.position.z - 20);
}
if (isCockpitView) {
const elAlt = document.getElementById('hud-alt'); const elSpd = document.getElementById('hud-spd');
if(elAlt) elAlt.innerText = (player.position.y * 10).toFixed(0).padStart(4, '0'); if(elSpd) elSpd.innerText = (currentSpeed * 1000).toFixed(0).padStart(4, '0');
}
if (hq.position.z > 200) {
player.position.y += (100 - player.position.y) * 0.04; player.rotation.x = Math.max(-0.4, (player.position.y - 100) * 0.01);
const ws = Math.min(1, wings.scale.x + 0.05); wings.scale.set(ws, ws, ws);
}
if (player.position.y > 95) { gameState = 'playing'; hq.visible = false; player.rotation.x = 0; }
} else if (gameState === 'playing') {
currentSpeed = Math.max(1.0, currentSpeed - 0.01);
score += currentSpeed * 0.1; uiElements.scoreVal.innerText = Math.floor(score);
let movedByKey = false;
if (keys['ArrowLeft'] || keys['KeyA']) { targetX -= 0.4; movedByKey = true; }
if (keys['ArrowRight'] || keys['KeyD']) { targetX += 0.4; movedByKey = true; }
if (keys['ArrowUp'] || keys['KeyW']) { targetY += 0.4; movedByKey = true; }
if (keys['ArrowDown'] || keys['KeyS']) { targetY -= 0.4; movedByKey = true; }
if (movedByKey) { targetX = Math.max(-playerMaxX, Math.min(playerMaxX, targetX)); targetY = Math.max(70, Math.min(130, targetY)); }
player.position.x += (targetX - player.position.x) * 0.1;
player.position.y += (targetY - player.position.y) * 0.1;
player.rotation.z += ((player.position.x - targetX) * 0.1 - player.rotation.z) * 0.15;
player.rotation.x = (player.position.y - targetY) * 0.02;
// 仲間機の追従と自動攻撃 (バラバラに動くように変更)
allies.forEach(ally => {
ally.userData.phaseX += ally.userData.speedX;
ally.userData.phaseY += ally.userData.speedY;
// 基本のオフセット位置に、独自の揺らぎ(サインカーブ)を加える
const targetAllyX = player.position.x + ally.userData.baseOffsetX + Math.sin(ally.userData.phaseX) * 3;
const targetAllyY = player.position.y + ally.userData.baseOffsetY + Math.cos(ally.userData.phaseY) * 2;
const targetAllyZ = player.position.z + ally.userData.baseOffsetZ;
// 各機体ごとに異なる追従スピードで動く
ally.position.x += (targetAllyX - ally.position.x) * ally.userData.followSpeed;
ally.position.y += (targetAllyY - ally.position.y) * ally.userData.followSpeed;
ally.position.z += (targetAllyZ - ally.position.z) * ally.userData.followSpeed;
// 機体の傾きも個別にバラつかせる
ally.rotation.z = player.rotation.z + Math.sin(ally.userData.phaseX) * 0.15;
ally.rotation.x = player.rotation.x + Math.sin(ally.userData.phaseY) * 0.1;
// 射撃タイミングもバラバラにする
if (now - ally.userData.lastShoot > 600 + Math.random() * 400 && Math.random() < 0.1) {
shootAllyBullet(ally); ally.userData.lastShoot = now;
}
});
if (Math.random() < 0.04 && kaijus.length === 0) createEnemyJet();
if (Math.random() < 0.1) createCityBuilding();
// 怪獣の出現 (スコアが貯まっており、かつ怪獣がいない時)
if (Math.random() < 0.002 && kaijus.length === 0 && score > 200) {
createKaiju();
playKaijuRoar();
uiElements.aiMsg.innerText = "WARNING: 地表に超巨大な生体反応を確認!ゴジラ型怪獣だ!総員攻撃せよ!";
uiElements.aiComms.classList.add('active');
setTimeout(() => { uiElements.aiComms.classList.remove('active'); }, 5000);
}
if (keys['Space'] && now - lastShootTime > shootInterval) { shootBullet(); lastShootTime = now; }
if (isCockpitView) {
const elAlt = document.getElementById('hud-alt'); const elSpd = document.getElementById('hud-spd');
if(elAlt) elAlt.innerText = (player.position.y * 10).toFixed(0).padStart(4, '0'); if(elSpd) elSpd.innerText = (currentSpeed * 1000).toFixed(0).padStart(4, '0');
}
if (!isCockpitView) {
camera.position.x += (player.position.x * 0.3 - camera.position.x) * 0.1;
camera.position.y += (player.position.y + 4 - camera.position.y) * 0.1;
camera.lookAt(player.position.x * 0.1, player.position.y, -20);
}
const bRemain = Math.ceil(Math.max(0, (bombCooldown - (now - lastBombTime))/1000));
uiElements.bombCD.innerText = bRemain > 0 ? bRemain : "READY"; uiElements.bombBtn.style.opacity = bRemain > 0 ? 0.5 : 1;
// プレイヤ弾
bullets.forEach((b, i) => {
b.position.z -= 4;
if (checkHitKaiju(b, 1)) { scene.remove(b); bullets.splice(i, 1); return; }
let hit = false;
for (let j = enemies.length - 1; j >= 0; j--) {
if (new THREE.Box3().setFromObject(b).intersectsBox(new THREE.Box3().setFromObject(enemies[j]))) {
createBigExplosion(enemies[j].position); scene.remove(enemies[j]); enemies.splice(j, 1); scene.remove(b); bullets.splice(i, 1); score += 150; hit = true; break;
}
}
if (!hit && b && b.position.z < -250) { scene.remove(b); bullets.splice(i, 1); }
});
// 仲間弾
allyBullets.forEach((b, i) => {
b.position.z -= 4;
if (checkHitKaiju(b, 1)) { scene.remove(b); allyBullets.splice(i, 1); return; }
let hit = false;
for (let j = enemies.length - 1; j >= 0; j--) {
if (new THREE.Box3().setFromObject(b).intersectsBox(new THREE.Box3().setFromObject(enemies[j]))) {
createBigExplosion(enemies[j].position); scene.remove(enemies[j]); enemies.splice(j, 1); scene.remove(b); allyBullets.splice(i, 1); score += 50; hit = true; break;
}
}
if (!hit && b && b.position.z < -250) { scene.remove(b); allyBullets.splice(i, 1); }
});
// 爆弾
bombs.forEach((b, i) => {
b.position.add(b.userData.velocity); b.userData.velocity.y -= 0.05;
if (checkHitKaiju(b, 10)) { createBigExplosion(b.position); scene.remove(b); bombs.splice(i, 1); return; }
let exploded = false;
for (let j = enemies.length - 1; j >= 0; j--) {
if (new THREE.Box3().setFromObject(b).intersectsBox(new THREE.Box3().setFromObject(enemies[j]))) {
createBigExplosion(b.position); scene.remove(enemies[j]); enemies.splice(j, 1); scene.remove(b); bombs.splice(i, 1); score += 200; exploded = true; break;
}
}
if (!exploded && b.position.y <= 0) { b.position.y = 0; createBigExplosion(b.position); scene.remove(b); bombs.splice(i, 1); }
});
bigExplosions.forEach((e, i) => {
e.mesh.scale.addScalar(1.5); e.life -= 0.02; e.mesh.material.opacity = e.life;
for (let j = enemies.length - 1; j >= 0; j--) {
if (enemies[j].position.distanceTo(e.mesh.position) < e.mesh.scale.x * 2 + 5) {
createBigExplosion(enemies[j].position); scene.remove(enemies[j]); enemies.splice(j, 1); score += 100;
}
}
if (e.life <= 0) { scene.remove(e.mesh); bigExplosions.splice(i, 1); }
});
buildings.forEach((b, i) => { b.position.z += currentSpeed; if (b.position.z > 20) { scene.remove(b); buildings.splice(i, 1); } });
// 敵弾・怪獣弾とプレイヤーの衝突
[...enemyBullets, ...kaijuBullets].forEach((b, i) => {
if (b.userData.velocity) {
b.position.add(b.userData.velocity); // 怪獣弾の移動
} else {
b.position.z += currentSpeed + 3; // 通常敵弾の移動
}
if (new THREE.Box3().setFromObject(player).expandByScalar(-0.3).intersectsBox(new THREE.Box3().setFromObject(b))) {
gameState = 'gameover'; player.visible = false;
uiElements.aiBtn.classList.add('hidden'); uiElements.bombBtn.classList.add('hidden'); uiElements.viewBtn.classList.add('hidden'); uiElements.hud.style.display = 'none';
createBigExplosion(player.position);
setTimeout(() => { uiElements.score.classList.add('hidden'); uiElements.gameOver.classList.remove('hidden'); uiElements.finalScore.innerText = Math.floor(score); }, 1000);
}
if (b.position.z > 50) {
scene.remove(b);
if(b.userData.velocity) { kaijuBullets.splice(kaijuBullets.indexOf(b), 1); } else { enemyBullets.splice(enemyBullets.indexOf(b), 1); }
}
});
enemies.forEach((e, i) => {
e.position.z += currentSpeed + 0.8;
e.userData.phase += e.userData.speedX;
e.position.x = e.userData.baseX + Math.sin(e.userData.phase) * 3;
e.rotation.z = Math.sin(e.userData.phase) * 0.2;
if (Math.random() < 0.015) shootEnemyBullet(e.position);
if (new THREE.Box3().setFromObject(player).expandByScalar(-0.3).intersectsBox(new THREE.Box3().setFromObject(e))) {
gameState = 'gameover'; player.visible = false; uiElements.aiBtn.classList.add('hidden'); uiElements.bombBtn.classList.add('hidden'); uiElements.viewBtn.classList.add('hidden'); uiElements.hud.style.display = 'none'; createBigExplosion(player.position); setTimeout(() => { uiElements.score.classList.add('hidden'); uiElements.gameOver.classList.remove('hidden'); uiElements.finalScore.innerText = Math.floor(score); }, 1000);
}
if (e.position.z > 20) { scene.remove(e); enemies.splice(i, 1); }
});
// 怪獣の処理
kaijus.forEach((k, i) => {
k.position.z += currentSpeed * 0.4; // 地上をゆっくり歩く
k.userData.phase += 0.05;
// 歩行の揺れ(重々しく)
k.position.y = Math.abs(Math.sin(k.userData.phase)) * 2;
k.rotation.z = Math.sin(k.userData.phase * 0.5) * 0.03;
k.rotation.y = Math.sin(k.userData.phase * 0.25) * 0.05; // 首を少し振る
if (now - k.userData.lastShoot > 2500) {
shootKaijuBullet(k); k.userData.lastShoot = now;
}
if (new THREE.Box3().setFromObject(player).expandByScalar(-0.5).intersectsBox(new THREE.Box3().setFromObject(k))) {
gameState = 'gameover'; player.visible = false; uiElements.aiBtn.classList.add('hidden'); uiElements.bombBtn.classList.add('hidden'); uiElements.viewBtn.classList.add('hidden'); uiElements.hud.style.display = 'none'; createBigExplosion(player.position); setTimeout(() => { uiElements.score.classList.add('hidden'); uiElements.gameOver.classList.remove('hidden'); uiElements.finalScore.innerText = Math.floor(score); }, 1000);
}
if (k.position.z > 50) { scene.remove(k); kaijus.splice(i, 1); }
});
roadLines.forEach(l => { l.position.z += currentSpeed; if (l.position.z > 15) l.position.z -= 450; });
}
renderer.render(scene, camera);
}
</script>
</body>
</html>
頑張れ