chci pokračovat v upravování hry hranatý svět


claude claude-code-guide Explore general-purpose Plan statusline-setup

- claude: Catch-all for any task that doesn't fit a more specific agent. FleetView's default when no agent name is typed. (Tools: *)

- claude-code-guide: Use this agent when the user asks questions ("Can Claude...", "Does Claude...", "How do I...") about: (1) Claude Code (the CLI tool) - features, hooks, slash commands, MCP servers, settings, IDE integrations, keyboard shortcuts; (2) Claude Agent SDK - building custom agents; (3) Claude API (formerly Anthropic API) - Messages API for directly passing messages to Claude, Tool Runner (`client.beta.messages.tool_runner`) for running an agentic loop over your own tools, manual tool-use loops, Managed Agents for server-hosted agents with a managed sandbox, prompt caching, and general Anthropic SDK usage; (4) Claude Tag (Claude in Slack) - what it is, setting it up for a Slack workspace, `/install-slack-app`; (5) `claude plugin eval` (writing and running plugin eval suites, its JSON/report, sandbox, CI, early-access enablement) and the `/skill-doctor` report. **IMPORTANT:** Before spawning a new agent, check if there is already a running or recently completed claude-code-guide agent that you can continue via SendMessage. (Tools: Bash, Read, WebFetch, WebSearch)

- Explore: Read-only search agent for broad fan-out searches — when answering means sweeping many files, directories, or naming conventions and you only need the conclusion, not the file dumps. It reads excerpts rather than whole files, so it locates code; it doesn't review or audit it. Specify search breadth: "medium" for moderate exploration, "very thorough" for multiple locations and naming conventions. (Tools: All tools except Agent, Artifact, ExitPlanMode, Edit, Write, NotebookEdit)

- general-purpose: General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. (Tools: *)

- Plan: Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs. (Tools: All tools except Agent, Artifact, ExitPlanMode, Edit, Write, NotebookEdit)

- statusline-setup: Use this agent to configure the user's Claude Code status line setting. (Tools: Read, Edit)


- design: Create a design canvas - a multi-artboard visual design published as an Artifact that runs Claude Design's canvas editor (an early preview of Claude Design inside Claude Code). You DRAFT the design as .dc.html artboards laid out on one pan/zoom canvas; where saving is enabled for the user's account they refine every element visually (click-to-select, a properties panel, inline text editing, undo/redo) and Save publishes a new version for everyone, otherwise they get a view-and-export (PNG/PDF) preview of your draft. Good for UI mockups and screen flows, landing pages, marketing and social graphics, and print pieces - posters, flyers, brochures as single-page artboards; memos and reports as one flowing artboard. Use when someone wants a design, mockup, wireframe, UI or screen design, landing page, poster, flyer, brochure, banner, card, one-pager, or any visual layout they would rather tweak by hand than in code. Only for CREATING or re-seeding a canvas; an existing one is edited in its published Artifact.
- dataviz: Use this skill whenever you are about to create ANY chart, graph, plot, dashboard, or data visualization, in ANY output medium — an HTML or React artifact, inline SVG, plotting code in any library (matplotlib, plotly, d3, Recharts, …), an image/PNG you will render and upload, or a chart shared into Slack. Read it BEFORE writing the first line of chart code, choosing chart colors, building a stat tile / meter / KPI row, or laying out a dashboard. Produces visualizations that read as one system — elegant, accessible, consistent in light and dark — using a brand-neutral placeholder palette you swap for your own. Teaches a design-system-agnostic method: a form heuristic, a color formula with a runnable validator, mark specs, and interaction rules. A validated default palette is documented in `references/palette.md` — swap that file's values for your brand's. Triggers on: "chart", "graph", "plot", "data viz", "visualization", "dashboard", "analytics", "visualize data", "categorical colors", "sequential / diverging palette", "stat tile", "sparkline", "heatmap", "legend", "axis", "tooltip", "chart colors", "color by series".
- artifact-design: Design guidance and fundamentals for Artifacts. - Load before writing any artifact, including a skill-instructed Markdown one - Markdown is never a shortcut past the design pass.
- artifact-diagramming: Diagramming know-how for Artifacts - when a picture earns its place, how to draw one that shows the real mechanism, and the inline-SVG mechanics that keep it legible in both themes.
- artifact-capabilities: Runtime capabilities a published Artifact page can be granted — behavior static HTML cannot provide on its own, such as the page reading live or connected data, remembering what people do on it (a poll, a sign-up sheet, a checklist, a document edited in place — it saves new versions of itself), keeping state shared across viewers, knowing who is viewing, asking Claude a question of its own, storing files people add, or handing the viewer a file to save. Serves this user's live capability roster and the typed call definitions. Load it whenever the user asks for an artifact needing any such runtime behavior.
- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, suggest the /config command.
- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: "rebind ctrl+s", "add a chord shortcut", "change the submit key", "customize keybindings".
- code-review: Review the current diff, or a PR number/branch/path target, for correctness bugs and reuse/simplification/efficiency cleanups at the given effort level (low/medium: fewer, high-confidence findings; high→max: broader coverage, may include uncertain findings; ultra: deep multi-agent review in the cloud); with no level given, it reuses the level you typed last. Pass --comment to post findings as inline PR comments, or --fix to apply the findings to the working tree after the review. For ultra on a GitHub.com PR target, --post asks to post the finished review’s findings to the PR as a single comment from the user’s GitHub account (not a review; the launch dialog still confirms in interactive sessions, while non-interactive mode posts on the flag alone) and --no-post hides that option.
- simplify: Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that.
- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.
- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.
- schedule: Create, update, list, or run scheduled cloud agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring cloud agent, set up automated tasks, create a cron job for Claude Code, or manage their scheduled agents/routines. Also use when the user wants a one-time scheduled run ("run this once at 3pm", "remind me to check X tomorrow").
- claude-api: Reference for the Claude API / Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.
TRIGGER — read BEFORE opening the target file; don't skip because it "looks like a one-liner" — whenever: the prompt names Claude/Anthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing/model choice/limits/caching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent/MCP/tool-definition/multi-agent/RAG/LLM-judge/computer-use; generate/summarize/extract/classify/rewrite/converse over NL; debugging refusals/cutoffs/streaming/tool-calls/tokens).
SKIP only when another provider is being worked on (overrides all triggers): OpenAI/GPT/Gemini/Llama/Mistral/Cohere/Ollama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).
- claude-in-chrome: Automates your Chrome browser to interact with web pages - clicking elements, filling forms, capturing screenshots, reading console logs, and navigating sites. Opens pages in new tabs within your existing Chrome session. Requires site-level permissions before executing (configured in the extension). - When the user wants to interact with web pages, automate browser tasks, capture screenshots, read console logs, or perform any browser-based actions. Always invoke BEFORE attempting to use any mcp__claude-in-chrome__* tools.
- run: Launch and drive this project's app to see a change working. Use when asked to run, start, or screenshot the app, or to confirm a change works in the real app (not just tests). First looks for a project skill that already covers launching the app; otherwise falls back to built-in patterns per project type (CLI, server, TUI, Electron, browser-driven, library).
- init: Initialize a new CLAUDE.md file with codebase documentation
- security-review: Complete a security review of the pending changes on the current branch

design dataviz artifact-design artifact-diagramming artifact-capabilities update-config keybindings-help code-review simplify fewer-permission-prompts loop schedule claude-api claude-in-chrome run init security-review


<total_tokens>15000000 tokens left</total_tokens>


Bash

ls -la /home/tonda && echo "---" && find /home/tonda -maxdepth 3 -iname "*hranat*" 2>/dev/null

List home dir and search for game project


total 23220
drwx------  7 tonda tonda     4096 Aug 27 18:27 .
drwxr-xr-x  3 root  root      4096 Aug 26 07:28 ..
-rw-------  1 tonda tonda      503 Aug 27 17:40 .bash_history
-rw-r--r--  1 tonda tonda      220 Aug 26 07:28 .bash_logout
-rw-r--r--  1 tonda tonda     3563 Aug 26 07:36 .bashrc
drwxrwxr-x  4 tonda tonda     4096 Aug 26 07:40 .cache
drwxrwxr-x 11 tonda tonda     4096 Aug 27 18:29 .claude
-rw-------  1 tonda tonda    47165 Aug 27 18:27 .claude.json
drwx------  3 tonda tonda     4096 Aug 26 07:40 .config
drwxrwxr-x  5 tonda tonda     4096 Aug 26 07:39 .local
-rw-r--r--  1 tonda tonda      807 Aug 26 07:28 .profile
-rw-rw-r--  1 tonda tonda 23592960 Aug 27 10:59 clear.tar.gz
-rwxrwxr-x  1 tonda tonda      466 Aug 27 16:21 hra-server.sh
-rw-rw-r--  1 tonda tonda    54085 Aug 27 16:52 hra.html
-rw-rw-r--  1 tonda tonda    25832 Aug 26 15:06 neon-drop.html
drwxrwxr-x  2 tonda tonda     4096 Aug 26 08:51 tmp
---


Read memory index


Exit code 1


<total_tokens>14971150 tokens left</total_tokens>


cat /home/tonda/hra-server.sh && echo "=== hra.html head ===" && head -c 2000 /home/tonda/hra.html

Inspect server script and game HTML head


#!/bin/bash
# Spustí webový server s hrou, pokud ještě neběží.
# Hru pak otevřeš v prohlížeči na  http://192.168.5.32:8000/hra.html
cd /home/tonda || exit 1
if curl -sf -o /dev/null http://localhost:8000/hra.html; then
  echo "Server už běží: http://192.168.5.32:8000/hra.html"
  exit 0
