499	let state = 'menu';      // menu | play | paused | shop | over | win
500	let mode  = 'levels';    // levels | endless
501	let menuSel = 0;
502	let world, levelIndex = 0, lives = 3, coinsCollected = 0, score = 0, distance = 0;
503	let camX = 0, camY = 0, scrollSpeed = 1.4, endlessStarted = false;
504	let wallX = -400, wallSpeed = 1.3;          // zeď smrti v nekonečném módu
505	let shake = 0;
506	const MAX_LIVES = 5;
507
508	/* --- Obchod mezi levely (později přibudou další položky) --- */
509	let shopSel = 0;
510	const SHOP_X = W/2 - 235, SHOP_W = 470, SHOP_ROW_H = 62, SHOP_GAP = 10, SHOP_Y0 = 185;
511	const shopBtnY = () => SHOP_Y0 + SHOP.length * (SHOP_ROW_H + SHOP_GAP) + 6;
512
513	const SHOP = [
514	  {
515	    name: 'Život navíc',
516	    desc: '+1 život (max ' + MAX_LIVES + ')',
517	    base: 10,
518	    bought: 0,
519	    price(){ return this.base * Math.pow(2, this.bought); },
520	    canBuy(){ return lives < MAX_LIVES && coinsCollected >= this.price(); },
521	    apply(){ lives++; },
522	  },
523	];
524
525	function buyShop(i){
526	  const it = SHOP[i];
527	  if (!it) return;
528	  if (!it.canBuy()){ beep(120, 0.09, 'square', 0.04); return; }
529	  coinsCollected -= it.price();
530	  it.bought++;
531	  it.apply();
532	  S.coin();
533	}
534	function shopContinue(){
535	  loadLevel(levelIndex);   // levelIndex je už zvýšený
536	  state = 'play';
537	}
538
539	function startGame(m){
540	  unlockAudio();
541	  mode = m;
542	  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
543	  shopSel = 0;
544	  SHOP.forEach(it => it.bought = 0);
545	  if (m === 'levels'){ levelIndex = 0; loadLevel(0); }
546	  else loadEndless();

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

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


<total_tokens>14994489 tokens left</total_tokens>


Edit

function shopContinue(){
  loadLevel(levelIndex);   // levelIndex je už zvýšený
  state = 'play';
}

function startGame(m){

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


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

function shopContinue(){
  loadLevel(levelIndex);   // levelIndex je už zvýšený
  state = 'play';
}

function startGame(m){

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

+/*  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í       */

+  { 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 newRecord = false, justUnlocked = [];

+const skinUnlocked = i => SKINS[i].unlock <= bestHS;

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

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

+  justUnlocked = SKINS.filter(k => k.unlock > old && k.unlock <= bestHS).map(k => k.name);


<total_tokens>14992621 tokens left</total_tokens>


zapisovat nejlepší skóré v nekonečném módu a odemykat jím nové barvy na charakter


  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0;
  SHOP.forEach(it => it.bought = 0);

  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0;
  newRecord = false; justUnlocked = [];
  SHOP.forEach(it => it.bought = 0);


  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0;
  SHOP.forEach(it => it.bought = 0);

  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0;
  newRecord = false; justUnlocked = [];
  SHOP.forEach(it => it.bought = 0);

+  newRecord = false; justUnlocked = [];

if (m === 'levels'){ levelIndex = 0; loadLevel(0); }


<total_tokens>14992263 tokens left</total_tokens>


  S.hurt();
  if (lives <= 0){ state = 'over'; return; }
  player.invuln = 100;

  S.hurt();
  if (lives <= 0){
    state = 'over';
    if (mode === 'endless') saveBest();
    return;
  }
  player.invuln = 100;


  S.hurt();
  if (lives <= 0){ state = 'over'; return; }
  player.invuln = 100;

  S.hurt();
  if (lives <= 0){
    state = 'over';
    if (mode === 'endless') saveBest();
    return;
  }
  player.invuln = 100;

-  if (lives <= 0){ state = 'over'; return; }

+    if (mode === 'endless') saveBest();


<total_tokens>14991932 tokens left</total_tokens>


  if (state === 'menu'){
    if (hit('ArrowUp', 'ArrowDown', 'KeyW', 'KeyS')) menuSel = 1 - menuSel;
    if (hit('Digit1')) return startGame('levels');

  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 (state === 'menu'){
    if (hit('ArrowUp', 'ArrowDown', 'KeyW', 'KeyS')) menuSel = 1 - menuSel;
    if (hit('Digit1')) return startGame('levels');

  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('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('Digit2')) return startGame('endless');

if (hit('Enter', 'Space')) return startGame(menuSel === 0 ? 'levels' : 'endless');


<total_tokens>14991365 tokens left</total_tokens>


  const { x, y, w, h } = player;
  ctx.fillStyle = COL.player;
  roundRect(x, y, w, h, 7); ctx.fill();

  const { x, y, w, h } = player;
  ctx.fillStyle = SKINS[equippedSkin].color;
  roundRect(x, y, w, h, 7); ctx.fill();


  const { x, y, w, h } = player;
  ctx.fillStyle = COL.player;
  roundRect(x, y, w, h, 7); ctx.fill();

  const { x, y, w, h } = player;
  ctx.fillStyle = SKINS[equippedSkin].color;
  roundRect(x, y, w, h, 7); ctx.fill();

if (player.invuln > 0 && Math.floor(player.invuln / 4) % 2) return;

+  ctx.fillStyle = SKINS[equippedSkin].color;

ctx.fillRect(x + w/2 + player.face * 5 - 2, y + 9, 4, 7);


<total_tokens>14990966 tokens left</total_tokens>


  } else {
    ctx.fillText(distance + ' m', W - 16, 26);
  }
  ctx.fillText('Skóre ' + score, W - 16, 48);
  ctx.restore();
}

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

  } 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 + ' ●';
}


  } else {
    ctx.fillText(distance + ' m', W - 16, 26);
  }
  ctx.fillText('Skóre ' + score, W - 16, 48);
  ctx.restore();
}

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

  } 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 + ' ●';
}

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

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

+    ctx.font = '500 12px system-ui, sans-serif';

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

-    : 'Vzdálenost ' + distance + ' m  ·  mincí ' + coinsCollected + '  ·  skóre ' + score;

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


<total_tokens>14990321 tokens left</total_tokens>


647	};
648
649	function draw(){
650	  ctx.clearRect(0, 0, W, H);
651
652	  const g = ctx.createLinearGradient(0, 0, 0, H);
653	  g.addColorStop(0, '#141829'); g.addColorStop(1, '#0b0d15');
654	  ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
655
656	  drawParallax();
657
658	  const sx = shake > 0.4 ? (Math.random() - 0.5) * shake : 0;
659	  const sy = shake > 0.4 ? (Math.random() - 0.5) * shake : 0;
660

};

function draw(){
  ctx.clearRect(0, 0, W, H);

  const g = ctx.createLinearGradient(0, 0, 0, H);
  g.addColorStop(0, '#141829'); g.addColorStop(1, '#0b0d15');
  ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);

  drawParallax();

  const sx = shake > 0.4 ? (Math.random() - 0.5) * shake : 0;
  const sy = shake > 0.4 ? (Math.random() - 0.5) * shake : 0;


<total_tokens>14990002 tokens left</total_tokens>


686	      grd.addColorStop(0, 'rgba(255,45,65,' + (a * pulse).toFixed(3) + ')');
687	      grd.addColorStop(1, 'rgba(255,45,65,0)');
688	      ctx.fillStyle = grd;
689	      ctx.fillRect(0, 0, W * 0.55, H);
690	    }
691	  }
692
693	  if (world) drawHUD();
694
695	  if (state === 'menu')            drawMenu();
696	  else if (state === 'paused')     overlay('PAUZA', 'P nebo Enter – pokračovat   ·   Esc – menu');
697	  else if (state === 'shop')       drawShop();
698	  else if (state === 'over')       overlay('KONEC HRY', statLine() + '   ·   Enter – menu');
699	  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');

      grd.addColorStop(0, 'rgba(255,45,65,' + (a * pulse).toFixed(3) + ')');
      grd.addColorStop(1, 'rgba(255,45,65,0)');
      ctx.fillStyle = grd;
      ctx.fillRect(0, 0, W * 0.55, H);
    }
  }

  if (world) drawHUD();

  if (state === 'menu')            drawMenu();
  else if (state === 'paused')     overlay('PAUZA', 'P nebo Enter – pokračovat   ·   Esc – menu');
  else if (state === 'shop')       drawShop();
  else if (state === 'over')       overlay('KONEC HRY', statLine() + '   ·   Enter – menu');
  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');


