DP-Logopedicka-Platforma/js/superjozino/game.js
2026-04-23 15:41:11 +02:00

3656 lines
125 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//////////////////////////////////////////////
// ============ LOADING SCREEN ============ //
// Čakanie na načítanie DOM obsahu //
// Skrytie loading screen s animáciou //
//////////////////////////////////////////////
document.addEventListener('DOMContentLoaded', function() {
window.addEventListener('load', function() {
setTimeout(hideLoadingScreen, 1000); // Čaká 1 sekundu potom skryje
});
console.log('Hra načítaná.');
});
function hideLoadingScreen() {
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) {
loadingScreen.style.opacity = '0';
setTimeout(() => {
loadingScreen.style.display = 'none';
}, 500);
}
}
/**
* ANIMATION MANAGER
* Správa animácií postavy - načítavanie a prehrávanie frame-ov
*/
class AnimationManager {
constructor() {
// Cesta k sprite-om
this.basePath = 'images/superjozino/assets/player/keyframes/yellow_hat/';
// Definícia animácií a počtu frame-ov
this.animations = {
idle: { frames: 20, speed: 15 }, // 000-019, pomalšie
walk: { frames: 8, speed: 10 }, // 000-007
run: { frames: 8, speed: 6 }, // 000-007, rýchlejšie
jump: { frames: 10, speed: 8 }, // 000-009
falling: { frames: 10, speed: 8 } // 000-009
};
// Načítané obrázky
this.images = {};
// Aktuálny stav animácie
this.currentAnimation = 'idle';
this.currentFrame = 0;
this.frameCounter = 0;
this.direction = 'right'; // 'left', 'right', 'front'
// Načítanie všetkých sprite-ov
this.loadAllSprites();
}
/**
* Načítanie všetkých sprite frame-ov
*/
loadAllSprites() {
console.log('🎨 Načítavam sprite-y postavy...');
for (let animName in this.animations) {
this.images[animName] = [];
const frameCount = this.animations[animName].frames;
for (let i = 0; i < frameCount; i++) {
const img = new Image();
// Formát názvu: __yellow_hat_idle_000.png
const frameNumber = i.toString().padStart(3, '0');
img.src = `${this.basePath}__yellow_hat_${animName}_${frameNumber}.png`;
this.images[animName].push(img);
// Log pre prvý frame každej animácie (pre debugging)
if (i === 0) {
img.onload = () => {
console.log(`✅ Načítaná animácia: ${animName} (${frameCount} frame-ov)`);
};
img.onerror = () => {
console.error(`❌ Chyba pri načítaní: ${img.src}`);
};
}
}
}
}
/**
* Nastavenie animácie
* @param {string} animationName - Názov animácie (idle, walk, run, jump, falling)
*/
setAnimation(animationName) {
if (this.currentAnimation !== animationName) {
this.currentAnimation = animationName;
this.currentFrame = 0;
this.frameCounter = 0;
}
}
/**
* Nastavenie smeru
* @param {string} direction - 'left', 'right', 'front'
*/
setDirection(direction) {
this.direction = direction;
}
/**
* Update animácie (volať v game loop-e)
* @param {number} deltaTime - Čas od posledného frame-u (normalizovaný na 60 FPS)
*/
update(deltaTime = 1) {
const anim = this.animations[this.currentAnimation];
if (!anim) return;
// Počítadlo frame-ov s deltaTime
this.frameCounter += deltaTime;
// Keď uplynie dosť frame-ov, prejdi na ďalší sprite
if (this.frameCounter >= anim.speed) {
this.frameCounter = 0;
this.currentFrame++;
// Loop animácie
if (this.currentFrame >= anim.frames) {
this.currentFrame = 0;
}
}
}
/**
* Vykreslenie aktuálneho frame-u
* @param {CanvasRenderingContext2D} ctx - Canvas kontext
* @param {number} x - X pozícia
* @param {number} y - Y pozícia
* @param {number} width - Šírka
* @param {number} height - Výška
*/
draw(ctx, x, y, width, height) {
const frameImage = this.images[this.currentAnimation]?.[this.currentFrame];
if (!frameImage || !frameImage.complete) {
ctx.fillStyle = 'red';
ctx.fillRect(x, y, width, height);
return;
}
ctx.save();
// ⭐ PRIDAJ TOTO - vypne anti-aliasing
ctx.imageSmoothingEnabled = false;
// Ak ide doľava, zrkadlovo otočíme sprite
if (this.direction === 'left') {
ctx.translate(x + width, y);
ctx.scale(-1, 1);
ctx.drawImage(frameImage, 0, 0, width, height);
} else {
ctx.drawImage(frameImage, x, y, width, height);
}
ctx.restore();
}
}
// ============================================
// ENEMY SYSTEM - Systém nepriateľov
// ============================================
/**
* ENEMY ANIMATION MANAGER
* Správa animácií pre všetkých nepriateľov v hre
*/
class EnemyAnimationManager {
constructor() {
// Základná cesta k sprite-om nepriateľov
this.basePath = 'images/superjozino/assets/mobs/';
// Načítané sprite sheety pre každý typ nepriateľa
this.spriteSheets = {};
// Konfigurácia pre každý typ nepriateľa
this.enemyConfig = {
AngryPig: {
path: 'AngryPig/',
animations: {
idle: {
frames: 9,
frameWidth: 36,
frameHeight: 30,
speed: 5, // Pomalšia idle animácia
loop: true
},
walk: {
frames: 16,
frameWidth: 36,
frameHeight: 30,
speed: 4, // Stredná rýchlosť animácie
loop: true
},
run: {
frames: 12,
frameWidth: 36,
frameHeight: 30,
speed: 3, // Rýchlejšia animácia
loop: true
},
hit: {
frames: 5,
frameWidth: 36,
frameHeight: 30,
speed: 2, // Rýchla hit animácia
loop: false // Prehráva sa len raz
}
}
},
Bat: {
path: 'Bat/',
animations: {
idle: {
frames: 12,
frameWidth: 46,
frameHeight: 30,
speed: 5, // Pomalá idle animácia (spí)
loop: true
},
flying: {
frames: 7,
frameWidth: 46,
frameHeight: 30,
speed: 4, // Stredná rýchlosť mávnutia krídel
loop: true
},
ceiling_in: { // Zasypanie (návrat na strop)
frames: 7,
frameWidth: 46,
frameHeight: 30,
speed: 5,
loop: false // Prehráva sa len raz
},
ceiling_out: { // Prebúdzanie (opustenie stropu)
frames: 7,
frameWidth: 46,
frameHeight: 30,
speed: 5,
loop: false // Prehráva sa len raz
},
hit: {
frames: 5,
frameWidth: 46,
frameHeight: 30,
speed: 2,
loop: false
}
}
},
Ghost: {
path: 'Ghost/',
animations: {
idle: {
frames: 10,
frameWidth: 44,
frameHeight: 30,
speed: 4, // Stredná rýchlosť animácie (vlnenie ducha)
loop: true
},
appear: {
frames: 4,
frameWidth: 44,
frameHeight: 30,
speed: 4, // Rýchlosť zjavenia
loop: false // Prehráva sa len raz
},
disappear: {
frames: 4,
frameWidth: 44,
frameHeight: 30,
speed: 4, // Rýchlosť zmiznutia
loop: false // Prehráva sa len raz
},
hit: {
frames: 5, // Použijeme idle animáciu aj pre hit (ghost sa nedá zabiť)
frameWidth: 44,
frameHeight: 30,
speed: 2,
loop: false
}
}
},
Chameleon: {
path: 'Chameleon/',
animations: {
idle: {
frames: 13,
frameWidth: 84,
frameHeight: 38,
speed: 5, // Pomalá idle animácia
loop: true
},
run: {
frames: 8,
frameWidth: 84,
frameHeight: 38,
speed: 4, // Bežná rýchlosť
loop: true
},
attack: {
frames: 10,
frameWidth: 84,
frameHeight: 38,
speed: 3, // Rýchla attack animácia
loop: false // Prehráva sa len raz
},
hit: {
frames: 5,
frameWidth: 84,
frameHeight: 38,
speed: 2,
loop: false
}
}
}
};
// Načítanie sprite sheetov
this.loadAllSprites();
}
/**
* Načítanie všetkých sprite sheetov pre nepriateľov
*/
loadAllSprites() {
console.log('👹 Načítavam sprite-y nepriateľov...');
// Pre každý typ nepriateľa
for (let enemyType in this.enemyConfig) {
const config = this.enemyConfig[enemyType];
const enemyPath = this.basePath + config.path;
// Inicializuj objekt pre tento typ nepriateľa
this.spriteSheets[enemyType] = {};
// Načítaj každú animáciu
for (let animName in config.animations) {
const img = new Image();
img.src = `${enemyPath}${animName}.png`;
// Ulož sprite sheet
this.spriteSheets[enemyType][animName] = img;
// Log pre debugging
img.onload = () => {
const anim = config.animations[animName];
console.log(`${enemyType} - ${animName}: ${anim.frames} frame-ov (${anim.frameWidth}x${anim.frameHeight}px)`);
};
img.onerror = () => {
console.error(`❌ Chyba pri načítaní: ${img.src}`);
};
}
}
}
/**
* Vykreslenie konkrétneho nepriateľa
* @param {CanvasRenderingContext2D} ctx - Canvas kontext
* @param {Enemy} enemy - Objekt nepriateľa
*/
draw(ctx, enemy) {
// Kontrola či je nepriateľ viditeľný
if (!enemy.visible) return;
// Získaj konfiguráciu pre tento typ nepriateľa
const config = this.enemyConfig[enemy.type];
if (!config) {
console.error(`❌ Neznámy typ nepriateľa: ${enemy.type}`);
return;
}
// Získaj aktuálnu animáciu
const animConfig = config.animations[enemy.currentAnimation];
if (!animConfig) {
console.error(`❌ Neznáma animácia: ${enemy.currentAnimation} pre ${enemy.type}`);
return;
}
// Získaj sprite sheet pre túto animáciu
const spriteSheet = this.spriteSheets[enemy.type][enemy.currentAnimation];
if (!spriteSheet || !spriteSheet.complete) {
// Placeholder ak sprite ešte nie je načítaný
ctx.fillStyle = 'red';
ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
return;
}
// Vypočítaj pozíciu frame-u v sprite sheete
const frameX = enemy.animationFrame * animConfig.frameWidth;
const frameY = 0; // Všetky frame-y sú v jednom riadku
// Vypni anti-aliasing pre pixel-perfect rendering
ctx.imageSmoothingEnabled = false;
// Zrkadlenie sprite-u podľa smeru pohybu
ctx.save();
if (enemy.direction === 1) { // ⬅️ ZMENENÉ: Bolo -1, teraz 1
// Otočenie doprava (flip horizontal)
ctx.translate(enemy.x + enemy.width, enemy.y);
ctx.scale(-1, 1);
ctx.drawImage(
spriteSheet,
frameX, frameY, // Pozícia v sprite sheete
animConfig.frameWidth, animConfig.frameHeight, // Veľkosť frame-u
0, 0, // Pozícia na canvase (upravené kvôli flip)
enemy.width, enemy.height // Veľkosť vykreslenia
);
} else {
// Normálne vykreslenie doľava (bez flip)
ctx.drawImage(
spriteSheet,
frameX, frameY, // Pozícia v sprite sheete
animConfig.frameWidth, animConfig.frameHeight, // Veľkosť frame-u
enemy.x, enemy.y, // Pozícia na canvase
enemy.width, enemy.height // Veľkosť vykreslenia
);
}
ctx.restore();
}
/**
* Aktualizácia animácie nepriateľa
* @param {Enemy} enemy - Objekt nepriateľa
* @param {number} deltaTime - Čas od posledného frame-u
*/
updateAnimation(enemy, deltaTime = 1) {
const config = this.enemyConfig[enemy.type];
if (!config) return;
const animConfig = config.animations[enemy.currentAnimation];
if (!animConfig) return;
// Počítadlo frame-ov pre rýchlosť animácie - s deltaTime
enemy.animationCounter += deltaTime;
if (enemy.animationCounter >= animConfig.speed) {
enemy.animationCounter = 0;
enemy.animationFrame++;
// Kontrola konca animácie
if (enemy.animationFrame >= animConfig.frames) {
if (animConfig.loop) {
// Loop animácia - vráť sa na začiatok
enemy.animationFrame = 0;
} else {
// Non-loop animácia (napr. hit) - zostane na poslednom frame
enemy.animationFrame = animConfig.frames - 1;
enemy.animationFinished = true;
}
}
}
}
}
/**
* ENEMY - Základná trieda pre nepriateľa
*/
class Enemy {
constructor(x, y, type, config = {}) {
// Pozícia
this.x = x;
this.y = y;
// Typ nepriateľa (AngryPig, Bat, Ghost, atď.)
this.type = type;
// Veľkosť collision boxu
this.width = config.width || 40;
this.height = config.height || 30;
// Pohyb
this.startX = config.startX || x;
this.endX = config.endX || x + 200;
this.speed = config.speed || 2;
this.direction = 1; // 1 = doprava, -1 = doľava
// Vlastnosti
this.hp = config.hp || 1;
this.maxHp = config.hp || 1;
this.damage = config.damage || 1; // Koľko životov zoberie hráčovi
this.killable = config.killable !== undefined ? config.killable : true;
this.stunnable = config.stunnable !== undefined ? config.stunnable : false;
// Stav
this.visible = true;
this.alive = true;
this.dying = false;
this.stunned = false;
this.stunnedTimer = 0;
// Animácia
this.currentAnimation = 'walk';
this.animationFrame = 0;
this.animationCounter = 0;
this.animationFinished = false;
// Správanie (pre rôzne typy nepriateľov)
this.behaviorType = config.behaviorType || 'patrol'; // patrol, flying, stationary
}
/**
* Nastavenie animácie
* @param {string} animationName - Názov animácie
*/
setAnimation(animationName) {
if (this.currentAnimation !== animationName) {
this.currentAnimation = animationName;
this.animationFrame = 0;
this.animationCounter = 0;
this.animationFinished = false;
}
}
/**
* Aktualizácia nepriateľa
* @param {number} deltaTime - Čas od posledného frame-u
*/
update(deltaTime) {
// Ak umiera, nezastavuj update
if (!this.alive && !this.dying) return;
// Ak umiera, nerobíme pohyb
if (this.dying) {
return;
}
// Ak je omráčený, počítaj čas
if (this.stunned) {
// Odpočítaj čas omráčenia (deltaTime už je normalizované)
this.stunnedTimer -= deltaTime;
if (this.stunnedTimer <= 0) {
this.stunned = false;
this.setAnimation('walk');
}
return;
}
// Pohyb podľa typu správania
if (this.behaviorType === 'patrol') {
this.patrolBehavior(deltaTime);
} else if (this.behaviorType === 'flying') {
this.patrolBehavior(deltaTime);
} else if (this.behaviorType === 'sleeping') {
// SleepingBat má vlastnú update()
} else if (this.behaviorType === 'ghost') {
this.patrolBehavior(deltaTime);
} else if (this.behaviorType === 'chameleon') {
// Chameleon má vlastnú update() metódu
// (nepotrebujeme tu nič robiť)
}
}
/**
* Správanie: Patrola medzi dvoma bodmi
* @param {number} deltaTime - Čas od posledného frame-u
*/
patrolBehavior(deltaTime) {
// Pohyb - rýchlosť × smer × deltaTime
this.x += this.speed * this.direction * deltaTime; // ✅ S deltaTime
// Zmena smeru pri dosiahnutí hraníc
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
}
// Animácia podľa rýchlosti
if (Math.abs(this.speed) > 1.3) {
this.setAnimation('run');
} else {
this.setAnimation('walk');
}
}
/**
* Zasah od hráča (skok zhora)
*/
hit() {
if (!this.alive) return;
this.hp--;
if (this.hp <= 0) {
if (this.killable) {
// ⬅️ OPRAVA: Namiesto alive=false použijeme dying stav
this.dying = true; // ⬅️ NOVÝ STAV
this.alive = false; // Stále označíme ako neživý (pre kolízie)
this.setAnimation('hit');
// Skryje sa po dokončení animácie
// Hit má 5 frame-ov, speed 4 = 5*4 = 20 update cyklov pri 60 FPS = ~333ms
setTimeout(() => {
this.visible = false;
this.dying = false; // ⬅️ Reset stavu
}, 400); // ⬅️ Trochu dlhší timeout aby určite stihla dobehnúť
} else if (this.stunnable) {
// Omráčiteľný nepriateľ - dočasne sa zastaví
this.stunned = true;
this.stunnedTimer = 120;
this.setAnimation('hit');
this.hp = this.maxHp;
}
}
}
/**
* Kontrola kolízie s iným objektom (napr. hráčom)
* @param {Object} other - Objekt na kontrolu kolízie
* @returns {boolean} - True ak došlo ku kolízii
*/
collidesWith(other) {
return this.alive &&
this.x < other.x + other.width &&
this.x + this.width > other.x &&
this.y < other.y + other.height &&
this.y + this.height > other.y;
}
}
/**
* ANGRY PIG - Špecifická trieda pre AngryPig nepriateľa
*/
class AngryPig extends Enemy {
constructor(x, y, config = {}) {
// Predvolené hodnoty pre AngryPig
const pigConfig = {
width: 40, // Stredná veľkosť
height: 30,
speed: config.speed || 4, // Stredná rýchlosť
hp: 1, // Zabije sa jedným skokom
damage: 1, // Zoberie 1 život hráčovi
killable: true, // Dá sa zabiť
stunnable: false, // Nedá sa omráčiť
behaviorType: 'patrol',
...config // Prepíš default hodnoty ak sú v config
};
// Zavolaj konštruktor rodiča
super(x, y, 'AngryPig', pigConfig);
}
}
/**
* GREEN PIG - Zelené prasa (iba walk, pomalé)
* Jednoduché prasa ktoré len pomaly chodí
*/
class GreenPig extends Enemy {
constructor(x, y, config = {}) {
const pigConfig = {
width: 40,
height: 30,
speed: 2, // ⬅️ Vždy 1 (walk animácia)
hp: 1,
damage: 1,
killable: true,
stunnable: false,
behaviorType: 'patrol',
...config
};
super(x, y, 'AngryPig', pigConfig);
this.pigType = 'green'; // Označenie typu
}
/**
* Prepísané správanie - vždy iba walk
* @param {number} deltaTime - Čas od posledného frame-u
*/
patrolBehavior(deltaTime) {
// Pohyb - s deltaTime
this.x += this.speed * this.direction * deltaTime;
// Zmena smeru
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
}
// Vždy iba walk animácia
this.setAnimation('walk');
}
}
/**
* RED PIG - Červené prasa (iba run, rýchle)
* Agresívne prasa ktoré stále beží
*/
class RedPig extends Enemy {
constructor(x, y, config = {}) {
const pigConfig = {
width: 40,
height: 30,
speed: config.speed || 1.7, // ⬅️ Default rýchlejšie (1.5-2)
hp: 1,
damage: 1,
killable: true,
stunnable: false,
behaviorType: 'patrol',
...config
};
super(x, y, 'AngryPig', pigConfig);
this.pigType = 'red';
}
/**
* Prepísané správanie - vždy iba run
* @param {number} deltaTime - Čas od posledného frame-u
*/
patrolBehavior(deltaTime) {
// Pohyb - s deltaTime
this.x += this.speed * this.direction * deltaTime;
// Zmena smeru
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
}
// Vždy iba run animácia
this.setAnimation('run');
}
}
/**
* COMBO PIG - Kombinované prasa (walk -> idle -> run -> idle)
* Inteligentné prasa s meniacim sa správaním
*/
class ComboPig extends Enemy {
constructor(x, y, config = {}) {
const pigConfig = {
width: 40,
height: 30,
speed: 1, // Začína pomaly
hp: 1,
damage: 1,
killable: true,
stunnable: false,
behaviorType: 'patrol',
...config
};
super(x, y, 'AngryPig', pigConfig);
this.pigType = 'combo';
// ⬅️ State machine pre kombináciu správania
this.state = 'walking'; // walking, waiting_to_run, running, waiting_to_walk
this.stateTimer = 0;
this.waitTime = 120; // ~2 sekundy čakania pri 60 FPS
// Rýchlosti pre rôzne stavy
this.walkSpeed = 1;
this.runSpeed = 4;
}
/**
* Prepísané update - vlastný state machine
* @param {number} deltaTime - Čas od posledného frame-u
*/
update(deltaTime) {
// Ak umiera, nezastavuj update
if (!this.alive && !this.dying) return;
// Ak umiera, nerobíme pohyb
if (this.dying) {
return;
}
if (this.stunned) {
this.stunnedTimer -= deltaTime;
if (this.stunnedTimer <= 0) {
this.stunned = false;
this.state = 'walking';
this.speed = this.walkSpeed;
}
return;
}
// Pohyb - s deltaTime
this.x += this.speed * this.direction * deltaTime;
// Zmena smeru na krajoch
let reachedEnd = false;
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
reachedEnd = true;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
reachedEnd = true;
}
// STATE MACHINE
switch(this.state) {
case 'walking':
this.speed = this.walkSpeed;
this.setAnimation('walk');
// Ak dosiahol koniec, prejdi do čakania
if (reachedEnd) {
this.state = 'waiting_to_run';
this.stateTimer = this.waitTime;
this.speed = 0; // Zastav sa
}
break;
case 'waiting_to_run':
this.speed = 0; // Stojí
this.setAnimation('idle');
this.stateTimer -= deltaTime; // Odpočítaj s deltaTime
// Po dočkaní sa rozbehni
if (this.stateTimer <= 0) {
this.state = 'running';
this.speed = this.runSpeed;
}
break;
case 'running':
this.speed = this.runSpeed;
this.setAnimation('run');
// Ak dosiahol koniec, prejdi do čakania
if (reachedEnd) {
this.state = 'waiting_to_walk';
this.stateTimer = this.waitTime;
this.speed = 0; // Zastav sa
}
break;
case 'waiting_to_walk':
this.speed = 0; // Stojí
this.setAnimation('idle');
this.stateTimer -= deltaTime; // Odpočítaj s deltaTime
// Po dočkaní sa pomaly pohni
if (this.stateTimer <= 0) {
this.state = 'walking';
this.speed = this.walkSpeed;
}
break;
}
}
}
/**
* ============================================
* BAT SYSTEM - Lietajúci nepriatelia
* ============================================
*/
/**
* SIMPLE BAT - Jednoduchý netopier
* Lieta horizontálne medzi dvoma bodmi (konštantná výška)
*/
class SimpleBat extends Enemy {
constructor(x, y, config = {}) {
const batConfig = {
width: 46,
height: 30,
speed: 1, // Pomalý pohyb
hp: 1,
damage: 1,
killable: true,
stunnable: false,
behaviorType: 'flying', // ⬅️ Nový typ správania
...config
};
super(x, y, 'Bat', batConfig);
this.batType = 'simple';
}
/**
* Prepísané správanie - horizontálny let
*/
patrolBehavior() {
// Pohyb
this.x += this.speed * this.direction;
// Zmena smeru
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
}
// ⬅️ Vždy flying animácia
this.setAnimation('flying');
}
}
/**
* WAVE BAT - Vlnovitý netopier
* Lieta horizontálne + vlní sa hore-dole (sine wave)
*/
class WaveBat extends Enemy {
constructor(x, y, config = {}) {
const batConfig = {
width: 46,
height: 30,
speed: 2,
hp: 1,
damage: 1,
killable: true,
stunnable: false,
behaviorType: 'flying',
...config
};
super(x, y, 'Bat', batConfig);
this.batType = 'wave';
// ⬅️ Pre vlnovitý pohyb
this.baseY = y; // Základná Y pozícia (stred vlny)
this.waveAmplitude = 30; // Amplitúda vlny (ako vysoko/nízko)
this.waveFrequency = 0.05; // Frekvencia vlny (ako rýchlo sa vlní)
this.waveOffset = 0; // Aktuálny offset pre výpočet
}
/**
* Prepísané správanie - horizontálny pohyb + vlnenie
* @param {number} deltaTime - Čas od posledného frame-u
*/
patrolBehavior(deltaTime) {
// Horizontálny pohyb - s deltaTime
this.x += this.speed * this.direction * deltaTime;
// Zmena smeru
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
}
// Vertikálne vlnenie (sine wave) - s deltaTime
this.waveOffset += this.waveFrequency * deltaTime;
this.y = this.baseY + Math.sin(this.waveOffset) * this.waveAmplitude;
// Vždy flying animácia
this.setAnimation('flying');
}
}
/**
* SLEEPING BAT - Spiaci netopier
* Visí na platforme → prebúdza sa → letí → vracia sa → zaspí
*/
class SleepingBat extends Enemy {
constructor(x, y, config = {}) {
const batConfig = {
width: 46,
height: 30,
speed: 3, // Rýchlejší keď lieta
hp: 1,
damage: 1,
killable: true,
stunnable: false,
behaviorType: 'sleeping', // ⬅️ Vlastný typ
...config
};
super(x, y, 'Bat', batConfig);
this.batType = 'sleeping';
// ⬅️ State machine
this.state = 'sleeping'; // sleeping, waking, flying, returning, falling_asleep
this.stateTimer = 0;
// Pozície
this.sleepX = x; // Pozícia kde spí (zavesený)
this.sleepY = y; // Y pozícia strechy
this.patrolStartX = config.patrolStartX || x - 50; // Začiatok patroly
this.patrolEndX = config.patrolEndX || x + 50; // Koniec patroly
// Časovanie
this.sleepDuration = config.sleepDuration || 320; // ~3 sekundy spí
this.flyDuration = config.flyDuration || 320; // ~4 sekundy letí
// Pre vlnenie počas letu (ako WaveBat)
this.waveAmplitude = 20;
this.waveFrequency = 0.05;
this.waveOffset = 0;
this.baseY = y + 50; // Lietať nižšie pod stropom
}
/**
* Prepísané update - state machine
* @param {number} deltaTime - Čas od posledného frame-u
*/
update(deltaTime) {
// Kontrola dying stavu
if (!this.alive && !this.dying) return;
if (this.dying) return;
if (this.stunned) {
this.stunnedTimer -= deltaTime;
if (this.stunnedTimer <= 0) {
this.stunned = false;
this.state = 'sleeping';
this.x = this.sleepX;
this.y = this.sleepY;
}
return;
}
// STATE MACHINE
switch(this.state) {
case 'sleeping':
// Spí zavesený na platforme
this.speed = 0;
this.x = this.sleepX;
this.y = this.sleepY;
this.setAnimation('idle');
this.stateTimer += deltaTime; // Odpočítaj s deltaTime
if (this.stateTimer >= this.sleepDuration) {
// Prebúdza sa
this.state = 'waking';
this.stateTimer = 0;
}
break;
case 'waking':
// Prebúdzanie - ceiling_out animácia
this.speed = 0;
this.setAnimation('ceiling_out');
// Počkaj na dokončenie animácie
this.stateTimer += deltaTime; // S deltaTime
if (this.stateTimer >= 50) {
// Začni lietať
this.state = 'flying';
this.stateTimer = 0;
this.direction = 1; // Začni letieť doprava
this.startX = this.patrolStartX;
this.endX = this.patrolEndX;
this.x = this.sleepX;
this.waveOffset = 0;
}
break;
case 'flying':
// Lietanie - wave pattern
this.speed = 1.5;
this.setAnimation('flying');
// Horizontálny pohyb - s deltaTime
this.x += this.speed * this.direction * deltaTime;
// Zmena smeru na krajoch
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
}
// Vertikálne vlnenie - s deltaTime
this.waveOffset += this.waveFrequency * deltaTime;
this.y = this.baseY + Math.sin(this.waveOffset) * this.waveAmplitude;
// Časovač letu - s deltaTime
this.stateTimer += deltaTime;
if (this.stateTimer >= this.flyDuration) {
// Vráť sa späť
this.state = 'returning';
this.stateTimer = 0;
}
break;
case 'returning':
// Návrat na spací miesto
this.speed = 2; // Rýchlejší návrat
this.setAnimation('flying');
// Pohyb smerom k sleep pozícii
const dx = this.sleepX - this.x;
const dy = this.sleepY - this.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
// Pohybuj sa k cieľu - s deltaTime
this.x += (dx / distance) * this.speed * deltaTime;
this.y += (dy / distance) * this.speed * deltaTime;
// Nastav smer podľa pohybu
this.direction = dx > 0 ? 1 : -1;
} else {
// Dosiahol spací bod
this.x = this.sleepX;
this.y = this.sleepY;
this.state = 'falling_asleep';
this.stateTimer = 0;
}
break;
case 'falling_asleep':
// Zasypanie - ceiling_in animácia
this.speed = 0;
this.x = this.sleepX;
this.y = this.sleepY;
this.setAnimation('ceiling_in');
// Počkaj na dokončenie animácie - s deltaTime
this.stateTimer += deltaTime;
if (this.stateTimer >= 50) {
// Zaspi
this.state = 'sleeping';
this.stateTimer = 0;
}
break;
}
}
}
/**
* ============================================
* GHOST SYSTEM - Duchovia (nezabiteľní)
* ============================================
*/
/**
* PATROL GHOST - Klasický duch
* Lietá medzi bodmi, prechádza stenami, nezabiteľný
*/
class PatrolGhost extends Enemy {
constructor(x, y, config = {}) {
const ghostConfig = {
width: 44,
height: 30,
speed: config.speed || 2,
hp: 999, // ⬅️ Veľa HP (nezabiteľný)
damage: 1,
killable: false, // ⬅️ Nedá sa zabiť!
stunnable: false,
behaviorType: 'ghost', // ⬅️ Nový typ
...config
};
super(x, y, 'Ghost', ghostConfig);
this.ghostType = 'patrol';
// ⬅️ Duch ignoruje gravitáciu a steny
this.ignoresWalls = true;
}
/**
* Prepísané správanie - patrol bez kolízií
*/
patrolBehavior() {
// Pohyb (prechádza stenami!)
this.x += this.speed * this.direction;
// Zmena smeru
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
}
// ⬅️ Vždy idle animácia (vlnenie ducha)
this.setAnimation('idle');
}
}
/**
* PHASING GHOST - Mizne a objavuje sa
* State machine: visible → disappearing → invisible → appearing → visible
*/
class PhasingGhost extends Enemy {
constructor(x, y, config = {}) {
const ghostConfig = {
width: 44,
height: 30,
speed: config.speed || 2,
hp: 999,
damage: 1,
killable: false,
stunnable: false,
behaviorType: 'ghost',
...config
};
super(x, y, 'Ghost', ghostConfig);
this.ghostType = 'phasing';
this.ignoresWalls = true;
// ⬅️ State machine pre phasing
this.state = 'visible'; // visible, disappearing, invisible, appearing
this.stateTimer = 0;
// Časovanie (v frame-och pri 60 FPS)
this.visibleDuration = config.visibleDuration || 180; // ~3 sekundy viditeľný
this.invisibleDuration = config.invisibleDuration || 120; // ~2 sekundy neviditeľný
// Phasing vlastnosti
this.canHurt = true; // Počas invisible nemôže ublížiť
}
/**
* Prepísané update - state machine s phasing
* @param {number} deltaTime - Čas od posledného frame-u
*/
update(deltaTime) {
// Duch nikdy neumiera, ale kontrolujeme dying stav pre istotu
if (this.dying) return;
// STATE MACHINE
switch(this.state) {
case 'visible':
// Normálny pohyb, viditeľný, môže ublížiť
this.visible = true;
this.canHurt = true;
this.setAnimation('idle');
// Pohyb - s deltaTime
this.x += this.speed * this.direction * deltaTime;
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
}
// Časovač - s deltaTime
this.stateTimer += deltaTime;
if (this.stateTimer >= this.visibleDuration) {
this.state = 'disappearing';
this.stateTimer = 0;
}
break;
case 'disappearing':
// Animácia zmiznutia
this.canHurt = false; // Už nemôže ublížiť
this.setAnimation('disappear');
// Stále sa pohybuje - s deltaTime
this.x += this.speed * this.direction * deltaTime;
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
}
// Počkaj na dokončenie animácie - s deltaTime
this.stateTimer += deltaTime;
if (this.stateTimer >= 40) {
this.state = 'invisible';
this.stateTimer = 0;
this.visible = false; // Skry ducha
}
break;
case 'invisible':
// Neviditeľný, pohybuje sa, nemôže ublížiť
this.visible = false;
this.canHurt = false;
// Stále sa pohybuje (aj keď neviditeľný) - s deltaTime
this.x += this.speed * this.direction * deltaTime;
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
}
// Časovač - s deltaTime
this.stateTimer += deltaTime;
if (this.stateTimer >= this.invisibleDuration) {
this.state = 'appearing';
this.stateTimer = 0;
}
break;
case 'appearing':
// Animácia zjavenia
this.visible = true;
this.canHurt = false; // Ešte nemôže ublížiť
this.setAnimation('appear');
// Stále sa pohybuje - s deltaTime
this.x += this.speed * this.direction * deltaTime;
if (this.x <= this.startX) {
this.direction = 1;
this.x = this.startX;
} else if (this.x >= this.endX) {
this.direction = -1;
this.x = this.endX;
}
// Počkaj na dokončenie animácie - s deltaTime
this.stateTimer += deltaTime;
if (this.stateTimer >= 40) {
this.state = 'visible';
this.stateTimer = 0;
this.canHurt = true; // Teraz môže ublížiť
}
break;
}
}
}
/**
* ============================================
* CHAMELEON - Chameleón s útokom jazykom
* ============================================
*/
/**
* CHAMELEON - Útočí jazykom na hráča
* Patruluje → detekuje hráča → zastaví → útok jazykom → cooldown
*/
class Chameleon extends Enemy {
constructor(x, y, config = {}) {
const chameleonConfig = {
width: 84,
height: 38,
speed: config.speed || 0.7,
hp: 1,
damage: 1,
killable: true,
stunnable: false,
behaviorType: 'chameleon',
...config
};
super(x, y, 'Chameleon', chameleonConfig);
// ⬅️ NOVÝ STATE: patrol, turning, preparing, attacking, cooldown
this.state = 'patrol';
this.stateTimer = 0;
// Útok jazykom
this.detectionRange = config.detectionRange || 150;
this.tongueRange = config.tongueRange || 100;
this.attackDuration = 50;
this.cooldownDuration = 90;
this.turningDuration = 40; // ⬅️ NOVÉ: ~0.7 sekundy idle na otočke
// ⬅️ NOVÉ: Vertikálna tolerancia pre detekciu (hráč musí byť približne na rovnakej výške)
this.verticalTolerance = config.verticalTolerance || 50; // 50px hore/dole
this.tongueHitbox = null;
this.tongueActive = false;
this.player = null;
}
/**
* Aktualizácia Chameleona
* @param {number} deltaTime - Čas od posledného frame-u
*/
update(deltaTime) {
if (!this.alive && !this.dying) return;
if (this.dying) return;
if (this.stunned) {
this.stunnedTimer -= deltaTime;
if (this.stunnedTimer <= 0) {
this.stunned = false;
this.state = 'patrol';
}
return;
}
switch(this.state) {
case 'patrol':
this.tongueActive = false;
this.tongueHitbox = null;
// Pohyb - s deltaTime
this.x += this.speed * this.direction * deltaTime;
// Pri dosiahnutí konca trasy prejdi do turning state
if (this.x <= this.startX) {
this.x = this.startX;
this.state = 'turning';
this.stateTimer = 0;
this.speed = 0;
this.direction = 1; // Priprav sa otočiť doprava
} else if (this.x >= this.endX) {
this.x = this.endX;
this.state = 'turning';
this.stateTimer = 0;
this.speed = 0;
this.direction = -1; // Priprav sa otočiť doľava
}
// Animácia
if (Math.abs(this.speed) > 0.1) {
this.setAnimation('run');
} else {
this.setAnimation('idle');
}
// Lepšia detekcia hráča
if (this.player && this.isPlayerInAttackRange()) {
this.state = 'preparing';
this.stateTimer = 0;
this.speed = 0;
// Otoč sa smerom k hráčovi
if (this.player.x < this.x) {
this.direction = -1;
} else {
this.direction = 1;
}
}
break;
case 'turning':
// Idle animácia na otočke
this.speed = 0;
this.setAnimation('idle');
this.stateTimer += deltaTime; // S deltaTime
if (this.stateTimer >= this.turningDuration) {
// Otočka dokončená, pokračuj v patrole
this.state = 'patrol';
this.stateTimer = 0;
this.speed = 0.7; // Obnov rýchlosť
}
// Aj počas otáčania môže útočiť ak je hráč blízko
if (this.player && this.isPlayerInAttackRange()) {
this.state = 'preparing';
this.stateTimer = 0;
// Otoč sa k hráčovi
if (this.player.x < this.x) {
this.direction = -1;
} else {
this.direction = 1;
}
}
break;
case 'preparing':
this.speed = 0;
this.setAnimation('idle');
this.stateTimer += deltaTime; // S deltaTime
if (this.stateTimer >= 20) {
this.state = 'attacking';
this.stateTimer = 0;
}
break;
case 'attacking':
this.speed = 0;
this.setAnimation('attack');
const attackProgress = this.stateTimer / this.attackDuration;
if (attackProgress >= 0.3 && attackProgress <= 0.8) {
this.tongueActive = true;
this.updateTongueHitbox();
} else {
this.tongueActive = false;
this.tongueHitbox = null;
}
this.stateTimer += deltaTime; // S deltaTime
if (this.stateTimer >= this.attackDuration) {
this.state = 'cooldown';
this.stateTimer = 0;
this.tongueActive = false;
this.tongueHitbox = null;
}
break;
case 'cooldown':
this.speed = 0;
this.setAnimation('idle');
this.tongueActive = false;
this.tongueHitbox = null;
this.stateTimer += deltaTime; // S deltaTime
if (this.stateTimer >= this.cooldownDuration) {
this.state = 'patrol';
this.stateTimer = 0;
this.speed = 0.7;
}
break;
}
}
/**
* ⬅️ OPRAVENÉ: Lepšia detekcia - kontrola horizontálnej A vertikálnej pozície
*/
isPlayerInAttackRange() {
if (!this.player) return false;
// Horizontálna vzdialenosť
const horizontalDistance = Math.abs(this.player.x - this.x);
// Vertikálna vzdialenosť (rozdiel Y súradníc)
const playerCenterY = this.player.y + this.player.height / 2;
const enemyCenterY = this.y + this.height / 2;
const verticalDistance = Math.abs(playerCenterY - enemyCenterY);
// Kontrola smeru (hráč musí byť PRED Chameleonom)
const isInFront = (this.direction === 1 && this.player.x > this.x) ||
(this.direction === -1 && this.player.x < this.x);
// ⬅️ NOVÉ: Hráč musí byť:
// 1. V horizontálnom dosahu (detection range)
// 2. V vertikálnom dosahu (verticalTolerance) - približne na rovnakej výške
// 3. Pred Chameleonom (nie za ním)
return horizontalDistance <= this.detectionRange &&
verticalDistance <= this.verticalTolerance &&
isInFront;
}
updateTongueHitbox() {
const tongueWidth = this.tongueRange;
const tongueHeight = 20;
let tongueX;
if (this.direction === 1) {
tongueX = this.x + this.width;
} else {
tongueX = this.x - tongueWidth;
}
const tongueY = this.y + (this.height / 2) - (tongueHeight / 2);
this.tongueHitbox = {
x: tongueX,
y: tongueY,
width: tongueWidth,
height: tongueHeight
};
}
}
//============================================================
//
/**
* COIN ANIMATION MANAGER
* Správa animácií odmien - mince, diamanty
*/
class CoinAnimationManager {
constructor() {
// Cesta k obrázkom odmien
this.basePath = 'images/superjozino/assets/treasure/';
// Definícia typov odmien
this.coinTypes = {
// 💛 Normálne mince
gold: {
folder: 'Gold Coin',
frames: 4,
speed: 8
},
// 🥈 Strieborné mince - posluchové cvičenia (TODO: overiť cestu)
silver: {
folder: 'Silver Coin',
frames: 4,
speed: 8
},
// 💙 Modrý diamant - rečové cvičenia
blueDiamond: {
folder: 'Blue Diamond',
frames: 4,
speed: 6
},
// 💚 Zelený diamant - bonusový predmet (power-up)
greenDiamond: {
folder: 'Green Diamond',
frames: 4,
speed: 10
},
// ❤️ Červený diamant - bonusový predmet (extra život)
redDiamond: {
folder: 'Red Diamond',
frames: 4,
speed: 10
}
};
// Načítané obrázky
this.images = {};
// Globálny frame counter (všetky mince sa animujú synchronizovane)
this.globalFrame = 0;
this.frameCounter = 0;
// Načítanie všetkých sprite-ov
this.loadAllSprites();
}
/**
* Načítanie všetkých sprite frame-ov
*/
loadAllSprites() {
console.log('💰 Načítavam sprite-y odmien...');
for (let typeName in this.coinTypes) {
const coinType = this.coinTypes[typeName];
this.images[typeName] = [];
for (let i = 1; i <= coinType.frames; i++) {
const img = new Image();
// Formát: 01.png, 02.png, 03.png, 04.png
const frameNumber = i.toString().padStart(2, '0');
img.src = `${this.basePath}${coinType.folder}/${frameNumber}.png`;
this.images[typeName].push(img);
// Log pre prvý frame každého typu (pre debugging)
if (i === 1) {
img.onload = () => {
console.log(`✅ Načítaná odmena: ${typeName} (${coinType.frames} frame-ov)`);
};
img.onerror = () => {
console.error(`❌ Chyba pri načítaní: ${img.src}`);
};
}
}
}
}
/**
* Update animácie (volať v game loop-e)
* @param {number} deltaTime - Čas od posledného frame-u
*/
update(deltaTime = 1) {
// Počítadlo frame-ov s deltaTime
this.frameCounter += deltaTime;
// Všetky mince používajú rovnakú rýchlosť animácie
if (this.frameCounter >= 10) { // speed z coinTypes
this.frameCounter = 0;
this.globalFrame++;
// Loop animácie (4 frame-y: 0,1,2,3,0,1,2,3...)
if (this.globalFrame >= 4) {
this.globalFrame = 0;
}
}
}
/**
* Vykreslenie odmeny
* @param {CanvasRenderingContext2D} ctx - Canvas kontext
* @param {string} type - Typ odmeny ('gold', 'blueDiamond', 'greenDiamond', 'redDiamond')
* @param {number} x - X pozícia
* @param {number} y - Y pozícia
* @param {number} size - Veľkosť (šírka/výška)
*/
draw(ctx, type, x, y, size) {
const frameImage = this.images[type]?.[this.globalFrame];
if (!frameImage || !frameImage.complete) {
// Placeholder ak sprite ešte nie je načítaný
ctx.fillStyle = type === 'gold' ? '#FFD700' : '#00FFFF';
ctx.beginPath();
ctx.arc(x + size/2, y + size/2, size/2, 0, Math.PI * 2);
ctx.fill();
return;
}
// Vypni anti-aliasing pre pixel-perfect rendering
ctx.imageSmoothingEnabled = false;
// Zachovanie aspect ratio (pôvodných proporcií obrázka)
const imgWidth = frameImage.naturalWidth || frameImage.width;
const imgHeight = frameImage.naturalHeight || frameImage.height;
const aspectRatio = imgWidth / imgHeight;
let drawWidth = size;
let drawHeight = size;
// Ak obrázok nie je štvorec, zachováme proporcie
if (aspectRatio > 1) {
// Širší ako vyšší
drawHeight = size / aspectRatio;
} else if (aspectRatio < 1) {
// Vyšší ako širší
drawWidth = size * aspectRatio;
}
// Vycentruj obrázok
const offsetX = (size - drawWidth) / 2;
const offsetY = (size - drawHeight) / 2;
// Vykresli animovaný sprite so zachovanými proporciami
ctx.drawImage(frameImage, x + offsetX, y + offsetY, drawWidth, drawHeight);
}
}
/**
* CHECKPOINT ANIMATION MANAGER
* Správa animácií checkpointov - neaktívny, aktivácia, idle
*/
class CheckpointAnimationManager {
constructor() {
// Cesta k obrázkom checkpointov
this.basePath = 'images/superjozino/assets/checkpoint/';
// Sprite sheety (veľké obrázky s frame-mi)
this.spriteSheets = {
noflag: null, // Jednoduchý obrázok (stĺpik bez vlajky)
flag: null, // 1664x64px (26 frame-ov po 64px)
idleflag: null // 640x64px (10 frame-ov po 64px)
};
// Definícia animácií
this.animations = {
inactive: {
spriteSheet: 'noflag',
frames: 1,
speed: 0 // Žiadna animácia
},
activating: {
spriteSheet: 'flag',
frames: 26,
frameWidth: 64,
speed: 2, // Rýchla animácia aktivácie
loop: false // Prehráva sa len raz
},
idle: {
spriteSheet: 'idleflag',
frames: 10,
frameWidth: 64,
speed:4, // Pomalšia idle animácia
loop: true
}
};
// Načítanie sprite sheetov
this.loadSprites();
}
/**
* Načítanie všetkých sprite sheetov
*/
loadSprites() {
console.log('🚩 Načítavam sprite-y checkpointov...');
// Načítaj noflag
this.spriteSheets.noflag = new Image();
this.spriteSheets.noflag.src = `${this.basePath}noflag.png`;
this.spriteSheets.noflag.onload = () => {
console.log('✅ Načítaný checkpoint: noflag');
};
// Načítaj flag (aktivácia)
this.spriteSheets.flag = new Image();
this.spriteSheets.flag.src = `${this.basePath}flag.png`;
this.spriteSheets.flag.onload = () => {
console.log('✅ Načítaný checkpoint: flag (26 frame-ov)');
};
// Načítaj idleflag
this.spriteSheets.idleflag = new Image();
this.spriteSheets.idleflag.src = `${this.basePath}idleflag.png`;
this.spriteSheets.idleflag.onload = () => {
console.log('✅ Načítaný checkpoint: idleflag (10 frame-ov)');
};
}
/**
* Update animácie checkpointu
* @param {Object} checkpoint - Checkpoint objekt
* @param {number} deltaTime - Čas od posledného frame-u
*/
updateCheckpoint(checkpoint, deltaTime = 1) {
// Inicializuj animačné vlastnosti ak neexistujú
if (!checkpoint.animState) {
// Prvý checkpoint (isStart) a finish začínajú ako idle
if (checkpoint.isStart || checkpoint.isFinish) {
checkpoint.animState = 'idle';
} else {
checkpoint.animState = 'inactive';
}
checkpoint.animFrame = 0;
checkpoint.animCounter = 0;
}
const anim = this.animations[checkpoint.animState];
if (!anim || anim.frames <= 1) return;
// Počítadlo s deltaTime
checkpoint.animCounter += deltaTime;
// Posun na ďalší frame
if (checkpoint.animCounter >= anim.speed) {
checkpoint.animCounter = 0;
checkpoint.animFrame++;
// Koniec animácie
if (checkpoint.animFrame >= anim.frames) {
if (anim.loop) {
// Loop animácia (idle)
checkpoint.animFrame = 0;
} else {
// Animácia sa prehráva len raz (activating)
checkpoint.animFrame = anim.frames - 1; // Zostaň na poslednom frame
checkpoint.animState = 'idle'; // Prejdi do idle stavu
checkpoint.animFrame = 0;
}
}
}
}
/**
* Aktivácia checkpointu (spustenie animácie)
* @param {Object} checkpoint - Checkpoint objekt
*/
activateCheckpoint(checkpoint) {
if (checkpoint.animState !== 'inactive') return;
console.log('🚩 Aktivujem checkpoint!');
checkpoint.animState = 'activating';
checkpoint.animFrame = 0;
checkpoint.animCounter = 0;
}
/**
* Vykreslenie checkpointu
* @param {CanvasRenderingContext2D} ctx - Canvas kontext
* @param {Object} checkpoint - Checkpoint objekt
*/
draw(ctx, checkpoint) {
// Inicializuj animačný stav ak neexistuje
if (!checkpoint.animState) {
// Prvý checkpoint (isStart) začína ako idle, ostatné ako inactive
if (checkpoint.isStart || checkpoint.isFinish) {
checkpoint.animState = 'idle';
} else {
checkpoint.animState = checkpoint.active ? 'idle' : 'inactive';
}
checkpoint.animFrame = 0;
checkpoint.animCounter = 0;
}
const anim = this.animations[checkpoint.animState];
const spriteSheet = this.spriteSheets[anim.spriteSheet];
if (!spriteSheet || !spriteSheet.complete) {
// Placeholder - starý spôsob vykreslenia
this.drawOldStyle(ctx, checkpoint);
return;
}
// Vypni anti-aliasing
ctx.imageSmoothingEnabled = false;
// Výpočet pozície frame-u v sprite sheete
let sourceX = 0;
if (anim.frames > 1) {
sourceX = checkpoint.animFrame * anim.frameWidth;
}
// Vypočítaj veľkosť vykreslenia
const drawWidth = checkpoint.width || 64;
const drawHeight = checkpoint.height || 64;
// Pre finish flag môžeme upraviť pozíciu aby bol vyššie
let drawY = checkpoint.y;
if (checkpoint.isFinish) {
// Posun Y hore aby vlajka bola vyššie (ale collision box zostane rovnaký)
drawY = checkpoint.y - (drawHeight - 64) / 2;
}
// Vykresli checkpoint
ctx.drawImage(
spriteSheet,
sourceX, 0, // Pozícia v sprite sheete
anim.frameWidth || 64, 64, // Veľkosť frame-u v sprite sheete
checkpoint.x,
drawY,
drawWidth, // Veľkosť na canvase
drawHeight
);
}
/**
* Starý štýl vykreslenia (fallback)
*/
drawOldStyle(ctx, checkpoint) {
// Kreslenie stožiaru vlajky
ctx.fillStyle = '#8B4513';
ctx.fillRect(
checkpoint.x,
checkpoint.y,
checkpoint.width,
checkpoint.height
);
// Kreslenie vlajky
ctx.beginPath();
ctx.fillStyle = checkpoint.isStart ? '#00FF00' : (checkpoint.active ? '#FF0000' : '#800000');
if (checkpoint.active) {
// Vztýčená vlajka
ctx.moveTo(checkpoint.x, checkpoint.y);
ctx.lineTo(checkpoint.x + 30, checkpoint.y + 15);
ctx.lineTo(checkpoint.x, checkpoint.y + 30);
} else {
// Spustená vlajka
ctx.moveTo(checkpoint.x, checkpoint.y + checkpoint.height - 30);
ctx.lineTo(checkpoint.x + 30, checkpoint.y + checkpoint.height - 15);
ctx.lineTo(checkpoint.x, checkpoint.y + checkpoint.height);
}
ctx.fill();
}
}
class Game {
constructor() {
this.canvas = document.getElementById('gameCanvas');
this.ctx = this.canvas.getContext('2d');
this.width = 800;
this.height = 800;
// Herné vlastnosti
this.gravity = 0.45;
this.friction = 0.75;
this.maxFallSpeed = 15;
this.currentLevel = 1;
this.gameState = 'playing'; // 'playing', 'paused', 'completed'
this.lives = 3;
this.isInvulnerable = false;
this.invulnerableTime = 2000; // 2 sekundy nezraniteľnosti po zásahu
this.lastCheckpoint = null;
// Mince a diamanty
this.collectedCoins = 0;
this.totalCoins = 0;
this.collectedDiamonds = 0;
this.requiredDiamonds = this.currentLevelData?.diamonds?.length || 4;
// debug mód
this.debug = false;
this.debugInfo = document.getElementById('debugInfo');
this.setupDebugControls();
this.lastTime = 0;
this.fps = 0;
// Inicializácia hráča
// Inicializácia hráča
this.player = {
x: 100,
y: 100,
// COLLISION BOX - skutočná veľkosť pre kolízie (menšia!)
width: 32, // Šírka collision boxu (užší)
height: 64, // Výška collision boxu (vyšší, ale menší ako sprite)
// SPRITE - veľkosť obrázka (väčší pre vizuál)
spriteWidth: 96, // Šírka sprite-u
spriteHeight: 96, // Výška sprite-u
// OFFSET - posun sprite-u relatívne k collision boxu
spriteOffsetX: -32, // Posun doľava aby bol sprite centrovaný (96-32)/2 = 32
spriteOffsetY: -22, // Posun hore aby nohy boli na spodku collision boxu
velocityX: 0,
velocityY: 0,
speed: 6,
jumpForce: -14,
isJumping: false
};
this.animationManager = new AnimationManager();
this.coinAnimationManager = new CoinAnimationManager();
this.checkpointAnimationManager = new CheckpointAnimationManager();
this.enemyAnimationManager = new EnemyAnimationManager();
// Kamera
this.camera = {
x: 0,
y: 0
};
// ==========================================
// NAČÍTANIE SPRITE SHEETOV
// ==========================================
// Terrain sprite sheet (obsahuje zem, platformy, bloky)
this.terrainSprite = new Image();
this.terrainSprite.src = 'images/superjozino/assets/terrain.png';
this.terrainSprite.loaded = false;
// Event listener pre načítanie sprite sheetu
this.terrainSprite.onload = () => {
this.terrainSprite.loaded = true;
console.log('✅ Terrain sprite sheet načítaný');
};
this.terrainSprite.onerror = () => {
console.error('❌ Chyba pri načítaní terrain sprite sheetu!');
console.error('Skontroluj cestu: images/superjozino/assets/terrain.png');
};
// Definícia tile-ov v sprite sheete (súradnice a rozmery)
this.tiles = {
ground: {
x: 98, // Pozícia X v sprite sheete
y: 2, // Pozícia Y v sprite sheete
width: 45, // Šírka tile-u
height: 45 // Výška tile-u
},
// Platformy (4 typy)
platform1: {
x: 193, // Začiatok x
y: 2, // Začiatok y
width: 46, // Šírka (239 - 193)
height: 13 // Výška (15 - 2)
},
platform2: {
x: 193, // Začiatok x
y: 16, // Začiatok y
width: 14, // Šírka (207 - 193)
height: 14 // Výška (30 - 16)
},
platform3: {
x: 209, // Začiatok x
y: 16, // Začiatok y
width: 30, // Šírka (239 - 209)
height: 30 // Výška (46 - 16)
},
platform4: {
x: 242, // Začiatok x
y: 2, // Začiatok y
width: 14, // Šírka (256 - 242)
height: 44 // Výška (46 - 2)
}
};
// Speech UI
this.createSpeechUI();
// Načítanie levelu
this.loadLevel(this.currentLevel);
// animacia smrti
this.deathAnimation = {
active: false,
timer: 0,
duration: 1000, // 1 sekunda na animáciu smrti
type: null // 'gap' alebo 'enemy'
};
// Ovládanie
this.keys = {};
this.setupControls();
// Exponuj game objekt pre joystick
window.game = this;
// Spustenie hernej slučky
this.gameLoop();
}
setupDebugControls() {
const toggleBtn = document.getElementById('toggleDebug');
toggleBtn.addEventListener('click', () => {
this.debug = !this.debug;
toggleBtn.textContent = `Debug Mode: ${this.debug ? 'ON' : 'OFF'}`;
});
// Pridáme aj klávesovú skratku 'D'
window.addEventListener('keydown', (e) => {
if (e.code === 'KeyD') {
this.debug = !this.debug;
toggleBtn.textContent = `Debug Mode: ${this.debug ? 'ON' : 'OFF'}`;
}
});
}
createSpeechUI() {
// Vytvorenie modálneho okna pre rečové cvičenia
this.speechModal = document.createElement('div');
this.speechModal.className = 'speech-modal';
this.speechModal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
`;
document.body.appendChild(this.speechModal);
}
showSpeechResult(success) {
const modal = document.querySelector('.speech-content');
const resultMessage = modal.querySelector('.result-message');
const recordButton = document.getElementById('startRecording');
if (success) {
resultMessage.textContent = 'Správne!';
resultMessage.style.color = '#4CAF50';
modal.classList.add('correct-answer');
setTimeout(() => {
modal.classList.remove('correct-answer');
this.hideSpeechExercise();
this.gameState = 'playing';
}, 1500);
} else {
exercise.attempts++;
resultMessage.textContent = 'Nesprávne, skús znova.';
resultMessage.style.color = '#f44336';
modal.classList.add('wrong-answer');
// Aktualizácia počtu pokusov
modal.querySelector('.attempts-info').textContent = `Počet pokusov: ${exercise.attempts}/5`;
setTimeout(() => {
modal.classList.remove('wrong-answer');
}, 500);
// Kontrola maximálneho počtu pokusov
if (exercise.attempts >= 5) {
resultMessage.textContent = 'Dosiahol si maximálny počet pokusov.';
recordButton.disabled = true;
recordButton.style.opacity = '0.5';
// Pridanie tlačidla na zatvorenie
const closeButton = document.createElement('button');
closeButton.className = 'button';
closeButton.style.marginTop = '10px';
closeButton.textContent = 'Zavrieť';
closeButton.onclick = () => {
this.hideSpeechExercise();
this.gameState = 'playing';
};
modal.appendChild(closeButton);
}
}
}
showSpeechExercise(exercise) {
this.speechModal.innerHTML = `
<div class="speech-content">
<h2>${exercise.word}</h2>
<img src="${exercise.imageUrl}" alt="${exercise.word}" style="max-width: 200px; margin-bottom: 20px;">
<div class="attempts-info">Počet pokusov: ${exercise.attempts}/5</div>
<button id="startRecording" class="button">
Začať nahrávanie
</button>
<div class="result-message" style="margin-top: 10px; min-height: 20px;"></div>
</div>
`;
this.speechModal.style.display = 'flex';
// Pridanie event listenera pre tlačidlo nahrávania
document.getElementById('startRecording').addEventListener('click', () => {
this.startSpeechRecognition(exercise);
});
}
hideSpeechExercise() {
this.speechModal.style.display = 'none';
}
showGameOver() {
const modalContent = `
<div class="game-over-content">
<h2>Game Over</h2>
<p>Zozbierané mince: ${this.collectedCoins}/${this.totalCoins}</p>
<p>Zozbierané diamanty: ${this.collectedDiamonds}/${this.requiredDiamonds}</p>
<div class="game-over-buttons">
<button class="button" onclick="location.reload()">Späť do menu</button>
<button class="button" id="restartLevel">Reštartovať level</button>
</div>
</div>
`;
this.speechModal.innerHTML = modalContent;
this.speechModal.style.display = 'flex';
this.speechModal.className = 'modal game-over-modal';
document.getElementById('restartLevel').addEventListener('click', () => {
this.restartLevel();
});
}
async startSpeechRecognition(exercise) {
if ('webkitSpeechRecognition' in window) {
const recognition = new webkitSpeechRecognition();
recognition.lang = 'sk-SK';
recognition.interimResults = false;
const recordButton = document.getElementById('startRecording');
recordButton.disabled = true;
recordButton.textContent = 'Počúvam...';
recognition.onresult = (event) => {
const result = event.results[0][0].transcript.toLowerCase();
if (result.includes(exercise.word.toLowerCase())) {
exercise.completed = true;
this.collectedDiamonds++;
this.showSpeechResult(true, exercise);
} else {
this.showSpeechResult(false, exercise);
}
recordButton.disabled = false;
recordButton.textContent = 'Začať nahrávanie';
};
recognition.onerror = (event) => {
console.error('Speech recognition error:', event.error);
recordButton.disabled = false;
recordButton.textContent = 'Začať nahrávanie';
const resultMessage = document.querySelector('.result-message');
resultMessage.textContent = 'Nastala chyba pri rozpoznávaní. Skús znova.';
resultMessage.style.color = '#f44336';
};
recognition.onend = () => {
recordButton.disabled = false;
recordButton.textContent = 'Začať nahrávanie';
};
recognition.start();
}
}
handleCheckpoints() {
for (let checkpoint of this.currentLevelData.checkpoints) {
if (!checkpoint.active && this.checkCollision(this.player, checkpoint)) {
this.activateCheckpoint(checkpoint);
}
}
}
handleWallCollisions() {
for (let wall of this.currentLevelData.walls) {
const nextPositionX = {
x: this.player.x + this.player.velocityX,
y: this.player.y,
width: this.player.width,
height: this.player.height
};
const nextPositionY = {
x: this.player.x,
y: this.player.y + this.player.velocityY,
width: this.player.width,
height: this.player.height
};
// Horizontálna kolízia
if (this.checkCollision(nextPositionX, wall)) {
if (this.player.velocityX > 0) {
this.player.x = wall.x - this.player.width;
} else if (this.player.velocityX < 0) {
this.player.x = wall.x + wall.width;
}
this.player.velocityX = 0;
}
// Vertikálna kolízia
if (this.checkCollision(nextPositionY, wall)) {
if (this.player.velocityY > 0) { // Padá dole
this.player.y = wall.y - this.player.height;
this.player.velocityY = 0;
this.player.isJumping = false;
} else if (this.player.velocityY < 0) { // Skáče hore
this.player.y = wall.y + wall.height;
this.player.velocityY = 0;
}
}
}
}
activateCheckpoint(checkpoint) {
// Deaktivujeme predchádzajúci checkpoint
if (this.lastCheckpoint && this.lastCheckpoint !== checkpoint) {
this.lastCheckpoint.active = false;
// Resetuj animáciu predchádzajúceho checkpointu
this.lastCheckpoint.animState = 'inactive';
this.lastCheckpoint.animFrame = 0;
}
// Aktivujeme nový checkpoint
checkpoint.active = true;
this.lastCheckpoint = checkpoint;
// Spustíme animáciu aktivácie
this.checkpointAnimationManager.activateCheckpoint(checkpoint);
console.log('🚩 Checkpoint aktivovaný!');
// TODO: Pridať zvukový efekt
}
respawnAtCheckpoint() {
if (this.lastCheckpoint) {
this.player.x = this.lastCheckpoint.x;
this.player.y = this.lastCheckpoint.y - this.player.height;
} else {
// Ak nie je žiadny checkpoint, začneme od začiatku levelu
this.player.x = 100;
this.player.y = 100;
}
this.player.velocityX = 0;
this.player.velocityY = 0;
this.player.isJumping = false;
this.isInvulnerable = true;
// Krátka doba nezraniteľnosti po respawne
setTimeout(() => {
this.isInvulnerable = false;
}, this.invulnerableTime);
}
drawDebugInfo() {
if (!this.debug) return;
this.ctx.save();
this.ctx.translate(-this.camera.x, 0);
this.drawDebugGrid();
this.drawCollisionBoxes();
this.drawObjectInfo();
this.ctx.restore();
this.drawFixedDebugInfo();
this.ctx.strokeStyle = 'cyan';
for (let diamond of this.currentLevelData.diamonds) {
if (!diamond.collected) {
this.ctx.strokeRect(
diamond.x,
diamond.y,
diamond.width,
diamond.height
);
}
}
}
drawDebugGrid() {
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
this.ctx.lineWidth = 1;
// Vertikálne čiary
for (let x = 0; x < this.currentLevelData.width; x += 100) {
this.ctx.beginPath();
this.ctx.moveTo(x, 0);
this.ctx.lineTo(x, this.height);
this.ctx.stroke();
}
// Horizontálne čiary
for (let y = 0; y < this.height; y += 100) {
this.ctx.beginPath();
this.ctx.moveTo(0, y);
this.ctx.lineTo(this.currentLevelData.width, y);
this.ctx.stroke();
}
}
drawCollisionBoxes() {
// Hráč
this.ctx.strokeStyle = 'red';
this.ctx.strokeRect(
this.player.x,
this.player.y,
this.player.width,
this.player.height
);
// Platformy
this.ctx.strokeStyle = 'yellow';
for (let platform of this.currentLevelData.platforms) {
this.ctx.strokeRect(
platform.x,
platform.y,
platform.width,
platform.height
);
}
//Nepriatelia
this.ctx.strokeStyle = 'purple';
for (let enemy of this.currentLevelData.enemies) {
this.ctx.strokeRect(
enemy.x,
enemy.y,
enemy.width,
enemy.height
);
// Trasa nepriateľa
this.ctx.setLineDash([5, 5]);
this.ctx.beginPath();
this.ctx.moveTo(enemy.startX, enemy.y + enemy.height/2);
this.ctx.lineTo(enemy.endX, enemy.y + enemy.height/2);
this.ctx.stroke();
this.ctx.setLineDash([]);
// ⬅️ PRIDANÉ: Debug tongue hitbox (Chameleon)
if (enemy.behaviorType === 'chameleon' && enemy.tongueActive && enemy.tongueHitbox) {
this.ctx.strokeStyle = 'red';
this.ctx.lineWidth = 2;
this.ctx.strokeRect(
enemy.tongueHitbox.x,
enemy.tongueHitbox.y,
enemy.tongueHitbox.width,
enemy.tongueHitbox.height
);
this.ctx.lineWidth = 1;
}
// ⬅️ PRIDANÉ: Debug detection range (Chameleon)
if (enemy.behaviorType === 'chameleon' && this.debug) {
this.ctx.strokeStyle = 'orange';
this.ctx.setLineDash([2, 2]);
this.ctx.beginPath();
// Detekčná zóna - polkruh pred chameleonom
if (enemy.direction === 1) {
// Doprava
this.ctx.arc(
enemy.x + enemy.width,
enemy.y + enemy.height/2,
enemy.detectionRange,
-Math.PI/2,
Math.PI/2
);
} else {
// Doľava
this.ctx.arc(
enemy.x,
enemy.y + enemy.height/2,
enemy.detectionRange,
Math.PI/2,
Math.PI*1.5
);
}
this.ctx.stroke();
this.ctx.setLineDash([]);
}
}
// Mince
this.ctx.strokeStyle = 'gold';
for (let coin of this.currentLevelData.coins) {
if (!coin.collected) {
this.ctx.strokeRect(
coin.x,
coin.y,
coin.width,
coin.height
);
}
}
}
drawObjectInfo() {
this.ctx.font = '12px monospace';
this.ctx.fillStyle = 'white';
// Informácie o hráčovi
this.ctx.fillText(
`x: ${Math.round(this.player.x)} y: ${Math.round(this.player.y)}`,
this.player.x,
this.player.y - 20
);
this.ctx.fillText(
`vx: ${this.player.velocityX.toFixed(2)} vy: ${this.player.velocityY.toFixed(2)}`,
this.player.x,
this.player.y - 35
);
}
drawFixedDebugInfo() {
// Aktualizácia debug panelu
if (this.debugInfo) {
this.debugInfo.innerHTML = `
<div>FPS: ${Math.round(this.fps || 0)}</div>
<div>Camera X: ${Math.round(this.camera.x)}</div>
<div>Player State: ${this.player.isJumping ? 'Jumping' : 'Grounded'}</div>
<div>Game State: ${this.gameState}</div>
<div>Lives: ${this.lives}</div>
<div>Coins: ${this.collectedCoins}/${this.totalCoins}</div>
<div>Diamonds: ${this.collectedDiamonds}/${this.requiredDiamonds}</div>
`;
}
}
loadLevel(levelNumber) {
if (!LEVELS[levelNumber]) {
console.error('Level neexistuje!');
return;
}
this.currentLevelData = LEVELS[levelNumber].data;
this.player.x = 100;
this.player.y = 100;
this.player.velocityX = 0;
this.player.velocityY = 0;
// Počítanie celkového počtu mincí a diamantov
this.totalCoins = this.currentLevelData.coins.length;
this.collectedCoins = 0;
this.collectedDiamonds = 0;
this.requiredDiamonds = this.currentLevelData.diamonds.length;
}
checkCollision(rect1, rect2) {
return rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y;
}
handleSpecialBlockCollision() {
for (let block of this.currentLevelData.specialBlocks) {
if (!block.hit && this.checkCollision(this.player, block)) {
// Zberie sa pri akomkoľvek kontakte (nie len pri skoku zdola)
block.hit = true;
this.collectSpecialItem(block.itemType);
// Zvukový efekt alebo animácia (TODO)
console.log(`✨ Získaný bonus: ${block.itemType}`);
}
}
}
collectSpecialItem(type) {
switch(type) {
case 'powerup':
// 💚 Zelený diamant - Power-up
// TODO: Implementovať power-up efekt (napr. dočasná nezraniteľnosť, rýchlejší beh...)
console.log('💚 Power-up aktivovaný!');
// Dočasne pridáme body
if (!this.score) this.score = 0;
this.score += 1000;
break;
case 'extraLife':
// ❤️ Červený diamant - Extra život
this.lives++;
console.log(`❤️ Extra život! Teraz máš ${this.lives} životov.`);
break;
}
// Ulož do collected items
if (!this.currentLevelData.collectedSpecialItems) {
this.currentLevelData.collectedSpecialItems = [];
}
this.currentLevelData.collectedSpecialItems.push(type);
}
/**
* Kontrola kolízií s nepriateľmi
*/
handleEnemyCollisions() {
if (this.isInvulnerable) return;
for (let enemy of this.currentLevelData.enemies) {
if (!enemy.alive || !enemy.visible) continue;
// ⬅️ NOVÉ: Kontrola tongue attack (Chameleon)
if (enemy.behaviorType === 'chameleon' && enemy.tongueActive && enemy.tongueHitbox) {
// Kontrola kolízie jazyka s hráčom
if (this.checkCollision(this.player, enemy.tongueHitbox)) {
console.log('👅 Chameleon ťa trafil jazykom!');
this.hitByEnemy();
// Deaktivuj jazyk aby neudieral opakovane
enemy.tongueActive = false;
enemy.tongueHitbox = null;
continue; // Preskočiť normálnu kolíziu
}
}
// PhasingGhost kontrola
if (enemy.ghostType === 'phasing' && !enemy.canHurt) {
continue;
}
if (this.checkCollision(this.player, enemy)) {
// Ghost je nezabiteľný
if (enemy.behaviorType === 'ghost') {
this.hitByEnemy();
continue;
}
// ⬅️ NOVÉ: Chameleon - dá sa zabiť len skokom zhora (ako Pig)
if (enemy.behaviorType === 'chameleon') {
const playerBottom = this.player.y + this.player.height;
const enemyTop = enemy.y;
const jumpTolerance = 10;
const isJumpingOnEnemy = this.player.velocityY > 0 &&
playerBottom <= enemyTop + jumpTolerance;
if (isJumpingOnEnemy) {
enemy.hit();
this.player.velocityY = -5;
console.log(`💀 Zabil si ${enemy.type}!`);
} else {
// Dotyk zboku/zdola = damage
this.hitByEnemy();
}
continue;
}
// Lietajúci nepriateľ (Bat) - zhora aj zdola
if (enemy.behaviorType === 'flying' || enemy.behaviorType === 'sleeping') {
const playerBottom = this.player.y + this.player.height;
const playerTop = this.player.y;
const enemyTop = enemy.y;
const enemyBottom = enemy.y + enemy.height;
const jumpTolerance = 10;
const isJumpingOnTop = this.player.velocityY > 0 &&
playerBottom <= enemyTop + jumpTolerance;
const isJumpingFromBelow = this.player.velocityY < 0 &&
playerTop >= enemyBottom - jumpTolerance;
if (isJumpingOnTop || isJumpingFromBelow) {
if (enemy.killable || enemy.stunnable) {
enemy.hit();
if (isJumpingOnTop) {
this.player.velocityY = -5;
} else {
this.player.velocityY = 5;
}
console.log(`💀 Zabil si ${enemy.type}!`);
} else {
this.hitByEnemy();
}
} else {
this.hitByEnemy();
}
} else {
// Pozemný nepriateľ (Pig) - len zhora
const playerBottom = this.player.y + this.player.height;
const enemyTop = enemy.y;
const jumpTolerance = 10;
const isJumpingOnEnemy = this.player.velocityY > 0 &&
playerBottom <= enemyTop + jumpTolerance;
if (isJumpingOnEnemy) {
if (enemy.killable || enemy.stunnable) {
enemy.hit();
this.player.velocityY = -5;
console.log(`💀 Zabil si ${enemy.type}!`);
} else {
this.hitByEnemy();
}
} else {
this.hitByEnemy();
}
}
}
}
}
handleCoinCollection() {
// Zbieranie normálnych mincí (gold, silver)
for (let coin of this.currentLevelData.coins) {
if (!coin.collected && this.checkCollision(this.player, coin)) {
coin.collected = true;
if (coin.type === 'gold') {
// Normálna zlatá minca
this.collectedCoins++;
// TODO: Pridať zvuk zbierania mince
} else if (coin.type === 'silver') {
// Strieborná minca - posluchové cvičenie
if (!coin.listeningExercise.completed) {
this.gameState = 'listening';
// TODO: Spustiť posluchové cvičenie
console.log('🎧 Posluchové cvičenie!');
coin.listeningExercise.completed = true;
this.collectedCoins++;
}
}
}
}
// Zbieranie diamantov (blue, green, red)
for (let diamond of this.currentLevelData.diamonds) {
if (!diamond.collected && this.checkCollision(this.player, diamond)) {
diamond.collected = true;
if (diamond.type === 'blueDiamond') {
// Modrý diamant - rečové cvičenie
if (!diamond.speechExercise.completed) {
this.gameState = 'speech';
this.showSpeechExercise(diamond.speechExercise);
this.collectedDiamonds++;
}
} else if (diamond.type === 'greenDiamond') {
// Zelený diamant - power-up
this.collectSpecialItem('powerup');
console.log('💚 Power-up získaný!');
// TODO: Implementovať power-up efekt
} else if (diamond.type === 'redDiamond') {
// Červený diamant - extra život
this.collectSpecialItem('extraLife');
console.log('❤️ Extra život získaný!');
}
}
}
}
async handleDiamondCollection(coin) {
if (!coin.collected) {
this.gameState = 'speech';
this.showSpeechExercise(coin.speechExercise);
}
}
showLevelComplete() {
const coinPercentage = (this.collectedCoins / this.totalCoins) * 100;
const stars = coinPercentage >= 90 ? 3 : coinPercentage >= 60 ? 2 : 1;
const modalContent = `
<div class="level-complete-content">
<h2>Level ${this.currentLevel} dokončený!</h2>
<div class="stars">
${Array(stars).fill('⭐').join('')}
</div>
<p>Zozbierané mince: ${this.collectedCoins}/${this.totalCoins} (${coinPercentage.toFixed(1)}%)</p>
<p>Zozbierané diamanty: ${this.collectedDiamonds}/${this.requiredDiamonds}</p>
<h3>Rečové cvičenia:</h3>
${this.currentLevelData.diamonds
.map(diamond => `
<p>${diamond.speechExercise.word}: ${diamond.speechExercise.attempts} pokusov</p>
`).join('')}
<div class="game-over-buttons">
<button class="button" onclick="location.reload()">Späť do menu</button>
<button class="button" id="nextLevel">Ďalší level</button>
</div>
</div>
`;
this.speechModal.innerHTML = modalContent;
this.speechModal.style.display = 'flex';
this.speechModal.className = 'modal level-complete-modal';
}
handlePlatformCollisions() {
const player = {
x: this.player.x,
y: this.player.y + this.player.velocityY,
width: this.player.width,
height: this.player.height
};
for (let block of this.currentLevelData.specialBlocks) {
if (!block.hit &&
this.player.velocityY < 0 &&
this.checkCollision({
x: this.player.x,
y: this.player.y,
width: this.player.width,
height: this.player.height
}, block)) {
block.hit = true;
this.collectSpecialItem(block.itemType);
}
}
for (let platform of this.currentLevelData.platforms) {
if (this.checkCollision(player, platform)) {
if (this.player.velocityY > 0) { // Padá dole
this.player.y = platform.y - this.player.height;
this.player.velocityY = 0;
this.player.isJumping = false;
} else if (this.player.velocityY < 0) { // Skáče hore
this.player.y = platform.y + platform.height;
this.player.velocityY = 0;
}
return true;
}
}
return false;
}
checkLevelCompletion() {
if (this.checkCollision(this.player, this.currentLevelData.endPoint)) {
this.gameState = 'completed';
this.completeLevel();
}
}
completeLevel() {
const level = LEVELS[this.currentLevel];
const completion = (this.currentLevelData.collected / this.currentLevelData.totalCoins) * 100;
// Výpočet hviezd
let stars = 0;
if (completion >= 90) stars = 3;
else if (completion >= 60) stars = 2;
else if (completion >= 30) stars = 1;
// Uloženie progresu
level.stars = Math.max(level.stars, stars);
level.completion = Math.max(level.completion, completion);
// Odomknutie ďalšieho levelu
if (LEVELS[this.currentLevel + 1] && stars === 3) {
LEVELS[this.currentLevel + 1].unlocked = true;
}
// Tu môžeme pridať zobrazenie výsledkovej obrazovky
console.log(`Level ${this.currentLevel} dokončený! Hviezdy: ${stars}, Completion: ${completion}%`);
}
setupControls() {
// Klávesnica
window.addEventListener('keydown', (e) => {
this.keys[e.code] = true;
});
window.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
});
}
updateCamera() {
// Sledovanie hráča kamerou
const targetX = this.player.x - this.width / 3; // kamera sleduje hráča v prvej tretine obrazovky
// Plynulý pohyb kamery
this.camera.x += (targetX - this.camera.x) * 0.1;
// Obmedzenia kamery
if (this.camera.x < 0) this.camera.x = 0;
if (this.camera.x > this.currentLevelData.width - this.width) {
this.camera.x = this.currentLevelData.width - this.width;
}
// KRITICKÉ: Zaokrúhli kameru na celé pixely (eliminuje biele čiary)
this.camera.x = Math.round(this.camera.x);
this.camera.y = Math.round(this.camera.y);
}
hitByEnemy() {
this.lives--;
this.isInvulnerable = true;
setTimeout(() => {
this.isInvulnerable = false;
}, this.invulnerableTime);
if (this.lives <= 0) {
this.gameOver();
} else {
this.respawnAtCheckpoint();
}
}
/**
* Aktualizácia všetkých nepriateľov v leveli
* @param {number} deltaTime - Čas od posledného frame-u (normalizovaný na 60 FPS)
*/
updateEnemies(deltaTime) {
for (let enemy of this.currentLevelData.enemies) {
// Nastav player referenciu pre Chameleon (potrebuje pre detekciu)
if (enemy.behaviorType === 'chameleon') {
enemy.player = this.player;
}
// Aktualizuj správanie nepriateľa - pošli deltaTime
enemy.update(deltaTime);
// Aktualizuj animáciu nepriateľa - pošli deltaTime
this.enemyAnimationManager.updateAnimation(enemy, deltaTime);
}
}
startDeathAnimation(type) {
this.deathAnimation.active = true;
this.deathAnimation.timer = 0;
this.deathAnimation.type = type;
this.lives--;
}
checkGapCollision() {
for (let gap of this.currentLevelData.gaps) {
if (this.checkCollision(this.player, gap)) {
if (!this.deathAnimation.active) {
this.startDeathAnimation('gap');
}
return true;
}
}
return false;
}
updateDeathAnimation(deltaTime) {
if (!this.deathAnimation.active) return;
this.deathAnimation.timer += deltaTime;
if (this.deathAnimation.type === 'gap') {
// Padanie do diery
this.player.velocityY += this.gravity;
this.player.y += this.player.velocityY;
}
// Keď hráč spadne dostatočne hlboko alebo uplynie čas animácie
if (this.player.y > this.height + 100 || this.deathAnimation.timer >= this.deathAnimation.duration) {
this.deathAnimation.active = false;
if (this.lives <= 0) {
this.gameOver();
} else {
this.respawnAtCheckpoint();
}
}
}
gameOver() {
this.gameState = 'gameOver';
// Vytvorenie game over obrazovky
const modalContent = `
<div style="background: white; padding: 20px; border-radius: 10px; text-align: center;">
<h2>Game Over</h2>
<p>Zozbierané mince: ${this.collectedCoins}/${this.totalCoins}</p>
<p>Zozbierané diamanty: ${this.collectedDiamonds}/${this.requiredDiamonds}</p>
<button onclick="location.reload()" style="padding: 10px 20px; margin: 5px;">Späť do menu</button>
<button id="restartLevel" style="padding: 10px 20px; margin: 5px;">Reštartovať level</button>
</div>
`;
this.speechModal.innerHTML = modalContent;
this.speechModal.style.display = 'flex';
// Pridanie event listenera pre reštart levelu
document.getElementById('restartLevel').addEventListener('click', () => {
this.restartLevel();
});
}
restartLevel() {
// Reset všetkých potrebných vlastností
this.lives = 3;
this.collectedCoins = 0;
this.collectedDiamonds = 0;
this.lastCheckpoint = null;
this.isInvulnerable = false;
this.deathAnimation.active = false;
// Znovu načítanie levelu
this.loadLevel(this.currentLevel);
// Skrytie modálneho okna
this.speechModal.style.display = 'none';
// Obnovenie hry
this.gameState = 'playing';
}
/**
* Aktualizácia hernej logiky
* @param {number} timestamp - Aktuálny čas v milisekundách
*/
update(timestamp) {
if (this.gameState !== 'playing') return;
// 🧪 TESTOVACÍ KÓD - spomalí hru na ~20 FPS
const now = performance.now();
while (performance.now() - now < 20) {
// Umelé spomalenie - čaká 30ms
}
// ✅ NOVÝ SYSTÉM: Výpočet delta time
// Ak toto je prvý frame, nastav lastTimestamp
if (!this.lastTimestamp) {
this.lastTimestamp = timestamp;
return; // Preskočíme prvý frame
}
// Vypočítaj čas od posledného frame-u v milisekundách
const deltaTimeMs = timestamp - this.lastTimestamp;
this.lastTimestamp = timestamp;
// Vypočítaj deltaTime ako multiplikátor oproti 60 FPS
// Pri 60 FPS (16.67ms) = 1.0 (normálna rýchlosť)
// Pri 30 FPS (33.33ms) = 2.0 (dvojnásobná rýchlosť, aby sa vyrovnala)
const targetFrameTime = 1000 / 60; // 16.67ms (60 FPS)
const deltaTime = deltaTimeMs / targetFrameTime;
// Obmedzíme deltaTime aby pri veľkých lagoch nepreskočila hra príliš veľa
const clampedDeltaTime = Math.min(deltaTime, 3); // Max 3x rýchlosť (20 FPS minimum)
if (this.deathAnimation.active) {
this.updateDeathAnimation(deltaTime);
return;
}
// Pohyb hráča
if (this.keys['ArrowLeft']) {
this.player.velocityX = -this.player.speed; // ✅ BEZ deltaTime
}
if (this.keys['ArrowRight']) {
this.player.velocityX = this.player.speed; // ✅ BEZ deltaTime
}
if (this.keys['Space'] && !this.player.isJumping) {
this.player.velocityY = this.player.jumpForce; // ✅ BEZ deltaTime (okamžitý impulz)
this.player.isJumping = true;
}
// Aplikácia fyziky
this.player.velocityY += this.gravity * clampedDeltaTime;
// Obmedzenie maximálnej rýchlosť padania (terminal velocity)
if (this.player.velocityY > this.maxFallSpeed) {
this.player.velocityY = this.maxFallSpeed; // ✅ BEZ deltaTime
}
this.player.velocityX *= this.friction;
// Aktualizácia pozície a kolízie
this.handleWallCollisions();
this.handlePlatformCollisions();
// Aktualizácia pozície
this.player.x += this.player.velocityX * clampedDeltaTime; // ✅ S deltaTime
this.player.y += this.player.velocityY * clampedDeltaTime;
// Kontrola dokončenia levelu
this.checkLevelCompletion();
// Aktualizácia nepriateľov
this.updateEnemies(clampedDeltaTime);
// Ostatné kontroly
this.handleSpecialBlockCollision();
this.handleEnemyCollisions();
this.handleCheckpoints();
this.checkGapCollision();
this.handleCoinCollection();
// Kontrola dokončenia levelu
const coinPercentage = (this.collectedCoins / this.totalCoins) * 100;
const hasAllDiamonds = this.collectedDiamonds >= this.requiredDiamonds;
if (coinPercentage >= 80 && hasAllDiamonds &&
this.checkCollision(this.player, this.currentLevelData.endPoint)) {
this.showLevelComplete();
}
// Hranice levelu (nie obrazovky)
if (this.player.x < 0) this.player.x = 0;
if (this.player.x + this.player.width > this.currentLevelData.width) {
this.player.x = this.currentLevelData.width - this.player.width;
}
// === ANIMÁCIE POSTAVY ===
// Určenie smeru
if (this.player.velocityX > 0.1) {
this.animationManager.setDirection('right');
} else if (this.player.velocityX < -0.1) {
this.animationManager.setDirection('left');
}
// Určenie animácie podľa stavu
if (this.player.isJumping && this.player.velocityY < 0) {
// Skáče hore
this.animationManager.setAnimation('jump');
} else if (this.player.velocityY > 1) {
// Padá dole
this.animationManager.setAnimation('falling');
} else if (Math.abs(this.player.velocityX) > 2) {
// Beží
this.animationManager.setAnimation('run');
} else if (Math.abs(this.player.velocityX) > 0.1) {
// Chodí
this.animationManager.setAnimation('walk');
} else {
// Stojí
this.animationManager.setAnimation('idle');
}
// Update animácie
this.animationManager.update(clampedDeltaTime);
// Update animácií odmien (mince, diamanty)
this.coinAnimationManager.update(clampedDeltaTime);
// Update animácií checkpointov - pošli deltaTime
for (let checkpoint of this.currentLevelData.checkpoints) {
this.checkpointAnimationManager.updateCheckpoint(checkpoint, clampedDeltaTime);
}
// Update animácie finish flag - pošli deltaTime
if (this.currentLevelData.endPoint) {
this.checkpointAnimationManager.updateCheckpoint(this.currentLevelData.endPoint, clampedDeltaTime);
}
// Aktualizácia pozície kamery
this.updateCamera();
}
/**
* Vykreslenie jedného tile-u zo sprite sheetu
* @param {string} tileName - Názov tile-u (napr. 'ground')
* @param {number} x - X pozícia kde vykresliť
* @param {number} y - Y pozícia kde vykresliť
* @param {number} width - Šírka výsledného tile-u
* @param {number} height - Výška výsledného tile-u
*/
drawTile(tileName, x, y, width, height) {
// Skontroluj, či je sprite sheet načítaný
if (!this.terrainSprite.loaded) {
// Ak ešte nie je načítaný, vykresli placeholder (farebný obdĺžnik)
this.ctx.fillStyle = '#8B4513'; // Hnedá farba
this.ctx.fillRect(x, y, width, height);
return;
}
// Získaj definíciu tile-u
const tile = this.tiles[tileName];
if (!tile) {
console.error(`Tile "${tileName}" neexistuje v definícii!`);
return;
}
// Vykresli tile zo sprite sheetu
// drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
// sx, sy = pozícia v sprite sheete
// sWidth, sHeight = veľkosť v sprite sheete
// dx, dy = pozícia na canvase
// dWidth, dHeight = veľkosť na canvase
this.ctx.drawImage(
this.terrainSprite, // Sprite sheet
tile.x, // X v sprite sheete
tile.y, // Y v sprite sheete
tile.width, // Šírka v sprite sheete
tile.height, // Výška v sprite sheete
x, // X na canvase
y, // Y na canvase
width, // Šírka na canvase
height // Výška na canvase
);
}
/**
* Vykreslenie veľkej plochy tile-om (opakovaním)
* Používa sa pre zem, dlhé platformy atď.
* @param {string} tileName - Názov tile-u
* @param {number} x - Začiatočná X pozícia
* @param {number} y - Začiatočná Y pozícia
* @param {number} width - Celková šírka plochy
* @param {number} height - Celková výška plochy
*/
drawTiledArea(tileName, x, y, width, height) {
const tile = this.tiles[tileName];
if (!tile) {
console.error(`Tile "${tileName}" neexistuje!`);
return;
}
// Vypočítaj, koľko tile-ov potrebujeme v X a Y smere
const tilesX = Math.ceil(width / tile.width);
const tilesY = Math.ceil(height / tile.height);
// Vykresli tile-y v mriežke s malým prekrytím (eliminuje čierne čiary)
for (let row = 0; row < tilesY; row++) {
for (let col = 0; col < tilesX; col++) {
// Zaokrúhli pozície na celé pixely (zabráni sub-pixel renderingu)
const tileX = Math.floor(x + (col * tile.width));
const tileY = Math.floor(y + (row * tile.height));
// Vypočítaj skutočnú veľkosť tile-u (posledné tile-y môžu byť orezané)
let tileWidth = tile.width;
let tileHeight = tile.height;
// Pre posledný tile v rade - orez šírku
if (col === tilesX - 1 && (x + width) < (tileX + tile.width)) {
tileWidth = Math.ceil(x + width - tileX);
}
// Pre posledný tile v stĺpci - orez výšku
if (row === tilesY - 1 && (y + height) < (tileY + tile.height)) {
tileHeight = Math.ceil(y + height - tileY);
}
// Pridaj 1px prekrytie okrem posledných tile-ov (eliminuje čierne čiary)
const drawWidth = (col < tilesX - 1) ? tileWidth + 1 : tileWidth;
const drawHeight = (row < tilesY - 1) ? tileHeight + 1 : tileHeight;
// Vykresli tile
this.drawTile(tileName, tileX, tileY, drawWidth, drawHeight);
}
}
}
/**
* Vykreslenie zeme - horizontálne opakuje tile, vertikálne natiahne
* @param {string} tileName - Názov tile-u
* @param {number} x - X pozícia
* @param {number} y - Y pozícia
* @param {number} width - Celková šírka
* @param {number} height - Celková výška (tile sa natiahne na túto výšku)
*/
drawGroundTerrain(tileName, x, y, width, height) {
// Skontroluj, či je sprite načítaný
if (!this.terrainSprite.loaded) {
this.ctx.fillStyle = '#8B4513';
this.ctx.fillRect(x, y, width, height);
return;
}
const tile = this.tiles[tileName];
if (!tile) {
console.error(`Tile "${tileName}" neexistuje!`);
return;
}
// Vypočítaj koľko tile-ov potrebujeme v ŠÍRKE (horizontálne opakujeme)
const tilesX = Math.ceil(width / tile.width);
// V ŠÍRKE opakujeme tile-y, v VÝŠKE jeden tile natiahneme
for (let col = 0; col < tilesX; col++) {
const tileX = Math.floor(x + (col * tile.width));
// Šírka tile-u (posledný môže byť orezaný)
let tileWidth = tile.width;
if (col === tilesX - 1 && (x + width) < (tileX + tile.width)) {
tileWidth = Math.ceil(x + width - tileX);
}
// Pridaj 1px prekrytie okrem posledného tile-u (eliminuje vertikálne čiary)
const drawWidth = (col < tilesX - 1) ? tileWidth + 1 : tileWidth;
// Vykresli tile - šírka sa opakuje, výška sa natiahne
this.ctx.drawImage(
this.terrainSprite, // Sprite sheet
tile.x, // X v sprite sheete
tile.y, // Y v sprite sheete
tile.width, // Šírka v sprite sheete (celá šírka tile-u)
tile.height, // Výška v sprite sheete (celá výška tile-u)
tileX, // X na canvase
y, // Y na canvase
drawWidth, // Šírka na canvase (opakuje sa)
height // Výška na canvase (NATIAHNE SA na celú výšku!)
);
}
}
/**
* Vykreslenie platformy - horizontálne opakuje tile, vertikálne natiahne
* @param {string} tileName - Názov platformy (platform1, platform2, platform3, platform4)
* @param {number} x - X pozícia
* @param {number} y - Y pozícia
* @param {number} width - Celková šírka
* @param {number} height - Celková výška
*/
drawPlatform(tileName, x, y, width, height) {
// Skontroluj, či je sprite načítaný
if (!this.terrainSprite.loaded) {
this.ctx.fillStyle = '#CD853F';
this.ctx.fillRect(x, y, width, height);
return;
}
const tile = this.tiles[tileName];
if (!tile) {
console.error(`Tile "${tileName}" neexistuje!`);
return;
}
// Vypočítaj koľko tile-ov potrebujeme v ŠÍRKE
const tilesX = Math.ceil(width / tile.width);
// V ŠÍRKE opakujeme tile-y, v VÝŠKE natiahnutie
for (let col = 0; col < tilesX; col++) {
const tileX = Math.floor(x + (col * tile.width));
// Šírka tile-u (posledný môže byť orezaný)
let tileWidth = tile.width;
if (col === tilesX - 1 && (x + width) < (tileX + tile.width)) {
tileWidth = Math.ceil(x + width - tileX);
}
// Pridaj 1px prekrytie okrem posledného tile-u
const drawWidth = (col < tilesX - 1) ? tileWidth + 1 : tileWidth;
// Vykresli tile
this.ctx.drawImage(
this.terrainSprite, // Sprite sheet
tile.x, // X v sprite sheete
tile.y, // Y v sprite sheete
tile.width, // Šírka v sprite sheete
tile.height, // Výška v sprite sheete
tileX, // X na canvase
y, // Y na canvase
drawWidth, // Šírka na canvase
height // Výška na canvase (natiahne sa)
);
}
}
draw() {
// Vyčistenie canvas
this.ctx.clearRect(0, 0, this.width, this.height);
// Uloženie kontextu pred transformáciou
this.ctx.save();
// Posun všetkého podľa pozície kamery
this.ctx.translate(-this.camera.x, 0);
// Vykreslenie pozadia (voliteľné)
this.ctx.fillStyle = '#87CEEB'; // svetlomodrá obloha
this.ctx.fillRect(this.camera.x, 0, this.width, this.height);
// Vykreslenie platforiem a zeme
this.ctx.imageSmoothingEnabled = false;
for (let platform of this.currentLevelData.platforms) {
if (platform.type === 'ground') {
// Automaticky predĺž zem až po spodný okraj canvasu
const groundHeight = this.height - platform.y; // Výška od platformy po spodok
// Zem - horizontálne opakuje tile, vertikálne natiahne
this.drawGroundTerrain('ground', platform.x, platform.y, platform.width, groundHeight);
// Pridaj čierny okraj okolo celej platformy zeme
this.ctx.strokeStyle = '#000000'; // Čierna farba
this.ctx.lineWidth = 1; // Hrúbka okraja (2px)
this.ctx.strokeRect(platform.x, platform.y, platform.width, groundHeight);
} else {
// Platformy - vykresli pomocou sprite sheetu
// Automatický výber typu platformy podľa veľkosti
let platformType = 'platform3'; // Default (30x30px)
if (platform.height <= 15) {
platformType = 'platform1'; // Tenká (13px výška)
} else if (platform.width <= 20) {
platformType = 'platform2'; // Malá štvorcová (14x14px)
} else if (platform.height >= 35) {
platformType = 'platform4'; // Vysoká (44px výška)
}
// Vykresli platformu
this.drawPlatform(platformType, platform.x, platform.y, platform.width, platform.height);
// Pridaj čierny okraj aj okolo platforiem
this.ctx.strokeStyle = '#000000'; // Čierna farba
this.ctx.lineWidth = 1;
this.ctx.strokeRect(platform.x, platform.y, platform.width, platform.height);
}
}
// Vykreslenie checkpointov
for (let checkpoint of this.currentLevelData.checkpoints) {
this.checkpointAnimationManager.draw(this.ctx, checkpoint);
}
// Vykreslenie špeciálnych blokov
for (let block of this.currentLevelData.specialBlocks) {
if (!block.hit) {
// Určenie typu diamantu podľa itemType
let diamondType = 'greenDiamond'; // Default
if (block.itemType === 'extraLife') {
diamondType = 'redDiamond';
} else if (block.itemType === 'powerup') {
diamondType = 'greenDiamond';
}
// Vykresli animovaný diamant
this.coinAnimationManager.draw(
this.ctx,
diamondType,
block.x,
block.y,
block.width
);
}
}
// Vykreslenie nepriateľov (animované sprite-y)
for (let enemy of this.currentLevelData.enemies) {
if (enemy.visible) { // ⬅️ Vykreslí všetkých viditeľných, vrátane dying
this.enemyAnimationManager.draw(this.ctx, enemy);
}
}
// Vykreslenie stien
this.ctx.fillStyle = '#666';
for (let wall of this.currentLevelData.walls) {
this.ctx.fillRect(wall.x, wall.y, wall.width, wall.height);
}
// Vykreslenie všetkých mincí (gold, silver)
for (let coin of this.currentLevelData.coins) {
if (!coin.collected) {
this.coinAnimationManager.draw(
this.ctx,
coin.getAnimationType(), // 'gold' alebo 'silver'
coin.x,
coin.y,
coin.width
);
}
}
// Vykreslenie všetkých diamantov (blue, green, red)
for (let diamond of this.currentLevelData.diamonds) {
if (!diamond.collected) {
this.coinAnimationManager.draw(
this.ctx,
diamond.getAnimationType(), // 'blueDiamond', 'greenDiamond', 'redDiamond'
diamond.x,
diamond.y,
diamond.width
);
}
}
// Vykreslenie hráča (animovaná postava)
if (this.isInvulnerable) {
// Efekt blikania pri nezraniteľnosti
this.ctx.globalAlpha = Math.sin(Date.now() / 100) > 0 ? 1 : 0.3;
}
// Vykresli sprite s offsetom relatívne k collision boxu
this.animationManager.draw(
this.ctx,
this.player.x + this.player.spriteOffsetX, // Použije offset z player objektu
this.player.y + this.player.spriteOffsetY,
this.player.spriteWidth, // Použije sprite rozmery
this.player.spriteHeight
);
// Reset alpha
this.ctx.globalAlpha = 1;
// Reset alpha
this.ctx.globalAlpha = 1;
// Vykreslenie cieľa (finish flag)
const endPoint = this.currentLevelData.endPoint;
// Priprav endPoint ako finish flag checkpoint
if (!endPoint.isFinish) {
endPoint.isFinish = true;
endPoint.animState = 'idle'; // Vždy viditeľný
endPoint.animFrame = 0;
endPoint.animCounter = 0;
}
// Vykresli finish flag pomocou checkpoint animation managera
this.checkpointAnimationManager.draw(this.ctx, endPoint);
// Debug informácie
if (this.debug) {
this.drawDebugInfo();
}
// Obnovenie kontextu
this.ctx.restore();
// Vykreslenie UI elementov (ak nejaké máme)
this.drawUI();
}
drawUI() {
// Životy a skóre
this.ctx.fillStyle = 'white';
this.ctx.font = '20px Arial';
this.ctx.fillText(`Mince: ${this.collectedCoins}/${this.totalCoins}`, 10, 30);
this.ctx.fillText(`Diamanty: ${this.collectedDiamonds}/${this.requiredDiamonds}`, 10, 60);
this.ctx.fillText(`Životy: ${this.lives}`, 10, 90);
if (this.debug) {
// Debug panel v pravom hornom rohu
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
this.ctx.fillRect(this.width - 200, 0, 200, 100);
this.ctx.fillStyle = 'white';
this.ctx.font = '12px Arial';
this.ctx.fillText(`FPS: ${Math.round(this.fps || 0)}`, this.width - 190, 20);
this.ctx.fillText(`Level Width: ${this.currentLevelData.width}`, this.width - 190, 40);
this.ctx.fillText(`Game State: ${this.gameState}`, this.width - 190, 60);
this.ctx.fillText(`Level: ${this.currentLevel}`, this.width - 190, 80);
}
}
/**
* Hlavný herný loop - volá update a draw každý frame
* @param {number} timestamp - Čas od spustenia stránky v milisekundách
*/
gameLoop(timestamp) {
// Výpočet FPS (len pre debug)
if (this.lastTime) {
this.fps = 1000 / (timestamp - this.lastTime);
}
this.lastTime = timestamp;
// ✅ OPRAVENÉ: Posielame timestamp do update()
this.update(timestamp);
this.draw();
// Požiadaj prehliadač o ďalší frame
requestAnimationFrame((ts) => this.gameLoop(ts));
}
}
/**
* COIN CLASS - Reprezentuje odmeny v hre
* Typy: gold, silver, blueDiamond, greenDiamond, redDiamond
*/
class Coin {
constructor(x, y, type = 'gold') {
this.x = x;
this.y = y;
this.width = 40; // Väčšie pre lepšiu viditeľnosť
this.height = 40;
this.type = type; // 'gold', 'silver', 'blueDiamond', 'greenDiamond', 'redDiamond'
this.collected = false;
// Rečové cvičenie (len pre Blue Diamond)
this.speechExercise = type === 'blueDiamond' ? {
word: this.getRandomWord(),
imageUrl: this.getRandomImage(),
attempts: 0,
completed: false
} : null;
// Posluchové cvičenie (len pre Silver Coin)
this.listeningExercise = type === 'silver' ? {
completed: false,
attempts: 0
} : null;
// Bonusové predmety (Green/Red Diamond)
this.bonusType = null;
if (type === 'greenDiamond') {
this.bonusType = 'powerup';
} else if (type === 'redDiamond') {
this.bonusType = 'extraLife';
}
}
/**
* Získanie náhodného slova pre rečové cvičenie
*/
getRandomWord() {
const words = ['pes', 'mačka', 'auto', 'dom', 'strom', 'slnko', 'voda', 'ruka'];
return words[Math.floor(Math.random() * words.length)];
}
/**
* Získanie náhodného obrázka pre rečové cvičenie
*/
getRandomImage() {
return `images/${this.getRandomWord()}.png`;
}
/**
* Získanie animačného typu pre CoinAnimationManager
*/
getAnimationType() {
return this.type; // 'gold', 'silver', 'blueDiamond', 'greenDiamond', 'redDiamond'
}
}
// Spustenie hry
window.onload = () => {
new Game();
};