655	}
656
657	function respawn(){
658	  player.vx = player.vy = 0;
659	  if (mode === 'levels'){
660	    player.x = world.playerStart.x;
661	    player.y = world.playerStart.y;
662	    camX = clamp(player.x - W*0.42, 0, world.width*TILE - W);
663	  } else {
664	    const col = Math.floor((camX + W*0.35) / TILE);
665	    let g = world.firstSolidRow(col, 3);
666	    if (g >= ROWS){
667	      for (let c = col - 2; c < col + 4; c++) for (let r = 12; r < ROWS; r++) world.setSolid(c, r);
668	      g = 12;
669	    }
670	    player.x = col*TILE;
671	    player.y = g*TILE - player.h - 2;
672	    camX = Math.max(0, player.x - W*0.4);
673	    wallX = player.x - W*0.75;                     // odstrč zeď daleko dozadu
674	    world.enemies = world.enemies.filter(e => Math.abs(e.x - player.x) > 130);
675	  }
676	}

}

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);
  }
}


<total_tokens>14993479 tokens left</total_tokens>


Edit

    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);

    camX = Math.max(0, player.x - W*0.4);
    camMax = camX;                                 // po smrti smí kamera zase couvnout sem
    wallX = player.x - W*0.75;                     // odstrč zeď daleko dozadu
    world.enemies = world.enemies.filter(e => Math.abs(e.x - player.x) > 130);


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

    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);

    camX = Math.max(0, player.x - W*0.4);
    camMax = camX;                                 // po smrti smí kamera zase couvnout sem
    wallX = player.x - W*0.75;                     // odstrč zeď daleko dozadu
    world.enemies = world.enemies.filter(e => Math.abs(e.x - player.x) > 130);

+    camMax = camX;                                 // po smrti smí kamera zase couvnout sem


<total_tokens>14992701 tokens left</total_tokens>


přidej v nekonečném běhu 3 živvoty jako jsou v příběhovém módu ale jenom do středního a do lehkého módu


Bash

Bracket balance check


{ } 243 243
( ) 997 997
[ ] 169 169


<total_tokens>14991975 tokens left</total_tokens>


Hotovo. V nekonečném běhu teď **Lehká a Střední mají 3 životy** (jako příběhový mód), **Těžká zůstává na jednom**.

Změny (`hra.html`):
- `startGame`: `lives = endless ? (hard ? 1 : 3) : 3`
- **HUD**: čtverečky životů se v nekonečném běhu zobrazí pro Lehkou/Střední (skryté jen na Těžké); počítadlo mincí se kvůli tomu posunulo níž, aby se nepřekrývalo
- `respawn` v nekonečném běhu: po ztrátě života se hráč vrátí na bezpečné místo před kamerou, vyčistí se okolní nepřátelé, a `camMax` se resetuje, aby se kamera po smrti mohla vrátit zpět (jinak by tě na Lehké „nechala za sebou")
- **menu** u nekonečného běhu píše „3 životy" / „jeden život" podle obtížnosti

Pozn.: na Střední teď dává smysl i respawn u zdi — po zásahu tě to odsune dozadu (zeď se odstrčí), takže přijdeš o kus vzdálenosti, ale ne o celý běh.


odeber v nekonečém módě mince a skóre nech pouze metry kam se hráč nejdál dostal a jak daleko je teď


<total_tokens>15000000 tokens left</total_tokens>


418	function genChunk(){
419	  const c0 = genCol;
420	  const r  = genRow;
421	  const prog = Math.min(distance / 500, 1);           // 0 → 1 podle ušlé vzdálenosti
422	  const D = ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium;
423	  const fill = (a, b, top) => { for (let c = a; c < b; c++) for (let rr = top; rr < ROWS; rr++) world.setSolid(c, rr); };
424
425	  if (genCol < 14){ fill(c0, c0 + 14, r); genCol += 14; return; }   // bezpečný start
426
427	  const t = Math.random();
428
429	  if (t < D.gapChance + prog * 0.10){                 // propast
430	    let gap = 2 + Math.floor(Math.random() * (D.gapMax - 1));
431	    gap = Math.min(gap, D.gapMax);
432	    if (gap >= 4 || Math.random() < 0.45){            // plošina nad širší propastí – drží průchodnost
433	      const pc = c0 + 1, plen = Math.max(2, gap - 1);
434	      for (let i = 0; i < plen; i++) world.setSolid(pc + i, r - 3);
435	      for (let i = 0; i < plen; i++)
436	        world.coins.push({ x: (pc + i)*TILE + TILE/2, y: (r - 4)*TILE + TILE/2, got: false });
437	    }
438	    genCol += gap;
439
440	  } else if (t < 0.52){                               // rovinka s mincemi / nepřáteli
441	    const len = 5 + Math.floor(Math.random() * 6);
442	    fill(c0, c0 + len, r);
443	    let n = 0;
444	    if (Math.random() < D.enemy1 + prog * 0.2) n = 1;
445	    if (n && Math.random() < D.enemy2 + prog * 0.25) n = 2;
446	    if (n === 2 && len < 7) n = 1;
447	    for (let e = 0; e < n; e++){
448	      const ec = c0 + 1 + Math.floor((e + 0.5) / n * (len - 2));
449	      const tier = rollTier(prog, D.tierBias);
450	      world.enemies.push(mkEnemy(ec*TILE + (TILE - ETIER[tier].w) / 2, (r - 3)*TILE, tier));
451	    }
452	    if (Math.random() < 0.6)
453	      for (let i = 0; i < Math.min(4, len - 1); i++)
454	        world.coins.push({ x: (c0 + 1 + i)*TILE + TILE/2, y: (r - 2)*TILE + TILE/2, got: false });
455	    genCol += len;
456
457	  } else if (t < 0.76){                               // schod nahoru / dolů
458	    genRow = clamp(r + (Math.random() < 0.5 ? -1 : 1), 8, 13);
459	    const len = 4 + Math.floor(Math.random() * 4);
460	    fill(c0, c0 + len, genRow);
461	    if (Math.random() < (0.2 + prog * 0.35) * D.enemyMul){
462	      const tier = rollTier(prog, D.tierBias);
463	      world.enemies.push(mkEnemy((c0 + (len >> 1))*TILE, (genRow - 3)*TILE, tier));
464	    }
465	    genCol += len;
466
467	  } else {                                            // plošinková sekce
468	    const len = 6 + Math.floor(Math.random() * 4);
469	    fill(c0, c0 + len, r);
470	    for (let k = 0; k < 2; k++){
471	      const pc = c0 + 2 + k * 3, pr = r - 3 - (k & 1);
472	      world.setSolid(pc, pr); world.setSolid(pc + 1, pr);
473	      world.coins.push({ x: pc*TILE + TILE, y: (pr - 1)*TILE + TILE/2, got: false });
474	    }
475	    if (Math.random() < (0.25 + prog * 0.35) * D.enemyMul)   // příšerka na zemi mezi plošinami
476	      world.enemies.push(mkEnemy((c0 + len - 2)*TILE, (r - 3)*TILE, rollTier(prog, D.tierBias - 0.15)));
477	    genCol += len;
478	  }
479	}
480
481	let pruneT = 0;
482	function pruneWorld(){

function genChunk(){
  const c0 = genCol;
  const r  = genRow;
  const prog = Math.min(distance / 500, 1);           // 0 → 1 podle ušlé vzdálenosti
  const D = ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium;
  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 < D.gapChance + prog * 0.10){                 // propast
    let gap = 2 + Math.floor(Math.random() * (D.gapMax - 1));
    gap = Math.min(gap, D.gapMax);
    if (gap >= 4 || Math.random() < 0.45){            // plošina nad širší propastí – drží průchodnost
      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);
    let n = 0;
    if (Math.random() < D.enemy1 + prog * 0.2) n = 1;
    if (n && Math.random() < D.enemy2 + prog * 0.25) 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(prog, D.tierBias);
      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.2 + prog * 0.35) * D.enemyMul){
      const tier = rollTier(prog, D.tierBias);
      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.25 + prog * 0.35) * D.enemyMul)   // příšerka na zemi mezi plošinami
      world.enemies.push(mkEnemy((c0 + len - 2)*TILE, (r - 3)*TILE, rollTier(prog, D.tierBias - 0.15)));
    genCol += len;
  }
}

