<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hranatý svět — 2D plošinovka</title>
<style>
  :root{
    --bg:#101320; --ink:#e8ecf5; --muted:#8b93a7; --accent:#5cc8ff;
  }
  *{box-sizing:border-box; margin:0; padding:0}
  html,body{height:100%}
  body{
    background:radial-gradient(1200px 600px at 50% -10%, #1a2036, var(--bg));
    color:var(--ink);
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    min-height:100%; gap:14px; padding:16px;
  }
  h1{font-size:16px; font-weight:650; letter-spacing:1px; color:var(--muted); text-transform:uppercase}
  canvas{
    background:#0c0e16; border-radius:16px; display:block;
    box-shadow:0 30px 80px rgba(0,0,0,.5); max-width:100%; height:auto;
    touch-action:none;
  }
  .hint{color:var(--muted); font-size:13px; text-align:center; line-height:1.7}
  kbd{
    background:#1b1f2e; border:1px solid #2b3042; border-bottom-width:2px;
    border-radius:6px; padding:1px 7px; font-family:inherit; font-size:12px; color:var(--ink);
  }
</style>

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>Hranatý svět — 2D plošinovka</title>
<style>
  :root{
    --bg:#101320; --ink:#e8ecf5; --muted:#8b93a7; --accent:#5cc8ff;
  }
  *{box-sizing:border-box; margin:0; padding:0}
  html,body{height:100%; overflow:hidden; overscroll-behavior:none}
  body{
    background:radial-gradient(1200px 600px at 50% -10%, #1a2036, var(--bg));
    color:var(--ink);
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    min-height:100%; gap:14px; padding:16px;
    -webkit-user-select:none; user-select:none; -webkit-touch-callout:none;
  }
  h1{font-size:16px; font-weight:650; letter-spacing:1px; color:var(--muted); text-transform:uppercase}
  canvas{
    background:#0c0e16; border-radius:16px; display:block;
    box-shadow:0 30px 80px rgba(0,0,0,.5); max-width:100%; height:auto;
    touch-action:none;
  }
  .hint{color:var(--muted); font-size:13px; text-align:center; line-height:1.7}
  kbd{
    background:#1b1f2e; border:1px solid #2b3042; border-bottom-width:2px;
    border-radius:6px; padding:1px 7px; font-family:inherit; font-size:12px; color:var(--ink);
  }
  /* na mobilu: klávesová nápověda pryč, canvas co největší */
  @media (max-width: 760px), (pointer: coarse){
    body{gap:8px; padding:6px; justify-content:flex-start}
    h1{display:none}
    .hint{display:none}
    canvas{width:100%; border-radius:10px}
  }
</style>


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

<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hranatý svět — 2D plošinovka</title>
<style>
  :root{
    --bg:#101320; --ink:#e8ecf5; --muted:#8b93a7; --accent:#5cc8ff;
  }
  *{box-sizing:border-box; margin:0; padding:0}
  html,body{height:100%}
  body{
    background:radial-gradient(1200px 600px at 50% -10%, #1a2036, var(--bg));
    color:var(--ink);
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    min-height:100%; gap:14px; padding:16px;
  }
  h1{font-size:16px; font-weight:650; letter-spacing:1px; color:var(--muted); text-transform:uppercase}
  canvas{
    background:#0c0e16; border-radius:16px; display:block;
    box-shadow:0 30px 80px rgba(0,0,0,.5); max-width:100%; height:auto;
    touch-action:none;
  }
  .hint{color:var(--muted); font-size:13px; text-align:center; line-height:1.7}
  kbd{
    background:#1b1f2e; border:1px solid #2b3042; border-bottom-width:2px;
    border-radius:6px; padding:1px 7px; font-family:inherit; font-size:12px; color:var(--ink);
  }
</style>

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>Hranatý svět — 2D plošinovka</title>
<style>
  :root{
    --bg:#101320; --ink:#e8ecf5; --muted:#8b93a7; --accent:#5cc8ff;
  }
  *{box-sizing:border-box; margin:0; padding:0}
  html,body{height:100%; overflow:hidden; overscroll-behavior:none}
  body{
    background:radial-gradient(1200px 600px at 50% -10%, #1a2036, var(--bg));
    color:var(--ink);
    font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    min-height:100%; gap:14px; padding:16px;
    -webkit-user-select:none; user-select:none; -webkit-touch-callout:none;
  }
  h1{font-size:16px; font-weight:650; letter-spacing:1px; color:var(--muted); text-transform:uppercase}
  canvas{
    background:#0c0e16; border-radius:16px; display:block;
    box-shadow:0 30px 80px rgba(0,0,0,.5); max-width:100%; height:auto;
    touch-action:none;
  }
  .hint{color:var(--muted); font-size:13px; text-align:center; line-height:1.7}
  kbd{
    background:#1b1f2e; border:1px solid #2b3042; border-bottom-width:2px;
    border-radius:6px; padding:1px 7px; font-family:inherit; font-size:12px; color:var(--ink);
  }
  /* na mobilu: klávesová nápověda pryč, canvas co největší */
  @media (max-width: 760px), (pointer: coarse){
    body{gap:8px; padding:6px; justify-content:flex-start}
    h1{display:none}
    .hint{display:none}
    canvas{width:100%; border-radius:10px}
  }
</style>

-<meta name="viewport" content="width=device-width, initial-scale=1">

+<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">

+  html,body{height:100%; overflow:hidden; overscroll-behavior:none}

+    -webkit-user-select:none; user-select:none; -webkit-touch-callout:none;

+  /* na mobilu: klávesová nápověda pryč, canvas co největší */

+  @media (max-width: 760px), (pointer: coarse){

+    body{gap:8px; padding:6px; justify-content:flex-start}

+    canvas{width:100%; border-radius:10px}


<total_tokens>14993030 tokens left</total_tokens>


57	/* ------------------------------------------------------------------ */
58	const canvas = document.getElementById('game');
59	const ctx = canvas.getContext('2d');
60	const W = canvas.width, H = canvas.height;
61	const TILE = 36;                 // velikost jedné dlaždice
62	const ROWS = 15;                 // výška světa v dlaždicích (15*36 = 540)
63	const GRAV = 0.62;
64	const MAX_FALL = 16;
65
66	const clamp = (v,a,b) => v < a ? a : v > b ? b : v;
67	const rectOverlap = (ax,ay,aw,ah, bx,by,bw,bh) =>
68	  ax < bx+bw && ax+aw > bx && ay < by+bh && ay+ah > by;
69
70	function roundRect(x,y,w,h,r){
71	  r = Math.min(r, w/2, h/2);
72	  ctx.beginPath();
73	  ctx.moveTo(x+r,y);
74	  ctx.arcTo(x+w,y,   x+w,y+h, r);
75	  ctx.arcTo(x+w,y+h, x,  y+h, r);
76	  ctx.arcTo(x,  y+h, x,  y,   r);
77	  ctx.arcTo(x,  y,   x+w,y,   r);
78	  ctx.closePath();
79	}
80
81	/* ------------------------------------------------------------------ */
82	/*  Vstup z klávesnice                                                */
83	/* ------------------------------------------------------------------ */
84	const codesDown = new Set();     // právě držené klávesy
85	const codesHit  = new Set();     // stisknuté v tomto snímku (náběžná hrana)
86	const BLOCK = ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space'];

/* ------------------------------------------------------------------ */
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const TILE = 36;                 // velikost jedné dlaždice
const ROWS = 15;                 // výška světa v dlaždicích (15*36 = 540)
const GRAV = 0.62;
const MAX_FALL = 16;

const clamp = (v,a,b) => v < a ? a : v > b ? b : v;
const rectOverlap = (ax,ay,aw,ah, bx,by,bw,bh) =>
  ax < bx+bw && ax+aw > bx && ay < by+bh && ay+ah > by;

function roundRect(x,y,w,h,r){
  r = Math.min(r, w/2, h/2);
  ctx.beginPath();
  ctx.moveTo(x+r,y);
  ctx.arcTo(x+w,y,   x+w,y+h, r);
  ctx.arcTo(x+w,y+h, x,  y+h, r);
  ctx.arcTo(x,  y+h, x,  y,   r);
  ctx.arcTo(x,  y,   x+w,y,   r);
  ctx.closePath();
}

/* ------------------------------------------------------------------ */
/*  Vstup z klávesnice                                                */
/* ------------------------------------------------------------------ */
const codesDown = new Set();     // právě držené klávesy
const codesHit  = new Set();     // stisknuté v tomto snímku (náběžná hrana)
const BLOCK = ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space'];


<total_tokens>14992802 tokens left</total_tokens>


86	const BLOCK = ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space'];
87
88	addEventListener('keydown', e => {
89	  if (BLOCK.includes(e.code)) e.preventDefault();
90	  if (!e.repeat) codesHit.add(e.code);
91	  codesDown.add(e.code);
92	  unlockAudio();
93	});
94	addEventListener('keyup', e => codesDown.delete(e.code));
95
96	const down = (...c) => c.some(k => codesDown.has(k));
97	const hit  = (...c) => c.some(k => codesHit.has(k));
98
99	/* ------------------------------------------------------------------ */
100	/*  Zvuk (WebAudio, generovaný – žádné externí soubory)               */
101	/* ------------------------------------------------------------------ */

const BLOCK = ['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space'];

addEventListener('keydown', e => {
  if (BLOCK.includes(e.code)) e.preventDefault();
  if (!e.repeat) codesHit.add(e.code);
  codesDown.add(e.code);
  unlockAudio();
});
addEventListener('keyup', e => codesDown.delete(e.code));

const down = (...c) => c.some(k => codesDown.has(k));
const hit  = (...c) => c.some(k => codesHit.has(k));

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


<total_tokens>14992009 tokens left</total_tokens>


Edit

addEventListener('keyup', e => codesDown.delete(e.code));

const down = (...c) => c.some(k => codesDown.has(k));
const hit  = (...c) => c.some(k => codesHit.has(k));

addEventListener('keyup', e => codesDown.delete(e.code));

const down = (...c) => c.some(k => codesDown.has(k));
const hit  = (...c) => c.some(k => codesHit.has(k));

/* ------------------------------------------------------------------ */
/*  Dotykové ovládání (mobil)                                         */
/* ------------------------------------------------------------------ */
let isTouch = (matchMedia && matchMedia('(pointer: coarse)').matches) || ('ontouchstart' in window);
const activePtr = new Map();     // pointerId -> 'left' | 'right' | 'jump'

const TB = {                     // rozmístění tlačítek (v souřadnicích plátna)
  left:  { x: 24,  y: H - 150, w: 116, h: 116 },
  right: { x: 156, y: H - 150, w: 116, h: 116 },
  jump:  { cx: W - 96, cy: H - 92, r: 78 },
  pause: { cx: W / 2, cy: 30, r: 24 },
};
const inRect = (p, b) => p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h;
const inCirc = (p, b) => Math.hypot(p.x - b.cx, p.y - b.cy) <= b.r;

function touchZone(p){
  if (inCirc(p, TB.pause)) return 'pause';
  if (inCirc(p, TB.jump))  return 'jump';
  if (inRect(p, TB.left))  return 'left';
  if (inRect(p, TB.right)) return 'right';
  return null;
}

/* promítne stav prstů do stejných "kláves", které používá zbytek hry */
function applyTouch(){
  let l = false, r = false, j = false;
  for (const z of activePtr.values()){
    if (z === 'left') l = true;
    else if (z === 'right') r = true;
    else if (z === 'jump') j = true;
  }
  codesDown[l ? 'add' : 'delete']('ArrowLeft');
  codesDown[r ? 'add' : 'delete']('ArrowRight');
  codesDown[j ? 'add' : 'delete']('Space');
}


addEventListener('keyup', e => codesDown.delete(e.code));

const down = (...c) => c.some(k => codesDown.has(k));
const hit  = (...c) => c.some(k => codesHit.has(k));

/* ------------------------------------------------------------------ */
/*  Dotykové ovládání (mobil)                                         */
/* ------------------------------------------------------------------ */
let isTouch = (matchMedia && matchMedia('(pointer: coarse)').matches) || ('ontouchstart' in window);
const activePtr = new Map();     // pointerId -> 'left' | 'right' | 'jump'

const TB = {                     // rozmístění tlačítek (v souřadnicích plátna)
  left:  { x: 24,  y: H - 150, w: 116, h: 116 },
  right: { x: 156, y: H - 150, w: 116, h: 116 },
  jump:  { cx: W - 96, cy: H - 92, r: 78 },
  pause: { cx: W / 2, cy: 30, r: 24 },
};
const inRect = (p, b) => p.x >= b.x && p.x <= b.x + b.w && p.y >= b.y && p.y <= b.y + b.h;
const inCirc = (p, b) => Math.hypot(p.x - b.cx, p.y - b.cy) <= b.r;

function touchZone(p){
  if (inCirc(p, TB.pause)) return 'pause';
  if (inCirc(p, TB.jump))  return 'jump';
  if (inRect(p, TB.left))  return 'left';
  if (inRect(p, TB.right)) return 'right';
  return null;
}

/* promítne stav prstů do stejných "kláves", které používá zbytek hry */
function applyTouch(){
  let l = false, r = false, j = false;
  for (const z of activePtr.values()){
    if (z === 'left') l = true;
    else if (z === 'right') r = true;
    else if (z === 'jump') j = true;
  }
  codesDown[l ? 'add' : 'delete']('ArrowLeft');
  codesDown[r ? 'add' : 'delete']('ArrowRight');
  codesDown[j ? 'add' : 'delete']('Space');
}

+/*  Dotykové ovládání (mobil)                                         */

+let isTouch = (matchMedia && matchMedia('(pointer: coarse)').matches) || ('ontouchstart' in window);

+const activePtr = new Map();     // pointerId -> 'left' | 'right' | 'jump'

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

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

+  if (inCirc(p, TB.pause)) return 'pause';

+  if (inCirc(p, TB.jump))  return 'jump';

+  if (inRect(p, TB.left))  return 'left';

+  if (inRect(p, TB.right)) return 'right';

+/* promítne stav prstů do stejných "kláves", které používá zbytek hry */

+  codesDown[l ? 'add' : 'delete']('ArrowLeft');

+  codesDown[r ? 'add' : 'delete']('ArrowRight');

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

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


<total_tokens>14990697 tokens left</total_tokens>


1044	  ctx.fillText('2D plošinovka — vyber si režim', W/2, H/2 - 90);
1045	  ctx.fillStyle = COL.coin;
1046	  ctx.font = '600 14px system-ui, sans-serif';
1047	  ctx.fillText('Rekord v nekonečném běhu: ' + bestHS, W/2, H/2 - 66);
1048
1049	  menuOption('1', 'Příběhové levely', LEVELS.length + ' ručně navržených levelů s cílem a životy', H/2 - 26, menuSel === 0);
1050	  menuOption('2', 'Nekonečný běh', 'Náhodná trať, která zrychluje — hraj na skóre', H/2 + 42, menuSel === 1);
1051
1052	  drawSkinPicker(H/2 + 118);
1053
1054	  ctx.fillStyle = '#8b93a7';
1055	  ctx.font = '500 13px system-ui, sans-serif';
1056	  ctx.fillText('↑ ↓ režim  ·  ← → barva postavy  ·  Enter start  ·  1 / 2', W/2, H - 26);
1057	  ctx.restore();
1058	}
1059
1060	function drawSkinPicker(y){
1061	  const n = SKINS.length, sw = 34, gap = 12;
1062	  const total = n * sw + (n - 1) * gap;
1063	  let x = W/2 - total / 2;
1064
1065	  ctx.textAlign = 'center';
1066	  ctx.fillStyle = '#8b93a7';
1067	  ctx.font = '600 12px system-ui, sans-serif';
1068	  ctx.fillText('POSTAVA', W/2, y - 14);
1069
1070	  for (let i = 0; i < n; i++){
1071	    const s = SKINS[i], open = skinUnlocked(i);
1072	    ctx.globalAlpha = open ? 1 : 0.25;
1073	    ctx.fillStyle = s.color;
1074	    roundRect(x, y, sw, sw, 8); ctx.fill();
1075	    ctx.globalAlpha = 1;
1076	    if (i === equippedSkin){
1077	      ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 3;
1078	      roundRect(x - 2, y - 2, sw + 4, sw + 4, 9); ctx.stroke();
1079	    }
1080	    if (i === skinSel){
1081	      ctx.strokeStyle = COL.player; ctx.lineWidth = 2;
1082	      roundRect(x - 6, y - 6, sw + 12, sw + 12, 12); ctx.stroke();
1083	    }
1084	    if (!open){
1085	      ctx.fillStyle = '#e8ecf5';
1086	      ctx.font = '14px system-ui, sans-serif';
1087	      ctx.fillText('🔒', x + sw/2, y + sw/2 + 5);
1088	    }
1089	    x += sw + gap;
1090	  }
1091
1092	  const s = SKINS[skinSel];
1093	  ctx.fillStyle = '#e8ecf5';
1094	  ctx.font = '600 14px system-ui, sans-serif';
1095	  ctx.fillText(
1096	    skinUnlocked(skinSel)
1097	      ? s.name + (skinSel === equippedSkin ? '  ✓ nasazeno' : '')

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


<total_tokens>14990474 tokens left</total_tokens>


1118	  ctx.textAlign = 'center';
1119	}
1120
1121	/* ------------------------------------------------------------------ */
1122	/*  Myš / dotyk pro menu a obrazovky                                  */
1123	/* ------------------------------------------------------------------ */
1124	function canvasPos(e){
1125	  const r = canvas.getBoundingClientRect();
1126	  return { x: (e.clientX - r.left) * (W / r.width), y: (e.clientY - r.top) * (H / r.height) };
1127	}
1128	canvas.addEventListener('pointerdown', e => {
1129	  unlockAudio();
1130	  const p = canvasPos(e);
1131	  if (state === 'menu'){
1132	    const ys = [H/2 - 26, H/2 + 42];
1133	    for (let i = 0; i < 2; i++)
1134	      if (Math.abs(p.y - ys[i]) < 29 && Math.abs(p.x - W/2) < 235)
1135	        return startGame(i === 0 ? 'levels' : 'endless');
1136	    // klik na barvu postavy
1137	    const n = SKINS.length, sw = 34, gap = 12, sy = H/2 + 118;
1138	    let sx = W/2 - (n * sw + (n - 1) * gap) / 2;
1139	    for (let i = 0; i < n; i++){
1140	      if (p.x >= sx && p.x <= sx + sw && p.y >= sy && p.y <= sy + sw){
1141	        skinSel = i;
1142	        if (skinUnlocked(i) && i !== equippedSkin){ equippedSkin = i; store('skin', i); S.coin(); }
1143	        return;
1144	      }
1145	      sx += sw + gap;
1146	    }
1147	  } else if (state === 'shop'){
1148	    for (let i = 0; i < SHOP.length; i++){
1149	      const y = SHOP_Y0 + i * (SHOP_ROW_H + SHOP_GAP);
1150	      if (p.x >= SHOP_X && p.x <= SHOP_X + SHOP_W && p.y >= y && p.y <= y + SHOP_ROW_H)
1151	        return buyShop(i);
1152	    }
1153	    const by = shopBtnY();
1154	    if (p.x >= W/2 - 120 && p.x <= W/2 + 120 && p.y >= by && p.y <= by + 44)
1155	      return shopContinue();
1156	  } else if (state === 'over' || state === 'win'){
1157	    state = 'menu';
1158	  } else if (state === 'paused'){
1159	    state = 'play';
1160	  }
1161	});
1162
1163	/* ------------------------------------------------------------------ */
1164	/*  Hlavní smyčka – pevný krok 60 Hz                                  */
1165	/* ------------------------------------------------------------------ */
1166	let last = performance.now(), acc = 0;
1167	const STEP = 1000 / 60;
1168
1169	function frame(now){
1170	  acc += Math.min(now - last, 100);
1171	  last = now;
1172	  let steps = 0;

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

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

function frame(now){
  acc += Math.min(now - last, 100);
  last = now;
  let steps = 0;


<total_tokens>14989101 tokens left</total_tokens>


canvas.addEventListener('pointerdown', e => {
  unlockAudio();
  const p = canvasPos(e);
  if (state === 'menu'){
    const ys = [H/2 - 26, H/2 + 42];

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

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

  // herní dotyková tlačítka mají přednost během hry
  if (state === 'play' || state === 'paused'){
    const z = touchZone(p);
    if (z === 'pause'){ state = state === 'play' ? 'paused' : 'play'; return; }
    if (z && state === 'play'){
      activePtr.set(e.pointerId, z);
      if (z === 'jump') codesHit.add('Space');   // náběžná hrana skoku
      applyTouch();
      return;
    }
  }

  if (state === 'menu'){
    const ys = [H/2 - 26, H/2 + 42];


canvas.addEventListener('pointerdown', e => {
  unlockAudio();
  const p = canvasPos(e);
  if (state === 'menu'){
    const ys = [H/2 - 26, H/2 + 42];

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

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

  // herní dotyková tlačítka mají přednost během hry
  if (state === 'play' || state === 'paused'){
    const z = touchZone(p);
    if (z === 'pause'){ state = state === 'play' ? 'paused' : 'play'; return; }
    if (z && state === 'play'){
      activePtr.set(e.pointerId, z);
      if (z === 'jump') codesHit.add('Space');   // náběžná hrana skoku
      applyTouch();
      return;
    }
  }

  if (state === 'menu'){
    const ys = [H/2 - 26, H/2 + 42];

const r = canvas.getBoundingClientRect();

return { x: (e.clientX - r.left) * (W / r.width), y: (e.clientY - r.top) * (H / r.height) };

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

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

+  // herní dotyková tlačítka mají přednost během hry

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

+    if (z === 'pause'){ state = state === 'play' ? 'paused' : 'play'; return; }

+      if (z === 'jump') codesHit.add('Space');   // náběžná hrana skoku


<total_tokens>14987449 tokens left</total_tokens>


  } else if (state === 'over' || state === 'win'){
    state = 'menu';
  } else if (state === 'paused'){
    state = 'play';
  }
});

  } else if (state === 'over' || state === 'win'){
    state = 'menu';
  } else if (state === 'paused'){
    state = 'play';
  }
});

canvas.addEventListener('pointermove', e => {
  if (!activePtr.has(e.pointerId)) return;
  const z = touchZone(canvasPos(e));
  const prev = activePtr.get(e.pointerId);
  if (z !== prev){
    if (z && z !== 'pause'){
      if (z === 'jump' && prev !== 'jump') codesHit.add('Space');
      activePtr.set(e.pointerId, z);
    } else {
      activePtr.delete(e.pointerId);
    }
    applyTouch();
  }
});

function endPtr(e){ if (activePtr.delete(e.pointerId)) applyTouch(); }
canvas.addEventListener('pointerup', endPtr);
canvas.addEventListener('pointercancel', endPtr);
addEventListener('blur', () => { activePtr.clear(); applyTouch(); });


  } else if (state === 'over' || state === 'win'){
    state = 'menu';
  } else if (state === 'paused'){
    state = 'play';
  }
});

canvas.addEventListener('pointermove', e => {
  if (!activePtr.has(e.pointerId)) return;
  const z = touchZone(canvasPos(e));
  const prev = activePtr.get(e.pointerId);
  if (z !== prev){
    if (z && z !== 'pause'){
      if (z === 'jump' && prev !== 'jump') codesHit.add('Space');
      activePtr.set(e.pointerId, z);
    } else {
      activePtr.delete(e.pointerId);
    }
    applyTouch();
  }
});

function endPtr(e){ if (activePtr.delete(e.pointerId)) applyTouch(); }
canvas.addEventListener('pointerup', endPtr);
canvas.addEventListener('pointercancel', endPtr);
addEventListener('blur', () => { activePtr.clear(); applyTouch(); });

+canvas.addEventListener('pointermove', e => {

+  if (!activePtr.has(e.pointerId)) return;

+  const prev = activePtr.get(e.pointerId);

+      if (z === 'jump' && prev !== 'jump') codesHit.add('Space');

+function endPtr(e){ if (activePtr.delete(e.pointerId)) applyTouch(); }

+addEventListener('blur', () => { activePtr.clear(); applyTouch(); });

/*  Hlavní smyčka – pevný krok 60 Hz                                  */


<total_tokens>14986829 tokens left</total_tokens>


764	  ctx.clearRect(0, 0, W, H);
765
766	  const g = ctx.createLinearGradient(0, 0, 0, H);
767	  g.addColorStop(0, '#141829'); g.addColorStop(1, '#0b0d15');
768	  ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);
769
770	  drawParallax();
771
772	  const sx = shake > 0.4 ? (Math.random() - 0.5) * shake : 0;
773	  const sy = shake > 0.4 ? (Math.random() - 0.5) * shake : 0;
774
775	  ctx.save();
776	  ctx.translate(Math.round(-camX + sx), Math.round(-camY + sy));
777
778	  if (world){
779	    const c0 = Math.floor(camX / TILE) - 1;
780	    const c1 = c0 + Math.ceil(W / TILE) + 2;
781	    for (let c = c0; c <= c1; c++)
782	      for (let r = 0; r < ROWS; r++)
783	        if (world.isSolid(c, r)) drawTile(c, r);

  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;

  ctx.save();
  ctx.translate(Math.round(-camX + sx), Math.round(-camY + sy));

  if (world){
    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);


<total_tokens>14986529 tokens left</total_tokens>


800	  if (mode === 'endless' && state === 'play' && endlessStarted){
801	    const gap = player.x - wallX;
802	    if (gap < 240){
803	      const a = clamp(1 - gap / 240, 0, 1);
804	      const pulse = 0.45 + 0.3 * Math.sin(performance.now() / 90);
805	      const grd = ctx.createLinearGradient(0, 0, W * 0.55, 0);
806	      grd.addColorStop(0, 'rgba(255,45,65,' + (a * pulse).toFixed(3) + ')');
807	      grd.addColorStop(1, 'rgba(255,45,65,0)');
808	      ctx.fillStyle = grd;
809	      ctx.fillRect(0, 0, W * 0.55, H);
810	    }
811	  }
812
813	  if (world) drawHUD();
814
815	  if (state === 'menu')            drawMenu();
816	  else if (state === 'paused')     overlay('PAUZA', 'P nebo Enter – pokračovat   ·   Esc – menu');
817	  else if (state === 'shop')       drawShop();
818	  else if (state === 'over')       drawOver();
819	  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');
820	}
821
822	function drawWall(){
823	  const wx = wallX, top = -160, h = (ROWS + 6) * TILE;
824	  ctx.fillStyle = '#6f0f1e';
825	  ctx.fillRect(wx - 2400, top, 2400, h);
826	  ctx.fillStyle = 'rgba(255,70,90,.9)';
827	  ctx.fillRect(wx - 5, top, 5, h);
828	  const t = performance.now() / 110;
829	  ctx.fillStyle = 'rgba(255,150,165,.5)';
830	  for (let y = top; y < top + h; y += 36){
831	    const o = (t * 10) % 36;
832	    ctx.beginPath();
833	    ctx.moveTo(wx - 22, y + o);
834	    ctx.lineTo(wx - 4,  y + o + 11);
835	    ctx.lineTo(wx - 22, y + o + 22);

  if (mode === 'endless' && state === 'play' && endlessStarted){
    const gap = player.x - wallX;
    if (gap < 240){
      const a = clamp(1 - gap / 240, 0, 1);
      const pulse = 0.45 + 0.3 * Math.sin(performance.now() / 90);
      const grd = ctx.createLinearGradient(0, 0, W * 0.55, 0);
      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')       drawOver();
  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');
}

function drawWall(){
  const wx = wallX, top = -160, h = (ROWS + 6) * TILE;
  ctx.fillStyle = '#6f0f1e';
  ctx.fillRect(wx - 2400, top, 2400, h);
  ctx.fillStyle = 'rgba(255,70,90,.9)';
  ctx.fillRect(wx - 5, top, 5, h);
  const t = performance.now() / 110;
  ctx.fillStyle = 'rgba(255,150,165,.5)';
  for (let y = top; y < top + h; y += 36){
    const o = (t * 10) % 36;
    ctx.beginPath();
    ctx.moveTo(wx - 22, y + o);
    ctx.lineTo(wx - 4,  y + o + 11);
    ctx.lineTo(wx - 22, y + o + 22);


<total_tokens>14985982 tokens left</total_tokens>


  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')       drawOver();
  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');
}

  if (world) drawHUD();

  if (state === 'menu')            drawMenu();
  else if (state === 'paused')     overlay('PAUZA', isTouch ? 'ťukni pro pokračování' : 'P nebo Enter – pokračovat   ·   Esc – menu');
  else if (state === 'shop')       drawShop();
  else if (state === 'over')       drawOver();
  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');

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

function padBtn(b, label, active){
  ctx.fillStyle = active ? 'rgba(92,200,255,.32)' : 'rgba(255,255,255,.10)';
  roundRect(b.x, b.y, b.w, b.h, 18); ctx.fill();
  ctx.strokeStyle = active ? COL.player : 'rgba(255,255,255,.22)';
  ctx.lineWidth = 2; roundRect(b.x, b.y, b.w, b.h, 18); ctx.stroke();
  ctx.fillStyle = '#e8ecf5';
  ctx.font = '600 34px system-ui, sans-serif';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText(label, b.x + b.w / 2, b.y + b.h / 2 + 2);
  ctx.textBaseline = 'alphabetic';
}
function circBtn(b, label, active, fs){
  ctx.fillStyle = active ? 'rgba(92,200,255,.32)' : 'rgba(255,255,255,.10)';
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = active ? COL.player : 'rgba(255,255,255,.22)';
  ctx.lineWidth = 2; ctx.stroke();
  ctx.fillStyle = '#e8ecf5';
  ctx.font = '600 ' + (fs || 30) + 'px system-ui, sans-serif';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText(label, b.cx, b.cy + 1);
  ctx.textBaseline = 'alphabetic';
}
function drawTouch(){
  ctx.save();
  const has = z => { for (const v of activePtr.values()) if (v === z) return true; return false; };
  padBtn(TB.left,  '◀', has('left'));
  padBtn(TB.right, '▶', has('right'));
  circBtn(TB.jump, 'SKOK', has('jump'), 20);
  circBtn(TB.pause, state === 'paused' ? '▶' : '❚❚', false, 18);
  ctx.restore();
}
function drawRotateHint(){
  ctx.save();
  ctx.fillStyle = 'rgba(8,10,16,.9)';
  ctx.fillRect(0, 0, W, H);
  ctx.textAlign = 'center';
  ctx.fillStyle = '#e8ecf5';
  ctx.font = '700 26px system-ui, sans-serif';
  ctx.fillText('🔄  Otoč telefon na šířku', W / 2, H / 2);
  ctx.restore();
}


  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')       drawOver();
  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');
}

  if (world) drawHUD();

  if (state === 'menu')            drawMenu();
  else if (state === 'paused')     overlay('PAUZA', isTouch ? 'ťukni pro pokračování' : 'P nebo Enter – pokračovat   ·   Esc – menu');
  else if (state === 'shop')       drawShop();
  else if (state === 'over')       drawOver();
  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');

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

function padBtn(b, label, active){
  ctx.fillStyle = active ? 'rgba(92,200,255,.32)' : 'rgba(255,255,255,.10)';
  roundRect(b.x, b.y, b.w, b.h, 18); ctx.fill();
  ctx.strokeStyle = active ? COL.player : 'rgba(255,255,255,.22)';
  ctx.lineWidth = 2; roundRect(b.x, b.y, b.w, b.h, 18); ctx.stroke();
  ctx.fillStyle = '#e8ecf5';
  ctx.font = '600 34px system-ui, sans-serif';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText(label, b.x + b.w / 2, b.y + b.h / 2 + 2);
  ctx.textBaseline = 'alphabetic';
}
function circBtn(b, label, active, fs){
  ctx.fillStyle = active ? 'rgba(92,200,255,.32)' : 'rgba(255,255,255,.10)';
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = active ? COL.player : 'rgba(255,255,255,.22)';
  ctx.lineWidth = 2; ctx.stroke();
  ctx.fillStyle = '#e8ecf5';
  ctx.font = '600 ' + (fs || 30) + 'px system-ui, sans-serif';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText(label, b.cx, b.cy + 1);
  ctx.textBaseline = 'alphabetic';
}
function drawTouch(){
  ctx.save();
  const has = z => { for (const v of activePtr.values()) if (v === z) return true; return false; };
  padBtn(TB.left,  '◀', has('left'));
  padBtn(TB.right, '▶', has('right'));
  circBtn(TB.jump, 'SKOK', has('jump'), 20);
  circBtn(TB.pause, state === 'paused' ? '▶' : '❚❚', false, 18);
  ctx.restore();
}
function drawRotateHint(){
  ctx.save();
  ctx.fillStyle = 'rgba(8,10,16,.9)';
  ctx.fillRect(0, 0, W, H);
  ctx.textAlign = 'center';
  ctx.fillStyle = '#e8ecf5';
  ctx.font = '700 26px system-ui, sans-serif';
  ctx.fillText('🔄  Otoč telefon na šířku', W / 2, H / 2);
  ctx.restore();
}

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

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

+  else if (state === 'paused')     overlay('PAUZA', isTouch ? 'ťukni pro pokračování' : 'P nebo Enter – pokračovat   ·   Esc – menu');

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

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

+  if (isTouch && (state === 'play' || state === 'paused')) drawTouch();

+  if (isTouch && innerHeight > innerWidth * 1.1) drawRotateHint();

+  roundRect(b.x, b.y, b.w, b.h, 18); ctx.fill();

+  ctx.strokeStyle = active ? COL.player : 'rgba(255,255,255,.22)';

+  ctx.lineWidth = 2; roundRect(b.x, b.y, b.w, b.h, 18); ctx.stroke();

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

+  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';

+  ctx.fillText(label, b.x + b.w / 2, b.y + b.h / 2 + 2);

+function circBtn(b, label, active, fs){

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

+  ctx.font = '600 ' + (fs || 30) + 'px system-ui, sans-serif';

+  const has = z => { for (const v of activePtr.values()) if (v === z) return true; return false; };

+  circBtn(TB.jump, 'SKOK', has('jump'), 20);

+  circBtn(TB.pause, state === 'paused' ? '▶' : '❚❚', false, 18);

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

+  ctx.fillText('🔄  Otoč telefon na šířku', W / 2, H / 2);

const wx = wallX, top = -160, h = (ROWS + 6) * TILE;


<total_tokens>14983639 tokens left</total_tokens>


  *{box-sizing:border-box; margin:0; padding:0}
  html,body{height:100%; overflow:hidden; overscroll-behavior:none}
  body{

  *{box-sizing:border-box; margin:0; padding:0}
  html,body{height:100%; overscroll-behavior:none}
  body{


  *{box-sizing:border-box; margin:0; padding:0}
  html,body{height:100%; overflow:hidden; overscroll-behavior:none}
  body{

  *{box-sizing:border-box; margin:0; padding:0}
  html,body{height:100%; overscroll-behavior:none}
  body{

-  html,body{height:100%; overflow:hidden; overscroll-behavior:none}

+  html,body{height:100%; overscroll-behavior:none}

background:radial-gradient(1200px 600px at 50% -10%, #1a2036, var(--bg));


<total_tokens>14981370 tokens left</total_tokens>


  @media (max-width: 760px), (pointer: coarse){
    body{gap:8px; padding:6px; justify-content:flex-start}
    h1{display:none}
    .hint{display:none}
    canvas{width:100%; border-radius:10px}
  }

  @media (max-width: 760px), (pointer: coarse){
    html,body{overflow:hidden}
    body{gap:8px; padding:6px; justify-content:center}
    h1{display:none}
    .hint{display:none}
    canvas{width:100%; border-radius:10px}
  }


  @media (max-width: 760px), (pointer: coarse){
    body{gap:8px; padding:6px; justify-content:flex-start}
    h1{display:none}
    .hint{display:none}
    canvas{width:100%; border-radius:10px}
  }

  @media (max-width: 760px), (pointer: coarse){
    html,body{overflow:hidden}
    body{gap:8px; padding:6px; justify-content:center}
    h1{display:none}
    .hint{display:none}
    canvas{width:100%; border-radius:10px}
  }

/* na mobilu: klávesová nápověda pryč, canvas co největší */

-    body{gap:8px; padding:6px; justify-content:flex-start}

+    body{gap:8px; padding:6px; justify-content:center}


<total_tokens>14980973 tokens left</total_tokens>


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

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


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

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

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

+    isTouch ? 'ťukni na režim pro start  ·  ťukni na barvu postavy'

+            : '↑ ↓ režim  ·  ← → barva postavy  ·  Enter start  ·  1 / 2',


<total_tokens>14980395 tokens left</total_tokens>


Bash

grep -n "Enter – menu\|Mezerník = pokračovat\|Enter / číslo\|Enter – pokračovat\|statLine() + '   ·   Enter" hra.html

Find keyboard-specific hint strings


817:  else if (state === 'paused')     overlay('PAUZA', isTouch ? 'ťukni pro pokračování' : 'P nebo Enter – pokračovat   ·   Esc – menu');
820:  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + '   ·   Enter – menu');
1017:  sub += '   ·   Enter – menu';
1075:  ctx.fillText('↑ ↓ výběr  ·  Enter / číslo = koupit  ·  Mezerník = pokračovat', W/2, H - 34);


<total_tokens>14980019 tokens left</total_tokens>


Edit

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

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


else if (state === 'paused')     overlay('PAUZA', isTouch ? 'ťukni pro pokračování' : 'P nebo Enter – pokračovat   ·   Esc – menu');

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

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

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

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

if (isTouch && (state === 'play' || state === 'paused')) drawTouch();

if (isTouch && innerHeight > innerWidth * 1.1) drawRotateHint();


<total_tokens>14979533 tokens left</total_tokens>


sub += isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu';


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

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

+  sub += isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu';

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


<total_tokens>14979270 tokens left</total_tokens>


ctx.fillText('↑ ↓ výběr  ·  Enter / číslo = koupit  ·  Mezerník = pokračovat', W/2, H - 34);

  ctx.fillText(
    isTouch ? 'ťukni na položku = koupit  ·  ťukni na „Pokračovat“'
            : '↑ ↓ výběr  ·  Enter / číslo = koupit  ·  Mezerník = pokračovat',
    W/2, H - 34);


  ctx.fillText(
    isTouch ? 'ťukni na položku = koupit  ·  ťukni na „Pokračovat“'
            : '↑ ↓ výběr  ·  Enter / číslo = koupit  ·  Mezerník = pokračovat',
    W/2, H - 34);

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

-  ctx.fillText('↑ ↓ výběr  ·  Enter / číslo = koupit  ·  Mezerník = pokračovat', W/2, H - 34);

+    isTouch ? 'ťukni na položku = koupit  ·  ťukni na „Pokračovat“'

+            : '↑ ↓ výběr  ·  Enter / číslo = koupit  ·  Mezerník = pokračovat',


<total_tokens>14978897 tokens left</total_tokens>


  // herní dotyková tlačítka mají přednost během hry
  if (state === 'play' || state === 'paused'){
    const z = touchZone(p);
    if (z === 'pause'){ state = state === 'play' ? 'paused' : 'play'; return; }
    if (z && state === 'play'){
      activePtr.set(e.pointerId, z);
      if (z === 'jump') codesHit.add('Space');   // náběžná hrana skoku
      applyTouch();
      return;
    }
  }

  // herní dotyková tlačítka mají přednost během hry
  if (state === 'play' || state === 'paused'){
    const z = touchZone(p);
    if (z === 'pause'){ state = state === 'play' ? 'paused' : 'play'; return; }
    if (state === 'paused' &&
        p.x >= W/2 - 80 && p.x <= W/2 + 80 && p.y >= H/2 + 40 && p.y <= H/2 + 84){
      state = 'menu'; return;
    }
    if (z && state === 'play'){
      activePtr.set(e.pointerId, z);
      if (z === 'jump') codesHit.add('Space');   // náběžná hrana skoku
      applyTouch();
      return;
    }
  }


  // herní dotyková tlačítka mají přednost během hry
  if (state === 'play' || state === 'paused'){
    const z = touchZone(p);
    if (z === 'pause'){ state = state === 'play' ? 'paused' : 'play'; return; }
    if (z && state === 'play'){
      activePtr.set(e.pointerId, z);
      if (z === 'jump') codesHit.add('Space');   // náběžná hrana skoku
      applyTouch();
      return;
    }
  }

  // herní dotyková tlačítka mají přednost během hry
  if (state === 'play' || state === 'paused'){
    const z = touchZone(p);
    if (z === 'pause'){ state = state === 'play' ? 'paused' : 'play'; return; }
    if (state === 'paused' &&
        p.x >= W/2 - 80 && p.x <= W/2 + 80 && p.y >= H/2 + 40 && p.y <= H/2 + 84){
      state = 'menu'; return;
    }
    if (z && state === 'play'){
      activePtr.set(e.pointerId, z);
      if (z === 'jump') codesHit.add('Space');   // náběžná hrana skoku
      applyTouch();
      return;
    }
  }


<total_tokens>14977011 tokens left</total_tokens>


  padBtn(TB.left,  '◀', has('left'));
  padBtn(TB.right, '▶', has('right'));
  circBtn(TB.jump, 'SKOK', has('jump'), 20);
  circBtn(TB.pause, state === 'paused' ? '▶' : '❚❚', false, 18);
  ctx.restore();
}

  padBtn(TB.left,  '◀', has('left'));
  padBtn(TB.right, '▶', has('right'));
  circBtn(TB.jump, 'SKOK', has('jump'), 20);
  circBtn(TB.pause, state === 'paused' ? '▶' : '❚❚', false, 18);

  if (state === 'paused'){
    const mb = { x: W/2 - 80, y: H/2 + 40, w: 160, h: 44 };
    ctx.fillStyle = 'rgba(255,255,255,.10)';
    roundRect(mb.x, mb.y, mb.w, mb.h, 12); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,.25)'; ctx.lineWidth = 2;
    roundRect(mb.x, mb.y, mb.w, mb.h, 12); ctx.stroke();
    ctx.fillStyle = '#e8ecf5';
    ctx.font = '600 15px system-ui, sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('← Menu', W/2, mb.y + mb.h / 2);
    ctx.textBaseline = 'alphabetic';
  }
  ctx.restore();
}


  padBtn(TB.left,  '◀', has('left'));
  padBtn(TB.right, '▶', has('right'));
  circBtn(TB.jump, 'SKOK', has('jump'), 20);
  circBtn(TB.pause, state === 'paused' ? '▶' : '❚❚', false, 18);
  ctx.restore();
}

  padBtn(TB.left,  '◀', has('left'));
  padBtn(TB.right, '▶', has('right'));
  circBtn(TB.jump, 'SKOK', has('jump'), 20);
  circBtn(TB.pause, state === 'paused' ? '▶' : '❚❚', false, 18);

  if (state === 'paused'){
    const mb = { x: W/2 - 80, y: H/2 + 40, w: 160, h: 44 };
    ctx.fillStyle = 'rgba(255,255,255,.10)';
    roundRect(mb.x, mb.y, mb.w, mb.h, 12); ctx.fill();
    ctx.strokeStyle = 'rgba(255,255,255,.25)'; ctx.lineWidth = 2;
    roundRect(mb.x, mb.y, mb.w, mb.h, 12); ctx.stroke();
    ctx.fillStyle = '#e8ecf5';
    ctx.font = '600 15px system-ui, sans-serif';
    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
    ctx.fillText('← Menu', W/2, mb.y + mb.h / 2);
    ctx.textBaseline = 'alphabetic';
  }
  ctx.restore();
}

+    roundRect(mb.x, mb.y, mb.w, mb.h, 12); ctx.fill();

+    ctx.strokeStyle = 'rgba(255,255,255,.25)'; ctx.lineWidth = 2;

+    roundRect(mb.x, mb.y, mb.w, mb.h, 12); ctx.stroke();

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

+    ctx.textAlign = 'center'; ctx.textBaseline = 'middle';

+    ctx.fillText('← Menu', W/2, mb.y + mb.h / 2);


<total_tokens>14976234 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 t in ['drawTouch','activePtr','applyTouch','TB.','isTouch','circBtn','padBtn']:
    print(t, js.count(t))
EOF
curl -sS -o /dev/null -w "server HTTP %{http_code}\n" http://localhost:8000/hra.html

Balance check and serve check


( ) 932 932 OK
{ } 196 196 OK
[ ] 165 165 OK
drawTouch 2
activePtr 10
applyTouch 5
TB. 8
isTouch 9
circBtn 3
padBtn 3
server HTTP 200


<total_tokens>14975506 tokens left</total_tokens>


Edit

let isTouch = (matchMedia && matchMedia('(pointer: coarse)').matches) || ('ontouchstart' in window);

let isTouch = !!(window.matchMedia && matchMedia('(pointer: coarse)').matches);


/*  Dotykové ovládání (mobil)                                         */

-let isTouch = (matchMedia && matchMedia('(pointer: coarse)').matches) || ('ontouchstart' in window);

+let isTouch = !!(window.matchMedia && matchMedia('(pointer: coarse)').matches);

const activePtr = new Map();     // pointerId -> 'left' | 'right' | 'jump'

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


<total_tokens>14973777 tokens left</total_tokens>


addEventListener('keydown', e => {
  if (BLOCK.includes(e.code)) e.preventDefault();
  if (!e.repeat) codesHit.add(e.code);
  codesDown.add(e.code);
  unlockAudio();
});

addEventListener('keydown', e => {
  if (BLOCK.includes(e.code)) e.preventDefault();
  if (!e.repeat) codesHit.add(e.code);
  codesDown.add(e.code);
  isTouch = false;                 // používá klávesnici → skryj dotyková tlačítka
  unlockAudio();
});


addEventListener('keydown', e => {
  if (BLOCK.includes(e.code)) e.preventDefault();
  if (!e.repeat) codesHit.add(e.code);
  codesDown.add(e.code);
  unlockAudio();
});

addEventListener('keydown', e => {
  if (BLOCK.includes(e.code)) e.preventDefault();
  if (!e.repeat) codesHit.add(e.code);
  codesDown.add(e.code);
  isTouch = false;                 // používá klávesnici → skryj dotyková tlačítka
  unlockAudio();
});

+  isTouch = false;                 // používá klávesnici → skryj dotyková tlačítka

addEventListener('keyup', e => codesDown.delete(e.code));


<total_tokens>14973365 tokens left</total_tokens>


Bash

python3 - <<'EOF'
import re
js=re.search(r'<script>(.*)</script>',open('hra.html').read(),re.S).group(1)
ok=all(js.count(a)==js.count(b) for a,b in [('(',')'),('{','}'),('[',']')])
print('brackets', 'OK' if ok else 'MISMATCH')
# naive check: no obvious 'function function' or double commas
print('lines', js.count(chr(10)))
EOF
curl -sS -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8000/hra.html
ls -la hra.html

Final checks


brackets OK
lines 1240
HTTP 200
-rw-rw-r-- 1 tonda tonda 47448 Aug 27 14:19 hra.html


<total_tokens>14972713 tokens left</total_tokens>


Hotovo — na telefonu otevři (stejná WiFi jako server):

```
http://192.168.5.32:8000/hra.html
```

Tip: přes menu prohlížeče **„Přidat na plochu"** → spustí se skoro jako appka, bez lišty.

## Co přibylo pro mobil

- **Dotykové ovládání** se samo zapne na dotykovém zařízení:
  - vlevo dole **◀ ▶** (pohyb), vpravo dole velké **SKOK**
  - krátké ťuknutí = malý skok, podržení = plný skok
  - nahoře uprostřed **❚❚** pauza, na pauze pak **▶** a **← Menu**
  - funguje víc prstů najednou (běh + skok)
- **Menu / obchod / konec** se ovládají ťuknutím (texty se přepnou z klávesových na dotykové)
- Stránka nejde omylem posunout ani zoomovat, na mobilu se schová nadpis i klávesová nápověda a plátno se roztáhne na celou šířku
- V **na výšku** se ukáže „🔄 Otoč telefon na šířku" (hra je 5:3, na šířku se hraje líp)
- Na počítači se nic nemění; když na hybridním zařízení sáhneš na klávesnici, dotyková tlačítka zmizí

Ladění rozmístění tlačítek: objekt `TB` na začátku skriptu (souřadnice jsou v plátně 900×540).


Building a 2D platformer as a single HTML file (/home/tonda/hra.html), served at http://192.168.5.32:8000/hra.html. Added story/endless modes, death wall, between-level shop, saved high scores with unlockable skins, enemy sizes, and mobile touch controls. Next: you test the mobile version and report back.


<task-notification>
<task-id>bouguomya</task-id>
<tool-use-id>toolu_01PaaWcp1GY3cYtMSnvQZXKr</tool-use-id>
<output-file>/tmp/claude-1000/-home-tonda/f4c9c9f7-23b2-4e1a-860a-276c7bb05d77/tasks/bouguomya.output</output-file>
<status>killed</status>
<summary>Background command "Serve /home/tonda over HTTP on port 8000" was stopped</summary>
</task-notification>