ブラウザで遊べるドライブゲームImageBooster
【更新機歴】
・2025/12/22 バージョン3.0公開。
・2025/12/24 バージョン4.0公開。
・2025/12/26 バージョン5.0公開。…トンネルの外側を追加。
・2026/1/4 バージョン6.0公開。… ブーストにロックオンを追加。
・2026/1/5 バージョン6.1公開。… ブースト時のロックオンを自動化。
・テキストファイルを新規作成して、
(index.html) 以下のソースコードをコピペして保存し、
ファイルをクリックすると、
webブラウザが起動してゲームが遊べます。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Image Booster 6.1 (Parallel Cam)</title>
<style>
body { margin: 0; overflow: hidden; background: #000; font-family: 'Arial Black', sans-serif; }
#ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; color: white; text-shadow: 2px 2px 4px #000; z-index: 10; }
#view-mode { position: absolute; top: 20px; right: 20px; font-size: 20px; color: #0f0; }
#stage-info { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); font-size: 20px; color: #ff0; }
#score-info { position: absolute; top: 20px; left: 20px; font-size: 24px; color: #fff; }
#speed-lines {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(circle, transparent 40%, rgba(255, 255, 255, 0.1) 80%),
repeating-conic-gradient(transparent 0deg, transparent 2deg, rgba(100, 255, 255, 0.3) 2.1deg, transparent 3deg);
opacity: 0;
transition: opacity 0.1s ease-out;
pointer-events: none;
z-index: 5;
mix-blend-mode: screen;
}
/* ロックオンサイト */
.lock-on-sight {
position: absolute;
width: 40px; height: 40px;
border: 3px solid #0f0;
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 0 10px rgba(255,255,255,0.5), inset 0 0 5px rgba(255,255,255,0.5);
z-index: 8;
transition: width 0.1s, height 0.1s;
}
.lock-on-sight.rushing {
border-width: 5px;
width: 60px; height: 60px;
background: rgba(255, 255, 255, 0.2);
}
#hud-bottom { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); width: 85%; display: none; flex-direction: row; justify-content: space-around; align-items: flex-end; }
.gauge-container { width: 30%; text-align: center; }
.gauge-label { font-size: 18px; margin-bottom: 5px; }
.bar-bg { width: 100%; height: 12px; border: 2px solid #fff; border-radius: 6px; overflow: hidden; background: rgba(0,0,0,0.5); transform: skewX(-20deg); }
.bar-fill { height: 100%; width: 100%; transition: width 0.05s ease-out; }
#hp-fill { background: #adff2f; box-shadow: 0 0 10px #adff2f; }
#speed-val { font-size: 56px; line-height: 1; color: #00ffff; text-shadow: 0 0 15px #00ffff; font-style: italic; }
#speed-unit { font-size: 20px; color: #aaa; margin-left: 5px; }
#speed-bg { }
#speed-fill { background: #00ffff; }
#time-bg { }
#time-fill { background: #ffcc00; }
#center-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100%; text-align: center; }
h1 { font-size: 70px; margin: 0; color: #0f0; text-shadow: 0 0 30px #0f0; font-style: italic; letter-spacing: -2px; }
p { font-size: 24px; margin: 10px 0; color: #fff; }
.pop-text { position: absolute; font-weight: bold; font-size: 40px; animation: moveUpFade 0.8s forwards; transform: translate(-50%, -50%); z-index: 999; text-shadow: 0 0 10px white; }
@keyframes moveUpFade { 0% { transform: translate(-50%, 0) scale(1); opacity: 1; } 100% { transform: translate(-50%, -100px) scale(1.5); opacity: 0; } }
</style>
<script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } } </script>
</head>
<body>
<div id="speed-lines"></div>
<div id="ui-layer">
<div id="score-info">SCORE: 0</div>
<div id="stage-info">STAGE: 1 / 8</div>
<div id="view-mode">VIEW: 3RD PERSON</div>
<div id="hud-bottom">
<div class="gauge-container">
<div class="gauge-label">SHIELD</div>
<div class="bar-bg" id="hp-bg"><div id="hp-fill" class="bar-fill"></div></div>
</div>
<div class="gauge-container">
<div style="margin-bottom:5px;">
<span id="speed-val">0</span><span id="speed-unit">km/h</span>
</div>
<div class="bar-bg" id="speed-bg"><div id="speed-fill" class="bar-fill"></div></div>
</div>
<div class="gauge-container">
<div class="gauge-label">TIME: <span id="time-num" style="font-size: 32px;">1:00</span></div>
<div class="bar-bg" id="time-bg"><div id="time-fill" class="bar-fill"></div></div>
</div>
</div>
<div id="center-text"></div>
</div>
<script type="module">
import * as THREE from 'three';
// --- Sound Manager ---
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let noiseBuffer = null;
let windSource = null, windGain = null, windFilter = null;
let jetOsc = null, jetGain = null;
function createNoiseBuffer() {
const bufferSize = audioCtx.sampleRate * 2.0;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
return buffer;
}
const sound = {
init: () => { if (!noiseBuffer) noiseBuffer = createNoiseBuffer(); },
suspend: () => { if(audioCtx.state === 'running') audioCtx.suspend(); },
resume: () => { if(audioCtx.state === 'suspended') audioCtx.resume(); },
startEngine: () => {
if (windSource) return;
windSource = audioCtx.createBufferSource();
windSource.buffer = noiseBuffer;
windSource.loop = true;
windFilter = audioCtx.createBiquadFilter();
windFilter.type = 'bandpass';
windFilter.Q.value = 1.0;
windFilter.frequency.value = 400;
windGain = audioCtx.createGain();
windGain.gain.value = 0;
windSource.connect(windFilter); windFilter.connect(windGain); windGain.connect(audioCtx.destination);
windSource.start();
jetOsc = audioCtx.createOscillator();
jetOsc.type = 'sawtooth';
jetOsc.frequency.value = 800;
jetGain = audioCtx.createGain();
jetGain.gain.value = 0;
jetOsc.connect(jetGain); jetGain.connect(audioCtx.destination);
jetOsc.start();
},
updateEngine: (speedRatio, isBoosting) => {
if (!windSource) return;
const now = audioCtx.currentTime;
// High pitch for speed
const targetFreq = 200 + (speedRatio * 2000);
const targetVol = Math.min(0.3, speedRatio * 0.2);
windFilter.frequency.setTargetAtTime(targetFreq, now, 0.1);
windGain.gain.setTargetAtTime(targetVol, now, 0.1);
const jetVol = isBoosting ? 0.15 : 0.02;
const jetFreq = 600 + (speedRatio * 3000);
jetGain.gain.setTargetAtTime(jetVol, now, 0.2);
jetOsc.frequency.setTargetAtTime(jetFreq, now, 0.1);
},
play: (type) => {
if (audioCtx.state === 'suspended' && type !== 'ui') audioCtx.resume();
const now = audioCtx.currentTime;
if (type === 'crash') {
const src = audioCtx.createBufferSource(); src.buffer = noiseBuffer;
const f = audioCtx.createBiquadFilter(); f.type = 'lowpass';
f.frequency.setValueAtTime(800, now); f.frequency.exponentialRampToValueAtTime(100, now + 1.2);
const g = audioCtx.createGain(); g.gain.setValueAtTime(1.0, now); g.gain.exponentialRampToValueAtTime(0.01, now + 1.2);
src.connect(f); f.connect(g); g.connect(audioCtx.destination); src.start(now); src.stop(now + 1.2);
} else if (type === 'brake') {
const src = audioCtx.createBufferSource(); src.buffer = noiseBuffer;
const f = audioCtx.createBiquadFilter(); f.type = 'bandpass';
f.frequency.setValueAtTime(1200, now); f.frequency.linearRampToValueAtTime(400, now + 0.3);
const g = audioCtx.createGain(); g.gain.setValueAtTime(0.3, now); g.gain.linearRampToValueAtTime(0.01, now + 0.3);
src.connect(f); f.connect(g); g.connect(audioCtx.destination); src.start(now); src.stop(now + 0.3);
} else if (type === 'heal') {
const osc = audioCtx.createOscillator(); const g = audioCtx.createGain();
osc.connect(g); g.connect(audioCtx.destination); osc.type = 'sine';
osc.frequency.setValueAtTime(600, now); osc.frequency.exponentialRampToValueAtTime(2000, now+0.4);
g.gain.setValueAtTime(0.2, now); g.gain.linearRampToValueAtTime(0, now+0.4);
osc.start(now); osc.stop(now+0.4);
} else if (type === 'lockon') {
const osc = audioCtx.createOscillator(); const g = audioCtx.createGain();
osc.connect(g); g.connect(audioCtx.destination); osc.type = 'square';
osc.frequency.setValueAtTime(1200, now);
g.gain.setValueAtTime(0.05, now); g.gain.exponentialRampToValueAtTime(0.01, now+0.08);
osc.start(now); osc.stop(now+0.08);
} else if (type === 'boost_dash') {
const src = audioCtx.createBufferSource(); src.buffer = noiseBuffer;
const f = audioCtx.createBiquadFilter(); f.type = 'highpass';
f.frequency.setValueAtTime(500, now); f.frequency.linearRampToValueAtTime(2000, now+0.5);
const g = audioCtx.createGain();
g.gain.setValueAtTime(0.5, now); g.gain.exponentialRampToValueAtTime(0.01, now + 0.5);
src.connect(f); f.connect(g); g.connect(audioCtx.destination); src.start(now); src.stop(now + 0.5);
} else if (type === 'jump') {
const osc = audioCtx.createOscillator(); const g = audioCtx.createGain();
osc.connect(g); g.connect(audioCtx.destination); osc.type = 'triangle';
osc.frequency.setValueAtTime(150, now); osc.frequency.linearRampToValueAtTime(300, now+0.2);
g.gain.setValueAtTime(0.8, now); g.gain.exponentialRampToValueAtTime(0.01, now+0.3);
osc.start(now); osc.stop(now+0.3);
} else if (type === 'land') {
const src = audioCtx.createBufferSource(); src.buffer = noiseBuffer;
const f = audioCtx.createBiquadFilter(); f.type = 'lowpass'; f.frequency.setValueAtTime(300, now);
const g = audioCtx.createGain(); g.gain.setValueAtTime(1.5, now); g.gain.exponentialRampToValueAtTime(0.01, now+0.2);
src.connect(f); f.connect(g); g.connect(audioCtx.destination); src.start(now); src.stop(now+0.2);
} else if (type === 'coin') {
const osc = audioCtx.createOscillator(); const g = audioCtx.createGain();
osc.connect(g); g.connect(audioCtx.destination); osc.type = 'square';
osc.frequency.setValueAtTime(1200, now); osc.frequency.setValueAtTime(1800, now+0.05);
g.gain.setValueAtTime(0.1, now); g.gain.exponentialRampToValueAtTime(0.01, now+0.1);
osc.start(now); osc.stop(now+0.1);
} else if (type === 'ui') {
if(audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator(); const g = audioCtx.createGain();
osc.connect(g); g.connect(audioCtx.destination); osc.frequency.setValueAtTime(880, now);
g.gain.setValueAtTime(0.1, now); g.gain.exponentialRampToValueAtTime(0.01, now+0.1);
osc.start(now); osc.stop(now+0.1);
}
}
};
// --- Config ---
const MAX_HP = 10, TUBE_R = 18, MAX_TIME = 60;
// Speed scaling (Halved from previous version)
// Previous: 700 / 3000
// New: 350 / 1500
const NORMAL_MAX_SPEED = 350.0;
const RUSH_SPEED = 1500.0;
const NORMAL_ACCEL = 200.0; // Acceleration per sec
const GRAVITY = 100.0;
const JUMP_POWER = 40.0;
const TOTAL_STAGES = 8;
const VISIBLE_SEGMENTS = 90;
const FOG_NEAR = 150;
const FOG_FAR = 900;
// Display Multipliers (Adjusted so the number looks cool)
const SPEED_DISPLAY_MULTIPLIER = 0.6;
let scene, camera, renderer, clock;
let gameState = "TITLE", viewMode = "F3";
let isPaused = false;
let score = 0, stageScore = 0;
let hp = MAX_HP, timeLeft = MAX_TIME;
let currentStage = 1;
let player = {
mesh: null,
angle: 0,
x: 0,
z: 0,
vz: 10, vAngle: 0, vx: 0,
altitude: 0, jumpV: 0,
surge: 0, bank: 0,
isBoosting: false,
dashOffset: 0,
wasAirborne: false,
barrier: null,
shadow: null,
sonicBoom: null,
barrierRadius: 3.5,
// Lock-on Rush System
mode: "NORMAL", // "NORMAL", "RUSHING"
lockTargets: [],
rushIndex: 0,
lockTimer: 0,
currentLockType: null // "block", "score", "heal", "hurdle"
};
let worldObjects = [], debris = [], stars;
let keys = {};
let nextSpawnZ = 150;
let stageHue = 0.0;
// --- Tunnel & Curve Logic ---
const TILE_SEGMENT_LENGTH = 12;
const TILES_PER_RING = 16;
let tunnelSegments = [];
function getCurve(z, mode) {
const coarseX = Math.sin(z * 0.0015) * 200;
const coarseY = Math.cos(z * 0.0015) * 150;
const detailY = Math.sin(z * 0.006) * 80 + Math.sin(z * 0.015) * 30;
const detailX = Math.cos(z * 0.004) * 60;
if (mode === "F2") {
return new THREE.Vector3(0, coarseY + detailY, z);
}
return new THREE.Vector3(coarseX + detailX, coarseY + detailY, z);
}
function getBasis(z, mode) {
const origin = getCurve(z, mode);
const forwardPoint = getCurve(z + 5.0, mode); // Lookahead slightly increased
const T = new THREE.Vector3().subVectors(forwardPoint, origin).normalize();
const tempUp = new THREE.Vector3(0, 1, 0);
const R = new THREE.Vector3().crossVectors(T, tempUp).normalize();
const U = new THREE.Vector3().crossVectors(R, T).normalize();
return { origin, T, U, R };
}
function getSectionPosition(angle, radius, basis, mode) {
let a = angle;
while (a > Math.PI) a -= Math.PI * 2;
while (a < -Math.PI) a += Math.PI * 2;
if (mode === "F2") {
const spread = TUBE_R * Math.PI;
const localX = (a / Math.PI) * spread;
const localY = -radius;
return new THREE.Vector3(
basis.origin.x + localX,
basis.origin.y + localY,
basis.origin.z
);
}
const localX = Math.sin(angle) * radius;
const localY = -Math.cos(angle) * radius;
return basis.origin.clone()
.addScaledVector(basis.R, localX)
.addScaledVector(basis.U, localY);
}
function getGrassColor(hueShift) {
// Pale transparent colors
const hue = (0.45 + hueShift) % 1.0;
const sat = 0.4;
const val = 0.8;
return new THREE.Color().setHSL(hue, sat, val);
}
function isGap(segmentIndex) {
if (segmentIndex < 15) return false;
return (segmentIndex % 60) >= 56; // Bigger gaps
}
function init() {
sound.init();
clock = new THREE.Clock();
scene = new THREE.Scene();
scene.background = new THREE.Color(0x050510);
scene.fog = new THREE.Fog(0x050510, FOG_NEAR, FOG_FAR);
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 3000);
scene.add(new THREE.AmbientLight(0x606060, 0.8));
const sun = new THREE.DirectionalLight(0xffffff, 1.2);
sun.position.set(0, 100, -50);
scene.add(sun);
const rim = new THREE.DirectionalLight(0x00ffff, 0.8);
rim.position.set(0, -50, 50);
scene.add(rim);
createStars();
// --- Player Model ---
const playerGeo = new THREE.Group();
const bodyGeo = new THREE.ConeGeometry(1.5, 6, 4);
const bodyMat = new THREE.MeshPhongMaterial({ color: 0xeeeeee, specular: 0xffffff, shininess: 80 });
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.rotation.x = -Math.PI / 2;
body.scale.set(1, 1, 0.5);
playerGeo.add(body);
const wingGeo = new THREE.BoxGeometry(5, 0.2, 2);
const wingMat = new THREE.MeshPhongMaterial({ color: 0x00ccff });
const wing = new THREE.Mesh(wingGeo, wingMat);
wing.position.set(0, 0, 1);
playerGeo.add(wing);
const thrustGeo = new THREE.ConeGeometry(0.8, 8, 8);
const thrustMat = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.7 });
const thrust = new THREE.Mesh(thrustGeo, thrustMat);
thrust.rotation.x = Math.PI / 2;
thrust.position.z = -3.5;
playerGeo.add(thrust);
player.thrust = thrust;
const barrierGeo = new THREE.IcosahedronGeometry(3.5, 1);
const barrierMat = new THREE.MeshPhongMaterial({ color: 0x00ffff, transparent: true, opacity: 0.3, wireframe: true, emissive: 0x00aaaa, shininess: 100 });
const barrier = new THREE.Mesh(barrierGeo, barrierMat);
barrier.position.z = 1.0;
playerGeo.add(barrier);
player.barrier = barrier;
const boomGeo = new THREE.SphereGeometry(4.0, 32, 16, 0, Math.PI * 2, 0, Math.PI * 0.5);
const boomMat = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.4,
side: THREE.DoubleSide,
blending: THREE.AdditiveBlending
});
const boom = new THREE.Mesh(boomGeo, boomMat);
boom.rotation.x = -Math.PI / 2;
boom.position.z = 3.0;
boom.visible = false;
playerGeo.add(boom);
player.sonicBoom = boom;
player.mesh = playerGeo;
scene.add(player.mesh);
const shadowGeo = new THREE.PlaneGeometry(3.5, 5.0);
const shadowMat = new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.8 });
player.shadow = new THREE.Mesh(shadowGeo, shadowMat);
scene.add(player.shadow);
initTunnel();
window.addEventListener('keydown', (e) => {
const views = ["F1", "F2", "F3", "F4", "F5"];
if(views.includes(e.code)) {
e.preventDefault();
viewMode = e.code;
updateViewLabel();
}
keys[e.code] = true;
if (e.code === 'Escape' && gameState === "PLAYING") {
isPaused = !isPaused;
if(isPaused) {
sound.suspend();
document.getElementById('center-text').innerHTML = "<h1 style='color:white; text-shadow:0 0 10px #000;'>PAUSED</h1>";
clock.stop();
} else {
sound.resume();
document.getElementById('center-text').innerHTML = "";
clock.start();
}
}
if(e.code === 'Space') { e.preventDefault(); handleSpace(); }
if(e.code === 'KeyX') handleJump();
// Z Key: Start Rush if targets exist
if(e.code === 'KeyZ' && gameState === "PLAYING" && !isPaused) {
if (player.lockTargets.length > 0) {
player.mode = "RUSHING";
player.rushIndex = 0;
player.isBoosting = true;
sound.play('boost_dash');
}
}
});
window.addEventListener('keyup', (e) => {
keys[e.code] = false;
// Release Z: Stop Rush immediately and clear locks
if (e.code === 'KeyZ') {
if (player.mode === "RUSHING" || player.lockTargets.length > 0) {
player.mode = "NORMAL";
player.isBoosting = false;
// Clear locks
clearAllLocks();
}
}
});
updateViewLabel();
changeState("TITLE");
animate();
}
function clearAllLocks() {
player.lockTargets.forEach(obj => {
obj.locked = false;
if(obj.sightDom) {
obj.sightDom.remove();
obj.sightDom = null;
}
});
player.lockTargets = [];
player.currentLockType = null;
player.rushIndex = 0;
}
function initTunnel() {
const tileWidth = (TUBE_R * 2 * Math.PI) / TILES_PER_RING;
const boxGeo = new THREE.BoxGeometry(tileWidth * 1.1, 0.5, TILE_SEGMENT_LENGTH * 1.05);
for (let i = 0; i < VISIBLE_SEGMENTS; i++) {
const segmentGroup = new THREE.Group();
segmentGroup.userData = { zIndex: i };
for (let j = 0; j < TILES_PER_RING; j++) {
const mat = new THREE.MeshPhongMaterial({
color: 0xffffff,
opacity: 0.1,
transparent: true,
side: THREE.DoubleSide,
shininess: 100
});
const tile = new THREE.Mesh(boxGeo, mat);
segmentGroup.add(tile);
}
tunnelSegments.push(segmentGroup);
scene.add(segmentGroup);
}
}
function updateTunnel() {
const playerSegmentIndex = Math.floor(player.z / TILE_SEGMENT_LENGTH);
const startSegment = playerSegmentIndex - 8;
tunnelSegments.forEach((seg) => {
let segIdx = seg.userData.zIndex;
if (segIdx < startSegment) {
segIdx += VISIBLE_SEGMENTS;
seg.userData.zIndex = segIdx;
seg.children.forEach(tile => {
tile.material.color.copy(getGrassColor(stageHue));
if(segIdx % 10 === 0) tile.material.emissive.setHex(0x222222);
else tile.material.emissive.setHex(0x000000);
});
}
const gap = isGap(segIdx);
seg.visible = !gap;
const zPos = segIdx * TILE_SEGMENT_LENGTH;
const basis = getBasis(zPos, viewMode);
seg.children.forEach((tile, j) => {
let angle;
if (viewMode === "F2") {
const ratio = j / TILES_PER_RING;
angle = (ratio * Math.PI * 2) - Math.PI;
} else {
angle = (j / TILES_PER_RING) * Math.PI * 2;
}
const pos = getSectionPosition(angle, TUBE_R, basis, viewMode);
tile.position.copy(pos);
if (viewMode === "F2") {
const forward = basis.T.clone();
const right = new THREE.Vector3(1, 0, 0);
const up = new THREE.Vector3().crossVectors(right, forward).normalize();
const orthoForward = new THREE.Vector3().crossVectors(up, right).normalize();
const m = new THREE.Matrix4();
m.makeBasis(right, up, orthoForward);
tile.rotation.setFromRotationMatrix(m);
} else {
const isOutside = (viewMode === "F4" || viewMode === "F5");
const forward = basis.T.clone();
const tileLocalX = Math.sin(angle);
const tileLocalY = -Math.cos(angle);
const normal = new THREE.Vector3()
.addScaledVector(basis.R, tileLocalX)
.addScaledVector(basis.U, tileLocalY)
.normalize();
const up = isOutside ? normal : normal.clone().multiplyScalar(-1);
const tangent = new THREE.Vector3().crossVectors(up, forward).normalize();
const orthoForward = new THREE.Vector3().crossVectors(tangent, up).normalize();
const m = new THREE.Matrix4();
m.makeBasis(tangent, up, orthoForward);
tile.rotation.setFromRotationMatrix(m);
}
});
});
}
function createStars() {
const starGeo = new THREE.BufferGeometry();
const starCount = 5000;
const posArray = new Float32Array(starCount * 3);
for(let i=0; i<starCount * 3; i++) posArray[i] = (Math.random() - 0.5) * 6000;
starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
const starMat = new THREE.PointsMaterial({color: 0xffffff, size: 2.0, transparent: true, opacity: 0.8});
stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
}
function updateStars() {
if(stars) {
stars.position.set(0, 0, player.z);
stars.rotation.z += 0.0005;
}
}
function updateViewLabel() {
const labels = {
"F1": "1ST PERSON (INNER)",
"F2": "FLAT MODE (FIXED)",
"F3": "3RD PERSON (INNER)",
"F4": "1ST PERSON (OUTER)",
"F5": "3RD PERSON (OUTER)"
};
document.getElementById('view-mode').innerText = "VIEW: " + labels[viewMode];
}
function handleSpace() {
if (gameState === "TITLE" || gameState === "GAMEOVER" || gameState === "ALL_CLEAR") {
sound.play('ui');
sound.startEngine();
if(gameState !== "TITLE") { score = 0; currentStage = 1; }
changeState("STAGE_START");
}
else if (gameState === "STAGE_CLEAR") {
sound.play('ui');
currentStage++;
changeState("STAGE_START");
}
else if (gameState === "STAGE_START") { sound.play('ui'); changeState("PLAYING"); }
}
function handleJump() {
if (gameState === "PLAYING" && player.altitude <= 0.1 && !isPaused) {
sound.play('jump');
player.jumpV = JUMP_POWER;
player.wasAirborne = true;
}
}
function changeState(s) {
gameState = s;
document.getElementById('hud-bottom').style.display = (s === "PLAYING") ? "flex" : "none";
const menu = document.getElementById('center-text');
document.getElementById('stage-info').innerText = `STAGE: ${currentStage} / ${TOTAL_STAGES}`;
document.getElementById('speed-lines').style.opacity = 0;
if(s === "TITLE") menu.innerHTML = "<h1>Image Booster 6.1</h1><p style='color:#adff2f;'>PARALLEL CAM</p><p>PRESS SPACE TO START</p><p style='font-size:18px; color:#aaa;'>[UP]: ACCEL / [DOWN]: BRAKE / [Z]: RUSH ATTACK / [X]: JUMP</p>";
else if(s === "STAGE_START") {
stageScore = 0;
stageHue = Math.random();
menu.innerHTML = "<h1>STAGE "+currentStage+"</h1><p>READY?</p><p style='font-size:16px; color:#aaa;'>SPACE: GO!</p>";
resetPlayerPos();
}
else if(s === "PLAYING") menu.innerHTML = "";
else if(s === "STAGE_CLEAR") {
if (currentStage >= TOTAL_STAGES) {
changeState("ALL_CLEAR");
} else {
menu.innerHTML = "<h1 style='color:#0f0;'>STAGE CLEAR!</h1><p>SCORE: "+stageScore+"</p><p>SPACE FOR NEXT STAGE</p>";
}
}
else if(s === "ALL_CLEAR") {
menu.innerHTML = "<h1 style='color:#0ff;'>COMPLETE!</h1><p>FINAL SCORE: "+score+"</p><p>YOU ARE THE CHAMPION</p><p>SPACE TO TITLE</p>";
}
else if(s === "GAMEOVER") menu.innerHTML = "<h1 style='color:red;'>CRITICAL FAILURE</h1><p>SCORE: "+score+"</p><p>SPACE TO RESTART</p>";
}
function resetPlayerPos() {
player.angle = 0; player.x = 0; player.z = 0; player.vz = 0.0; player.altitude = 0; player.jumpV = 0;
player.surge = 0; player.bank = 0; player.dashOffset = 0;
hp = MAX_HP; timeLeft = MAX_TIME;
player.mode = "NORMAL";
clearAllLocks();
player.isBoosting = false;
worldObjects.forEach(obj => {
scene.remove(obj.mesh);
if(obj.sightDom) obj.sightDom.remove();
});
debris.forEach(d => scene.remove(d));
worldObjects = []; debris = [];
nextSpawnZ = 150;
tunnelSegments.forEach((seg, i) => { seg.userData.zIndex = i; });
}
function animate() {
requestAnimationFrame(animate);
const dt = clock.getDelta();
if (gameState === "PLAYING" && !isPaused) {
const safeDt = Math.min(dt, 0.1);
updatePhysics(safeDt);
updateObjects(safeDt);
updateDebris(safeDt);
updateAutoLock(safeDt);
}
if(gameState === "PLAYING" && !isPaused) {
let ratio = player.vz / NORMAL_MAX_SPEED;
if(player.mode === "RUSHING") ratio = 3.0;
sound.updateEngine(ratio, player.isBoosting);
} else if (!isPaused) {
sound.updateEngine(0, false);
}
if(!isPaused) {
updateTunnel();
updatePlayerVisuals();
updateStars();
}
renderer.render(scene, camera);
}
function updatePhysics(dt) {
if (player.mode === "RUSHING") {
handleRushPhysics(dt);
return;
}
let currentAccel = 0;
const isBraking = keys['ArrowDown'];
if (keys['ArrowUp']) {
currentAccel = NORMAL_ACCEL;
player.surge = THREE.MathUtils.lerp(player.surge, 3.0, 5 * dt);
player.dashOffset = THREE.MathUtils.lerp(player.dashOffset, 0, 5 * dt);
}
else {
currentAccel = 0;
player.surge = THREE.MathUtils.lerp(player.surge, 0, 5 * dt);
}
if (isBraking) {
player.vz -= NORMAL_ACCEL * 1.5 * dt;
player.surge = THREE.MathUtils.lerp(player.surge, -3.0, 5 * dt);
if (player.vz > 50) {
if (Math.random() < 0.5) sound.play('brake');
const basis = getBasis(player.z, viewMode);
const pos = player.mesh.position.clone();
createDebris(pos, 0xffaa00, 1.0, 1, "spark_brake");
}
}
if (currentAccel > 0) {
if (player.vz < NORMAL_MAX_SPEED) player.vz += currentAccel * dt;
else player.vz *= (1.0 - (0.5 * dt));
} else if (!isBraking) {
player.vz *= (1.0 - (0.2 * dt));
}
player.vz = Math.max(0.0, player.vz);
document.getElementById('speed-lines').style.opacity = 0;
handleGravity(dt);
handleSteering(dt);
player.z += player.vz * dt;
updateGameTimers(dt);
updateUI();
}
function updateAutoLock(dt) {
if (player.mode === "RUSHING") return;
player.lockTimer += dt;
if (player.lockTimer < 0.1) return;
player.lockTimer = 0;
const scanRange = 250;
const lineThreshold = 0.2;
let candidates = worldObjects.filter(obj => {
if (obj.locked) return false;
if (obj.z < player.z) return false;
if (obj.z > player.z + scanRange) return false;
let da = Math.abs(obj.angle - player.angle);
if (da > Math.PI) da = Math.PI * 2 - da;
if (da > lineThreshold) return false;
if (player.currentLockType && player.currentLockType !== obj.type) return false;
return true;
});
if (candidates.length > 0) {
candidates.sort((a,b) => a.z - b.z);
const target = candidates[0];
target.locked = true;
player.lockTargets.push(target);
player.currentLockType = target.type;
createLockSight(target);
sound.play('lockon');
}
}
function handleRushPhysics(dt) {
if (player.lockTargets.length === 0 || player.rushIndex >= player.lockTargets.length) {
player.mode = "NORMAL";
player.isBoosting = false;
player.surge = 0;
clearAllLocks();
return;
}
const targetObj = player.lockTargets[player.rushIndex];
if (!worldObjects.includes(targetObj)) {
player.rushIndex++;
return;
}
const rushDist = RUSH_SPEED * dt;
const dz = targetObj.z - player.z;
if (dz <= rushDist) {
player.z = targetObj.z;
player.angle = targetObj.angle;
handleCollision(targetObj, worldObjects.indexOf(targetObj), true);
player.surge = 15.0;
player.rushIndex++;
} else {
player.z += rushDist;
const t = 10.0 * dt;
let targetAngle = targetObj.angle;
if (targetAngle - player.angle > Math.PI) targetAngle -= Math.PI*2;
if (player.angle - targetAngle > Math.PI) targetAngle += Math.PI*2;
player.angle = THREE.MathUtils.lerp(player.angle, targetAngle, t);
player.altitude = THREE.MathUtils.lerp(player.altitude, 0, t);
player.surge = THREE.MathUtils.lerp(player.surge, 20.0, 5 * dt);
document.getElementById('speed-lines').style.opacity = 1.0;
}
updateGameTimers(dt);
updateUI();
}
function createLockSight(obj) {
const div = document.createElement('div');
div.className = 'lock-on-sight';
let col = "#fff";
if (obj.type === "block") col = "#ddd";
else if (obj.type === "hurdle") col = "#f30";
else if (obj.type === "heal") col = "#0f0";
else if (obj.type === "score") col = "#ff0";
div.style.borderColor = col;
div.style.boxShadow = `0 0 10px ${col}, inset 0 0 10px ${col}`;
document.getElementById('ui-layer').appendChild(div);
obj.sightDom = div;
}
function updateLockSights() {
worldObjects.forEach(obj => {
if (obj.sightDom) {
if (!worldObjects.includes(obj)) {
obj.sightDom.remove();
obj.sightDom = null;
return;
}
if (obj.z < player.z) {
obj.locked = false;
obj.sightDom.remove();
obj.sightDom = null;
return;
}
const vector = obj.mesh.position.clone();
vector.project(camera);
const x = (vector.x * .5 + .5) * window.innerWidth;
const y = -(vector.y * .5 - .5) * window.innerHeight;
obj.sightDom.style.left = x + 'px';
obj.sightDom.style.top = y + 'px';
if (player.mode === "RUSHING" && player.lockTargets[player.rushIndex] === obj) {
obj.sightDom.classList.add('rushing');
}
}
});
}
function handleGravity(dt) {
const currentCurveY = getCurve(player.z, viewMode).y;
const nextCurveY = getCurve(player.z + player.vz * 0.1, viewMode).y;
const dropRate = (currentCurveY - nextCurveY) * 60;
if (dropRate > 100 && player.vz > 100 && player.altitude <= 0.5) {
player.jumpV += dropRate * 0.05;
player.wasAirborne = true;
}
player.altitude += player.jumpV * dt;
const currentSegIdx = Math.floor(player.z / TILE_SEGMENT_LENGTH);
const inGap = isGap(currentSegIdx);
const speedKmh = player.vz * SPEED_DISPLAY_MULTIPLIER;
const isFloating = (speedKmh > 30 && !player.wasAirborne);
if (inGap && !isFloating) {
player.jumpV -= GRAVITY * dt;
} else {
if (player.altitude > 0) player.jumpV -= GRAVITY * dt;
else {
if (player.wasAirborne && player.jumpV < -10) {
sound.play('land');
player.wasAirborne = false;
}
player.altitude = 0;
player.jumpV = 0;
}
}
if (player.altitude < -40) {
hp = 0;
changeState("GAMEOVER");
}
}
function handleSteering(dt) {
let steerFactor = 1.0;
if (player.vz > 300.0) steerFactor = 0.5;
if (viewMode === "F2") steerFactor = 4.0; else steerFactor = 2.0;
let targetBank = 0;
const isOutside = (viewMode === "F4" || viewMode === "F5");
const isFlat = (viewMode === "F2");
const steerDirection = (isOutside || isFlat) ? -1 : 1;
const BANK_LIMIT = 0.78;
if (keys['ArrowLeft']) {
player.vAngle = THREE.MathUtils.lerp(player.vAngle, -1.5 * steerFactor * steerDirection, 5 * dt);
targetBank = -BANK_LIMIT;
}
else if (keys['ArrowRight']) {
player.vAngle = THREE.MathUtils.lerp(player.vAngle, 1.5 * steerFactor * steerDirection, 5 * dt);
targetBank = BANK_LIMIT;
}
else {
player.vAngle = THREE.MathUtils.lerp(player.vAngle, 0, 10 * dt);
}
player.angle += player.vAngle * dt;
if (viewMode === "F2") {
const limit = Math.PI * 1.0;
if (player.angle > limit) { player.angle = limit; player.vAngle = 0; }
else if (player.angle < -limit) { player.angle = -limit; player.vAngle = 0; }
}
player.bank = THREE.MathUtils.lerp(player.bank, targetBank, 5 * dt);
}
function updateGameTimers(dt) {
timeLeft -= dt;
if (timeLeft <= 0) { timeLeft = 0; changeState("STAGE_CLEAR"); }
if (hp <= 0) { hp = 0; changeState("GAMEOVER"); }
}
function updatePlayerVisuals() {
const visualZ = player.z + player.surge + player.dashOffset;
const basis = getBasis(visualZ, viewMode);
const isOutside = (viewMode === "F4" || viewMode === "F5");
const r = isOutside
? TUBE_R + 2.0 + player.altitude
: TUBE_R - 2.0 - player.altitude;
const pos = getSectionPosition(player.angle, r, basis, viewMode);
const center = basis.origin.clone();
let playerUp, playerForward;
if (viewMode === "F2") {
playerForward = basis.T.clone();
const right = new THREE.Vector3(1, 0, 0);
playerUp = new THREE.Vector3().crossVectors(right, playerForward).normalize();
} else {
if (isOutside) {
playerUp = new THREE.Vector3().subVectors(pos, center).normalize();
} else {
playerUp = new THREE.Vector3().subVectors(center, pos).normalize();
}
playerForward = basis.T.clone();
}
player.mesh.position.copy(pos);
const m = new THREE.Matrix4();
const right = new THREE.Vector3().crossVectors(playerForward, playerUp).normalize();
const orthoUp = new THREE.Vector3().crossVectors(right, playerForward).normalize();
m.makeBasis(right, orthoUp, playerForward);
player.mesh.rotation.setFromRotationMatrix(m);
player.mesh.rotateZ(player.bank);
if (player.altitude > 0.5) player.mesh.rotateX(-0.1);
player.mesh.visible = (viewMode !== "F1" && viewMode !== "F4");
if (player.sonicBoom) {
player.sonicBoom.visible = (player.mode === "RUSHING");
if(player.mode === "RUSHING") {
const s = 1.0 + Math.random() * 0.1;
player.sonicBoom.scale.set(s, s, s);
}
}
if (Math.abs(player.bank) > 0.7 && player.vz > 100.0) {
const sideOffset = (player.bank > 0) ? 2.0 : -2.0;
const sparkPos = player.mesh.position.clone()
.addScaledVector(right, sideOffset)
.addScaledVector(playerUp, -0.5);
createDebris(sparkPos, 0xffff00, 0.3, 2, "spark");
}
if (player.thrust) {
const speedRatio = player.vz / (player.mode === "RUSHING" ? RUSH_SPEED : NORMAL_MAX_SPEED);
player.thrust.visible = (player.vz > 10.0 || player.mode === "RUSHING");
player.thrust.scale.z = 1.0 + speedRatio * 3.0;
player.thrust.material.opacity = 0.6 + speedRatio * 0.4;
player.thrust.material.color.setHex(player.mode === "RUSHING" ? 0xff00ff : 0x00ffff);
}
if (player.barrier) {
const hpRatio = Math.max(0, hp / MAX_HP);
const targetScale = 0.8 + (hpRatio * 0.2);
player.barrier.scale.setScalar(targetScale);
player.barrier.rotation.y += 0.05;
if (player.mode === "RUSHING") {
player.barrier.material.color.setHex(0xff0000);
player.barrier.material.emissive.setHex(0x550000);
} else {
player.barrier.material.color.setHex(0x00ffff);
player.barrier.material.emissive.setHex(0x004444);
}
}
if (player.shadow) {
const currentSegIdx = Math.floor(player.z / TILE_SEGMENT_LENGTH);
const inGap = isGap(currentSegIdx);
const isFirstPerson = (viewMode === "F1" || viewMode === "F4");
if (!isFirstPerson && !inGap) {
player.shadow.visible = true;
const groundR = isOutside ? TUBE_R + 0.2 : TUBE_R - 0.2;
const groundPos = getSectionPosition(player.angle, groundR, basis, viewMode);
player.shadow.position.copy(groundPos);
if (viewMode === "F2") {
const mShadow = new THREE.Matrix4();
mShadow.makeBasis(right, orthoUp, playerForward);
player.shadow.rotation.setFromRotationMatrix(mShadow);
player.shadow.rotateX(-Math.PI/2);
player.shadow.material.opacity = 0.9;
} else {
player.shadow.lookAt(center);
if(isOutside) player.shadow.rotateY(Math.PI);
player.shadow.material.opacity = Math.max(0.4, 0.9 - player.altitude * 0.02);
}
const distFromGround = Math.max(0, player.altitude);
const shadowScale = Math.max(0.1, 1.0 - distFromGround * 0.05);
player.shadow.scale.setScalar(shadowScale);
} else {
player.shadow.visible = false;
}
}
updateCamera(pos, orthoUp, playerForward, visualZ, isOutside, basis);
updateLockSights();
}
function updateCamera(playerPos, playerUp, playerForward, visualZ, isOutside, playerBasis) {
const shake = (player.mode === "RUSHING") ? (Math.random()-0.5)*1.0 : (Math.random()-0.5)*0.1;
const cameraRoll = player.bank * 0.6;
const cameraUp = playerUp.clone();
cameraUp.applyAxisAngle(playerForward, cameraRoll);
if (viewMode === "F1" || viewMode === "F4") {
const eyePos = playerPos.clone().addScaledVector(playerForward, 2.0).addScaledVector(playerUp, 0.5);
camera.position.copy(eyePos);
camera.position.y += shake;
camera.up.copy(cameraUp);
const target = eyePos.clone().add(playerForward);
camera.lookAt(target);
}
else if (viewMode === "F2") {
const camOffsetZ = -60;
const camOffsetY = 50;
const camPos = new THREE.Vector3(
playerPos.x,
playerPos.y + camOffsetY,
player.z + camOffsetZ
);
camera.position.copy(camPos);
camera.up.set(0, 1, 0);
camera.lookAt(playerPos);
}
else {
// 3RD PERSON (F3, F5) - Fixed relative parallel camera
// Defined relative offset (Back, Up)
const dist = 30;
const height = 8;
// Calculate camera position strictly relative to player orientation
// This ensures "Fixed at front of screen" behavior
const camPos = playerPos.clone()
.addScaledVector(playerForward, -dist)
.addScaledVector(playerUp, height);
// Add Shake
const right = new THREE.Vector3().crossVectors(playerForward, playerUp).normalize();
camPos.addScaledVector(right, shake);
camera.position.copy(camPos);
// Set Up vector (Synchronized with player's tile surface normal + bank)
camera.up.copy(cameraUp);
// Look At: Project forward parallel to player
// Looking at "camPos + playerForward" ensures the view vector is parallel to playerForward
camera.lookAt(camPos.clone().add(playerForward));
}
}
function updateObjects(dt) {
let speed = (player.mode === "RUSHING") ? RUSH_SPEED : player.vz;
let spawnDist = (150 - (currentStage * 10.0)) + (speed * 0.5);
const spawnHorizon = player.z + 1500;
if (spawnHorizon > nextSpawnZ) {
spawnObject(nextSpawnZ);
nextSpawnZ += Math.random() * 50 + spawnDist;
}
for (let i = worldObjects.length - 1; i >= 0; i--) {
const obj = worldObjects[i];
if(obj.flying) {
obj.localPos.addScaledVector(obj.vel, dt * 60);
obj.mesh.rotation.x += 10 * dt;
obj.mesh.rotation.y += 10 * dt;
}
const objVisualZ = obj.z;
const basis = getBasis(objVisualZ, viewMode);
let worldPos;
if (obj.flying) {
worldPos = basis.origin.clone().add(obj.localPos);
} else {
const isOutside = (viewMode === "F4" || viewMode === "F5");
worldPos = getSectionPosition(obj.angle, obj.r, basis, viewMode);
const forward = basis.T.clone();
let up;
if (viewMode === "F2") {
const right = new THREE.Vector3(1, 0, 0);
up = new THREE.Vector3().crossVectors(right, forward).normalize();
} else {
const objLocalX = Math.sin(obj.angle);
const objLocalY = -Math.cos(obj.angle);
const normal = new THREE.Vector3()
.addScaledVector(basis.R, objLocalX)
.addScaledVector(basis.U, objLocalY)
.normalize();
up = isOutside ? normal : normal.clone().multiplyScalar(-1);
}
const right = new THREE.Vector3().crossVectors(up, forward).normalize();
const parallelForward = new THREE.Vector3().crossVectors(right, up).normalize();
const m = new THREE.Matrix4();
m.makeBasis(right, up, parallelForward);
obj.mesh.rotation.setFromRotationMatrix(m);
}
obj.mesh.position.copy(worldPos);
if (player.mode !== "RUSHING" && !obj.flying) {
const dz = Math.abs(player.z - obj.z);
let collisionThreshold = 10.0 + (player.vz * 0.02);
if (dz < collisionThreshold) {
const dist = player.mesh.position.distanceTo(obj.mesh.position);
let distThreshold = 8.0;
if (dist < distThreshold) {
handleCollision(obj, i);
if(!obj.flying && obj.type !== "block" && obj.type !== "hurdle") {
scene.remove(obj.mesh);
if(obj.sightDom) obj.sightDom.remove();
worldObjects.splice(i, 1);
continue;
}
}
}
}
if (obj.z < player.z - 100) {
scene.remove(obj.mesh);
if(obj.sightDom) obj.sightDom.remove();
worldObjects.splice(i, 1);
}
}
}
function spawnObject(z) {
let hurdleRate = Math.min(0.4, 0.1 + (currentStage * 0.04));
let type = (Math.random() < 0.1) ? "score" : (Math.random() < 0.05) ? "heal" : "block";
if (Math.random() < hurdleRate) type = "hurdle";
const seg = Math.floor(Math.random() * TILES_PER_RING);
let tubeAngle = (seg / TILES_PER_RING) * Math.PI * 2 - Math.PI;
let length = (type === 'block') ? 3 : 1;
for(let i = 0; i < length; i++) {
let zOffset = z + (i * 12.0);
let stackHeight = (type === 'block') ? Math.floor(Math.random() * 2) + 1 : 1;
for (let k = 0; k < stackHeight; k++) {
let geo;
let blockSize = 2.5;
if (type === 'hurdle') { geo = new THREE.BoxGeometry(4,4,4); blockSize=4.0; }
else if (type === 'heal') { geo = new THREE.BoxGeometry(2,2,2); blockSize=2.0; }
else { geo = new THREE.BoxGeometry(3,3,3); blockSize=3.0; }
let color;
if (type === 'block') {
const gVal = 0.3 + Math.random() * 0.5;
color = new THREE.Color(gVal, gVal, gVal);
} else {
const colors = { hurdle: 0xff3300, score: 0xffff00, heal: 0x00ff00 };
color = new THREE.Color(colors[type]);
}
const mesh = new THREE.Mesh(geo, new THREE.MeshPhongMaterial({ color: color }));
let baseR = TUBE_R;
const isOutside = (viewMode === "F4" || viewMode === "F5");
const offset = (type === 'block') ? (1.5 + k * 3.0) : 1.5;
if (isOutside) baseR += offset;
else baseR -= offset;
const lx = Math.sin(tubeAngle) * baseR;
const ly = -Math.cos(tubeAngle) * baseR;
scene.add(mesh);
worldObjects.push({
mesh, type, flying: false,
z: zOffset,
angle: tubeAngle,
r: baseR,
localPos: new THREE.Vector3(lx, ly, 0),
vel: new THREE.Vector3(),
locked: false,
sightDom: null
});
}
}
}
function handleCollision(obj, index, forcedKill = false) {
const isHighSpeed = player.vz >= 350.0;
if (obj.type === "block" || obj.type === "hurdle") {
if (forcedKill) {
sound.play('crash');
createDebris(obj.mesh.position, obj.mesh.material.color, 2.0, 15, "explode");
if (obj.type === "hurdle") {
hp -= 1;
showPopText("BREAK! -1HP", "#ff3300");
} else {
const pts = 50; score += pts; stageScore += pts;
showPopText("SMASH! +50", "#ffaa00");
}
scene.remove(obj.mesh);
if(obj.sightDom) obj.sightDom.remove();
worldObjects.splice(index, 1);
} else {
if (obj.type === "hurdle") {
hp -= 2;
showPopText("CRASH! -2HP", "#ff0000");
sound.play('crash');
createDebris(obj.mesh.position, obj.mesh.material.color, 1.0, 10, "explode");
obj.flying = true;
obj.vel.set((Math.random()-0.5)*2, 5, 8);
} else {
const pts = 10; score += pts; stageScore += pts;
showPopText("HIT! +10", "#ffffff");
sound.play('crash');
createDebris(obj.mesh.position, obj.mesh.material.color, 1.0, 5, "explode");
obj.flying = true;
obj.vel.set((Math.random()-0.5)*4, 10, 15);
}
if(obj.sightDom) { obj.sightDom.remove(); obj.sightDom = null; }
}
}
else if (obj.type === "score") {
sound.play('coin');
const pts = 100; score += pts; stageScore += pts;
showPopText("+100", "#ffff00");
scene.remove(obj.mesh);
if(obj.sightDom) obj.sightDom.remove();
worldObjects.splice(index, 1);
}
else if (obj.type === "heal") {
sound.play('heal');
hp = Math.min(MAX_HP, hp + 1); showPopText("RECOVER!", "#00ff00");
scene.remove(obj.mesh);
if(obj.sightDom) obj.sightDom.remove();
worldObjects.splice(index, 1);
}
}
function createDebris(pos, color, size, count, type) {
for(let i=0; i<count; i++) {
const s = (type.includes("spark")) ? size * (Math.random()*0.5 + 0.5) : size;
const mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 1.0 });
const geo = new THREE.BoxGeometry(s, s, s);
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(pos).add(new THREE.Vector3((Math.random()-0.5)*2, (Math.random()-0.5)*2, (Math.random()-0.5)*2));
scene.add(mesh);
let vel, life;
if (type === "spark") {
vel = new THREE.Vector3((Math.random()-0.5)*5, (Math.random())*5, (Math.random()-0.5)*5);
life = 0.5;
} else if (type === "spark_brake") {
vel = new THREE.Vector3((Math.random()-0.5)*10, 5 + Math.random()*10, 5 + Math.random()*10);
life = 0.3;
} else {
vel = new THREE.Vector3((Math.random()-0.5)*30, (Math.random()-0.5)*30, (Math.random()-0.5)*30);
life = 1.0;
}
debris.push({ mesh, vel, life, type });
}
}
function updateDebris(dt) {
for(let i = debris.length - 1; i >= 0; i--) {
const d = debris[i];
d.mesh.position.addScaledVector(d.vel, dt * 20);
d.life -= dt;
if (d.type.includes("spark")) d.mesh.rotation.z += 10 * dt;
else d.mesh.rotation.x += 5 * dt;
if(d.life <= 0) {
scene.remove(d.mesh);
debris.splice(i, 1);
}
}
}
function showPopText(text, color) {
const div = document.createElement('div');
div.className = 'pop-text';
div.style.color = color;
div.innerText = text;
div.style.left = "50%"; div.style.top = "40%";
document.getElementById('ui-layer').appendChild(div);
setTimeout(() => div.remove(), 800);
}
function updateUI() {
document.getElementById('score-info').innerText = "SCORE: " + score;
document.getElementById('hp-fill').style.width = Math.max(0, (hp / MAX_HP * 100)) + "%";
const val = (player.mode === "RUSHING") ? RUSH_SPEED : player.vz;
const spd = (val * SPEED_DISPLAY_MULTIPLIER).toFixed(0);
document.getElementById('speed-val').innerText = spd;
const speedRatio = val / (player.mode==="RUSHING" ? RUSH_SPEED : NORMAL_MAX_SPEED);
document.getElementById('speed-fill').style.width = Math.min(100, speedRatio * 100) + "%";
if (player.mode === "RUSHING") document.getElementById('speed-fill').style.backgroundColor = "#ff00ff";
else document.getElementById('speed-fill').style.backgroundColor = "#00ff88";
const remainingTime = Math.max(0, timeLeft);
const minutes = Math.floor(remainingTime / 60);
const seconds = Math.floor(remainingTime % 60);
const timeStr = minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
document.getElementById('time-num').innerText = timeStr;
document.getElementById('time-fill').style.width = (timeLeft / MAX_TIME * 100) + "%";
}
init();
</script>
</body>
</html>・ダウンロードされる方はこちら。↓
【操作説明】
・左右キー … 自機を左右に移動。
・上キー … アクセル。
・下キー … ブレーキ。
・Zキー … キー押下で前方のオブジェクトをロックオン、
その状態でキー押上するとブースト。(※ver.6.0)
ver6.1では、ロックオンは自動化され、
Zキー押下中はブースト。
・Xキー … ジャンプ。
・F1キー … チューブ内側モードの一人称視点に切り替え。
・F2キー … 平面モードに切り替え。
・F3キー … チューブ内側モードの三人称視点に切り替え。
・F4キー … チューブ外側モードの一人称視点に切り替え。
・F5キー … チューブ外側モードの三人称視点に切り替え。
・Escキー … ゲームを一時停止、ゲームを再開。
・障害物を避けて、高得点を目指しましょう。
・灰色ブロック … バリアで破壊可能。+10点。
・赤色ブロック … バリアでは防げず、HPにダメージ。
・黄緑色ブロック … HPを回復。
・黄色ブロック … +100点。
・HPが減るとバリアが小さくなる。
・HPがゼロになるとゲームオーバー。
【おまけ】
・チューブ系のゲームを検索してみたところ、
やはりいくつかあったので紹介しておきます。
1981 Atari Tempest Arcade
・これはレースゲームというか、シューティングみたいな感じですね。
1983 ジャイラス コナミ
・これもシューティングゲームで、やや滑らかに回る感じ。
Bullfrog - Tube - 1995
・かなりレースゲームっぽい感じになってきました。
Ballistics (2001) - PC
これは障害物を避けていくパターンで、一応、レースゲームなのかな?
Tube Slider (2003) GameCube
・これは任天堂のレースゲームで、
「F-ZERO」の後継シリーズとして開発されていたとのこと。
・一回転できる区間がない感じだけど、遊んだこと無いので不明。
Torus Trooper(2004)長健太
・これはシューティングゲームだけど、
一回転できる区間があるし、雰囲気的には一番似ているかも。
・Zキーでショット発射。Xキーで貯め撃ち。
・敵弾は、避けやすいけど、たまに当たる。難易度はやや高い。
・2004年11月にver.0.1が公開。2005年のver.0.22以降は更新無し。
Space Giraffe(2007)Llamasoft
・これはまたシューティングゲームで、テンペストっぽい感じ。
「Race The Sun」Flippfly (2013)
・これはチューブ系ではないものの、平面モードはわりと近いかも。
「Thumper」Drool(2016)
・これはリズム系のゲームで、
タイミングを合わせると障害物を回避できる。
「Tunnel Rush」(2024?)
・これも障害物を避けまくるゲーム。
「Redout」(2016)34BigThings
「Redout 2」(2022)34BigThings
・これはレースゲームですが、
ブーストが付いていて1000km/hまで加速可能。
とまあ、似たような雰囲気のゲームは
これまでにもあったみたいなんですが、
ぶっ壊しに主軸をおいたゲームでは無かったみたいなので、
まあまあセーフだったりするのかも知れない。(^^;
3Dのこういう真正面を向いたゲームは、
合わせるのが難しいので、
気軽に遊べるのがいいのではないだろうか。uu



コメント