<total_tokens>14989622 tokens left</total_tokens>


  else if (state === 'over')       overlay('KONEC HRY', statLine() + '   ·   Enter – menu');
  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');

  else if (state === 'over')       drawOver();
  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');


  else if (state === 'over')       overlay('KONEC HRY', statLine() + '   ·   Enter – menu');
  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');

  else if (state === 'over')       drawOver();
  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');

if (state === 'menu')            drawMenu();

else if (state === 'paused')     overlay('PAUZA', 'P nebo Enter – pokračovat   ·   Esc – menu');

else if (state === 'shop')       drawShop();

-  else if (state === 'over')       overlay('KONEC HRY', statLine() + '   ·   Enter – menu');

+  else if (state === 'over')       drawOver();


<total_tokens>14988970 tokens left</total_tokens>


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

function drawShop(){


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

function drawShop(){

+  if (mode === 'endless' && justUnlocked.length)

+    sub += '   ·   odemčeno: ' + justUnlocked.join(', ');

+  overlay(mode === 'endless' && newRecord ? 'NOVÝ REKORD! 🎉' : 'KONEC HRY', sub);


<total_tokens>14988603 tokens left</total_tokens>


866
867	  SHOP.forEach((it, i) => {
868	    const y = SHOP_Y0 + i * (SHOP_ROW_H + SHOP_GAP);
869	    const sel = i === shopSel;
870	    const price = it.price();
871	    const ok = it.canBuy();
872
873	    ctx.fillStyle = sel ? 'rgba(92,200,255,.16)' : 'rgba(255,255,255,.04)';
874	    roundRect(SHOP_X, y, SHOP_W, SHOP_ROW_H, 12); ctx.fill();
875	    ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.12)';
876	    ctx.lineWidth = 2;
877	    roundRect(SHOP_X, y, SHOP_W, SHOP_ROW_H, 12); ctx.stroke();
878
879	    ctx.textAlign = 'left';
880	    ctx.fillStyle = '#e8ecf5';
881	    ctx.font = '700 17px system-ui, sans-serif';
882	    ctx.fillText((i + 1) + '.   ' + it.name, SHOP_X + 18, y + 26);
883	    ctx.fillStyle = '#8b93a7';
884	    ctx.font = '400 13px system-ui, sans-serif';
885	    ctx.fillText(it.desc + '     ·     koupeno ' + it.bought + '×', SHOP_X + 18, y + 46);
886
887	    ctx.textAlign = 'right';
888	    ctx.fillStyle = ok ? COL.coin : '#ff6b7d';
889	    ctx.font = '700 18px system-ui, sans-serif';
890	    ctx.fillText('● ' + price, SHOP_X + SHOP_W - 18, y + 38);
891	  });
892
893	  const by = shopBtnY();
894	  ctx.fillStyle = 'rgba(74,222,128,.14)';
895	  roundRect(W/2 - 120, by, 240, 44, 12); ctx.fill();

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

    ctx.fillStyle = sel ? 'rgba(92,200,255,.16)' : 'rgba(255,255,255,.04)';
    roundRect(SHOP_X, y, SHOP_W, SHOP_ROW_H, 12); ctx.fill();
    ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.12)';
    ctx.lineWidth = 2;
    roundRect(SHOP_X, y, SHOP_W, SHOP_ROW_H, 12); ctx.stroke();

    ctx.textAlign = 'left';
    ctx.fillStyle = '#e8ecf5';
    ctx.font = '700 17px system-ui, sans-serif';
    ctx.fillText((i + 1) + '.   ' + it.name, SHOP_X + 18, y + 26);
    ctx.fillStyle = '#8b93a7';
    ctx.font = '400 13px system-ui, sans-serif';
    ctx.fillText(it.desc + '     ·     koupeno ' + it.bought + '×', SHOP_X + 18, y + 46);

    ctx.textAlign = 'right';
    ctx.fillStyle = ok ? COL.coin : '#ff6b7d';
    ctx.font = '700 18px system-ui, sans-serif';
    ctx.fillText('● ' + price, SHOP_X + SHOP_W - 18, y + 38);
  });

  const by = shopBtnY();
  ctx.fillStyle = 'rgba(74,222,128,.14)';
  roundRect(W/2 - 120, by, 240, 44, 12); ctx.fill();


<total_tokens>14988387 tokens left</total_tokens>


906	  ctx.restore();
907	}
908
909	function drawMenu(){
910	  ctx.save();
911	  ctx.fillStyle = 'rgba(8,10,16,.85)';
912	  ctx.fillRect(0, 0, W, H);
913	  ctx.textAlign = 'center';
914
915	  ctx.fillStyle = '#e8ecf5';
916	  ctx.font = '800 46px system-ui, sans-serif';
917	  ctx.fillText('HRANATÝ SVĚT', W/2, H/2 - 96);
918	  ctx.fillStyle = '#8b93a7';
919	  ctx.font = '500 17px system-ui, sans-serif';
920	  ctx.fillText('2D plošinovka — vyber si režim', W/2, H/2 - 64);
921
922	  menuOption('1', 'Příběhové levely', LEVELS.length + ' ručně navržených levelů s cílem a životy', H/2 - 6, menuSel === 0);
923	  menuOption('2', 'Nekonečný běh', 'Náhodná trať, která zrychluje — hraj na skóre', H/2 + 66, menuSel === 1);
924
925	  ctx.fillStyle = '#8b93a7';
926	  ctx.font = '500 14px system-ui, sans-serif';
927	  ctx.fillText('↑ ↓ výběr  ·  Enter start  ·  nebo rovnou klávesa 1 / 2', W/2, H - 46);
928	  ctx.restore();
929	}
930
931	function menuOption(num, title, desc, y, sel){
932	  const w = 470, x = W/2 - w/2, h = 58;
933	  ctx.fillStyle = sel ? 'rgba(92,200,255,.16)' : 'rgba(255,255,255,.04)';
934	  roundRect(x, y - h/2, w, h, 12); ctx.fill();
935	  ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.12)';
936	  ctx.lineWidth = 2;
937	  roundRect(x, y - h/2, w, h, 12); ctx.stroke();
938
939	  ctx.textAlign = 'left';
940	  ctx.fillStyle = '#e8ecf5';
941	  ctx.font = '700 18px system-ui, sans-serif';
942	  ctx.fillText(num + '.   ' + title, x + 20, y - 3);
943	  ctx.fillStyle = '#8b93a7';
944	  ctx.font = '400 13px system-ui, sans-serif';
945	  ctx.fillText(desc, x + 20, y + 17);

  ctx.restore();
}

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 46px system-ui, sans-serif';
  ctx.fillText('HRANATÝ SVĚT', W/2, H/2 - 96);
  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 17px system-ui, sans-serif';
  ctx.fillText('2D plošinovka — vyber si režim', W/2, H/2 - 64);

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

  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 14px system-ui, sans-serif';
  ctx.fillText('↑ ↓ výběr  ·  Enter start  ·  nebo rovnou klávesa 1 / 2', W/2, H - 46);
  ctx.restore();
}

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


<total_tokens>14987516 tokens left</total_tokens>


  ctx.fillStyle = '#e8ecf5';
  ctx.font = '800 46px system-ui, sans-serif';
  ctx.fillText('HRANATÝ SVĚT', W/2, H/2 - 96);
  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 17px system-ui, sans-serif';
  ctx.fillText('2D plošinovka — vyber si režim', W/2, H/2 - 64);

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

  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 14px system-ui, sans-serif';
  ctx.fillText('↑ ↓ výběr  ·  Enter start  ·  nebo rovnou klávesa 1 / 2', W/2, H - 46);
  ctx.restore();
}

  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('↑ ↓ 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
  );
}


  ctx.fillStyle = '#e8ecf5';
  ctx.font = '800 46px system-ui, sans-serif';
  ctx.fillText('HRANATÝ SVĚT', W/2, H/2 - 96);
  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 17px system-ui, sans-serif';
  ctx.fillText('2D plošinovka — vyber si režim', W/2, H/2 - 64);

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

  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 14px system-ui, sans-serif';
  ctx.fillText('↑ ↓ výběr  ·  Enter start  ·  nebo rovnou klávesa 1 / 2', W/2, H - 46);
  ctx.restore();
}

  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('↑ ↓ 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
  );
}

