@littlepartytime/dev-kit 1.19.1 → 1.20.1

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.
Files changed (52) hide show
  1. package/dist/__tests__/engine-loader.test.d.ts +2 -0
  2. package/dist/__tests__/engine-loader.test.d.ts.map +1 -0
  3. package/dist/__tests__/engine-loader.test.js +48 -0
  4. package/dist/__tests__/engine-loader.test.js.map +1 -0
  5. package/dist/__tests__/games-api.test.d.ts +2 -0
  6. package/dist/__tests__/games-api.test.d.ts.map +1 -0
  7. package/dist/__tests__/games-api.test.js +109 -0
  8. package/dist/__tests__/games-api.test.js.map +1 -0
  9. package/dist/__tests__/lan-address.test.d.ts +2 -0
  10. package/dist/__tests__/lan-address.test.d.ts.map +1 -0
  11. package/dist/__tests__/lan-address.test.js +14 -0
  12. package/dist/__tests__/lan-address.test.js.map +1 -0
  13. package/dist/__tests__/zip-manager.test.d.ts +2 -0
  14. package/dist/__tests__/zip-manager.test.d.ts.map +1 -0
  15. package/dist/__tests__/zip-manager.test.js +111 -0
  16. package/dist/__tests__/zip-manager.test.js.map +1 -0
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +19 -0
  19. package/dist/cli.js.map +1 -1
  20. package/dist/commands/dev.d.ts.map +1 -1
  21. package/dist/commands/dev.js +11 -0
  22. package/dist/commands/dev.js.map +1 -1
  23. package/dist/commands/play.d.ts +7 -0
  24. package/dist/commands/play.d.ts.map +1 -0
  25. package/dist/commands/play.js +160 -0
  26. package/dist/commands/play.js.map +1 -0
  27. package/dist/server/engine-loader.d.ts +5 -0
  28. package/dist/server/engine-loader.d.ts.map +1 -1
  29. package/dist/server/engine-loader.js +23 -0
  30. package/dist/server/engine-loader.js.map +1 -1
  31. package/dist/server/games-api.d.ts +7 -0
  32. package/dist/server/games-api.d.ts.map +1 -0
  33. package/dist/server/games-api.js +189 -0
  34. package/dist/server/games-api.js.map +1 -0
  35. package/dist/server/lan-address.d.ts +2 -0
  36. package/dist/server/lan-address.d.ts.map +1 -0
  37. package/dist/server/lan-address.js +19 -0
  38. package/dist/server/lan-address.js.map +1 -0
  39. package/dist/server/socket-server.d.ts +3 -1
  40. package/dist/server/socket-server.d.ts.map +1 -1
  41. package/dist/server/socket-server.js +32 -5
  42. package/dist/server/socket-server.js.map +1 -1
  43. package/dist/server/zip-manager.d.ts +24 -0
  44. package/dist/server/zip-manager.d.ts.map +1 -0
  45. package/dist/server/zip-manager.js +99 -0
  46. package/dist/server/zip-manager.js.map +1 -0
  47. package/dist/webapp/App.tsx +3 -1
  48. package/dist/webapp/components/GameSelector.tsx +161 -0
  49. package/dist/webapp/pages/Debug.tsx +3 -1
  50. package/dist/webapp/pages/Play.tsx +143 -94
  51. package/dist/webapp/pages/Preview.tsx +273 -219
  52. package/package.json +1 -1
@@ -2,6 +2,9 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'
2
2
  import PhoneFrame from '../components/PhoneFrame';
3
3
  import PlatformTakeover from '../components/PlatformTakeover';
4
4
  import { captureScreen, downloadScreenshot } from '../utils/captureScreen';
5
+ import GameSelector from '../components/GameSelector';
6
+
7
+ declare const __DEV_KIT_MODE__: string;
5
8
 
6
9
  const PLAYER_NAMES = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank', 'Grace', 'Heidi'];
7
10
 
