4. Fázis · Játékok
4. Fázis · 1. Lecke

🐸 Froggy Rush — Pályagenerálás

Hogyan helyezzük el a tojásokat, köveket és sirályokat igazságosan a teljes pályán? Az evenSpread() függvény megoldja — nem véletlenszerűen, hanem egyenletesen elosztva.

⏱ 60 perc
📁 spawnObjects
🎯 evenSpread · world-space spawn

1A probléma: miért nem Math.random()?

🐸 Froggy Rush

Ha teljesen véletlenszerűen rakjuk le az objektumokat, könnyen az lesz hogy az első 100 méteren 20 tojás van, az utolsó 400 méteren pedig semmi. Az evenSpread() ezt oldja meg.

froggy_rush.html — spawnObjects()JavaScript
function spawnObjects(level) {
  const objs = [];
  const span = LEVEL_DIST * 8; // teljes pálya px-ben

  // evenSpread: egyenletes elosztás a pályán
  function evenSpread(count, S_start, typeObj) {
    const S_end = span - 200;         // utolsó 200px = célterület
    const gap  = (S_end - S_start) / (count - 1);
    for (let i = 0; i < count; i++) {
      // Kis véletlenszerűség a gapon belül (nem pontosan egyenletes)
      const S = S_start + i*gap + (Math.random()-.5)*gap*.5;
      // World Y: a játékos ekkor éri el (screen_y = o.y + scrollY = PLAYER_Y)
      const y = PLAYER_Y - S;
      objs.push({ ...typeObj, y });
    }
  }

  evenSpread(20+level*4, 150, { type:'egg',   r:9  });
  evenSpread(lv.rocks*3,  250, { type:'rock',  r:13 });
  evenSpread(lv.gulls*3,  300, { type:'gull',  r:15 });

  // X pozíció: véletlenszerű (pálya szélességén belül)
  objs.forEach(o => { o.x = TL + 14 + Math.random()*(TW-28); });
  return objs;
}
A kulcsképlet: y = PLAYER_Y - S

Az objektum akkor éri el a játékost, amikor o.y + scrollY = PLAYER_Y. Tehát ha azt akarjuk, hogy scrollY=S értéknél érje el a játékos: o.y = PLAYER_Y - S. Ez a world-space spawn lényege.

Miért S_start paraméter?

Különböző akadályok különböző távolságból jelennek meg — a tojások hamarabb (150px után), a kövek kicsit később (250px), a sirályok még később (300px). Így az elején könnyebb, a végén nehezebb.

TípusS_startDarab (1. szint)Miért?
🥚 Tojás15024Hamar jutalmat kap a játékos
🪨 Kő25012Kicsit felkészülési idő kell
🦅 Sirály3006A legnehezebb veszély legkésőbb jelenik meg
⚡ Boost5005Csak a pálya második felén
✏️ Feladat

Nyisd meg a froggy_rush.html-t VS Code-ban:

  • Keresd meg a spawnObjects függvényt (Ctrl+F)
  • Figyeld meg az evenSpread hívásokat — hány tojás jelenik meg 2. szinten?
  • Módosítsd a tojások S_start értékét 150-ről 50-re — mi változik? (hamarabb jön az első tojás)
  • Adj hozzá még egy evenSpread hívást: 3 "energia" tárgyat 400-as S_start-tal

🧠 Miért y = PLAYER_Y - S a spawn képlet?

Mert a canvas Y tengelye fordított
Mert az objektum akkor éri el a játékost amikor scrollY=S — és screen_y = o.y + scrollY = PLAYER_Y → o.y = PLAYER_Y - S
Mert az objektumok felülről lefelé mozognak
Véletlenszerű — bármi működne
4. Fázis · 2. Lecke

🐸 Froggy Rush — Felszínek és sebesség

A SURFACES objektum tartalmaz mindent a fű, homok, jég, sár és aszfalt felszínekről — sebességüktől a csúszásig. Egy helyen, könnyen módosítható.

⏱ 45 perc
🎯 SURFACES · strategy pattern

1A SURFACES objektum — strategy pattern

🐸 Froggy Rush
turtle_race.html — SURFACES objektumJavaScript
// Minden felszín tulajdonságai egy helyen
const SURFACES = {
  grass: { name:'🌿 Fű',    col:'#4aaa22', spd:1.00, slip:0.0 },
  sand:  { name:'🏖️ Homok', col:'#e8c84a', spd:0.58, slip:0.0 },
  ice:   { name:'❄️ Jég',   col:'#b8e8ff', spd:1.22, slip:0.88 },
  mud:   { name:'💧 Sár',   col:'#8B5a30', spd:0.40, slip:0.0 },
  road:  { name:'🛤️ Út',   col:'#777',    spd:1.38, slip:0.0 },
};

// Felhasználás: a sebesség a felszíntől függ
const surf = SURFACES[getSurface(dist, level)];
const scrollSpd = BASE_SPD * surf.spd * lv.spdMul;

// Csúszás jégen: vx nem csökken gyorsan
if (!K.left && !K.right) {
  p.vx *= (surf.slip > 0 ? surf.slip : 0.74);
  // Jégen slip=0.88 → lassan lassul (csúszik)
  // Fűn slip=0   → 0.74-gyel szorozva gyorsan megáll
}
Miért jobb ez mint sok if-else?

