899 lines
33 KiB
HTML
Executable File
899 lines
33 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Trench Run: Final Mission</title>
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
background-color: #000;
|
|
font-family: 'Courier New', Courier, monospace;
|
|
user-select: none;
|
|
}
|
|
|
|
#game-container {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
#ui-layer {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 20px;
|
|
color: #0ff;
|
|
font-size: 20px;
|
|
z-index: 10;
|
|
text-shadow: 0 0 5px #0ff;
|
|
pointer-events: none;
|
|
width: 95%;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.hud-column {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.shield-bar-container {
|
|
margin-top: 5px;
|
|
width: 200px;
|
|
height: 15px;
|
|
border: 2px solid #00ffff;
|
|
background: rgba(0, 50, 50, 0.5);
|
|
}
|
|
|
|
#shield-bar {
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: #00ffff;
|
|
transition: width 0.1s;
|
|
}
|
|
|
|
#distance-display {
|
|
color: #ffcc00;
|
|
text-align: right;
|
|
}
|
|
|
|
#center-message {
|
|
position: absolute;
|
|
top: 30%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
font-size: 30px;
|
|
color: #ff0000;
|
|
text-shadow: 0 0 10px red;
|
|
text-align: center;
|
|
opacity: 0;
|
|
transition: opacity 0.5s;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#damage-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: radial-gradient(circle, transparent 50%, rgba(255,0,0,0.5) 100%);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.2s;
|
|
z-index: 5;
|
|
}
|
|
|
|
#target-reticle {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 200px;
|
|
height: 200px;
|
|
border: 2px dashed rgba(255, 0, 0, 0.5);
|
|
border-radius: 50%;
|
|
display: none;
|
|
pointer-events: none;
|
|
box-shadow: 0 0 20px rgba(255, 0, 0, 0.2);
|
|
transition: border 0.1s, background 0.1s;
|
|
}
|
|
|
|
.locked-on {
|
|
border: 4px solid #ff0000 !important;
|
|
background: rgba(255, 0, 0, 0.2);
|
|
box-shadow: 0 0 50px #ff0000 !important;
|
|
}
|
|
|
|
#start-screen, #game-over-screen, #win-screen {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
color: white;
|
|
z-index: 20;
|
|
cursor: pointer;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 40px;
|
|
color: #ffcc00;
|
|
text-shadow: 0 0 10px #ffaa00;
|
|
margin-bottom: 10px;
|
|
text-transform: uppercase;
|
|
text-align: center;
|
|
}
|
|
|
|
.instruction { color: #00ffff; font-weight: bold; }
|
|
.warning { color: #ff0000; font-weight: bold; animation: blink 0.5s infinite; }
|
|
@keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
|
|
.hidden { display: none !important; }
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="damage-overlay"></div>
|
|
|
|
<!-- UI Overlay -->
|
|
<div id="ui-layer">
|
|
<div class="hud-column">
|
|
<div>SCORE: <span id="score-display">0</span></div>
|
|
<div class="shield-bar-container">
|
|
<div id="shield-bar"></div>
|
|
</div>
|
|
<div style="font-size: 14px;">SCHILD (Blockt Laser)</div>
|
|
</div>
|
|
<div class="hud-column">
|
|
<div id="distance-display">DISTANZ: <span id="dist-val">5000</span></div>
|
|
<div id="mode-display" style="color: #0f0; font-size: 16px;">MODUS: ANFLUG</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="center-message">ACHTUNG: TRENCH RUN GESTARTET</div>
|
|
<div id="target-reticle">
|
|
<div style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); color:red; font-size:12px;">ZIELCOMPUTER</div>
|
|
</div>
|
|
|
|
<!-- Start Screen -->
|
|
<div id="start-screen">
|
|
<h1>Star Mission</h1>
|
|
<p>1. Weiche Lasern aus (1 Leben)</p>
|
|
<p>2. Schild blockt Laser!</p>
|
|
<p>3. <span class="warning">LEERTASTE</span> = Schild / Torpedo</p>
|
|
<br>
|
|
<p class="instruction">Klicken zum Starten</p>
|
|
</div>
|
|
|
|
<!-- Game Over Screen -->
|
|
<div id="game-over-screen" class="hidden">
|
|
<h1>Zerstört</h1>
|
|
<p>Der Todesstern hat gewonnen.</p>
|
|
<p>Klicken für Neustart</p>
|
|
</div>
|
|
|
|
<!-- Win Screen -->
|
|
<div id="win-screen" class="hidden">
|
|
<h1 style="color: #00ff00; text-shadow: 0 0 20px #00ff00;">SIEG!</h1>
|
|
<p>Du hast die Galaxis gerettet.</p>
|
|
<p>Klicken um erneut zu spielen</p>
|
|
</div>
|
|
|
|
<div id="game-container"></div>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
|
|
<script>
|
|
// --- Audio System ---
|
|
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
|
const Sound = {
|
|
playTone: (freq, type, duration, vol = 0.1) => {
|
|
if(audioCtx.state === 'suspended') audioCtx.resume();
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
osc.type = type;
|
|
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
|
|
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration);
|
|
osc.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
osc.start();
|
|
osc.stop(audioCtx.currentTime + duration);
|
|
},
|
|
playExplosion: () => { Sound.playTone(80, 'sawtooth', 0.8, 0.3); },
|
|
playShieldBlock: () => { Sound.playTone(800, 'sine', 0.1, 0.2); },
|
|
playLaser: () => { Sound.playTone(600, 'sawtooth', 0.1, 0.05); },
|
|
playLockOn: () => { Sound.playTone(1200, 'square', 0.1, 0.05); },
|
|
playWin: () => {
|
|
let now = audioCtx.currentTime;
|
|
[440, 554, 659, 880].forEach((freq, i) => {
|
|
const osc = audioCtx.createOscillator();
|
|
const gain = audioCtx.createGain();
|
|
osc.frequency.value = freq;
|
|
gain.gain.setValueAtTime(0.1, now + i*0.2);
|
|
gain.gain.exponentialRampToValueAtTime(0.01, now + i*0.2 + 1);
|
|
osc.connect(gain);
|
|
gain.connect(audioCtx.destination);
|
|
osc.start(now + i*0.2);
|
|
osc.stop(now + i*0.2 + 1);
|
|
});
|
|
}
|
|
};
|
|
|
|
// --- Game State ---
|
|
const GameState = {
|
|
APPROACH: 'APPROACH',
|
|
TRANSITION: 'TRANSITION',
|
|
TRENCH: 'TRENCH',
|
|
BOMB_RUN: 'BOMB_RUN',
|
|
WIN: 'WIN',
|
|
GAMEOVER: 'GAMEOVER'
|
|
};
|
|
|
|
let currentState = GameState.APPROACH;
|
|
let distanceToDeathStar = 5000;
|
|
const TRENCH_START_DIST = 1000;
|
|
const BOMB_DIST = 200;
|
|
|
|
// --- Setup ---
|
|
let scene, camera, renderer;
|
|
let playerGroup, playerShield;
|
|
let deathStar, deathStarLaserBeam;
|
|
let trenchGroup, floorMesh, wallL, wallR, exhaustPort;
|
|
let obstacles = [];
|
|
let backgroundShips = [];
|
|
let tieMasterMesh;
|
|
|
|
let score = 0;
|
|
let speed = 1.5;
|
|
let animationId;
|
|
|
|
let mouse = { x: 0, y: 0 };
|
|
const targetPlayerPos = { x: 0, y: 0 };
|
|
|
|
let isShieldActive = false;
|
|
let shieldEnergy = 100;
|
|
let bombReady = false;
|
|
|
|
let laserState = 'IDLE';
|
|
let laserTimer = 0;
|
|
|
|
// UI
|
|
const scoreEl = document.getElementById('score-display');
|
|
const distEl = document.getElementById('dist-val');
|
|
const modeEl = document.getElementById('mode-display');
|
|
const centerMsg = document.getElementById('center-message');
|
|
const reticle = document.getElementById('target-reticle');
|
|
const shieldBar = document.getElementById('shield-bar');
|
|
const damageOverlay = document.getElementById('damage-overlay');
|
|
|
|
// Screens
|
|
const startScreen = document.getElementById('start-screen');
|
|
const gameOverScreen = document.getElementById('game-over-screen');
|
|
const winScreen = document.getElementById('win-screen');
|
|
|
|
function init() {
|
|
scene = new THREE.Scene();
|
|
scene.fog = new THREE.FogExp2(0x000000, 0.002);
|
|
|
|
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
|
|
camera.position.z = 8;
|
|
camera.position.y = 2;
|
|
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
document.getElementById('game-container').appendChild(renderer.domElement);
|
|
|
|
const ambLight = new THREE.AmbientLight(0xffffff, 0.3);
|
|
scene.add(ambLight);
|
|
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
|
|
dirLight.position.set(-10, 20, 10);
|
|
scene.add(dirLight);
|
|
|
|
createTieMasterMesh();
|
|
createXWing();
|
|
createDeathStar();
|
|
createTrenchEnvironment();
|
|
createStarfield();
|
|
createLaserBeam();
|
|
|
|
document.addEventListener('mousemove', onMouseMove);
|
|
document.addEventListener('mousedown', () => isShieldActive = true);
|
|
document.addEventListener('mouseup', () => isShieldActive = false);
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if(e.code === 'Space') {
|
|
if (bombReady && currentState === GameState.BOMB_RUN) {
|
|
fireProtonTorpedo();
|
|
} else {
|
|
isShieldActive = true;
|
|
}
|
|
}
|
|
});
|
|
document.addEventListener('keyup', (e) => { if(e.code === 'Space') isShieldActive = false; });
|
|
|
|
window.addEventListener('resize', onWindowResize);
|
|
startScreen.addEventListener('click', startGame);
|
|
gameOverScreen.addEventListener('click', resetGame);
|
|
winScreen.addEventListener('click', resetGame);
|
|
}
|
|
|
|
function triggerDamageEffect() {
|
|
damageOverlay.style.opacity = 1;
|
|
setTimeout(() => { damageOverlay.style.opacity = 0; }, 200);
|
|
camera.position.x += (Math.random()-0.5) * 1;
|
|
camera.position.y += (Math.random()-0.5) * 1;
|
|
}
|
|
|
|
// --- Model Creation ---
|
|
|
|
function generateDeathStarTexture() {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 1024; canvas.height = 512;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#666'; ctx.fillRect(0,0,1024,512);
|
|
// Mehr Details
|
|
for(let i=0; i<3000; i++) {
|
|
const c = Math.floor(Math.random() * 50 + 80);
|
|
ctx.fillStyle = `rgb(${c},${c},${c})`;
|
|
ctx.fillRect(Math.random()*1024, Math.random()*512, Math.random()*4, Math.random()*4);
|
|
}
|
|
// Äquator
|
|
ctx.fillStyle = '#111'; ctx.fillRect(0, 250, 1024, 12);
|
|
|
|
return new THREE.CanvasTexture(canvas);
|
|
}
|
|
|
|
function createDeathStar() {
|
|
const geo = new THREE.SphereGeometry(100, 64, 64);
|
|
const mat = new THREE.MeshPhongMaterial({
|
|
map: generateDeathStarTexture(),
|
|
color: 0x999999,
|
|
specular: 0x111111
|
|
});
|
|
deathStar = new THREE.Mesh(geo, mat);
|
|
deathStar.position.set(0, 0, -1000);
|
|
|
|
// Superlaser Dish (Detailierter)
|
|
const dishGeo = new THREE.SphereGeometry(25, 32, 32);
|
|
const dishMat = new THREE.MeshPhongMaterial({ color: 0x555555 });
|
|
const dish = new THREE.Mesh(dishGeo, dishMat);
|
|
dish.position.set(30, 40, 75);
|
|
dish.lookAt(0,0,-1000); // Zur Mitte des Todessterns
|
|
dish.scale.z = 0.2; // Eindrücken
|
|
deathStar.add(dish);
|
|
|
|
scene.add(deathStar);
|
|
}
|
|
|
|
function createTrenchEnvironment() {
|
|
trenchGroup = new THREE.Group();
|
|
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 512; canvas.height = 512;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.fillStyle = '#444'; ctx.fillRect(0,0,512,512);
|
|
ctx.strokeStyle = '#222'; ctx.lineWidth = 4;
|
|
// Tech Grid
|
|
for(let i=0; i<512; i+=64) {
|
|
ctx.strokeRect(i, 0, 64, 512);
|
|
ctx.strokeRect(0, i, 512, 64);
|
|
}
|
|
// Pipes and Greebles
|
|
for(let i=0; i<50; i++) {
|
|
ctx.fillStyle = '#555';
|
|
ctx.fillRect(Math.random()*500, Math.random()*500, Math.random()*40+10, Math.random()*40+10);
|
|
}
|
|
|
|
const trenchTex = new THREE.CanvasTexture(canvas);
|
|
trenchTex.wrapS = THREE.RepeatWrapping;
|
|
trenchTex.wrapT = THREE.RepeatWrapping;
|
|
trenchTex.repeat.set(5, 20);
|
|
|
|
const mat = new THREE.MeshPhongMaterial({ map: trenchTex, side: THREE.DoubleSide });
|
|
|
|
floorMesh = new THREE.Mesh(new THREE.PlaneGeometry(100, 2000), mat);
|
|
floorMesh.rotation.x = -Math.PI/2;
|
|
floorMesh.position.y = -5;
|
|
floorMesh.position.z = -500;
|
|
trenchGroup.add(floorMesh);
|
|
|
|
wallL = new THREE.Mesh(new THREE.PlaneGeometry(2000, 50), mat);
|
|
wallL.rotation.y = Math.PI/2;
|
|
wallL.position.set(-20, 10, -500);
|
|
trenchGroup.add(wallL);
|
|
|
|
wallR = new THREE.Mesh(new THREE.PlaneGeometry(2000, 50), mat);
|
|
wallR.rotation.y = -Math.PI/2;
|
|
wallR.position.set(20, 10, -500);
|
|
trenchGroup.add(wallR);
|
|
|
|
const portGeo = new THREE.RingGeometry(2, 4, 32);
|
|
const portMat = new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.DoubleSide });
|
|
exhaustPort = new THREE.Mesh(portGeo, portMat);
|
|
exhaustPort.rotation.x = -Math.PI/2;
|
|
exhaustPort.position.set(0, -4.8, -100);
|
|
exhaustPort.visible = false;
|
|
trenchGroup.add(exhaustPort);
|
|
|
|
trenchGroup.visible = false;
|
|
scene.add(trenchGroup);
|
|
}
|
|
|
|
function createTieMasterMesh() {
|
|
const tieGroup = new THREE.Group();
|
|
const greyMat = new THREE.MeshPhongMaterial({ color: 0x999999 });
|
|
const blackMat = new THREE.MeshPhongMaterial({ color: 0x111111 });
|
|
|
|
const cockpit = new THREE.Mesh(new THREE.SphereGeometry(0.8, 16, 16), greyMat);
|
|
tieGroup.add(cockpit);
|
|
|
|
const windowFrame = new THREE.Mesh(new THREE.TorusGeometry(0.4, 0.05, 8, 8), greyMat);
|
|
windowFrame.position.z = 0.75;
|
|
tieGroup.add(windowFrame);
|
|
|
|
const wingGeo = new THREE.CylinderGeometry(2.2, 2.2, 0.1, 6);
|
|
|
|
const wingL = new THREE.Mesh(wingGeo, blackMat);
|
|
wingL.rotation.x = Math.PI / 2;
|
|
wingL.rotation.z = Math.PI / 2;
|
|
wingL.position.x = -2;
|
|
|
|
const pylonGeo = new THREE.CylinderGeometry(0.3, 0.2, 1.2);
|
|
const pylonL = new THREE.Mesh(pylonGeo, greyMat);
|
|
pylonL.rotation.z = Math.PI/2;
|
|
pylonL.position.x = -1.0;
|
|
tieGroup.add(pylonL);
|
|
|
|
const pylonR = pylonL.clone();
|
|
pylonR.position.x = 1.0;
|
|
tieGroup.add(pylonR);
|
|
|
|
const wingR = wingL.clone();
|
|
wingR.position.x = 2;
|
|
|
|
tieGroup.add(wingL);
|
|
tieGroup.add(wingR);
|
|
|
|
tieMasterMesh = tieGroup;
|
|
}
|
|
|
|
function createXWing() {
|
|
playerGroup = new THREE.Group();
|
|
const bodyMat = new THREE.MeshPhongMaterial({ color: 0xeeeeee, flatShading: false });
|
|
const darkMat = new THREE.MeshPhongMaterial({ color: 0x333333 });
|
|
const redMat = new THREE.MeshPhongMaterial({ color: 0xaa0000 });
|
|
const engineGlow = new THREE.MeshBasicMaterial({ color: 0xff6600 });
|
|
|
|
const fuselage = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.6, 4.5), bodyMat);
|
|
playerGroup.add(fuselage);
|
|
|
|
const nose = new THREE.Mesh(new THREE.ConeGeometry(0.3, 2.5, 8), bodyMat);
|
|
nose.rotation.x = -Math.PI / 2;
|
|
nose.position.z = -3.5;
|
|
playerGroup.add(nose);
|
|
|
|
const cockpit = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.4, 1.2), darkMat);
|
|
cockpit.position.set(0, 0.4, -0.5);
|
|
playerGroup.add(cockpit);
|
|
|
|
const droid = new THREE.Mesh(new THREE.SphereGeometry(0.25, 8, 8), new THREE.MeshPhongMaterial({color:0x0000ff}));
|
|
droid.position.set(0, 0.4, 0.5);
|
|
playerGroup.add(droid);
|
|
|
|
const wingGeo = new THREE.BoxGeometry(2.0, 0.1, 1.2);
|
|
const engineGeo = new THREE.CylinderGeometry(0.25, 0.25, 1.5, 8);
|
|
|
|
const wings = [
|
|
{ x:1, y:0.2, rot: 0.3 },
|
|
{ x:-1, y:0.2, rot: -0.3 },
|
|
{ x:1, y:-0.2, rot: -0.3 },
|
|
{ x:-1, y:-0.2, rot: 0.3 }
|
|
];
|
|
|
|
wings.forEach(w => {
|
|
const wingContainer = new THREE.Group();
|
|
const blade = new THREE.Mesh(wingGeo, bodyMat);
|
|
blade.position.x = w.x > 0 ? 1.0 : -1.0;
|
|
wingContainer.add(blade);
|
|
|
|
const engine = new THREE.Mesh(engineGeo, bodyMat);
|
|
engine.rotation.x = Math.PI/2;
|
|
engine.position.set(w.x > 0 ? 0.5 : -0.5, 0, 0.5);
|
|
wingContainer.add(engine);
|
|
|
|
const glow = new THREE.Mesh(new THREE.CircleGeometry(0.2, 8), engineGlow);
|
|
glow.position.set(w.x > 0 ? 0.5 : -0.5, 0, 1.26);
|
|
wingContainer.add(glow);
|
|
|
|
const gun = new THREE.Mesh(new THREE.CylinderGeometry(0.05, 0.05, 3), bodyMat);
|
|
gun.rotation.x = Math.PI/2;
|
|
gun.position.set(w.x > 0 ? 2.0 : -2.0, 0, -1.0);
|
|
wingContainer.add(gun);
|
|
|
|
const stripe = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.11, 0.4), redMat);
|
|
stripe.position.set(w.x > 0 ? 1.0 : -1.0, 0, 0);
|
|
wingContainer.add(stripe);
|
|
|
|
wingContainer.rotation.z = w.rot;
|
|
playerGroup.add(wingContainer);
|
|
});
|
|
|
|
const shieldGeo = new THREE.SphereGeometry(3.5, 16, 16);
|
|
const shieldMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0 });
|
|
playerShield = new THREE.Mesh(shieldGeo, shieldMat);
|
|
playerGroup.add(playerShield);
|
|
|
|
playerGroup.scale.set(0.6, 0.6, 0.6);
|
|
scene.add(playerGroup);
|
|
}
|
|
|
|
function createLaserBeam() {
|
|
const geo = new THREE.CylinderGeometry(4, 8, 400, 16, 1, true);
|
|
const mat = new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0, blending: THREE.AdditiveBlending, side: THREE.DoubleSide });
|
|
deathStarLaserBeam = new THREE.Mesh(geo, mat);
|
|
deathStarLaserBeam.rotation.x = Math.PI / 2;
|
|
deathStarLaserBeam.visible = false;
|
|
scene.add(deathStarLaserBeam);
|
|
}
|
|
|
|
function createStarfield() {
|
|
const geometry = new THREE.BufferGeometry();
|
|
const vertices = [];
|
|
for (let i = 0; i < 2000; i++) vertices.push((Math.random()-0.5)*200, (Math.random()-0.5)*200, (Math.random()-0.5)*400-100);
|
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
|
|
particles = new THREE.Points(geometry, new THREE.PointsMaterial({ color: 0xffffff, size: 0.2 }));
|
|
scene.add(particles);
|
|
}
|
|
|
|
// --- BACKGROUND TIE LOGIC ---
|
|
function createBackgroundTie() {
|
|
if(!tieMasterMesh) return;
|
|
const tie = tieMasterMesh.clone();
|
|
|
|
const angle = Math.random() * Math.PI * 2;
|
|
const radius = 30 + Math.random() * 50;
|
|
|
|
tie.position.x = Math.cos(angle) * radius;
|
|
tie.position.y = Math.sin(angle) * radius;
|
|
tie.position.z = -100 - Math.random() * 200;
|
|
|
|
tie.userData = {
|
|
velX: (Math.random() - 0.5) * 0.4,
|
|
velY: (Math.random() - 0.5) * 0.4,
|
|
velZ: 0.5 + Math.random() * 1.0,
|
|
rotX: (Math.random() - 0.5) * 0.05,
|
|
rotY: (Math.random() - 0.5) * 0.05
|
|
};
|
|
|
|
const scale = 0.5 + Math.random() * 0.5;
|
|
tie.scale.set(scale, scale, scale);
|
|
|
|
scene.add(tie);
|
|
backgroundShips.push(tie);
|
|
}
|
|
|
|
function updateBackgroundShips() {
|
|
if(currentState === GameState.APPROACH && Math.random() < 0.03) {
|
|
createBackgroundTie();
|
|
}
|
|
|
|
for (let i = backgroundShips.length - 1; i >= 0; i--) {
|
|
const ship = backgroundShips[i];
|
|
ship.position.x += ship.userData.velX;
|
|
ship.position.y += ship.userData.velY;
|
|
ship.position.z += ship.userData.velZ;
|
|
|
|
ship.rotation.x += ship.userData.rotX;
|
|
ship.rotation.y += ship.userData.rotY;
|
|
|
|
if (ship.position.z > 20 || Math.abs(ship.position.x) > 200) {
|
|
scene.remove(ship);
|
|
backgroundShips.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
function createLaserObstacle() {
|
|
const isTrench = currentState === GameState.TRENCH || currentState === GameState.BOMB_RUN;
|
|
|
|
const geometry = new THREE.CylinderGeometry(0.15, 0.15, 8, 8);
|
|
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, blending: THREE.AdditiveBlending });
|
|
const laser = new THREE.Mesh(geometry, material);
|
|
laser.rotation.x = Math.PI / 2;
|
|
|
|
if (isTrench) {
|
|
laser.position.x = (Math.random() - 0.5) * 20;
|
|
laser.position.y = (Math.random() * 10) - 4;
|
|
laser.position.z = -200;
|
|
} else {
|
|
laser.position.x = (Math.random() - 0.5) * 35;
|
|
laser.position.y = (Math.random() - 0.5) * 20;
|
|
laser.position.z = -150;
|
|
}
|
|
|
|
scene.add(laser);
|
|
obstacles.push(laser);
|
|
}
|
|
|
|
// --- Game Logic ---
|
|
|
|
function enterTrenchMode() {
|
|
currentState = GameState.TRENCH;
|
|
modeEl.innerText = "MODUS: TRENCH RUN";
|
|
modeEl.style.color = "#ff00ff";
|
|
|
|
deathStar.visible = false;
|
|
trenchGroup.visible = true;
|
|
camera.position.y = 0;
|
|
|
|
centerMsg.innerText = "TRENCH RUN INITIATED";
|
|
centerMsg.style.opacity = 1;
|
|
setTimeout(() => centerMsg.style.opacity = 0, 2000);
|
|
}
|
|
|
|
function startBombRun() {
|
|
currentState = GameState.BOMB_RUN;
|
|
modeEl.innerText = "MODUS: ZIELANFLUG";
|
|
modeEl.style.color = "#ff0000";
|
|
exhaustPort.visible = true;
|
|
exhaustPort.position.z = -300;
|
|
reticle.style.display = 'block';
|
|
centerMsg.innerText = "ZIEL IN REICHWEITE!";
|
|
centerMsg.style.opacity = 1;
|
|
}
|
|
|
|
function fireProtonTorpedo() {
|
|
const torpedo = new THREE.Mesh(new THREE.SphereGeometry(0.5), new THREE.MeshBasicMaterial({color:0xffff00}));
|
|
torpedo.position.copy(playerGroup.position);
|
|
scene.add(torpedo);
|
|
|
|
let t = 0;
|
|
const targetPos = new THREE.Vector3(0, -5, exhaustPort.position.z);
|
|
|
|
const torpedoAnim = setInterval(() => {
|
|
t += 0.1;
|
|
torpedo.position.lerp(targetPos, 0.2);
|
|
if(t > 20 || torpedo.position.distanceTo(targetPos) < 2) {
|
|
clearInterval(torpedoAnim);
|
|
scene.remove(torpedo);
|
|
triggerWin();
|
|
}
|
|
}, 30);
|
|
|
|
currentState = GameState.WIN;
|
|
}
|
|
|
|
function triggerWin() {
|
|
isGameRunning = false;
|
|
Sound.playWin();
|
|
|
|
const expGeo = new THREE.SphereGeometry(1, 32, 32);
|
|
const expMat = new THREE.MeshBasicMaterial({ color: 0xffaa00 });
|
|
const explosion = new THREE.Mesh(expGeo, expMat);
|
|
explosion.position.set(0,0, -50);
|
|
scene.add(explosion);
|
|
|
|
let scale = 1;
|
|
const expInterval = setInterval(() => {
|
|
scale += 2;
|
|
explosion.scale.set(scale, scale, scale);
|
|
explosion.material.opacity -= 0.01;
|
|
if(scale > 200) {
|
|
clearInterval(expInterval);
|
|
winScreen.classList.remove('hidden');
|
|
reticle.style.display = 'none';
|
|
}
|
|
}, 16);
|
|
}
|
|
|
|
function updateGameLogic() {
|
|
if (currentState === GameState.APPROACH) {
|
|
distanceToDeathStar -= speed;
|
|
deathStar.position.z += speed * 0.2;
|
|
|
|
if (distanceToDeathStar <= TRENCH_START_DIST) {
|
|
enterTrenchMode();
|
|
}
|
|
} else if (currentState === GameState.TRENCH) {
|
|
distanceToDeathStar -= speed;
|
|
floorMesh.material.map.offset.y -= 0.01 * speed;
|
|
wallL.material.map.offset.y -= 0.01 * speed;
|
|
wallR.material.map.offset.y -= 0.01 * speed;
|
|
|
|
if (distanceToDeathStar <= BOMB_DIST) {
|
|
startBombRun();
|
|
}
|
|
} else if (currentState === GameState.BOMB_RUN) {
|
|
floorMesh.material.map.offset.y -= 0.01 * speed;
|
|
exhaustPort.position.z += speed;
|
|
|
|
if (exhaustPort.position.z > 10) {
|
|
gameOverScreen.querySelector('h1').innerText = "ZIEL VERFEHLT";
|
|
gameOver();
|
|
}
|
|
|
|
const distZ = Math.abs(exhaustPort.position.z - playerGroup.position.z);
|
|
const distX = Math.abs(playerGroup.position.x);
|
|
|
|
if (distZ < 20 && distX < 4) {
|
|
bombReady = true;
|
|
reticle.classList.add('locked-on');
|
|
exhaustPort.material.color.setHex(0xff0000);
|
|
if(Math.random()<0.2) Sound.playLockOn();
|
|
} else {
|
|
bombReady = false;
|
|
reticle.classList.remove('locked-on');
|
|
exhaustPort.material.color.setHex(0xffff00);
|
|
}
|
|
}
|
|
|
|
distEl.innerText = Math.max(0, Math.floor(distanceToDeathStar));
|
|
}
|
|
|
|
function onMouseMove(event) {
|
|
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
}
|
|
|
|
function updatePlayer() {
|
|
let limitX = 14;
|
|
let limitY = 8;
|
|
|
|
if (currentState === GameState.TRENCH || currentState === GameState.BOMB_RUN) {
|
|
limitX = 9;
|
|
limitY = 4;
|
|
}
|
|
|
|
targetPlayerPos.x = mouse.x * limitX;
|
|
targetPlayerPos.y = mouse.y * limitY;
|
|
|
|
playerGroup.position.x += (targetPlayerPos.x - playerGroup.position.x) * 0.1;
|
|
playerGroup.position.y += (targetPlayerPos.y - playerGroup.position.y) * 0.1;
|
|
|
|
playerGroup.rotation.z = -playerGroup.position.x * 0.05;
|
|
playerGroup.rotation.x = playerGroup.position.y * 0.05;
|
|
|
|
// Shield
|
|
if (isShieldActive && shieldEnergy > 0) {
|
|
shieldEnergy -= 0.5;
|
|
playerShield.material.opacity = 0.4 + Math.random()*0.1;
|
|
} else {
|
|
if (shieldEnergy < 100) shieldEnergy += 0.2;
|
|
playerShield.material.opacity = 0;
|
|
}
|
|
shieldBar.style.width = shieldEnergy + "%";
|
|
|
|
const screenX = 50 + (playerGroup.position.x / 14) * 40;
|
|
const screenY = 50 - (playerGroup.position.y / 8) * 40;
|
|
reticle.style.left = screenX + '%';
|
|
reticle.style.top = screenY + '%';
|
|
}
|
|
|
|
function updateObstacles() {
|
|
let chance = 0.015 + (score * 0.00002);
|
|
if (currentState === GameState.TRENCH) chance = 0.04;
|
|
|
|
if (Math.random() < chance && laserState !== 'FIRING') {
|
|
createLaserObstacle();
|
|
}
|
|
|
|
for (let i = obstacles.length - 1; i >= 0; i--) {
|
|
const obs = obstacles[i];
|
|
obs.position.z += speed * 1.5;
|
|
|
|
const zDiff = Math.abs(playerGroup.position.z - obs.position.z);
|
|
const xyDist = Math.sqrt(Math.pow(playerGroup.position.x - obs.position.x, 2) + Math.pow(playerGroup.position.y - obs.position.y, 2));
|
|
|
|
if (zDiff < 4 && xyDist < 1.2) {
|
|
if (isShieldActive && shieldEnergy > 5) {
|
|
scene.remove(obs);
|
|
obstacles.splice(i, 1);
|
|
shieldEnergy -= 15;
|
|
Sound.playShieldBlock();
|
|
continue;
|
|
} else {
|
|
// GAME OVER BEI TREFFER (Kein Health System mehr)
|
|
triggerDamageEffect();
|
|
gameOver();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (obs.position.z > 10) {
|
|
scene.remove(obs);
|
|
obstacles.splice(i, 1);
|
|
score += 50;
|
|
scoreEl.innerText = score;
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateEnvironment() {
|
|
if (currentState === GameState.APPROACH) {
|
|
const positions = particles.geometry.attributes.position.array;
|
|
for(let i=2; i<positions.length; i+=3) {
|
|
positions[i] += speed * 5;
|
|
if(positions[i] > 50) positions[i] = -300;
|
|
}
|
|
particles.geometry.attributes.position.needsUpdate = true;
|
|
}
|
|
}
|
|
|
|
function animate() {
|
|
if (!isGameRunning && currentState !== GameState.WIN) return;
|
|
animationId = requestAnimationFrame(animate);
|
|
|
|
updatePlayer();
|
|
updateObstacles();
|
|
updateEnvironment();
|
|
updateBackgroundShips();
|
|
updateGameLogic();
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
function startGame() {
|
|
if(audioCtx.state === 'suspended') audioCtx.resume();
|
|
startScreen.classList.add('hidden');
|
|
score = 0;
|
|
distanceToDeathStar = 5000;
|
|
currentState = GameState.APPROACH;
|
|
speed = 1.5;
|
|
|
|
deathStar.visible = true;
|
|
deathStar.position.set(0, 0, -1000);
|
|
trenchGroup.visible = false;
|
|
exhaustPort.visible = false;
|
|
reticle.style.display = 'none';
|
|
centerMsg.style.opacity = 0;
|
|
modeEl.innerText = "MODUS: ANFLUG";
|
|
modeEl.style.color = "#0f0";
|
|
|
|
isGameRunning = true;
|
|
animate();
|
|
}
|
|
|
|
function gameOver() {
|
|
isGameRunning = false;
|
|
cancelAnimationFrame(animationId);
|
|
gameOverScreen.classList.remove('hidden');
|
|
Sound.playExplosion();
|
|
}
|
|
|
|
function resetGame() {
|
|
obstacles.forEach(o => scene.remove(o));
|
|
obstacles = [];
|
|
|
|
backgroundShips.forEach(s => scene.remove(s));
|
|
backgroundShips = [];
|
|
|
|
playerGroup.position.set(0,0,0);
|
|
shieldEnergy = 100;
|
|
gameOverScreen.classList.add('hidden');
|
|
winScreen.classList.add('hidden');
|
|
startGame();
|
|
}
|
|
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth/window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
init();
|
|
|
|
</script>
|
|
</body>
|
|
</html> |