let pruneT = 0;
function pruneWorld(){


<total_tokens>14998440 tokens left</total_tokens>


Edit

    if (gap >= 4 || Math.random() < 0.45){            // plošina nad širší propastí – drží průchodnost
      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);
    let n = 0;
    if (Math.random() < D.enemy1 + prog * 0.2) n = 1;
    if (n && Math.random() < D.enemy2 + prog * 0.25) 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(prog, D.tierBias);
      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;

    if (gap >= 4 || Math.random() < 0.45){            // plošina nad širší propastí – drží průchodnost
      const pc = c0 + 1, plen = Math.max(2, gap - 1);
      for (let i = 0; i < plen; i++) world.setSolid(pc + i, r - 3);
    }
    genCol += gap;

  } else if (t < 0.52){                               // rovinka s nepřáteli
    const len = 5 + Math.floor(Math.random() * 6);
    fill(c0, c0 + len, r);
    let n = 0;
    if (Math.random() < D.enemy1 + prog * 0.2) n = 1;
    if (n && Math.random() < D.enemy2 + prog * 0.25) 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(prog, D.tierBias);
      world.enemies.push(mkEnemy(ec*TILE + (TILE - ETIER[tier].w) / 2, (r - 3)*TILE, tier));
    }
    genCol += len;


    if (gap >= 4 || Math.random() < 0.45){            // plošina nad širší propastí – drží průchodnost
      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);
    let n = 0;
    if (Math.random() < D.enemy1 + prog * 0.2) n = 1;
    if (n && Math.random() < D.enemy2 + prog * 0.25) 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(prog, D.tierBias);
      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;

    if (gap >= 4 || Math.random() < 0.45){            // plošina nad širší propastí – drží průchodnost
      const pc = c0 + 1, plen = Math.max(2, gap - 1);
      for (let i = 0; i < plen; i++) world.setSolid(pc + i, r - 3);
    }
    genCol += gap;

  } else if (t < 0.52){                               // rovinka s nepřáteli
    const len = 5 + Math.floor(Math.random() * 6);
    fill(c0, c0 + len, r);
    let n = 0;
    if (Math.random() < D.enemy1 + prog * 0.2) n = 1;
    if (n && Math.random() < D.enemy2 + prog * 0.25) 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(prog, D.tierBias);
      world.enemies.push(mkEnemy(ec*TILE + (TILE - ETIER[tier].w) / 2, (r - 3)*TILE, tier));
    }
    genCol += len;

-        world.coins.push({ x: (pc + i)*TILE + TILE/2, y: (r - 4)*TILE + TILE/2, got: false });

-  } else if (t < 0.52){                               // rovinka s mincemi / nepřáteli

+  } else if (t < 0.52){                               // rovinka s nepřáteli

-      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 });

} else if (t < 0.76){                               // schod nahoru / dolů


<total_tokens>14995328 tokens left</total_tokens>


    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 });
    }

    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);
    }


    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 });
    }

    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.25 + prog * 0.35) * D.enemyMul)   // příšerka na zemi mezi plošinami

world.enemies.push(mkEnemy((c0 + len - 2)*TILE, (r - 3)*TILE, rollTier(prog, D.tierBias - 0.15)));


<total_tokens>14994901 tokens left</total_tokens>


1123	  ctx.fillStyle = '#e8ecf5';
1124	  ctx.fillRect(g.x + TILE/2 - 2, topY, 4, TILE * 2.1);
1125	  ctx.fillStyle = COL.flag;
1126	  ctx.beginPath();
1127	  ctx.moveTo(g.x + TILE/2 + 2, topY);
1128	  ctx.lineTo(g.x + TILE/2 + 30, topY + 10);
1129	  ctx.lineTo(g.x + TILE/2 + 2, topY + 20);
1130	  ctx.fill();
1131	}
1132
1133	function drawHUD(){
1134	  ctx.save();
1135	  const endless = mode === 'endless';
1136	  // životy: vždy v příběhu; v nekonečném běhu jen když jich je víc než jeden (Lehká/Střední)
1137	  const showLives = !endless || lives > 1 || endlessDiff !== 'hard';
1138	  if (showLives){
1139	    const slots = Math.min(Math.max(3, lives), MAX_LIVES);
1140	    for (let i = 0; i < slots; i++){
1141	      ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';
1142	      roundRect(16 + i * 20, 16, 14, 14, 4); ctx.fill();
1143	    }
1144	  }
1145	  ctx.fillStyle = COL.coin;
1146	  ctx.font = '600 15px system-ui, sans-serif';
1147	  ctx.textAlign = 'left';
1148	  ctx.fillText('● ' + coinsCollected, 16, showLives ? 54 : 28);
1149	  ctx.textAlign = 'right';
1150	  if (mode === 'levels'){
1151	    ctx.fillText(LEVELS[levelIndex].name + '  ·  ' + (levelIndex + 1) + '/' + LEVELS.length, W - 16, 26);
1152	  } else {
1153	    ctx.fillText(distance + ' m', W - 16, 26);
1154	  }
1155	  ctx.fillText('Skóre ' + escore(), W - 16, 48);
1156	  if (mode === 'endless'){
1157	    ctx.fillStyle = '#8b93a7';
1158	    ctx.font = '500 12px system-ui, sans-serif';
1159	    ctx.fillText('rekord ' + bestHS, W - 16, 66);
1160	  }
1161	  ctx.restore();
1162	}
1163
1164	function statLine(){
1165	  return mode === 'levels'
1166	    ? 'Skóre ' + score + '  ·  mincí ' + coinsCollected
1167	    : 'Skóre ' + escore() + '  ·  ' + distance + ' m  ·  ' + coinsCollected + ' ●';
1168	}
1169
1170	function overlay(title, sub){
1171	  ctx.save();
1172	  ctx.fillStyle = 'rgba(8,10,16,.78)';
1173	  ctx.fillRect(0, 0, W, H);
1174	  ctx.textAlign = 'center';
1175	  ctx.fillStyle = '#e8ecf5';
1176	  ctx.font = '700 44px system-ui, sans-serif';
1177	  ctx.fillText(title, W/2, H/2 - 6);

  ctx.fillStyle = '#e8ecf5';
  ctx.fillRect(g.x + TILE/2 - 2, topY, 4, TILE * 2.1);
  ctx.fillStyle = COL.flag;
  ctx.beginPath();
  ctx.moveTo(g.x + TILE/2 + 2, topY);
  ctx.lineTo(g.x + TILE/2 + 30, topY + 10);
  ctx.lineTo(g.x + TILE/2 + 2, topY + 20);
  ctx.fill();
}