Ha külön if (surface === 'ice') spd *= 1.22 stb. lenne, 5 felszínre 10+ feltétel kellene. A SURFACES objektummal csak az adatot kell megváltoztatni — a logika ugyanaz marad. Ezt hívják strategy pattern-nek.

getSurface() — melyik felszínen vagyunk?

turtle_race.htmlJavaScript
function getSurface(dist, level) {
  const lv   = LEVELS[level - 1];
  const segH = LEVEL_DIST / lv.segs.length;  // szegmens hossza
  const idx  = Math.min(
    Math.floor(dist / segH),
    lv.segs.length - 1
  );
  return lv.segs[idx]; // pl. 'mud'
}
// pl. level 2 = ['grass','mud','grass','mud','grass']
// dist=180m → szegmens=120m → idx=1 → 'mud'
✏️ Feladat

Kísérletezz a felszínekkel:

  • Nyisd meg a turtle_race.html-t, keresd meg a SURFACES objektumot
  • Változtasd meg a sár (mud) sebességét 0.40-ről 0.10-re — milyen hatása van?
  • Adj hozzá egy új felszínt: 'lava': { spd: 1.8, slip: 0.0 }
  • Adj hozzá egy "lava" szegmenst az 5. pálya (Vulkán) tömbébe

🧠 Mi a slip tulajdonság szerepe a SURFACES-ben?

A felszín csúszósságát jelenti a rajzolásnál
Ha > 0, a vx ilyen arányban marad meg gombfelengedéskor — nagy értéknél a teknős hosszan csúszik
A scroll sebességet módosítja
Csak vizuális effekt, a mozgásra nincs hatása
4. Fázis · 3. Lecke

🐸 Froggy Rush — Ugrás fizikája

A béka parabolás ugrása sin() függvénnyel valósul meg. Pontosan 22 frame alatt felugrik és visszaér — ez tökéletes ívet ad nehéz fizika számítás nélkül.

⏱ 45 perc
🎯 jumpT · jumpH · sin() parabolaív

1A teljes ugrás kódja

🐸 Froggy Rush
froggy_rush.html — updatePlayer()JavaScript
// 1. Ugrás INDÍTÁSA — billentyű lenyomásakor
if (K.up && !p.jumping && !p.inShell) {
  p.jumping = true;
  p.jumpT   = 0;     // idő számláló nullára
}

// 2. Ugrás ANIMÁLÁSA — minden frame-ben
if (p.jumping) {
  p.jumpT++;
  // sin(0)=0 → sin(π/2)=1 (csúcs) → sin(π)=0 (le)
  p.jumpH = Math.sin(p.jumpT / 22 * Math.PI) * 38;
  //                 ↑ 22 frame alatt tér vissza   ↑ 38px magas

  if (p.jumpT >= 22) {  // 22 frame eltelt
    p.jumping = false;
    p.jumpH   = 0;
  }
}

// 3. Rajzoláskor: jumpH-val feljebb kerül a béka
ctx.translate(p.x, PLAYER_Y - p.jumpH);

// 4. Ütközésnél: ugrás közben a béka "feljebb" van
const frogScreenY = PLAYER_Y - p.jumpH;
const d = Math.hypot(p.x - rock.x, frogScreenY - sy);
Miért 22 frame?

22 frame ≈ 0.37 másodperc 60fps-nél. Ez elég ahhoz, hogy vizuálisan szép ívnek látsszon, de elég rövid ahogy ne legyen frusztráló. Megváltoztatható: nagyobb szám = lassabb ugrás, kisebb = gyorsabb.

A ugrás hatása az ütközésre

Kőnél az ütközés csak akkor aktív, ha NEM ugrik. Sirálynál viszont ugrás sem véd — csak a páncél:

froggy_rush.html — checkCollisions()JavaScript
// Kő: !p.jumping szükséges → ugrással elkerülhető
if (o.type === 'rock' && !p.jumping && pd < o.r+11) {
  loseLife(p);
}

// Sirály: !p.inShell kell → csak páncéllal kerülhető el
if (o.type === 'gull' && !p.inShell && pd < o.r+12) {
  loseLife(p);
}
✏️ Feladat
  • Keresd meg a jumpH és jumpT változókat a froggy_rush.html-ben
  • Változtasd meg 22-t 40-re — hogyan változik az ugrás?
  • Változtasd meg 38-at 70-re — mi az eredmény?
  • Bónusz: adj hozzá dupla ugrást — ha ugrik és újra megnyomják, ugrik még egyszer

🧠 Miért ideális a sin() a parabolás ugráshoz?

Mert a sin() gyorsabb mint más számítás
Mert sin(0)=0, sin(π/2)=1, sin(π)=0 — pontosan azt az ívet írja le amit ugráskor látni szeretnénk: indul, eléri a csúcsot, visszatér
Mert a canvas csak sin()-t ért el
Nincs különbség, bármi más is működne
4. Fázis · 4. Lecke

🐸 Froggy Rush — State Machine

A játék nem mindig "játékban van" — hol halott a béka, hol szintet lép, hol vége van. Az állapotgép kezeli, hogy mikor mi történhet és mikor mi nem.

⏱ 60 perc
🎯 phase · state machine · phTimer

1Az állapotok és átmenetek

