@nexusgame/dev 0.2.0 → 0.4.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/README.md +132 -17
- package/dist/console/assets/room-CiSjgtT0.js +4 -0
- package/dist/console/assets/room-CxWGYLZL.css +1 -0
- package/dist/console/assets/styles-BDGeSlj5.js +40 -0
- package/dist/console/assets/styles-DhMceq41.css +1 -0
- package/dist/console/assets/workspace-B2jb_b0k.css +1 -0
- package/dist/console/assets/workspace-DS4AI-v8.js +1 -0
- package/dist/console/index.html +15 -0
- package/dist/console/workspace.html +15 -0
- package/dist/engine-worker.js +801 -781
- package/dist/index.js +3040 -206
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -7,53 +7,168 @@ Nexus Playground 的**本地开发调试工具**(bin:`npgame-dev`)。它
|
|
|
7
7
|
|
|
8
8
|
## 快速开始
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
本地运行时分为三个独立的部分,生命周期互不绑定:
|
|
11
|
+
|
|
12
|
+
| 部分 | 命令 | 说明 |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| Workspace host | `npgame-dev serve` | 常驻进程,内置本地引擎、管理页和房间控制台;也负责托管它启动的 Worker |
|
|
15
|
+
| Game Worker | `npgame-dev game start/stop/status/logs` | 游戏仓库的本地 dev server(进程本身) |
|
|
16
|
+
| 房间连接 | `npgame-dev connect` / `disconnect` | 一份持久化配置,不会启动任何服务 |
|
|
17
|
+
|
|
18
|
+
`game` 指游戏项目和它的本地 Worker 进程;`room` 指引擎里的房间实例。两者分开管理,互不隐式启动。
|
|
11
19
|
|
|
12
20
|
> 以下命令都可以用 `npx @nexusgame/dev ...` 直接运行,无需全局安装。
|
|
13
21
|
|
|
14
|
-
### 1.
|
|
22
|
+
### 1. 启动 Workspace host
|
|
23
|
+
|
|
24
|
+
在工作区根目录运行:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npgame-dev serve
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- 管理页:`http://127.0.0.1:56512/`
|
|
31
|
+
- 房间控制台:`http://127.0.0.1:56512/rooms/<roomId>`
|
|
32
|
+
- 引擎内部端口:默认 `56513`(`--engine-port` 可覆盖),浏览器只和 host 通信
|
|
33
|
+
- 状态与配置:`./.nexus/`
|
|
34
|
+
|
|
35
|
+
host 只绑定 `127.0.0.1`。它在服务端代签 5 分钟 WS token,并在转发 `/api/engine/*` 时注入 `ADMIN_SECRET`;浏览器不会接触任何 secret。host 必须显式启动,`connect`、`room start` 或打开页面都不会隐式拉起它。
|
|
36
|
+
|
|
37
|
+
### 2. 启停游戏 Worker
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npgame-dev game start gomoku # 由 host 启动并托管
|
|
41
|
+
npgame-dev game status gomoku # Worker 状态、URL、PID、所属房间
|
|
42
|
+
npgame-dev game logs gomoku # 查看 Worker 日志(-f 持续跟随)
|
|
43
|
+
npgame-dev game stop gomoku # 停止 host 托管的 Worker
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- `game start` 读取游戏仓库 `package.json` 的 `scripts.dev`,用对应的包管理器后台启动;端口取 `worker/wrangler.toml` 的 `[dev] port`,被占用时自动顺延(`--worker-port` 可指定)。
|
|
47
|
+
- 进程、日志和 PID 记录由 host 管理(`.nexus/workers/<gameId>.json`、`.nexus/logs/<gameId>.worker.log`)。host 正常退出时会停止自己启动的 Worker。
|
|
48
|
+
- 在终端里自己 `pnpm dev` 启动的 Worker 会被识别为**外部运行**:host 不会停止它,`game logs` 仍然可读。
|
|
49
|
+
- 房间处于 `playing` / `paused` 时,`game stop` 会拒绝执行;先 `room stop`,或加 `--force`。
|
|
50
|
+
|
|
51
|
+
### 3. 连接游戏
|
|
15
52
|
|
|
16
53
|
```bash
|
|
17
|
-
npgame-dev
|
|
54
|
+
npgame-dev connect gomoku
|
|
18
55
|
```
|
|
19
56
|
|
|
20
|
-
|
|
57
|
+
`connect` 只做配置:验证 Worker、创建/复用房间、写入 `set-game`,然后把连接保存到 `.nexus/workspace.json`。它不会开始游戏,也不会启动 Worker。
|
|
21
58
|
|
|
22
|
-
|
|
59
|
+
- 默认使用 `game start` 记录的 Worker 地址;也可以 `--worker-url` 显式指定。
|
|
60
|
+
- `npgame-dev disconnect gomoku` 会停止正在运行的房间并移除连接配置,但不会停止 Worker。
|
|
23
61
|
|
|
24
|
-
|
|
62
|
+
### 4. 管理房间
|
|
25
63
|
|
|
26
64
|
```bash
|
|
27
|
-
npgame-dev
|
|
65
|
+
npgame-dev room start gomoku # 分配 dev 玩家并开局
|
|
66
|
+
npgame-dev room status gomoku # 阶段、角色、玩家、Worker 状态、控制台地址
|
|
67
|
+
npgame-dev room stop gomoku # 停止游戏(Worker 继续运行)
|
|
28
68
|
```
|
|
29
69
|
|
|
30
|
-
`
|
|
70
|
+
`room start` 会为每个角色创建 `dev:<roleId>` 虚拟玩家、分配角色并开始游戏,最后打印房间控制台地址。
|
|
71
|
+
|
|
72
|
+
也可以在管理页完成 Worker 启停、连接、开始、停止和断开;页面只调用 host API,不会自行启动服务。
|
|
31
73
|
|
|
32
|
-
###
|
|
74
|
+
### 5. 在控制台调试
|
|
75
|
+
|
|
76
|
+
- **模拟前端**:用真实 WebSocket 玩家流程渲染游戏 UI(回合校验、广播、LLM 触发)。顶部栏和底部栏属于房间(玩家/角色下拉菜单、游戏名与阶段、暂停/继续与重新开始);顶部栏下方是浏览器式标签页,每个玩家一个标签页(标题为角色名,或 `spectator | 玩家名`),点 `+` 选择玩家,标签页可以左右拆分,切换标签不会重建连接。暂停后画面冻结,等同于当前视角的快照;底部栏目前只显示房间级生命周期提示。
|
|
77
|
+
- **管理面板**:权威 `gameState`、玩家/角色映射、加入申请、各角色视角、历史与快照回溯、完整生命周期、强制提交动作。
|
|
78
|
+
- 底部请求日志记录每个请求的状态码和耗时。
|
|
79
|
+
|
|
80
|
+
### 6. 命令行交互
|
|
33
81
|
|
|
34
82
|
```bash
|
|
35
83
|
# 查看房间全局状态(权威 gameState、roleMapping、history)
|
|
36
84
|
npgame-dev state
|
|
37
85
|
|
|
38
|
-
#
|
|
86
|
+
# 查看某角色的视角
|
|
39
87
|
npgame-dev perspective player_1
|
|
40
88
|
|
|
41
89
|
# 以指定角色执行动作
|
|
42
90
|
npgame-dev action player_1 '{"action_id":"place","params":{"row":0,"col":0}}'
|
|
43
91
|
```
|
|
44
92
|
|
|
45
|
-
|
|
93
|
+
只连接了一个房间时 `--room` 可省略;有多个房间时命令会列出房间并要求显式指定。CLI 的 `state`/`perspective`/`action` 也走 host 代理,因此不需要传递 secret。
|
|
94
|
+
|
|
95
|
+
### 7. 工作区命令
|
|
96
|
+
|
|
97
|
+
不进入游戏仓库,直接在工作区根目录扫描并管理游戏。扫描范围:
|
|
98
|
+
|
|
99
|
+
- 当前目录下的 `nexus.game.json`
|
|
100
|
+
- 当前目录的直接子目录
|
|
101
|
+
- 当前目录下 `games/`、`nexus-games/` 的子目录
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npgame-dev game ls # 列出发现的游戏、Worker 与连接状态
|
|
105
|
+
npgame-dev game start gomoku # 启动并托管 Worker
|
|
106
|
+
npgame-dev game status # 省略 id 则列出全部游戏
|
|
107
|
+
npgame-dev game status --json # 机器可读输出
|
|
108
|
+
npgame-dev game logs gomoku -n 100 # 查看 Worker 日志;-f 持续跟随
|
|
109
|
+
npgame-dev game stop gomoku # 停止 host 托管的 Worker;--all 停止全部
|
|
110
|
+
npgame-dev host logs # 查看 host 自身日志
|
|
111
|
+
npgame-dev host logs -f # 持续跟随 host 日志
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`game stop` 只停止由 host 启动的 Worker;外部启动的 Worker 会跳过,需要在其终端按 Ctrl+C。`game stop --force` 只跳过“房间正在运行”的检查,不会接管外部 Worker。
|
|
115
|
+
|
|
116
|
+
### 8. 连接配置
|
|
117
|
+
|
|
118
|
+
`.nexus/workspace.json` 保存每个游戏的连接:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"version": 1,
|
|
123
|
+
"root": "/path/to/workspace",
|
|
124
|
+
"connections": [
|
|
125
|
+
{
|
|
126
|
+
"roomId": "gomoku",
|
|
127
|
+
"gameId": "gomoku",
|
|
128
|
+
"name": "Gomoku",
|
|
129
|
+
"gameDir": "/path/to/workspace/gomoku",
|
|
130
|
+
"workerUrl": "http://127.0.0.1:8787",
|
|
131
|
+
"roleIds": ["black", "white"],
|
|
132
|
+
"createdAt": "...",
|
|
133
|
+
"updatedAt": "..."
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`.nexus/host.json` 记录运行中的 host(PID、端口、URL),供其他命令发现。host 退出时会删除该记录;异常退出留下的记录会在下一次 `serve` 时按 PID 判定并清理。
|
|
140
|
+
|
|
141
|
+
## 常用参数
|
|
46
142
|
|
|
47
143
|
| 命令 | 参数 | 说明 |
|
|
48
144
|
| --- | --- | --- |
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
145
|
+
| `serve` | `-r, --root <dir>` | 工作区根目录,默认当前目录 |
|
|
146
|
+
| `serve` | `-p, --port <port>` | Host / 管理页端口,默认 `56512` |
|
|
147
|
+
| `serve` | `--engine-port <port>` | 引擎内部端口,默认 Host 端口 + 1 |
|
|
148
|
+
| `game start` | `--worker-port <port>` | 指定 Worker 端口;默认取 `wrangler.toml`,被占用时自动顺延 |
|
|
149
|
+
| `game start` | `--json` | 机器可读输出 |
|
|
150
|
+
| `game status` | `--json` | 机器可读输出 |
|
|
151
|
+
| `game logs` | `-n, --lines <n>` / `-f, --follow` | 输出行数(默认 200)/ 持续跟随 |
|
|
152
|
+
| `game stop` | `--all` / `--force` | 停止全部 host 托管的 Worker / 跳过房间正在运行的检查 |
|
|
153
|
+
| `connect` | `-w, --worker-url <url>` | Game Worker URL,默认使用 `game start` 记录的地址 |
|
|
154
|
+
| `connect` | `-n, --players <count>` | 参与人数(适用于 UNO 等可选人数游戏) |
|
|
155
|
+
| `connect` | `--room <roomId>` | 房间 id,默认游戏 id |
|
|
156
|
+
| `room start` / `room stop` / `room status` | `<room>` | 房间 id 或游戏 id |
|
|
157
|
+
| `state` / `perspective` / `action` | `--room <roomId>` | 仅一个已连接房间时可省略 |
|
|
158
|
+
| `game status` / `connect` | `--json` | 机器可读输出 |
|
|
159
|
+
|
|
160
|
+
所有需要 host 的命令都接受 `-p, --port <port>` 以覆盖 host 端口。
|
|
54
161
|
|
|
55
162
|
## 构建
|
|
56
163
|
|
|
57
164
|
```bash
|
|
58
|
-
pnpm run build # 打包 CLI
|
|
165
|
+
pnpm run build # 打包 CLI,复制引擎 worker 和 engine-console 构建产物进 dist/
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
构建前需要先构建引擎 bundle 和 console(`make publish-cli` 已按顺序串好):
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
make build-cli-engine
|
|
172
|
+
make build-console
|
|
173
|
+
cd packages/cli && pnpm run build
|
|
59
174
|
```
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{r as c,j as e,c as pe,R as fe}from"./styles-BDGeSlj5.js";const ge={setGame:!1,managePlayers:!1,assignRoles:!1,startGame:!1,pauseGame:!1,resumeGame:!1,stopGame:!1,restartGame:!1,backtrack:!1,approveJoin:!1,editRoomMeta:!1,forceAct:!1,inspectFullState:!1},be=t=>{const s=t.gameConfig;if(!s)return!1;const a=new Set;for(const n of Object.values(t.players))n.role&&a.add(n.role);return s.roleIds.every(n=>a.has(n))};function xe(t,s){if(!t||t.phase===null)return ge;const{phase:a}=t,n=s.kind==="operator",r=n||t.you.isOwner;return{setGame:r&&a==="lobby",managePlayers:r&&a==="lobby",assignRoles:r&&a==="lobby",startGame:r&&(a==="lobby"||a==="finished")&&be(t),pauseGame:r&&a==="playing",resumeGame:r&&a==="paused",stopGame:r&&a!=="lobby",restartGame:r&&(a==="playing"||a==="paused"||a==="finished"),backtrack:r&&a==="paused"&&t.stateHistory.length>0,approveJoin:r,editRoomMeta:r,forceAct:n&&a==="playing",inspectFullState:n}}const ue="/api/engine",ye=`${ue}/rooms`;function D(t,s=""){return`${ye}/${encodeURIComponent(t)}${s}`}const M={createRoom:`${ue}/create`,roomState:t=>D(t),roomDebug:t=>D(t,"/debug"),roomPerspective:(t,s)=>{const a=s?`?roleId=${encodeURIComponent(s)}`:"";return D(t,`/perspective${a}`)},roleAction:t=>D(t,"/role-action"),setGame:t=>D(t,"/set-game"),startGame:t=>D(t,"/start-game"),stopGame:t=>D(t,"/stop-game"),restartGame:t=>D(t,"/restart-game"),pauseGame:t=>D(t,"/pause-game"),resumeGame:t=>D(t,"/resume-game"),backtrack:t=>D(t,"/backtrack"),players:t=>D(t,"/players"),player:(t,s)=>D(t,`/players/${encodeURIComponent(s)}`),assignRole:t=>D(t,"/assign-role"),addBot:t=>D(t,"/add-bot")},je=0,oe=1,Ne=[1e3,1008],_e={status:"idle",isConnected:!1,isConnecting:!1,isRetrying:!1,error:null,engineState:null,gameState:null,actionThrottled:!1};class we{constructor(s){this.snapshot=_e,this.listeners=new Set,this.ws=null,this.connectionId=0,this.isConnecting=!1,this.reconnectAttempt=0,this.reconnectTimer=null,this.errorClearTimer=null,this.actionThrottleTimer=null,this.lastAction=null,this.lastEngineState=null,this.lastGameState=null,this.subscribe=a=>(this.listeners.add(a),()=>{this.listeners.delete(a)}),this.getSnapshot=()=>this.snapshot,this.options=s}async connect(){if(this.isConnecting||this.ws&&(this.ws.readyState===je||this.ws.readyState===oe))return;const s=++this.connectionId;this.isConnecting=!0,this.clearReconnectTimer(),this.update({status:this.reconnectAttempt>0?"retrying":"connecting",isConnecting:!0,isConnected:!1,isRetrying:this.reconnectAttempt>0});try{const{url:a,token:n}=await this.options.getConnection();if(s!==this.connectionId)return;const r=new URL(a);r.searchParams.set("token",n);const m=(this.options.webSocketFactory??(o=>new WebSocket(o)))(r.toString());this.ws=m,m.onopen=()=>{if(s!==this.connectionId){m.close(1e3,"stale connection");return}this.isConnecting=!1,this.reconnectAttempt=0,this.update({status:"connected",isConnected:!0,isConnecting:!1,isRetrying:!1,error:null})},m.onmessage=o=>{s===this.connectionId&&this.handleMessage(o.data)},m.onclose=o=>{s===this.connectionId&&(this.ws=null,this.isConnecting=!1,this.update({status:"disconnected",isConnected:!1,isConnecting:!1}),this.isNonRetryableCloseCode(o.code)||this.scheduleReconnect(`close code ${o.code}`))},m.onerror=()=>{}}catch(a){if(s!==this.connectionId)return;this.isConnecting=!1,this.ws=null,this.update({status:"disconnected",isConnected:!1,isConnecting:!1,error:a instanceof Error?a.message:"Connection failed"}),this.scheduleReconnect("connection setup failed")}}retry(){this.reconnectAttempt=0,this.update({error:null}),this.connect()}disconnect(){this.connectionId++,this.isConnecting=!1,this.reconnectAttempt=0,this.clearReconnectTimer(),this.clearErrorTimer(),this.clearActionThrottle(),this.lastAction=null;const s=this.ws;this.ws=null,s&&(s.onopen=null,s.onmessage=null,s.onclose=null,s.onerror=null,s.close(1e3,"cleanup")),this.update({status:"idle",isConnected:!1,isConnecting:!1,isRetrying:!1,error:null,actionThrottled:!1})}send(s){return!this.ws||this.ws.readyState!==oe?!1:(this.ws.send(JSON.stringify(s)),!0)}act(s,a=!1){var r,h;if(a&&this.actionThrottleTimer&&((r=this.lastAction)==null?void 0:r.action_id)===s.action_id&&JSON.stringify((h=this.lastAction)==null?void 0:h.params)===JSON.stringify(s.params))return!1;const n=this.send({type:"ACT",payload:s});return this.lastAction=s,a&&(this.clearActionThrottle(),this.update({actionThrottled:!0}),this.actionThrottleTimer=setTimeout(()=>{this.actionThrottleTimer=null,this.lastAction=null,this.update({actionThrottled:!1})},this.options.actThrottleMs??300)),n}handleMessage(s){var n,r,h,m,o,u,g;let a;try{a=JSON.parse(String(s))}catch(y){console.error("[NexusEngine] Failed to parse message",y);return}switch(a.type){case"SYNC_STATE":{const y=a.payload.engine,C=a.payload.game;y&&(this.lastEngineState=y),C&&(this.lastGameState=C),this.update({engineState:y??this.lastEngineState,gameState:C??this.lastGameState});break}case"JOIN_REQUEST_INTERNAL":(r=(n=this.options).onJoinRequest)==null||r.call(n,a.payload.userId,a.payload.displayName);break;case"KICKED":{const y=a.payload||"You were removed by the room owner";(m=(h=this.options).onKicked)==null||m.call(h,y),this.clearErrorTimer(),this.update({error:y}),(o=this.ws)==null||o.close(1e3,"kicked");break}case"ERROR":(g=(u=this.options).onError)==null||g.call(u,a.payload),this.setError(a.payload);break}}setError(s){this.clearErrorTimer(),this.update({error:s}),this.errorClearTimer=setTimeout(()=>{this.errorClearTimer=null,this.update({error:null})},5e3)}scheduleReconnect(s){if(this.reconnectTimer||this.isConnecting)return;this.reconnectAttempt+=1;const a=this.reconnectAttempt<=1;this.update({status:"retrying",isRetrying:a,isConnecting:!1,isConnected:!1,error:a?null:this.snapshot.error});const n=this.reconnectDelay(this.reconnectAttempt);console.warn(`[NexusEngine] Scheduling reconnect #${this.reconnectAttempt} in ${n}ms (${s})`),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.connect()},n)}reconnectDelay(s){const a=this.options.baseReconnectDelayMs??1e3,n=this.options.maxReconnectDelayMs??15e3,r=this.options.reconnectJitterMs??300,h=Math.min(n,a*2**Math.min(s-1,4)),m=r>0?Math.floor(Math.random()*r):0;return h+m}isNonRetryableCloseCode(s){return(this.options.nonRetryableCloseCodes??Ne).includes(s)}clearReconnectTimer(){this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}clearErrorTimer(){this.errorClearTimer&&(clearTimeout(this.errorClearTimer),this.errorClearTimer=null)}clearActionThrottle(){this.actionThrottleTimer&&(clearTimeout(this.actionThrottleTimer),this.actionThrottleTimer=null)}update(s){var n,r;const a=this.snapshot.status;this.snapshot={...this.snapshot,...s};for(const h of this.listeners)h();this.snapshot.status!==a&&((r=(n=this.options).onStatusChange)==null||r.call(n,this.snapshot.status))}}function ke(t){return{selectRole:s=>t.send({type:"LOBBY_SELECT_ROLE",payload:{roleId:s}}),leaveRoom:()=>t.send({type:"LOBBY_LEAVE"}),requestJoin:s=>t.send({type:"LOBBY_JOIN_REQUEST",payload:{displayName:s}}),setGame:(s,a,n)=>t.send({type:"ADMIN_SET_GAME",payload:{gameId:s,gameWorkerUrl:a,selectedPlayerCount:n}}),addBot:s=>t.send({type:"ADMIN_ADD_BOT",payload:{displayName:s.displayName,modelName:s.modelName,systemPrompt:s.systemPrompt,temperature:s.temperature}}),removePlayer:s=>t.send({type:"ADMIN_REMOVE_PLAYER",payload:{userId:s}}),assignRole:(s,a)=>t.send({type:"ADMIN_ASSIGN_ROLE",payload:{roleId:s,userId:a}}),approveJoin:(s,a)=>t.send({type:"ADMIN_APPROVE_JOIN",payload:{userId:s,displayName:a}}),ignoreJoin:s=>t.send({type:"ADMIN_IGNORE_JOIN",payload:{userId:s}}),startGame:()=>t.send({type:"ADMIN_START_GAME"}),stopGame:()=>t.send({type:"ADMIN_STOP_GAME"}),restartGame:()=>t.send({type:"ADMIN_RESTART_GAME"}),pauseGame:()=>t.send({type:"ADMIN_PAUSE_GAME"}),resumeGame:()=>t.send({type:"ADMIN_RESUME_GAME"}),backtrackState:s=>t.send({type:"ADMIN_BACKTRACK_STATE",payload:{index:s}}),updateRoomMeta:(s,a)=>t.send({type:"ADMIN_UPDATE_ROOM_META",payload:{name:s,isPublic:a}}),act:(s,a=!1)=>t.act(s,a)}}class Ce{constructor(s){this.baseUrl=s.baseUrl.replace(/\/+$/,""),this.token=s.token,this.fetchImpl=s.fetchImpl??((a,n)=>globalThis.fetch(a,n))}async createRoom(s,a){return this.request("POST",M.createRoom,{roomId:s,ownerId:a})}async getState(s){return this.request("GET",M.roomState(s))}async getDebug(s){return this.request("GET",M.roomDebug(s))}async getPerspective(s,a){return this.request("GET",M.roomPerspective(s,a))}async submitAction(s,a,n){return this.request("POST",M.roleAction(s),{roleId:a,action:n})}async setGame(s,a){return this.request("POST",M.setGame(s),a)}async startGame(s){return this.request("POST",M.startGame(s))}async stopGame(s){return this.request("POST",M.stopGame(s))}async restartGame(s){return this.request("POST",M.restartGame(s))}async pauseGame(s){return this.request("POST",M.pauseGame(s))}async resumeGame(s){return this.request("POST",M.resumeGame(s))}async backtrack(s,a){return this.request("POST",M.backtrack(s),{index:a})}async createPlayer(s,a){return this.request("POST",M.players(s),a)}async removePlayer(s,a){return this.request("DELETE",M.player(s,a))}async assignRole(s,a,n){return this.request("POST",M.assignRole(s),{roleId:a,userId:n})}async addBot(s,a){return this.request("POST",M.addBot(s),a)}async request(s,a,n){const r={"Content-Type":"application/json"},h=typeof this.token=="function"?await this.token():this.token;h&&(r.Authorization=`Bearer ${h}`);let m;try{m=await this.fetchImpl(`${this.baseUrl}${a}`,{method:s,headers:r,body:n===void 0?void 0:JSON.stringify(n)})}catch(g){return{ok:!1,error:g instanceof Error?g.message:"Request failed"}}const o=await m.text();let u=null;try{u=o?JSON.parse(o):null}catch{u=o}return m.ok?u&&typeof u=="object"&&typeof u.ok=="boolean"?u.ok?{ok:!0,data:u.data}:{ok:!1,error:u.error||"Command failed",data:u}:{ok:!0,data:u}:{ok:!1,error:(u==null?void 0:u.error)||`HTTP ${m.status}`,status:m.status,data:u}}}function ve(){const t=/^\/rooms\/([^/]+)\/?$/.exec(window.location.pathname);if(!t)return"";try{return decodeURIComponent(t[1])}catch{return""}}function Te(t){try{const s=new URL(t,window.location.origin);return`${s.pathname}${s.search}`}catch{return t}}function he(t){return t instanceof Error?t.message:String(t)}function Se(t){const s=async(a,n)=>{const r=performance.now(),h=((n==null?void 0:n.method)||"GET").toUpperCase(),m=Te(a);try{const o=await fetch(a,n);return t({at:Date.now(),method:h,path:m,status:o.status,durationMs:Math.round(performance.now()-r),ok:o.ok}),o}catch(o){throw t({at:Date.now(),method:h,path:m,durationMs:Math.round(performance.now()-r),ok:!1,error:he(o)}),o}};return new Ce({baseUrl:window.location.origin,fetchImpl:s})}async function Re(t,s,a,n){const h=`/dev/token?${new URLSearchParams({roomId:t,userId:s,name:a}).toString()}`,m=performance.now();let o;try{o=await fetch(h)}catch(y){throw n({at:Date.now(),method:"GET",path:h,durationMs:Math.round(performance.now()-m),ok:!1,error:he(y)}),y}const u=Math.round(performance.now()-m);if(!o.ok){const y=await o.text()||`HTTP ${o.status}`;throw n({at:Date.now(),method:"GET",path:h,status:o.status,durationMs:u,ok:!1,error:y}),new Error(y)}n({at:Date.now(),method:"GET",path:h,status:o.status,durationMs:u,ok:!0});const g=await o.json();if(!g.token)throw new Error("Dev token response has no token");return g.token}function Ee(t){return`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/connect/${encodeURIComponent(t)}`}function Ae(t){var a;const s=(a=t==null?void 0:t.gameConfig)==null?void 0:a.gameWorkerUrl;return s?`${s.replace(/\/$/,"")}/game-ui.html`:null}function ne(t){const s={};for(const[a,n]of Object.entries((t==null?void 0:t.roleMapping)||{}))s[n]=a;return s}function Ie(t){const s={};for(const[a,n]of Object.entries((t==null?void 0:t.roleMapping)||{})){const r=t==null?void 0:t.players[n];r&&(s[a]={name:r.displayName||n})}return s}function Ge(t){const s=ne(t),a={};for(const[n,r]of Object.entries(t.players))a[n]={displayName:r.displayName,connected:r.connected,isOwner:r.isOwner,type:r.type,role:s[n]??null,modelName:r.modelName};return{roomId:t.roomId,ownerId:t.ownerId,ownerDisplayName:t.ownerDisplayName,name:t.name,isPublic:t.isPublic,phase:t.phase,players:a,gameConfig:t.gameConfig?{gameId:t.gameConfig.gameId,maxPlayers:t.gameConfig.maxPlayers,roleIds:t.gameConfig.roleIds,auto_save_mode:t.gameConfig.auto_save_mode}:null,stateHistory:t.stateHistory.map(n=>({index:n.index,name:n.name,timestamp:n.timestamp})),runtimeId:t.runtimeId,stateIndex:t.stateIndex,you:{userId:"operator",isOwner:!1,role:null,isAuthorized:!0}}}function Pe(t){return t?xe(Ge(t),{kind:"operator"}):{setGame:!1,managePlayers:!1,assignRoles:!1,startGame:!1,pauseGame:!1,resumeGame:!1,stopGame:!1,restartGame:!1,backtrack:!1,approveJoin:!1,editRoomMeta:!1,forceAct:!1,inspectFullState:!1}}async function $e(t,s,a){const n=[];let r=!0;for(const[h,m]of a.entries()){const o=`dev${h+1}`,u=await t.createPlayer(s,{roleId:m,displayName:o});if(!u.ok){r=!1,n.push(`创建 ${m} 失败: ${u.error}`);continue}const g=await t.assignRole(s,m,u.data.userId);if(!g.ok){r=!1,n.push(`分配 ${m} 失败: ${g.error}`);continue}n.push(`${o} → ${m}`)}return{ok:r,messages:n}}async function de(t,s,a){var r;const n=await t.getPerspective(s,a);return n.ok?(r=n.data)!=null&&r.data?{ok:!0,data:n.data.data}:{ok:!1,error:"Perspective response is empty"}:n}function ae(t){return new Date(t).toLocaleTimeString("zh-CN",{hour12:!1})}function Oe(t){return t<1e3?`${t}ms`:`${(t/1e3).toFixed(2)}s`}function Me(t,s=64){if(t.length<=s)return t;const a=Math.floor((s-1)/2);return`${t.slice(0,a)}…${t.slice(-a)}`}const Z=({value:t,emptyText:s="(空)",maxHeight:a=320,className:n})=>{const[r,h]=c.useState(!1),m=c.useMemo(()=>{if(t==null)return"";try{return JSON.stringify(t,null,2)}catch{return String(t)}},[t]),o=async()=>{if(m)try{await navigator.clipboard.writeText(m),h(!0),window.setTimeout(()=>h(!1),1500)}catch{h(!1)}};return m?e.jsxs("div",{className:`json-block ${n??""}`,children:[e.jsx("button",{type:"button",className:"json-block__copy",onClick:o,children:r?"已复制":"复制"}),e.jsx("pre",{className:"json-block__body",style:{maxHeight:a},children:m})]}):e.jsx("div",{className:`json-block json-block--empty ${n??""}`,children:s})},te=t=>window.confirm(t),De=({room:t,debug:s,admin:a,execute:n,refresh:r})=>{var X,A,z,ie,ce;const[h,m]=c.useState(null),[o,u]=c.useState(null),[g,y]=c.useState(""),[C,w]=c.useState(""),[k,f]=c.useState(""),[T,R]=c.useState(""),[v,p]=c.useState(null),[x,I]=c.useState(null),[H,Q]=c.useState(null),[L,O]=c.useState(null),[U,P]=c.useState(""),[B,re]=c.useState(""),[Y,ee]=c.useState(null),$=c.useMemo(()=>{var i;return((i=t==null?void 0:t.gameConfig)==null?void 0:i.roleIds)??[]},[(X=t==null?void 0:t.gameConfig)==null?void 0:X.roleIds]),le=c.useMemo(()=>ne(t),[t]),q=c.useMemo(()=>Pe(t),[t]);if(c.useEffect(()=>{t!=null&&t.gameConfig&&(y(t.gameConfig.gameId),w(t.gameConfig.gameWorkerUrl),f(t.gameConfig.selectedPlayerCount?String(t.gameConfig.selectedPlayerCount):""))},[(A=t==null?void 0:t.gameConfig)==null?void 0:A.gameId,(z=t==null?void 0:t.gameConfig)==null?void 0:z.gameWorkerUrl,(ie=t==null?void 0:t.gameConfig)==null?void 0:ie.selectedPlayerCount]),c.useEffect(()=>{$.length!==0&&((!T||!$.includes(T))&&R($[0]),(!U||!$.includes(U))&&P($[0]))},[$,T,U]),!t)return e.jsx("div",{className:"panel-empty",children:"房间尚未就绪。"});const l=(i,S)=>m({tone:i,text:S}),d=async(i,S,J)=>{u(i),m(null);const W=await n(S);return u(null),W.ok?l("success",J):l("error",W.error),W},N=async()=>{if(!C.trim()){l("error","请填写 Game Worker 地址");return}await d("set-game",()=>a.setGame(t.roomId,{gameId:g.trim()||void 0,gameWorkerUrl:C.trim(),selectedPlayerCount:k?Number(k):void 0}),"游戏已设置")},b=async()=>{u("prepare"),m(null);const i=await $e(a,t.roomId,$);await r(),u(null),l(i.ok?"success":"error",i.ok?`已准备 dev 玩家:${i.messages.join(",")}`:i.messages.join(";"))},_=async(i,S)=>{S&&await d(`assign-${i}`,()=>a.assignRole(t.roomId,i,S),`${i} 已分配`)},j=async i=>{te(`确定移除玩家 ${i}?`)&&await d(`remove-${i}`,()=>a.removePlayer(t.roomId,i),`已移除 ${i}`)},E=async()=>{if(!T)return;I(null);const i=await de(a,t.roomId,T);i.ok?p(i.data):(p(null),I(i.error))},G=async()=>{m(null);const i={},S=[];for(const J of $){const W=await de(a,t.roomId,J);W.ok?i[J]=W.data:S.push(`${J}: ${W.error}`)}Q(i),S.length>0?l("error",S.join(";")):l("success",`已加载 ${Object.keys(i).length} 个角色视角`)},F=async i=>{te(`确定回溯到快照 #${i}?`)&&(await d(`backtrack-${i}`,()=>a.backtrack(t.roomId,i),`已回溯到 #${i}`),O(null))},V=async()=>{let i;try{i=JSON.parse(B)}catch{l("error","动作 JSON 无效");return}if(!i.action_id){l("error","动作必须包含 action_id");return}const S=await d("force-action",()=>a.submitAction(t.roomId,U,i),"强制动作已提交");ee(S)},K=L===null?null:t.stateHistory.find(i=>i.index===L)??null;return e.jsxs("section",{className:"panel admin-panel",children:[h&&e.jsx("div",{className:`notice notice--${h.tone}`,children:h.text}),e.jsxs("div",{className:"admin-grid",children:[e.jsxs("section",{className:"card",children:[e.jsx("h2",{children:"房间概览"}),e.jsxs("dl",{className:"kv",children:[e.jsx("dt",{children:"roomId"}),e.jsx("dd",{className:"mono",children:t.roomId}),e.jsx("dt",{children:"owner"}),e.jsx("dd",{className:"mono",children:t.ownerId}),e.jsx("dt",{children:"phase"}),e.jsx("dd",{children:e.jsx("span",{className:`phase phase--${t.phase}`,children:t.phase})}),e.jsx("dt",{children:"runtimeId"}),e.jsx("dd",{className:"mono",children:t.runtimeId||"—"}),e.jsx("dt",{children:"stateIndex"}),e.jsx("dd",{className:"mono",children:t.stateIndex}),e.jsx("dt",{children:"players"}),e.jsx("dd",{className:"mono",children:Object.keys(t.players).length}),e.jsx("dt",{children:"connections"}),e.jsx("dd",{className:"mono",children:(s==null?void 0:s.connectedCount)??"—"}),e.jsx("dt",{children:"history"}),e.jsxs("dd",{className:"mono",children:[t.history.length," 条事件 / ",t.stateHistory.length," 个快照"]}),e.jsx("dt",{children:"game"}),e.jsx("dd",{className:"mono",children:t.gameConfig?`${t.gameConfig.gameId} (${t.gameConfig.maxPlayers}P)`:"未选择"}),e.jsx("dt",{children:"worker"}),e.jsx("dd",{className:"mono break-all",children:((ce=t.gameConfig)==null?void 0:ce.gameWorkerUrl)||"—"})]})]}),e.jsxs("section",{className:"card",children:[e.jsx("h2",{children:"生命周期"}),e.jsxs("div",{className:"form-row",children:[e.jsxs("label",{className:"field",children:[e.jsx("span",{className:"field-label",children:"Game ID(可留空,由 worker 决定)"}),e.jsx("input",{className:"input",value:g,onChange:i=>y(i.target.value),placeholder:"gomoku"})]}),e.jsxs("label",{className:"field field--grow",children:[e.jsx("span",{className:"field-label",children:"Game Worker URL"}),e.jsx("input",{className:"input",value:C,onChange:i=>w(i.target.value),placeholder:"http://127.0.0.1:8787"})]}),e.jsxs("label",{className:"field field--narrow",children:[e.jsx("span",{className:"field-label",children:"人数"}),e.jsx("input",{className:"input",type:"number",min:1,value:k,onChange:i=>f(i.target.value),placeholder:"自动"})]}),e.jsx("button",{type:"button",className:"button",onClick:()=>void N(),disabled:o!==null||!q.setGame,children:o==="set-game"?"设置中…":"设置游戏"})]}),e.jsxs("div",{className:"button-row",children:[e.jsx("button",{type:"button",className:"button button--primary",onClick:()=>void d("start",()=>a.startGame(t.roomId),"游戏已开始"),disabled:o!==null||!q.startGame,children:"开始"}),e.jsx("button",{type:"button",className:"button",onClick:()=>void d("pause",()=>a.pauseGame(t.roomId),"已暂停"),disabled:o!==null||!q.pauseGame,children:"暂停"}),e.jsx("button",{type:"button",className:"button",onClick:()=>void d("resume",()=>a.resumeGame(t.roomId),"已继续"),disabled:o!==null||!q.resumeGame,children:"继续"}),e.jsx("button",{type:"button",className:"button",onClick:()=>{te("确定重新开始?当前进度会丢失。")&&d("restart",()=>a.restartGame(t.roomId),"已重新开始")},disabled:o!==null||!q.restartGame,children:"重开"}),e.jsx("button",{type:"button",className:"button button--danger",onClick:()=>{te("确定停止游戏并回到大厅?")&&d("stop",()=>a.stopGame(t.roomId),"已停止")},disabled:o!==null||!q.stopGame,children:"停止"})]})]}),e.jsxs("section",{className:"card card--wide",children:[e.jsxs("div",{className:"card__header",children:[e.jsx("h2",{children:"玩家与角色映射"}),e.jsx("button",{type:"button",className:"button button--small",onClick:()=>void b(),disabled:o!==null||!q.managePlayers||$.length===0,children:o==="prepare"?"准备中…":"创建并分配 dev 玩家"})]}),Object.keys(t.players).length===0?e.jsx("p",{className:"muted",children:"暂无玩家。"}):e.jsxs("table",{className:"table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"userId"}),e.jsx("th",{children:"显示名"}),e.jsx("th",{children:"类型"}),e.jsx("th",{children:"连接"}),e.jsx("th",{children:"角色"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:Object.entries(t.players).map(([i,S])=>e.jsxs("tr",{children:[e.jsx("td",{className:"mono",children:i}),e.jsx("td",{children:S.displayName}),e.jsx("td",{children:S.type}),e.jsx("td",{children:S.connected?"在线":"离线"}),e.jsx("td",{className:"mono",children:le[i]||"—"}),e.jsx("td",{children:i!==t.ownerId&&e.jsx("button",{type:"button",className:"button button--ghost button--small",onClick:()=>void j(i),disabled:o!==null||!q.managePlayers,children:"移除"})})]},i))})]}),$.length>0&&e.jsx("div",{className:"role-mapping",children:$.map(i=>e.jsxs("label",{className:"role-mapping__row",children:[e.jsx("span",{className:"mono",children:i}),e.jsxs("select",{className:"select",value:t.roleMapping[i]||"",onChange:S=>void _(i,S.target.value),disabled:o!==null||!q.assignRoles,children:[e.jsx("option",{value:"",children:"未分配"}),Object.entries(t.players).map(([S,J])=>e.jsxs("option",{value:S,children:[J.displayName," (",S,")"]},S))]})]},i))}),t.joinRequests&&Object.keys(t.joinRequests).length>0&&e.jsxs("div",{className:"subcard",children:[e.jsx("h3",{children:"加入申请"}),e.jsx("ul",{className:"plain-list",children:Object.entries(t.joinRequests).map(([i,S])=>e.jsxs("li",{children:[e.jsx("span",{className:"mono",children:i}),e.jsx("span",{children:S.displayName}),S.requestedAt&&e.jsx("span",{className:"muted",children:ae(S.requestedAt)})]},i))})]})]}),e.jsxs("section",{className:"card card--wide",children:[e.jsxs("div",{className:"card__header",children:[e.jsx("h2",{children:"权威 gameState"}),e.jsx("span",{className:"muted",children:"仅 console 可见"})]}),e.jsx(Z,{value:t.gameState,maxHeight:360,emptyText:"游戏尚未开始"})]}),e.jsxs("section",{className:"card card--wide",children:[e.jsxs("div",{className:"card__header",children:[e.jsx("h2",{children:"角色视角"}),e.jsxs("div",{className:"button-row",children:[e.jsx("select",{className:"select",value:T,onChange:i=>R(i.target.value),children:$.map(i=>e.jsx("option",{value:i,children:i},i))}),e.jsx("button",{type:"button",className:"button button--small",onClick:()=>void E(),children:"加载视角"}),e.jsx("button",{type:"button",className:"button button--small",onClick:()=>void G(),disabled:$.length===0,children:"加载全部"})]})]}),x&&e.jsx("div",{className:"notice notice--error",children:x}),H&&e.jsxs("table",{className:"table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"角色"}),e.jsx("th",{children:"当前回合"}),e.jsx("th",{children:"身份"})]})}),e.jsx("tbody",{children:Object.entries(H).map(([i,S])=>{var J,W;return e.jsxs("tr",{children:[e.jsx("td",{className:"mono",children:i}),e.jsx("td",{children:(J=S.your_role)!=null&&J.is_current?"是":"否"}),e.jsx("td",{className:"mono",children:((W=S.your_role)==null?void 0:W.identity)||i})]},i)})})]}),v&&e.jsx(Z,{value:v,maxHeight:360})]}),e.jsxs("section",{className:"card",children:[e.jsx("h2",{children:"历史事件"}),t.history.length===0?e.jsx("p",{className:"muted",children:"暂无事件。"}):e.jsx("ol",{className:"event-list",children:t.history.map((i,S)=>e.jsx("li",{children:e.jsxs("details",{children:[e.jsxs("summary",{children:[e.jsxs("span",{className:"mono",children:["#",i.stateIndex??"?"]})," ",i.type||"event",i.roleId?` · ${i.roleId}`:""]}),e.jsx(Z,{value:i,maxHeight:200})]})},`${i.stateIndex??"x"}-${S}`))})]}),e.jsxs("section",{className:"card",children:[e.jsx("h2",{children:"状态快照"}),t.stateHistory.length===0?e.jsx("p",{className:"muted",children:"暂无快照(游戏未开始或未触发 SAVE_STATE)。"}):e.jsx("ul",{className:"snapshot-list",children:t.stateHistory.map(i=>e.jsxs("li",{children:[e.jsxs("div",{className:"snapshot-list__main",children:[e.jsxs("span",{className:"mono",children:["#",i.index]}),e.jsx("span",{children:i.name}),e.jsx("span",{className:"muted",children:ae(i.timestamp)})]}),e.jsxs("div",{className:"button-row",children:[e.jsx("button",{type:"button",className:"button button--ghost button--small",onClick:()=>O(i.index),children:"预览"}),e.jsx("button",{type:"button",className:"button button--small",onClick:()=>void F(i.index),disabled:o!==null||!q.backtrack,children:"回溯"})]})]},i.index))}),K&&e.jsxs("div",{className:"subcard",children:[e.jsxs("h3",{children:["快照 #",K.index," · ",K.name]}),e.jsx(Z,{value:K.state,maxHeight:260})]})]}),e.jsxs("section",{className:"card card--wide",children:[e.jsxs("div",{className:"card__header",children:[e.jsx("h2",{children:"强制提交动作"}),e.jsx("span",{className:"muted",children:"operator:不检查回合,用于构造测试状态"})]}),e.jsxs("div",{className:"form-row",children:[e.jsxs("label",{className:"field field--narrow",children:[e.jsx("span",{className:"field-label",children:"角色"}),e.jsx("select",{className:"select",value:U,onChange:i=>P(i.target.value),children:$.map(i=>e.jsx("option",{value:i,children:i},i))})]}),e.jsxs("label",{className:"field field--grow",children:[e.jsx("span",{className:"field-label",children:"动作 JSON"}),e.jsx("textarea",{className:"textarea mono",rows:4,value:B,onChange:i=>re(i.target.value),placeholder:`{
|
|
2
|
+
"action_id": "place",
|
|
3
|
+
"params": { "x": 7, "y": 7 }
|
|
4
|
+
}`})]}),e.jsx("button",{type:"button",className:"button button--primary",onClick:()=>void V(),disabled:o!==null||!q.forceAct||!B.trim(),children:o==="force-action"?"提交中…":"提交动作"})]}),!q.forceAct&&e.jsx("p",{className:"muted",children:"只有 playing 阶段可以强制提交动作。"}),Y&&e.jsxs("div",{className:"subcard",children:[e.jsx("h3",{children:Y.ok?"提交成功":`提交失败: ${Y.error}`}),e.jsx(Z,{value:Y.data??Y,maxHeight:260})]})]})]})]})},Le=({entries:t,onClear:s})=>{const[a,n]=c.useState(!0);return e.jsxs("section",{className:`request-log ${a?"request-log--open":""}`,children:[e.jsxs("header",{className:"request-log__header",children:[e.jsxs("button",{type:"button",className:"request-log__toggle",onClick:()=>n(r=>!r),children:[a?"▾":"▸"," 请求日志",e.jsx("span",{className:"request-log__count",children:t.length})]}),e.jsxs("div",{className:"request-log__actions",children:[e.jsx("span",{className:"request-log__hint",children:"仅本页请求,最多保留 200 条"}),e.jsx("button",{type:"button",className:"button button--ghost button--small",onClick:s,disabled:t.length===0,children:"清空"})]})]}),a&&e.jsx("div",{className:"request-log__body",children:t.length===0?e.jsx("div",{className:"request-log__empty",children:"还没有请求"}):e.jsxs("table",{className:"request-log__table",children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"时间"}),e.jsx("th",{children:"方法"}),e.jsx("th",{children:"路径"}),e.jsx("th",{children:"状态"}),e.jsx("th",{children:"耗时"})]})}),e.jsx("tbody",{children:t.map(r=>e.jsxs("tr",{className:r.ok?"":"is-error",children:[e.jsx("td",{className:"mono",children:ae(r.at)}),e.jsx("td",{className:"mono",children:r.method}),e.jsx("td",{className:"mono request-log__path",title:r.path,children:Me(r.path,92)}),e.jsxs("td",{className:"mono",children:[r.status??"ERR",r.error?` · ${r.error}`:""]}),e.jsx("td",{className:"mono",children:Oe(r.durationMs)})]},r.id))})]})})]})},Ue=({message:t,tone:s="info"})=>e.jsx("div",{className:`game-statusbar${s==="error"?" game-statusbar--error":""}`,"data-tone":s,children:t&&e.jsx("span",{className:"game-statusbar__message",children:t})}),qe={lobby:"准备中",playing:"游戏进行中",paused:"游戏暂停",finished:"游戏已结束"},Fe=({room:t,gameName:s,phase:a,forceReadonly:n,onForceReadonlyChange:r,canPause:h,canResume:m,canRestart:o,busy:u,onPlayPause:g,onRestart:y})=>{const[C,w]=c.useState(!1),[k,f]=c.useState(!1),T=Object.entries(t.players),R=Object.entries(t.roleMapping);return e.jsx("div",{className:"game-topbar",children:e.jsxs("div",{className:"game-topbar__row",children:[e.jsxs("div",{className:"game-topbar__left",children:[T.length>0&&e.jsxs("div",{className:"game-topbar__dropdown",children:[e.jsxs("button",{type:"button",className:"game-topbar__dropdown-button",onClick:()=>w(v=>!v),title:"玩家列表",children:["👥 玩家 (",T.length,")"]}),C&&e.jsx("div",{className:"game-topbar__dropdown-menu",children:T.map(([v,p])=>e.jsxs("div",{className:"game-topbar__dropdown-item",children:[e.jsx("span",{className:"game-topbar__player-name",children:p.displayName}),e.jsx("span",{className:"game-topbar__player-type",children:p.type})]},v))})]}),R.length>0&&e.jsxs("div",{className:"game-topbar__dropdown",children:[e.jsxs("button",{type:"button",className:"game-topbar__dropdown-button",onClick:()=>f(v=>!v),title:"角色映射",children:["🎭 角色 (",R.length,")"]}),k&&e.jsx("div",{className:"game-topbar__dropdown-menu",children:R.map(([v,p])=>{const x=t.players[p];return e.jsxs("div",{className:"game-topbar__dropdown-item",children:[e.jsx("span",{className:"game-topbar__role-name",children:v}),e.jsx("span",{className:"game-topbar__arrow",children:"→"}),e.jsx("span",{className:"game-topbar__player-name",children:(x==null?void 0:x.displayName)||"未分配"})]},v)})})]})]}),e.jsx("div",{className:"game-topbar__center",children:e.jsxs("div",{className:"game-topbar__info",children:[s&&e.jsx("h2",{className:"game-topbar__game-name",children:s}),e.jsx("p",{className:"game-topbar__phase",children:qe[a]})]})}),e.jsxs("div",{className:"game-topbar__right",children:[e.jsxs("label",{className:`game-topbar__readonly${n?" is-on":""}`,title:"开启后不会向游戏提交动作,只做只读预览",children:[e.jsx("input",{type:"checkbox",checked:n,onChange:v=>r(v.target.checked)}),e.jsx("span",{className:"game-topbar__readonly-switch","aria-hidden":"true"}),"只读"]}),(h||m)&&e.jsx("button",{type:"button",className:`play-pause-button ${h?"pause":"play"}`,onClick:g,disabled:u,title:h?"暂停游戏(当前视角即快照)":"继续游戏","aria-label":h?"暂停游戏":"继续游戏",children:h?"⏸":"▶"}),o&&e.jsx("button",{type:"button",className:"restart-button",onClick:y,disabled:u,title:"重新开始游戏","aria-label":"重新开始游戏",children:"↻"})]})]})})},Be=({room:t,openUserIds:s,onSelect:a,onClose:n,onGoAdmin:r})=>{const h=ne(t),m=Object.entries(t.players);return c.useEffect(()=>{const o=u=>{u.key==="Escape"&&n()};return window.addEventListener("keydown",o),()=>window.removeEventListener("keydown",o)},[n]),e.jsx("div",{className:"sim-modal",role:"presentation",onMouseDown:n,children:e.jsxs("div",{className:"sim-modal__card",role:"dialog","aria-modal":"true","aria-label":"选择玩家",onMouseDown:o=>o.stopPropagation(),children:[e.jsxs("header",{className:"sim-modal__header",children:[e.jsxs("div",{children:[e.jsx("h3",{children:"打开玩家视角"}),e.jsx("p",{children:"每个标签页是一条真实的玩家连接。"})]}),e.jsx("button",{type:"button",className:"sim-modal__close",onClick:n,"aria-label":"关闭",children:"×"})]}),m.length===0?e.jsxs("div",{className:"sim-modal__empty",children:[e.jsx("p",{children:"房间里还没有玩家。"}),e.jsx("p",{className:"muted",children:"请先在管理面板创建并分配 dev 玩家。"})]}):e.jsx("ul",{className:"sim-modal__list",children:m.map(([o,u])=>{const g=o.startsWith("dev:"),y=s.has(o),C=h[o],w=!g||y;let k="";return y?k="已在标签页中打开":g||(k="真实玩家:连接会挤掉其当前会话"),e.jsxs("li",{children:[e.jsxs("button",{type:"button",className:"sim-modal__player",onClick:()=>a(o),disabled:w,title:k||`打开 ${u.displayName} 的视角`,children:[e.jsxs("span",{className:"sim-modal__player-main",children:[e.jsx("span",{className:"sim-modal__player-name",children:u.displayName||o}),e.jsx("span",{className:"sim-modal__player-id",children:o})]}),e.jsxs("span",{className:"sim-modal__player-meta",children:[e.jsx("span",{className:`sim-modal__role${C?"":" is-spectator"}`,children:C??"spectator"}),y&&e.jsx("span",{className:"sim-modal__state",children:"已打开"})]})]}),k&&e.jsx("p",{className:"sim-modal__reason",children:k})]},o)})}),e.jsxs("footer",{className:"sim-modal__footer",children:[e.jsx("span",{className:"muted",children:"只有 dev 用户可以在 console 中建立连接。"}),e.jsx("button",{type:"button",className:"button button--ghost",onClick:r,children:"去管理面板"})]})]})})};function Je(t){const{roomId:s,getConnection:a,isAuthLoading:n=!1,onJoinRequest:r,onError:h,onKicked:m,connectionOptions:o}=t,u=c.useRef(s);u.current=s;const g=c.useRef(a);g.current=a;const y=c.useRef(r);y.current=r;const C=c.useRef(h);C.current=h;const w=c.useRef(m);w.current=m;const k=c.useRef(o);k.current=o;const[f]=c.useState(()=>new we({getConnection:()=>{const p=u.current;if(!p)throw new Error("useEngineConnection: roomId is required");return g.current(p)},onJoinRequest:(p,x)=>{var I;return(I=y.current)==null?void 0:I.call(y,p,x)},onError:p=>{var x;return(x=C.current)==null?void 0:x.call(C,p)},onKicked:p=>{var x;return(x=w.current)==null?void 0:x.call(w,p)},...k.current})),T=c.useCallback(()=>f.getSnapshot(),[f]),R=c.useSyncExternalStore(f.subscribe,T,T),v=c.useMemo(()=>ke(f),[f]);return c.useEffect(()=>{if(!(!s||n))return f.connect(),()=>f.disconnect()},[f,s,n]),c.useMemo(()=>{const p=R.engineState;return{...R,...v,connection:f,retry:()=>f.retry(),sendAction:(x,I,H=!1)=>f.act({action_id:x,params:I},H),phase:(p==null?void 0:p.phase)??null,ownerId:(p==null?void 0:p.ownerId)??null,ownerDisplayName:(p==null?void 0:p.ownerDisplayName)??null,roomName:(p==null?void 0:p.name)??null,isPublic:(p==null?void 0:p.isPublic)??!0,isOwner:(p==null?void 0:p.you.isOwner)??!1,myRole:(p==null?void 0:p.you.role)??null,myUserId:(p==null?void 0:p.you.userId)??null}},[R,v,f])}function se(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function We(t){return se(t)&&t.type==="IFRAME_READY"}function He(t){if(!se(t)||t.type!=="ACT")return!1;const s=t.payload;return!(!se(s)||typeof s.action_id!="string"||s.action_id.length===0||typeof s.role_id!="string"||s.role_id.length===0||s.params!=null&&!se(s.params)&&!Array.isArray(s.params))}function Ke(t){try{return JSON.stringify(t)}catch{return null}}const Ye=3e3,Ve=({uiUrl:t,gameId:s,perspective:a,readonly:n,isMyTurn:r,metadata:h,onAction:m,className:o})=>{const u=c.useRef(null),g=c.useRef(m);g.current=m;const[y,C]=c.useState(!1),[w,k]=c.useState(!1),[f,T]=c.useState(null),[R,v]=c.useState(0),p=c.useRef(null),x=r??(a==null?void 0:a.your_role.is_current)??!1;c.useEffect(()=>{C(!1),k(!1),T(null),p.current=null},[t,R]),c.useEffect(()=>{const L=O=>{var B;const U=(B=u.current)==null?void 0:B.contentWindow;if(!U||O.source!==U)return;const P=O.data;if(We(P)){k(!0),C(!0);return}if(He(P)){g.current({action_id:P.payload.action_id,role_id:P.payload.role_id,params:P.payload.params});return}P&&typeof P=="object"&&P.type==="ACT"&&console.warn("[GameUI] Ignored invalid ACT message from iframe:",P)};return window.addEventListener("message",L),()=>window.removeEventListener("message",L)},[]),c.useEffect(()=>{if(!y||w)return;const L=setTimeout(()=>{k(!0)},Ye);return()=>clearTimeout(L)},[y,w]),c.useEffect(()=>{var P;if(!w||!a)return;const L=(P=u.current)==null?void 0:P.contentWindow;if(!L)return;const O={type:"SYNC_STATE",payload:{perspective:a,isMyTurn:x,readonly:n,metadata:h}},U=Ke(O.payload);U!==null&&U===p.current||(p.current=U,L.postMessage(O,"*"))},[w,a,x,n,h]);const I=c.useCallback(()=>{C(!0)},[]),H=c.useCallback(()=>{T(`Failed to load game UI for: ${s}`)},[s]),Q=c.useCallback(()=>{v(L=>L+1)},[]);return t?e.jsxs("div",{className:`game-frame ${o??""}`,children:[!w&&!f&&e.jsxs("div",{className:"game-frame__overlay",children:[e.jsx("span",{className:"game-frame__spinner","aria-hidden":"true"}),e.jsx("p",{children:a?"Loading game UI...":"Waiting for game perspective..."})]}),f&&e.jsxs("div",{className:"game-frame__overlay game-frame__overlay--error",children:[e.jsx("h3",{children:"Failed to Load Game UI"}),e.jsx("p",{children:f}),e.jsx("button",{type:"button",onClick:Q,children:"Retry"})]}),!f&&e.jsx("iframe",{ref:u,src:t,sandbox:"allow-scripts",onLoad:I,onError:H,className:"game-frame__iframe",title:`Game UI: ${s}`},`${t}:${R}`)]}):e.jsx("div",{className:`game-frame game-frame--message ${o??""}`,children:e.jsxs("div",{className:"game-frame__message",children:[e.jsx("h3",{children:"Game UI Not Available"}),e.jsxs("p",{children:["No UI URL configured for game: ",s]})]})})},Xe=({room:t,userId:s,roleId:a,forceReadonly:n,log:r,onStatusChange:h,onErrorChange:m})=>{var f;const o=c.useCallback(async()=>{const T=await Re(t.roomId,s,s,r);return{url:Ee(t.roomId),token:T}},[t.roomId,s,r]),u=Je({roomId:t.roomId,getConnection:o}),g=u.isConnected?"connected":u.isRetrying?"retrying":u.isConnecting?"connecting":"disconnected";c.useEffect(()=>{h(s,g)},[s,g,h]),c.useEffect(()=>{m(s,u.error)},[s,u.error,m]);const y=u.gameState,C=u.phase??t.phase,w=n||C!=="playing",k=T=>{u.sendAction(T.action_id,T.params,!0)};return e.jsx(Ve,{uiUrl:Ae(t),gameId:((f=t.gameConfig)==null?void 0:f.gameId)||"unknown",perspective:y,readonly:w,metadata:{roomId:t.roomId,roleId:a??"",playerId:s,roleDisplayMapping:Ie(t)},onAction:k,className:"sim-frame"})},me="left",ze="right",Qe=[{id:me,tabs:[],activeTab:null}],Ze=({room:t,forceReadonly:s,log:a,onGoAdmin:n})=>{const[r,h]=c.useState(Qe),[m,o]=c.useState(null),[u,g]=c.useState({}),[y,C]=c.useState({}),[w,k]=c.useState(null),[f,T]=c.useState(null),R=c.useMemo(()=>ne(t),[t]),v=c.useMemo(()=>Object.keys(t.players).sort().join("\0"),[t.players]);c.useEffect(()=>{const l=new Set(Object.keys(t.players));h(d=>{let N=!1;const b=d.map(_=>{const j=_.tabs.filter(G=>l.has(G));if(j.length===_.tabs.length)return _;N=!0;const E=_.activeTab&&j.includes(_.activeTab)?_.activeTab:j[0]??null;return{..._,tabs:j,activeTab:E}});return N?b:d})},[t,v]);const p=c.useCallback((l,d)=>{g(N=>N[l]===d?N:{...N,[l]:d})},[]),x=c.useCallback((l,d)=>{C(N=>N[l]===d?N:{...N,[l]:d})},[]),I=l=>o(l),H=l=>{const d=m??me,N=r.find(b=>b.tabs.includes(l));if(N){h(b=>b.map(_=>_.id===N.id?{..._,activeTab:l}:_)),o(null);return}h(b=>b.map(_=>_.id===d?{..._,tabs:[..._.tabs,l],activeTab:l}:_)),o(null)},Q=(l,d)=>{h(N=>N.map(b=>{if(b.id!==l)return b;const _=b.tabs.indexOf(d);if(_<0)return b;const j=b.tabs.filter(G=>G!==d),E=b.activeTab===d?j[Math.min(_,j.length-1)]??null:b.activeTab;return{...b,tabs:j,activeTab:E}}))},L=(l,d)=>{h(N=>N.map(b=>b.id===l?{...b,activeTab:d}:b))},O=r.length>1,U=()=>{if(T(null),o(null),O){h(l=>{if(l.length<2)return l;const[d,...N]=l,b=[...d.tabs];for(const j of N)for(const E of j.tabs)b.includes(E)||b.push(E);const _=N.map(j=>j.activeTab).find(j=>!!j&&b.includes(j));return[{...d,tabs:b,activeTab:d.activeTab??_??null}]});return}h(l=>l.length===1?[l[0],{id:ze,tabs:[],activeTab:null}]:l)},P=(l,d,N,b)=>{h(_=>{const j=_.find(A=>A.id===l),E=_.find(A=>A.id===N);if(!j||!E||!j.tabs.includes(d))return _;const G=j.tabs.indexOf(d);let F=Math.max(0,Math.min(b,E.tabs.length));if(j.id===E.id&&G<F&&(F-=1),j.id===E.id){if(F===G)return _;const A=[...j.tabs];return A.splice(G,1),A.splice(F,0,d),_.map(z=>z.id===j.id?{...z,tabs:A}:z)}const V=j.tabs.filter(A=>A!==d),K=j.activeTab===d?V[Math.min(G,V.length-1)]??null:j.activeTab,X=[...E.tabs];return X.splice(F,0,d),_.map(A=>A.id===j.id?{...A,tabs:V,activeTab:K}:A.id===E.id?{...A,tabs:X,activeTab:d}:A)})},B=()=>{k(null),T(null)},re=(l,d,N)=>{k({userId:N,fromPane:d}),l.dataTransfer.effectAllowed="move",l.dataTransfer.setData("text/plain",N)},Y=(l,d,N)=>{if(!w)return;l.preventDefault(),l.stopPropagation(),l.dataTransfer.dropEffect="move";const b=l.currentTarget.getBoundingClientRect(),_=N+(l.clientX>b.left+b.width/2?1:0);T(j=>(j==null?void 0:j.paneId)===d&&j.index===_?j:{paneId:d,index:_})},ee=(l,d,N)=>{w&&(l.preventDefault(),l.dataTransfer.dropEffect="move",T(b=>(b==null?void 0:b.paneId)===d&&b.index===N?b:{paneId:d,index:N}))},$=(l,d)=>{var j,E;if(!w)return;l.preventDefault();const N=r.find(G=>G.id===d);if(!N){B();return}const b=(E=(j=l.target)==null?void 0:j.closest)==null?void 0:E.call(j,"[data-tab-id]");let _=N.tabs.length;if(b!=null&&b.dataset.tabId){const G=N.tabs.indexOf(b.dataset.tabId);if(G>=0){const F=b.getBoundingClientRect();_=G+(l.clientX>F.left+F.width/2?1:0)}}P(w.fromPane,w.userId,d,_),B()},le=l=>{const d=t.players[l];return{roleId:R[l]??null,playerName:(d==null?void 0:d.displayName)||l}},q=new Set(r.flatMap(l=>l.tabs));return e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"sim-strips",children:[r.map(l=>e.jsxs("div",{"data-pane-id":l.id,className:`sim-strip${(f==null?void 0:f.paneId)===l.id?" is-drop-target":""}`,onDragOver:d=>ee(d,l.id,l.tabs.length),onDrop:d=>$(d,l.id),children:[e.jsxs("div",{className:"sim-strip__list",role:"tablist","aria-label":"玩家视角",children:[l.tabs.map((d,N)=>{const{roleId:b,playerName:_}=le(d),j=d===l.activeTab,E=u[d]??"connecting",G=y[d],F=G?`连接错误: ${G}`:E==="connected"?"已连接":E==="retrying"?"重连中…":E==="connecting"?"连接中…":"未连接",V=(f==null?void 0:f.paneId)===l.id&&f.index===N,K=(f==null?void 0:f.paneId)===l.id&&f.index===l.tabs.length&&N===l.tabs.length-1,X=(w==null?void 0:w.userId)===d&&w.fromPane===l.id;return e.jsxs("div",{"data-tab-id":d,className:`sim-tab${j?" is-active":""}${X?" is-dragging":""}${V?" is-drop-before":""}${K?" is-drop-after":""}`,onDragOver:A=>Y(A,l.id,N),children:[e.jsxs("button",{type:"button",role:"tab","aria-selected":j,className:"sim-tab__title",draggable:!0,onDragStart:A=>re(A,l.id,d),onDragEnd:B,onClick:()=>L(l.id,d),title:`${_} · ${b??"spectator"} · ${F}`,children:[e.jsx("span",{className:`sim-tab__dot sim-tab__dot--${E}${G?" is-error":""}`,title:F,"aria-hidden":"true"}),b?e.jsx("span",{className:"sim-tab__role",children:b}):e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"sim-tab__spectator",children:"spectator"}),e.jsx("span",{className:"sim-tab__sep",children:"|"}),e.jsx("span",{className:"sim-tab__player",children:_})]})]}),e.jsx("button",{type:"button",className:"sim-tab__action sim-tab__close",onClick:()=>Q(l.id,d),title:"关闭标签页","aria-label":"关闭标签页",children:"×"})]},d)}),l.tabs.length===0&&w&&e.jsx("span",{className:"sim-strip__hint",children:"拖动标签到这里"})]}),e.jsx("button",{type:"button",className:"sim-strip__add",onClick:()=>I(l.id),title:"打开玩家视角","aria-label":"打开玩家视角",children:"+"})]},l.id)),e.jsx("button",{type:"button",className:`sim-split-toggle${O?" is-on":""}`,onClick:U,title:O?"取消拆分(合并标签)":"左右拆分","aria-label":O?"取消拆分":"左右拆分","aria-pressed":O,children:e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 16 16","aria-hidden":"true",children:O?e.jsx("rect",{x:"2",y:"2.5",width:"12",height:"11",rx:"2",fill:"none",stroke:"currentColor",strokeWidth:"1.5"}):e.jsxs(e.Fragment,{children:[e.jsx("rect",{x:"1.5",y:"2.5",width:"5.5",height:"11",rx:"1.5",fill:"none",stroke:"currentColor",strokeWidth:"1.5"}),e.jsx("rect",{x:"9",y:"2.5",width:"5.5",height:"11",rx:"1.5",fill:"none",stroke:"currentColor",strokeWidth:"1.5"})]})})})]}),e.jsx("div",{className:"sim-frames",children:r.map(l=>e.jsx("div",{"data-pane-id":l.id,className:`sim-pane${(f==null?void 0:f.paneId)===l.id?" is-drop-target":""}`,onDragOver:d=>ee(d,l.id,l.tabs.length),onDrop:d=>$(d,l.id),children:l.tabs.length===0?e.jsx("div",{className:"sim-pane__empty",children:O?e.jsxs(e.Fragment,{children:[e.jsx("p",{children:"把标签拖到这里,或"}),e.jsx("button",{type:"button",className:"button button--primary",onClick:()=>I(l.id),children:"打开玩家视角"})]}):e.jsxs(e.Fragment,{children:[e.jsx("h3",{children:"还没有打开的玩家视角"}),e.jsx("p",{children:"每个标签页是一条真实的玩家连接,可以拖动标签左右拆分对比。"}),e.jsx("button",{type:"button",className:"button button--primary",onClick:()=>I(l.id),children:"选择玩家"})]})}):l.tabs.map(d=>e.jsx("div",{"data-tab-id":d,className:`sim-pane__frame${l.activeTab===d?"":" is-hidden"}`,children:e.jsx(Xe,{room:t,userId:d,roleId:R[d]??null,forceReadonly:s,log:a,onStatusChange:p,onErrorChange:x})},d))},l.id))}),m&&e.jsx(Be,{room:t,openUserIds:q,onSelect:H,onClose:()=>o(null),onGoAdmin:n})]})},et=5e3,tt=({room:t,admin:s,execute:a,log:n,onGoAdmin:r})=>{const[h,m]=c.useState(!1),[o,u]=c.useState(null),[g,y]=c.useState(null);c.useEffect(()=>{if(!g)return;const x=window.setTimeout(()=>y(null),et);return()=>window.clearTimeout(x)},[g]);const C=async x=>{if(!t||x==="restart"&&!confirm("确定要重新开始游戏吗?当前进度将会丢失。"))return;u(x),y(null);const I=await a(()=>x==="pause"?s.pauseGame(t.roomId):x==="resume"?s.resumeGame(t.roomId):s.restartGame(t.roomId));u(null),y(I.ok?{tone:"success",text:x==="pause"?"已暂停 · 当前视角即快照":x==="resume"?"已继续游戏":"已重新开始"}:{tone:"error",text:`操作失败: ${I.error}`})};if(!t)return e.jsxs("div",{className:"panel-empty",children:[e.jsx("h2",{children:"房间尚未就绪"}),e.jsxs("p",{children:["先运行 ",e.jsx("code",{children:"npgame-dev serve"}),",再在管理页或 CLI 中 connect 游戏。"]}),e.jsx("a",{className:"button",href:"/",children:"打开管理页"})]});if(!t.gameConfig)return e.jsxs("div",{className:"panel-empty",children:[e.jsx("h2",{children:"还没有选择游戏"}),e.jsx("p",{children:"在管理面板里填写 Game Worker 地址并选择游戏。"}),e.jsx("button",{type:"button",className:"button button--primary",onClick:r,children:"去管理面板"})]});if(t.gameConfig.roleIds.length===0)return e.jsx("div",{className:"panel-empty",children:"游戏配置里没有角色。"});const w=t.gameConfig.gameId,k=t.phase,f=k==="playing",T=k==="paused",R=f||T||k==="finished",v=k==="playing"?"游戏进行中":k==="paused"?"游戏已暂停":k==="finished"?"游戏已结束":"等待开始",p=k==="paused"?"warning":"info";return e.jsx("section",{className:"panel",children:e.jsxs("div",{className:"game-stage",children:[e.jsx(Fe,{room:t,gameName:w,phase:k,forceReadonly:h,onForceReadonlyChange:m,canPause:f,canResume:T,canRestart:R,busy:o!==null,onPlayPause:()=>void C(f?"pause":"resume"),onRestart:()=>void C("restart")}),e.jsx(Ze,{room:t,forceReadonly:h,log:n,onGoAdmin:r}),e.jsx(Ue,{message:(g==null?void 0:g.text)??v,tone:(g==null?void 0:g.tone)??p})]})})},st=3e3;function at(t,s){const a=c.useRef(s);a.current=s;const[n]=c.useState(()=>Se(x=>a.current(x))),[r,h]=c.useState(null),[m,o]=c.useState(null),[u,g]=c.useState(null),[y,C]=c.useState(!0),[w,k]=c.useState(null),[f,T]=c.useState(!0),R=c.useRef(!1),v=c.useCallback(async()=>{if(!t){g("缺少 roomId:请从管理页打开房间控制台"),C(!1);return}if(!R.current){R.current=!0;try{const[x,I]=await Promise.all([n.getState(t),n.getDebug(t)]);x.ok?(h({...x.data,roomId:x.data.roomId||t}),g(null)):g(x.error),I.ok&&o(I.data),k(Date.now())}finally{R.current=!1,C(!1)}}},[n,t]);c.useEffect(()=>{v()},[v]),c.useEffect(()=>{if(!f)return;const x=window.setInterval(()=>void v(),st);return()=>window.clearInterval(x)},[f,v]);const p=c.useCallback(async x=>{const I=await x();return await v(),I},[v]);return{admin:n,room:r,debug:m,error:u,isLoading:y,lastUpdated:w,refresh:v,execute:p,autoRefresh:f,setAutoRefresh:T}}const nt=()=>{const[t]=c.useState(ve),[s,a]=c.useState("simulation"),[n,r]=c.useState([]),h=c.useRef(1),m=c.useCallback(v=>{r(p=>{const x=[{...v,id:h.current++},...p];return x.length>200?x.slice(0,200):x})},[]),{admin:o,room:u,debug:g,error:y,isLoading:C,lastUpdated:w,refresh:k,execute:f,autoRefresh:T,setAutoRefresh:R}=at(t,m);return t?e.jsxs("div",{className:"app",children:[e.jsxs("header",{className:"app-header",children:[e.jsxs("div",{className:"app-header__title",children:[e.jsx("a",{className:"app-header__logo",href:"/rooms",title:"返回房间列表",children:"N"}),e.jsxs("div",{children:[e.jsxs("nav",{className:"app-breadcrumb","aria-label":"面包屑",children:[e.jsx("a",{href:"/dashboard",children:"Workspace"}),e.jsx("span",{className:"app-breadcrumb__sep",children:"/"}),e.jsx("a",{href:"/rooms",children:"房间"}),e.jsx("span",{className:"app-breadcrumb__sep",children:"/"}),e.jsx("span",{className:"ws-mono app-breadcrumb__current",children:t})]}),e.jsx("h1",{children:"房间控制台"}),e.jsx("p",{className:"muted",children:"本地开发宿主 · 模拟前端 + 管理界面"})]})]}),e.jsxs("div",{className:"app-header__meta",children:[e.jsx("span",{className:"badge badge--mono",children:t}),u&&e.jsx("span",{className:`phase phase--${u.phase}`,children:u.phase}),g&&e.jsxs("span",{className:"muted",children:[g.connectedCount," 连接 / ",g.playerCount," 玩家"]}),e.jsxs("label",{className:"checkbox",children:[e.jsx("input",{type:"checkbox",checked:T,onChange:v=>R(v.target.checked)}),"自动刷新"]}),e.jsx("button",{type:"button",className:"button button--small",onClick:()=>void k(),disabled:C,children:C?"刷新中…":"刷新"}),e.jsx("span",{className:"app-header__updated muted",children:w?ae(w):"—"})]})]}),e.jsxs("nav",{className:"tabs",children:[e.jsx("button",{type:"button",className:s==="simulation"?"is-active":"",onClick:()=>a("simulation"),children:"模拟前端"}),e.jsx("button",{type:"button",className:s==="admin"?"is-active":"",onClick:()=>a("admin"),children:"管理界面"})]}),e.jsxs("main",{className:"app-main",children:[y&&e.jsxs("div",{className:"notice notice--error",children:["读取房间失败: ",y]}),s==="simulation"?e.jsx(tt,{room:u,admin:o,execute:f,log:m,onGoAdmin:()=>a("admin")}):e.jsx(De,{room:u,debug:g,admin:o,execute:f,refresh:k})]}),e.jsx(Le,{entries:n,onClear:()=>r([])})]}):e.jsxs("div",{className:"app room-missing",children:[e.jsx("h1",{children:"缺少房间 ID"}),e.jsx("p",{className:"muted",children:"房间控制台的地址是 /rooms/<roomId>。"}),e.jsx("p",{className:"muted",children:"请从 Workspace 管理页打开对应房间。"}),e.jsx("a",{className:"button button--primary",href:"/rooms",children:"返回房间列表"})]})};pe.createRoot(document.getElementById("root")).render(e.jsx(fe.StrictMode,{children:e.jsx(nt,{})}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.game-shell{display:flex;flex-direction:column;width:100%;height:100%;min-height:0;overflow:hidden;background:var(--game-shell-bg, #ffffff);color:var(--game-shell-fg, #111827)}.game-shell__top,.game-shell__bottom{display:flex;align-items:center;gap:8px;padding:6px 12px;flex:0 0 auto;background:var(--game-shell-bar-bg, #f9fafb);border-color:var(--game-shell-border, #e5e7eb)}.game-shell__top{border-bottom:1px solid var(--game-shell-border, #e5e7eb)}.game-shell__bottom{border-top:1px solid var(--game-shell-border, #e5e7eb);justify-content:space-between;min-height:32px}.game-shell__side{display:flex;align-items:center;gap:6px;min-width:0;flex:1 1 0}.game-shell__side--right{justify-content:flex-end}.game-shell__top-center{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-width:0;flex:0 1 auto;text-align:center}.game-shell__game-name{font-size:.95rem;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:40vw}.game-shell__phase{font-size:.75rem;color:var(--game-shell-muted, #6b7280)}.game-shell__body{flex:1 1 auto;min-height:0;position:relative}.game-shell__status{flex:0 1 auto;min-width:0;text-align:center;font-size:.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.game-shell__status-text--info{color:var(--game-shell-muted, #6b7280)}.game-shell__status-text--success{color:#15803d}.game-shell__status-text--warning{color:#b45309}.game-shell__status-text--error{color:#b91c1c}.game-shell__status-text--waiting{color:#6d28d9}.game-frame{position:relative;width:100%;height:100%;min-height:0;overflow:hidden;background:var(--game-shell-bg, #ffffff)}.game-frame__iframe{display:block;width:100%;height:100%;border:none;background:transparent}.game-frame__overlay{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:16px;text-align:center;color:var(--game-shell-muted, #6b7280);background:var(--game-shell-bg, #ffffff)}.game-frame__overlay--error{color:#b91c1c}.game-frame__overlay--error h3{margin:0;font-size:1rem}.game-frame__overlay--error p{margin:0;font-size:.85rem;word-break:break-word}.game-frame__spinner{width:22px;height:22px;border:2px solid #e5e7eb;border-top-color:#6b7280;border-radius:50%;animation:game-frame-spin .8s linear infinite}@keyframes game-frame-spin{to{transform:rotate(360deg)}}.game-frame--message{display:flex;align-items:center;justify-content:center;padding:24px}.game-frame__message{text-align:center;color:var(--game-shell-muted, #6b7280)}.game-frame__message h3{margin:0 0 6px;font-size:1rem;color:var(--game-shell-fg, #111827)}.game-frame__message p{margin:0;font-size:.85rem}@media (max-width: 640px){.game-shell__top,.game-shell__bottom{padding:4px 8px}.game-shell__game-name{max-width:32vw;font-size:.85rem}.game-shell__phase{display:none}}
|