{
  "name": "2d-game",
  "title": "2D Game - Phaser 3 Games",
  "description": "Build 2D games with Phaser: scene structure, physics, input, sprites, genre recipes (platformer, scroller, arcade, puzzle, endless runner)",
  "guid": "sk_plat_2dgm",
  "category": "Templates",
  "requiredTools": [
    "add",
    "file_write",
    "project_deploy"
  ],
  "content": "# 2D Game - Phaser 3 Games\n\n**2D Game** is the Phaser-based game template on Gipity. All files are fully editable - no locked template layer. Uses Phaser 3.80.1 via CDN with arcade physics.\n\n**When to use this:** When the user asks for a 2D game - platformer, side-scroller, arcade, puzzle, endless runner, top-down, shooter, or RPG. For simple games (wordle, quiz, card games), use `web-simple`. For 3D multiplayer apps, use `3d-engine` for a blank template or `3d-world` for a playable rocket-launcher app.\n\n## Quick Start - Start Here\n\n**STRONGLY RECOMMENDED:** Begin every 2D game by adding the `2d-game` template with `add`. It sets up Phaser 3, boot/game scenes, config, settings, plus generated icons, a PWA manifest, and a social share card. Only hand-roll files if the user explicitly tells you to skip the template.\n\n```\nadd name=2d-game title=\"<Game Name>\"\n```\n\n**Starting over in an existing project:** If `src/` already exists and the user wants a clean rebuild, delete `src/` first, then run `gipity add` normally. Or pass `--force` to `gipity add` to overwrite in one step - destructive, so confirm with the user first. Unrelated content (media, data, notes) is preserved either way.\n\n**Naming:** Use the user's name verbatim if given. If they didn't specify, blend \"Gip\" or \"Gipity\" into the name (e.g. \"Gipity Racer\", \"Gip Tac Toe\") - be creative but don't force it.\n\nThis creates a playable game immediately - colored rectangle player, arcade physics, ground platform, and controls that work on desktop AND mobile out of the box (WASD/arrows + Space on keyboard; a floating virtual joystick, Jump button, and fullscreen toggle on touch devices). Then edit `scenes/game.js` and `settings.js` to build your game.\n\n## Project Structure\n\n```\nsrc/\n  index.html            - Phaser CDN, game container, module entry\n  js/\n    config.js           - Phaser.Game config, scene registration\n    controls.js         - Touch controls overlay (virtual joystick, buttons, fullscreen)\n    settings.js         - Tunable values (canvas, colors, physics, player, gameplay)\n    strings.js          - User-facing display text\n    scenes/\n      boot.js           - Preloader with progress bar\n      game.js           - Main game scene (gameplay logic)\n  css/\n    styles.css          - Page layout, canvas styling\n  images/               - generated app icons + share card (favicon-*.png/.ico,\n                          apple-touch-icon.png, og-image.png); customize with\n                          `gipity brand set --emoji <e>`, then redeploy\n```\n\n## Phaser Essentials\n\n### Scene Lifecycle\nEvery scene has three key methods:\n- `preload()` - load assets (images, spritesheets, audio)\n- `create()` - set up game objects, physics, input\n- `update()` - runs every frame, game logic here\n\n### Loading Assets\nIn `boot.js` preload():\n```js\nthis.load.image('player', './images/player.png');\nthis.load.spritesheet('hero', './images/hero.png', { frameWidth: 32, frameHeight: 48 });\nthis.load.audio('jump', './audio/jump.mp3');\n```\n\n### Creating Game Objects\n```js\n// Sprite (from loaded image)\nthis.player = this.physics.add.sprite(400, 300, 'player');\n\n// Rectangle (no image needed)\nthis.player = this.add.rectangle(400, 300, 32, 48, 0xf26522);\nthis.physics.add.existing(this.player);\n\n// Static group (platforms)\nthis.platforms = this.physics.add.staticGroup();\nthis.platforms.create(400, 568, 'ground');\n```\n\n### Physics (Arcade)\n```js\n// Gravity is set in config.js via settings.physics.gravity\nbody.setVelocityX(speed);\nbody.setVelocityY(jumpForce);  // negative = up\nbody.setBounce(0.2);\nbody.setCollideWorldBounds(true);\n\n// Collisions\nthis.physics.add.collider(player, platforms);\nthis.physics.add.overlap(player, coins, collectCoin, null, this);\n```\n\n### Input\n```js\n// Keyboard\nthis.cursors = this.input.keyboard.createCursorKeys();  // up, down, left, right\nif (this.cursors.left.isDown) body.setVelocityX(-speed);\n\n// Specific keys\nthis.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);\n\n// Touch/pointer\nthis.input.on('pointerdown', (pointer) => { ... });\n```\n\n### Text\n```js\nthis.add.text(400, 30, 'Score: 0', {\n  fontSize: '24px', color: '#ffffff', fontStyle: 'bold',\n}).setOrigin(0.5);\n```\n\n### Camera\n```js\nthis.cameras.main.startFollow(this.player, true, 0.1, 0.1);\nthis.cameras.main.setBounds(0, 0, worldWidth, worldHeight);\n```\n\n## Common Phaser 3 Pitfalls\n\nThese are the failure modes that most often turn the whole screen black. Read before writing a game scene.\n\n- **`Graphics.fillEllipse` does NOT exist.** Phaser 3 Graphics has `fillCircle(x, y, r)`, `fillRect`, `fillTriangle`, `fillRoundedRect`, and `beginPath` + `arc` + `fillPath` - but no ellipse drawcall. For an oval, use `fillCircle` and set `gfx.scaleX` / `gfx.scaleY`, or draw it with `beginPath`/`arc`/`fillPath`.\n- **Don't attach physics to a raw `Graphics` object.** Graphics has no width/height for the body to size against, so the body ends up with `{0,0}` dimensions or drifts from the drawn shape. Use a `Sprite` (from a loaded image) or a `Rectangle` / `Zone` for the physics body, and draw a follower `Graphics` that tracks `body.x`/`body.y` each frame.\n- **Set world bounds for scrolling worlds.** The default physics world is the canvas size. For a side-scroller or large map, call `this.physics.world.setBounds(0, 0, worldWidth, height)` in `create()` - otherwise platforms and objects beyond the canvas get ignored by physics.\n- **Use object hashes, not sparse arrays, for spatial maps.** `this.segments = {}` with string/number keys works; `this.segments = []` with large indices becomes sparse and breaks `Object.keys`/`values` enumeration in subtle ways.\n- **Don't double-create physics bodies.** Either call `this.physics.add.existing(obj)` OR add `obj` to a `staticGroup` with `group.add(obj)` - not both. Double-creation silently misplaces the body relative to the visual.\n- **DOM inputs never receive the game's captured keys unless you shield them.** Phaser registers its keys (WASD, Space, arrows via `addKeys`/`createCursorKeys`) as \"captured\": its window-level listener calls `preventDefault()` on them no matter where focus is, so a player literally cannot type W/A/S/D or Space into a high-score name field or chat box overlaid on the canvas. The template ships a shield in `js/config.js` (a document-level `keydown`/`keyup` listener that `stopPropagation()`s events targeting `input`/`textarea`/`select`/contenteditable, plus `resetKeys()` on focus) - keep it, and if you add your OWN `window`/`document` key listeners, start them with a guard: `if (e.target.closest('input, textarea, select') || e.target.isContentEditable) return;`. Test any name-entry UI by actually typing the movement keys into it.\n- **Always verify renders, not just console.** A missing API or a bad draw can throw once during init and leave a black canvas with nothing further to report. After deploying, capture a screenshot and look at it - `gipity page screenshot <url>` from the CLI, or the `browser` agent tool's `screenshot` action in chat - don't trust \"clean console\" as proof. If gameplay only starts after a \"play\" click, drive it in the same shot so you capture the game and not the menu: `gipity page screenshot <url> --action \"document.getElementById('play').click()\"` (use your start button's selector).\n\n## Build Incrementally\n\nFor anything non-trivial, don't write the whole game in one `Write` call. Work in small, verified steps:\n\n1. Add the app (`gipity add 2d-game`) and deploy - confirm the game renders.\n2. Customize ONE element (e.g. replace the player rectangle with your sprite). Deploy, screenshot, confirm it renders.\n3. Add the next element (ground, enemies, collectibles) one at a time, deploying and verifying between each.\n4. Only after the core loop works, layer on polish (parallax, particles, HUD, touch controls).\n\nA 500-line rewrite of `scenes/game.js` is very hard to debug when something breaks - a single bad API call turns the whole screen black with no useful error. Small steps keep the failure surface tiny.\n\n## Verification After Deploy\n\nAfter every `gipity deploy dev`:\n- Run `gipity page inspect <deploy-url>` to surface console errors.\n- **On the first deploy of a new game**, and any time you've made significant visual changes, also capture a screenshot and look at the image - `gipity page screenshot <url>` (CLI) or the `browser` agent tool's `screenshot` action (chat). To capture actual gameplay rather than the title screen, start the game in the same shot: `gipity page screenshot <url> --action \"document.getElementById('play').click()\"`. A clean console is NOT sufficient proof for Canvas/WebGL apps - render failures are often silent.\n- If you see a black screen with a clean console: assume a sync error fired during Phaser init (most commonly a missing API like `fillEllipse`, or physics attached to a raw Graphics). Re-read the \"Common Phaser 3 Pitfalls\" section above before rewriting.\n\n### Asserting on real game state (score, lives, collisions, win/lose)\n\nA screenshot proves the game *renders*; it can't prove the ball bounces, the score increments, or the win screen ever fires. Drive the **live game object** instead. `js/config.js` exports the Phaser game, and `index.html` loads it as a module - so a dynamic `import()` from `gipity page eval` resolves out of the browser's module cache and hands you **the running instance** (relative specifiers resolve against the page URL). No second game boots.\n\n**Never ship a `window.game` debug hook to do this.** That leaves instrumentation in your production bundle and costs an extra deploy.\n\n```bash\ngipity page eval \"<deploy-url>\" \"\n  const { game } = await import('./js/config.js');\n  const s = game.scene.getScene('Game');\n  s.startGame();                               // call your own scene methods\n  return JSON.stringify({ score: s.score, lives: s.lives, state: s.state });\n\"\n```\n\nFor a longer driver (play a full round, force a win, assert the game-over overlay), put the script in a file and pass `--file` - no shell quoting, and the body may use `await` and `return`:\n\n```bash\ngipity page eval \"<deploy-url>\" --file ./tests/drive-game.js --json\n```\n\nThe eval body has a **~20s in-page budget**, so split a long sequence into one call per state you're verifying.\n\n**Never wait wall-clock time for physics/animation to play out** - the headless browser can paint at ~15 fps, so `setTimeout(2000)` advances far less than 2s of game time and assertions report false negatives. `config.js` also exports `advance(seconds)`: it pauses the browser loop and ticks Phaser's TimeStep by hand, so N simulated seconds (physics, timers, collisions) run in milliseconds, deterministically:\n\n```bash\ngipity page eval \"<deploy-url>\" \"\n  const { game, advance } = await import('./js/config.js');\n  const s = game.scene.getScene('Game');\n  s.startGame();\n  advance(5);                                  // 5s of game time, instantly\n  return JSON.stringify({ score: s.score, bricks: s.bricks.countActive() });\n\"\n```\n\nTo *capture* a driven state visually, `page screenshot --action \"<js>\"` runs the same kind of script before the shot - same async body, same app-relative `import()`. If the action throws, you still get the image, plus a `⚠ --action failed:` line telling you it shows the **undriven** page:\n\n```bash\ngipity page screenshot \"<deploy-url>\" -o win.png \\\n  --action \"const { game } = await import('./js/config.js'); game.scene.getScene('Game').winGame();\"\n```\n\nWrite throwaway driver scripts and screenshots **outside the project directory** (e.g. `/tmp`) - the project auto-syncs to Gipity, so scratch files land in the user's storage. If they must live in the project, add the path to `.gipityignore`.\n\n### Animations\n```js\nthis.anims.create({\n  key: 'walk',\n  frames: this.anims.generateFrameNumbers('hero', { start: 0, end: 3 }),\n  frameRate: 10,\n  repeat: -1,\n});\nthis.player.anims.play('walk', true);\n```\n\n## Adding a New Scene\n\n1. Create `src/js/scenes/myScene.js`:\n```js\nexport class MyScene extends Phaser.Scene {\n  constructor() { super('MyScene'); }\n  create() { /* ... */ }\n  update() { /* ... */ }\n}\n```\n\n2. Register in `config.js`:\n```js\nimport { MyScene } from './scenes/myScene.js';\n// Add to scene array: scene: [Boot, Game, MyScene],\n```\n\n3. Switch scenes: `this.scene.start('MyScene');`\n\n## Genre Recipes\n\n### Side-Scroller / Platformer\n- Keep gravity enabled (default)\n- Add platforms as static physics bodies\n- Camera follows player: `this.cameras.main.startFollow(player)`\n- Set world bounds larger than canvas: `this.physics.world.setBounds(0, 0, 3200, 600)`\n\n### Top-Down (Zelda-style)\n- Disable gravity: set `settings.physics.gravity = 0`\n- Add 4-way movement (up/down/left/right)\n- Use `body.setVelocity(x, y)` for diagonal movement\n\n### Endless Runner\n- Auto-scroll: move obstacles left each frame, spawn new ones off-screen right\n- Single input: jump on tap/space\n- Increase speed over time: `speed += dt * acceleration`\n\n### Arcade / Shooter\n- Bullets: `this.physics.add.group()` with `body.setVelocity()`\n- Enemy spawning: `this.time.addEvent({ delay: 1000, callback: spawnEnemy, loop: true })`\n\n### Puzzle\n- Disable gravity, disable physics or use minimal physics\n- Grid-based: snap positions to grid `Math.round(x / tileSize) * tileSize`\n- Input: pointer clicks to select/move pieces\n\n## Mobile / Touch\n\nThe template is mobile-ready by default via `js/controls.js` - a DOM overlay above the canvas that renders only on touch devices (nothing shows on desktop). It gives the standard mobile-game layout: a **floating virtual joystick** on the left half (the pad appears wherever the thumb lands), **round action buttons** bottom-right, and a **fullscreen toggle** top-right (auto-hidden where the Fullscreen API is unavailable, e.g. iPhone Safari - there, \"Add to Home Screen\" runs fullscreen via the template's PWA meta tags). Multi-touch is tracked per pointer, so joystick + buttons work simultaneously.\n\n```js\nimport { touch, initTouchControls } from '../controls.js';\n\n// create(): declare the buttons your game needs (first = primary, biggest)\ninitTouchControls({ buttons: [{ id: 'jump', label: 'Jump' }, { id: 'fire', label: 'Fire' }] });\n// tap-only game? disable the joystick so it doesn't swallow lower-left taps:\n// initTouchControls({ joystick: false, buttons: [...] });\n\n// update(): merge with keyboard - joystick is analog (-1..1)\nconst keyX = (cursors.right.isDown ? 1 : 0) - (cursors.left.isDown ? 1 : 0);\nconst moveX = keyX !== 0 ? keyX : touch.x;            // touch.y for vertical\nbody.setVelocityX(moveX * speed);\nif (touch.isDown('jump')) { /* held */ }\nif (touch.justPressed('fire')) { /* once per press */ }\n```\n\nAlways keep desktop AND touch input working: keyboard = WASD + arrows (both), plus the matching touch buttons. `touch.enabled` tells you touch controls are active (e.g. to swap instruction text). Phaser still handles in-canvas taps via pointer events (`this.input.on('pointerdown', ...)`), with 3 active pointers configured.\n\nFor mobile-responsive canvas, the template uses `Phaser.Scale.FIT` + `CENTER_BOTH` by default - the game keeps its `settings.canvas` coordinate system and letterboxes to fit any screen or orientation. The page is hardened for games (no pinch-zoom, no overscroll, no text selection; `dvh` viewport).\n\n## Deploy Verification\n\nVerify a deploy when it matters - the first deploy, structural changes (new pages, new frameworks, changed imports), or anything that might have broken. Skip it for trivial changes (copy tweaks, style values).\n\n`gipity deploy dev --inspect` deploys and reports the live page in one step: console errors, failed resources, timing, layout overflow. A clean console is necessary but NOT sufficient for Canvas/WebGL - also capture `gipity page screenshot <url>` and look at it, because render failures are silent. A blank page, black canvas, or wrong-looking UI with a clean console is a real failure, not a pass.\n\nFull loop - reading function logs, calling a function directly, driving the page: the `app-debugging` skill.\n"
}