🐸 Froggy Rush
froggy_rush.html — állapotokJavaScript
// G.phase lehetséges értékei:
// 'play'    — normál játék folyik
// 'dead'    — béka halott, animáció lejátszódik
// 'levelup' — szint teljesítve, átmenet
// 'gameover'— nincs több élet

// Update-ben: csak a megfelelő állapot fut
function update() {
  G.frame++;
  if (G.phase === 'play')     updatePlay();
  if (G.phase === 'dead')     updateDead();
  if (G.phase === 'levelup')  updateLevelUp();
}

// Halál — állapot váltás
function dieFrog() {
  if (G.frog.dead) return; // már halott → ne fusson le kétszer
  G.frog.dead = true;
  G.phase     = 'dead';     // átváltás dead állapotba
  G.phTimer   = 80;         // 80 frame-ig marad dead-ben
  G.lives--;
}

// Dead állapot: visszaszámol majd visszaáll
function updateDead() {
  G.phTimer--;
  if (G.phTimer <= 0) {
    if (G.lives <= 0) {
      G.phase = 'gameover';
    } else {
      G.phase = 'play';    // visszaáll játékba
      resetFrog();         // béka kezdőpozícióba
    }
  }
}
Miért hasznos a State Machine?

Nélküle a kód tele lenne ilyen feltételekkel: if (!dead && !levelup && !gameover) minden egyes helyen. Az állapotgéppel minden feltétel egy helyen van, és az update() automatikusan a megfelelő logikát futtatja.

✏️ Feladat
  • Keresd meg a G.phase összes előfordulását a froggy_rush.html-ben (Ctrl+F)
  • Rajzolj papírra egy állapot-diagramot: körök az állapotoknak, nyilak az átmeneteknek
  • Adj hozzá egy új "pause" állapotot: szóközre megáll, újra szóközre folytatódik
  • Bónusz: írj egy saját mini state machine-t: traffic light (piros→sárga→zöld→piros)

🧠 Mi a phTimer szerepe a dead állapotban?

Azt méri mennyi pontot veszít a játékos
Visszaszámol: N frame-ig marad a dead állapotban, utána automatikusan vált play-re (ha van élet)
A halál animáció sebességét szabályozza
Csak a froggy rush-ban van, máshol nem használatos
4. Fázis · 5. Lecke

🐸 Froggy Rush — Nehézségi szintek

Gyerek, Lassú, Normal, Gyors — egyetlen DIFF változóval az egész játék viselkedése megváltozik. A DIFF_CFG objektum tartalmazza az összes különbséget.

⏱ 45 perc
🎯 DIFF_CFG · konfigurációs objektum

1A DIFF_CFG konfigurációs objektum

🐸 Froggy Rush
froggy_rush.htmlJavaScript
// Nehézségi szintek konfigurációja
const DIFF_CFG = [
  { name:'🐌 Gyerek', carSpd:0.45, logSpd:0.45, logGap:0.7 },
  { name:'🐢 Lassú',  carSpd:0.75, logSpd:0.75, logGap:0.85 },
  { name:'⚔️ Normal', carSpd:1.0,  logSpd:1.0,  logGap:1.0 },
  { name:'💀 Gyors',  carSpd:1.6,  logSpd:1.5,  logGap:1.2 },
];

// Kiválasztott nehézség indexe (0-3)
let DIFF = 2; // Normal az alapértelmezett

// Felhasználás — csak a konfigot kell kiolvasni:
function makeCars(level, diff) {
  const dc = DIFF_CFG[diff];
  const spdMul = dc.carSpd * (1 + level * .08);
  // spdMul = alap nehézség * szint-szorzó
  // Gyerek, 1. szint: 0.45 * 1.0 = 0.45
  // Gyors, 5. szint:  1.6  * 1.4 = 2.24
}
A konfig objektum előnye

Ha minden nehézségnél külön if-else lenne, a kódban 20+ helyen kellene módosítani ha változtatni akarsz. A DIFF_CFG-vel egyetlen sorban változtatod meg bármelyik nehézség bármelyik értékét, és az egész játékban érvényesül.

✏️ Feladat
  • Keresd meg a DIFF_CFG-t a froggy_rush.html-ben — hány helyen hivatkoznak rá?
  • Adj hozzá egy 5. nehézségi szintet: '💥 Lehetetlen' — carSpd: 2.5, logSpd: 2.2
  • A gombok közé adj hozzá egy 5. gombot és kösd össze a DIFF = 4 értékkel
  • Figyeld meg mi változik a játékban — arányos a változás?

🧠 Ha carSpd = 1.6 és level = 3, mennyi lesz a spdMul értéke?

1.6
1.84
1.984  (1.6 × (1 + 3×0.08) = 1.6 × 1.24)
2.24
4. Fázis · 6. Lecke

🐢 Teknős Verseny — AI ellenfél

Az AI teknős kerüli a sirályokat, gyűjti a tojásokat, és nem mindig tökéletes — egy kis véletlenszerűséggel emberibbnek érzi magát a játékos.

⏱ 75 perc
🎯 AI célkeresés · aiTimer · aiErr

1Az AI döntéshozatal

