🐸 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.
1A probléma: miért nem Math.random()?
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.
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; }
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ípus | S_start | Darab (1. szint) | Miért? |
|---|---|---|---|
| 🥚 Tojás | 150 | 24 | Hamar jutalmat kap a játékos |
| 🪨 Kő | 250 | 12 | Kicsit felkészülési idő kell |
| 🦅 Sirály | 300 | 6 | A legnehezebb veszély legkésőbb jelenik meg |
| ⚡ Boost | 500 | 5 | Csak a pálya második felén |
Nyisd meg a froggy_rush.html-t VS Code-ban:
- Keresd meg a
spawnObjectsfü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?
🐸 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ó.
1A SURFACES objektum — strategy pattern
// 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 }
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?
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'
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?
🐸 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.
1A teljes ugrás kódja
// 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);
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:
// 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); }
- 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?
🐸 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.
1Az állapotok és átmenetek
// 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 } } }
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.
- 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?
🐸 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.
1A DIFF_CFG konfigurációs objektum
// 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 }
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.
- 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?
🐢 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.
1Az AI döntéshozatal
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 = (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
// 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; } }); }
- Keresd meg az
updateAIfü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?
🐢 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.
1Az energia fogyás és töltés logikája
// 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 } }
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.
- 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?
🐢 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.
1A teljes koordináta rendszer
// 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);
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.
- 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ésG.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?
🐢 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.
1A drawTurtle() felépítése rétegekbe
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
// 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();
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.
- Keresd meg a
drawTurtlefü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?
🏰 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.
1A G játékállapot objektum
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
| Rendszer | Bemenete | Kimenete | Függvény |
|---|---|---|---|
| Hős mozgás | K objektum (billentyűk) | hero.x, hero.y változik | moveHero() |
| Ellenségek | Pályaadatok, hullám szám | Ellenségek mozognak, sebzik a kastélyt | moveEnemies() |
| Tornyok | Ellenségek pozíciója | Lövedékek spawolódnak | towerAI() |
| Ütközés | Lövedékek + ellenségek | HP csökkentés, arany szerzés | checkCollisions() |
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? }
- 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?
🏰 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.
1A genLevel() függvény logikája
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 }; }
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.
- Keresd meg a
genLevelfü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))
🏰 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.
1A torony AI logikája
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(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.
- 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?
🏰 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.
1Az előrejelző célzás matematikája
// 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, });
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.
- 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?
🏰 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.
1Mentés és betöltés localStorage-val
// 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))); }
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á.
- 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?
🏆 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.
1A projekt elvárásai
✓ 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:
| Ötlet | Fő mechanika | Példa a kurzusból |
|---|---|---|
| 🐍 Kígyó játék | Növekvő kígyó, ételgyűjtés | Tömb kezelés, ütközés |
| 🌌 Aszteroida | Forgó hajó, lövedékek, kövek | Transzformáció, atan2 célzás |
| 🏓 Pong | Két ütő, pattanó labda | AABB, AI ellenfél |
| 🌧️ Gyűjtögetős | Esnek a tárgyak, el kell kapni | Spawn, scroll, ütközés |
| 🐦 Flappy Bird | Gravitáció, akadályok | Fizika, state machine |
| 🧱 Breakout | Ütő, labda, téglák | AABB, tömb kezelés |
- 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
⭐ 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