create-remix-game 1.2.9 → 1.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/auth.js CHANGED
File without changes
package/bin/link.js CHANGED
File without changes
package/dist/cli.js CHANGED
@@ -171,6 +171,16 @@ async function main() {
171
171
  ],
172
172
  initial: 0,
173
173
  },
174
+ {
175
+ type: 'select',
176
+ name: 'phaserVersion',
177
+ message: 'Phaser version:',
178
+ choices: [
179
+ { title: 'Phaser 3 (stable)', value: '3' },
180
+ { title: 'Phaser 4 (beta)', value: '4' },
181
+ ],
182
+ initial: 0,
183
+ },
174
184
  {
175
185
  type: 'confirm',
176
186
  name: 'initGit',
@@ -3,6 +3,7 @@ export interface ScaffoldConfig {
3
3
  gameName: string;
4
4
  multiplayer: boolean;
5
5
  packageManager: string;
6
+ phaserVersion: '3' | '4';
6
7
  initGit: boolean;
7
8
  gameId: string;
8
9
  isRemixGame: boolean;
package/dist/scaffold.js CHANGED
@@ -63,12 +63,15 @@ async function processTemplates(targetPath, config) {
63
63
  'README.md.template',
64
64
  'index.html.template',
65
65
  'remix.config.ts.template',
66
+ 'CLAUDE.md.template',
66
67
  ];
67
68
  // Get remix-dev version for dependency injection
68
69
  const remixDevVersion = config.useLocalDeps ? 'workspace:*' : `^${getRemixDevVersion()}`;
69
70
  for (const templateFile of templateFiles) {
70
71
  const filePath = path.join(targetPath, templateFile);
71
72
  const content = await fs.readFile(filePath, 'utf-8');
73
+ // Determine Phaser CDN version
74
+ const phaserCdnVersion = config.phaserVersion === '4' ? 'phaser@beta' : 'phaser@3';
72
75
  // Replace template variables
73
76
  const processed = content
74
77
  .replace(/\{\{GAME_NAME\}\}/g, config.gameName)
@@ -77,7 +80,9 @@ async function processTemplates(targetPath, config) {
77
80
  .replace(/\{\{PACKAGE_MANAGER\}\}/g, config.packageManager)
78
81
  .replace(/\{\{REMIX_DEV_VERSION\}\}/g, remixDevVersion)
79
82
  .replace(/\{\{GAME_ID\}\}/g, config.gameId)
80
- .replace(/\{\{IS_REMIX_GAME\}\}/g, String(config.isRemixGame));
83
+ .replace(/\{\{IS_REMIX_GAME\}\}/g, String(config.isRemixGame))
84
+ .replace(/\{\{PHASER_VERSION\}\}/g, config.phaserVersion)
85
+ .replace(/\{\{PHASER_CDN_VERSION\}\}/g, phaserCdnVersion);
81
86
  // Write to actual file (remove .template)
82
87
  const outputPath = filePath.replace('.template', '');
83
88
  await fs.writeFile(outputPath, processed);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-remix-game",
3
- "version": "1.2.9",
3
+ "version": "1.2.12",
4
4
  "description": "CLI for scaffolding Remix games",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -17,6 +17,15 @@
17
17
  "README.md",
18
18
  "LICENSE"
19
19
  ],
20
+ "scripts": {
21
+ "dev": "tsc --watch",
22
+ "build": "tsc",
23
+ "clean": "rm -rf dist",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "vitest",
26
+ "test:run": "vitest run",
27
+ "test:coverage": "vitest run --coverage"
28
+ },
20
29
  "repository": {
21
30
  "type": "git",
22
31
  "url": "https://github.com/InsideTheSim/remix-dev"
@@ -34,14 +43,5 @@
34
43
  "@types/node": "^22.18.6",
35
44
  "@types/prompts": "^2.4.9",
36
45
  "typescript": "^5.9.3"
37
- },
38
- "scripts": {
39
- "dev": "tsc --watch",
40
- "build": "tsc",
41
- "clean": "rm -rf dist",
42
- "typecheck": "tsc --noEmit",
43
- "test": "vitest",
44
- "test:run": "vitest run",
45
- "test:coverage": "vitest run --coverage"
46
46
  }
47
- }
47
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "purchasedItems": [],
3
+ "tierRewards": {
4
+ "tier-1": ["Reward 1"],
5
+ "tier-2": ["Reward 2"],
6
+ "tier-3": ["Reward 3"]
7
+ }
8
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "9c0b",
3
+ "label": "Red - Test Save State",
4
+ "timestamp": 1766357482660,
5
+ "gameState": {
6
+ "id": "1766357481051-33c32rx0",
7
+ "gameState": {
8
+ "color": "red",
9
+ "timestamp": 1766357481025
10
+ }
11
+ }
12
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "panelStates": {
3
+ "showBuildPanel": false,
4
+ "showGameStatePanel": true,
5
+ "showBoostConfigPanel": false
6
+ }
7
+ }
@@ -0,0 +1,715 @@
1
+ # CLAUDE.md - Game Development Guide
2
+
3
+ This file provides guidance to Claude Code when working with this Remix game project.
4
+
5
+ ## Project Overview
6
+
7
+ This is a **Phaser {{PHASER_VERSION}} game** built for the **Remix/Farcade platform** (Farcaster mini-apps). The game uses:
8
+
9
+ - **Phaser {{PHASER_VERSION}}** for game engine (loaded via CDN)
10
+ - **@farcade/game-sdk** for platform integration (loaded via CDN in production, mocked in dev)
11
+ - **@insidethesim/remix-dev** for development tooling and build pipeline
12
+
13
+ ## Critical Environment Details
14
+
15
+ ### CDN-Loaded Libraries (DO NOT import directly)
16
+
17
+ **Phaser** and **FarcadeSDK** are loaded globally via CDN. They are NOT npm dependencies you can import.
18
+
19
+ ```typescript
20
+ // WRONG - Will cause build errors
21
+ import Phaser from 'phaser'
22
+ import { FarcadeSDK } from '@farcade/game-sdk'
23
+
24
+ // CORRECT - Access from global scope
25
+ // Phaser is available as a global constant
26
+ const game = new Phaser.Game(config)
27
+
28
+ // FarcadeSDK is on window object
29
+ window.FarcadeSDK.singlePlayer.actions.ready()
30
+ ```
31
+
32
+ Type definitions are provided in `src/globals.d.ts`:
33
+
34
+ ```typescript
35
+ declare const Phaser: typeof import('phaser')
36
+ declare global {
37
+ interface Window {
38
+ FarcadeSDK?: FarcadeSDKType
39
+ }
40
+ }
41
+ ```
42
+
43
+ ### Canvas Size (Mobile-First)
44
+
45
+ Games are designed for **Farcaster mobile** with a **2:3 aspect ratio**:
46
+
47
+ ```typescript
48
+ // src/config/GameSettings.ts
49
+ canvas: {
50
+ width: 720,
51
+ height: 1080,
52
+ }
53
+ ```
54
+
55
+ ## @farcade/game-sdk API Reference
56
+
57
+ The SDK provides the interface between your game and the Remix/Farcade platform.
58
+
59
+ ### Initialization Flow
60
+
61
+ **CRITICAL**: Always await `ready()` before using any SDK features.
62
+
63
+ ```typescript
64
+ // In your main scene's create() or a dedicated init function
65
+ async initializeSDK() {
66
+ if (!window.FarcadeSDK) {
67
+ // SDK not available (standalone mode)
68
+ return
69
+ }
70
+
71
+ // Single player mode
72
+ const gameInfo = await window.FarcadeSDK.singlePlayer.actions.ready()
73
+
74
+ // gameInfo contains:
75
+ // - players: Player[] - all players in the game
76
+ // - player: Player - the current player
77
+ // - viewContext: 'full_screen' | 'mini' | etc.
78
+ // - initialGameState: GameStateEnvelope | null - saved state or null if new game
79
+
80
+ // Load saved state if exists
81
+ if (gameInfo.initialGameState?.gameState) {
82
+ this.loadState(gameInfo.initialGameState.gameState)
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### Player Type
88
+
89
+ ```typescript
90
+ interface Player {
91
+ id: string // Unique player ID
92
+ name: string // Display name
93
+ imageUrl?: string // Profile image URL
94
+ purchasedItems: string[] // Array of owned item slugs
95
+ }
96
+ ```
97
+
98
+ ### Single Player API
99
+
100
+ ```typescript
101
+ // Initialize and get game info
102
+ const gameInfo = await window.FarcadeSDK.singlePlayer.actions.ready()
103
+
104
+ // Save game state (persists across sessions)
105
+ window.FarcadeSDK.singlePlayer.actions.saveGameState({
106
+ gameState: {
107
+ score: 100,
108
+ level: 5,
109
+ // any serializable data
110
+ },
111
+ })
112
+
113
+ // Trigger game over (shows score screen)
114
+ window.FarcadeSDK.singlePlayer.actions.gameOver({
115
+ score: 1500,
116
+ })
117
+
118
+ // Haptic feedback (mobile vibration)
119
+ window.FarcadeSDK.singlePlayer.actions.hapticFeedback()
120
+ ```
121
+
122
+ ### Multiplayer API
123
+
124
+ ```typescript
125
+ // Initialize multiplayer
126
+ const gameInfo = await window.FarcadeSDK.multiplayer.actions.ready()
127
+
128
+ // Save and broadcast state to other players
129
+ window.FarcadeSDK.multiplayer.actions.saveGameState({
130
+ gameState: {
131
+ board: [...],
132
+ currentTurn: 'player1',
133
+ },
134
+ alertUserIds: [otherPlayerId] // Notify specific players
135
+ })
136
+
137
+ // Listen for state updates from other players
138
+ window.FarcadeSDK.on('game_state_updated', (envelope) => {
139
+ if (envelope?.gameState) {
140
+ this.loadState(envelope.gameState)
141
+ }
142
+ })
143
+
144
+ // Multiplayer game over
145
+ window.FarcadeSDK.multiplayer.actions.gameOver({
146
+ scores: [
147
+ { playerId: '1', score: 100 },
148
+ { playerId: '2', score: 85 },
149
+ ]
150
+ })
151
+ ```
152
+
153
+ ### Event Listeners
154
+
155
+ ```typescript
156
+ // Listen for events from the platform
157
+ window.FarcadeSDK.on('play_again', () => {
158
+ this.restartGame()
159
+ })
160
+
161
+ window.FarcadeSDK.on('toggle_mute', (data: { isMuted: boolean }) => {
162
+ this.sound.mute = data.isMuted
163
+ })
164
+
165
+ // Purchase completion (for boost tiers)
166
+ window.FarcadeSDK.on('purchase_complete', (data: { success: boolean }) => {
167
+ if (data.success) {
168
+ this.updatePurchasedItems()
169
+ }
170
+ })
171
+ ```
172
+
173
+ ### Purchased Items / Boost Tiers
174
+
175
+ **IMPORTANT**: The `purchasedItems` array and `hasItem()` contain **reward IDs only**, NOT tier names.
176
+
177
+ ```typescript
178
+ // ❌ WRONG - Tier names are NOT in purchasedItems
179
+ if (window.FarcadeSDK.hasItem('tier-1')) { } // NEVER works in production
180
+ if (window.FarcadeSDK.hasItem('tier-2')) { } // NEVER works in production
181
+ if (window.FarcadeSDK.hasItem('tier-3')) { } // NEVER works in production
182
+
183
+ // ✅ CORRECT - Check for specific reward IDs
184
+ if (window.FarcadeSDK.hasItem('double-jump')) {
185
+ this.player.enableDoubleJump()
186
+ }
187
+
188
+ if (window.FarcadeSDK.hasItem('speed-boost')) {
189
+ this.player.speed *= 1.5
190
+ }
191
+ ```
192
+
193
+ **How Boost Tiers Work:**
194
+ 1. Users purchase a **tier** (Bronze/Silver/Gold aka tier-1/tier-2/tier-3)
195
+ 2. Each tier contains **rewards** configured by the game developer
196
+ 3. When purchased, the **reward IDs** (not tier names) are added to `purchasedItems`
197
+ 4. Use `hasItem('reward-id')` to check if a player owns that reward
198
+
199
+ ```typescript
200
+ // Get all purchased reward IDs
201
+ const items = window.FarcadeSDK.purchasedItems // ['double-jump', 'speed-boost', ...]
202
+
203
+ // Initiate a purchase (opens platform modal)
204
+ const result = await window.FarcadeSDK.purchase({ item: 'tier-1' })
205
+ if (result.success) {
206
+ // Rewards from tier-1 are now in purchasedItems
207
+ }
208
+ ```
209
+
210
+ ## The .remix Directory
211
+
212
+ The `.remix` directory stores local development state. This is NOT deployed to production.
213
+
214
+ ### Directory Structure
215
+
216
+ ```
217
+ .remix/
218
+ ├── boost-config.json # Boost tiers and rewards configuration
219
+ ├── current-state.json # Current game state (generated during play)
220
+ ├── settings.json # Dashboard panel states
221
+ └── saved-states/ # Manually saved game states for testing
222
+ └── *.json
223
+ ```
224
+
225
+ ### boost-config.json
226
+
227
+ Defines boost tier rewards. When a tier is "purchased" in dev mode, its **rewards** (not tier names) become available via `hasItem()`.
228
+
229
+ ```json
230
+ {
231
+ "purchasedItems": ["tier-1"], // Tracks which tiers are owned (internal only)
232
+ "tierRewards": {
233
+ "tier-1": ["Double Jump", "Speed Boost"], // Rewards unlocked by tier 1
234
+ "tier-2": ["Extra Life"], // Rewards unlocked by tier 2
235
+ "tier-3": ["Invincibility"] // Rewards unlocked by tier 3
236
+ }
237
+ }
238
+ ```
239
+
240
+ **How it works:**
241
+ - When `tier-1` is in `purchasedItems`, `hasItem('double-jump')` and `hasItem('speed-boost')` return true
242
+ - `hasItem('tier-1')` does **NOT** work - tier names are never in the SDK's purchasedItems
243
+
244
+ **Reward Slugs**: Reward names are converted to slugs for `hasItem()`:
245
+
246
+ - "Double Jump" → `double-jump`
247
+ - "Speed Boost" → `speed-boost`
248
+ - "Extra Life" → `extra-life`
249
+
250
+ ### saved-states/
251
+
252
+ Store test states to quickly restore game scenarios:
253
+
254
+ ```json
255
+ {
256
+ "id": "unique-id",
257
+ "label": "Level 5 - Boss Fight",
258
+ "timestamp": 1234567890,
259
+ "gameState": {
260
+ "id": "state-id",
261
+ "gameState": {
262
+ "level": 5,
263
+ "health": 100,
264
+ "score": 5000
265
+ }
266
+ }
267
+ }
268
+ ```
269
+
270
+ ## Development Dashboard
271
+
272
+ Run `pnpm dev` to start the development server with the dashboard.
273
+
274
+ ### Dashboard Panels
275
+
276
+ 1. **Game State Panel** - View/edit current game state, save/load states
277
+ 2. **Boost Config Panel** - Configure boost tiers and simulate purchases
278
+ 3. **Build Panel** - Build production bundle
279
+
280
+ ### Simulating Purchases
281
+
282
+ In the Boost Config Panel:
283
+
284
+ 1. Add custom rewards to each tier
285
+ 2. Toggle tier ownership with the checkbox
286
+ 3. The game receives `purchase_complete` events
287
+ 4. Use `hasItem('reward-slug')` to check ownership
288
+
289
+ ## Game Architecture Patterns
290
+
291
+ ### Scene Structure
292
+
293
+ ```typescript
294
+ export class GameScene extends Phaser.Scene {
295
+ constructor() {
296
+ super({ key: 'GameScene' })
297
+ }
298
+
299
+ preload(): void {
300
+ // Load assets (images, audio, etc.)
301
+ this.load.image('player', 'assets/player.png')
302
+ }
303
+
304
+ create(): void {
305
+ // Create game objects, initialize SDK
306
+ this.initializeSDK()
307
+ this.createPlayer()
308
+ this.setupInput()
309
+ }
310
+
311
+ update(time: number, delta: number): void {
312
+ // Game loop - called every frame
313
+ this.updatePlayer(delta)
314
+ this.checkCollisions()
315
+ }
316
+
317
+ private async initializeSDK(): Promise<void> {
318
+ if (!window.FarcadeSDK) return
319
+
320
+ const gameInfo = await window.FarcadeSDK.singlePlayer.actions.ready()
321
+
322
+ // Load saved state
323
+ if (gameInfo.initialGameState?.gameState) {
324
+ this.loadState(gameInfo.initialGameState.gameState)
325
+ }
326
+
327
+ // Setup event listeners
328
+ window.FarcadeSDK.on('play_again', () => this.restartGame())
329
+ window.FarcadeSDK.on('purchase_complete', () => this.updateRewards())
330
+ }
331
+ }
332
+ ```
333
+
334
+ ### State Management Pattern
335
+
336
+ ```typescript
337
+ // Save state through SDK (persists to .remix/current-state.json in dev)
338
+ private saveGameState(): void {
339
+ if (!window.FarcadeSDK?.singlePlayer?.actions?.saveGameState) return
340
+
341
+ window.FarcadeSDK.singlePlayer.actions.saveGameState({
342
+ gameState: {
343
+ score: this.score,
344
+ level: this.level,
345
+ playerPosition: { x: this.player.x, y: this.player.y },
346
+ timestamp: Date.now(),
347
+ }
348
+ })
349
+ }
350
+
351
+ // Load state from SDK response
352
+ private loadState(state: any): void {
353
+ if (!state) return
354
+
355
+ if (typeof state.score === 'number') {
356
+ this.score = state.score
357
+ }
358
+ if (typeof state.level === 'number') {
359
+ this.level = state.level
360
+ }
361
+ // etc.
362
+ }
363
+ ```
364
+
365
+ ### Reward-Gated Features
366
+
367
+ ```typescript
368
+ private updateRewards(): void {
369
+ // Check for specific rewards
370
+ if (window.FarcadeSDK?.hasItem('double-jump')) {
371
+ this.player.doubleJumpEnabled = true
372
+ }
373
+
374
+ if (window.FarcadeSDK?.hasItem('speed-boost')) {
375
+ this.player.speed *= 1.5
376
+ }
377
+
378
+ // Or iterate all purchased items
379
+ const items = window.FarcadeSDK?.purchasedItems || []
380
+ items.forEach(item => {
381
+ this.applyReward(item)
382
+ })
383
+ }
384
+ ```
385
+
386
+ ## Inital Project Structure
387
+ (This may change over time)
388
+
389
+ ```
390
+ my-game/
391
+ ├── src/
392
+ │ ├── main.ts # Entry point - creates Phaser game, initializes Remix
393
+ │ ├── globals.d.ts # Type declarations for CDN libraries (Phaser, FarcadeSDK)
394
+ │ ├── config/
395
+ │ │ └── GameSettings.ts # Centralized game configuration (canvas size, tuning)
396
+ │ ├── scenes/
397
+ │ │ └── GameScene.ts # Main game scene (extend with more scenes as needed)
398
+ │ ├── objects/ # Game object classes (Player, Enemy, Projectile, etc.)
399
+ │ ├── systems/ # Game systems (Physics, Spawning, Scoring, Audio, etc.)
400
+ │ └── utils/ # Utility functions and helpers
401
+ ├── .remix/ # Development state (not deployed)
402
+ │ ├── boost-config.json # Boost tier rewards configuration
403
+ │ ├── settings.json # Dashboard panel states
404
+ │ └── saved-states/ # Saved game states for testing
405
+ ├── index.html # HTML entry with CDN scripts
406
+ ├── vite.config.ts # Vite configuration with Remix plugin
407
+ ├── remix.config.ts # Game configuration (multiplayer, gameId)
408
+ ├── package.json # Dependencies and scripts
409
+ └── tsconfig.json # TypeScript configuration
410
+ ```
411
+
412
+ ## Modular Code Architecture
413
+
414
+ ### Separation of Concerns
415
+
416
+ Organize code into logical modules to keep scenes clean and maintainable:
417
+
418
+ **Objects** (`src/objects/`) - Game entities with their own state and behavior:
419
+
420
+ ```typescript
421
+ // src/objects/Player.ts
422
+ export class Player {
423
+ private sprite: Phaser.GameObjects.Sprite
424
+ private speed: number = 200
425
+ private health: number = 100
426
+
427
+ constructor(scene: Phaser.Scene, x: number, y: number) {
428
+ this.sprite = scene.add.sprite(x, y, 'player')
429
+ // Setup physics, animations, etc.
430
+ }
431
+
432
+ update(delta: number, cursors: Phaser.Types.Input.Keyboard.CursorKeys): void {
433
+ // Movement logic
434
+ }
435
+
436
+ takeDamage(amount: number): void {
437
+ this.health -= amount
438
+ }
439
+
440
+ getPosition(): { x: number; y: number } {
441
+ return { x: this.sprite.x, y: this.sprite.y }
442
+ }
443
+ }
444
+ ```
445
+
446
+ **Systems** (`src/systems/`) - Cross-cutting game logic:
447
+
448
+ ```typescript
449
+ // src/systems/ScoreSystem.ts
450
+ export class ScoreSystem {
451
+ private score: number = 0
452
+ private highScore: number = 0
453
+ private onScoreChange?: (score: number) => void
454
+
455
+ addScore(points: number): void {
456
+ this.score += points
457
+ if (this.score > this.highScore) {
458
+ this.highScore = this.score
459
+ }
460
+ this.onScoreChange?.(this.score)
461
+ }
462
+
463
+ reset(): void {
464
+ this.score = 0
465
+ }
466
+
467
+ getState(): { score: number; highScore: number } {
468
+ return { score: this.score, highScore: this.highScore }
469
+ }
470
+
471
+ loadState(state: { score?: number; highScore?: number }): void {
472
+ this.score = state.score ?? 0
473
+ this.highScore = state.highScore ?? 0
474
+ }
475
+ }
476
+ ```
477
+
478
+ **Utils** (`src/utils/`) - Pure helper functions:
479
+
480
+ ```typescript
481
+ // src/utils/math.ts
482
+ export function clamp(value: number, min: number, max: number): number {
483
+ return Math.max(min, Math.min(max, value))
484
+ }
485
+
486
+ export function randomBetween(min: number, max: number): number {
487
+ return Math.random() * (max - min) + min
488
+ }
489
+
490
+ // src/utils/collision.ts
491
+ export function circlesOverlap(
492
+ x1: number,
493
+ y1: number,
494
+ r1: number,
495
+ x2: number,
496
+ y2: number,
497
+ r2: number
498
+ ): boolean {
499
+ const dx = x2 - x1
500
+ const dy = y2 - y1
501
+ const distance = Math.sqrt(dx * dx + dy * dy)
502
+ return distance < r1 + r2
503
+ }
504
+ ```
505
+
506
+ ### Scene Composition
507
+
508
+ Keep scenes focused by delegating to objects and systems:
509
+
510
+ ```typescript
511
+ // src/scenes/GameScene.ts
512
+ import { Player } from '../objects/Player'
513
+ import { EnemySpawner } from '../systems/EnemySpawner'
514
+ import { ScoreSystem } from '../systems/ScoreSystem'
515
+ import GameSettings from '../config/GameSettings'
516
+
517
+ export class GameScene extends Phaser.Scene {
518
+ private player!: Player
519
+ private enemySpawner!: EnemySpawner
520
+ private scoreSystem!: ScoreSystem
521
+
522
+ create(): void {
523
+ // Initialize systems
524
+ this.scoreSystem = new ScoreSystem()
525
+ this.enemySpawner = new EnemySpawner(this)
526
+
527
+ // Create objects
528
+ this.player = new Player(this, GameSettings.canvas.width / 2, GameSettings.canvas.height - 100)
529
+
530
+ // Initialize SDK
531
+ this.initializeSDK()
532
+ }
533
+
534
+ update(time: number, delta: number): void {
535
+ this.player.update(delta, this.cursors)
536
+ this.enemySpawner.update(delta)
537
+ this.checkCollisions()
538
+ }
539
+
540
+ private checkCollisions(): void {
541
+ // Collision logic using objects
542
+ }
543
+ }
544
+ ```
545
+
546
+ ### Configuration-Driven Design
547
+
548
+ Centralize tunable values in `GameSettings.ts`:
549
+
550
+ ```typescript
551
+ // src/config/GameSettings.ts
552
+ export const GameSettings = {
553
+ canvas: {
554
+ width: 720,
555
+ height: 1080,
556
+ },
557
+ player: {
558
+ speed: 200,
559
+ startHealth: 100,
560
+ invincibilityDuration: 1500,
561
+ },
562
+ enemies: {
563
+ spawnRate: 2000, // ms between spawns
564
+ minSpeed: 100,
565
+ maxSpeed: 300,
566
+ pointValue: 10,
567
+ },
568
+ difficulty: {
569
+ speedIncreasePerLevel: 1.1,
570
+ spawnRateDecreasePerLevel: 0.9,
571
+ },
572
+ }
573
+
574
+ export default GameSettings
575
+ ```
576
+
577
+ If game state grows large enough, consider breaking it into modules for maintainability.
578
+
579
+ ### State Serialization Pattern
580
+
581
+ Each module handles its own state serialization:
582
+
583
+ ```typescript
584
+ // In GameScene
585
+ private getGameState(): object {
586
+ return {
587
+ player: this.player.getState(),
588
+ score: this.scoreSystem.getState(),
589
+ level: this.currentLevel,
590
+ timestamp: Date.now(),
591
+ }
592
+ }
593
+
594
+ private loadGameState(state: any): void {
595
+ if (state.player) this.player.loadState(state.player)
596
+ if (state.score) this.scoreSystem.loadState(state.score)
597
+ if (typeof state.level === 'number') this.currentLevel = state.level
598
+ }
599
+
600
+ private saveGameState(): void {
601
+ window.FarcadeSDK?.singlePlayer.actions.saveGameState({
602
+ gameState: this.getGameState()
603
+ })
604
+ }
605
+ ```
606
+
607
+ ### Adding New Features
608
+
609
+ When adding new game features:
610
+
611
+ 1. **Create an object class** if it's a visible entity (e.g., `src/objects/PowerUp.ts`)
612
+ 2. **Create a system** if it's cross-cutting logic (e.g., `src/systems/PowerUpSystem.ts`)
613
+ 3. **Add configuration** to `GameSettings.ts` for tunable values
614
+ 4. **Wire it up** in the scene's `create()` and `update()` methods
615
+ 5. **Add state methods** (`getState()`, `loadState()`) for persistence
616
+
617
+ ## Build & Production
618
+
619
+ ### Development
620
+
621
+ ```bash
622
+ pnpm dev # Start dev server with dashboard
623
+ ```
624
+
625
+ ### Production Build
626
+
627
+ ```bash
628
+ pnpm build # Creates dist/index.html (single file)
629
+ pnpm preview # Preview production build
630
+ ```
631
+
632
+ ### Production Notes
633
+
634
+ - **Single HTML file** - All assets inlined for Farcaster
635
+ - **No SDK mock** - Real SDK provided by Remix platform
636
+ - **CDN libraries** - Phaser/SDK loaded from CDN (not bundled)
637
+
638
+ ## Common Patterns
639
+
640
+ ### Checking SDK Availability
641
+
642
+ ```typescript
643
+ // Always guard SDK calls
644
+ if (window.FarcadeSDK?.singlePlayer?.actions?.ready) {
645
+ const gameInfo = await window.FarcadeSDK.singlePlayer.actions.ready()
646
+ }
647
+
648
+ // For hasItem checks
649
+ const hasReward = window.FarcadeSDK?.hasItem('reward-slug') ?? false
650
+ ```
651
+
652
+ ### Game Over Flow
653
+
654
+ ```typescript
655
+ private triggerGameOver(): void {
656
+ if (!window.FarcadeSDK) return
657
+
658
+ // Single player
659
+ window.FarcadeSDK.singlePlayer.actions.gameOver({
660
+ score: this.score
661
+ })
662
+
663
+ // Platform shows game over screen
664
+ // User can tap "Play Again" which triggers 'play_again' event
665
+ }
666
+ ```
667
+
668
+ ### Color/Theme from Player Data
669
+
670
+ ```typescript
671
+ // Players have profile images
672
+ const player = gameInfo.player
673
+ if (player.imageUrl) {
674
+ this.load.image('playerAvatar', player.imageUrl)
675
+ }
676
+
677
+ // Use player name for display
678
+ this.add.text(100, 100, `Welcome, ${player.name}!`)
679
+ ```
680
+
681
+ ## Troubleshooting
682
+
683
+ ### "Cannot find module 'phaser'"
684
+
685
+ Phaser is loaded via CDN, not npm. Use the global `Phaser` constant.
686
+
687
+ ### SDK methods not working
688
+
689
+ 1. Ensure you awaited `ready()` first
690
+ 2. Check `window.FarcadeSDK` exists before calling methods
691
+ 3. In dev mode, the mock SDK handles everything
692
+
693
+ ### State not persisting
694
+
695
+ 1. Always use SDK methods, never localStorage directly
696
+ 2. Check `.remix/current-state.json` to see saved state
697
+ 3. Clear state with "Reset State" in Game State Panel
698
+
699
+ ### hasItem() returning false
700
+
701
+ 1. **Never check for tier names** - `hasItem('tier-1')` will ALWAYS fail in production
702
+ 2. Check `.remix/boost-config.json` has the tier in `purchasedItems`
703
+ 3. Verify the reward is listed under that tier in `tierRewards`
704
+ 4. Reward names are slugified: "My Reward" → `my-reward`
705
+
706
+ ### Common Boost Tier Mistakes
707
+
708
+ ```typescript
709
+ // ❌ WRONG - Will fail in production
710
+ const boostTier = sdk.hasItem('tier-3') ? 3 : sdk.hasItem('tier-2') ? 2 : sdk.hasItem('tier-1') ? 1 : 0
711
+
712
+ // ✅ CORRECT - Check for actual reward IDs
713
+ const hasDoubleJump = sdk.hasItem('double-jump')
714
+ const hasSpeedBoost = sdk.hasItem('speed-boost')
715
+ ```
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>{{GAME_NAME}}</title>
7
- <script src="https://cdn.jsdelivr.net/npm/phaser@3/dist/phaser.min.js"></script>
7
+ <script src="https://cdn.jsdelivr.net/npm/{{PHASER_CDN_VERSION}}/dist/phaser.min.js"></script>
8
8
  <style>
9
9
  body {
10
10
  margin: 0;
@@ -55,6 +55,9 @@ export class DemoScene extends Phaser.Scene {
55
55
  private player2ScoreText?: Phaser.GameObjects.Text
56
56
  private turnIndicatorText?: Phaser.GameObjects.Text
57
57
  private colorSwatches?: Phaser.GameObjects.Container
58
+ private tierIndicators?: Phaser.GameObjects.Container
59
+ private tierCircles: Phaser.GameObjects.Arc[] = []
60
+ private tierLabels: Phaser.GameObjects.Text[] = []
58
61
 
59
62
  constructor() {
60
63
  super({ key: 'DemoScene' })
@@ -293,22 +296,8 @@ export class DemoScene extends Phaser.Scene {
293
296
  this.createColorSwatches()
294
297
  }
295
298
 
296
- // Add removal instructions at bottom
297
- const removeInstructions = this.add
298
- .text(
299
- GameSettings.canvas.width / 2,
300
- GameSettings.canvas.height - 60,
301
- 'To remove this demo, ask your AI:\n"Remove the demo and create a minimal GameScene"',
302
- {
303
- fontSize: '24px',
304
- color: '#cccccc',
305
- fontFamily: 'Arial',
306
- align: 'center',
307
- wordWrap: { width: GameSettings.canvas.width - 40 },
308
- }
309
- )
310
- .setOrigin(0.5)
311
- .setDepth(100)
299
+ // Create tier indicators at the bottom
300
+ this.createTierIndicators()
312
301
 
313
302
  // Create bouncing balls
314
303
  this.createBalls(15)
@@ -521,6 +510,13 @@ export class DemoScene extends Phaser.Scene {
521
510
  }
522
511
  })
523
512
 
513
+ // Listen for purchase completion to update tier indicators
514
+ window.FarcadeSDK.on('purchase_complete', (data) => {
515
+ console.log('[DemoScene] purchase_complete event received:', data)
516
+ console.log('[DemoScene] Current purchasedItems:', window.FarcadeSDK.purchasedItems)
517
+ this.updateTierIndicators()
518
+ })
519
+
524
520
  if (this.isMultiplayer) {
525
521
  // Multiplayer setup - Set up listeners BEFORE calling ready
526
522
  window.FarcadeSDK.on('game_state_updated', (gameState: any) => {
@@ -852,6 +848,135 @@ export class DemoScene extends Phaser.Scene {
852
848
  this.game.canvas.focus()
853
849
  }
854
850
 
851
+ private createTierIndicators(): void {
852
+ // Container for tier indicators at bottom center
853
+ this.tierIndicators = this.add.container(
854
+ GameSettings.canvas.width / 2,
855
+ GameSettings.canvas.height - 80
856
+ )
857
+ this.tierIndicators.setDepth(101)
858
+
859
+ // Title label
860
+ const title = this.add
861
+ .text(0, -45, 'TIER REWARDS TEST', {
862
+ fontSize: '18px',
863
+ color: '#aaaaaa',
864
+ fontFamily: 'Arial',
865
+ fontStyle: 'bold',
866
+ })
867
+ .setOrigin(0.5)
868
+ this.tierIndicators.add(title)
869
+
870
+ this.tierCircles = []
871
+ this.tierLabels = []
872
+
873
+ // Check for actual reward slugs, not tier slugs
874
+ const tierConfig = [
875
+ { slug: 'reward-1', label: 'Reward 1', activeColor: 0x22c55e },
876
+ { slug: 'reward-2', label: 'Reward 2', activeColor: 0xa855f7 },
877
+ { slug: 'reward-3', label: 'Reward 3', activeColor: 0xec4899 },
878
+ ]
879
+
880
+ const pillWidth = 130
881
+ const pillHeight = 50
882
+ const spacing = 16
883
+ const totalWidth = tierConfig.length * pillWidth + (tierConfig.length - 1) * spacing
884
+ const startX = -totalWidth / 2 + pillWidth / 2
885
+
886
+ tierConfig.forEach((tier, index) => {
887
+ const x = startX + index * (pillWidth + spacing)
888
+
889
+ // Pill background (using graphics for rounded rect)
890
+ const pill = this.add.graphics()
891
+ pill.setData('index', index)
892
+ this.tierIndicators?.add(pill)
893
+ this.tierCircles.push(pill as unknown as Phaser.GameObjects.Arc) // Store for updates
894
+
895
+ // Label
896
+ const label = this.add
897
+ .text(x, 0, tier.label, {
898
+ fontSize: '22px',
899
+ color: '#666666',
900
+ fontFamily: 'Arial',
901
+ fontStyle: 'bold',
902
+ })
903
+ .setOrigin(0.5)
904
+ this.tierIndicators?.add(label)
905
+ this.tierLabels.push(label)
906
+ })
907
+
908
+ // Initial update
909
+ this.updateTierIndicators()
910
+ }
911
+
912
+ private drawPill(
913
+ graphics: Phaser.GameObjects.Graphics,
914
+ x: number,
915
+ y: number,
916
+ width: number,
917
+ height: number,
918
+ fillColor: number,
919
+ fillAlpha: number,
920
+ strokeColor: number,
921
+ strokeAlpha: number
922
+ ): void {
923
+ graphics.clear()
924
+ const radius = height / 2
925
+
926
+ // Fill
927
+ graphics.fillStyle(fillColor, fillAlpha)
928
+ graphics.fillRoundedRect(x - width / 2, y - height / 2, width, height, radius)
929
+
930
+ // Stroke
931
+ graphics.lineStyle(1.5, strokeColor, strokeAlpha)
932
+ graphics.strokeRoundedRect(x - width / 2, y - height / 2, width, height, radius)
933
+ }
934
+
935
+ private updateTierIndicators(): void {
936
+ if (!window.FarcadeSDK || !this.tierCircles.length) return
937
+
938
+ // Check for actual reward slugs, not tier slugs
939
+ const rewardSlugs = ['reward-1', 'reward-2', 'reward-3']
940
+ const tierConfig = [
941
+ { activeColor: 0x22c55e, activeBorder: 0x4ade80 },
942
+ { activeColor: 0xa855f7, activeBorder: 0xc084fc },
943
+ { activeColor: 0xec4899, activeBorder: 0xf472b6 },
944
+ ]
945
+
946
+ const pillWidth = 130
947
+ const pillHeight = 50
948
+ const spacing = 16
949
+ const totalWidth = rewardSlugs.length * pillWidth + (rewardSlugs.length - 1) * spacing
950
+ const startX = -totalWidth / 2 + pillWidth / 2
951
+
952
+ rewardSlugs.forEach((slug, index) => {
953
+ const graphics = this.tierCircles[index] as unknown as Phaser.GameObjects.Graphics
954
+ const label = this.tierLabels[index]
955
+ if (!graphics || !label) return
956
+
957
+ const x = startX + index * (pillWidth + spacing)
958
+ const isOwned = window.FarcadeSDK.hasItem(slug)
959
+
960
+ if (isOwned) {
961
+ this.drawPill(
962
+ graphics,
963
+ x,
964
+ 0,
965
+ pillWidth,
966
+ pillHeight,
967
+ tierConfig[index].activeColor,
968
+ 1,
969
+ tierConfig[index].activeBorder,
970
+ 1
971
+ )
972
+ label.setColor('#ffffff')
973
+ } else {
974
+ this.drawPill(graphics, x, 0, pillWidth, pillHeight, 0x000000, 0.4, 0x444444, 0.6)
975
+ label.setColor('#666666')
976
+ }
977
+ })
978
+ }
979
+
855
980
  private createColorSwatches(): void {
856
981
  // Container for color swatches
857
982
  this.colorSwatches = this.add.container(GameSettings.canvas.width - 150, 50)