Compare commits
13
Commits
main
..
f16b486a89
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f16b486a89 | ||
|
|
6c69e14630 | ||
|
|
34118d59d3 | ||
|
|
b82474913a | ||
|
|
9d9f005ca3 | ||
|
|
605a0b2d91 | ||
|
|
c9bfa6792b | ||
|
|
b0ddf147aa | ||
|
|
3caa53d4be | ||
|
|
f4f63954eb | ||
|
|
b89744802a | ||
|
|
fd6be25ba5 | ||
|
|
bad3c8ff9a |
+4
-8
@@ -4,6 +4,10 @@
|
||||
"libVersion": "3.7.7",
|
||||
"packOptions": {
|
||||
"ignore": [
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "static/coin"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"value": "static/common/dialog-light.png"
|
||||
@@ -243,14 +247,6 @@
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "static/training-difficulty-design"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "static/coin"
|
||||
},
|
||||
{
|
||||
"type": "folder",
|
||||
"value": "static/home-device"
|
||||
}
|
||||
],
|
||||
"include": []
|
||||
|
||||
@@ -18,10 +18,6 @@ const TRAINING_START_AUDIO_KEY_MAP = Object.freeze({
|
||||
export const getTrainingStartAudioKey = (trainingType, fallback = "") =>
|
||||
TRAINING_START_AUDIO_KEY_MAP[trainingType] || fallback;
|
||||
|
||||
// 与服务端通关判定保持一致,proto3 省略的 false 同样按失败处理。
|
||||
export const getTrainingEndAudioKey = (completed) =>
|
||||
completed === true ? "通关成功" : "通关失败";
|
||||
|
||||
export const getPrecisionShotAudioKeys = (shootData, directionText = "") => {
|
||||
if (!shootData || typeof shootData !== "object") return [];
|
||||
const ring = Number(shootData.ring);
|
||||
@@ -205,10 +201,6 @@ 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/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",
|
||||
命中:
|
||||
|
||||
@@ -164,8 +164,6 @@ const defaultHighlightStyle = {
|
||||
color: "rgba(255, 228, 0, 0.6)",
|
||||
strokeColor: "rgba(254, 216, 71, 0.82)",
|
||||
lineWidthRatio: 0.003,
|
||||
// 在最小线宽计算后应用倍数,确保不同靶面尺寸下按比例加粗。
|
||||
lineWidthScale: 1,
|
||||
};
|
||||
|
||||
// 合并默认靶纸样式和外部传入样式,ringColors 单独深合并。
|
||||
@@ -321,7 +319,7 @@ const drawSectorHighlight = (
|
||||
(angles.endAngle - angles.startAngle) * safeRevealProgress,
|
||||
style.color,
|
||||
style.strokeColor,
|
||||
Math.max(1, targetRadius * style.lineWidthRatio) * style.lineWidthScale
|
||||
Math.max(1, targetRadius * style.lineWidthRatio)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
+4
-8
@@ -55,6 +55,10 @@
|
||||
"appid" : "wxa8f5989dcd45cc23",
|
||||
"packOptions" : {
|
||||
"ignore" : [
|
||||
{
|
||||
"type" : "folder",
|
||||
"value" : "static/coin"
|
||||
},
|
||||
{
|
||||
"type" : "file",
|
||||
"value" : "static/common/dialog-light.png"
|
||||
@@ -294,14 +298,6 @@
|
||||
{
|
||||
"type" : "folder",
|
||||
"value" : "static/training-difficulty-design"
|
||||
},
|
||||
{
|
||||
"type" : "folder",
|
||||
"value" : "static/coin"
|
||||
},
|
||||
{
|
||||
"type" : "folder",
|
||||
"value" : "static/home-device"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
getPrecisionShotAudioKeys,
|
||||
getRhythmShotAudioKeys,
|
||||
getStabilityShotAudioKeys,
|
||||
getTrainingEndAudioKey,
|
||||
getTrainingStartAudioKey,
|
||||
STABILITY_ENERGY_50_AUDIO_KEY,
|
||||
STABILITY_ENERGY_70_AUDIO_KEY,
|
||||
@@ -604,10 +603,6 @@ function getAckAudioKeys(message, businessMessage) {
|
||||
if (businessMessage?.type === MESSAGETYPESV2.HalfRest) return ["中场休息"];
|
||||
return ["比赛结束"];
|
||||
case ServerMessageType.SERVER_MSG_PRACTICE_END:
|
||||
// 个人训练的 ACK 等待实际播放的通关语音,其他练习保留原有配置。
|
||||
if (currentContext?.practiceEndAudioKey === "练习结束") {
|
||||
return [getTrainingEndAudioKey(businessMessage?.completed)];
|
||||
}
|
||||
return [currentContext?.practiceEndAudioKey || "比赛结束"];
|
||||
case ServerMessageType.SERVER_MSG_CHECK:
|
||||
return getTestDistanceAudioKeys(businessMessage?.shootData);
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
{
|
||||
"path": "pages/audio-test"
|
||||
},
|
||||
{
|
||||
"path": "pages/calibration"
|
||||
},
|
||||
{
|
||||
"path": "pages/about-us"
|
||||
},
|
||||
@@ -60,12 +57,6 @@
|
||||
{
|
||||
"path": "pages/match-page"
|
||||
},
|
||||
{
|
||||
"path": "pages/my-device"
|
||||
},
|
||||
{
|
||||
"path": "pages/device-intro"
|
||||
},
|
||||
{
|
||||
"path": "pages/user"
|
||||
},
|
||||
@@ -107,12 +98,6 @@
|
||||
},
|
||||
{
|
||||
"path": "pages/mine-bow-data"
|
||||
},
|
||||
{
|
||||
"path": "pages/ota-wifi",
|
||||
"style": {
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
||||
@@ -447,12 +447,12 @@ onShow(async () => {
|
||||
<text class="highlight-text">射灵智能弓箭</text>
|
||||
<text>设备上的二维码</text>
|
||||
</view>
|
||||
<!-- <view class="benefit-copy">
|
||||
<view class="benefit-copy">
|
||||
<text>新设备首次绑定账号可获赠</text>
|
||||
<text class="highlight-text">6个月射灵会员礼包</text>
|
||||
<text>,</text>
|
||||
<text>该礼包仅可使用一次,请确保当前登录账号为您本人账号。</text>
|
||||
</view> -->
|
||||
</view>
|
||||
<button class="help-link" hover-class="none" @click="showTip = true">
|
||||
<text>没找到二维码或遇到问题></text>
|
||||
</button>
|
||||
@@ -866,7 +866,7 @@ onShow(async () => {
|
||||
}
|
||||
|
||||
.help-link {
|
||||
margin-top: 36rpx;
|
||||
margin-top: 72rpx;
|
||||
color: #ffd947;
|
||||
font-size: 26rpx;
|
||||
line-height: 36rpx;
|
||||
@@ -874,8 +874,8 @@ onShow(async () => {
|
||||
|
||||
.product-link {
|
||||
position: fixed;
|
||||
bottom: 208rpx;
|
||||
/* bottom: calc(env(safe-area-inset-bottom) + 104rpx); */
|
||||
bottom: 104rpx;
|
||||
bottom: calc(env(safe-area-inset-bottom) + 104rpx);
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
|
||||
@@ -39,7 +39,6 @@ const connectMode = ref("secure"); // secure | open | manual
|
||||
const connectError = ref("");
|
||||
const connectStatusText = ref("");
|
||||
const isSubmittingWifi = ref(false);
|
||||
const wifiSuccessVisible = ref(false);
|
||||
const keyboardHeight = ref(0);
|
||||
// 控制密码输入框是否显示明文
|
||||
const showPassword = ref(false);
|
||||
@@ -365,13 +364,10 @@ const submitDeviceWifiConfig = async ({ ssid, password }) => {
|
||||
secure: connectMode.value === "secure" || !!password,
|
||||
};
|
||||
connectError.value = "";
|
||||
currentState.value = STATES.CONNECTED;
|
||||
void refreshConnectedWifiName();
|
||||
if (fromFirmwareUpdate.value) {
|
||||
currentState.value = STATES.CONNECTED;
|
||||
openFirmwareCountdown();
|
||||
} else {
|
||||
// 保留配网输入界面,避免成功提示出现前先露出 WiFi 列表。
|
||||
wifiSuccessVisible.value = true;
|
||||
}
|
||||
} catch (err) {
|
||||
if (requestId === wifiConnectRequestId) {
|
||||
@@ -386,13 +382,6 @@ const submitDeviceWifiConfig = async ({ ssid, password }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 普通配网成功后,确认并收起输入界面,停留在当前 WiFi 页面。
|
||||
const confirmWifiSuccess = () => {
|
||||
if (!wifiSuccessVisible.value) return;
|
||||
wifiSuccessVisible.value = false;
|
||||
currentState.value = STATES.CONNECTED;
|
||||
};
|
||||
|
||||
// 校验用户输入并提交 WiFi 配置,不再把手机连接结果当作设备连接成功。
|
||||
const joinNetwork = () => {
|
||||
if (joinDisabled.value) return;
|
||||
@@ -766,14 +755,6 @@ onUnmounted(() => {
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<ModalDialog
|
||||
:show="wifiSuccessVisible"
|
||||
content="WIFI连接成功"
|
||||
confirmText="确定"
|
||||
:showCancel="false"
|
||||
:onConfirm="confirmWifiSuccess"
|
||||
></ModalDialog>
|
||||
|
||||
<ModalDialog
|
||||
:show="countdownVisible"
|
||||
title="WiFi连接成功"
|
||||
|
||||
@@ -546,7 +546,6 @@ onBeforeUnmount(() => {
|
||||
:showCrosshair="false"
|
||||
:showRingLabels="false"
|
||||
:highlightOnly="true"
|
||||
:highlightStyle="{ strokeColor: '#00fffc', lineWidthScale: 3 }"
|
||||
:sectorCount="sectorCount"
|
||||
:activeSector="activeSector"
|
||||
:activeRings="activeRings"
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
nextTick,
|
||||
} from "vue";
|
||||
import audioManager, {
|
||||
getTrainingEndAudioKey,
|
||||
getTrainingStartAudioKey,
|
||||
RHYTHM_SHOOT_WINDOW_AUDIO_KEY,
|
||||
} from "@/audioManager";
|
||||
@@ -112,8 +111,6 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["rhythm-round-complete"]);
|
||||
|
||||
const trainingTitleIconMap = Object.freeze({
|
||||
base:
|
||||
"https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/text-icon-jcxl.png",
|
||||
@@ -146,7 +143,6 @@ let rhythmCountdownTimer = null;
|
||||
let rhythmTransitionTimer = null;
|
||||
let rhythmServerClockOffsetMs = 0;
|
||||
let rhythmSyncGeneration = 0;
|
||||
let rhythmRoundCompletionTimer = null;
|
||||
|
||||
const isRhythmTraining = computed(() => props.trainingType === "rhythm");
|
||||
const isStabilityTraining = computed(
|
||||
@@ -310,30 +306,6 @@ 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 nextIsInWindow = Boolean(isInWindow);
|
||||
const enteredShootWindow =
|
||||
@@ -392,8 +364,7 @@ const syncRhythmCountdown = async () => {
|
||||
const generation = ++rhythmSyncGeneration;
|
||||
clearRhythmTimers();
|
||||
|
||||
if (!isRhythmTraining.value || !props.start) {
|
||||
clearRhythmRoundCompletionTimer();
|
||||
if (!isRhythmTraining.value) {
|
||||
rhythmTransitionStyle.value = "none";
|
||||
rhythmRemainingMs.value = 0;
|
||||
rhythmRemainingSeconds.value = 0;
|
||||
@@ -411,7 +382,6 @@ const syncRhythmCountdown = async () => {
|
||||
!validRhythmShootTime.value ||
|
||||
!normalizeTimestamp(props.shootWindowStart)
|
||||
) {
|
||||
clearRhythmRoundCompletionTimer();
|
||||
rhythmTransitionStyle.value = "none";
|
||||
rhythmRemainingMs.value = 0;
|
||||
rhythmRemainingSeconds.value = 0;
|
||||
@@ -421,7 +391,6 @@ const syncRhythmCountdown = async () => {
|
||||
|
||||
rhythmTransitionStyle.value = "none";
|
||||
rhythmRemainingMs.value = getRhythmRoundRemainingMs();
|
||||
scheduleRhythmRoundCompletion(rhythmRemainingMs.value);
|
||||
rhythmRemainingSeconds.value = Math.ceil(rhythmRemainingMs.value / 1000);
|
||||
updateRhythmShootWindow(
|
||||
rhythmRemainingMs.value > 0 &&
|
||||
@@ -493,7 +462,6 @@ watch(
|
||||
|
||||
watch(
|
||||
() => [
|
||||
props.start,
|
||||
props.trainingType,
|
||||
props.roundTime,
|
||||
props.shootTime,
|
||||
@@ -525,7 +493,7 @@ async function onReceiveMessage(msg) {
|
||||
audioManager.play("比赛开始");
|
||||
}
|
||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||
audioManager.play(getTrainingEndAudioKey(msg.completed), false);
|
||||
audioManager.play("练习结束", false);
|
||||
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||
// 精准和节奏训练由页面统一处理专属结果语音,其他训练保持原播放链路。
|
||||
if (props.externalShootResultAudio) return;
|
||||
@@ -582,7 +550,6 @@ onBeforeUnmount(() => {
|
||||
clearTimer();
|
||||
rhythmSyncGeneration += 1;
|
||||
clearRhythmTimers();
|
||||
clearRhythmRoundCompletionTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -111,50 +111,57 @@ const createDifficultySummary = (item = {}) => {
|
||||
const hitReq = toNumber(item.hit_req);
|
||||
const totalReq = toNumber(item.total_req);
|
||||
const rhythmRequiredArrows = toNumber(item.total_req, arrows) || arrows;
|
||||
const timeParts = timeLimit > 0
|
||||
? [{ text: `${timeLimit}`, highlight: true }, { text: "秒内," }]
|
||||
: [{ text: "不限时," }];
|
||||
const timeLine = {
|
||||
parts: timeLimit > 0
|
||||
? [{ text: "限时" }, { text: `${timeLimit}秒`, highlight: true }]
|
||||
: [{ text: "不限时" }],
|
||||
};
|
||||
|
||||
const summaryMap = {
|
||||
base: [
|
||||
{ parts: [
|
||||
...timeParts,
|
||||
{ text: "命中" }, { text: `${arrows}`, highlight: true },
|
||||
{ text: "支" }, { text: `≥${hitReq}`, highlight: true },
|
||||
{ text: "环的箭" },
|
||||
{ text: "需有" }, { text: `${arrows}箭`, highlight: true },
|
||||
{ text: "命中" }, { text: `≥${hitReq}环`, highlight: true },
|
||||
] },
|
||||
timeLine,
|
||||
],
|
||||
endurance: [
|
||||
{ parts: [
|
||||
...timeParts,
|
||||
{ text: "共射出" }, { text: `≥${arrows}`, highlight: true },
|
||||
{ text: "支箭,且总环数" }, { text: `≥${totalReq}`, highlight: true },
|
||||
{ text: "环" },
|
||||
{ text: "需射箭数" }, { text: `≥${arrows}箭`, highlight: true },
|
||||
{ text: ",累计环数" }, { text: `≥${totalReq}环`, highlight: true },
|
||||
] },
|
||||
timeLine,
|
||||
],
|
||||
precision: [
|
||||
{ parts: [
|
||||
...timeParts,
|
||||
{ text: "命中" }, { text: `${arrows}`, highlight: true },
|
||||
{ text: "箭高亮区域" },
|
||||
{ text: "需有" }, { text: `${arrows}箭`, highlight: true },
|
||||
{ text: "命中高亮区域" },
|
||||
] },
|
||||
timeLine,
|
||||
],
|
||||
rhythm: [
|
||||
{ parts: [
|
||||
{ text: `${arrows}`, highlight: true },
|
||||
{ text: "个节奏内,随节奏射箭,命中" },
|
||||
{ text: `${rhythmRequiredArrows}`, highlight: true },
|
||||
{ text: "箭成绩" }, { text: `≥${hitReq}`, highlight: true },
|
||||
{ text: "环且踩中节拍区间" },
|
||||
{ text: "跟随节拍撒放,共" }, { text: `${arrows}箭`, highlight: true },
|
||||
{ text: "需" }, { text: `${rhythmRequiredArrows}箭`, highlight: true },
|
||||
] },
|
||||
{ parts: [
|
||||
{ text: "命中" }, { text: `≥${hitReq}环`, highlight: true },
|
||||
{ text: "且踏中节拍区间" },
|
||||
] },
|
||||
],
|
||||
stability: [
|
||||
{
|
||||
parts: [
|
||||
...timeParts,
|
||||
{ text: "每射一箭" },
|
||||
{ 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 rhythmFallbackTimestamp = ref(0);
|
||||
const rhythmHasActiveServerAnchor = ref(false);
|
||||
// 仅由前端每轮倒计时完成事件累加,不根据服务端箭数推算。
|
||||
const rhythmCompletedRounds = ref(0);
|
||||
// 节奏训练目标箭数以服务端 total_arrows 为准;首帧缺失时用 arrows_left 初始化并保持稳定。
|
||||
const rhythmTargetArrows = ref(0);
|
||||
// 服务端状态立即落到 practiceInfo,精准训练的目标区域单独延迟展示。
|
||||
const visiblePrecisionTarget = ref({
|
||||
randomBlock: 0,
|
||||
@@ -440,11 +440,10 @@ const trainingCopy = computed(() => {
|
||||
return {
|
||||
inline: true,
|
||||
details: [
|
||||
{ text: "命中 " },
|
||||
{ text: "需有 " },
|
||||
{ text: `(${completedArrows}/${targetArrows})`, highlight: true },
|
||||
{ text: " 支" },
|
||||
{ text: `≥${hitReq}`, highlight: true },
|
||||
{ text: "环的箭" },
|
||||
{ text: " 箭命中" },
|
||||
{ text: `≥${hitReq}环`, highlight: true },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -464,11 +463,10 @@ const trainingCopy = computed(() => {
|
||||
return {
|
||||
inline: true,
|
||||
details: [
|
||||
{ text: "射出≥ " },
|
||||
{ text: `(${currentArrows}/${targetArrows})`, highlight: true },
|
||||
{ text: " 支箭,总环数达到≥ " },
|
||||
{ text: `(${currentRings}/${targetRings})`, highlight: true },
|
||||
{ text: " 环" },
|
||||
{ text: "需射箭数" },
|
||||
{ text: `≥ (${currentArrows}/${targetArrows}) 支`, highlight: true },
|
||||
{ text: ",累计环数\n" },
|
||||
{ text: `≥ (${currentRings}/${targetRings}) 环`, highlight: true },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -487,34 +485,38 @@ const trainingCopy = computed(() => {
|
||||
return {
|
||||
inline: true,
|
||||
details: [
|
||||
{ text: "命中高亮区域 " },
|
||||
{ text: "需有 " },
|
||||
{ text: `(${completedArrows}/${targetArrows})`, highlight: true },
|
||||
{ text: " 箭" },
|
||||
{ text: " 箭命中高亮区域" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (trainingType.value === "rhythm") {
|
||||
// 总节奏数沿用关卡配置,与选择页保持一致,不使用消息中的箭数覆盖。
|
||||
const totalArrows = getPositiveInteger(total.value);
|
||||
// 节奏数由前端倒计时累计,达标箭数与下方金色箭记录使用同一判定。
|
||||
const totalArrows = getPositiveInteger(rhythmTargetArrows.value) || total.value;
|
||||
// 节奏训练进度由总箭数减剩余箭数计算,qualifiedArrows 仅用于稳定训练。
|
||||
const targetArrows =
|
||||
getPositiveInteger(practiceInfo.value.targetArrows) ||
|
||||
getPositiveInteger(trainingParams.value.totalReq) || totalArrows;
|
||||
// 展示当前节奏序号,从 1 开始;内部仍累计已完成次数。
|
||||
const currentRound = Math.min(totalArrows, rhythmCompletedRounds.value + 1);
|
||||
const completedArrows = Math.min(
|
||||
totalArrows,
|
||||
Math.max(
|
||||
0,
|
||||
totalArrows - getPracticeNumber(practiceInfo.value.arrowsLeft, totalArrows)
|
||||
)
|
||||
);
|
||||
const hitReq = rhythmHitReq.value;
|
||||
const qualifiedArrows = scores.value.filter((arrow) => arrow?.ok === true).length;
|
||||
|
||||
return {
|
||||
inline: true,
|
||||
details: [
|
||||
{ text: `(${currentRound}/${totalArrows})`, highlight: true },
|
||||
{ text: " 个节奏内,跟随节奏射箭,命中 " },
|
||||
{ text: `(${qualifiedArrows}/${targetArrows})`, highlight: true },
|
||||
{ text: " 箭" },
|
||||
{ text: `≥${hitReq}`, highlight: true },
|
||||
{ text: "环且踩中节拍区间" },
|
||||
{ text: "跟随节拍撒放,共" },
|
||||
{ text: `${totalArrows}箭`, highlight: true },
|
||||
{ text: "需 " },
|
||||
{ text: `(${completedArrows}/${targetArrows}) 箭`, highlight: true },
|
||||
{ text: "\n命中" },
|
||||
{ text: `≥${hitReq}环`, highlight: true },
|
||||
{ text: "且踏中节拍区间" },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -527,9 +529,9 @@ const trainingCopy = computed(() => {
|
||||
return {
|
||||
inline: true,
|
||||
details: [
|
||||
{ text: "每射一箭" },
|
||||
{ text: `≥${hitReq}`, highlight: true },
|
||||
{ text: "环可为能量槽充能,充满即过关" },
|
||||
{ text: "命中" },
|
||||
{ text: `≥${hitReq}环`, highlight: true },
|
||||
{ text: "可为能量槽充能,能量打满即过关" },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -694,14 +696,31 @@ const cacheRhythmTrainingConfig = (message = {}) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onRhythmRoundComplete = () => {
|
||||
if (!isShootingStage.value || !start.value || !isRhythmTraining.value || practiceEnded.value) return;
|
||||
const totalRounds = getPositiveInteger(total.value);
|
||||
rhythmCompletedRounds.value = Math.min(totalRounds, rhythmCompletedRounds.value + 1);
|
||||
const cacheRhythmTargetArrows = (message = {}) => {
|
||||
const messageTrainingType = String(
|
||||
message.trainingType ?? message.training_type ?? trainingType.value
|
||||
).trim();
|
||||
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 = {}) => {
|
||||
cacheRhythmTrainingConfig(message);
|
||||
cacheRhythmTargetArrows(message);
|
||||
const messageTrainingType = String(
|
||||
message.trainingType ?? message.training_type ?? trainingType.value
|
||||
).trim();
|
||||
@@ -722,9 +741,11 @@ const syncPracticeInfo = (message = {}) => {
|
||||
].includes(message.type));
|
||||
|
||||
if (isRhythmSnapshot) {
|
||||
// 达标箭数的 0 按 protobuf 默认值处理;剩余数缺失不能用于推算节奏进度。
|
||||
if (!Object.prototype.hasOwnProperty.call(nextInfo, "qualifiedArrows")) {
|
||||
nextInfo.qualifiedArrows = 0;
|
||||
// 完整快照中 proto3 会省略 0;最后一箭及重连时不能保留旧剩余数。
|
||||
for (const field of ["arrowsLeft", "qualifiedArrows"]) {
|
||||
if (!Object.prototype.hasOwnProperty.call(nextInfo, field)) {
|
||||
nextInfo[field] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1406,7 +1427,7 @@ onLoad((options = {}) => {
|
||||
serverAddr.value = trainingContext.serverAddr || "";
|
||||
|
||||
const startAudioKey = getTrainingStartAudioKey(trainingParams.value.type);
|
||||
const warmupKeys = [startAudioKey, "通关成功", "通关失败", "胜利", "失败"];
|
||||
const warmupKeys = [startAudioKey, "胜利", "失败"];
|
||||
if (trainingParams.value.type === "precision") {
|
||||
warmupKeys.push("Bingo命中目标", "未命中");
|
||||
} else if (trainingParams.value.type === "rhythm") {
|
||||
@@ -1436,7 +1457,6 @@ const onReady = async () => {
|
||||
}
|
||||
|
||||
practiceStarting.value = true;
|
||||
rhythmCompletedRounds.value = 0;
|
||||
pageStage.value = pageStages.LOADING;
|
||||
clearHighlightTestTimer();
|
||||
useHighlightTest.value = false;
|
||||
@@ -1678,7 +1698,7 @@ async function onRetry() {
|
||||
rhythmFallbackWindowStart.value = 0;
|
||||
rhythmFallbackTimestamp.value = 0;
|
||||
rhythmHasActiveServerAnchor.value = false;
|
||||
rhythmCompletedRounds.value = 0;
|
||||
rhythmTargetArrows.value = 0;
|
||||
invalidateShotPresentations({ resetVisible: true });
|
||||
start.value = false;
|
||||
scores.value = [];
|
||||
@@ -1701,7 +1721,7 @@ function onAudioEnded(key) {
|
||||
Array.from(audioWaiters).forEach((waiter) => {
|
||||
if (waiter.expectedKey === key) waiter.done();
|
||||
});
|
||||
if (["比赛结束", "练习结束", "通关成功", "通关失败"].includes(String(key || ""))) {
|
||||
if (["比赛结束", "练习结束"].includes(String(key || ""))) {
|
||||
void onOver();
|
||||
}
|
||||
}
|
||||
@@ -1849,7 +1869,6 @@ onBeforeUnmount(() => {
|
||||
trainingType === 'stability'
|
||||
"
|
||||
:onStop="onTimeLimitReached"
|
||||
@rhythm-round-complete="onRhythmRoundComplete"
|
||||
/>
|
||||
<view class="user-row">
|
||||
<!-- <Avatar :src="user.avatar" :size="35" /> -->
|
||||
|
||||
+32
-25
@@ -93,42 +93,49 @@ function createWebSocket(token, onMessage) {
|
||||
"/addons/shoot/otaResult",
|
||||
];
|
||||
if (passthroughEvents.includes(event)) {
|
||||
if ((code == null || Number(code) === 0) && onMessage && data && typeof data === "object") {
|
||||
if (
|
||||
(code == null || Number(code) === 0) &&
|
||||
onMessage &&
|
||||
data &&
|
||||
typeof data === "object"
|
||||
) {
|
||||
onMessage({ event, data, code, timestamp });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data?.type) {
|
||||
if (!data || typeof data !== "object") return;
|
||||
if (data.type) {
|
||||
if (ENABLE_REALTIME_MESSAGE_LOG) {
|
||||
console.log("收到 WebSocket 消息", getMessageTypeName(data.type));
|
||||
}
|
||||
if (onMessage) onMessage({ ...(data.data || {}), type: data.type });
|
||||
return;
|
||||
}
|
||||
const updates = Array.isArray(data?.updates) ? data.updates : [];
|
||||
if (!updates.length) return;
|
||||
if (onMessage) onMessage(updates);
|
||||
const msg = updates[0];
|
||||
if (msg) {
|
||||
if (ENABLE_REALTIME_MESSAGE_LOG) {
|
||||
console.log(
|
||||
"收到 WebSocket 更新",
|
||||
getMessageTypeName(msg.constructor),
|
||||
{ updateCount: data.updates.length }
|
||||
);
|
||||
}
|
||||
if (msg.constructor === MESSAGETYPES.RankUpdate) {
|
||||
uni.setStorageSync("latestRank", msg.lvl);
|
||||
} else if (msg.constructor === MESSAGETYPES.LvlUpdate) {
|
||||
uni.setStorageSync("latestLvl", msg.lvl);
|
||||
} else if (msg.constructor === MESSAGETYPES.DeviceOnline) {
|
||||
uni.$emit("update-online", true);
|
||||
} else if (msg.constructor === MESSAGETYPES.DeviceOffline) {
|
||||
uni.$emit("update-online", false);
|
||||
} else if (msg.constructor === MESSAGETYPES.DeviceCharging) {
|
||||
uni.$emit("device-charging");
|
||||
}
|
||||
if (!Array.isArray(data.updates)) return;
|
||||
if (onMessage) onMessage(data.updates);
|
||||
const firstMessage = data.updates[0];
|
||||
if (firstMessage && ENABLE_REALTIME_MESSAGE_LOG) {
|
||||
console.log(
|
||||
"收到 WebSocket 更新",
|
||||
getMessageTypeName(firstMessage.constructor),
|
||||
{ updateCount: data.updates.length }
|
||||
);
|
||||
}
|
||||
data.updates.forEach((msg) => {
|
||||
if (msg) {
|
||||
if (msg.constructor === MESSAGETYPES.RankUpdate) {
|
||||
uni.setStorageSync("latestRank", msg.lvl);
|
||||
} else if (msg.constructor === MESSAGETYPES.LvlUpdate) {
|
||||
uni.setStorageSync("latestLvl", msg.lvl);
|
||||
} else if (msg.constructor === MESSAGETYPES.DeviceOnline) {
|
||||
uni.$emit("update-online", true);
|
||||
} else if (msg.constructor === MESSAGETYPES.DeviceOffline) {
|
||||
uni.$emit("update-online", false);
|
||||
} else if (msg.constructor === MESSAGETYPES.DeviceCharging) {
|
||||
uni.$emit("device-charging");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socketTask.onError((err) => {
|
||||
|
||||
Reference in New Issue
Block a user