🐢 Teknős Verseny
turtle_race.html — updateAI()JavaScript
function updateAI() {
  // Nem dönt minden frame-ben — csak 25-65 frame-enként
  a.aiTimer--;
  if (a.aiTimer <= 0) {
    a.aiTimer = 25 + Math.floor(Math.random() * 40);

    let best = W/2, bestSc = -Infinity;

    // Minden objektumot értékel
    G.objects.forEach(o => {
      if (o.done) return;
      const sy = o.y + G.scrollY;
      if (sy < AI_Y+100 && sy > AI_Y-60) {
        if (o.type === 'egg' || o.type === 'boost') {
          const sc = 90 - Math.abs(o.x - a.x); // közelebb = jobb
          if (sc > bestSc) { bestSc = sc; best = o.x; }
        }
        if (o.type === 'gull') {
          if (Math.abs(o.x - a.x) < 55) best = a.x + (a.x < W/2 ? 55 : -55);
        }
      }
    });

    // aiErr: kis hiba → nem tökéletes az AI
    a.aiTargetX = best + a.aiErr * TW * .4;
  }

  // Simán mozog a célpont felé
  const diff = a.aiTargetX - a.x;
  a.vx += Math.sign(diff) * Math.min(Math.abs(diff)*.09, .85);
}
Az aiErr trükk

Az aiErr = (Math.random()-.5) * .3 értéket egyszer kapja az AI és nem változik. Ez azt jelenti az AI szisztematikusan kicsit jobbra vagy balra "téved" — emberibbnek tűnik mintha minden frame-ben véletlen hibát csinálna.

AI ugrás — követ ha kő van előtte

turtle_race.htmlJavaScript
// AI automatikusan ugrik ha kő van közel előtte
if (!a.jumping) {
  G.objects.forEach(o => {
    if (o.type !== 'rock' || o.done) return;
    const sy = o.y + G.scrollY;
    if (sy > AI_Y-60 && sy < AI_Y+20
        && Math.abs(o.x - a.x) < 35) {
      a.jumping = true; a.jumpT = 0;
    }
  });
}
✏️ Feladat
  • Keresd meg az updateAI függvényt — figyeld meg az aiTimer értékét
  • Tedd az AI-t "okosabbá": csökkentsd az aiTimer minimum értékét 25-ről 5-re
  • Tedd "butábbá": növeld az aiErr-t 0.3-ról 1.0-ra
  • Bónusz: implementálj egy egyszerű AI-t: egy téglalapot, ami követi az egér X pozícióját simán

🧠 Miért nem dönt az AI minden frame-ben?

Mert az lassítaná a játékot
Mert az emberi döntéshozatal sem azonnali — a késleltetett döntés természetesebben hat, és a véletlenszerű időköz kiszámíthatatlanabbá teszi
Technikai korlát: a canvas nem bírja
Az AI mindig dönt, csak nem mindig cselekszik
4. Fázis · 7. Lecke

🐢 Teknős Verseny — Energia rendszer

Az energia a játék másodlagos erőforrása — sárban gyorsan fogy, úton töltődik, vízcsomag feltölti. Ha elfogy, élet vész. Ez ad döntési kényszert a játékosnak.

⏱ 45 perc
🎯 drainMap · fillMap · resource management

1Az energia fogyás és töltés logikája

🐢 Teknős Verseny
turtle_race.html — updatePlayer()JavaScript
// Fogyás/töltés mértéke felszínenként (per frame)
const drainMap = { grass:0.04, sand:0.08, ice:0.05, mud:0.08, road:0 };
const fillMap  = { grass:0,    sand:0,    ice:0,    mud:0,    road:0.06 };

// Melyik felszínen vagyunk?
const surfName = getSurface(G.totalDist, G.level);

// Energia frissítés: fogyás - töltés, 0 és 100 között
p.energy = Math.max(0, Math.min(100,
  p.energy - drainMap[surfName] + fillMap[surfName]
));

// Ha elfogy → élet vész, energia visszaáll 100-ra
if (p.energy <= 0) {
  p.energy = 100;
  loseLife(p);
  addFloat(p.x, PLAYER_Y-40, '⚡ Energia kimerült!', '#e03030');
}

// Vízcsomag felvétel
if (o.type === 'energy' && pd < o.r+13) {
  o.done = true;
  if (p.energy >= 100) {
    G.score += 25 * G.level; // teli → bónusz pont
  } else {
    p.energy = Math.min(100, p.energy + 40); // tölt
  }
}
Miért érdekes ez game design szempontból?

Az energia döntési kényszert teremt: a játékos mérlegeli, hogy kerülje a sarat (energia miatt lassabb is!), vagy inkább a rövidebb utat válassza. A vízcsomag bónuszpontot is adhat ha teli az energia — ez is döntés.

✏️ Feladat
  • Keresd meg a drainMap-et a turtle_race.html-ben
  • Módosítsd a mud drain értékét 0.08-ról 0.25-re — sokkal nehezebb lesz a 2. pálya
  • Adj hozzá energiát a sand felszínre is: fillMap.sand = 0.03
  • Bónusz: egy HP sávot rajzolj a canvasra ami az energia értékét mutatja (gradienssel: zöld→sárga→piros)

🧠 Mi történik ha a játékos energia = 100 és felvesz egy vízcsomag?

Az energia 140-re emelkedik
Semmi, a csomag eltűnik de semmi hatása nincs
Bónusz pontot kap (25 × szint) de az energia marad 100-on
Élete nő eggyel
4. Fázis · 8. Lecke

🐢 Teknős Verseny — World-space koordináták

