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#!/usr/bin/env node
  2. 2// Copy the browser-game-starter template (or the Block Dodge example) into the project.
  3. 3// Usage:
  4. 4// node scripts/new-game.mjs --title "Star Catcher" --idea "catch stars, dodge meteors" [--out game/index.html] [--from template|example] [--force]
  5. 5import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
  6. 6import { dirname, isAbsolute, normalize } from "node:path";
  7. 7import { fileURLToPath } from "node:url";
  8. 8
  9. 9const here = dirname(fileURLToPath(import.meta.url));
  10. 10const SOURCES = {
  11. 11 template: `${here}/../templates/game.html`,
  12. 12 example: `${here}/../examples/block-dodge.html`,
  13. 13};
  14. 15function fail(msg) {
  15. 16 console.error(`new-game: ${msg}`);
  16. 17 process.exit(1);
  17. 20const args = process.argv.slice(2);
  18. 21const opts = { out: "game/index.html", from: "template", title: "", idea: "", force: false };
  19. 22for (let i = 0; i < args.length; i++) {
  20. 23 const a = args[i];
  21. 24 if (a === "--force") opts.force = true;
  22. 25 else if (["--out", "--from", "--title", "--idea"].includes(a)) {
  23. 26 const v = args[++i];
  24. 27 if (v === undefined) fail(`${a} needs a value`);
  25. 28 opts[a.slice(2)] = v;
  26. 29 } else if (a === "--help" || a === "-h") {
  27. 30 console.log('Usage: node scripts/new-game.mjs --title "Name" --idea "one line" [--out game/index.html] [--from template|example] [--force]');
  28. 31 process.exit(0);
  29. 32 } else fail(`unknown argument ${a}`);
  30. 35if (!(opts.from in SOURCES)) fail(`--from must be "template" or "example", got "${opts.from}"`);
  31. 36if (opts.from === "template" && !opts.title.trim()) fail('--title is required, for example --title "Star Catcher"');
  32. 37const out = normalize(opts.out);
  33. 38if (isAbsolute(out) || out.startsWith("..")) fail(`--out must be a path inside the project, got "${opts.out}"`);
  34. 39if (!out.endsWith(".html")) fail(`--out should end in .html, got "${opts.out}"`);
  35. 40if (existsSync(out) && !opts.force) fail(`${out} already exists. Pick another --out, or pass --force to replace it.`);
  36. 42const escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
  37. 43// The title also lands inside JS strings, so keep it to safe characters there.
  38. 44const safeTitle = escapeHtml(opts.title.trim()).replace(/[\\`'{}]/g, "");
  39. 45const safeIdea = escapeHtml(opts.idea.trim() || "no idea given").replace(/--/g, "-");
  40. 47let html = readFileSync(SOURCES[opts.from], "utf8");
  41. 48if (opts.from === "template") {
  42. 49 html = html.replaceAll("{{TITLE}}", safeTitle).replaceAll("{{IDEA}}", safeIdea);
  43. 52mkdirSync(dirname(out), { recursive: true });
  44. 53writeFileSync(out, html);
  45. 54console.log(`Wrote ${out} (${opts.from === "template" ? `"${safeTitle}"` : "Block Dodge example"}). Open it in a browser to play.`);