Guest User

Super Mini Mario

a guest
Sep 1st, 2026
1,084
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
HTML 25.12 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Super Mario Mini · one short level</title>
  7. <style>
  8.   html, body { margin: 0; height: 100%; background: #10131a; }
  9.   body { display: flex; align-items: center; justify-content: center; font-family: 'Courier New', monospace; }
  10.   #wrap { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 12px; }
  11.   canvas { border: 4px solid #2a2f3a; border-radius: 6px; box-shadow: 0 14px 44px rgba(0,0,0,.55); max-width: 96vw; height: auto; }
  12.   #help { color: #8a93a6; font-size: 13px; letter-spacing: 1px; }
  13.   #help b { color: #cdd5e4; }
  14. </style>
  15. </head>
  16. <body>
  17. <div id="wrap">
  18.   <canvas id="game" width="960" height="480"></canvas>
  19.   <div id="help"><b>&larr;/&rarr; or A/D</b> move &nbsp;·&nbsp; <b>SPACE / &uarr; / W</b> jump &nbsp;·&nbsp; <b>R</b> restart &nbsp;·&nbsp; <b>M</b> mute</div>
  20. </div>
  21. <script>
  22. 'use strict';
  23. /* ================================================================
  24.    SUPER MARIO MINI — one short level, fully self-contained
  25.    ================================================================ */
  26. const canvas = document.getElementById('game');
  27. const ctx = canvas.getContext('2d');
  28. const TILE = 32, VW = canvas.width, VH = canvas.height;
  29. const W = 100, H = 15, LVL_W = W * TILE, GROUND_TOP = 14 * TILE;
  30.  
  31. // tile codes
  32. const G = 1, BRICK = 2, Q = 3, USED = 4, PIPE = 5, STAIR = 6, COIN = 7;
  33.  
  34. // physics (px, px/s, px/s^2) — jump reaches ~5 tiles, flight crosses ~5.5 tiles
  35. const GRAV = 2600, JUMP_V = 912, MAX_RUN = 251;
  36.  
  37. let grid, goombas, popups, particles, texts, mario;
  38. let camX = 0, time = 150, score = 0, coins = 0, lives = 3;
  39. let state = 'title', deathT = 0, winT = 0, flagY = 160;
  40. let tGlobal = 0, paused = false, jumpBuffered = 0, coyote = 0, combo = 0, muted = false;
  41.  
  42. const JUMP_KEYS = new Set(['Space', 'ArrowUp', 'KeyW']);
  43. const keys = {};
  44. const pad = (n, w) => String(Math.max(0, n | 0)).padStart(w, '0');
  45.  
  46. /* ------------------------------ audio ------------------------------ */
  47. let actx = null;
  48. function audio() {
  49.   if (!actx) { const AC = window.AudioContext || window.webkitAudioContext; if (AC) actx = new AC(); }
  50.   if (actx && actx.state === 'suspended') actx.resume();
  51. }
  52. function tone(f0, f1, dur, type, vol, delay) {
  53.   if (muted || !actx) return;
  54.   type = type || 'square'; vol = vol || 0.12; delay = delay || 0;
  55.   const t = actx.currentTime + delay;
  56.   const o = actx.createOscillator(), g = actx.createGain();
  57.   o.type = type;
  58.   o.frequency.setValueAtTime(f0, t);
  59.   if (f1) o.frequency.exponentialRampToValueAtTime(Math.max(1, f1), t + dur);
  60.   g.gain.setValueAtTime(vol, t);
  61.   g.gain.exponentialRampToValueAtTime(0.001, t + dur);
  62.   o.connect(g); g.connect(actx.destination);
  63.   o.start(t); o.stop(t + dur + 0.03);
  64. }
  65. const sfx = {
  66.   jump()  { tone(300, 720, 0.18, 'square', 0.09); },
  67.   coin()  { tone(988, 0, 0.07, 'square', 0.11); tone(1319, 0, 0.28, 'square', 0.11, 0.07); },
  68.   stomp() { tone(320, 70, 0.16, 'square', 0.13); },
  69.   bump()  { tone(130, 55, 0.10, 'square', 0.13); },
  70.   brk()   { tone(240, 55, 0.22, 'sawtooth', 0.11); },
  71.   die()   { tone(494, 0, 0.12, 'square', 0.12); tone(370, 0, 0.12, 'square', 0.12, 0.13); tone(262, 0, 0.35, 'square', 0.12, 0.26); },
  72.   win()   { [523, 659, 784, 1047, 1319, 1568].forEach((f, i) => tone(f, 0, 0.16, 'square', 0.11, i * 0.11)); }
  73. };
  74.  
  75. /* --------------------------- level building --------------------------- */
  76. function buildLevel() {
  77.   grid = Array.from({ length: H }, () => new Array(W).fill(0));
  78.   const set = (x, y, v) => { grid[y][x] = v; };
  79.   const row = (a, b, y, v) => { for (let x = a; x <= b; x++) grid[y][x] = v; };
  80.  const pipe = (c, h) => { for (let i = 0; i < h; i++) { set(c, 13 - i, PIPE); set(c + 1, 13 - i, PIPE); } };
  81.  
  82.  // ground (two gaps: 3 wide at cols 38-40, 4 wide at cols 58-61)
  83.  row(0, 37, 14, G);
  84.  row(41, 57, 14, G);
  85.  row(62, 99, 14, G);
  86.  
  87.  // pipes
  88.  pipe(26, 2); pipe(44, 3); pipe(70, 4); pipe(84, 2);
  89.  
  90.  // blocks
  91.  set(16, 10, Q);
  92.  set(18, 10, BRICK); set(19, 10, Q); set(20, 10, BRICK); set(21, 10, Q); set(22, 10, BRICK);
  93.  set(56, 10, Q); set(56, 8, Q); set(66, 10, Q);
  94.  
  95.  // staircase (heights 1..4)
  96.  for (let i = 0; i < 4; i++) for (let j = 0; j <= i; j++) set(78 + i, 13 - j, STAIR);
  97.  
  98.  // coins
  99.  [[18,9],[19,9],[20,9],[21,9],[22,9],
  100.   [38,8],[39,8],[40,8],
  101.   [58,8],[59,8],[60,8],[61,8],
  102.   [46,9],[47,9],[48,9],
  103.   [72,9],[73,9],[74,9]
  104.  ].forEach(([x, y]) => set(x, y, COIN));
  105. }
  106. const ENEMY_COLS = [12, 30, 47, 51, 66, 82];
  107.  
  108. function resetLevel() {
  109.   buildLevel();
  110.   goombas = ENEMY_COLS.map(c => ({ x: c * TILE + 2, y: GROUND_TOP - 26, w: 28, h: 26, dir: -1, vy: 0, dead: false, squashT: 0 }));
  111.   popups = []; particles = []; texts = [];
  112.   mario = { x: 2 * TILE, y: GROUND_TOP - 30, w: 24, h: 30, vx: 0, vy: 0, onGround: false, facing: 1, anim: 0 };
  113.   camX = 0; time = 150; jumpBuffered = 0; coyote = 0; combo = 0;
  114.   flagY = 160;
  115. }
  116. function startGame() { lives = 3; score = 0; coins = 0; resetLevel(); state = 'playing'; paused = false; }
  117.  
  118. /* ------------------------------ helpers ------------------------------ */
  119. function solid(tx, ty) {
  120.   if (tx < 0 || tx >= W) return true;
  121.   if (ty < 0 || ty >= H) return false;
  122.   const c = grid[ty][tx];
  123.   return c === G || c === BRICK || c === Q || c === USED || c === PIPE || c === STAIR;
  124. }
  125. function rectHit(a, b) { return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; }
  126.  
  127. function collide(e, dt, isMario) {
  128.   // horizontal
  129.   e.x += e.vx * dt;
  130.   if (e.x < 0) { e.x = 0; if (e.vx < 0) e.vx = 0; }
  131.  if (e.x > LVL_W - e.w) { e.x = LVL_W - e.w; if (e.vx > 0) e.vx = 0; }
  132.   let y0 = Math.floor(e.y / TILE), y1 = Math.floor((e.y + e.h - 0.01) / TILE);
  133.   if (e.vx > 0) {
  134.     const tx = Math.floor((e.x + e.w - 0.01) / TILE);
  135.     for (let ty = y0; ty <= y1; ty++) if (solid(tx, ty)) { e.x = tx * TILE - e.w; e.vx = 0; break; }
  136.  } else if (e.vx < 0) {
  137.    const tx = Math.floor(e.x / TILE);
  138.    for (let ty = y0; ty <= y1; ty++) if (solid(tx, ty)) { e.x = (tx + 1) * TILE; e.vx = 0; break; }
  139.  }
  140.  // vertical
  141.  const prevVy = e.vy;
  142.  e.y += e.vy * dt;
  143.  e.onGround = false;
  144.  const x0 = Math.floor(e.x / TILE), x1 = Math.floor((e.x + e.w - 0.01) / TILE);
  145.  if (e.vy > 0) {
  146.     const ty = Math.floor((e.y + e.h - 0.01) / TILE);
  147.     for (let tx = x0; tx <= x1; tx++) if (solid(tx, ty)) { e.y = ty * TILE - e.h; e.vy = 0; e.onGround = true; break; }
  148.  } else if (e.vy < 0) {
  149.    const ty = Math.floor(e.y / TILE);
  150.    for (let tx = x0; tx <= x1; tx++) if (solid(tx, ty)) { e.y = (ty + 1) * TILE; e.vy = 0; if (isMario) hitBlock(tx, ty); break; }
  151.  }
  152.  if (isMario && e.onGround && prevVy > 540) puff(e);
  153. }
  154.  
  155. /* --------------------------- gameplay actions --------------------------- */
  156. function addText(x, y, str) { texts.push({ x, y, str, t: 0 }); }
  157. function hitBlock(tx, ty) {
  158.   const c = grid[ty][tx];
  159.   if (c === Q) {
  160.     grid[ty][tx] = USED;
  161.     coins++; score += 100;
  162.     popups.push({ x: tx * TILE + TILE / 2, y: ty * TILE - 6, vy: -330, t: 0 });
  163.     addText(tx * TILE + 16, ty * TILE - 10, '+100');
  164.     sfx.coin();
  165.   } else if (c === BRICK) {
  166.     grid[ty][tx] = 0;
  167.     score += 50;
  168.     for (let i = 0; i < 6; i++) particles.push({
  169.      x: tx * TILE + 8 + (i % 3) * 8, y: ty * TILE + 8 + ((i / 3) | 0) * 10,
  170.      vx: (Math.random() - 0.5) * 280, vy: -160 - Math.random() * 160, t: 0, color: '#b5502a', size: 7
  171.    });
  172.    addText(tx * TILE + 16, ty * TILE - 10, '+50');
  173.    sfx.brk();
  174.  } else {
  175.    sfx.bump();
  176.  }
  177. }
  178. function sparkle(tx, ty) {
  179.  const cx = tx * TILE + 16, cy = ty * TILE + 16;
  180.  for (let i = 0; i < 5; i++) {
  181.    const a = Math.PI * 2 * i / 5;
  182.    particles.push({ x: cx, y: cy, vx: Math.cos(a) * 130, vy: Math.sin(a) * 130 - 60, t: 0, color: '#ffd54f', size: 4 });
  183.  }
  184. }
  185. function puff(m) {
  186.  for (let i = 0; i < 4; i++) particles.push({
  187.    x: m.x + 4 + i * 6, y: m.y + m.h - 2, vx: (i - 1.5) * 45, vy: -50 - Math.random() * 50, t: 0, color: '#e8e0d0', size: 5
  188.  });
  189. }
  190. function die() {
  191.  if (state !== 'playing') return;
  192.  state = 'dying'; deathT = 0;
  193.  mario.vx = 0; mario.vy = -620;
  194.  sfx.die();
  195. }
  196. function startWin() {
  197.  if (state !== 'playing') return;
  198.  state = 'win'; winT = 0;
  199.  score += 1000 + Math.ceil(time) * 10;
  200.  addText(mario.x + 12, mario.y - 14, '+' + (1000 + Math.ceil(time) * 10));
  201.  mario.vx = 0; mario.vy = 0;
  202.  mario.x = 88 * TILE + 16 - mario.w / 2;
  203.  flagY = Math.max(160, mario.y + 4);
  204.  sfx.win();
  205. }
  206.  
  207. /* ------------------------------- update ------------------------------- */
  208. function update(dt) {
  209.  if (state === 'title') return;
  210.  
  211.  if (state === 'dying') {
  212.    deathT += dt;
  213.    if (deathT > 0.35) { mario.vy += GRAV * dt; mario.y += mario.vy * dt; }
  214.     if (deathT > 1.5) {
  215.       lives--;
  216.       if (lives <= 0) state = 'gameover';
  217.      else { resetLevel(); state = 'playing'; }
  218.    }
  219.    return;
  220.  }
  221.  
  222.  if (state === 'win') {
  223.    winT += dt;
  224.    const targetY = GROUND_TOP - mario.h;
  225.    if (mario.y < targetY) mario.y = Math.min(targetY, mario.y + 210 * dt);
  226.    flagY = Math.min(400, flagY + 260 * dt);
  227.    if (winT > 1.9) state = 'clear';
  228.     return;
  229.   }
  230.  
  231.   if (state !== 'playing') return;
  232.  
  233.   /* --- mario movement --- */
  234.   const L = keys['ArrowLeft'] || keys['KeyA'];
  235.   const R = keys['ArrowRight'] || keys['KeyD'];
  236.   const dir = (R ? 1 : 0) - (L ? 1 : 0);
  237.   const ACC = mario.onGround ? 2000 : 1300;
  238.   if (dir !== 0) {
  239.     mario.facing = dir;
  240.     mario.vx += dir * ACC * dt;
  241.     if (Math.abs(mario.vx) > MAX_RUN) mario.vx = Math.sign(mario.vx) * MAX_RUN;
  242.   } else {
  243.     const fr = (mario.onGround ? 2400 : 300) * dt;
  244.     if (Math.abs(mario.vx) <= fr) mario.vx = 0;
  245.    else mario.vx -= Math.sign(mario.vx) * fr;
  246.  }
  247.  
  248.  // jumping (with coyote time + input buffer + variable height)
  249.  jumpBuffered -= dt;
  250.  if (mario.onGround) coyote = 0.09; else coyote -= dt;
  251.  if (jumpBuffered > 0 && coyote > 0) {
  252.    mario.vy = -JUMP_V;
  253.     coyote = 0; jumpBuffered = 0;
  254.     sfx.jump();
  255.   }
  256.   mario.vy += GRAV * dt;
  257.   if (mario.vy > 950) mario.vy = 950;
  258.  
  259.   collide(mario, dt, true);
  260.   mario.anim += Math.abs(mario.vx) * dt;
  261.   if (mario.onGround) combo = 0;
  262.  
  263.   // fell into a pit
  264.   if (mario.y > VH + 80) { die(); return; }
  265.  
  266.   // collect coins
  267.   {
  268.     const x0 = Math.floor(mario.x / TILE), x1 = Math.floor((mario.x + mario.w - 0.01) / TILE);
  269.     const y0 = Math.floor(mario.y / TILE), y1 = Math.floor((mario.y + mario.h - 0.01) / TILE);
  270.     for (let ty = y0; ty <= y1; ty++) for (let tx = x0; tx <= x1; tx++) {
  271.      if (tx < 0 || tx >= W || ty < 0 || ty >= H) continue;
  272.       if (grid[ty][tx] === COIN) {
  273.         grid[ty][tx] = 0;
  274.         coins++; score += 100;
  275.         sparkle(tx, ty);
  276.         sfx.coin();
  277.       }
  278.     }
  279.   }
  280.  
  281.   /* --- goombas --- */
  282.   for (const g of goombas) {
  283.     if (g.dead) continue;
  284.     g.x += g.dir * 70 * dt;
  285.     if (g.x < 0) { g.x = 0; g.dir = 1; }
  286.    if (g.x > LVL_W - g.w) { g.x = LVL_W - g.w; g.dir = -1; }
  287.     const ax = g.dir > 0 ? g.x + g.w : g.x;
  288.     const tx = Math.floor(ax / TILE);
  289.     const ty0 = Math.floor((g.y + 6) / TILE), ty1 = Math.floor((g.y + g.h - 1) / TILE);
  290.     if (solid(tx, ty0) || solid(tx, ty1)) g.dir *= -1;
  291.     // turn around at pit edges instead of falling in
  292.     const footTy = Math.floor((g.y + g.h) / TILE);
  293.     if (footTy < H && !solid(Math.floor((g.dir > 0 ? g.x + g.w + 2 : g.x - 2) / TILE), footTy)) g.dir *= -1;
  294.     g.vy += GRAV * dt;
  295.     if (g.vy > 950) g.vy = 950;
  296.     g.y += g.vy * dt;
  297.     if (g.vy > 0) {
  298.       const ty = Math.floor((g.y + g.h - 0.01) / TILE);
  299.       const lx = Math.floor(g.x / TILE), rx = Math.floor((g.x + g.w - 0.01) / TILE);
  300.       if (solid(lx, ty) || solid(rx, ty)) { g.y = ty * TILE - g.h; g.vy = 0; }
  301.     }
  302.     if (g.y > VH + 80) { g.dead = true; continue; }
  303.  
  304.     if (rectHit(mario, g)) {
  305.       if (mario.vy > 0 && (mario.y + mario.h - g.y) < 26) {
  306.        g.dead = true; g.squashT = 0;
  307.         combo++;
  308.         score += 100 * combo;
  309.         addText(g.x + 14, g.y - 8, '+' + (100 * combo));
  310.         mario.vy = -480;
  311.         sfx.stomp();
  312.       } else {
  313.         die();
  314.       }
  315.     }
  316.   }
  317.   goombas = goombas.filter(g => {
  318.     if (!g.dead) return true;
  319.     if (g.y > VH + 60) return false; // fell into a pit
  320.     g.squashT += dt;
  321.     return g.squashT < 0.5;
  322.  });
  323.  
  324.  /* --- effects --- */
  325.  for (const p of popups) { p.t += dt; p.vy += 1800 * dt; p.y += p.vy * dt; }
  326.  popups = popups.filter(p => p.t < 0.45);
  327.  for (const p of particles) { p.t += dt; p.vy += 2000 * dt; p.x += p.vx * dt; p.y += p.vy * dt; }
  328.  particles = particles.filter(p => p.t < 0.8);
  329.  for (const t of texts) { t.t += dt; t.y -= 34 * dt; }
  330.  texts = texts.filter(t => t.t < 0.9);
  331.  
  332.  /* --- flag --- */
  333.  if (mario.x + mario.w > 88 * TILE + 10) { startWin(); return; }
  334.  
  335.   /* --- timer & camera --- */
  336.  time -= dt;
  337.   if (time <= 0) { time = 0; die(); return; }
  338.  camX = Math.max(0, Math.min(LVL_W - VW, mario.x + 12 - VW / 2));
  339. }
  340.  
  341. /* ------------------------------- input ------------------------------- */
  342. function handleKey(c) {
  343.  if (c === 'KeyM') { muted = !muted; return; }
  344.  if ((c === 'Enter' || c === 'KeyR') && (state === 'title' || state === 'gameover' || state === 'clear')) { startGame(); return; }
  345.  if (c === 'KeyR' && state === 'playing') { startGame(); return; }
  346.  if (paused && (c === 'Enter' || c === 'Space')) { paused = false; }
  347. }
  348. window.addEventListener('keydown', e => {
  349.   if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Space'].includes(e.code)) e.preventDefault();
  350.   if (e.repeat) { keys[e.code] = true; return; }
  351.   keys[e.code] = true;
  352.   audio();
  353.   handleKey(e.code);
  354.   if (JUMP_KEYS.has(e.code)) jumpBuffered = 0.12;
  355. });
  356. window.addEventListener('keyup', e => {
  357.   keys[e.code] = false;
  358.   if (JUMP_KEYS.has(e.code) && state === 'playing' && mario.vy < -320) mario.vy = -320;
  359. });
  360. window.addEventListener('blur', () => { if (state === 'playing') paused = true; });
  361. canvas.addEventListener('click', () => { paused = false; audio(); });
  362.  
  363. /* ------------------------------- render ------------------------------- */
  364. function drawBackground() {
  365.   const sky = ctx.createLinearGradient(0, 0, 0, VH);
  366.   sky.addColorStop(0, '#5c94fc');
  367.   sky.addColorStop(1, '#9fd4ff');
  368.   ctx.fillStyle = sky;
  369.   ctx.fillRect(0, 0, VW, VH);
  370.  
  371.   // clouds (parallax)
  372.   ctx.fillStyle = 'rgba(255,255,255,0.95)';
  373.   const co = camX * 0.4;
  374.   for (let k = 0; k < 3; k++) for (let i = 0; i < 4; i++) {
  375.    const x = k * 1400 + i * 330 + ((i * 137 + k * 53) % 90) - co;
  376.    if (x < -140 || x > VW + 40) continue;
  377.     const y = 46 + ((i * 83 + k * 61) % 110);
  378.     const s = 0.8 + (i % 2) * 0.5;
  379.     ctx.beginPath();
  380.     ctx.arc(x, y, 16 * s, 0, 7);
  381.     ctx.arc(x + 20 * s, y - 9 * s, 19 * s, 0, 7);
  382.     ctx.arc(x + 42 * s, y, 16 * s, 0, 7);
  383.     ctx.fill();
  384.   }
  385.  
  386.   // hills (parallax)
  387.   ctx.fillStyle = '#3fa04a';
  388.   const ho = camX * 0.7;
  389.   for (let k = 0; k < 4; k++) for (let i = 0; i < 2; i++) {
  390.    const x = k * 1000 + i * 430 + ((i * 97 + k * 41) % 120) - ho;
  391.    const r = 85 + (i % 2) * 55;
  392.    if (x < -r * 2 || x > VW + r) continue;
  393.     ctx.beginPath();
  394.     ctx.moveTo(x - r, GROUND_TOP);
  395.     ctx.quadraticCurveTo(x, GROUND_TOP - r * 1.7, x + r, GROUND_TOP);
  396.     ctx.fill();
  397.   }
  398.  
  399.   // bushes (screen-space, behind tiles)
  400.   ctx.fillStyle = '#4db85a';
  401.   for (const c of [4, 13, 33, 50, 63, 76, 93]) {
  402.     const x = c * TILE - camX;
  403.     if (x < -90 || x > VW + 20) continue;
  404.     ctx.beginPath();
  405.     ctx.arc(x, GROUND_TOP, 15, Math.PI, 0);
  406.     ctx.arc(x + 18, GROUND_TOP, 20, Math.PI, 0);
  407.     ctx.arc(x + 38, GROUND_TOP, 14, Math.PI, 0);
  408.     ctx.fill();
  409.   }
  410. }
  411.  
  412. function drawGround(x, y, stair) {
  413.   ctx.fillStyle = stair ? '#d8752c' : '#c2622a';
  414.   ctx.fillRect(x, y, TILE, TILE);
  415.   ctx.fillStyle = '#e8935a';
  416.   ctx.fillRect(x + 3, y + 3, TILE - 6, 2);
  417.   ctx.fillStyle = '#7a3a10';
  418.   ctx.fillRect(x, y, TILE, 3);
  419.   ctx.fillRect(x, y, 3, TILE);
  420.   ctx.fillRect(x, y + 15, TILE, 2);
  421.   ctx.fillRect(x + 15, y + 3, 2, 12);
  422.   ctx.fillRect(x + 7, y + 17, 2, 12);
  423.   ctx.fillRect(x + 23, y + 17, 2, 12);
  424. }
  425. function drawBrick(x, y) {
  426.   ctx.fillStyle = '#b5502a';
  427.   ctx.fillRect(x, y, TILE, TILE);
  428.   ctx.fillStyle = '#e07a4a';
  429.   ctx.fillRect(x + 2, y + 2, 28, 2);
  430.   ctx.fillStyle = '#5e2410';
  431.   ctx.fillRect(x, y + 15, TILE, 2);
  432.   ctx.fillRect(x, y, 2, TILE);
  433.   ctx.fillRect(x + 30, y, 2, TILE);
  434.   ctx.fillRect(x + 14, y + 2, 2, 13);
  435.   ctx.fillRect(x + 7, y + 17, 2, 13);
  436.   ctx.fillRect(x + 21, y + 17, 2, 13);
  437. }
  438. function drawQ(x, y, tx) {
  439.   ctx.fillStyle = '#8a5a00';
  440.   ctx.fillRect(x, y, TILE, TILE);
  441.   ctx.fillStyle = '#f8b800';
  442.   ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
  443.   ctx.fillStyle = '#8a5a00';
  444.   [[4, 4], [TILE - 8, 4], [4, TILE - 8], [TILE - 8, TILE - 8]].forEach(([rx, ry]) => ctx.fillRect(x + rx, y + ry, 4, 4));
  445.   ctx.fillStyle = '#fff';
  446.   ctx.font = 'bold 20px "Courier New", monospace';
  447.   ctx.textAlign = 'center';
  448.   ctx.textBaseline = 'middle';
  449.   ctx.fillText('?', x + 16, y + 18);
  450.   void tx;
  451. }
  452. function drawUsed(x, y) {
  453.   ctx.fillStyle = '#8a5a2a';
  454.   ctx.fillRect(x, y, TILE, TILE);
  455.   ctx.fillStyle = '#6b4220';
  456.   [[4, 4], [TILE - 8, 4], [4, TILE - 8], [TILE - 8, TILE - 8]].forEach(([rx, ry]) => ctx.fillRect(x + rx, y + ry, 4, 4));
  457.   ctx.strokeStyle = '#6b4220';
  458.   ctx.lineWidth = 2;
  459.   ctx.strokeRect(x + 1.5, y + 1.5, TILE - 3, TILE - 3);
  460. }
  461. function drawPipe(x, y, tx, ty) {
  462.   const isLeft = !(tx > 0 && grid[ty][tx - 1] === PIPE);
  463.   const isRight = !(tx < W - 1 && grid[ty][tx + 1] === PIPE);
  464.  const isTop = ty === 0 || grid[ty - 1][tx] !== PIPE;
  465.  ctx.fillStyle = '#2f9e3e';
  466.  ctx.fillRect(x, y, TILE, TILE);
  467.  ctx.fillStyle = '#7fd98a';
  468.  ctx.fillRect(x + (isLeft ? 6 : 10), y, 4, TILE);
  469.  if (isLeft) { ctx.fillStyle = '#1c6b2a'; ctx.fillRect(x, y, 3, TILE); }
  470.  if (isRight) { ctx.fillStyle = '#1c6b2a'; ctx.fillRect(x + TILE - 3, y, 3, TILE); }
  471.  if (isTop) {
  472.    const lw = TILE + (isLeft ? 6 : 0) + (isRight ? 6 : 0);
  473.    const lx = x - (isLeft ? 6 : 0);
  474.    ctx.fillStyle = '#2f9e3e';
  475.    ctx.fillRect(lx, y, lw, 22);
  476.    ctx.fillStyle = '#7fd98a';
  477.    ctx.fillRect(lx + 4, y, 6, 22);
  478.    ctx.strokeStyle = '#14501f';
  479.    ctx.lineWidth = 2;
  480.    ctx.strokeRect(lx + 1, y + 1, lw - 2, 20);
  481.  }
  482. }
  483. function drawCoinSprite(cx, cy, r, phase) {
  484.  const s = 0.35 + 0.65 * Math.abs(Math.sin(tGlobal * 4 + phase));
  485.  ctx.fillStyle = '#b8860b';
  486.  ctx.beginPath(); ctx.ellipse(cx, cy, r * s, r, 0, 0, 7); ctx.fill();
  487.  ctx.fillStyle = '#ffd54f';
  488.  ctx.beginPath(); ctx.ellipse(cx, cy, r * s * 0.72, r * 0.72, 0, 0, 7); ctx.fill();
  489. }
  490. function drawTiles() {
  491.  const c0 = Math.max(0, Math.floor(camX / TILE));
  492.  const c1 = Math.min(W - 1, Math.ceil((camX + VW) / TILE));
  493.  for (let ty = 0; ty < H; ty++) for (let tx = c0; tx <= c1; tx++) {
  494.    const c = grid[ty][tx];
  495.    if (!c) continue;
  496.    const x = tx * TILE - camX, y = ty * TILE;
  497.    switch (c) {
  498.      case G: drawGround(x, y, false); break;
  499.      case STAIR: drawGround(x, y, true); break;
  500.      case BRICK: drawBrick(x, y); break;
  501.      case Q: drawQ(x, y, tx); break;
  502.      case USED: drawUsed(x, y); break;
  503.      case PIPE: drawPipe(x, y, tx, ty); break;
  504.      case COIN: drawCoinSprite(x + 16, y + 16, 10, tx * 0.7); break;
  505.    }
  506.  }
  507. }
  508. function drawFlag() {
  509.  const x = 88 * TILE + 16 - camX;
  510.  if (x < -80 || x > VW + 80) return;
  511.   ctx.fillStyle = '#cfd8dc';
  512.   ctx.fillRect(x - 3, 128, 6, GROUND_TOP - 128);
  513.   ctx.fillStyle = '#90a4ae';
  514.   ctx.fillRect(x + 1, 128, 2, GROUND_TOP - 128);
  515.   ctx.fillStyle = '#ffd54f';
  516.   ctx.beginPath(); ctx.arc(x, 122, 10, 0, 7); ctx.fill();
  517.   ctx.fillStyle = '#ffb300';
  518.   ctx.beginPath(); ctx.arc(x, 122, 5, 0, 7); ctx.fill();
  519.   ctx.fillStyle = '#2e7d32';
  520.   ctx.beginPath();
  521.   ctx.moveTo(x - 3, flagY);
  522.   ctx.lineTo(x - 40, flagY + 16);
  523.   ctx.lineTo(x - 3, flagY + 32);
  524.   ctx.closePath();
  525.   ctx.fill();
  526. }
  527. function drawGoomba(g) {
  528.   const x = g.x - camX, y = g.y;
  529.   if (x < -50 || x > VW + 50) return;
  530.   if (g.dead) {
  531.     if (g.squashT > 0.5) return;
  532.     ctx.globalAlpha = Math.max(0, 1 - g.squashT * 2);
  533.     ctx.fillStyle = '#a05a2c';
  534.     ctx.fillRect(x, y + g.h - 10, g.w, 10);
  535.     ctx.globalAlpha = 1;
  536.     return;
  537.   }
  538.   ctx.fillStyle = '#4a2508';
  539.   ctx.beginPath(); ctx.ellipse(x + 7, y + g.h - 4, 7, 4, 0, 0, 7); ctx.fill();
  540.   ctx.beginPath(); ctx.ellipse(x + g.w - 7, y + g.h - 4, 7, 4, 0, 0, 7); ctx.fill();
  541.   ctx.fillStyle = '#a05a2c';
  542.   ctx.beginPath(); ctx.ellipse(x + 14, y + 12, 14, 12, 0, 0, 7); ctx.fill();
  543.   ctx.fillStyle = '#c88b5a';
  544.   ctx.beginPath(); ctx.ellipse(x + 14, y + 17, 10, 7, 0, 0, 7); ctx.fill();
  545.   ctx.fillStyle = '#fff';
  546.   ctx.beginPath(); ctx.ellipse(x + 9, y + 10, 4, 5, 0, 0, 7); ctx.fill();
  547.   ctx.beginPath(); ctx.ellipse(x + 19, y + 10, 4, 5, 0, 0, 7); ctx.fill();
  548.   ctx.fillStyle = '#222';
  549.   ctx.beginPath(); ctx.arc(x + 10, y + 11, 1.8, 0, 7); ctx.fill();
  550.   ctx.beginPath(); ctx.arc(x + 18, y + 11, 1.8, 0, 7); ctx.fill();
  551.   ctx.strokeStyle = '#222';
  552.   ctx.lineWidth = 2;
  553.   ctx.beginPath();
  554.   ctx.moveTo(x + 5, y + 4); ctx.lineTo(x + 11, y + 8);
  555.   ctx.moveTo(x + 23, y + 4); ctx.lineTo(x + 17, y + 8);
  556.   ctx.stroke();
  557. }
  558. function drawMario() {
  559.   const m = mario;
  560.   ctx.save();
  561.   ctx.translate(m.x + m.w / 2 - camX, 0);
  562.   ctx.scale(m.facing, 1);
  563.   ctx.translate(-12, 0);
  564.   const y = Math.round(m.y);
  565.   const moving = Math.abs(m.vx) > 20 && m.onGround;
  566.   const ph = moving ? Math.sin(m.anim * 0.12) : 0;
  567.  
  568.   // shoes
  569.   ctx.fillStyle = '#6d3a12';
  570.   if (!m.onGround) {
  571.     ctx.fillRect(2, y + 24, 9, 6);
  572.     ctx.fillRect(13, y + 26, 9, 4);
  573.   } else {
  574.     ctx.fillRect(2 + ph * 2, y + 25, 9, 5);
  575.     ctx.fillRect(13 - ph * 2, y + 25, 9, 5);
  576.   }
  577.   // overalls
  578.   ctx.fillStyle = '#2b58c8';
  579.   ctx.fillRect(5, y + 16, 14, 10);
  580.   ctx.fillRect(6, y + 12, 4, 5);
  581.   ctx.fillRect(14, y + 12, 4, 5);
  582.   ctx.fillStyle = '#ffd54f';
  583.   ctx.fillRect(7, y + 17, 2, 2);
  584.   ctx.fillRect(15, y + 17, 2, 2);
  585.   // shirt + arms
  586.   ctx.fillStyle = '#d8302a';
  587.   ctx.fillRect(4, y + 12, 16, 6);
  588.   // hands
  589.   ctx.fillStyle = '#fcbf92';
  590.   ctx.fillRect(2, y + 15, 4, 4);
  591.   ctx.fillRect(18, y + 15, 4, 4);
  592.   // face
  593.   ctx.fillStyle = '#fcbf92';
  594.   ctx.fillRect(6, y + 5, 12, 8);
  595.   // sideburn
  596.   ctx.fillStyle = '#5a2d0c';
  597.   ctx.fillRect(5, y + 5, 3, 4);
  598.   // eye
  599.   ctx.fillStyle = '#222';
  600.   ctx.fillRect(14, y + 6, 2, 4);
  601.   // nose
  602.   ctx.fillStyle = '#f5a86e';
  603.   ctx.fillRect(17, y + 8, 4, 4);
  604.   // mustache
  605.   ctx.fillStyle = '#4a2508';
  606.   ctx.fillRect(12, y + 11, 7, 2);
  607.   // cap
  608.   ctx.fillStyle = '#d8302a';
  609.   ctx.fillRect(5, y, 13, 5);
  610.   ctx.fillRect(12, y + 3, 10, 3);
  611.   ctx.fillStyle = '#fff';
  612.   ctx.fillRect(9, y + 1, 4, 3);
  613.   ctx.restore();
  614. }
  615. function drawEffects() {
  616.   for (const p of popups) {
  617.     ctx.globalAlpha = Math.max(0, 1 - p.t / 0.45);
  618.     drawCoinSprite(p.x - camX, p.y, 9, 0);
  619.   }
  620.   ctx.globalAlpha = 1;
  621.   for (const p of particles) {
  622.     ctx.globalAlpha = Math.max(0, 1 - p.t * 1.3);
  623.     ctx.fillStyle = p.color;
  624.     ctx.fillRect(p.x - camX - p.size / 2, p.y - p.size / 2, p.size, p.size);
  625.   }
  626.   ctx.globalAlpha = 1;
  627.   ctx.font = 'bold 14px "Courier New", monospace';
  628.   ctx.textAlign = 'center';
  629.   ctx.textBaseline = 'alphabetic';
  630.   for (const t of texts) {
  631.     ctx.globalAlpha = Math.max(0, 1 - t.t / 0.9);
  632.     ctx.fillStyle = '#fff';
  633.     ctx.fillText(t.str, t.x - camX, t.y);
  634.   }
  635.   ctx.globalAlpha = 1;
  636. }
  637. function drawHUD() {
  638.   ctx.fillStyle = 'rgba(10,20,40,0.35)';
  639.   ctx.fillRect(0, 0, VW, 34);
  640.   ctx.fillStyle = '#fff';
  641.   ctx.font = 'bold 15px "Courier New", monospace';
  642.   ctx.textAlign = 'left';
  643.   ctx.textBaseline = 'middle';
  644.   const s = ' SCORE ' + pad(score, 6) + '   COINS ×' + pad(coins, 2) + '   WORLD 1-1   TIME ' + Math.ceil(time) + '   LIVES ×' + lives + (muted ? '   MUTED' : '');
  645.   ctx.fillText(s, 8, 18);
  646. }
  647. function panel(title, lines) {
  648.   ctx.fillStyle = 'rgba(0,0,20,0.55)';
  649.   ctx.fillRect(0, 0, VW, VH);
  650.   ctx.textAlign = 'center';
  651.   ctx.textBaseline = 'middle';
  652.   ctx.fillStyle = '#ffd54f';
  653.   ctx.font = 'bold 46px "Courier New", monospace';
  654.   ctx.fillText(title, VW / 2, 165);
  655.   ctx.fillStyle = '#fff';
  656.   ctx.font = 'bold 20px "Courier New", monospace';
  657.   lines.forEach((l, i) => ctx.fillText(l, VW / 2, 225 + i * 38));
  658. }
  659. function drawOverlays() {
  660.   if (state === 'title') {
  661.     panel('SUPER MARIO MINI', [
  662.       'One short level · 3 lives · 150 seconds',
  663.       'Stomp goombas · grab coins · break bricks · reach the flag!',
  664.       'Press ENTER to start    (M = mute)'
  665.     ]);
  666.   } else if (state === 'gameover') {
  667.     panel('GAME OVER', [
  668.       'Final score  ' + pad(score, 6),
  669.       'Press ENTER to try again'
  670.     ]);
  671.   } else if (state === 'clear') {
  672.     panel('COURSE CLEAR!', [
  673.       'Final score  ' + pad(score, 6),
  674.       'Press ENTER to play again'
  675.     ]);
  676.   } else if (paused) {
  677.     panel('PAUSED', ['Click the game or press ENTER to resume']);
  678.   }
  679. }
  680. function draw() {
  681.   drawBackground();
  682.   drawTiles();
  683.   drawFlag();
  684.   drawEffects();
  685.   for (const g of goombas) drawGoomba(g);
  686.   drawMario();
  687.   drawHUD();
  688.   drawOverlays();
  689. }
  690.  
  691. /* -------------------------------- loop -------------------------------- */
  692. let last = 0;
  693. function frame(ts) {
  694.   requestAnimationFrame(frame);
  695.   const dt = Math.min(0.033, (ts - last) / 1000 || 0);
  696.   last = ts;
  697.   if (!paused) {
  698.     tGlobal += dt;
  699.     update(dt);
  700.   }
  701.   draw();
  702. }
  703. resetLevel();
  704. requestAnimationFrame(frame);
  705. </script>
  706. </body>
  707. </html>
  708.  
Advertisement
Add Comment
Please, Sign In to add comment