function drawHUD(){
  ctx.save();
  const endless = mode === 'endless';
  // životy: vždy v příběhu; v nekonečném běhu jen když jich je víc než jeden (Lehká/Střední)
  const showLives = !endless || lives > 1 || endlessDiff !== 'hard';
  if (showLives){
    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, showLives ? 54 : 28);
  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'){
    ctx.fillStyle = '#8b93a7';
    ctx.font = '500 12px system-ui, sans-serif';
    ctx.fillText('rekord ' + bestHS, W - 16, 66);
  }
  ctx.restore();
}

function statLine(){
  return mode === 'levels'
    ? 'Skóre ' + score + '  ·  mincí ' + coinsCollected
    : 'Skóre ' + escore() + '  ·  ' + distance + ' m  ·  ' + coinsCollected + ' ●';
}

function overlay(title, sub){
  ctx.save();
  ctx.fillStyle = 'rgba(8,10,16,.78)';
  ctx.fillRect(0, 0, W, H);
  ctx.textAlign = 'center';
  ctx.fillStyle = '#e8ecf5';
  ctx.font = '700 44px system-ui, sans-serif';
  ctx.fillText(title, W/2, H/2 - 6);


<total_tokens>14994690 tokens left</total_tokens>


795	const skinUnlocked = i => {
796	  const s = SKINS[i];
797	  return !s.diff || (bestDist[s.diff] || 0) >= s.dist;
798	};
799
800	let equippedSkin = clamp(loadInt('skin'), 0, SKINS.length - 1);
801	if (!skinUnlocked(equippedSkin)) equippedSkin = 0;   // pojistka
802	let skinSel = equippedSkin;
803	let newRecord = false, justUnlocked = [];
804
805	const escore = () => mode === 'endless' ? score + distance : score;
806
807	function saveBest(){
808	  const s = escore();
809	  if (s > bestHS){ bestHS = s; store('hs', bestHS); newRecord = true; }
810	  const d = endlessDiff, prev = bestDist[d] || 0;
811	  if (distance > prev){
812	    bestDist[d] = distance; store('dist_' + d, distance);
813	    justUnlocked = SKINS.filter(k => k.diff === d && k.dist > prev && k.dist <= distance).map(k => k.name);
814	  }
815	}
816
817	function startGame(m){
818	  unlockAudio();
819	  mode = m;
820	  // nekonečný běh: Těžká = jeden život, Lehká/Střední = 3 životy jako v příběhu
821	  lives = m === 'endless' ? (endlessDiff === 'hard' ? 1 : 3) : 3;
822	  coinsCollected = 0; score = 0; distance = 0; shake = 0;
823	  shopSel = 0; pops = [];
824	  newRecord = false; justUnlocked = [];

const skinUnlocked = i => {
  const s = SKINS[i];
  return !s.diff || (bestDist[s.diff] || 0) >= s.dist;
};

let equippedSkin = clamp(loadInt('skin'), 0, SKINS.length - 1);
if (!skinUnlocked(equippedSkin)) equippedSkin = 0;   // pojistka
let skinSel = equippedSkin;
let newRecord = false, justUnlocked = [];

const escore = () => mode === 'endless' ? score + distance : score;

function saveBest(){
  const s = escore();
  if (s > bestHS){ bestHS = s; store('hs', bestHS); newRecord = true; }
  const d = endlessDiff, prev = bestDist[d] || 0;
  if (distance > prev){
    bestDist[d] = distance; store('dist_' + d, distance);
    justUnlocked = SKINS.filter(k => k.diff === d && k.dist > prev && k.dist <= distance).map(k => k.name);
  }
}

function startGame(m){
  unlockAudio();
  mode = m;
  // nekonečný běh: Těžká = jeden život, Lehká/Střední = 3 životy jako v příběhu
  lives = m === 'endless' ? (endlessDiff === 'hard' ? 1 : 3) : 3;
  coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0; pops = [];
  newRecord = false; justUnlocked = [];


<total_tokens>14992363 tokens left</total_tokens>


1180	  ctx.fillText(sub, W/2, H/2 + 30);
1181	  ctx.restore();
1182	}
1183
1184	function drawOver(){
1185	  let sub = statLine();
1186	  if (mode === 'endless' && justUnlocked.length)
1187	    sub += '   ·   odemčeno: ' + justUnlocked.join(', ');
1188	  sub += isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu';
1189	  overlay(mode === 'endless' && newRecord ? 'NOVÝ REKORD! 🎉' : 'KONEC HRY', sub);
1190	}
1191
1192	function drawShop(){
1193	  ctx.save();
1194	  ctx.fillStyle = 'rgba(8,10,16,.88)';
1195	  ctx.fillRect(0, 0, W, H);

  ctx.fillText(sub, W/2, H/2 + 30);
  ctx.restore();
}

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);


<total_tokens>14990982 tokens left</total_tokens>


786	];
787
788	function store(k, v){ try { localStorage.setItem('hranaty_' + k, String(v)); } catch (e) {} }
789	function loadStr(k, d){ try { const v = localStorage.getItem('hranaty_' + k); return v == null ? d : v; } catch (e) { return d; } }
790	const loadInt = k => parseInt(loadStr(k, '0'), 10) || 0;
791
792	let bestHS = loadInt('hs');
793	const bestDist = { easy: loadInt('dist_easy'), medium: loadInt('dist_medium'), hard: loadInt('dist_hard') };
794
795	const skinUnlocked = i => {

];

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; } }
const loadInt = k => parseInt(loadStr(k, '0'), 10) || 0;

let bestHS = loadInt('hs');
const bestDist = { easy: loadInt('dist_easy'), medium: loadInt('dist_medium'), hard: loadInt('dist_hard') };

