@littlepartytime/dev-kit 1.19.1 → 1.20.0
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/dist/__tests__/engine-loader.test.d.ts +2 -0
- package/dist/__tests__/engine-loader.test.d.ts.map +1 -0
- package/dist/__tests__/engine-loader.test.js +48 -0
- package/dist/__tests__/engine-loader.test.js.map +1 -0
- package/dist/__tests__/games-api.test.d.ts +2 -0
- package/dist/__tests__/games-api.test.d.ts.map +1 -0
- package/dist/__tests__/games-api.test.js +109 -0
- package/dist/__tests__/games-api.test.js.map +1 -0
- package/dist/__tests__/lan-address.test.d.ts +2 -0
- package/dist/__tests__/lan-address.test.d.ts.map +1 -0
- package/dist/__tests__/lan-address.test.js +14 -0
- package/dist/__tests__/lan-address.test.js.map +1 -0
- package/dist/__tests__/zip-manager.test.d.ts +2 -0
- package/dist/__tests__/zip-manager.test.d.ts.map +1 -0
- package/dist/__tests__/zip-manager.test.js +111 -0
- package/dist/__tests__/zip-manager.test.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +11 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/play.d.ts +7 -0
- package/dist/commands/play.d.ts.map +1 -0
- package/dist/commands/play.js +154 -0
- package/dist/commands/play.js.map +1 -0
- package/dist/server/engine-loader.d.ts +5 -0
- package/dist/server/engine-loader.d.ts.map +1 -1
- package/dist/server/engine-loader.js +23 -0
- package/dist/server/engine-loader.js.map +1 -1
- package/dist/server/games-api.d.ts +7 -0
- package/dist/server/games-api.d.ts.map +1 -0
- package/dist/server/games-api.js +189 -0
- package/dist/server/games-api.js.map +1 -0
- package/dist/server/lan-address.d.ts +2 -0
- package/dist/server/lan-address.d.ts.map +1 -0
- package/dist/server/lan-address.js +19 -0
- package/dist/server/lan-address.js.map +1 -0
- package/dist/server/socket-server.d.ts +3 -1
- package/dist/server/socket-server.d.ts.map +1 -1
- package/dist/server/socket-server.js +32 -5
- package/dist/server/socket-server.js.map +1 -1
- package/dist/server/zip-manager.d.ts +24 -0
- package/dist/server/zip-manager.d.ts.map +1 -0
- package/dist/server/zip-manager.js +99 -0
- package/dist/server/zip-manager.js.map +1 -0
- package/dist/webapp/App.tsx +3 -1
- package/dist/webapp/components/GameSelector.tsx +163 -0
- package/dist/webapp/pages/Debug.tsx +3 -1
- package/dist/webapp/pages/Play.tsx +143 -94
- package/dist/webapp/pages/Preview.tsx +273 -219
- 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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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) =>
|
|
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',
|
|
222
|
-
{
|
|
223
|
-
<div style={{
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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:
|
|
337
|
-
height: 24,
|
|
338
|
-
borderRadius: '50%',
|
|
371
|
+
width: '100%',
|
|
339
372
|
display: 'flex',
|
|
340
373
|
alignItems: 'center',
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
{
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
{
|
|
358
|
-
<div style={{
|
|
359
|
-
{
|
|
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
|
-
|
|
363
|
-
|
|
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
|
-
{/*
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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={
|
|
471
|
+
onClick={applyState}
|
|
375
472
|
className="dk-btn-amber"
|
|
376
|
-
style={{ ...btnAmber,
|
|
473
|
+
style={{ ...btnAmber, marginTop: 8 }}
|
|
377
474
|
>
|
|
378
|
-
|
|
475
|
+
Apply State
|
|
379
476
|
</button>
|
|
380
477
|
</div>
|
|
381
|
-
)}
|
|
382
478
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
style={
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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>
|