// ======================================================== // MYTHIC DEPTHS v3.0 — High Fantasy Mystery Dungeon RPG // Larger Map (72x52), Zero-to-Hero Starter, Header Materials, // Clean 1-Click Inventory, Auto-Save & Mobile Touch Controls // ======================================================== const MAP_W = 72; const MAP_H = 52; const MAX_FLOORS = 10; let tileSize = window.innerWidth < 640 ? 38 : 44; // --- Sound Synthesizer (Web Audio API) --- let audioCtx = null; let soundEnabled = true; function initAudio() { if (!audioCtx) { const AudioContext = window.AudioContext || window.webkitAudioContext; if (AudioContext) audioCtx = new AudioContext(); } } function playSound(type) { if (!soundEnabled) return; initAudio(); if (!audioCtx) return; try { if (audioCtx.state === "suspended") audioCtx.resume(); const now = audioCtx.currentTime; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); if (type === "step") { osc.type = "sine"; osc.frequency.setValueAtTime(135, now); osc.frequency.exponentialRampToValueAtTime(60, now + 0.045); gain.gain.setValueAtTime(0.04, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.045); osc.start(now); osc.stop(now + 0.045); } else if (type === "hit") { osc.type = "sawtooth"; osc.frequency.setValueAtTime(230, now); osc.frequency.exponentialRampToValueAtTime(65, now + 0.11); gain.gain.setValueAtTime(0.15, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.11); osc.start(now); osc.stop(now + 0.11); } else if (type === "shoot") { osc.type = "triangle"; osc.frequency.setValueAtTime(520, now); osc.frequency.exponentialRampToValueAtTime(160, now + 0.10); gain.gain.setValueAtTime(0.13, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.10); osc.start(now); osc.stop(now + 0.10); } else if (type === "spell") { osc.type = "sine"; osc.frequency.setValueAtTime(360, now); osc.frequency.linearRampToValueAtTime(760, now + 0.15); gain.gain.setValueAtTime(0.14, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18); osc.start(now); osc.stop(now + 0.18); } else if (type === "potion" || type === "craft") { osc.type = "triangle"; osc.frequency.setValueAtTime(330, now); osc.frequency.linearRampToValueAtTime(580, now + 0.12); gain.gain.setValueAtTime(0.12, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.22); osc.start(now); osc.stop(now + 0.22); } else if (type === "chest") { osc.type = "triangle"; osc.frequency.setValueAtTime(440, now); osc.frequency.setValueAtTime(587, now + 0.09); gain.gain.setValueAtTime(0.14, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25); osc.start(now); osc.stop(now + 0.25); } else if (type === "stairs" || type === "levelup") { [330, 415, 493, 659].forEach((freq, idx) => { const o = audioCtx.createOscillator(); const g = audioCtx.createGain(); o.type = "triangle"; o.connect(g); g.connect(audioCtx.destination); o.frequency.setValueAtTime(freq, now + idx * 0.07); g.gain.setValueAtTime(0.12, now + idx * 0.07); g.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.07 + 0.25); o.start(now + idx * 0.07); o.stop(now + idx * 0.07 + 0.25); }); } else if (type === "gameover") { osc.type = "sawtooth"; osc.frequency.setValueAtTime(210, now); osc.frequency.exponentialRampToValueAtTime(55, now + 0.55); gain.gain.setValueAtTime(0.16, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.55); osc.start(now); osc.stop(now + 0.55); } } catch (e) {} } function toggleSound() { soundEnabled = !soundEnabled; const icon = document.getElementById("icon-sound"); if (icon) icon.textContent = soundEnabled ? "🔊" : "🔇"; showToast(soundEnabled ? "Sound aktiviert" : "Sound stummgeschaltet"); } function cycleZoom() { const sizes = [32, 38, 46, 54]; const idx = sizes.indexOf(tileSize); tileSize = sizes[(idx + 1) % sizes.length]; showToast(`🔍 Karten-Zoom: ${tileSize}px`); } // --- 3 Selectable 10-Floor Dungeons --- const DUNGEONS_DB = { catacombs: { id: "catacombs", name: "Vergessene Katakomben", icon: "🕯️", recLevel: 1, diffMult: 1.0, goldMult: 1.0, bossId: "boss_goblin", midBossId: "midboss_orc", desc: "10 Ebenen unter der Feste Grimhold. Ideal für den Einstieg (Stufe 1–6).", wallTint: null, floorTint: null }, frost_caverns: { id: "frost_caverns", name: "Frostklamm-Kavernen", icon: "❄️", recLevel: 4, diffMult: 1.38, goldMult: 1.5, bossId: "boss_frostwyrm", midBossId: "golem", desc: "10 vereiste Tiefen voller Mana-Kristalle & Frostbestien (+50% Gold & Kristall-Beute, empf. ab St. 4).", wallTint: "#1e3a5f", floorTint: "#0f223d" }, magma_sanctum: { id: "magma_sanctum", name: "Glutkern-Sanktum", icon: "🌋", recLevel: 7, diffMult: 1.80, goldMult: 2.0, bossId: "boss_ignis", midBossId: "boss_goblin", desc: "10 glühende Vulkankammern voller Feueressenz & Mythril (+100% Gold, reich an 🔥 & 🔷, empf. ab St. 7).", wallTint: "#541b1b", floorTint: "#2b0e0e" } }; // --- 10 Dungeon Zones --- const ZONES = [ { floor: 1, name: "Verlassener Schacht", desc: "Sammle Holz, Kräuter, Wandfackeln (🔥) und Erz für deine Ausrüstung.", wall: "#2e2540", floor: "#1b1728" }, { floor: 2, name: "Goblin-Minen", desc: "Kleine Plünderer bewachen reiche Eisenerz-Adern.", wall: "#35283c", floor: "#1f1826" }, { floor: 3, name: "Kristall-Grotten", desc: "Leuchtende Mana-Kristalle und klappernde Skelett-Schützen.", wall: "#1b3152", floor: "#111f36" }, { floor: 4, name: "Spinnen-Katakomben", desc: "Dichte Netze und wertvolle Spinnenseide in dunklen Kammern.", wall: "#3f202a", floor: "#23141b" }, { floor: 5, name: "Wachstube des Aufsehers", desc: "Zwischenboss-Ebene! Der Kriegsherr versperrt den Weg.", wall: "#472518", floor: "#26150e" }, { floor: 6, name: "Tiefe Zwergenschmiede", desc: "Uralte Ambosse und seltene Mythril-Einschlüsse im Gestein.", wall: "#2c3540", floor: "#171d24" }, { floor: 7, name: "Schamanen-Heiligtum", desc: "Feueressenzen und arkane Rituale durchdringen die Luft.", wall: "#3d1e4d", floor: "#21102b" }, { floor: 8, name: "Golem-Kammern", desc: "Massive Kristall-Golems bewachen uralte Schätze.", wall: "#193b47", floor: "#0e2229" }, { floor: 9, name: "Vorhalle der Glut", desc: "Die Hitze des tiefsten Kerkers kündigt den Herrscher an.", wall: "#4d1919", floor: "#290d0d" }, { floor: 10, name: "Thronsaal des Endbosses", desc: "Endkampf auf Ebene 10! Bezwinge den Herrscher des Dungeons!", wall: "#4a1233", floor: "#29081d" } ]; // --- Full RPG Paperdoll Equipment Slots (7 Rüstung & Relikte + genau 1 Waffe für Nah- oder Fernkampf) --- const EQUIP_SLOT_META = [ { key: "helmet", side: "left", label: "Kopf / Hut", emptyIcon: "🪖" }, { key: "armor", side: "left", label: "Brust / Rüstung", emptyIcon: "👕" }, { key: "pants", side: "left", label: "Beine / Hose", emptyIcon: "👖" }, { key: "boots", side: "left", label: "Füße / Schuhe", emptyIcon: "👢" }, { key: "weapon", side: "right", label: "Waffe (Nah- oder Fernkampf)", emptyIcon: "⚔️" }, { key: "shield", side: "right", label: "Nebenhand / Schild", emptyIcon: "🛡️" }, { key: "ring", side: "right", label: "Relikt 1: Ring", emptyIcon: "💍" }, { key: "amulet", side: "right", label: "Relikt 2: Amulett", emptyIcon: "📿" } ]; const ALL_EQUIP_SLOTS = EQUIP_SLOT_META.map(s => s.key); const MATERIAL_META = { iron_ore: { id: "iron_ore", name: "Eisenerz", icon: "🪨" }, wood_branch: { id: "wood_branch", name: "Eschenholz", icon: "🪵" }, healing_herb: { id: "healing_herb", name: "Blutkraut", icon: "🌿" }, mana_shard: { id: "mana_shard", name: "Kristall", icon: "💎" }, spider_silk: { id: "spider_silk", name: "Spinnenseide", icon: "🕸️" }, monster_bone: { id: "monster_bone", name: "Knochen", icon: "🦴" }, fire_essence: { id: "fire_essence", name: "Feueressenz", icon: "🔥" }, mythril_ingot: { id: "mythril_ingot", name: "Mythril", icon: "🔷" } }; const MATERIAL_KEYS = Object.keys(MATERIAL_META); // --- Items Database (Includes Helmet, Armor, Pants, Boots, Weapon, Ranged, Shield, Ring, Amulet & Consumables) --- const ITEMS = { // === WORN STARTER WEAPONS === rusty_sword: { id: "rusty_sword", name: "Rostiges Kurzschwert", type: "weapon", icon: "🗡️", atk: 5, desc: "+5 Nahkampf (Abgenutzte Startwaffe)", rarity: "common" }, apprentice_wand: { id: "apprentice_wand", name: "Alter Lehrstab", type: "ranged", icon: "🪄", rangedAtk: 6, range: 4, ammoType: "mana", manaCost: 2, projIcon: "✨", desc: "+6 Magie-Schuss (4 Felder, 2 MP)", rarity: "common" }, branch_bow: { id: "branch_bow", name: "Ast-Kurzbogen", type: "ranged", icon: "🏹", rangedAtk: 6, range: 4, ammoType: "arrow", projIcon: "🏹", desc: "+6 Fernkampf (4 Felder, 1 Pfeil)", rarity: "common" }, // === CRAFTABLE & DROPPED MELEE WEAPONS === dagger: { id: "dagger", name: "Jagddolch", type: "weapon", icon: "🗡️", atk: 9, crit: 0.12, desc: "+9 Nahkampf, +12% Krit-Chance", rarity: "common" }, iron_sword: { id: "iron_sword", name: "Eisenschwert", type: "weapon", icon: "⚔️", atk: 14, desc: "+14 Solider Nahkampf-Schaden", rarity: "common" }, spear: { id: "spear", name: "Kriegsspeer", type: "weapon", icon: "🔱", atk: 16, reach: 2, desc: "+16 Nahkampf, 2 Felder Reichweite!", rarity: "rare" }, bone_blade: { id: "bone_blade", name: "Knochenbrecher-Klinge", type: "weapon", icon: "🦴", atk: 21, crit: 0.12, desc: "+21 Nahkampf, +12% Krit-Chance", rarity: "rare" }, battle_axe: { id: "battle_axe", name: "Henkersaxt", type: "weapon", icon: "🪓", atk: 26, desc: "+26 Wuchtiger Nahkampf-Schaden", rarity: "rare" }, mythril_sword: { id: "mythril_sword", name: "Mythril-Runenschwert", type: "weapon", icon: "✨", atk: 34, crit: 0.20, lifesteal: 0.15, desc: "+34 Angriff, +20% Crit, 15% Lebensraub", rarity: "legendary" }, // === RANGED WEAPONS === throwing_belt: { id: "throwing_belt", name: "Wurfmesser-Gurt", type: "ranged", icon: "🗡️", rangedAtk: 11, range: 4, ammoType: "arrow", projIcon: "🗡️", desc: "+11 Fernkampf (4 Felder)", rarity: "common" }, short_bow: { id: "short_bow", name: "Eschen-Jagdbogen", type: "ranged", icon: "🏹", rangedAtk: 13, range: 4, ammoType: "arrow", projIcon: "🏹", desc: "+13 Fernkampf (4 Felder)", rarity: "common" }, elven_bow: { id: "elven_bow", name: "Elfen-Langbogen", type: "ranged", icon: "🏹", rangedAtk: 18, range: 5, crit: 0.12, ammoType: "arrow", projIcon: "🏹", desc: "+18 Fernkampf, +12% Crit (5 Felder)", rarity: "rare" }, composite_bow: { id: "composite_bow", name: "Komposit-Kriegsbogen", type: "ranged", icon: "🏹", rangedAtk: 23, range: 5, crit: 0.15, ammoType: "arrow", projIcon: "🏹", desc: "+23 Fernkampf, +15% Crit (5 Felder)", rarity: "epic" }, heavy_crossbow: { id: "heavy_crossbow", name: "Zwergen-Armbrust", type: "ranged", icon: "🎯", rangedAtk: 30, range: 5, crit: 0.20, ammoType: "arrow", projIcon: "🔩", desc: "+30 Durchschlag, +20% Crit (5 Felder)", rarity: "epic" }, fire_staff: { id: "fire_staff", name: "Feuerfunken-Stab", type: "ranged", icon: "🪄", rangedAtk: 16, range: 5, ammoType: "mana", manaCost: 3, spellPwr: 0.25, projIcon: "🔥", desc: "+16 Magie-Schuss (5 Felder, 3 MP), +25% Magie", rarity: "rare" }, archmage_staff: { id: "archmage_staff", name: "Sternensplitter-Stab", type: "ranged", icon: "🪄", rangedAtk: 26, range: 6, ammoType: "mana", manaCost: 4, maxMp: 30, spellPwr: 0.45, projIcon: "⚡", desc: "+26 Arkan-Schuss (6 Felder), +30 MP, +45% Magie", rarity: "legendary" }, // === HELMETS (KOPF / HUT) === leather_cap: { id: "leather_cap", name: "Lederkappe", type: "helmet", icon: "🧢", def: 2, crit: 0.05, desc: "+2 Rüstung, +5% Krit-Chance", rarity: "common" }, iron_helm: { id: "iron_helm", name: "Eisen-Visierhelm", type: "helmet", icon: "🪖", def: 5, maxHp: 12, desc: "+5 Rüstung, +12 Max HP", rarity: "common" }, arcane_circlet: { id: "arcane_circlet", name: "Diadem der Weisheit", type: "helmet", icon: "👑", def: 3, maxMp: 25, spellPwr: 0.15, desc: "+3 Rüstung, +25 Max MP, +15% Magie", rarity: "rare" }, bone_crown: { id: "bone_crown", name: "Knochenkrone", type: "helmet", icon: "💀", def: 6, atk: 6, maxHp: 20, desc: "+6 Rüstung, +6 Angriff, +20 Max HP", rarity: "epic" }, // === ARMOR (BRUST / RÜSTUNG) === hide_vest: { id: "hide_vest", name: "Einfaches Pelzwams", type: "armor", icon: "🦺", def: 3, maxHp: 8, desc: "+3 Rüstung, +8 Max HP", rarity: "common" }, leather_armor: { id: "leather_armor", name: "Gehärteter Lederharnisch", type: "armor", icon: "👕", def: 6, maxHp: 12, desc: "+6 Rüstung, +12 Max HP", rarity: "common" }, mage_robe: { id: "mage_robe", name: "Robe des Arkanisten", type: "armor", icon: "🥋", def: 5, maxMp: 30, spellPwr: 0.20, desc: "+5 Rüstung, +30 Max MP, +20% Magie", rarity: "rare" }, silk_tunic: { id: "silk_tunic", name: "Spinnenseiden-Gewand", type: "armor", icon: "🕸️", def: 9, maxMp: 25, crit: 0.10, desc: "+9 Rüstung, +25 Max MP, +10% Crit", rarity: "epic" }, plate_armor: { id: "plate_armor", name: "Ritter-Plattenrüstung", type: "armor", icon: "🦾", def: 13, maxHp: 28, desc: "+13 Schwere Rüstung, +28 Max HP", rarity: "rare" }, mythril_mail: { id: "mythril_mail", name: "Mythril-Schuppenpanzer", type: "armor", icon: "🛡️", def: 19, maxHp: 45, crit: 0.10, desc: "+19 Rüstung, +45 Max HP, +10% Crit", rarity: "legendary" }, // === PANTS (BEINE / HOSE) === cloth_trousers: { id: "cloth_trousers", name: "Leinenhose des Spähers", type: "pants", icon: "👖", def: 2, maxHp: 8, desc: "+2 Rüstung, +8 Max HP", rarity: "common" }, leather_pants: { id: "leather_pants", name: "Verstärkte Jagdhose", type: "pants", icon: "👖", def: 4, maxHp: 15, crit: 0.04, desc: "+4 Rüstung, +15 Max HP, +4% Crit", rarity: "common" }, iron_greaves: { id: "iron_greaves", name: "Eisen-Beinschienen", type: "pants", icon: "👖", def: 7, maxHp: 24, desc: "+7 Rüstung, +24 Max HP", rarity: "rare" }, mythril_leggings: { id: "mythril_leggings", name: "Mythril-Beinplatten", type: "pants", icon: "👖", def: 12, maxHp: 40, crit: 0.08, desc: "+12 Rüstung, +40 Max HP, +8% Crit", rarity: "legendary" }, // === BOOTS (FÜSSE / SCHUHE) === traveler_boots: { id: "traveler_boots", name: "Robuste Wanderstiefel", type: "boots", icon: "👢", def: 2, maxHp: 10, crit: 0.04, desc: "+2 Rüstung, +10 Max HP, +4% Crit", rarity: "common" }, iron_sabatons: { id: "iron_sabatons", name: "Gepanzerte Eisenstiefel", type: "boots", icon: "👢", def: 5, maxHp: 18, desc: "+5 Rüstung, +18 Max HP", rarity: "rare" }, shadow_boots: { id: "shadow_boots", name: "Schattenläufer-Stiefel", type: "boots", icon: "👢", def: 6, atk: 4, crit: 0.10, desc: "+6 Rüstung, +4 ATK, +10% Crit", rarity: "epic" }, flame_striders: { id: "flame_striders", name: "Glutläufer-Stiefel", type: "boots", icon: "👢", def: 9, atk: 6, maxHp: 30, crit: 0.08, desc: "+9 Rüstung, +6 ATK, +30 HP, +8% Crit", rarity: "legendary" }, // === SHIELDS & OFFHAND === wood_shield: { id: "wood_shield", name: "Holzschild", type: "shield", icon: "🪵", def: 3, desc: "+3 Leichte Schild-Abwehr", rarity: "common" }, buckler: { id: "buckler", name: "Eisen-Rundschild", type: "shield", icon: "🛡️", def: 5, desc: "+5 Solide Schild-Abwehr", rarity: "common" }, quiver: { id: "quiver", name: "Elfen-Pfeilköcher", type: "shield", icon: "🏹", rangedAtk: 6, crit: 0.08, desc: "+6 Fernkampf-Schaden, +8% Crit, spart Pfeile", rarity: "rare" }, focus_orb: { id: "focus_orb", name: "Kristall-Fokusorb", type: "shield", icon: "🔮", maxMp: 25, spellPwr: 0.25, desc: "+25 Max MP, +25% Zauberkraft", rarity: "rare" }, tower_shield: { id: "tower_shield", name: "Turmschild der Feste", type: "shield", icon: "🛡️", def: 11, maxHp: 25, desc: "+11 Rüstung, +25 Max HP", rarity: "epic" }, // === RELIC 1: RINGS === bone_ring: { id: "bone_ring", name: "Knochenring der Kraft", type: "ring", icon: "💍", atk: 4, maxHp: 10, desc: "+4 Angriff, +10 Max HP", rarity: "common" }, ruby_ring: { id: "ruby_ring", name: "Rubin-Ring des Lebens", type: "ring", icon: "💍", maxHp: 35, desc: "+35 Maximale Lebenspunkte", rarity: "rare" }, sapphire_ring: { id: "sapphire_ring", name: "Saphir-Ring des Geistes", type: "ring", icon: "💎", maxMp: 35, desc: "+35 Maximales Mana", rarity: "rare" }, amulet_sun: { id: "amulet_sun", name: "Sonnen-Siegelring", type: "ring", icon: "☀️", lightRadius: 3, atk: 5, desc: "+3 Sichtweite im Dunkeln, +5 Angriff", rarity: "epic" }, vampire_ring: { id: "vampire_ring", name: "Blutring des Vampirs", type: "ring", icon: "🩸", lifesteal: 0.20, crit: 0.10, desc: "20% Lebensraub bei Treffern, +10% Crit", rarity: "legendary" }, // === RELIC 2: AMULETS & TALISMANS === bone_talisman: { id: "bone_talisman", name: "Knochen-Talisman", type: "amulet", icon: "📿", atk: 3, maxHp: 15, crit: 0.04, desc: "Relikt: +3 ATK, +15 Max HP, +4% Crit", rarity: "common" }, ward_amulet: { id: "ward_amulet", name: "Runen-Schutzamulett", type: "amulet", icon: "📿", def: 4, maxHp: 22, maxMp: 15, desc: "Relikt: +4 Rüstung, +22 HP, +15 MP", rarity: "rare" }, ember_amulet: { id: "ember_amulet", name: "Glutherz-Amulett", type: "amulet", icon: "🔥", atk: 7, maxMp: 25, spellPwr: 0.22, desc: "Relikt: +7 ATK, +25 MP, +22% Magie", rarity: "epic" }, eye_of_grimhold: { id: "eye_of_grimhold", name: "Auge von Grimhold", type: "amulet", icon: "🧿", atk: 9, def: 5, maxHp: 35, crit: 0.10, lifesteal: 0.10, desc: "Relikt: +9 ATK, +5 Rüst, +35 HP, +10% Lebensraub", rarity: "legendary" }, // === CONSUMABLES & THROWABLES === health_potion: { id: "health_potion", name: "Heiltrank", type: "consumable", icon: "🧪", heal: 55, desc: "Stellt sofort 55 Lebenspunkte her", rarity: "common" }, mana_potion: { id: "mana_potion", name: "Mana-Elixier", type: "consumable", icon: "🍶", mana: 45, desc: "Füllt sofort 45 Mana-Punkte auf", rarity: "common" }, titan_elixir: { id: "titan_elixir", name: "Elixier der Titanen", type: "consumable", icon: "🍷", permHp: 18, permAtk: 3, desc: "Permanent +18 Max HP & +3 Angriff (+Vollheilung)!", rarity: "legendary" }, throwing_knife: { id: "throwing_knife", name: "Wurfmesser", type: "consumable", icon: "🗡️", throwDmg: 24, range: 5, projIcon: "🗡️", desc: "Wirft eine Klinge auf das nächste Ziel (24 Schaden)", rarity: "common" }, fire_bomb: { id: "fire_bomb", name: "Alchemie-Feuerbombe", type: "consumable", icon: "💣", bombDmg: 40, range: 4, projIcon: "💣", desc: "38–40 Flächen-Feuerschaden auf Gegner", rarity: "rare" }, spider_net: { id: "spider_net", name: "Spinnenseiden-Fangnetz", type: "consumable", icon: "🕸️", stunTurns: 3, range: 5, projIcon: "🕸️", desc: "Fesselt ein sichtbares Monster für 3 Runden!", rarity: "rare" }, scroll_fire: { id: "scroll_fire", name: "Schriftrolle: Feuersbrunst", type: "consumable", icon: "📜", spell: "fireball", desc: "Verbrennt alle Gegner im Umkreis (45 Schaden)", rarity: "rare" }, scroll_teleport: { id: "scroll_teleport", name: "Schriftrolle: Fluchtportal", type: "consumable", icon: "📜", spell: "teleport", desc: "Teleportiert dich sofort in einen sicheren Raum", rarity: "rare" }, scroll_map: { id: "scroll_map", name: "Schriftrolle: Hellsicht", type: "consumable", icon: "📜", spell: "map", desc: "Deckt die gesamte aktuelle Ebene auf", rarity: "rare" } }; // --- Crafting Recipes (Includes Pants, Boots, Amulets & Starter Gear) --- const CRAFT_RECIPES = [ // Starter Gear (Floor 1-2 craftables) { id: "c_wood_shield", category: "starter", name: "Holzschild zimmern", icon: "🪵", resultItem: "wood_shield", resultCount: 1, desc: "Nebenhand: +3 Rüstung.", cost: { wood_branch: 2 } }, { id: "c_leather_cap", category: "starter", name: "Lederkappe nähen", icon: "🧢", resultItem: "leather_cap", resultCount: 1, desc: "Hut/Helm: +2 Rüstung, +5% Crit.", cost: { healing_herb: 1, monster_bone: 1 } }, { id: "c_hide_vest", category: "starter", name: "Einfaches Pelzwams", icon: "🦺", resultItem: "hide_vest", resultCount: 1, desc: "Brust: +3 Rüstung, +8 Max HP.", cost: { wood_branch: 1, monster_bone: 1 } }, { id: "c_cloth_trousers", category: "starter", name: "Leinenhose nähen", icon: "👖", resultItem: "cloth_trousers", resultCount: 1, desc: "Hose: +2 Rüstung, +8 Max HP.", cost: { wood_branch: 1, healing_herb: 1 } }, { id: "c_traveler_boots", category: "starter", name: "Robuste Wanderstiefel", icon: "👢", resultItem: "traveler_boots", resultCount: 1, desc: "Schuhe: +2 Rüstung, +10 HP, +4% Crit.", cost: { wood_branch: 1, monster_bone: 1 } }, { id: "c_iron_sword", category: "starter", name: "Eisenschwert schmieden", icon: "⚔️", resultItem: "iron_sword", resultCount: 1, desc: "Nahkampf: +14 Angriff.", cost: { iron_ore: 2, wood_branch: 1 } }, { id: "c_short_bow", category: "starter", name: "Eschen-Jagdbogen", icon: "🏹", resultItem: "short_bow", resultCount: 1, desc: "Fernkampf: +13 Schaden (4 Felder).", cost: { wood_branch: 2, healing_herb: 1 } }, { id: "c_iron_helm", category: "starter", name: "Eisen-Visierhelm", icon: "🪖", resultItem: "iron_helm", resultCount: 1, desc: "Hut/Helm: +5 Rüstung, +12 Max HP.", cost: { iron_ore: 2 } }, { id: "c_leather_pants", category: "starter", name: "Verstärkte Jagdhose", icon: "👖", resultItem: "leather_pants", resultCount: 1, desc: "Hose: +4 Rüstung, +15 HP, +4% Crit.", cost: { spider_silk: 1, monster_bone: 1 } }, { id: "c_iron_sabatons", category: "starter", name: "Gepanzerte Eisenstiefel", icon: "👢", resultItem: "iron_sabatons", resultCount: 1, desc: "Schuhe: +5 Rüstung, +18 Max HP.", cost: { iron_ore: 2, wood_branch: 1 } }, { id: "c_buckler", category: "starter", name: "Eisen-Rundschild", icon: "🛡️", resultItem: "buckler", resultCount: 1, desc: "Nebenhand: +5 Rüstung.", cost: { iron_ore: 2, wood_branch: 1 } }, { id: "c_bone_ring", category: "starter", name: "Knochenring schnitzen", icon: "💍", resultItem: "bone_ring", resultCount: 1, desc: "Relikt 1 (Ring): +4 Angriff, +10 Max HP.", cost: { monster_bone: 2 } }, { id: "c_bone_talisman", category: "starter", name: "Knochen-Talisman", icon: "📿", resultItem: "bone_talisman", resultCount: 1, desc: "Relikt 2 (Amulett): +3 ATK, +15 HP, +4% Crit.", cost: { monster_bone: 2, mana_shard: 1 } }, // Ammo & Throwables { id: "c_arrows_wood", category: "ammo", name: "10x Pfeile schnitzen", icon: "🏹", resultArrows: 10, desc: "+10 Pfeile für Bögen & Armbrüste.", cost: { wood_branch: 1 } }, { id: "c_arrows_bone", category: "ammo", name: "15x Knochenpfeile", icon: "🏹", resultArrows: 15, desc: "+15 Pfeile aus Monsterknochen.", cost: { monster_bone: 1, wood_branch: 1 } }, { id: "c_knives", category: "ammo", name: "3x Wurfmesser schmieden", icon: "🗡️", resultItem: "throwing_knife", resultCount: 3, desc: "Wurfwaffe (24 Schaden, 5 Felder).", cost: { iron_ore: 1, wood_branch: 1 } }, { id: "c_nets", category: "ammo", name: "2x Spinnenseiden-Fangnetz", icon: "🕸️", resultItem: "spider_net", resultCount: 2, desc: "Fesselt Gegner für 3 Runden.", cost: { spider_silk: 1 } }, // Alchemy { id: "c_hp_pot", category: "alchemy", name: "Heiltrank brauen", icon: "🧪", resultItem: "health_potion", resultCount: 1, desc: "Stellt 55 HP wieder her.", cost: { healing_herb: 2 } }, { id: "c_mp_pot", category: "alchemy", name: "Mana-Elixier brauen", icon: "🍶", resultItem: "mana_potion", resultCount: 1, desc: "Stellt 45 MP wieder her.", cost: { healing_herb: 1, mana_shard: 1 } }, { id: "c_fire_bomb", category: "alchemy", name: "2x Feuerbombe mischen", icon: "💣", resultItem: "fire_bomb", resultCount: 2, desc: "Flächen-Explosion (40 Schaden).", cost: { fire_essence: 1, iron_ore: 1 } }, { id: "c_titan", category: "alchemy", name: "Elixier der Titanen", icon: "🍷", resultItem: "titan_elixir", resultCount: 1, desc: "Permanent +18 Max HP & +3 Angriff!", cost: { healing_herb: 2, mana_shard: 2, fire_essence: 1 } }, // Master Smithing { id: "c_spear", category: "master", name: "Kriegsspeer (2 Felder)", icon: "🔱", resultItem: "spear", resultCount: 1, desc: "+16 Nahkampf, stößt 2 Felder weit!", cost: { iron_ore: 2, wood_branch: 2 } }, { id: "c_comp_bow", category: "master", name: "Komposit-Kriegsbogen", icon: "🏹", resultItem: "composite_bow", resultCount: 1, desc: "+23 Fernkampf, +15% Crit (5 Felder).", cost: { wood_branch: 2, spider_silk: 2 } }, { id: "c_crossbow", category: "master", name: "Zwergen-Armbrust", icon: "🎯", resultItem: "heavy_crossbow", resultCount: 1, desc: "+30 Fernkampf, +20% Crit (5 Felder).", cost: { iron_ore: 3, wood_branch: 2 } }, { id: "c_fire_staff", category: "master", name: "Feuerfunken-Stab", icon: "🪄", resultItem: "fire_staff", resultCount: 1, desc: "+16 Magie-Schuss (5 Felder), +25% Magie.", cost: { wood_branch: 2, fire_essence: 1, mana_shard: 1 } }, { id: "c_bone_blade", category: "master", name: "Knochenbrecher-Klinge", icon: "🦴", resultItem: "bone_blade", resultCount: 1, desc: "+21 Nahkampf, +12% Crit.", cost: { iron_ore: 2, monster_bone: 2 } }, { id: "c_bone_crown", category: "master", name: "Knochenkrone", icon: "💀", resultItem: "bone_crown", resultCount: 1, desc: "Helm: +6 Def, +6 Atk, +20 HP.", cost: { monster_bone: 3, mana_shard: 1 } }, { id: "c_silk_tunic", category: "master", name: "Spinnenseiden-Gewand", icon: "🕸️", resultItem: "silk_tunic", resultCount: 1, desc: "Brust: +9 Def, +25 MP, +10% Crit.", cost: { spider_silk: 3, mana_shard: 1 } }, { id: "c_plate_armor", category: "master", name: "Ritter-Plattenrüstung", icon: "🦾", resultItem: "plate_armor", resultCount: 1, desc: "Brust: +13 Rüstung, +28 Max HP.", cost: { iron_ore: 4, monster_bone: 1 } }, { id: "c_iron_greaves", category: "master", name: "Eisen-Beinschienen", icon: "👖", resultItem: "iron_greaves", resultCount: 1, desc: "Hose: +7 Rüstung, +24 Max HP.", cost: { iron_ore: 3, monster_bone: 1 } }, { id: "c_shadow_boots", category: "master", name: "Schattenläufer-Stiefel", icon: "👢", resultItem: "shadow_boots", resultCount: 1, desc: "Schuhe: +6 Rüstung, +4 ATK, +10% Crit.", cost: { spider_silk: 2, fire_essence: 1 } }, { id: "c_ember_amulet", category: "master", name: "Glutherz-Amulett", icon: "🔥", resultItem: "ember_amulet", resultCount: 1, desc: "Amulett: +7 ATK, +25 MP, +22% Magie.", cost: { fire_essence: 2, mana_shard: 2 } }, { id: "c_quiver", category: "master", name: "Elfen-Pfeilköcher", icon: "🏹", resultItem: "quiver", resultCount: 1, desc: "Nebenhand: +6 Fernkampf, spart Pfeile.", cost: { spider_silk: 2, wood_branch: 1 } }, { id: "c_mythril_bar", category: "master", name: "Mythril-Barren schmelzen", icon: "🔷", resultMat: "mythril_ingot", resultCount: 1, desc: "Edles Metall aus Erz & Kristall.", cost: { iron_ore: 3, mana_shard: 2 } }, { id: "c_mythril_sword", category: "master", name: "Mythril-Runenschwert", icon: "✨", resultItem: "mythril_sword", resultCount: 1, desc: "+34 Angriff, +20% Crit, 15% Lebensraub!", cost: { mythril_ingot: 2, fire_essence: 1 } }, { id: "c_mythril_mail", category: "master", name: "Mythril-Schuppenpanzer", icon: "🛡️", resultItem: "mythril_mail", resultCount: 1, desc: "+19 Rüstung, +45 Max HP, +10% Crit!", cost: { mythril_ingot: 2, iron_ore: 2 } }, { id: "c_mythril_leggings", category: "master", name: "Mythril-Beinplatten", icon: "👖", resultItem: "mythril_leggings", resultCount: 1, desc: "Hose: +12 Rüstung, +40 Max HP, +8% Crit!", cost: { mythril_ingot: 2, spider_silk: 2 } }, { id: "c_flame_striders", category: "master", name: "Glutläufer-Stiefel", icon: "👢", resultItem: "flame_striders", resultCount: 1, desc: "Schuhe: +9 Rüstung, +6 ATK, +30 HP!", cost: { mythril_ingot: 1, fire_essence: 2 } }, { id: "c_eye_grimhold", category: "master", name: "Auge von Grimhold", icon: "🧿", resultItem: "eye_of_grimhold", resultCount: 1, desc: "Amulett: +9 ATK, +5 Def, +35 HP, +10% Lebensraub!", cost: { mythril_ingot: 2, fire_essence: 2, mana_shard: 2 } }, { id: "c_vampire_ring", category: "master", name: "Blutring des Vampirs", icon: "🩸", resultItem: "vampire_ring", resultCount: 1, desc: "Ring: 20% Lebensraub, +10% Crit.", cost: { mana_shard: 2, healing_herb: 2, fire_essence: 1 } } ]; const SKILLS_DB = { ranged_shot: { id: "ranged_shot", name: "Fernkampf-Schuss", icon: "🎯", mpCost: 0, desc: "Feuert deine angelegte Fernkampfwaffe auf ein sichtbares Ziel." }, whirlwind: { id: "whirlwind", name: "Wirbelwind-Hieb", icon: "⚔️", mpCost: 6, desc: "Trifft alle angrenzenden Feinde mit 125% Nahkampfschaden." }, shield_bash: { id: "shield_bash", name: "Schildstoß", icon: "🛡️", mpCost: 7, desc: "Schmettert einen nahen Gegner nieder (Schaden + 2 Runden Betäubung)." }, power_snipe: { id: "power_snipe", name: "Meisterschuss", icon: "🏹", mpCost: 9, desc: "Präzisionsschuss mit +65% Fernkampfschaden." }, thorn_trap: { id: "thorn_trap", name: "Dornenfalle", icon: "🪤", mpCost: 7, desc: "Legt eine Falle auf dein Feld (30 Schaden + 2 Runden Fesselung)." }, magic_missile: { id: "magic_missile", name: "Arkanpfeil", icon: "⚡", mpCost: 7, desc: "Zielsuchendes Arkan-Projektil auf ein Monster (bis 6 Felder)." }, fireball: { id: "fireball", name: "Feuerball-Nova", icon: "🔥", mpCost: 15, desc: "Feurige Explosion auf alle Gegner im Umkreis von 3.5 Feldern." }, frost_nova: { id: "frost_nova", name: "Frostnova", icon: "❄️", mpCost: 13, desc: "Friert alle Gegner im Umkreis von 3 Feldern für 2 Runden ein." } }; const MONSTER_TYPES = { rat: { name: "Höhlenratte", icon: "🐀", hp: 16, atk: 6, def: 0, xp: 14, gold: 7, drops: ["healing_herb", "monster_bone", "wood_branch"] }, slime: { name: "Grünschleim", icon: "🟢", hp: 26, atk: 8, def: 1, xp: 20, gold: 11, drops: ["healing_herb", "mana_shard"] }, goblin: { name: "Goblin-Sammler", icon: "👺", hp: 34, atk: 10, def: 1, xp: 26, gold: 15, drops: ["iron_ore", "wood_branch"] }, skel_archer: { name: "Skelett-Schütze", icon: "🏹", hp: 40, atk: 13, def: 2, ranged: 5, projIcon: "🏹", xp: 36, gold: 20, drops: ["wood_branch", "monster_bone"] }, skeleton: { name: "Skelett-Krieger", icon: "💀", hp: 52, atk: 15, def: 3, xp: 42, gold: 24, drops: ["monster_bone", "iron_ore"] }, spider: { name: "Höhlen-Spinne", icon: "🕷️", hp: 48, atk: 14, def: 2, xp: 46, gold: 26, drops: ["spider_silk", "healing_herb"] }, orc: { name: "Ork-Plünderer", icon: "👹", hp: 88, atk: 20, def: 4, xp: 70, gold: 42, drops: ["iron_ore", "monster_bone", "fire_essence"] }, shaman: { name: "Goblin-Schamane", icon: "🧙♂️", hp: 68, atk: 19, def: 3, ranged: 5, projIcon: "🔥", xp: 80, gold: 50, drops: ["fire_essence", "fire_essence", "mana_shard"] }, fire_elemental: { name: "Feuer-Elementar", icon: "🔥", hp: 75, atk: 21, def: 2, ranged: 4, projIcon: "🔥", xp: 95, gold: 55, drops: ["fire_essence", "fire_essence", "fire_essence"] }, magma_slime: { name: "Magma-Schleim", icon: "🌋", hp: 58, atk: 17, def: 3, xp: 65, gold: 40, drops: ["fire_essence", "iron_ore"] }, golem: { name: "Kristall-Golem", icon: "🗿", hp: 145, atk: 26, def: 8, xp: 130, gold: 85, drops: ["iron_ore", "mana_shard", "mythril_ingot"] }, midboss_orc: { name: "Ork-Kriegsherr Grak", icon: "👹", hp: 210, atk: 25, def: 6, xp: 220, gold: 160, isMidBoss: true, drops: ["mythril_ingot", "iron_ore"] }, boss_goblin: { name: "Goblin-König Kraxx", icon: "👑", hp: 380, atk: 32, def: 8, ranged: 5, projIcon: "🔥", xp: 500, gold: 400, isBoss: true, drops: ["mythril_ingot", "fire_essence"] }, boss_frostwyrm: { name: "Frostwyrm Vaelgor", icon: "🐉", hp: 460, atk: 36, def: 10, ranged: 5, projIcon: "❄️", xp: 750, gold: 650, isBoss: true, drops: ["mythril_ingot", "mana_shard"] }, boss_ignis: { name: "Feuerfürst Ignis", icon: "👿", hp: 560, atk: 42, def: 12, ranged: 6, projIcon: "🔥", xp: 1100, gold: 950, isBoss: true, drops: ["mythril_ingot", "fire_essence"] } }; // ================= GAME STATE ================= let currentDungeonId = "catacombs"; let inTown = false; let savedDungeonFloorBeforeTown = 1; let currentFloor = 1; let mapGrid = []; let dungeonRooms = []; let fovMap = []; let monsters = []; let floorItems = []; let floorTraps = []; let projectiles = []; let floatingNumbers = []; let stairsDown = { x: -1, y: -1 }; let stairsUp = { x: -1, y: -1 }; let floorCache = {}; let hasActiveRun = false; let currentTownShopTab = "buy_gear"; let currentInventoryFilter = "all"; let currentTownShopFilter = "all"; const ITEM_FILTER_CATEGORIES = [ { id: "all", label: "Alle", icon: "✨" }, { id: "weapon", label: "Waffe", icon: "⚔️" }, { id: "helmet", label: "Hut", icon: "🪖" }, { id: "armor", label: "Rüstung", icon: "👕" }, { id: "pants", label: "Hose", icon: "👖" }, { id: "boots", label: "Schuhe", icon: "👢" }, { id: "shield", label: "Schild", icon: "🛡️" }, { id: "relic", label: "Relikt", icon: "💍" }, { id: "consumable", label: "Tränke", icon: "🧪" } ]; let onlinePlayers = []; let chatMessages = []; let aimMode = { active: false, targetIndex: 0, validTargets: [], pendingSkillOrItem: null }; let currentCraftFilter = "all"; let selectedShortcutSlot = 0; let awaitingKeybindTarget = null; // Persistent Client ID for Server Auto-Save let clientId = localStorage.getItem("mythic_client_id"); if (!clientId) { clientId = "cid_" + Math.random().toString(36).substring(2, 11) + "_" + Date.now(); try { localStorage.setItem("mythic_client_id", clientId); } catch (e) {} } let hero = { playerName: localStorage.getItem("mythic_player_name") || "Abenteurer", classId: "knight", name: "Klingensänger", icon: "⚔️", x: 5, y: 5, level: 1, xp: 0, xpNeeded: 100, statPoints: 0, spentPoints: { hp: 0, mana: 0, str: 0 }, hp: 110, maxHp: 110, baseMaxHp: 110, mp: 25, maxMp: 25, baseMaxMp: 25, baseAtk: 10, baseDef: 1, baseCrit: 0.08, baseSpellPwr: 1.0, arrows: 0, gold: 0, turns: 0, kills: 0, score: 0, house: { owned: false, tier: 0, chest: [] }, materials: { iron_ore: 0, wood_branch: 0, healing_herb: 0, mana_shard: 0, spider_silk: 0, monster_bone: 0, fire_essence: 0, mythril_ingot: 0 }, equipped: { helmet: null, armor: null, pants: null, boots: null, weapon: null, shield: null, ring: null, amulet: null }, inventory: [] }; // Bag capacity: strictly 5 slots at level 1, growing by +2 slots each level up! function getMaxBagSlots() { return Math.min(30, 5 + Math.max(0, ((hero.level || 1) - 1) * 2)); } // Class-specific attribute scaling per stat point const CLASS_STAT_GROWTH = { knight: { hp: { amount: 15, label: "+15 Max HP", desc: "+15 Lebenspunkte" }, mana: { amount: 5, label: "+5 Max MP", desc: "+5 Mana" }, str: { amount: 2, label: "+2 Angriff", desc: "+2 Nahkampf-Angriff" } }, mage: { hp: { amount: 6, label: "+6 Max HP", desc: "+6 Lebenspunkte" }, mana: { amount: 15, label: "+15 Max MP", desc: "+15 Mana" }, str: { amount: 1, label: "+1 ATK & +5% Magie", desc: "+1 Angriff & +5% Zaubermacht" } }, ranger: { hp: { amount: 10, label: "+10 Max HP", desc: "+10 Lebenspunkte" }, mana: { amount: 8, label: "+8 Max MP", desc: "+8 Mana" }, str: { amount: 2, label: "+2 Schaden & +0.5% Krit", desc: "+2 Schaden & +0.5% Krit-Chance" } } }; function spendAttributePoint(statType) { if (!hero.statPoints || hero.statPoints <= 0) { showToast("Keine freien Attributspunkte verfügbar!"); return; } hero.statPoints--; if (!hero.spentPoints) hero.spentPoints = { hp: 0, mana: 0, str: 0 }; hero.spentPoints[statType] = (hero.spentPoints[statType] || 0) + 1; const cid = hero.classId || "knight"; const growth = CLASS_STAT_GROWTH[cid] || CLASS_STAT_GROWTH.knight; if (statType === "hp") { const gain = growth.hp.amount; hero.baseMaxHp += gain; hero.hp = Math.min(hero.maxHp + gain, hero.hp + gain); playSound("potion"); showToast(`❤️ Lebenskraft gesteigert (+${gain} Max HP)!`); } else if (statType === "mana") { const gain = growth.mana.amount; hero.baseMaxMp += gain; hero.mp = Math.min(hero.maxMp + gain, hero.mp + gain); playSound("spell"); showToast(`✨ Willenskraft gesteigert (+${gain} Max MP)!`); } else if (statType === "str") { const gain = growth.str.amount; hero.baseAtk += gain; if (cid === "mage") hero.baseSpellPwr = (hero.baseSpellPwr || 1.0) + 0.05; if (cid === "ranger") hero.baseCrit = (hero.baseCrit || 0.08) + 0.005; playSound("hit"); showToast(`💪 Stärke gesteigert (${growth.str.label})!`); } recalcHeroStats(); updateHUD(); updateInventoryUI(); saveGameProgress(); } const DEFAULT_QUICKSLOTS = { knight: [ { type: "skill", id: "whirlwind", key: "1" }, { type: "skill", id: "shield_bash", key: "2" }, { type: "skill", id: "ranged_shot", key: "3" }, { type: "item", id: "health_potion", key: "4" }, { type: "item", id: "throwing_knife", key: "5" }, { type: "item", id: "scroll_teleport", key: "6" } ], mage: [ { type: "skill", id: "magic_missile", key: "1" }, { type: "skill", id: "fireball", key: "2" }, { type: "skill", id: "frost_nova", key: "3" }, { type: "item", id: "mana_potion", key: "4" }, { type: "item", id: "health_potion", key: "5" }, { type: "item", id: "scroll_teleport", key: "6" } ], ranger: [ { type: "skill", id: "ranged_shot", key: "1" }, { type: "skill", id: "power_snipe", key: "2" }, { type: "skill", id: "thorn_trap", key: "3" }, { type: "item", id: "health_potion", key: "4" }, { type: "item", id: "spider_net", key: "5" }, { type: "item", id: "fire_bomb", key: "6" } ] }; let quickslots = JSON.parse(JSON.stringify(DEFAULT_QUICKSLOTS.knight)); let sysKeybinds = { ranged: "f", wait: " ", inv: "i", craft: "c" }; try { const savedSys = localStorage.getItem("mythic_sys_keys_v3"); if (savedSys) sysKeybinds = { ...sysKeybinds, ...JSON.parse(savedSys) }; } catch (e) {} let canvas = null; let ctx = null; let camera = { x: 0, y: 0 }; let animFrameId = null; let torchFlicker = 0; // ================= AUTO-SAVE & RESTORE SYSTEM ================= let serverSaveTimeout = null; function getFloorCacheKey(floorNum = currentFloor, dId = currentDungeonId) { return `${dId || "catacombs"}_${floorNum}`; } function saveCurrentFloorToCache() { if (inTown || !mapGrid || mapGrid.length === 0) return; floorCache[getFloorCacheKey(currentFloor, currentDungeonId)] = { mapGrid, fovMap, dungeonRooms, monsters, floorItems, floorTraps, stairsDown: { ...stairsDown }, stairsUp: { ...stairsUp } }; } function saveGameProgress() { if (!hasActiveRun || hero.hp <= 0) return; saveCurrentFloorToCache(); const stateObj = { v: 3, balanceVer: 2, currentDungeonId, inTown, savedDungeonFloorBeforeTown, currentFloor, hero, mapGrid, fovMap, dungeonRooms, monsters, floorItems, floorTraps, stairsDown, stairsUp, floorCache, quickslots }; try { const serialized = JSON.stringify(stateObj); localStorage.setItem("mythic_saved_run_v3", serialized); const badge = document.getElementById("hud-save-badge"); if (badge) { badge.textContent = "💾 Gespeichert"; badge.classList.remove("hidden"); } // Debounced Server Sync clearTimeout(serverSaveTimeout); serverSaveTimeout = setTimeout(() => { const token = localStorage.getItem("grimhold_token"); const headers = { "Content-Type": "application/json" }; if (token) headers["Authorization"] = `Bearer ${token}`; fetch(`/api/dungeon/save-active?client_id=${encodeURIComponent(clientId)}`, { method: "POST", headers, body: JSON.stringify({ client_id: clientId, hero_class: hero.name, run_state: serialized }) }).catch(() => {}); }, 1000); } catch (e) {} } function clearSavedGameProgress() { hasActiveRun = false; floorCache = {}; try { localStorage.removeItem("mythic_saved_run_v3"); } catch (e) {} } function restoreFromStateObject(st) { if (!st || st.v !== 3 || !st.hero || st.hero.hp <= 0) return false; currentDungeonId = st.currentDungeonId || "catacombs"; inTown = Boolean(st.inTown); savedDungeonFloorBeforeTown = st.savedDungeonFloorBeforeTown || st.currentFloor || 1; currentFloor = st.currentFloor || 1; hero = st.hero; if (!hero.highestFloor || hero.highestFloor < currentFloor) { hero.highestFloor = currentFloor; } if (hero.classId === "mage") { hero.icon = "🔮"; hero.name = "Arkanist"; } else if (hero.classId === "ranger") { hero.icon = "🏹"; hero.name = "Waldläufer"; } else { hero.icon = "⚔️"; hero.name = "Klingensänger"; } if (!hero.materials) { hero.materials = { iron_ore: 0, wood_branch: 0, healing_herb: 0, mana_shard: 0, spider_silk: 0, monster_bone: 0, fire_essence: 0, mythril_ingot: 0 }; } if (!hero.equipped) hero.equipped = {}; ALL_EQUIP_SLOTS.forEach(k => { if (hero.equipped[k] === undefined) hero.equipped[k] = null; }); // Retroactive Attribute Points Calculation: // (hero.level - 1) * 3 total earned points minus already spent points if (!hero.spentPoints) hero.spentPoints = { hp: 0, mana: 0, str: 0 }; if (hero.statPoints === undefined || hero.statPoints === null) hero.statPoints = 0; const totalEarnedPoints = Math.max(0, ((hero.level || 1) - 1) * 3); const totalSpentPoints = (hero.spentPoints.hp || 0) + (hero.spentPoints.mana || 0) + (hero.spentPoints.str || 0); if (hero.statPoints + totalSpentPoints < totalEarnedPoints) { hero.statPoints = totalEarnedPoints - totalSpentPoints; } mapGrid = st.mapGrid; fovMap = st.fovMap; dungeonRooms = st.dungeonRooms || []; floorCache = st.floorCache || {}; stairsDown = st.stairsDown || { x: -1, y: -1 }; stairsUp = st.stairsUp || { x: -1, y: -1 }; // Migrate old numeric floorCache keys (e.g. floorCache[5]) to dungeon-prefixed keys Object.keys(floorCache).forEach(k => { if (/^\d+$/.test(k)) { floorCache[`catacombs_${k}`] = floorCache[k]; } }); // Ensure existing saves on any Dungeon Floor >= 1 have a Stairs Up tile in start room! if (!inTown && currentFloor >= 1 && dungeonRooms.length > 0) { const sr = dungeonRooms[0]; if (stairsUp.x < 0 || mapGrid[stairsUp.y]?.[stairsUp.x]?.type !== "stairs_up") { const ux = sr.cx; const uy = sr.cy; stairsUp = { x: ux, y: uy }; if (mapGrid[uy]?.[ux]) mapGrid[uy][ux] = { type: "stairs_up" }; } } const needsRebalance = !st.balanceVer || st.balanceVer < 2; monsters = (st.monsters || []).map(m => { const proto = MONSTER_TYPES[m.typeId]; if (proto) { m.icon = proto.icon; m.name = proto.name; m.projIcon = proto.projIcon || "🔥"; if (needsRebalance && m.hp > 0) { const fresh = createMonster(m.typeId, m.x, m.y); const hpRatio = m.maxHp > 0 ? (m.hp / m.maxHp) : 1; m.maxHp = fresh.maxHp; m.hp = Math.max(1, Math.round(fresh.maxHp * hpRatio)); m.atk = fresh.atk; m.def = fresh.def; } } return m; }); floorItems = st.floorItems || []; floorTraps = st.floorTraps || []; if (st.quickslots) quickslots = st.quickslots; hasActiveRun = true; recalcHeroStats(); computeFOV(); centerCameraOnHero(); updateHUD(); updateInventoryUI(); return true; } async function tryLoadSavedRun() { const token = localStorage.getItem("grimhold_token"); // 1. If logged into an account, check Server Account Save first! if (token) { try { const res = await fetch(`/api/dungeon/load-active?client_id=${encodeURIComponent(clientId)}`, { headers: { "Authorization": `Bearer ${token}` } }); const data = await res.json(); if (data && data.has_saved_run && data.run_state) { const parsed = JSON.parse(data.run_state); if (restoreFromStateObject(parsed)) { showToast(`☁️ Konto-Spielstand geladen (Ebene ${currentFloor}, Stufe ${hero.level})!`); return true; } } } catch (e) {} } // 2. Check localStorage (instant) try { const raw = localStorage.getItem("mythic_saved_run_v3"); if (raw) { const parsed = JSON.parse(raw); if (restoreFromStateObject(parsed)) { showToast(`💾 Spielstand geladen (Ebene ${currentFloor}, Stufe ${hero.level})!`); return true; } } } catch (e) {} // 3. Fallback: Check Server SQLite backup by clientId try { const res = await fetch(`/api/dungeon/load-active?client_id=${encodeURIComponent(clientId)}`); const data = await res.json(); if (data && data.has_saved_run && data.run_state) { const parsed = JSON.parse(data.run_state); if (restoreFromStateObject(parsed)) { showToast(`💾 Server-Spielstand wiederhergestellt (Ebene ${currentFloor})!`); return true; } } } catch (e) {} return false; } // ================= TOWN HUB ("STADT GRIMHOLD-FESTE") & MULTI-DUNGEON GENERATOR ================= function toggleTownOrDungeon() { if (!hasActiveRun || isGameOver) return; if (inTown) { leaveTownToDungeon(currentDungeonId, savedDungeonFloorBeforeTown || 1); } else { enterTownHub(); } } function enterTownHub() { if (!hasActiveRun || isGameOver) return; if (!inTown) { saveCurrentFloorToCache(); savedDungeonFloorBeforeTown = Math.max(1, currentFloor); } inTown = true; projectiles = []; floatingNumbers = []; monsters = []; floorItems = []; floorTraps = []; dungeonRooms = []; cancelAimMode(); mapGrid = []; fovMap = []; for (let y = 0; y < MAP_H; y++) { mapGrid[y] = []; fovMap[y] = []; for (let x = 0; x < MAP_W; x++) { mapGrid[y][x] = { type: "wall", hasTorch: false }; fovMap[y][x] = { explored: false, visible: false }; } } // Carve a lively 22x14 Town Square in the center of the map const tx0 = 8, ty0 = 5, tw = 20, th = 13; for (let y = ty0; y < ty0 + th; y++) { for (let x = tx0; x < tx0 + tw; x++) { mapGrid[y][x] = { type: "floor" }; fovMap[y][x] = { explored: true, visible: true }; } } // Reveal outer border walls of town for (let y = ty0 - 1; y <= ty0 + th; y++) { for (let x = tx0 - 1; x <= tx0 + tw; x++) { if (fovMap[y]?.[x]) { fovMap[y][x].explored = true; fovMap[y][x].visible = true; } } } // Decorative wall torches around town [tx0 + 3, tx0 + 9, tx0 + 16].forEach(wx => { if (mapGrid[ty0 - 1]?.[wx]) mapGrid[ty0 - 1][wx].hasTorch = true; }); const cx = tx0 + Math.floor(tw / 2); const cy = ty0 + Math.floor(th / 2); // Town Interactive Buildings (Fixed Positions) mapGrid[ty0 + 2][cx] = { type: "town_portal" }; // 🌀 Weltportal (Dungeon-Auswahl) mapGrid[cy + 1][cx + 5] = { type: "anvil" }; // ⚒️ Zentrale Stadt-Schmiede (Standard-Kosten) mapGrid[cy + 2][cx - 6] = { type: "player_house" }; // 🏡 Spieler-Grundstück & Eigenheim mapGrid[ty0 + th - 2][cx] = { type: "stairs_down" }; // 🪜 Direkt zurück in den aktuellen Dungeon // Dynamic Town NPCs (Wander freely on the square!) initTownNpcs(cx, cy); clearInterval(townNpcTimer); townNpcTimer = setInterval(updateTownNpcs, 2800); stairsDown = { x: cx, y: ty0 + th - 2 }; stairsUp = { x: -1, y: -1 }; hero.x = cx; hero.y = cy + 2; playSound("stairs"); centerCameraOnHero(); updateHUD(); saveGameProgress(); syncMultiplayerPresence(); showFloorBanner(0, "Stadt Grimhold-Feste", "Sichere Zuflucht • Händler Balthasar, Schmied Thorgar, Eigenheim & Weltportal"); logCombat(`🏘️ Willkommen in der Stadt Grimhold-Feste! Sprich mit 🧔 Händler Balthasar, 🧝♀️ Heilerin Seraphina, besuche dein 🏡 Eigenheim oder wähle am 🌀 Weltportal deinen Dungeon.`); } let townNpcTimer = null; let townNpcs = []; function initTownNpcs(cx, cy) { townNpcs = [ { id: "merchant", type: "npc_merchant", name: "Händler Balthasar", icon: "🧔", homeX: cx - 4, homeY: cy - 1, x: cx - 4, y: cy - 1 }, { id: "smith", type: "npc_smith", name: "Meister-Schmied Thorgar", icon: "👨🏭", homeX: cx + 4, homeY: cy - 1, x: cx + 4, y: cy - 1 }, { id: "healer", type: "npc_healer", name: "Heilerin Seraphina", icon: "🧝♀️", homeX: cx, homeY: cy, x: cx, y: cy }, { id: "citizen1", type: "npc_citizen", name: "Bürger Aldous", icon: "🧑", homeX: cx - 2, homeY: cy + 3, x: cx - 2, y: cy + 3 }, { id: "citizen2", type: "npc_citizen", name: "Wache Roderic", icon: "💂", homeX: cx + 2, homeY: cy + 3, x: cx + 2, y: cy + 3 } ]; } function updateTownNpcs() { if (!inTown || townNpcs.length === 0) return; const dirs = [{ dx: 0, dy: 1 }, { dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: -1, dy: 0 }, { dx: 0, dy: 0 }]; townNpcs.forEach(npc => { const d = dirs[Math.floor(Math.random() * dirs.length)]; if (d.dx === 0 && d.dy === 0) return; const nx = npc.x + d.dx; const ny = npc.y + d.dy; if (Math.hypot(nx - npc.homeX, ny - npc.homeY) <= 2.6 && isWalkable(nx, ny)) { if (mapGrid[ny]?.[nx]?.type === "floor" && !(nx === hero.x && ny === hero.y)) { const blocked = townNpcs.some(o => o.id !== npc.id && o.x === nx && o.y === ny); if (!blocked) { npc.x = nx; npc.y = ny; } } } }); } function leaveTownToDungeon(targetDungeonId = currentDungeonId, targetFloor = savedDungeonFloorBeforeTown || 1) { clearInterval(townNpcTimer); inTown = false; currentDungeonId = targetDungeonId || "catacombs"; const f = Math.max(1, Math.min(MAX_FLOORS, targetFloor || 1)); playSound("stairs"); generateDungeonFloor(f, "down"); syncMultiplayerPresence(); } function generateDungeonFloor(floorNum, arrivalMode = "down") { if (!inTown && mapGrid && mapGrid.length > 0 && currentFloor !== floorNum) { saveCurrentFloorToCache(); } inTown = false; currentFloor = floorNum; savedDungeonFloorBeforeTown = floorNum; if (!hero.highestFloor || floorNum > hero.highestFloor) { hero.highestFloor = floorNum; } projectiles = []; floatingNumbers = []; cancelAimMode(); const dCfg = DUNGEONS_DB[currentDungeonId] || DUNGEONS_DB.catacombs; const cacheKey = getFloorCacheKey(floorNum, currentDungeonId); // Check if this floor was already visited in this dungeon! if (floorCache[cacheKey] && arrivalMode !== "fresh") { const cached = floorCache[cacheKey]; mapGrid = cached.mapGrid; fovMap = cached.fovMap; dungeonRooms = cached.dungeonRooms; monsters = cached.monsters; floorItems = cached.floorItems; floorTraps = cached.floorTraps; stairsDown = cached.stairsDown || { x: -1, y: -1 }; stairsUp = cached.stairsUp || { x: -1, y: -1 }; // Ensure Floor 1 also has stairs_up leading to Town Hub if (dungeonRooms.length > 0 && (stairsUp.x < 0 || mapGrid[stairsUp.y]?.[stairsUp.x]?.type !== "stairs_up")) { stairsUp = { x: dungeonRooms[0].cx, y: dungeonRooms[0].cy }; mapGrid[stairsUp.y][stairsUp.x] = { type: "stairs_up" }; } if (arrivalMode === "up" && stairsDown.x >= 0) { hero.x = stairsDown.x; hero.y = stairsDown.y; } else if (arrivalMode === "down" && stairsUp.x >= 0) { hero.x = stairsUp.x; hero.y = stairsUp.y; } else if (dungeonRooms.length > 0) { hero.x = dungeonRooms[0].cx; hero.y = dungeonRooms[0].cy; } const aliveCount = monsters.filter(m => m.hp > 0).length; if (aliveCount < 3 && dungeonRooms.length > 2) { const r = dungeonRooms[Math.floor(Math.random() * (dungeonRooms.length - 1)) + 1]; if (Math.hypot(r.cx - hero.x, r.cy - hero.y) > 6) { monsters.push(createMonster(chooseMonsterType(floorNum), r.cx, r.cy)); } } recalcHeroStats(); computeFOV(); centerCameraOnHero(); updateHUD(); saveGameProgress(); const zone = ZONES[Math.min(floorNum - 1, ZONES.length - 1)]; showFloorBanner(floorNum, `${dCfg.icon} ${dCfg.name} — ${zone.name}`, "Bereits erkundete Ebene"); return; } monsters = []; floorItems = []; floorTraps = []; dungeonRooms = []; mapGrid = []; fovMap = []; for (let y = 0; y < MAP_H; y++) { mapGrid[y] = []; fovMap[y] = []; for (let x = 0; x < MAP_W; x++) { mapGrid[y][x] = { type: "wall", hasTorch: false }; fovMap[y][x] = { explored: false, visible: false }; } } const numRoomAttempts = 34; const minRoomSize = 5; const maxRoomSize = 11; for (let i = 0; i < numRoomAttempts; i++) { const rw = Math.floor(Math.random() * (maxRoomSize - minRoomSize + 1)) + minRoomSize; const rh = Math.floor(Math.random() * (maxRoomSize - minRoomSize + 1)) + minRoomSize; const rx = Math.floor(Math.random() * (MAP_W - rw - 4)) + 2; const ry = Math.floor(Math.random() * (MAP_H - rh - 4)) + 2; const newRoom = { x: rx, y: ry, w: rw, h: rh, cx: Math.floor(rx + rw / 2), cy: Math.floor(ry + rh / 2) }; let overlaps = false; for (const r of dungeonRooms) { if (rx - 1 < r.x + r.w && rx + rw + 1 > r.x && ry - 1 < r.y + r.h && ry + rh + 1 > r.y) { overlaps = true; break; } } if (!overlaps) { for (let y = ry; y < ry + rh; y++) { for (let x = rx; x < rx + rw; x++) { mapGrid[y][x] = { type: "floor" }; } } dungeonRooms.push(newRoom); } } if (dungeonRooms.length < 8) { return generateDungeonFloor(floorNum, arrivalMode); } for (let i = 0; i < dungeonRooms.length - 1; i++) { carveCorridor(dungeonRooms[i].cx, dungeonRooms[i].cy, dungeonRooms[i + 1].cx, dungeonRooms[i + 1].cy); } carveCorridor(dungeonRooms[dungeonRooms.length - 1].cx, dungeonRooms[dungeonRooms.length - 1].cy, dungeonRooms[0].cx, dungeonRooms[0].cy); // Place harvestable wall torches (🔥 Feueressenz!) on room walls dungeonRooms.forEach(r => { if (Math.random() < 0.72 && mapGrid[r.y - 1]?.[r.cx]?.type === "wall") mapGrid[r.y - 1][r.cx].hasTorch = true; if (Math.random() < 0.50 && mapGrid[r.y + r.h]?.[r.cx]?.type === "wall") mapGrid[r.y + r.h][r.cx].hasTorch = true; }); // Start Room: Player + Stairs Up (Floor 1 -> Town Hub, Floor >= 2 -> Floor - 1) + Anvil const startRoom = dungeonRooms[0]; hero.x = startRoom.cx; hero.y = startRoom.cy; stairsUp = { x: startRoom.cx, y: startRoom.cy }; mapGrid[stairsUp.y][stairsUp.x] = { type: "stairs_up" }; if (floorNum === 1 && isWalkable(startRoom.x + startRoom.w - 2, startRoom.y + 1)) { mapGrid[startRoom.y + 1][startRoom.x + startRoom.w - 2] = { type: "node_wood" }; } // Place 1 Damaged Dungeon Forge in one of the explored rooms (+20% Materialkosten) if (dungeonRooms.length > 2) { const fRoom = dungeonRooms[1]; if (isWalkable(fRoom.cx, fRoom.cy)) { mapGrid[fRoom.cy][fRoom.cx] = { type: "forge_damaged" }; } } // Place Stairs Down in furthest room const endRoom = dungeonRooms[dungeonRooms.length - 1]; if (floorNum < MAX_FLOORS) { stairsDown = { x: endRoom.cx, y: endRoom.cy }; mapGrid[stairsDown.y][stairsDown.x] = { type: "stairs_down" }; } else { stairsDown = { x: -1, y: -1 }; } if (arrivalMode === "up" && stairsDown.x >= 0) { hero.x = stairsDown.x; hero.y = stairsDown.y; } // Populate Rooms const monsterSpawnRooms = dungeonRooms.slice(1); monsterSpawnRooms.forEach((r, idx) => { const isFinalBoss = (floorNum === MAX_FLOORS && idx === monsterSpawnRooms.length - 1); const isMidBoss = (floorNum === 5 && idx === monsterSpawnRooms.length - 1); if (isFinalBoss) { monsters.push(createMonster(dCfg.bossId || "boss_goblin", r.cx, r.cy)); monsters.push(createMonster("shaman", r.cx - 2, r.cy)); monsters.push(createMonster("golem", r.cx + 2, r.cy)); return; } if (isMidBoss) { monsters.push(createMonster(dCfg.midBossId || "midboss_orc", r.cx, r.cy)); monsters.push(createMonster("shaman", r.cx - 2, r.cy)); monsters.push(createMonster("skel_archer", r.cx + 2, r.cy)); return; } const count = Math.floor(Math.random() * 2) + 1 + Math.floor(floorNum / 2.2); for (let c = 0; c < count; c++) { const mx = r.x + 1 + Math.floor(Math.random() * (r.w - 2)); const my = r.y + 1 + Math.floor(Math.random() * (r.h - 2)); if (isWalkable(mx, my) && !(mx === stairsDown.x && my === stairsDown.y) && !getMonsterAt(mx, my)) { monsters.push(createMonster(chooseMonsterType(floorNum), mx, my)); } } // Chests if (Math.random() < 0.50) { const cx = r.x + 1 + Math.floor(Math.random() * (r.w - 2)); const cy = r.y + 1 + Math.floor(Math.random() * (r.h - 2)); if (isWalkable(cx, cy) && mapGrid[cy][cx].type === "floor" && !getMonsterAt(cx, cy)) { mapGrid[cy][cx] = { type: "chest" }; } } // Resource Nodes const nodeCount = 1 + (Math.random() < 0.70 ? 1 : 0); for (let n = 0; n < nodeCount; n++) { const nx = r.x + 1 + Math.floor(Math.random() * (r.w - 2)); const ny = r.y + 1 + Math.floor(Math.random() * (r.h - 2)); if (isWalkable(nx, ny) && mapGrid[ny][nx].type === "floor" && !getMonsterAt(nx, ny)) { const roll = Math.random(); const nodeType = roll < 0.30 ? "node_ore" : (roll < 0.58 ? "node_wood" : (roll < 0.82 ? "node_herb" : "node_crystal")); mapGrid[ny][nx] = { type: nodeType }; } } }); recalcHeroStats(); computeFOV(); centerCameraOnHero(); updateHUD(); saveGameProgress(); const zone = ZONES[Math.min(floorNum - 1, ZONES.length - 1)]; showFloorBanner(floorNum, `${dCfg.icon} ${dCfg.name} — ${zone.name}`, zone.desc); } function centerCameraOnHero() { const rectW = canvas ? canvas.getBoundingClientRect().width : window.innerWidth; const rectH = canvas ? canvas.getBoundingClientRect().height : window.innerHeight; camera.x = hero.x * tileSize + tileSize / 2 - (rectW > 0 ? rectW / 2 : 400); camera.y = hero.y * tileSize + tileSize / 2 - (rectH > 0 ? rectH / 2 : 300); } function carveCorridor(x1, y1, x2, y2) { let cx = x1, cy = y1; while (cx !== x2) { if (mapGrid[cy][cx].type === "wall") mapGrid[cy][cx] = { type: "floor" }; cx += (x2 > cx) ? 1 : -1; } while (cy !== y2) { if (mapGrid[cy][cx].type === "wall") mapGrid[cy][cx] = { type: "floor" }; cy += (y2 > cy) ? 1 : -1; } if (mapGrid[cy][cx].type === "wall") mapGrid[cy][cx] = { type: "floor" }; } function chooseMonsterType(floor) { const r = Math.random(); if (currentDungeonId === "frost_caverns") { if (floor <= 2) return r < 0.35 ? "slime" : (r < 0.70 ? "skel_archer" : "skeleton"); if (floor <= 6) return r < 0.30 ? "skel_archer" : (r < 0.65 ? "spider" : "golem"); return r < 0.35 ? "shaman" : (r < 0.70 ? "orc" : "golem"); } if (currentDungeonId === "magma_sanctum") { if (floor <= 3) return r < 0.35 ? "orc" : (r < 0.75 ? "shaman" : "spider"); return r < 0.30 ? "orc" : (r < 0.65 ? "shaman" : "golem"); } if (floor === 1) return r < 0.50 ? "rat" : (r < 0.82 ? "slime" : "goblin"); if (floor === 2) return r < 0.25 ? "rat" : (r < 0.58 ? "slime" : (r < 0.85 ? "goblin" : "skel_archer")); if (floor <= 4) return r < 0.20 ? "goblin" : (r < 0.50 ? "skel_archer" : (r < 0.78 ? "skeleton" : "spider")); if (floor <= 7) return r < 0.22 ? "spider" : (r < 0.48 ? "skeleton" : (r < 0.76 ? "orc" : "shaman")); return r < 0.32 ? "orc" : (r < 0.65 ? "shaman" : "golem"); } function chooseRandomDrop(floor) { const roll = Math.random(); if (roll < 0.28) return "health_potion"; if (roll < 0.42) return "mana_potion"; if (roll < 0.52) return "throwing_knife"; if (roll < 0.60) return "scroll_teleport"; if (roll < 0.66) return "scroll_map"; const gearPool = floor <= 2 ? ["dagger", "iron_sword", "short_bow", "leather_cap", "hide_vest", "cloth_trousers", "traveler_boots", "wood_shield", "buckler", "bone_ring", "bone_talisman"] : (floor <= 5 ? ["spear", "elven_bow", "iron_helm", "leather_armor", "leather_pants", "iron_greaves", "iron_sabatons", "arcane_circlet", "mage_robe", "ruby_ring", "sapphire_ring", "ward_amulet", "quiver"] : ["battle_axe", "composite_bow", "heavy_crossbow", "fire_staff", "plate_armor", "mythril_leggings", "shadow_boots", "flame_striders", "tower_shield", "focus_orb", "amulet_sun", "ember_amulet", "eye_of_grimhold", "archmage_staff"]); return gearPool[Math.floor(Math.random() * gearPool.length)]; } function createMonster(typeId, x, y) { const proto = MONSTER_TYPES[typeId] || MONSTER_TYPES.goblin; const f = Math.max(1, currentFloor); const dCfg = DUNGEONS_DB[currentDungeonId] || DUNGEONS_DB.catacombs; const dMult = dCfg.diffMult || 1.0; const gMult = dCfg.goldMult || 1.0; const hpScale = (1 + (f - 1) * 0.24 + Math.pow(Math.max(0, f - 2), 1.35) * 0.14) * dMult; const atkScale = (1 + (f - 1) * 0.20 + Math.max(0, f - 3) * 0.11) * Math.pow(dMult, 0.85); const defBonus = Math.floor((f - 1) * 0.9 * dMult); const hp = Math.round(proto.hp * hpScale); return { id: Math.random().toString(), typeId, name: proto.name, icon: proto.icon, x, y, hp, maxHp: hp, atk: Math.round(proto.atk * atkScale), def: (proto.def || 0) + defBonus, ranged: proto.ranged || 0, projIcon: proto.projIcon || "🔥", xp: Math.round(proto.xp * (1 + (f - 1) * 0.12) * dMult), gold: Math.round(proto.gold * (1 + (f - 1) * 0.12) * gMult), drops: proto.drops || ["iron_ore"], isBoss: proto.isBoss || false, isMidBoss: proto.isMidBoss || false, alerted: false, stunnedTurns: 0 }; } let isGameOver = false; let selectedClassId = "knight"; let miniCanvas = null; let miniCtx = null; // ================= STATS, FOV & COMBAT ENGINE ================= function getHeroCombatStats() { let atk = hero.baseAtk; let def = hero.baseDef; let maxHp = hero.baseMaxHp; let maxMp = hero.baseMaxMp; let crit = hero.baseCrit || 0.08; let rangedAtk = 0; let range = 4; let spellPwr = hero.baseSpellPwr || 1.0; let lifesteal = 0; let lightBonus = 0; if (!hero.equipped) { hero.equipped = {}; } ALL_EQUIP_SLOTS.forEach(slot => { const entry = hero.equipped[slot]; if (!entry) return; const itemId = typeof entry === "string" ? entry : entry.id; const lvl = typeof entry === "object" ? (entry.upgrade || 0) : 0; const it = ITEMS[itemId]; if (!it) return; if (slot === "weapon") { if (it.type === "ranged" || it.rangedAtk > 0) { rangedAtk = (it.rangedAtk || 0) + lvl * 3; if (it.range && it.range > range) range = it.range; if (it.spellPwr) spellPwr += it.spellPwr + lvl * 0.05; // Ranged weapon does not increase base melee atk (bow in melee deals fixed 1 ATK + knockback) } else { if (it.atk) atk += it.atk + lvl * 2; } } else { if (it.atk) atk += it.atk + lvl * 2; } if (it.def) def += it.def + lvl * 2; if (it.maxHp) maxHp += it.maxHp + lvl * 8; if (it.maxMp) maxMp += it.maxMp + lvl * 5; if (it.crit) crit += it.crit + lvl * 0.02; if (it.spellPwr && slot !== "weapon") spellPwr += it.spellPwr + lvl * 0.05; if (it.lifesteal) lifesteal += it.lifesteal; if (it.lightRadius) lightBonus += it.lightRadius; }); return { atk, def, maxHp, maxMp, crit, rangedAtk, range, spellPwr, lifesteal, lightBonus }; } function recalcHeroStats() { const s = getHeroCombatStats(); hero.maxHp = s.maxHp; hero.maxMp = s.maxMp; if (hero.hp > hero.maxHp) hero.hp = hero.maxHp; if (hero.mp > hero.maxMp) hero.mp = hero.maxMp; } function computeFOV() { if (inTown) { for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { if (fovMap[y]?.[x] && mapGrid[y]?.[x]?.type !== "wall") { fovMap[y][x].visible = true; fovMap[y][x].explored = true; } } } return; } const s = getHeroCombatStats(); const radius = 8.5 + (s.lightBonus || 0); for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { if (fovMap[y]?.[x]) fovMap[y][x].visible = false; } } for (let angle = 0; angle < 360; angle += 1.5) { const rad = angle * (Math.PI / 180); const dx = Math.cos(rad); const dy = Math.sin(rad); let ox = hero.x + 0.5; let oy = hero.y + 0.5; for (let step = 0; step < radius; step++) { const tx = Math.floor(ox); const ty = Math.floor(oy); if (tx < 0 || tx >= MAP_W || ty < 0 || ty >= MAP_H) break; fovMap[ty][tx].visible = true; fovMap[ty][tx].explored = true; if (mapGrid[ty][tx].type === "wall") break; ox += dx * 0.85; oy += dy * 0.85; } } // Wall Torches cast a persistent illumination radius (~2.8 tiles), permanently revealing fog of war! for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { if (mapGrid[y]?.[x]?.hasTorch) { for (let dy = -3; dy <= 3; dy++) { for (let dx = -3; dx <= 3; dx++) { if (Math.hypot(dx, dy) <= 2.8) { const tx = x + dx; const ty = y + dy; if (tx >= 0 && tx < MAP_W && ty >= 0 && ty < MAP_H && fovMap[ty]?.[tx]) { fovMap[ty][tx].visible = true; fovMap[ty][tx].explored = true; } } } } } } } } function isWalkable(x, y) { if (x < 0 || x >= MAP_W || y < 0 || y >= MAP_H) return false; return mapGrid[y]?.[x]?.type !== "wall"; } function getMonsterAt(x, y) { return monsters.find(m => m.x === x && m.y === y && m.hp > 0); } function spawnProjectile(sx, sy, tx, ty, icon, color = "#fbbf24") { projectiles.push({ x: sx * tileSize + tileSize / 2, y: sy * tileSize + tileSize / 2, tx: tx * tileSize + tileSize / 2, ty: ty * tileSize + tileSize / 2, icon, color, progress: 0 }); } // ================= PLAYER ACTIONS & HARVESTING ================= function handleDirectionalMove(dx, dy) { if (isGameOver || !hasActiveRun) return; if (aimMode.active) cancelAimMode(); const nx = hero.x + dx; const ny = hero.y + dy; if (nx < 0 || nx >= MAP_W || ny < 0 || ny >= MAP_H) return; // Town NPC Collision Check (Dynamic NPCs) if (inTown && townNpcs.length > 0) { const npc = townNpcs.find(n => n.x === nx && n.y === ny); if (npc) { if (npc.type === "npc_merchant") { openTownShopModal(); return; } if (npc.type === "npc_smith") { openCraftingModal(); return; } if (npc.type === "npc_healer") { interactWithHealerNpc(nx, ny); return; } if (npc.type === "npc_citizen") { showToast(`${npc.icon} ${npc.name}: "Sei gegrüßt in der Grimhold-Feste, Abenteurer!"`); return; } } } const targetMonster = getMonsterAt(nx, ny); if (targetMonster) { executePlayerMelee(targetMonster); endPlayerTurn(); return; } // Spear 2-tile reach check const wepEntry = hero.equipped?.weapon; const wepId = typeof wepEntry === "string" ? wepEntry : wepEntry?.id; if (!inTown && wepId && ITEMS[wepId]?.reach === 2 && isWalkable(nx, ny)) { const reachMonster = getMonsterAt(nx + dx, ny + dy); if (reachMonster) { executePlayerMelee(reachMonster); endPlayerTurn(); return; } } const tile = mapGrid[ny][nx]; if (tile.type === "wall") { // Wall Torches are persistent light sources, not harvestable! return; } // Town Buildings & Interactive Objects if (tile.type === "player_house") { openPlayerHouseModal(); return; } if (tile.type === "forge_damaged") { openCraftingModal(); return; } if (tile.type === "npc_merchant") { openTownShopModal(); return; } if (tile.type === "npc_smith" || tile.type === "anvil") { openCraftingModal(); return; } if (tile.type === "npc_healer") { interactWithHealerNpc(nx, ny); return; } if (tile.type === "town_portal") { openDungeonsModal(); return; } if (tile.type.startsWith("node_")) { harvestResourceNode(nx, ny, tile.type); endPlayerTurn(); return; } hero.x = nx; hero.y = ny; playSound("step"); if (tile.type === "chest") { openChestAt(nx, ny); } checkLootPickup(nx, ny); if (tile.type === "stairs_down") { if (inTown) { leaveTownToDungeon(currentDungeonId, savedDungeonFloorBeforeTown || 1); } else { descendStairs(); } return; } if (tile.type === "stairs_up") { ascendStairs(); return; } if (inTown) { saveGameProgress(); } else { endPlayerTurn(); } } function interactWithHealerNpc(tx, ty) { if (hero.hp >= hero.maxHp && hero.mp >= hero.maxMp) { showToast("🧝♀️ Heilerin Seraphina: Deine Lebenskraft und dein Mana sind bereits voll!"); return; } const cost = 15; if (hero.gold < cost) { hero.hp = Math.min(hero.maxHp, hero.hp + Math.round(hero.maxHp * 0.4)); hero.mp = Math.min(hero.maxMp, hero.mp + Math.round(hero.maxMp * 0.4)); playSound("potion"); updateHUD(); saveGameProgress(); showToast("🧝♀️ Seraphina spendet dir einen kostenlosen Not-Segen (+40% LP/MP)!"); return; } hero.gold -= cost; hero.hp = hero.maxHp; hero.mp = hero.maxMp; playSound("levelup"); spawnFloatingText(tx, ty, "✨ Vollheilung!", "#34d399"); updateHUD(); saveGameProgress(); logCombat(`🧝♀️ Heilerin Seraphina heilt deine Wunden und dein Mana vollständig (-${cost}💰)!`); } function addMaterial(matKey, qty = 1) { if (!hero.materials) hero.materials = {}; hero.materials[matKey] = (hero.materials[matKey] || 0) + qty; updateHUD(); } function harvestResourceNode(x, y, nodeType) { playSound("hit"); mapGrid[y][x] = { type: "floor" }; let matId = "iron_ore"; let qty = 1 + (Math.random() < 0.65 ? 1 : 0); if (nodeType === "node_ore") { matId = (currentFloor >= 4 && Math.random() < 0.30) ? "mythril_ingot" : "iron_ore"; } else if (nodeType === "node_wood") { matId = "wood_branch"; qty = 2 + (Math.random() < 0.5 ? 1 : 0); } else if (nodeType === "node_herb") { matId = "healing_herb"; } else if (nodeType === "node_crystal") { matId = (currentDungeonId === "magma_sanctum" && Math.random() < 0.55) ? "fire_essence" : "mana_shard"; } addMaterial(matId, qty); const meta = MATERIAL_META[matId] || { name: matId, icon: "📦" }; spawnFloatingText(x, y, `+${qty} ${meta.icon}`, "#6ee7b7"); logCombat(`⛏️ Abgebaut: ${qty}x ${meta.icon} ${meta.name}!`); } function executePlayerMelee(monster) { const wepEntry = hero.equipped?.weapon; const wepId = typeof wepEntry === "string" ? wepEntry : wepEntry?.id; const isRangedWep = wepId && (ITEMS[wepId]?.type === "ranged" || ITEMS[wepId]?.rangedAtk > 0); // RANGED WEAPON IN MELEE RANGE: Deals minimal 1 ATK damage + Knocks enemy back 1 tile! if (isRangedWep) { const dmg = 1; monster.hp -= dmg; monster.alerted = true; playSound("hit"); const kx = Math.sign(monster.x - hero.x); const ky = Math.sign(monster.y - hero.y); const destX = monster.x + kx; const destY = monster.y + ky; let knocked = false; if (isWalkable(destX, destY) && !getMonsterAt(destX, destY) && !(destX === hero.x && destY === hero.y)) { monster.x = destX; monster.y = destY; knocked = true; } spawnFloatingText(monster.x, monster.y, knocked ? "💥 1 + Rückstoß!" : "💥 1", "#fbbf24"); logCombat(`🏹 Bogen-Nahkampf: Du stößt ${monster.name} mit dem Bogen zurück (1 Schaden${knocked ? " + 1 Feld Rückstoß" : ""})!`); if (monster.hp <= 0) handleMonsterDeath(monster); return; } const stats = getHeroCombatStats(); const isCrit = Math.random() < stats.crit; const rawDmg = stats.atk + Math.floor(Math.random() * 4) - 1; const dmg = Math.max(1, Math.round((isCrit ? rawDmg * 1.65 : rawDmg) - (monster.def || 0))); monster.hp -= dmg; monster.alerted = true; playSound("hit"); if (stats.lifesteal > 0) { const heal = Math.max(1, Math.round(dmg * stats.lifesteal)); hero.hp = Math.min(hero.maxHp, hero.hp + heal); } spawnFloatingText(monster.x, monster.y, isCrit ? `💥 ${dmg}!` : `-${dmg}`, isCrit ? "#fbbf24" : "#f87171"); logCombat(`⚔️ Du triffst ${monster.name} für ${dmg} Schaden.`); if (monster.hp <= 0) handleMonsterDeath(monster); } function openChestAt(x, y) { mapGrid[y][x] = { type: "floor" }; playSound("chest"); const dCfg = DUNGEONS_DB[currentDungeonId] || DUNGEONS_DB.catacombs; const goldAmt = Math.round((Math.floor(Math.random() * 25) + 12 * currentFloor) * (dCfg.goldMult || 1)); hero.gold += goldAmt; const dropId = chooseRandomDrop(currentFloor); addItemToInventory(dropId, 1); const bonusMat = ["iron_ore", "wood_branch", "healing_herb", "mana_shard", "fire_essence"][Math.floor(Math.random() * 5)]; addMaterial(bonusMat, 2); spawnFloatingText(x, y, `📦 +${goldAmt}G`, "#fbbf24"); logCombat(`📦 Truhe: +${goldAmt}G, ${ITEMS[dropId]?.name || dropId} & +2 ${MATERIAL_META[bonusMat].icon}!`); } function checkLootPickup(x, y) { for (let i = floorItems.length - 1; i >= 0; i--) { const fi = floorItems[i]; if (fi.x === x && fi.y === y) { if (fi.matKey || MATERIAL_META[fi.itemId]) { const mk = fi.matKey || fi.itemId; addMaterial(mk, fi.qty || 1); const m = MATERIAL_META[mk]; spawnFloatingText(x, y, `+${fi.qty || 1} ${m.icon}`, "#34d399"); logCombat(`👜 Gesammelt: ${fi.qty || 1}x ${m.icon} ${m.name}.`); floorItems.splice(i, 1); } else if (addItemToInventory(fi.itemId, fi.qty || 1)) { logCombat(`🎒 Aufgehoben: ${ITEMS[fi.itemId]?.name || fi.itemId}.`); floorItems.splice(i, 1); } else { showToast("Beutel voll! Öffne das Inventar.", "#ef4444"); } } } } function playerWaitTurn() { if (isGameOver || !hasActiveRun) return; if (aimMode.active) cancelAimMode(); const curTile = mapGrid[hero.y]?.[hero.x]; if (curTile?.type === "stairs_down") { if (inTown) leaveTownToDungeon(currentDungeonId, savedDungeonFloorBeforeTown || 1); else descendStairs(); return; } if (curTile?.type === "stairs_up") { ascendStairs(); return; } if (hero.mp < hero.maxMp) hero.mp = Math.min(hero.maxMp, hero.mp + 1); spawnFloatingText(hero.x, hero.y, "⏳ +1 MP", "#94a3b8"); if (!inTown) endPlayerTurn(); } function descendStairs() { if (currentFloor >= MAX_FLOORS) return; playSound("stairs"); const nextFloor = currentFloor + 1; const cacheKey = getFloorCacheKey(nextFloor, currentDungeonId); const isFirstVisit = !floorCache[cacheKey] && nextFloor > (hero.highestFloor || 1); if (isFirstVisit) { hero.hp = Math.min(hero.maxHp, hero.hp + Math.round(hero.maxHp * 0.12)); hero.mp = Math.min(hero.maxMp, hero.mp + Math.round(hero.maxMp * 0.20)); logCombat(`🔽 Du steigst hinab auf Ebene ${nextFloor} (+12% LP regeneriert).`); } else { logCombat(`🔽 Du steigst wieder hinab auf Ebene ${nextFloor}.`); } generateDungeonFloor(nextFloor, "down"); syncMultiplayerPresence(); } function ascendStairs() { if (currentFloor <= 1) { enterTownHub(); return; } playSound("stairs"); const prevFloor = currentFloor - 1; logCombat(`🔼 Du kletterst zurück nach oben auf Ebene ${prevFloor}.`); generateDungeonFloor(prevFloor, "up"); syncMultiplayerPresence(); } // ================= RANGED COMBAT & SKILLS ================= function getVisibleMonstersInRange(maxRange) { return monsters.filter(m => { if (m.hp <= 0) return false; if (!fovMap[m.y]?.[m.x]?.visible) return false; const dist = Math.hypot(m.x - hero.x, m.y - hero.y); return dist <= maxRange; }).sort((a, b) => Math.hypot(a.x - hero.x, a.y - hero.y) - Math.hypot(b.x - hero.x, b.y - hero.y)); } function triggerRangedAttackOrAim() { if (isGameOver || !hasActiveRun) return; const wepEntry = hero.equipped?.weapon || hero.equipped?.ranged; const wepId = typeof wepEntry === "string" ? wepEntry : wepEntry?.id; const wepProto = wepId ? ITEMS[wepId] : null; const usesMana = (wepProto && wepProto.ammoType === "mana") || (!wepProto && hero.classId === "mage"); if (!usesMana && (hero.arrows || 0) <= 0) { cancelAimMode(); openNoArrowsModal(); return; } const s = getHeroCombatStats(); const maxR = Math.max(5.5, s.range + 1); if (aimMode.active) { const targets = getVisibleMonstersInRange(maxR); const chosen = targets[aimMode.targetIndex] || targets[0]; if (chosen) { fireRangedAtMonster(chosen); cancelAimMode(); return; } } const targets = getVisibleMonstersInRange(maxR); if (targets.length === 0) { showToast("Kein sichtbares Ziel in Fernkampfreichweite!"); return; } aimMode.active = true; aimMode.validTargets = targets; aimMode.targetIndex = 0; const banner = document.getElementById("aim-mode-banner"); const txt = document.getElementById("aim-mode-text"); if (banner) banner.classList.remove("hidden"); if (txt) txt.textContent = `🎯 Ziel: ${targets[0].name} (Nochmal drücken oder antippen zum Feuern!)`; } function cycleAimTarget() { const targets = getVisibleMonstersInRange(6.5); if (targets.length === 0) { cancelAimMode(); return; } aimMode.validTargets = targets; aimMode.targetIndex = (aimMode.targetIndex + 1) % targets.length; const txt = document.getElementById("aim-mode-text"); if (txt) txt.textContent = `🎯 Ziel: ${targets[aimMode.targetIndex].name}`; } function cancelAimMode() { aimMode.active = false; aimMode.validTargets = []; aimMode.targetIndex = 0; document.getElementById("aim-mode-banner")?.classList.add("hidden"); } function fireRangedAtMonster(monster, bonusMult = 1.0) { const stats = getHeroCombatStats(); const rangedEntry = hero.equipped?.weapon || hero.equipped?.ranged; const rangedId = typeof rangedEntry === "string" ? rangedEntry : rangedEntry?.id; const rangedItem = rangedId ? ITEMS[rangedId] : null; const usesMana = (rangedItem && rangedItem.ammoType === "mana") || (!rangedItem && hero.classId === "mage"); if (usesMana) { const mpCost = rangedItem ? (rangedItem.manaCost || 2) : 2; if (hero.mp < mpCost) { showToast(`Nicht genug Mana (${mpCost} MP benötigt)!`); return false; } hero.mp -= mpCost; spawnProjectile(hero.x, hero.y, monster.x, monster.y, rangedItem?.projIcon || "✨", "#38bdf8"); } else { if ((hero.arrows || 0) <= 0) { cancelAimMode(); openNoArrowsModal(); return false; } // Elven quiver 35% chance to save arrow const shEntry = hero.equipped?.shield; const shId = typeof shEntry === "string" ? shEntry : shEntry?.id; if (!(shId === "quiver" && Math.random() < 0.35)) { hero.arrows--; } spawnProjectile(hero.x, hero.y, monster.x, monster.y, rangedItem?.projIcon || "🏹", "#fbbf24"); } playSound("shoot"); const isCrit = Math.random() < stats.crit; const baseDmg = (stats.rangedAtk > 0 ? stats.rangedAtk + Math.round(stats.atk * 0.45) : Math.round(stats.atk * 0.85)) * bonusMult; const dmg = Math.max(2, Math.round((isCrit ? baseDmg * 1.6 : baseDmg) - Math.floor((monster.def || 0) * 0.5))); monster.hp -= dmg; monster.alerted = true; spawnFloatingText(monster.x, monster.y, isCrit ? `🎯 ${dmg}!` : `🏹 -${dmg}`, isCrit ? "#fbbf24" : "#34d399"); logCombat(`🏹 Fernkampf auf ${monster.name}: ${dmg} Schaden!`); if (monster.hp <= 0) handleMonsterDeath(monster); endPlayerTurn(); return true; } function executeSkillById(skillId) { if (isGameOver || !hasActiveRun) return; const sk = SKILLS_DB[skillId]; if (!sk) return; if (skillId === "ranged_shot") { triggerRangedAttackOrAim(); return; } if (hero.mp < sk.mpCost) { showToast(`Nicht genug Mana für ${sk.name} (${sk.mpCost} MP)!`); return; } const stats = getHeroCombatStats(); if (skillId === "whirlwind") { hero.mp -= sk.mpCost; playSound("hit"); let hits = 0; monsters.forEach(m => { if (m.hp > 0 && Math.abs(m.x - hero.x) <= 1 && Math.abs(m.y - hero.y) <= 1) { const dmg = Math.max(3, Math.round(stats.atk * 1.3)); m.hp -= dmg; hits++; spawnFloatingText(m.x, m.y, `⚔️ -${dmg}`, "#fbbf24"); if (m.hp <= 0) handleMonsterDeath(m); } }); spawnFloatingText(hero.x, hero.y, "⚔️ WIRBELWIND", "#fbbf24"); logCombat(`⚔️ Wirbelwind-Hieb trifft ${hits} Gegner!`); endPlayerTurn(); } else if (skillId === "shield_bash") { const adj = monsters.find(m => m.hp > 0 && Math.abs(m.x - hero.x) <= 1.5 && Math.abs(m.y - hero.y) <= 1.5); if (!adj) { showToast("Kein Gegner direkt neben dir für Schildstoß!"); return; } hero.mp -= sk.mpCost; playSound("hit"); const dmg = Math.max(3, Math.round(stats.atk * 1.1 + stats.def)); adj.hp -= dmg; adj.stunnedTurns = 2; spawnFloatingText(adj.x, adj.y, `🛡️ BETÄUBT -${dmg}`, "#38bdf8"); logCombat(`🛡️ Schildstoß betäubt ${adj.name} für 2 Runden (-${dmg} LP)!`); if (adj.hp <= 0) handleMonsterDeath(adj); endPlayerTurn(); } else if (skillId === "power_snipe") { const targets = getVisibleMonstersInRange(6.5); if (targets.length === 0) { showToast("Kein sichtbares Ziel in Reichweite!"); return; } hero.mp -= sk.mpCost; fireRangedAtMonster(targets[0], 1.65); } else if (skillId === "thorn_trap") { hero.mp -= sk.mpCost; floorTraps.push({ x: hero.x, y: hero.y, dmg: Math.round(stats.atk * 1.5 + 14) }); playSound("chest"); spawnFloatingText(hero.x, hero.y, "🪤 Dornenfalle", "#34d399"); logCombat(`🪤 Du legst eine Dornenfalle auf dein Feld.`); endPlayerTurn(); } else if (skillId === "magic_missile") { const targets = getVisibleMonstersInRange(6.5); if (targets.length === 0) { showToast("Kein sichtbarer Gegner in Reichweite!"); return; } hero.mp -= sk.mpCost; playSound("spell"); const t = targets[0]; spawnProjectile(hero.x, hero.y, t.x, t.y, "⚡", "#38bdf8"); const dmg = Math.round((stats.atk + 10) * stats.spellPwr); t.hp -= dmg; t.alerted = true; spawnFloatingText(t.x, t.y, `⚡ -${dmg}`, "#38bdf8"); logCombat(`⚡ Arkanpfeil trifft ${t.name} für ${dmg} Schaden!`); if (t.hp <= 0) handleMonsterDeath(t); endPlayerTurn(); } else if (skillId === "fireball") { hero.mp -= sk.mpCost; playSound("spell"); let count = 0; monsters.forEach(m => { if (m.hp > 0 && Math.hypot(m.x - hero.x, m.y - hero.y) <= 3.8) { const dmg = Math.round((stats.atk * 1.3 + 12) * stats.spellPwr); m.hp -= dmg; count++; spawnFloatingText(m.x, m.y, `🔥 -${dmg}`, "#f97316"); if (m.hp <= 0) handleMonsterDeath(m); } }); spawnFloatingText(hero.x, hero.y, "🔥 FEUERBALL-NOVA", "#f97316"); logCombat(`🔥 Feuerball-Nova verbrennt ${count} Gegner!`); endPlayerTurn(); } else if (skillId === "frost_nova") { hero.mp -= sk.mpCost; playSound("spell"); let count = 0; monsters.forEach(m => { if (m.hp > 0 && Math.hypot(m.x - hero.x, m.y - hero.y) <= 3.5) { m.stunnedTurns = 2; const dmg = Math.max(4, Math.round(stats.atk * 0.8 * stats.spellPwr)); m.hp -= dmg; count++; spawnFloatingText(m.x, m.y, `❄️ FROST -${dmg}`, "#38bdf8"); if (m.hp <= 0) handleMonsterDeath(m); } }); logCombat(`❄️ Frostnova friert ${count} Gegner für 2 Runden ein!`); endPlayerTurn(); } } function useQuickSlot(slotIdx) { if (isGameOver || !hasActiveRun) return; const entry = quickslots[slotIdx]; if (!entry || !entry.id) { openShortcutsModal(); return; } if (entry.type === "skill") { executeSkillById(entry.id); } else if (entry.type === "item") { const invIdx = hero.inventory.findIndex(s => s.id === entry.id); if (invIdx === -1) { showToast(`Kein ${ITEMS[entry.id]?.name || "Gegenstand"} im Rucksack!`); return; } useInventoryItem(invIdx); } } // ================= MONSTER TURN AI ================= function endPlayerTurn() { hero.turns = (hero.turns || 0) + 1; for (const m of monsters) { if (m.hp <= 0) continue; if (m.stunnedTurns > 0) { m.stunnedTurns--; continue; } const dist = Math.hypot(m.x - hero.x, m.y - hero.y); if (fovMap[m.y]?.[m.x]?.visible && dist <= 9.0) { m.alerted = true; } if (!m.alerted) continue; // Shaman special: 30% chance to heal a wounded ally in range if (m.typeId === "shaman" && Math.random() < 0.30) { const woundedAlly = monsters.find(a => a.hp > 0 && a.hp < a.maxHp * 0.75 && Math.hypot(a.x - m.x, a.y - m.y) <= 4.5); if (woundedAlly) { const healAmt = Math.round(14 + currentFloor * 3); woundedAlly.hp = Math.min(woundedAlly.maxHp, woundedAlly.hp + healAmt); spawnFloatingText(woundedAlly.x, woundedAlly.y, `💚 +${healAmt}`, "#34d399"); logCombat(`🧙♂️ ${m.name} heilt ${woundedAlly.name} um +${healAmt} LP!`); continue; } } if (Math.abs(m.x - hero.x) <= 1 && Math.abs(m.y - hero.y) <= 1 && (m.x === hero.x || m.y === hero.y)) { executeMonsterMelee(m); } else if (m.ranged > 0 && fovMap[m.y]?.[m.x]?.visible && dist <= m.ranged && Math.random() < 0.72) { executeMonsterRanged(m); } else { moveMonsterTowardsHero(m); } if (hero.hp <= 0) { triggerGameOver(false, m.name); return; } } recalcHeroStats(); computeFOV(); updateHUD(); saveGameProgress(); } function executeMonsterMelee(m) { const stats = getHeroCombatStats(); let atkPower = m.atk + Math.floor(Math.random() * 4); let specialNote = ""; // Orc / Boss Berserk below 60% HP if ((m.typeId === "orc" || m.isMidBoss || m.isBoss) && m.hp < m.maxHp * 0.60) { atkPower = Math.round(atkPower * 1.30); specialNote = " (🔥 Kampfrausch!)"; } const minFloorDmg = Math.max(2, Math.floor(currentFloor * 0.85)); let dmg = Math.max(minFloorDmg, atkPower - Math.floor(stats.def * 0.55)); // Spider Poison Bite if (m.typeId === "spider" && Math.random() < 0.35) { const poisonDmg = 3 + Math.floor(currentFloor * 0.8); dmg += poisonDmg; specialNote = ` (+${poisonDmg} 🕷️ Gift!)`; } hero.hp -= dmg; playSound("hit"); spawnFloatingText(hero.x, hero.y, `-${dmg}`, "#ef4444"); logCombat(`🩸 ${m.name} trifft dich für ${dmg} Schaden${specialNote}.`); } function executeMonsterRanged(m) { const stats = getHeroCombatStats(); spawnProjectile(m.x, m.y, hero.x, hero.y, m.projIcon || "🏹", "#f87171"); const minFloorDmg = Math.max(2, Math.floor(currentFloor * 0.75)); const defFactor = m.typeId === "shaman" ? 0.30 : 0.48; // Shaman magic pierces armor const dmg = Math.max(minFloorDmg, Math.round(m.atk * 0.90) - Math.floor(stats.def * defFactor)); hero.hp -= dmg; playSound("shoot"); spawnFloatingText(hero.x, hero.y, `-${dmg}`, "#fb923c"); logCombat(`${m.projIcon || "🏹"} ${m.name} trifft dich aus der Distanz (-${dmg} LP)!`); } function moveMonsterTowardsHero(m) { const dx = Math.sign(hero.x - m.x); const dy = Math.sign(hero.y - m.y); const candidates = []; // Primary cardinal steps prioritized by greater distance axis if (Math.abs(hero.x - m.x) >= Math.abs(hero.y - m.y)) { if (dx !== 0) candidates.push({ x: m.x + dx, y: m.y }); if (dy !== 0) candidates.push({ x: m.x, y: m.y + dy }); } else { if (dy !== 0) candidates.push({ x: m.x, y: m.y + dy }); if (dx !== 0) candidates.push({ x: m.x + dx, y: m.y }); } // Diagonal flanking if cardinal path is blocked if (dx !== 0 && dy !== 0) { candidates.push({ x: m.x + dx, y: m.y + dy }); } for (const pos of candidates) { if (pos.x === hero.x && pos.y === hero.y) { executeMonsterMelee(m); return; } if (isWalkable(pos.x, pos.y) && !getMonsterAt(pos.x, pos.y)) { m.x = pos.x; m.y = pos.y; const trapIdx = floorTraps.findIndex(t => t.x === m.x && t.y === m.y); if (trapIdx !== -1) { const trap = floorTraps[trapIdx]; m.hp -= trap.dmg; m.stunnedTurns = 2; floorTraps.splice(trapIdx, 1); spawnFloatingText(m.x, m.y, `🪤 -${trap.dmg}!`, "#34d399"); if (m.hp <= 0) handleMonsterDeath(m); } return; } } } function handleMonsterDeath(m) { m.hp = 0; hero.kills = (hero.kills || 0) + 1; hero.xp += m.xp; hero.gold += m.gold; if (m.drops && m.drops.length > 0 && Math.random() < 0.78) { const dropKey = m.drops[Math.floor(Math.random() * m.drops.length)]; if (MATERIAL_META[dropKey]) { addMaterial(dropKey, 1); spawnFloatingText(m.x, m.y - 0.3, `+1 ${MATERIAL_META[dropKey].icon}`, "#6ee7b7"); } else if (ITEMS[dropKey]) { floorItems.push({ x: m.x, y: m.y, itemId: dropKey, qty: 1 }); } } else if (Math.random() < 0.25) { floorItems.push({ x: m.x, y: m.y, itemId: chooseRandomDrop(currentFloor), qty: 1 }); } logCombat(`💀 ${m.name} besiegt! (+${m.xp} XP, +${m.gold}G)`); checkLevelUp(); if (m.isBoss) { triggerGameOver(true, m.name); } } function checkLevelUp() { while (hero.xp >= hero.xpNeeded) { hero.xp -= hero.xpNeeded; hero.level++; hero.xpNeeded = Math.round(hero.xpNeeded * 1.48); // 3 Attribute points per Level-Up! hero.statPoints = (hero.statPoints || 0) + 3; recalcHeroStats(); hero.hp = Math.min(hero.maxHp, hero.hp + Math.round(hero.maxHp * 0.30)); hero.mp = Math.min(hero.maxMp, hero.mp + Math.round(hero.maxMp * 0.35)); playSound("levelup"); spawnFloatingText(hero.x, hero.y, `⭐ STUFE ${hero.level}! (+3 Punkte)`, "#fbbf24"); showToast(`⭐ STUFENAUFSTIEG! Du bist jetzt Stufe ${hero.level}! (+3 Attributspunkte & +2 Beutelplätze)`); } } // ================= CLEAN 1-CLICK INVENTORY & PAPERDOLL ================= function addItemToInventory(itemId, qty = 1) { if (MATERIAL_META[itemId]) { addMaterial(itemId, qty); return true; } const proto = ITEMS[itemId]; if (!proto) return false; if (proto.type === "consumable") { const existing = hero.inventory.find(s => s.id === itemId); if (existing) { existing.qty += qty; updateHUD(); updateInventoryUI(); return true; } } const maxSlots = getMaxBagSlots(); if (hero.inventory.length >= maxSlots) { showToast(`Beutel voll (${hero.inventory.length}/${maxSlots} Plätze)!`); return false; } hero.inventory.push({ id: itemId, qty, upgrade: 0 }); updateHUD(); updateInventoryUI(); return true; } function useInventoryItem(bagIdx) { const entry = hero.inventory[bagIdx]; if (!entry) return; const proto = ITEMS[entry.id]; if (!proto) return; // Equip Gear directly with 1 Click (Single Weapon Slot for Melee OR Ranged!) const isWeaponType = (proto.type === "weapon" || proto.type === "ranged"); const targetSlot = isWeaponType ? "weapon" : proto.type; if (ALL_EQUIP_SLOTS.includes(targetSlot)) { const slot = targetSlot; if (!hero.equipped) hero.equipped = {}; const prev = hero.equipped[slot]; hero.equipped[slot] = { id: entry.id, qty: 1, upgrade: entry.upgrade || 0 }; hero.inventory.splice(bagIdx, 1); if (prev) { const prevObj = typeof prev === "string" ? { id: prev, qty: 1, upgrade: 0 } : prev; hero.inventory.push(prevObj); } playSound("chest"); recalcHeroStats(); updateHUD(); updateInventoryUI(); saveGameProgress(); showToast(`🛡️ ${proto.name} angelegt!`); return; } // Use Consumable if (proto.type === "consumable") { playSound("potion"); if (proto.heal) { hero.hp = Math.min(hero.maxHp, hero.hp + proto.heal); spawnFloatingText(hero.x, hero.y, `+${proto.heal} ❤️`, "#10b981"); } if (proto.mana) { hero.mp = Math.min(hero.maxMp, hero.mp + proto.mana); spawnFloatingText(hero.x, hero.y, `+${proto.mana} 🔮`, "#38bdf8"); } if (proto.permHp || proto.permAtk) { hero.baseMaxHp += (proto.permHp || 0); hero.baseAtk += (proto.permAtk || 0); recalcHeroStats(); hero.hp = hero.maxHp; spawnFloatingText(hero.x, hero.y, `🍷 +${proto.permHp} HP / +${proto.permAtk} ATK!`, "#fbbf24"); } if (proto.throwDmg) { const targets = getVisibleMonstersInRange(proto.range || 5); if (targets.length === 0) { showToast("Kein sichtbarer Gegner für Wurfmesser!"); return; } const t = targets[0]; spawnProjectile(hero.x, hero.y, t.x, t.y, proto.projIcon || "🗡️", "#cbd5e1"); t.hp -= proto.throwDmg; spawnFloatingText(t.x, t.y, `🗡️ -${proto.throwDmg}`, "#f87171"); if (t.hp <= 0) handleMonsterDeath(t); } if (proto.bombDmg) { const targets = getVisibleMonstersInRange(proto.range || 4); if (targets.length === 0) { showToast("Kein Gegner in Reichweite für die Feuerbombe!"); return; } const main = targets[0]; spawnProjectile(hero.x, hero.y, main.x, main.y, "💣", "#f97316"); monsters.forEach(m => { if (m.hp > 0 && Math.abs(m.x - main.x) <= 1 && Math.abs(m.y - main.y) <= 1) { m.hp -= proto.bombDmg; spawnFloatingText(m.x, m.y, `💣 -${proto.bombDmg}`, "#f97316"); if (m.hp <= 0) handleMonsterDeath(m); } }); } if (proto.stunTurns) { const targets = getVisibleMonstersInRange(proto.range || 5); if (targets.length === 0) { showToast("Kein Gegner in Reichweite für das Fangnetz!"); return; } const t = targets[0]; spawnProjectile(hero.x, hero.y, t.x, t.y, "🕸️", "#e2e8f0"); t.stunnedTurns = proto.stunTurns; spawnFloatingText(t.x, t.y, `🕸️ GEFESSELT (${proto.stunTurns}R)`, "#c084fc"); } if (proto.spell === "fireball") { monsters.forEach(m => { if (m.hp > 0 && Math.hypot(m.x - hero.x, m.y - hero.y) <= 4.5) { m.hp -= 45; spawnFloatingText(m.x, m.y, "🔥 -45", "#ef4444"); if (m.hp <= 0) handleMonsterDeath(m); } }); } if (proto.spell === "teleport") { if (dungeonRooms.length > 0) { const r = dungeonRooms[Math.floor(Math.random() * dungeonRooms.length)]; hero.x = r.cx; hero.y = r.cy; centerCameraOnHero(); spawnFloatingText(hero.x, hero.y, "✨ TELEPORT!", "#a855f7"); } } if (proto.spell === "map") { for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { if (mapGrid[y][x].type !== "wall") fovMap[y][x].explored = true; } } showToast("🗺️ Gesamte Ebene aufgedeckt!"); } entry.qty--; if (entry.qty <= 0) hero.inventory.splice(bagIdx, 1); updateHUD(); updateInventoryUI(); if (!inTown) endPlayerTurn(); } } function unequipSlot(slotName) { const eq = hero.equipped?.[slotName]; if (!eq) return; const maxSlots = getMaxBagSlots(); if (hero.inventory.length >= maxSlots) { showToast(`Beutel ist voll (${hero.inventory.length}/${maxSlots})! Mache Platz.`); return; } const eqObj = typeof eq === "string" ? { id: eq, qty: 1, upgrade: 0 } : eq; hero.inventory.push(eqObj); hero.equipped[slotName] = null; recalcHeroStats(); updateHUD(); updateInventoryUI(); saveGameProgress(); } function upgradeGearItem(slotName) { const raw = hero.equipped?.[slotName]; if (!raw) return; const entry = typeof raw === "string" ? (hero.equipped[slotName] = { id: raw, qty: 1, upgrade: 0 }) : raw; const proto = ITEMS[entry.id]; if (!proto) return; const nextLvl = (entry.upgrade || 0) + 1; const goldCost = 35 * nextLvl; const oreCost = nextLvl; if (hero.gold < goldCost || (hero.materials.iron_ore || 0) < oreCost) { showToast(`Benötigt: ${goldCost} Gold & ${oreCost}x 🪨 Eisenerz!`); return; } hero.gold -= goldCost; hero.materials.iron_ore -= oreCost; entry.upgrade = nextLvl; playSound("craft"); recalcHeroStats(); updateHUD(); updateInventoryUI(); saveGameProgress(); showToast(`⚒️ ${proto.name} auf +${nextLvl} verbessert!`); } function getItemSalvageReward(entry) { if (!entry || !ITEMS[entry.id]) return { gold: 5, mats: {} }; const proto = ITEMS[entry.id]; const qty = entry.qty || 1; const upg = entry.upgrade || 0; const mats = {}; const recipe = CRAFT_RECIPES.find(r => r.resultItem === entry.id); if (recipe && recipe.cost) { for (const [mKey, cVal] of Object.entries(recipe.cost)) { const refund = Math.max(1, Math.floor((cVal * qty) / 2)); mats[mKey] = (mats[mKey] || 0) + refund; } } else { if (["weapon", "helmet", "armor", "pants", "boots", "shield"].includes(proto.type)) { if (entry.id.includes("mythril")) mats.mythril_ingot = 1 * qty; else if (entry.id.includes("leather") || entry.id.includes("cloth") || entry.id.includes("wood") || entry.id.includes("rusty") || entry.id.includes("traveler")) mats.wood_branch = 1 * qty; else mats.iron_ore = 1 * qty; } else if (proto.type === "ranged") { mats.wood_branch = 1 * qty; } else if (proto.type === "ring" || proto.type === "amulet") { mats.mana_shard = 1 * qty; } else if (proto.type === "consumable") { if (proto.heal) mats.healing_herb = 1 * qty; else if (proto.mana) mats.mana_shard = 1 * qty; else mats.iron_ore = 1 * qty; } } if (upg > 0) { mats.iron_ore = (mats.iron_ore || 0) + upg; } const baseGold = (proto.atk || 0) * 2 + (proto.rangedAtk || 0) * 2 + (proto.def || 0) * 3 + (proto.maxHp || 0) * 0.5 + (proto.heal || 0) * 0.3 + (proto.mana || 0) * 0.4; const gold = Math.max(6, Math.round((baseGold + 8 + upg * 18) * qty)); return { gold, mats }; } function salvageBagItem(bagIdx) { const entry = hero.inventory[bagIdx]; if (!entry) return; const proto = ITEMS[entry.id]; const reward = getItemSalvageReward(entry); hero.inventory.splice(bagIdx, 1); hero.gold += reward.gold; const matParts = [`+${reward.gold}💰`]; for (const [mKey, count] of Object.entries(reward.mats)) { if (count > 0) { addMaterial(mKey, count); const meta = MATERIAL_META[mKey] || { icon: "📦", name: mKey }; matParts.push(`+${count}${meta.icon}`); } } playSound("pickup"); updateHUD(); updateInventoryUI(); saveGameProgress(); showToast(`♻️ ${proto ? proto.name : "Gegenstand"} zerlegt/verkauft (${matParts.join(" ")})!`); } function dropBagItem(bagIdx) { salvageBagItem(bagIdx); } function pinItemToShortcut(itemId) { const emptyIdx = quickslots.findIndex(s => !s || !s.id); const targetIdx = emptyIdx !== -1 ? emptyIdx : 3; quickslots[targetIdx] = { type: "item", id: itemId, key: quickslots[targetIdx]?.key || `${targetIdx + 1}` }; renderQuickslotsBar(); saveGameProgress(); showToast(`📌 Auf Schnellzugriff-Slot ${targetIdx + 1} gelegt!`); } function getItemFilterCategory(proto) { if (!proto) return "other"; if (proto.type === "weapon" || proto.type === "ranged") return "weapon"; if (proto.type === "helmet") return "helmet"; if (proto.type === "armor") return "armor"; if (proto.type === "pants") return "pants"; if (proto.type === "boots") return "boots"; if (proto.type === "shield") return "shield"; if (proto.type === "ring" || proto.type === "amulet") return "relic"; if (proto.type === "consumable") return "consumable"; return "other"; } function getItemSlotLabel(proto) { if (!proto) return "GEGENSTAND"; if (proto.type === "weapon") return "⚔️ WAFFE (NAHKAMPF)"; if (proto.type === "ranged") return "🏹 WAFFE (FERNKAMPF)"; if (proto.type === "helmet") return "🪖 KOPF / HUT"; if (proto.type === "armor") return "👕 BRUST / RÜSTUNG"; if (proto.type === "pants") return "👖 BEINE / HOSE"; if (proto.type === "boots") return "👢 FÜSSE / SCHUHE"; if (proto.type === "shield") return "🛡️ NEBENHAND / SCHILD"; if (proto.type === "ring") return "💍 RELIKT 1: RING"; if (proto.type === "amulet") return "📿 RELIKT 2: AMULETT"; if (proto.type === "consumable") { if (proto.heal) return "🧪 HEILTRANK"; if (proto.mana) return "🔮 MANA-ELIXIER"; if (proto.bombDmg) return "💣 ALCHEMIE-BOMBE"; if (proto.throwDmg) return "🗡️ WURFMESSER"; if (proto.permHp) return "🍷 TITANEN-ELIXIER"; return "📜 SCHRIFTROLLE / VERBRAUCH"; } return "GEGENSTAND"; } function formatItemStatsBadge(proto, upgrade = 0) { if (!proto) return ""; const parts = []; if (proto.atk) parts.push(`⚔️ +${proto.atk + upgrade * 2} ATK`); if (proto.rangedAtk) parts.push(`🏹 +${proto.rangedAtk + upgrade * 3} Fern`); if (proto.def) parts.push(`🛡️ +${proto.def + upgrade * 2} Rüst`); if (proto.maxHp) parts.push(`❤️ +${proto.maxHp + upgrade * 8} HP`); if (proto.maxMp) parts.push(`🔮 +${proto.maxMp + upgrade * 5} MP`); if (proto.crit) parts.push(`🎯 +${Math.round((proto.crit + upgrade * 0.02) * 100)}% Crit`); if (proto.spellPwr) parts.push(`⚡ +${Math.round((proto.spellPwr + upgrade * 0.05) * 100)}% Magie`); if (proto.lifesteal) parts.push(`🩸 +${Math.round(proto.lifesteal * 100)}% Raub`); if (proto.lightRadius) parts.push(`☀️ +${proto.lightRadius} Sicht`); if (proto.heal) parts.push(`💚 Heilt ${proto.heal} HP`); if (proto.mana) parts.push(`💙 +${proto.mana} MP`); if (proto.permHp) parts.push(`❤️ Perm +${proto.permHp} HP`); if (proto.permAtk) parts.push(`⚔️ Perm +${proto.permAtk} ATK`); if (proto.throwDmg) parts.push(`🗡️ ${proto.throwDmg} Schaden`); if (proto.bombDmg) parts.push(`💣 ${proto.bombDmg} Flächenschaden`); return parts.join(" · ") || proto.desc || ""; } function formatItemStatsBadgesHtml(proto, upg = 0) { if (!proto) return ""; const badges = []; if (proto.atk) badges.push(`⚔️ +${proto.atk + upg * 2} ATK`); if (proto.rangedAtk) badges.push(`🏹 +${proto.rangedAtk + upg * 3} Fern`); if (proto.def) badges.push(`🛡️ +${proto.def + upg * 2} Rüst`); if (proto.maxHp) badges.push(`❤️ +${proto.maxHp + upg * 8} HP`); if (proto.maxMp) badges.push(`🔮 +${proto.maxMp + upg * 5} MP`); if (proto.crit) badges.push(`🎯 +${Math.round((proto.crit + upg * 0.02) * 100)}% Crit`); if (proto.spellPwr) badges.push(`⚡ +${Math.round((proto.spellPwr + upg * 0.05) * 100)}% Magie`); if (proto.lifesteal) badges.push(`🩸 +${Math.round(proto.lifesteal * 100)}% Raub`); if (proto.lightRadius) badges.push(`☀️ +${proto.lightRadius} Sicht`); if (proto.heal) badges.push(`💚 Heilt ${proto.heal} HP`); if (proto.mana) badges.push(`💙 +${proto.mana} MP`); if (proto.permHp) badges.push(`❤️ Perm +${proto.permHp} HP`); if (proto.permAtk) badges.push(`⚔️ Perm +${proto.permAtk} ATK`); if (proto.throwDmg) badges.push(`🗡️ ${proto.throwDmg} Schaden`); if (proto.bombDmg) badges.push(`💣 ${proto.bombDmg} Explosion`); if (badges.length === 0) { if (proto.desc) return `${proto.desc}`; return `Basis-Gegenstand`; } return badges.join(" "); } function renderSingleEquipSlotCard(sm) { const raw = hero.equipped?.[sm.key]; const eq = typeof raw === "string" ? { id: raw, qty: 1, upgrade: 0 } : raw; if (!eq || !ITEMS[eq.id]) { return `
Erwirb dein eigenes Account-gebundenes Anwesen! Beinhaltet sofort eine 📦 Truhe mit unendlichem Platz und lässt sich mit einem 🛏️ Bett und einer ⚒️ privaten Schmiede ausbauen.
${d.desc}