A tojások, kövek, sirályok fix helyen vannak a pályán — a kamera mozog, nem ők. Ez a world-space vs screen-space megkülönböztetés kulcsa.

⏱ 45 perc
🎯 screen_y = o.y + scrollY

1A teljes koordináta rendszer

🐢 Teknős Verseny
turtle_race.htmlJavaScript
// SPAWN: objektum world Y kiszámítása
// Akkor éri el a játékost amikor scrollY = S
// screen_y = o.y + scrollY = PLAYER_Y
// → o.y = PLAYER_Y - S
const y = PLAYER_Y - S;  // negatív szám (képernyő felett)

// UPDATE: csak scrollY nő — objektumok NEM mozognak!
G.scrollY += scrollSpd;  // ez a "haladás"
G.totalDist = G.scrollY / 8; // méterré konvertálva

// DRAW: screen pozíció kiszámítása
G.objects.forEach(o => {
  const sy = o.y + G.scrollY; // screen_y
  if (sy < -40 || sy > H+40) return; // képernyőn kívül: skip
  drawEgg(o.x, sy);  // screen koordinátán rajzolódik
});

// ÜTKÖZÉS: screen_y-t kell összehasonlítani
const d = Math.hypot(p.x - o.x, PLAYER_Y - sy);
A leggyakoribb hiba

Ha az objektum mozgásakor o.y += scrollSpd-t is írsz, kétszer mozdul el — egyszer a scrollY, egyszer saját maga. Ezt a hibát csináltuk mi is az egyik korábbi verzióban! Az objektumok world-space-ben fixek, csak a scrollY változik.

✏️ Feladat
  • Nyomj F12-t a turtle_race.html-ben, a Console-ba írd: G.scrollY — mi a jelenlegi érték?
  • Írd ki az első tojás world és screen pozícióját: G.objects[0].y és G.objects[0].y + G.scrollY
  • Figyeld meg ahogy scrollY nő és a screen_y változik — melyik éri el a PLAYER_Y-t?

🧠 Ha egy tojás world Y-ja -800 és scrollY = 600, hol látszik a képernyőn?

Y = -800 (képernyő felett)
screen_y = -800 + 600 = -200 (még éppen a képernyő felett, hamarosan belép)
Y = 800 - 600 = 200
Nem számítható ki scrollY nélkül
4. Fázis · 9. Lecke

🐢 Teknős Verseny — Karakter rajzolása rétegekbe

A teknős nem egy kép — vonalakból, ellipszisekből és körökből épül fel, rétegről rétegre. A sorrend számít: előbb az árnyék, aztán a lábak, majd a páncél, végül a fej.

⏱ 75 perc
🎯 Rétegelt rajzolás · save/translate/restore

1A drawTurtle() felépítése rétegekbe

🐢 Teknős Verseny
turtle_race.html — drawTurtle()JavaScript
function drawTurtle(x, y, t, frame) {
  if (t.stunned > 0 && Math.floor(frame/5) % 2) return; // villog
  const jy = t.jumpH || 0;

  ctx.save();
  ctx.translate(x, y - jy);           // origó a teknős közepébe
  ctx.rotate(tilt);                    // vx alapján dőlés

  // 1. RÉTEG: árnyék (mindig lejjebb)
  ctx.fillStyle = 'rgba(0,0,0,.18)';
  ctx.ellipse(2, 18, 14, 4, 0, 0, Math.PI*2);

  // 2. RÉTEG: lábak (páncél alá kerülnek)
  drawLegs(walk);

  // 3. RÉTEG: páncél (a lábak felett)
  drawShell(t.inShell);

  // 4. RÉTEG: fej (páncél felett)
  if (!t.inShell) drawHead(walk);

  ctx.restore();

  // 5. RÉTEG: felirat (külön save/restore)
  ctx.save();
  ctx.translate(x, y - jy);
  ctx.fillText(t.isAI ? '🤖 AI' : '🐢 Te', 0, -28);
  ctx.restore();
}

A páncél rajzolása — radiális gradienssel

turtle_race.html — drawShell()JavaScript
// Páncél: radiális gradiens + hex minta
const g2 = ctx.createRadialGradient(-3,-4,2, 0,1,14);
g2.addColorStop(0, '#3a9a28'); // belül világos
g2.addColorStop(1, '#1a5a10'); // kívül sötét
ctx.fillStyle = g2;
ctx.beginPath();
ctx.ellipse(0, 1, 14, 11, 0, 0, Math.PI*2);
ctx.fill();

// Hex minta vonalakkal
ctx.strokeStyle = 'rgba(0,0,0,.22)';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.ellipse(0,1,8,6,0,0,Math.PI*2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0,-8); ctx.lineTo(0,10);  ctx.stroke();
ctx.beginPath(); ctx.moveTo(-14,1); ctx.lineTo(14,1); ctx.stroke();
A villogó sebzés effekt

if (t.stunned > 0 && Math.floor(frame/5) % 2) return; — ha stunned és a frame/5 páratlan, nem rajzoljuk ki a teknőst. Minden 5. frame-ben felváltva látható/láthatatlan = villog.

✏️ Feladat
  • Keresd meg a drawTurtle függvényt — számold meg hány ctx.save/restore pár van benne
  • Rajzolj egy saját karaktert rétegekbe: árnyék → test → ruha → fej → szem sorrenden
  • Add hozzá a villogást: sebzés után 60 frame-ig villogjon
  • Bónusz: változtasd meg az AI teknős színét pirosra (az isAI alapján más col változó)

