Merge branch 'feat-training'
This commit is contained in:
@@ -18,6 +18,10 @@ const TRAINING_START_AUDIO_KEY_MAP = Object.freeze({
|
|||||||
export const getTrainingStartAudioKey = (trainingType, fallback = "") =>
|
export const getTrainingStartAudioKey = (trainingType, fallback = "") =>
|
||||||
TRAINING_START_AUDIO_KEY_MAP[trainingType] || fallback;
|
TRAINING_START_AUDIO_KEY_MAP[trainingType] || fallback;
|
||||||
|
|
||||||
|
// 与服务端通关判定保持一致,proto3 省略的 false 同样按失败处理。
|
||||||
|
export const getTrainingEndAudioKey = (completed) =>
|
||||||
|
completed === true ? "通关成功" : "通关失败";
|
||||||
|
|
||||||
export const getPrecisionShotAudioKeys = (shootData, directionText = "") => {
|
export const getPrecisionShotAudioKeys = (shootData, directionText = "") => {
|
||||||
if (!shootData || typeof shootData !== "object") return [];
|
if (!shootData || typeof shootData !== "object") return [];
|
||||||
const ring = Number(shootData.ring);
|
const ring = Number(shootData.ring);
|
||||||
@@ -201,6 +205,10 @@ export const audioFils = {
|
|||||||
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E6%9C%AA%E5%91%BD%E4%B8%AD.MP3",
|
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E6%9C%AA%E5%91%BD%E4%B8%AD.MP3",
|
||||||
练习结束:
|
练习结束:
|
||||||
"https://static.shelingxingqiu.com/shootmini/static/audio/%E7%BB%83%E4%B9%A0%E7%BB%93%E6%9D%9F.mp3",
|
"https://static.shelingxingqiu.com/shootmini/static/audio/%E7%BB%83%E4%B9%A0%E7%BB%93%E6%9D%9F.mp3",
|
||||||
|
通关成功:
|
||||||
|
"https://static.shelingxingqiu.com/shootaudio/%E9%80%9A%E5%85%B3%E6%88%90%E5%8A%9F.mp3",
|
||||||
|
通关失败:
|
||||||
|
"https://static.shelingxingqiu.com/shootaudio/%E9%80%9A%E5%85%B3%E5%A4%B1%E8%B4%A5.mp3",
|
||||||
射箭声音:
|
射箭声音:
|
||||||
"https://static.shelingxingqiu.com/shootaudio/v4/v4/%E7%AE%AD%E9%A3%9E%E8%A1%8C.mp3",
|
"https://static.shelingxingqiu.com/shootaudio/v4/v4/%E7%AE%AD%E9%A3%9E%E8%A1%8C.mp3",
|
||||||
命中:
|
命中:
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
getPrecisionShotAudioKeys,
|
getPrecisionShotAudioKeys,
|
||||||
getRhythmShotAudioKeys,
|
getRhythmShotAudioKeys,
|
||||||
getStabilityShotAudioKeys,
|
getStabilityShotAudioKeys,
|
||||||
|
getTrainingEndAudioKey,
|
||||||
getTrainingStartAudioKey,
|
getTrainingStartAudioKey,
|
||||||
STABILITY_ENERGY_50_AUDIO_KEY,
|
STABILITY_ENERGY_50_AUDIO_KEY,
|
||||||
STABILITY_ENERGY_70_AUDIO_KEY,
|
STABILITY_ENERGY_70_AUDIO_KEY,
|
||||||
@@ -603,6 +604,10 @@ function getAckAudioKeys(message, businessMessage) {
|
|||||||
if (businessMessage?.type === MESSAGETYPESV2.HalfRest) return ["中场休息"];
|
if (businessMessage?.type === MESSAGETYPESV2.HalfRest) return ["中场休息"];
|
||||||
return ["比赛结束"];
|
return ["比赛结束"];
|
||||||
case ServerMessageType.SERVER_MSG_PRACTICE_END:
|
case ServerMessageType.SERVER_MSG_PRACTICE_END:
|
||||||
|
// 个人训练的 ACK 等待实际播放的通关语音,其他练习保留原有配置。
|
||||||
|
if (currentContext?.practiceEndAudioKey === "练习结束") {
|
||||||
|
return [getTrainingEndAudioKey(businessMessage?.completed)];
|
||||||
|
}
|
||||||
return [currentContext?.practiceEndAudioKey || "比赛结束"];
|
return [currentContext?.practiceEndAudioKey || "比赛结束"];
|
||||||
case ServerMessageType.SERVER_MSG_CHECK:
|
case ServerMessageType.SERVER_MSG_CHECK:
|
||||||
return getTestDistanceAudioKeys(businessMessage?.shootData);
|
return getTestDistanceAudioKeys(businessMessage?.shootData);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
nextTick,
|
nextTick,
|
||||||
} from "vue";
|
} from "vue";
|
||||||
import audioManager, {
|
import audioManager, {
|
||||||
|
getTrainingEndAudioKey,
|
||||||
getTrainingStartAudioKey,
|
getTrainingStartAudioKey,
|
||||||
RHYTHM_SHOOT_WINDOW_AUDIO_KEY,
|
RHYTHM_SHOOT_WINDOW_AUDIO_KEY,
|
||||||
} from "@/audioManager";
|
} from "@/audioManager";
|
||||||
@@ -111,6 +112,8 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["rhythm-round-complete"]);
|
||||||
|
|
||||||
const trainingTitleIconMap = Object.freeze({
|
const trainingTitleIconMap = Object.freeze({
|
||||||
base:
|
base:
|
||||||
"https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/text-icon-jcxl.png",
|
"https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/text-icon-jcxl.png",
|
||||||
@@ -143,6 +146,7 @@ let rhythmCountdownTimer = null;
|
|||||||
let rhythmTransitionTimer = null;
|
let rhythmTransitionTimer = null;
|
||||||
let rhythmServerClockOffsetMs = 0;
|
let rhythmServerClockOffsetMs = 0;
|
||||||
let rhythmSyncGeneration = 0;
|
let rhythmSyncGeneration = 0;
|
||||||
|
let rhythmRoundCompletionTimer = null;
|
||||||
|
|
||||||
const isRhythmTraining = computed(() => props.trainingType === "rhythm");
|
const isRhythmTraining = computed(() => props.trainingType === "rhythm");
|
||||||
const isStabilityTraining = computed(
|
const isStabilityTraining = computed(
|
||||||
@@ -306,6 +310,30 @@ const getRhythmRoundRemainingMs = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearRhythmRoundCompletionTimer = () => {
|
||||||
|
if (rhythmRoundCompletionTimer !== null) {
|
||||||
|
clearTimeout(rhythmRoundCompletionTimer);
|
||||||
|
rhythmRoundCompletionTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 与当前显示的倒计时一起计时;消息校准不重建此任务,避免同一轮重复计数。
|
||||||
|
const scheduleRhythmRoundCompletion = (remainingMs) => {
|
||||||
|
if (
|
||||||
|
rhythmRoundCompletionTimer !== null ||
|
||||||
|
!props.start ||
|
||||||
|
!isRhythmTraining.value ||
|
||||||
|
remainingMs <= 0
|
||||||
|
) return;
|
||||||
|
|
||||||
|
rhythmRoundCompletionTimer = setTimeout(() => {
|
||||||
|
rhythmRoundCompletionTimer = null;
|
||||||
|
if (!props.start || !isRhythmTraining.value) return;
|
||||||
|
emit("rhythm-round-complete");
|
||||||
|
scheduleRhythmRoundCompletion(rhythmRoundDurationMs.value);
|
||||||
|
}, remainingMs);
|
||||||
|
};
|
||||||
|
|
||||||
const updateRhythmShootWindow = (isInWindow) => {
|
const updateRhythmShootWindow = (isInWindow) => {
|
||||||
const nextIsInWindow = Boolean(isInWindow);
|
const nextIsInWindow = Boolean(isInWindow);
|
||||||
const enteredShootWindow =
|
const enteredShootWindow =
|
||||||
@@ -364,7 +392,8 @@ const syncRhythmCountdown = async () => {
|
|||||||
const generation = ++rhythmSyncGeneration;
|
const generation = ++rhythmSyncGeneration;
|
||||||
clearRhythmTimers();
|
clearRhythmTimers();
|
||||||
|
|
||||||
if (!isRhythmTraining.value) {
|
if (!isRhythmTraining.value || !props.start) {
|
||||||
|
clearRhythmRoundCompletionTimer();
|
||||||
rhythmTransitionStyle.value = "none";
|
rhythmTransitionStyle.value = "none";
|
||||||
rhythmRemainingMs.value = 0;
|
rhythmRemainingMs.value = 0;
|
||||||
rhythmRemainingSeconds.value = 0;
|
rhythmRemainingSeconds.value = 0;
|
||||||
@@ -382,6 +411,7 @@ const syncRhythmCountdown = async () => {
|
|||||||
!validRhythmShootTime.value ||
|
!validRhythmShootTime.value ||
|
||||||
!normalizeTimestamp(props.shootWindowStart)
|
!normalizeTimestamp(props.shootWindowStart)
|
||||||
) {
|
) {
|
||||||
|
clearRhythmRoundCompletionTimer();
|
||||||
rhythmTransitionStyle.value = "none";
|
rhythmTransitionStyle.value = "none";
|
||||||
rhythmRemainingMs.value = 0;
|
rhythmRemainingMs.value = 0;
|
||||||
rhythmRemainingSeconds.value = 0;
|
rhythmRemainingSeconds.value = 0;
|
||||||
@@ -391,6 +421,7 @@ const syncRhythmCountdown = async () => {
|
|||||||
|
|
||||||
rhythmTransitionStyle.value = "none";
|
rhythmTransitionStyle.value = "none";
|
||||||
rhythmRemainingMs.value = getRhythmRoundRemainingMs();
|
rhythmRemainingMs.value = getRhythmRoundRemainingMs();
|
||||||
|
scheduleRhythmRoundCompletion(rhythmRemainingMs.value);
|
||||||
rhythmRemainingSeconds.value = Math.ceil(rhythmRemainingMs.value / 1000);
|
rhythmRemainingSeconds.value = Math.ceil(rhythmRemainingMs.value / 1000);
|
||||||
updateRhythmShootWindow(
|
updateRhythmShootWindow(
|
||||||
rhythmRemainingMs.value > 0 &&
|
rhythmRemainingMs.value > 0 &&
|
||||||
@@ -462,6 +493,7 @@ watch(
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [
|
() => [
|
||||||
|
props.start,
|
||||||
props.trainingType,
|
props.trainingType,
|
||||||
props.roundTime,
|
props.roundTime,
|
||||||
props.shootTime,
|
props.shootTime,
|
||||||
@@ -493,7 +525,7 @@ async function onReceiveMessage(msg) {
|
|||||||
audioManager.play("比赛开始");
|
audioManager.play("比赛开始");
|
||||||
}
|
}
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
audioManager.play("练习结束", false);
|
audioManager.play(getTrainingEndAudioKey(msg.completed), false);
|
||||||
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||||
// 精准和节奏训练由页面统一处理专属结果语音,其他训练保持原播放链路。
|
// 精准和节奏训练由页面统一处理专属结果语音,其他训练保持原播放链路。
|
||||||
if (props.externalShootResultAudio) return;
|
if (props.externalShootResultAudio) return;
|
||||||
@@ -550,6 +582,7 @@ onBeforeUnmount(() => {
|
|||||||
clearTimer();
|
clearTimer();
|
||||||
rhythmSyncGeneration += 1;
|
rhythmSyncGeneration += 1;
|
||||||
clearRhythmTimers();
|
clearRhythmTimers();
|
||||||
|
clearRhythmRoundCompletionTimer();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -111,57 +111,50 @@ const createDifficultySummary = (item = {}) => {
|
|||||||
const hitReq = toNumber(item.hit_req);
|
const hitReq = toNumber(item.hit_req);
|
||||||
const totalReq = toNumber(item.total_req);
|
const totalReq = toNumber(item.total_req);
|
||||||
const rhythmRequiredArrows = toNumber(item.total_req, arrows) || arrows;
|
const rhythmRequiredArrows = toNumber(item.total_req, arrows) || arrows;
|
||||||
const timeLine = {
|
const timeParts = timeLimit > 0
|
||||||
parts: timeLimit > 0
|
? [{ text: `${timeLimit}`, highlight: true }, { text: "秒内," }]
|
||||||
? [{ text: "限时" }, { text: `${timeLimit}秒`, highlight: true }]
|
: [{ text: "不限时," }];
|
||||||
: [{ text: "不限时" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const summaryMap = {
|
const summaryMap = {
|
||||||
base: [
|
base: [
|
||||||
{ parts: [
|
{ parts: [
|
||||||
{ text: "需有" }, { text: `${arrows}箭`, highlight: true },
|
...timeParts,
|
||||||
{ text: "命中" }, { text: `≥${hitReq}环`, highlight: true },
|
{ text: "命中" }, { text: `${arrows}`, highlight: true },
|
||||||
|
{ text: "支" }, { text: `≥${hitReq}`, highlight: true },
|
||||||
|
{ text: "环的箭" },
|
||||||
] },
|
] },
|
||||||
timeLine,
|
|
||||||
],
|
],
|
||||||
endurance: [
|
endurance: [
|
||||||
{ parts: [
|
{ parts: [
|
||||||
{ text: "需射箭数" }, { text: `≥${arrows}箭`, highlight: true },
|
...timeParts,
|
||||||
{ text: ",累计环数" }, { text: `≥${totalReq}环`, highlight: true },
|
{ text: "共射出" }, { text: `≥${arrows}`, highlight: true },
|
||||||
|
{ text: "支箭,且总环数" }, { text: `≥${totalReq}`, highlight: true },
|
||||||
|
{ text: "环" },
|
||||||
] },
|
] },
|
||||||
timeLine,
|
|
||||||
],
|
],
|
||||||
precision: [
|
precision: [
|
||||||
{ parts: [
|
{ parts: [
|
||||||
{ text: "需有" }, { text: `${arrows}箭`, highlight: true },
|
...timeParts,
|
||||||
{ text: "命中高亮区域" },
|
{ text: "命中" }, { text: `${arrows}`, highlight: true },
|
||||||
|
{ text: "箭高亮区域" },
|
||||||
] },
|
] },
|
||||||
timeLine,
|
|
||||||
],
|
],
|
||||||
rhythm: [
|
rhythm: [
|
||||||
{ parts: [
|
{ parts: [
|
||||||
{ text: "跟随节拍撒放,共" }, { text: `${arrows}箭`, highlight: true },
|
{ text: `${arrows}`, highlight: true },
|
||||||
{ text: "需" }, { text: `${rhythmRequiredArrows}箭`, highlight: true },
|
{ text: "个节奏内,随节奏射箭,命中" },
|
||||||
] },
|
{ text: `${rhythmRequiredArrows}`, highlight: true },
|
||||||
{ parts: [
|
{ text: "箭成绩" }, { text: `≥${hitReq}`, highlight: true },
|
||||||
{ text: "命中" }, { text: `≥${hitReq}环`, highlight: true },
|
{ text: "环且踩中节拍区间" },
|
||||||
{ text: "且踏中节拍区间" },
|
|
||||||
] },
|
] },
|
||||||
],
|
],
|
||||||
stability: [
|
stability: [
|
||||||
{
|
{
|
||||||
parts: [
|
parts: [
|
||||||
{ text: "命中" },
|
...timeParts,
|
||||||
{ text: `≥${hitReq}环`, highlight: true },
|
{ text: "每射一箭" },
|
||||||
{ text: "可为能量槽充能" },
|
{ text: `≥${hitReq}`, highlight: true },
|
||||||
],
|
{ text: "环可为能量槽充能,能量充满即过关" },
|
||||||
},
|
|
||||||
{
|
|
||||||
parts: [
|
|
||||||
{ text: timeLimit > 0 ? "限时 " : "不限时" },
|
|
||||||
{ text: timeLimit > 0 ? `${timeLimit}` : "", highlight: true },
|
|
||||||
{ text: timeLimit > 0 ? " 秒,能量打满即过关" : ",能量打满即过关" },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ const rhythmServerTimestamp = ref(0);
|
|||||||
const rhythmFallbackWindowStart = ref(0);
|
const rhythmFallbackWindowStart = ref(0);
|
||||||
const rhythmFallbackTimestamp = ref(0);
|
const rhythmFallbackTimestamp = ref(0);
|
||||||
const rhythmHasActiveServerAnchor = ref(false);
|
const rhythmHasActiveServerAnchor = ref(false);
|
||||||
// 节奏训练目标箭数以服务端 total_arrows 为准;首帧缺失时用 arrows_left 初始化并保持稳定。
|
// 仅由前端每轮倒计时完成事件累加,不根据服务端箭数推算。
|
||||||
const rhythmTargetArrows = ref(0);
|
const rhythmCompletedRounds = ref(0);
|
||||||
// 服务端状态立即落到 practiceInfo,精准训练的目标区域单独延迟展示。
|
// 服务端状态立即落到 practiceInfo,精准训练的目标区域单独延迟展示。
|
||||||
const visiblePrecisionTarget = ref({
|
const visiblePrecisionTarget = ref({
|
||||||
randomBlock: 0,
|
randomBlock: 0,
|
||||||
@@ -440,10 +440,11 @@ const trainingCopy = computed(() => {
|
|||||||
return {
|
return {
|
||||||
inline: true,
|
inline: true,
|
||||||
details: [
|
details: [
|
||||||
{ text: "需有 " },
|
{ text: "命中 " },
|
||||||
{ text: `(${completedArrows}/${targetArrows})`, highlight: true },
|
{ text: `(${completedArrows}/${targetArrows})`, highlight: true },
|
||||||
{ text: " 箭命中" },
|
{ text: " 支" },
|
||||||
{ text: `≥${hitReq}环`, highlight: true },
|
{ text: `≥${hitReq}`, highlight: true },
|
||||||
|
{ text: "环的箭" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -463,10 +464,11 @@ const trainingCopy = computed(() => {
|
|||||||
return {
|
return {
|
||||||
inline: true,
|
inline: true,
|
||||||
details: [
|
details: [
|
||||||
{ text: "需射箭数" },
|
{ text: "射出≥ " },
|
||||||
{ text: `≥ (${currentArrows}/${targetArrows}) 支`, highlight: true },
|
{ text: `(${currentArrows}/${targetArrows})`, highlight: true },
|
||||||
{ text: ",累计环数\n" },
|
{ text: " 支箭,总环数达到≥ " },
|
||||||
{ text: `≥ (${currentRings}/${targetRings}) 环`, highlight: true },
|
{ text: `(${currentRings}/${targetRings})`, highlight: true },
|
||||||
|
{ text: " 环" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -485,38 +487,34 @@ const trainingCopy = computed(() => {
|
|||||||
return {
|
return {
|
||||||
inline: true,
|
inline: true,
|
||||||
details: [
|
details: [
|
||||||
{ text: "需有 " },
|
{ text: "命中高亮区域 " },
|
||||||
{ text: `(${completedArrows}/${targetArrows})`, highlight: true },
|
{ text: `(${completedArrows}/${targetArrows})`, highlight: true },
|
||||||
{ text: " 箭命中高亮区域" },
|
{ text: " 箭" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trainingType.value === "rhythm") {
|
if (trainingType.value === "rhythm") {
|
||||||
const totalArrows = getPositiveInteger(rhythmTargetArrows.value) || total.value;
|
// 总节奏数沿用关卡配置,与选择页保持一致,不使用消息中的箭数覆盖。
|
||||||
// 节奏训练进度由总箭数减剩余箭数计算,qualifiedArrows 仅用于稳定训练。
|
const totalArrows = getPositiveInteger(total.value);
|
||||||
|
// 节奏数由前端倒计时累计,达标箭数与下方金色箭记录使用同一判定。
|
||||||
const targetArrows =
|
const targetArrows =
|
||||||
getPositiveInteger(practiceInfo.value.targetArrows) ||
|
getPositiveInteger(practiceInfo.value.targetArrows) ||
|
||||||
getPositiveInteger(trainingParams.value.totalReq) || totalArrows;
|
getPositiveInteger(trainingParams.value.totalReq) || totalArrows;
|
||||||
const completedArrows = Math.min(
|
// 展示当前节奏序号,从 1 开始;内部仍累计已完成次数。
|
||||||
totalArrows,
|
const currentRound = Math.min(totalArrows, rhythmCompletedRounds.value + 1);
|
||||||
Math.max(
|
|
||||||
0,
|
|
||||||
totalArrows - getPracticeNumber(practiceInfo.value.arrowsLeft, totalArrows)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const hitReq = rhythmHitReq.value;
|
const hitReq = rhythmHitReq.value;
|
||||||
|
const qualifiedArrows = scores.value.filter((arrow) => arrow?.ok === true).length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
inline: true,
|
inline: true,
|
||||||
details: [
|
details: [
|
||||||
{ text: "跟随节拍撒放,共" },
|
{ text: `(${currentRound}/${totalArrows})`, highlight: true },
|
||||||
{ text: `${totalArrows}箭`, highlight: true },
|
{ text: " 个节奏内,跟随节奏射箭,命中 " },
|
||||||
{ text: "需 " },
|
{ text: `(${qualifiedArrows}/${targetArrows})`, highlight: true },
|
||||||
{ text: `(${completedArrows}/${targetArrows}) 箭`, highlight: true },
|
{ text: " 箭" },
|
||||||
{ text: "\n命中" },
|
{ text: `≥${hitReq}`, highlight: true },
|
||||||
{ text: `≥${hitReq}环`, highlight: true },
|
{ text: "环且踩中节拍区间" },
|
||||||
{ text: "且踏中节拍区间" },
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -529,9 +527,9 @@ const trainingCopy = computed(() => {
|
|||||||
return {
|
return {
|
||||||
inline: true,
|
inline: true,
|
||||||
details: [
|
details: [
|
||||||
{ text: "命中" },
|
{ text: "每射一箭" },
|
||||||
{ text: `≥${hitReq}环`, highlight: true },
|
{ text: `≥${hitReq}`, highlight: true },
|
||||||
{ text: "可为能量槽充能,能量打满即过关" },
|
{ text: "环可为能量槽充能,充满即过关" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -696,31 +694,14 @@ const cacheRhythmTrainingConfig = (message = {}) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheRhythmTargetArrows = (message = {}) => {
|
const onRhythmRoundComplete = () => {
|
||||||
const messageTrainingType = String(
|
if (!isShootingStage.value || !start.value || !isRhythmTraining.value || practiceEnded.value) return;
|
||||||
message.trainingType ?? message.training_type ?? trainingType.value
|
const totalRounds = getPositiveInteger(total.value);
|
||||||
).trim();
|
rhythmCompletedRounds.value = Math.min(totalRounds, rhythmCompletedRounds.value + 1);
|
||||||
if (messageTrainingType !== "rhythm") return;
|
|
||||||
|
|
||||||
const totalArrows = getPositiveInteger(
|
|
||||||
message.totalArrows ?? message.total_arrows
|
|
||||||
);
|
|
||||||
if (totalArrows > 0) {
|
|
||||||
rhythmTargetArrows.value = totalArrows;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兼容服务端首帧 total_arrows 暂为 0:只初始化一次,避免剩余数递减时分母跟着变化。
|
|
||||||
if (rhythmTargetArrows.value === 0) {
|
|
||||||
rhythmTargetArrows.value = getPositiveInteger(
|
|
||||||
message.arrowsLeft ?? message.arrows_left
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncPracticeInfo = (message = {}) => {
|
const syncPracticeInfo = (message = {}) => {
|
||||||
cacheRhythmTrainingConfig(message);
|
cacheRhythmTrainingConfig(message);
|
||||||
cacheRhythmTargetArrows(message);
|
|
||||||
const messageTrainingType = String(
|
const messageTrainingType = String(
|
||||||
message.trainingType ?? message.training_type ?? trainingType.value
|
message.trainingType ?? message.training_type ?? trainingType.value
|
||||||
).trim();
|
).trim();
|
||||||
@@ -741,11 +722,9 @@ const syncPracticeInfo = (message = {}) => {
|
|||||||
].includes(message.type));
|
].includes(message.type));
|
||||||
|
|
||||||
if (isRhythmSnapshot) {
|
if (isRhythmSnapshot) {
|
||||||
// 完整快照中 proto3 会省略 0;最后一箭及重连时不能保留旧剩余数。
|
// 达标箭数的 0 按 protobuf 默认值处理;剩余数缺失不能用于推算节奏进度。
|
||||||
for (const field of ["arrowsLeft", "qualifiedArrows"]) {
|
if (!Object.prototype.hasOwnProperty.call(nextInfo, "qualifiedArrows")) {
|
||||||
if (!Object.prototype.hasOwnProperty.call(nextInfo, field)) {
|
nextInfo.qualifiedArrows = 0;
|
||||||
nextInfo[field] = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1427,7 +1406,7 @@ onLoad((options = {}) => {
|
|||||||
serverAddr.value = trainingContext.serverAddr || "";
|
serverAddr.value = trainingContext.serverAddr || "";
|
||||||
|
|
||||||
const startAudioKey = getTrainingStartAudioKey(trainingParams.value.type);
|
const startAudioKey = getTrainingStartAudioKey(trainingParams.value.type);
|
||||||
const warmupKeys = [startAudioKey, "胜利", "失败"];
|
const warmupKeys = [startAudioKey, "通关成功", "通关失败", "胜利", "失败"];
|
||||||
if (trainingParams.value.type === "precision") {
|
if (trainingParams.value.type === "precision") {
|
||||||
warmupKeys.push("Bingo命中目标", "未命中");
|
warmupKeys.push("Bingo命中目标", "未命中");
|
||||||
} else if (trainingParams.value.type === "rhythm") {
|
} else if (trainingParams.value.type === "rhythm") {
|
||||||
@@ -1457,6 +1436,7 @@ const onReady = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
practiceStarting.value = true;
|
practiceStarting.value = true;
|
||||||
|
rhythmCompletedRounds.value = 0;
|
||||||
pageStage.value = pageStages.LOADING;
|
pageStage.value = pageStages.LOADING;
|
||||||
clearHighlightTestTimer();
|
clearHighlightTestTimer();
|
||||||
useHighlightTest.value = false;
|
useHighlightTest.value = false;
|
||||||
@@ -1698,7 +1678,7 @@ async function onRetry() {
|
|||||||
rhythmFallbackWindowStart.value = 0;
|
rhythmFallbackWindowStart.value = 0;
|
||||||
rhythmFallbackTimestamp.value = 0;
|
rhythmFallbackTimestamp.value = 0;
|
||||||
rhythmHasActiveServerAnchor.value = false;
|
rhythmHasActiveServerAnchor.value = false;
|
||||||
rhythmTargetArrows.value = 0;
|
rhythmCompletedRounds.value = 0;
|
||||||
invalidateShotPresentations({ resetVisible: true });
|
invalidateShotPresentations({ resetVisible: true });
|
||||||
start.value = false;
|
start.value = false;
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
@@ -1721,7 +1701,7 @@ function onAudioEnded(key) {
|
|||||||
Array.from(audioWaiters).forEach((waiter) => {
|
Array.from(audioWaiters).forEach((waiter) => {
|
||||||
if (waiter.expectedKey === key) waiter.done();
|
if (waiter.expectedKey === key) waiter.done();
|
||||||
});
|
});
|
||||||
if (["比赛结束", "练习结束"].includes(String(key || ""))) {
|
if (["比赛结束", "练习结束", "通关成功", "通关失败"].includes(String(key || ""))) {
|
||||||
void onOver();
|
void onOver();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1869,6 +1849,7 @@ onBeforeUnmount(() => {
|
|||||||
trainingType === 'stability'
|
trainingType === 'stability'
|
||||||
"
|
"
|
||||||
:onStop="onTimeLimitReached"
|
:onStop="onTimeLimitReached"
|
||||||
|
@rhythm-round-complete="onRhythmRoundComplete"
|
||||||
/>
|
/>
|
||||||
<view class="user-row">
|
<view class="user-row">
|
||||||
<!-- <Avatar :src="user.avatar" :size="35" /> -->
|
<!-- <Avatar :src="user.avatar" :size="35" /> -->
|
||||||
|
|||||||
Reference in New Issue
Block a user