@littlepartytime/dev-kit 1.11.0 → 1.12.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.
package/dist/webapp/App.tsx
CHANGED
|
@@ -14,15 +14,25 @@ export default function App() {
|
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
return (
|
|
17
|
-
<div
|
|
17
|
+
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
18
18
|
{/* Nav */}
|
|
19
|
-
<nav
|
|
20
|
-
<span
|
|
19
|
+
<nav style={{ background: '#18181b', borderBottom: '1px solid #27272a', padding: '8px 16px', display: 'flex', gap: 16, alignItems: 'center' }}>
|
|
20
|
+
<span style={{ color: '#f59e0b', fontWeight: 700, marginRight: 16 }}>LPT Dev Kit</span>
|
|
21
21
|
{(['preview', 'play', 'debug'] as Page[]).map((p) => (
|
|
22
22
|
<button
|
|
23
23
|
key={p}
|
|
24
24
|
onClick={() => { setPage(p); history.pushState(null, '', `/${p}`); }}
|
|
25
|
-
className=
|
|
25
|
+
className="dk-nav-btn"
|
|
26
|
+
style={{
|
|
27
|
+
padding: '4px 12px',
|
|
28
|
+
borderRadius: 4,
|
|
29
|
+
border: 'none',
|
|
30
|
+
cursor: 'pointer',
|
|
31
|
+
fontSize: 14,
|
|
32
|
+
...(page === p
|
|
33
|
+
? { background: '#d97706', color: '#fff' }
|
|
34
|
+
: { background: 'transparent', color: '#a1a1aa' }),
|
|
35
|
+
}}
|
|
26
36
|
>
|
|
27
37
|
{p.charAt(0).toUpperCase() + p.slice(1)}
|
|
28
38
|
</button>
|
|
@@ -30,7 +40,7 @@ export default function App() {
|
|
|
30
40
|
</nav>
|
|
31
41
|
|
|
32
42
|
{/* Content */}
|
|
33
|
-
<main
|
|
43
|
+
<main style={{ flex: 1, padding: 16 }}>
|
|
34
44
|
{page === 'preview' && <Preview />}
|
|
35
45
|
{page === 'play' && <Play />}
|
|
36
46
|
{page === 'debug' && <Debug />}
|
|
@@ -40,15 +40,14 @@ export default function PhoneFrame({ children }: { children: React.ReactNode })
|
|
|
40
40
|
return (
|
|
41
41
|
<div
|
|
42
42
|
ref={containerRef}
|
|
43
|
-
|
|
44
|
-
style={{ flex: 1, minWidth: 0, minHeight: 0 }}
|
|
43
|
+
style={{ flex: 1, minWidth: 0, minHeight: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', overflow: 'hidden' }}
|
|
45
44
|
>
|
|
46
45
|
{/* Wrapper sized to the scaled phone for correct layout flow */}
|
|
47
46
|
<div style={{ width: BODY_W * scale, height: BODY_H * scale, flexShrink: 0 }}>
|
|
48
47
|
{/* Phone body at original pixel size, visually scaled */}
|
|
49
48
|
<div
|
|
50
|
-
className="relative"
|
|
51
49
|
style={{
|
|
50
|
+
position: 'relative',
|
|
52
51
|
width: BODY_W,
|
|
53
52
|
height: BODY_H,
|
|
54
53
|
transform: `scale(${scale})`,
|
|
@@ -57,8 +56,9 @@ export default function PhoneFrame({ children }: { children: React.ReactNode })
|
|
|
57
56
|
>
|
|
58
57
|
{/* Frame / bezel */}
|
|
59
58
|
<div
|
|
60
|
-
className="absolute inset-0"
|
|
61
59
|
style={{
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
inset: 0,
|
|
62
62
|
borderRadius: OUTER_R,
|
|
63
63
|
background: '#1c1c1e',
|
|
64
64
|
boxShadow:
|
|
@@ -68,8 +68,10 @@ export default function PhoneFrame({ children }: { children: React.ReactNode })
|
|
|
68
68
|
|
|
69
69
|
{/* Screen */}
|
|
70
70
|
<div
|
|
71
|
-
className="absolute overflow-hidden bg-black"
|
|
72
71
|
style={{
|
|
72
|
+
position: 'absolute',
|
|
73
|
+
overflow: 'hidden',
|
|
74
|
+
background: '#000',
|
|
73
75
|
top: BEZEL,
|
|
74
76
|
left: BEZEL,
|
|
75
77
|
width: SCREEN_W,
|
|
@@ -82,8 +84,9 @@ export default function PhoneFrame({ children }: { children: React.ReactNode })
|
|
|
82
84
|
contain:paint makes this the containing block for
|
|
83
85
|
position:fixed elements inside the game. */}
|
|
84
86
|
<div
|
|
85
|
-
className="absolute overflow-hidden"
|
|
86
87
|
style={{
|
|
88
|
+
position: 'absolute',
|
|
89
|
+
overflow: 'hidden',
|
|
87
90
|
top: SAFE_AREA_TOP,
|
|
88
91
|
left: 0,
|
|
89
92
|
right: 0,
|
|
@@ -97,14 +100,26 @@ export default function PhoneFrame({ children }: { children: React.ReactNode })
|
|
|
97
100
|
|
|
98
101
|
{/* Dynamic Island */}
|
|
99
102
|
<div
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
style={{
|
|
104
|
+
position: 'absolute',
|
|
105
|
+
left: '50%',
|
|
106
|
+
transform: 'translateX(-50%)',
|
|
107
|
+
background: '#000',
|
|
108
|
+
borderRadius: 9999,
|
|
109
|
+
top: BEZEL + 11,
|
|
110
|
+
width: 126,
|
|
111
|
+
height: 37,
|
|
112
|
+
zIndex: 10,
|
|
113
|
+
}}
|
|
102
114
|
/>
|
|
103
115
|
|
|
104
116
|
{/* Home Indicator */}
|
|
105
117
|
<div
|
|
106
|
-
className="absolute left-1/2 -translate-x-1/2 rounded-full"
|
|
107
118
|
style={{
|
|
119
|
+
position: 'absolute',
|
|
120
|
+
left: '50%',
|
|
121
|
+
transform: 'translateX(-50%)',
|
|
122
|
+
borderRadius: 9999,
|
|
108
123
|
bottom: BEZEL + 8,
|
|
109
124
|
width: 134,
|
|
110
125
|
height: 5,
|
package/dist/webapp/index.html
CHANGED
|
@@ -4,9 +4,22 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>LPT Dev Kit</title>
|
|
7
|
-
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
7
|
<style>
|
|
9
|
-
|
|
8
|
+
/* ── Dev-Kit UI base ── */
|
|
9
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
10
|
+
body { background: #0a0a0a; color: #e5e5e5; font-family: system-ui, -apple-system, sans-serif; }
|
|
11
|
+
|
|
12
|
+
/* ── Interactive states (can't be expressed as inline styles) ── */
|
|
13
|
+
.dk-nav-btn:hover { color: #fff; }
|
|
14
|
+
.dk-btn-amber:hover { background: #f59e0b; }
|
|
15
|
+
.dk-btn-zinc:hover { background: #52525b; }
|
|
16
|
+
.dk-btn-green:hover { background: #16a34a; }
|
|
17
|
+
.dk-player-btn { transition: background 0.15s; }
|
|
18
|
+
.dk-player-btn:hover { background: #3f3f46; }
|
|
19
|
+
.dk-resize-bar .dk-resize-line { transition: background 0.15s; }
|
|
20
|
+
.dk-resize-bar:hover .dk-resize-line { background: #71717a; }
|
|
21
|
+
.dk-input:focus { outline: none; border-color: #71717a; }
|
|
22
|
+
.dk-btn-amber:disabled, .dk-btn-green:disabled { opacity: 0.5; }
|
|
10
23
|
</style>
|
|
11
24
|
</head>
|
|
12
25
|
<body>
|
|
@@ -18,20 +18,22 @@ export default function Debug() {
|
|
|
18
18
|
return () => { sock.disconnect(); };
|
|
19
19
|
}, []);
|
|
20
20
|
|
|
21
|
+
const panelStyle: React.CSSProperties = { background: '#18181b', borderRadius: 8, padding: 16, overflow: 'auto' };
|
|
22
|
+
|
|
21
23
|
return (
|
|
22
|
-
<div
|
|
24
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, height: 'calc(100vh - 80px)' }}>
|
|
23
25
|
{/* Room State */}
|
|
24
|
-
<div
|
|
25
|
-
<h2
|
|
26
|
-
<pre
|
|
26
|
+
<div style={panelStyle}>
|
|
27
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, color: '#fbbf24' }}>Room State</h2>
|
|
28
|
+
<pre style={{ fontSize: 11, fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
|
|
27
29
|
{JSON.stringify(room, null, 2)}
|
|
28
30
|
</pre>
|
|
29
31
|
</div>
|
|
30
32
|
|
|
31
33
|
{/* Game State */}
|
|
32
|
-
<div
|
|
33
|
-
<h2
|
|
34
|
-
<pre
|
|
34
|
+
<div style={panelStyle}>
|
|
35
|
+
<h2 style={{ fontSize: 18, fontWeight: 700, marginBottom: 16, color: '#fbbf24' }}>Full Game State</h2>
|
|
36
|
+
<pre style={{ fontSize: 11, fontFamily: 'monospace', whiteSpace: 'pre-wrap' }}>
|
|
35
37
|
{fullState ? JSON.stringify(fullState, null, 2) : 'No game in progress'}
|
|
36
38
|
</pre>
|
|
37
39
|
</div>
|
|
@@ -2,6 +2,10 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|
|
2
2
|
import { io, Socket } from 'socket.io-client';
|
|
3
3
|
import PhoneFrame from '../components/PhoneFrame';
|
|
4
4
|
|
|
5
|
+
const card: React.CSSProperties = { background: '#18181b', borderRadius: 8, padding: 24 };
|
|
6
|
+
const inputStyle: React.CSSProperties = { width: '100%', background: '#27272a', border: '1px solid #3f3f46', borderRadius: 4, padding: '8px 12px', marginBottom: 16, color: '#e5e5e5', fontSize: 14 };
|
|
7
|
+
const btnAmber: React.CSSProperties = { width: '100%', background: '#d97706', color: '#fff', border: 'none', padding: '8px 0', borderRadius: 4, fontWeight: 600, cursor: 'pointer', fontSize: 14 };
|
|
8
|
+
|
|
5
9
|
export default function Play() {
|
|
6
10
|
const [socket, setSocket] = useState<Socket | null>(null);
|
|
7
11
|
const [nickname, setNickname] = useState('');
|
|
@@ -61,20 +65,22 @@ export default function Play() {
|
|
|
61
65
|
|
|
62
66
|
if (!joined) {
|
|
63
67
|
return (
|
|
64
|
-
<div
|
|
65
|
-
<div
|
|
66
|
-
<h2
|
|
68
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '60vh' }}>
|
|
69
|
+
<div style={{ ...card, width: 320 }}>
|
|
70
|
+
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 16 }}>Join Game</h2>
|
|
67
71
|
<input
|
|
68
72
|
type="text"
|
|
69
73
|
placeholder="Your nickname"
|
|
70
74
|
value={nickname}
|
|
71
75
|
onChange={(e) => setNickname(e.target.value)}
|
|
72
76
|
onKeyDown={(e) => e.key === 'Enter' && join()}
|
|
73
|
-
className="
|
|
77
|
+
className="dk-input"
|
|
78
|
+
style={inputStyle}
|
|
74
79
|
/>
|
|
75
80
|
<button
|
|
76
81
|
onClick={join}
|
|
77
|
-
className="
|
|
82
|
+
className="dk-btn-amber"
|
|
83
|
+
style={btnAmber}
|
|
78
84
|
>
|
|
79
85
|
Join
|
|
80
86
|
</button>
|
|
@@ -85,23 +91,35 @@ export default function Play() {
|
|
|
85
91
|
|
|
86
92
|
if (room.phase === 'lobby' || room.phase === 'ready') {
|
|
87
93
|
return (
|
|
88
|
-
<div
|
|
89
|
-
<div
|
|
90
|
-
<h2
|
|
91
|
-
<div
|
|
94
|
+
<div style={{ maxWidth: 448, margin: '32px auto 0' }}>
|
|
95
|
+
<div style={card}>
|
|
96
|
+
<h2 style={{ fontSize: 20, fontWeight: 700, marginBottom: 16 }}>Lobby</h2>
|
|
97
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 24 }}>
|
|
92
98
|
{room.players.map((p: any) => (
|
|
93
|
-
<div key={p.id}
|
|
99
|
+
<div key={p.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: '#27272a', borderRadius: 4, padding: '8px 12px' }}>
|
|
94
100
|
<span>{p.nickname} {p.isHost && '(Host)'}</span>
|
|
95
|
-
<span
|
|
101
|
+
<span style={{ color: p.ready ? '#4ade80' : '#71717a' }}>
|
|
96
102
|
{p.ready ? 'Ready' : 'Not Ready'}
|
|
97
103
|
</span>
|
|
98
104
|
</div>
|
|
99
105
|
))}
|
|
100
106
|
</div>
|
|
101
|
-
<div
|
|
107
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
102
108
|
<button
|
|
103
109
|
onClick={() => socket?.emit('player:ready', !isReady)}
|
|
104
|
-
className={
|
|
110
|
+
className={isReady ? 'dk-btn-zinc' : 'dk-btn-green'}
|
|
111
|
+
style={{
|
|
112
|
+
flex: 1,
|
|
113
|
+
padding: '8px 0',
|
|
114
|
+
borderRadius: 4,
|
|
115
|
+
fontWeight: 600,
|
|
116
|
+
border: 'none',
|
|
117
|
+
cursor: 'pointer',
|
|
118
|
+
fontSize: 14,
|
|
119
|
+
...(isReady
|
|
120
|
+
? { background: '#3f3f46', color: '#d4d4d8' }
|
|
121
|
+
: { background: '#16a34a', color: '#fff' }),
|
|
122
|
+
}}
|
|
105
123
|
>
|
|
106
124
|
{isReady ? 'Cancel Ready' : 'Ready'}
|
|
107
125
|
</button>
|
|
@@ -109,7 +127,8 @@ export default function Play() {
|
|
|
109
127
|
<button
|
|
110
128
|
onClick={() => socket?.emit('game:start')}
|
|
111
129
|
disabled={!room.players.every((p: any) => p.ready) || room.players.length < 2}
|
|
112
|
-
className="
|
|
130
|
+
className="dk-btn-amber"
|
|
131
|
+
style={{ ...btnAmber, flex: 1, width: 'auto' }}
|
|
113
132
|
>
|
|
114
133
|
Start Game
|
|
115
134
|
</button>
|
|
@@ -122,19 +141,20 @@ export default function Play() {
|
|
|
122
141
|
|
|
123
142
|
// Playing or ended
|
|
124
143
|
return (
|
|
125
|
-
<div
|
|
144
|
+
<div style={{ height: 'calc(100vh - 80px)', position: 'relative' }}>
|
|
126
145
|
<PhoneFrame>
|
|
127
146
|
{GameRenderer && platform && gameState ? (
|
|
128
147
|
<GameRenderer platform={platform} state={gameState} />
|
|
129
148
|
) : (
|
|
130
|
-
<div
|
|
149
|
+
<div style={{ padding: 16, color: '#71717a' }}>Loading game...</div>
|
|
131
150
|
)}
|
|
132
151
|
</PhoneFrame>
|
|
133
152
|
{room.phase === 'ended' && isHost && (
|
|
134
|
-
<div
|
|
153
|
+
<div style={{ position: 'absolute', bottom: 16, left: '50%', transform: 'translateX(-50%)', zIndex: 9999 }}>
|
|
135
154
|
<button
|
|
136
155
|
onClick={() => socket?.emit('game:playAgain')}
|
|
137
|
-
className="
|
|
156
|
+
className="dk-btn-amber"
|
|
157
|
+
style={{ ...btnAmber, width: 'auto', padding: '8px 24px', borderRadius: 9999 }}
|
|
138
158
|
>
|
|
139
159
|
Play Again
|
|
140
160
|
</button>
|
|
@@ -3,12 +3,20 @@ import PhoneFrame from '../components/PhoneFrame';
|
|
|
3
3
|
|
|
4
4
|
const PLAYER_NAMES = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank', 'Grace', 'Heidi'];
|
|
5
5
|
|
|
6
|
+
/* ── shared inline-style helpers ── */
|
|
7
|
+
const card: React.CSSProperties = { background: '#18181b', borderRadius: 8, padding: 12 };
|
|
8
|
+
const label: React.CSSProperties = { fontSize: 13, fontWeight: 700, color: '#a1a1aa', marginBottom: 8 };
|
|
9
|
+
const inputBase: React.CSSProperties = { width: '100%', background: '#27272a', border: '1px solid #3f3f46', borderRadius: 4, padding: '4px 8px', fontSize: 13, color: '#e5e5e5' };
|
|
10
|
+
const btnAmber: React.CSSProperties = { background: '#d97706', color: '#fff', border: 'none', padding: '4px 12px', borderRadius: 4, fontSize: 13, cursor: 'pointer' };
|
|
11
|
+
const btnZinc: React.CSSProperties = { background: '#3f3f46', color: '#fff', border: 'none', padding: '4px 12px', borderRadius: 4, fontSize: 13, cursor: 'pointer', width: '100%' };
|
|
12
|
+
|
|
6
13
|
export default function Preview() {
|
|
7
|
-
const [playerCount, setPlayerCount] = useState(
|
|
14
|
+
const [playerCount, setPlayerCount] = useState<number | null>(null);
|
|
8
15
|
const [playerIndex, setPlayerIndex] = useState(0);
|
|
9
16
|
const [actions, setActions] = useState<any[]>([]);
|
|
10
17
|
const [GameRenderer, setGameRenderer] = useState<React.ComponentType<any> | null>(null);
|
|
11
18
|
const [engine, setEngine] = useState<any>(null);
|
|
19
|
+
const [config, setConfig] = useState<{ minPlayers?: number; maxPlayers?: number } | null>(null);
|
|
12
20
|
const [fullState, setFullState] = useState<any>(null);
|
|
13
21
|
const [viewState, setViewState] = useState<any>(null);
|
|
14
22
|
const [gameOver, setGameOver] = useState(false);
|
|
@@ -24,6 +32,7 @@ export default function Preview() {
|
|
|
24
32
|
|
|
25
33
|
// Generate mock players
|
|
26
34
|
const mockPlayers = useMemo(() => {
|
|
35
|
+
if (playerCount === null) return [];
|
|
27
36
|
return Array.from({ length: playerCount }, (_, i) => ({
|
|
28
37
|
id: `player-${i + 1}`,
|
|
29
38
|
nickname: PLAYER_NAMES[i] || `Player ${i + 1}`,
|
|
@@ -35,15 +44,25 @@ export default function Preview() {
|
|
|
35
44
|
const mockPlayersRef = useRef(mockPlayers);
|
|
36
45
|
mockPlayersRef.current = mockPlayers;
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
const minPlayers = config?.minPlayers ?? 2;
|
|
48
|
+
const maxPlayers = config?.maxPlayers ?? 32;
|
|
49
|
+
|
|
50
|
+
// Load renderer, engine, and config dynamically
|
|
39
51
|
useEffect(() => {
|
|
40
52
|
import('/src/index.ts').then((mod) => {
|
|
41
53
|
setGameRenderer(() => mod.Renderer || mod.default);
|
|
42
54
|
if (mod.engine) {
|
|
43
55
|
setEngine(mod.engine);
|
|
44
56
|
}
|
|
57
|
+
if (mod.config) {
|
|
58
|
+
setConfig(mod.config);
|
|
59
|
+
setPlayerCount(mod.config.minPlayers ?? 3);
|
|
60
|
+
} else {
|
|
61
|
+
setPlayerCount(3);
|
|
62
|
+
}
|
|
45
63
|
}).catch((err) => {
|
|
46
64
|
console.error('Failed to load game module:', err);
|
|
65
|
+
setPlayerCount(3);
|
|
47
66
|
// Fallback: try loading renderer directly
|
|
48
67
|
import('/src/renderer.tsx').then((mod) => {
|
|
49
68
|
setGameRenderer(() => mod.default || mod.Renderer);
|
|
@@ -55,7 +74,7 @@ export default function Preview() {
|
|
|
55
74
|
|
|
56
75
|
// Initialize game when engine loads or player count changes
|
|
57
76
|
useEffect(() => {
|
|
58
|
-
if (!engine) return;
|
|
77
|
+
if (!engine || mockPlayers.length === 0) return;
|
|
59
78
|
const initialState = engine.init(mockPlayers);
|
|
60
79
|
setFullState(initialState);
|
|
61
80
|
setGameOver(false);
|
|
@@ -159,7 +178,7 @@ export default function Preview() {
|
|
|
159
178
|
|
|
160
179
|
// Clamp playerIndex when playerCount decreases
|
|
161
180
|
useEffect(() => {
|
|
162
|
-
if (playerIndex >= playerCount) {
|
|
181
|
+
if (playerCount !== null && playerIndex >= playerCount) {
|
|
163
182
|
setPlayerIndex(0);
|
|
164
183
|
}
|
|
165
184
|
}, [playerCount, playerIndex]);
|
|
@@ -183,14 +202,14 @@ export default function Preview() {
|
|
|
183
202
|
}, []);
|
|
184
203
|
|
|
185
204
|
return (
|
|
186
|
-
<div
|
|
205
|
+
<div style={{ display: 'flex', gap: 16, height: 'calc(100vh - 80px)' }}>
|
|
187
206
|
{/* Renderer — half the screen width */}
|
|
188
|
-
<div
|
|
207
|
+
<div style={{ width: '50%', height: '100%' }}>
|
|
189
208
|
<PhoneFrame>
|
|
190
209
|
{GameRenderer && platform && viewState ? (
|
|
191
210
|
<GameRenderer platform={platform} state={viewState} />
|
|
192
211
|
) : (
|
|
193
|
-
<div
|
|
212
|
+
<div style={{ padding: 16, color: '#71717a' }}>
|
|
194
213
|
{!engine ? 'Loading engine...' : 'Initializing game...'}
|
|
195
214
|
</div>
|
|
196
215
|
)}
|
|
@@ -198,26 +217,27 @@ export default function Preview() {
|
|
|
198
217
|
</div>
|
|
199
218
|
|
|
200
219
|
{/* Control Panel — fills remaining width, resizable two-column */}
|
|
201
|
-
<div ref={panelRef}
|
|
220
|
+
<div ref={panelRef} style={{ flex: 1, minWidth: 0, display: 'flex', height: '100%' }}>
|
|
202
221
|
{/* Left column: Players & Controls */}
|
|
203
|
-
<div
|
|
222
|
+
<div style={{ width: `${splitRatio * 100}%`, display: 'flex', flexDirection: 'column', gap: 16, overflow: 'auto', paddingRight: 4 }}>
|
|
204
223
|
{/* Player Count */}
|
|
205
|
-
<div
|
|
206
|
-
<h3
|
|
224
|
+
<div style={card}>
|
|
225
|
+
<h3 style={label}>Player Count</h3>
|
|
207
226
|
<input
|
|
208
227
|
type="number"
|
|
209
|
-
min={
|
|
210
|
-
max={
|
|
211
|
-
value={playerCount}
|
|
212
|
-
onChange={(e) => setPlayerCount(Math.max(
|
|
213
|
-
className="
|
|
228
|
+
min={minPlayers}
|
|
229
|
+
max={maxPlayers}
|
|
230
|
+
value={playerCount ?? ''}
|
|
231
|
+
onChange={(e) => setPlayerCount(Math.max(minPlayers, Math.min(maxPlayers, Number(e.target.value))))}
|
|
232
|
+
className="dk-input"
|
|
233
|
+
style={inputBase}
|
|
214
234
|
/>
|
|
215
235
|
</div>
|
|
216
236
|
|
|
217
237
|
{/* Player Switcher */}
|
|
218
|
-
<div
|
|
219
|
-
<h3
|
|
220
|
-
<div
|
|
238
|
+
<div style={{ ...card, flex: 1, overflow: 'auto' }}>
|
|
239
|
+
<h3 style={label}>Current Player</h3>
|
|
240
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
221
241
|
{mockPlayers.map((p, i) => {
|
|
222
242
|
const isActive = i === playerIndex;
|
|
223
243
|
const hue = (i * 137) % 360; // deterministic color per player
|
|
@@ -245,28 +265,49 @@ export default function Preview() {
|
|
|
245
265
|
<button
|
|
246
266
|
key={p.id}
|
|
247
267
|
onClick={() => setPlayerIndex(i)}
|
|
248
|
-
className={
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
268
|
+
className={isActive ? '' : 'dk-player-btn'}
|
|
269
|
+
style={{
|
|
270
|
+
width: '100%',
|
|
271
|
+
display: 'flex',
|
|
272
|
+
alignItems: 'center',
|
|
273
|
+
gap: 8,
|
|
274
|
+
padding: '6px 8px',
|
|
275
|
+
borderRadius: 4,
|
|
276
|
+
fontSize: 13,
|
|
277
|
+
textAlign: 'left' as const,
|
|
278
|
+
border: 'none',
|
|
279
|
+
cursor: 'pointer',
|
|
280
|
+
...(isActive
|
|
281
|
+
? { background: 'rgba(217, 119, 6, 0.2)', boxShadow: 'inset 0 0 0 1px #f59e0b', color: '#fff' }
|
|
282
|
+
: { background: '#27272a', color: '#d4d4d8' }),
|
|
283
|
+
}}
|
|
253
284
|
>
|
|
254
285
|
{/* Avatar */}
|
|
255
286
|
<div
|
|
256
|
-
|
|
257
|
-
|
|
287
|
+
style={{
|
|
288
|
+
width: 24,
|
|
289
|
+
height: 24,
|
|
290
|
+
borderRadius: '50%',
|
|
291
|
+
display: 'flex',
|
|
292
|
+
alignItems: 'center',
|
|
293
|
+
justifyContent: 'center',
|
|
294
|
+
fontSize: 11,
|
|
295
|
+
fontWeight: 700,
|
|
296
|
+
flexShrink: 0,
|
|
297
|
+
background: `hsl(${hue}, 55%, 45%)`,
|
|
298
|
+
}}
|
|
258
299
|
>
|
|
259
300
|
{p.nickname[0]}
|
|
260
301
|
</div>
|
|
261
|
-
<div
|
|
262
|
-
<div
|
|
263
|
-
<span
|
|
302
|
+
<div style={{ minWidth: 0, flex: 1 }}>
|
|
303
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
304
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.nickname}</span>
|
|
264
305
|
{p.isHost && (
|
|
265
|
-
<span
|
|
306
|
+
<span style={{ fontSize: 10, color: '#fbbf24', flexShrink: 0 }}>HOST</span>
|
|
266
307
|
)}
|
|
267
308
|
</div>
|
|
268
309
|
{roleLabel && (
|
|
269
|
-
<div
|
|
310
|
+
<div style={{ fontSize: 10, color: '#71717a', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
270
311
|
{roleLabel}
|
|
271
312
|
</div>
|
|
272
313
|
)}
|
|
@@ -279,14 +320,15 @@ export default function Preview() {
|
|
|
279
320
|
|
|
280
321
|
{/* Game Result */}
|
|
281
322
|
{gameOver && gameResult && (
|
|
282
|
-
<div
|
|
283
|
-
<h3
|
|
284
|
-
<pre
|
|
323
|
+
<div style={card}>
|
|
324
|
+
<h3 style={{ ...label, color: '#4ade80' }}>Game Over</h3>
|
|
325
|
+
<pre style={{ fontSize: 11, fontFamily: 'monospace', background: '#27272a', borderRadius: 4, padding: 8, overflow: 'auto', maxHeight: 128, whiteSpace: 'pre-wrap' }}>
|
|
285
326
|
{JSON.stringify(gameResult, null, 2)}
|
|
286
327
|
</pre>
|
|
287
328
|
<button
|
|
288
329
|
onClick={resetGame}
|
|
289
|
-
className="
|
|
330
|
+
className="dk-btn-amber"
|
|
331
|
+
style={{ ...btnAmber, marginTop: 8, width: '100%' }}
|
|
290
332
|
>
|
|
291
333
|
Reset Game
|
|
292
334
|
</button>
|
|
@@ -295,10 +337,11 @@ export default function Preview() {
|
|
|
295
337
|
|
|
296
338
|
{/* Reset button (when game is not over) */}
|
|
297
339
|
{!gameOver && engine && (
|
|
298
|
-
<div
|
|
340
|
+
<div style={card}>
|
|
299
341
|
<button
|
|
300
342
|
onClick={resetGame}
|
|
301
|
-
className="
|
|
343
|
+
className="dk-btn-zinc"
|
|
344
|
+
style={btnZinc}
|
|
302
345
|
>
|
|
303
346
|
Reset Game
|
|
304
347
|
</button>
|
|
@@ -308,40 +351,43 @@ export default function Preview() {
|
|
|
308
351
|
|
|
309
352
|
{/* Drag handle */}
|
|
310
353
|
<div
|
|
311
|
-
className="
|
|
354
|
+
className="dk-resize-bar"
|
|
355
|
+
style={{ flexShrink: 0, width: 8, cursor: 'col-resize', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
|
312
356
|
onMouseDown={() => { dragging.current = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }}
|
|
313
357
|
>
|
|
314
|
-
<div className="
|
|
358
|
+
<div className="dk-resize-line" style={{ width: 2, height: 32, background: '#3f3f46', borderRadius: 2 }} />
|
|
315
359
|
</div>
|
|
316
360
|
|
|
317
361
|
{/* Right column: State & Logs */}
|
|
318
|
-
<div
|
|
362
|
+
<div style={{ width: `${(1 - splitRatio) * 100}%`, display: 'flex', flexDirection: 'column', gap: 16, overflow: 'auto', paddingLeft: 4 }}>
|
|
319
363
|
{/* State Editor */}
|
|
320
|
-
<div
|
|
321
|
-
<h3
|
|
364
|
+
<div style={{ ...card, flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
|
365
|
+
<h3 style={label}>Game State (Full)</h3>
|
|
322
366
|
<textarea
|
|
323
367
|
value={stateJson}
|
|
324
368
|
onChange={(e) => setStateJson(e.target.value)}
|
|
325
|
-
className="
|
|
369
|
+
className="dk-input"
|
|
370
|
+
style={{ flex: 1, background: '#27272a', border: '1px solid #3f3f46', borderRadius: 4, padding: 8, fontFamily: 'monospace', fontSize: 11, resize: 'none', minHeight: 120, color: '#e5e5e5' }}
|
|
326
371
|
/>
|
|
327
372
|
<button
|
|
328
373
|
onClick={applyState}
|
|
329
|
-
className="
|
|
374
|
+
className="dk-btn-amber"
|
|
375
|
+
style={{ ...btnAmber, marginTop: 8 }}
|
|
330
376
|
>
|
|
331
377
|
Apply State
|
|
332
378
|
</button>
|
|
333
379
|
</div>
|
|
334
380
|
|
|
335
381
|
{/* Action Log */}
|
|
336
|
-
<div
|
|
337
|
-
<h3
|
|
382
|
+
<div style={{ ...card, height: 192, overflow: 'auto' }}>
|
|
383
|
+
<h3 style={label}>Action Log</h3>
|
|
338
384
|
{actions.length === 0 ? (
|
|
339
|
-
<p
|
|
385
|
+
<p style={{ color: '#71717a', fontSize: 11 }}>No actions yet</p>
|
|
340
386
|
) : (
|
|
341
|
-
<div
|
|
387
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
342
388
|
{actions.map((a, i) => (
|
|
343
|
-
<div key={i}
|
|
344
|
-
<span
|
|
389
|
+
<div key={i} style={{ fontSize: 11, fontFamily: 'monospace', background: '#27272a', borderRadius: 4, padding: 4 }}>
|
|
390
|
+
<span style={{ color: '#fbbf24' }}>{a.player}</span>: {JSON.stringify(a.action)}
|
|
345
391
|
</div>
|
|
346
392
|
))}
|
|
347
393
|
</div>
|