🧠 Miért kell a lábakat a páncél ELŐTT rajzolni?

Nincs különbség, a sorrend nem számít
Mert a canvas rétegszerűen épül fel: ami később rajzolódik, az felül van. Ha a páncél után rajzolnánk a lábakat, a lábak a páncélon kívül jelennének meg
Mert az ellipszis mérete miatt a páncél nem fed mindent
A lábak átlátszók ezért akárhogy jó
4. Fázis · 10. Lecke

🏰 Castle Siege — Tower Defense architektúra

A Castle Siege a legkomplexebb játékunk. A G objektumban él az összes játékelem — hős, kastély, tornyok, ellenségek, lövedékek, arany. Minden egy helyen.

⏱ 75 perc
🎯 G állapot · initLevel · komponens struktúra

1A G játékállapot objektum

🏰 Castle Siege
castle_siege.html — initLevel()JavaScript
function initLevel(lvNum) {
  G = {
    // Játékelemek
    hero:    { x:155, y:300, hp:100, maxHp:100, spd:2, vx:0, vy:0 },
    castle:  { hp:200, maxHp:200, x:0, w:80 },
    towers:  [],    // épített tornyok tömbje
    enemies: [],    // aktív ellenségek
    hProj:   [],    // hős lövedékei
    tProj:   [],    // torony lövedékek
    floats:  [],    // lebegő szövegek

    // Erőforrások
    gold:    50,
    score:   0,

    // Pálya állapot
    level:   lvNum,
    wave:    0,
    phase:   'play',
    frame:   0,
  };
}

A 4 fő rendszer és kapcsolatuk

RendszerBemeneteKimeneteFüggvény
Hős mozgásK objektum (billentyűk)hero.x, hero.y változikmoveHero()
EllenségekPályaadatok, hullám számEllenségek mozognak, sebzik a kastélytmoveEnemies()
TornyokEllenségek pozíciójaLövedékek spawolódnaktowerAI()
ÜtközésLövedékek + ellenségekHP csökkentés, arany szerzéscheckCollisions()
castle_siege.html — update()JavaScript
function update() {
  if (!G || G.phase !== 'play') return;
  G.frame++;

  // Sorrendben fut le minden frame-ben:
  moveHero(G.hero);       // 1. Hős mozog
  spawnWave();            // 2. Új ellenségek jönnek?
  moveEnemies();          // 3. Ellenségek mozognak
  towerAI();              // 4. Tornyok lőnek
  moveProjectiles();      // 5. Lövedékek mozognak
  checkCollisions();      // 6. Ütközések vizsgálata
  checkWinLose();         // 7. Nyert/veszített?
}
✏️ Feladat
  • Nyisd meg a castle_siege.html F12 → Console, írd be: G.hero — látod a hős adatait?
  • Próbáld: G.castle.hp = 5 — mi történik?
  • Próbáld: G.gold = 9999 — tudod-e felvenni a legtöbb tornyot?
  • Keresd meg az update() függvényt — milyen sorrendben hívja a részeket?

🧠 Miért jó hogy az összes játékadat a G objektumban van?

Mert a globális változók gyorsabbak
Mert könnyen menthető/betölthető (JSON.stringify(G)), átlátható struktúra, és egyszerű debugolni a konzolból
Mert JavaScript csak egy objektumot kezel egyszerre
Teljesítmény okokból
4. Fázis · 11. Lecke

🏰 Castle Siege — Dinamikus pályagenerálás

50 különböző pálya egyetlen genLevel() függvényből. A szintszám alapján matematikával számítja ki az ellenségek számát, erősségét és az arany jutalmát.

⏱ 60 perc
🎯 genLevel · procedurális generálás · scaling

1A genLevel() függvény logikája

🏰 Castle Siege
castle_siege.html — genLevel()JavaScript
function genLevel(lvNum) {
  // Hullámok száma: 5-től 10-ig nő
  const waves = 5 + Math.floor(lvNum / 5);

  // Ellenség típusok — magasabb szinten több és erősebb
  const ENEMY_TYPES = [
    { hp: 20+lvNum*4,  spd: 0.8+lvNum*0.02, gold: 5,  col: '#c83030' },
    { hp: 60+lvNum*8,  spd: 0.5+lvNum*0.01, gold: 12, col: '#8030c8' },
    { hp: 150+lvNum*15, spd: 0.35,            gold: 25, col: '#303080' },
  ];

  // Hullámok generálása
  const waveData = [];
  for (let w = 0; w < waves; w++) {
    // Ellenség szám: szinttel és hullámmal nő
    const count = 3 + Math.floor(lvNum/2) + Math.floor(w/2);
    // Típus: magasabb hullámban erősebb típus is megjelenhet
    const maxType = Math.min(2, Math.floor(lvNum/10) + Math.floor(w/3));
    waveData.push({ count, maxType });
  }
  return { waves, waveData, ENEMY_TYPES };
}
A procedurális generálás előnye

Ha kézzel kellene mind az 50 pályát definiálni, az rengeteg munka lenne és nehezen módosítható. A matematika alapú generálással egyetlen képlet változtatásával az összes pálya automatikusan nehezedik.