@@ -32,6 +35,7 @@ export default function Preview() {
32
35
  const playerIndexRef = useRef(playerIndex);
33
36
  playerIndexRef.current = playerIndex;
34
37
  const stateUpdateListeners = useRef<Set<(...args: unknown[]) => void>>(new Set());
38
+ const [activeGameId, setActiveGameId] = useState<string | null>(null);
35
39
 
36
40
  // Generate mock players
37
41
  const mockPlayers = useMemo(() => {
@@ -50,30 +54,59 @@ export default function Preview() {
50
54
  const minPlayers = config?.minPlayers ?? 2;
51
55
  const maxPlayers = config?.maxPlayers ?? 32;
52
56
 
57
+ // Fetch active game ID on mount (play mode only)
58
+ useEffect(() => {
59
+ if (__DEV_KIT_MODE__ !== 'play') return;
60
+ const fetchActive = async () => {
61
+ try {
62
+ const res = await fetch(`http://${window.location.hostname}:${window.location.port}/api/games`);
63
+ const data = await res.json();
64
+ if (data.activeGameId) setActiveGameId(data.activeGameId);
65
+ } catch { /* ignore */ }
66
+ };
67
+ fetchActive();
68
+ }, []);
69
+
53
70
  // Load renderer, engine, and config dynamically
54
71
  useEffect(() => {
55
- import('/src/index.ts').then((mod) => {
56
- setGameRenderer(() => mod.Renderer || mod.default);
57
- if (mod.engine) {
58
- setEngine(mod.engine);
59
- }
60
- if (mod.config) {
61
- setConfig(mod.config);
62
- setPlayerCount(mod.config.minPlayers ?? 3);
63
- } else {
72
+ if (__DEV_KIT_MODE__ === 'play') {
73
+ if (!activeGameId) return;
74
+ import(/* @vite-ignore */ `virtual:active-game?id=${activeGameId}&t=${Date.now()}`)
75
+ .then((mod) => {
76
+ setGameRenderer(() => mod.Renderer || mod.default);
77
+ if (mod.engine) setEngine(mod.engine);
78
+ if (mod.config) {
79
+ setConfig(mod.config);
80
+ setPlayerCount(mod.config.minPlayers ?? 3);
81
+ } else {
82
+ setPlayerCount(3);
83
+ }
84
+ })
85
+ .catch((err) => {
86
+ console.error('Failed to load game module:', err);
87
+ setPlayerCount(3);
88
+ });
89
+ } else {
90
+ import('/src/index.ts').then((mod) => {
91
+ setGameRenderer(() => mod.Renderer || mod.default);
92
+ if (mod.engine) {
93
+ setEngine(mod.engine);
94
+ }
95
+ if (mod.config) {
96
+ setConfig(mod.config);
97
+ setPlayerCount(mod.config.minPlayers ?? 3);
98
+ } else {
99
+ setPlayerCount(3);
100
+ }
101
+ }).catch((err) => {
102
+ console.error('Failed to load game module:', err);
64
103
  setPlayerCount(3);
65
- }
66
- }).catch((err) => {
67
- console.error('Failed to load game module:', err);
68
- setPlayerCount(3);
69
- // Fallback: try loading renderer directly
70
- import('/src/renderer.tsx').then((mod) => {
71
- setGameRenderer(() => mod.default || mod.Renderer);
72
- }).catch((err2) => {
73
- console.error('Failed to load renderer:', err2);
104
+ import('/src/renderer.tsx').then((mod) => {
105
+ setGameRenderer(() => mod.default || mod.Renderer);
106
+ }).catch(console.error);
74
107
  });
75
- });
76
- }, []);
108
+ }
109
+ }, [activeGameId]);
77
110
 
78
111
  // Initialize game when engine loads or player count changes
79
112
  useEffect(() => {
@@ -143,7 +176,10 @@ export default function Preview() {
143
176
  reportResult: (result: any) => {
144
177
  console.log('Game result reported:', result);
145
178
  },
146
- getAssetUrl: (assetPath: string) => `/assets/${assetPath}`,
179
+ getAssetUrl: (assetPath: string) =>
180
+ __DEV_KIT_MODE__ === 'play'
181
+ ? `/api/games/active/assets/${assetPath}`
182
+ : `/assets/${assetPath}`,
147
183
  getDeviceCapabilities: () => ({ haptics: false, motion: false }),
148
184
  haptic: () => {},
149
185
  onShake: () => () => {},
@@ -151,6 +187,21 @@ export default function Preview() {
151
187
  };
152
188
  }, [engine]);
153
189
 
190
+ const handleGameActivated = useCallback(async () => {
191
+ setGameRenderer(null);
192
+ setEngine(null);
193
+ setFullState(null);
194
+ setViewState(null);
195
+ setGameOver(false);
196
+ setGameResult(null);
197
+ setActions([]);
198
+ try {
199
+ const res = await fetch(`http://${window.location.hostname}:${window.location.port}/api/games`);
200
+ const data = await res.json();
201
+ if (data.activeGameId) setActiveGameId(data.activeGameId);
202
+ } catch { /* ignore */ }
203
+ }, []);
204
+
154
205
  // Apply manual state override from JSON editor
155
206
  const applyState = useCallback(() => {
156
207
  try {
@@ -218,225 +269,228 @@ export default function Preview() {
218
269
  }, [capturing]);
219
270
 
220
271
  return (
221
- <div style={{ display: 'flex', gap: 16, height: 'calc(100vh - 80px)' }}>
222
- {/* Renderer half the screen width */}
223
- <div style={{ width: '50%', height: '100%', position: 'relative' }}>
224
- <PhoneFrame>
225
- {gameOver && gameResult ? (
226
- <PlatformTakeover result={gameResult} players={mockPlayers} onReturn={resetGame} />
227
- ) : GameRenderer && platform && viewState ? (
228
- <GameRenderer key={mockPlayers[playerIndex].id} platform={platform} state={viewState} />
229
- ) : (
230
- <div style={{ padding: 16, color: '#71717a' }}>
231
- {!engine ? 'Loading engine...' : 'Initializing game...'}
232
- </div>
233
- )}
234
- </PhoneFrame>
235
- {/* Screenshot button — floats below the centered phone */}
236
- <button
237
- onClick={handleScreenshot}
238
- disabled={capturing}
239
- title="Capture game screen (without phone frame)"
240
- style={{
241
- position: 'absolute',
242
- bottom: 12,
243
- left: '50%',
244
- transform: 'translateX(-50%)',
245
- display: 'flex',
246
- alignItems: 'center',
247
- gap: 6,
248
- padding: '5px 14px',
249
- borderRadius: 6,
250
- border: '1px solid #3f3f46',
251
- background: capturing ? '#27272a' : '#18181b',
252
- color: capturing ? '#71717a' : '#a1a1aa',
253
- fontSize: 13,
254
- cursor: capturing ? 'default' : 'pointer',
255
- transition: 'background 0.15s, color 0.15s',
256
- zIndex: 10,
257
- }}
258
- >
259
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
260
- <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
261
- <circle cx="12" cy="13" r="4"/>
262
- </svg>
263
- {capturing ? 'Capturing...' : 'Screenshot'}
264
- </button>
265
- </div>
272
+ <div style={{ display: 'flex', flexDirection: 'column', height: 'calc(100vh - 80px)' }}>
273
+ {__DEV_KIT_MODE__ === 'play' && <GameSelector onGameActivated={handleGameActivated} />}
274
+ <div style={{ display: 'flex', gap: 16, flex: 1, minHeight: 0 }}>
275
+ {/* Renderer — half the screen width */}
276
+ <div style={{ width: '50%', height: '100%', position: 'relative' }}>
277
+ <PhoneFrame>
278
+ {gameOver && gameResult ? (
279
+ <PlatformTakeover result={gameResult} players={mockPlayers} onReturn={resetGame} />
280
+ ) : GameRenderer && platform && viewState ? (
281
+ <GameRenderer key={mockPlayers[playerIndex].id} platform={platform} state={viewState} />
282
+ ) : (
283
+ <div style={{ padding: 16, color: '#71717a' }}>
284
+ {!engine ? 'Loading engine...' : 'Initializing game...'}
285
+ </div>
286
+ )}
287
+ </PhoneFrame>
288
+ {/* Screenshot button — floats below the centered phone */}
289
+ <button
290
+ onClick={handleScreenshot}
291
+ disabled={capturing}
292
+ title="Capture game screen (without phone frame)"
293
+ style={{
294
+ position: 'absolute',
295
+ bottom: 12,
296
+ left: '50%',
297
+ transform: 'translateX(-50%)',
298
+ display: 'flex',
299
+ alignItems: 'center',
300
+ gap: 6,
301
+ padding: '5px 14px',
302
+ borderRadius: 6,
303
+ border: '1px solid #3f3f46',
304
+ background: capturing ? '#27272a' : '#18181b',
305
+ color: capturing ? '#71717a' : '#a1a1aa',
306
+ fontSize: 13,
307
+ cursor: capturing ? 'default' : 'pointer',
308
+ transition: 'background 0.15s, color 0.15s',
309
+ zIndex: 10,
310
+ }}
311
+ >
312
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
313
+ <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
314
+ <circle cx="12" cy="13" r="4"/>
315
+ </svg>
316
+ {capturing ? 'Capturing...' : 'Screenshot'}
317
+ </button>
318
+ </div>
266
319
 
267
- {/* Control Panel — fills remaining width, resizable two-column */}
268
- <div ref={panelRef} style={{ flex: 1, minWidth: 0, display: 'flex', height: '100%' }}>
269
- {/* Left column: Players & Controls */}
270
- <div style={{ width: `${splitRatio * 100}%`, display: 'flex', flexDirection: 'column', gap: 16, overflow: 'auto', paddingRight: 4 }}>
271
- {/* Player Count */}
272
- <div style={card}>
273
- <h3 style={label}>Player Count</h3>
274
- <input
275
- type="number"
276
- min={minPlayers}
277
- max={maxPlayers}
278
- value={playerCount ?? ''}
279
- onChange={(e) => setPlayerCount(Math.max(minPlayers, Math.min(maxPlayers, Number(e.target.value))))}
280
- className="dk-input"
281
- style={inputBase}
282
- />
283
- </div>
320
+ {/* Control Panel — fills remaining width, resizable two-column */}
321
+ <div ref={panelRef} style={{ flex: 1, minWidth: 0, display: 'flex', height: '100%' }}>
322
+ {/* Left column: Players & Controls */}
323
+ <div style={{ width: `${splitRatio * 100}%`, display: 'flex', flexDirection: 'column', gap: 16, overflow: 'auto', paddingRight: 4 }}>
324
+ {/* Player Count */}
325
+ <div style={card}>
326
+ <h3 style={label}>Player Count</h3>
327
+ <input
328
+ type="number"
329
+ min={minPlayers}
330
+ max={maxPlayers}
331
+ value={playerCount ?? ''}
332
+ onChange={(e) => setPlayerCount(Math.max(minPlayers, Math.min(maxPlayers, Number(e.target.value))))}
333
+ className="dk-input"
334
+ style={inputBase}
335
+ />
336
+ </div>
284
337
 
285
- {/* Player Switcher */}
286
- <div style={{ ...card, flex: 1, overflow: 'auto' }}>
287
- <h3 style={label}>Current Player</h3>
288
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
289
- {mockPlayers.map((p, i) => {
290
- const isActive = i === playerIndex;
291
- const hue = (i * 137) % 360; // deterministic color per player
292
- const playerState = fullState?.players?.find((ps: any) => ps.id === p.id);
293
- // Fallback chain: 1) PlayerState field 2) GameState.data mapping table
294
- const ROLE_KEYS = ['role', 'character', 'team', 'class', 'job', 'faction', 'type'];
295
- const DATA_MAP_KEYS = ['playerRoles', 'roles', 'playerCharacters', 'characters', 'playerTeams', 'teams'];
296
- let roleLabel: string | undefined;
297
- // Try PlayerState direct field
298
- if (playerState) {
299
- const entry = Object.entries(playerState).find(([k]) => ROLE_KEYS.includes(k.toLowerCase()));
300
- if (entry) roleLabel = String(entry[1]);
301
- }
302
- // Try GameState.data lookup table
303
- if (!roleLabel && fullState?.data) {
304
- for (const mapKey of DATA_MAP_KEYS) {
305
- const map = fullState.data[mapKey];
306
- if (map && typeof map === 'object' && !Array.isArray(map)) {
307
- const val = (map as Record<string, unknown>)[p.id];
308
- if (val != null) { roleLabel = String(val); break; }
338
+ {/* Player Switcher */}
339
+ <div style={{ ...card, flex: 1, overflow: 'auto' }}>
340
+ <h3 style={label}>Current Player</h3>
341
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
342
+ {mockPlayers.map((p, i) => {
343
+ const isActive = i === playerIndex;
344
+ const hue = (i * 137) % 360; // deterministic color per player
345
+ const playerState = fullState?.players?.find((ps: any) => ps.id === p.id);
346
+ // Fallback chain: 1) PlayerState field 2) GameState.data mapping table
347
+ const ROLE_KEYS = ['role', 'character', 'team', 'class', 'job', 'faction', 'type'];
348
+ const DATA_MAP_KEYS = ['playerRoles', 'roles', 'playerCharacters', 'characters', 'playerTeams', 'teams'];
349
+ let roleLabel: string | undefined;
350
+ // Try PlayerState direct field
351
+ if (playerState) {
352
+ const entry = Object.entries(playerState).find(([k]) => ROLE_KEYS.includes(k.toLowerCase()));
353
+ if (entry) roleLabel = String(entry[1]);
354
+ }
355
+ // Try GameState.data lookup table
356
+ if (!roleLabel && fullState?.data) {
357
+ for (const mapKey of DATA_MAP_KEYS) {
358
+ const map = fullState.data[mapKey];
359
+ if (map && typeof map === 'object' && !Array.isArray(map)) {
360
+ const val = (map as Record<string, unknown>)[p.id];
361
+ if (val != null) { roleLabel = String(val); break; }
362
+ }
309
363
  }
310
364
  }
311
- }
312
- return (
313
- <button
314
- key={p.id}
315
- onClick={() => setPlayerIndex(i)}
316
- className={isActive ? '' : 'dk-player-btn'}
317
- style={{
318
- width: '100%',
319
- display: 'flex',
320
- alignItems: 'center',
321
- gap: 8,
322
- padding: '6px 8px',
323
- borderRadius: 4,
324
- fontSize: 13,
325
- textAlign: 'left' as const,
326
- border: 'none',
327
- cursor: 'pointer',
328
- ...(isActive
329
- ? { background: 'rgba(217, 119, 6, 0.2)', boxShadow: 'inset 0 0 0 1px #f59e0b', color: '#fff' }
330
- : { background: '#27272a', color: '#d4d4d8' }),
331
- }}
332
- >
333
- {/* Avatar */}
334
- <div
365
+ return (
366
+ <button
367
+ key={p.id}
368
+ onClick={() => setPlayerIndex(i)}
369
+ className={isActive ? '' : 'dk-player-btn'}
335
370
  style={{
336
- width: 24,
337
- height: 24,
338
- borderRadius: '50%',
371
+ width: '100%',
339
372
  display: 'flex',
340
373
  alignItems: 'center',
341
- justifyContent: 'center',
342
- fontSize: 11,
343
- fontWeight: 700,
344
- flexShrink: 0,
345
- background: `hsl(${hue}, 55%, 45%)`,
374
+ gap: 8,
375
+ padding: '6px 8px',
376
+ borderRadius: 4,
377
+ fontSize: 13,
378
+ textAlign: 'left' as const,
379
+ border: 'none',
380
+ cursor: 'pointer',
381
+ ...(isActive
382
+ ? { background: 'rgba(217, 119, 6, 0.2)', boxShadow: 'inset 0 0 0 1px #f59e0b', color: '#fff' }
383
+ : { background: '#27272a', color: '#d4d4d8' }),
346
384
  }}
347
385
  >
348
- {p.nickname[0]}
349
- </div>
350
- <div style={{ minWidth: 0, flex: 1 }}>
351
- <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
352
- <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.nickname}</span>
353
- {p.isHost && (
354
- <span style={{ fontSize: 10, color: '#fbbf24', flexShrink: 0 }}>HOST</span>
355
- )}
386
+ {/* Avatar */}
387
+ <div
388
+ style={{
389
+ width: 24,
390
+ height: 24,
391
+ borderRadius: '50%',
392
+ display: 'flex',
393
+ alignItems: 'center',
394
+ justifyContent: 'center',
395
+ fontSize: 11,
396
+ fontWeight: 700,
397
+ flexShrink: 0,
398
+ background: `hsl(${hue}, 55%, 45%)`,
399
+ }}
400
+ >
401
+ {p.nickname[0]}
356
402
  </div>
357
- {roleLabel && (
358
- <div style={{ fontSize: 10, color: '#71717a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
359
- {roleLabel}
403
+ <div style={{ minWidth: 0, flex: 1 }}>
404
+ <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
405
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.nickname}</span>
406
+ {p.isHost && (
407
+ <span style={{ fontSize: 10, color: '#fbbf24', flexShrink: 0 }}>HOST</span>
408
+ )}
360
409
  </div>
361
- )}
362
- </div>
363
- </button>
364
- );
365
- })}
410
+ {roleLabel && (
411
+ <div style={{ fontSize: 10, color: '#71717a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
412
+ {roleLabel}
413
+ </div>
414
+ )}
415
+ </div>
416
+ </button>
417
+ );
418
+ })}
419
+ </div>
366
420
  </div>
421
+
422
+ {/* Game Result (shown in PhoneFrame as Platform Takeover) */}
423
+ {gameOver && gameResult && (
424
+ <div style={card}>
425
+ <h3 style={{ ...label, color: '#4ade80' }}>Game Over</h3>
426
+ <button
427
+ onClick={resetGame}
428
+ className="dk-btn-amber"
429
+ style={{ ...btnAmber, width: '100%' }}
430
+ >
431
+ Reset Game
432
+ </button>
433
+ </div>
434
+ )}
435
+
436
+ {/* Reset button (when game is not over) */}
437
+ {!gameOver && engine && (
438
+ <div style={card}>
439
+ <button
440
+ onClick={resetGame}
441
+ className="dk-btn-zinc"
442
+ style={btnZinc}
443
+ >
444
+ Reset Game
445
+ </button>
446
+ </div>
447
+ )}
367
448
  </div>
368
449
 
369
- {/* Game Result (shown in PhoneFrame as Platform Takeover) */}
370
- {gameOver && gameResult && (
371
- <div style={card}>
372
- <h3 style={{ ...label, color: '#4ade80' }}>Game Over</h3>
450
+ {/* Drag handle */}
451
+ <div
452
+ className="dk-resize-bar"
453
+ style={{ flexShrink: 0, width: 8, cursor: 'col-resize', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
454
+ onMouseDown={() => { dragging.current = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }}
455
+ >
456
+ <div className="dk-resize-line" style={{ width: 2, height: 32, background: '#3f3f46', borderRadius: 2 }} />
457
+ </div>
458
+
459
+ {/* Right column: State & Logs */}
460
+ <div style={{ width: `${(1 - splitRatio) * 100}%`, display: 'flex', flexDirection: 'column', gap: 16, overflow: 'auto', paddingLeft: 4 }}>
461
+ {/* State Editor */}
462
+ <div style={{ ...card, flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
463
+ <h3 style={label}>Game State (Full)</h3>
464
+ <textarea
465
+ value={stateJson}
466
+ onChange={(e) => setStateJson(e.target.value)}
467
+ className="dk-input"
468
+ style={{ flex: 1, background: '#27272a', border: '1px solid #3f3f46', borderRadius: 4, padding: 8, fontFamily: 'monospace', fontSize: 11, resize: 'none', minHeight: 120, color: '#e5e5e5' }}
469
+ />
373
470
  <button
374
- onClick={resetGame}
471
+ onClick={applyState}
375
472
  className="dk-btn-amber"
376
- style={{ ...btnAmber, width: '100%' }}
473
+ style={{ ...btnAmber, marginTop: 8 }}
377
474
  >
378
- Reset Game
475
+ Apply State
379
476
  </button>
380
477
  </div>
381
- )}
382
478
 
383
- {/* Reset button (when game is not over) */}
384
- {!gameOver && engine && (
385
- <div style={card}>
386
- <button
387
- onClick={resetGame}
388
- className="dk-btn-zinc"
389
- style={btnZinc}
390
- >
391
- Reset Game
392
- </button>
479
+ {/* Action Log */}
480
+ <div style={{ ...card, height: 192, overflow: 'auto' }}>
481
+ <h3 style={label}>Action Log</h3>
482
+ {actions.length === 0 ? (
483
+ <p style={{ color: '#71717a', fontSize: 11 }}>No actions yet</p>
484
+ ) : (
485
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
486
+ {actions.map((a, i) => (
487
+ <div key={i} style={{ fontSize: 11, fontFamily: 'monospace', background: '#27272a', borderRadius: 4, padding: 4 }}>
488
+ <span style={{ color: '#fbbf24' }}>{a.player}</span>: {JSON.stringify(a.action)}
489
+ </div>
490
+ ))}
491
+ </div>
492
+ )}
393
493
  </div>
394
- )}
395
- </div>
396
-
397
- {/* Drag handle */}
398
- <div
399
- className="dk-resize-bar"
400
- style={{ flexShrink: 0, width: 8, cursor: 'col-resize', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
401
- onMouseDown={() => { dragging.current = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }}
402
- >
403
- <div className="dk-resize-line" style={{ width: 2, height: 32, background: '#3f3f46', borderRadius: 2 }} />
404
- </div>
405
-
406
- {/* Right column: State & Logs */}
407
- <div style={{ width: `${(1 - splitRatio) * 100}%`, display: 'flex', flexDirection: 'column', gap: 16, overflow: 'auto', paddingLeft: 4 }}>
408
- {/* State Editor */}
409
- <div style={{ ...card, flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
410
- <h3 style={label}>Game State (Full)</h3>
411
- <textarea
412
- value={stateJson}
413
- onChange={(e) => setStateJson(e.target.value)}
414
- className="dk-input"
415
- style={{ flex: 1, background: '#27272a', border: '1px solid #3f3f46', borderRadius: 4, padding: 8, fontFamily: 'monospace', fontSize: 11, resize: 'none', minHeight: 120, color: '#e5e5e5' }}
416
- />
417
- <button
418
- onClick={applyState}
419
- className="dk-btn-amber"
420
- style={{ ...btnAmber, marginTop: 8 }}
421
- >
422
- Apply State
423
- </button>
424
- </div>
425
-
426
- {/* Action Log */}
427
- <div style={{ ...card, height: 192, overflow: 'auto' }}>
428
- <h3 style={label}>Action Log</h3>
429
- {actions.length === 0 ? (
430
- <p style={{ color: '#71717a', fontSize: 11 }}>No actions yet</p>
431
- ) : (
432
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
433
- {actions.map((a, i) => (
434
- <div key={i} style={{ fontSize: 11, fontFamily: 'monospace', background: '#27272a', borderRadius: 4, padding: 4 }}>
435
- <span style={{ color: '#fbbf24' }}>{a.player}</span>: {JSON.stringify(a.action)}
436
- </div>
437
- ))}
438
- </div>
439
- )}
440
494
  </div>
441
495
  </div>
442
496
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@littlepartytime/dev-kit",
3
- "version": "1.19.1",
3
+ "version": "1.20.1",
4
4
  "description": "Development toolkit CLI for Little Party Time game developers",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",