From fa7ad8f538156eb7c891021729dacceeeb53f03f Mon Sep 17 00:00:00 2001
From: zhangyibo95 <690096405@qq.com>
Date: Wed, 15 Jul 2026 16:57:48 +0800
Subject: [PATCH 1/3] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E6=AF=94=E8=B5=9B?=
=?UTF-8?q?=E7=BB=93=E6=9D=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/pages/friend-battle-result.vue | 17 ++++-
src/pages/team-battle/index.vue | 100 ++++++++++++++++++++---------
2 files changed, 83 insertions(+), 34 deletions(-)
diff --git a/src/pages/friend-battle-result.vue b/src/pages/friend-battle-result.vue
index 3e3ac6c..6d09031 100644
--- a/src/pages/friend-battle-result.vue
+++ b/src/pages/friend-battle-result.vue
@@ -27,6 +27,8 @@ const data = ref({});
const showOverlay = ref(false);
/** 自动关闭覆盖层的计时器 */
const overlayTimer = ref(null);
+const overlayDelayTimer = ref(null);
+let loadGeneration = 0;
/** 控制经验条是否执行进场动画(弹窗关闭后才置 true,避免动画被遮挡) */
const showExpAnim = ref(false);
@@ -175,9 +177,11 @@ function getMeleeAvatarBorderColor(rank) {
// ---- 生命周期 ----
onLoad(async (options) => {
+ const currentLoadId = ++loadGeneration;
if (!options.battleId) return;
const result = await getBattleAPI(options.battleId);
+ if (currentLoadId !== loadGeneration) return;
data.value = result;
// 从 teams 各队伍的 players 中找到当前用户,解析经验进度条所需字段
@@ -219,7 +223,8 @@ onLoad(async (options) => {
}
// 数据加载完成后延迟 1 秒再显示激励弹窗,避免进场时画面太杂
- setTimeout(() => {
+ overlayDelayTimer.value = setTimeout(() => {
+ overlayDelayTimer.value = null;
showOverlay.value = true;
// 弹窗显示后 2.5 秒自动关闭
overlayTimer.value = setTimeout(() => {
@@ -229,7 +234,15 @@ onLoad(async (options) => {
});
onBeforeUnmount(() => {
- if (overlayTimer.value) clearTimeout(overlayTimer.value);
+ loadGeneration += 1;
+ if (overlayDelayTimer.value) {
+ clearTimeout(overlayDelayTimer.value);
+ overlayDelayTimer.value = null;
+ }
+ if (overlayTimer.value) {
+ clearTimeout(overlayTimer.value);
+ overlayTimer.value = null;
+ }
});
// ---- 方法 ----
diff --git a/src/pages/team-battle/index.vue b/src/pages/team-battle/index.vue
index 32d3a19..e699b21 100644
--- a/src/pages/team-battle/index.vue
+++ b/src/pages/team-battle/index.vue
@@ -105,6 +105,8 @@ let restoreLoadingTimer = null;
let pendingRoundAudio = false;
// 一旦收到 BattleEnd,后续普通消息就不再进入队列。
let battleEnded = false;
+let battleEndTaskRunning = false;
+let resultNavigationPending = false;
let skipNextRestoreOnShow = false;
let pendingReturnSnapshot = null;
const handledMessageKeys = new Set();
@@ -1094,39 +1096,70 @@ async function runNewRoundTask(task, runId) {
pendingRoundAudio = true;
}
+function navigateToBattleResultOnce(matchId) {
+ const targetMatchId = matchId || battleId.value;
+ if (!targetMatchId || resultNavigationPending) return false;
+
+ resultNavigationPending = true;
+ battleEnded = true;
+ restoreGeneration += 1;
+ if (pendingRestoreTimer) {
+ clearTimeout(pendingRestoreTimer);
+ pendingRestoreTimer = null;
+ }
+ hideRestoreLoading();
+
+ uni.redirectTo({
+ url: `/pages/friend-battle-result?battleId=${targetMatchId}`,
+ fail: (err) => {
+ resultNavigationPending = false;
+ console.log("navigate to battle result failed:", err);
+ },
+ });
+ return true;
+}
+
// 终局任务:播放结束语音后,根据状态跳结果页或返回上一页。
async function runBattleEndTask(task, runId) {
- const battleInfo = task.message;
- applyBattleBase(battleInfo);
- battleEnded = true;
- clearXRingStreaks();
- matchStatus.value = battleInfo.status;
- if (battleInfo.status === 4) {
- showRoundTip.value = true;
- currentBluePoint.value = 0;
- currentRedPoint.value = 0;
+ if (resultNavigationPending) {
+ notifyMatchAudioAck(task);
+ return;
}
- // 终局语音必须完整播完,再决定跳转或返回。
- await playAudioKeys("比赛结束", { interrupt: false, timeout: AUDIO_TIMEOUT_MAX });
- notifyMatchAudioAck(task);
- if (!isQueueAlive(runId)) return;
+ battleEndTaskRunning = true;
+ try {
+ const battleInfo = task.message;
+ applyBattleBase(battleInfo);
+ battleEnded = true;
+ clearXRingStreaks();
+ matchStatus.value = battleInfo.status;
+ if (battleInfo.status === 4) {
+ showRoundTip.value = true;
+ currentBluePoint.value = 0;
+ currentRedPoint.value = 0;
+ }
- if (matchStatus.value === 2) {
- uni.redirectTo({
- url: `/pages/friend-battle-result?battleId=${battleId.value}`,
- });
- } else if (matchStatus.value === 4) {
- const roomNumber = battleInfo.roomId || store.game.roomNumber || store.game.roomID;
- setTimeout(() => {
- if (roomNumber) {
- uni.redirectTo({
- url: `/pages/battle-room?roomNumber=${encodeURIComponent(roomNumber)}&fromResult=1`,
- });
- } else {
- uni.navigateBack();
- }
- }, BATTLE_CANCEL_RETURN_DELAY);
+ // 终局语音必须完整播完,再决定跳转或返回。
+ await playAudioKeys("比赛结束", { interrupt: false, timeout: AUDIO_TIMEOUT_MAX });
+ notifyMatchAudioAck(task);
+ if (!isQueueAlive(runId)) return;
+
+ if (matchStatus.value === 2) {
+ navigateToBattleResultOnce(battleId.value);
+ } else if (matchStatus.value === 4) {
+ const roomNumber = battleInfo.roomId || store.game.roomNumber || store.game.roomID;
+ setTimeout(() => {
+ if (roomNumber) {
+ uni.redirectTo({
+ url: `/pages/battle-room?roomNumber=${encodeURIComponent(roomNumber)}&fromResult=1`,
+ });
+ } else {
+ uni.navigateBack();
+ }
+ }, BATTLE_CANCEL_RETURN_DELAY);
+ }
+ } finally {
+ battleEndTaskRunning = false;
}
}
@@ -1143,7 +1176,7 @@ async function runInvalidShotTask(task, runId) {
// 回前台恢复入口:拉取服务端快照,处理结束态,然后继续消费增量队列。
async function restoreLatestBattle() {
- if (!battleId.value) return;
+ if (!battleId.value || resultNavigationPending) return;
const currentRestoreId = ++restoreGeneration;
showRestoreLoading();
@@ -1175,9 +1208,8 @@ async function restoreLatestBattle() {
if (result.status === 2) {
clearXRingStreaks();
hideRestoreLoading();
- uni.redirectTo({
- url: `/pages/friend-battle-result?battleId=${result.matchId}`,
- });
+ battleEnded = true;
+ if (!battleEndTaskRunning) navigateToBattleResultOnce(result.matchId);
return;
}
if (result.status === 4) {
@@ -1284,6 +1316,8 @@ onLoad((options) => {
restoreGeneration += 1;
pendingRoundAudio = false;
battleEnded = false;
+ battleEndTaskRunning = false;
+ resultNavigationPending = false;
handledMessageKeys.clear();
handledMessageKeyOrder.length = 0;
queuedMessageKeys.clear();
@@ -1328,6 +1362,8 @@ onMounted(async () => {
// 离开页面时清理监听、停止音频,并让当前队列整体失效。
onBeforeUnmount(() => {
+ restoreGeneration += 1;
+ battleEndTaskRunning = false;
uni.setKeepScreenOn({
keepScreenOn: false,
});
From 621397e25d7dd9ea0cc9769cd31f8997233e34cc Mon Sep 17 00:00:00 2001
From: zhangyibo95 <690096405@qq.com>
Date: Thu, 16 Jul 2026 11:38:17 +0800
Subject: [PATCH 2/3] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E6=AF=94=E8=B5=9B?=
=?UTF-8?q?=E6=B5=81=E7=A8=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/pages/melee-battle.vue | 3 ++-
src/pages/team-battle/index.vue | 43 ++++++++++++++++++++++++---------
src/utils/match.min.js | 2 +-
src/utils/matchProtocol.js | 1 +
4 files changed, 35 insertions(+), 14 deletions(-)
diff --git a/src/pages/melee-battle.vue b/src/pages/melee-battle.vue
index efb56d8..8bf1cd3 100644
--- a/src/pages/melee-battle.vue
+++ b/src/pages/melee-battle.vue
@@ -319,7 +319,8 @@ function recoverData(battleInfo, { force = false } = {}) {
}
leaveHalfRest();
if (force) {
- const remain = (Date.now() - (battleInfo.current?.startTime || Date.now())) / 1000;
+ const ackTime = normalizeTimestamp(battleInfo.current?.ackTime);
+ const remain = ackTime ? Math.max(0, (Date.now() - ackTime) / 1000) : 0;
console.log(`当前轮已进行${remain}秒`);
if (remain > 0 && remain < 90) {
setTimeout(() => {
diff --git a/src/pages/team-battle/index.vue b/src/pages/team-battle/index.vue
index e699b21..481ba1f 100644
--- a/src/pages/team-battle/index.vue
+++ b/src/pages/team-battle/index.vue
@@ -397,6 +397,16 @@ function showRestoreLoading() {
}, RESTORE_LOADING_TIMEOUT);
}
+// 终局开始后作废恢复请求,避免恢复遮罩与结算跳转抢占画面。
+function cancelPendingBattleRestore() {
+ restoreGeneration += 1;
+ if (pendingRestoreTimer) {
+ clearTimeout(pendingRestoreTimer);
+ pendingRestoreTimer = null;
+ }
+ hideRestoreLoading();
+}
+
// 页面重置、离开或恢复快照时调用,统一清理队列、等待器、音频和进度条。
function invalidateBattleQueue({ stopAudio = false, stopProgress = false } = {}) {
// 提升队列代际并清空待执行消息,确保恢复快照时不会继续跑旧战况。
@@ -416,7 +426,10 @@ function invalidateBattleQueue({ stopAudio = false, stopProgress = false } = {})
function enqueueBattleMessage(message) {
if (Array.isArray(message) || !message?.type) return;
if (battleEnded && message.type !== MESSAGETYPESV2.BattleEnd) return;
- if (message.type === MESSAGETYPESV2.BattleEnd) battleEnded = true;
+ if (message.type === MESSAGETYPESV2.BattleEnd) {
+ battleEnded = true;
+ cancelPendingBattleRestore();
+ }
if (message.type === MESSAGETYPESV2.InvalidShot) {
const receivedAt = Date.now();
@@ -782,7 +795,7 @@ function getBackendRemainingSeconds(battleInfo, total) {
return null;
}
-// 计算当前倒计时剩余秒数:后端字段优先,其次按开始时间和当前时间补算。
+// 计算当前倒计时剩余秒数:后端字段优先,其次按 ACK 时间和当前时间补算。
function getRemainingSeconds(battleInfo, task, options = {}) {
const total = getShootTimeSeconds(battleInfo);
const backendRemain = getBackendRemainingSeconds(battleInfo, total);
@@ -790,8 +803,9 @@ function getRemainingSeconds(battleInfo, task, options = {}) {
// 新轮首箭由后端 shootTime 定完整时长;没有明确剩余时间时,不扣前端轮次弹窗/语音耗时。
if (options.fullDurationIfNoBackendRemain) return total;
- const startTime = normalizeTimestamp(battleInfo?.current?.startTime);
- if (!startTime) return total;
+ const ackTime = normalizeTimestamp(battleInfo?.current?.ackTime);
+ console.log(11111111111111111111111111,ackTime)
+ if (!ackTime) return total;
// 后端时间是准的,前端只根据语音耗时和接收延迟做近似补偿。
// 但回前台的 API 快照不一定带“当前后端时间”,恢复场景优先用本机当前时间计算最新剩余秒数。
@@ -800,7 +814,8 @@ function getRemainingSeconds(battleInfo, task, options = {}) {
: task?.serverTime || getServerTime(battleInfo) || Date.now();
const waitAfterReceive = task?.serverTime ? Date.now() - task.receivedAt : 0;
const effectiveNow = anchorTime + waitAfterReceive;
- const elapsed = Math.max(0, (effectiveNow - startTime) / 1000);
+ const elapsed = Math.max(0, (effectiveNow - ackTime) / 1000);
+ console.log('剩余:', elapsed, waitAfterReceive, Math.max(0, Math.min(total, total - elapsed)))
return Math.max(0, Math.min(total, total - elapsed));
}
@@ -1102,12 +1117,7 @@ function navigateToBattleResultOnce(matchId) {
resultNavigationPending = true;
battleEnded = true;
- restoreGeneration += 1;
- if (pendingRestoreTimer) {
- clearTimeout(pendingRestoreTimer);
- pendingRestoreTimer = null;
- }
- hideRestoreLoading();
+ cancelPendingBattleRestore();
uni.redirectTo({
url: `/pages/friend-battle-result?battleId=${targetMatchId}`,
@@ -1176,7 +1186,14 @@ async function runInvalidShotTask(task, runId) {
// 回前台恢复入口:拉取服务端快照,处理结束态,然后继续消费增量队列。
async function restoreLatestBattle() {
- if (!battleId.value || resultNavigationPending) return;
+ if (
+ !battleId.value ||
+ battleEnded ||
+ battleEndTaskRunning ||
+ resultNavigationPending
+ ) {
+ return;
+ }
const currentRestoreId = ++restoreGeneration;
showRestoreLoading();
@@ -1245,6 +1262,8 @@ async function restoreLatestBattle() {
}
function scheduleRestoreLatestBattle() {
+ if (battleEnded || battleEndTaskRunning || resultNavigationPending) return;
+
if (pendingRestoreTimer) {
clearTimeout(pendingRestoreTimer);
pendingRestoreTimer = null;
diff --git a/src/utils/match.min.js b/src/utils/match.min.js
index 0423291..afdd3e5 100644
--- a/src/utils/match.min.js
+++ b/src/utils/match.min.js
@@ -1,4 +1,4 @@
/* eslint-disable */
import * as $protobuf from "protobufjs";
-const $root=$protobuf.Root.create({nested:{rpc:{ServerMessageType:{SERVER_MSG_UNKNOWN:0,SERVER_MSG_MATCH_READY:1,SERVER_MSG_MATCH_START:2,SERVER_MSG_NOW_YOU:3,SERVER_MSG_SHOT:4,SERVER_MSG_NEW_ROUND:5,SERVER_MSG_MATCH_END:6,SERVER_MSG_TIMEOUT:7,SERVER_MSG_CHECK:8,SERVER_MSG_RAND:9,SERVER_MSG_NOT_ENOUGH_DISTANCE:10,SERVER_MSG_PLAYER_LEFT:11,SERVER_MSG_HEARTBEAT:12,SERVER_MSG_PRACTICE_END:13},ClientMessageType:{CLIENT_MSG_UNKNOWN:0,CLIENT_MSG_HEARTBEAT_ACK:1,CLIENT_MSG_SHOOT_DATA:2,CLIENT_MSG_ACK:3,CLIENT_MSG_LEAVE:4},MatchStatus:{MATCH_STATUS_READY:0,MATCH_STATUS_STARTED:1,MATCH_STATUS_END:2,MATCH_STATUS_TIMEOUT:3,MATCH_STATUS_UNEXPECTEDLY:4},MatchPlayerStatus:{MATCH_PLAYER_STATUS_READY:0,MATCH_PLAYER_STATUS_STARTED:1,MATCH_PLAYER_STATUS_END:2},MatchShoot:{fields:{player_id:{type:"int64",id:1},status:{type:"int32",id:2},x:{type:"float",id:3},y:{type:"float",id:4},ring:{type:"int32",id:5},ring_x:{type:"bool",id:6},angle:{type:"float",id:7},distance:{type:"float",id:8}}},MatchShootList:{fields:{items:{rule:"repeated",type:"MatchShoot",id:1}}},RoundScore:{fields:{total_ring:{type:"int32",id:1},score:{type:"int32",id:2},if_win:{type:"bool",id:3}}},MatchRound:{fields:{shoots:{rule:"map",type:"MatchShootList",id:1,keytype:"int64"},scores:{rule:"map",type:"RoundScore",id:2,keytype:"int32"},round:{type:"int32",id:3},if_gold:{type:"bool",id:4},status:{type:"int32",id:5},gold_round:{type:"int32",id:6}}},PlayerMatchResult:{fields:{total_ring:{type:"int32",id:1},user_id:{type:"int64",id:2},ten_ring_count:{type:"int32",id:3},average_ring:{type:"float",id:4}}},PlayerFull:{fields:{id:{type:"int64",id:1},name:{type:"string",id:2},avatar:{type:"string",id:3},exp:{type:"int32",id:4},score:{type:"int32",id:5},level:{type:"int32",id:6},before_level:{type:"int32",id:7},before_exp:{type:"int32",id:8},current_exp:{type:"int32",id:9},upgrade_exp:{type:"int32",id:10},active:{type:"int32",id:11},device_id:{type:"string",id:12},s_vip:{type:"bool",id:13},vip:{type:"bool",id:14},player_match_result:{type:"PlayerMatchResult",id:15}}},TeamInfo:{fields:{players:{rule:"repeated",type:"PlayerFull",id:1},id:{type:"int32",id:2},name:{type:"string",id:3},score:{type:"int32",id:4}}},CurrentShoot:{fields:{round:{type:"int32",id:1},round_id:{type:"int64",id:2},index:{type:"int32",id:3},start_time:{type:"int64",id:4},player_id:{type:"int64",id:5},gold_round:{type:"bool",id:6},start_time_text:{type:"string",id:7},my_index:{type:"int32",id:8},index_map:{rule:"map",type:"int32",id:9,keytype:"int64"}}},ShootData:{fields:{x:{type:"float",id:1},y:{type:"float",id:2},r:{type:"float",id:3},dst:{type:"float",id:4},m:{type:"string",id:5},adc:{type:"float",id:6},device_id:{type:"string",id:7},shoot_id:{type:"string",id:8}}},PracticeInfo:{fields:{id:{type:"int64",id:1},user_id:{type:"int64",id:2},status:{type:"int32",id:3},status_text:{type:"string",id:4},start_time:{type:"int64",id:5},target_type:{type:"int32",id:6},vip:{type:"bool",id:7},s_vip:{type:"bool",id:8},device_id:{type:"string",id:9},shoot_data:{type:"MatchShoot",id:10},details:{rule:"repeated",type:"MatchShoot",id:11}}},MatchInfo:{fields:{match_id:{type:"string",id:1},create_time:{type:"int64",id:2},start_time:{type:"int64",id:3},server_time:{type:"int64",id:4},shoot_time:{type:"int32",id:5},shoot_number:{type:"int32",id:6},ready_time:{type:"int32",id:7},way:{type:"int32",id:8},mode:{type:"int32",id:9},status:{type:"MatchStatus",id:10},status_text:{type:"string",id:11},rounds:{rule:"repeated",type:"MatchRound",id:12},teams:{rule:"map",type:"TeamInfo",id:13,keytype:"int32"},current:{type:"CurrentShoot",id:14},next:{type:"CurrentShoot",id:15},shoot_data:{type:"MatchShoot",id:16},win_team:{type:"int32",id:17},mvp:{type:"PlayerFull",id:18},room_id:{type:"string",id:19},result_list:{rule:"repeated",type:"PlayerMatchResult",id:20},timeout_time:{type:"int64",id:21},target_type:{type:"int32",id:22},event_type:{type:"int32",id:23},timeout:{type:"int32",id:24},server_addr:{type:"string",id:25},countdown_start_time:{type:"int64",id:26}}},ServerMessage:{fields:{type:{type:"ServerMessageType",id:1},match_id:{type:"string",id:2},timestamp:{type:"int64",id:3},match_info:{type:"MatchInfo",id:4,oneof:"payload"},shoot_data:{type:"ShootData",id:5,oneof:"payload"},practice_info:{type:"PracticeInfo",id:6,oneof:"payload"},sequence:{type:"int64",id:7}}},ClientMessage:{fields:{type:{type:"ClientMessageType",id:1},match_id:{type:"string",id:2},user_id:{type:"int64",id:3},sequence:{type:"int64",id:4},data:{type:"bytes",id:5}}}}}});
+const $root=$protobuf.Root.create({nested:{rpc:{ServerMessageType:{SERVER_MSG_UNKNOWN:0,SERVER_MSG_MATCH_READY:1,SERVER_MSG_MATCH_START:2,SERVER_MSG_NOW_YOU:3,SERVER_MSG_SHOT:4,SERVER_MSG_NEW_ROUND:5,SERVER_MSG_MATCH_END:6,SERVER_MSG_TIMEOUT:7,SERVER_MSG_CHECK:8,SERVER_MSG_RAND:9,SERVER_MSG_NOT_ENOUGH_DISTANCE:10,SERVER_MSG_PLAYER_LEFT:11,SERVER_MSG_HEARTBEAT:12,SERVER_MSG_PRACTICE_END:13},ClientMessageType:{CLIENT_MSG_UNKNOWN:0,CLIENT_MSG_HEARTBEAT_ACK:1,CLIENT_MSG_SHOOT_DATA:2,CLIENT_MSG_ACK:3,CLIENT_MSG_LEAVE:4},MatchStatus:{MATCH_STATUS_READY:0,MATCH_STATUS_STARTED:1,MATCH_STATUS_END:2,MATCH_STATUS_TIMEOUT:3,MATCH_STATUS_UNEXPECTEDLY:4},MatchPlayerStatus:{MATCH_PLAYER_STATUS_READY:0,MATCH_PLAYER_STATUS_STARTED:1,MATCH_PLAYER_STATUS_END:2},MatchShoot:{fields:{player_id:{type:"int64",id:1},status:{type:"int32",id:2},x:{type:"float",id:3},y:{type:"float",id:4},ring:{type:"int32",id:5},ring_x:{type:"bool",id:6},angle:{type:"float",id:7},distance:{type:"float",id:8}}},MatchShootList:{fields:{items:{rule:"repeated",type:"MatchShoot",id:1}}},RoundScore:{fields:{total_ring:{type:"int32",id:1},score:{type:"int32",id:2},if_win:{type:"bool",id:3}}},MatchRound:{fields:{shoots:{rule:"map",type:"MatchShootList",id:1,keytype:"int64"},scores:{rule:"map",type:"RoundScore",id:2,keytype:"int32"},round:{type:"int32",id:3},if_gold:{type:"bool",id:4},status:{type:"int32",id:5},gold_round:{type:"int32",id:6}}},PlayerMatchResult:{fields:{total_ring:{type:"int32",id:1},user_id:{type:"int64",id:2},ten_ring_count:{type:"int32",id:3},average_ring:{type:"float",id:4}}},PlayerFull:{fields:{id:{type:"int64",id:1},name:{type:"string",id:2},avatar:{type:"string",id:3},exp:{type:"int32",id:4},score:{type:"int32",id:5},level:{type:"int32",id:6},before_level:{type:"int32",id:7},before_exp:{type:"int32",id:8},current_exp:{type:"int32",id:9},upgrade_exp:{type:"int32",id:10},active:{type:"int32",id:11},device_id:{type:"string",id:12},s_vip:{type:"bool",id:13},vip:{type:"bool",id:14},player_match_result:{type:"PlayerMatchResult",id:15}}},TeamInfo:{fields:{players:{rule:"repeated",type:"PlayerFull",id:1},id:{type:"int32",id:2},name:{type:"string",id:3},score:{type:"int32",id:4}}},CurrentShoot:{fields:{round:{type:"int32",id:1},round_id:{type:"int64",id:2},index:{type:"int32",id:3},start_time:{type:"int64",id:4},player_id:{type:"int64",id:5},gold_round:{type:"bool",id:6},start_time_text:{type:"string",id:7},my_index:{type:"int32",id:8},index_map:{rule:"map",type:"int32",id:9,keytype:"int64"},ack_time:{type:"int64",id:10}}},ShootData:{fields:{x:{type:"float",id:1},y:{type:"float",id:2},r:{type:"float",id:3},dst:{type:"float",id:4},m:{type:"string",id:5},adc:{type:"float",id:6},device_id:{type:"string",id:7},shoot_id:{type:"string",id:8}}},PracticeInfo:{fields:{id:{type:"int64",id:1},user_id:{type:"int64",id:2},status:{type:"int32",id:3},status_text:{type:"string",id:4},start_time:{type:"int64",id:5},target_type:{type:"int32",id:6},vip:{type:"bool",id:7},s_vip:{type:"bool",id:8},device_id:{type:"string",id:9},shoot_data:{type:"MatchShoot",id:10},details:{rule:"repeated",type:"MatchShoot",id:11}}},MatchInfo:{fields:{match_id:{type:"string",id:1},create_time:{type:"int64",id:2},start_time:{type:"int64",id:3},server_time:{type:"int64",id:4},shoot_time:{type:"int32",id:5},shoot_number:{type:"int32",id:6},ready_time:{type:"int32",id:7},way:{type:"int32",id:8},mode:{type:"int32",id:9},status:{type:"MatchStatus",id:10},status_text:{type:"string",id:11},rounds:{rule:"repeated",type:"MatchRound",id:12},teams:{rule:"map",type:"TeamInfo",id:13,keytype:"int32"},current:{type:"CurrentShoot",id:14},next:{type:"CurrentShoot",id:15},shoot_data:{type:"MatchShoot",id:16},win_team:{type:"int32",id:17},mvp:{type:"PlayerFull",id:18},room_id:{type:"string",id:19},result_list:{rule:"repeated",type:"PlayerMatchResult",id:20},timeout_time:{type:"int64",id:21},target_type:{type:"int32",id:22},event_type:{type:"int32",id:23},timeout:{type:"int32",id:24},server_addr:{type:"string",id:25},countdown_start_time:{type:"int64",id:26}}},ServerMessage:{fields:{type:{type:"ServerMessageType",id:1},match_id:{type:"string",id:2},timestamp:{type:"int64",id:3},match_info:{type:"MatchInfo",id:4,oneof:"payload"},shoot_data:{type:"ShootData",id:5,oneof:"payload"},practice_info:{type:"PracticeInfo",id:6,oneof:"payload"},sequence:{type:"int64",id:7}}},ClientMessage:{fields:{type:{type:"ClientMessageType",id:1},match_id:{type:"string",id:2},user_id:{type:"int64",id:3},sequence:{type:"int64",id:4},data:{type:"bytes",id:5}}}}}});
export default $root;
diff --git a/src/utils/matchProtocol.js b/src/utils/matchProtocol.js
index f9df842..ac0cd47 100644
--- a/src/utils/matchProtocol.js
+++ b/src/utils/matchProtocol.js
@@ -123,6 +123,7 @@ const SCHEMAS = {
keyKind: "int64",
valueKind: "int32",
},
+ 10: { name: "ack_time", kind: "int64" },
},
ShootData: {
1: { name: "x", kind: "float" },
From f64a6b5c9587197013b62d54299f1155ea5ed1d9 Mon Sep 17 00:00:00 2001
From: zhangyibo95 <690096405@qq.com>
Date: Thu, 16 Jul 2026 16:40:32 +0800
Subject: [PATCH 3/3] =?UTF-8?q?update:=E6=96=B0=E5=A2=9E=E6=88=BF=E9=97=B4?=
=?UTF-8?q?=E9=87=8D=E8=BF=9E=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BC=98=E5=8C=96?=
=?UTF-8?q?tententen=E8=AF=AD=E9=9F=B3=EF=BC=8C=E4=BF=AE=E5=A4=8Dmvp?=
=?UTF-8?q?=E6=96=A9=E8=8E=B7=E7=8E=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/components/ShootProgress.vue | 4 +
src/matchWebsocket.js | 3 +
src/pages/battle-room.vue | 179 +++++++++++++++++++++++++++--
src/pages/friend-battle-result.vue | 9 +-
src/pages/match-page.vue | 176 ++++++++++++++++++++--------
src/pages/melee-battle.vue | 50 +-------
src/pages/practise-one.vue | 34 +-----
src/pages/practise-two.vue | 34 +-----
src/pages/team-battle.vue | 33 +-----
src/pages/team-battle/index.vue | 55 +--------
src/utils/match.min.js | 2 +-
src/utils/matchAdapter.js | 2 +-
src/utils/matchProtocol.js | 2 +
13 files changed, 328 insertions(+), 255 deletions(-)
diff --git a/src/components/ShootProgress.vue b/src/components/ShootProgress.vue
index f2448bd..0e0d407 100644
--- a/src/components/ShootProgress.vue
+++ b/src/components/ShootProgress.vue
@@ -157,6 +157,10 @@ async function onReceiveMessage(msg) {
key.push(arrow.ring ? `${arrow.ringX ? "X" : arrow.ring}环` : "未上靶");
if (arrow.angle)
key.push(`向${getDirectionText(arrow.angle)}调整`);
+ const shouldPlayTententen =
+ arrow.threeConsecutive10Rings === true ||
+ msg.shootData?.threeConsecutive10Rings === true;
+ if (!props.melee && shouldPlayTententen) key.push("tententen");
audioManager.play(key, false);
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
halfTime.value = true;
diff --git a/src/matchWebsocket.js b/src/matchWebsocket.js
index 9ce0746..cc0b803 100644
--- a/src/matchWebsocket.js
+++ b/src/matchWebsocket.js
@@ -310,6 +310,9 @@ function getShootResultAudioKeys(shootData) {
if (shootData.angle !== null && shootData.angle !== undefined) {
keys.push(`向${getDirectionText(shootData.angle)}调整`);
}
+ if (shootData.threeConsecutive10Rings === true) {
+ keys.push("tententen");
+ }
return keys;
}
diff --git a/src/pages/battle-room.vue b/src/pages/battle-room.vue
index 294823b..d711151 100644
--- a/src/pages/battle-room.vue
+++ b/src/pages/battle-room.vue
@@ -1,6 +1,6 @@
diff --git a/src/pages/melee-battle.vue b/src/pages/melee-battle.vue
index 8bf1cd3..2cc71cc 100644
--- a/src/pages/melee-battle.vue
+++ b/src/pages/melee-battle.vue
@@ -52,8 +52,6 @@ const readyTime = ref(DEFAULT_READY_TIME);
let halfRestTimer = null;
/** 控制设备离线提示弹窗的显示状态 */
const showOfflineModal = ref(false);
-/** 记录每位玩家当前半场连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
-const xRingStreaks = ref({});
let battleEnded = false;
let skipNextRestoreOnShow = false;
let restoreGeneration = 0;
@@ -399,32 +397,6 @@ onLoad(async (options) => {
// });
});
-/**
- * 检测指定玩家连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
- * @param {number|string} playerId - 本次射手的 ID(大乱斗中 ShootResult 保留 playerId)
- * @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
- */
-function isTenPlusRing(shot) {
- return !!(shot?.ringX || Number(shot?.ring) >= 10);
-}
-
-function checkAndPlayTententen(playerId, isTenPlusRingShot) {
- if (!playerId) return;
- const id = parseInt(playerId);
- if (isTenPlusRingShot) {
- xRingStreaks.value[id] = (xRingStreaks.value[id] || 0) + 1;
- // 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
- if (xRingStreaks.value[id] >= 3) {
- xRingStreaks.value[id] = 0;
- // nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
- nextTick(() => audioManager.play("tententen", false));
- }
- } else {
- // 低于 10 环或未上靶则重置该玩家的连续计数
- xRingStreaks.value[id] = 0;
- }
-}
-
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.AboutToStart) {
@@ -433,28 +405,10 @@ async function onReceiveMessage(msg) {
leaveHalfRest();
recoverData(msg);
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
- // 更新前快照各玩家本轮已射箭数,用于事后识别本次射手
- const curRound = playersScores.value[playersScores.value.length - 1] || {};
- const prevCounts = {};
- for (const pid of Object.keys(curRound)) {
- prevCounts[pid] = (curRound[pid] || []).length;
- }
recoverData(msg);
- // 对比更新后数据找出箭数增加的玩家(即本次射手),并读取其最新箭的 ring 数据
- const newRound = playersScores.value[playersScores.value.length - 1] || {};
- let shooterId = null;
- let isTenPlusRingShot = false;
- for (const pid of Object.keys(newRound)) {
- const newLen = (newRound[pid] || []).length;
- if (newLen > (prevCounts[pid] || 0)) {
- shooterId = parseInt(pid);
- const shot = newRound[pid][newLen - 1];
- isTenPlusRingShot = isTenPlusRing(shot);
- break;
- }
+ if (msg.shootData?.threeConsecutive10Rings === true) {
+ nextTick(() => audioManager.play("tententen", false));
}
- // 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
- checkAndPlayTententen(shooterId, isTenPlusRingShot);
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
halfTimeTip.value = true;
halfRest.value = true;
diff --git a/src/pages/practise-one.vue b/src/pages/practise-one.vue
index fcac288..28390ad 100644
--- a/src/pages/practise-one.vue
+++ b/src/pages/practise-one.vue
@@ -1,5 +1,5 @@