536	function handleInteractions(){
537	  // mince
538	  for (const co of world.coins){
539	    if (co.got) continue;
540	    if (Math.abs(player.x + player.w/2 - co.x) < 18 &&
541	        Math.abs(player.y + player.h/2 - co.y) < 20){
542	      co.got = true; coinsCollected++; score += 10; S.coin();
543	    }
544	  }
545	  // nepřátelé
546	  for (const e of world.enemies){
547	    if (e.dead) continue;
548	    if (!rectOverlap(player.x, player.y, player.w, player.h, e.x, e.y, e.w, e.h)) continue;
549	    const stomp = player.vy > 1.5 && (player.y + player.h) - e.y < e.h * 0.5 + 6;
550	    if (stomp){
551	      const td = ETIER[e.tier || 0];
552	      e.dead = true; e.t = 0;
553	      player.vy = -10 - (e.tier || 0) * 1.5;          // z větší příšery větší odraz
554	      score += td.score;
555	      coinsCollected += td.coins;                     // odměna podle velikosti
556	      shake = 3 + (e.tier || 0) * 3;
557	      spawnCoinPops(e.x + e.w / 2, e.y, td.coins);
558	      beep(200 - (e.tier || 0) * 40, 0.12 + (e.tier || 0) * 0.03, 'sawtooth', 0.05);
559	    } else {
560	      hurtPlayer();
561	    }
562	  }
563	  // cíl
564	  if (world.goal && mode === 'levels' &&
565	      rectOverlap(player.x, player.y, player.w, player.h,
566	                  world.goal.x, world.goal.y - TILE*2.2, TILE, TILE*2.2)){
567	    S.win();
568	    score += 200 + lives * 100;
569	    levelIndex++;
570	    if (levelIndex >= LEVELS.length){ state = 'win'; }
571	    else { state = 'shop'; shopSel = 0; }
572	  }
573	  // pád mimo svět
574	  if (player.y > (ROWS + 1) * TILE) hurtPlayer();
575	}

function handleInteractions(){
  // mince
  for (const co of world.coins){
    if (co.got) continue;
    if (Math.abs(player.x + player.w/2 - co.x) < 18 &&
        Math.abs(player.y + player.h/2 - co.y) < 20){
      co.got = true; coinsCollected++; score += 10; S.coin();
    }
  }
  // nepřátelé
  for (const e of world.enemies){
    if (e.dead) continue;
    if (!rectOverlap(player.x, player.y, player.w, player.h, e.x, e.y, e.w, e.h)) continue;
    const stomp = player.vy > 1.5 && (player.y + player.h) - e.y < e.h * 0.5 + 6;
    if (stomp){
      const td = ETIER[e.tier || 0];
      e.dead = true; e.t = 0;
      player.vy = -10 - (e.tier || 0) * 1.5;          // z větší příšery větší odraz
      score += td.score;
      coinsCollected += td.coins;                     // odměna podle velikosti
      shake = 3 + (e.tier || 0) * 3;
      spawnCoinPops(e.x + e.w / 2, e.y, td.coins);
      beep(200 - (e.tier || 0) * 40, 0.12 + (e.tier || 0) * 0.03, 'sawtooth', 0.05);
    } else {
      hurtPlayer();
    }
  }
  // cíl
  if (world.goal && mode === 'levels' &&
      rectOverlap(player.x, player.y, player.w, player.h,
                  world.goal.x, world.goal.y - TILE*2.2, TILE, TILE*2.2)){
    S.win();
    score += 200 + lives * 100;
    levelIndex++;
    if (levelIndex >= LEVELS.length){ state = 'win'; }
    else { state = 'shop'; shopSel = 0; }
  }
  // pád mimo svět
  if (player.y > (ROWS + 1) * TILE) hurtPlayer();
}


<total_tokens>14998845 tokens left</total_tokens>


Bash

grep -n "player = {\|player\.px\|player\.py\|prevY\|prevX\|player\.prev" /home/tonda/hra.html; echo ---; sed -n '440,485p' /home/tonda/hra.html

Inspect player object and physics step


446:const player = {
---
    if (+k.slice(0, k.indexOf(',')) < minC) world.solid.delete(k);
}

/* ------------------------------------------------------------------ */
/*  Hráč                                                              */
/* ------------------------------------------------------------------ */
const player = {
  x: 0, y: 0, w: 26, h: 32,
  vx: 0, vy: 0, onGround: false,
  coyote: 0, jumpBuf: 0, face: 1, invuln: 0,
};