-  ctx.font = '800 46px system-ui, sans-serif';

-  ctx.fillText('HRANATÝ SVĚT', W/2, H/2 - 96);

+  ctx.font = '800 44px system-ui, sans-serif';

+  ctx.fillText('HRANATÝ SVĚT', W/2, H/2 - 118);

-  ctx.font = '500 17px system-ui, sans-serif';

-  ctx.fillText('2D plošinovka — vyber si režim', W/2, H/2 - 64);

+  ctx.font = '500 16px system-ui, sans-serif';

+  ctx.fillText('2D plošinovka — vyber si režim', W/2, H/2 - 90);

+  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 - 6, menuSel === 0);

-  menuOption('2', 'Nekonečný běh', 'Náhodná trať, která zrychluje — hraj na skóre', H/2 + 66, menuSel === 1);

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

-  ctx.font = '500 14px system-ui, sans-serif';

-  ctx.fillText('↑ ↓ výběr  ·  Enter start  ·  nebo rovnou klávesa 1 / 2', W/2, H - 46);

+  ctx.font = '500 13px system-ui, sans-serif';

+  ctx.fillText('↑ ↓ režim  ·  ← → barva postavy  ·  Enter start  ·  1 / 2', W/2, H - 26);

+  const n = SKINS.length, sw = 34, gap = 12;

+  const total = n * sw + (n - 1) * gap;

+  ctx.font = '600 12px system-ui, sans-serif';

+  ctx.fillText('POSTAVA', W/2, y - 14);

+    const s = SKINS[i], open = skinUnlocked(i);

+    roundRect(x, y, sw, sw, 8); ctx.fill();

+      ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 3;

+      roundRect(x - 2, y - 2, sw + 4, sw + 4, 9); ctx.stroke();

+      ctx.strokeStyle = COL.player; ctx.lineWidth = 2;

+      roundRect(x - 6, y - 6, sw + 12, sw + 12, 12); ctx.stroke();

+      ctx.font = '14px system-ui, sans-serif';

+      ctx.fillText('🔒', x + sw/2, y + sw/2 + 5);

+      ? s.name + (skinSel === equippedSkin ? '  ✓ nasazeno' : '')

+      : s.name + ' 🔒  ·  potřebuješ ' + s.unlock + ' bodů v nekonečném běhu',

function menuOption(num, title, desc, y, sel){


<total_tokens>14984364 tokens left</total_tokens>


973	      ? s.name + (skinSel === equippedSkin ? '  ✓ nasazeno' : '')
974	      : s.name + ' 🔒  ·  potřebuješ ' + s.unlock + ' bodů v nekonečném běhu',
975	    W/2, y + sw + 22
976	  );
977	}
978
979	function menuOption(num, title, desc, y, sel){
980	  const w = 470, x = W/2 - w/2, h = 58;
981	  ctx.fillStyle = sel ? 'rgba(92,200,255,.16)' : 'rgba(255,255,255,.04)';
982	  roundRect(x, y - h/2, w, h, 12); ctx.fill();
983	  ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.12)';
984	  ctx.lineWidth = 2;
985	  roundRect(x, y - h/2, w, h, 12); ctx.stroke();
986
987	  ctx.textAlign = 'left';
988	  ctx.fillStyle = '#e8ecf5';
989	  ctx.font = '700 18px system-ui, sans-serif';
990	  ctx.fillText(num + '.   ' + title, x + 20, y - 3);
991	  ctx.fillStyle = '#8b93a7';
992	  ctx.font = '400 13px system-ui, sans-serif';
993	  ctx.fillText(desc, x + 20, y + 17);
994	  ctx.textAlign = 'center';
995	}
996
997	/* ------------------------------------------------------------------ */
998	/*  Myš / dotyk pro menu a obrazovky                                  */
999	/* ------------------------------------------------------------------ */
1000	function canvasPos(e){
1001	  const r = canvas.getBoundingClientRect();
1002	  return { x: (e.clientX - r.left) * (W / r.width), y: (e.clientY - r.top) * (H / r.height) };
1003	}
1004	canvas.addEventListener('pointerdown', e => {

      ? 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();
  return { x: (e.clientX - r.left) * (W / r.width), y: (e.clientY - r.top) * (H / r.height) };
}
canvas.addEventListener('pointerdown', e => {


<total_tokens>14983937 tokens left</total_tokens>


1004	canvas.addEventListener('pointerdown', e => {
1005	  unlockAudio();
1006	  const p = canvasPos(e);
1007	  if (state === 'menu'){
1008	    const ys = [H/2 - 6, H/2 + 66];
1009	    for (let i = 0; i < 2; i++)
1010	      if (Math.abs(p.y - ys[i]) < 29 && Math.abs(p.x - W/2) < 235)
1011	        return startGame(i === 0 ? 'levels' : 'endless');
1012	  } else if (state === 'shop'){
1013	    for (let i = 0; i < SHOP.length; i++){
1014	      const y = SHOP_Y0 + i * (SHOP_ROW_H + SHOP_GAP);
1015	      if (p.x >= SHOP_X && p.x <= SHOP_X + SHOP_W && p.y >= y && p.y <= y + SHOP_ROW_H)
1016	        return buyShop(i);
1017	    }
1018	    const by = shopBtnY();
1019	    if (p.x >= W/2 - 120 && p.x <= W/2 + 120 && p.y >= by && p.y <= by + 44)
1020	      return shopContinue();
1021	  } else if (state === 'over' || state === 'win'){
1022	    state = 'menu';
1023	  } else if (state === 'paused'){
1024	    state = 'play';
1025	  }
1026	});
1027
1028	/* ------------------------------------------------------------------ */
1029	/*  Hlavní smyčka – pevný krok 60 Hz                                  */
1030	/* ------------------------------------------------------------------ */
1031	let last = performance.now(), acc = 0;
1032	const STEP = 1000 / 60;
1033

canvas.addEventListener('pointerdown', e => {
  unlockAudio();
  const p = canvasPos(e);
  if (state === 'menu'){
    const ys = [H/2 - 6, H/2 + 66];
    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');
  } 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';
  }
});

/* ------------------------------------------------------------------ */
/*  Hlavní smyčka – pevný krok 60 Hz                                  */
/* ------------------------------------------------------------------ */
let last = performance.now(), acc = 0;
const STEP = 1000 / 60;


<total_tokens>14983080 tokens left</total_tokens>


  if (state === 'menu'){
    const ys = [H/2 - 6, H/2 + 66];
    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');
  } else if (state === 'shop'){

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


  if (state === 'menu'){
    const ys = [H/2 - 6, H/2 + 66];
    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');
  } else if (state === 'shop'){

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

+    const n = SKINS.length, sw = 34, gap = 12, sy = H/2 + 118;

+        if (skinUnlocked(i) && i !== equippedSkin){ equippedSkin = i; store('skin', i); S.coin(); }

const y = SHOP_Y0 + i * (SHOP_ROW_H + SHOP_GAP);


<total_tokens>14981804 tokens left</total_tokens>


Bash

python3 - <<'EOF'
import re
js=re.search(r'<script>(.*)</script>',open('hra.html').read(),re.S).group(1)
for op,cl in [('(',')'),('{','}'),('[',']')]:
    a,b=js.count(op),js.count(cl)
    print(op,cl,a,b,'OK' if a==b else 'MISMATCH')
print('COL.player refs:', js.count('COL.player'))
print('SKINS refs:', js.count('SKINS'))
EOF
grep -n "COL.player" hra.html

Bracket balance and reference check


( ) 746 746 OK
{ } 160 160 OK
[ ] 147 147 OK
COL.player refs: 3
SKINS refs: 13
875:    ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.12)';
957:      ctx.strokeStyle = COL.player; ctx.lineWidth = 2;
983:  ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.12)';


<total_tokens>14981094 tokens left</total_tokens>


curl -sS -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8000/hra.html

Confirm server serving updated file


HTTP 200


<total_tokens>14980573 tokens left</total_tokens>


Hotovo — **F5** a zkus nekonečný běh.

## Rekord (ukládá se v prohlížeči)

- Skóre v nekonečném běhu = `mince*10 + dupnutí*50 + ušlé metry`. Ukazuje se v HUDu jako „Skóre" a pod tím „rekord".
- Po smrti se nejlepší výsledek uloží do `localStorage` prohlížeče (klíč `hranaty_hs`). Přežije zavření i restart.
- Když padne rekord, na obrazovce konce je **„NOVÝ REKORD! 🎉"**.
- Poznámka: uloženo je to pro adresu `http://192.168.5.32:8000`. Když si soubor jednou otevřeš jinak (jiná adresa / `file://`), má to vlastní paměť.

## Barvy postavy (odemykají se rekordem)

V menu je řádek **POSTAVA** — vybíráš **← →** (nebo klikem):

| Barva | Odemkne se při |
|---|---|
| Nebe (modrá) | zdarma |
| Limetka | 250 bodů |
| Meruňka | 600 |
| Malina | 1200 |
| Ametyst | 2500 |
| Zlato | 5000 |

Zamčené jsou ztmavené se zámkem. Jakmile najedeš na odemčenou, hned se nasadí a uloží (klíč `hranaty_skin`). Na obrazovce konce běhu se vypíše, co jsi tím během odemkl.

## Kde se to ladí

- Prahy a barvy: pole `SKINS` (u proměnných pro rekord)
- Vzorec skóre: funkce `escore()`


We're building a single-file 2D platformer in /home/tonda/hra.html, served at http://192.168.5.32:8000/hra.html. Story levels, endless mode with death wall, between-level shop, saved high score, and unlockable character colors are all done. Next: wait for your feedback on what to adjust. (disable recaps in /config)


chci aby bylo více příšerek čím dál se člověk posune v příběhu nebo v nekonečném módu budou tam větší příšerky a taky aby z nich dostával nějakou odměnu z malých nějakou malou a z velkých velkou


<total_tokens>15000000 tokens left</total_tokens>


154	    name: 'Rozcvička', width: 56, floorRow: 12,
155	    playerStart: [2,11], goal: [53,11],
156	    pits: [[20,22],[33,35]],
157	    platforms: [[10,9,4],[26,9,3],[30,7,3],[42,9,4]],
158	    coins: [].concat(cr(10,8,4), cr(16,10,3), cr(26,8,3), cr(31,6,2), cr(42,8,4), cr(48,11,2)),
159	    enemies: [[15,10],[28,8],[40,10],[46,10]],
160	  },
161	  {
162	    name: 'Mezery', width: 68, floorRow: 12,
163	    playerStart: [2,11], goal: [65,11],
164	    pits: [[14,16],[24,26],[38,40],[48,50],[58,60]],
165	    platforms: [[18,9,3],[30,9,3],[33,7,3],[43,9,3],[52,10,4]],
166	    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)),
167	    enemies: [[8,10],[20,10],[35,10],[45,10],[54,10],[62,10]],

    name: 'Rozcvička', width: 56, floorRow: 12,
    playerStart: [2,11], goal: [53,11],
    pits: [[20,22],[33,35]],
    platforms: [[10,9,4],[26,9,3],[30,7,3],[42,9,4]],
    coins: [].concat(cr(10,8,4), cr(16,10,3), cr(26,8,3), cr(31,6,2), cr(42,8,4), cr(48,11,2)),
    enemies: [[15,10],[28,8],[40,10],[46,10]],
  },
  {
    name: 'Mezery', width: 68, floorRow: 12,
    playerStart: [2,11], goal: [65,11],
    pits: [[14,16],[24,26],[38,40],[48,50],[58,60]],
    platforms: [[18,9,3],[30,9,3],[33,7,3],[43,9,3],[52,10,4]],
    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)),
    enemies: [[8,10],[20,10],[35,10],[45,10],[54,10],[62,10]],


