1607 lines
53 KiB
JavaScript

// ===== PIXEL-ÜBERLEBENDER - 2D SURVIVAL GAME =====
// Eine Schritt-für-Schritt Einführung in die 2D-Spieleentwicklung
// 1. CANVAS-GRUNDLAGEN
// Das Canvas-Element ist wie eine digitale Leinwand, auf die wir mit JavaScript zeichnen können.
// Es bietet uns eine 2D-Zeichenfläche mit Pixeln, die wir einzeln kontrollieren können.
// Canvas-Element aus dem HTML-Dokument holen
const canvas = document.getElementById('gameCanvas');
// Den 2D-Rendering-Kontext erhalten
// Der Kontext (ctx) ist unser "Pinsel" - damit zeichnen wir auf das Canvas
const ctx = canvas.getContext('2d');
// Canvas-Dimensionen für spätere Berechnungen speichern
const CANVAS_WIDTH = canvas.width; // 800 Pixel
const CANVAS_HEIGHT = canvas.height; // 600 Pixel
// ===== ASSET-SYSTEM =====
/**
* Asset Manager für das Laden und Verwalten von Sprites
*/
class AssetManager {
constructor() {
this.images = new Map();
this.loadedCount = 0;
this.totalCount = 0;
this.isLoaded = false;
}
/**
* Lädt ein Bild und speichert es im Asset Manager
* @param {string} key - Eindeutiger Schlüssel für das Bild
* @param {string} path - Pfad zur Bilddatei
*/
loadImage(key, path) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
this.images.set(key, img);
this.loadedCount++;
console.log(`✅ Bild geladen: ${key} (${this.loadedCount}/${this.totalCount})`);
resolve(img);
};
img.onerror = () => {
console.error(`❌ Fehler beim Laden: ${key} von ${path}`);
reject(new Error(`Failed to load ${path}`));
};
img.src = path;
});
}
/**
* Lädt alle benötigten Assets
*/
async loadAllAssets() {
const assetsToLoad = [
// Boden-Tiles
{ key: 'grass', path: 'assets/1 Tiles/gras.png' },
{ key: 'path', path: 'assets/1 Tiles/weg.png' },
{ key: 'swamp1', path: 'assets/1 Tiles/SwampTile_01.png' },
{ key: 'swamp2', path: 'assets/1 Tiles/SwampTile_02.png' },
{ key: 'swamp3', path: 'assets/1 Tiles/SwampTile_03.png' },
// Bäume
{ key: 'tree1', path: 'assets/2 Objects/2 Tree/1.png' },
{ key: 'tree2', path: 'assets/2 Objects/2 Tree/2.png' },
{ key: 'tree3', path: 'assets/2 Objects/2 Tree/3.png' },
{ key: 'tree4', path: 'assets/2 Objects/2 Tree/4.png' },
// Steine
{ key: 'stone1', path: 'assets/2 Objects/4 Stone/1.png' },
{ key: 'stone2', path: 'assets/2 Objects/4 Stone/2.png' },
{ key: 'stone3', path: 'assets/2 Objects/4 Stone/3.png' },
// Gras/Stöcker
{ key: 'stick1', path: 'assets/2 Objects/3 Grass/1.png' },
{ key: 'stick2', path: 'assets/2 Objects/3 Grass/2.png' },
{ key: 'stick3', path: 'assets/2 Objects/3 Grass/3.png' },
// Schatten
{ key: 'shadow1', path: 'assets/2 Objects/1 Shadow/1.png' },
{ key: 'shadow2', path: 'assets/2 Objects/1 Shadow/2.png' },
// Spieler-Sprite
{ key: 'pointer1', path: 'assets/2 Objects/5 Pointer/1.png' },
// Animierte Objekte (Lagerfeuer)
{ key: 'torch1', path: 'assets/3 Animated Objects/2 Torch/1.png' },
{ key: 'torch2', path: 'assets/3 Animated Objects/2 Torch/2.png' },
{ key: 'torch3', path: 'assets/3 Animated Objects/2 Torch/3.png' },
{ key: 'torch4', path: 'assets/3 Animated Objects/2 Torch/4.png' }
];
this.totalCount = assetsToLoad.length;
console.log(`🎨 Lade ${this.totalCount} Assets...`);
try {
await Promise.all(assetsToLoad.map(asset =>
this.loadImage(asset.key, asset.path)
));
this.isLoaded = true;
console.log('🎉 Alle Assets erfolgreich geladen!');
return true;
} catch (error) {
console.error('❌ Fehler beim Laden der Assets:', error);
return false;
}
}
/**
* Gibt ein geladenes Bild zurück
* @param {string} key - Schlüssel des Bildes
* @returns {Image|null} Das Bild oder null falls nicht gefunden
*/
getImage(key) {
return this.images.get(key) || null;
}
/**
* Zeichnet ein Sprite auf das Canvas
* @param {CanvasRenderingContext2D} ctx - Canvas-Kontext
* @param {string} key - Sprite-Schlüssel
* @param {number} x - X-Position
* @param {number} y - Y-Position
* @param {number} width - Breite (optional)
* @param {number} height - Höhe (optional)
*/
drawSprite(ctx, key, x, y, width = null, height = null) {
const img = this.getImage(key);
if (!img) {
// Fallback: Zeichne ein farbiges Rechteck
ctx.fillStyle = '#FF0000';
ctx.fillRect(x, y, width || 32, height || 32);
return;
}
if (width && height) {
ctx.drawImage(img, x, y, width, height);
} else {
ctx.drawImage(img, x, y);
}
}
}
// Globaler Asset Manager
let assetManager = new AssetManager();
// ===== WELT-SYSTEM =====
// Welt-Konfiguration
const WORLD_CONFIG = {
CHUNK_SIZE: 400, // Größe eines Chunks in Pixeln
RENDER_DISTANCE: 2, // Wie viele Chunks um den Spieler geladen werden
WORLD_SEED: 12345, // Seed für prozedurale Generierung
RESOURCE_DENSITY: {
stick: 0.8, // Stöcker pro 100x100 Pixel Bereich
tree: 0.4, // Bäume pro 100x100 Pixel Bereich
rock: 0.3 // Felsen pro 100x100 Pixel Bereich
}
};
/**
* Kamera-System für die scrollende Welt
*/
class Camera {
constructor() {
this.x = 0; // Kamera-Position in Weltkoordinaten
this.y = 0;
this.targetX = 0; // Ziel-Position (für smooth following)
this.targetY = 0;
this.smoothing = 0.1; // Wie sanft die Kamera folgt (0-1)
}
/**
* Aktualisiert die Kamera-Position um dem Spieler zu folgen
* @param {Player} player - Der Spieler
*/
update(player) {
// Kamera soll auf den Spieler zentriert sein
this.targetX = player.x - CANVAS_WIDTH / 2;
this.targetY = player.y - CANVAS_HEIGHT / 2;
// Sanfte Kamera-Bewegung (Lerp)
this.x += (this.targetX - this.x) * this.smoothing;
this.y += (this.targetY - this.y) * this.smoothing;
}
/**
* Konvertiert Weltkoordinaten zu Bildschirmkoordinaten
* @param {number} worldX - X in Weltkoordinaten
* @param {number} worldY - Y in Weltkoordinaten
* @returns {Object} - {x, y} in Bildschirmkoordinaten
*/
worldToScreen(worldX, worldY) {
return {
x: worldX - this.x,
y: worldY - this.y
};
}
/**
* Konvertiert Bildschirmkoordinaten zu Weltkoordinaten
* @param {number} screenX - X in Bildschirmkoordinaten
* @param {number} screenY - Y in Bildschirmkoordinaten
* @returns {Object} - {x, y} in Weltkoordinaten
*/
screenToWorld(screenX, screenY) {
return {
x: screenX + this.x,
y: screenY + this.y
};
}
}
/**
* Chunk-System für Performance und prozedurale Generierung
*/
class Chunk {
constructor(chunkX, chunkY) {
this.chunkX = chunkX; // Chunk-Koordinaten
this.chunkY = chunkY;
this.worldX = chunkX * WORLD_CONFIG.CHUNK_SIZE; // Welt-Position
this.worldY = chunkY * WORLD_CONFIG.CHUNK_SIZE;
this.resources = []; // Ressourcen in diesem Chunk
this.tiles = []; // Boden-Tiles für diesen Chunk
this.generated = false; // Ob der Chunk bereits generiert wurde
// Perlin Noise Permutation Table initialisieren
this.initPerlinNoise();
}
/**
* Generiert Ressourcen für diesen Chunk prozedural
*/
generate() {
if (this.generated) return;
this.resources = [];
this.tiles = [];
// Pseudo-Zufallsgenerator basierend auf Chunk-Position und Seed
const random = this.seededRandom(this.chunkX, this.chunkY, WORLD_CONFIG.WORLD_SEED);
// Berechne wie viele Ressourcen basierend auf Chunk-Größe
const area = (WORLD_CONFIG.CHUNK_SIZE / 100) ** 2; // Normalisiert auf 100x100 Bereiche
const tileSize = 32; // Größe eines Tiles in Pixeln
// Boden-Tiles generieren mit Perlin Noise
for (let x = 0; x < WORLD_CONFIG.CHUNK_SIZE; x += tileSize) {
for (let y = 0; y < WORLD_CONFIG.CHUNK_SIZE; y += tileSize) {
const worldX = this.worldX + x;
const worldY = this.worldY + y;
// Perlin Noise für natürlichere Boden-Verteilung
const noiseScale = 0.05; // Skalierung des Noise
const noiseValue = this.noise(worldX * noiseScale, worldY * noiseScale);
// Normalisiere Noise-Wert von [-1,1] zu [0,1]
const normalizedNoise = (noiseValue + 1) / 2;
let tileType;
if (normalizedNoise < 0.3) {
// Sumpfgebiete in niedrigen Bereichen
const swampNoise = this.noise(worldX * 0.1, worldY * 0.1);
const swampVariant = Math.floor(((swampNoise + 1) / 2) * 64) + 1;
tileType = `SwampTile_${swampVariant.toString().padStart(2, '0')}`;
} else if (normalizedNoise < 0.6) {
// Übergangsbereich - gemischte Tiles
const mixNoise = this.noise(worldX * 0.08, worldY * 0.08);
if (mixNoise > 0) {
tileType = 'grass';
} else {
const swampVariant = Math.floor(((mixNoise + 1) / 2) * 20) + 1;
tileType = `SwampTile_${swampVariant.toString().padStart(2, '0')}`;
}
} else {
// Höhere Bereiche - hauptsächlich Gras
tileType = 'grass';
}
this.tiles.push({
x: worldX,
y: worldY,
type: tileType,
size: tileSize
});
}
}
// Ressourcen mit Perlin Noise-basierten Clustern generieren
const resourceDensity = 8; // Anzahl Versuche pro 64x64 Bereich
for (let attempts = 0; attempts < resourceDensity * area; attempts++) {
const x = this.worldX + random() * WORLD_CONFIG.CHUNK_SIZE;
const y = this.worldY + random() * WORLD_CONFIG.CHUNK_SIZE;
// Verschiedene Noise-Frequenzen für verschiedene Ressourcen
const stickNoise = this.noise(x * 0.02, y * 0.02);
const treeNoise = this.noise(x * 0.015, y * 0.015 + 100); // Offset für Variation
const rockNoise = this.noise(x * 0.01, y * 0.01 + 200);
// Normalisiere zu [0,1]
const stickDensity = (stickNoise + 1) / 2;
const treeDensity = (treeNoise + 1) / 2;
const rockDensity = (rockNoise + 1) / 2;
// Stöcker in Bereichen mit hoher Stick-Dichte
if (stickDensity > 0.6 && random() < WORLD_CONFIG.RESOURCE_DENSITY.stick * stickDensity) {
const stickVariant = Math.floor(random() * 3) + 1;
this.resources.push(new Resource(x, y, 'stick', `stick${stickVariant}`));
}
// Bäume in Clustern
if (treeDensity > 0.5 && random() < WORLD_CONFIG.RESOURCE_DENSITY.tree * treeDensity) {
const treeVariant = Math.floor(random() * 4) + 1;
this.resources.push(new Resource(x, y, 'tree', `tree${treeVariant}`));
}
// Felsen in spezifischen Gebieten
if (rockDensity > 0.7 && random() < WORLD_CONFIG.RESOURCE_DENSITY.rock * rockDensity) {
const stoneVariant = Math.floor(random() * 3) + 1;
this.resources.push(new Resource(x, y, 'rock', `stone${stoneVariant}`));
}
}
this.generated = true;
console.log(`🗺️ Chunk (${this.chunkX}, ${this.chunkY}) generiert: ${this.resources.length} Ressourcen`);
}
/**
* Seeded Random Number Generator für konsistente prozedurale Generierung
*/
seededRandom(x, y, seed) {
let counter = 0;
const hash = (x * 374761393 + y * 668265263 + seed) % 2147483647;
return function() {
counter++;
const x = Math.sin(hash + counter) * 43758.5453;
return x - Math.floor(x);
};
}
/**
* Initialisiert Perlin Noise Permutation Table
*/
initPerlinNoise() {
this.p = new Array(512);
const permutation = [];
// Generiere Permutation basierend auf Chunk-Position und Seed
for (let i = 0; i < 256; i++) {
permutation[i] = i;
}
// Shuffle basierend auf Seed und Chunk-Position
for (let i = 255; i > 0; i--) {
const seed = this.chunkX * 73856093 + this.chunkY * 19349663 + WORLD_CONFIG.WORLD_SEED;
const hash = Math.sin(seed + i) * 43758.5453;
const j = Math.floor((hash - Math.floor(hash)) * (i + 1));
[permutation[i], permutation[j]] = [permutation[j], permutation[i]];
}
for (let i = 0; i < 512; i++) {
this.p[i] = permutation[i & 255];
}
}
/**
* Perlin Noise Implementation für natürlichere Generierung
*/
noise(x, y) {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
x -= Math.floor(x);
y -= Math.floor(y);
const u = this.fade(x);
const v = this.fade(y);
const A = this.p[X] + Y;
const B = this.p[X + 1] + Y;
return this.lerp(v,
this.lerp(u, this.grad(this.p[A], x, y), this.grad(this.p[B], x - 1, y)),
this.lerp(u, this.grad(this.p[A + 1], x, y - 1), this.grad(this.p[B + 1], x - 1, y - 1))
);
}
fade(t) {
return t * t * t * (t * (t * 6 - 15) + 10);
}
lerp(t, a, b) {
return a + t * (b - a);
}
grad(hash, x, y) {
const h = hash & 15;
const u = h < 8 ? x : y;
const v = h < 4 ? y : h === 12 || h === 14 ? x : 0;
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
}
/**
* Prüft ob dieser Chunk im sichtbaren Bereich ist
* @param {Camera} camera - Die Kamera
* @returns {boolean} - True wenn sichtbar
*/
isVisible(camera) {
const screenPos = camera.worldToScreen(this.worldX, this.worldY);
return screenPos.x < CANVAS_WIDTH + WORLD_CONFIG.CHUNK_SIZE &&
screenPos.x + WORLD_CONFIG.CHUNK_SIZE > 0 &&
screenPos.y < CANVAS_HEIGHT + WORLD_CONFIG.CHUNK_SIZE &&
screenPos.y + WORLD_CONFIG.CHUNK_SIZE > 0;
}
/**
* Rendert alle Boden-Tiles dieses Chunks
* @param {CanvasRenderingContext2D} ctx - Canvas-Kontext
* @param {Camera} camera - Kamera für Koordinaten-Transformation
*/
renderTiles(ctx, camera) {
if (!assetManager.isLoaded) return;
this.tiles.forEach(tile => {
const screenPos = camera.worldToScreen(tile.x, tile.y);
// Nur zeichnen wenn im sichtbaren Bereich
if (screenPos.x + tile.size < 0 || screenPos.x > CANVAS_WIDTH ||
screenPos.y + tile.size < 0 || screenPos.y > CANVAS_HEIGHT) {
return;
}
assetManager.drawSprite(ctx, tile.type, screenPos.x, screenPos.y, tile.size, tile.size);
});
}
}
/**
* Welt-Manager für Chunk-Loading und -Unloading
*/
class World {
constructor() {
this.chunks = new Map(); // Geladene Chunks (key: "x,y")
this.camera = new Camera();
}
/**
* Aktualisiert die Welt (lädt/entlädt Chunks basierend auf Spielerposition)
* @param {Player} player - Der Spieler
*/
update(player) {
// Kamera aktualisieren
this.camera.update(player);
// Berechne welche Chunks geladen werden müssen
const playerChunkX = Math.floor(player.x / WORLD_CONFIG.CHUNK_SIZE);
const playerChunkY = Math.floor(player.y / WORLD_CONFIG.CHUNK_SIZE);
const neededChunks = new Set();
// Chunks um den Spieler laden
for (let dx = -WORLD_CONFIG.RENDER_DISTANCE; dx <= WORLD_CONFIG.RENDER_DISTANCE; dx++) {
for (let dy = -WORLD_CONFIG.RENDER_DISTANCE; dy <= WORLD_CONFIG.RENDER_DISTANCE; dy++) {
const chunkX = playerChunkX + dx;
const chunkY = playerChunkY + dy;
const key = `${chunkX},${chunkY}`;
neededChunks.add(key);
// Chunk laden falls nicht vorhanden
if (!this.chunks.has(key)) {
const chunk = new Chunk(chunkX, chunkY);
chunk.generate();
this.chunks.set(key, chunk);
}
}
}
// Entfernte Chunks entladen (Performance)
for (const [key, chunk] of this.chunks) {
if (!neededChunks.has(key)) {
this.chunks.delete(key);
console.log(`🗑️ Chunk (${chunk.chunkX}, ${chunk.chunkY}) entladen`);
}
}
}
/**
* Gibt alle Ressourcen in geladenen Chunks zurück
* @returns {Array} - Array aller Ressourcen
*/
getAllResources() {
const allResources = [];
for (const chunk of this.chunks.values()) {
allResources.push(...chunk.resources);
}
return allResources;
}
/**
* Zeichnet Chunk-Grenzen für Debugging
* @param {CanvasRenderingContext2D} ctx - Canvas-Kontext
*/
renderChunkBorders(ctx) {
ctx.strokeStyle = 'rgba(255, 255, 0, 0.3)';
ctx.lineWidth = 1;
for (const chunk of this.chunks.values()) {
if (chunk.isVisible(this.camera)) {
const screenPos = this.camera.worldToScreen(chunk.worldX, chunk.worldY);
ctx.strokeRect(screenPos.x, screenPos.y, WORLD_CONFIG.CHUNK_SIZE, WORLD_CONFIG.CHUNK_SIZE);
}
}
}
}
// ===== SPIELER-KLASSE =====
/**
* Die Player-Klasse repräsentiert unseren Spielcharakter
*
* Warum eine Klasse?
* - Organisiert alle spielerbezogenen Daten und Funktionen
* - Macht den Code wiederverwendbar und erweiterbar
* - Trennt Spielerlogik vom Rest des Spiels (Separation of Concerns)
*/
class Player {
/**
* Konstruktor - wird beim Erstellen eines neuen Spielers aufgerufen
* @param {number} x - Startposition X
* @param {number} y - Startposition Y
*/
constructor(x, y) {
// Position des Spielers (Mittelpunkt)
this.x = x;
this.y = y;
// Größe des Spielers (Quadrat)
this.width = 20;
this.height = 20;
// Bewegungsgeschwindigkeit (Pixel pro Frame)
this.speed = 3;
// Farbe des Spielers
this.color = '#FF6B6B'; // Rot
// Gesundheit (Survival-Aspekt)
this.health = 100;
this.maxHealth = 100;
// Gesundheits-Timer (für langsame Abnahme)
this.healthTimer = 0;
this.healthDecayRate = 300; // Alle 5 Sekunden (300 Frames bei 60 FPS)
// Inventar
this.inventory = {
stick: 0, // Stöcker (für Werkzeuge)
wood: 0, // Holz (von Bäumen mit Axt)
stone: 0 // Stein (von Felsen mit Pickaxe)
};
// Werkzeug-System
this.tools = {
axe: false, // Kann Bäume abbauen
pickaxe: false // Kann Steine abbauen
};
}
/**
* Update-Methode - wird jeden Frame aufgerufen
* Hier wird die Spielerbewegung basierend auf Eingaben berechnet
*/
update() {
// Bewegungsvektor berechnen (Richtung der Bewegung)
let moveX = 0;
let moveY = 0;
// Eingaben in Bewegungsvektor umwandeln
if (keys.left || keys.a) {
moveX -= 1;
}
if (keys.right || keys.d) {
moveX += 1;
}
if (keys.up || keys.w) {
moveY -= 1;
}
if (keys.down || keys.s) {
moveY += 1;
}
// Vektornormalisierung für gleichmäßige Geschwindigkeit
// Problem: Bei diagonaler Bewegung ist die Geschwindigkeit √2 ≈ 1.41x schneller
// Lösung: Vektor normalisieren (Länge = 1) und dann mit Geschwindigkeit multiplizieren
if (moveX !== 0 || moveY !== 0) {
// Länge des Vektors berechnen (Pythagoras: √(x² + y²))
const length = Math.sqrt(moveX * moveX + moveY * moveY);
// Vektor normalisieren (durch Länge teilen)
moveX = moveX / length;
moveY = moveY / length;
// Mit gewünschter Geschwindigkeit multiplizieren
this.x += moveX * this.speed;
this.y += moveY * this.speed;
}
// In der unendlichen Welt gibt es keine Grenzen!
// Der Spieler kann sich frei bewegen, die Kamera folgt ihm
// Gesundheits-System aktualisieren
this.updateHealth();
}
/**
* Aktualisiert das Gesundheitssystem
* Gesundheit nimmt langsam ab, regeneriert sich am Lagerfeuer
*/
updateHealth() {
this.healthTimer++;
// Prüfen ob Spieler in der Nähe eines Lagerfeuers ist
let nearCampfire = false;
campfires.forEach(campfire => {
const distance = Math.sqrt(
(this.x - campfire.x) ** 2 + (this.y - campfire.y) ** 2
);
if (distance < 50) { // 50 Pixel Radius
nearCampfire = true;
}
});
if (nearCampfire) {
// Gesundheit regenerieren am Lagerfeuer
if (this.healthTimer >= 60) { // Jede Sekunde
this.health = Math.min(this.maxHealth, this.health + 2);
this.healthTimer = 0;
}
} else {
// Gesundheit nimmt langsam ab
if (this.healthTimer >= this.healthDecayRate) {
this.health = Math.max(0, this.health - 5);
this.healthTimer = 0;
if (this.health <= 0) {
console.log('💀 Game Over! Deine Gesundheit ist auf 0 gefallen.');
// Hier könnte später ein Game Over Screen kommen
}
}
}
}
/**
* Render-Methode - zeichnet den Spieler auf das Canvas
* @param {CanvasRenderingContext2D} ctx - Der Canvas-Kontext
* @param {Camera} camera - Die Kamera für Koordinaten-Transformation
*/
render(ctx, camera) {
// Weltkoordinaten zu Bildschirmkoordinaten konvertieren
const screenPos = camera.worldToScreen(this.x, this.y);
// Spieler zeichnen
if (assetManager.isLoaded) {
// Verwende ein Pointer-Sprite für den Spieler
assetManager.drawSprite(
ctx,
'pointer1', // Verwende das erste Pointer-Sprite
screenPos.x - this.width / 2,
screenPos.y - this.height / 2,
this.width,
this.height
);
} else {
// Fallback: Einfaches Rechteck
ctx.fillStyle = this.color;
ctx.fillRect(
screenPos.x - this.width/2, // X-Position (links)
screenPos.y - this.height/2, // Y-Position (oben)
this.width, // Breite
this.height // Höhe
);
// Schwarzer Rand für bessere Sichtbarkeit
ctx.strokeStyle = '#000000';
ctx.lineWidth = 2;
ctx.strokeRect(
screenPos.x - this.width/2,
screenPos.y - this.height/2,
this.width,
this.height
);
}
}
}
// ===== EINGABE-SYSTEM =====
/**
* Das keys-Objekt speichert den Zustand aller Tasten
*
* Warum so?
* - Ermöglicht gleichzeitiges Drücken mehrerer Tasten (z.B. diagonal laufen)
* - Saubere Trennung zwischen Eingabe-Erkennung und Spiellogik
* - Einfach erweiterbar für neue Tasten
*/
const keys = {
left: false,
right: false,
up: false,
down: false,
w: false,
a: false,
s: false,
d: false,
e: false, // Lagerfeuer
q: false, // Axt
r: false, // Pickaxe
tab: false // Inventar toggle
};
// UI-Zustand
let showInventory = false; // Inventar standardmäßig ausgeblendet
/**
* Event-Listener für Tastendruck (keydown)
* Wird aufgerufen, wenn eine Taste gedrückt wird
*/
window.addEventListener('keydown', (event) => {
// event.code gibt uns den physischen Tastencode
switch(event.code) {
case 'ArrowLeft':
keys.left = true;
break;
case 'ArrowRight':
keys.right = true;
break;
case 'ArrowUp':
keys.up = true;
break;
case 'ArrowDown':
keys.down = true;
break;
case 'KeyW':
keys.w = true;
break;
case 'KeyA':
keys.a = true;
break;
case 'KeyS':
keys.s = true;
break;
case 'KeyD':
keys.d = true;
break;
case 'KeyE':
keys.e = true;
break;
case 'KeyQ':
keys.q = true;
break;
case 'KeyR':
keys.r = true;
break;
case 'Tab':
keys.tab = true;
break;
}
// Verhindert Standard-Browser-Verhalten (z.B. Scrollen mit Pfeiltasten)
event.preventDefault();
});
/**
* Event-Listener für Taste loslassen (keyup)
* Wird aufgerufen, wenn eine Taste losgelassen wird
*/
window.addEventListener('keyup', (event) => {
switch(event.code) {
case 'ArrowLeft':
keys.left = false;
break;
case 'ArrowRight':
keys.right = false;
break;
case 'ArrowUp':
keys.up = false;
break;
case 'ArrowDown':
keys.down = false;
break;
case 'KeyW':
keys.w = false;
break;
case 'KeyA':
keys.a = false;
break;
case 'KeyS':
keys.s = false;
break;
case 'KeyD':
keys.d = false;
break;
case 'KeyE':
keys.e = false;
break;
case 'KeyQ':
keys.q = false;
break;
case 'KeyR':
keys.r = false;
break;
case 'Tab':
keys.tab = false;
break;
}
});
// ===== RESSOURCEN-KLASSE =====
/**
* Die Resource-Klasse repräsentiert sammelbare Objekte in der Spielwelt
*
* Verschiedene Ressourcentypen:
* - 'tree': Bäume für Holz
* - 'rock': Felsen für Stein
*/
class Resource {
/**
* Konstruktor für eine neue Ressource
* @param {number} x - X-Position
* @param {number} y - Y-Position
* @param {string} type - Typ der Ressource ('tree', 'rock', 'stick')
* @param {string} spriteKey - Schlüssel für das Sprite im AssetManager
*/
constructor(x, y, type, spriteKey = null) {
this.x = x;
this.y = y;
this.type = type;
this.spriteKey = spriteKey;
// Eigenschaften basierend auf Typ setzen
if (type === 'tree') {
this.width = 64;
this.height = 64;
this.color = '#228B22'; // Waldgrün (Fallback)
this.resource = 'wood'; // Was der Spieler erhält
this.amount = 3; // Wie viel er erhält
this.spriteKey = spriteKey || 'tree1';
} else if (type === 'rock') {
this.width = 48;
this.height = 48;
this.color = '#696969'; // Dunkelgrau (Fallback)
this.resource = 'stone'; // Was der Spieler erhält
this.amount = 2; // Wie viel er erhält
this.spriteKey = spriteKey || 'stone1';
} else if (type === 'stick') {
this.width = 32;
this.height = 32;
this.color = '#8B4513'; // Braun (Fallback)
this.resource = 'stick'; // Was der Spieler erhält
this.amount = 1; // Wie viel er erhält
this.spriteKey = spriteKey || 'stick1';
}
// Ob die Ressource noch sammelbar ist
this.collected = false;
}
/**
* Prüft, ob der Spieler diese Ressource berührt (Kollisionserkennung)
* @param {Player} player - Der Spieler
* @returns {boolean} - True wenn Kollision
*/
checkCollision(player) {
// AABB (Axis-Aligned Bounding Box) Kollisionserkennung
// Zwei Rechtecke überlappen sich, wenn sie sich in beiden Achsen überlappen
// Grenzen der Ressource
const resLeft = this.x - this.width / 2;
const resRight = this.x + this.width / 2;
const resTop = this.y - this.height / 2;
const resBottom = this.y + this.height / 2;
// Grenzen des Spielers
const playerLeft = player.x - player.width / 2;
const playerRight = player.x + player.width / 2;
const playerTop = player.y - player.height / 2;
const playerBottom = player.y + player.height / 2;
// Kollision prüfen
return resLeft < playerRight &&
resRight > playerLeft &&
resTop < playerBottom &&
resBottom > playerTop;
}
/**
* Sammelt die Ressource und gibt sie an das Inventar weiter
* @param {Object} inventory - Das Spieler-Inventar
* @param {Player} player - Der Spieler (für Werkzeug-Überprüfung)
*/
collect(player) {
if (!this.collected) {
// Prüfen ob das richtige Werkzeug vorhanden ist
if (this.type === 'tree' && !player.tools.axe) {
console.log('🪓 Du brauchst eine Axt um Bäume zu fällen!');
return false;
}
if (this.type === 'rock' && !player.tools.pickaxe) {
console.log('⛏️ Du brauchst eine Spitzhacke um Steine abzubauen!');
return false;
}
this.collected = true;
// Ressource zum Spieler-Inventar hinzufügen
if (!player.inventory[this.resource]) {
player.inventory[this.resource] = 0;
}
player.inventory[this.resource] += this.amount;
console.log(`📦 ${this.amount} ${this.resource} gesammelt! Gesamt: ${player.inventory[this.resource]}`);
return true;
}
return false;
}
/**
* Zeichnet die Ressource auf das Canvas
* @param {CanvasRenderingContext2D} ctx - Der Canvas-Kontext
* @param {Camera} camera - Die Kamera für Koordinaten-Transformation
*/
render(ctx, camera) {
if (!this.collected) {
// Weltkoordinaten zu Bildschirmkoordinaten konvertieren
const screenPos = camera.worldToScreen(this.x, this.y);
// Nur zeichnen wenn im sichtbaren Bereich
if (screenPos.x + this.width/2 < 0 || screenPos.x - this.width/2 > CANVAS_WIDTH ||
screenPos.y + this.height/2 < 0 || screenPos.y - this.height/2 > CANVAS_HEIGHT) {
return; // Außerhalb des Bildschirms
}
// Schatten zeichnen (für 3D-Effekt)
if (assetManager.isLoaded && (this.type === 'tree' || this.type === 'rock')) {
const shadowKey = this.type === 'tree' ? 'shadow2' : 'shadow1';
assetManager.drawSprite(
ctx,
shadowKey,
screenPos.x - this.width/2 + 5,
screenPos.y - this.height/2 + this.height - 10,
this.width,
16
);
}
// Sprite zeichnen falls Assets geladen sind
if (assetManager.isLoaded && this.spriteKey) {
assetManager.drawSprite(
ctx,
this.spriteKey,
screenPos.x - this.width/2,
screenPos.y - this.height/2,
this.width,
this.height
);
} else {
// Fallback: Einfache Rechtecke
ctx.fillStyle = this.color;
ctx.fillRect(
screenPos.x - this.width / 2,
screenPos.y - this.height / 2,
this.width,
this.height
);
// Schwarzer Rand
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1;
ctx.strokeRect(
screenPos.x - this.width / 2,
screenPos.y - this.height / 2,
this.width,
this.height
);
// Spezielle Darstellung für Bäume (Krone)
if (this.type === 'tree') {
// Baumkrone (Kreis oben)
ctx.fillStyle = '#006400'; // Dunkelgrün
ctx.beginPath();
ctx.arc(screenPos.x, screenPos.y - this.height/3, this.width/2, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
}
}
}
}
}
// ===== LAGERFEUER-KLASSE =====
/**
* Die Campfire-Klasse repräsentiert ein gebautes Lagerfeuer
*
* Funktionen:
* - Regeneriert Spieler-Gesundheit in der Nähe
* - Kann mit Holz gebaut werden
* - Visueller Indikator für sichere Zonen
*/
class Campfire {
/**
* Konstruktor für ein neues Lagerfeuer
* @param {number} x - X-Position
* @param {number} y - Y-Position
*/
constructor(x, y) {
this.x = x;
this.y = y;
this.width = 48;
this.height = 48;
// Animation für Torch-Sprites
this.animationTimer = 0;
this.animationSpeed = 0.15; // Geschwindigkeit der Animation
this.currentFrame = 0;
this.totalFrames = 4; // torch1, torch2, torch3, torch4
}
/**
* Aktualisiert die Lagerfeuer-Animation
*/
update() {
// Torch-Sprite Animation
this.animationTimer += this.animationSpeed;
if (this.animationTimer >= 1) {
this.currentFrame = (this.currentFrame + 1) % this.totalFrames;
this.animationTimer = 0;
}
}
/**
* Zeichnet das Lagerfeuer
* @param {CanvasRenderingContext2D} ctx - Der Canvas-Kontext
* @param {Camera} camera - Die Kamera für Koordinaten-Transformation
*/
render(ctx, camera) {
// Weltkoordinaten zu Bildschirmkoordinaten konvertieren
const screenPos = camera.worldToScreen(this.x, this.y);
// Nur zeichnen wenn im sichtbaren Bereich
if (screenPos.x + 50 < 0 || screenPos.x - 50 > CANVAS_WIDTH ||
screenPos.y + 50 < 0 || screenPos.y - 50 > CANVAS_HEIGHT) {
return; // Außerhalb des Bildschirms
}
// Heilungsradius anzeigen (halbtransparent)
ctx.fillStyle = 'rgba(0, 255, 0, 0.1)';
ctx.beginPath();
ctx.arc(screenPos.x, screenPos.y, 50, 0, Math.PI * 2);
ctx.fill();
// Rand um Heilungsradius
ctx.strokeStyle = 'rgba(0, 255, 0, 0.3)';
ctx.lineWidth = 2;
ctx.stroke();
// Animiertes Torch-Sprite zeichnen
if (assetManager.isLoaded) {
const torchKey = `torch${this.currentFrame + 1}`;
assetManager.drawSprite(
ctx,
torchKey,
screenPos.x - this.width/2,
screenPos.y - this.height/2,
this.width,
this.height
);
} else {
// Fallback: Einfaches Lagerfeuer
ctx.fillStyle = '#8B4513'; // Braun
ctx.fillRect(
screenPos.x - this.width/2,
screenPos.y - this.height/2,
this.width,
this.height
);
// Flammen (animiert)
ctx.fillStyle = '#FF4500'; // Orange-Rot
ctx.fillRect(
screenPos.x - this.width/3,
screenPos.y - this.height/2 - 15,
this.width/1.5,
15
);
}
}
}
// ===== SPIEL-OBJEKTE =====
// Welt-Instanz erstellen
let world;
// Spieler-Instanz erstellen (Startposition in der Weltmitte)
let player;
// Array für alle Lagerfeuer (bleiben global, da sie vom Spieler gebaut werden)
let campfires = [];
// Crafting-Rezepte
const recipes = {
axe: {
name: 'Axt',
cost: { stick: 2 },
description: 'Ermöglicht das Fällen von Bäumen',
tool: true
},
pickaxe: {
name: 'Spitzhacke',
cost: { stick: 1, wood: 2 },
description: 'Ermöglicht das Abbauen von Steinen',
tool: true
},
campfire: {
name: 'Lagerfeuer',
cost: { wood: 5 },
description: 'Regeneriert Gesundheit in der Nähe'
}
};
/**
* Versucht ein Item zu craften
* @param {string} itemType - Der Typ des Items (z.B. 'axe', 'pickaxe', 'campfire')
*/
function craftItem(itemType) {
const recipe = recipes[itemType];
if (!recipe) {
console.log(`❌ Unbekanntes Rezept: ${itemType}`);
return false;
}
// Prüfen ob genügend Ressourcen vorhanden sind
for (const [resource, amount] of Object.entries(recipe.cost)) {
if (!player.inventory[resource] || player.inventory[resource] < amount) {
console.log(`❌ Nicht genügend ${resource}! Benötigt: ${amount}, Vorhanden: ${player.inventory[resource] || 0}`);
return false;
}
}
// Spezielle Überprüfungen für Werkzeuge
if (itemType === 'axe' && player.tools.axe) {
console.log('🪓 Du hast bereits eine Axt!');
return false;
}
if (itemType === 'pickaxe' && player.tools.pickaxe) {
console.log('⛏️ Du hast bereits eine Spitzhacke!');
return false;
}
// Ressourcen abziehen
for (const [resource, amount] of Object.entries(recipe.cost)) {
player.inventory[resource] -= amount;
}
// Item erstellen
if (itemType === 'axe') {
player.tools.axe = true;
console.log(`🪓 ${recipe.name} erfolgreich hergestellt!`);
} else if (itemType === 'pickaxe') {
player.tools.pickaxe = true;
console.log(`⛏️ ${recipe.name} erfolgreich hergestellt!`);
} else if (itemType === 'campfire') {
// Lagerfeuer an Spielerposition erstellen
campfires.push(new Campfire(player.x, player.y));
console.log(`🔥 ${recipe.name} erfolgreich gebaut!`);
}
return true;
}
// Die Ressourcen-Generierung wird jetzt von den Chunks übernommen!
// Jeder Chunk generiert automatisch Ressourcen basierend auf seiner Position
// ===== GRUNDLEGENDE ZEICHENFUNKTIONEN =====
/**
* Diese Funktion löscht das gesamte Canvas und zeichnet einen blauen Hintergrund
*
* Warum brauchen wir das?
* - In jedem Frame müssen wir das Canvas "sauber machen"
* - Sonst würden sich die gezeichneten Objekte überlagern
* - Der blaue Hintergrund simuliert einen Himmel oder Wasser
*/
function clearCanvas() {
// Schritt 1: Das gesamte Canvas löschen (transparent machen)
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// Schritt 2: Einen blauen Hintergrund zeichnen
ctx.fillStyle = '#87CEEB'; // Himmelblau (Sky Blue)
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
}
/**
* Die Render-Funktion - hier wird alles gezeichnet
*
* Das ist das Herzstück unseres Spiels:
* - Wird in jedem Frame aufgerufen
* - Zeichnet alle Spielobjekte in der richtigen Reihenfolge
* - Reihenfolge: Hintergrund → Boden-Tiles → Schatten → Objekte (Y-sortiert) → UI
*/
function render() {
// Canvas leeren und Hintergrund zeichnen
clearCanvas();
if (!world || !player) return;
// 1. Boden-Tiles zeichnen (unterste Ebene)
for (const chunk of world.chunks.values()) {
if (chunk.isVisible(world.camera)) {
chunk.renderTiles(ctx, world.camera);
}
}
// 2. Alle sichtbaren Objekte sammeln und nach Y-Position sortieren
const allObjects = [];
// Ressourcen hinzufügen (nur sichtbare)
const allResources = world.getAllResources();
allResources.forEach(resource => {
if (!resource.collected) {
const screenPos = world.camera.worldToScreen(resource.x, resource.y);
// Nur hinzufügen wenn im sichtbaren Bereich
if (screenPos.x + resource.width >= -50 && screenPos.x <= CANVAS_WIDTH + 50 &&
screenPos.y + resource.height >= -50 && screenPos.y <= CANVAS_HEIGHT + 50) {
allObjects.push({
type: 'resource',
object: resource,
// Sortierung nach Fußpunkt für korrekte Tiefe
y: resource.y + resource.height,
zIndex: resource.type === 'stick' ? 1 : (resource.type === 'tree' ? 3 : 2)
});
}
}
});
// Lagerfeuer hinzufügen (nur sichtbare)
campfires.forEach(campfire => {
const screenPos = world.camera.worldToScreen(campfire.x, campfire.y);
if (screenPos.x + 50 >= -50 && screenPos.x <= CANVAS_WIDTH + 50 &&
screenPos.y + 50 >= -50 && screenPos.y <= CANVAS_HEIGHT + 50) {
allObjects.push({
type: 'campfire',
object: campfire,
y: campfire.y + campfire.height,
zIndex: 2
});
}
});
// Spieler hinzufügen
allObjects.push({
type: 'player',
object: player,
y: player.y + player.height,
zIndex: 4 // Spieler immer über allem anderen
});
// 3. Nach Z-Index und dann Y-Position sortieren
allObjects.sort((a, b) => {
// Erst nach Z-Index sortieren
if (a.zIndex !== b.zIndex) {
return a.zIndex - b.zIndex;
}
// Dann nach Y-Position (Tiefe)
return a.y - b.y;
});
// 4. Alle Objekte in der sortierten Reihenfolge zeichnen
allObjects.forEach(item => {
switch(item.type) {
case 'resource':
item.object.render(ctx, world.camera);
break;
case 'campfire':
item.object.render(ctx, world.camera);
break;
case 'player':
item.object.render(ctx, world.camera);
break;
}
});
// 5. UI zeichnen (immer ganz oben)
renderUI();
// Hier werden später weitere Spielobjekte gezeichnet:
// - Feinde
// - Partikeleffekte
// - Weitere Gebäude
}
/**
* Zeichnet die Benutzeroberfläche (UI)
* Zeigt Gesundheit immer an, Inventar nur bei Tab-Taste
*/
function renderUI() {
// Gesundheitsbalken (immer sichtbar, oben links)
const healthBarWidth = 200;
const healthBarHeight = 20;
const healthBarX = 10;
const healthBarY = 10;
// Hintergrund der Gesundheitsleiste
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.fillRect(healthBarX, healthBarY, healthBarWidth, healthBarHeight + 30);
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 2;
ctx.strokeRect(healthBarX, healthBarY, healthBarWidth, healthBarHeight + 30);
// Gesundheits-Text
ctx.fillStyle = '#FFFFFF';
ctx.font = '16px Arial';
ctx.fillText('❤️ Gesundheit:', healthBarX + 10, healthBarY + 20);
// Gesundheitsbalken-Hintergrund
ctx.fillStyle = '#333333';
ctx.fillRect(healthBarX + 10, healthBarY + 25, healthBarWidth - 20, 15);
// Gesundheitsbalken (rot bis grün je nach Gesundheit)
const healthPercent = player ? player.health / player.maxHealth : 0;
const healthColor = healthPercent > 0.6 ? '#00FF00' :
healthPercent > 0.3 ? '#FFFF00' : '#FF0000';
ctx.fillStyle = healthColor;
ctx.fillRect(healthBarX + 10, healthBarY + 25, (healthBarWidth - 20) * healthPercent, 15);
// Gesundheits-Zahlen
ctx.fillStyle = '#FFFFFF';
ctx.font = '12px Arial';
const healthText = player ? `${Math.round(player.health)}/${player.maxHealth}` : '0/100';
ctx.fillText(healthText, healthBarX + healthBarWidth/2 - 15, healthBarY + 36);
// Tab-Hinweis (klein, unten rechts)
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(CANVAS_WIDTH - 120, CANVAS_HEIGHT - 30, 110, 20);
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 1;
ctx.strokeRect(CANVAS_WIDTH - 120, CANVAS_HEIGHT - 30, 110, 20);
ctx.fillStyle = '#FFFFFF';
ctx.font = '12px Arial';
ctx.fillText('Tab = Inventar', CANVAS_WIDTH - 115, CANVAS_HEIGHT - 15);
// Inventar nur anzeigen wenn Tab gedrückt wurde
if (showInventory) {
renderInventoryPanel();
}
}
/**
* Zeichnet das Inventar-Panel (nur wenn sichtbar)
*/
function renderInventoryPanel() {
// Inventar-Panel (zentriert)
const panelWidth = 500;
const panelHeight = 300;
const panelX = (CANVAS_WIDTH - panelWidth) / 2;
const panelY = (CANVAS_HEIGHT - panelHeight) / 2;
// Halbtransparenter Hintergrund über dem ganzen Bildschirm
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
// Inventar-Panel
ctx.fillStyle = 'rgba(20, 20, 20, 0.95)';
ctx.fillRect(panelX, panelY, panelWidth, panelHeight);
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 3;
ctx.strokeRect(panelX, panelY, panelWidth, panelHeight);
// Titel
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 24px Arial';
ctx.fillText('📦 INVENTAR', panelX + 20, panelY + 40);
// Schließen-Hinweis
ctx.font = '14px Arial';
ctx.fillText('Tab drücken zum Schließen', panelX + panelWidth - 180, panelY + 40);
// Ressourcen (linke Spalte)
ctx.font = '18px Arial';
ctx.fillText('🎒 Ressourcen:', panelX + 30, panelY + 80);
ctx.font = '16px Arial';
ctx.fillText(`🪵 Stöcker: ${player.inventory.stick}`, panelX + 50, panelY + 110);
ctx.fillText(`🌳 Holz: ${player.inventory.wood}`, panelX + 50, panelY + 140);
ctx.fillText(`🪨 Stein: ${player.inventory.stone}`, panelX + 50, panelY + 170);
// Werkzeuge (rechte Spalte)
ctx.font = '18px Arial';
ctx.fillText('🔧 Werkzeuge:', panelX + 280, panelY + 80);
ctx.font = '16px Arial';
ctx.fillText(`🪓 Axt: ${player.tools.axe ? '✅ Vorhanden' : '❌ Nicht vorhanden'}`, panelX + 300, panelY + 110);
ctx.fillText(`⛏️ Pickaxe: ${player.tools.pickaxe ? '✅ Vorhanden' : '❌ Nicht vorhanden'}`, panelX + 300, panelY + 140);
// Crafting-Rezepte (unten)
ctx.font = '18px Arial';
ctx.fillText('🔨 Crafting-Rezepte:', panelX + 30, panelY + 220);
ctx.font = '14px Arial';
ctx.fillText('Q = Axt (2 Stöcker)', panelX + 50, panelY + 250);
ctx.fillText('R = Pickaxe (1 Stock + 2 Holz)', panelX + 220, panelY + 250);
ctx.fillText('E = Lagerfeuer (5 Holz)', panelX + 50, panelY + 275);
}
/**
* Die Update-Funktion - hier wird die Spiellogik berechnet
*
* Trennung von Logik und Darstellung:
* - update() = Was passiert (Bewegung, Kollisionen, Spielregeln)
* - render() = Wie es aussieht (Zeichnen auf das Canvas)
*/
function update() {
// Spieler aktualisieren (Bewegung basierend auf Eingaben)
if (player) {
player.update();
}
// Welt aktualisieren (Chunks laden/entladen basierend auf Spielerposition)
if (world) {
world.update(player);
}
// Lagerfeuer aktualisieren (Animation)
campfires.forEach(campfire => {
campfire.update();
});
// Kollisionserkennung: Spieler mit Ressourcen aus allen geladenen Chunks
if (world) {
const allResources = world.getAllResources();
allResources.forEach(resource => {
if (!resource.collected && resource.checkCollision(player)) {
resource.collect(player);
}
});
}
// Tab-Taste für Inventar-Toggle
if (keys.tab) {
showInventory = !showInventory;
keys.tab = false; // Verhindert mehrfaches Umschalten
}
// Crafting-Eingaben prüfen
if (keys.q) {
craftItem('axe');
keys.q = false; // Verhindert mehrfaches Crafting
}
if (keys.r) {
craftItem('pickaxe');
keys.r = false;
}
if (keys.e) {
craftItem('campfire');
keys.e = false;
}
// Hier wird später weitere Spiellogik stehen:
// - Feinde bewegen
// - Weitere Crafting-Rezepte
// - Wetter-System
}
/**
* Der Game Loop - das Herz jedes Spiels
*
* Was ist ein Game Loop?
* - Eine endlose Schleife, die das Spiel am Leben hält
* - Läuft idealerweise 60 mal pro Sekunde (60 FPS)
* - In jedem Durchlauf: Update → Render → Warten → Wiederholen
*
* requestAnimationFrame():
* - Browser-optimierte Funktion für Animationen
* - Synchronisiert sich automatisch mit der Bildschirmfrequenz
* - Pausiert automatisch, wenn der Tab nicht sichtbar ist (Energiesparen)
*/
function gameLoop() {
// 1. Spiellogik aktualisieren
update();
// 2. Alles neu zeichnen
render();
// 3. Nächsten Frame anfordern (rekursiver Aufruf)
requestAnimationFrame(gameLoop);
}
// ===== SPIEL STARTEN =====
/**
* Initialisierungsfunktion - wird einmal beim Laden der Seite aufgerufen
*/
async function init() {
console.log('🎮 Pixel-Überlebender wird gestartet...');
console.log(`📐 Canvas-Größe: ${CANVAS_WIDTH}x${CANVAS_HEIGHT} Pixel`);
// Assets laden
console.log('🎨 Lade Grafiken...');
const assetsLoaded = await assetManager.loadAllAssets();
if (!assetsLoaded) {
console.error('❌ Fehler beim Laden der Assets! Spiel startet mit Fallback-Grafiken.');
}
// Welt erstellen
world = new World();
console.log('🌍 Unendliche prozedurale Welt erstellt!');
// Spieler in der Weltmitte erstellen (0, 0 in Weltkoordinaten)
player = new Player(0, 0);
console.log(`👤 Spieler erstellt an Position (${player.x}, ${player.y})`);
// Ersten Frame zeichnen
render();
// Game Loop starten
gameLoop();
console.log('✅ Spiel erfolgreich gestartet!');
console.log('🎯 Verwende Pfeiltasten oder WASD zum Bewegen!');
console.log('📦 Berühre Bäume und Felsen zum Sammeln!');
console.log('📋 Tab = Inventar öffnen/schließen');
console.log('🔨 Crafting: Q=Axt, R=Pickaxe, E=Lagerfeuer');
console.log('🪵 Sammle zuerst Stöcker für eine Axt!');
console.log('🪓 Mit Axt kannst du Bäume fällen');
console.log('⛏️ Mit Pickaxe kannst du Steine abbauen');
console.log('❤️ Deine Gesundheit nimmt langsam ab - bleibe am Lagerfeuer!');
console.log('🗺️ Erkunde die unendliche Welt - neue Chunks werden automatisch geladen!');
if (assetsLoaded) {
console.log('✨ Alle Grafiken geladen! Genieße das verbesserte Spielerlebnis!');
}
}
// Warten bis die HTML-Seite vollständig geladen ist, dann das Spiel starten
// Das ist wichtig, damit das Canvas-Element bereits existiert
window.addEventListener('load', init);
// ===== ERKLÄRUNG DER CANVAS-GRUNDLAGEN =====
/*
WAS IST DAS CANVAS-ELEMENT?
============================
Das <canvas>-Element ist ein HTML5-Element, das uns eine programmierbare
Zeichenfläche zur Verfügung stellt. Stell dir vor, es ist wie ein digitales
Blatt Papier, auf das wir mit JavaScript zeichnen können.
Wichtige Eigenschaften:
- Pixelbasiert: Jeder Punkt auf dem Canvas ist ein Pixel
- Koordinatensystem: (0,0) ist oben links, X geht nach rechts, Y nach unten
- Sofortiger Modus: Was gezeichnet wird, bleibt bis zum nächsten Löschen
WIE GREIFEN WIR DARAUF ZU?
==========================
1. Element holen: document.getElementById('gameCanvas')
2. Kontext erhalten: canvas.getContext('2d')
3. Mit dem Kontext zeichnen: ctx.fillRect(), ctx.drawImage(), etc.
DER 2D-KONTEXT:
===============
Der Kontext ist unser "Werkzeugkasten" zum Zeichnen:
- fillRect(): Gefüllte Rechtecke
- strokeRect(): Rechteck-Umrisse
- fillStyle: Füllfarbe setzen
- clearRect(): Bereich löschen
- drawImage(): Bilder zeichnen
KOORDINATENSYSTEM:
==================
(0,0) -----> X (800)
|
|
|
v
Y (600)
Das ist anders als in der Mathematik - hier ist Y=0 oben!
*/