fi
nohup python3 -m http.server 8000 --bind 0.0.0.0 > /home/tonda/.hra-server.log 2>&1 &
disown
sleep 1
echo "Server spuštěn: http://192.168.5.32:8000/hra.html"
=== hra.html head ===
<!doctype html>
<html lang="cs">
<head>
<meta charset="utf-8">
<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%; 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){
    html,body{overflow:hidden}
    body{gap:8px; padding:6px; justify-content:center}
    h1{display:none}
    .hint{display:none}
    canvas{width:100%; border-radius:10px}
  }
  /* celá obrazovka: vejít se do výšky i šířky, žádné okraje */
  :fullscreen, :-webkit-full-screen{ background:#101320; }
  :fullscreen body, :-webkit-full-screen body{ padding:0; gap:0; }
  :fullscreen canvas, :-webkit


<total_tokens>14970309 tokens left</total_tokens>


grep -n "function \|// ===\|/\* ===\|const LEVEL\|SECTION\|=== " /home/tonda/hra.html | head -100

Outline functions and sections in hra.html


77:function roundRect(x,y,w,h,r){
126:function joyStart(id, p){
133:function joyMove(p){
141:function joyEnd(){
148:function applyTouch(){
150:  for (const z of activePtr.values()) if (z === 'jump') j = true;
162:function unlockAudio(){
165:    if (actx.state === 'suspended') actx.resume();
168:function beep(freq, dur = 0.08, type = 'square', vol = 0.05){
216:function mkEnemy(x, y, tier = 0){
218:  return { x, y, w: td.w, h: td.h, tier, hp: tier === 2 ? 2 : 1,
224:function rollTier(prog, bias = 0){
235:function spawnCoinPops(x, y, n){
249:const LEVELS = [
284:function buildLevel(d, idx = 0){
303:    if (tier === 2 && g < fr) tier = 1;            // velká jen na hlavní podlaze
340:function loadEndless(){
356:function generateAhead(cx){
362:function genChunk(){
390:    if (n === 2 && len < 7) n = 1;
426:function pruneWorld(){
443:function updatePlayer(){
481:function resolve(axis){
490:      if (axis === 'x'){
506:function updateEnemy(e){
529:function handleInteractions(){
574:  if (world.goal && mode === 'levels' &&
587:function hurtPlayer(){
594:    if (mode === 'endless') saveBest();
601:function respawn(){
603:  if (mode === 'levels'){
625:function updateCamera(){
626:  if (mode === 'endless'){
653:    else if (wgap < 150 && pruneT % 15 === 0) beep(150, 0.05, 'square', 0.03);
692:function buyShop(i){
701:function shopContinue(){
724:function store(k, v){ try { localStorage.setItem('hranaty_' + k, String(v)); } catch (e) {} }
725:function loadStr(k, d){ try { const v = localStorage.getItem('hranaty_' + k); return v == null ? d : v; } catch (e) { return d; } }
741:const escore = () => mode === 'endless' ? score + distance : score;
743:function saveBest(){
749:    justUnlocked = SKINS.filter(k => k.diff === d && k.dist > prev && k.dist <= distance).map(k => k.name);
753:function startGame(m){
756:  lives = m === 'endless' ? 1 : 3;   // nekonečný běh: jeden život
761:  if (m === 'levels'){ levelIndex = 0; loadLevel(0); }
766:function loadLevel(i){
776:function update(){
777:  if (state === 'menu'){
779:    if (menuSel === 1){                       // řádek Nekonečný běh → šipky mění obtížnost
792:    if (hit('Enter', 'Space')) return startGame(menuSel === 0 ? 'levels' : 'endless');
795:  if (state === 'over' || state === 'win'){
799:  if (state === 'paused'){
804:  if (state === 'shop'){
814:  // --- state === 'play' ---
817:  if (hit('KeyR'))   { mode === 'levels' ? loadLevel(levelIndex) : loadEndless(); return; }
824:  if (state === 'play') updateCamera();
838:function draw(){
870:    if (mode === 'endless' && endlessStarted && wallX > camX - 100) drawWall();
875:  if (mode === 'endless' && state === 'play' && endlessStarted){
890:  if (state === 'menu')            drawMenu();
891:  else if (state === 'paused')     overlay('PAUZA', isTouch ? 'ťukni pro pokračování' : 'P nebo Enter – pokračovat   ·   Esc – menu');
892:  else if (state === 'shop')       drawShop();
893:  else if (state === 'over')       drawOver();
894:  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + (isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu'));
896:  if (isTouch && (state === 'play' || state === 'paused')) drawTouch();
900:function drawJoy(){
923:function circBtn(b, label, active, fs){
934:function drawTouch(){
936:  const has = z => { for (const v of activePtr.values()) if (v === z) return true; return false; };
939:  circBtn(TB.pause, state === 'paused' ? '▶' : '❚❚', false, 18);
941:  if (state === 'paused'){
955:function drawRotateHint(){
966:function drawWall(){
985:function drawParallax(){
1001:function drawTile(c, r){
1014:function drawPlayer(){
1023:function drawEnemy(e){
1045:function drawCoin(co){
1053:function drawGoal(g){
1065:function drawHUD(){
1067:  const endless = mode === 'endless';
1080:  if (mode === 'levels'){
1086:  if (mode === 'endless'){
1094:function statLine(){
1095:  return mode === 'levels'
1100:function overlay(title, sub){
1114:function drawOver(){
1116:  if (mode === 'endless' && justUnlocked.length)
1119:  overlay(mode === 'endless' && newRecord ? 'NOVÝ REKORD! 🎉' : 'KONEC HRY', sub);
1122:function drawShop(){
1140:    const sel = i === shopSel;
1188:function diffChipRect(i){
1192:function skinChipRect(i){
1197:function drawMenu(){
1213:  menuOption('1', 'Příběhové levely', LEVELS.length + ' ručně navržených levelů s cílem a životy', MENU.opt1Y, menuSel === 0);
1214:  menuOption('2', 'Nekonečný běh', 'Obtížnost: ' + ENDLESS_DIFF[endlessDiff].name + '   ·   jeden život, hraj na vzdálenost', MENU.opt2Y, menuSel === 1);
1224:            : '↑↓ režim   ·   ←→ ' + (menuSel === 1 ? 'obtížnost' : 'barva') + '   ·   Enter = start',
1229:function drawDiffPicker(){
1236:    const b = diffChipRect(i), sel = i === diffSel;
1249:function drawSkinPicker(){
1261:    if (i === equippedSkin){


<total_tokens>14968893 tokens left</total_tokens>


Mám hru načtenou — `/home/tonda/hra.html` (2D plošinovka „Hranatý svět", jeden soubor ~54 kB: příběhové levely + nekonečný běh, dotykové ovládání, obchod se skiny, zvuky). Server se spouští přes `hra-server.sh` na `http://192.168.5.32:8000/hra.html`.

Nemám uloženo, na čem jsme minule skončili. Co chceš upravit nebo přidat?


přidej na mobilní verzi při zapnutí hry tlačítko na zvětšení na celou obrazovku


grep -n -i "fullscreen\|requestFullscreen\|celou obrazovku\|celá obrazovka\|isTouch\|rotate\|drawRotateHint\|orientation" /home/tonda/hra.html

Find fullscreen and touch handling


42:  /* celá obrazovka: vejít se do výšky i šířky, žádné okraje */
43:  :fullscreen, :-webkit-full-screen{ background:#101320; }
44:  :fullscreen body, :-webkit-full-screen body{ padding:0; gap:0; }
45:  :fullscreen canvas, :-webkit-full-screen canvas{
99:  isTouch = false;                 // používá klávesnici → skryj dotyková tlačítka
110:let isTouch = !!(window.matchMedia && matchMedia('(pointer: coarse)').matches);
891:  else if (state === 'paused')     overlay('PAUZA', isTouch ? 'ťukni pro pokračování' : 'P nebo Enter – pokračovat   ·   Esc – menu');
894:  else if (state === 'win')        overlay('DOKONČENO! 🎉', statLine() + (isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu'));
896:  if (isTouch && (state === 'play' || state === 'paused')) drawTouch();
897:  if (isTouch && innerHeight > innerWidth * 1.1) drawRotateHint();
955:function drawRotateHint(){
994:    ctx.rotate(0.5);
1118:  sub += isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu';
1177:    isTouch ? 'ťukni na položku = koupit  ·  ťukni na „Pokračovat“'
1223:    isTouch ? 'ťukni na režim = start   ·   ťukni na obtížnost a barvu'
1321:  if (e.pointerType === 'touch') isTouch = true;


<total_tokens>14999724 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, maximum-scale=1, user-scalable=no, viewport-fit=cover">
6	<meta name="mobile-web-app-capable" content="yes">
7	<meta name="apple-mobile-web-app-capable" content="yes">
8	<title>Hranatý svět — 2D plošinovka</title>
9
49
50
51
Hranatý svět

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

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

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

57
58

<!doctype html>

<meta charset="utf-8">
<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>

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.


[Truncated: PARTIAL view — /home/tonda/hra.html: showing lines 1-1088 of 1424 total (27806 tokens, cap 25000). Call Read with offset=1089 limit=1088 for the next page, or Grep to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]


<total_tokens>14998939 tokens left</total_tokens>


1089	    ctx.fillText('rekord ' + bestHS, W - 16, 66);
1090	  }
1091	  ctx.restore();
1092	}
1093
1094	function statLine(){
1095	  return mode === 'levels'
1096	    ? 'Skóre ' + score + '  ·  mincí ' + coinsCollected
1097	    : 'Skóre ' + escore() + '  ·  ' + distance + ' m  ·  ' + coinsCollected + ' ●';
1098	}
1099
1100	function overlay(title, sub){
1101	  ctx.save();
1102	  ctx.fillStyle = 'rgba(8,10,16,.78)';
1103	  ctx.fillRect(0, 0, W, H);
1104	  ctx.textAlign = 'center';
1105	  ctx.fillStyle = '#e8ecf5';
1106	  ctx.font = '700 44px system-ui, sans-serif';
1107	  ctx.fillText(title, W/2, H/2 - 6);
1108	  ctx.fillStyle = '#8b93a7';
1109	  ctx.font = '500 16px system-ui, sans-serif';
1110	  ctx.fillText(sub, W/2, H/2 + 30);
1111	  ctx.restore();
1112	}
1113
1114	function drawOver(){
1115	  let sub = statLine();
1116	  if (mode === 'endless' && justUnlocked.length)
1117	    sub += '   ·   odemčeno: ' + justUnlocked.join(', ');
1118	  sub += isTouch ? '   ·   ťukni pro menu' : '   ·   Enter – menu';
1119	  overlay(mode === 'endless' && newRecord ? 'NOVÝ REKORD! 🎉' : 'KONEC HRY', sub);
1120	}
1121
1122	function drawShop(){
1123	  ctx.save();
1124	  ctx.fillStyle = 'rgba(8,10,16,.88)';
1125	  ctx.fillRect(0, 0, W, H);
1126	  ctx.textAlign = 'center';
1127
1128	  ctx.fillStyle = '#e8ecf5';
1129	  ctx.font = '800 32px system-ui, sans-serif';
1130	  ctx.fillText('LEVEL ' + levelIndex + ' HOTOV', W/2, 84);
1131	  ctx.fillStyle = COL.coin;
1132	  ctx.font = '600 18px system-ui, sans-serif';
1133	  ctx.fillText('● ' + coinsCollected + ' mincí k utracení', W/2, 116);
1134	  ctx.fillStyle = '#8b93a7';
1135	  ctx.font = '500 14px system-ui, sans-serif';
1136	  ctx.fillText('OBCHOD', W/2, 154);
1137
1138	  SHOP.forEach((it, i) => {
1139	    const y = SHOP_Y0 + i * (SHOP_ROW_H + SHOP_GAP);
1140	    const sel = i === shopSel;
1141	    const price = it.price();
1142	    const ok = it.canBuy();
1143
1144	    ctx.fillStyle = sel ? 'rgba(92,200,255,.16)' : 'rgba(255,255,255,.04)';
1145	    roundRect(SHOP_X, y, SHOP_W, SHOP_ROW_H, 12); ctx.fill();
1146	    ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.12)';
1147	    ctx.lineWidth = 2;
1148	    roundRect(SHOP_X, y, SHOP_W, SHOP_ROW_H, 12); ctx.stroke();
1149
1150	    ctx.textAlign = 'left';
1151	    ctx.fillStyle = '#e8ecf5';
1152	    ctx.font = '700 17px system-ui, sans-serif';
1153	    ctx.fillText((i + 1) + '.   ' + it.name, SHOP_X + 18, y + 26);
1154	    ctx.fillStyle = '#8b93a7';
1155	    ctx.font = '400 13px system-ui, sans-serif';
1156	    ctx.fillText(it.desc + '     ·     koupeno ' + it.bought + '×', SHOP_X + 18, y + 46);
1157
1158	    ctx.textAlign = 'right';
1159	    ctx.fillStyle = ok ? COL.coin : '#ff6b7d';
1160	    ctx.font = '700 18px system-ui, sans-serif';
1161	    ctx.fillText('● ' + price, SHOP_X + SHOP_W - 18, y + 38);
1162	  });
1163
1164	  const by = shopBtnY();
1165	  ctx.fillStyle = 'rgba(74,222,128,.14)';
1166	  roundRect(W/2 - 120, by, 240, 44, 12); ctx.fill();
1167	  ctx.strokeStyle = COL.flag; ctx.lineWidth = 2;
1168	  roundRect(W/2 - 120, by, 240, 44, 12); ctx.stroke();
1169	  ctx.textAlign = 'center';
1170	  ctx.fillStyle = '#e8ecf5';
1171	  ctx.font = '700 16px system-ui, sans-serif';
1172	  ctx.fillText('Pokračovat  →', W/2, by + 28);
1173
1174	  ctx.fillStyle = '#8b93a7';
1175	  ctx.font = '500 14px system-ui, sans-serif';
1176	  ctx.fillText(
1177	    isTouch ? 'ťukni na položku = koupit  ·  ťukni na „Pokračovat“'
1178	            : '↑ ↓ výběr  ·  Enter / číslo = koupit  ·  Mezerník = pokračovat',
1179	    W/2, H - 34);
1180	  ctx.restore();
1181	}
1182
1183	const MENU = {
1184	  opt1Y: 226, opt2Y: 280, optW: 470, optH: 48,
1185	  diffY: 330, diffBW: 122, diffH: 30, diffGap: 16,
1186	  skinY: 400, skinSW: 30, skinGap: 8, skinNameY: 450,
1187	};
1188	function diffChipRect(i){
1189	  const total = 3 * MENU.diffBW + 2 * MENU.diffGap;
1190	  return { x: W/2 - total/2 + i * (MENU.diffBW + MENU.diffGap), y: MENU.diffY, w: MENU.diffBW, h: MENU.diffH };
1191	}
1192	function skinChipRect(i){
1193	  const n = SKINS.length, total = n * MENU.skinSW + (n - 1) * MENU.skinGap;
1194	  return { x: W/2 - total/2 + i * (MENU.skinSW + MENU.skinGap), y: MENU.skinY, w: MENU.skinSW, h: MENU.skinSW };
1195	}
1196
1197	function drawMenu(){
1198	  ctx.save();
1199	  ctx.fillStyle = 'rgba(8,10,16,.9)';
1200	  ctx.fillRect(0, 0, W, H);
1201	  ctx.textAlign = 'center';
1202
1203	  ctx.fillStyle = '#e8ecf5';
1204	  ctx.font = '800 40px system-ui, sans-serif';
1205	  ctx.fillText('HRANATÝ SVĚT', W/2, 128);
1206	  ctx.fillStyle = '#8b93a7';
1207	  ctx.font = '500 14px system-ui, sans-serif';
1208	  ctx.fillText('2D plošinovka — vyber si režim', W/2, 152);
1209	  ctx.fillStyle = COL.coin;
1210	  ctx.font = '600 12px system-ui, sans-serif';
1211	  ctx.fillText('Nejdál   ·   Lehká ' + bestDist.easy + ' m    ·    Střední ' + bestDist.medium + ' m    ·    Těžká ' + bestDist.hard + ' m', W/2, 176);
1212
1213	  menuOption('1', 'Příběhové levely', LEVELS.length + ' ručně navržených levelů s cílem a životy', MENU.opt1Y, menuSel === 0);
1214	  menuOption('2', 'Nekonečný běh', 'Obtížnost: ' + ENDLESS_DIFF[endlessDiff].name + '   ·   jeden život, hraj na vzdálenost', MENU.opt2Y, menuSel === 1);
1215
1216	  drawDiffPicker();
1217	  drawSkinPicker();
1218
1219	  ctx.textAlign = 'center';
1220	  ctx.fillStyle = '#8b93a7';
1221	  ctx.font = '500 12px system-ui, sans-serif';
1222	  ctx.fillText(
1223	    isTouch ? 'ťukni na režim = start   ·   ťukni na obtížnost a barvu'
1224	            : '↑↓ režim   ·   ←→ ' + (menuSel === 1 ? 'obtížnost' : 'barva') + '   ·   Enter = start',
1225	    W/2, H - 16);
1226	  ctx.restore();
1227	}
1228
1229	function drawDiffPicker(){
1230	  ctx.textAlign = 'center';
1231	  ctx.fillStyle = '#8b93a7';
1232	  ctx.font = '600 11px system-ui, sans-serif';
1233	  ctx.fillText('OBTÍŽNOST NEKONEČNÉHO BĚHU', W/2, MENU.diffY - 10);
1234
1235	  DIFF_KEYS.forEach((k, i) => {
1236	    const b = diffChipRect(i), sel = i === diffSel;
1237	    ctx.fillStyle = sel ? 'rgba(92,200,255,.18)' : 'rgba(255,255,255,.05)';
1238	    roundRect(b.x, b.y, b.w, b.h, 8); ctx.fill();
1239	    ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.14)';
1240	    ctx.lineWidth = 2; roundRect(b.x, b.y, b.w, b.h, 8); ctx.stroke();
1241	    ctx.fillStyle = sel ? '#e8ecf5' : '#aab2c2';
1242	    ctx.font = '700 14px system-ui, sans-serif';
1243	    ctx.textBaseline = 'middle';
1244	    ctx.fillText(ENDLESS_DIFF[k].name, b.x + b.w/2, b.y + b.h/2 + 1);
1245	    ctx.textBaseline = 'alphabetic';
1246	  });
1247	}
1248
1249	function drawSkinPicker(){
1250	  ctx.textAlign = 'center';
1251	  ctx.fillStyle = '#8b93a7';
1252	  ctx.font = '600 11px system-ui, sans-serif';
1253	  ctx.fillText('BARVA POSTAVY', W/2, MENU.skinY - 10);
1254
1255	  for (let i = 0; i < SKINS.length; i++){
1256	    const b = skinChipRect(i), s = SKINS[i], open = skinUnlocked(i);
1257	    ctx.globalAlpha = open ? 1 : 0.22;
1258	    ctx.fillStyle = s.color;
1259	    roundRect(b.x, b.y, b.w, b.h, 8); ctx.fill();
1260	    ctx.globalAlpha = 1;
1261	    if (i === equippedSkin){
1262	      ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 3;
1263	      roundRect(b.x - 2, b.y - 2, b.w + 4, b.h + 4, 9); ctx.stroke();
1264	    }
1265	    if (i === skinSel && menuSel === 0){
1266	      ctx.strokeStyle = COL.player; ctx.lineWidth = 2;
1267	      roundRect(b.x - 6, b.y - 6, b.w + 12, b.h + 12, 12); ctx.stroke();
1268	    }
1269	    if (!open){
1270	      ctx.fillStyle = '#e8ecf5';
1271	      ctx.font = '12px system-ui, sans-serif';
1272	      ctx.textBaseline = 'middle';
1273	      ctx.fillText('🔒', b.x + b.w/2, b.y + b.h/2 + 1);
1274	      ctx.textBaseline = 'alphabetic';
1275	    }
1276	  }
1277
1278	  const s = SKINS[skinSel];
1279	  ctx.fillStyle = '#e8ecf5';
1280	  ctx.font = '600 13px system-ui, sans-serif';
1281	  ctx.fillText(
1282	    skinUnlocked(skinSel)
1283	      ? s.name + (skinSel === equippedSkin ? '  ✓ nasazeno' : '')
1284	      : s.name + '  🔒  ·  ' + ENDLESS_DIFF[s.diff].name + ' ' + s.dist + ' m',
1285	    W/2, MENU.skinNameY);
1286	}
1287
1288	function menuOption(num, title, desc, y, sel){
1289	  const w = MENU.optW, x = W/2 - w/2, h = MENU.optH;
1290	  ctx.fillStyle = sel ? 'rgba(92,200,255,.16)' : 'rgba(255,255,255,.04)';
1291	  roundRect(x, y - h/2, w, h, 12); ctx.fill();
1292	  ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.12)';
1293	  ctx.lineWidth = 2;
1294	  roundRect(x, y - h/2, w, h, 12); ctx.stroke();
1295
1296	  ctx.textAlign = 'left';
1297	  ctx.fillStyle = '#e8ecf5';
1298	  ctx.font = '700 17px system-ui, sans-serif';
1299	  ctx.fillText(num + '.   ' + title, x + 20, y - 4);
1300	  ctx.fillStyle = '#8b93a7';
1301	  ctx.font = '400 12px system-ui, sans-serif';
1302	  ctx.fillText(desc, x + 20, y + 15);
1303	  ctx.textAlign = 'center';
1304	}
1305
1306	/* ------------------------------------------------------------------ */
1307	/*  Myš / dotyk pro menu a obrazovky                                  */
1308	/* ------------------------------------------------------------------ */
1309	function canvasPos(e){
1310	  const r = canvas.getBoundingClientRect();
1311	  // počítá i s object-fit: contain (režim celé obrazovky) – letterbox okraje
1312	  const scale = Math.min(r.width / W, r.height / H) || (r.width / W);
1313	  const ox = r.left + (r.width  - W * scale) / 2;
1314	  const oy = r.top  + (r.height - H * scale) / 2;
1315	  return { x: (e.clientX - ox) / scale, y: (e.clientY - oy) / scale };
1316	}
1317	canvas.addEventListener('contextmenu', e => e.preventDefault());
1318
1319	canvas.addEventListener('pointerdown', e => {
1320	  unlockAudio();
1321	  if (e.pointerType === 'touch') isTouch = true;
1322	  const p = canvasPos(e);
1323
1324	  // herní dotykové ovládání má přednost během hry
1325	  if (state === 'play' || state === 'paused'){
1326	    if (inCirc(p, TB.pause)){ state = state === 'play' ? 'paused' : 'play'; return; }
1327	    if (state === 'paused' &&
1328	        p.x >= W/2 - 80 && p.x <= W/2 + 80 && p.y >= H/2 + 40 && p.y <= H/2 + 84){
1329	      state = 'menu'; return;
1330	    }
1331	    if (state === 'play'){
1332	      if (inCirc(p, TB.jump)){
1333	        activePtr.set(e.pointerId, 'jump');
1334	        codesHit.add('Space');                   // náběžná hrana skoku
1335	        applyTouch();
1336	        return;
1337	      }
1338	      if (joy.id === null && p.x < W * 0.6){      // levá část obrazovky = joystick
1339	        joyStart(e.pointerId, p);
1340	        return;
1341	      }
1342	    }
1343	  }
1344
1345	  if (state === 'menu'){
1346	    // volba režimu (ťuknutí = start)
1347	    if (Math.abs(p.x - W/2) <= MENU.optW/2){
1348	      if (Math.abs(p.y - MENU.opt1Y) <= MENU.optH/2 + 4){ menuSel = 0; return startGame('levels'); }
1349	      if (Math.abs(p.y - MENU.opt2Y) <= MENU.optH/2 + 4){ menuSel = 1; return startGame('endless'); }
1350	    }
1351	    // volba obtížnosti
1352	    for (let i = 0; i < 3; i++){
1353	      const b = diffChipRect(i);
1354	      if (p.x >= b.x - 4 && p.x <= b.x + b.w + 4 && p.y >= b.y - 6 && p.y <= b.y + b.h + 6){
1355	        diffSel = i; endlessDiff = DIFF_KEYS[i]; menuSel = 1; S.coin();
1356	        return;
1357	      }
1358	    }
1359	    // volba barvy postavy
1360	    for (let i = 0; i < SKINS.length; i++){
1361	      const b = skinChipRect(i);
1362	      if (p.x >= b.x - 3 && p.x <= b.x + b.w + 3 && p.y >= b.y - 3 && p.y <= b.y + b.h + 3){
1363	        skinSel = i; menuSel = 0;
1364	        if (skinUnlocked(i) && i !== equippedSkin){ equippedSkin = i; store('skin', i); S.coin(); }
1365	        return;
1366	      }
1367	    }
1368	  } else if (state === 'shop'){
1369	    for (let i = 0; i < SHOP.length; i++){
1370	      const y = SHOP_Y0 + i * (SHOP_ROW_H + SHOP_GAP);
1371	      if (p.x >= SHOP_X && p.x <= SHOP_X + SHOP_W && p.y >= y && p.y <= y + SHOP_ROW_H)
1372	        return buyShop(i);
1373	    }
1374	    const by = shopBtnY();
1375	    if (p.x >= W/2 - 120 && p.x <= W/2 + 120 && p.y >= by && p.y <= by + 44)
1376	      return shopContinue();
1377	  } else if (state === 'over' || state === 'win'){
1378	    state = 'menu';
1379	  } else if (state === 'paused'){
1380	    state = 'play';
1381	  }
1382	});
1383
1384	canvas.addEventListener('pointermove', e => {
1385	  if (e.pointerId === joy.id) joyMove(canvasPos(e));
1386	});
1387
1388	function endPtr(e){
1389	  let changed = false;
1390	  if (e.pointerId === joy.id){ joyEnd(); changed = false; }   // joyEnd už volá applyTouch
1391	  if (activePtr.delete(e.pointerId)) changed = true;
1392	  if (changed) applyTouch();
1393	}
1394	canvas.addEventListener('pointerup', endPtr);
1395	canvas.addEventListener('pointercancel', endPtr);
1396	addEventListener('blur', () => { activePtr.clear(); joyEnd(); });
1397
1398	/* ------------------------------------------------------------------ */
1399	/*  Hlavní smyčka – pevný krok 60 Hz                                  */
1400	/* ------------------------------------------------------------------ */
1401	let last = performance.now(), acc = 0;
1402	const STEP = 1000 / 60;
1403
1404	function frame(now){
1405	  acc += Math.min(now - last, 100);
1406	  last = now;
1407	  let steps = 0;
1408	  while (acc >= STEP && steps < 5){
1409	    update();
1410	    codesHit.clear();
1411	    acc -= STEP;
1412	    steps++;
1413	  }
1414	  if (steps === 5) acc = 0;
1415	  draw();
1416	  requestAnimationFrame(frame);
1417	}
1418	requestAnimationFrame(frame);
1419
1420	})();
1421	</script>
1422	</body>
1423	</html>
1424

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

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

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

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

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

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

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

    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();
  ctx.strokeStyle = COL.flag; ctx.lineWidth = 2;
  roundRect(W/2 - 120, by, 240, 44, 12); ctx.stroke();
  ctx.textAlign = 'center';
  ctx.fillStyle = '#e8ecf5';
  ctx.font = '700 16px system-ui, sans-serif';
  ctx.fillText('Pokračovat  →', W/2, by + 28);

  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 14px system-ui, sans-serif';
  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.restore();
}

const MENU = {
  opt1Y: 226, opt2Y: 280, optW: 470, optH: 48,
  diffY: 330, diffBW: 122, diffH: 30, diffGap: 16,
  skinY: 400, skinSW: 30, skinGap: 8, skinNameY: 450,
};
function diffChipRect(i){
  const total = 3 * MENU.diffBW + 2 * MENU.diffGap;
  return { x: W/2 - total/2 + i * (MENU.diffBW + MENU.diffGap), y: MENU.diffY, w: MENU.diffBW, h: MENU.diffH };
}
function skinChipRect(i){
  const n = SKINS.length, total = n * MENU.skinSW + (n - 1) * MENU.skinGap;
  return { x: W/2 - total/2 + i * (MENU.skinSW + MENU.skinGap), y: MENU.skinY, w: MENU.skinSW, h: MENU.skinSW };
}

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

  ctx.fillStyle = '#e8ecf5';
  ctx.font = '800 40px system-ui, sans-serif';
  ctx.fillText('HRANATÝ SVĚT', W/2, 128);
  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 14px system-ui, sans-serif';
  ctx.fillText('2D plošinovka — vyber si režim', W/2, 152);
  ctx.fillStyle = COL.coin;
  ctx.font = '600 12px system-ui, sans-serif';
  ctx.fillText('Nejdál   ·   Lehká ' + bestDist.easy + ' m    ·    Střední ' + bestDist.medium + ' m    ·    Těžká ' + bestDist.hard + ' m', W/2, 176);

  menuOption('1', 'Příběhové levely', LEVELS.length + ' ručně navržených levelů s cílem a životy', MENU.opt1Y, menuSel === 0);
  menuOption('2', 'Nekonečný běh', 'Obtížnost: ' + ENDLESS_DIFF[endlessDiff].name + '   ·   jeden život, hraj na vzdálenost', MENU.opt2Y, menuSel === 1);

  drawDiffPicker();
  drawSkinPicker();

  ctx.textAlign = 'center';
  ctx.fillStyle = '#8b93a7';
  ctx.font = '500 12px system-ui, sans-serif';
  ctx.fillText(
    isTouch ? 'ťukni na režim = start   ·   ťukni na obtížnost a barvu'
            : '↑↓ režim   ·   ←→ ' + (menuSel === 1 ? 'obtížnost' : 'barva') + '   ·   Enter = start',
    W/2, H - 16);
  ctx.restore();
}

function drawDiffPicker(){
  ctx.textAlign = 'center';
  ctx.fillStyle = '#8b93a7';
  ctx.font = '600 11px system-ui, sans-serif';
  ctx.fillText('OBTÍŽNOST NEKONEČNÉHO BĚHU', W/2, MENU.diffY - 10);

  DIFF_KEYS.forEach((k, i) => {
    const b = diffChipRect(i), sel = i === diffSel;
    ctx.fillStyle = sel ? 'rgba(92,200,255,.18)' : 'rgba(255,255,255,.05)';
    roundRect(b.x, b.y, b.w, b.h, 8); ctx.fill();
    ctx.strokeStyle = sel ? COL.player : 'rgba(255,255,255,.14)';
    ctx.lineWidth = 2; roundRect(b.x, b.y, b.w, b.h, 8); ctx.stroke();
    ctx.fillStyle = sel ? '#e8ecf5' : '#aab2c2';
    ctx.font = '700 14px system-ui, sans-serif';
    ctx.textBaseline = 'middle';
    ctx.fillText(ENDLESS_DIFF[k].name, b.x + b.w/2, b.y + b.h/2 + 1);
    ctx.textBaseline = 'alphabetic';
  });
}

function drawSkinPicker(){
  ctx.textAlign = 'center';
  ctx.fillStyle = '#8b93a7';
  ctx.font = '600 11px system-ui, sans-serif';
  ctx.fillText('BARVA POSTAVY', W/2, MENU.skinY - 10);

  for (let i = 0; i < SKINS.length; i++){
    const b = skinChipRect(i), s = SKINS[i], open = skinUnlocked(i);
    ctx.globalAlpha = open ? 1 : 0.22;
    ctx.fillStyle = s.color;
    roundRect(b.x, b.y, b.w, b.h, 8); ctx.fill();
    ctx.globalAlpha = 1;
    if (i === equippedSkin){
      ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 3;
      roundRect(b.x - 2, b.y - 2, b.w + 4, b.h + 4, 9); ctx.stroke();
    }
    if (i === skinSel && menuSel === 0){
      ctx.strokeStyle = COL.player; ctx.lineWidth = 2;
      roundRect(b.x - 6, b.y - 6, b.w + 12, b.h + 12, 12); ctx.stroke();
    }
    if (!open){
      ctx.fillStyle = '#e8ecf5';
      ctx.font = '12px system-ui, sans-serif';
      ctx.textBaseline = 'middle';
      ctx.fillText('🔒', b.x + b.w/2, b.y + b.h/2 + 1);
      ctx.textBaseline = 'alphabetic';
    }
  }

  const s = SKINS[skinSel];
  ctx.fillStyle = '#e8ecf5';
  ctx.font = '600 13px system-ui, sans-serif';
  ctx.fillText(
    skinUnlocked(skinSel)
      ? s.name + (skinSel === equippedSkin ? '  ✓ nasazeno' : '')
      : s.name + '  🔒  ·  ' + ENDLESS_DIFF[s.diff].name + ' ' + s.dist + ' m',
    W/2, MENU.skinNameY);
}

function menuOption(num, title, desc, y, sel){
  const w = MENU.optW, x = W/2 - w/2, h = MENU.optH;
  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 17px system-ui, sans-serif';
  ctx.fillText(num + '.   ' + title, x + 20, y - 4);
  ctx.fillStyle = '#8b93a7';
  ctx.font = '400 12px system-ui, sans-serif';
  ctx.fillText(desc, x + 20, y + 15);
  ctx.textAlign = 'center';
}

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

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

  // herní dotykové ovládání má přednost během hry
  if (state === 'play' || state === 'paused'){
    if (inCirc(p, TB.pause)){ state = state === 'play' ? 'paused' : 'play'; return; }
    if (state === 'paused' &&
        p.x >= W/2 - 80 && p.x <= W/2 + 80 && p.y >= H/2 + 40 && p.y <= H/2 + 84){
      state = 'menu'; return;
    }
    if (state === 'play'){
      if (inCirc(p, TB.jump)){
        activePtr.set(e.pointerId, 'jump');
        codesHit.add('Space');                   // náběžná hrana skoku
        applyTouch();
        return;
      }
      if (joy.id === null && p.x < W * 0.6){      // levá část obrazovky = joystick
        joyStart(e.pointerId, p);
        return;
      }
    }
  }

  if (state === 'menu'){
    // volba režimu (ťuknutí = start)
    if (Math.abs(p.x - W/2) <= MENU.optW/2){
      if (Math.abs(p.y - MENU.opt1Y) <= MENU.optH/2 + 4){ menuSel = 0; return startGame('levels'); }
      if (Math.abs(p.y - MENU.opt2Y) <= MENU.optH/2 + 4){ menuSel = 1; return startGame('endless'); }
    }
    // volba obtížnosti
    for (let i = 0; i < 3; i++){
      const b = diffChipRect(i);
      if (p.x >= b.x - 4 && p.x <= b.x + b.w + 4 && p.y >= b.y - 6 && p.y <= b.y + b.h + 6){
        diffSel = i; endlessDiff = DIFF_KEYS[i]; menuSel = 1; S.coin();
        return;
      }
    }
    // volba barvy postavy
    for (let i = 0; i < SKINS.length; i++){
      const b = skinChipRect(i);
      if (p.x >= b.x - 3 && p.x <= b.x + b.w + 3 && p.y >= b.y - 3 && p.y <= b.y + b.h + 3){
        skinSel = i; menuSel = 0;
        if (skinUnlocked(i) && i !== equippedSkin){ equippedSkin = i; store('skin', i); S.coin(); }
        return;
      }
    }
  } else if (state === 'shop'){
    for (let i = 0; i < SHOP.length; i++){
      const y = SHOP_Y0 + i * (SHOP_ROW_H + SHOP_GAP);
      if (p.x >= SHOP_X && p.x <= SHOP_X + SHOP_W && p.y >= y && p.y <= y + SHOP_ROW_H)
        return buyShop(i);
    }
    const by = shopBtnY();
    if (p.x >= W/2 - 120 && p.x <= W/2 + 120 && p.y >= by && p.y <= by + 44)
      return shopContinue();
  } else if (state === 'over' || state === 'win'){
    state = 'menu';
  } else if (state === 'paused'){
    state = 'play';
  }
});

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

function endPtr(e){
  let changed = false;
  if (e.pointerId === joy.id){ joyEnd(); changed = false; }   // joyEnd už volá applyTouch
  if (activePtr.delete(e.pointerId)) changed = true;
  if (changed) applyTouch();
}
canvas.addEventListener('pointerup', endPtr);
canvas.addEventListener('pointercancel', endPtr);
addEventListener('blur', () => { activePtr.clear(); joyEnd(); });

/* ------------------------------------------------------------------ */
/*  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;
  while (acc >= STEP && steps < 5){
    update();
    codesHit.clear();
    acc -= STEP;
    steps++;
  }
  if (steps === 5) acc = 0;
  draw();
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

})();
</script>
</body>
</html>


<total_tokens>14974940 tokens left</total_tokens>


112
113	const TB = {                     // rozmístění tlačítek (v souřadnicích plátna)
114	  jump:  { cx: W - 108, cy: H - 104, r: 88 },
115	  pause: { cx: W / 2, cy: 30, r: 24 },
116	};
117	const inCirc = (p, b) => Math.hypot(p.x - b.cx, p.y - b.cy) <= b.r;
118
119	/* Plovoucí joystick (styl Brawl Stars) – používá se jen vodorovná osa */
120	const JOY_HOME = { x: 148, y: H - 112 };
121	const JOY_R    = 78;             // dosah palce

const TB = {                     // rozmístění tlačítek (v souřadnicích plátna)
  jump:  { cx: W - 108, cy: H - 104, r: 88 },
  pause: { cx: W / 2, cy: 30, r: 24 },
};
const inCirc = (p, b) => Math.hypot(p.x - b.cx, p.y - b.cy) <= b.r;

/* Plovoucí joystick (styl Brawl Stars) – používá se jen vodorovná osa */
const JOY_HOME = { x: 148, y: H - 112 };
const JOY_R    = 78;             // dosah palce


<total_tokens>14959797 tokens left</total_tokens>


Edit

const TB = {                     // rozmístění tlačítek (v souřadnicích plátna)
  jump:  { cx: W - 108, cy: H - 104, r: 88 },
  pause: { cx: W / 2, cy: 30, r: 24 },
};
const inCirc = (p, b) => Math.hypot(p.x - b.cx, p.y - b.cy) <= b.r;

const TB = {                     // rozmístění tlačítek (v souřadnicích plátna)
  jump:  { cx: W - 108, cy: H - 104, r: 88 },
  pause: { cx: W / 2, cy: 30, r: 24 },
};
const FSB_MENU  = { cx: W - 40, cy: 40, r: 22 };        // celá obrazovka – v menu
const FSB_PAUSE = { cx: W / 2, cy: H / 2 + 120, r: 22 }; // celá obrazovka – v pauze
const inCirc = (p, b) => Math.hypot(p.x - b.cx, p.y - b.cy) <= b.r;


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

const TB = {                     // rozmístění tlačítek (v souřadnicích plátna)
  jump:  { cx: W - 108, cy: H - 104, r: 88 },
  pause: { cx: W / 2, cy: 30, r: 24 },
};
const inCirc = (p, b) => Math.hypot(p.x - b.cx, p.y - b.cy) <= b.r;

const TB = {                     // rozmístění tlačítek (v souřadnicích plátna)
  jump:  { cx: W - 108, cy: H - 104, r: 88 },
  pause: { cx: W / 2, cy: 30, r: 24 },
};
const FSB_MENU  = { cx: W - 40, cy: 40, r: 22 };        // celá obrazovka – v menu
const FSB_PAUSE = { cx: W / 2, cy: H / 2 + 120, r: 22 }; // celá obrazovka – v pauze
const inCirc = (p, b) => Math.hypot(p.x - b.cx, p.y - b.cy) <= b.r;

+const FSB_MENU  = { cx: W - 40, cy: 40, r: 22 };        // celá obrazovka – v menu

+const FSB_PAUSE = { cx: W / 2, cy: H / 2 + 120, r: 22 }; // celá obrazovka – v pauze

/* Plovoucí joystick (styl Brawl Stars) – používá se jen vodorovná osa */


<total_tokens>14959056 tokens left</total_tokens>


/* ------------------------------------------------------------------ */
/*  Celá obrazovka (hlavně pro mobil)                                 */
/* ------------------------------------------------------------------ */
const fsEl = () => document.fullscreenElement || document.webkitFullscreenElement;
const fsOK = () => {
  const e = document.documentElement;
  return !!(e.requestFullscreen || e.webkitRequestFullscreen);
};
function toggleFullscreen(){
  try {
    if (fsEl()){
      (document.exitFullscreen || document.webkitExitFullscreen).call(document);
    } else {
      const e = document.documentElement;
      (e.requestFullscreen || e.webkitRequestFullscreen).call(e);
    }
  } catch (err) {}
}

/* kulaté tlačítko s ikonou „rohů“ – plný = zvětšit, stažený = zmenšit */
function drawFsBtn(b){
  if (!isTouch || !fsOK()) return;
  const on = !!fsEl();
  ctx.save();
  ctx.fillStyle = 'rgba(255,255,255,.10)';
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = 'rgba(255,255,255,.22)'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.stroke();
  ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 2.4; ctx.lineCap = 'round';
  const arm = 5, off = on ? 2 : 7;
  for (let i = 0; i < 4; i++){
    const sx = (i === 1 || i === 3) ? 1 : -1;
    const sy = i < 2 ? -1 : 1;
    const x = b.cx + sx * off, y = b.cy + sy * off;
    ctx.beginPath();
    ctx.moveTo(x + sx * arm, y);
    ctx.lineTo(x, y);
    ctx.lineTo(x, y + sy * arm);
    ctx.stroke();
  }
  ctx.lineCap = 'butt';
  ctx.restore();
}

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


/* ------------------------------------------------------------------ */
/*  Celá obrazovka (hlavně pro mobil)                                 */
/* ------------------------------------------------------------------ */
const fsEl = () => document.fullscreenElement || document.webkitFullscreenElement;
const fsOK = () => {
  const e = document.documentElement;
  return !!(e.requestFullscreen || e.webkitRequestFullscreen);
};
function toggleFullscreen(){
  try {
    if (fsEl()){
      (document.exitFullscreen || document.webkitExitFullscreen).call(document);
    } else {
      const e = document.documentElement;
      (e.requestFullscreen || e.webkitRequestFullscreen).call(e);
    }
  } catch (err) {}
}

/* kulaté tlačítko s ikonou „rohů“ – plný = zvětšit, stažený = zmenšit */
function drawFsBtn(b){
  if (!isTouch || !fsOK()) return;
  const on = !!fsEl();
  ctx.save();
  ctx.fillStyle = 'rgba(255,255,255,.10)';
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.fill();
  ctx.strokeStyle = 'rgba(255,255,255,.22)'; ctx.lineWidth = 2;
  ctx.beginPath(); ctx.arc(b.cx, b.cy, b.r, 0, Math.PI * 2); ctx.stroke();
  ctx.strokeStyle = '#e8ecf5'; ctx.lineWidth = 2.4; ctx.lineCap = 'round';
  const arm = 5, off = on ? 2 : 7;
  for (let i = 0; i < 4; i++){
    const sx = (i === 1 || i === 3) ? 1 : -1;
    const sy = i < 2 ? -1 : 1;
    const x = b.cx + sx * off, y = b.cy + sy * off;
    ctx.beginPath();
    ctx.moveTo(x + sx * arm, y);
    ctx.lineTo(x, y);
    ctx.lineTo(x, y + sy * arm);
    ctx.stroke();
  }
  ctx.lineCap = 'butt';
  ctx.restore();
}

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

+/*  Celá obrazovka (hlavně pro mobil)                                 */

+const fsEl = () => document.fullscreenElement || document.webkitFullscreenElement;

+  return !!(e.requestFullscreen || e.webkitRequestFullscreen);

+      (document.exitFullscreen || document.webkitExitFullscreen).call(document);

+      const e = document.documentElement;

+      (e.requestFullscreen || e.webkitRequestFullscreen).call(e);

+/* kulaté tlačítko s ikonou „rohů“ – plný = zvětšit, stažený = zmenšit */

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

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

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

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

+    const x = b.cx + sx * off, y = b.cy + sy * off;


<total_tokens>14957873 tokens left</total_tokens>


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

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

  if (state === 'menu')        drawFsBtn(FSB_MENU);
  else if (state === 'paused') drawFsBtn(FSB_PAUSE);
}


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

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

  if (state === 'menu')        drawFsBtn(FSB_MENU);
  else if (state === 'paused') drawFsBtn(FSB_PAUSE);
}

+  if (state === 'menu')        drawFsBtn(FSB_MENU);

+  else if (state === 'paused') drawFsBtn(FSB_PAUSE);


<total_tokens>14957419 tokens left</total_tokens>


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

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

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

  // tlačítko celé obrazovky (mobil) – musí být z gesta uživatele
  if (isTouch && fsOK()){
    if (state === 'menu'   && inCirc(p, FSB_MENU )){ toggleFullscreen(); return; }
    if (state === 'paused' && inCirc(p, FSB_PAUSE)){ toggleFullscreen(); return; }
  }

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


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

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

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

  // tlačítko celé obrazovky (mobil) – musí být z gesta uživatele
  if (isTouch && fsOK()){
    if (state === 'menu'   && inCirc(p, FSB_MENU )){ toggleFullscreen(); return; }
    if (state === 'paused' && inCirc(p, FSB_PAUSE)){ toggleFullscreen(); return; }
  }

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

+  // tlačítko celé obrazovky (mobil) – musí být z gesta uživatele

+    if (state === 'menu'   && inCirc(p, FSB_MENU )){ toggleFullscreen(); return; }

+    if (state === 'paused' && inCirc(p, FSB_PAUSE)){ toggleFullscreen(); return; }

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

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


<total_tokens>14956962 tokens left</total_tokens>


Bash

node -e "const s=require('fs').readFileSync('hra.html','utf8');const m=s.match(/<script>([\s\S]*)<\/script>/);new Function(m[1]);console.log('JS OK');"

Syntax-check the game script


Error: Exit code 127
/bin/bash: line 1: node: command not found


<total_tokens>14956705 tokens left</total_tokens>


(python3 -c "
import re,sys
s=open('hra.html').read()
body=re.search(r'<script>(.*)</script>',s,re.S).group(1)
open('/tmp/claude-1000/-home-tonda/52dd4f1e-d3f2-4a6d-a4bc-68641bfe625f/scratchpad/g.js','w').write(body)
print('extracted',len(body))
"; which deno esbuild 2>/dev/null; ls ~/.local/bin 2>/dev/null)

Extract script, look for a JS engine


extracted 53071
claude


<total_tokens>14956381 tokens left</total_tokens>


Rough bracket balance check


{ } 239 239
( ) 984 984
[ ] 170 170


<total_tokens>14955906 tokens left</total_tokens>


./hra-server.sh && curl -s http://localhost:8000/hra.html | grep -c "drawFsBtn"

Start server and verify new code served


Server už běží: http://192.168.5.32:8000/hra.html
3


<total_tokens>14955335 tokens left</total_tokens>


Hotovo. Přidal jsem tlačítko pro celou obrazovku, které se zobrazí **jen na mobilu** (dotykové ovládání) a jen když prohlížeč fullscreen podporuje:

- **V menu** – kulaté tlačítko vpravo nahoře (ikona rohů). Ťuknutím se hra roztáhne na celou obrazovku ještě před startem.
- **V pauze** – stejné tlačítko pod tlačítkem „← Menu", kdyby to chtěl někdo zapnout/vypnout během hraní.
- Ikona se přepíná: rozevřené rohy = zvětšit, stažené k sobě = zmenšit. Opětovné ťuknutí fullscreen ukončí.

Změny (vše v `hra.html`):
- nová sekce s `toggleFullscreen()` + `drawFsBtn()` (kreslení na canvas, ve stylu ostatních dotykových tlačítek)
- `FSB_MENU` / `FSB_PAUSE` souřadnice tlačítek
- volání v `draw()` a obsluha ťuknutí v `pointerdown` (fullscreen musí přijít z gesta uživatele, proto je to hned v pointer handleru)

CSS pro `:fullscreen` už v souboru bylo, takže canvas se v režimu celé obrazovky správně roztáhne s letterboxem a `canvasPos()` s tím počítá.

Server běží, stačí v prohlížeči načíst znovu `http://192.168.5.32:8000/hra.html`. Fullscreen na canvas/`<html>` nefunguje na iPhonu (Safari to povoluje jen pro video) – na Androidu a desktopu ano.


na mobilu ze zhora kouká adresa stránky a kvůli nelzevidět dólu protuže se to celé posunulo


<total_tokens>15000000 tokens left</total_tokens>


Edit

  /* na mobilu: klávesová nápověda pryč, canvas co největší */
  @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 se celý vejde na obrazovku */
  @media (max-width: 760px), (pointer: coarse){
    html,body{overflow:hidden}
    body{
      height:100vh;          /* fallback pro starší prohlížeče */
      height:100dvh;         /* skutečná viditelná výška – bez adresního řádku */
      gap:0; padding:0; justify-content:center;
    }
    h1{display:none}
    .hint{display:none}
    canvas{
      width:auto; height:auto;      /* drž poměr stran 900:540 */
      max-width:100vw;
      max-height:100vh;
      max-height:100dvh;
      border-radius:0;
    }
  }


  /* na mobilu: klávesová nápověda pryč, canvas co největší */
  @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 se celý vejde na obrazovku */
  @media (max-width: 760px), (pointer: coarse){
    html,body{overflow:hidden}
    body{
      height:100vh;          /* fallback pro starší prohlížeče */
      height:100dvh;         /* skutečná viditelná výška – bez adresního řádku */
      gap:0; padding:0; justify-content:center;
    }
    h1{display:none}
    .hint{display:none}
    canvas{
      width:auto; height:auto;      /* drž poměr stran 900:540 */
      max-width:100vw;
      max-height:100vh;
      max-height:100dvh;
      border-radius:0;
    }
  }

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

+  /* na mobilu: klávesová nápověda pryč, canvas se celý vejde na obrazovku */

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

+      height:100vh;          /* fallback pro starší prohlížeče */

+      height:100dvh;         /* skutečná viditelná výška – bez adresního řádku */

+      gap:0; padding:0; justify-content:center;

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

+      width:auto; height:auto;      /* drž poměr stran 900:540 */

/* celá obrazovka: vejít se do výšky i šířky, žádné okraje */

:fullscreen, :-webkit-full-screen{ background:#101320; }


<total_tokens>14997238 tokens left</total_tokens>


  :fullscreen canvas, :-webkit-full-screen canvas{
    width:100vw; height:100vh; object-fit:contain; border-radius:0;
  }

  :fullscreen canvas, :-webkit-full-screen canvas{
    width:100vw; height:100dvh; object-fit:contain; border-radius:0;
  }


  :fullscreen canvas, :-webkit-full-screen canvas{
    width:100vw; height:100vh; object-fit:contain; border-radius:0;
  }

  :fullscreen canvas, :-webkit-full-screen canvas{
    width:100vw; height:100dvh; object-fit:contain; border-radius:0;
  }

:fullscreen body, :-webkit-full-screen body{ padding:0; gap:0; }

-    width:100vw; height:100vh; object-fit:contain; border-radius:0;

+    width:100vw; height:100dvh; object-fit:contain; border-radius:0;


<total_tokens>14996826 tokens left</total_tokens>


Bash

curl -s http://localhost:8000/hra.html | grep -n "dvh\|max-height:100vh\|width:auto"

Verify updated CSS is served


39:      height:100dvh;         /* skutečná viditelná výška – bez adresního řádku */
45:      width:auto; height:auto;      /* drž poměr stran 900:540 */
47:      max-height:100vh;
48:      max-height:100dvh;
56:    width:100vw; height:100dvh; object-fit:contain; border-radius:0;


<total_tokens>14996219 tokens left</total_tokens>


Opravil jsem to. Příčina: mobilní prohlížeč nechává nahoře adresní řádek a stránka přitom počítala s výškou, jako by tam nebyl (`100vh` = výška bez lišty). Hra se tak zvětšila přes celou tu „vyšší" výšku a odspoda ji lišta ořízla.

Změny v CSS (`hra.html`):

- Výška `body` na mobilu je teď `100dvh` (*dynamic viewport height*) — skutečná viditelná výška **včetně** místa, které zabírá adresní řádek. Fallback `100vh` zůstává pro starší prohlížeče.
- Canvas se už neroztahuje natvrdo na `width:100%` (to ho v ležato dělalo vyšší než obrazovka). Nově má `width:auto; height:auto` s limity `max-width:100vw` a `max-height:100dvh`, takže si drží poměr 900:540 a celý se vejde — nahoře i dole. Kolem něj je tmavý okraj (letterbox).
- Dotykové souřadnice se přepočítávají z reálné velikosti canvasu, takže ovládání sedí dál.

Načti v prohlížeči znovu. Tlačítkem „celá obrazovka" v menu adresní řádek zmizí úplně a hra vyplní celou plochu (na Androidu; iPhone Safari fullscreen na stránku neumí).


We're editing the game hra.html (Hranatý svět platformer). Just added a mobile fullscreen button and fixed the mobile layout so the address bar no longer cuts off the bottom. Next: you reload on your phone and confirm it looks right. (disable recaps in /config)


funguje to. udělej zakladní barvu charakteru bílou


Edit

{ name: 'Nebe',        color: '#5cc8ff', diff: null,     dist: 0 },

{ name: 'Bílá',        color: '#ffffff', diff: null,     dist: 0 },


/*  podle nejdelšího běhu (v metrech) v té obtížnosti.                */

-  { name: 'Nebe',        color: '#5cc8ff', diff: null,     dist: 0 },

+  { name: 'Bílá',        color: '#ffffff', diff: null,     dist: 0 },

{ name: 'Lehká I',     color: '#86efac', diff: 'easy',   dist: 100 },

{ name: 'Lehká II',    color: '#4ade80', diff: 'easy',   dist: 300 },

{ name: 'Lehká III',   color: '#15a34a', diff: 'easy',   dist: 600 },


<total_tokens>14999457 tokens left</total_tokens>


Hotovo. Základní barva postavy (skin „Bílá", index 0, odemčený od začátku) je teď `#ffffff` místo modré.

Pozn.: pokud jsi měl v menu vybranou jinou barvu, uloží se v prohlížeči a zůstane — bílou nastavíš ťuknutím na první políčko v „BARVA POSTAVY".


u příběhových levelů udělej rozcvičku lehčí


name: 'Rozcvička', width: 56, floorRow: 12,

name: 'Mezery', width: 68, floorRow: 12,


<total_tokens>14998386 tokens left</total_tokens>


Rozcvička je teď mírnější:

- **Propasti zúžené z 3 na 2 dlaždice** (`21–22` a `34–35`) — přeskočíš je pohodlně i bez rozběhu.
- **Nepřátelé ze 4 na 2** — zůstali jen dva malí na hlavní podlaze (`15`, `40`); zmizel ten na plošině u propasti a jeden u cíle.
- **Plošina nad druhou propastí snížena** z řádku 7 na 8, aby se na ni dalo snáz doskočit (i mince nad ní).

Ostatní levely beze změny.


odeber v nekonečném běhu tu zeď která když mě dožene tak mězabije ale jenom na lehkou obtížnost


Bash

grep -n "wall\|Wall\|endlessDiff\|drawWall\|wgap" /home/tonda/hra.html

Find all wall references


389:  easy:   { name: 'Lehká',   gapChance: 0.09, gapMax: 3, enemy1: 0.30, enemy2: 0.06, tierBias: -0.55, wallBase: 1.0, wallRamp: 2.2, enemyMul: 0.45 },
390:  medium: { name: 'Střední', gapChance: 0.20, gapMax: 4, enemy1: 0.55, enemy2: 0.42, tierBias:  0.00, wallBase: 1.3, wallRamp: 3.3, enemyMul: 1.0 },
391:  hard:   { name: 'Těžká',   gapChance: 0.32, gapMax: 5, enemy1: 0.85, enemy2: 0.80, tierBias:  0.50, wallBase: 1.6, wallRamp: 4.2, enemyMul: 1.7 },
407:  wallSpeed = (ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium).wallBase;
408:  wallX = -TILE*12;
422:  const D = ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium;
673:    wallX = player.x - W*0.75;                     // odstrč zeď daleko dozadu
686:      wallX = camX - TILE*9;
691:    const D = ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium;
692:    wallSpeed = D.wallBase + Math.min(distance / 280, D.wallRamp);
693:    wallX += wallSpeed;
697:    if (camX < wallX - 30) camX = wallX - 30;      // zeď tlačí kameru
699:    if (camX - wallX > W*1.5) wallX = camX - W*1.5; // zeď nezůstane moc pozadu
707:    const wgap = player.x - wallX;
708:    if (wgap <= 0 && player.invuln <= 0) hurtPlayer();
709:    else if (wgap < 150 && pruneT % 15 === 0) beep(150, 0.05, 'square', 0.03);
723:let endlessDiff = 'medium';   // easy | medium | hard
727:let wallX = -400, wallSpeed = 1.3;          // zeď smrti v nekonečném módu
802:  const d = endlessDiff, prev = bestDist[d] || 0;
838:      endlessDiff = DIFF_KEYS[diffSel];
926:    if (mode === 'endless' && endlessStarted && wallX > camX - 100) drawWall();
932:    const gap = player.x - wallX;
1025:function drawWall(){
1026:  const wx = wallX, top = -160, h = (ROWS + 6) * TILE;
1273:  menuOption('2', 'Nekonečný běh', 'Obtížnost: ' + ENDLESS_DIFF[endlessDiff].name + '   ·   jeden život, hraj na vzdálenost', MENU.opt2Y, menuSel === 1);
1420:        diffSel = i; endlessDiff = DIFF_KEYS[i]; menuSel = 1; S.coin();


<total_tokens>14999392 tokens left</total_tokens>


678	/* ------------------------------------------------------------------ */
679	/*  Kamera                                                            */
680	/* ------------------------------------------------------------------ */
681	function updateCamera(){
682	  if (mode === 'endless'){
683	    if (!endlessStarted){
684	      if (down('ArrowRight', 'KeyD') || player.x > 5*TILE) endlessStarted = true;
685	      camX = Math.max(0, player.x + player.w/2 - W*0.42);
686	      wallX = camX - TILE*9;
687	      generateAhead(camX);
688	      return;
689	    }
690	    // zeď se plíží zleva doprava a zrychluje – tempo podle obtížnosti
691	    const D = ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium;
692	    wallSpeed = D.wallBase + Math.min(distance / 280, D.wallRamp);
693	    wallX += wallSpeed;
694
695	    const follow = player.x + player.w/2 - W*0.42;
696	    camX += (follow - camX) * 0.16;
697	    if (camX < wallX - 30) camX = wallX - 30;      // zeď tlačí kameru
698	    if (camX < 0) camX = 0;
699	    if (camX - wallX > W*1.5) wallX = camX - W*1.5; // zeď nezůstane moc pozadu
700
701	    distance = Math.floor(camX / TILE);
702	    generateAhead(camX);
703	    pruneWorld();
704	    world.enemies = world.enemies.filter(e => e.x > camX - 300);
705	    world.coins   = world.coins.filter(c => !c.got && c.x > camX - 200);
706
707	    const wgap = player.x - wallX;
708	    if (wgap <= 0 && player.invuln <= 0) hurtPlayer();
709	    else if (wgap < 150 && pruneT % 15 === 0) beep(150, 0.05, 'square', 0.03);
710	    return;
711	  }
712	  const target = player.x + player.w/2 - W*0.42;
713	  camX += (target - camX) * 0.14;
714	  camX = clamp(camX, 0, world.width*TILE - W);
715	}
716
717	/* ------------------------------------------------------------------ */
718	/*  Stavy hry                                                         */
719	/* ------------------------------------------------------------------ */
720	let state = 'menu';      // menu | play | paused | shop | over | win
721	let mode  = 'levels';    // levels | endless
722	let menuSel = 0;

/* ------------------------------------------------------------------ */
/*  Kamera                                                            */
/* ------------------------------------------------------------------ */
function updateCamera(){
  if (mode === 'endless'){
    if (!endlessStarted){
      if (down('ArrowRight', 'KeyD') || player.x > 5*TILE) endlessStarted = true;
      camX = Math.max(0, player.x + player.w/2 - W*0.42);
      wallX = camX - TILE*9;
      generateAhead(camX);
      return;
    }
    // zeď se plíží zleva doprava a zrychluje – tempo podle obtížnosti
    const D = ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium;
    wallSpeed = D.wallBase + Math.min(distance / 280, D.wallRamp);
    wallX += wallSpeed;

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

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

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

/* ------------------------------------------------------------------ */
/*  Stavy hry                                                         */
/* ------------------------------------------------------------------ */
let state = 'menu';      // menu | play | paused | shop | over | win
let mode  = 'levels';    // levels | endless
let menuSel = 0;


<total_tokens>14998156 tokens left</total_tokens>


920	      ctx.globalAlpha = Math.max(0, p.life / 34);
921	      ctx.fillStyle = COL.coin;
922	      ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, Math.PI * 2); ctx.fill();
923	    }
924	    ctx.globalAlpha = 1;
925	    if (state !== 'shop') drawPlayer();
926	    if (mode === 'endless' && endlessStarted && wallX > camX - 100) drawWall();
927	  }
928	  ctx.restore();
929
930	  // varovné blikání, když je zeď blízko
931	  if (mode === 'endless' && state === 'play' && endlessStarted){
932	    const gap = player.x - wallX;
933	    if (gap < 240){
934	      const a = clamp(1 - gap / 240, 0, 1);
935	      const pulse = 0.45 + 0.3 * Math.sin(performance.now() / 90);
936	      const grd = ctx.createLinearGradient(0, 0, W * 0.55, 0);
937	      grd.addColorStop(0, 'rgba(255,45,65,' + (a * pulse).toFixed(3) + ')');
938	      grd.addColorStop(1, 'rgba(255,45,65,0)');
939	      ctx.fillStyle = grd;
940	      ctx.fillRect(0, 0, W * 0.55, H);
941	    }
942	  }
943
944	  if (world) drawHUD();

      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();
    if (mode === 'endless' && endlessStarted && wallX > camX - 100) drawWall();
  }
  ctx.restore();

  // varovné blikání, když je zeď blízko
  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();


<total_tokens>14995350 tokens left</total_tokens>


Edit

  easy:   { name: 'Lehká',   gapChance: 0.09, gapMax: 3, enemy1: 0.30, enemy2: 0.06, tierBias: -0.55, wallBase: 1.0, wallRamp: 2.2, enemyMul: 0.45 },
  medium: { name: 'Střední', gapChance: 0.20, gapMax: 4, enemy1: 0.55, enemy2: 0.42, tierBias:  0.00, wallBase: 1.3, wallRamp: 3.3, enemyMul: 1.0 },
  hard:   { name: 'Těžká',   gapChance: 0.32, gapMax: 5, enemy1: 0.85, enemy2: 0.80, tierBias:  0.50, wallBase: 1.6, wallRamp: 4.2, enemyMul: 1.7 },

  easy:   { name: 'Lehká',   wall: false, gapChance: 0.09, gapMax: 3, enemy1: 0.30, enemy2: 0.06, tierBias: -0.55, wallBase: 1.0, wallRamp: 2.2, enemyMul: 0.45 },
  medium: { name: 'Střední', wall: true,  gapChance: 0.20, gapMax: 4, enemy1: 0.55, enemy2: 0.42, tierBias:  0.00, wallBase: 1.3, wallRamp: 3.3, enemyMul: 1.0 },
  hard:   { name: 'Těžká',   wall: true,  gapChance: 0.32, gapMax: 5, enemy1: 0.85, enemy2: 0.80, tierBias:  0.50, wallBase: 1.6, wallRamp: 4.2, enemyMul: 1.7 },


  easy:   { name: 'Lehká',   gapChance: 0.09, gapMax: 3, enemy1: 0.30, enemy2: 0.06, tierBias: -0.55, wallBase: 1.0, wallRamp: 2.2, enemyMul: 0.45 },
  medium: { name: 'Střední', gapChance: 0.20, gapMax: 4, enemy1: 0.55, enemy2: 0.42, tierBias:  0.00, wallBase: 1.3, wallRamp: 3.3, enemyMul: 1.0 },
  hard:   { name: 'Těžká',   gapChance: 0.32, gapMax: 5, enemy1: 0.85, enemy2: 0.80, tierBias:  0.50, wallBase: 1.6, wallRamp: 4.2, enemyMul: 1.7 },

  easy:   { name: 'Lehká',   wall: false, gapChance: 0.09, gapMax: 3, enemy1: 0.30, enemy2: 0.06, tierBias: -0.55, wallBase: 1.0, wallRamp: 2.2, enemyMul: 0.45 },
  medium: { name: 'Střední', wall: true,  gapChance: 0.20, gapMax: 4, enemy1: 0.55, enemy2: 0.42, tierBias:  0.00, wallBase: 1.3, wallRamp: 3.3, enemyMul: 1.0 },
  hard:   { name: 'Těžká',   wall: true,  gapChance: 0.32, gapMax: 5, enemy1: 0.85, enemy2: 0.80, tierBias:  0.50, wallBase: 1.6, wallRamp: 4.2, enemyMul: 1.7 },

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

-  easy:   { name: 'Lehká',   gapChance: 0.09, gapMax: 3, enemy1: 0.30, enemy2: 0.06, tierBias: -0.55, wallBase: 1.0, wallRamp: 2.2, enemyMul: 0.45 },

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

-  hard:   { name: 'Těžká',   gapChance: 0.32, gapMax: 5, enemy1: 0.85, enemy2: 0.80, tierBias:  0.50, wallBase: 1.6, wallRamp: 4.2, enemyMul: 1.7 },

+  easy:   { name: 'Lehká',   wall: false, gapChance: 0.09, gapMax: 3, enemy1: 0.30, enemy2: 0.06, tierBias: -0.55, wallBase: 1.0, wallRamp: 2.2, enemyMul: 0.45 },

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

+  hard:   { name: 'Těžká',   wall: true,  gapChance: 0.32, gapMax: 5, enemy1: 0.85, enemy2: 0.80, tierBias:  0.50, wallBase: 1.6, wallRamp: 4.2, enemyMul: 1.7 },


<total_tokens>14993793 tokens left</total_tokens>


/* ------------------------------------------------------------------ */
/*  Kamera                                                            */
/* ------------------------------------------------------------------ */
function updateCamera(){
  if (mode === 'endless'){
    if (!endlessStarted){
      if (down('ArrowRight', 'KeyD') || player.x > 5*TILE) endlessStarted = true;
      camX = Math.max(0, player.x + player.w/2 - W*0.42);
      wallX = camX - TILE*9;
      generateAhead(camX);
      return;
    }
    // zeď se plíží zleva doprava a zrychluje – tempo podle obtížnosti
    const D = ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium;
    wallSpeed = D.wallBase + Math.min(distance / 280, D.wallRamp);
    wallX += wallSpeed;

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

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

    const wgap = player.x - wallX;
    if (wgap <= 0 && player.invuln <= 0) hurtPlayer();
    else if (wgap < 150 && pruneT % 15 === 0) beep(150, 0.05, 'square', 0.03);
    return;
  }

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

function updateCamera(){
  if (mode === 'endless'){
    if (!endlessStarted){
      if (down('ArrowRight', 'KeyD') || player.x > 5*TILE) endlessStarted = true;
      camX = Math.max(0, player.x + player.w/2 - W*0.42);
      wallX = camX - TILE*9;
      generateAhead(camX);
      return;
    }
    const D = ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium;
    const wall = D.wall;

    // zeď se plíží zleva doprava a zrychluje – tempo podle obtížnosti
    if (wall){
      wallSpeed = D.wallBase + Math.min(distance / 280, D.wallRamp);
      wallX += wallSpeed;
    }

    const follow = player.x + player.w/2 - W*0.42;
    camX += (follow - camX) * 0.16;
    if (wall){
      if (camX < wallX - 30) camX = wallX - 30;        // zeď tlačí kameru
      if (camX - wallX > W*1.5) wallX = camX - W*1.5;   // zeď nezůstane moc pozadu
    } else if (camX < camMax){
      camX = camMax;                                    // lehká: kamera se nevrací zpět
    }
    if (camX < 0) camX = 0;
    camMax = Math.max(camMax, camX);

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

    if (wall){
      const wgap = player.x - wallX;
      if (wgap <= 0 && player.invuln <= 0) hurtPlayer();
      else if (wgap < 150 && pruneT % 15 === 0) beep(150, 0.05, 'square', 0.03);
    }
    return;
  }


/* ------------------------------------------------------------------ */
/*  Kamera                                                            */
/* ------------------------------------------------------------------ */
function updateCamera(){
  if (mode === 'endless'){
    if (!endlessStarted){
      if (down('ArrowRight', 'KeyD') || player.x > 5*TILE) endlessStarted = true;
      camX = Math.max(0, player.x + player.w/2 - W*0.42);
      wallX = camX - TILE*9;
      generateAhead(camX);
      return;
    }
    // zeď se plíží zleva doprava a zrychluje – tempo podle obtížnosti
    const D = ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium;
    wallSpeed = D.wallBase + Math.min(distance / 280, D.wallRamp);
    wallX += wallSpeed;

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

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

    const wgap = player.x - wallX;
    if (wgap <= 0 && player.invuln <= 0) hurtPlayer();
    else if (wgap < 150 && pruneT % 15 === 0) beep(150, 0.05, 'square', 0.03);
    return;
  }

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

function updateCamera(){
  if (mode === 'endless'){
    if (!endlessStarted){
      if (down('ArrowRight', 'KeyD') || player.x > 5*TILE) endlessStarted = true;
      camX = Math.max(0, player.x + player.w/2 - W*0.42);
      wallX = camX - TILE*9;
      generateAhead(camX);
      return;
    }
    const D = ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium;
    const wall = D.wall;

    // zeď se plíží zleva doprava a zrychluje – tempo podle obtížnosti
    if (wall){
      wallSpeed = D.wallBase + Math.min(distance / 280, D.wallRamp);
      wallX += wallSpeed;
    }

    const follow = player.x + player.w/2 - W*0.42;
    camX += (follow - camX) * 0.16;
    if (wall){
      if (camX < wallX - 30) camX = wallX - 30;        // zeď tlačí kameru
      if (camX - wallX > W*1.5) wallX = camX - W*1.5;   // zeď nezůstane moc pozadu
    } else if (camX < camMax){
      camX = camMax;                                    // lehká: kamera se nevrací zpět
    }
    if (camX < 0) camX = 0;
    camMax = Math.max(camMax, camX);

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

    if (wall){
      const wgap = player.x - wallX;
      if (wgap <= 0 && player.invuln <= 0) hurtPlayer();
      else if (wgap < 150 && pruneT % 15 === 0) beep(150, 0.05, 'square', 0.03);
    }
    return;
  }

/*  Kamera                                                            */

+const wallOn = () => (ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium).wall;

-    // zeď se plíží zleva doprava a zrychluje – tempo podle obtížnosti

-    wallSpeed = D.wallBase + Math.min(distance / 280, D.wallRamp);

+    // zeď se plíží zleva doprava a zrychluje – tempo podle obtížnosti

+      wallSpeed = D.wallBase + Math.min(distance / 280, D.wallRamp);

-    if (camX < wallX - 30) camX = wallX - 30;      // zeď tlačí kameru

+      if (camX < wallX - 30) camX = wallX - 30;        // zeď tlačí kameru

+      if (camX - wallX > W*1.5) wallX = camX - W*1.5;   // zeď nezůstane moc pozadu

+      camX = camMax;                                    // lehká: kamera se nevrací zpět

-    if (camX - wallX > W*1.5) wallX = camX - W*1.5; // zeď nezůstane moc pozadu

-    if (wgap <= 0 && player.invuln <= 0) hurtPlayer();

+      if (wgap <= 0 && player.invuln <= 0) hurtPlayer();

const target = player.x + player.w/2 - W*0.42;


<total_tokens>14992130 tokens left</total_tokens>


Bash

grep -n "let camX\|camX = 0; camY\|camX = 0, camY" /home/tonda/hra.html

Find camX declaration and reset


405:  camX = 0; camY = 0;
739:let camX = 0, camY = 0, scrollSpeed = 1.4, endlessStarted = false;


<total_tokens>14991877 tokens left</total_tokens>


Edit

let camX = 0, camY = 0, scrollSpeed = 1.4, endlessStarted = false;

let camX = 0, camY = 0, camMax = 0, scrollSpeed = 1.4, endlessStarted = false;


let endlessDiff = 'medium';   // easy | medium | hard

let diffSel = 1;              // index do DIFF_KEYS

let world, levelIndex = 0, lives = 3, coinsCollected = 0, score = 0, distance = 0;

-let camX = 0, camY = 0, scrollSpeed = 1.4, endlessStarted = false;

+let camX = 0, camY = 0, camMax = 0, scrollSpeed = 1.4, endlessStarted = false;

let wallX = -400, wallSpeed = 1.3;          // zeď smrti v nekonečném módu


<total_tokens>14991575 tokens left</total_tokens>


396	function loadEndless(){
397	  world = new World();
398	  world.width = 0;
399	  pops = [];
400	  genCol = 0; genRow = 12;
401	  generateAhead(0);
402	  player.x = 3*TILE;
403	  player.y = 11*TILE - player.h;
404	  player.vx = player.vy = 0; player.invuln = 40;
405	  camX = 0; camY = 0;
406	  scrollSpeed = 1.4;
407	  wallSpeed = (ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium).wallBase;
408	  wallX = -TILE*12;
409	  endlessStarted = false;
410	}
411

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


<total_tokens>14991390 tokens left</total_tokens>


  camX = 0; camY = 0;
  scrollSpeed = 1.4;

  camX = 0; camY = 0; camMax = 0;
  scrollSpeed = 1.4;


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

  camX = 0; camY = 0;
  scrollSpeed = 1.4;

  camX = 0; camY = 0; camMax = 0;
  scrollSpeed = 1.4;

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

wallSpeed = (ENDLESS_DIFF[endlessDiff] || ENDLESS_DIFF.medium).wallBase;


<total_tokens>14990906 tokens left</total_tokens>


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

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


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

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


<total_tokens>14990599 tokens left</total_tokens>


  // varovné blikání, když je zeď blízko
  if (mode === 'endless' && state === 'play' && endlessStarted){
    const gap = player.x - wallX;

  // varovné blikání, když je zeď blízko
  if (mode === 'endless' && state === 'play' && endlessStarted && wallOn()){
    const gap = player.x - wallX;


  // varovné blikání, když je zeď blízko
  if (mode === 'endless' && state === 'play' && endlessStarted){
    const gap = player.x - wallX;

  // varovné blikání, když je zeď blízko
  if (mode === 'endless' && state === 'play' && endlessStarted && wallOn()){
    const gap = player.x - wallX;

-  if (mode === 'endless' && state === 'play' && endlessStarted){

+  if (mode === 'endless' && state === 'play' && endlessStarted && wallOn()){


<total_tokens>14990267 tokens left</total_tokens>


Bash

grep -n "jeden život, hraj na vzdálenost\|Obtížnost: " /home/tonda/hra.html

Find menu endless description


1286:  menuOption('2', 'Nekonečný běh', 'Obtížnost: ' + ENDLESS_DIFF[endlessDiff].name + '   ·   jeden život, hraj na vzdálenost', MENU.opt2Y, menuSel === 1);


<total_tokens>14989665 tokens left</total_tokens>


Edit

menuOption('2', 'Nekonečný běh', 'Obtížnost: ' + ENDLESS_DIFF[endlessDiff].name + '   ·   jeden život, hraj na vzdálenost', MENU.opt2Y, menuSel === 1);

menuOption('2', 'Nekonečný běh', 'Obtížnost: ' + ENDLESS_DIFF[endlessDiff].name + (wallOn() ? '   ·   utíkej před zdí' : '   ·   bez zdi, v klidu') + '   ·   jeden život', MENU.opt2Y, menuSel === 1);


ctx.fillText('Nejdál   ·   Lehká ' + bestDist.easy + ' m    ·    Střední ' + bestDist.medium + ' m    ·    Těžká ' + bestDist.hard + ' m', W/2, 176);

menuOption('1', 'Příběhové levely', LEVELS.length + ' ručně navržených levelů s cílem a životy', MENU.opt1Y, menuSel === 0);

-  menuOption('2', 'Nekonečný běh', 'Obtížnost: ' + ENDLESS_DIFF[endlessDiff].name + '   ·   jeden život, hraj na vzdálenost', MENU.opt2Y, menuSel === 1);

+  menuOption('2', 'Nekonečný běh', 'Obtížnost: ' + ENDLESS_DIFF[endlessDiff].name + (wallOn() ? '   ·   utíkej před zdí' : '   ·   bez zdi, v klidu') + '   ·   jeden život', MENU.opt2Y, menuSel === 1);


<total_tokens>14989188 tokens left</total_tokens>


Bash

Bracket balance + serve check


{ } 243 243
( ) 995 995
[ ] 169 169
4


<total_tokens>14988470 tokens left</total_tokens>


Hotovo. Zeď smrti je teď vázaná na obtížnost přes nový příznak `wall` v `ENDLESS_DIFF` (`easy: false`, `medium/hard: true`), přes helper `wallOn()`.

Na **Lehké** obtížnosti:
- zeď se vůbec nehýbe, nekreslí se, nezabíjí a nebliká varování
- kamera se místo toho jen posouvá s hráčem doprava a **nevrací se zpět** (`camMax`) — aby hráč nezacouval do už zahozené části světa
- vzdálenost/skóre se počítá dál z pozice kamery

Na **Střední** a **Těžké** vše zůstává jako dřív.

V menu se u nekonečného běhu podle obtížnosti píše „utíkej před zdí" / „bez zdi, v klidu".


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


<total_tokens>15000000 tokens left</total_tokens>


818	    justUnlocked = SKINS.filter(k => k.diff === d && k.dist > prev && k.dist <= distance).map(k => k.name);
819	  }
820	}
821
822	function startGame(m){
823	  unlockAudio();
824	  mode = m;
825	  lives = m === 'endless' ? 1 : 3;   // nekonečný běh: jeden život
826	  coinsCollected = 0; score = 0; distance = 0; shake = 0;
827	  shopSel = 0; pops = [];
828	  newRecord = false; justUnlocked = [];
829	  SHOP.forEach(it => it.bought = 0);

    justUnlocked = SKINS.filter(k => k.diff === d && k.dist > prev && k.dist <= distance).map(k => k.name);
  }
}

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


<total_tokens>14997041 tokens left</total_tokens>


Edit

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

  // nekonečný běh: Těžká = jeden život, Lehká/Střední = 3 životy jako v příběhu
  lives = m === 'endless' ? (endlessDiff === 'hard' ? 1 : 3) : 3;


  // nekonečný běh: Těžká = jeden život, Lehká/Střední = 3 životy jako v příběhu
  lives = m === 'endless' ? (endlessDiff === 'hard' ? 1 : 3) : 3;

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

+  // nekonečný běh: Těžká = jeden život, Lehká/Střední = 3 životy jako v příběhu

+  lives = m === 'endless' ? (endlessDiff === 'hard' ? 1 : 3) : 3;

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


<total_tokens>14996532 tokens left</total_tokens>


1135	  ctx.fill();
1136	}
1137
1138	function drawHUD(){
1139	  ctx.save();
1140	  const endless = mode === 'endless';
1141	  if (!endless){                       // životy se v nekonečném běhu nezobrazují
1142	    const slots = Math.min(Math.max(3, lives), MAX_LIVES);
1143	    for (let i = 0; i < slots; i++){
1144	      ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';
1145	      roundRect(16 + i * 20, 16, 14, 14, 4); ctx.fill();
1146	    }
1147	  }
1148	  ctx.fillStyle = COL.coin;
1149	  ctx.font = '600 15px system-ui, sans-serif';
1150	  ctx.textAlign = 'left';
1151	  ctx.fillText('● ' + coinsCollected, 16, endless ? 28 : 54);
1152	  ctx.textAlign = 'right';
1153	  if (mode === 'levels'){
1154	    ctx.fillText(LEVELS[levelIndex].name + '  ·  ' + (levelIndex + 1) + '/' + LEVELS.length, W - 16, 26);
1155	  } else {
1156	    ctx.fillText(distance + ' m', W - 16, 26);

  ctx.fill();
}

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


<total_tokens>14996346 tokens left</total_tokens>


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

  const endless = mode === 'endless';
  // životy: vždy v příběhu; v nekonečném běhu jen když jich je víc než jeden (Lehká/Střední)
  const showLives = !endless || lives > 1 || endlessDiff !== 'hard';
  if (showLives){
    const slots = Math.min(Math.max(3, lives), MAX_LIVES);
    for (let i = 0; i < slots; i++){
      ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';
      roundRect(16 + i * 20, 16, 14, 14, 4); ctx.fill();
    }
  }
  ctx.fillStyle = COL.coin;
  ctx.font = '600 15px system-ui, sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('● ' + coinsCollected, 16, showLives ? 54 : 28);


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

  const endless = mode === 'endless';
  // životy: vždy v příběhu; v nekonečném běhu jen když jich je víc než jeden (Lehká/Střední)
  const showLives = !endless || lives > 1 || endlessDiff !== 'hard';
  if (showLives){
    const slots = Math.min(Math.max(3, lives), MAX_LIVES);
    for (let i = 0; i < slots; i++){
      ctx.fillStyle = i < lives ? COL.enemy : '#333a4f';
      roundRect(16 + i * 20, 16, 14, 14, 4); ctx.fill();
    }
  }
  ctx.fillStyle = COL.coin;
  ctx.font = '600 15px system-ui, sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('● ' + coinsCollected, 16, showLives ? 54 : 28);

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

+  // životy: vždy v příběhu; v nekonečném běhu jen když jich je víc než jeden (Lehká/Střední)

+  const showLives = !endless || lives > 1 || endlessDiff !== 'hard';

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

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

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


<total_tokens>14995139 tokens left</total_tokens>


(wallOn() ? '   ·   utíkej před zdí' : '   ·   bez zdi, v klidu') + '   ·   jeden život', MENU.opt2Y, menuSel === 1);

(wallOn() ? '   ·   utíkej před zdí' : '   ·   bez zdi, v klidu') + (endlessDiff === 'hard' ? '   ·   jeden život' : '   ·   3 životy'), MENU.opt2Y, menuSel === 1);


ctx.fillText('Nejdál   ·   Lehká ' + bestDist.easy + ' m    ·    Střední ' + bestDist.medium + ' m    ·    Těžká ' + bestDist.hard + ' m', W/2, 176);

menuOption('1', 'Příběhové levely', LEVELS.length + ' ručně navržených levelů s cílem a životy', MENU.opt1Y, menuSel === 0);

-  menuOption('2', 'Nekonečný běh', 'Obtížnost: ' + ENDLESS_DIFF[endlessDiff].name + (wallOn() ? '   ·   utíkej před zdí' : '   ·   bez zdi, v klidu') + '   ·   jeden život', MENU.opt2Y, menuSel === 1);

+  menuOption('2', 'Nekonečný běh', 'Obtížnost: ' + ENDLESS_DIFF[endlessDiff].name + (wallOn() ? '   ·   utíkej před zdí' : '   ·   bez zdi, v klidu') + (endlessDiff === 'hard' ? '   ·   jeden život' : '   ·   3 životy'), MENU.opt2Y, menuSel === 1);


<total_tokens>14994629 tokens left</total_tokens>