update:优化比赛队列
This commit is contained in:
@@ -13,6 +13,7 @@ import TestDistance from "@/components/TestDistance.vue";
|
||||
import SModal from "@/components/SModal.vue";
|
||||
import audioManager from "@/audioManager";
|
||||
import { getBattleAPI, laserCloseAPI } from "@/apis";
|
||||
import { connectMatchWebSocket } from "@/matchWebsocket";
|
||||
import { MESSAGETYPESV2 } from "@/constants";
|
||||
import useStore from "@/store";
|
||||
import { storeToRefs } from "pinia";
|
||||
@@ -31,6 +32,14 @@ const playersScores = ref([]);
|
||||
const halfTimeTip = ref(false);
|
||||
const halfRest = ref(false);
|
||||
const HALF_REST_SECONDS = 20;
|
||||
const MATCH_READY_SNAPSHOT_PREFIX = "match-ready-snapshot:";
|
||||
const MATCH_STATUS_BY_TEXT = {
|
||||
MATCH_STATUS_READY: 0,
|
||||
MATCH_STATUS_STARTED: 1,
|
||||
MATCH_STATUS_END: 2,
|
||||
MATCH_STATUS_TIMEOUT: 3,
|
||||
MATCH_STATUS_UNEXPECTEDLY: 4,
|
||||
};
|
||||
const halfRestRemain = ref(HALF_REST_SECONDS);
|
||||
let halfRestTimer = null;
|
||||
/** 控制设备离线提示弹窗的显示状态 */
|
||||
@@ -93,6 +102,68 @@ const currentPlayer = computed(() =>
|
||||
);
|
||||
const isCurrentUserSvip = computed(() => currentPlayer.value?.sVip === true);
|
||||
|
||||
function getReadySnapshotKey(matchId) {
|
||||
return `${MATCH_READY_SNAPSHOT_PREFIX}${String(matchId || "")}`;
|
||||
}
|
||||
|
||||
function normalizeBattleInfo(battleInfo) {
|
||||
if (!battleInfo || typeof battleInfo !== "object") return battleInfo;
|
||||
|
||||
const statusText = battleInfo.statusText || battleInfo.status_text;
|
||||
if (
|
||||
(battleInfo.status === undefined ||
|
||||
battleInfo.status === null ||
|
||||
battleInfo.status === "") &&
|
||||
MATCH_STATUS_BY_TEXT[statusText] !== undefined
|
||||
) {
|
||||
return {
|
||||
...battleInfo,
|
||||
status: MATCH_STATUS_BY_TEXT[statusText],
|
||||
};
|
||||
}
|
||||
|
||||
return battleInfo;
|
||||
}
|
||||
|
||||
function hasKnownStatus(battleInfo) {
|
||||
return !(
|
||||
battleInfo?.status === undefined ||
|
||||
battleInfo?.status === null ||
|
||||
battleInfo?.status === ""
|
||||
);
|
||||
}
|
||||
|
||||
function hasStartedSnapshot(battleInfo) {
|
||||
if (hasKnownStatus(battleInfo)) return Number(battleInfo.status) !== 0;
|
||||
|
||||
const current = battleInfo?.current || {};
|
||||
return !!(
|
||||
current.playerId ||
|
||||
current.startTime ||
|
||||
current.round ||
|
||||
(Array.isArray(battleInfo?.rounds) && battleInfo.rounds.length)
|
||||
);
|
||||
}
|
||||
|
||||
function takeReadySnapshot(matchId) {
|
||||
const key = getReadySnapshotKey(matchId);
|
||||
const snapshot = uni.getStorageSync(key);
|
||||
if (snapshot) uni.removeStorageSync(key);
|
||||
return normalizeBattleInfo(snapshot);
|
||||
}
|
||||
|
||||
function reconnectMatchServer(battleInfo) {
|
||||
const status = Number(battleInfo?.status);
|
||||
if ([2, 3, 4].includes(status)) return;
|
||||
if (!battleInfo?.serverAddr) return;
|
||||
|
||||
connectMatchWebSocket({
|
||||
serverAddr: battleInfo.serverAddr,
|
||||
matchId: battleInfo.matchId || battleId.value,
|
||||
userId: user.value.id,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听设备在线状态,大乱斗比赛进行中设备离线时弹窗提示用户
|
||||
*/
|
||||
@@ -103,6 +174,7 @@ watch(online, (newVal, oldVal) => {
|
||||
});
|
||||
|
||||
function recoverData(battleInfo, { force = false } = {}) {
|
||||
battleInfo = normalizeBattleInfo(battleInfo);
|
||||
if (!battleInfo) return;
|
||||
try {
|
||||
if (battleInfo.way === 1) title.value = "好友约战 - 大乱斗";
|
||||
@@ -121,7 +193,14 @@ function recoverData(battleInfo, { force = false } = {}) {
|
||||
players.value = [];
|
||||
}
|
||||
|
||||
start.value = battleInfo.status !== 0;
|
||||
if (hasKnownStatus(battleInfo)) {
|
||||
start.value = battleInfo.status !== 0;
|
||||
} else if (!hasStartedSnapshot(battleInfo)) {
|
||||
if (start.value !== true) start.value = false;
|
||||
return;
|
||||
} else {
|
||||
start.value = true;
|
||||
}
|
||||
|
||||
if (battleInfo.status === 0) {
|
||||
const readyRemain = (Date.now() - (battleInfo.createTime || Date.now())) / 1000;
|
||||
@@ -131,9 +210,10 @@ function recoverData(battleInfo, { force = false } = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rounds = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : [];
|
||||
tips.value =
|
||||
(battleInfo.rounds.length !== 2 ? "上" : "下") + "半场:请先射6箭";
|
||||
playersScores.value = battleInfo.rounds.map((r) => ({ ...r.shoots }));
|
||||
(rounds.length !== 2 ? "上" : "下") + "半场:请先射6箭";
|
||||
playersScores.value = rounds.map((r) => ({ ...r.shoots }));
|
||||
const totals = {};
|
||||
players.value.forEach((p) => {
|
||||
const total = playersScores.value.reduce((acc, round) => {
|
||||
@@ -171,6 +251,8 @@ function recoverData(battleInfo, { force = false } = {}) {
|
||||
|
||||
onLoad(async (options) => {
|
||||
if (options.battleId) battleId.value = options.battleId;
|
||||
const readySnapshot = takeReadySnapshot(battleId.value);
|
||||
if (readySnapshot?.status === 0) recoverData(readySnapshot);
|
||||
// uni.enableAlertBeforeUnload({
|
||||
// message: "离开比赛可能导致比赛失败,是否继续?",
|
||||
// success: (res) => {
|
||||
@@ -207,7 +289,9 @@ function checkAndPlayTententen(playerId, isTenPlusRingShot) {
|
||||
|
||||
async function onReceiveMessage(msg) {
|
||||
if (Array.isArray(msg)) return;
|
||||
if (msg.type === MESSAGETYPESV2.BattleStart) {
|
||||
if (msg.type === MESSAGETYPESV2.AboutToStart) {
|
||||
recoverData(msg);
|
||||
} else if (msg.type === MESSAGETYPESV2.BattleStart) {
|
||||
clearHalfRestCountdown();
|
||||
halfTimeTip.value = false;
|
||||
halfRest.value = false;
|
||||
@@ -267,7 +351,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
onShow(async () => {
|
||||
if (battleId.value) {
|
||||
const result = await getBattleAPI(battleId.value);
|
||||
const result = normalizeBattleInfo(await getBattleAPI(battleId.value));
|
||||
if (!result) return;
|
||||
if (result.status === 2) {
|
||||
uni.showToast({
|
||||
@@ -278,6 +362,7 @@ onShow(async () => {
|
||||
delta: 2,
|
||||
});
|
||||
} else {
|
||||
reconnectMatchServer(result);
|
||||
recoverData(result, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import TeamAvatars from "./components/TeamAvatars.vue";
|
||||
import ShootProgress2 from "./components/ShootProgress2.vue";
|
||||
import SModal from "./components/SModal.vue";
|
||||
import { laserCloseAPI, getBattleAPI } from "@/apis";
|
||||
import { connectMatchWebSocket, MATCH_WS_AUDIO_ACK_EVENT } from "@/matchWebsocket";
|
||||
import { MESSAGETYPESV2 } from "@/constants";
|
||||
import { getDirectionText } from "@/util";
|
||||
import audioManager, {
|
||||
@@ -42,6 +43,14 @@ const AUDIO_TIMEOUT_MAX = 12000;
|
||||
const BATTLE_CANCEL_RETURN_DELAY = 2000;
|
||||
const ROUND_AUDIO_NAMES = ["一", "二", "三", "四", "五"];
|
||||
const X_RING_STREAKS_KEY = "team-battle-x-ring-streaks";
|
||||
const MATCH_READY_SNAPSHOT_PREFIX = "match-ready-snapshot:";
|
||||
const MATCH_STATUS_BY_TEXT = {
|
||||
MATCH_STATUS_READY: 0,
|
||||
MATCH_STATUS_STARTED: 1,
|
||||
MATCH_STATUS_END: 2,
|
||||
MATCH_STATUS_TIMEOUT: 3,
|
||||
MATCH_STATUS_UNEXPECTEDLY: 4,
|
||||
};
|
||||
const PROGRESS_ZERO_EVENT = "team-battle-progress-zero";
|
||||
const COUNTDOWN_READY_EVENT = "team-battle-countdown-ready";
|
||||
|
||||
@@ -129,6 +138,68 @@ function normalizeTimestamp(value) {
|
||||
return numberValue < 1000000000000 ? numberValue * 1000 : numberValue;
|
||||
}
|
||||
|
||||
function getReadySnapshotKey(matchId) {
|
||||
return `${MATCH_READY_SNAPSHOT_PREFIX}${String(matchId || "")}`;
|
||||
}
|
||||
|
||||
function normalizeBattleInfo(battleInfo) {
|
||||
if (!battleInfo || typeof battleInfo !== "object") return battleInfo;
|
||||
|
||||
const statusText = battleInfo.statusText || battleInfo.status_text;
|
||||
if (
|
||||
(battleInfo.status === undefined ||
|
||||
battleInfo.status === null ||
|
||||
battleInfo.status === "") &&
|
||||
MATCH_STATUS_BY_TEXT[statusText] !== undefined
|
||||
) {
|
||||
return {
|
||||
...battleInfo,
|
||||
status: MATCH_STATUS_BY_TEXT[statusText],
|
||||
};
|
||||
}
|
||||
|
||||
return battleInfo;
|
||||
}
|
||||
|
||||
function hasKnownStatus(battleInfo) {
|
||||
return !(
|
||||
battleInfo?.status === undefined ||
|
||||
battleInfo?.status === null ||
|
||||
battleInfo?.status === ""
|
||||
);
|
||||
}
|
||||
|
||||
function hasStartedSnapshot(battleInfo) {
|
||||
if (hasKnownStatus(battleInfo)) return Number(battleInfo.status) !== 0;
|
||||
|
||||
const current = battleInfo?.current || {};
|
||||
return !!(
|
||||
current.playerId ||
|
||||
current.startTime ||
|
||||
current.round ||
|
||||
(Array.isArray(battleInfo?.rounds) && battleInfo.rounds.length)
|
||||
);
|
||||
}
|
||||
|
||||
function takeReadySnapshot(matchId) {
|
||||
const key = getReadySnapshotKey(matchId);
|
||||
const snapshot = uni.getStorageSync(key);
|
||||
if (snapshot) uni.removeStorageSync(key);
|
||||
return normalizeBattleInfo(snapshot);
|
||||
}
|
||||
|
||||
function reconnectMatchServer(battleInfo) {
|
||||
const status = Number(battleInfo?.status);
|
||||
if ([2, 3, 4].includes(status)) return;
|
||||
if (!battleInfo?.serverAddr) return;
|
||||
|
||||
connectMatchWebSocket({
|
||||
serverAddr: battleInfo.serverAddr,
|
||||
matchId: battleInfo.matchId || battleId.value,
|
||||
userId: user.value.id,
|
||||
});
|
||||
}
|
||||
|
||||
// 从不同消息结构中提取服务端时间,作为恢复和去重的时间基准。
|
||||
function getServerTime(message) {
|
||||
return normalizeTimestamp(
|
||||
@@ -289,6 +360,10 @@ function hideRestoreLoading() {
|
||||
}
|
||||
|
||||
function showRestoreLoading() {
|
||||
if (start.value === false) {
|
||||
hideRestoreLoading();
|
||||
return;
|
||||
}
|
||||
clearRestoreLoadingTimer();
|
||||
restoreLoading.value = true;
|
||||
restoreLoadingTimer = setTimeout(() => {
|
||||
@@ -391,7 +466,9 @@ async function executeBattleTask(task, runId) {
|
||||
if (!task || !isQueueAlive(runId)) return;
|
||||
const type = task.type;
|
||||
|
||||
if (type === MESSAGETYPESV2.BattleStart) {
|
||||
if (type === MESSAGETYPESV2.AboutToStart) {
|
||||
applyBattleSnapshot(task.message, { restore: true });
|
||||
} else if (type === MESSAGETYPESV2.BattleStart) {
|
||||
await runBattleStartTask(task, runId);
|
||||
} else if (type === MESSAGETYPESV2.ToSomeoneShoot) {
|
||||
await runToSomeoneShootTask(task, runId);
|
||||
@@ -402,7 +479,7 @@ async function executeBattleTask(task, runId) {
|
||||
} else if (type === MESSAGETYPESV2.BattleEnd) {
|
||||
await runBattleEndTask(task, runId);
|
||||
} else if (type === MESSAGETYPESV2.InvalidShot) {
|
||||
await runInvalidShotTask(runId);
|
||||
await runInvalidShotTask(task, runId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,6 +525,24 @@ function onAudioEnded(key) {
|
||||
});
|
||||
}
|
||||
|
||||
function notifyMatchAudioAck(task) {
|
||||
const message = task?.message;
|
||||
if (
|
||||
message?.sequence === undefined ||
|
||||
message?.sequence === null ||
|
||||
message?.sequence === ""
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
uni.$emit(MATCH_WS_AUDIO_ACK_EVENT, {
|
||||
matchId: message.matchId || battleId.value,
|
||||
sequence: message.sequence,
|
||||
matchWsType: message.matchWsType,
|
||||
matchWsTypeName: message.matchWsTypeName,
|
||||
});
|
||||
}
|
||||
|
||||
function handleBattleCovered() {
|
||||
if (pendingRestoreTimer) {
|
||||
clearTimeout(pendingRestoreTimer);
|
||||
@@ -526,6 +621,7 @@ function applyRestoreNewRoundSnapshot(battleInfo) {
|
||||
|
||||
// 回填比赛基础信息:队伍、比分、轮次、金箭状态等公共字段都在这里统一处理。
|
||||
function applyBattleBase(battleInfo) {
|
||||
battleInfo = normalizeBattleInfo(battleInfo);
|
||||
if (!battleInfo) return;
|
||||
if (battleInfo.matchId) battleId.value = battleInfo.matchId;
|
||||
if (battleInfo.status !== undefined) {
|
||||
@@ -745,6 +841,8 @@ function applyReadyState(battleInfo) {
|
||||
|
||||
// 快照恢复入口:只把页面拉到服务端最新状态,不重放已经发生过的语音。
|
||||
function applyBattleSnapshot(battleInfo, { restore = false, restoreEventType = 0 } = {}) {
|
||||
battleInfo = normalizeBattleInfo(battleInfo);
|
||||
if (!battleInfo) return;
|
||||
// 快照恢复只负责“把页面拉回最新状态”,不重放历史语音。
|
||||
applyBattleBase(battleInfo);
|
||||
if (battleInfo.status === 0) {
|
||||
@@ -752,6 +850,12 @@ function applyBattleSnapshot(battleInfo, { restore = false, restoreEventType = 0
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasStartedSnapshot(battleInfo)) {
|
||||
hideRestoreLoading();
|
||||
if (start.value === false) applyReadyState(battleInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
start.value = true;
|
||||
showRoundTip.value = false;
|
||||
|
||||
@@ -793,6 +897,7 @@ async function runBattleStartTask(task, runId) {
|
||||
pendingRoundAudio = true;
|
||||
updateShotInfo(task.message);
|
||||
await playAudioKeys("比赛开始", { interrupt: false });
|
||||
notifyMatchAudioAck(task);
|
||||
if (!isQueueAlive(runId)) return;
|
||||
}
|
||||
|
||||
@@ -846,6 +951,7 @@ async function runToSomeoneShootTask(task, runId) {
|
||||
});
|
||||
|
||||
await audioPromise;
|
||||
notifyMatchAudioAck(task);
|
||||
if (!isQueueAlive(runId)) return;
|
||||
|
||||
const remainingSeconds = getRemainingSeconds(battleInfo, task, {
|
||||
@@ -918,6 +1024,7 @@ async function runShootResultTask(task) {
|
||||
const audioKeys = buildShootResultAudioKeys(battleInfo.shootData);
|
||||
if (isTententen) audioKeys.push("tententen");
|
||||
await playAudioKeys(audioKeys, { interrupt: false });
|
||||
notifyMatchAudioAck(task);
|
||||
}
|
||||
|
||||
// 新回合任务:展示上一回合结算弹窗,弹窗关闭后再允许下一轮继续。
|
||||
@@ -977,6 +1084,7 @@ async function runBattleEndTask(task, runId) {
|
||||
|
||||
// 终局语音必须完整播完,再决定跳转或返回。
|
||||
await playAudioKeys("比赛结束", { interrupt: false, timeout: AUDIO_TIMEOUT_MAX });
|
||||
notifyMatchAudioAck(task);
|
||||
if (!isQueueAlive(runId)) return;
|
||||
|
||||
if (matchStatus.value === 2) {
|
||||
@@ -991,13 +1099,14 @@ async function runBattleEndTask(task, runId) {
|
||||
}
|
||||
|
||||
// 无效射击任务:弹 toast 并播报无效射击,不修改比赛主状态。
|
||||
async function runInvalidShotTask(runId) {
|
||||
async function runInvalidShotTask(task, runId) {
|
||||
if (!isQueueAlive(runId)) return;
|
||||
uni.showToast({
|
||||
title: "距离不足,无效",
|
||||
icon: "none",
|
||||
});
|
||||
await playAudioKeys("射击无效", { interrupt: false });
|
||||
notifyMatchAudioAck(task);
|
||||
}
|
||||
|
||||
// 回前台恢复入口:拉取服务端快照,处理结束态,然后继续消费增量队列。
|
||||
@@ -1016,6 +1125,7 @@ async function restoreLatestBattle() {
|
||||
console.log("restore latest battle failed:", err);
|
||||
}
|
||||
if (currentRestoreId !== restoreGeneration) return;
|
||||
result = normalizeBattleInfo(result);
|
||||
if (!result) {
|
||||
hideRestoreLoading();
|
||||
runBattleQueue();
|
||||
@@ -1041,6 +1151,7 @@ async function restoreLatestBattle() {
|
||||
if (result.status === 4) {
|
||||
clearXRingStreaks();
|
||||
}
|
||||
reconnectMatchServer(result);
|
||||
|
||||
if (restoreEventType === MESSAGETYPESV2.NewRound) {
|
||||
const prevRound = getRestorePrevRound(result);
|
||||
@@ -1135,6 +1246,10 @@ onLoad((options) => {
|
||||
store.updateShotInfo(0, 0);
|
||||
store.updateTips("");
|
||||
latestShotFlash.value = null;
|
||||
const readySnapshot = takeReadySnapshot(battleId.value);
|
||||
if (readySnapshot?.status === 0) {
|
||||
applyBattleSnapshot(readySnapshot, { restore: true });
|
||||
}
|
||||
scheduleRestoreLatestBattle();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user