<total_tokens>14993670 tokens left</total_tokens>


126	    this.enemies = [];
127	    this.width   = 0;           // šířka v dlaždicích (0 = nekonečno)
128	    this.goal    = null;
129	    this.playerStart = { x: TILE*2, y: TILE*10 };
130	  }
131	  setSolid(c,r){ if (r >= 0 && r < ROWS) this.solid.add(c + ',' + r); }
132	  isSolid(c,r){ return this.solid.has(c + ',' + r); }
133	  firstSolidRow(c, from = 0){
134	    for (let r = from; r < ROWS; r++) if (this.isSolid(c,r)) return r;
135	    return ROWS;
136	  }
137	}
138
139	function mkEnemy(x, y){
140	  return { x, y, w: 28, h: 24, vx: (Math.random() < 0.5 ? -1 : 1) * 1.1, vy: 0, dead: false, t: 0 };
141	}
142
143	/* pomůcka: řada souřadnic [c,r],[c+1,r]... */
144	const cr = (c, r, n = 1) => Array.from({ length: n }, (_, i) => [c + i, r]);
145
146	/* ------------------------------------------------------------------ */
147	/*  Ruční levely                                                      */

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

function mkEnemy(x, y){
  return { x, y, w: 28, h: 24, vx: (Math.random() < 0.5 ? -1 : 1) * 1.1, vy: 0, dead: false, t: 0 };
}

/* pomůcka: řada souřadnic [c,r],[c+1,r]... */
const cr = (c, r, n = 1) => Array.from({ length: n }, (_, i) => [c + i, r]);

/* ------------------------------------------------------------------ */
/*  Ruční levely                                                      */


<total_tokens>14993067 tokens left</total_tokens>


Edit

function mkEnemy(x, y){
  return { x, y, w: 28, h: 24, vx: (Math.random() < 0.5 ? -1 : 1) * 1.1, vy: 0, dead: false, t: 0 };
}

/* 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.4) * 0.7, 0, 0.4);   // velké až později
  const pMed = clamp(0.15 + prog * 0.45, 0, 0.5);
  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){
  return { x, y, w: 28, h: 24, vx: (Math.random() < 0.5 ? -1 : 1) * 1.1, vy: 0, dead: false, t: 0 };
}

/* 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.4) * 0.7, 0, 0.4);   // velké až později
  const pMed = clamp(0.15 + prog * 0.45, 0, 0.5);
  if (r < pBig) return 2;
  if (r < pBig + pMed) return 1;
  return 0;
}

+/* Druhy příšerek: malá / střední / velká.

+   Větší = pomalejší, ale dává víc mincí i bodů. */

+  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) */

+  const pBig = clamp((prog - 0.4) * 0.7, 0, 0.4);   // velké až později

/* pomůcka: řada souřadnic [c,r],[c+1,r]... */

const cr = (c, r, n = 1) => Array.from({ length: n }, (_, i) => [c + i, r]);


<total_tokens>14991909 tokens left</total_tokens>


198	    name: 'Trhliny', width: 96, floorRow: 12,
199	    playerStart: [2,11], goal: [93,11],
200	    pits: [[10,12],[16,18],[22,25],[30,32],[36,39],[44,46],[50,52],[56,59],[64,66],[70,73],[78,80],[85,88]],
201	    platforms: [[14,10,2],[23,10,2],[28,9,3],[37,10,2],[42,9,3],[48,10,2],[57,10,2],[62,9,3],[71,10,2],[76,9,3],[86,10,2]],
202	    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)),
203	    enemies: [[6,10],[20,10],[28,8],[34,10],[42,8],[48,10],[54,10],[62,8],[68,10],[76,8],[82,10],[91,10]],
204	  },
205	];
206
207	function buildLevel(d){
208	  const w = new World();
209	  w.width = d.width;
210	  const fr = d.floorRow ?? 12;
211
212	  for (let c = 0; c < d.width; c++){
213	    if ((d.pits || []).some(([a,b]) => c >= a && c <= b)) continue;
214	    for (let r = fr; r < ROWS; r++) w.setSolid(c, r);
215	  }
216	  for (const [c,r,len] of (d.platforms || []))
217	    for (let i = 0; i < len; i++) w.setSolid(c + i, r);
218
219	  for (const [c,r] of (d.coins || []))
220	    w.coins.push({ x: c*TILE + TILE/2, y: r*TILE + TILE/2, got: false });
221
222	  for (const [c,r] of (d.enemies || [])){
223	    const g = w.firstSolidRow(c, r);              // spadni na nejbližší zem
224	    if (g >= ROWS) continue;
225	    w.enemies.push(mkEnemy(c*TILE + 4, g*TILE - 24));
226	  }
227
228	  const ps = w.firstSolidRow(d.playerStart[0], d.playerStart[1]);
229	  w.playerStart = { x: d.playerStart[0]*TILE, y: ps*TILE - player.h };
230
231	  const gg = w.firstSolidRow(d.goal[0], d.goal[1]);
232	  w.goal = { x: d.goal[0]*TILE, y: gg*TILE };
233	  return w;
234	}
235
236	/* ------------------------------------------------------------------ */
237	/*  Nekonečný běh – procedurální generování                           */

    name: 'Trhliny', width: 96, floorRow: 12,
    playerStart: [2,11], goal: [93,11],
    pits: [[10,12],[16,18],[22,25],[30,32],[36,39],[44,46],[50,52],[56,59],[64,66],[70,73],[78,80],[85,88]],
    platforms: [[14,10,2],[23,10,2],[28,9,3],[37,10,2],[42,9,3],[48,10,2],[57,10,2],[62,9,3],[71,10,2],[76,9,3],[86,10,2]],
    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)),
    enemies: [[6,10],[20,10],[28,8],[34,10],[42,8],[48,10],[54,10],[62,8],[68,10],[76,8],[82,10],[91,10]],
  },
];

