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 +0 -0
- package/bin/link.js +0 -0
- package/dist/cli.js +10 -0
- package/dist/scaffold.d.ts +1 -0
- package/dist/scaffold.js +6 -1
- package/package.json +11 -11
- package/templates/base/.remix/boost-config.json +8 -0
- package/templates/base/.remix/saved-states/red-test-save-state-9c0b.json +12 -0
- package/templates/base/.remix/settings.json +7 -0
- package/templates/base/CLAUDE.md.template +681 -0
- package/templates/base/index.html.template +1 -1
- package/templates/base/src/scenes/DemoScene.ts +141 -16
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',
|
package/dist/scaffold.d.ts
CHANGED
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.
|
|
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,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/
|
|
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
|
-
//
|
|
297
|
-
|
|
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)
|