✏️ Feladat
  • Keresd meg a genLevel függvényt — hány ellenség jelenik meg az 1. hullámban az 1. szinten?
  • Számold ki: 50. szinten hány ellenség van az 1. hullámban?
  • Módosítsd: 10. szinttől jelenjen meg egy 4. ellenség típus (boss): HP = lvNum*30, gold = 50
  • Bónusz: vizualizáld egy grafikonon (canvason) hogyan skálázódik az ellenség HP 1-50. szintig

🧠 Hány ellenség van a 10. szint 3. hullámában? (count = 3 + floor(lvNum/2) + floor(w/2))

8
9  (3 + floor(10/2) + floor(2/2) = 3 + 5 + 1 = 9)
10
7
4. Fázis · 12. Lecke

🏰 Castle Siege — Torony AI és lövedékek

Minden torony megkeresi a legközelebbi ellenséget a hatótávolán belül, célba veszi és lövedéket indít. A lövedék előre jelzi hova kell lőni — nem oda ahol az ellenség van, hanem ahol lesz.

⏱ 75 perc
🎯 towerAI · célzás · lövedék mozgás

1A torony AI logikája

🏰 Castle Siege
castle_siege.html — towerAI()JavaScript
function towerAI() {
  G.towers.forEach(tower => {
    // Töltési idő (cooldown) csökken
    if (tower.cooldown > 0) { tower.cooldown -= DT; return; }

    // Legközelebbi ellenség keresése hatótávon belül
    let best = null, bestDist = tower.range;
    G.enemies.forEach(e => {
      const d = Math.hypot(e.x - tower.x, e.y - tower.y);
      if (d < bestDist) { bestDist = d; best = e; }
    });

    if (!best) return; // nincs célpont

    // Lövedék irány kiszámítása
    const angle = Math.atan2(best.y - tower.y, best.x - tower.x);
    G.tProj.push({
      x: tower.x, y: tower.y,
      vx: Math.cos(angle) * tower.projSpd,
      vy: Math.sin(angle) * tower.projSpd,
      dmg: tower.dmg, life: 120
    });
    tower.cooldown = tower.fireRate;
  });
}
Math.atan2 — szög kiszámítása két pont között

Math.atan2(dy, dx) megadja a két pont közötti szöget radiánban. Aztán Math.cos(angle) és Math.sin(angle) adja a vx és vy sebességkomponenseket — a lövedék pontosan a célpont felé indul.

✏️ Feladat
  • Keresd meg a towerAI függvényt a castle_siege.html-ben
  • Figyeld meg: ha kicsire állítod a tower.range értékét (pl. 50), a torony nem lő — miért?
  • Adj hozzá egy "splash damage" tornyot: ha a lövedék eltalál, a 50px-en belüli összes ellenség kap 30% sebzést
  • Bónusz: rajzolj egy hatókör kört a torony köré (ctx.arc a tower.range sugarával, alacsony opacity)

🧠 Mire való a Math.atan2(dy, dx) a célzásnál?

A távolságot számítja ki
A torony és célpont közötti szöget adja meg radiánban, amiből a lövedék vx és vy sebessége kiszámítható
Az ellenség sebességét adja vissza
Csak körök esetén működik
4. Fázis · 13. Lecke

🏰 Castle Siege — Lövedék előrejelzés

Az alap célzás az ellenség jelenlegi pozíciójára lő — de mire a lövedék odaér, az ellenség már máshol van. Az előrejelző célzás ezt oldja meg.

⏱ 60 perc
🎯 lead prediction · útidő · célpont előrejelzés

1Az előrejelző célzás matematikája

🏰 Castle Siege
castle_siege.html — predictiveCelzas()JavaScript
// ALAP célzás: oda lő ahol AZ ELLENSÉG VAN
const angle = Math.atan2(e.y - t.y, e.x - t.x);

// ELŐREJELZŐ célzás: oda lő ahol AZ ELLENSÉG LESZ
// 1. Mennyi idő alatt ér a lövedék az ellenséghez?
const dist = Math.hypot(e.x - t.x, e.y - t.y);
const travelTime = dist / t.projSpd; // frame-ekben

// 2. Hol lesz az ellenség travelTime frame múlva?
const predX = e.x + e.vx * travelTime;
const predY = e.y + e.vy * travelTime;

// 3. Az előrejelzett pozícióra célzunk
const angle = Math.atan2(predY - t.y, predX - t.x);

// Lövedék indítása az előrejelzett szögben:
G.tProj.push({
  vx: Math.cos(angle) * t.projSpd,
  vy: Math.sin(angle) * t.projSpd,
});
Mikor érdemes használni?

Ha az ellenség gyorsan mozog és a lövedék lassú, az alap célzás szinte soha nem talál. Az előrejelző célzás sokkal pontosabb — de kiszámíthatóbbá is teszi az AI-t. A Castle Siege-ben az ágyú torony használja ezt.

✏️ Feladat
  • A console-ból figyeld meg: G.enemies[0].vx — milyen gyorsan mozog az ellenség?
  • Számítsd ki papíron: ha egy ellenség x=300-on van, vx=1 és a lövedék 60 frame alatt ér oda, hol lövünk?
  • A towerAI-ba adj be egy logCapjel hogy mikor lő a torony — console.log-gal
  • Bónusz: implementáld az előrejelző célzást egy egyszerű lövedékes mini játékban

