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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 👥
+ {{ room.player_count }}/{{ room.max_players }} 玩家
+
+
+ 📡
+ 延迟: {{ Math.floor(Math.random() * 50) + 20 }}ms
+
+
+
+
+
+
+ 加入
+
+
+ 围观
+
+
+
+
+
+
+
+ 🛰️
+ 未监测到活跃战局
+ 去发起匹配
+
+
+
+
+
+
+
+
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;
+ }
+
/**
* 发送游戏状态
*/