--- description: Rules for building a small 2D browser game with HTML5 canvas or Phaser (JavaScript or TypeScript) alwaysApply: true --- # Small browser game rules You are helping build a small 2D browser game. It must stay playable after every change. Prefer simple, readable code over clever architecture. Small, finished and fun beats big and broken. ## Game facts (edit these) - Working title: [GAME NAME] - Genre and core loop in one sentence: [e.g. "Endless runner: jump over obstacles, collect coins, speed increases over time"] - Engine: [Plain HTML5 canvas | Phaser 3 | Phaser (version from package.json)] - Language: [JavaScript | TypeScript] - Target: desktop and mobile browsers, controls: [keyboard / mouse / touch] - Logical resolution: [e.g. 800x450], scaled to fit the window while keeping the aspect ratio - Run command: [e.g. `npm run dev`] Build command: [e.g. `npm run build`] ## Project structure - `index.html` loads one entry file (for example `src/main.js` or `src/main.ts`). - Keep game logic in `src/`, assets in `public/assets/` (or `assets/`), and tunable numbers in `src/config.js` (speeds, gravity, spawn rates, colors, sizes). - One file per scene or major system (for example `scenes/Boot`, `scenes/Menu`, `scenes/Game`, `scenes/GameOver`, `systems/input`, `systems/audio`). - If using Phaser: use Scenes for game states, load assets in a preload or Boot scene, and use the version of Phaser that is already in package.json. Check the API for that version before you use a method; do not mix Phaser 2 and Phaser 3 APIs. - If using plain canvas: one `requestAnimationFrame` loop that calls `update(dt)` then `render()`. No `setInterval` game loops. ## Game loop and timing - All movement uses delta time (`dt` in seconds), so the game runs at the same speed at 30, 60 or 144 FPS. - Clamp `dt` (for example to a maximum of 0.05 s) so a background tab or a lag spike does not teleport objects through walls. - Pause the game when the tab is hidden (`document.visibilitychange`) and resume cleanly. ## Input - Map raw input to actions (`left`, `right`, `jump`, `pause`) in one place, so keyboard, mouse and touch all drive the same actions. - Support touch if the target includes mobile: use pointer events and add on-screen buttons when the game needs more than a tap. - Prevent page scrolling only on the game canvas, not on the whole page. ## Rendering and assets - Draw placeholder shapes first (rectangles, circles, colors from config). Replace them with sprites later. Do not block gameplay work on art. - Keep the canvas crisp: handle `devicePixelRatio`, and set `image-rendering: pixelated` for pixel art. - Use texture atlases or sprite sheets when there are many small images. - Audio only starts after the first user interaction (browsers block autoplay). Provide a mute toggle and remember it in `localStorage`. ## State and saving - Keep a single source of truth for game state (score, lives, level). UI reads from it; it does not store its own copies. - Save only small things in `localStorage` (high score, settings), under one namespaced key such as `[gamename]-save-v1`. Handle missing or corrupted data without crashing. ## Performance - Do not create new objects every frame in hot loops. Reuse arrays and use object pools for bullets, particles and enemies. - Remove or recycle objects that leave the screen. - Target 60 FPS on a mid-range phone. If you add an effect, say what it might cost. ## How to work - Make one change at a time and keep the game runnable after each step. Tell me how to test the change (which keys to press, what should happen). - Before editing, read the files involved. Do not rename or move files unless I ask. - Do not add new libraries without asking. Ask before changing the engine version. - Put magic numbers in `src/config.js` instead of hard-coding them. - When you fix a bug, explain the cause in one or two sentences. - Do not invent features I did not ask for. If you think something would improve the game, suggest it at the end instead of building it. - If a change touches physics, collisions or timing, describe an edge case you checked (for example a very large `dt`, or two collisions in one frame).