🧠 Miért jobb az előrejelző célzás ha az ellenség gyorsan mozog?

Mert a lövedék is gyorsabb lesz
Mert az alap célzás az aktuális pozícióra lő, de mire a lövedék odaér az ellenség már máshol jár — az előrejelzés a jövőbeli pozícióra céloz
Kevesebb számítást igényel
Csak statikus ellenségeknél működik
4. Fázis · 14. Lecke

🏰 Castle Siege — Mentés és Ranglista

A localStorage segítségével a játék menti az előrehaladást és a Top 10 eredményt. A JSON.stringify és JSON.parse konvertálja az objektumokat szöveggé és vissza.

⏱ 45 perc
🎯 localStorage · JSON · Top 10 lista

1Mentés és betöltés localStorage-val

🏰 Castle Siege
castle_siege.html — mentés rendszerJavaScript
// MENTÉS: objektum → JSON szöveg → localStorage
function saveGame(name) {
  const saveData = {
    level: G.level, score: G.score,
    heroSpd: G.hero.spd, gold: G.gold
  };
  localStorage.setItem('cs_save_'+name, JSON.stringify(saveData));
}

// BETÖLTÉS: localStorage → JSON szöveg → objektum
function loadGame(name) {
  const raw = localStorage.getItem('cs_save_'+name);
  if (!raw) return null;
  return JSON.parse(raw);
}

// TOP 10 lista kezelése
function saveTop(name, score, level) {
  let top = getTop();
  top.push({ name, score, level, date: new Date().toLocaleDateString('hu') });
  top.sort((a, b) => b.score - a.score); // csökkenő sorrend
  localStorage.setItem('cs_top10', JSON.stringify(top.slice(0, 10)));
}
localStorage korlátai

Csak szöveg tárolható (ezért kell JSON.stringify). Böngészőnként max ~5MB. Privátos módban törlődik. Netlify-on is működik — nem kell szerver hozzá.

✏️ Feladat
  • F12 → Application tab → Local Storage — látod a Castle Siege mentéseit?
  • Console-ban: localStorage.getItem('cs_top10') — mi a tartalom?
  • Töröld és nézd meg mi történik: localStorage.clear()
  • Bónusz: írj egy saját mentés/betöltés rendszert a turtle_race-hez — mentse a szintet és a pontszámot

🧠 Miért kell JSON.stringify mielőtt localStorage-ba mentünk?

Mert a localStorage titkosítja az adatot
Mert a localStorage csak szöveget tárol — a JavaScript objektumokat JSON.stringify-jal kell szöveggé alakítani, majd JSON.parse-szal visszaalakítani
Mert különben lassabb a tárolás
Nem kötelező, objektumot is be lehet rakni
4. Fázis · 15. Lecke

🏆 Projektzáró — Saját mini játék

A 4. Fázis lezárása: egy saját, önállóan tervezett mini játék. A három játékból tanultak alapján — de a saját ötleted szerint.

⏱ 5–8 óra
🎯 Önálló tervezés és fejlesztés
📁 Egyetlen HTML fájl

1A projekt elvárásai

Kötelező elemek

✓ Canvas alapú, requestAnimationFrame loop
✓ Legalább 2 játékelem (játékos + valami amivel interaktál)
✓ Billentyűzet vagy érintéses irányítás
✓ Pontszám-számítás és megjelenítés (ctx.fillText)
✓ Game Over állapot (state machine!)
✓ Sin() alapú animáció valamelyik elemen
✓ Ütközésdetektálás (Math.hypot vagy AABB)

Ötletek — válassz egyet vagy kombinálj:

ÖtletFő mechanikaPélda a kurzusból
🐍 Kígyó játékNövekvő kígyó, ételgyűjtésTömb kezelés, ütközés
🌌 AszteroidaForgó hajó, lövedékek, kövekTranszformáció, atan2 célzás
🏓 PongKét ütő, pattanó labdaAABB, AI ellenfél
🌧️ GyűjtögetősEsnek a tárgyak, el kell kapniSpawn, scroll, ütközés
🐦 Flappy BirdGravitáció, akadályokFizika, state machine
🧱 BreakoutÜtő, labda, téglákAABB, tömb kezelés
✏️ Projekt Checklist
  • Tervezési fázis: papíron vázolj fel egy játékképernyőt és listázd az elemeket
  • Struktúra: G objektum, update(), draw(), loop() elkülönítve
  • State machine: legalább play és gameover állapot
  • Karakter save/translate/restore-ral rajzolódik
  • Sin() animáció: legalább egy elem animált
  • Spawn rendszer: legalább 5 objektum jelenik meg a pályán
  • Top 10 ranglista localStorage-ban
  • Mobilos D-pad gombok (ha csak billentyűzetes, az is OK)
  • Egyetlen HTML fájlba van sűrítve minden
  • Netlify-ra feltöltve és kipróbálva mobilon
Bónusz kihívások

⭐ Véletlenszerű pályagenerálás (evenSpread minta alapján)
⭐⭐ Nehézségi szintek (DIFF_CFG minta alapján)
⭐⭐ Előrejelző célzás egy toronyhoz / ellenséghez
⭐⭐⭐ Zene beágyazva base64-ként, Android kompatibilis audio unlock
⭐⭐⭐ Mentés és betöltés JSON-nal, névvel azonosított mentési helyek