function buildLevel(d){
  const w = new World();
  w.width = d.width;
  const fr = d.floorRow ?? 12;

  for (let c = 0; c < d.width; c++){
    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 });

  for (const [c,r] of (d.enemies || [])){
    const g = w.firstSolidRow(c, r);              // spadni na nejbližší zem
    if (g >= ROWS) continue;
    w.enemies.push(mkEnemy(c*TILE + 4, g*TILE - 24));
  }

  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í                           */


<total_tokens>14991683 tokens left</total_tokens>


function buildLevel(d){
  const w = new World();
  w.width = d.width;
  const fr = d.floorRow ?? 12;

  for (let c = 0; c < d.width; c++){
    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 });

  for (const [c,r] of (d.enemies || [])){
    const g = w.firstSolidRow(c, r);              // spadni na nejbližší zem
    if (g >= ROWS) continue;
    w.enemies.push(mkEnemy(c*TILE + 4, g*TILE - 24));
  }

function buildLevel(d, idx = 0){
  const w = new World();
  w.width = d.width;
  const fr = d.floorRow ?? 12;
  const prog = LEVELS.length > 1 ? idx / (LEVELS.length - 1) : 0;

  for (let c = 0; c < d.width; c++){
    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 = Math.round(idx * 2.5);
  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));
  }


function buildLevel(d){
  const w = new World();
  w.width = d.width;
  const fr = d.floorRow ?? 12;

  for (let c = 0; c < d.width; c++){
    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 });

  for (const [c,r] of (d.enemies || [])){
    const g = w.firstSolidRow(c, r);              // spadni na nejbližší zem
    if (g >= ROWS) continue;
    w.enemies.push(mkEnemy(c*TILE + 4, g*TILE - 24));
  }

