Skip to content
SlashClone
Made by@gameforge

Browser game starter

@gameforge/browser-game-starter

Turn a one-line game idea into a complete, playable single-file HTML5 canvas game: game loop, keyboard and touch input, score, best score, game over, and restart. Ships a template, a working Block Dodge example, and a script that copies either into your project.

SkillOtherv1.0.0 (latest)Tested
Activity
New: be the first to try it
  1. 1<!doctype html>
  2. 2<html lang="en">
  3. 3<head>
  4. 4<meta charset="utf-8">
  5. 5<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  6. 6<title>Block Dodge</title>
  7. 7<style>
  8. 8 html, body { margin: 0; height: 100%; background: #0e1116; color: #eef1f5; font-family: system-ui, sans-serif; }
  9. 9 body { display: flex; align-items: center; justify-content: center; overflow: hidden; touch-action: none; }
  10. 10 canvas { background: #182030; border-radius: 8px; max-width: 100vw; max-height: 100vh; }
  11. 11</style>
  12. 12</head>
  13. 13<body>
  14. 14<!-- Idea: dodge the falling blocks for as long as you can; it gets faster every few seconds. -->
  15. 15<canvas id="game" width="480" height="720"></canvas>
  16. 16<script>
  17. 17"use strict";
  18. 18// Block Dodge: an example game made with browser-game-starter.
  19. 19// Move with arrow keys, A and D, or drag on a phone. Survive as long as you can.
  20. 21const CONFIG = {
  21. 22 playerSpeed: 380,
  22. 23 playerW: 44,
  23. 24 playerH: 18,
  24. 25 blockMin: 24,
  25. 26 blockMax: 70,
  26. 27 spawnEvery: 0.8,
  27. 28 minSpawnEvery: 0.22,
  28. 29 fallSpeed: 200,
  29. 30 speedUpPerSecond: 9,
  30. 31 shakeOnHit: 12,
  31. 32};
  32. 34const canvas = document.getElementById("game");
  33. 35const ctx = canvas.getContext("2d");
  34. 36const W = canvas.width, H = canvas.height;
  35. 38let state;
  36. 39function reset(best) {
  37. 40 state = {
  38. 41 mode: "title",
  39. 42 player: { x: W / 2, y: H - 50 },
  40. 43 blocks: [],
  41. 44 particles: [],
  42. 45 time: 0,
  43. 46 spawnTimer: 0,
  44. 47 shake: 0,
  45. 48 best: best ?? loadBest(),
  46. 49 };
  47. 51function loadBest() { try { return Number(localStorage.getItem("best:block-dodge")) || 0; } catch { return 0; } }
  48. 52function saveBest(v) { try { localStorage.setItem("best:block-dodge", String(v)); } catch { /* storage blocked */ } }
  49. 54const keys = new Set();
  50. 55let pointerX = null;
  51. 56addEventListener("keydown", (e) => {
  52. 57 keys.add(e.key);
  53. 58 if (e.key === " " || e.key === "Enter") start();
  54. 59});
  55. 60addEventListener("keyup", (e) => keys.delete(e.key));
  56. 61function toCanvasX(clientX) {
  57. 62 const r = canvas.getBoundingClientRect();
  58. 63 return ((clientX - r.left) / r.width) * W;
  59. 65canvas.addEventListener("pointerdown", (e) => { pointerX = toCanvasX(e.clientX); start(); });
  60. 66canvas.addEventListener("pointermove", (e) => { if (e.buttons || e.pointerType === "touch") pointerX = toCanvasX(e.clientX); });
  61. 67canvas.addEventListener("pointerup", () => { pointerX = null; });
  62. 69function start() {
  63. 70 if (state.mode === "playing") return;
  64. 71 reset(state.best);
  65. 72 state.mode = "playing";
  66. 75function update(dt) {
  67. 76 state.shake = Math.max(0, state.shake - 40 * dt);
  68. 77 for (const pt of state.particles) { pt.x += pt.vx * dt; pt.y += pt.vy * dt; pt.vy += 600 * dt; pt.life -= dt; }
  69. 78 state.particles = state.particles.filter((pt) => pt.life > 0);
  70. 79 if (state.mode !== "playing") return;
  71. 81 state.time += dt;
  72. 82 const p = state.player;
  73. 83 let dir = 0;
  74. 84 if (keys.has("ArrowLeft") || keys.has("a")) dir -= 1;
  75. 85 if (keys.has("ArrowRight") || keys.has("d")) dir += 1;
  76. 86 if (pointerX !== null) dir = Math.sign(pointerX - p.x) * Math.min(1, Math.abs(pointerX - p.x) / 24);
  77. 87 p.x = Math.max(CONFIG.playerW / 2, Math.min(W - CONFIG.playerW / 2, p.x + dir * CONFIG.playerSpeed * dt));
  78. 89 state.spawnTimer -= dt;
  79. 90 if (state.spawnTimer <= 0) {
  80. 91 state.spawnTimer = Math.max(CONFIG.minSpawnEvery, CONFIG.spawnEvery - state.time * 0.015);
  81. 92 const size = CONFIG.blockMin + Math.random() * (CONFIG.blockMax - CONFIG.blockMin);
  82. 93 state.blocks.push({ x: Math.random() * (W - size), y: -size, size, hue: 330 + Math.random() * 60 });
  83. 94 }
  84. 95 const speed = CONFIG.fallSpeed + state.time * CONFIG.speedUpPerSecond;
  85. 96 for (const b of state.blocks) b.y += speed * dt;
  86. 97 state.blocks = state.blocks.filter((b) => b.y < H + b.size);
  87. 99 const px = p.x - CONFIG.playerW / 2, py = p.y - CONFIG.playerH / 2;
  88. 100 for (const b of state.blocks) {
  89. 101 if (px < b.x + b.size && px + CONFIG.playerW > b.x && py < b.y + b.size && py + CONFIG.playerH > b.y) {
  90. 102 return crash();
  91. 103 }
  92. 104 }
  93. 107function crash() {
  94. 108 state.mode = "over";
  95. 109 state.shake = CONFIG.shakeOnHit;
  96. 110 for (let i = 0; i < 30; i++) {
  97. 111 const a = Math.random() * Math.PI * 2, v = 120 + Math.random() * 260;
  98. 112 state.particles.push({ x: state.player.x, y: state.player.y, vx: Math.cos(a) * v, vy: Math.sin(a) * v - 200, life: 0.8 });
  99. 113 }
  100. 114 const score = Math.floor(state.time * 10);
  101. 115 if (score > state.best) { state.best = score; saveBest(score); }
  102. 118function draw() {
  103. 119 ctx.save();
  104. 120 ctx.clearRect(0, 0, W, H);
  105. 121 if (state.shake > 0) ctx.translate((Math.random() - 0.5) * state.shake, (Math.random() - 0.5) * state.shake);
  106. 123 for (const b of state.blocks) {
  107. 124 ctx.fillStyle = `hsl(${b.hue % 360} 80% 62%)`;
  108. 125 ctx.fillRect(b.x, b.y, b.size, b.size);
  109. 126 }
  110. 127 if (state.mode === "playing" || state.mode === "title") {
  111. 128 const p = state.player;
  112. 129 ctx.fillStyle = "#5ee0b5";
  113. 130 ctx.fillRect(p.x - CONFIG.playerW / 2, p.y - CONFIG.playerH / 2, CONFIG.playerW, CONFIG.playerH);
  114. 131 }
  115. 132 ctx.fillStyle = "#5ee0b5";
  116. 133 for (const pt of state.particles) ctx.fillRect(pt.x, pt.y, 4, 4);
  117. 134 ctx.restore();
  118. 136 const score = Math.floor(state.time * 10);
  119. 137 ctx.fillStyle = "#eef1f5";
  120. 138 ctx.font = "bold 24px system-ui, sans-serif";
  121. 139 ctx.textAlign = "left";
  122. 140 ctx.fillText(`Score ${score}`, 16, 36);
  123. 141 ctx.textAlign = "right";
  124. 142 ctx.fillText(`Best ${state.best}`, W - 16, 36);
  125. 144 if (state.mode !== "playing") {
  126. 145 ctx.fillStyle = "rgba(14, 17, 22, 0.7)";
  127. 146 ctx.fillRect(0, 0, W, H);
  128. 147 ctx.fillStyle = "#eef1f5";
  129. 148 ctx.textAlign = "center";
  130. 149 ctx.font = "bold 42px system-ui, sans-serif";
  131. 150 ctx.fillText(state.mode === "title" ? "Block Dodge" : "Squashed!", W / 2, H / 2 - 30);
  132. 151 ctx.font = "20px system-ui, sans-serif";
  133. 152 ctx.fillText("Arrows or drag to move", W / 2, H / 2 + 10);
  134. 153 ctx.fillText("Tap, click, or press Space to play", W / 2, H / 2 + 44);
  135. 154 }
  136. 157let last = performance.now();
  137. 158function frame(now) {
  138. 159 const dt = Math.min(0.05, (now - last) / 1000);
  139. 160 last = now;
  140. 161 update(dt);
  141. 162 draw();
  142. 163 requestAnimationFrame(frame);
  143. 165reset();
  144. 166requestAnimationFrame(frame);
  145. 167</script>
  146. 168</body>
  147. 169</html>