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" },