From game-creator
Builds 2D browser games with Phaser 3 using scene-based architecture, TypeScript, Vite bundling, and EventBus. Use for new games, Phaser features, or sprite-based web games.
npx claudepluginhub opusgamelabs/game-creator --plugin game-creatorThis skill uses the workspace's default tool permissions.
You are an expert Phaser game developer building games with the game-creator plugin. Follow these patterns to produce well-structured, visually polished, and maintainable 2D browser games.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
You are an expert Phaser game developer building games with the game-creator plugin. Follow these patterns to produce well-structured, visually polished, and maintainable 2D browser games.
SPECTACLE_* events) alongside the core loop — they are part of scaffolding, not deferred polish.phaserjs/template-vite-ts templateGameState.reset() must restore a clean slate. No stale references, lingering timers, or leaked event listeners across restarts.Every player action and game event must emit at least one spectacle event. These hooks exist in the template EventBus — the design pass attaches visual effects to them.
| Event | Constant | When to Emit |
|---|---|---|
spectacle:entrance | SPECTACLE_ENTRANCE | In create() when the player/entities first appear on screen |
spectacle:action | SPECTACLE_ACTION | On every player input (tap, jump, shoot, swipe) |
spectacle:hit | SPECTACLE_HIT | When player hits/destroys an enemy, collects an item, or scores |
spectacle:combo | SPECTACLE_COMBO | When consecutive hits/scores happen without a miss. Pass { combo: n } |
spectacle:streak | SPECTACLE_STREAK | When combo reaches milestones (5, 10, 25, 50). Pass { streak: n } |
spectacle:near_miss | SPECTACLE_NEAR_MISS | When player narrowly avoids danger (within ~20% of collision radius) |
Rule: If a gameplay moment has no spectacle event, add one. The design pass cannot polish what it cannot hook into.
All games MUST follow the game-creator conventions:
core/ directory with EventBus, GameState, and Constantsdomain:action event naming, no direct scene referencesreset() for clean restartsshutdown()See conventions.md for full details and code examples.
Use the official Vite + TypeScript template as your starting point:
npx degit phaserjs/template-vite-ts my-game
cd my-game && npm install
src/
├── core/
│ ├── EventBus.ts # Singleton event bus + event constants
│ ├── GameState.ts # Centralized state with reset()
│ └── Constants.ts # ALL config values
├── scenes/
│ ├── Boot.ts # Minimal setup, start Game scene
│ ├── Preloader.ts # Load all assets, show progress bar
│ ├── Game.ts # Main gameplay (starts immediately, no title screen)
│ └── GameOver.ts # End screen with restart
├── objects/ # Game entities (Player, Enemy, etc.)
├── systems/ # Managers and subsystems
├── ui/ # UI components (buttons, bars, dialogs)
├── audio/ # Audio manager, music, SFX
├── config.ts # Phaser.Types.Core.GameConfig
└── main.ts # Entry point
See project-setup.md for full config and tooling details.
init() → preload() → create() → update(time, delta)init() for receiving data from scene transitionsPreloader scene, not in every sceneupdate() lean — delegate to subsystems and game objectsWhen games run inside the Play.fun dashboard on mobile Safari, the SDK sets CSS custom properties on the game iframe's document.documentElement:
--ogp-safe-top-inset — space below the Play.fun header bubbles (~68px on mobile)--ogp-safe-bottom-inset — space above Safari bottom controls (~148px on mobile)Both default to 0px when not running inside the dashboard (desktop, standalone).
The template's Constants.js reads these at boot and exposes SAFE_ZONE.TOP and SAFE_ZONE.BOTTOM in canvas pixels (CSS value × DPR). A static fallback (GAME.HEIGHT * 0.08) ensures the top safe zone works even without the SDK.
Rules:
SAFE_ZONE.TOP and above GAME.HEIGHT - SAFE_ZONE.BOTTOMSAFE_ZONE.TOP and SAFE_ZONE.BOTTOMconst usableH = GAME.HEIGHT - SAFE_ZONE.TOP - SAFE_ZONE.BOTTOM for calculating proportional positions in UI scenesSAFE_ZONE.BOTTOMimport { SAFE_ZONE } from '../core/Constants.js';
// In any UI scene:
const safeTop = SAFE_ZONE.TOP;
const safeBottom = SAFE_ZONE.BOTTOM;
const usableH = GAME.HEIGHT - safeTop - safeBottom;
const title = this.add.text(cx, safeTop + usableH * 0.15, 'GAME OVER', { ... });
const button = createButton(scene, cx, safeTop + usableH * 0.6, 'PLAY AGAIN', callback);
// Touch controls / bottom HUD:
const bottomY = GAME.HEIGHT - safeBottom - 40 * PX;
How it works in Constants.js:
function _readSafeInsets() {
const s = getComputedStyle(document.documentElement);
const top = parseInt(s.getPropertyValue('--ogp-safe-top-inset')) || 0;
const bottom = parseInt(s.getPropertyValue('--ogp-safe-bottom-inset')) || 0;
return { top: top * DPR, bottom: bottom * DPR };
}
const _insets = _readSafeInsets();
export const SAFE_ZONE = {
TOP: Math.max(GAME.HEIGHT * 0.08, _insets.top),
BOTTOM: _insets.bottom,
LEFT: 0,
RIGHT: 0,
};
See scenes-and-lifecycle.md for patterns and examples.
Phaser.GameObjects.Sprite (or other base classes) for custom objectsPhaser.GameObjects.Group for object pooling (bullets, coins, enemies)Phaser.GameObjects.Container for composite objects, but avoid deep nestingGameObjectFactory for scene-level accessSee game-objects.md for implementation patterns.
See physics-and-movement.md for details.
maxSize; recycle with setActive(false) / setVisible(false)getChildren().filter(c => c.active)pixelArt: true — Enable in game config for pixel art games (nearest-neighbor scaling)See assets-and-performance.md for full optimization guide.
See patterns.md for implementations.
All games MUST work on desktop AND mobile unless explicitly specified otherwise. Focus 60% mobile / 40% desktop for tradeoffs. Pick the best mobile input for each game concept:
| Game Type | Primary Mobile Input | Desktop Input |
|---|---|---|
| Platformer | Tap left/right half + tap-to-jump | Arrow keys / WASD |
| Runner/endless | Tap / swipe up to jump | Space / Up arrow |
| Puzzle/match | Tap targets (44px min) | Click |
| Shooter | Virtual joystick + tap-to-fire | Mouse + WASD |
| Top-down | Virtual joystick | Arrow keys / WASD |
Abstract input into an inputState object so game logic is source-agnostic:
// In Scene update():
const isMobile = this.sys.game.device.os.android ||
this.sys.game.device.os.iOS || this.sys.game.device.os.iPad;
let left = false, right = false, jump = false;
// Keyboard
left = this.cursors.left.isDown || this.wasd.left.isDown;
right = this.cursors.right.isDown || this.wasd.right.isDown;
jump = Phaser.Input.Keyboard.JustDown(this.spaceKey);
// Touch (merge with keyboard)
if (isMobile) {
// Left half tap = left, right half = right, or use tap zones
this.input.on('pointerdown', (p) => {
if (p.x < this.scale.width / 2) left = true;
else right = true;
});
}
this.player.update({ left, right, jump });
See project-setup.md for the full responsive canvas config, entity sizing, HTML boilerplate, and portrait-first game patterns.
Always show visual touch indicators on touch-capable devices — never rely on invisible tap zones. Use capability detection (not OS-based detection) to determine touch support:
// Good — detects touch laptops, tablets, 2-in-1s
const hasTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
// Bad — misses touch-screen laptops, iPadOS (reports as desktop)
const isMobile = device.os.android || device.os.iOS;
Render semi-transparent arrow buttons (or direction indicators) at the bottom of the screen. Use TOUCH constants from Constants.js for sizing (12% of canvas width), alpha (0.35 idle / 0.6 active), and margins. Update alpha in the update() loop based on input state for visual feedback.
Enable pointer input (pointerdown, pointermove, pointerup) on all devices — pointer events work for both mouse and touch. This eliminates the need for separate mobile/desktop input code paths.
Collectibles, hazards, and interactive items must be at least 7–8% of GAME.WIDTH to be recognizable on phone screens. Smaller entities become indistinguishable blobs on mobile.
// Good — recognizable on mobile
ATTACK_WIDTH: _canvasW * 0.09,
POWERUP_WIDTH: _canvasW * 0.072,
// Bad — too small on phone screens
ATTACK_WIDTH: _canvasW * 0.04,
POWERUP_WIDTH: _canvasW * 0.035,
For the main player character, use 12–15% of GAME.WIDTH (see Entity Sizing above).
See game-objects.md for the full button implementation pattern (Container + Graphics + Text with hover/press states) and the list of broken patterns to avoid.
update() methods — Don't put all game logic in one giant update with nested conditionals. Delegate to objects and systems.world, input, cameras, add, make, scene, sys, game, cache, registry, sound, textures, events, physics, matter, time, tweens, lights, data, load, anims, renderer, or plugins. These are reserved by Phaser.update() without pooling — This causes GC spikes. Always pool frequently created/destroyed objects. Avoid expensive per-frame allocations — reuse objects, arrays, and temporary variables.delta in update — Always use delta for time-based movement, not frame-based.shutdown() to prevent memory leaks. This is critical for restart-safety — stale listeners cause double-firing and ghost behavior after restart.Constants.ts. No magic numbers in game logic.physics.add.existing(obj, true) does nothing on its own. You MUST call physics.add.collider(bodyA, bodyB, callback) to connect two bodies. Every static collider (ground, walls, platforms) needs an explicit collider or overlap call wiring it to the entities that should interact with it.setAlpha(0) on an interactive game object and layer Graphics or other display objects on top. For buttons, always use the Container + Graphics + Text pattern (see game-objects.md). Common broken patterns: (1) Drawing a Graphics rect after adding Text, hiding the label behind it. (2) Creating a Zone for hit area with Graphics drawn over it, making the Zone unreachable. (3) Making Text interactive but covering it with a Graphics background drawn afterward. The fix is always: Container first, Graphics added to container, Text added to container (in that order), Container is the interactive element.mute-button rule. Games with audio must have a mute toggle.Before considering a game complete, verify:
GameState.reset() restores a clean slate, no stale listeners or timersScale.FIT + CENTER_BOTH + zoom: 1/DPR with DPR-multiplied dimensions, crisp on Retinashutdown()collider() or overlap() callmaxSizedelta, not frame countmute-button ruleSPECTACLE_* event; entrance sequence fires in create()npm run build succeeds with no errors| File | Topic |
|---|---|
| conventions.md | Mandatory game-creator architecture conventions |
| project-setup.md | Scaffolding, Vite, TypeScript config, responsive canvas, entity sizing, portrait mode |
| scenes-and-lifecycle.md | Scene system deep dive |
| game-objects.md | Custom objects, groups, containers, button pattern |
| physics-and-movement.md | Physics engines, movement patterns |
| assets-and-performance.md | Assets, optimization, mobile |
| patterns.md | ECS, state machines, singletons |
| no-asset-design.md | Procedural visuals: gradients, parallax, particles, juice |