create-remix-game 1.2.9 → 1.2.11

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.11",
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,681 @@
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
+ Check if player owns specific items (boost rewards):
176
+
177
+ ```typescript
178
+ // Check if player has a specific reward
179
+ if (window.FarcadeSDK.hasItem('double-jump')) {
180
+ this.player.enableDoubleJump()
181
+ }
182
+
183
+ // Get all purchased items
184
+ const items = window.FarcadeSDK.purchasedItems // string[]
185
+
186
+ // Initiate a purchase (opens platform modal)
187
+ const result = await window.FarcadeSDK.purchase({ item: 'tier-1' })
188
+ if (result.success) {
189
+ // Purchase completed
190
+ }
191
+ ```
192
+
193
+ ## The .remix Directory
194
+
195
+ The `.remix` directory stores local development state. This is NOT deployed to production.
196
+
197
+ ### Directory Structure
198
+
199
+ ```
200
+ .remix/
201
+ ├── boost-config.json # Boost tiers and rewards configuration
202
+ ├── current-state.json # Current game state (generated during play)
203
+ ├── settings.json # Dashboard panel states
204
+ └── saved-states/ # Manually saved game states for testing
205
+ └── *.json
206
+ ```
207
+
208
+ ### boost-config.json
209
+
210
+ Defines boost tier rewards. When a tier is "purchased" in dev mode, its rewards become available via `hasItem()`.
211
+
212
+ ```json
213
+ {
214
+ "purchasedItems": ["tier-1"], // Owned tier slugs
215
+ "tierRewards": {
216
+ "tier-1": ["Reward 1"], // Rewards for tier 1
217
+ "tier-2": ["Reward 2"], // Rewards for tier 2
218
+ "tier-3": ["Reward 3"] // Rewards for tier 3
219
+ }
220
+ }
221
+ ```
222
+
223
+ **Reward Slugs**: Reward names are converted to slugs for `hasItem()`:
224
+
225
+ - "Double Jump" → `double-jump`
226
+ - "Reward 1" → `reward-1`
227
+
228
+ ### saved-states/
229
+
230
+ Store test states to quickly restore game scenarios:
231
+
232
+ ```json
233
+ {
234
+ "id": "unique-id",
235
+ "label": "Level 5 - Boss Fight",
236
+ "timestamp": 1234567890,
237
+ "gameState": {
238
+ "id": "state-id",
239
+ "gameState": {
240
+ "level": 5,
241
+ "health": 100,
242
+ "score": 5000
243
+ }
244
+ }
245
+ }
246
+ ```
247
+
248
+ ## Development Dashboard
249
+
250
+ Run `pnpm dev` to start the development server with the dashboard.
251
+
252
+ ### Dashboard Panels
253
+
254
+ 1. **Game State Panel** - View/edit current game state, save/load states
255
+ 2. **Boost Config Panel** - Configure boost tiers and simulate purchases
256
+ 3. **Build Panel** - Build production bundle
257
+
258
+ ### Simulating Purchases
259
+
260
+ In the Boost Config Panel:
261
+
262
+ 1. Add custom rewards to each tier
263
+ 2. Toggle tier ownership with the checkbox
264
+ 3. The game receives `purchase_complete` events
265
+ 4. Use `hasItem('reward-slug')` to check ownership
266
+
267
+ ## Game Architecture Patterns
268
+
269
+ ### Scene Structure
270
+
271
+ ```typescript
272
+ export class GameScene extends Phaser.Scene {
273
+ constructor() {
274
+ super({ key: 'GameScene' })
275
+ }
276
+
277
+ preload(): void {
278
+ // Load assets (images, audio, etc.)
279
+ this.load.image('player', 'assets/player.png')
280
+ }
281
+
282
+ create(): void {
283
+ // Create game objects, initialize SDK
284
+ this.initializeSDK()
285
+ this.createPlayer()
286
+ this.setupInput()
287
+ }
288
+
289
+ update(time: number, delta: number): void {
290
+ // Game loop - called every frame
291
+ this.updatePlayer(delta)
292
+ this.checkCollisions()
293
+ }
294
+
295
+ private async initializeSDK(): Promise<void> {
296
+ if (!window.FarcadeSDK) return
297
+
298
+ const gameInfo = await window.FarcadeSDK.singlePlayer.actions.ready()
299
+
300
+ // Load saved state
301
+ if (gameInfo.initialGameState?.gameState) {
302
+ this.loadState(gameInfo.initialGameState.gameState)
303
+ }
304
+
305
+ // Setup event listeners
306
+ window.FarcadeSDK.on('play_again', () => this.restartGame())
307
+ window.FarcadeSDK.on('purchase_complete', () => this.updateRewards())
308
+ }
309
+ }
310
+ ```
311
+
312
+ ### State Management Pattern
313
+
314
+ ```typescript
315
+ // Save state through SDK (persists to .remix/current-state.json in dev)
316
+ private saveGameState(): void {
317
+ if (!window.FarcadeSDK?.singlePlayer?.actions?.saveGameState) return
318
+
319
+ window.FarcadeSDK.singlePlayer.actions.saveGameState({
320
+ gameState: {
321
+ score: this.score,
322
+ level: this.level,
323
+ playerPosition: { x: this.player.x, y: this.player.y },
324
+ timestamp: Date.now(),
325
+ }
326
+ })
327
+ }
328
+
329
+ // Load state from SDK response
330
+ private loadState(state: any): void {
331
+ if (!state) return
332
+
333
+ if (typeof state.score === 'number') {
334
+ this.score = state.score
335
+ }
336
+ if (typeof state.level === 'number') {
337
+ this.level = state.level
338
+ }
339
+ // etc.
340
+ }
341
+ ```
342
+
343
+ ### Reward-Gated Features
344
+
345
+ ```typescript
346
+ private updateRewards(): void {
347
+ // Check for specific rewards
348
+ if (window.FarcadeSDK?.hasItem('double-jump')) {
349
+ this.player.doubleJumpEnabled = true
350
+ }
351
+
352
+ if (window.FarcadeSDK?.hasItem('speed-boost')) {
353
+ this.player.speed *= 1.5
354
+ }
355
+
356
+ // Or iterate all purchased items
357
+ const items = window.FarcadeSDK?.purchasedItems || []
358
+ items.forEach(item => {
359
+ this.applyReward(item)
360
+ })
361
+ }
362
+ ```
363
+
364
+ ## Inital Project Structure
365
+ (This may change over time)
366
+
367
+ ```
368
+ my-game/
369
+ ├── src/
370
+ │ ├── main.ts # Entry point - creates Phaser game, initializes Remix
371
+ │ ├── globals.d.ts # Type declarations for CDN libraries (Phaser, FarcadeSDK)
372
+ │ ├── config/
373
+ │ │ └── GameSettings.ts # Centralized game configuration (canvas size, tuning)
374
+ │ ├── scenes/
375
+ │ │ └── GameScene.ts # Main game scene (extend with more scenes as needed)
376
+ │ ├── objects/ # Game object classes (Player, Enemy, Projectile, etc.)
377
+ │ ├── systems/ # Game systems (Physics, Spawning, Scoring, Audio, etc.)
378
+ │ └── utils/ # Utility functions and helpers
379
+ ├── .remix/ # Development state (not deployed)
380
+ │ ├── boost-config.json # Boost tier rewards configuration
381
+ │ ├── settings.json # Dashboard panel states
382
+ │ └── saved-states/ # Saved game states for testing
383
+ ├── index.html # HTML entry with CDN scripts
384
+ ├── vite.config.ts # Vite configuration with Remix plugin
385
+ ├── remix.config.ts # Game configuration (multiplayer, gameId)
386
+ ├── package.json # Dependencies and scripts
387
+ └── tsconfig.json # TypeScript configuration
388
+ ```
389
+
390
+ ## Modular Code Architecture
391
+
392
+ ### Separation of Concerns
393
+
394
+ Organize code into logical modules to keep scenes clean and maintainable:
395
+
396
+ **Objects** (`src/objects/`) - Game entities with their own state and behavior:
397
+
398
+ ```typescript
399
+ // src/objects/Player.ts
400
+ export class Player {
401
+ private sprite: Phaser.GameObjects.Sprite
402
+ private speed: number = 200
403
+ private health: number = 100
404
+
405
+ constructor(scene: Phaser.Scene, x: number, y: number) {
406
+ this.sprite = scene.add.sprite(x, y, 'player')
407
+ // Setup physics, animations, etc.
408
+ }
409
+
410
+ update(delta: number, cursors: Phaser.Types.Input.Keyboard.CursorKeys): void {
411
+ // Movement logic
412
+ }
413
+
414
+ takeDamage(amount: number): void {
415
+ this.health -= amount
416
+ }
417
+
418
+ getPosition(): { x: number; y: number } {
419
+ return { x: this.sprite.x, y: this.sprite.y }
420
+ }
421
+ }
422
+ ```
423
+
424
+ **Systems** (`src/systems/`) - Cross-cutting game logic:
425
+
426
+ ```typescript
427
+ // src/systems/ScoreSystem.ts
428
+ export class ScoreSystem {
429
+ private score: number = 0
430
+ private highScore: number = 0
431
+ private onScoreChange?: (score: number) => void
432
+
433
+ addScore(points: number): void {
434
+ this.score += points
435
+ if (this.score > this.highScore) {
436
+ this.highScore = this.score
437
+ }
438
+ this.onScoreChange?.(this.score)
439
+ }
440
+
441
+ reset(): void {
442
+ this.score = 0
443
+ }
444
+
445
+ getState(): { score: number; highScore: number } {
446
+ return { score: this.score, highScore: this.highScore }
447
+ }
448
+
449
+ loadState(state: { score?: number; highScore?: number }): void {
450
+ this.score = state.score ?? 0
451
+ this.highScore = state.highScore ?? 0
452
+ }
453
+ }
454
+ ```
455
+
456
+ **Utils** (`src/utils/`) - Pure helper functions:
457
+
458
+ ```typescript
459
+ // src/utils/math.ts
460
+ export function clamp(value: number, min: number, max: number): number {
461
+ return Math.max(min, Math.min(max, value))
462
+ }
463
+
464
+ export function randomBetween(min: number, max: number): number {
465
+ return Math.random() * (max - min) + min
466
+ }
467
+
468
+ // src/utils/collision.ts
469
+ export function circlesOverlap(
470
+ x1: number,
471
+ y1: number,
472
+ r1: number,
473
+ x2: number,
474
+ y2: number,
475
+ r2: number
476
+ ): boolean {
477
+ const dx = x2 - x1
478
+ const dy = y2 - y1
479
+ const distance = Math.sqrt(dx * dx + dy * dy)
480
+ return distance < r1 + r2
481
+ }
482
+ ```
483
+
484
+ ### Scene Composition
485
+
486
+ Keep scenes focused by delegating to objects and systems:
487
+
488
+ ```typescript
489
+ // src/scenes/GameScene.ts
490
+ import { Player } from '../objects/Player'
491
+ import { EnemySpawner } from '../systems/EnemySpawner'
492
+ import { ScoreSystem } from '../systems/ScoreSystem'
493
+ import GameSettings from '../config/GameSettings'
494
+
495
+ export class GameScene extends Phaser.Scene {
496
+ private player!: Player
497
+ private enemySpawner!: EnemySpawner
498
+ private scoreSystem!: ScoreSystem
499
+
500
+ create(): void {
501
+ // Initialize systems
502
+ this.scoreSystem = new ScoreSystem()
503
+ this.enemySpawner = new EnemySpawner(this)
504
+
505
+ // Create objects
506
+ this.player = new Player(this, GameSettings.canvas.width / 2, GameSettings.canvas.height - 100)
507
+
508
+ // Initialize SDK
509
+ this.initializeSDK()
510
+ }
511
+
512
+ update(time: number, delta: number): void {
513
+ this.player.update(delta, this.cursors)
514
+ this.enemySpawner.update(delta)
515
+ this.checkCollisions()
516
+ }
517
+
518
+ private checkCollisions(): void {
519
+ // Collision logic using objects
520
+ }
521
+ }
522
+ ```
523
+
524
+ ### Configuration-Driven Design
525
+
526
+ Centralize tunable values in `GameSettings.ts`:
527
+
528
+ ```typescript
529
+ // src/config/GameSettings.ts
530
+ export const GameSettings = {
531
+ canvas: {
532
+ width: 720,
533
+ height: 1080,
534
+ },
535
+ player: {
536
+ speed: 200,
537
+ startHealth: 100,
538
+ invincibilityDuration: 1500,
539
+ },
540
+ enemies: {
541
+ spawnRate: 2000, // ms between spawns
542
+ minSpeed: 100,
543
+ maxSpeed: 300,
544
+ pointValue: 10,
545
+ },
546
+ difficulty: {
547
+ speedIncreasePerLevel: 1.1,
548
+ spawnRateDecreasePerLevel: 0.9,
549
+ },
550
+ }
551
+
552
+ export default GameSettings
553
+ ```
554
+
555
+ If game state grows large enough, consider breaking it into modules for maintainability.
556
+
557
+ ### State Serialization Pattern
558
+
559
+ Each module handles its own state serialization:
560
+
561
+ ```typescript
562
+ // In GameScene
563
+ private getGameState(): object {
564
+ return {
565
+ player: this.player.getState(),
566
+ score: this.scoreSystem.getState(),
567
+ level: this.currentLevel,
568
+ timestamp: Date.now(),
569
+ }
570
+ }
571
+
572
+ private loadGameState(state: any): void {
573
+ if (state.player) this.player.loadState(state.player)
574
+ if (state.score) this.scoreSystem.loadState(state.score)
575
+ if (typeof state.level === 'number') this.currentLevel = state.level
576
+ }
577
+
578
+ private saveGameState(): void {
579
+ window.FarcadeSDK?.singlePlayer.actions.saveGameState({
580
+ gameState: this.getGameState()
581
+ })
582
+ }
583
+ ```
584
+
585
+ ### Adding New Features
586
+
587
+ When adding new game features:
588
+
589
+ 1. **Create an object class** if it's a visible entity (e.g., `src/objects/PowerUp.ts`)
590
+ 2. **Create a system** if it's cross-cutting logic (e.g., `src/systems/PowerUpSystem.ts`)
591
+ 3. **Add configuration** to `GameSettings.ts` for tunable values
592
+ 4. **Wire it up** in the scene's `create()` and `update()` methods
593
+ 5. **Add state methods** (`getState()`, `loadState()`) for persistence
594
+
595
+ ## Build & Production
596
+
597
+ ### Development
598
+
599
+ ```bash
600
+ pnpm dev # Start dev server with dashboard
601
+ ```
602
+
603
+ ### Production Build
604
+
605
+ ```bash
606
+ pnpm build # Creates dist/index.html (single file)
607
+ pnpm preview # Preview production build
608
+ ```
609
+
610
+ ### Production Notes
611
+
612
+ - **Single HTML file** - All assets inlined for Farcaster
613
+ - **No SDK mock** - Real SDK provided by Remix platform
614
+ - **CDN libraries** - Phaser/SDK loaded from CDN (not bundled)
615
+
616
+ ## Common Patterns
617
+
618
+ ### Checking SDK Availability
619
+
620
+ ```typescript
621
+ // Always guard SDK calls
622
+ if (window.FarcadeSDK?.singlePlayer?.actions?.ready) {
623
+ const gameInfo = await window.FarcadeSDK.singlePlayer.actions.ready()
624
+ }
625
+
626
+ // For hasItem checks
627
+ const hasReward = window.FarcadeSDK?.hasItem('reward-slug') ?? false
628
+ ```
629
+
630
+ ### Game Over Flow
631
+
632
+ ```typescript
633
+ private triggerGameOver(): void {
634
+ if (!window.FarcadeSDK) return
635
+
636
+ // Single player
637
+ window.FarcadeSDK.singlePlayer.actions.gameOver({
638
+ score: this.score
639
+ })
640
+
641
+ // Platform shows game over screen
642
+ // User can tap "Play Again" which triggers 'play_again' event
643
+ }
644
+ ```
645
+
646
+ ### Color/Theme from Player Data
647
+
648
+ ```typescript
649
+ // Players have profile images
650
+ const player = gameInfo.player
651
+ if (player.imageUrl) {
652
+ this.load.image('playerAvatar', player.imageUrl)
653
+ }
654
+
655
+ // Use player name for display
656
+ this.add.text(100, 100, `Welcome, ${player.name}!`)
657
+ ```
658
+
659
+ ## Troubleshooting
660
+
661
+ ### "Cannot find module 'phaser'"
662
+
663
+ Phaser is loaded via CDN, not npm. Use the global `Phaser` constant.
664
+
665
+ ### SDK methods not working
666
+
667
+ 1. Ensure you awaited `ready()` first
668
+ 2. Check `window.FarcadeSDK` exists before calling methods
669
+ 3. In dev mode, the mock SDK handles everything
670
+
671
+ ### State not persisting
672
+
673
+ 1. Always use SDK methods, never localStorage directly
674
+ 2. Check `.remix/current-state.json` to see saved state
675
+ 3. Clear state with "Reset State" in Game State Panel
676
+
677
+ ### hasItem() returning false
678
+
679
+ 1. Check `.remix/boost-config.json` has the tier in `purchasedItems`
680
+ 2. Verify the reward is listed under that tier in `tierRewards`
681
+ 3. Reward names are slugified: "My Reward" → `my-reward`
@@ -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)