@littlepartytime/dev-kit 1.13.2 โ 1.14.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.
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface Ranking {
|
|
4
|
+
playerId: string;
|
|
5
|
+
rank: number;
|
|
6
|
+
score: number;
|
|
7
|
+
isWinner: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface GameResult {
|
|
11
|
+
rankings: Ranking[];
|
|
12
|
+
data?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Player {
|
|
16
|
+
id: string;
|
|
17
|
+
nickname: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
result: GameResult;
|
|
22
|
+
players: Player[];
|
|
23
|
+
onReturn: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MEDAL = ['๐ฅ', '๐ฅ', '๐ฅ'];
|
|
27
|
+
|
|
28
|
+
export default function PlatformTakeover({ result, players, onReturn }: Props) {
|
|
29
|
+
const getName = (playerId: string) =>
|
|
30
|
+
players.find((p) => p.id === playerId)?.nickname ?? playerId;
|
|
31
|
+
|
|
32
|
+
const sorted = [...result.rankings].sort((a, b) => a.rank - b.rank);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
style={{
|
|
37
|
+
width: '100%',
|
|
38
|
+
height: '100%',
|
|
39
|
+
background: 'linear-gradient(180deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%)',
|
|
40
|
+
display: 'flex',
|
|
41
|
+
flexDirection: 'column',
|
|
42
|
+
fontFamily: 'var(--font-body, system-ui, sans-serif)',
|
|
43
|
+
color: '#e2e8f0',
|
|
44
|
+
overflow: 'hidden',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{/* Dev-Kit banner */}
|
|
48
|
+
<div
|
|
49
|
+
style={{
|
|
50
|
+
background: 'rgba(217, 119, 6, 0.15)',
|
|
51
|
+
borderBottom: '1px solid rgba(217, 119, 6, 0.3)',
|
|
52
|
+
padding: '6px 12px',
|
|
53
|
+
fontSize: 11,
|
|
54
|
+
color: '#fbbf24',
|
|
55
|
+
textAlign: 'center',
|
|
56
|
+
flexShrink: 0,
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
โ Dev-Kit ยท Platform Takeover
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Content */}
|
|
63
|
+
<div
|
|
64
|
+
style={{
|
|
65
|
+
flex: 1,
|
|
66
|
+
display: 'flex',
|
|
67
|
+
flexDirection: 'column',
|
|
68
|
+
alignItems: 'center',
|
|
69
|
+
justifyContent: 'center',
|
|
70
|
+
padding: '24px 20px',
|
|
71
|
+
gap: 24,
|
|
72
|
+
overflow: 'auto',
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{/* Title */}
|
|
76
|
+
<div style={{ textAlign: 'center' }}>
|
|
77
|
+
<div style={{ fontSize: 40, marginBottom: 8 }}>๐</div>
|
|
78
|
+
<div
|
|
79
|
+
style={{
|
|
80
|
+
fontSize: 22,
|
|
81
|
+
fontWeight: 700,
|
|
82
|
+
fontFamily: 'var(--font-display, var(--font-body, system-ui, sans-serif))',
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
Game Over
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Rankings */}
|
|
90
|
+
<div
|
|
91
|
+
style={{
|
|
92
|
+
width: '100%',
|
|
93
|
+
maxWidth: 300,
|
|
94
|
+
display: 'flex',
|
|
95
|
+
flexDirection: 'column',
|
|
96
|
+
gap: 6,
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
{sorted.map((r) => (
|
|
100
|
+
<div
|
|
101
|
+
key={r.playerId}
|
|
102
|
+
style={{
|
|
103
|
+
display: 'flex',
|
|
104
|
+
alignItems: 'center',
|
|
105
|
+
gap: 10,
|
|
106
|
+
padding: '10px 14px',
|
|
107
|
+
borderRadius: 8,
|
|
108
|
+
background: r.isWinner
|
|
109
|
+
? 'rgba(250, 204, 21, 0.12)'
|
|
110
|
+
: 'rgba(255, 255, 255, 0.05)',
|
|
111
|
+
border: r.isWinner
|
|
112
|
+
? '1px solid rgba(250, 204, 21, 0.25)'
|
|
113
|
+
: '1px solid rgba(255, 255, 255, 0.06)',
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{/* Rank */}
|
|
117
|
+
<span style={{ fontSize: 18, width: 28, textAlign: 'center', flexShrink: 0 }}>
|
|
118
|
+
{MEDAL[r.rank - 1] ?? `#${r.rank}`}
|
|
119
|
+
</span>
|
|
120
|
+
|
|
121
|
+
{/* Name */}
|
|
122
|
+
<span
|
|
123
|
+
style={{
|
|
124
|
+
flex: 1,
|
|
125
|
+
fontSize: 15,
|
|
126
|
+
fontWeight: r.isWinner ? 600 : 400,
|
|
127
|
+
overflow: 'hidden',
|
|
128
|
+
textOverflow: 'ellipsis',
|
|
129
|
+
whiteSpace: 'nowrap',
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{getName(r.playerId)}
|
|
133
|
+
</span>
|
|
134
|
+
|
|
135
|
+
{/* Score */}
|
|
136
|
+
<span
|
|
137
|
+
style={{
|
|
138
|
+
fontSize: 15,
|
|
139
|
+
fontWeight: 600,
|
|
140
|
+
color: r.isWinner ? '#facc15' : '#94a3b8',
|
|
141
|
+
fontVariantNumeric: 'tabular-nums',
|
|
142
|
+
flexShrink: 0,
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
{r.score}
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Return button */}
|
|
152
|
+
<button
|
|
153
|
+
onClick={onReturn}
|
|
154
|
+
style={{
|
|
155
|
+
marginTop: 8,
|
|
156
|
+
padding: '10px 32px',
|
|
157
|
+
borderRadius: 8,
|
|
158
|
+
border: 'none',
|
|
159
|
+
background: '#d97706',
|
|
160
|
+
color: '#fff',
|
|
161
|
+
fontSize: 15,
|
|
162
|
+
fontWeight: 600,
|
|
163
|
+
cursor: 'pointer',
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
Return to Lobby
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
2
2
|
import { io, Socket } from 'socket.io-client';
|
|
3
3
|
import PhoneFrame from '../components/PhoneFrame';
|
|
4
|
+
import PlatformTakeover from '../components/PlatformTakeover';
|
|
4
5
|
|
|
5
6
|
const card: React.CSSProperties = { background: '#18181b', borderRadius: 8, padding: 24 };
|
|
6
7
|
const inputStyle: React.CSSProperties = { width: '100%', background: '#27272a', border: '1px solid #3f3f46', borderRadius: 4, padding: '8px 12px', marginBottom: 16, color: '#e5e5e5', fontSize: 14 };
|
|
@@ -14,6 +15,7 @@ export default function Play() {
|
|
|
14
15
|
const [gameState, setGameState] = useState<any>(null);
|
|
15
16
|
const [myId, setMyId] = useState<string | null>(null);
|
|
16
17
|
const [GameRenderer, setGameRenderer] = useState<React.ComponentType<any> | null>(null);
|
|
18
|
+
const [gameResult, setGameResult] = useState<any>(null);
|
|
17
19
|
|
|
18
20
|
// Load renderer
|
|
19
21
|
useEffect(() => {
|
|
@@ -32,10 +34,18 @@ export default function Play() {
|
|
|
32
34
|
setJoined(true);
|
|
33
35
|
});
|
|
34
36
|
|
|
35
|
-
sock.on('room:update',
|
|
37
|
+
sock.on('room:update', (r: any) => {
|
|
38
|
+
setRoom(r);
|
|
39
|
+
// Clear game result when room resets to lobby
|
|
40
|
+
if (r.phase === 'lobby') {
|
|
41
|
+
setGameResult(null);
|
|
42
|
+
setGameState(null);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
36
45
|
sock.on('game:state', setGameState);
|
|
37
46
|
sock.on('game:result', (result) => {
|
|
38
47
|
console.log('Game result:', result);
|
|
48
|
+
setGameResult(result);
|
|
39
49
|
});
|
|
40
50
|
|
|
41
51
|
setSocket(sock);
|
|
@@ -45,6 +55,12 @@ export default function Play() {
|
|
|
45
55
|
const isHost = me?.isHost;
|
|
46
56
|
const isReady = me?.ready;
|
|
47
57
|
|
|
58
|
+
const gameOver = room.phase === 'ended' && gameResult !== null;
|
|
59
|
+
|
|
60
|
+
const handleReturn = useCallback(() => {
|
|
61
|
+
socket?.emit('game:playAgain');
|
|
62
|
+
}, [socket]);
|
|
63
|
+
|
|
48
64
|
// Use refs to avoid recreating platform on every room/me change
|
|
49
65
|
const roomRef = useRef(room);
|
|
50
66
|
roomRef.current = room;
|
|
@@ -155,7 +171,13 @@ export default function Play() {
|
|
|
155
171
|
return (
|
|
156
172
|
<div style={{ height: 'calc(100vh - 80px)', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 24 }}>
|
|
157
173
|
<PhoneFrame>
|
|
158
|
-
{
|
|
174
|
+
{gameOver ? (
|
|
175
|
+
<PlatformTakeover
|
|
176
|
+
result={gameResult}
|
|
177
|
+
players={room.players.map((p: any) => ({ id: p.id, nickname: p.nickname }))}
|
|
178
|
+
onReturn={handleReturn}
|
|
179
|
+
/>
|
|
180
|
+
) : GameRenderer && platform && gameState ? (
|
|
159
181
|
<GameRenderer platform={platform} state={gameState} />
|
|
160
182
|
) : (
|
|
161
183
|
<div style={{ padding: 16, color: '#71717a' }}>Loading game...</div>
|
|
@@ -163,15 +185,6 @@ export default function Play() {
|
|
|
163
185
|
</PhoneFrame>
|
|
164
186
|
{isHost && (
|
|
165
187
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, width: 160 }}>
|
|
166
|
-
{room.phase === 'ended' && (
|
|
167
|
-
<button
|
|
168
|
-
onClick={() => socket?.emit('game:playAgain')}
|
|
169
|
-
className="dk-btn-amber"
|
|
170
|
-
style={{ ...btnAmber, width: '100%', padding: '8px 0', borderRadius: 6 }}
|
|
171
|
-
>
|
|
172
|
-
Play Again
|
|
173
|
-
</button>
|
|
174
|
-
)}
|
|
175
188
|
<button
|
|
176
189
|
onClick={() => socket?.emit('game:forceReset')}
|
|
177
190
|
style={{ width: '100%', background: '#d97706', color: '#fff', border: 'none', padding: '8px 0', borderRadius: 6, fontWeight: 600, cursor: 'pointer', fontSize: 13 }}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
2
2
|
import PhoneFrame from '../components/PhoneFrame';
|
|
3
|
+
import PlatformTakeover from '../components/PlatformTakeover';
|
|
3
4
|
|
|
4
5
|
const PLAYER_NAMES = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank', 'Grace', 'Heidi'];
|
|
5
6
|
|
|
@@ -206,7 +207,9 @@ export default function Preview() {
|
|
|
206
207
|
{/* Renderer โ half the screen width */}
|
|
207
208
|
<div style={{ width: '50%', height: '100%' }}>
|
|
208
209
|
<PhoneFrame>
|
|
209
|
-
{
|
|
210
|
+
{gameOver && gameResult ? (
|
|
211
|
+
<PlatformTakeover result={gameResult} players={mockPlayers} onReturn={resetGame} />
|
|
212
|
+
) : GameRenderer && platform && viewState ? (
|
|
210
213
|
<GameRenderer key={mockPlayers[playerIndex].id} platform={platform} state={viewState} />
|
|
211
214
|
) : (
|
|
212
215
|
<div style={{ padding: 16, color: '#71717a' }}>
|
|
@@ -318,17 +321,14 @@ export default function Preview() {
|
|
|
318
321
|
</div>
|
|
319
322
|
</div>
|
|
320
323
|
|
|
321
|
-
{/* Game Result */}
|
|
324
|
+
{/* Game Result (shown in PhoneFrame as Platform Takeover) */}
|
|
322
325
|
{gameOver && gameResult && (
|
|
323
326
|
<div style={card}>
|
|
324
327
|
<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' }}>
|
|
326
|
-
{JSON.stringify(gameResult, null, 2)}
|
|
327
|
-
</pre>
|
|
328
328
|
<button
|
|
329
329
|
onClick={resetGame}
|
|
330
330
|
className="dk-btn-amber"
|
|
331
|
-
style={{ ...btnAmber,
|
|
331
|
+
style={{ ...btnAmber, width: '100%' }}
|
|
332
332
|
>
|
|
333
333
|
Reset Game
|
|
334
334
|
</button>
|