function buildLevel(d, idx = 0){
  const w = new World();
  w.width = d.width;
  const fr = d.floorRow ?? 12;
  const prog = LEVELS.length > 1 ? idx / (LEVELS.length - 1) : 0;

  for (let c = 0; c < d.width; c++){
    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 = Math.round(idx * 2.5);
  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 prog = LEVELS.length > 1 ? idx / (LEVELS.length - 1) : 0;

-  for (const [c,r] of (d.enemies || [])){

-    const g = w.firstSolidRow(c, r);              // spadni na nejbližší zem

-    w.enemies.push(mkEnemy(c*TILE + 4, g*TILE - 24));

+  const addEnemy = (c, fromRow, tier) => {

+    const g = w.firstSolidRow(c, fromRow);

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

const ps = w.firstSolidRow(d.playerStart[0], d.playerStart[1]);


<total_tokens>14989408 tokens left</total_tokens>


function loadLevel(i){
  world = buildLevel(LEVELS[i]);

function loadLevel(i){
  world = buildLevel(LEVELS[i], i);
  pops = [];


function loadLevel(i){
  world = buildLevel(LEVELS[i]);

function loadLevel(i){
  world = buildLevel(LEVELS[i], i);
  pops = [];

player.vx = player.vy = 0; player.invuln = 40;


<total_tokens>14989106 tokens left</total_tokens>


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

/* náhodný druh podle postupu (0 = začátek, 1 = konec / daleko) */
function rollTier(prog){
  const r = Math.random();
  const pBig = clamp((prog - 0.4) * 0.7, 0, 0.4);   // velké až později
  const pMed = clamp(0.15 + prog * 0.45, 0, 0.5);
  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 });
}


/* náhodný druh podle postupu (0 = začátek, 1 = konec / daleko) */
function rollTier(prog){
  const r = Math.random();
  const pBig = clamp((prog - 0.4) * 0.7, 0, 0.4);   // velké až později
  const pMed = clamp(0.15 + prog * 0.45, 0, 0.5);
  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 });
}

+/* poletující mince po zabití příšerky (jen efekt) */

+  for (let i = 0; i < Math.min(n, 10); i++)

/* pomůcka: řada souřadnic [c,r],[c+1,r]... */

const cr = (c, r, n = 1) => Array.from({ length: n }, (_, i) => [c + i, r]);


<total_tokens>14988199 tokens left</total_tokens>


432	/* ------------------------------------------------------------------ */
433	/*  Interakce                                                         */
434	/* ------------------------------------------------------------------ */
435	function handleInteractions(){
436	  // mince
437	  for (const co of world.coins){
438	    if (co.got) continue;
439	    if (Math.abs(player.x + player.w/2 - co.x) < 18 &&
440	        Math.abs(player.y + player.h/2 - co.y) < 20){
441	      co.got = true; coinsCollected++; score += 10; S.coin();
442	    }
443	  }
444	  // nepřátelé
445	  for (const e of world.enemies){
446	    if (e.dead) continue;
447	    if (!rectOverlap(player.x, player.y, player.w, player.h, e.x, e.y, e.w, e.h)) continue;
448	    const stomp = player.vy > 1.5 && (player.y + player.h) - e.y < 18;
449	    if (stomp){
450	      e.dead = true; e.t = 0; player.vy = -10; score += 50; shake = 4; S.stomp();
451	    } else {
452	      hurtPlayer();
453	    }
454	  }
455	  // cíl
456	  if (world.goal && mode === 'levels' &&
457	      rectOverlap(player.x, player.y, player.w, player.h,

/* ------------------------------------------------------------------ */
/*  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é
  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 < 18;
    if (stomp){
      e.dead = true; e.t = 0; player.vy = -10; score += 50; shake = 4; S.stomp();
    } else {
      hurtPlayer();
    }
  }
  // cíl
  if (world.goal && mode === 'levels' &&
      rectOverlap(player.x, player.y, player.w, player.h,


<total_tokens>14987990 tokens left</total_tokens>


    const stomp = player.vy > 1.5 && (player.y + player.h) - e.y < 18;
    if (stomp){
      e.dead = true; e.t = 0; player.vy = -10; score += 50; shake = 4; S.stomp();
    } else {
      hurtPlayer();
    }

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


    const stomp = player.vy > 1.5 && (player.y + player.h) - e.y < 18;
    if (stomp){
      e.dead = true; e.t = 0; player.vy = -10; score += 50; shake = 4; S.stomp();
    } else {
      hurtPlayer();
    }

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

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

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

-      e.dead = true; e.t = 0; player.vy = -10; score += 50; shake = 4; S.stomp();

+      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>14986969 tokens left</total_tokens>


610	if (SKINS[equippedSkin].unlock > bestHS) equippedSkin = 0;   // pojistka
611	let skinSel = equippedSkin;
612	let newRecord = false, justUnlocked = [];
613
614	const skinUnlocked = i => SKINS[i].unlock <= bestHS;
615	const escore = () => mode === 'endless' ? score + distance : score;
616
617	function saveBest(){
618	  const s = escore();
619	  const old = bestHS;
620	  if (s > bestHS){ bestHS = s; store('hs', bestHS); newRecord = true; }
621	  justUnlocked = SKINS.filter(k => k.unlock > old && k.unlock <= bestHS).map(k => k.name);
622	}
623
624	function startGame(m){
625	  unlockAudio();

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


<total_tokens>14986697 tokens left</total_tokens>


624	function startGame(m){
625	  unlockAudio();
626	  mode = m;
627	  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
628	  shopSel = 0;
629	  newRecord = false; justUnlocked = [];
630	  SHOP.forEach(it => it.bought = 0);
631	  if (m === 'levels'){ levelIndex = 0; loadLevel(0); }
632	  else loadEndless();
633	  state = 'play';
634	}
635
636	function loadLevel(i){
637	  world = buildLevel(LEVELS[i], i);
638	  pops = [];
639	  player.x = world.playerStart.x;
640	  player.y = world.playerStart.y;
641	  player.vx = player.vy = 0; player.invuln = 40;
642	  camX = clamp(player.x - W*0.42, 0, world.width*TILE - W);
643	  camY = 0;

function startGame(m){
  unlockAudio();
  mode = m;
  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0;
  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;


<total_tokens>14986262 tokens left</total_tokens>


  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0;
  newRecord = false; justUnlocked = [];

  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0; pops = [];
  newRecord = false; justUnlocked = [];


  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0;
  newRecord = false; justUnlocked = [];

  lives = 3; coinsCollected = 0; score = 0; distance = 0; shake = 0;
  shopSel = 0; pops = [];
  newRecord = false; justUnlocked = [];

if (m === 'levels'){ levelIndex = 0; loadLevel(0); }


<total_tokens>14985632 tokens left</total_tokens>


686	  for (const e of world.enemies) updateEnemy(e);
687	  handleInteractions();
688	  if (state === 'play') updateCamera();
689	}
690
691	/* ------------------------------------------------------------------ */
692	/*  Vykreslení                                                        */
693	/* ------------------------------------------------------------------ */
694	const COL = {
695	  tile:'#2a3350', tileTop:'#41527f', player:'#5cc8ff', enemy:'#ff5c72',
696	  coin:'#ffd45c', flag:'#4ade80', dark:'#0c0e16',
697	};
698
699	function draw(){
700	  ctx.clearRect(0, 0, W, H);
701

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

/* ------------------------------------------------------------------ */
/*  Vykreslení                                                        */
/* ------------------------------------------------------------------ */
const COL = {
  tile:'#2a3350', tileTop:'#41527f', player:'#5cc8ff', enemy:'#ff5c72',
  coin:'#ffd45c', flag:'#4ade80', dark:'#0c0e16',
};

function draw(){
  ctx.clearRect(0, 0, W, H);


<total_tokens>14985426 tokens left</total_tokens>


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

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


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

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

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


<total_tokens>14984840 tokens left</total_tokens>


718	    const c0 = Math.floor(camX / TILE) - 1;
719	    const c1 = c0 + Math.ceil(W / TILE) + 2;
720	    for (let c = c0; c <= c1; c++)
721	      for (let r = 0; r < ROWS; r++)
722	        if (world.isSolid(c, r)) drawTile(c, r);
723
724	    for (const co of world.coins) if (!co.got) drawCoin(co);
725	    if (world.goal) drawGoal(world.goal);
726	    for (const e of world.enemies) drawEnemy(e);
727	    if (state !== 'shop') drawPlayer();
728	    if (mode === 'endless' && endlessStarted && wallX > camX - 100) drawWall();
729	  }
730	  ctx.restore();
731
732	  // varovné blikání, když je zeď blízko
733	  if (mode === 'endless' && state === 'play' && endlessStarted){

    const c0 = Math.floor(camX / TILE) - 1;
    const c1 = c0 + Math.ceil(W / TILE) + 2;
    for (let c = c0; c <= c1; c++)
      for (let r = 0; r < ROWS; r++)
        if (world.isSolid(c, r)) drawTile(c, r);

    for (const co of world.coins) if (!co.got) drawCoin(co);
    if (world.goal) drawGoal(world.goal);
    for (const e of world.enemies) drawEnemy(e);
    if (state !== 'shop') drawPlayer();
    if (mode === 'endless' && endlessStarted && wallX > camX - 100) drawWall();
  }
  ctx.restore();

  // varovné blikání, když je zeď blízko
  if (mode === 'endless' && state === 'play' && endlessStarted){


<total_tokens>14984614 tokens left</total_tokens>


    for (const e of world.enemies) drawEnemy(e);
    if (state !== 'shop') drawPlayer();

    for (const e of world.enemies) drawEnemy(e);
    for (const p of pops){
      ctx.globalAlpha = Math.max(0, p.life / 34);
      ctx.fillStyle = COL.coin;
      ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, Math.PI * 2); ctx.fill();
    }
    ctx.globalAlpha = 1;
    if (state !== 'shop') drawPlayer();


    for (const e of world.enemies) drawEnemy(e);
    if (state !== 'shop') drawPlayer();

    for (const e of world.enemies) drawEnemy(e);
    for (const p of pops){
      ctx.globalAlpha = Math.max(0, p.life / 34);
      ctx.fillStyle = COL.coin;
      ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, Math.PI * 2); ctx.fill();
    }
    ctx.globalAlpha = 1;
    if (state !== 'shop') drawPlayer();

for (const co of world.coins) if (!co.got) drawCoin(co);

+      ctx.globalAlpha = Math.max(0, p.life / 34);

+      ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, Math.PI * 2); ctx.fill();

if (mode === 'endless' && endlessStarted && wallX > camX - 100) drawWall();


<total_tokens>14983910 tokens left</total_tokens>


781	  ctx.save();
782	  ctx.globalAlpha = 0.05;
783	  ctx.fillStyle = '#6ea8ff';
784	  const p = camX * 0.25;
785	  for (let i = -1; i < 8; i++){
786	    const x = i * 250 - (p % 250);
787	    ctx.save();
788	    ctx.translate(x, 130 + (i & 1 ? 150 : 0));
789	    ctx.rotate(0.5);
790	    ctx.fillRect(-55, -55, 110, 110);
791	    ctx.restore();
792	  }
793	  ctx.restore();
794	}
795
796	function drawTile(c, r){
797	  const x = c*TILE, y = r*TILE;
798	  const open = !world.isSolid(c, r - 1);
799	  ctx.fillStyle = COL.tile;
800	  roundRect(x + 1, y + 1, TILE - 2, TILE - 2, open ? 7 : 2);
801	  ctx.fill();
802	  if (open){
803	    ctx.fillStyle = COL.tileTop;
804	    roundRect(x + 1, y + 1, TILE - 2, 6, 3);

  ctx.save();
  ctx.globalAlpha = 0.05;
  ctx.fillStyle = '#6ea8ff';
  const p = camX * 0.25;
  for (let i = -1; i < 8; i++){
    const x = i * 250 - (p % 250);
    ctx.save();
    ctx.translate(x, 130 + (i & 1 ? 150 : 0));
    ctx.rotate(0.5);
    ctx.fillRect(-55, -55, 110, 110);
    ctx.restore();
  }
  ctx.restore();
}

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


<total_tokens>14983725 tokens left</total_tokens>


808
809	function drawPlayer(){
810	  if (player.invuln > 0 && Math.floor(player.invuln / 4) % 2) return;
811	  const { x, y, w, h } = player;
812	  ctx.fillStyle = SKINS[equippedSkin].color;
813	  roundRect(x, y, w, h, 7); ctx.fill();
814	  ctx.fillStyle = COL.dark;
815	  ctx.fillRect(x + w/2 + player.face * 5 - 2, y + 9, 4, 7);
816	}
817
818	function drawEnemy(e){
819	  if (e.dead){
820	    const k = Math.max(0, 1 - e.t / 18);
821	    if (k <= 0) return;
822	    ctx.globalAlpha = k;
823	    ctx.fillStyle = COL.enemy;
824	    roundRect(e.x, e.y + e.h * 0.62, e.w, e.h * 0.38, 4); ctx.fill();
825	    ctx.globalAlpha = 1;
826	    return;
827	  }
828	  ctx.fillStyle = COL.enemy;
829	  roundRect(e.x, e.y, e.w, e.h, 6); ctx.fill();
830	  ctx.fillStyle = COL.dark;
831	  const d = Math.sign(e.vx) || 1;
832	  ctx.fillRect(e.x + 6 + (d > 0 ? 3 : 0), e.y + 8, 4, 4);
833	  ctx.fillRect(e.x + e.w - 10 + (d > 0 ? 3 : 0), e.y + 8, 4, 4);
834	}
835
836	function drawCoin(co){
837	  const b = Math.sin(performance.now() / 300 + co.x) * 2;
838	  ctx.fillStyle = COL.coin;
839	  ctx.beginPath(); ctx.arc(co.x, co.y + b, 7, 0, Math.PI * 2); 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){
  if (e.dead){
    const k = Math.max(0, 1 - e.t / 18);
    if (k <= 0) return;
    ctx.globalAlpha = k;
    ctx.fillStyle = COL.enemy;
    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.enemy;
  roundRect(e.x, e.y, e.w, e.h, 6); ctx.fill();
  ctx.fillStyle = COL.dark;
  const d = Math.sign(e.vx) || 1;
  ctx.fillRect(e.x + 6 + (d > 0 ? 3 : 0), e.y + 8, 4, 4);
  ctx.fillRect(e.x + e.w - 10 + (d > 0 ? 3 : 0), e.y + 8, 4, 4);
}

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


<total_tokens>14983163 tokens left</total_tokens>


function drawEnemy(e){
  if (e.dead){
    const k = Math.max(0, 1 - e.t / 18);
    if (k <= 0) return;
    ctx.globalAlpha = k;
    ctx.fillStyle = COL.enemy;
    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.enemy;
  roundRect(e.x, e.y, e.w, e.h, 6); ctx.fill();
  ctx.fillStyle = COL.dark;
  const d = Math.sign(e.vx) || 1;
  ctx.fillRect(e.x + 6 + (d > 0 ? 3 : 0), e.y + 8, 4, 4);
  ctx.fillRect(e.x + e.w - 10 + (d > 0 ? 3 : 0), e.y + 8, 4, 4);
}

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


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

function drawEnemy(e){
  if (e.dead){
    const k = Math.max(0, 1 - e.t / 18);
    if (k <= 0) return;
    ctx.globalAlpha = k;
    ctx.fillStyle = COL.enemy;
    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.enemy;
  roundRect(e.x, e.y, e.w, e.h, 6); ctx.fill();
  ctx.fillStyle = COL.dark;
  const d = Math.sign(e.vx) || 1;
  ctx.fillRect(e.x + 6 + (d > 0 ? 3 : 0), e.y + 8, 4, 4);
  ctx.fillRect(e.x + e.w - 10 + (d > 0 ? 3 : 0), e.y + 8, 4, 4);
}

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

-  roundRect(e.x, e.y, e.w, e.h, 6); ctx.fill();

+  roundRect(e.x, e.y, e.w, e.h, Math.min(9, e.w * 0.22)); ctx.fill();

+  ctx.fillRect(cx - e.w * 0.18 - es / 2, ey, es, es);

+  ctx.fillRect(cx + e.w * 0.18 - es / 2, ey, es, es);


<total_tokens>14981534 tokens left</total_tokens>


263	function loadEndless(){
264	  world = new World();
265	  world.width = 0;
266	  genCol = 0; genRow = 12;
267	  generateAhead(0);
268	  player.x = 3*TILE;
269	  player.y = 11*TILE - player.h;
270	  player.vx = player.vy = 0; player.invuln = 40;
271	  camX = 0; camY = 0;
272	  scrollSpeed = 1.4;
273	  wallSpeed = 1.3;
274	  wallX = -TILE*12;
275	  endlessStarted = false;
276	}
277
278	function generateAhead(cx){
279	  const need = Math.floor((cx + W) / TILE) + 22;
280	  let guard = 0;
281	  while (genCol < need && guard++ < 400) genChunk();
282	}
283
284	function genChunk(){
285	  const c0 = genCol;
286	  const r  = genRow;
287	  const diff = Math.min(distance / 500, 1);           // 0 → 1 podle ušlé vzdálenosti
288	  const fill = (a, b, top) => { for (let c = a; c < b; c++) for (let rr = top; rr < ROWS; rr++) world.setSolid(c, rr); };
289
290	  if (genCol < 14){ fill(c0, c0 + 14, r); genCol += 14; return; }   // bezpečný start
291
292	  const t = Math.random();
293
294	  if (t < 0.20 + diff * 0.12){                        // propast
295	    let gap = 2 + Math.floor(Math.random() * (1 + Math.round(diff * 3)));
296	    gap = Math.min(gap, 4);
297	    if (gap >= 4 || Math.random() < 0.45){            // plošina nad propastí
298	      const pc = c0 + 1, plen = Math.max(2, gap - 1);
299	      for (let i = 0; i < plen; i++) world.setSolid(pc + i, r - 3);
300	      for (let i = 0; i < plen; i++)
301	        world.coins.push({ x: (pc + i)*TILE + TILE/2, y: (r - 4)*TILE + TILE/2, got: false });
302	    }
303	    genCol += gap;
304
305	  } else if (t < 0.52){                               // rovinka s mincemi / nepřítelem
306	    const len = 5 + Math.floor(Math.random() * 6);
307	    fill(c0, c0 + len, r);
308	    if (Math.random() < 0.35 + diff * 0.3)
309	      world.enemies.push(mkEnemy((c0 + (len >> 1))*TILE, (r - 2)*TILE));
310	    if (Math.random() < 0.6)
311	      for (let i = 0; i < Math.min(4, len - 1); i++)
312	        world.coins.push({ x: (c0 + 1 + i)*TILE + TILE/2, y: (r - 2)*TILE + TILE/2, got: false });
313	    genCol += len;
314
315	  } else if (t < 0.76){                               // schod nahoru / dolů
316	    genRow = clamp(r + (Math.random() < 0.5 ? -1 : 1), 8, 13);
317	    const len = 4 + Math.floor(Math.random() * 4);
318	    fill(c0, c0 + len, genRow);
319	    genCol += len;
320

function loadEndless(){
  world = new World();
  world.width = 0;
  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řítelem
    const len = 5 + Math.floor(Math.random() * 6);
    fill(c0, c0 + len, r);
    if (Math.random() < 0.35 + diff * 0.3)
      world.enemies.push(mkEnemy((c0 + (len >> 1))*TILE, (r - 2)*TILE));
    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);
    genCol += len;


<total_tokens>14981315 tokens left</total_tokens>


320
321	  } else {                                            // plošinková sekce
322	    const len = 6 + Math.floor(Math.random() * 4);
323	    fill(c0, c0 + len, r);
324	    for (let k = 0; k < 2; k++){
325	      const pc = c0 + 2 + k * 3, pr = r - 3 - (k & 1);
326	      world.setSolid(pc, pr); world.setSolid(pc + 1, pr);
327	      world.coins.push({ x: pc*TILE + TILE, y: (pr - 1)*TILE + TILE/2, got: false });
328	    }
329	    genCol += len;
330	  }
331	}
332
333	let pruneT = 0;
334	function pruneWorld(){
335	  if (++pruneT < 120) return;
336	  pruneT = 0;
337	  const minC = Math.floor(camX / TILE) - 40;
338	  for (const k of world.solid)
339	    if (+k.slice(0, k.indexOf(',')) < minC) world.solid.delete(k);
340	}
341

  } 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 });
    }
    genCol += len;
  }
}

let pruneT = 0;
function pruneWorld(){
  if (++pruneT < 120) return;
  pruneT = 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);
}


<total_tokens>14979993 tokens left</total_tokens>


  } else if (t < 0.52){                               // rovinka s mincemi / nepřítelem
    const len = 5 + Math.floor(Math.random() * 6);
    fill(c0, c0 + len, r);
    if (Math.random() < 0.35 + diff * 0.3)
      world.enemies.push(mkEnemy((c0 + (len >> 1))*TILE, (r - 2)*TILE));
    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);
    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 });
    }
    genCol += len;
  }
}

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


  } else if (t < 0.52){                               // rovinka s mincemi / nepřítelem
    const len = 5 + Math.floor(Math.random() * 6);
    fill(c0, c0 + len, r);
    if (Math.random() < 0.35 + diff * 0.3)
      world.enemies.push(mkEnemy((c0 + (len >> 1))*TILE, (r - 2)*TILE));
    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);
    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 });
    }
    genCol += len;
  }
}

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

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

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