function updatePlayer(){
  if (player.invuln > 0) player.invuln--;

  const left  = down('ArrowLeft', 'KeyA');
  const right = down('ArrowRight', 'KeyD');
  const accel = 0.9, fric = 0.80, maxSpd = 4.4;

  if (left && !right){ player.vx -= accel; player.face = -1; }
  else if (right && !left){ player.vx += accel; player.face = 1; }
  else player.vx *= fric;
  player.vx = clamp(player.vx, -maxSpd, maxSpd);

  if (hit('ArrowUp', 'KeyW', 'Space')) player.jumpBuf = 9;
  if (player.jumpBuf > 0) player.jumpBuf--;
  if (player.coyote > 0) player.coyote--;

  if (player.jumpBuf > 0 && player.coyote > 0){
    player.vy = -13.5;
    player.jumpBuf = player.coyote = 0;
    player.onGround = false;
    S.jump();
  }
  // nižší skok při krátkém stisku
  if (!down('ArrowUp', 'KeyW', 'Space') && player.vy < -4) player.vy = -4;

  player.vy = Math.min(player.vy + GRAV, MAX_FALL);

  player.onGround = false;
  player.x += player.vx; resolve('x');
  player.y += player.vy; resolve('y');

  if (player.onGround) player.coyote = 7;

  // hranice světa (v nekonečném módu vlevo nic – tam si tě hlídá zeď)


<total_tokens>14997382 tokens left</total_tokens>


Edit

  player.onGround = false;
  player.x += player.vx; resolve('x');
  player.y += player.vy; resolve('y');

  player.onGround = false;
  player.x += player.vx; resolve('x');
  player.prevBottom = player.y + player.h;   // spodní hrana před svislým pohybem
  player.y += player.vy; resolve('y');


The file /home/tonda/hra.html has been updated successfully. (file state is current in your context — no need to Read it back)

  player.onGround = false;
  player.x += player.vx; resolve('x');
  player.y += player.vy; resolve('y');

  player.onGround = false;
  player.x += player.vx; resolve('x');
  player.prevBottom = player.y + player.h;   // spodní hrana před svislým pohybem
  player.y += player.vy; resolve('y');

+  player.prevBottom = player.y + player.h;   // spodní hrana před svislým pohybem


<total_tokens>14995536 tokens left</total_tokens>


ještě uprav když skočím na nepřátele ne roh tak mě zabijí místo abych je zabil já chtěl bych aby mě zabili pouze z boku


const stomp = player.vy > 1.5 && (player.y + player.h) - e.y < e.h * 0.5 + 6;

    // dopad shora = zničíš nepřítele (i rohem). Zabije tě jen zásah z boku / zespodu.
    const feet = player.y + player.h;
    const fromAbove = player.vy > 0 &&
      ((player.prevBottom ?? feet) <= e.y + 8 || feet - e.y < e.h * 0.5 + 6);
    const stomp = fromAbove;


    // dopad shora = zničíš nepřítele (i rohem). Zabije tě jen zásah z boku / zespodu.
    const feet = player.y + player.h;
    const fromAbove = player.vy > 0 &&
      ((player.prevBottom ?? feet) <= e.y + 8 || feet - e.y < e.h * 0.5 + 6);
    const stomp = fromAbove;

if (!rectOverlap(player.x, player.y, player.w, player.h, e.x, e.y, e.w, e.h)) continue;

-    const stomp = player.vy > 1.5 && (player.y + player.h) - e.y < e.h * 0.5 + 6;

+    // dopad shora = zničíš nepřítele (i rohem). Zabije tě jen zásah z boku / zespodu.

+      ((player.prevBottom ?? feet) <= e.y + 8 || feet - e.y < e.h * 0.5 + 6);


<total_tokens>14995146 tokens left</total_tokens>


Bash

python3 - <<'EOF'
import re
js=re.search(r'<script>(.*)</script>',open('hra.html').read(),re.S).group(1)
for a,b in [('(',')'),('{','}'),('[',']')]:
    assert js.count(a)==js.count(b), (a, js.count(a), js.count(b))
print("brackets OK")
EOF
sed -n '546,556p' hra.html
curl -sf -o /dev/null -w "server HTTP %{http_code}\n" http://localhost:8000/hra.html

Syntax check and show stomp block


brackets OK
  // nepřátelé
  for (const e of world.enemies){
    if (e.dead) continue;
    if (!rectOverlap(player.x, player.y, player.w, player.h, e.x, e.y, e.w, e.h)) continue;
    // dopad shora = zničíš nepřítele (i rohem). Zabije tě jen zásah z boku / zespodu.
    const feet = player.y + player.h;
    const fromAbove = player.vy > 0 &&
      ((player.prevBottom ?? feet) <= e.y + 8 || feet - e.y < e.h * 0.5 + 6);
    const stomp = fromAbove;
    if (stomp){
      const td = ETIER[e.tier || 0];
server HTTP 200


<total_tokens>14993905 tokens left</total_tokens>


Hotovo — dej **F5**.

Změna v `hra.html`:

- Přidal jsem sledování spodní hrany hráče z minulého snímku (`player.prevBottom`).
- **Stomp (zničení nepřítele) se teď spustí při jakémkoli dopadu shora** — stačí, že padáš (`vy > 0`) a nohy jsi měl nad hlavou nepřítele, i když ho trefíš jen rohem.
- **Zabije tě jen zásah z boku** (nebo zespodu při výskoku) — když do nepřítele narazíš vodorovně a nohy máš v úrovni jeho těla.
- Zrušil jsem starou podmínku „musíš padat dost rychle" (`vy > 1.5`), takže i pomalý dopad na okraj nepřítele ho zničí.


We're improving the mobile version of your 2D platformer game hra.html, served at http://192.168.5.32:8000/hra.html. Just finished: touch joystick, jump button, fullscreen button, and fixing corner-stomps on enemies. Next, press F5 and test, then tell me what to adjust. (disable recaps in /config)


odeber tlačítko seretu


<total_tokens>15000000 tokens left</total_tokens>


Které tlačítko myslíš? V dotykovém ovládání jsou teď:


AskUserQuestion

Question: Které tlačítko mám odebrat z hry?

Choices:
Ikona rohů nahoře uprostřed vlevo od pauzy (přidaná posledně)
Ikona ❚❚ nahoře uprostřed
Velký kruh SKOK vpravo dole
Ovládání chůze vlevo dole


Error: The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). To tell you how to proceed, the user said:
The user wants to clarify these questions.
    This means they may have additional information, context or questions for you.
    Take their response into account and then reformulate the questions if appropriate.
    Start by asking them what they would like to clarify.

    Questions asked:
- "Které tlačítko mám odebrat z hry?"
  (No answer provided)

Note: The user's next message may contain a correction or preference. Pay close attention — if they explain what went wrong or how they'd prefer you to work, consider saving that to memory for future sessions.


<total_tokens>14999240 tokens left</total_tokens>


Co chceš upřesnit? Jen mi napiš, které tlačítko ti vadí (nebo kde na obrazovce je), a odeberu ho.


tlačítko seretování a když už u odebírání nekonečném běhu udělej jenom jeden život a ani ho nezobrazuj


Bash

grep -ni "reset\|restart\|KeyR\|'R'\|drawOver\|Menu\|celá obrazovk\|full" /home/tonda/hra.html | grep -iv "world\|solid\|//" | head -40

Look for reset/restart button


42:  /* celá obrazovka: vejít se do výšky i šířky, žádné okraje */
43:  :fullscreen, :-webkit-full-screen{ background:#101320; }
44:  :fullscreen body, :-webkit-full-screen body{ padding:0; gap:0; }
45:  :fullscreen canvas, :-webkit-full-screen canvas{
54:    Pohyb ← → / A D  ·  Skok Space / ↑ / W  ·  Pauza P  ·  Restart R

116:  full:  { cx: W / 2 - 30, cy: 30, r: 22 },
159:function toggleFullscreen(){
161:  const req = el.requestFullscreen || el.webkitRequestFullscreen;
162:  const exit = document.exitFullscreen || document.webkitExitFullscreen;
163:  const fs = document.fullscreenElement || document.webkitFullscreenElement;
660:let menuSel = 0;
753:  if (state === 'menu'){
754:    if (hit('ArrowUp', 'ArrowDown', 'KeyW', 'KeyS')) menuSel = 1 - menuSel;
762:    if (hit('Enter', 'Space')) return startGame(menuSel === 0 ? 'levels' : 'endless');
766:    if (hit('Enter', 'Space', 'Escape')) state = 'menu';
771:    if (hit('Escape')) state = 'menu';
785:  if (hit('Escape')) { state = 'menu'; return; }
787:  if (hit('KeyR'))   { mode === 'levels' ? loadLevel(levelIndex) : loadEndless(); return; }
860:  if (state === 'menu')            drawMenu();
861:  else if (state === 'paused')     overlay('PAUZA', isTouch ? 'ťukni pro pokračování' : 'P nebo Enter – pokračovat   ·   Esc – menu');
863:  else if (state === 'over')       drawOver();
864:  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + (isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu'));
895:  const b = TB.full;
940:    ctx.fillText('← Menu', W/2, mb.y + mb.h / 2);
1101:function drawOver(){
1105:  sub += isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu';
1170:function drawMenu(){
1186:  menuOption('1', 'Příběhové levely', LEVELS.length + ' ručně navržených levelů s cílem a životy', H/2 - 26, menuSel === 0);
1187:  menuOption('2', 'Nekonečný běh', 'Náhodná trať, která zrychluje — hraj na skóre', H/2 + 42, menuSel === 1);
1243:function menuOption(num, title, desc, y, sel){
1262:/*  Myš / dotyk pro menu a obrazovky                                  */
1272:canvas.addEventListener('contextmenu', e => e.preventDefault());
1280:  if (isTouch && inCirc(p, TB.full)){ toggleFullscreen(); return; }
1287:      state = 'menu'; return;
1303:  if (state === 'menu'){
1329:    state = 'menu';


<total_tokens>14999410 tokens left</total_tokens>


1101	function drawOver(){
1102	  let sub = statLine();
1103	  if (mode === 'endless' && justUnlocked.length)
1104	    sub += '   ·   odemčeno: ' + justUnlocked.join(', ');
1105	  sub += isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu';
1106	  overlay(mode === 'endless' && newRecord ? 'NOVÝ REKORD! 🎉' : 'KONEC HRY', sub);
1107	}
1108
1109	function drawShop(){
1110	  ctx.save();
1111	  ctx.fillStyle = 'rgba(8,10,16,.88)';
1112	  ctx.fillRect(0, 0, W, H);
1113	  ctx.textAlign = 'center';
1114
1115	  ctx.fillStyle = '#e8ecf5';
1116	  ctx.font = '800 32px system-ui, sans-serif';
1117	  ctx.fillText('LEVEL ' + levelIndex + ' HOTOV', W/2, 84);
1118	  ctx.fillStyle = COL.coin;
1119	  ctx.font = '600 18px system-ui, sans-serif';
1120	  ctx.fillText('● ' + coinsCollected + ' mincí k utracení', W/2, 116);
1121	  ctx.fillStyle = '#8b93a7';
1122	  ctx.font = '500 14px system-ui, sans-serif';
1123	  ctx.fillText('OBCHOD', W/2, 154);
1124
1125	  SHOP.forEach((it, i) => {
1126	    const y = SHOP_Y0 + i * (SHOP_ROW_H + SHOP_GAP);
1127	    const sel = i === shopSel;
1128	    const price = it.price();
1129	    const ok = it.canBuy();
1130

function drawOver(){
  let sub = statLine();
  if (mode === 'endless' && justUnlocked.length)
    sub += '   ·   odemčeno: ' + justUnlocked.join(', ');
  sub += isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu';
  overlay(mode === 'endless' && newRecord ? 'NOVÝ REKORD! 🎉' : 'KONEC HRY', sub);
}

function drawShop(){
  ctx.save();
  ctx.fillStyle = 'rgba(8,10,16,.88)';
  ctx.fillRect(0, 0, W, H);
  ctx.textAlign = 'center';

  ctx.fillStyle = '#e8ecf5';
  ctx.font = '800 32px system-ui, sans-serif';
  ctx.fillText('LEVEL ' + levelIndex + ' HOTOV', W/2, 84);
  ctx.fillStyle = COL.coin;
  ctx.font = '600 18px system-ui, sans-serif';
  ctx.fillText('● ' + coinsCollected + ' mincí k utracení', W/2, 116);
  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 14px system-ui, sans-serif';
  ctx.fillText('OBCHOD', W/2, 154);

  SHOP.forEach((it, i) => {
    const y = SHOP_Y0 + i * (SHOP_ROW_H + SHOP_GAP);
    const sel = i === shopSel;
    const price = it.price();
    const ok = it.canBuy();


<total_tokens>14996787 tokens left</total_tokens>


grep -n "lives = \|lives=\|function startGame\|MAX_LIVES\|lives <= 0\|lives--\|drawHUD" /home/tonda/hra.html

Find lives assignments


584:  lives--;
587:  if (lives <= 0){
661:let world, levelIndex = 0, lives = 3, coinsCollected = 0, score = 0, distance = 0;
665:const MAX_LIVES = 5;
675:    desc: '+1 život (max ' + MAX_LIVES + ')',
679:    canBuy(){ return lives < MAX_LIVES && coinsCollected >= this.price(); },
730:function startGame(m){
733:  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
858:  if (world) drawHUD();
1055:function drawHUD(){
1057:  const slots = Math.min(Math.max(3, lives), MAX_LIVES);


<total_tokens>14995623 tokens left</total_tokens>


730	function startGame(m){
731	  unlockAudio();
732	  mode = m;
733	  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
734	  shopSel = 0; pops = [];
735	  newRecord = false; justUnlocked = [];
736	  SHOP.forEach(it => it.bought = 0);
737	  if (m === 'levels'){ levelIndex = 0; loadLevel(0); }
738	  else loadEndless();
739	  state = 'play';
740	}
741
742	function loadLevel(i){
743	  world = buildLevel(LEVELS[i], i);
744	  pops = [];
745	  player.x = world.playerStart.x;

function startGame(m){
  unlockAudio();
  mode = m;
  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0; pops = [];
  newRecord = false; justUnlocked = [];
  SHOP.forEach(it => it.bought = 0);
  if (m === 'levels'){ levelIndex = 0; loadLevel(0); }
  else loadEndless();
  state = 'play';
}

function loadLevel(i){
  world = buildLevel(LEVELS[i], i);
  pops = [];
  player.x = world.playerStart.x;


<total_tokens>14995225 tokens left</total_tokens>


Edit

lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;

  lives = m === 'endless' ? 1 : 3;   // nekonečný běh: jeden život
  coinsCollected = 0; score = 0; distance = 0; shake = 0;


  lives = m === 'endless' ? 1 : 3;   // nekonečný běh: jeden život
  coinsCollected = 0; score = 0; distance = 0; shake = 0;

-  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;

+  lives = m === 'endless' ? 1 : 3;   // nekonečný běh: jeden život

+  coinsCollected = 0; score = 0; distance = 0; shake = 0;


<total_tokens>14994709 tokens left</total_tokens>


1055
1056	function drawHUD(){
1057	  ctx.save();
1058	  const slots = Math.min(Math.max(3, lives), MAX_LIVES);
1059	  for (let i = 0; i < slots; i++){
1060	    ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';
1061	    roundRect(16 + i * 20, 16, 14, 14, 4); ctx.fill();
1062	  }
1063	  ctx.fillStyle = COL.coin;
1064	  ctx.font = '600 15px system-ui, sans-serif';
1065	  ctx.textAlign = 'left';
1066	  ctx.fillText('● ' + coinsCollected, 16, 54);
1067	  ctx.textAlign = 'right';
1068	  if (mode === 'levels'){
1069	    ctx.fillText(LEVELS[levelIndex].name + '  ·  ' + (levelIndex + 1) + '/' + LEVELS.length, W - 16, 26);
1070	  } else {
1071	    ctx.fillText(distance + ' m', W - 16, 26);
1072	  }
1073	  ctx.fillText('Skóre ' + escore(), W - 16, 48);
1074	  if (mode === 'endless'){

function drawHUD(){
  ctx.save();
  const slots = Math.min(Math.max(3, lives), MAX_LIVES);
  for (let i = 0; i < slots; i++){
    ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';
    roundRect(16 + i * 20, 16, 14, 14, 4); ctx.fill();
  }
  ctx.fillStyle = COL.coin;
  ctx.font = '600 15px system-ui, sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('● ' + coinsCollected, 16, 54);
  ctx.textAlign = 'right';
  if (mode === 'levels'){
    ctx.fillText(LEVELS[levelIndex].name + '  ·  ' + (levelIndex + 1) + '/' + LEVELS.length, W - 16, 26);
  } else {
    ctx.fillText(distance + ' m', W - 16, 26);
  }
  ctx.fillText('Skóre ' + escore(), W - 16, 48);
  if (mode === 'endless'){


<total_tokens>14994523 tokens left</total_tokens>


  ctx.save();
  const slots = Math.min(Math.max(3, lives), MAX_LIVES);
  for (let i = 0; i < slots; i++){
    ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';
    roundRect(16 + i * 20, 16, 14, 14, 4); ctx.fill();
  }
  ctx.fillStyle = COL.coin;
  ctx.font = '600 15px system-ui, sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('● ' + coinsCollected, 16, 54);

  ctx.save();
  const endless = mode === 'endless';
  if (!endless){                       // životy se v nekonečném běhu nezobrazují
    const slots = Math.min(Math.max(3, lives), MAX_LIVES);
    for (let i = 0; i < slots; i++){
      ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';
      roundRect(16 + i * 20, 16, 14, 14, 4); ctx.fill();
    }
  }
  ctx.fillStyle = COL.coin;
  ctx.font = '600 15px system-ui, sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('● ' + coinsCollected, 16, endless ? 28 : 54);


  ctx.save();
  const slots = Math.min(Math.max(3, lives), MAX_LIVES);
  for (let i = 0; i < slots; i++){
    ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';
    roundRect(16 + i * 20, 16, 14, 14, 4); ctx.fill();
  }
  ctx.fillStyle = COL.coin;
  ctx.font = '600 15px system-ui, sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('● ' + coinsCollected, 16, 54);

  ctx.save();
  const endless = mode === 'endless';
  if (!endless){                       // životy se v nekonečném běhu nezobrazují
    const slots = Math.min(Math.max(3, lives), MAX_LIVES);
    for (let i = 0; i < slots; i++){
      ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';
      roundRect(16 + i * 20, 16, 14, 14, 4); ctx.fill();
    }
  }
  ctx.fillStyle = COL.coin;
  ctx.font = '600 15px system-ui, sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('● ' + coinsCollected, 16, endless ? 28 : 54);

-  const slots = Math.min(Math.max(3, lives), MAX_LIVES);

-    ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';

+  if (!endless){                       // životy se v nekonečném běhu nezobrazují

+    const slots = Math.min(Math.max(3, lives), MAX_LIVES);

+      ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';

-  ctx.fillText('● ' + coinsCollected, 16, 54);

+  ctx.fillText('● ' + coinsCollected, 16, endless ? 28 : 54);

ctx.fillText(LEVELS[levelIndex].name + '  ·  ' + (levelIndex + 1) + '/' + LEVELS.length, W - 16, 26);


<total_tokens>14993294 tokens left</total_tokens>


Odeberu tlačítko celé obrazovky (ikona rohů nahoře) a zároveň v nekonečném běhu nastavím jeden život bez zobrazení.


const TB = {                     // rozmístění tlačítek (v souřadnicích plátna)

const inCirc = (p, b) => Math.hypot(p.x - b.cx, p.y - b.cy) <= b.r;


<total_tokens>14991826 tokens left</total_tokens>


function toggleFullscreen(){
  const el = document.documentElement;
  const req = el.requestFullscreen || el.webkitRequestFullscreen;
  const exit = document.exitFullscreen || document.webkitExitFullscreen;
  const fs = document.fullscreenElement || document.webkitFullscreenElement;
  try {
    if (!fs && req){
      const p = req.call(el);
      if (p && p.then) p.then(() => { try { screen.orientation.lock('landscape'); } catch (e) {} }).catch(() => {});
      else { try { screen.orientation.lock('landscape'); } catch (e) {} }
    } else if (fs && exit){
      exit.call(document);
    }
  } catch (e) {}
}


function toggleFullscreen(){
  const el = document.documentElement;
  const req = el.requestFullscreen || el.webkitRequestFullscreen;
  const exit = document.exitFullscreen || document.webkitExitFullscreen;
  const fs = document.fullscreenElement || document.webkitFullscreenElement;
  try {
    if (!fs && req){
      const p = req.call(el);
      if (p && p.then) p.then(() => { try { screen.orientation.lock('landscape'); } catch (e) {} }).catch(() => {});
      else { try { screen.orientation.lock('landscape'); } catch (e) {} }
    } else if (fs && exit){
      exit.call(document);
    }
  } catch (e) {}
}

codesDown[j ? 'add' : 'delete']('Space');

-  const req = el.requestFullscreen || el.webkitRequestFullscreen;

-  const exit = document.exitFullscreen || document.webkitExitFullscreen;

-  const fs = document.fullscreenElement || document.webkitFullscreenElement;

-      if (p && p.then) p.then(() => { try { screen.orientation.lock('landscape'); } catch (e) {} }).catch(() => {});

-      else { try { screen.orientation.lock('landscape'); } catch (e) {} }

/*  Zvuk (WebAudio, generovaný – žádné externí soubory)               */


<total_tokens>14991363 tokens left</total_tokens>


Bash

grep -n "drawFsBtn\|TB.full\|toggleFullscreen" /home/tonda/hra.html

Find remaining fullscreen refs


852:  if (isTouch) drawFsBtn();
878:function drawFsBtn(){
879:  const b = TB.full;
1267:  if (isTouch && inCirc(p, TB.full)){ toggleFullscreen(); return; }


<total_tokens>14991162 tokens left</total_tokens>


848	  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + (isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu'));
849
850	  if (isTouch && (state === 'play' || state === 'paused')) drawTouch();
851	  if (isTouch && innerHeight > innerWidth * 1.1) drawRotateHint();
852	  if (isTouch) drawFsBtn();
853	}
854
855	function drawJoy(){

  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + (isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu'));

  if (isTouch && (state === 'play' || state === 'paused')) drawTouch();
  if (isTouch && innerHeight > innerWidth * 1.1) drawRotateHint();
  if (isTouch) drawFsBtn();
}

function drawJoy(){


<total_tokens>14990933 tokens left</total_tokens>


Edit

  if (isTouch && (state === 'play' || state === 'paused')) drawTouch();
  if (isTouch && innerHeight > innerWidth * 1.1) drawRotateHint();
  if (isTouch) drawFsBtn();
}

  if (isTouch && (state === 'play' || state === 'paused')) drawTouch();
  if (isTouch && innerHeight > innerWidth * 1.1) drawRotateHint();
}


  if (isTouch && (state === 'play' || state === 'paused')) drawTouch();
  if (isTouch && innerHeight > innerWidth * 1.1) drawRotateHint();
  if (isTouch) drawFsBtn();
}

  if (isTouch && (state === 'play' || state === 'paused')) drawTouch();
  if (isTouch && innerHeight > innerWidth * 1.1) drawRotateHint();
}


<total_tokens>14990433 tokens left</total_tokens>


875	  ctx.restore();
876	}
877	function drawFsBtn(){
878	  const b = TB.full;
879	  ctx.save();
880	  ctx.fillStyle = 'rgba(255,255,255,.10)';
881	  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.fill();
882	  ctx.strokeStyle = 'rgba(255,255,255,.28)'; ctx.lineWidth = 2;
883	  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.stroke();
884	  ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 2.4; ctx.lineCap = 'round';
885	  const g = 3, s = 7;
886	  for (const sx of [-1, 1]) for (const sy of [-1, 1]){
887	    ctx.beginPath();
888	    ctx.moveTo(b.cx + sx * (g + s), b.cy + sy * g);
889	    ctx.lineTo(b.cx + sx * g,       b.cy + sy * g);
890	    ctx.lineTo(b.cx + sx * g,       b.cy + sy * (g + s));
891	    ctx.stroke();
892	  }
893	  ctx.lineCap = 'butt';
894	  ctx.restore();

  ctx.restore();
}
function drawFsBtn(){
  const b = TB.full;
  ctx.save();
  ctx.fillStyle = 'rgba(255,255,255,.10)';
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = 'rgba(255,255,255,.28)'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.stroke();
  ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 2.4; ctx.lineCap = 'round';
  const g = 3, s = 7;
  for (const sx of [-1, 1]) for (const sy of [-1, 1]){
    ctx.beginPath();
    ctx.moveTo(b.cx + sx * (g + s), b.cy + sy * g);
    ctx.lineTo(b.cx + sx * g,       b.cy + sy * g);
    ctx.lineTo(b.cx + sx * g,       b.cy + sy * (g + s));
    ctx.stroke();
  }
  ctx.lineCap = 'butt';
  ctx.restore();


<total_tokens>14990248 tokens left</total_tokens>


function drawFsBtn(){
  const b = TB.full;
  ctx.save();
  ctx.fillStyle = 'rgba(255,255,255,.10)';
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = 'rgba(255,255,255,.28)'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.stroke();
  ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 2.4; ctx.lineCap = 'round';
  const g = 3, s = 7;
  for (const sx of [-1, 1]) for (const sy of [-1, 1]){
    ctx.beginPath();
    ctx.moveTo(b.cx + sx * (g + s), b.cy + sy * g);
    ctx.lineTo(b.cx + sx * g,       b.cy + sy * g);
    ctx.lineTo(b.cx + sx * g,       b.cy + sy * (g + s));
    ctx.stroke();
  }
  ctx.lineCap = 'butt';
  ctx.restore();
}


The file /home/tonda/hra.html has been updated successfully. (file state is current in your context — no need to Read it back)

function drawFsBtn(){
  const b = TB.full;
  ctx.save();
  ctx.fillStyle = 'rgba(255,255,255,.10)';
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = 'rgba(255,255,255,.28)'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.stroke();
  ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 2.4; ctx.lineCap = 'round';
  const g = 3, s = 7;
  for (const sx of [-1, 1]) for (const sy of [-1, 1]){
    ctx.beginPath();
    ctx.moveTo(b.cx + sx * (g + s), b.cy + sy * g);
    ctx.lineTo(b.cx + sx * g,       b.cy + sy * g);
    ctx.lineTo(b.cx + sx * g,       b.cy + sy * (g + s));
    ctx.stroke();
  }
  ctx.lineCap = 'butt';
  ctx.restore();
}

-  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.fill();

-  ctx.strokeStyle = 'rgba(255,255,255,.28)'; ctx.lineWidth = 2;

-  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.stroke();

-  ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 2.4; ctx.lineCap = 'round';

-  for (const sx of [-1, 1]) for (const sy of [-1, 1]){

-    ctx.moveTo(b.cx + sx * (g + s), b.cy + sy * g);

-    ctx.lineTo(b.cx + sx * g,       b.cy + sy * g);

-    ctx.lineTo(b.cx + sx * g,       b.cy + sy * (g + s));


<total_tokens>14989126 tokens left</total_tokens>


1238	}
1239	canvas.addEventListener('contextmenu', e => e.preventDefault());
1240
1241	canvas.addEventListener('pointerdown', e => {
1242	  unlockAudio();
1243	  if (e.pointerType === 'touch') isTouch = true;
1244	  const p = canvasPos(e);
1245
1246	  // tlačítko celé obrazovky – kdykoli na mobilu
1247	  if (isTouch && inCirc(p, TB.full)){ toggleFullscreen(); return; }
1248
1249	  // herní dotykové ovládání má přednost během hry

}
canvas.addEventListener('contextmenu', e => e.preventDefault());

canvas.addEventListener('pointerdown', e => {
  unlockAudio();
  if (e.pointerType === 'touch') isTouch = true;
  const p = canvasPos(e);

  // tlačítko celé obrazovky – kdykoli na mobilu
  if (isTouch && inCirc(p, TB.full)){ toggleFullscreen(); return; }

  // herní dotykové ovládání má přednost během hry


<total_tokens>14988940 tokens left</total_tokens>


  const p = canvasPos(e);

  // tlačítko celé obrazovky – kdykoli na mobilu
  if (isTouch && inCirc(p, TB.full)){ toggleFullscreen(); return; }

  // herní dotykové ovládání má přednost během hry

  const p = canvasPos(e);

  // herní dotykové ovládání má přednost během hry


  const p = canvasPos(e);

  // tlačítko celé obrazovky – kdykoli na mobilu
  if (isTouch && inCirc(p, TB.full)){ toggleFullscreen(); return; }

  // herní dotykové ovládání má přednost během hry

  const p = canvasPos(e);

  // herní dotykové ovládání má přednost během hry

if (e.pointerType === 'touch') isTouch = true;

-  // tlačítko celé obrazovky – kdykoli na mobilu

-  if (isTouch && inCirc(p, TB.full)){ toggleFullscreen(); return; }

if (state === 'play' || state === 'paused'){

if (inCirc(p, TB.pause)){ state = state === 'play' ? 'paused' : 'play'; return; }


<total_tokens>14988428 tokens left</total_tokens>


Bash

grep -n "TB.full\|drawFsBtn\|toggleFullscreen\|fullscreen\|Fullscreen" hra.html; echo "--- brackets ---"; python3 - <<'EOF'
import re
js=re.search(r'<script>(.*)</script>',open('hra.html').read(),re.S).group(1)
for a,b in [('(',')'),('{','}'),('[',']')]:
    print(a,b,js.count(a),js.count(b),'OK' if js.count(a)==js.count(b) else 'MISMATCH')
EOF
curl -sf -o /dev/null -w "server HTTP %{http_code}\n" http://localhost:8000/hra.html

Verify all fullscreen refs gone and syntax OK


43:  :fullscreen, :-webkit-full-screen{ background:#101320; }
44:  :fullscreen body, :-webkit-full-screen body{ padding:0; gap:0; }
45:  :fullscreen canvas, :-webkit-full-screen canvas{
--- brackets ---
( ) 946 946 OK
{ } 199 199 OK
[ ] 165 165 OK
server HTTP 200


<total_tokens>14988017 tokens left</total_tokens>


Hotovo — dej **F5**.

**1. Tlačítko celé obrazovky — odebráno**
Předpokládám, že jsi myslel tohle (ikona rohů, kterou jsem přidal minule nahoře uprostřed). Odstranil jsem tlačítko i všechen jeho kód, pauza se vrátila doprostřed. Kdybys myslel jiné tlačítko, řekni které.

**2. Nekonečný běh — jeden život, neviditelný**
- Start nekonečného běhu = `lives = 1` (příběhové levely mají dál 3).
- Ukazatel životů se v nekonečném běhu vůbec nekreslí; počítadlo mincí jsem posunul nahoru na jeho místo.
- První zásah (příšerka z boku, propast, dobíhající zeď) = konec běhu.


taky bych chtěl aby byli velcí nepřátelé byli na dvě zašlápnutí. Taky udělěj u nekonečného módu tři obtížnosti(mometální jediná obtížnost bude střední takže udělej lehčí s méně mezerama s méně a menšíma nepřátelema, taky těžkou obtížnost kde bude více mezer ale všechny budou přeskočitelné a více protivníků a taky budou větší) které si bude moct vybrat taky bude z každé obtížnosti dotávat jiné barvy a jak se daleko dostal jiné odstíny


<total_tokens>15000000 tokens left</total_tokens>


Map skin + endless generation code


223:function rollTier(prog){
307:  for (const [c,r] of (d.enemies || [])) addEnemy(c, r, rollTier(prog));
316:    addEnemy(c, fr, rollTier(prog));
330:let genCol = 0, genRow = 12;
332:function loadEndless(){
336:  genCol = 0; genRow = 12;
337:  generateAhead(0);
342:  scrollSpeed = 1.4;
344:  wallX = -TILE*12;
348:function generateAhead(cx){
351:  while (genCol < need && guard++ < 400) genChunk();
355:  const c0 = genCol;
356:  const r  = genRow;
357:  const diff = Math.min(distance / 500, 1);           // 0 → 1 podle ušlé vzdálenosti
360:  if (genCol < 14){ fill(c0, c0 + 14, r); genCol += 14; return; }   // bezpečný start
364:  if (t < 0.20 + diff * 0.12){                        // propast
365:    let gap = 2 + Math.floor(Math.random() * (1 + Math.round(diff * 3)));
373:    genCol += gap;
380:    if (Math.random() < 0.4 + diff * 0.4) n = 1;
381:    if (n && Math.random() < diff * 0.6) n = 2;
385:      const tier = rollTier(diff);
391:    genCol += len;
394:    genRow = clamp(r + (Math.random() < 0.5 ? -1 : 1), 8, 13);
396:    fill(c0, c0 + len, genRow);
397:    if (Math.random() < 0.25 + diff * 0.4){
398:      const tier = rollTier(diff);
399:      world.enemies.push(mkEnemy((c0 + (len >> 1))*TILE, (genRow - 3)*TILE, tier));
401:    genCol += len;
411:    if (Math.random() < 0.3 + diff * 0.4)             // malá příšerka na zemi mezi plošinami
412:      world.enemies.push(mkEnemy((c0 + len - 2)*TILE, (r - 3)*TILE, Math.random() < 0.5 + diff * 0.3 ? 1 : 0));
413:    genCol += len;
595:    wallX = player.x - W*0.75;                     // odstrč zeď daleko dozadu
608:      wallX = camX - TILE*9;
609:      generateAhead(camX);
613:    wallSpeed = 1.3 + Math.min(distance / 280, 3.3);
614:    wallX += wallSpeed;
618:    if (camX < wallX - 30) camX = wallX - 30;      // zeď tlačí kameru
620:    if (camX - wallX > W*1.5) wallX = camX - W*1.5; // zeď nezůstane moc pozadu
622:    distance = Math.floor(camX / TILE);
623:    generateAhead(camX);
628:    const wgap = player.x - wallX;
644:let world, levelIndex = 0, lives = 3, coinsCollected = 0, score = 0, distance = 0;
645:let camX = 0, camY = 0, scrollSpeed = 1.4, endlessStarted = false;
646:let wallX = -400, wallSpeed = 1.3;          // zeď smrti v nekonečném módu
685:const SKINS = [
694:function store(k, v){ try { localStorage.setItem('hranaty_' + k, String(v)); } catch (e) {} }
697:let bestHS = parseInt(loadStr('hs', '0'), 10) || 0;
698:let equippedSkin = clamp(parseInt(loadStr('skin', '0'), 10) || 0, 0, SKINS.length - 1);
699:if (SKINS[equippedSkin].unlock > bestHS) equippedSkin = 0;   // pojistka
700:let skinSel = equippedSkin;
703:const skinUnlocked = i => SKINS[i].unlock <= bestHS;
704:const escore = () => mode === 'endless' ? score + distance : score;
707:  const s = escore();
708:  const old = bestHS;
709:  if (s > bestHS){ bestHS = s; store('hs', bestHS); newRecord = true; }
710:  justUnlocked = SKINS.filter(k => k.unlock > old && k.unlock <= bestHS).map(k => k.name);
717:  coinsCollected = 0; score = 0; distance = 0; shake = 0;
722:  else loadEndless();
739:    if (hit('ArrowLeft', 'KeyA'))  skinSel = (skinSel - 1 + SKINS.length) % SKINS.length;
740:    if (hit('ArrowRight', 'KeyD')) skinSel = (skinSel + 1) % SKINS.length;
741:    if (skinUnlocked(skinSel) && skinSel !== equippedSkin){
742:      equippedSkin = skinSel; store('skin', equippedSkin); S.coin();
771:  if (hit('KeyR'))   { mode === 'levels' ? loadLevel(levelIndex) : loadEndless(); return; }
792:function draw(){
824:    if (mode === 'endless' && endlessStarted && wallX > camX - 100) drawWall();
826:  ctx.restore();
830:    const gap = player.x - wallX;
875:  ctx.restore();
907:  ctx.restore();
917:  ctx.restore();
921:  const wx = wallX, top = -160, h = (ROWS + 6) * TILE;
950:    ctx.restore();
952:  ctx.restore();
971:  ctx.fillStyle = SKINS[equippedSkin].color;
1037:    ctx.fillText(distance + ' m', W - 16, 26);
1039:  ctx.fillText('Skóre ' + escore(), W - 16, 48);
1043:    ctx.fillText('rekord ' + bestHS, W - 16, 66);
1045:  ctx.restore();
1051:    : 'Skóre ' + escore() + '  ·  ' + distance + ' m  ·  ' + coinsCollected + ' ●';
1065:  ctx.restore();
1134:  ctx.restore();
1151:  ctx.fillText('Rekord v nekonečném běhu: ' + bestHS, W/2, H/2 - 66);
1164:  ctx.restore();
1168:  const n = SKINS.length, sw = 34, gap = 12;
1178:    const s = SKINS[i], open = skinUnlocked(i);
1183:    if (i === equippedSkin){
1187:    if (i === skinSel){
1199:  const s = SKINS[skinSel];
1203:    skinUnlocked(skinSel)
1204:      ? s.name + (skinSel === equippedSkin ? '  ✓ nasazeno' : '')
1273:    const n = SKINS.length, sw = 34, gap = 12, sy = H/2 + 118;
1277:        skinSel = i;
1278:        if (skinUnlocked(i) && i !== equippedSkin){ equippedSkin = i; store('skin', i); S.coin(); }


<total_tokens>14999207 tokens left</total_tokens>


taky bych chtěl aby byli velcí nepřátelé byli na dvě zašlápnutí. Taky udělěj u nekonečného módu tři obtížnosti(mometální jediná obtížnost bude střední takže udělej lehčí s méně mezerama s méně a menší…


183	  stomp: () => beep(190, 0.13, 'sawtooth', 0.05),
184	  hurt : () => { beep(200, 0.2, 'sawtooth', 0.06); setTimeout(() => beep(120, 0.3, 'sawtooth', 0.06), 90); },
185	  win  : () => [523,659,784,1046].forEach((f,i) => setTimeout(() => beep(f, 0.12, 'triangle', 0.05), i*95)),
186	};
187
188	/* ------------------------------------------------------------------ */
189	/*  Svět (dlaždicová mapa + entity)                                   */
190	/* ------------------------------------------------------------------ */
191	class World {
192	  constructor(){
193	    this.solid   = new Set();   // "col,row" pevných dlaždic
194	    this.coins   = [];
195	    this.enemies = [];
196	    this.width   = 0;           // šířka v dlaždicích (0 = nekonečno)
197	    this.goal    = null;
198	    this.playerStart = { x: TILE*2, y: TILE*10 };
199	  }
200	  setSolid(c,r){ if (r >= 0 && r < ROWS) this.solid.add(c + ',' + r); }
201	  isSolid(c,r){ return this.solid.has(c + ',' + r); }
202	  firstSolidRow(c, from = 0){
203	    for (let r = from; r < ROWS; r++) if (this.isSolid(c,r)) return r;
204	    return ROWS;
205	  }
206	}
207
208	/* Druhy příšerek: malá / střední / velká.
209	   Větší = pomalejší, ale dává víc mincí i bodů. */
210	const ETIER = [
211	  { w: 24, h: 20, spd: 1.15, coins: 1, score: 40,  col: '#ff8a98' },
212	  { w: 32, h: 30, spd: 0.95, coins: 3, score: 90,  col: '#ff5c72' },
213	  { w: 44, h: 42, spd: 0.75, coins: 6, score: 180, col: '#e03150' },
214	];
215
216	function mkEnemy(x, y, tier = 0){
217	  const td = ETIER[tier];
218	  return { x, y, w: td.w, h: td.h, tier,
219	           vx: (Math.random() < 0.5 ? -1 : 1) * td.spd, vy: 0, dead: false, t: 0 };
220	}
221
222	/* náhodný druh podle postupu (0 = začátek, 1 = konec / daleko) */
223	function rollTier(prog){
224	  const r = Math.random();
225	  const pBig = clamp((prog - 0.45) * 0.55, 0, 0.32);   // velké až později
226	  const pMed = clamp(0.12 + prog * 0.4, 0, 0.45);
227	  if (r < pBig) return 2;
228	  if (r < pBig + pMed) return 1;
229	  return 0;
230	}
231
232	/* poletující mince po zabití příšerky (jen efekt) */
233	let pops = [];
234	function spawnCoinPops(x, y, n){
235	  for (let i = 0; i < Math.min(n, 10); i++)
236	    pops.push({ x, y, vx: (Math.random() - 0.5) * 3.4, vy: -2 - Math.random() * 2.4, life: 34 });
237	}

  stomp: () => beep(190, 0.13, 'sawtooth', 0.05),
  hurt : () => { beep(200, 0.2, 'sawtooth', 0.06); setTimeout(() => beep(120, 0.3, 'sawtooth', 0.06), 90); },
  win  : () => [523,659,784,1046].forEach((f,i) => setTimeout(() => beep(f, 0.12, 'triangle', 0.05), i*95)),
};

/* ------------------------------------------------------------------ */
/*  Svět (dlaždicová mapa + entity)                                   */
/* ------------------------------------------------------------------ */
class World {
  constructor(){
    this.solid   = new Set();   // "col,row" pevných dlaždic
    this.coins   = [];
    this.enemies = [];
    this.width   = 0;           // šířka v dlaždicích (0 = nekonečno)
    this.goal    = null;
    this.playerStart = { x: TILE*2, y: TILE*10 };
  }
  setSolid(c,r){ if (r >= 0 && r < ROWS) this.solid.add(c + ',' + r); }
  isSolid(c,r){ return this.solid.has(c + ',' + r); }
  firstSolidRow(c, from = 0){
    for (let r = from; r < ROWS; r++) if (this.isSolid(c,r)) return r;
    return ROWS;
  }
}

/* Druhy příšerek: malá / střední / velká.
   Větší = pomalejší, ale dává víc mincí i bodů. */
const ETIER = [
  { w: 24, h: 20, spd: 1.15, coins: 1, score: 40,  col: '#ff8a98' },
  { w: 32, h: 30, spd: 0.95, coins: 3, score: 90,  col: '#ff5c72' },
  { w: 44, h: 42, spd: 0.75, coins: 6, score: 180, col: '#e03150' },
];

function mkEnemy(x, y, tier = 0){
  const td = ETIER[tier];
  return { x, y, w: td.w, h: td.h, tier,
           vx: (Math.random() < 0.5 ? -1 : 1) * td.spd, vy: 0, dead: false, t: 0 };
}

/* náhodný druh podle postupu (0 = začátek, 1 = konec / daleko) */
function rollTier(prog){
  const r = Math.random();
  const pBig = clamp((prog - 0.45) * 0.55, 0, 0.32);   // velké až později
  const pMed = clamp(0.12 + prog * 0.4, 0, 0.45);
  if (r < pBig) return 2;
  if (r < pBig + pMed) return 1;
  return 0;
}

/* poletující mince po zabití příšerky (jen efekt) */
let pops = [];
function spawnCoinPops(x, y, n){
  for (let i = 0; i < Math.min(n, 10); i++)
    pops.push({ x, y, vx: (Math.random() - 0.5) * 3.4, vy: -2 - Math.random() * 2.4, life: 34 });
}


<total_tokens>14996268 tokens left</total_tokens>


290	    if ((d.pits || []).some(([a,b]) => c >= a && c <= b)) continue;
291	    for (let r = fr; r < ROWS; r++) w.setSolid(c, r);
292	  }
293	  for (const [c,r,len] of (d.platforms || []))
294	    for (let i = 0; i < len; i++) w.setSolid(c + i, r);
295
296	  for (const [c,r] of (d.coins || []))
297	    w.coins.push({ x: c*TILE + TILE/2, y: r*TILE + TILE/2, got: false });
298
299	  const addEnemy = (c, fromRow, tier) => {
300	    const g = w.firstSolidRow(c, fromRow);
301	    if (g >= ROWS) return;
302	    if (tier === 2 && g < fr) tier = 1;            // velká jen na hlavní podlaze
303	    w.enemies.push(mkEnemy(c*TILE + (TILE - ETIER[tier].w) / 2, g*TILE - ETIER[tier].h, tier));
304	  };
305
306	  // ručně rozmístěné příšerky – druh podle postupu v příběhu
307	  for (const [c,r] of (d.enemies || [])) addEnemy(c, r, rollTier(prog));
308
309	  // příšerky navíc – čím dál v příběhu, tím víc
310	  const extra = idx * 2;
311	  for (let k = 0; k < extra; k++){
312	    const c = 6 + Math.floor(Math.random() * Math.max(1, d.width - 12));
313	    if (Math.abs(c - d.playerStart[0]) < 6) continue;
314	    if (d.goal && Math.abs(c - d.goal[0]) < 4) continue;
315	    if ((d.pits || []).some(([a,b]) => c >= a - 1 && c <= b + 1)) continue;
316	    addEnemy(c, fr, rollTier(prog));
317	  }
318
319	  const ps = w.firstSolidRow(d.playerStart[0], d.playerStart[1]);
320	  w.playerStart = { x: d.playerStart[0]*TILE, y: ps*TILE - player.h };
321
322	  const gg = w.firstSolidRow(d.goal[0], d.goal[1]);
323	  w.goal = { x: d.goal[0]*TILE, y: gg*TILE };
324	  return w;
325	}
326
327	/* ------------------------------------------------------------------ */
328	/*  Nekonečný běh – procedurální generování                           */
329	/* ------------------------------------------------------------------ */
330	let genCol = 0, genRow = 12;
331
332	function loadEndless(){
333	  world = new World();
334	  world.width = 0;
335	  pops = [];
336	  genCol = 0; genRow = 12;
337	  generateAhead(0);
338	  player.x = 3*TILE;
339	  player.y = 11*TILE - player.h;
340	  player.vx = player.vy = 0; player.invuln = 40;
341	  camX = 0; camY = 0;
342	  scrollSpeed = 1.4;
343	  wallSpeed = 1.3;
344	  wallX = -TILE*12;
345	  endlessStarted = false;
346	}
347
348	function generateAhead(cx){
349	  const need = Math.floor((cx + W) / TILE) + 22;
350	  let guard = 0;
351	  while (genCol < need && guard++ < 400) genChunk();
352	}
353
354	function genChunk(){
355	  const c0 = genCol;
356	  const r  = genRow;
357	  const diff = Math.min(distance / 500, 1);           // 0 → 1 podle ušlé vzdálenosti
358	  const fill = (a, b, top) => { for (let c = a; c < b; c++) for (let rr = top; rr < ROWS; rr++) world.setSolid(c, rr); };
359
360	  if (genCol < 14){ fill(c0, c0 + 14, r); genCol += 14; return; }   // bezpečný start
361
362	  const t = Math.random();
363
364	  if (t < 0.20 + diff * 0.12){                        // propast
365	    let gap = 2 + Math.floor(Math.random() * (1 + Math.round(diff * 3)));
366	    gap = Math.min(gap, 4);
367	    if (gap >= 4 || Math.random() < 0.45){            // plošina nad propastí
368	      const pc = c0 + 1, plen = Math.max(2, gap - 1);
369	      for (let i = 0; i < plen; i++) world.setSolid(pc + i, r - 3);
370	      for (let i = 0; i < plen; i++)
371	        world.coins.push({ x: (pc + i)*TILE + TILE/2, y: (r - 4)*TILE + TILE/2, got: false });
372	    }
373	    genCol += gap;
374
375	  } else if (t < 0.52){                               // rovinka s mincemi / nepřáteli
376	    const len = 5 + Math.floor(Math.random() * 6);
377	    fill(c0, c0 + len, r);
378	    // počet nepřátel roste s ušlou vzdáleností
379	    let n = 0;
380	    if (Math.random() < 0.4 + diff * 0.4) n = 1;
381	    if (n && Math.random() < diff * 0.6) n = 2;
382	    if (n === 2 && len < 7) n = 1;
383	    for (let e = 0; e < n; e++){
384	      const ec = c0 + 1 + Math.floor((e + 0.5) / n * (len - 2));
385	      const tier = rollTier(diff);
386	      world.enemies.push(mkEnemy(ec*TILE + (TILE - ETIER[tier].w) / 2, (r - 3)*TILE, tier));
387	    }
388	    if (Math.random() < 0.6)
389	      for (let i = 0; i < Math.min(4, len - 1); i++)
390	        world.coins.push({ x: (c0 + 1 + i)*TILE + TILE/2, y: (r - 2)*TILE + TILE/2, got: false });
391	    genCol += len;
392
393	  } else if (t < 0.76){                               // schod nahoru / dolů
394	    genRow = clamp(r + (Math.random() < 0.5 ? -1 : 1), 8, 13);
395	    const len = 4 + Math.floor(Math.random() * 4);
396	    fill(c0, c0 + len, genRow);
397	    if (Math.random() < 0.25 + diff * 0.4){
398	      const tier = rollTier(diff);
399	      world.enemies.push(mkEnemy((c0 + (len >> 1))*TILE, (genRow - 3)*TILE, tier));
400	    }
401	    genCol += len;
402
403	  } else {                                            // plošinková sekce
404	    const len = 6 + Math.floor(Math.random() * 4);
405	    fill(c0, c0 + len, r);
406	    for (let k = 0; k < 2; k++){
407	      const pc = c0 + 2 + k * 3, pr = r - 3 - (k & 1);
408	      world.setSolid(pc, pr); world.setSolid(pc + 1, pr);
409	      world.coins.push({ x: pc*TILE + TILE, y: (pr - 1)*TILE + TILE/2, got: false });
410	    }
411	    if (Math.random() < 0.3 + diff * 0.4)             // malá příšerka na zemi mezi plošinami
412	      world.enemies.push(mkEnemy((c0 + len - 2)*TILE, (r - 3)*TILE, Math.random() < 0.5 + diff * 0.3 ? 1 : 0));
413	    genCol += len;
414	  }
415	}
416
417	let pruneT = 0;
418	function pruneWorld(){
419	  if (++pruneT < 120) return;

    if ((d.pits || []).some(([a,b]) => c >= a && c <= b)) continue;
    for (let r = fr; r < ROWS; r++) w.setSolid(c, r);
  }
  for (const [c,r,len] of (d.platforms || []))
    for (let i = 0; i < len; i++) w.setSolid(c + i, r);

  for (const [c,r] of (d.coins || []))
    w.coins.push({ x: c*TILE + TILE/2, y: r*TILE + TILE/2, got: false });

  const addEnemy = (c, fromRow, tier) => {
    const g = w.firstSolidRow(c, fromRow);
    if (g >= ROWS) return;
    if (tier === 2 && g < fr) tier = 1;            // velká jen na hlavní podlaze
    w.enemies.push(mkEnemy(c*TILE + (TILE - ETIER[tier].w) / 2, g*TILE - ETIER[tier].h, tier));
  };

  // ručně rozmístěné příšerky – druh podle postupu v příběhu
  for (const [c,r] of (d.enemies || [])) addEnemy(c, r, rollTier(prog));

  // příšerky navíc – čím dál v příběhu, tím víc
  const extra = idx * 2;
  for (let k = 0; k < extra; k++){
    const c = 6 + Math.floor(Math.random() * Math.max(1, d.width - 12));
    if (Math.abs(c - d.playerStart[0]) < 6) continue;
    if (d.goal && Math.abs(c - d.goal[0]) < 4) continue;
    if ((d.pits || []).some(([a,b]) => c >= a - 1 && c <= b + 1)) continue;
    addEnemy(c, fr, rollTier(prog));
  }

  const ps = w.firstSolidRow(d.playerStart[0], d.playerStart[1]);
  w.playerStart = { x: d.playerStart[0]*TILE, y: ps*TILE - player.h };

  const gg = w.firstSolidRow(d.goal[0], d.goal[1]);
  w.goal = { x: d.goal[0]*TILE, y: gg*TILE };
  return w;
}

/* ------------------------------------------------------------------ */
/*  Nekonečný běh – procedurální generování                           */
/* ------------------------------------------------------------------ */
let genCol = 0, genRow = 12;

function loadEndless(){
  world = new World();
  world.width = 0;
  pops = [];
  genCol = 0; genRow = 12;
  generateAhead(0);
  player.x = 3*TILE;
  player.y = 11*TILE - player.h;
  player.vx = player.vy = 0; player.invuln = 40;
  camX = 0; camY = 0;
  scrollSpeed = 1.4;
  wallSpeed = 1.3;
  wallX = -TILE*12;
  endlessStarted = false;
}

function generateAhead(cx){
  const need = Math.floor((cx + W) / TILE) + 22;
  let guard = 0;
  while (genCol < need && guard++ < 400) genChunk();
}

function genChunk(){
  const c0 = genCol;
  const r  = genRow;
  const diff = Math.min(distance / 500, 1);           // 0 → 1 podle ušlé vzdálenosti
  const fill = (a, b, top) => { for (let c = a; c < b; c++) for (let rr = top; rr < ROWS; rr++) world.setSolid(c, rr); };

  if (genCol < 14){ fill(c0, c0 + 14, r); genCol += 14; return; }   // bezpečný start

  const t = Math.random();

  if (t < 0.20 + diff * 0.12){                        // propast
    let gap = 2 + Math.floor(Math.random() * (1 + Math.round(diff * 3)));
    gap = Math.min(gap, 4);
    if (gap >= 4 || Math.random() < 0.45){            // plošina nad propastí
      const pc = c0 + 1, plen = Math.max(2, gap - 1);
      for (let i = 0; i < plen; i++) world.setSolid(pc + i, r - 3);
      for (let i = 0; i < plen; i++)
        world.coins.push({ x: (pc + i)*TILE + TILE/2, y: (r - 4)*TILE + TILE/2, got: false });
    }
    genCol += gap;

  } else if (t < 0.52){                               // rovinka s mincemi / nepřáteli
    const len = 5 + Math.floor(Math.random() * 6);
    fill(c0, c0 + len, r);
    // počet nepřátel roste s ušlou vzdáleností
    let n = 0;
    if (Math.random() < 0.4 + diff * 0.4) n = 1;
    if (n && Math.random() < diff * 0.6) n = 2;
    if (n === 2 && len < 7) n = 1;
    for (let e = 0; e < n; e++){
      const ec = c0 + 1 + Math.floor((e + 0.5) / n * (len - 2));
      const tier = rollTier(diff);
      world.enemies.push(mkEnemy(ec*TILE + (TILE - ETIER[tier].w) / 2, (r - 3)*TILE, tier));
    }
    if (Math.random() < 0.6)
      for (let i = 0; i < Math.min(4, len - 1); i++)
        world.coins.push({ x: (c0 + 1 + i)*TILE + TILE/2, y: (r - 2)*TILE + TILE/2, got: false });
    genCol += len;

  } else if (t < 0.76){                               // schod nahoru / dolů
    genRow = clamp(r + (Math.random() < 0.5 ? -1 : 1), 8, 13);
    const len = 4 + Math.floor(Math.random() * 4);
    fill(c0, c0 + len, genRow);
    if (Math.random() < 0.25 + diff * 0.4){
      const tier = rollTier(diff);
      world.enemies.push(mkEnemy((c0 + (len >> 1))*TILE, (genRow - 3)*TILE, tier));
    }
    genCol += len;

  } else {                                            // plošinková sekce
    const len = 6 + Math.floor(Math.random() * 4);
    fill(c0, c0 + len, r);
    for (let k = 0; k < 2; k++){
      const pc = c0 + 2 + k * 3, pr = r - 3 - (k & 1);
      world.setSolid(pc, pr); world.setSolid(pc + 1, pr);
      world.coins.push({ x: pc*TILE + TILE, y: (pr - 1)*TILE + TILE/2, got: false });
    }
    if (Math.random() < 0.3 + diff * 0.4)             // malá příšerka na zemi mezi plošinami
      world.enemies.push(mkEnemy((c0 + len - 2)*TILE, (r - 3)*TILE, Math.random() < 0.5 + diff * 0.3 ? 1 : 0));
    genCol += len;
  }
}

let pruneT = 0;
function pruneWorld(){
  if (++pruneT < 120) return;


<total_tokens>14994929 tokens left</total_tokens>


540	      e.dead = true; e.t = 0;
541	      player.vy = -10 - (e.tier || 0) * 1.5;          // z větší příšery větší odraz
542	      score += td.score;
543	      coinsCollected += td.coins;                     // odměna podle velikosti
544	      shake = 3 + (e.tier || 0) * 3;
545	      spawnCoinPops(e.x + e.w / 2, e.y, td.coins);
546	      beep(200 - (e.tier || 0) * 40, 0.12 + (e.tier || 0) * 0.03, 'sawtooth', 0.05);
547	    } else {
548	      hurtPlayer();
549	    }
550	  }
551	  // cíl
552	  if (world.goal && mode === 'levels' &&
553	      rectOverlap(player.x, player.y, player.w, player.h,
554	                  world.goal.x, world.goal.y - TILE*2.2, TILE, TILE*2.2)){
555	    S.win();
556	    score += 200 + lives * 100;
557	    levelIndex++;
558	    if (levelIndex >= LEVELS.length){ state = 'win'; }
559	    else { state = 'shop'; shopSel = 0; }
560	  }
561	  // pád mimo svět
562	  if (player.y > (ROWS + 1) * TILE) hurtPlayer();
563	}
564
565	function hurtPlayer(){
566	  if (player.invuln > 0) return;
567	  lives--;
568	  shake = 9;
569	  S.hurt();
570	  if (lives <= 0){
571	    state = 'over';
572	    if (mode === 'endless') saveBest();
573	    return;
574	  }
575	  player.invuln = 100;
576	  respawn();
577	}
578
579	function respawn(){
580	  player.vx = player.vy = 0;
581	  if (mode === 'levels'){
582	    player.x = world.playerStart.x;
583	    player.y = world.playerStart.y;
584	    camX = clamp(player.x - W*0.42, 0, world.width*TILE - W);
585	  } else {
586	    const col = Math.floor((camX + W*0.35) / TILE);
587	    let g = world.firstSolidRow(col, 3);
588	    if (g >= ROWS){
589	      for (let c = col - 2; c < col + 4; c++) for (let r = 12; r < ROWS; r++) world.setSolid(c, r);
590	      g = 12;
591	    }
592	    player.x = col*TILE;
593	    player.y = g*TILE - player.h - 2;
594	    camX = Math.max(0, player.x - W*0.4);
595	    wallX = player.x - W*0.75;                     // odstrč zeď daleko dozadu
596	    world.enemies = world.enemies.filter(e => Math.abs(e.x - player.x) > 130);
597	  }
598	}
599
600	/* ------------------------------------------------------------------ */
601	/*  Kamera                                                            */
602	/* ------------------------------------------------------------------ */
603	function updateCamera(){
604	  if (mode === 'endless'){
605	    if (!endlessStarted){
606	      if (down('ArrowRight', 'KeyD') || player.x > 5*TILE) endlessStarted = true;
607	      camX = Math.max(0, player.x + player.w/2 - W*0.42);
608	      wallX = camX - TILE*9;
609	      generateAhead(camX);
610	      return;
611	    }
612	    // zeď se plíží zprava doleva… teda zleva doprava, a zrychluje
613	    wallSpeed = 1.3 + Math.min(distance / 280, 3.3);
614	    wallX += wallSpeed;
615
616	    const follow = player.x + player.w/2 - W*0.42;
617	    camX += (follow - camX) * 0.16;
618	    if (camX < wallX - 30) camX = wallX - 30;      // zeď tlačí kameru
619	    if (camX < 0) camX = 0;
620	    if (camX - wallX > W*1.5) wallX = camX - W*1.5; // zeď nezůstane moc pozadu
621
622	    distance = Math.floor(camX / TILE);
623	    generateAhead(camX);
624	    pruneWorld();
625	    world.enemies = world.enemies.filter(e => e.x > camX - 300);
626	    world.coins   = world.coins.filter(c => !c.got && c.x > camX - 200);
627
628	    const wgap = player.x - wallX;
629	    if (wgap <= 0 && player.invuln <= 0) hurtPlayer();
630	    else if (wgap < 150 && pruneT % 15 === 0) beep(150, 0.05, 'square', 0.03);
631	    return;
632	  }
633	  const target = player.x + player.w/2 - W*0.42;
634	  camX += (target - camX) * 0.14;
635	  camX = clamp(camX, 0, world.width*TILE - W);
636	}
637
638	/* ------------------------------------------------------------------ */
639	/*  Stavy hry                                                         */
640	/* ------------------------------------------------------------------ */
641	let state = 'menu';      // menu | play | paused | shop | over | win
642	let mode  = 'levels';    // levels | endless
643	let menuSel = 0;
644	let world, levelIndex = 0, lives = 3, coinsCollected = 0, score = 0, distance = 0;
645	let camX = 0, camY = 0, scrollSpeed = 1.4, endlessStarted = false;
646	let wallX = -400, wallSpeed = 1.3;          // zeď smrti v nekonečném módu
647	let shake = 0;
648	const MAX_LIVES = 5;
649
650	/* --- Obchod mezi levely (později přibudou další položky) --- */
651	let shopSel = 0;
652	const SHOP_X = W/2 - 235, SHOP_W = 470, SHOP_ROW_H = 62, SHOP_GAP = 10, SHOP_Y0 = 185;
653	const shopBtnY = () => SHOP_Y0 + SHOP.length * (SHOP_ROW_H + SHOP_GAP) + 6;
654
655	const SHOP = [
656	  {
657	    name: 'Život navíc',
658	    desc: '+1 život (max ' + MAX_LIVES + ')',
659	    base: 10,

      e.dead = true; e.t = 0;
      player.vy = -10 - (e.tier || 0) * 1.5;          // z větší příšery větší odraz
      score += td.score;
      coinsCollected += td.coins;                     // odměna podle velikosti
      shake = 3 + (e.tier || 0) * 3;
      spawnCoinPops(e.x + e.w / 2, e.y, td.coins);
      beep(200 - (e.tier || 0) * 40, 0.12 + (e.tier || 0) * 0.03, 'sawtooth', 0.05);
    } else {
      hurtPlayer();
    }
  }
  // cíl
  if (world.goal && mode === 'levels' &&
      rectOverlap(player.x, player.y, player.w, player.h,
                  world.goal.x, world.goal.y - TILE*2.2, TILE, TILE*2.2)){
    S.win();
    score += 200 + lives * 100;
    levelIndex++;
    if (levelIndex >= LEVELS.length){ state = 'win'; }
    else { state = 'shop'; shopSel = 0; }
  }
  // pád mimo svět
  if (player.y > (ROWS + 1) * TILE) hurtPlayer();
}

function hurtPlayer(){
  if (player.invuln > 0) return;
  lives--;
  shake = 9;
  S.hurt();
  if (lives <= 0){
    state = 'over';
    if (mode === 'endless') saveBest();
    return;
  }
  player.invuln = 100;
  respawn();
}

function respawn(){
  player.vx = player.vy = 0;
  if (mode === 'levels'){
    player.x = world.playerStart.x;
    player.y = world.playerStart.y;
    camX = clamp(player.x - W*0.42, 0, world.width*TILE - W);
  } else {
    const col = Math.floor((camX + W*0.35) / TILE);
    let g = world.firstSolidRow(col, 3);
    if (g >= ROWS){
      for (let c = col - 2; c < col + 4; c++) for (let r = 12; r < ROWS; r++) world.setSolid(c, r);
      g = 12;
    }
    player.x = col*TILE;
    player.y = g*TILE - player.h - 2;
    camX = Math.max(0, player.x - W*0.4);
    wallX = player.x - W*0.75;                     // odstrč zeď daleko dozadu
    world.enemies = world.enemies.filter(e => Math.abs(e.x - player.x) > 130);
  }
}

/* ------------------------------------------------------------------ */
/*  Kamera                                                            */
/* ------------------------------------------------------------------ */
function updateCamera(){
  if (mode === 'endless'){
    if (!endlessStarted){
      if (down('ArrowRight', 'KeyD') || player.x > 5*TILE) endlessStarted = true;
      camX = Math.max(0, player.x + player.w/2 - W*0.42);
      wallX = camX - TILE*9;
      generateAhead(camX);
      return;
    }
    // zeď se plíží zprava doleva… teda zleva doprava, a zrychluje
    wallSpeed = 1.3 + Math.min(distance / 280, 3.3);
    wallX += wallSpeed;

    const follow = player.x + player.w/2 - W*0.42;
    camX += (follow - camX) * 0.16;
    if (camX < wallX - 30) camX = wallX - 30;      // zeď tlačí kameru
    if (camX < 0) camX = 0;
    if (camX - wallX > W*1.5) wallX = camX - W*1.5; // zeď nezůstane moc pozadu

    distance = Math.floor(camX / TILE);
    generateAhead(camX);
    pruneWorld();
    world.enemies = world.enemies.filter(e => e.x > camX - 300);
    world.coins   = world.coins.filter(c => !c.got && c.x > camX - 200);

    const wgap = player.x - wallX;
    if (wgap <= 0 && player.invuln <= 0) hurtPlayer();
    else if (wgap < 150 && pruneT % 15 === 0) beep(150, 0.05, 'square', 0.03);
    return;
  }
  const target = player.x + player.w/2 - W*0.42;
  camX += (target - camX) * 0.14;
  camX = clamp(camX, 0, world.width*TILE - W);
}

/* ------------------------------------------------------------------ */
/*  Stavy hry                                                         */
/* ------------------------------------------------------------------ */
let state = 'menu';      // menu | play | paused | shop | over | win
let mode  = 'levels';    // levels | endless
let menuSel = 0;
let world, levelIndex = 0, lives = 3, coinsCollected = 0, score = 0, distance = 0;
let camX = 0, camY = 0, scrollSpeed = 1.4, endlessStarted = false;
let wallX = -400, wallSpeed = 1.3;          // zeď smrti v nekonečném módu
let shake = 0;
const MAX_LIVES = 5;

/* --- Obchod mezi levely (později přibudou další položky) --- */
let shopSel = 0;
const SHOP_X = W/2 - 235, SHOP_W = 470, SHOP_ROW_H = 62, SHOP_GAP = 10, SHOP_Y0 = 185;
const shopBtnY = () => SHOP_Y0 + SHOP.length * (SHOP_ROW_H + SHOP_GAP) + 6;

const SHOP = [
  {
    name: 'Život navíc',
    desc: '+1 život (max ' + MAX_LIVES + ')',
    base: 10,


<total_tokens>14992001 tokens left</total_tokens>


660	    bought: 0,
661	    price(){ return this.base * Math.pow(2, this.bought); },
662	    canBuy(){ return lives < MAX_LIVES && coinsCollected >= this.price(); },
663	    apply(){ lives++; },
664	  },
665	];
666
667	function buyShop(i){
668	  const it = SHOP[i];
669	  if (!it) return;
670	  if (!it.canBuy()){ beep(120, 0.09, 'square', 0.04); return; }
671	  coinsCollected -= it.price();
672	  it.bought++;
673	  it.apply();
674	  S.coin();
675	}
676	function shopContinue(){
677	  loadLevel(levelIndex);   // levelIndex je už zvýšený
678	  state = 'play';
679	}
680
681	/* ------------------------------------------------------------------ */
682	/*  Rekord + barvy postavy (uloženo v prohlížeči přes localStorage)   */
683	/*  unlock = kolik bodů v nekonečném běhu je potřeba k odemčení       */
684	/* ------------------------------------------------------------------ */
685	const SKINS = [
686	  { name: 'Nebe',     color: '#5cc8ff', unlock: 0 },
687	  { name: 'Limetka',  color: '#4ade80', unlock: 250 },
688	  { name: 'Meruňka',  color: '#ffb454', unlock: 600 },
689	  { name: 'Malina',   color: '#ff6ea8', unlock: 1200 },
690	  { name: 'Ametyst',  color: '#b98cff', unlock: 2500 },
691	  { name: 'Zlato',    color: '#ffd45c', unlock: 5000 },
692	];
693
694	function store(k, v){ try { localStorage.setItem('hranaty_' + k, String(v)); } catch (e) {} }
695	function loadStr(k, d){ try { const v = localStorage.getItem('hranaty_' + k); return v == null ? d : v; } catch (e) { return d; } }
696
697	let bestHS = parseInt(loadStr('hs', '0'), 10) || 0;
698	let equippedSkin = clamp(parseInt(loadStr('skin', '0'), 10) || 0, 0, SKINS.length - 1);
699	if (SKINS[equippedSkin].unlock > bestHS) equippedSkin = 0;   // pojistka
700	let skinSel = equippedSkin;
701	let newRecord = false, justUnlocked = [];
702
703	const skinUnlocked = i => SKINS[i].unlock <= bestHS;
704	const escore = () => mode === 'endless' ? score + distance : score;
705
706	function saveBest(){
707	  const s = escore();
708	  const old = bestHS;
709	  if (s > bestHS){ bestHS = s; store('hs', bestHS); newRecord = true; }
710	  justUnlocked = SKINS.filter(k => k.unlock > old && k.unlock <= bestHS).map(k => k.name);
711	}
712
713	function startGame(m){
714	  unlockAudio();
715	  mode = m;
716	  lives = m === 'endless' ? 1 : 3;   // nekonečný běh: jeden život
717	  coinsCollected = 0; score = 0; distance = 0; shake = 0;
718	  shopSel = 0; pops = [];
719	  newRecord = false; justUnlocked = [];
720	  SHOP.forEach(it => it.bought = 0);
721	  if (m === 'levels'){ levelIndex = 0; loadLevel(0); }
722	  else loadEndless();
723	  state = 'play';
724	}
725
726	function loadLevel(i){
727	  world = buildLevel(LEVELS[i], i);
728	  pops = [];
729	  player.x = world.playerStart.x;
730	  player.y = world.playerStart.y;
731	  player.vx = player.vy = 0; player.invuln = 40;
732	  camX = clamp(player.x - W*0.42, 0, world.width*TILE - W);
733	  camY = 0;
734	}
735
736	function update(){
737	  if (state === 'menu'){
738	    if (hit('ArrowUp', 'ArrowDown', 'KeyW', 'KeyS')) menuSel = 1 - menuSel;
739	    if (hit('ArrowLeft', 'KeyA'))  skinSel = (skinSel - 1 + SKINS.length) % SKINS.length;
740	    if (hit('ArrowRight', 'KeyD')) skinSel = (skinSel + 1) % SKINS.length;
741	    if (skinUnlocked(skinSel) && skinSel !== equippedSkin){
742	      equippedSkin = skinSel; store('skin', equippedSkin); S.coin();
743	    }
744	    if (hit('Digit1')) return startGame('levels');
745	    if (hit('Digit2')) return startGame('endless');
746	    if (hit('Enter', 'Space')) return startGame(menuSel === 0 ? 'levels' : 'endless');
747	    return;
748	  }
749	  if (state === 'over' || state === 'win'){
750	    if (hit('Enter', 'Space', 'Escape')) state = 'menu';
751	    return;
752	  }
753	  if (state === 'paused'){
754	    if (hit('KeyP', 'Enter')) state = 'play';
755	    if (hit('Escape')) state = 'menu';
756	    return;
757	  }
758	  if (state === 'shop'){
759	    if (hit('ArrowUp', 'KeyW'))   shopSel = (shopSel - 1 + SHOP.length) % SHOP.length;
760	    if (hit('ArrowDown', 'KeyS')) shopSel = (shopSel + 1) % SHOP.length;
761	    if (hit('Enter')) buyShop(shopSel);
762	    for (let i = 0; i < SHOP.length && i < 9; i++)
763	      if (hit('Digit' + (i + 1))) buyShop(i);
764	    if (hit('Space')) shopContinue();
765	    return;
766	  }
767
768	  // --- state === 'play' ---
769	  if (hit('Escape')) { state = 'menu'; return; }
770	  if (hit('KeyP'))   { state = 'paused'; return; }
771	  if (hit('KeyR'))   { mode === 'levels' ? loadLevel(levelIndex) : loadEndless(); return; }
772
773	  if (shake > 0) shake *= 0.88;
774
775	  updatePlayer();
776	  for (const e of world.enemies) updateEnemy(e);
777	  handleInteractions();
778	  if (state === 'play') updateCamera();
779

    bought: 0,
    price(){ return this.base * Math.pow(2, this.bought); },
    canBuy(){ return lives < MAX_LIVES && coinsCollected >= this.price(); },
    apply(){ lives++; },
  },
];

function buyShop(i){
  const it = SHOP[i];
  if (!it) return;
  if (!it.canBuy()){ beep(120, 0.09, 'square', 0.04); return; }
  coinsCollected -= it.price();
  it.bought++;
  it.apply();
  S.coin();
}
function shopContinue(){
  loadLevel(levelIndex);   // levelIndex je už zvýšený
  state = 'play';
}

/* ------------------------------------------------------------------ */
/*  Rekord + barvy postavy (uloženo v prohlížeči přes localStorage)   */
/*  unlock = kolik bodů v nekonečném běhu je potřeba k odemčení       */
/* ------------------------------------------------------------------ */
const SKINS = [
  { name: 'Nebe',     color: '#5cc8ff', unlock: 0 },
  { name: 'Limetka',  color: '#4ade80', unlock: 250 },
  { name: 'Meruňka',  color: '#ffb454', unlock: 600 },
  { name: 'Malina',   color: '#ff6ea8', unlock: 1200 },
  { name: 'Ametyst',  color: '#b98cff', unlock: 2500 },
  { name: 'Zlato',    color: '#ffd45c', unlock: 5000 },
];

function store(k, v){ try { localStorage.setItem('hranaty_' + k, String(v)); } catch (e) {} }
function loadStr(k, d){ try { const v = localStorage.getItem('hranaty_' + k); return v == null ? d : v; } catch (e) { return d; } }

let bestHS = parseInt(loadStr('hs', '0'), 10) || 0;
let equippedSkin = clamp(parseInt(loadStr('skin', '0'), 10) || 0, 0, SKINS.length - 1);
if (SKINS[equippedSkin].unlock > bestHS) equippedSkin = 0;   // pojistka
let skinSel = equippedSkin;
let newRecord = false, justUnlocked = [];

const skinUnlocked = i => SKINS[i].unlock <= bestHS;
const escore = () => mode === 'endless' ? score + distance : score;

function saveBest(){
  const s = escore();
  const old = bestHS;
  if (s > bestHS){ bestHS = s; store('hs', bestHS); newRecord = true; }
  justUnlocked = SKINS.filter(k => k.unlock > old && k.unlock <= bestHS).map(k => k.name);
}

function startGame(m){
  unlockAudio();
  mode = m;
  lives = m === 'endless' ? 1 : 3;   // nekonečný běh: jeden život
  coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0; pops = [];
  newRecord = false; justUnlocked = [];
  SHOP.forEach(it => it.bought = 0);
  if (m === 'levels'){ levelIndex = 0; loadLevel(0); }
  else loadEndless();
  state = 'play';
}

function loadLevel(i){
  world = buildLevel(LEVELS[i], i);
  pops = [];
  player.x = world.playerStart.x;
  player.y = world.playerStart.y;
  player.vx = player.vy = 0; player.invuln = 40;
  camX = clamp(player.x - W*0.42, 0, world.width*TILE - W);
  camY = 0;
}

function update(){
  if (state === 'menu'){
    if (hit('ArrowUp', 'ArrowDown', 'KeyW', 'KeyS')) menuSel = 1 - menuSel;
    if (hit('ArrowLeft', 'KeyA'))  skinSel = (skinSel - 1 + SKINS.length) % SKINS.length;
    if (hit('ArrowRight', 'KeyD')) skinSel = (skinSel + 1) % SKINS.length;
    if (skinUnlocked(skinSel) && skinSel !== equippedSkin){
      equippedSkin = skinSel; store('skin', equippedSkin); S.coin();
    }
    if (hit('Digit1')) return startGame('levels');
    if (hit('Digit2')) return startGame('endless');
    if (hit('Enter', 'Space')) return startGame(menuSel === 0 ? 'levels' : 'endless');
    return;
  }
  if (state === 'over' || state === 'win'){
    if (hit('Enter', 'Space', 'Escape')) state = 'menu';
    return;
  }
  if (state === 'paused'){
    if (hit('KeyP', 'Enter')) state = 'play';
    if (hit('Escape')) state = 'menu';
    return;
  }
  if (state === 'shop'){
    if (hit('ArrowUp', 'KeyW'))   shopSel = (shopSel - 1 + SHOP.length) % SHOP.length;
    if (hit('ArrowDown', 'KeyS')) shopSel = (shopSel + 1) % SHOP.length;
    if (hit('Enter')) buyShop(shopSel);
    for (let i = 0; i < SHOP.length && i < 9; i++)
      if (hit('Digit' + (i + 1))) buyShop(i);
    if (hit('Space')) shopContinue();
    return;
  }

  // --- state === 'play' ---
  if (hit('Escape')) { state = 'menu'; return; }
  if (hit('KeyP'))   { state = 'paused'; return; }
  if (hit('KeyR'))   { mode === 'levels' ? loadLevel(levelIndex) : loadEndless(); return; }

  if (shake > 0) shake *= 0.88;

  updatePlayer();
  for (const e of world.enemies) updateEnemy(e);
  handleInteractions();
  if (state === 'play') updateCamera();


<total_tokens>14989608 tokens left</total_tokens>


521	  // mince
522	  for (const co of world.coins){
523	    if (co.got) continue;
524	    if (Math.abs(player.x + player.w/2 - co.x) < 18 &&
525	        Math.abs(player.y + player.h/2 - co.y) < 20){
526	      co.got = true; coinsCollected++; score += 10; S.coin();
527	    }
528	  }
529	  // nepřátelé
530	  for (const e of world.enemies){
531	    if (e.dead) continue;
532	    if (!rectOverlap(player.x, player.y, player.w, player.h, e.x, e.y, e.w, e.h)) continue;
533	    // dopad shora = zničíš nepřítele (i rohem). Zabije tě jen zásah z boku / zespodu.
534	    const feet = player.y + player.h;
535	    const fromAbove = player.vy > 0 &&
536	      ((player.prevBottom ?? feet) <= e.y + 8 || feet - e.y < e.h * 0.5 + 6);
537	    const stomp = fromAbove;
538	    if (stomp){
539	      const td = ETIER[e.tier || 0];
540	      e.dead = true; e.t = 0;

  // mince
  for (const co of world.coins){
    if (co.got) continue;
    if (Math.abs(player.x + player.w/2 - co.x) < 18 &&
        Math.abs(player.y + player.h/2 - co.y) < 20){
      co.got = true; coinsCollected++; score += 10; S.coin();
    }
  }
  // nepřátelé
  for (const e of world.enemies){
    if (e.dead) continue;
    if (!rectOverlap(player.x, player.y, player.w, player.h, e.x, e.y, e.w, e.h)) continue;
    // dopad shora = zničíš nepřítele (i rohem). Zabije tě jen zásah z boku / zespodu.
    const feet = player.y + player.h;
    const fromAbove = player.vy > 0 &&
      ((player.prevBottom ?? feet) <= e.y + 8 || feet - e.y < e.h * 0.5 + 6);
    const stomp = fromAbove;
    if (stomp){
      const td = ETIER[e.tier || 0];
      e.dead = true; e.t = 0;


<total_tokens>14987041 tokens left</total_tokens>


955	function drawTile(c, r){
956	  const x = c*TILE, y = r*TILE;
957	  const open = !world.isSolid(c, r - 1);
958	  ctx.fillStyle = COL.tile;
959	  roundRect(x + 1, y + 1, TILE - 2, TILE - 2, open ? 7 : 2);
960	  ctx.fill();
961	  if (open){
962	    ctx.fillStyle = COL.tileTop;
963	    roundRect(x + 1, y + 1, TILE - 2, 6, 3);
964	    ctx.fill();
965	  }
966	}
967
968	function drawPlayer(){
969	  if (player.invuln > 0 && Math.floor(player.invuln / 4) % 2) return;
970	  const { x, y, w, h } = player;
971	  ctx.fillStyle = SKINS[equippedSkin].color;
972	  roundRect(x, y, w, h, 7); ctx.fill();
973	  ctx.fillStyle = COL.dark;
974	  ctx.fillRect(x + w/2 + player.face * 5 - 2, y + 9, 4, 7);
975	}
976
977	function drawEnemy(e){
978	  const col = ETIER[e.tier || 0].col;
979	  if (e.dead){
980	    const k = Math.max(0, 1 - e.t / 18);
981	    if (k <= 0) return;
982	    ctx.globalAlpha = k;
983	    ctx.fillStyle = col;
984	    roundRect(e.x, e.y + e.h * 0.62, e.w, e.h * 0.38, 4); ctx.fill();
985	    ctx.globalAlpha = 1;
986	    return;
987	  }
988	  ctx.fillStyle = col;
989	  roundRect(e.x, e.y, e.w, e.h, Math.min(9, e.w * 0.22)); ctx.fill();
990	  ctx.fillStyle = COL.dark;
991	  const d = Math.sign(e.vx) || 1;
992	  const es = Math.max(3, e.w * 0.12);
993	  const ey = e.y + e.h * 0.3;
994	  const cx = e.x + e.w / 2 + d * e.w * 0.12;
995	  ctx.fillRect(cx - e.w * 0.18 - es / 2, ey, es, es);
996	  ctx.fillRect(cx + e.w * 0.18 - es / 2, ey, es, es);
997	}
998
999	function drawCoin(co){
1000	  const b = Math.sin(performance.now() / 300 + co.x) * 2;
1001	  ctx.fillStyle = COL.coin;
1002	  ctx.beginPath(); ctx.arc(co.x, co.y + b, 7, 0, Math.PI * 2); ctx.fill();
1003	  ctx.strokeStyle = 'rgba(0,0,0,.25)'; ctx.lineWidth = 2;
1004	  ctx.beginPath(); ctx.arc(co.x, co.y + b, 7, 0, Math.PI * 2); ctx.stroke();
1005	}
1006
1007	function drawGoal(g){
1008	  const topY = g.y - TILE * 2.1;
1009	  ctx.fillStyle = '#e8ecf5';

function drawTile(c, r){
  const x = c*TILE, y = r*TILE;
  const open = !world.isSolid(c, r - 1);
  ctx.fillStyle = COL.tile;
  roundRect(x + 1, y + 1, TILE - 2, TILE - 2, open ? 7 : 2);
  ctx.fill();
  if (open){
    ctx.fillStyle = COL.tileTop;
    roundRect(x + 1, y + 1, TILE - 2, 6, 3);
    ctx.fill();
  }
}

function drawPlayer(){
  if (player.invuln > 0 && Math.floor(player.invuln / 4) % 2) return;
  const { x, y, w, h } = player;
  ctx.fillStyle = SKINS[equippedSkin].color;
  roundRect(x, y, w, h, 7); ctx.fill();
  ctx.fillStyle = COL.dark;
  ctx.fillRect(x + w/2 + player.face * 5 - 2, y + 9, 4, 7);
}

function drawEnemy(e){
  const col = ETIER[e.tier || 0].col;
  if (e.dead){
    const k = Math.max(0, 1 - e.t / 18);
    if (k <= 0) return;
    ctx.globalAlpha = k;
    ctx.fillStyle = col;
    roundRect(e.x, e.y + e.h * 0.62, e.w, e.h * 0.38, 4); ctx.fill();
    ctx.globalAlpha = 1;
    return;
  }
  ctx.fillStyle = col;
  roundRect(e.x, e.y, e.w, e.h, Math.min(9, e.w * 0.22)); ctx.fill();
  ctx.fillStyle = COL.dark;
  const d = Math.sign(e.vx) || 1;
  const es = Math.max(3, e.w * 0.12);
  const ey = e.y + e.h * 0.3;
  const cx = e.x + e.w / 2 + d * e.w * 0.12;
  ctx.fillRect(cx - e.w * 0.18 - es / 2, ey, es, es);
  ctx.fillRect(cx + e.w * 0.18 - es / 2, ey, es, es);
}

function drawCoin(co){
  const b = Math.sin(performance.now() / 300 + co.x) * 2;
  ctx.fillStyle = COL.coin;
  ctx.beginPath(); ctx.arc(co.x, co.y + b, 7, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = 'rgba(0,0,0,.25)'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.arc(co.x, co.y + b, 7, 0, Math.PI * 2); ctx.stroke();
}

function drawGoal(g){
  const topY = g.y - TILE * 2.1;
  ctx.fillStyle = '#e8ecf5';


<total_tokens>14986476 tokens left</total_tokens>


1136
1137	function drawMenu(){
1138	  ctx.save();
1139	  ctx.fillStyle = 'rgba(8,10,16,.85)';
1140	  ctx.fillRect(0, 0, W, H);
1141	  ctx.textAlign = 'center';
1142
1143	  ctx.fillStyle = '#e8ecf5';
1144	  ctx.font = '800 44px system-ui, sans-serif';
1145	  ctx.fillText('HRANATÝ SVĚT', W/2, H/2 - 118);
1146	  ctx.fillStyle = '#8b93a7';
1147	  ctx.font = '500 16px system-ui, sans-serif';
1148	  ctx.fillText('2D plošinovka — vyber si režim', W/2, H/2 - 90);
1149	  ctx.fillStyle = COL.coin;
1150	  ctx.font = '600 14px system-ui, sans-serif';
1151	  ctx.fillText('Rekord v nekonečném běhu: ' + bestHS, W/2, H/2 - 66);
1152
1153	  menuOption('1', 'Příběhové levely', LEVELS.length + ' ručně navržených levelů s cílem a životy', H/2 - 26, menuSel === 0);
1154	  menuOption('2', 'Nekonečný běh', 'Náhodná trať, která zrychluje — hraj na skóre', H/2 + 42, menuSel === 1);
1155
1156	  drawSkinPicker(H/2 + 118);
1157
1158	  ctx.fillStyle = '#8b93a7';
1159	  ctx.font = '500 13px system-ui, sans-serif';
1160	  ctx.fillText(
1161	    isTouch ? 'ťukni na režim pro start  ·  ťukni na barvu postavy'
1162	            : '↑ ↓ režim  ·  ← → barva postavy  ·  Enter start  ·  1 / 2',
1163	    W/2, H - 26);
1164	  ctx.restore();
1165	}
1166
1167	function drawSkinPicker(y){
1168	  const n = SKINS.length, sw = 34, gap = 12;
1169	  const total = n * sw + (n - 1) * gap;
1170	  let x = W/2 - total / 2;
1171
1172	  ctx.textAlign = 'center';
1173	  ctx.fillStyle = '#8b93a7';
1174	  ctx.font = '600 12px system-ui, sans-serif';
1175	  ctx.fillText('POSTAVA', W/2, y - 14);
1176
1177	  for (let i = 0; i < n; i++){
1178	    const s = SKINS[i], open = skinUnlocked(i);
1179	    ctx.globalAlpha = open ? 1 : 0.25;
1180	    ctx.fillStyle = s.color;
1181	    roundRect(x, y, sw, sw, 8); ctx.fill();
1182	    ctx.globalAlpha = 1;
1183	    if (i === equippedSkin){
1184	      ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 3;
1185	      roundRect(x - 2, y - 2, sw + 4, sw + 4, 9); ctx.stroke();
1186	    }
1187	    if (i === skinSel){
1188	      ctx.strokeStyle = COL.player; ctx.lineWidth = 2;
1189	      roundRect(x - 6, y - 6, sw + 12, sw + 12, 12); ctx.stroke();
1190	    }
1191	    if (!open){
1192	      ctx.fillStyle = '#e8ecf5';
1193	      ctx.font = '14px system-ui, sans-serif';
1194	      ctx.fillText('🔒', x + sw/2, y + sw/2 + 5);
1195	    }
1196	    x += sw + gap;
1197	  }
1198
1199	  const s = SKINS[skinSel];
1200	  ctx.fillStyle = '#e8ecf5';
1201	  ctx.font = '600 14px system-ui, sans-serif';
1202	  ctx.fillText(
1203	    skinUnlocked(skinSel)
1204	      ? s.name + (skinSel === equippedSkin ? '  ✓ nasazeno' : '')
1205	      : s.name + ' 🔒  ·  potřebuješ ' + s.unlock + ' bodů v nekonečném běhu',
1206	    W/2, y + sw + 22
1207	  );
1208	}
1209
1210	function menuOption(num, title, desc, y, sel){
1211	  const w = 470, x = W/2 - w/2, h = 58;
1212	  ctx.fillStyle = sel ? 'rgba(92,200,255,.16)' : 'rgba(255,255,255,.04)';
1213	  roundRect(x, y - h/2, w, h, 12); ctx.fill();
1214	  ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.12)';
1215	  ctx.lineWidth = 2;
1216	  roundRect(x, y - h/2, w, h, 12); ctx.stroke();
1217
1218	  ctx.textAlign = 'left';
1219	  ctx.fillStyle = '#e8ecf5';
1220	  ctx.font = '700 18px system-ui, sans-serif';
1221	  ctx.fillText(num + '.   ' + title, x + 20, y - 3);
1222	  ctx.fillStyle = '#8b93a7';
1223	  ctx.font = '400 13px system-ui, sans-serif';
1224	  ctx.fillText(desc, x + 20, y + 17);
1225	  ctx.textAlign = 'center';
1226	}
1227
1228	/* ------------------------------------------------------------------ */
1229	/*  Myš / dotyk pro menu a obrazovky                                  */
1230	/* ------------------------------------------------------------------ */
1231	function canvasPos(e){
1232	  const r = canvas.getBoundingClientRect();
1233	  // počítá i s object-fit: contain (režim celé obrazovky) – letterbox okraje
1234	  const scale = Math.min(r.width / W, r.height / H) || (r.width / W);
1235	  const ox = r.left + (r.width  - W * scale) / 2;
1236	  const oy = r.top  + (r.height - H * scale) / 2;
1237	  return { x: (e.clientX - ox) / scale, y: (e.clientY - oy) / scale };
1238	}
1239	canvas.addEventListener('contextmenu', e => e.preventDefault());
1240
1241	canvas.addEventListener('pointerdown', e => {
1242	  unlockAudio();
1243	  if (e.pointerType === 'touch') isTouch = true;
1244	  const p = canvasPos(e);
1245
1246	  // herní dotykové ovládání má přednost během hry
1247	  if (state === 'play' || state === 'paused'){
1248	    if (inCirc(p, TB.pause)){ state = state === 'play' ? 'paused' : 'play'; return; }
1249	    if (state === 'paused' &&
1250	        p.x >= W/2 - 80 && p.x <= W/2 + 80 && p.y >= H/2 + 40 && p.y <= H/2 + 84){

function drawMenu(){
  ctx.save();
  ctx.fillStyle = 'rgba(8,10,16,.85)';
  ctx.fillRect(0, 0, W, H);
  ctx.textAlign = 'center';

  ctx.fillStyle = '#e8ecf5';
  ctx.font = '800 44px system-ui, sans-serif';
  ctx.fillText('HRANATÝ SVĚT', W/2, H/2 - 118);
  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 16px system-ui, sans-serif';
  ctx.fillText('2D plošinovka — vyber si režim', W/2, H/2 - 90);
  ctx.fillStyle = COL.coin;
  ctx.font = '600 14px system-ui, sans-serif';
  ctx.fillText('Rekord v nekonečném běhu: ' + bestHS, W/2, H/2 - 66);

  menuOption('1', 'Příběhové levely', LEVELS.length + ' ručně navržených levelů s cílem a životy', H/2 - 26, menuSel === 0);
  menuOption('2', 'Nekonečný běh', 'Náhodná trať, která zrychluje — hraj na skóre', H/2 + 42, menuSel === 1);

  drawSkinPicker(H/2 + 118);

  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 13px system-ui, sans-serif';
  ctx.fillText(
    isTouch ? 'ťukni na režim pro start  ·  ťukni na barvu postavy'
            : '↑ ↓ režim  ·  ← → barva postavy  ·  Enter start  ·  1 / 2',
    W/2, H - 26);
  ctx.restore();
}

function drawSkinPicker(y){
  const n = SKINS.length, sw = 34, gap = 12;
  const total = n * sw + (n - 1) * gap;
  let x = W/2 - total / 2;

  ctx.textAlign = 'center';
  ctx.fillStyle = '#8b93a7';
  ctx.font = '600 12px system-ui, sans-serif';
  ctx.fillText('POSTAVA', W/2, y - 14);

  for (let i = 0; i < n; i++){
    const s = SKINS[i], open = skinUnlocked(i);
    ctx.globalAlpha = open ? 1 : 0.25;
    ctx.fillStyle = s.color;
    roundRect(x, y, sw, sw, 8); ctx.fill();
    ctx.globalAlpha = 1;
    if (i === equippedSkin){
      ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 3;
      roundRect(x - 2, y - 2, sw + 4, sw + 4, 9); ctx.stroke();
    }
    if (i === skinSel){
      ctx.strokeStyle = COL.player; ctx.lineWidth = 2;
      roundRect(x - 6, y - 6, sw + 12, sw + 12, 12); ctx.stroke();
    }
    if (!open){
      ctx.fillStyle = '#e8ecf5';
      ctx.font = '14px system-ui, sans-serif';
      ctx.fillText('🔒', x + sw/2, y + sw/2 + 5);
    }
    x += sw + gap;
  }

  const s = SKINS[skinSel];
  ctx.fillStyle = '#e8ecf5';
  ctx.font = '600 14px system-ui, sans-serif';
  ctx.fillText(
    skinUnlocked(skinSel)
      ? s.name + (skinSel === equippedSkin ? '  ✓ nasazeno' : '')
      : s.name + ' 🔒  ·  potřebuješ ' + s.unlock + ' bodů v nekonečném běhu',
    W/2, y + sw + 22
  );
}

function menuOption(num, title, desc, y, sel){
  const w = 470, x = W/2 - w/2, h = 58;
  ctx.fillStyle = sel ? 'rgba(92,200,255,.16)' : 'rgba(255,255,255,.04)';
  roundRect(x, y - h/2, w, h, 12); ctx.fill();
  ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.12)';
  ctx.lineWidth = 2;
  roundRect(x, y - h/2, w, h, 12); ctx.stroke();

  ctx.textAlign = 'left';
  ctx.fillStyle = '#e8ecf5';
  ctx.font = '700 18px system-ui, sans-serif';
  ctx.fillText(num + '.   ' + title, x + 20, y - 3);
  ctx.fillStyle = '#8b93a7';
  ctx.font = '400 13px system-ui, sans-serif';
  ctx.fillText(desc, x + 20, y + 17);
  ctx.textAlign = 'center';
}

/* ------------------------------------------------------------------ */
/*  Myš / dotyk pro menu a obrazovky                                  */
/* ------------------------------------------------------------------ */
function canvasPos(e){
  const r = canvas.getBoundingClientRect();
  // počítá i s object-fit: contain (režim celé obrazovky) – letterbox okraje
  const scale = Math.min(r.width / W, r.height / H) || (r.width / W);
  const ox = r.left + (r.width  - W * scale) / 2;
  const oy = r.top  + (r.height - H * scale) / 2;
  return { x: (e.clientX - ox) / scale, y: (e.clientY - oy) / scale };
}
canvas.addEventListener('contextmenu', e => e.preventDefault());

canvas.addEventListener('pointerdown', e => {
  unlockAudio();
  if (e.pointerType === 'touch') isTouch = true;
  const p = canvasPos(e);

  // herní dotykové ovládání má přednost během hry
  if (state === 'play' || state === 'paused'){
    if (inCirc(p, TB.pause)){ state = state === 'play' ? 'paused' : 'play'; return; }
    if (state === 'paused' &&
        p.x >= W/2 - 80 && p.x <= W/2 + 80 && p.y >= H/2 + 40 && p.y <= H/2 + 84){


<total_tokens>14985175 tokens left</total_tokens>


1250	        p.x >= W/2 - 80 && p.x <= W/2 + 80 && p.y >= H/2 + 40 && p.y <= H/2 + 84){
1251	      state = 'menu'; return;
1252	    }
1253	    if (state === 'play'){
1254	      if (inCirc(p, TB.jump)){
1255	        activePtr.set(e.pointerId, 'jump');
1256	        codesHit.add('Space');                   // náběžná hrana skoku
1257	        applyTouch();
1258	        return;
1259	      }
1260	      if (joy.id === null && p.x < W * 0.6){      // levá část obrazovky = joystick
1261	        joyStart(e.pointerId, p);
1262	        return;
1263	      }
1264	    }
1265	  }
1266
1267	  if (state === 'menu'){
1268	    const ys = [H/2 - 26, H/2 + 42];
1269	    for (let i = 0; i < 2; i++)
1270	      if (Math.abs(p.y - ys[i]) < 29 && Math.abs(p.x - W/2) < 235)
1271	        return startGame(i === 0 ? 'levels' : 'endless');
1272	    // klik na barvu postavy
1273	    const n = SKINS.length, sw = 34, gap = 12, sy = H/2 + 118;
1274	    let sx = W/2 - (n * sw + (n - 1) * gap) / 2;
1275	    for (let i = 0; i < n; i++){
1276	      if (p.x >= sx && p.x <= sx + sw && p.y >= sy && p.y <= sy + sw){
1277	        skinSel = i;
1278	        if (skinUnlocked(i) && i !== equippedSkin){ equippedSkin = i; store('skin', i); S.coin(); }
1279	        return;
1280	      }
1281	      sx += sw + gap;
1282	    }
1283	  } else if (state === 'shop'){
1284	    for (let i = 0; i < SHOP.length; i++){
1285	      const y = SHOP_Y0 + i * (SHOP_ROW_H + SHOP_GAP);
1286	      if (p.x >= SHOP_X && p.x <= SHOP_X + SHOP_W && p.y >= y && p.y <= y + SHOP_ROW_H)
1287	        return buyShop(i);
1288	    }
1289	    const by = shopBtnY();
1290	    if (p.x >= W/2 - 120 && p.x <= W/2 + 120 && p.y >= by && p.y <= by + 44)
1291	      return shopContinue();
1292	  } else if (state === 'over' || state === 'win'){
1293	    state = 'menu';
1294	  } else if (state === 'paused'){
1295	    state = 'play';
1296	  }
1297	});
1298
1299	canvas.addEventListener('pointermove', e => {
1300	  if (e.pointerId === joy.id) joyMove(canvasPos(e));
1301	});
1302
1303	function endPtr(e){
1304	  let changed = false;

        p.x >= W/2 - 80 && p.x <= W/2 + 80 && p.y >= H/2 + 40 && p.y <= H/2 + 84){
      state = 'menu'; return;
    }
    if (state === 'play'){
      if (inCirc(p, TB.jump)){
        activePtr.set(e.pointerId, 'jump');
        codesHit.add('Space');                   // náběžná hrana skoku
        applyTouch();
        return;
      }
      if (joy.id === null && p.x < W * 0.6){      // levá část obrazovky = joystick
        joyStart(e.pointerId, p);
        return;
      }
    }
  }

  if (state === 'menu'){
    const ys = [H/2 - 26, H/2 + 42];
    for (let i = 0; i < 2; i++)
      if (Math.abs(p.y - ys[i]) < 29 && Math.abs(p.x - W/2) < 235)
        return startGame(i === 0 ? 'levels' : 'endless');
    // klik na barvu postavy
    const n = SKINS.length, sw = 34, gap = 12, sy = H/2 + 118;
    let sx = W/2 - (n * sw + (n - 1) * gap) / 2;
    for (let i = 0; i < n; i++){
      if (p.x >= sx && p.x <= sx + sw && p.y >= sy && p.y <= sy + sw){
        skinSel = i;
        if (skinUnlocked(i) && i !== equippedSkin){ equippedSkin = i; store('skin', i); S.coin(); }
        return;
      }
      sx += sw + gap;
    }
  } else if (state === 'shop'){
    for (let i = 0; i < SHOP.length; i++){
      const y = SHOP_Y0 + i * (SHOP_ROW_H + SHOP_GAP);
      if (p.x >= SHOP_X && p.x <= SHOP_X + SHOP_W && p.y >= y && p.y <= y + SHOP_ROW_H)
        return buyShop(i);
    }
    const by = shopBtnY();
    if (p.x >= W/2 - 120 && p.x <= W/2 + 120 && p.y >= by && p.y <= by + 44)
      return shopContinue();
  } else if (state === 'over' || state === 'win'){
    state = 'menu';
  } else if (state === 'paused'){
    state = 'play';
  }
});

canvas.addEventListener('pointermove', e => {
  if (e.pointerId === joy.id) joyMove(canvasPos(e));
});

function endPtr(e){
  let changed = false;


<total_tokens>14982437 tokens left</total_tokens>


490	      }
491	    }
492	  }
493	}
494
495	/* ------------------------------------------------------------------ */
496	/*  Nepřátelé                                                         */
497	/* ------------------------------------------------------------------ */
498	function updateEnemy(e){
499	  if (e.dead){ e.t++; return; }
500
501	  e.x += e.vx;
502	  const dir   = Math.sign(e.vx) || 1;
503	  const aheadC = Math.floor((dir > 0 ? e.x + e.w + 1 : e.x - 1) / TILE);
504	  const midR   = Math.floor((e.y + e.h / 2) / TILE);
505	  const footR  = Math.floor((e.y + e.h + 3) / TILE);
506	  if (world.isSolid(aheadC, midR) || !world.isSolid(aheadC, footR)) e.vx = -e.vx;
507
508	  e.vy = Math.min(e.vy + GRAV, MAX_FALL);
509	  e.y += e.vy;
510	  const fr = Math.floor((e.y + e.h) / TILE);
511	  for (let c = Math.floor(e.x / TILE); c <= Math.floor((e.x + e.w - 1) / TILE); c++){
512	    if (world.isSolid(c, fr) && e.vy > 0){ e.y = fr*TILE - e.h; e.vy = 0; break; }
513	  }
514	  if (e.y > ROWS*TILE + 300){ e.dead = true; e.t = 999; }
515	}
516
517	/* ------------------------------------------------------------------ */
518	/*  Interakce                                                         */
519	/* ------------------------------------------------------------------ */
520	function handleInteractions(){
521	  // mince

      }
    }
  }
}

/* ------------------------------------------------------------------ */
/*  Nepřátelé                                                         */
/* ------------------------------------------------------------------ */
function updateEnemy(e){
  if (e.dead){ e.t++; return; }

  e.x += e.vx;
  const dir   = Math.sign(e.vx) || 1;
  const aheadC = Math.floor((dir > 0 ? e.x + e.w + 1 : e.x - 1) / TILE);
  const midR   = Math.floor((e.y + e.h / 2) / TILE);
  const footR  = Math.floor((e.y + e.h + 3) / TILE);
  if (world.isSolid(aheadC, midR) || !world.isSolid(aheadC, footR)) e.vx = -e.vx;

  e.vy = Math.min(e.vy + GRAV, MAX_FALL);
  e.y += e.vy;
  const fr = Math.floor((e.y + e.h) / TILE);
  for (let c = Math.floor(e.x / TILE); c <= Math.floor((e.x + e.w - 1) / TILE); c++){
    if (world.isSolid(c, fr) && e.vy > 0){ e.y = fr*TILE - e.h; e.vy = 0; break; }
  }
  if (e.y > ROWS*TILE + 300){ e.dead = true; e.t = 999; }
}

/* ------------------------------------------------------------------ */
/*  Interakce                                                         */
/* ------------------------------------------------------------------ */
function handleInteractions(){
  // mince


<total_tokens>14972148 tokens left</total_tokens>


Rozsáhlá změna — jdu na to po částech. Nejdřív nepřátelé a obtížnosti, pak menu a barvy.


Edit

function mkEnemy(x, y, tier = 0){
  const td = ETIER[tier];
  return { x, y, w: td.w, h: td.h, tier,
           vx: (Math.random() < 0.5 ? -1 : 1) * td.spd, vy: 0, dead: false, t: 0 };
}

/* náhodný druh podle postupu (0 = začátek, 1 = konec / daleko) */
function rollTier(prog){
  const r = Math.random();
  const pBig = clamp((prog - 0.45) * 0.55, 0, 0.32);   // velké až později
  const pMed = clamp(0.12 + prog * 0.4, 0, 0.45);
  if (r < pBig) return 2;
  if (r < pBig + pMed) return 1;
  return 0;
}

function mkEnemy(x, y, tier = 0){
  const td = ETIER[tier];
  return { x, y, w: td.w, h: td.h, tier, hp: tier === 2 ? 2 : 1,
           vx: (Math.random() < 0.5 ? -1 : 1) * td.spd, vy: 0, dead: false, t: 0, flash: 0 };
}

/* náhodný druh podle postupu (0 = začátek, 1 = konec / daleko).
   bias < 0 → menší příšerky, bias > 0 → větší. */
function rollTier(prog, bias = 0){
  const r = Math.random();
  const pBig = clamp((prog - 0.45) * 0.55 + bias * 0.5, 0, 0.55);
  const pMed = clamp(0.12 + prog * 0.4 + bias * 0.3, 0, 0.6);
  if (r < pBig) return 2;
  if (r < pBig + pMed) return 1;
  return 0;
}


The file /home/tonda/hra.html has been updated successfully. (file state is current in your context — no need to Read it back)

function mkEnemy(x, y, tier = 0){
  const td = ETIER[tier];
  return { x, y, w: td.w, h: td.h, tier,
           vx: (Math.random() < 0.5 ? -1 : 1) * td.spd, vy: 0, dead: false, t: 0 };
}

/* náhodný druh podle postupu (0 = začátek, 1 = konec / daleko) */
function rollTier(prog){
  const r = Math.random();
  const pBig = clamp((prog - 0.45) * 0.55, 0, 0.32);   // velké až později
  const pMed = clamp(0.12 + prog * 0.4, 0, 0.45);
  if (r < pBig) return 2;
  if (r < pBig + pMed) return 1;
  return 0;
}

function mkEnemy(x, y, tier = 0){
  const td = ETIER[tier];
  return { x, y, w: td.w, h: td.h, tier, hp: tier === 2 ? 2 : 1,
           vx: (Math.random() < 0.5 ? -1 : 1) * td.spd, vy: 0, dead: false, t: 0, flash: 0 };
}

/* náhodný druh podle postupu (0 = začátek, 1 = konec / daleko).
   bias < 0 → menší příšerky, bias > 0 → větší. */
function rollTier(prog, bias = 0){
  const r = Math.random();
  const pBig = clamp((prog - 0.45) * 0.55 + bias * 0.5, 0, 0.55);
  const pMed = clamp(0.12 + prog * 0.4 + bias * 0.3, 0, 0.6);
  if (r < pBig) return 2;
  if (r < pBig + pMed) return 1;
  return 0;
}

-  return { x, y, w: td.w, h: td.h, tier,

-           vx: (Math.random() < 0.5 ? -1 : 1) * td.spd, vy: 0, dead: false, t: 0 };

+  return { x, y, w: td.w, h: td.h, tier, hp: tier === 2 ? 2 : 1,

+           vx: (Math.random() < 0.5 ? -1 : 1) * td.spd, vy: 0, dead: false, t: 0, flash: 0 };

-/* náhodný druh podle postupu (0 = začátek, 1 = konec / daleko) */

+/* náhodný druh podle postupu (0 = začátek, 1 = konec / daleko).

+   bias < 0 → menší příšerky, bias > 0 → větší. */

-  const pBig = clamp((prog - 0.45) * 0.55, 0, 0.32);   // velké až později


<total_tokens>14959714 tokens left</total_tokens>


function updateEnemy(e){
  if (e.dead){ e.t++; return; }

  e.x += e.vx;

function updateEnemy(e){
  if (e.dead){ e.t++; return; }
  if (e.flash > 0) e.flash--;

  e.x += e.vx;


function updateEnemy(e){
  if (e.dead){ e.t++; return; }

  e.x += e.vx;

function updateEnemy(e){
  if (e.dead){ e.t++; return; }
  if (e.flash > 0) e.flash--;

  e.x += e.vx;


<total_tokens>14959408 tokens left</total_tokens>


    if (stomp){
      const td = ETIER[e.tier || 0];
      e.dead = true; e.t = 0;
      player.vy = -10 - (e.tier || 0) * 1.5;          // z větší příšery větší odraz
      score += td.score;
      coinsCollected += td.coins;                     // odměna podle velikosti
      shake = 3 + (e.tier || 0) * 3;
      spawnCoinPops(e.x + e.w / 2, e.y, td.coins);
      beep(200 - (e.tier || 0) * 40, 0.12 + (e.tier || 0) * 0.03, 'sawtooth', 0.05);
    } else {
      hurtPlayer();
    }

    if (stomp){
      const td = ETIER[e.tier || 0];
      e.hp--;
      if (e.hp > 0){
        // velká příšerka – první zášlap ji jen zmenší, druhý zabije
        const nw = 32, nh = 30;
        e.y += e.h - nh; e.x += (e.w - nw) / 2;
        e.w = nw; e.h = nh; e.flash = 10;
        player.vy = -9;
        score += 60; coinsCollected += 2;
        shake = 4;
        spawnCoinPops(e.x + e.w / 2, e.y, 2);
        beep(150, 0.1, 'sawtooth', 0.05);
      } else {
        e.dead = true; e.t = 0;
        player.vy = -10 - (e.tier || 0) * 1.5;        // z větší příšery větší odraz
        score += td.score;
        coinsCollected += td.coins;                   // odměna podle velikosti
        shake = 3 + (e.tier || 0) * 3;
        spawnCoinPops(e.x + e.w / 2, e.y, td.coins);
        beep(200 - (e.tier || 0) * 40, 0.12 + (e.tier || 0) * 0.03, 'sawtooth', 0.05);
      }
    } else {
      hurtPlayer();
    }


    if (stomp){
      const td = ETIER[e.tier || 0];
      e.dead = true; e.t = 0;
      player.vy = -10 - (e.tier || 0) * 1.5;          // z větší příšery větší odraz
      score += td.score;
      coinsCollected += td.coins;                     // odměna podle velikosti
      shake = 3 + (e.tier || 0) * 3;
      spawnCoinPops(e.x + e.w / 2, e.y, td.coins);
      beep(200 - (e.tier || 0) * 40, 0.12 + (e.tier || 0) * 0.03, 'sawtooth', 0.05);
    } else {
      hurtPlayer();
    }

    if (stomp){
      const td = ETIER[e.tier || 0];
      e.hp--;
      if (e.hp > 0){
        // velká příšerka – první zášlap ji jen zmenší, druhý zabije
        const nw = 32, nh = 30;
        e.y += e.h - nh; e.x += (e.w - nw) / 2;
        e.w = nw; e.h = nh; e.flash = 10;
        player.vy = -9;
        score += 60; coinsCollected += 2;
        shake = 4;
        spawnCoinPops(e.x + e.w / 2, e.y, 2);
        beep(150, 0.1, 'sawtooth', 0.05);
      } else {
        e.dead = true; e.t = 0;
        player.vy = -10 - (e.tier || 0) * 1.5;        // z větší příšery větší odraz
        score += td.score;
        coinsCollected += td.coins;                   // odměna podle velikosti
        shake = 3 + (e.tier || 0) * 3;
        spawnCoinPops(e.x + e.w / 2, e.y, td.coins);
        beep(200 - (e.tier || 0) * 40, 0.12 + (e.tier || 0) * 0.03, 'sawtooth', 0.05);
      }
    } else {
      hurtPlayer();
    }

-      player.vy = -10 - (e.tier || 0) * 1.5;          // z větší příšery větší odraz

-      coinsCollected += td.coins;                     // odměna podle velikosti

-      spawnCoinPops(e.x + e.w / 2, e.y, td.coins);

+        // velká příšerka – první zášlap ji jen zmenší, druhý zabije

+        e.w = nw; e.h = nh; e.flash = 10;

+        score += 60; coinsCollected += 2;

+        spawnCoinPops(e.x + e.w / 2, e.y, 2);

+        player.vy = -10 - (e.tier || 0) * 1.5;        // z větší příšery větší odraz

+        coinsCollected += td.coins;                   // odměna podle velikosti

+        spawnCoinPops(e.x + e.w / 2, e.y, td.coins);


<total_tokens>14958503 tokens left</total_tokens>


  ctx.fillStyle = col;
  roundRect(e.x, e.y, e.w, e.h, Math.min(9, e.w * 0.22)); ctx.fill();
  ctx.fillStyle = COL.dark;

  ctx.fillStyle = e.flash > 0 ? '#ffffff' : col;
  roundRect(e.x, e.y, e.w, e.h, Math.min(9, e.w * 0.22)); ctx.fill();
  ctx.fillStyle = COL.dark;


  ctx.fillStyle = col;
  roundRect(e.x, e.y, e.w, e.h, Math.min(9, e.w * 0.22)); ctx.fill();
  ctx.fillStyle = COL.dark;

  ctx.fillStyle = e.flash > 0 ? '#ffffff' : col;
  roundRect(e.x, e.y, e.w, e.h, Math.min(9, e.w * 0.22)); ctx.fill();
  ctx.fillStyle = COL.dark;

+  ctx.fillStyle = e.flash > 0 ? '#ffffff' : col;


<total_tokens>14958119 tokens left</total_tokens>


/*  Nekonečný běh – procedurální generování                           */

+const DIFF_KEYS = ['easy', 'medium', 'hard'];

+  easy:   { name: 'Lehká',   gapChance: 0.10, gapMax: 3, enemy1: 0.35, enemy2: 0.10, tierBias: -0.35, wallBase: 1.0, wallRamp: 2.2, enemyMul: 0.5 },

+  medium: { name: 'Střední', gapChance: 0.20, gapMax: 4, enemy1: 0.55, enemy2: 0.42, tierBias:  0.00, wallBase: 1.3, wallRamp: 3.3, enemyMul: 1.0 },

+  hard:   { name: 'Těžká',   gapChance: 0.30, gapMax: 5, enemy1: 0.82, enemy2: 0.78, tierBias:  0.42, wallBase: 1.6, wallRamp: 4.2, enemyMul: 1.6 },


<total_tokens>14957381 tokens left</total_tokens>


  genCol = 0; genRow = 12;
  generateAhead(0);
  player.x = 3*TILE;
  player.y = 11*TILE - player.h;
  player.vx = player.vy = 0; player.invuln = 40;
  camX = 0; camY = 0;
  scrollSpeed = 1.4;
  wallSpeed = 1.3;
  wallX = -TILE*12;
  endlessStarted = false;
}

  genCol = 0; genRow = 12;
  generateAhead(0);
  player.x = 3*TILE;
  player.y = 11*TILE - player.h;
  player.vx = player.vy = 0; player.invuln = 40;
  camX = 0; camY = 0;
  scrollSpeed = 1.4;
  wallSpeed = (ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium).wallBase;
  wallX = -TILE*12;
  endlessStarted = false;
}