@littlepartytime/dev-kit 1.0.0 → 1.2.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/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/dev.d.ts +12 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +76 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/server/engine-loader.d.ts +11 -0
- package/dist/server/engine-loader.d.ts.map +1 -0
- package/dist/server/engine-loader.js +45 -0
- package/dist/server/engine-loader.js.map +1 -0
- package/dist/server/game-room.d.ts +25 -0
- package/dist/server/game-room.d.ts.map +1 -0
- package/dist/server/game-room.js +78 -0
- package/dist/server/game-room.js.map +1 -0
- package/dist/server/socket-server.d.ts +16 -0
- package/dist/server/socket-server.d.ts.map +1 -0
- package/dist/server/socket-server.js +167 -0
- package/dist/server/socket-server.js.map +1 -0
- package/dist/testing/game-preview.d.ts +53 -0
- package/dist/testing/game-preview.d.ts.map +1 -0
- package/dist/testing/game-preview.js +158 -0
- package/dist/testing/game-preview.js.map +1 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +6 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/webapp/App.d.ts +3 -0
- package/dist/webapp/App.d.ts.map +1 -0
- package/dist/webapp/App.js +70 -0
- package/dist/webapp/App.js.map +1 -0
- package/dist/webapp/App.tsx +40 -0
- package/dist/webapp/index.html +16 -0
- package/dist/webapp/main.d.ts +2 -0
- package/dist/webapp/main.d.ts.map +1 -0
- package/dist/webapp/main.js +12 -0
- package/dist/webapp/main.js.map +1 -0
- package/dist/webapp/main.tsx +9 -0
- package/dist/webapp/pages/Debug.d.ts +3 -0
- package/dist/webapp/pages/Debug.d.ts.map +1 -0
- package/dist/webapp/pages/Debug.js +69 -0
- package/dist/webapp/pages/Debug.js.map +1 -0
- package/dist/webapp/pages/Debug.tsx +40 -0
- package/dist/webapp/pages/Play.d.ts +3 -0
- package/dist/webapp/pages/Play.d.ts.map +1 -0
- package/dist/webapp/pages/Play.js +129 -0
- package/dist/webapp/pages/Play.js.map +1 -0
- package/dist/webapp/pages/Play.tsx +137 -0
- package/dist/webapp/pages/Preview.d.ts +3 -0
- package/dist/webapp/pages/Preview.d.ts.map +1 -0
- package/dist/webapp/pages/Preview.js +118 -0
- package/dist/webapp/pages/Preview.js.map +1 -0
- package/dist/webapp/pages/Preview.tsx +265 -0
- package/package.json +31 -3
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { io, Socket } from 'socket.io-client';
|
|
3
|
+
|
|
4
|
+
export default function Play() {
|
|
5
|
+
const [socket, setSocket] = useState<Socket | null>(null);
|
|
6
|
+
const [nickname, setNickname] = useState('');
|
|
7
|
+
const [joined, setJoined] = useState(false);
|
|
8
|
+
const [room, setRoom] = useState<any>({ players: [], phase: 'lobby' });
|
|
9
|
+
const [gameState, setGameState] = useState<any>(null);
|
|
10
|
+
const [myId, setMyId] = useState<string | null>(null);
|
|
11
|
+
const [GameRenderer, setGameRenderer] = useState<React.ComponentType<any> | null>(null);
|
|
12
|
+
|
|
13
|
+
// Load renderer
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
import('/src/renderer.tsx').then((mod) => {
|
|
16
|
+
setGameRenderer(() => mod.default || mod.Renderer);
|
|
17
|
+
}).catch(console.error);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
const join = useCallback(() => {
|
|
21
|
+
if (!nickname.trim()) return;
|
|
22
|
+
|
|
23
|
+
const sock = io('http://localhost:4001', { query: { nickname } });
|
|
24
|
+
|
|
25
|
+
sock.on('connect', () => {
|
|
26
|
+
setMyId(sock.id);
|
|
27
|
+
setJoined(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
sock.on('room:update', setRoom);
|
|
31
|
+
sock.on('game:state', setGameState);
|
|
32
|
+
sock.on('game:result', (result) => {
|
|
33
|
+
console.log('Game result:', result);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
setSocket(sock);
|
|
37
|
+
}, [nickname]);
|
|
38
|
+
|
|
39
|
+
const me = room.players.find((p: any) => socket && p.socketId === socket.id) || room.players.find((p: any) => p.nickname === nickname);
|
|
40
|
+
const isHost = me?.isHost;
|
|
41
|
+
const isReady = me?.ready;
|
|
42
|
+
|
|
43
|
+
const platform = socket ? {
|
|
44
|
+
getPlayers: () => room.players.map((p: any) => ({ id: p.id, nickname: p.nickname, avatarUrl: null, isHost: p.isHost })),
|
|
45
|
+
getLocalPlayer: () => me ? { id: me.id, nickname: me.nickname, avatarUrl: null, isHost: me.isHost } : { id: '', nickname: '', avatarUrl: null, isHost: false },
|
|
46
|
+
send: (action: any) => socket.emit('game:action', action),
|
|
47
|
+
on: (event: string, handler: Function) => {
|
|
48
|
+
if (event === 'stateUpdate') socket.on('game:state', handler as any);
|
|
49
|
+
},
|
|
50
|
+
off: (event: string, handler: Function) => {
|
|
51
|
+
if (event === 'stateUpdate') socket.off('game:state', handler as any);
|
|
52
|
+
},
|
|
53
|
+
reportResult: () => {},
|
|
54
|
+
} : null;
|
|
55
|
+
|
|
56
|
+
if (!joined) {
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex items-center justify-center h-[60vh]">
|
|
59
|
+
<div className="bg-zinc-900 rounded-lg p-6 w-80">
|
|
60
|
+
<h2 className="text-xl font-bold mb-4">Join Game</h2>
|
|
61
|
+
<input
|
|
62
|
+
type="text"
|
|
63
|
+
placeholder="Your nickname"
|
|
64
|
+
value={nickname}
|
|
65
|
+
onChange={(e) => setNickname(e.target.value)}
|
|
66
|
+
onKeyDown={(e) => e.key === 'Enter' && join()}
|
|
67
|
+
className="w-full bg-zinc-800 border border-zinc-700 rounded px-3 py-2 mb-4"
|
|
68
|
+
/>
|
|
69
|
+
<button
|
|
70
|
+
onClick={join}
|
|
71
|
+
className="w-full bg-amber-600 hover:bg-amber-500 text-white py-2 rounded font-semibold"
|
|
72
|
+
>
|
|
73
|
+
Join
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (room.phase === 'lobby' || room.phase === 'ready') {
|
|
81
|
+
return (
|
|
82
|
+
<div className="max-w-md mx-auto mt-8">
|
|
83
|
+
<div className="bg-zinc-900 rounded-lg p-6">
|
|
84
|
+
<h2 className="text-xl font-bold mb-4">Lobby</h2>
|
|
85
|
+
<div className="space-y-2 mb-6">
|
|
86
|
+
{room.players.map((p: any) => (
|
|
87
|
+
<div key={p.id} className="flex items-center justify-between bg-zinc-800 rounded px-3 py-2">
|
|
88
|
+
<span>{p.nickname} {p.isHost && '(Host)'}</span>
|
|
89
|
+
<span className={p.ready ? 'text-green-400' : 'text-zinc-500'}>
|
|
90
|
+
{p.ready ? 'Ready' : 'Not Ready'}
|
|
91
|
+
</span>
|
|
92
|
+
</div>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
<div className="flex gap-2">
|
|
96
|
+
<button
|
|
97
|
+
onClick={() => socket?.emit('player:ready', !isReady)}
|
|
98
|
+
className={`flex-1 py-2 rounded font-semibold ${isReady ? 'bg-zinc-700 text-zinc-300' : 'bg-green-600 text-white'}`}
|
|
99
|
+
>
|
|
100
|
+
{isReady ? 'Cancel Ready' : 'Ready'}
|
|
101
|
+
</button>
|
|
102
|
+
{isHost && (
|
|
103
|
+
<button
|
|
104
|
+
onClick={() => socket?.emit('game:start')}
|
|
105
|
+
disabled={!room.players.every((p: any) => p.ready) || room.players.length < 2}
|
|
106
|
+
className="flex-1 bg-amber-600 hover:bg-amber-500 disabled:opacity-50 text-white py-2 rounded font-semibold"
|
|
107
|
+
>
|
|
108
|
+
Start Game
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Playing or ended
|
|
118
|
+
return (
|
|
119
|
+
<div className="h-[calc(100vh-80px)]">
|
|
120
|
+
{GameRenderer && platform && gameState ? (
|
|
121
|
+
<GameRenderer platform={platform} state={gameState} />
|
|
122
|
+
) : (
|
|
123
|
+
<div className="p-4 text-zinc-500">Loading game...</div>
|
|
124
|
+
)}
|
|
125
|
+
{room.phase === 'ended' && isHost && (
|
|
126
|
+
<div className="fixed bottom-4 left-1/2 -translate-x-1/2">
|
|
127
|
+
<button
|
|
128
|
+
onClick={() => socket?.emit('game:playAgain')}
|
|
129
|
+
className="bg-amber-600 hover:bg-amber-500 text-white px-6 py-2 rounded-full font-semibold"
|
|
130
|
+
>
|
|
131
|
+
Play Again
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Preview.d.ts","sourceRoot":"","sources":["../../../src/webapp/pages/Preview.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAEhE,MAAM,CAAC,OAAO,UAAU,OAAO,sBA6G9B"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.default = Preview;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
function Preview() {
|
|
39
|
+
const [state, setState] = (0, react_1.useState)({ phase: 'playing', players: [], data: {} });
|
|
40
|
+
const [stateJson, setStateJson] = (0, react_1.useState)('');
|
|
41
|
+
const [playerIndex, setPlayerIndex] = (0, react_1.useState)(0);
|
|
42
|
+
const [actions, setActions] = (0, react_1.useState)([]);
|
|
43
|
+
const [GameRenderer, setGameRenderer] = (0, react_1.useState)(null);
|
|
44
|
+
// Load renderer dynamically
|
|
45
|
+
(0, react_1.useEffect)(() => {
|
|
46
|
+
Promise.resolve().then(() => __importStar(require('/src/renderer.tsx'))).then((mod) => {
|
|
47
|
+
setGameRenderer(() => mod.default || mod.Renderer);
|
|
48
|
+
}).catch((err) => {
|
|
49
|
+
console.error('Failed to load renderer:', err);
|
|
50
|
+
});
|
|
51
|
+
}, []);
|
|
52
|
+
// Mock players
|
|
53
|
+
const mockPlayers = [
|
|
54
|
+
{ id: 'player-1', nickname: 'Alice', avatarUrl: null, isHost: true },
|
|
55
|
+
{ id: 'player-2', nickname: 'Bob', avatarUrl: null, isHost: false },
|
|
56
|
+
{ id: 'player-3', nickname: 'Carol', avatarUrl: null, isHost: false },
|
|
57
|
+
];
|
|
58
|
+
// Mock platform
|
|
59
|
+
const platform = {
|
|
60
|
+
getPlayers: () => mockPlayers,
|
|
61
|
+
getLocalPlayer: () => mockPlayers[playerIndex],
|
|
62
|
+
send: (action) => {
|
|
63
|
+
setActions((prev) => [...prev, { time: new Date().toISOString(), player: mockPlayers[playerIndex].nickname, action }]);
|
|
64
|
+
},
|
|
65
|
+
on: () => { },
|
|
66
|
+
off: () => { },
|
|
67
|
+
reportResult: () => { },
|
|
68
|
+
};
|
|
69
|
+
const updateState = (0, react_1.useCallback)(() => {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(stateJson);
|
|
72
|
+
setState(parsed);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
}, [stateJson]);
|
|
78
|
+
(0, react_1.useEffect)(() => {
|
|
79
|
+
setStateJson(JSON.stringify(state, null, 2));
|
|
80
|
+
}, []);
|
|
81
|
+
return (<div className="flex gap-4 h-[calc(100vh-80px)]">
|
|
82
|
+
{/* Renderer */}
|
|
83
|
+
<div className="flex-1 bg-zinc-900 rounded-lg overflow-auto">
|
|
84
|
+
{GameRenderer ? (<GameRenderer platform={platform} state={state}/>) : (<div className="p-4 text-zinc-500">Loading renderer...</div>)}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Control Panel */}
|
|
88
|
+
<div className="w-80 flex flex-col gap-4">
|
|
89
|
+
{/* Player Switcher */}
|
|
90
|
+
<div className="bg-zinc-900 rounded-lg p-3">
|
|
91
|
+
<h3 className="text-sm font-bold text-zinc-400 mb-2">Current Player</h3>
|
|
92
|
+
<select value={playerIndex} onChange={(e) => setPlayerIndex(Number(e.target.value))} className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1">
|
|
93
|
+
{mockPlayers.map((p, i) => (<option key={p.id} value={i}>{p.nickname}</option>))}
|
|
94
|
+
</select>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* State Editor */}
|
|
98
|
+
<div className="bg-zinc-900 rounded-lg p-3 flex-1 flex flex-col">
|
|
99
|
+
<h3 className="text-sm font-bold text-zinc-400 mb-2">Game State</h3>
|
|
100
|
+
<textarea value={stateJson} onChange={(e) => setStateJson(e.target.value)} className="flex-1 bg-zinc-800 border border-zinc-700 rounded p-2 font-mono text-xs resize-none"/>
|
|
101
|
+
<button onClick={updateState} className="mt-2 bg-amber-600 hover:bg-amber-500 text-white px-3 py-1 rounded text-sm">
|
|
102
|
+
Apply State
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Action Log */}
|
|
107
|
+
<div className="bg-zinc-900 rounded-lg p-3 h-48 overflow-auto">
|
|
108
|
+
<h3 className="text-sm font-bold text-zinc-400 mb-2">Action Log</h3>
|
|
109
|
+
{actions.length === 0 ? (<p className="text-zinc-500 text-xs">No actions yet</p>) : (<div className="space-y-1">
|
|
110
|
+
{actions.map((a, i) => (<div key={i} className="text-xs font-mono bg-zinc-800 rounded p-1">
|
|
111
|
+
<span className="text-amber-400">{a.player}</span>: {JSON.stringify(a.action)}
|
|
112
|
+
</div>))}
|
|
113
|
+
</div>)}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>);
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=Preview.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Preview.js","sourceRoot":"","sources":["../../../src/webapp/pages/Preview.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
const PLAYER_NAMES = ['Alice', 'Bob', 'Carol', 'Dave', 'Eve', 'Frank', 'Grace', 'Heidi'];
|
|
4
|
+
|
|
5
|
+
export default function Preview() {
|
|
6
|
+
const [playerCount, setPlayerCount] = useState(3);
|
|
7
|
+
const [playerIndex, setPlayerIndex] = useState(0);
|
|
8
|
+
const [actions, setActions] = useState<any[]>([]);
|
|
9
|
+
const [GameRenderer, setGameRenderer] = useState<React.ComponentType<any> | null>(null);
|
|
10
|
+
const [engine, setEngine] = useState<any>(null);
|
|
11
|
+
const [fullState, setFullState] = useState<any>(null);
|
|
12
|
+
const [viewState, setViewState] = useState<any>(null);
|
|
13
|
+
const [gameOver, setGameOver] = useState(false);
|
|
14
|
+
const [gameResult, setGameResult] = useState<any>(null);
|
|
15
|
+
const [stateJson, setStateJson] = useState('');
|
|
16
|
+
|
|
17
|
+
// Refs to avoid recreating platform on every state change
|
|
18
|
+
const fullStateRef = useRef(fullState);
|
|
19
|
+
fullStateRef.current = fullState;
|
|
20
|
+
const playerIndexRef = useRef(playerIndex);
|
|
21
|
+
playerIndexRef.current = playerIndex;
|
|
22
|
+
const stateUpdateListeners = useRef<Set<(...args: unknown[]) => void>>(new Set());
|
|
23
|
+
|
|
24
|
+
// Generate mock players
|
|
25
|
+
const mockPlayers = useMemo(() => {
|
|
26
|
+
return Array.from({ length: playerCount }, (_, i) => ({
|
|
27
|
+
id: `player-${i + 1}`,
|
|
28
|
+
nickname: PLAYER_NAMES[i] || `Player ${i + 1}`,
|
|
29
|
+
avatarUrl: null,
|
|
30
|
+
isHost: i === 0,
|
|
31
|
+
}));
|
|
32
|
+
}, [playerCount]);
|
|
33
|
+
|
|
34
|
+
const mockPlayersRef = useRef(mockPlayers);
|
|
35
|
+
mockPlayersRef.current = mockPlayers;
|
|
36
|
+
|
|
37
|
+
// Load renderer and engine dynamically
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
import('/src/index.ts').then((mod) => {
|
|
40
|
+
setGameRenderer(() => mod.Renderer || mod.default);
|
|
41
|
+
if (mod.engine) {
|
|
42
|
+
setEngine(mod.engine);
|
|
43
|
+
}
|
|
44
|
+
}).catch((err) => {
|
|
45
|
+
console.error('Failed to load game module:', err);
|
|
46
|
+
// Fallback: try loading renderer directly
|
|
47
|
+
import('/src/renderer.tsx').then((mod) => {
|
|
48
|
+
setGameRenderer(() => mod.default || mod.Renderer);
|
|
49
|
+
}).catch((err2) => {
|
|
50
|
+
console.error('Failed to load renderer:', err2);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
// Initialize game when engine loads or player count changes
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!engine) return;
|
|
58
|
+
const initialState = engine.init(mockPlayers);
|
|
59
|
+
setFullState(initialState);
|
|
60
|
+
setGameOver(false);
|
|
61
|
+
setGameResult(null);
|
|
62
|
+
setActions([]);
|
|
63
|
+
}, [engine, mockPlayers]);
|
|
64
|
+
|
|
65
|
+
// Compute view state whenever fullState or playerIndex changes
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!engine || !fullState) return;
|
|
68
|
+
const view = engine.getPlayerView(fullState, mockPlayers[playerIndex].id);
|
|
69
|
+
setViewState(view);
|
|
70
|
+
// Notify renderer of new view
|
|
71
|
+
stateUpdateListeners.current.forEach(handler => handler(view));
|
|
72
|
+
}, [fullState, playerIndex, engine, mockPlayers]);
|
|
73
|
+
|
|
74
|
+
// Sync state JSON editor
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (fullState) {
|
|
77
|
+
setStateJson(JSON.stringify(fullState, null, 2));
|
|
78
|
+
}
|
|
79
|
+
}, [fullState]);
|
|
80
|
+
|
|
81
|
+
// Platform object — stable reference via useMemo on engine + mockPlayers
|
|
82
|
+
const platform = useMemo(() => {
|
|
83
|
+
if (!engine) return null;
|
|
84
|
+
return {
|
|
85
|
+
getPlayers: () => mockPlayersRef.current,
|
|
86
|
+
getLocalPlayer: () => mockPlayersRef.current[playerIndexRef.current],
|
|
87
|
+
send: (action: any) => {
|
|
88
|
+
const currentState = fullStateRef.current;
|
|
89
|
+
if (!currentState) return;
|
|
90
|
+
|
|
91
|
+
const playerId = mockPlayersRef.current[playerIndexRef.current].id;
|
|
92
|
+
|
|
93
|
+
// Log the action
|
|
94
|
+
setActions(prev => [...prev, {
|
|
95
|
+
time: new Date().toISOString(),
|
|
96
|
+
player: mockPlayersRef.current[playerIndexRef.current].nickname,
|
|
97
|
+
action,
|
|
98
|
+
}]);
|
|
99
|
+
|
|
100
|
+
// Run through engine
|
|
101
|
+
const newState = engine.handleAction(currentState, playerId, action);
|
|
102
|
+
setFullState(newState);
|
|
103
|
+
|
|
104
|
+
// Check game over
|
|
105
|
+
if (engine.isGameOver(newState)) {
|
|
106
|
+
setGameOver(true);
|
|
107
|
+
setGameResult(engine.getResult(newState));
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
on: (event: string, handler: (...args: unknown[]) => void) => {
|
|
111
|
+
if (event === 'stateUpdate') {
|
|
112
|
+
stateUpdateListeners.current.add(handler);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
off: (event: string, handler: (...args: unknown[]) => void) => {
|
|
116
|
+
if (event === 'stateUpdate') {
|
|
117
|
+
stateUpdateListeners.current.delete(handler);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
reportResult: (result: any) => {
|
|
121
|
+
console.log('Game result reported:', result);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}, [engine]);
|
|
125
|
+
|
|
126
|
+
// Apply manual state override from JSON editor
|
|
127
|
+
const applyState = useCallback(() => {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(stateJson);
|
|
130
|
+
setFullState(parsed);
|
|
131
|
+
if (engine) {
|
|
132
|
+
setGameOver(engine.isGameOver(parsed));
|
|
133
|
+
if (engine.isGameOver(parsed)) {
|
|
134
|
+
setGameResult(engine.getResult(parsed));
|
|
135
|
+
} else {
|
|
136
|
+
setGameResult(null);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// ignore invalid JSON
|
|
141
|
+
}
|
|
142
|
+
}, [stateJson, engine]);
|
|
143
|
+
|
|
144
|
+
// Reset game
|
|
145
|
+
const resetGame = useCallback(() => {
|
|
146
|
+
if (!engine) return;
|
|
147
|
+
const newState = engine.init(mockPlayersRef.current);
|
|
148
|
+
setFullState(newState);
|
|
149
|
+
setGameOver(false);
|
|
150
|
+
setGameResult(null);
|
|
151
|
+
setActions([]);
|
|
152
|
+
}, [engine]);
|
|
153
|
+
|
|
154
|
+
// Clamp playerIndex when playerCount decreases
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (playerIndex >= playerCount) {
|
|
157
|
+
setPlayerIndex(0);
|
|
158
|
+
}
|
|
159
|
+
}, [playerCount, playerIndex]);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className="flex gap-4 h-[calc(100vh-80px)]">
|
|
163
|
+
{/* Renderer */}
|
|
164
|
+
<div className="flex-1 bg-zinc-900 rounded-lg overflow-auto">
|
|
165
|
+
{GameRenderer && platform && viewState ? (
|
|
166
|
+
<GameRenderer platform={platform} state={viewState} />
|
|
167
|
+
) : (
|
|
168
|
+
<div className="p-4 text-zinc-500">
|
|
169
|
+
{!engine ? 'Loading engine...' : 'Initializing game...'}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Control Panel */}
|
|
175
|
+
<div className="w-80 flex flex-col gap-4 overflow-auto">
|
|
176
|
+
{/* Player Count */}
|
|
177
|
+
<div className="bg-zinc-900 rounded-lg p-3">
|
|
178
|
+
<h3 className="text-sm font-bold text-zinc-400 mb-2">Player Count</h3>
|
|
179
|
+
<input
|
|
180
|
+
type="number"
|
|
181
|
+
min={2}
|
|
182
|
+
max={8}
|
|
183
|
+
value={playerCount}
|
|
184
|
+
onChange={(e) => setPlayerCount(Math.max(2, Math.min(8, Number(e.target.value))))}
|
|
185
|
+
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-sm"
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Player Switcher */}
|
|
190
|
+
<div className="bg-zinc-900 rounded-lg p-3">
|
|
191
|
+
<h3 className="text-sm font-bold text-zinc-400 mb-2">Current Player</h3>
|
|
192
|
+
<select
|
|
193
|
+
value={playerIndex}
|
|
194
|
+
onChange={(e) => setPlayerIndex(Number(e.target.value))}
|
|
195
|
+
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-sm"
|
|
196
|
+
>
|
|
197
|
+
{mockPlayers.map((p, i) => (
|
|
198
|
+
<option key={p.id} value={i}>{p.nickname}{p.isHost ? ' (Host)' : ''}</option>
|
|
199
|
+
))}
|
|
200
|
+
</select>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* Game Result */}
|
|
204
|
+
{gameOver && gameResult && (
|
|
205
|
+
<div className="bg-zinc-900 rounded-lg p-3">
|
|
206
|
+
<h3 className="text-sm font-bold text-green-400 mb-2">Game Over</h3>
|
|
207
|
+
<pre className="text-xs font-mono bg-zinc-800 rounded p-2 overflow-auto max-h-32 whitespace-pre-wrap">
|
|
208
|
+
{JSON.stringify(gameResult, null, 2)}
|
|
209
|
+
</pre>
|
|
210
|
+
<button
|
|
211
|
+
onClick={resetGame}
|
|
212
|
+
className="mt-2 w-full bg-amber-600 hover:bg-amber-500 text-white px-3 py-1 rounded text-sm"
|
|
213
|
+
>
|
|
214
|
+
Reset Game
|
|
215
|
+
</button>
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{/* Reset button (when game is not over) */}
|
|
220
|
+
{!gameOver && engine && (
|
|
221
|
+
<div className="bg-zinc-900 rounded-lg p-3">
|
|
222
|
+
<button
|
|
223
|
+
onClick={resetGame}
|
|
224
|
+
className="w-full bg-zinc-700 hover:bg-zinc-600 text-white px-3 py-1 rounded text-sm"
|
|
225
|
+
>
|
|
226
|
+
Reset Game
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{/* State Editor */}
|
|
232
|
+
<div className="bg-zinc-900 rounded-lg p-3 flex-1 flex flex-col min-h-0">
|
|
233
|
+
<h3 className="text-sm font-bold text-zinc-400 mb-2">Game State (Full)</h3>
|
|
234
|
+
<textarea
|
|
235
|
+
value={stateJson}
|
|
236
|
+
onChange={(e) => setStateJson(e.target.value)}
|
|
237
|
+
className="flex-1 bg-zinc-800 border border-zinc-700 rounded p-2 font-mono text-xs resize-none min-h-[120px]"
|
|
238
|
+
/>
|
|
239
|
+
<button
|
|
240
|
+
onClick={applyState}
|
|
241
|
+
className="mt-2 bg-amber-600 hover:bg-amber-500 text-white px-3 py-1 rounded text-sm"
|
|
242
|
+
>
|
|
243
|
+
Apply State
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
{/* Action Log */}
|
|
248
|
+
<div className="bg-zinc-900 rounded-lg p-3 h-48 overflow-auto">
|
|
249
|
+
<h3 className="text-sm font-bold text-zinc-400 mb-2">Action Log</h3>
|
|
250
|
+
{actions.length === 0 ? (
|
|
251
|
+
<p className="text-zinc-500 text-xs">No actions yet</p>
|
|
252
|
+
) : (
|
|
253
|
+
<div className="space-y-1">
|
|
254
|
+
{actions.map((a, i) => (
|
|
255
|
+
<div key={i} className="text-xs font-mono bg-zinc-800 rounded p-1">
|
|
256
|
+
<span className="text-amber-400">{a.player}</span>: {JSON.stringify(a.action)}
|
|
257
|
+
</div>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
);
|
|
265
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlepartytime/dev-kit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./testing": {
|
|
13
|
+
"types": "./dist/testing/index.d.ts",
|
|
14
|
+
"default": "./dist/testing/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"bin": {
|
|
8
18
|
"lpt-dev-kit": "./dist/cli.js"
|
|
9
19
|
},
|
|
10
20
|
"scripts": {
|
|
11
|
-
"build": "tsc",
|
|
21
|
+
"build": "tsc && cp -r src/webapp dist/",
|
|
12
22
|
"dev": "tsc --watch",
|
|
13
23
|
"test": "vitest run",
|
|
14
24
|
"test:watch": "vitest"
|
|
@@ -21,12 +31,30 @@
|
|
|
21
31
|
"url": "https://github.com/chesterli710/littlepartytime-sdk.git",
|
|
22
32
|
"directory": "packages/dev-kit"
|
|
23
33
|
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"playwright": ">=1.40.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"playwright": {
|
|
39
|
+
"optional": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
24
42
|
"dependencies": {
|
|
25
|
-
"
|
|
43
|
+
"@vitejs/plugin-react": "^5",
|
|
44
|
+
"archiver": "^7",
|
|
45
|
+
"chokidar": "^4",
|
|
46
|
+
"express": "^5",
|
|
47
|
+
"react": "^19",
|
|
48
|
+
"react-dom": "^19",
|
|
49
|
+
"socket.io": "^4",
|
|
50
|
+
"vite": "^7"
|
|
26
51
|
},
|
|
27
52
|
"devDependencies": {
|
|
28
53
|
"@types/archiver": "^6",
|
|
54
|
+
"@types/express": "^5",
|
|
29
55
|
"@types/node": "^22",
|
|
56
|
+
"@types/react": "^19",
|
|
57
|
+
"@types/react-dom": "^19",
|
|
30
58
|
"typescript": "^5",
|
|
31
59
|
"vitest": "^3"
|
|
32
60
|
}
|