const skinUnlocked = i => {


<total_tokens>14990265 tokens left</total_tokens>


let bestHS = loadInt('hs');
const bestDist = { easy: loadInt('dist_easy'), medium: loadInt('dist_medium'), hard: loadInt('dist_hard') };

const bestDist = { easy: loadInt('dist_easy'), medium: loadInt('dist_medium'), hard: loadInt('dist_hard') };


let bestHS = loadInt('hs');
const bestDist = { easy: loadInt('dist_easy'), medium: loadInt('dist_medium'), hard: loadInt('dist_hard') };

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

const loadInt = k => parseInt(loadStr(k, '0'), 10) || 0;


<total_tokens>14989737 tokens left</total_tokens>


const escore = () => mode === 'endless' ? score + distance : score;

function saveBest(){
  const s = escore();
  if (s > bestHS){ bestHS = s; store('hs', bestHS); newRecord = true; }
  const d = endlessDiff, prev = bestDist[d] || 0;
  if (distance > prev){
    bestDist[d] = distance; store('dist_' + d, distance);
    justUnlocked = SKINS.filter(k => k.diff === d && k.dist > prev && k.dist <= distance).map(k => k.name);
  }
}

function saveBest(){
  const d = endlessDiff, prev = bestDist[d] || 0;
  if (distance > prev){
    bestDist[d] = distance; store('dist_' + d, distance);
    newRecord = true;
    justUnlocked = SKINS.filter(k => k.diff === d && k.dist > prev && k.dist <= distance).map(k => k.name);
  }
}


const escore = () => mode === 'endless' ? score + distance : score;

function saveBest(){
  const s = escore();
  if (s > bestHS){ bestHS = s; store('hs', bestHS); newRecord = true; }
  const d = endlessDiff, prev = bestDist[d] || 0;
  if (distance > prev){
    bestDist[d] = distance; store('dist_' + d, distance);
    justUnlocked = SKINS.filter(k => k.diff === d && k.dist > prev && k.dist <= distance).map(k => k.name);
  }
}

function saveBest(){
  const d = endlessDiff, prev = bestDist[d] || 0;
  if (distance > prev){
    bestDist[d] = distance; store('dist_' + d, distance);
    newRecord = true;
    justUnlocked = SKINS.filter(k => k.diff === d && k.dist > prev && k.dist <= distance).map(k => k.name);
  }
}

let newRecord = false, justUnlocked = [];

-const escore = () => mode === 'endless' ? score + distance : score;

-  if (s > bestHS){ bestHS = s; store('hs', bestHS); newRecord = true; }


<total_tokens>14989201 tokens left</total_tokens>


  ctx.fillStyle = COL.coin;
  ctx.font = '600 15px system-ui, sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('● ' + coinsCollected, 16, showLives ? 54 : 28);
  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'){
    ctx.fillStyle = '#8b93a7';
    ctx.font = '500 12px system-ui, sans-serif';
    ctx.fillText('rekord ' + bestHS, W - 16, 66);
  }
  ctx.restore();

  ctx.font = '600 15px system-ui, sans-serif';
  ctx.textAlign = 'right';

  if (endless){
    ctx.fillStyle = COL.coin;
    ctx.fillText(distance + ' m', W - 16, 26);
    ctx.fillStyle = '#8b93a7';
    ctx.font = '500 12px system-ui, sans-serif';
    ctx.fillText('nejdál ' + (bestDist[endlessDiff] || 0) + ' m', W - 16, 46);
  } else {
    ctx.fillStyle = COL.coin;
    ctx.textAlign = 'left';
    ctx.fillText('● ' + coinsCollected, 16, 54);
    ctx.textAlign = 'right';
    ctx.fillText(LEVELS[levelIndex].name + '  ·  ' + (levelIndex + 1) + '/' + LEVELS.length, W - 16, 26);
    ctx.fillText('Skóre ' + score, W - 16, 48);
  }
  ctx.restore();


  ctx.fillStyle = COL.coin;
  ctx.font = '600 15px system-ui, sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('● ' + coinsCollected, 16, showLives ? 54 : 28);
  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'){
    ctx.fillStyle = '#8b93a7';
    ctx.font = '500 12px system-ui, sans-serif';
    ctx.fillText('rekord ' + bestHS, W - 16, 66);
  }
  ctx.restore();

  ctx.font = '600 15px system-ui, sans-serif';
  ctx.textAlign = 'right';

  if (endless){
    ctx.fillStyle = COL.coin;
    ctx.fillText(distance + ' m', W - 16, 26);
    ctx.fillStyle = '#8b93a7';
    ctx.font = '500 12px system-ui, sans-serif';
    ctx.fillText('nejdál ' + (bestDist[endlessDiff] || 0) + ' m', W - 16, 46);
  } else {
    ctx.fillStyle = COL.coin;
    ctx.textAlign = 'left';
    ctx.fillText('● ' + coinsCollected, 16, 54);
    ctx.textAlign = 'right';
    ctx.fillText(LEVELS[levelIndex].name + '  ·  ' + (levelIndex + 1) + '/' + LEVELS.length, W - 16, 26);
    ctx.fillText('Skóre ' + score, W - 16, 48);
  }
  ctx.restore();

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

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

-  ctx.fillText('Skóre ' + escore(), W - 16, 48);

-    ctx.fillText('rekord ' + bestHS, W - 16, 66);

+    ctx.fillText('nejdál ' + (bestDist[endlessDiff] || 0) + ' m', W - 16, 46);

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

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

+    ctx.fillText('Skóre ' + score, W - 16, 48);


<total_tokens>14988287 tokens left</total_tokens>


function statLine(){
  return mode === 'levels'
    ? 'Skóre ' + score + '  ·  mincí ' + coinsCollected
    : 'Skóre ' + escore() + '  ·  ' + distance + ' m  ·  ' + coinsCollected + ' ●';
}

function statLine(){
  return mode === 'levels'
    ? 'Skóre ' + score + '  ·  mincí ' + coinsCollected
    : distance + ' m   ·   nejdál ' + (bestDist[endlessDiff] || 0) + ' m';
}


function statLine(){
  return mode === 'levels'
    ? 'Skóre ' + score + '  ·  mincí ' + coinsCollected
    : 'Skóre ' + escore() + '  ·  ' + distance + ' m  ·  ' + coinsCollected + ' ●';
}

function statLine(){
  return mode === 'levels'
    ? 'Skóre ' + score + '  ·  mincí ' + coinsCollected
    : distance + ' m   ·   nejdál ' + (bestDist[endlessDiff] || 0) + ' m';
}

-    : 'Skóre ' + escore() + '  ·  ' + distance + ' m  ·  ' + coinsCollected + ' ●';

+    : distance + ' m   ·   nejdál ' + (bestDist[endlessDiff] || 0) + ' m';


<total_tokens>14987884 tokens left</total_tokens>


Bash

grep -n "escore\|bestHS\|coinsCollected\|world.coins\|coins:" /home/tonda/hra.html

Check remaining references


