> 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();
    }
});