diff --git a/pages-game/game/minesweeper/index.vue b/pages-game/game/minesweeper/index.vue index 5cf4b37..f7764bd 100644 --- a/pages-game/game/minesweeper/index.vue +++ b/pages-game/game/minesweeper/index.vue @@ -50,6 +50,13 @@ > {{ entering ? '正在进入...' : (ticketCount > 0 ? '立即开局' : '资格不足') }} + + 📡 对战列表 / 围观 + @@ -120,6 +127,28 @@ export default { this.entering = false this.loadTickets() } + }, + async goRoomList() { + // 获取配置以拿到 nakama 参数,虽然 room-list 也会尝试连接,但通过 URL 传参更稳妥 + try { + const res = await authRequest({ + url: '/api/app/games/enter', + method: 'POST', + data: { + game_code: this.gameCode + } + }) + + const gameToken = encodeURIComponent(res.game_token) + const nakamaServer = encodeURIComponent(res.nakama_server) + const nakamaKey = encodeURIComponent(res.nakama_key) + + uni.navigateTo({ + url: `/pages-game/game/minesweeper/room-list?game_token=${gameToken}&nakama_server=${nakamaServer}&nakama_key=${nakamaKey}` + }) + } catch (e) { + uni.showToast({ title: '无法获取对战列表', icon: 'none' }) + } } } } diff --git a/pages-game/game/minesweeper/play.scss b/pages-game/game/minesweeper/play.scss index f5b359c..c5c6ad7 100644 --- a/pages-game/game/minesweeper/play.scss +++ b/pages-game/game/minesweeper/play.scss @@ -901,6 +901,16 @@ } } +.spectator-badge { + padding: $spacing-sm $spacing-md; + background: rgba($brand-secondary, 0.2); + border: 1px solid rgba($brand-secondary, 0.4); + border-radius: $radius-round; + font-size: $font-sm; + font-weight: 700; + color: $brand-secondary; +} + .timer-progress-bg { width: 660rpx; height: 6rpx; diff --git a/pages-game/game/minesweeper/play.vue b/pages-game/game/minesweeper/play.vue index 28c3966..897bd39 100644 --- a/pages-game/game/minesweeper/play.vue +++ b/pages-game/game/minesweeper/play.vue @@ -98,7 +98,7 @@ :key="p.userId" class="player-card opponent" :class="{ - 'active-turn': gameState.turnOrder[gameState.currentTurnIndex] === p.userId, + 'active-turn': gameState.gameStarted && gameState.turnOrder[gameState.currentTurnIndex] === p.userId, 'damaged': damagedPlayers.includes(p.userId), 'healed': healedPlayers.includes(p.userId) }" @@ -141,6 +141,7 @@ ⏱️ {{ turnTimer }}s + 围观中 @@ -232,6 +233,7 @@ +<<<<<<< HEAD {{ getGameResultEmoji() }} @@ -241,6 +243,14 @@ + + {{ settlementWinnerId === 'draw' ? '🤝' : (settlementWinnerId === myUserId ? '🏆' : '💀') }} + {{ settlementWinnerId === 'draw' ? '平局' : (settlementWinnerId === myUserId ? '胜利!' : '很遗憾失败了') }} + >>>>>> 5ec793a (feat: 为扫雷游戏添加房间列表功能,支持加入和围观现有对局。) :class="{ disabled: isRefreshing }" @tap="refreshAndPlayAgain" > @@ -313,6 +323,9 @@ export default { healedPlayers: [], isRefreshing: false, logsScrollTop: 0, + isSpectator: false, + showSettlement: false, + settlementWinnerId: '', // Timers matchInterval: null, turnInterval: null, @@ -393,7 +406,10 @@ export default { }, onLoad(options) { this.fetchGameConfig(); - const { game_token, nakama_server, nakama_key } = options; + const { game_token, nakama_server, nakama_key, match_id, is_spectator } = options; + if (is_spectator) this.isSpectator = true; + if (match_id) this.matchId = match_id; + if (game_token) { this.initNakama(game_token, decodeURIComponent(nakama_server || ''), decodeURIComponent(nakama_key || '')); } else { @@ -455,6 +471,20 @@ export default { this.addLog('system', '✅ 已连接到远程节点'); // 先设置监听器,再继续后续操作 this.setupSocketListeners(); + + // 如果是直连模式(加入指定房间或围观) + if (this.matchId) { + this.addLog('system', this.isSpectator ? '🔭 正在切入观察视角...' : '🚪 正在进入指定战局...'); + try { + await nakamaManager.joinMatch(this.matchId); + this.addLog('system', '✅ 接入成功'); + setTimeout(() => { + nakamaManager.sendMatchState(this.matchId, 100, JSON.stringify({ action: 'getState' })); + }, 100); + } catch(err) { + this.addLog('system', '❌ 接入失败: ' + err.message); + } + } } catch (e) { this.addLog('system', '❌ 通讯异常: ' + e.message); } @@ -629,45 +659,51 @@ export default { // 游戏结束 - 确保正确设置游戏状态 console.log('[游戏结束] 接收到游戏结束数据:', data); + const winnerId = data.winnerId || (data.gameState && data.gameState.winnerId) || ''; + this.settlementWinnerId = winnerId; + this.showSettlement = !!winnerId; + if (data.gameState) { // 更新游戏状态并标记为已结束 this.gameState = { ...data.gameState, gameStarted: false, // 明确标记游戏已结束 - winnerId: data.winnerId || data.gameState.winnerId + winnerId: winnerId }; + } else if (this.gameState) { + this.gameState.gameStarted = false; + if (winnerId) this.gameState.winnerId = winnerId; } else { this.gameState = { ...data, - gameStarted: false // 明确标记游戏已结束 + gameStarted: false, + winnerId: winnerId }; } - // 确保 winnerId 存在 - if (!this.gameState.winnerId && data.winnerId) { - this.gameState.winnerId = data.winnerId; + // 确保 winnerId 存在于全局状态中以便计算属性使用 + if (this.gameState && !this.gameState.winnerId && winnerId) { + this.gameState.winnerId = winnerId; } console.log('[游戏结束] 最终游戏状态:', this.gameState); - console.log('[游戏结束] winnerId:', this.gameState.winnerId); - console.log('[游戏结束] gameStarted:', this.gameState.gameStarted); - console.log('[游戏结束] 是否显示弹窗:', !this.gameState.gameStarted && !!this.gameState.winnerId); + console.log('[游戏结束] winnerId:', winnerId); // 根据结果显示不同的日志消息 let endMsg = ''; - if (this.gameState.winnerId === 'draw') { + if (winnerId === 'draw') { endMsg = '平局:无人幸存'; - } else if (this.gameState.winnerId === this.myUserId) { + } else if (winnerId === this.myUserId) { endMsg = '🎉 您获得了胜利!'; } else { endMsg = '💀 很遗憾失败了'; } - this.addLog('system', `战局结束:${endMsg}`); + this.addLog('system', endMsg); // 添加震动反馈 - if (this.gameState.winnerId === this.myUserId) { + if (winnerId === this.myUserId) { uni.vibrateShort({ type: 'success' }); - } else if (this.gameState.winnerId === 'draw') { + } else if (winnerId === 'draw') { uni.vibrateShort({ type: 'warning' }); } else { uni.vibrateShort({ type: 'fail' }); @@ -731,6 +767,7 @@ export default { this.addLog('system', '已切断匹配信号'); }, handleCellClick(idx) { + if (this.isSpectator) return; if (!this.gameState || !this.gameState.gameStarted) return; if (!this.isMyTurn) return; if (this.gameState.grid[idx].revealed) return; diff --git a/pages-game/game/minesweeper/room-list.vue b/pages-game/game/minesweeper/room-list.vue new file mode 100644 index 0000000..ee264f1 --- /dev/null +++ b/pages-game/game/minesweeper/room-list.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/pages.json b/pages.json index 13deaab..1a29685 100644 --- a/pages.json +++ b/pages.json @@ -189,6 +189,14 @@ } } }, + { + "path": "game/minesweeper/room-list", + "style": { + "navigationStyle": "default", + "navigationBarTitleText": "对战列表", + "disableScroll": true + } + }, { "path": "game/webview", "style": { diff --git a/utils/nakamaManager.js b/utils/nakamaManager.js index b0ccb28..b04bc70 100644 --- a/utils/nakamaManager.js +++ b/utils/nakamaManager.js @@ -282,12 +282,16 @@ class NakamaManager { await this.authenticateWithGameToken(this.gameToken); } + if (!this.gameToken) { + throw new Error('Missing game token in manager'); + } + console.log('[Nakama] Adding to matchmaker:', minCount, '-', maxCount); const response = await this._send({ matchmaker_add: { min_count: minCount || 2, max_count: maxCount || 2, - query: '*', + query: '+properties.game_token:*', string_properties: { game_token: this.gameToken } } }); @@ -315,6 +319,28 @@ class NakamaManager { return response.match; } + /** + * 调用 RPC + */ + async rpc(id, payload) { + console.log('[Nakama] RPC call:', id); + const response = await this._send({ + rpc: { + id: id, + payload: typeof payload === 'string' ? payload : JSON.stringify(payload) + } + }); + + if (response.rpc && response.rpc.payload) { + try { + return JSON.parse(response.rpc.payload); + } catch (e) { + return response.rpc.payload; + } + } + return response; + } + /** * 发送游戏状态 */