267:  { w: 24, h: 20, spd: 1.15, coins: 1, score: 40,  col: '#ff8a98' },
268:  { w: 32, h: 30, spd: 0.95, coins: 3, score: 90,  col: '#ff5c72' },
269:  { w: 44, h: 42, spd: 0.75, coins: 6, score: 180, col: '#e03150' },
311:    coins: [].concat(cr(10,8,4), cr(16,10,3), cr(26,8,3), cr(31,7,2), cr(42,8,4), cr(48,11,2)),
319:    coins: [].concat(cr(18,8,3), cr(24,10,3), cr(34,6,2), cr(43,8,3), cr(48,10,3), cr(10,11,2), cr(61,11,3)),
327:    coins: [].concat(cr(16,9,3), cr(24,8,3), cr(32,9,3), cr(44,9,3), cr(50,8,3), cr(56,11,2), cr(61,9,3), cr(66,8,2), cr(74,11,2)),
335:    coins: [].concat(cr(14,9,2), cr(23,9,2), cr(28,8,3), cr(37,9,2), cr(42,8,3), cr(48,9,2), cr(57,9,2), cr(62,8,3), cr(71,9,2), cr(76,8,3), cr(83,11,2), cr(90,11,2)),
581:  for (const co of world.coins){
585:      co.got = true; coinsCollected++; score += 10; S.coin();
606:        score += 60; coinsCollected += 2;
614:        coinsCollected += td.coins;                   // odměna podle velikosti
711:    world.coins   = world.coins.filter(c => !c.got && c.x > camX - 200);
733:let world, levelIndex = 0, lives = 3, coinsCollected = 0, score = 0, distance = 0;
751:    canBuy(){ return lives < MAX_LIVES && coinsCollected >= this.price(); },
760:  coinsCollected -= it.price();
818:  coinsCollected = 0; score = 0; distance = 0; shake = 0;
921:    for (const co of world.coins) if (!co.got) drawCoin(co);
1153:    ctx.fillText('● ' + coinsCollected, 16, 54);
1163:    ? 'Skóre ' + score + '  ·  mincí ' + coinsCollected
1200:  ctx.fillText('● ' + coinsCollected + ' mincí k utracení', W/2, 116);


<total_tokens>14987496 tokens left</total_tokens>


Bracket balance check


{ } 238 238
( ) 978 978
[ ] 171 171


<total_tokens>14985928 tokens left</total_tokens>


Hotovo. V nekonečném běhu jsou mince i skóre pryč, zůstávají jen metry.

Změny (`hra.html`):
- **`genChunk`** – zrušené všechny 3 místa, kde se do nekonečného světa sázely mince (plošina nad propastí, rovinka, plošinková sekce). Příběhové levely mají mince beze změny.
- **HUD (nekonečný běh)** – místo „● mince / Skóre / rekord" jen: vpravo nahoře **aktuální vzdálenost `X m`** a pod ní tlumeně **`nejdál Y m`** (rekord pro zvolenou obtížnost).
- **Koncová obrazovka** – `statLine` pro nekonečný běh = `X m · nejdál Y m`; „NOVÝ REKORD!" se teď spouští při překonání nejdelší vzdálenosti (dřív podle skóre).
- **`saveBest`** zjednodušené – ukládá už jen `bestDist` per obtížnost; smazané `bestHS` (staré skóre v localStorage se prostě přestane používat) a nepotřebná funkce `escore()`.
- Příběhový mód: HUD i obchod fungují dál se skóre a mincemi jako předtím.

Jedna drobnost, kterou jsem nechal: když skočíš nepříteli na hlavu, vyletí pár zlatých částic (efekt zásahu, sdílený s příběhovým módem). Nejsou to sbíratelné mince. Můžu je v nekonečném běhu taky vypnout, jestli chceš.


odeber v nekonečném módu animaci při jabití nepřátele vyskočení mince deber jen tu minci z té animace


595	      ((player.prevBottom ?? feet) <= e.y + 8 || feet - e.y < e.h * 0.5 + 6);
596	    const stomp = fromAbove;
597	    if (stomp){
598	      const td = ETIER[e.tier || 0];
599	      e.hp--;
600	      if (e.hp > 0){
601	        // velká příšerka – první zášlap ji jen zmenší, druhý zabije
602	        const nw = 32, nh = 30;
603	        e.y += e.h - nh; e.x += (e.w - nw) / 2;
604	        e.w = nw; e.h = nh; e.flash = 10;
605	        player.vy = -9;
606	        score += 60; coinsCollected += 2;
607	        shake = 4;
608	        spawnCoinPops(e.x + e.w / 2, e.y, 2);
609	        beep(150, 0.1, 'sawtooth', 0.05);
610	      } else {
611	        e.dead = true; e.t = 0;
612	        player.vy = -10 - (e.tier || 0) * 1.5;        // z větší příšery větší odraz
613	        score += td.score;
614	        coinsCollected += td.coins;                   // odměna podle velikosti
615	        shake = 3 + (e.tier || 0) * 3;
616	        spawnCoinPops(e.x + e.w / 2, e.y, td.coins);
617	        beep(200 - (e.tier || 0) * 40, 0.12 + (e.tier || 0) * 0.03, 'sawtooth', 0.05);
618	      }
619	    } else {

      ((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.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 {


<total_tokens>14999557 tokens left</total_tokens>


Edit

        shake = 4;
        spawnCoinPops(e.x + e.w / 2, e.y, 2);
        beep(150, 0.1, 'sawtooth', 0.05);

        shake = 4;
        if (mode !== 'endless') spawnCoinPops(e.x + e.w / 2, e.y, 2);
        beep(150, 0.1, 'sawtooth', 0.05);


        shake = 4;
        spawnCoinPops(e.x + e.w / 2, e.y, 2);
        beep(150, 0.1, 'sawtooth', 0.05);

        shake = 4;
        if (mode !== 'endless') spawnCoinPops(e.x + e.w / 2, e.y, 2);
        beep(150, 0.1, 'sawtooth', 0.05);

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

+        if (mode !== 'endless') spawnCoinPops(e.x + e.w / 2, e.y, 2);


<total_tokens>14998724 tokens left</total_tokens>


        shake = 3 + (e.tier || 0) * 3;
        spawnCoinPops(e.x + e.w / 2, e.y, td.coins);

        shake = 3 + (e.tier || 0) * 3;
        if (mode !== 'endless') spawnCoinPops(e.x + e.w / 2, e.y, td.coins);


        shake = 3 + (e.tier || 0) * 3;
        spawnCoinPops(e.x + e.w / 2, e.y, td.coins);

        shake = 3 + (e.tier || 0) * 3;
        if (mode !== 'endless') spawnCoinPops(e.x + e.w / 2, e.y, td.coins);

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

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

+        if (mode !== 'endless') spawnCoinPops(e.x + e.w / 2, e.y, td.coins);


<total_tokens>14998375 tokens left</total_tokens>


Hotovo. V nekonečném módu se při zabití (i zmenšení) nepřítele už nevytvoří ty vyskakující zlaté „mince" — `spawnCoinPops(...)` se v tomto módu přeskočí. Zbytek efektu zůstává: odraz hráče nahoru, otřes obrazovky, zvuk a animace zmáčknutí/zmizení nepřítele. Příběhový mód beze změny.


Editing the single-file game /home/tonda/hra.html (Hranatý svět platformer). Recent changes: mobile fullscreen button, dvh viewport fix, white default skin, easier Rozcvička, easy-mode wall removal, 3 lives on easy/medium endless, and stripped coins/score from endless mode. Next: waiting for your next requested tweak. (disable recaps in /config)


našel jsem bug díru ale na dně podlahu a nemůžu vyskočit ven můžeš udělat něco abys to upravil?


<total_tokens>15000000 tokens left</total_tokens>


657	  } else {
658	    const col = Math.floor((camX + W*0.35) / TILE);
659	    let g = world.firstSolidRow(col, 3);
660	    if (g >= ROWS){
661	      for (let c = col - 2; c < col + 4; c++) for (let r = 12; r < ROWS; r++) world.setSolid(c, r);
662	      g = 12;
663	    }
664	    player.x = col*TILE;
665	    player.y = g*TILE - player.h - 2;
666	    camX = Math.max(0, player.x - W*0.4);
667	    camMax = camX;                                 // po smrti smí kamera zase couvnout sem
668	    wallX = player.x - W*0.75;                     // odstrč zeď daleko dozadu
669	    world.enemies = world.enemies.filter(e => Math.abs(e.x - player.x) > 130);
670	  }
671	}
672
673	/* ------------------------------------------------------------------ */
674	/*  Kamera                                                            */
675	/* ------------------------------------------------------------------ */
676	const wallOn = () => (ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium).wall;
677
678	function updateCamera(){

  } 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);
    camMax = camX;                                 // po smrti smí kamera zase couvnout sem
    wallX = player.x - W*0.75;                     // odstrč zeď daleko dozadu
    world.enemies = world.enemies.filter(e => Math.abs(e.x - player.x) > 130);
  }
}

/* ------------------------------------------------------------------ */
/*  Kamera                                                            */
/* ------------------------------------------------------------------ */
const wallOn = () => (ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium).wall;

