Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>Super Mario Mini · one short level</title>
- <style>
- html, body { margin: 0; height: 100%; background: #10131a; }
- body { display: flex; align-items: center; justify-content: center; font-family: 'Courier New', monospace; }
- #wrap { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 12px; }
- canvas { border: 4px solid #2a2f3a; border-radius: 6px; box-shadow: 0 14px 44px rgba(0,0,0,.55); max-width: 96vw; height: auto; }
- #help { color: #8a93a6; font-size: 13px; letter-spacing: 1px; }
- #help b { color: #cdd5e4; }
- </style>
- </head>
- <body>
- <div id="wrap">
- <canvas id="game" width="960" height="480"></canvas>
- <div id="help"><b>←/→ or A/D</b> move · <b>SPACE / ↑ / W</b> jump · <b>R</b> restart · <b>M</b> mute</div>
- </div>
- <script>
- 'use strict';
- /* ================================================================
- SUPER MARIO MINI — one short level, fully self-contained
- ================================================================ */
- const canvas = document.getElementById('game');
- const ctx = canvas.getContext('2d');
- const TILE = 32, VW = canvas.width, VH = canvas.height;
- const W = 100, H = 15, LVL_W = W * TILE, GROUND_TOP = 14 * TILE;
- // tile codes
- const G = 1, BRICK = 2, Q = 3, USED = 4, PIPE = 5, STAIR = 6, COIN = 7;
- // physics (px, px/s, px/s^2) — jump reaches ~5 tiles, flight crosses ~5.5 tiles
- const GRAV = 2600, JUMP_V = 912, MAX_RUN = 251;
- let grid, goombas, popups, particles, texts, mario;
- let camX = 0, time = 150, score = 0, coins = 0, lives = 3;
- let state = 'title', deathT = 0, winT = 0, flagY = 160;
- let tGlobal = 0, paused = false, jumpBuffered = 0, coyote = 0, combo = 0, muted = false;
- const JUMP_KEYS = new Set(['Space', 'ArrowUp', 'KeyW']);
- const keys = {};
- const pad = (n, w) => String(Math.max(0, n | 0)).padStart(w, '0');
- /* ------------------------------ audio ------------------------------ */
- let actx = null;
- function audio() {
- if (!actx) { const AC = window.AudioContext || window.webkitAudioContext; if (AC) actx = new AC(); }
- if (actx && actx.state === 'suspended') actx.resume();
- }
- function tone(f0, f1, dur, type, vol, delay) {
- if (muted || !actx) return;
- type = type || 'square'; vol = vol || 0.12; delay = delay || 0;
- const t = actx.currentTime + delay;
- const o = actx.createOscillator(), g = actx.createGain();
- o.type = type;
- o.frequency.setValueAtTime(f0, t);
- if (f1) o.frequency.exponentialRampToValueAtTime(Math.max(1, f1), t + dur);
- g.gain.setValueAtTime(vol, t);
- g.gain.exponentialRampToValueAtTime(0.001, t + dur);
- o.connect(g); g.connect(actx.destination);
- o.start(t); o.stop(t + dur + 0.03);
- }
- const sfx = {
- jump() { tone(300, 720, 0.18, 'square', 0.09); },
- coin() { tone(988, 0, 0.07, 'square', 0.11); tone(1319, 0, 0.28, 'square', 0.11, 0.07); },
- stomp() { tone(320, 70, 0.16, 'square', 0.13); },
- bump() { tone(130, 55, 0.10, 'square', 0.13); },
- brk() { tone(240, 55, 0.22, 'sawtooth', 0.11); },
- 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); },
- win() { [523, 659, 784, 1047, 1319, 1568].forEach((f, i) => tone(f, 0, 0.16, 'square', 0.11, i * 0.11)); }
- };
- /* --------------------------- level building --------------------------- */
- function buildLevel() {
- grid = Array.from({ length: H }, () => new Array(W).fill(0));
- const set = (x, y, v) => { grid[y][x] = v; };
- const row = (a, b, y, v) => { for (let x = a; x <= b; x++) grid[y][x] = v; };
- const pipe = (c, h) => { for (let i = 0; i < h; i++) { set(c, 13 - i, PIPE); set(c + 1, 13 - i, PIPE); } };
- // ground (two gaps: 3 wide at cols 38-40, 4 wide at cols 58-61)
- row(0, 37, 14, G);
- row(41, 57, 14, G);
- row(62, 99, 14, G);
- // pipes
- pipe(26, 2); pipe(44, 3); pipe(70, 4); pipe(84, 2);
- // blocks
- set(16, 10, Q);
- set(18, 10, BRICK); set(19, 10, Q); set(20, 10, BRICK); set(21, 10, Q); set(22, 10, BRICK);
- set(56, 10, Q); set(56, 8, Q); set(66, 10, Q);
- // staircase (heights 1..4)
- for (let i = 0; i < 4; i++) for (let j = 0; j <= i; j++) set(78 + i, 13 - j, STAIR);
- // coins
- [[18,9],[19,9],[20,9],[21,9],[22,9],
- [38,8],[39,8],[40,8],
- [58,8],[59,8],[60,8],[61,8],
- [46,9],[47,9],[48,9],
- [72,9],[73,9],[74,9]
- ].forEach(([x, y]) => set(x, y, COIN));
- }
- const ENEMY_COLS = [12, 30, 47, 51, 66, 82];
- function resetLevel() {
- buildLevel();
- 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 }));
- popups = []; particles = []; texts = [];
- mario = { x: 2 * TILE, y: GROUND_TOP - 30, w: 24, h: 30, vx: 0, vy: 0, onGround: false, facing: 1, anim: 0 };
- camX = 0; time = 150; jumpBuffered = 0; coyote = 0; combo = 0;
- flagY = 160;
- }
- function startGame() { lives = 3; score = 0; coins = 0; resetLevel(); state = 'playing'; paused = false; }
- /* ------------------------------ helpers ------------------------------ */
- function solid(tx, ty) {
- if (tx < 0 || tx >= W) return true;
- if (ty < 0 || ty >= H) return false;
- const c = grid[ty][tx];
- return c === G || c === BRICK || c === Q || c === USED || c === PIPE || c === STAIR;
- }
- 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; }
- function collide(e, dt, isMario) {
- // horizontal
- e.x += e.vx * dt;
- if (e.x < 0) { e.x = 0; if (e.vx < 0) e.vx = 0; }
- if (e.x > LVL_W - e.w) { e.x = LVL_W - e.w; if (e.vx > 0) e.vx = 0; }
- let y0 = Math.floor(e.y / TILE), y1 = Math.floor((e.y + e.h - 0.01) / TILE);
- if (e.vx > 0) {
- const tx = Math.floor((e.x + e.w - 0.01) / TILE);
- for (let ty = y0; ty <= y1; ty++) if (solid(tx, ty)) { e.x = tx * TILE - e.w; e.vx = 0; break; }
- } else if (e.vx < 0) {
- const tx = Math.floor(e.x / TILE);
- for (let ty = y0; ty <= y1; ty++) if (solid(tx, ty)) { e.x = (tx + 1) * TILE; e.vx = 0; break; }
- }
- // vertical
- const prevVy = e.vy;
- e.y += e.vy * dt;
- e.onGround = false;
- const x0 = Math.floor(e.x / TILE), x1 = Math.floor((e.x + e.w - 0.01) / TILE);
- if (e.vy > 0) {
- const ty = Math.floor((e.y + e.h - 0.01) / TILE);
- for (let tx = x0; tx <= x1; tx++) if (solid(tx, ty)) { e.y = ty * TILE - e.h; e.vy = 0; e.onGround = true; break; }
- } else if (e.vy < 0) {
- const ty = Math.floor(e.y / TILE);
- 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; }
- }
- if (isMario && e.onGround && prevVy > 540) puff(e);
- }
- /* --------------------------- gameplay actions --------------------------- */
- function addText(x, y, str) { texts.push({ x, y, str, t: 0 }); }
- function hitBlock(tx, ty) {
- const c = grid[ty][tx];
- if (c === Q) {
- grid[ty][tx] = USED;
- coins++; score += 100;
- popups.push({ x: tx * TILE + TILE / 2, y: ty * TILE - 6, vy: -330, t: 0 });
- addText(tx * TILE + 16, ty * TILE - 10, '+100');
- sfx.coin();
- } else if (c === BRICK) {
- grid[ty][tx] = 0;
- score += 50;
- for (let i = 0; i < 6; i++) particles.push({
- x: tx * TILE + 8 + (i % 3) * 8, y: ty * TILE + 8 + ((i / 3) | 0) * 10,
- vx: (Math.random() - 0.5) * 280, vy: -160 - Math.random() * 160, t: 0, color: '#b5502a', size: 7
- });
- addText(tx * TILE + 16, ty * TILE - 10, '+50');
- sfx.brk();
- } else {
- sfx.bump();
- }
- }
- function sparkle(tx, ty) {
- const cx = tx * TILE + 16, cy = ty * TILE + 16;
- for (let i = 0; i < 5; i++) {
- const a = Math.PI * 2 * i / 5;
- particles.push({ x: cx, y: cy, vx: Math.cos(a) * 130, vy: Math.sin(a) * 130 - 60, t: 0, color: '#ffd54f', size: 4 });
- }
- }
- function puff(m) {
- for (let i = 0; i < 4; i++) particles.push({
- 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
- });
- }
- function die() {
- if (state !== 'playing') return;
- state = 'dying'; deathT = 0;
- mario.vx = 0; mario.vy = -620;
- sfx.die();
- }
- function startWin() {
- if (state !== 'playing') return;
- state = 'win'; winT = 0;
- score += 1000 + Math.ceil(time) * 10;
- addText(mario.x + 12, mario.y - 14, '+' + (1000 + Math.ceil(time) * 10));
- mario.vx = 0; mario.vy = 0;
- mario.x = 88 * TILE + 16 - mario.w / 2;
- flagY = Math.max(160, mario.y + 4);
- sfx.win();
- }
- /* ------------------------------- update ------------------------------- */
- function update(dt) {
- if (state === 'title') return;
- if (state === 'dying') {
- deathT += dt;
- if (deathT > 0.35) { mario.vy += GRAV * dt; mario.y += mario.vy * dt; }
- if (deathT > 1.5) {
- lives--;
- if (lives <= 0) state = 'gameover';
- else { resetLevel(); state = 'playing'; }
- }
- return;
- }
- if (state === 'win') {
- winT += dt;
- const targetY = GROUND_TOP - mario.h;
- if (mario.y < targetY) mario.y = Math.min(targetY, mario.y + 210 * dt);
- flagY = Math.min(400, flagY + 260 * dt);
- if (winT > 1.9) state = 'clear';
- return;
- }
- if (state !== 'playing') return;
- /* --- mario movement --- */
- const L = keys['ArrowLeft'] || keys['KeyA'];
- const R = keys['ArrowRight'] || keys['KeyD'];
- const dir = (R ? 1 : 0) - (L ? 1 : 0);
- const ACC = mario.onGround ? 2000 : 1300;
- if (dir !== 0) {
- mario.facing = dir;
- mario.vx += dir * ACC * dt;
- if (Math.abs(mario.vx) > MAX_RUN) mario.vx = Math.sign(mario.vx) * MAX_RUN;
- } else {
- const fr = (mario.onGround ? 2400 : 300) * dt;
- if (Math.abs(mario.vx) <= fr) mario.vx = 0;
- else mario.vx -= Math.sign(mario.vx) * fr;
- }
- // jumping (with coyote time + input buffer + variable height)
- jumpBuffered -= dt;
- if (mario.onGround) coyote = 0.09; else coyote -= dt;
- if (jumpBuffered > 0 && coyote > 0) {
- mario.vy = -JUMP_V;
- coyote = 0; jumpBuffered = 0;
- sfx.jump();
- }
- mario.vy += GRAV * dt;
- if (mario.vy > 950) mario.vy = 950;
- collide(mario, dt, true);
- mario.anim += Math.abs(mario.vx) * dt;
- if (mario.onGround) combo = 0;
- // fell into a pit
- if (mario.y > VH + 80) { die(); return; }
- // collect coins
- {
- const x0 = Math.floor(mario.x / TILE), x1 = Math.floor((mario.x + mario.w - 0.01) / TILE);
- const y0 = Math.floor(mario.y / TILE), y1 = Math.floor((mario.y + mario.h - 0.01) / TILE);
- for (let ty = y0; ty <= y1; ty++) for (let tx = x0; tx <= x1; tx++) {
- if (tx < 0 || tx >= W || ty < 0 || ty >= H) continue;
- if (grid[ty][tx] === COIN) {
- grid[ty][tx] = 0;
- coins++; score += 100;
- sparkle(tx, ty);
- sfx.coin();
- }
- }
- }
- /* --- goombas --- */
- for (const g of goombas) {
- if (g.dead) continue;
- g.x += g.dir * 70 * dt;
- if (g.x < 0) { g.x = 0; g.dir = 1; }
- if (g.x > LVL_W - g.w) { g.x = LVL_W - g.w; g.dir = -1; }
- const ax = g.dir > 0 ? g.x + g.w : g.x;
- const tx = Math.floor(ax / TILE);
- const ty0 = Math.floor((g.y + 6) / TILE), ty1 = Math.floor((g.y + g.h - 1) / TILE);
- if (solid(tx, ty0) || solid(tx, ty1)) g.dir *= -1;
- // turn around at pit edges instead of falling in
- const footTy = Math.floor((g.y + g.h) / TILE);
- if (footTy < H && !solid(Math.floor((g.dir > 0 ? g.x + g.w + 2 : g.x - 2) / TILE), footTy)) g.dir *= -1;
- g.vy += GRAV * dt;
- if (g.vy > 950) g.vy = 950;
- g.y += g.vy * dt;
- if (g.vy > 0) {
- const ty = Math.floor((g.y + g.h - 0.01) / TILE);
- const lx = Math.floor(g.x / TILE), rx = Math.floor((g.x + g.w - 0.01) / TILE);
- if (solid(lx, ty) || solid(rx, ty)) { g.y = ty * TILE - g.h; g.vy = 0; }
- }
- if (g.y > VH + 80) { g.dead = true; continue; }
- if (rectHit(mario, g)) {
- if (mario.vy > 0 && (mario.y + mario.h - g.y) < 26) {
- g.dead = true; g.squashT = 0;
- combo++;
- score += 100 * combo;
- addText(g.x + 14, g.y - 8, '+' + (100 * combo));
- mario.vy = -480;
- sfx.stomp();
- } else {
- die();
- }
- }
- }
- goombas = goombas.filter(g => {
- if (!g.dead) return true;
- if (g.y > VH + 60) return false; // fell into a pit
- g.squashT += dt;
- return g.squashT < 0.5;
- });
- /* --- effects --- */
- for (const p of popups) { p.t += dt; p.vy += 1800 * dt; p.y += p.vy * dt; }
- popups = popups.filter(p => p.t < 0.45);
- for (const p of particles) { p.t += dt; p.vy += 2000 * dt; p.x += p.vx * dt; p.y += p.vy * dt; }
- particles = particles.filter(p => p.t < 0.8);
- for (const t of texts) { t.t += dt; t.y -= 34 * dt; }
- texts = texts.filter(t => t.t < 0.9);
- /* --- flag --- */
- if (mario.x + mario.w > 88 * TILE + 10) { startWin(); return; }
- /* --- timer & camera --- */
- time -= dt;
- if (time <= 0) { time = 0; die(); return; }
- camX = Math.max(0, Math.min(LVL_W - VW, mario.x + 12 - VW / 2));
- }
- /* ------------------------------- input ------------------------------- */
- function handleKey(c) {
- if (c === 'KeyM') { muted = !muted; return; }
- if ((c === 'Enter' || c === 'KeyR') && (state === 'title' || state === 'gameover' || state === 'clear')) { startGame(); return; }
- if (c === 'KeyR' && state === 'playing') { startGame(); return; }
- if (paused && (c === 'Enter' || c === 'Space')) { paused = false; }
- }
- window.addEventListener('keydown', e => {
- if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Space'].includes(e.code)) e.preventDefault();
- if (e.repeat) { keys[e.code] = true; return; }
- keys[e.code] = true;
- audio();
- handleKey(e.code);
- if (JUMP_KEYS.has(e.code)) jumpBuffered = 0.12;
- });
- window.addEventListener('keyup', e => {
- keys[e.code] = false;
- if (JUMP_KEYS.has(e.code) && state === 'playing' && mario.vy < -320) mario.vy = -320;
- });
- window.addEventListener('blur', () => { if (state === 'playing') paused = true; });
- canvas.addEventListener('click', () => { paused = false; audio(); });
- /* ------------------------------- render ------------------------------- */
- function drawBackground() {
- const sky = ctx.createLinearGradient(0, 0, 0, VH);
- sky.addColorStop(0, '#5c94fc');
- sky.addColorStop(1, '#9fd4ff');
- ctx.fillStyle = sky;
- ctx.fillRect(0, 0, VW, VH);
- // clouds (parallax)
- ctx.fillStyle = 'rgba(255,255,255,0.95)';
- const co = camX * 0.4;
- for (let k = 0; k < 3; k++) for (let i = 0; i < 4; i++) {
- const x = k * 1400 + i * 330 + ((i * 137 + k * 53) % 90) - co;
- if (x < -140 || x > VW + 40) continue;
- const y = 46 + ((i * 83 + k * 61) % 110);
- const s = 0.8 + (i % 2) * 0.5;
- ctx.beginPath();
- ctx.arc(x, y, 16 * s, 0, 7);
- ctx.arc(x + 20 * s, y - 9 * s, 19 * s, 0, 7);
- ctx.arc(x + 42 * s, y, 16 * s, 0, 7);
- ctx.fill();
- }
- // hills (parallax)
- ctx.fillStyle = '#3fa04a';
- const ho = camX * 0.7;
- for (let k = 0; k < 4; k++) for (let i = 0; i < 2; i++) {
- const x = k * 1000 + i * 430 + ((i * 97 + k * 41) % 120) - ho;
- const r = 85 + (i % 2) * 55;
- if (x < -r * 2 || x > VW + r) continue;
- ctx.beginPath();
- ctx.moveTo(x - r, GROUND_TOP);
- ctx.quadraticCurveTo(x, GROUND_TOP - r * 1.7, x + r, GROUND_TOP);
- ctx.fill();
- }
- // bushes (screen-space, behind tiles)
- ctx.fillStyle = '#4db85a';
- for (const c of [4, 13, 33, 50, 63, 76, 93]) {
- const x = c * TILE - camX;
- if (x < -90 || x > VW + 20) continue;
- ctx.beginPath();
- ctx.arc(x, GROUND_TOP, 15, Math.PI, 0);
- ctx.arc(x + 18, GROUND_TOP, 20, Math.PI, 0);
- ctx.arc(x + 38, GROUND_TOP, 14, Math.PI, 0);
- ctx.fill();
- }
- }
- function drawGround(x, y, stair) {
- ctx.fillStyle = stair ? '#d8752c' : '#c2622a';
- ctx.fillRect(x, y, TILE, TILE);
- ctx.fillStyle = '#e8935a';
- ctx.fillRect(x + 3, y + 3, TILE - 6, 2);
- ctx.fillStyle = '#7a3a10';
- ctx.fillRect(x, y, TILE, 3);
- ctx.fillRect(x, y, 3, TILE);
- ctx.fillRect(x, y + 15, TILE, 2);
- ctx.fillRect(x + 15, y + 3, 2, 12);
- ctx.fillRect(x + 7, y + 17, 2, 12);
- ctx.fillRect(x + 23, y + 17, 2, 12);
- }
- function drawBrick(x, y) {
- ctx.fillStyle = '#b5502a';
- ctx.fillRect(x, y, TILE, TILE);
- ctx.fillStyle = '#e07a4a';
- ctx.fillRect(x + 2, y + 2, 28, 2);
- ctx.fillStyle = '#5e2410';
- ctx.fillRect(x, y + 15, TILE, 2);
- ctx.fillRect(x, y, 2, TILE);
- ctx.fillRect(x + 30, y, 2, TILE);
- ctx.fillRect(x + 14, y + 2, 2, 13);
- ctx.fillRect(x + 7, y + 17, 2, 13);
- ctx.fillRect(x + 21, y + 17, 2, 13);
- }
- function drawQ(x, y, tx) {
- ctx.fillStyle = '#8a5a00';
- ctx.fillRect(x, y, TILE, TILE);
- ctx.fillStyle = '#f8b800';
- ctx.fillRect(x + 2, y + 2, TILE - 4, TILE - 4);
- ctx.fillStyle = '#8a5a00';
- [[4, 4], [TILE - 8, 4], [4, TILE - 8], [TILE - 8, TILE - 8]].forEach(([rx, ry]) => ctx.fillRect(x + rx, y + ry, 4, 4));
- ctx.fillStyle = '#fff';
- ctx.font = 'bold 20px "Courier New", monospace';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText('?', x + 16, y + 18);
- void tx;
- }
- function drawUsed(x, y) {
- ctx.fillStyle = '#8a5a2a';
- ctx.fillRect(x, y, TILE, TILE);
- ctx.fillStyle = '#6b4220';
- [[4, 4], [TILE - 8, 4], [4, TILE - 8], [TILE - 8, TILE - 8]].forEach(([rx, ry]) => ctx.fillRect(x + rx, y + ry, 4, 4));
- ctx.strokeStyle = '#6b4220';
- ctx.lineWidth = 2;
- ctx.strokeRect(x + 1.5, y + 1.5, TILE - 3, TILE - 3);
- }
- function drawPipe(x, y, tx, ty) {
- const isLeft = !(tx > 0 && grid[ty][tx - 1] === PIPE);
- const isRight = !(tx < W - 1 && grid[ty][tx + 1] === PIPE);
- const isTop = ty === 0 || grid[ty - 1][tx] !== PIPE;
- ctx.fillStyle = '#2f9e3e';
- ctx.fillRect(x, y, TILE, TILE);
- ctx.fillStyle = '#7fd98a';
- ctx.fillRect(x + (isLeft ? 6 : 10), y, 4, TILE);
- if (isLeft) { ctx.fillStyle = '#1c6b2a'; ctx.fillRect(x, y, 3, TILE); }
- if (isRight) { ctx.fillStyle = '#1c6b2a'; ctx.fillRect(x + TILE - 3, y, 3, TILE); }
- if (isTop) {
- const lw = TILE + (isLeft ? 6 : 0) + (isRight ? 6 : 0);
- const lx = x - (isLeft ? 6 : 0);
- ctx.fillStyle = '#2f9e3e';
- ctx.fillRect(lx, y, lw, 22);
- ctx.fillStyle = '#7fd98a';
- ctx.fillRect(lx + 4, y, 6, 22);
- ctx.strokeStyle = '#14501f';
- ctx.lineWidth = 2;
- ctx.strokeRect(lx + 1, y + 1, lw - 2, 20);
- }
- }
- function drawCoinSprite(cx, cy, r, phase) {
- const s = 0.35 + 0.65 * Math.abs(Math.sin(tGlobal * 4 + phase));
- ctx.fillStyle = '#b8860b';
- ctx.beginPath(); ctx.ellipse(cx, cy, r * s, r, 0, 0, 7); ctx.fill();
- ctx.fillStyle = '#ffd54f';
- ctx.beginPath(); ctx.ellipse(cx, cy, r * s * 0.72, r * 0.72, 0, 0, 7); ctx.fill();
- }
- function drawTiles() {
- const c0 = Math.max(0, Math.floor(camX / TILE));
- const c1 = Math.min(W - 1, Math.ceil((camX + VW) / TILE));
- for (let ty = 0; ty < H; ty++) for (let tx = c0; tx <= c1; tx++) {
- const c = grid[ty][tx];
- if (!c) continue;
- const x = tx * TILE - camX, y = ty * TILE;
- switch (c) {
- case G: drawGround(x, y, false); break;
- case STAIR: drawGround(x, y, true); break;
- case BRICK: drawBrick(x, y); break;
- case Q: drawQ(x, y, tx); break;
- case USED: drawUsed(x, y); break;
- case PIPE: drawPipe(x, y, tx, ty); break;
- case COIN: drawCoinSprite(x + 16, y + 16, 10, tx * 0.7); break;
- }
- }
- }
- function drawFlag() {
- const x = 88 * TILE + 16 - camX;
- if (x < -80 || x > VW + 80) return;
- ctx.fillStyle = '#cfd8dc';
- ctx.fillRect(x - 3, 128, 6, GROUND_TOP - 128);
- ctx.fillStyle = '#90a4ae';
- ctx.fillRect(x + 1, 128, 2, GROUND_TOP - 128);
- ctx.fillStyle = '#ffd54f';
- ctx.beginPath(); ctx.arc(x, 122, 10, 0, 7); ctx.fill();
- ctx.fillStyle = '#ffb300';
- ctx.beginPath(); ctx.arc(x, 122, 5, 0, 7); ctx.fill();
- ctx.fillStyle = '#2e7d32';
- ctx.beginPath();
- ctx.moveTo(x - 3, flagY);
- ctx.lineTo(x - 40, flagY + 16);
- ctx.lineTo(x - 3, flagY + 32);
- ctx.closePath();
- ctx.fill();
- }
- function drawGoomba(g) {
- const x = g.x - camX, y = g.y;
- if (x < -50 || x > VW + 50) return;
- if (g.dead) {
- if (g.squashT > 0.5) return;
- ctx.globalAlpha = Math.max(0, 1 - g.squashT * 2);
- ctx.fillStyle = '#a05a2c';
- ctx.fillRect(x, y + g.h - 10, g.w, 10);
- ctx.globalAlpha = 1;
- return;
- }
- ctx.fillStyle = '#4a2508';
- ctx.beginPath(); ctx.ellipse(x + 7, y + g.h - 4, 7, 4, 0, 0, 7); ctx.fill();
- ctx.beginPath(); ctx.ellipse(x + g.w - 7, y + g.h - 4, 7, 4, 0, 0, 7); ctx.fill();
- ctx.fillStyle = '#a05a2c';
- ctx.beginPath(); ctx.ellipse(x + 14, y + 12, 14, 12, 0, 0, 7); ctx.fill();
- ctx.fillStyle = '#c88b5a';
- ctx.beginPath(); ctx.ellipse(x + 14, y + 17, 10, 7, 0, 0, 7); ctx.fill();
- ctx.fillStyle = '#fff';
- ctx.beginPath(); ctx.ellipse(x + 9, y + 10, 4, 5, 0, 0, 7); ctx.fill();
- ctx.beginPath(); ctx.ellipse(x + 19, y + 10, 4, 5, 0, 0, 7); ctx.fill();
- ctx.fillStyle = '#222';
- ctx.beginPath(); ctx.arc(x + 10, y + 11, 1.8, 0, 7); ctx.fill();
- ctx.beginPath(); ctx.arc(x + 18, y + 11, 1.8, 0, 7); ctx.fill();
- ctx.strokeStyle = '#222';
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(x + 5, y + 4); ctx.lineTo(x + 11, y + 8);
- ctx.moveTo(x + 23, y + 4); ctx.lineTo(x + 17, y + 8);
- ctx.stroke();
- }
- function drawMario() {
- const m = mario;
- ctx.save();
- ctx.translate(m.x + m.w / 2 - camX, 0);
- ctx.scale(m.facing, 1);
- ctx.translate(-12, 0);
- const y = Math.round(m.y);
- const moving = Math.abs(m.vx) > 20 && m.onGround;
- const ph = moving ? Math.sin(m.anim * 0.12) : 0;
- // shoes
- ctx.fillStyle = '#6d3a12';
- if (!m.onGround) {
- ctx.fillRect(2, y + 24, 9, 6);
- ctx.fillRect(13, y + 26, 9, 4);
- } else {
- ctx.fillRect(2 + ph * 2, y + 25, 9, 5);
- ctx.fillRect(13 - ph * 2, y + 25, 9, 5);
- }
- // overalls
- ctx.fillStyle = '#2b58c8';
- ctx.fillRect(5, y + 16, 14, 10);
- ctx.fillRect(6, y + 12, 4, 5);
- ctx.fillRect(14, y + 12, 4, 5);
- ctx.fillStyle = '#ffd54f';
- ctx.fillRect(7, y + 17, 2, 2);
- ctx.fillRect(15, y + 17, 2, 2);
- // shirt + arms
- ctx.fillStyle = '#d8302a';
- ctx.fillRect(4, y + 12, 16, 6);
- // hands
- ctx.fillStyle = '#fcbf92';
- ctx.fillRect(2, y + 15, 4, 4);
- ctx.fillRect(18, y + 15, 4, 4);
- // face
- ctx.fillStyle = '#fcbf92';
- ctx.fillRect(6, y + 5, 12, 8);
- // sideburn
- ctx.fillStyle = '#5a2d0c';
- ctx.fillRect(5, y + 5, 3, 4);
- // eye
- ctx.fillStyle = '#222';
- ctx.fillRect(14, y + 6, 2, 4);
- // nose
- ctx.fillStyle = '#f5a86e';
- ctx.fillRect(17, y + 8, 4, 4);
- // mustache
- ctx.fillStyle = '#4a2508';
- ctx.fillRect(12, y + 11, 7, 2);
- // cap
- ctx.fillStyle = '#d8302a';
- ctx.fillRect(5, y, 13, 5);
- ctx.fillRect(12, y + 3, 10, 3);
- ctx.fillStyle = '#fff';
- ctx.fillRect(9, y + 1, 4, 3);
- ctx.restore();
- }
- function drawEffects() {
- for (const p of popups) {
- ctx.globalAlpha = Math.max(0, 1 - p.t / 0.45);
- drawCoinSprite(p.x - camX, p.y, 9, 0);
- }
- ctx.globalAlpha = 1;
- for (const p of particles) {
- ctx.globalAlpha = Math.max(0, 1 - p.t * 1.3);
- ctx.fillStyle = p.color;
- ctx.fillRect(p.x - camX - p.size / 2, p.y - p.size / 2, p.size, p.size);
- }
- ctx.globalAlpha = 1;
- ctx.font = 'bold 14px "Courier New", monospace';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'alphabetic';
- for (const t of texts) {
- ctx.globalAlpha = Math.max(0, 1 - t.t / 0.9);
- ctx.fillStyle = '#fff';
- ctx.fillText(t.str, t.x - camX, t.y);
- }
- ctx.globalAlpha = 1;
- }
- function drawHUD() {
- ctx.fillStyle = 'rgba(10,20,40,0.35)';
- ctx.fillRect(0, 0, VW, 34);
- ctx.fillStyle = '#fff';
- ctx.font = 'bold 15px "Courier New", monospace';
- ctx.textAlign = 'left';
- ctx.textBaseline = 'middle';
- const s = ' SCORE ' + pad(score, 6) + ' COINS ×' + pad(coins, 2) + ' WORLD 1-1 TIME ' + Math.ceil(time) + ' LIVES ×' + lives + (muted ? ' MUTED' : '');
- ctx.fillText(s, 8, 18);
- }
- function panel(title, lines) {
- ctx.fillStyle = 'rgba(0,0,20,0.55)';
- ctx.fillRect(0, 0, VW, VH);
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillStyle = '#ffd54f';
- ctx.font = 'bold 46px "Courier New", monospace';
- ctx.fillText(title, VW / 2, 165);
- ctx.fillStyle = '#fff';
- ctx.font = 'bold 20px "Courier New", monospace';
- lines.forEach((l, i) => ctx.fillText(l, VW / 2, 225 + i * 38));
- }
- function drawOverlays() {
- if (state === 'title') {
- panel('SUPER MARIO MINI', [
- 'One short level · 3 lives · 150 seconds',
- 'Stomp goombas · grab coins · break bricks · reach the flag!',
- 'Press ENTER to start (M = mute)'
- ]);
- } else if (state === 'gameover') {
- panel('GAME OVER', [
- 'Final score ' + pad(score, 6),
- 'Press ENTER to try again'
- ]);
- } else if (state === 'clear') {
- panel('COURSE CLEAR!', [
- 'Final score ' + pad(score, 6),
- 'Press ENTER to play again'
- ]);
- } else if (paused) {
- panel('PAUSED', ['Click the game or press ENTER to resume']);
- }
- }
- function draw() {
- drawBackground();
- drawTiles();
- drawFlag();
- drawEffects();
- for (const g of goombas) drawGoomba(g);
- drawMario();
- drawHUD();
- drawOverlays();
- }
- /* -------------------------------- loop -------------------------------- */
- let last = 0;
- function frame(ts) {
- requestAnimationFrame(frame);
- const dt = Math.min(0.033, (ts - last) / 1000 || 0);
- last = ts;
- if (!paused) {
- tGlobal += dt;
- update(dt);
- }
- draw();
- }
- resetLevel();
- requestAnimationFrame(frame);
- </script>
- </body>
- </html>
Advertisement
Add Comment
Please, Sign In to add comment