-    if (Math.random() < 0.35 + diff * 0.3)

-      world.enemies.push(mkEnemy((c0 + (len >> 1))*TILE, (r - 2)*TILE));

+    // počet nepřátel roste s ušlou vzdáleností

+    if (n && Math.random() < diff * 0.6) n = 2;

+      const ec = c0 + 1 + Math.floor((e + 0.5) / n * (len - 2));

+      world.enemies.push(mkEnemy(ec*TILE + (TILE - ETIER[tier].w) / 2, (r - 3)*TILE, tier));

+      world.enemies.push(mkEnemy((c0 + (len >> 1))*TILE, (genRow - 3)*TILE, tier));

} else {                                            // plošinková sekce

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


<total_tokens>14977961 tokens left</total_tokens>


  world = new World();
  world.width = 0;
  genCol = 0; genRow = 12;
  generateAhead(0);

  world = new World();
  world.width = 0;
  pops = [];
  genCol = 0; genRow = 12;
  generateAhead(0);


  world = new World();
  world.width = 0;
  genCol = 0; genRow = 12;
  generateAhead(0);

  world = new World();
  world.width = 0;
  pops = [];
  genCol = 0; genRow = 12;
  generateAhead(0);

function loadEndless(){
world = new World();
world.width = 0;
+  pops = [];
genCol = 0; genRow = 12;
generateAhead(0);
player.x = 3*TILE;


<total_tokens>14977372 tokens left</total_tokens>


406	  const r1 = Math.floor(player.y / TILE);
407	  const r2 = Math.floor((player.y + player.h - 1) / TILE);
408
409	  for (let r = r1; r <= r2; r++){
410	    for (let c = c1; c <= c2; c++){
411	      if (!world.isSolid(c, r)) continue;
412	      if (axis === 'x'){
413	        if (player.vx > 0)      player.x = c*TILE - player.w;
414	        else if (player.vx < 0) player.x = c*TILE + TILE;
415	        player.vx = 0;
416	      } else {
417	        if (player.vy > 0){ player.y = r*TILE - player.h; player.onGround = true; }
418	        else if (player.vy < 0) player.y = r*TILE + TILE;
419	        player.vy = 0;
420	      }
421	    }
422	  }
423	}
424
425	/* ------------------------------------------------------------------ */
426	/*  Nepřátelé                                                         */
427	/* ------------------------------------------------------------------ */
428	function updateEnemy(e){
429	  if (e.dead){ e.t++; return; }
430
431	  e.x += e.vx;

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

  e.x += e.vx;


<total_tokens>14977148 tokens left</total_tokens>


431	  e.x += e.vx;
432	  const dir   = Math.sign(e.vx) || 1;
433	  const aheadC = Math.floor((dir > 0 ? e.x + e.w + 1 : e.x - 1) / TILE);
434	  const midR   = Math.floor((e.y + e.h / 2) / TILE);
435	  const footR  = Math.floor((e.y + e.h + 3) / TILE);
436	  if (world.isSolid(aheadC, midR) || !world.isSolid(aheadC, footR)) e.vx = -e.vx;
437
438	  e.vy = Math.min(e.vy + GRAV, MAX_FALL);
439	  e.y += e.vy;
440	  const fr = Math.floor((e.y + e.h) / TILE);
441	  for (let c = Math.floor(e.x / TILE); c <= Math.floor((e.x + e.w - 1) / TILE); c++){
442	    if (world.isSolid(c, fr) && e.vy > 0){ e.y = fr*TILE - e.h; e.vy = 0; break; }
443	  }
444	  if (e.y > ROWS*TILE + 300){ e.dead = true; e.t = 999; }
445	}
446
447	/* ------------------------------------------------------------------ */
448	/*  Interakce                                                         */
449	/* ------------------------------------------------------------------ */
450	function handleInteractions(){
451	  // mince
452	  for (const co of world.coins){

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


<total_tokens>14976584 tokens left</total_tokens>


Bash

python3 - <<'EOF'
import re
js=re.search(r'<script>(.*)</script>',open('hra.html').read(),re.S).group(1)
for op,cl in [('(',')'),('{','}'),('[',']')]:
    a,b=js.count(op),js.count(cl); print(op,cl,a,b,'OK' if a==b else 'MISMATCH')
for tok in ['ETIER','rollTier(','pops','e.tier','spawnCoinPops']:
    print(tok, js.count(tok))
EOF
curl -sS -o /dev/null -w "server HTTP %{http_code}\n" http://localhost:8000/hra.html

Balance check and serve check


( ) 818 818 OK
{ } 171 171 OK
[ ] 162 162 OK
ETIER 7
rollTier( 5
pops 10
e.tier 6
spawnCoinPops 2
server HTTP 200


<total_tokens>14974686 tokens left</total_tokens>


Edit

function rollTier(prog){
  const r = Math.random();
  const pBig = clamp((prog - 0.4) * 0.7, 0, 0.4);   // velké až později
  const pMed = clamp(0.15 + prog * 0.45, 0, 0.5);
  if (r < pBig) return 2;
  if (r < pBig + pMed) return 1;
  return 0;
}

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 rollTier(prog){
  const r = Math.random();
  const pBig = clamp((prog - 0.4) * 0.7, 0, 0.4);   // velké až později
  const pMed = clamp(0.15 + prog * 0.45, 0, 0.5);
  if (r < pBig) return 2;
  if (r < pBig + pMed) return 1;
  return 0;
}

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

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

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

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


<total_tokens>14973315 tokens left</total_tokens>


  // příšerky navíc – čím dál v příběhu, tím víc
  const extra = Math.round(idx * 2.5);

  // příšerky navíc – čím dál v příběhu, tím víc
  const extra = idx * 2;


  // příšerky navíc – čím dál v příběhu, tím víc
  const extra = Math.round(idx * 2.5);

  // příšerky navíc – čím dál v příběhu, tím víc
  const extra = idx * 2;

for (const [c,r] of (d.enemies || [])) addEnemy(c, r, rollTier(prog));

const c = 6 + Math.floor(Math.random() * Math.max(1, d.width - 12));

if (Math.abs(c - d.playerStart[0]) < 6) continue;


<total_tokens>14973026 tokens left</total_tokens>


Hotovo — **F5**.

## Příšerky: 3 velikosti, škálovaná odměna

| Velikost | Rychlost | Odměna za zničení |
|---|---|---|
| 🟥 malá | rychlá | +1 mince, +40 skóre |
| 🟥 střední | střední | +3 mince, +90 skóre |
| 🟥 velká | pomalá | +6 mincí, +180 skóre |

Větší = pomalejší, snáz se přeskočí/dupne, ale odmění líp. Po zničení odletí mince (efekt) a ozve se hlubší zvuk podle velikosti. Větší příšerka tě odrazí výš.

## Postup = víc a větší příšerky

**Příběh:** každý level má navíc `číslo levelu × 2` příšerek k původním. Šance na větší roste s číslem levelu:
- Level 1: skoro jen malé
- Level 4: ~⅓ velkých, polovina středních

**Nekonečný běh:** čím dál dojedeš, tím víc příšerek na rovinkách (i po 2), častěji na schodech a mezi plošinami, a tím vyšší šance na velké. Řídí to `diff = distance / 500`.

Vzniká z toho hezká smyčka: velké příšerky = hodně mincí = víc životů koupených v obchodu.

## Ladění

- `ETIER` — rozměry, rychlost, odměny a barvy tří velikostí
- `rollTier(prog)` — pravděpodobnosti velikostí (`prog` 0→1)
- `buildLevel`: `const extra = idx * 2` — kolik příšerek navíc v příběhu
- `genChunk` (nekonečný) — četnost a počet příšerek v jednotlivých typech úseků


We're building a 2D platformer in a single file /home/tonda/hra.html, served at http://192.168.5.32:8000/hra.html. Latest change added three enemy sizes with scaled rewards and progression-based spawning. Next: you refresh the browser and tell me how it plays.


můžeš udelat mobilni verzi


<total_tokens>15000000 tokens left</total_tokens>


1	<!doctype html>
2
3
4	<meta charset="utf-8">
5	<meta name="viewport" content="width=device-width, initial-scale=1">
6	<title>Hranatý svět — 2D plošinovka</title>
7
32
33
34
Hranatý svět

35	  <canvas id="game" width="900" height="540"></canvas>
36

37	    Pohyb ← → / A D  ·  Skok Space / ↑ / W  ·  Pauza P  ·  Restart R

38	    Skoč nepříteli na hlavu a zničíš ho. Sbírej mince. Nespadni do propasti.
39

40
41

<!doctype html>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hranatý svět — 2D plošinovka</title>

Hranatý svět

  <canvas id="game" width="900" height="540"></canvas>

    Pohyb ← → / A D  ·  Skok Space / ↑ / W  ·  Pauza P  ·  Restart R

    Skoč nepříteli na hlavu a zničíš ho. Sbírej mince. Nespadni do propasti.


<total_tokens>14995489 tokens left</total_tokens>