function updateCamera(){


<total_tokens>14983383 tokens left</total_tokens>


  } 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;

  } else {
    // najdi poblíž sloupec s pevnou zemí a přistaň hráče na její skutečnou výšku
    let col = Math.floor((camX + W*0.35) / TILE);
    let g = world.firstSolidRow(col, 3);
    if (g >= ROWS){
      for (let d = 1; d <= 16 && g >= ROWS; d++){
        for (const c of [col + d, col - d]){
          const fr = world.firstSolidRow(c, 3);
          if (fr < g){ g = fr; col = c; }
        }
      }
    }
    if (g >= ROWS){                                 // nikde nic – postav rovnou plošinu
      g = 12;
      for (let c = col - 3; c <= col + 3; c++)
        for (let r = g; r < ROWS; r++) world.setSolid(c, r);
    } else {
      // ať hráč nespadne do úzké kapsy: srovnej okolí na stejnou výšku
      for (let c = col - 2; c <= col + 2; c++){
        const fr = world.firstSolidRow(c, 3);
        if (fr > g) for (let r = g; r < fr; r++) world.setSolid(c, r);
      }
    }
    player.x = col*TILE;
    player.y = g*TILE - player.h - 2;


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

  } 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;

  } else {
    // najdi poblíž sloupec s pevnou zemí a přistaň hráče na její skutečnou výšku
    let col = Math.floor((camX + W*0.35) / TILE);
    let g = world.firstSolidRow(col, 3);
    if (g >= ROWS){
      for (let d = 1; d <= 16 && g >= ROWS; d++){
        for (const c of [col + d, col - d]){
          const fr = world.firstSolidRow(c, 3);
          if (fr < g){ g = fr; col = c; }
        }
      }
    }
    if (g >= ROWS){                                 // nikde nic – postav rovnou plošinu
      g = 12;
      for (let c = col - 3; c <= col + 3; c++)
        for (let r = g; r < ROWS; r++) world.setSolid(c, r);
    } else {
      // ať hráč nespadne do úzké kapsy: srovnej okolí na stejnou výšku
      for (let c = col - 2; c <= col + 2; c++){
        const fr = world.firstSolidRow(c, 3);
        if (fr > g) for (let r = g; r < fr; r++) world.setSolid(c, r);
      }
    }
    player.x = col*TILE;
    player.y = g*TILE - player.h - 2;

camX = clamp(player.x - W*0.42, 0, world.width*TILE - W);

-    const col = Math.floor((camX + W*0.35) / TILE);

+    // najdi poblíž sloupec s pevnou zemí a přistaň hráče na její skutečnou výšku

+    let col = Math.floor((camX + W*0.35) / TILE);

-      for (let c = col - 2; c < col + 4; c++) for (let r = 12; r < ROWS; r++) world.setSolid(c, r);

+        for (const c of [col + d, col - d]){

+          const fr = world.firstSolidRow(c, 3);

+          if (fr < g){ g = fr; col = c; }

+    if (g >= ROWS){                                 // nikde nic – postav rovnou plošinu

+      for (let c = col - 3; c <= col + 3; c++)

+        for (let r = g; r < ROWS; r++) world.setSolid(c, r);

+      // ať hráč nespadne do úzké kapsy: srovnej okolí na stejnou výšku

+      for (let c = col - 2; c <= col + 2; c++){

+        const fr = world.firstSolidRow(c, 3);

+        if (fr > g) for (let r = g; r < fr; r++) world.setSolid(c, r);


<total_tokens>14982183 tokens left</total_tokens>


479	  const minC = Math.floor(camX / TILE) - 40;
480	  for (const k of world.solid)
481	    if (+k.slice(0, k.indexOf(',')) < minC) world.solid.delete(k);
482	}
483
484	/* ------------------------------------------------------------------ */
485	/*  Hráč                                                              */
486	/* ------------------------------------------------------------------ */
487	const player = {
488	  x: 0, y: 0, w: 26, h: 32,
489	  vx: 0, vy: 0, onGround: false,
490	  coyote: 0, jumpBuf: 0, face: 1, invuln: 0,

  const minC = Math.floor(camX / TILE) - 40;
  for (const k of world.solid)
    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,


<total_tokens>14981254 tokens left</total_tokens>


483
484	/* ------------------------------------------------------------------ */
485	/*  Hráč                                                              */
486	/* ------------------------------------------------------------------ */

/* ------------------------------------------------------------------ */
/*  Hráč                                                              */
/* ------------------------------------------------------------------ */


<total_tokens>14980885 tokens left</total_tokens>


  const minC = Math.floor(camX / TILE) - 40;
  for (const k of world.solid)
    if (+k.slice(0, k.indexOf(',')) < minC) world.solid.delete(k);
}

  const minC = Math.floor(camX / TILE) - 40;
  for (const k of world.solid)
    if (+k.slice(0, k.indexOf(',')) < minC) world.solid.delete(k);
}

/* Pojistka: kdyby se hráč zasekl v úzké díře mezi dvěma stěnami
   (třeba po nešťastném respawnu), po chvíli mu prokopeme cestu ven. */
let stuckT = 0;
function digOutIfStuck(){
  if (mode !== 'endless' || !world || state !== 'play'){ stuckT = 0; return; }
  const cL   = Math.floor((player.x - 2) / TILE);
  const cR   = Math.floor((player.x + player.w + 1) / TILE);
  const head = Math.floor((player.y + 2) / TILE);
  const mid  = Math.floor((player.y + player.h - 3) / TILE);
  const wallL = world.isSolid(cL, head) || world.isSolid(cL, mid);
  const wallR = world.isSolid(cR, head) || world.isSolid(cR, mid);
  if (wallL && wallR){
    if (++stuckT > 60){                       // ~1 s sevřený mezi dvěma stěnami
      const foot = Math.floor((player.y + player.h - 1) / TILE);
      for (let r = foot; r >= foot - 3; r--) world.solid.delete(cR + ',' + r);
      stuckT = 0; shake = 4;
      beep(320, 0.09, 'square', 0.04);
    }
  } else {
    stuckT = 0;
  }
}


  const minC = Math.floor(camX / TILE) - 40;
  for (const k of world.solid)
    if (+k.slice(0, k.indexOf(',')) < minC) world.solid.delete(k);
}

/* Pojistka: kdyby se hráč zasekl v úzké díře mezi dvěma stěnami
   (třeba po nešťastném respawnu), po chvíli mu prokopeme cestu ven. */
let stuckT = 0;
function digOutIfStuck(){
  if (mode !== 'endless' || !world || state !== 'play'){ stuckT = 0; return; }
  const cL   = Math.floor((player.x - 2) / TILE);
  const cR   = Math.floor((player.x + player.w + 1) / TILE);
  const head = Math.floor((player.y + 2) / TILE);
  const mid  = Math.floor((player.y + player.h - 3) / TILE);
  const wallL = world.isSolid(cL, head) || world.isSolid(cL, mid);
  const wallR = world.isSolid(cR, head) || world.isSolid(cR, mid);
  if (wallL && wallR){
    if (++stuckT > 60){                       // ~1 s sevřený mezi dvěma stěnami
      const foot = Math.floor((player.y + player.h - 1) / TILE);
      for (let r = foot; r >= foot - 3; r--) world.solid.delete(cR + ',' + r);
      stuckT = 0; shake = 4;
      beep(320, 0.09, 'square', 0.04);
    }
  } else {
    stuckT = 0;
  }
}

+/* Pojistka: kdyby se hráč zasekl v úzké díře mezi dvěma stěnami

+   (třeba po nešťastném respawnu), po chvíli mu prokopeme cestu ven. */

+  if (mode !== 'endless' || !world || state !== 'play'){ stuckT = 0; return; }

+  const cL   = Math.floor((player.x - 2) / TILE);

+  const cR   = Math.floor((player.x + player.w + 1) / TILE);

+  const head = Math.floor((player.y + 2) / TILE);

+  const mid  = Math.floor((player.y + player.h - 3) / TILE);

+  const wallL = world.isSolid(cL, head) || world.isSolid(cL, mid);

+  const wallR = world.isSolid(cR, head) || world.isSolid(cR, mid);

+    if (++stuckT > 60){                       // ~1 s sevřený mezi dvěma stěnami

+      const foot = Math.floor((player.y + player.h - 1) / TILE);

+      for (let r = foot; r >= foot - 3; r--) world.solid.delete(cR + ',' + r);

/*  Hráč                                                              */


<total_tokens>14980032 tokens left</total_tokens>


895	  if (state === 'over' || state === 'win'){
896	    if (hit('Enter', 'Space', 'Escape')) state = 'menu';
897	    return;
898	  }
899	  if (state === 'paused'){
900	    if (hit('KeyP', 'Enter')) state = 'play';
901	    if (hit('Escape')) state = 'menu';
902	    return;
903	  }
904	  if (state === 'shop'){
905	    if (hit('ArrowUp', 'KeyW'))   shopSel = (shopSel - 1 + SHOP.length) % SHOP.length;
906	    if (hit('ArrowDown', 'KeyS')) shopSel = (shopSel + 1) % SHOP.length;
907	    if (hit('Enter')) buyShop(shopSel);
908	    for (let i = 0; i < SHOP.length && i < 9; i++)
909	      if (hit('Digit' + (i + 1))) buyShop(i);
910	    if (hit('Space')) shopContinue();
911	    return;
912	  }

  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;
  }


<total_tokens>14979830 tokens left</total_tokens>


913
914	  // --- state === 'play' ---
915	  if (hit('Escape')) { state = 'menu'; return; }
916	  if (hit('KeyP'))   { state = 'paused'; return; }
917	  if (hit('KeyR'))   { mode === 'levels' ? loadLevel(levelIndex) : loadEndless(); return; }
918
919	  if (shake > 0) shake *= 0.88;
920
921	  updatePlayer();
922	  for (const e of world.enemies) updateEnemy(e);
923	  handleInteractions();
924	  if (state === 'play') updateCamera();
925
926	  for (const p of pops){ p.x += p.vx; p.y += p.vy; p.vy += 0.16; p.life--; }
927	  if (pops.length) pops = pops.filter(p => p.life > 0);
928	}
929
930	/* ------------------------------------------------------------------ */
931	/*  Vykreslení                                                        */
932	/* ------------------------------------------------------------------ */

  // --- 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();

  for (const p of pops){ p.x += p.vx; p.y += p.vy; p.vy += 0.16; p.life--; }
  if (pops.length) pops = pops.filter(p => p.life > 0);
}

