// game.js — Main Game Loop const TILE_WIDTH = 64; const TILE_HEIGHT = 32; class Game { constructor() { this.canvas = document.getElementById('game-canvas'); this.ctx = this.canvas.getContext('2d'); this.minimapCanvas = document.getElementById('minimap'); this.minimapCtx = this.minimapCanvas.getContext('2d'); this.camera = new Camera(); this.map = TileMap.generateEichenhafen(); this.player = new Player(20, 20); this.monsters = []; this.npcs = []; this.damageNumbers = []; this.events = []; // Spawn some NPCs this.spawnNPCs(); // Spawn some monsters outside the city this.spawnMonsters(); this.input = new InputHandler(this.canvas, this); this.running = true; this.frameCount = 0; this.updateUI(); this.addEvent('Willkommen in Eichenhafen!', 'system'); this.addEvent('Klicke um dich zu bewegen. Klicke auf Monster zum angreifen.', 'system'); this.loop(); } spawnNPCs() { // NPC near the first building this.npcs.push({ id: 'merchant_eichenhafen', name: 'Händler Bruno', tileX: 22, tileY: 19, type: 'merchant', dialogue: 'Willkommen! Schau dich in meinem Laden um.' }); // NPC near second building this.npcs.push({ id: 'class_master', name: 'Meisterin Vera', tileX: 15, tileY: 19, type: 'class_master', dialogue: 'Wenn du Level 10 erreichst, kann ich dir eine Klasse zuweisen.' }); // NPC near third building this.npcs.push({ id: 'innkeeper', name: 'Wirtin Helga', tileX: 15, tileY: 29, type: 'innkeeper', dialogue: 'Ruhe dich aus. Deine Wunden heilen in der Schenke.' }); } spawnMonsters() { // Spawn wolves outside the city area const spawnArea = [ {x: 30, y: 5}, {x: 32, y: 8}, {x: 28, y: 10}, {x: 35, y: 15}, {x: 33, y: 25}, {x: 30, y: 30} ]; for (const pos of spawnArea) { this.monsters.push({ id: Math.random().toString(36).substr(2, 9), name: 'Junger Wolf', tileX: pos.x, tileY: pos.y, x: pos.x, y: pos.y, hp: 30, maxHp: 30, atk: 8, def: 2, level: 1, exp: 15, gold: 5, aggroRange: 4, attackCooldown: 0, attackSpeed: 45, alive: true, respawnTimer: 0 }); } } findPath(start, end) { // Simple A* pathfinding const openSet = [{x: start.x, y: start.y, g: 0, h: 0, f: 0, parent: null}]; const closedSet = []; const gridSize = this.map.width * this.map.height; while (openSet.length > 0) { // Find lowest f openSet.sort((a, b) => a.f - b.f); const current = openSet.shift(); if (current.x === end.x && current.y === end.y) { // Reconstruct path const path = []; let node = current; while (node.parent) { path.unshift({x: node.x, y: node.y}); node = node.parent; } return path; } closedSet.push(current); // Check neighbors const neighbors = [ {x: current.x + 1, y: current.y}, {x: current.x - 1, y: current.y}, {x: current.x, y: current.y + 1}, {x: current.x, y: current.y - 1}, // Diagonal {x: current.x + 1, y: current.y + 1}, {x: current.x - 1, y: current.y - 1}, {x: current.x + 1, y: current.y - 1}, {x: current.x - 1, y: current.y + 1} ]; for (const n of neighbors) { if (n.x < 0 || n.x >= this.map.width || n.y < 0 || n.y >= this.map.height) continue; if (this.map.isSolid(n.x, n.y)) continue; // Skip if in closed set if (closedSet.find(c => c.x === n.x && c.y === n.y)) continue; const g = current.g + 1; const h = Math.abs(n.x - end.x) + Math.abs(n.y - end.y); const f = g + h; // Skip if already in open set with lower f const existing = openSet.find(o => o.x === n.x && o.y === n.y); if (existing && existing.f <= f) continue; openSet.push({x: n.x, y: n.y, g, h, f, parent: current}); } // Prevent infinite loop if (closedSet.length > 500) break; } return null; } update() { this.frameCount++; // Update player this.player.update(this.map); // Camera follows player const playerScreen = this.map.tileToScreen(this.player.x, this.player.y); this.camera.follow(playerScreen.x - TILE_WIDTH/2, playerScreen.y - TILE_HEIGHT/2); this.camera.update(); // Update monsters for (const monster of this.monsters) { if (!monster.alive) { monster.respawnTimer--; if (monster.respawnTimer <= 0) { monster.alive = true; monster.hp = monster.maxHp; } continue; } // Aggro check const dx = this.player.x - monster.x; const dy = this.player.y - monster.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < monster.aggroRange) { // Chase player if (dist > 1.2) { monster.x += (dx / dist) * 0.05; monster.y += (dy / dist) * 0.05; monster.tileX = Math.floor(monster.x); monster.tileY = Math.floor(monster.y); } else { // Attack player if (monster.attackCooldown <= 0) { const damage = Math.max(1, monster.atk - this.player.def); this.player.takeDamage(damage); this.addDamageNumber(this.player.x, this.player.y, damage, '#ff4444'); this.addEvent(`Wolf greift an: -${damage} HP`, 'combat'); monster.attackCooldown = monster.attackSpeed; if (this.player.hp <= 0) { this.onPlayerDeath(); } } } } if (monster.attackCooldown > 0) monster.attackCooldown--; } // Player auto-attack if (this.player.attackTarget && this.player.attackTarget.alive) { const target = this.player.attackTarget; const dx = target.x - this.player.x; const dy = target.y - this.player.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist <= this.player.attackRange) { // In range — attack if (this.player.attackCooldown <= 0) { const damage = Math.max(1, this.player.atk - target.def); target.hp -= damage; this.addDamageNumber(target.x, target.y, damage, '#ffff44'); this.addEvent(`Du trifst ${target.name}: -${damage} HP`, 'combat'); this.player.attackCooldown = this.player.attackSpeed; if (target.hp <= 0) { target.alive = false; target.respawnTimer = 300; // 5 seconds at 60fps this.player.gainExp(target.exp); this.player.gold += target.gold; this.addEvent(`${target.name} besiegt! +${target.exp} EXP, +${target.gold} Gold`, 'combat'); this.player.attackTarget = null; } } } else { // Move towards target const tile = {x: Math.floor(target.x), y: Math.floor(target.y)}; const playerTile = {x: Math.floor(this.player.x), y: Math.floor(this.player.y)}; const path = this.findPath(playerTile, tile); if (path) this.player.setPath(path); } } // Update damage numbers this.damageNumbers = this.damageNumbers.filter(d => { d.y -= 0.5; d.life--; return d.life > 0; }); // Update UI if (this.frameCount % 10 === 0) { this.updateUI(); } } render() { // Clear this.ctx.fillStyle = '#1a1a2e'; this.ctx.fillRect(0, 0, 960, 640); // Render map this.map.render(this.ctx, this.camera); // Render NPCs for (const npc of this.npcs) { const screen = this.map.tileToScreen(npc.tileX, npc.tileY); const drawX = screen.x - this.camera.x + 480; const drawY = screen.y - this.camera.y + 320; // NPC body (different colors by type) const colors = { merchant: '#4a8a4a', class_master: '#8a4a8a', innkeeper: '#8a8a4a' }; ctx = this.ctx; ctx.fillStyle = colors[npc.type] || '#666'; ctx.fillRect(drawX - 5, drawY + TILE_HEIGHT/2 - 15, 10, 12); ctx.fillStyle = '#e0b890'; ctx.beginPath(); ctx.arc(drawX, drawY + TILE_HEIGHT/2 - 18, 6, 0, Math.PI * 2); ctx.fill(); // Name ctx.fillStyle = '#88ff88'; ctx.font = '10px sans-serif'; ctx.textAlign = 'center'; ctx.fillText(npc.name, drawX, drawY + TILE_HEIGHT/2 - 26); // "!" indicator for interactable ctx.fillStyle = '#ffcc44'; ctx.font = '14px sans-serif'; ctx.fillText('!', drawX + 8, drawY + TILE_HEIGHT/2 - 22); } // Render monsters for (const monster of this.monsters) { if (!monster.alive) continue; const screen = this.map.tileToScreen(monster.x, monster.y); const drawX = screen.x - this.camera.x + 480; const drawY = screen.y - this.camera.y + 320; // Shadow this.ctx.fillStyle = 'rgba(0,0,0,0.3)'; this.ctx.beginPath(); this.ctx.ellipse(drawX, drawY + TILE_HEIGHT/2 + 2, 8, 3, 0, 0, Math.PI * 2); this.ctx.fill(); // Wolf body (gray) this.ctx.fillStyle = '#666'; this.ctx.fillRect(drawX - 6, drawY + TILE_HEIGHT/2 - 10, 12, 8); this.ctx.fillStyle = '#555'; ctx.beginPath(); ctx.arc(drawX, drawY + TILE_HEIGHT/2 - 12, 5, 0, Math.PI * 2); ctx.fill(); // Eyes (red) this.ctx.fillStyle = '#ff3333'; this.ctx.fillRect(drawX - 3, drawY + TILE_HEIGHT/2 - 13, 2, 2); this.ctx.fillRect(drawX + 1, drawY + TILE_HEIGHT/2 - 13, 2, 2); // Name + HP bar this.ctx.fillStyle = '#ff8888'; this.ctx.font = '9px sans-serif'; this.ctx.textAlign = 'center'; this.ctx.fillText(monster.name, drawX, drawY + TILE_HEIGHT/2 - 22); const hpPercent = monster.hp / monster.maxHp; this.ctx.fillStyle = '#333'; this.ctx.fillRect(drawX - 12, drawY + TILE_HEIGHT/2 - 20, 24, 2); this.ctx.fillStyle = '#dd4444'; this.ctx.fillRect(drawX - 12, drawY + TILE_HEIGHT/2 - 20, 24 * hpPercent, 2); } // Render player this.player.render(this.ctx, this.map, this.camera); // Render damage numbers for (const dmg of this.damageNumbers) { const screen = this.map.tileToScreen(dmg.x, dmg.y); const drawX = screen.x - this.camera.x + 480; const drawY = screen.y - this.camera.y + 320 - (30 - dmg.life); this.ctx.fillStyle = dmg.color; this.ctx.font = 'bold 14px sans-serif'; this.ctx.textAlign = 'center'; this.ctx.globalAlpha = dmg.life / 30; this.ctx.fillText(dmg.value, drawX, drawY); this.ctx.globalAlpha = 1; } // Render minimap this.map.renderMinimap(this.minimapCtx, this.player); } addDamageNumber(x, y, value, color) { this.damageNumbers.push({x, y, value: Math.floor(value), color, life: 30}); } addEvent(text, type = 'normal') { const messages = document.getElementById('event-messages'); const div = document.createElement('div'); div.className = `event-msg ${type}`; div.textContent = text; messages.appendChild(div); messages.scrollTop = messages.scrollHeight; // Keep only last 20 messages while (messages.children.length > 20) { messages.removeChild(messages.firstChild); } } updateUI() { // HP const hpPercent = (this.player.hp / this.player.maxHp) * 100; document.getElementById('hp-bar').style.width = hpPercent + '%'; document.getElementById('hp-text').textContent = `${this.player.hp}/${this.player.maxHp}`; // SP const spPercent = (this.player.sp / this.player.maxSp) * 100; document.getElementById('sp-bar').style.width = spPercent + '%'; document.getElementById('sp-text').textContent = `${this.player.sp}/${this.player.maxSp}`; // EXP const expPercent = (this.player.exp / this.player.expNeeded) * 100; document.getElementById('exp-bar').style.width = expPercent + '%'; document.getElementById('exp-text').textContent = `${this.player.exp}/${this.player.expNeeded}`; // Char info document.getElementById('char-name').textContent = this.player.name; document.getElementById('char-level').textContent = `Lv. ${this.player.level}`; document.getElementById('char-class').textContent = this.player.class; } interactNPC(npc) { this.addEvent(`${npc.name}: "${npc.dialogue}"`, 'quest'); } useSkill(slot) { this.addEvent(`Skill ${slot} (noch nicht implementiert)`, 'system'); } toggleInventory() { this.addEvent('Inventar (noch nicht implementiert)', 'system'); } toggleQuestLog() { this.addEvent('Quest-Log (noch nicht implementiert)', 'system'); } closeMenus() { // Close any open menus } onPlayerDeath() { this.addEvent('Du bist gestorben! Respawne in Eichenhafen...', 'system'); this.player.hp = this.player.maxHp; this.player.sp = this.player.maxSp; this.player.x = 20; this.player.y = 20; this.player.path = []; this.player.attackTarget = null; this.player.exp = Math.max(0, this.player.exp - this.player.expNeeded * 0.1); } loop() { if (!this.running) return; this.update(); this.render(); requestAnimationFrame(() => this.loop()); } } // Start the game when page loads window.addEventListener('load', () => { new Game(); });