> PROJEKTE_ÜBERSICHT
01
EndoCare
Eine Progressive Web App zur Symptom-Verfolgung mit MongoDB-Anbindung und Kalenderfunktion.
PROJEKT ANSEHEN LIVE DEMO
Journal.js
// journal.js - Logik für Einträge speichern, bearbeiten und Formular zurücksetzen
async function saveEntry() {
const userEmail = sessionStorage.getItem('loggedInUser');
const rawDate = document.getElementById('entryDate').value;
const timeStr = document.getElementById('entryTime').value;
if(!rawDate || !timeStr) return showToast("Bitte Datum und Uhrzeit angeben.");
const entry = {
// WICHTIG: Falls wir bearbeiten, schicken wir die ID mit!
_id: currentEditId,
userEmail: userEmail,
date: rawDate,
type: 'journal',
time: timeStr,
cycleDay: document.getElementById('cycleDay').value,
energie: parseInt(document.getElementById('val_energie').value),
schlaf: parseInt(document.getElementById('val_schlaf').value),
mood: parseInt(document.getElementById('val_mood').value),
aktivitaet: parseInt(document.getElementById('val_aktivitaet').value),
hydration: parseInt(document.getElementById('val_hydration').value),
symptoms: Array.from(document.querySelectorAll('#symptomsGrid input:checked')).map(el => el.value),
customSymptom: document.getElementById('customSymptom').value,
triggers: Array.from(document.querySelectorAll('#triggerGrid input:checked')).map(el => el.value),
customTrigger: document.getElementById('customTrigger').value,
note: document.getElementById('entryNotes').value
};
console.log("Sende Daten an Cloud:", entry); // Zum Testen in der Konsole
try {
const response = await fetch(`${API_URL}/save-entry`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entry)
});
if (response.ok) {
showToast(currentEditId ? "Änderung gespeichert! ✨" : "Neu gespeichert! ✨");
// WICHTIG: Die Liste im Hintergrund aktualisieren
if(typeof loadUserEntries === 'function') {
await loadUserEntries();
}
currentEditId = null;
showSection('calendarView');
if(typeof renderCalendar === 'function') renderCalendar();
} else {
const errorData = await response.json();
console.error("Server-Fehler Details:", errorData);
showToast("Fehler: " + (errorData.error || "Server-Problem"));
}
} catch (err) {
console.error("Netzwerk-Fehler:", err);
saveLocally(userEmail, rawDate, entry);
showToast("Server nicht erreichbar - lokal gespeichert.");
}
}
// Hilfsfunktion für lokales Backup
function saveLocally(userEmail, rawDate, entry) {
let entriesDict = JSON.parse(localStorage.getItem('entriesDict_' + userEmail) || '{}');
if(!entriesDict[rawDate]) entriesDict[rawDate] = [];
entriesDict[rawDate].push(entry);
localStorage.setItem('entriesDict_' + userEmail, JSON.stringify(entriesDict));
if(typeof renderCalendar === 'function') renderCalendar();
}
function resetForm() {
currentEditId = null;
const heute = new Date();
const datumString = heute.toISOString().split('T')[0];
document.getElementById('entryDate').value = datumString;
const zeitString = heute.getHours().toString().padStart(2, '0') + ":" +
heute.getMinutes().toString().padStart(2, '0');
document.getElementById('entryTime').value = zeitString;
document.querySelectorAll('#symptomsGrid input, #triggerGrid input').forEach(cb => cb.checked = false);
['energie', 'schlaf', 'mood', 'aktivitaet', 'hydration'].forEach(id => {
const el = document.getElementById('val_' + id);
if(el) el.value = 3;
});
document.getElementById('cycleDay').value = '';
document.getElementById('customSymptom').value = '';
document.getElementById('customTrigger').value = '';
document.getElementById('entryNotes').value = '';
}
function fillFormWithEntry(dateStr, entry) {
currentEditId = entry.id || entry._id;
showSection('appContent');
document.getElementById('entryDate').value = dateStr;
document.getElementById('entryTime').value = entry.time || "12:00";
document.getElementById('cycleDay').value = entry.cycleDay || '';
document.getElementById('val_energie').value = entry.energie || entry.energyLevel || 3;
document.getElementById('val_schlaf').value = entry.schlaf || entry.sleepLevel || 3;
document.getElementById('val_mood').value = entry.mood || entry.moodLevel || 3;
document.getElementById('val_aktivitaet').value = entry.aktivitaet || 3;
document.getElementById('val_hydration').value = entry.hydration || 3;
document.getElementById('customSymptom').value = entry.customSymptom || '';
document.getElementById('customTrigger').value = entry.customTrigger || '';
document.getElementById('entryNotes').value = entry.notes || entry.note || '';
document.querySelectorAll('#symptomsGrid input, #triggerGrid input').forEach(cb => cb.checked = false);
if(entry.symptoms) {
entry.symptoms.forEach(s => {
const cb = document.querySelector(`#symptomsGrid input[value="${s}"]`);
if(cb) cb.checked = true;
});
}
if(entry.triggers) {
entry.triggers.forEach(t => {
const cb = document.querySelector(`#triggerGrid input[value="${t}"]`);
if(cb) cb.checked = true;
});
}
window.scrollTo(0, 0);
}
02
FpvDroneGame
Ein spannendes FPV-Drohnen-Erlebnis direkt im Browser.
PROJEKT ANSEHEN LIVE DEMO
game.js
/// ACRO MODE FPV DRONE GAME - ACTUAL RATES EDITION
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js";
let scene, camera, renderer, drone, hudCanvas, hudContext;
let gamepadIndex = null;
let city = [], trees = [];
// --- PHYSIK SETUP ---
const clock = new THREE.Clock();
const droneVelocity = new THREE.Vector3();
const droneSpeed = { throttle: 0, yaw: 0, pitch: 0, roll: 0 };
// NEU: Trägheit für die Drehung
let angularVelocity = new THREE.Vector3(0, 0, 0);
const rotationInertia = 0.85; // Dämpfung der Drehung
// --- GLÄTTUNG FÜR MOBILE ---
let smoothPitch = 0;
let smoothRoll = 0;
let smoothYaw = 0;
const SMOOTH_FACTOR = 1;
// --- NEUE ACTUAL RATES KONFIGURATION ---
const RATES = {
centerSens: 250,
maxRate: 700,
expo: 0.70
};
// --- FLIGHT MODE CONFIGURATION ---
let flightMode = 'ACRO'; // ACRO oder ANGLE
const ANGLE_config = {
angleLimit: 60 * Math.PI / 180, // 60° in radians
P_gain: 3.0, // Proportional
I_gain: 0.05, // Integral
D_gain: 0.2 // Derivative
};
// --- PID STATE für Angle Mode ---
let pidPitch = { integral: 0, lastError: 0 };
let pidRoll = { integral: 0, lastError: 0 };
// --- MODE BUTTON DEBOUNCE ---
let modeButtonPressed = false;
let modeButtonLastPressed = 0;
// --- MOBILE LOGIK ---
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const touchState = {
left: { x: 0, y: -1, id: -1 },
right: { x: 0, y: 0, id: -1 },
active: false
};
window.startGame = function() {
init();
if (isTouchDevice) setupTouchControls();
// Global Keyboard Handler für "A" Taste (funktioniert auf allen Geräten)
document.addEventListener('keydown', (e) => {
if (e.key.toLowerCase() === 'a') {
flightMode = flightMode === 'ACRO' ? 'ANGLE' : 'ACRO';
}
});
};
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x050505);
scene.fog = new THREE.Fog(0x050505, 5, 1200);
camera = new THREE.PerspectiveCamera(120, window.innerWidth / window.innerHeight, 0.1, 5000);
const canvas = document.getElementById('gameCanvas');
renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
scene.add(hemiLight);
const sunLight = new THREE.DirectionalLight(0xffffff, 1.2);
sunLight.position.set(1000, 2000, 1000);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 0.5;
sunLight.shadow.camera.far = 5000;
sunLight.shadow.camera.left = -2000;
sunLight.shadow.camera.right = 2000;
sunLight.shadow.camera.top = 2000;
sunLight.shadow.camera.bottom = -2000;
scene.add(sunLight);
const sunGeo = new THREE.SphereGeometry(100, 32, 32);
const sunMat = new THREE.MeshBasicMaterial({ color: 0xfff5e1 });
const sunVisual = new THREE.Mesh(sunGeo, sunMat);
sunVisual.position.copy(sunLight.position);
scene.add(sunVisual);
const glowGeo = new THREE.SphereGeometry(180, 32, 32);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, opacity: 0.25 });
const glow = new THREE.Mesh(glowGeo, glowMat);
sunVisual.add(glow);
const ground = new THREE.Mesh(
new THREE.PlaneGeometry(3000, 3000),
new THREE.MeshStandardMaterial({ color: 0x111111 })
);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
const grid = new THREE.GridHelper(3000, 80, 0x00ff00, 0x222222);
grid.position.y = 0.05;
scene.add(grid);
const droneGeo = new THREE.BoxGeometry(0.25, 0.08, 0.25);
const droneMat = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
drone = new THREE.Mesh(droneGeo, droneMat);
drone.rotation.order = "ZYX";
drone.position.y = 2;
scene.add(drone);
createCity();
createTrees();
hudCanvas = document.createElement('canvas');
hudCanvas.width = window.innerWidth;
hudCanvas.height = window.innerHeight;
hudCanvas.style.position = 'absolute';
hudCanvas.style.top = '0';
hudCanvas.style.left = '0';
hudCanvas.style.pointerEvents = 'none';
hudCanvas.style.zIndex = "10";
document.body.appendChild(hudCanvas);
hudContext = hudCanvas.getContext('2d');
animate();
}
function setupTouchControls() {
const overlay = document.createElement('div');
overlay.style.cssText = "position:absolute; top:0; left:0; width:100%; height:100%; z-index:5; touch-action:none;";
document.body.appendChild(overlay);
// Click-Handler für ganzen Screen - mit Button-Area Exception
document.addEventListener('click', (e) => {
const modeButtonX = window.innerWidth - 120;
const modeButtonY = 30;
const modeButtonWidth = 100;
const modeButtonHeight = 40;
if (e.clientX >= modeButtonX && e.clientX <= modeButtonX + modeButtonWidth &&
e.clientY >= modeButtonY && e.clientY <= modeButtonY + modeButtonHeight) {
flightMode = flightMode === 'ACRO' ? 'ANGLE' : 'ACRO';
}
});
const updateTouches = (e) => {
touchState.active = true;
const touches = e.touches;
const mid = window.innerWidth / 2;
let leftTouchActive = false;
let rightTouchActive = false;
// Wir definieren die Zentren genau dort, wo sie auch gezeichnet werden:
const leftCenter = { x: window.innerWidth * 0.13, y: window.innerHeight * 0.70 };
const rightCenter = { x: window.innerWidth * 0.87, y: window.innerHeight * 0.70 };
const size = 190; // Exakt die Größe aus drawSquareStick
const halfRange = (size / 2) - 15; // Der Bereich, in dem der Stick sich bewegen darf
for (let i = 0; i < touches.length; i++) {
const t = touches[i];
if (t.clientX < mid) {
// Links: Throttle & Yaw
touchState.left.x = Math.max(-1, Math.min(1, (t.clientX - leftCenter.x) / halfRange));
touchState.left.y = Math.max(-1, Math.min(1, (t.clientY - leftCenter.y) / -halfRange));
touchState.left.id = t.identifier;
leftTouchActive = true;
} else {
// Rechts: Pitch & Roll
touchState.right.x = Math.max(-1, Math.min(1, (t.clientX - rightCenter.x) / halfRange));
touchState.right.y = Math.max(-1, Math.min(1, (t.clientY - rightCenter.y) / -halfRange));
touchState.right.id = t.identifier;
rightTouchActive = true;
}
}
if (!leftTouchActive) {
touchState.left.x = 0; // Yaw zurücksetzen
// Throttle lassen wir auf dem aktuellen Wert (y), damit die Drohne nicht abstürzt
touchState.left.id = -1;
}
if (!rightTouchActive) {
touchState.right.x = 0;
touchState.right.y = 0;
touchState.right.id = -1;
}
};
overlay.addEventListener('touchstart', updateTouches);
overlay.addEventListener('touchmove', updateTouches);
overlay.addEventListener('touchend', updateTouches);
overlay.addEventListener('touchcancel', updateTouches);
}
function createCity() {
const winGeo = new THREE.PlaneGeometry(3.0, 3.0);
for (let i = 0; i < 25; i++) {
const w = (Math.random() * 25 + 10) * 4;
const h = (Math.random() * 100 + 50) * 6;
const d = (Math.random() * 25 + 10) * 4;
const gray = Math.random() * 0.1 + 0.05;
const building = new THREE.Mesh(
new THREE.BoxGeometry(w, h, d),
new THREE.MeshStandardMaterial({ color: new THREE.Color(gray, gray, gray) })
);
const range = 1700;
building.position.set((Math.random() - 0.5) * range, h / 2, (Math.random() - 0.5) * range);
building.castShadow = true;
building.receiveShadow = true;
scene.add(building);
city.push(building);
const addWindowsOnFace = (faceWidth, faceHeight, rotY, offX, offZ, isSide) => {
const stepX = 10; const stepY = 16;
const cols = Math.floor(faceWidth / stepX) - 1;
const rows = Math.floor(faceHeight / stepY) - 1;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (Math.random() > 0.7) {
const isLit = Math.random() > 0.4;
const winMat = new THREE.MeshStandardMaterial({
color: isLit ? 0xffcc33 : 0x112233,
emissive: isLit ? 0xffaa00 : 0x000000,
emissiveIntensity: isLit ? 0.5 : 0
});
const win = new THREE.Mesh(winGeo, winMat);
const localX = (c - (cols - 1) / 2) * stepX;
const localY = (r - (rows - 1) / 2) * stepY;
if (!isSide) win.position.set(localX, localY, offZ);
else win.position.set(offX, localY, localX);
win.rotation.y = rotY;
building.add(win);
}
}
}
};
addWindowsOnFace(w, h, 0, 0, d / 2 + 0.1, false);
addWindowsOnFace(w, h, Math.PI, 0, -d / 2 - 0.1, false);
addWindowsOnFace(d, h, Math.PI / 2, w / 2 + 0.1, 0, true);
addWindowsOnFace(d, h, -Math.PI / 2, -w / 2 - 0.1, 0, true);
}
}
function createTrees() {
for (let i = 0; i < 45; i++) {
const trunkHeight = 70;
const trunk = new THREE.Mesh(
new THREE.CylinderGeometry(4, 6, trunkHeight),
new THREE.MeshStandardMaterial({ color: 0x0a0a0a })
);
const leaves = new THREE.Mesh(
new THREE.SphereGeometry(40, 8, 8),
new THREE.MeshStandardMaterial({ color: 0x1a1a1a })
);
const range = 3000;
const x = (Math.random() - 0.5) * range;
const z = (Math.random() - 0.5) * range;
trunk.position.set(x, trunkHeight / 2, z);
leaves.position.set(x, trunkHeight + 10, z);
scene.add(trunk, leaves);
trees.push(trunk, leaves);
}
}
function applyActualRates(input) {
const deadzone = 0.02;
if (Math.abs(input) < deadzone) return 0;
const i = (Math.abs(input) - deadzone) / (1 - deadzone) * Math.sign(input);
const expoInput = i * Math.pow(Math.abs(i), 2) * RATES.expo + i * (1 - RATES.expo);
const rateDegPerSec = expoInput * (RATES.maxRate - RATES.centerSens) + i * RATES.centerSens;
return (rateDegPerSec * Math.PI) / 180;
}
function pollInput() {
const gp = navigator.getGamepads()[gamepadIndex];
if (gp) {
// LB (Button 4) zum Umschalten des Modus - mit Debounce
const now = Date.now();
if (gp.buttons[4] && gp.buttons[4].pressed && !modeButtonPressed && (now - modeButtonLastPressed > 300)) {
flightMode = flightMode === 'ACRO' ? 'ANGLE' : 'ACRO';
modeButtonPressed = true;
modeButtonLastPressed = now;
} else if (!gp.buttons[4] || !gp.buttons[4].pressed) {
modeButtonPressed = false;
}
droneSpeed.pitch = applyActualRates(-gp.axes[1]);
droneSpeed.roll = applyActualRates(-gp.axes[0]);
droneSpeed.yaw = applyActualRates(gp.axes[3]);
droneSpeed.throttle = (gp.axes[2] + 1) / 2;
} else if (isTouchDevice && touchState.active) {
const targetPitch = applyActualRates(-touchState.right.y) * 0.5;
const targetRoll = applyActualRates(-touchState.right.x) * 0.5;
const targetYaw = applyActualRates(touchState.left.x) * 0.5;
smoothPitch += (targetPitch - smoothPitch) * SMOOTH_FACTOR;
smoothRoll += (targetRoll - smoothRoll) * SMOOTH_FACTOR;
smoothYaw += (targetYaw - smoothYaw) * SMOOTH_FACTOR;
droneSpeed.pitch = smoothPitch;
droneSpeed.roll = smoothRoll;
droneSpeed.yaw = smoothYaw;
let rawThrottle = Math.max(0, (touchState.left.y + 1) / 2);
droneSpeed.throttle = Math.pow(rawThrottle, 1.5);
}
}
function checkCollision() {
const droneBox = new THREE.Box3().setFromObject(drone);
droneBox.expandByScalar(-0.06);
for (let obj of [...city, ...trees]) {
const objBox = new THREE.Box3().setFromObject(obj);
if (droneBox.intersectsBox(objBox)) {
droneVelocity.multiplyScalar(-0.4);
angularVelocity.multiplyScalar(-0.5); // Stoppt Drehung bei Crash
drone.position.add(droneVelocity.clone().multiplyScalar(0.05));
return true;
}
}
return false;
}
function animate() {
requestAnimationFrame(animate);
const delta = Math.min(clock.getDelta(), 0.05);
pollInput();
if (flightMode === 'ANGLE') {
const droneUp = new THREE.Vector3(0, 1, 0).applyQuaternion(drone.quaternion);
const worldUp = new THREE.Vector3(0, 1, 0);
// 1. Fehlervektor berechnen und ins lokale System transformieren
let errorAxis = new THREE.Vector3().crossVectors(droneUp, worldUp);
const inverseQuat = drone.quaternion.clone().invert();
errorAxis.applyQuaternion(inverseQuat);
// 2. STÄRKERES KIPPEN: Multiplikator erhöht (0.4 -> 1.2)
// Damit hast du deutlich mehr Neigungswinkel bei Stick-Ausschlag
const targetPitch = droneSpeed.pitch * 1.2;
const targetRoll = droneSpeed.roll * 1.2;
const levelStrength = 15.0;
// 3. ZIEL-DREHRATE: Wir kombinieren Aufrichten und Stick-Input sanfter
const targetVelX = (errorAxis.x * levelStrength) + targetPitch;
const targetVelZ = (errorAxis.z * levelStrength) + targetRoll;
// 4. ANTI-ZUCKELN: Sanfter Übergang zur Zielgeschwindigkeit
angularVelocity.x += (targetVelX - angularVelocity.x) * 0.25;
angularVelocity.z += (targetVelZ - angularVelocity.z) * 0.25;
angularVelocity.y += (droneSpeed.yaw - angularVelocity.y) * 0.15;
// 5. WEICHES LIMIT: Anstatt hart zu stoppen, bremsen wir nur die Kraft, die nach außen drückt
const currentTilt = Math.acos(Math.max(-1, Math.min(1, droneUp.dot(worldUp))));
if (currentTilt > ANGLE_config.angleLimit) {
// Dämpft die Rotation nur, wenn sie vom Zentrum wegführt
angularVelocity.x *= 0.7;
angularVelocity.z *= 0.7;
}
} else {
// ACRO MODE
angularVelocity.x += (droneSpeed.pitch - angularVelocity.x) * 0.15;
angularVelocity.y += (droneSpeed.yaw - angularVelocity.y) * 0.15;
angularVelocity.z += (droneSpeed.roll - angularVelocity.z) * 0.15;
}
angularVelocity.multiplyScalar(rotationInertia);
// --- ROTATIONS-ANWENDUNG mit korrekter Euler-Order ---
// 'ZYX' passt zu drone.rotation.order = "ZYX"
const movementQuat = new THREE.Quaternion().setFromEuler(new THREE.Euler(
angularVelocity.x * delta,
-angularVelocity.y * delta,
angularVelocity.z * delta, // Roll: korrigiertes Vorzeichen
'ZYX'
));
drone.quaternion.multiply(movementQuat);
drone.quaternion.normalize();
// --- PHYSIK & SCHUB ---
const thrustPower = 85.0;
const gravity = -35.0;
const localUp = new THREE.Vector3(0, 1, 0).applyQuaternion(drone.quaternion);
const thrustVector = localUp.multiplyScalar(droneSpeed.throttle * thrustPower * delta);
const gravityVector = new THREE.Vector3(0, gravity * delta, 0);
droneVelocity.add(thrustVector).add(gravityVector);
droneVelocity.multiplyScalar(0.985);
drone.position.add(droneVelocity.clone().multiplyScalar(delta * 13));
if (drone.position.y < 0.15) {
drone.position.y = 0.15;
droneVelocity.set(0, 0, 0);
}
checkCollision();
camera.position.copy(drone.position);
const camQuat = drone.quaternion.clone();
const camTilt = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0.35);
camera.quaternion.copy(camQuat.multiply(camTilt));
drawHUD();
renderer.render(scene, camera);
}
function drawHUD() {
hudContext.clearRect(0, 0, hudCanvas.width, hudCanvas.height);
hudContext.fillStyle = "#00ff00";
hudContext.font = "bold 18px monospace";
hudContext.fillText(`THR: ${(droneSpeed.throttle * 100).toFixed(0)}%`, 40, 50);
const speedDisplay = (droneVelocity.length() * 5).toFixed(1);
hudContext.fillText(`SPD: ${speedDisplay}`, 40, 80);
const altDisplay = (drone.position.y / 10).toFixed(1);
hudContext.fillText(`ALT: ${altDisplay}m`, 40, 110);
// --- MODE BUTTON (ACRO / ANGLE) ---
const buttonX = hudCanvas.width - 120;
const buttonY = 30;
const buttonWidth = 100;
const buttonHeight = 40;
hudContext.strokeStyle = "#00ff00";
hudContext.lineWidth = 2;
hudContext.strokeRect(buttonX, buttonY, buttonWidth, buttonHeight);
hudContext.fillStyle = "#00ff00";
hudContext.font = "bold 14px monospace";
hudContext.textAlign = "center";
hudContext.textBaseline = "middle";
hudContext.fillText(flightMode, buttonX + buttonWidth / 2, buttonY + buttonHeight / 2);
hudContext.textAlign = "left";
hudContext.strokeStyle = "#00ff00";
hudContext.lineWidth = 2;
hudContext.beginPath();
hudContext.moveTo(hudCanvas.width / 2 - 15, hudCanvas.height / 2);
hudContext.lineTo(hudCanvas.width / 2 + 15, hudCanvas.height / 2);
hudContext.moveTo(hudCanvas.width / 2, hudCanvas.height / 2 - 15);
hudContext.lineTo(hudCanvas.width / 2, hudCanvas.height / 2 + 15);
hudContext.stroke();
if (isTouchDevice && touchState.active && !navigator.getGamepads()[gamepadIndex]) {
drawSquareStick(hudCanvas.width * 0.13, hudCanvas.height * 0.70, touchState.left.x, -touchState.left.y, true);
drawSquareStick(hudCanvas.width * 0.87, hudCanvas.height * 0.70, touchState.right.x, -touchState.right.y, false);
}
}
function drawSquareStick(x, y, jX, jY, isLeft) {
const size = 190;
hudContext.strokeStyle = "rgba(0, 255, 0, 0.2)";
hudContext.strokeRect(x - size/2, y - size/2, size, size);
hudContext.fillStyle = "rgba(0, 255, 0, 0.4)";
hudContext.fillRect(x + jX * 80 - 15, y + jY * 80 - 15, 30, 30);
}
window.addEventListener("gamepadconnected", (e) => { gamepadIndex = e.gamepad.index; });
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
hudCanvas.width = window.innerWidth;
hudCanvas.height = window.innerHeight;
});
03
SpaceWar
Ein klassischer Space-Shooter mit moderner Web-Technologie.
PROJEKT ANSEHEN LIVE DEMO
game.js
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// --- WEB AUDIO API SYSTEM ---
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
let shotBuffer, crashBuffer;
async function loadSound(url) {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`Status: ${response.status}`);
const arrayBuffer = await response.arrayBuffer();
return await audioCtx.decodeAudioData(arrayBuffer);
} catch (e) {
console.warn("Fehler beim Laden von: " + url + ". Spiel läuft ohne diesen Sound weiter.", e);
return null;
}
}
loadSound('sound/shot.mp3').then(buffer => shotBuffer = buffer);
loadSound('sound/crash.mp3').then(buffer => crashBuffer = buffer);
function unlockMobileAudio() {
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
const s = document.getElementById('snd_shot');
const c = document.getElementById('snd_crash');
if (s && c) {
s.play().then(() => { s.pause(); s.currentTime = 0; }).catch(()=>{});
c.play().then(() => { c.pause(); c.currentTime = 0; }).catch(()=>{});
}
}
function playSound(buffer, volume = 0.3) {
if (!buffer || audioCtx.state === 'suspended') return;
const source = audioCtx.createBufferSource();
const gainNode = audioCtx.createGain();
source.buffer = buffer;
gainNode.gain.value = volume;
source.connect(gainNode);
gainNode.connect(audioCtx.destination);
source.start(0);
}
// --- SPIEL-LOGIK ---
function resizeCanvas() {
setTimeout(() => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}, 100);
}
window.addEventListener('resize', resizeCanvas);
window.addEventListener('orientationchange', resizeCanvas);
resizeCanvas();
let isShooting = false, KEY_UP = false, KEY_DOWN = false;
let rocketTouchId = null; // Für entkoppelte Steuerung
const rocket = { X: 0, Y: canvas.height / 3, width: 75, height: 110, img: new Image() };
rocket.img.src = 'image/rocket.png';
const ufos = [], shots = [];
const ufoImages = ['image/ufo.png', 'image/ufo1.png', 'image/ufo2.png', 'image/ufo3.png', 'image/ufo4.png', 'image/ufo5.png', 'image/ufo6.png',
'image/ufo7.png', 'image/ufo8.png', 'image/ufo9.png', 'image/ufo10.png', 'image/ufo11.png'];
const background = new Image(); background.src = 'image/Space.png';
let bgX = 0;
const stageImage = new Image(); stageImage.src = 'image/stage.png';
const shotRateImage = new Image(); shotRateImage.src = 'image/shotrate.png';
const shotRateMinusImage = new Image(); shotRateMinusImage.src = 'image/shotrate-.png';
let score = 0, ufoSpeed = 6, ufoCount = 0, shotCooldown = 250, lastShotTime = 0;
let alternateShot = true, gameRunning = false, showStageImage = false, showShotRateFloat = false, showShotRateMinusFloat = false;
let ufoSpawnTimeout;
const minUfoDelay = 500;
const maxUfoDelay = 2000;
function startUfoSpawnLoop() {
const delay = Math.random() * (maxUfoDelay - minUfoDelay) + minUfoDelay;
ufoSpawnTimeout = setTimeout(() => {
if (gameRunning) {
createUfo();
startUfoSpawnLoop();
}
}, delay);
}
// --- ENTKOPPELTE STEUERUNG (TOUCH) ---
const shootBtn = document.getElementById('shoot-button');
// Dauerfeuer Button
shootBtn.addEventListener('touchstart', (e) => {
e.preventDefault();
e.stopPropagation();
if (audioCtx.state === 'suspended') audioCtx.resume();
isShooting = true;
}, { passive: false });
shootBtn.addEventListener('touchend', (e) => {
e.preventDefault();
isShooting = false;
}, { passive: false });
// Raketen Bewegung (Entkoppelt durch ID)
canvas.addEventListener('touchstart', e => {
for (let i = 0; i < e.changedTouches.length; i++) {
const t = e.changedTouches[i];
if (t.clientX >= rocket.X && t.clientX <= rocket.X + rocket.width * 2 &&
t.clientY >= rocket.Y && t.clientY <= rocket.Y + rocket.height) {
rocketTouchId = t.identifier;
}
}
}, { passive: false });
canvas.addEventListener('touchmove', e => {
if (rocketTouchId !== null) {
for (let i = 0; i < e.touches.length; i++) {
const t = e.touches[i];
if (t.identifier === rocketTouchId) {
rocket.Y = t.clientY - rocket.height / 2;
e.preventDefault();
}
}
}
}, { passive: false });
canvas.addEventListener('touchend', e => {
for (let i = 0; i < e.changedTouches.length; i++) {
if (e.changedTouches[i].identifier === rocketTouchId) {
rocketTouchId = null;
}
}
});
// --- KEYBOARD CONTROLS ---
document.addEventListener('keydown', e => {
if (e.code === 'Space') isShooting = true;
if (e.code === 'ArrowUp') KEY_UP = true;
if (e.code === 'ArrowDown') KEY_DOWN = true;
});
document.addEventListener('keyup', e => {
if (e.code === 'Space') isShooting = false;
if (e.code === 'ArrowUp') KEY_UP = false;
if (e.code === 'ArrowDown') KEY_DOWN = false;
});
function createUfo() {
const ufo = {
X: canvas.width,
Y: Math.random() * (canvas.height - 100),
width: 70,
height: 40,
img: new Image(),
speed: ufoSpeed
};
ufo.img.src = ufoImages[Math.floor(Math.random() * ufoImages.length)];
ufos.push(ufo);
ufoCount++;
if (ufoCount % 15 === 0) {
ufoSpeed += 0.1;
showStageImage = true;
setTimeout(() => showStageImage = false, 1000);
}
}
function shoot() {
const now = Date.now();
if (isShooting && now - lastShotTime > shotCooldown) {
const yOffset = alternateShot ? rocket.height / 5.4 : rocket.height / 1.4;
const shot = {
X: rocket.X + rocket.width,
Y: rocket.Y + yOffset,
width: 32,
height: 8,
img: new Image()
};
shot.img.src = 'image/shot.png';
shots.push(shot);
alternateShot = !alternateShot;
lastShotTime = now;
playSound(shotBuffer, 0.2);
}
}
function checkCollisions() {
for (let i = ufos.length - 1; i >= 0; i--) {
const ufo = ufos[i];
if (
rocket.X + rocket.width > ufo.X &&
rocket.Y + rocket.height > ufo.Y &&
rocket.X < ufo.X + ufo.width &&
rocket.Y < ufo.Y + ufo.height
) {
rocket.img.src = 'image/boom.png';
playSound(crashBuffer, 0.5);
if (navigator.vibrate) navigator.vibrate(50);
endGame();
return;
}
if (ufo.X + ufo.width < 0) {
ufos.splice(i, 1);
if (shotCooldown < 500) {
shotCooldown += 25;
showShotRateMinusFloat = true;
setTimeout(() => showShotRateMinusFloat = false, 1000);
}
continue;
}
for (let j = shots.length - 1; j >= 0; j--) {
const shot = shots[j];
if (
shot.X + shot.width > ufo.X &&
shot.Y + shot.height > ufo.Y &&
shot.X < ufo.X + ufo.width &&
shot.Y < ufo.Y + ufo.height
) {
ufo.img.src = 'image/boom.png';
setTimeout(() => ufos.splice(i, 1), 200);
shots.splice(j, 1);
score++;
playSound(crashBuffer, 0.3);
if (score % 20 === 0 && shotCooldown > 100) {
shotCooldown -= 25;
showShotRateFloat = true;
setTimeout(() => showShotRateFloat = false, 1000);
}
break;
}
}
}
}
function endGame() {
gameRunning = false;
isShooting = false;
clearTimeout(ufoSpawnTimeout);
const oldHighscore = parseInt(localStorage.getItem('highscore')) || 0;
if (score > oldHighscore) {
localStorage.setItem('highscore', score);
}
document.getElementById('end-score').textContent = 'UFOs: ' + score;
document.getElementById('end-highscore').textContent = 'HIGHSCORE: ' + localStorage.getItem('highscore');
document.getElementById('end-screen').style.display = 'flex';
}
function gameLoop() {
if (!gameRunning) { draw(); return; }
if (KEY_UP) rocket.Y -= 8;
if (KEY_DOWN) rocket.Y += 8;
// Begrenzung, damit Rakete nicht aus dem Bild fliegt
if (rocket.Y < 0) rocket.Y = 0;
if (rocket.Y > canvas.height - rocket.height) rocket.Y = canvas.height - rocket.height;
shoot();
ufos.forEach(ufo => ufo.X -= ufo.speed);
shots.forEach(shot => shot.X += 20);
checkCollisions();
draw();
requestAnimationFrame(gameLoop);
}
function draw() {
ctx.drawImage(background, bgX, 0, canvas.width, canvas.height);
ctx.drawImage(background, bgX + canvas.width, 0, canvas.width, canvas.height);
bgX -= ufoSpeed / 2;
if (bgX <= -canvas.width) bgX = 0;
ctx.drawImage(rocket.img, rocket.X, rocket.Y, rocket.width, rocket.height);
ufos.forEach(ufo => ctx.drawImage(ufo.img, ufo.X, ufo.Y, ufo.width, ufo.height));
shots.forEach(shot => ctx.drawImage(shot.img, shot.X, shot.Y, shot.width, shot.height));
ctx.fillStyle = 'white';
ctx.font = '18px Arial';
ctx.fillText("UFOs: " + score, 30, 20);
const highscore = localStorage.getItem('highscore') || 0;
ctx.fillText("HIGHSCORE: " + highscore, 550, 20);
if (showStageImage) {
ctx.drawImage(stageImage, canvas.width / 2 - 100, canvas.height / 2 - 50, 200, 100);
}
if (showShotRateFloat) {
ctx.drawImage(shotRateImage, rocket.X + rocket.width / 2 - 25, rocket.Y - 30, 60, 20);
}
if (showShotRateMinusFloat) {
ctx.drawImage(shotRateMinusImage, rocket.X + rocket.width / 2 - 25, rocket.Y - 30, 60, 20);
}
}
window.onload = () => {
const hs = localStorage.getItem('highscore') || 0;
document.getElementById('highscore-display').textContent = 'HIGHSCORE: ' + hs;
};
document.getElementById('start-button').addEventListener('click', () => {
unlockMobileAudio();
document.getElementById('start-screen').style.display = 'none';
startUfoSpawnLoop();
gameRunning = true;
gameLoop();
});
document.getElementById('restart-button').addEventListener('click', () => {
unlockMobileAudio();
score = 0;
ufoSpeed = 6;
ufoCount = 0;
shotCooldown = 250;
alternateShot = true;
isShooting = false;
rocket.img.src = 'image/rocket.png';
rocket.Y = canvas.height / 2;
ufos.length = 0;
shots.length = 0;
document.getElementById('end-screen').style.display = 'none';
clearTimeout(ufoSpawnTimeout);
startUfoSpawnLoop();
gameRunning = true;
gameLoop();
});
window.addEventListener('pageshow', event => {
if (event.persisted) {
location.reload();
}
});