/* ------------------------------------------------------------------ */
/*  Vykreslení                                                        */
/* ------------------------------------------------------------------ */


<total_tokens>14979322 tokens left</total_tokens>


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

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


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

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

for (const p of pops){ p.x += p.vx; p.y += p.vy; p.vy += 0.16; p.life--; }


<total_tokens>14978676 tokens left</total_tokens>


Bash

Bracket balance and serve check


{ } 249 249
( ) 1012 1012
[ ] 172 172
2


<total_tokens>14975940 tokens left</total_tokens>


Našel jsem příčinu. V nekonečném běhu se po ztrátě života respawn choval takhle: když místo, kam tě to vrátilo, nemělo zem, hra tam natvrdo postavila podlahu na pevné výšce (řádek 12). Pokud byl okolní terén výš, ocitl ses na té podlaze zaboxovaný stěnami, které se nedaly přeskočit. A na Lehké (bez zdi, 3 životy) ses z toho ani nemohl „vyhrát smrtí" – klasický soft-lock.

Opravil jsem to dvěma způsoby (`hra.html`):

**1. Chytřejší respawn** (`respawn`)
- Místo natvrdo postavené podlahy teď hra hledá v okolí (±16 dlaždic) sloupec se skutečnou zemí a postaví tě na její pravou výšku.
- Okolí přistání (±2 dlaždice) srovná na stejnou úroveň, takže nikdy neskončíš v úzké kapse.
- Vlastní plošinu dělá jen jako poslední možnost, když v dosahu opravdu nic není.

**2. Pojistka proti zaseknutí** (`digOutIfStuck`, volaná každý snímek)
- Když jsi v nekonečném běhu ~1 sekundu sevřený mezi dvěma stěnami, hra ti prokope průchod pravou stěnou (s malým otřesem a zvukem). Takže i kdyby vznikla past jakkoli jinak, vždycky se dostaneš ven.


Editing the single-file game hra.html ("Hranatý svět"). Just fixed an endless-mode soft-lock where respawn could trap the player in a walled pit, adding a smarter respawn plus a stuck-detection failsafe. Next: you test it in the browser and confirm. (disable recaps in /config)


když skočím na dvě příšery najednou tak mě to zabije


557	  const r1 = Math.floor(player.y / TILE);
558	  const r2 = Math.floor((player.y + player.h - 1) / TILE);
559
560	  for (let r = r1; r <= r2; r++){
561	    for (let c = c1; c <= c2; c++){
562	      if (!world.isSolid(c, r)) continue;
563	      if (axis === 'x'){
564	        if (player.vx > 0)      player.x = c*TILE - player.w;
565	        else if (player.vx < 0) player.x = c*TILE + TILE;
566	        player.vx = 0;
567	      } else {
568	        if (player.vy > 0){ player.y = r*TILE - player.h; player.onGround = true; }
569	        else if (player.vy < 0) player.y = r*TILE + TILE;
570	        player.vy = 0;
571	      }
572	    }
573	  }
574	}
575
576	/* ------------------------------------------------------------------ */
577	/*  Nepřátelé                                                         */
578	/* ------------------------------------------------------------------ */
579	function updateEnemy(e){
580	  if (e.dead){ e.t++; return; }
581	  if (e.flash > 0) e.flash--;
582
583	  e.x += e.vx;
584	  const dir   = Math.sign(e.vx) || 1;
585	  const aheadC = Math.floor((dir > 0 ? e.x + e.w + 1 : e.x - 1) / TILE);
586	  const midR   = Math.floor((e.y + e.h / 2) / TILE);
587	  const footR  = Math.floor((e.y + e.h + 3) / TILE);
588	  if (world.isSolid(aheadC, midR) || !world.isSolid(aheadC, footR)) e.vx = -e.vx;
589
590	  e.vy = Math.min(e.vy + GRAV, MAX_FALL);
591	  e.y += e.vy;
592	  const fr = Math.floor((e.y + e.h) / TILE);
593	  for (let c = Math.floor(e.x / TILE); c <= Math.floor((e.x + e.w - 1) / TILE); c++){
594	    if (world.isSolid(c, fr) && e.vy > 0){ e.y = fr*TILE - e.h; e.vy = 0; break; }
595	  }
596	  if (e.y > ROWS*TILE + 300){ e.dead = true; e.t = 999; }
597	}
598
599	/* ------------------------------------------------------------------ */
600	/*  Interakce                                                         */
601	/* ------------------------------------------------------------------ */
602	function handleInteractions(){
603	  // mince
604	  for (const co of world.coins){
605	    if (co.got) continue;
606	    if (Math.abs(player.x + player.w/2 - co.x) < 18 &&
607	        Math.abs(player.y + player.h/2 - co.y) < 20){
608	      co.got = true; coinsCollected++; score += 10; S.coin();
609	    }
610	  }
611	  // nepřátelé

  const r1 = Math.floor(player.y / TILE);
  const r2 = Math.floor((player.y + player.h - 1) / TILE);

  for (let r = r1; r <= r2; r++){
    for (let c = c1; c <= c2; c++){
      if (!world.isSolid(c, r)) continue;
      if (axis === 'x'){
        if (player.vx > 0)      player.x = c*TILE - player.w;
        else if (player.vx < 0) player.x = c*TILE + TILE;
        player.vx = 0;
      } else {
        if (player.vy > 0){ player.y = r*TILE - player.h; player.onGround = true; }
        else if (player.vy < 0) player.y = r*TILE + TILE;
        player.vy = 0;
      }
    }
  }
}

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

  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
  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é


