Spaces:
Running
Running
| // Globální proměnné a Stavy | |
| let state = { | |
| screen: 'main-menu', | |
| phase: 'build', // build, defend, qte, shop, gameover | |
| stats: { | |
| score: 0, | |
| coins: 0, | |
| wave: 1, | |
| streak: 0, | |
| maxStreak: 0, | |
| kills: 0, | |
| totalCoins: 0, | |
| gamesPlayed: 0, | |
| maxWave: 0 | |
| }, | |
| build: { | |
| progress: 0, // aktuální krok | |
| target: 7, // 7 kroků k postavení | |
| style: 'castle1' // výchozí vzhled | |
| }, | |
| castle: { | |
| hp: 100, | |
| maxHp: 100 | |
| }, | |
| upgrades: { | |
| slowMultiplier: 1, // 1 = normální rychlost, < 1 zpomaleno | |
| fairyActive: false | |
| }, | |
| currentProblem: null, // objekt { text: "2 + 2", answer: 4, type: "normal" } | |
| enemies: [], // pole aktivních nepřátel | |
| qte: { | |
| active: false, | |
| timer: null, | |
| timeLeft: 0, | |
| maxTime: 5000 // 5 sekund | |
| }, | |
| intervals: { | |
| gameLoop: null, | |
| spawner: null, | |
| fairy: null | |
| } | |
| }; | |
| // --- DOM Elementy --- | |
| const els = { | |
| // Screens | |
| mainMenu: document.getElementById('main-menu'), | |
| buildScreen: document.getElementById('build-screen'), | |
| defendScreen: document.getElementById('defend-screen'), | |
| shopScreen: document.getElementById('shop-screen'), | |
| gameOverScreen: document.getElementById('game-over-screen'), | |
| statsMenuScreen: document.getElementById('stats-menu-screen'), | |
| // Tlačítka Menu | |
| btnStart: document.getElementById('btn-start'), | |
| btnStatsMenu: document.getElementById('btn-stats-menu'), | |
| btnStatsBack: document.getElementById('btn-stats-back'), | |
| // Build | |
| buildStyleSelection: document.getElementById('build-style-selection'), | |
| btnStartBuild: document.getElementById('btn-start-build'), | |
| buildActionArea: document.getElementById('build-action-area'), | |
| buildVisualStep: document.getElementById('build-visual-step'), | |
| buildProgress: document.getElementById('build-progress'), | |
| buildProgressText: document.getElementById('build-progress-text'), | |
| buildMathProblem: document.getElementById('build-math-problem'), | |
| buildInput: document.getElementById('build-input'), | |
| buildCompareBtns: document.getElementById('build-compare-buttons'), | |
| // Defend HUD | |
| waveDisplay: document.getElementById('wave-display'), | |
| coinsDisplay: document.getElementById('coins-display'), | |
| streakDisplay: document.getElementById('streak-display'), | |
| castleHpBar: document.getElementById('castle-hp-bar'), | |
| castleHpText: document.getElementById('castle-hp-text'), | |
| mainCastle: document.getElementById('main-castle'), | |
| enemiesArea: document.getElementById('enemies-area'), | |
| gameContainer: document.getElementById('game-container'), | |
| defendInput: document.getElementById('defend-input'), | |
| defendCompareBtns: document.getElementById('defend-compare-buttons'), | |
| // QTE | |
| qteContainer: document.getElementById('qte-container'), | |
| qteMathProblem: document.getElementById('qte-math-problem'), | |
| qteTimerBar: document.getElementById('qte-timer-bar'), | |
| // Shop | |
| shopWaveNum: document.getElementById('shop-wave-num'), | |
| shopCoins: document.getElementById('shop-coins'), | |
| btnNextWave: document.getElementById('btn-next-wave'), | |
| buyHpBtn: document.getElementById('buy-hp'), | |
| buySlowBtn: document.getElementById('buy-slow'), | |
| buyFairyBtn: document.getElementById('buy-fairy'), | |
| // Game Over | |
| statWave: document.getElementById('stat-wave'), | |
| statScore: document.getElementById('stat-score'), | |
| statStreak: document.getElementById('stat-streak'), | |
| statKills: document.getElementById('stat-kills'), | |
| btnRestart: document.getElementById('btn-restart'), | |
| btnToMain: document.getElementById('btn-to-main'), | |
| // Global Stats | |
| globalBestWave: document.getElementById('global-best-wave'), | |
| globalGamesPlayed: document.getElementById('global-games-played'), | |
| globalTotalCoins: document.getElementById('global-total-coins'), | |
| // Particles | |
| particles: document.getElementById('particles') | |
| }; | |
| // --- Inicializace --- | |
| function init() { | |
| loadGlobalStats(); | |
| attachEventListeners(); | |
| showScreen('main-menu'); | |
| } | |
| // --- Systém Obrazovek --- | |
| function showScreen(screenId) { | |
| // Skrytí všech | |
| els.mainMenu.classList.add('hidden'); | |
| els.buildScreen.classList.add('hidden'); | |
| els.defendScreen.classList.add('hidden'); | |
| els.shopScreen.classList.add('hidden'); | |
| els.gameOverScreen.classList.add('hidden'); | |
| els.statsMenuScreen.classList.add('hidden'); | |
| // Zobrazení vybrané | |
| if (screenId === 'main-menu') els.mainMenu.classList.remove('hidden'); | |
| else if (screenId === 'build-screen') els.buildScreen.classList.remove('hidden'); | |
| else if (screenId === 'defend-screen') els.defendScreen.classList.remove('hidden'); | |
| else if (screenId === 'shop-screen') els.shopScreen.classList.remove('hidden'); | |
| else if (screenId === 'game-over-screen') els.gameOverScreen.classList.remove('hidden'); | |
| else if (screenId === 'stats-menu-screen') els.statsMenuScreen.classList.remove('hidden'); | |
| state.screen = screenId; | |
| } | |
| // --- Správa událostí (Event Listeners) --- | |
| function attachEventListeners() { | |
| // Main Menu | |
| els.btnStart.addEventListener('click', startGame); | |
| els.btnStatsMenu.addEventListener('click', () => { | |
| updateGlobalStatsUI(); | |
| showScreen('stats-menu-screen'); | |
| }); | |
| els.btnStatsBack.addEventListener('click', () => showScreen('main-menu')); | |
| // Game Over | |
| els.btnRestart.addEventListener('click', startGame); | |
| els.btnToMain.addEventListener('click', () => showScreen('main-menu')); | |
| // Build Style Selection | |
| document.querySelectorAll('.style-option').forEach(option => { | |
| option.addEventListener('click', (e) => { | |
| document.querySelectorAll('.style-option').forEach(opt => opt.classList.remove('selected')); | |
| e.target.classList.add('selected'); | |
| state.build.style = e.target.dataset.style; | |
| }); | |
| }); | |
| els.btnStartBuild.addEventListener('click', () => { | |
| els.buildStyleSelection.classList.add('hidden'); | |
| els.buildActionArea.classList.remove('hidden'); | |
| els.buildActionArea.style.display = 'flex'; | |
| // Nastavíme vizuál budování | |
| els.buildVisualStep.style.backgroundImage = `url('assets/${state.build.style}.svg')`; | |
| els.buildVisualStep.style.height = '0%'; | |
| generateBuildProblem(); | |
| }); | |
| } | |
| // --- Generování matematických příkladů --- | |
| function generateMathProblem(difficulty) { | |
| // difficulty: 1 (lehké, stavba), 2 (střední, vlny 1-3), 3 (těžší, vlny 4+), 4 (QTE - nejtěžší) | |
| const types = ['add', 'sub', 'mul', 'div', 'comp', 'round']; | |
| const type = types[Math.floor(Math.random() * types.length)]; | |
| let text = ""; | |
| let answer = null; | |
| let probType = "num"; // num (číselný vstup), comp (porovnávání) | |
| const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; | |
| switch (type) { | |
| case 'add': | |
| let maxAdd = difficulty === 1 ? 20 : (difficulty === 2 ? 50 : 100); | |
| let aAdd = getRandomInt(1, maxAdd); | |
| let bAdd = getRandomInt(1, maxAdd - aAdd > 0 ? maxAdd - aAdd : 10); | |
| if (difficulty >= 3) { | |
| aAdd = getRandomInt(10, 50); | |
| bAdd = getRandomInt(10, 50); | |
| } | |
| text = `${aAdd} + ${bAdd}`; | |
| answer = aAdd + bAdd; | |
| break; | |
| case 'sub': | |
| let maxSub = difficulty <= 2 ? 30 : 100; | |
| let aSub = getRandomInt(5, maxSub); | |
| let bSub = getRandomInt(1, aSub); | |
| text = `${aSub} - ${bSub}`; | |
| answer = aSub - bSub; | |
| break; | |
| case 'mul': | |
| let maxMul = difficulty === 1 ? 5 : (difficulty === 2 ? 8 : 10); | |
| let aMul = getRandomInt(1, maxMul); | |
| let bMul = getRandomInt(1, 10); | |
| text = `${aMul} × ${bMul}`; | |
| answer = aMul * bMul; | |
| break; | |
| case 'div': | |
| let divisor = getRandomInt(2, difficulty <= 2 ? 5 : 10); | |
| let quotient = getRandomInt(1, 10); | |
| let dividend = divisor * quotient; | |
| text = `${dividend} : ${divisor}`; | |
| answer = quotient; | |
| break; | |
| case 'comp': | |
| probType = "comp"; | |
| let aComp = getRandomInt(1, 100); | |
| let bComp = getRandomInt(1, 100); | |
| // snaha o blízka čísla | |
| if(Math.random() > 0.5) bComp = aComp + getRandomInt(-5, 5); | |
| text = `${aComp} ? ${bComp}`; | |
| if (aComp < bComp) answer = '<'; | |
| else if (aComp > bComp) answer = '>'; | |
| else answer = '='; | |
| break; | |
| case 'round': | |
| let numRound = getRandomInt(11, 99); | |
| // Nechceme končící na 0 | |
| if (numRound % 10 === 0) numRound += getRandomInt(1, 4); | |
| text = `Zaokrouhli ${numRound}`; | |
| answer = Math.round(numRound / 10) * 10; | |
| break; | |
| } | |
| return { text, answer, type: probType }; | |
| } | |
| // --- Herní Loop a Základní stavy --- | |
| function startGame() { | |
| console.log("Start hry..."); | |
| // Reset statistik pro novou hru | |
| state.stats.score = 0; | |
| state.stats.coins = 0; | |
| state.stats.wave = 1; | |
| state.stats.streak = 0; | |
| state.stats.maxStreak = 0; | |
| state.stats.kills = 0; | |
| // Reset hradu | |
| state.castle.maxHp = 100; | |
| state.castle.hp = state.castle.maxHp; | |
| // Reset upgrady | |
| state.upgrades.slowMultiplier = 1; | |
| state.upgrades.fairyActive = false; | |
| // Zvýšit počítadlo her | |
| state.stats.gamesPlayed++; | |
| saveGlobalStats(); | |
| startBuildPhase(); | |
| } | |
| function startBuildPhase() { | |
| state.phase = 'build'; | |
| state.build.progress = 0; | |
| // Obnovit UI pro výběr stylu | |
| els.buildStyleSelection.classList.remove('hidden'); | |
| els.buildActionArea.classList.add('hidden'); | |
| els.buildActionArea.style.display = 'none'; | |
| updateBuildUI(); | |
| showScreen('build-screen'); | |
| } | |
| function updateBuildUI() { | |
| let percent = (state.build.progress / state.build.target) * 100; | |
| els.buildProgress.style.width = `${percent}%`; | |
| els.buildProgressText.textContent = `Krok ${state.build.progress} / ${state.build.target}`; | |
| // Vizuál rostoucího hradu | |
| els.buildVisualStep.style.height = `${percent}%`; | |
| } | |
| function generateBuildProblem() { | |
| state.currentProblem = generateMathProblem(1); // obtížnost 1 pro stavbu | |
| els.buildMathProblem.textContent = state.currentProblem.text; | |
| if (state.currentProblem.type === 'comp') { | |
| els.buildInput.classList.add('hidden'); | |
| els.buildCompareBtns.classList.remove('hidden'); | |
| } else { | |
| els.buildInput.classList.remove('hidden'); | |
| els.buildCompareBtns.classList.add('hidden'); | |
| els.buildInput.value = ''; | |
| els.buildInput.focus(); | |
| } | |
| } | |
| // Zpracování vstupu ve fázi stavby | |
| function handleBuildInput(val) { | |
| if (state.phase !== 'build') return; | |
| // Převod na string pro snadné porovnání (čísla i znaky) | |
| if (val.toString() === state.currentProblem.answer.toString()) { | |
| // Správně | |
| state.build.progress += 1; | |
| createParticles(els.buildMathProblem, '#43bccd'); // modré jiskry | |
| // Mince za stavbu (1 za správnou odpověď) | |
| state.stats.coins += 1; | |
| state.stats.totalCoins += 1; | |
| saveGlobalStats(); | |
| if (state.build.progress >= state.build.target) { | |
| state.build.progress = state.build.target; | |
| updateBuildUI(); | |
| setTimeout(startDefendPhase, 1000); // pauza před obranou | |
| } else { | |
| updateBuildUI(); | |
| generateBuildProblem(); | |
| } | |
| } else { | |
| // Špatně | |
| createParticles(els.buildMathProblem, '#e94560'); // červené jiskry, shake | |
| els.buildMathProblem.style.animation = 'shake 0.5s'; | |
| setTimeout(() => els.buildMathProblem.style.animation = '', 500); | |
| if (state.currentProblem.type !== 'comp') { | |
| els.buildInput.value = ''; | |
| els.buildInput.focus(); | |
| } | |
| } | |
| } | |
| // Bindování inputů pro Build | |
| els.buildInput.addEventListener('keyup', (e) => { | |
| if (e.key === 'Enter' && els.buildInput.value !== '') { | |
| handleBuildInput(els.buildInput.value); | |
| } | |
| }); | |
| Array.from(els.buildCompareBtns.children).forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| handleBuildInput(e.target.dataset.val); | |
| }); | |
| }); | |
| function startDefendPhase() { | |
| state.phase = 'defend'; | |
| console.log("Zahájení obrany vlna:", state.stats.wave); | |
| // Aktualizace HUD | |
| els.waveDisplay.textContent = state.stats.wave; | |
| els.coinsDisplay.textContent = state.stats.coins; | |
| els.streakDisplay.textContent = state.stats.streak; | |
| updateCastleHpUI(); | |
| els.enemiesArea.innerHTML = ''; | |
| state.enemies = []; | |
| showScreen('defend-screen'); | |
| els.defendInput.value = ''; | |
| els.defendInput.focus(); | |
| // Změna pozadí a vzhledu hradu | |
| els.mainCastle.style.backgroundImage = `url('assets/${state.build.style}.svg')`; | |
| updateBackground(); | |
| // Nastavení obtížnosti podle vlny | |
| let difficulty = 2; | |
| if (state.stats.wave > 3) difficulty = 3; | |
| // Parametry vlny | |
| let totalEnemies = 5 + (state.stats.wave * 2); | |
| let spawnedEnemies = 0; | |
| // Hlavní herní loop pro obranu (pohyb nepřátel) | |
| state.intervals.gameLoop = setInterval(() => { | |
| if (state.phase !== 'defend') return; | |
| updateEnemies(); | |
| }, 50); // 20 FPS | |
| // Spawner nepřátel | |
| let spawnRate = Math.max(1000, 3000 - (state.stats.wave * 200)); // Rychlejší spawn v dalších vlnách | |
| // Jestli je to boss level | |
| const isBossWave = state.stats.wave % 5 === 0; | |
| state.intervals.spawner = setInterval(() => { | |
| if (state.phase !== 'defend' || state.qte.active) return; | |
| if (spawnedEnemies < totalEnemies) { | |
| // Poslední nepřítel ve vlny dělitelné 5 je Boss | |
| let spawnBoss = false; | |
| if (isBossWave && spawnedEnemies === totalEnemies - 1) { | |
| spawnBoss = true; | |
| } | |
| spawnEnemy(difficulty, spawnBoss); | |
| spawnedEnemies++; | |
| } else if (state.enemies.length === 0 && !state.qte.active) { | |
| // Vlna dokončena | |
| endWave(); | |
| } | |
| // Šance na QTE (Záchrana jednorožce) - cca 15% každou vteřinu spawnu, pokud už není a vlna ještě neskončila | |
| if (!state.qte.active && Math.random() < 0.15 && spawnedEnemies < totalEnemies) { | |
| triggerQTE(); | |
| } | |
| }, spawnRate); | |
| } | |
| function updateBackground() { | |
| els.gameContainer.classList.remove('bg-1', 'bg-2', 'bg-3'); | |
| if (state.stats.wave <= 3) els.gameContainer.classList.add('bg-1'); | |
| else if (state.stats.wave <= 6) els.gameContainer.classList.add('bg-2'); | |
| else els.gameContainer.classList.add('bg-3'); | |
| } | |
| function spawnEnemy(difficulty, isBoss = false) { | |
| const enemyEl = document.createElement('div'); | |
| enemyEl.classList.add('enemy'); | |
| // Grafika | |
| const spriteEl = document.createElement('div'); | |
| spriteEl.classList.add('enemy-sprite'); | |
| if (isBoss) { | |
| spriteEl.classList.add('enemy-boss'); | |
| difficulty = Math.min(difficulty + 1, 4); // boss je o něco těžší | |
| } else { | |
| const sprites = ['enemy1.svg', 'enemy2.svg', 'enemy3.svg']; | |
| const chosenSprite = sprites[Math.floor(Math.random() * sprites.length)]; | |
| spriteEl.style.backgroundImage = `url('assets/${chosenSprite}')`; | |
| } | |
| // Příklad | |
| const mathEl = document.createElement('div'); | |
| mathEl.classList.add('enemy-math'); | |
| const problem = generateMathProblem(difficulty); | |
| mathEl.textContent = problem.text; | |
| enemyEl.appendChild(spriteEl); | |
| enemyEl.appendChild(mathEl); | |
| // Pozice (zprava, náhodná výška) | |
| const startX = els.enemiesArea.clientWidth; | |
| const heightOffset = isBoss ? 150 : 80; | |
| const startY = Math.random() * (els.enemiesArea.clientHeight - heightOffset); | |
| enemyEl.style.left = `${startX}px`; | |
| enemyEl.style.top = `${startY}px`; | |
| els.enemiesArea.appendChild(enemyEl); | |
| const speed = (Math.random() * 0.5 + 0.5 + (state.stats.wave * 0.1)) * state.upgrades.slowMultiplier; // Rychlost podle vlny a upgrady | |
| const id = Date.now() + Math.random(); | |
| state.enemies.push({ | |
| id: id, | |
| el: enemyEl, | |
| x: startX, | |
| y: startY, | |
| speed: speed, | |
| problem: problem | |
| }); | |
| // Aktualizuj input mód podle toho, co je nejblíž (nejblížící se nepřítel) | |
| updateDefendInputMode(); | |
| } | |
| function updateEnemies() { | |
| for (let i = state.enemies.length - 1; i >= 0; i--) { | |
| let enemy = state.enemies[i]; | |
| // Pohyb doleva | |
| enemy.x -= enemy.speed; | |
| enemy.el.style.left = `${enemy.x}px`; | |
| // Kolize s hradem (x <= 0) | |
| if (enemy.x <= 0) { | |
| damageCastle(10); | |
| enemy.el.remove(); | |
| state.enemies.splice(i, 1); | |
| // Reset streak | |
| resetStreak(); | |
| updateDefendInputMode(); | |
| } | |
| } | |
| } | |
| function damageCastle(amount) { | |
| state.castle.hp -= amount; | |
| if (state.castle.hp < 0) state.castle.hp = 0; | |
| updateCastleHpUI(); | |
| // Efekt na hrad | |
| const castleEl = document.querySelector('.castle'); | |
| castleEl.style.animation = 'shake 0.5s'; | |
| setTimeout(() => castleEl.style.animation = '', 500); | |
| if (state.castle.hp === 0) { | |
| gameOver(); | |
| } | |
| } | |
| function updateDefendInputMode() { | |
| let aliveEnemies = state.enemies.filter(e => !e.dying); | |
| if (aliveEnemies.length === 0) return; | |
| if (state.qte.active) return; // Nepřepínat vstupy, pokud běží QTE | |
| // Najdi nejbližšího nepřítele | |
| let closestEnemy = aliveEnemies.reduce((prev, curr) => (prev.x < curr.x) ? prev : curr); | |
| // Vizuální označení cíle | |
| state.enemies.forEach(e => e.el.classList.remove('targeted')); | |
| closestEnemy.el.classList.add('targeted'); | |
| if (closestEnemy.problem.type === 'comp') { | |
| els.defendInput.classList.add('hidden'); | |
| els.defendCompareBtns.classList.remove('hidden'); | |
| } else { | |
| els.defendInput.classList.remove('hidden'); | |
| els.defendCompareBtns.classList.add('hidden'); | |
| els.defendInput.focus(); | |
| } | |
| } | |
| function handleDefendInput(val) { | |
| if (state.phase !== 'defend' || state.qte.active) return; | |
| if (state.enemies.length === 0) { | |
| els.defendInput.value = ''; | |
| return; | |
| } | |
| let hit = false; | |
| let hitEnemyIndex = -1; | |
| // Najít nepřítele se správnou odpovědí (priorita nejbližší) | |
| // Seřadit nepřátele od nejbližšího (nejmenší X) | |
| let sortedEnemies = [...state.enemies].sort((a, b) => a.x - b.x); | |
| for (let enemy of sortedEnemies) { | |
| if (!enemy.dying && enemy.problem.answer.toString() === val.toString()) { | |
| hit = true; | |
| hitEnemyIndex = state.enemies.findIndex(e => e.id === enemy.id); | |
| break; | |
| } | |
| } | |
| if (hit && hitEnemyIndex !== -1) { | |
| // Zásah | |
| let enemy = state.enemies[hitEnemyIndex]; | |
| // Označíme nepřítele jako mrtvého, aby už na něj nešlo dál útočit | |
| enemy.dying = true; | |
| // Princezna vystřelí projektil | |
| shootProjectile(enemy, () => { | |
| createParticles(enemy.el, '#fca311'); // zlaté částice | |
| enemy.el.remove(); | |
| let realIdx = state.enemies.findIndex(e => e.id === enemy.id); | |
| if (realIdx !== -1) { | |
| state.enemies.splice(realIdx, 1); | |
| state.stats.kills++; | |
| state.stats.score += 10 * (1 + Math.floor(state.stats.streak / 5)); // Bonus za streak | |
| // Mince + Streak | |
| state.stats.streak++; | |
| if (state.stats.streak > state.stats.maxStreak) state.stats.maxStreak = state.stats.streak; | |
| // Každých 5 ve streaku = bonusová mince | |
| let coinsEarned = 1 + Math.floor(state.stats.streak / 5); | |
| state.stats.coins += coinsEarned; | |
| state.stats.totalCoins += coinsEarned; | |
| els.streakDisplay.textContent = state.stats.streak; | |
| els.coinsDisplay.textContent = state.stats.coins; | |
| updateDefendInputMode(); | |
| } | |
| }); | |
| els.defendInput.value = ''; | |
| } else { | |
| // Minutí | |
| resetStreak(); | |
| // Efekt chyby | |
| els.defendInput.style.animation = 'shake 0.3s'; | |
| els.defendCompareBtns.style.animation = 'shake 0.3s'; | |
| setTimeout(() => { | |
| els.defendInput.style.animation = ''; | |
| els.defendCompareBtns.style.animation = ''; | |
| }, 300); | |
| if (els.defendInput.classList.contains('hidden') === false) { | |
| els.defendInput.value = ''; | |
| } | |
| } | |
| } | |
| function shootProjectile(enemy, onHitCallback) { | |
| const proj = document.createElement('div'); | |
| proj.classList.add('projectile'); | |
| // Zjistit pozici princezny | |
| const princessEl = document.querySelector('.princess'); | |
| const pRect = princessEl.getBoundingClientRect(); | |
| const cRect = els.gameContainer.getBoundingClientRect(); | |
| let startX = pRect.left - cRect.left + pRect.width / 2; | |
| let startY = pRect.top - cRect.top + pRect.height / 2; | |
| proj.style.left = `${startX}px`; | |
| proj.style.top = `${startY}px`; | |
| els.gameContainer.appendChild(proj); | |
| // Animace k nepříteli | |
| let duration = 300; // ms | |
| let startTime = performance.now(); | |
| function animateProjectile(currentTime) { | |
| let elapsed = currentTime - startTime; | |
| let progress = Math.min(elapsed / duration, 1); | |
| let targetX = enemy.x + els.enemiesArea.offsetLeft; | |
| let targetY = enemy.y + els.enemiesArea.offsetTop + 40; // zhruba střed nepřítele | |
| let currentX = startX + (targetX - startX) * progress; | |
| let currentY = startY + (targetY - startY) * progress; | |
| proj.style.left = `${currentX}px`; | |
| proj.style.top = `${currentY}px`; | |
| if (progress < 1) { | |
| requestAnimationFrame(animateProjectile); | |
| } else { | |
| proj.remove(); | |
| if (onHitCallback) onHitCallback(); | |
| } | |
| } | |
| requestAnimationFrame(animateProjectile); | |
| } | |
| function resetStreak() { | |
| state.stats.streak = 0; | |
| els.streakDisplay.textContent = state.stats.streak; | |
| } | |
| // Bindování inputů pro Defend | |
| els.defendInput.addEventListener('keyup', (e) => { | |
| if (state.phase !== 'defend') return; | |
| if (e.key === 'Enter' && els.defendInput.value !== '') { | |
| handleDefendInput(els.defendInput.value); | |
| } | |
| }); | |
| Array.from(els.defendCompareBtns.children).forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| if (state.phase !== 'defend') return; | |
| handleDefendInput(e.target.dataset.val); | |
| }); | |
| }); | |
| // --- QTE (Záchrana jednorožce) --- | |
| function triggerQTE() { | |
| console.log("QTE triggered!"); | |
| state.qte.active = true; | |
| state.qte.timeLeft = state.qte.maxTime; | |
| // Zobrazení QTE UI | |
| els.qteContainer.classList.remove('hidden'); | |
| els.qteTimerBar.style.width = '100%'; | |
| // Generování těžšího příkladu (obtížnost 4) | |
| state.currentProblem = generateMathProblem(4); | |
| els.qteMathProblem.textContent = state.currentProblem.text; | |
| // Nastavení vstupu pro QTE | |
| if (state.currentProblem.type === 'comp') { | |
| els.defendInput.classList.add('hidden'); | |
| els.defendCompareBtns.classList.remove('hidden'); | |
| } else { | |
| els.defendInput.classList.remove('hidden'); | |
| els.defendCompareBtns.classList.add('hidden'); | |
| els.defendInput.value = ''; | |
| els.defendInput.focus(); | |
| } | |
| // Spuštění odpočtu (interval 50ms) | |
| state.qte.timer = setInterval(() => { | |
| state.qte.timeLeft -= 50; | |
| let percent = (state.qte.timeLeft / state.qte.maxTime) * 100; | |
| els.qteTimerBar.style.width = `${percent}%`; | |
| if (state.qte.timeLeft <= 0) { | |
| failQTE(); | |
| } | |
| }, 50); | |
| } | |
| function resolveQTEInput(val) { | |
| if (val.toString() === state.currentProblem.answer.toString()) { | |
| successQTE(); | |
| } else { | |
| failQTE(); | |
| } | |
| } | |
| function successQTE() { | |
| clearInterval(state.qte.timer); | |
| // Efekt záchrany (magická vlna) | |
| createParticles(els.qteContainer, '#8e44ad'); // fialové | |
| createParticles(els.qteContainer, '#f1c40f'); // žluté | |
| // Odměna: Zničit všechny aktuální nepřátele na obrazovce | |
| state.enemies.forEach(enemy => { | |
| createParticles(enemy.el, '#8e44ad'); | |
| enemy.el.remove(); | |
| state.stats.kills++; | |
| state.stats.score += 20; | |
| }); | |
| state.enemies = []; // vyprázdnit pole | |
| // Bonusové mince | |
| state.stats.coins += 10; | |
| state.stats.totalCoins += 10; | |
| els.coinsDisplay.textContent = state.stats.coins; | |
| closeQTE(); | |
| } | |
| function failQTE() { | |
| clearInterval(state.qte.timer); | |
| // Jednorožec uteče - efekt zklamání | |
| els.qteContainer.style.animation = 'shake 0.5s'; | |
| setTimeout(() => els.qteContainer.style.animation = '', 500); | |
| closeQTE(); | |
| } | |
| function closeQTE() { | |
| els.qteContainer.classList.add('hidden'); | |
| state.qte.active = false; | |
| // Návrat vstupu zpět na obranu (nebo skrytí, pokud nejsou nepřátelé) | |
| updateDefendInputMode(); | |
| if (state.enemies.length === 0) { | |
| els.defendInput.classList.add('hidden'); | |
| els.defendCompareBtns.classList.add('hidden'); | |
| } | |
| } | |
| // Aktualizace vstupů v obraně (sloučení s QTE logic) | |
| // Nahradíme původní handlery (ty z předchozího kroku) za nové, které zohledňují QTE | |
| els.defendInput.removeEventListener('keyup', () => {}); // Nelze jednoduše odstranit anonymní funkce, takže je přepíšeme | |
| // Ošetříme to přidáním nové podmínky přímo do `handleDefendInput` a zachováme původní event listenery | |
| // ale přesuneme QTE logiku přímo tam, abychom zamezili duplikacím a problémům. | |
| // Protože JS neumožňuje snadno odstranit anonymní event listenery vytvořené dříve, upravíme logiku uvnitř listenerů | |
| // tím, že `handleDefendInput` bude vědět o QTE (což už dělá přes return). | |
| function checkGlobalInput(e) { | |
| if (state.phase !== 'defend') return; | |
| // Pokud je aktivní QTE | |
| if (state.qte.active && e.key === 'Enter' && els.defendInput.value !== '') { | |
| resolveQTEInput(els.defendInput.value); | |
| } | |
| } | |
| // Přiřadíme do globálního listeneru, abychom nepoužívali duplicitní event listenery na inputu | |
| window.addEventListener('keyup', (e) => { | |
| if (state.phase === 'defend' && state.qte.active) { | |
| if (e.key === 'Enter' && els.defendInput.value !== '') { | |
| resolveQTEInput(els.defendInput.value); | |
| els.defendInput.value = ''; | |
| } | |
| } | |
| }); | |
| // A pro tlačítka na porovnávání (pokud je QTE typu comp): | |
| Array.from(els.defendCompareBtns.children).forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| if (state.phase === 'defend' && state.qte.active) { | |
| resolveQTEInput(e.target.dataset.val); | |
| } | |
| }); | |
| }); | |
| function endWave() { | |
| console.log("Konec vlny", state.stats.wave); | |
| // Clear intervals | |
| clearInterval(state.intervals.gameLoop); | |
| clearInterval(state.intervals.spawner); | |
| state.stats.wave++; | |
| if (state.stats.wave > state.stats.maxWave) state.stats.maxWave = state.stats.wave; | |
| saveGlobalStats(); | |
| showShopPhase(); | |
| } | |
| function showShopPhase() { | |
| state.phase = 'shop'; | |
| els.shopWaveNum.textContent = state.stats.wave - 1; | |
| els.shopCoins.textContent = state.stats.coins; | |
| // Obnovení tlačítek a jejich textů | |
| els.buyHpBtn.disabled = state.stats.coins < 50; | |
| if (state.upgrades.slowMultiplier <= 0.5) { | |
| els.buySlowBtn.textContent = 'MAX (Vyprodáno)'; | |
| els.buySlowBtn.disabled = true; | |
| } else { | |
| els.buySlowBtn.disabled = state.stats.coins < 100; | |
| els.buySlowBtn.textContent = 'Koupit (100 🪙)'; | |
| } | |
| if (state.upgrades.fairyActive) { | |
| els.buyFairyBtn.textContent = 'Aktivní (Vyprodáno)'; | |
| els.buyFairyBtn.disabled = true; | |
| } else { | |
| els.buyFairyBtn.disabled = state.stats.coins < 150; | |
| els.buyFairyBtn.textContent = 'Koupit (150 🪙)'; | |
| } | |
| showScreen('shop-screen'); | |
| } | |
| function updateShopUI() { | |
| els.shopCoins.textContent = state.stats.coins; | |
| showShopPhase(); // znovuzavolání pro přehodnocení disabled stavů | |
| } | |
| // Event Listenery pro Obchod | |
| els.buyHpBtn.addEventListener('click', () => { | |
| if (state.stats.coins >= 50) { | |
| state.stats.coins -= 50; | |
| state.castle.maxHp += 20; | |
| state.castle.hp += 20; // vyléčí i aktuální | |
| updateShopUI(); | |
| } | |
| }); | |
| els.buySlowBtn.addEventListener('click', () => { | |
| if (state.stats.coins >= 100 && state.upgrades.slowMultiplier > 0.5) { | |
| state.stats.coins -= 100; | |
| state.upgrades.slowMultiplier -= 0.1; // zpomalení o 10% | |
| updateShopUI(); | |
| } | |
| }); | |
| els.buyFairyBtn.addEventListener('click', () => { | |
| if (state.stats.coins >= 150 && !state.upgrades.fairyActive) { | |
| state.stats.coins -= 150; | |
| state.upgrades.fairyActive = true; | |
| startFairy(); | |
| updateShopUI(); | |
| } | |
| }); | |
| els.btnNextWave.addEventListener('click', () => { | |
| if (state.phase === 'shop') { | |
| startDefendPhase(); | |
| } | |
| }); | |
| function startFairy() { | |
| if (state.intervals.fairy) clearInterval(state.intervals.fairy); | |
| state.intervals.fairy = setInterval(() => { | |
| if (state.phase === 'defend' && state.enemies.length > 0 && !state.qte.active) { | |
| // Víla zničí náhodného nepřítele (nebo prvního) | |
| let enemyToKill = state.enemies[0]; | |
| createParticles(enemyToKill.el, '#ff9ff3'); // růžová vílí barva | |
| enemyToKill.el.remove(); | |
| state.enemies.shift(); // odstraní prvního z pole | |
| state.stats.kills++; | |
| state.stats.score += 10; | |
| updateDefendInputMode(); | |
| // Ukázat vílu graficky (krátká animace) | |
| const fairySprite = document.createElement('div'); | |
| fairySprite.textContent = '🧚♀️'; | |
| fairySprite.style.position = 'absolute'; | |
| fairySprite.style.left = enemyToKill.x + 'px'; | |
| fairySprite.style.top = enemyToKill.y - 30 + 'px'; | |
| fairySprite.style.fontSize = '3rem'; | |
| fairySprite.style.animation = 'flyout 1s forwards'; | |
| fairySprite.style.setProperty('--tx', `0px`); | |
| fairySprite.style.setProperty('--ty', `-100px`); | |
| els.enemiesArea.appendChild(fairySprite); | |
| setTimeout(() => fairySprite.remove(), 1000); | |
| } | |
| }, 5000); // Každých 5 sekund víla zasáhne | |
| } | |
| // Game Over logiky | |
| function gameOver() { | |
| console.log("Hra skončila"); | |
| state.phase = 'gameover'; | |
| // Vyčištění všech intervalů | |
| if (state.intervals.gameLoop) clearInterval(state.intervals.gameLoop); | |
| if (state.intervals.spawner) clearInterval(state.intervals.spawner); | |
| if (state.intervals.fairy) clearInterval(state.intervals.fairy); | |
| if (state.qte.timer) clearInterval(state.qte.timer); | |
| // Skrytí QTE, pokud běželo | |
| els.qteContainer.classList.add('hidden'); | |
| // Uložení aktuálních max statistik | |
| if (state.stats.wave > state.stats.maxWave) state.stats.maxWave = state.stats.wave; | |
| saveGlobalStats(); | |
| // Zobrazení UI | |
| els.statWave.textContent = state.stats.wave; | |
| els.statScore.textContent = state.stats.score; | |
| els.statStreak.textContent = state.stats.maxStreak; | |
| els.statKills.textContent = state.stats.kills; | |
| showScreen('game-over-screen'); | |
| } | |
| function updateCastleHpUI() { | |
| els.castleHpText.textContent = `${state.castle.hp} / ${state.castle.maxHp}`; | |
| let hpPercent = (state.castle.hp / state.castle.maxHp) * 100; | |
| els.castleHpBar.style.width = `${hpPercent}%`; | |
| if (hpPercent < 30) els.castleHpBar.style.backgroundColor = 'red'; | |
| else els.castleHpBar.style.backgroundColor = '#e94560'; | |
| } | |
| // Particle system helper | |
| function createParticles(element, color) { | |
| const rect = element.getBoundingClientRect(); | |
| const containerRect = document.getElementById('game-container').getBoundingClientRect(); | |
| const centerX = (rect.left - containerRect.left) + rect.width / 2; | |
| const centerY = (rect.top - containerRect.top) + rect.height / 2; | |
| for (let i = 0; i < 10; i++) { | |
| const p = document.createElement('div'); | |
| p.classList.add('particle'); | |
| p.style.backgroundColor = color; | |
| p.style.width = Math.random() * 10 + 5 + 'px'; | |
| p.style.height = p.style.width; | |
| p.style.left = centerX + 'px'; | |
| p.style.top = centerY + 'px'; | |
| // Náhodný směr | |
| const angle = Math.random() * Math.PI * 2; | |
| const velocity = Math.random() * 50 + 20; | |
| const tx = Math.cos(angle) * velocity; | |
| const ty = Math.sin(angle) * velocity; | |
| p.style.setProperty('--tx', `${tx}px`); | |
| p.style.setProperty('--ty', `${ty}px`); | |
| // Upravit animaci aby použila translate | |
| p.style.animation = 'flyout 0.8s forwards'; | |
| document.getElementById('particles').appendChild(p); | |
| setTimeout(() => p.remove(), 800); | |
| } | |
| } | |
| // Add keyframes for flyout dynamically | |
| const styleSheet = document.createElement("style"); | |
| styleSheet.innerText = ` | |
| @keyframes flyout { | |
| 0% { transform: translate(0, 0) scale(1); opacity: 1; } | |
| 100% { transform: translate(var(--tx), var(--ty)) scale(0); opacity: 0; } | |
| }`; | |
| document.head.appendChild(styleSheet); | |
| // --- Ukládání / Načítání --- | |
| function loadGlobalStats() { | |
| const saved = localStorage.getItem('mathFortressStats'); | |
| if (saved) { | |
| const parsed = JSON.parse(saved); | |
| state.stats.maxWave = parsed.maxWave || 0; | |
| state.stats.gamesPlayed = parsed.gamesPlayed || 0; | |
| state.stats.totalCoins = parsed.totalCoins || 0; | |
| } | |
| } | |
| function saveGlobalStats() { | |
| localStorage.setItem('mathFortressStats', JSON.stringify({ | |
| maxWave: state.stats.maxWave, | |
| gamesPlayed: state.stats.gamesPlayed, | |
| totalCoins: state.stats.totalCoins | |
| })); | |
| } | |
| function updateGlobalStatsUI() { | |
| els.globalBestWave.textContent = state.stats.maxWave; | |
| els.globalGamesPlayed.textContent = state.stats.gamesPlayed; | |
| els.globalTotalCoins.textContent = state.stats.totalCoins; | |
| } | |
| // Spuštění po načtení | |
| window.onload = init; | |