<total_tokens>14999373 tokens left</total_tokens>


611	  // nepřátelé
612	  for (const e of world.enemies){
613	    if (e.dead) continue;
614	    if (!rectOverlap(player.x, player.y, player.w, player.h, e.x, e.y, e.w, e.h)) continue;
615	    // dopad shora = zničíš nepřítele (i rohem). Zabije tě jen zásah z boku / zespodu.
616	    const feet = player.y + player.h;
617	    const fromAbove = player.vy > 0 &&
618	      ((player.prevBottom ?? feet) <= e.y + 8 || feet - e.y < e.h * 0.5 + 6);
619	    const stomp = fromAbove;
620	    if (stomp){
621	      const td = ETIER[e.tier || 0];
622	      e.hp--;
623	      if (e.hp > 0){
624	        // velká příšerka – první zášlap ji jen zmenší, druhý zabije
625	        const nw = 32, nh = 30;
626	        e.y += e.h - nh; e.x += (e.w - nw) / 2;
627	        e.w = nw; e.h = nh; e.flash = 10;
628	        player.vy = -9;
629	        score += 60; coinsCollected += 2;
630	        shake = 4;
631	        if (mode !== 'endless') spawnCoinPops(e.x + e.w / 2, e.y, 2);
632	        beep(150, 0.1, 'sawtooth', 0.05);
633	      } else {
634	        e.dead = true; e.t = 0;
635	        player.vy = -10 - (e.tier || 0) * 1.5;        // z větší příšery větší odraz
636	        score += td.score;
637	        coinsCollected += td.coins;                   // odměna podle velikosti
638	        shake = 3 + (e.tier || 0) * 3;
639	        if (mode !== 'endless') spawnCoinPops(e.x + e.w / 2, e.y, td.coins);
640	        beep(200 - (e.tier || 0) * 40, 0.12 + (e.tier || 0) * 0.03, 'sawtooth', 0.05);
641	      }
642	    } else {
643	      hurtPlayer();
644	    }
645	  }
646	  // cíl

  // 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.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;
        if (mode !== 'endless') 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;
        if (mode !== 'endless') 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


<total_tokens>14998165 tokens left</total_tokens>


Edit

  // 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){

  // nepřátelé
  // stav pádu zjisti JEDNOU – po prvním zášlapu se player.vy obrátí nahoru,
  // takže bez tohohle by druhá příšerka v témže snímku počítala jako zásah z boku
  const wasFalling = player.vy > 0;
  const dropBottom = player.prevBottom ?? (player.y + player.h);
  let stompedAny = false;
  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 = (wasFalling &&
      (dropBottom <= e.y + 8 || feet - e.y < e.h * 0.5 + 6)) || stompedAny;
    const stomp = fromAbove;
    if (stomp){
      stompedAny = true;


  // 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){

  // nepřátelé
  // stav pádu zjisti JEDNOU – po prvním zášlapu se player.vy obrátí nahoru,
  // takže bez tohohle by druhá příšerka v témže snímku počítala jako zásah z boku
  const wasFalling = player.vy > 0;
  const dropBottom = player.prevBottom ?? (player.y + player.h);
  let stompedAny = false;
  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 = (wasFalling &&
      (dropBottom <= e.y + 8 || feet - e.y < e.h * 0.5 + 6)) || stompedAny;
    const stomp = fromAbove;
    if (stomp){
      stompedAny = true;

+  // stav pádu zjisti JEDNOU – po prvním zášlapu se player.vy obrátí nahoru,

+  // takže bez tohohle by druhá příšerka v témže snímku počítala jako zásah z boku

+  const dropBottom = player.prevBottom ?? (player.y + player.h);

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

+      (dropBottom <= e.y + 8 || feet - e.y < e.h * 0.5 + 6)) || stompedAny;


<total_tokens>14995855 tokens left</total_tokens>


611	  // nepřátelé
612	  // stav pádu zjisti JEDNOU – po prvním zášlapu se player.vy obrátí nahoru,
613	  // takže bez tohohle by druhá příšerka v témže snímku počítala jako zásah z boku
614	  const wasFalling = player.vy > 0;
615	  const dropBottom = player.prevBottom ?? (player.y + player.h);
616	  let stompedAny = false;
617	  for (const e of world.enemies){
618	    if (e.dead) continue;
619	    if (!rectOverlap(player.x, player.y, player.w, player.h, e.x, e.y, e.w, e.h)) continue;
620	    // dopad shora = zničíš nepřítele (i rohem). Zabije tě jen zásah z boku / zespodu.
621	    const feet = player.y + player.h;
622	    const fromAbove = (wasFalling &&
623	      (dropBottom <= e.y + 8 || feet - e.y < e.h * 0.5 + 6)) || stompedAny;
624	    const stomp = fromAbove;
625	    if (stomp){
626	      stompedAny = true;
627	      const td = ETIER[e.tier || 0];
628	      e.hp--;
629	      if (e.hp > 0){
630	        // velká příšerka – první zášlap ji jen zmenší, druhý zabije
631	        const nw = 32, nh = 30;
632	        e.y += e.h - nh; e.x += (e.w - nw) / 2;
633	        e.w = nw; e.h = nh; e.flash = 10;
634	        player.vy = -9;

  // nepřátelé
  // stav pádu zjisti JEDNOU – po prvním zášlapu se player.vy obrátí nahoru,
  // takže bez tohohle by druhá příšerka v témže snímku počítala jako zásah z boku
  const wasFalling = player.vy > 0;
  const dropBottom = player.prevBottom ?? (player.y + player.h);
  let stompedAny = false;
  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 = (wasFalling &&
      (dropBottom <= e.y + 8 || feet - e.y < e.h * 0.5 + 6)) || stompedAny;
    const stomp = fromAbove;
    if (stomp){
      stompedAny = true;
      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;


<total_tokens>14995603 tokens left</total_tokens>


Bash

Bracket balance check


{ } 249 249
( ) 1013 1013
[ ] 172 172


<total_tokens>14994532 tokens left</total_tokens>


Opraveno. Příčina: kód rozhodoval „dopad shora vs. zásah z boku" podle `player.vy` uvnitř smyčky přes příšery. První zášlap ale hráče odrazí nahoru (`player.vy` se změní na záporné), takže druhá příšerka ve stejném snímku spadla do větve „zásah z boku" → `hurtPlayer()`.

Změna v `handleInteractions` (`hra.html`):
- Stav pádu (`wasFalling`) a spodní hrana před pohybem (`dropBottom`) se zjistí **jednou před smyčkou**, ne u každé příšery zvlášť.
- Přidán příznak `stompedAny` — jakmile v daném snímku dupneš na jednu příšeru, všechny další, se kterými se zároveň překrýváš, se taky počítají jako zášlap.

Takže skok do hloučku dvou (i víc) příšer je teď dvojité zabití + jeden odraz, ne smrt.


Working on the browser game Hranatý svět (hra.html), making mobile and endless-mode improvements. Just fixed the bug where jumping onto two enemies at once killed the player. Next: reload the game and test that fix.