update:新增稳定训练
This commit is contained in:
@@ -18,6 +18,7 @@ const trainingTypeNameMap = Object.freeze({
|
||||
endurance: "耐力训练",
|
||||
precision: "精准训练",
|
||||
rhythm: "节奏训练",
|
||||
stability: "稳定训练",
|
||||
});
|
||||
|
||||
const trainingType = computed(() =>
|
||||
|
||||
@@ -20,6 +20,7 @@ const trainingTypeNameMap = Object.freeze({
|
||||
endurance: "耐力训练",
|
||||
precision: "精准训练",
|
||||
rhythm: "节奏训练",
|
||||
stability: "稳定训练",
|
||||
});
|
||||
|
||||
const getTrainingTypeName = (trainingType) => {
|
||||
|
||||
@@ -39,7 +39,9 @@ const getDisplayText = (arrow) => {
|
||||
|
||||
const isLowScore = (arrow) => {
|
||||
if (!arrow) return false;
|
||||
if (props.trainingType === "rhythm") return arrow.ok !== true;
|
||||
if (["rhythm", "stability"].includes(props.trainingType)) {
|
||||
return arrow.ok !== true;
|
||||
}
|
||||
if (arrow.ringX) return false;
|
||||
const ring = Number(arrow.ring);
|
||||
return Number.isFinite(ring) && ring < 6;
|
||||
|
||||
@@ -255,6 +255,32 @@ const metricConfigs = {
|
||||
deltaUnit: "次",
|
||||
},
|
||||
],
|
||||
stability: [
|
||||
{
|
||||
label: "能量最高值",
|
||||
valueKeys: ["maxEnergyPercent", "max_energy_percent"],
|
||||
unit: "%",
|
||||
deltaKeys: [
|
||||
"deltaMaxEnergyPercent",
|
||||
"delta_max_energy_percent",
|
||||
],
|
||||
deltaUnit: "%",
|
||||
},
|
||||
{
|
||||
label: "达标箭数",
|
||||
valueKeys: ["qualifiedArrows", "qualified_arrows"],
|
||||
unit: "箭",
|
||||
deltaKeys: ["deltaQualifiedArrows", "delta_qualified_arrows"],
|
||||
deltaUnit: "箭",
|
||||
},
|
||||
{
|
||||
label: "达标率",
|
||||
valueKeys: ["qualifiedRate", "qualified_rate"],
|
||||
unit: "%",
|
||||
deltaKeys: ["deltaQualifiedRate", "delta_qualified_rate"],
|
||||
deltaUnit: "%",
|
||||
},
|
||||
],
|
||||
endurance: [
|
||||
{
|
||||
label: "完成箭数",
|
||||
@@ -304,7 +330,8 @@ const metricConfigs = {
|
||||
};
|
||||
|
||||
const resultRows = computed(() => {
|
||||
const configs = metricConfigs[resultTrainingType.value] || metricConfigs.precision;
|
||||
const configs =
|
||||
metricConfigs[resultTrainingType.value] || metricConfigs.precision;
|
||||
return configs.map((config) => {
|
||||
const fallback = config.fallback ? config.fallback() : 0;
|
||||
const value = readMetricNumber(config.valueKeys, fallback);
|
||||
@@ -371,7 +398,10 @@ const calories = computed(
|
||||
<view v-else class="stat-value">--</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="resultTrainingType !== 'rhythm'" class="stat-row">
|
||||
<view
|
||||
v-if="!['rhythm', 'stability'].includes(resultTrainingType)"
|
||||
class="stat-row"
|
||||
>
|
||||
<image class="stat-bg" src="https://static.shelingxingqiu.com/shootmini/static/training-difficulty-design/result-c-bg.png" mode="scaleToFill" />
|
||||
<view class="stat-cell">
|
||||
<text class="stat-label">消耗卡路里</text>
|
||||
|
||||
@@ -73,6 +73,14 @@ const props = defineProps({
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
energyPercent: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
energyReqPercent: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
isVip: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -137,6 +145,9 @@ let rhythmServerClockOffsetMs = 0;
|
||||
let rhythmSyncGeneration = 0;
|
||||
|
||||
const isRhythmTraining = computed(() => props.trainingType === "rhythm");
|
||||
const isStabilityTraining = computed(
|
||||
() => props.trainingType === "stability"
|
||||
);
|
||||
|
||||
const normalizePositiveInteger = (value) => {
|
||||
const numberValue = Number(value);
|
||||
@@ -176,6 +187,18 @@ const progressPercent = computed(() => {
|
||||
return Math.max(0, Math.min(100, (remain.value / props.total) * 100));
|
||||
});
|
||||
|
||||
const stabilityEnergyPercent = computed(() =>
|
||||
Math.max(0, Math.min(100, Number(props.energyPercent) || 0))
|
||||
);
|
||||
const stabilityEnergyReqPercent = computed(() =>
|
||||
Math.max(0, Math.min(100, Number(props.energyReqPercent) || 0))
|
||||
);
|
||||
const stabilityEnergyReached = computed(
|
||||
() =>
|
||||
stabilityEnergyReqPercent.value > 0 &&
|
||||
stabilityEnergyPercent.value >= stabilityEnergyReqPercent.value
|
||||
);
|
||||
|
||||
const rhythmMarkerPercent = computed(() => {
|
||||
if (!validRhythmRoundTime.value) return 0;
|
||||
return Math.max(
|
||||
@@ -536,9 +559,34 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<view
|
||||
v-if="show"
|
||||
:class="isRhythmTraining ? 'rhythm-progress' : 'progress-card'"
|
||||
:class="
|
||||
isRhythmTraining
|
||||
? 'rhythm-progress'
|
||||
: isStabilityTraining
|
||||
? 'stability-progress'
|
||||
: 'progress-card'
|
||||
"
|
||||
>
|
||||
<template v-if="isRhythmTraining">
|
||||
<template v-if="isStabilityTraining">
|
||||
<text class="stability-progress__time">{{ remain }}秒</text>
|
||||
<view class="stability-progress__track">
|
||||
<view
|
||||
class="stability-progress__fill"
|
||||
:class="{
|
||||
'stability-progress__fill--reached': stabilityEnergyReached,
|
||||
}"
|
||||
:style="{ width: `${stabilityEnergyPercent}%` }"
|
||||
/>
|
||||
<view
|
||||
class="stability-progress__marker"
|
||||
:style="{ left: `${stabilityEnergyReqPercent}%` }"
|
||||
/>
|
||||
<text class="stability-progress__value">
|
||||
{{ Math.round(stabilityEnergyPercent) }}%
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else-if="isRhythmTraining">
|
||||
<text class="rhythm-progress__title">{{ rhythmTitle }}</text>
|
||||
<view class="rhythm-progress__track">
|
||||
<view
|
||||
@@ -758,6 +806,68 @@ onBeforeUnmount(() => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stability-progress {
|
||||
box-sizing: border-box;
|
||||
margin: 32rpx 84rpx 0;
|
||||
}
|
||||
|
||||
.stability-progress__time {
|
||||
display: block;
|
||||
margin-bottom: 20rpx;
|
||||
color: #ffffff;
|
||||
font-size: 34rpx;
|
||||
font-weight: 500;
|
||||
line-height: 48rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stability-progress__track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 24rpx;
|
||||
overflow: hidden;
|
||||
border-radius: 18rpx;
|
||||
background: #444444;
|
||||
}
|
||||
|
||||
.stability-progress__fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(90deg, #87f1df 0%, #5ba8e8 100%);
|
||||
transition: width 240ms linear, background 240ms ease;
|
||||
}
|
||||
|
||||
.stability-progress__fill--reached {
|
||||
background: linear-gradient(90deg, #a5df62 0%, #61c787 100%);
|
||||
}
|
||||
|
||||
.stability-progress__marker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
width: 4rpx;
|
||||
transform: translateX(-2rpx);
|
||||
background: rgba(26, 24, 22, 0.92);
|
||||
}
|
||||
|
||||
.stability-progress__value {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #eefaff;
|
||||
font-size: 18rpx;
|
||||
line-height: 24rpx;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rhythm-progress {
|
||||
box-sizing: border-box;
|
||||
margin: 32rpx 84rpx 0;
|
||||
|
||||
@@ -13,7 +13,20 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const previewLines = computed(() => {
|
||||
return props.lines.map((line) => String(line || "").trim()).filter(Boolean);
|
||||
return props.lines
|
||||
.map((line) => {
|
||||
const rawParts = Array.isArray(line?.parts)
|
||||
? line.parts
|
||||
: [{ text: line }];
|
||||
const parts = rawParts
|
||||
.map((part) => ({
|
||||
text: String(part?.text ?? part ?? "").trim(),
|
||||
}))
|
||||
.filter((part) => part.text);
|
||||
|
||||
return { parts };
|
||||
})
|
||||
.filter((line) => line.parts.length > 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -27,13 +40,16 @@ const previewLines = computed(() => {
|
||||
<view class="difficulty-preview__content">
|
||||
<text class="difficulty-preview__title">{{ title }}</text>
|
||||
<view class="difficulty-preview__copy">
|
||||
<text
|
||||
<view
|
||||
v-for="(line, index) in previewLines"
|
||||
:key="`${line}-${index}`"
|
||||
:key="index"
|
||||
class="difficulty-preview__line"
|
||||
>
|
||||
{{ line }}
|
||||
</text>
|
||||
<text
|
||||
v-for="(part, partIndex) in line.parts"
|
||||
:key="partIndex"
|
||||
>{{ part.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -86,6 +102,4 @@ const previewLines = computed(() => {
|
||||
.difficulty-preview__line {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/apis";
|
||||
|
||||
// 难度页接口数据源:
|
||||
// 1. 接口:GET /training/difficulty/list?type=base/endurance/precision/rhythm
|
||||
// 1. 接口:GET /training/difficulty/list?type=base/endurance/precision/rhythm/stability
|
||||
// 2. 当前进度:接口 user_levels / list.completed,路由参数可覆盖选中难度
|
||||
const trainingDifficultyStorageKey = "training-selection";
|
||||
const defaultTrainingType = "precision";
|
||||
@@ -35,6 +35,10 @@ const trainingTypeMetaMap = {
|
||||
key: "rhythm",
|
||||
title: "节奏训练",
|
||||
},
|
||||
stability: {
|
||||
key: "stability",
|
||||
title: "稳定训练",
|
||||
},
|
||||
};
|
||||
const routeModeTypeMap = {
|
||||
basic: "base",
|
||||
@@ -42,6 +46,9 @@ const routeModeTypeMap = {
|
||||
endurance: "endurance",
|
||||
precision: "precision",
|
||||
rhythm: "rhythm",
|
||||
stability: "stability",
|
||||
// 兼容历史上已经分享出去的 power 深链,实际创建仍统一传 stability。
|
||||
power: "stability",
|
||||
};
|
||||
const defaultTargetType = 1;
|
||||
|
||||
@@ -103,6 +110,7 @@ const createDifficultySummary = (item = {}) => {
|
||||
const timeLimit = toNumber(item.time_limit);
|
||||
const hitReq = toNumber(item.hit_req);
|
||||
const totalReq = toNumber(item.total_req);
|
||||
const energyReqPercent = toNumber(item.energy_req_percent);
|
||||
const shootingTimeText =
|
||||
timeLimit > 0 ? `在${timeLimit}秒内进行射箭` : "不限时进行射箭";
|
||||
const enduranceTimeText =
|
||||
@@ -129,6 +137,26 @@ const createDifficultySummary = (item = {}) => {
|
||||
: "需要在指定时间节点射箭",
|
||||
hitReq > 0 ? `且每箭命中${hitReq}环内` : "且每箭命中指定区域",
|
||||
],
|
||||
stability: [
|
||||
{
|
||||
parts: [
|
||||
{ text: timeLimit > 0 ? "在" : "" },
|
||||
{
|
||||
text: timeLimit > 0 ? `${timeLimit}秒` : "不限时",
|
||||
highlight: true,
|
||||
},
|
||||
{ text: timeLimit > 0 ? "内射箭,命中" : "射箭,命中" },
|
||||
{ text: `${hitReq}环`, highlight: true },
|
||||
{ text: "可获得能量" },
|
||||
],
|
||||
},
|
||||
{
|
||||
parts: [
|
||||
{ text: "计时结束能量需要大于" },
|
||||
{ text: `${energyReqPercent}%`, highlight: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (summaryMap[type] || [desc]).filter(Boolean);
|
||||
@@ -604,6 +632,21 @@ const createPracticeQuery = (difficulty) => {
|
||||
roundTime: toNumber(difficulty.round_time ?? difficulty.roundTime),
|
||||
shootTime: toNumber(difficulty.shoot_time ?? difficulty.shootTime),
|
||||
},
|
||||
stability: {
|
||||
hitReq: toNumber(difficulty.hit_req),
|
||||
scoreSlot: toNumber(
|
||||
difficulty.score_slot ?? difficulty.scoreSlot
|
||||
),
|
||||
energyPerHit: toNumber(
|
||||
difficulty.energy_per_hit ?? difficulty.energyPerHit
|
||||
),
|
||||
energyCostPerSec: toNumber(
|
||||
difficulty.energy_cost_per_sec ?? difficulty.energyCostPerSec
|
||||
),
|
||||
energyReqPercent: toNumber(
|
||||
difficulty.energy_req_percent ?? difficulty.energyReqPercent
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -638,6 +681,19 @@ const saveTrainingContext = (practice = {}) => {
|
||||
targetPaperType: difficulty.targetPaperType,
|
||||
roundTime: toNumber(difficulty.round_time ?? difficulty.roundTime),
|
||||
shootTime: toNumber(difficulty.shoot_time ?? difficulty.shootTime),
|
||||
hitReq: toNumber(difficulty.hit_req ?? difficulty.hitReq),
|
||||
scoreSlot: toNumber(
|
||||
difficulty.score_slot ?? difficulty.scoreSlot
|
||||
),
|
||||
energyPerHit: toNumber(
|
||||
difficulty.energy_per_hit ?? difficulty.energyPerHit
|
||||
),
|
||||
energyCostPerSec: toNumber(
|
||||
difficulty.energy_cost_per_sec ?? difficulty.energyCostPerSec
|
||||
),
|
||||
energyReqPercent: toNumber(
|
||||
difficulty.energy_req_percent ?? difficulty.energyReqPercent
|
||||
),
|
||||
practiceId: practice.id || "",
|
||||
serverAddr: practice.serverAddr || "",
|
||||
createdAt: practice.id ? Date.now() : 0,
|
||||
|
||||
@@ -15,9 +15,9 @@ const trainingModeRouteMap = {
|
||||
endurance: "endurance",
|
||||
precision: "precision",
|
||||
rhythm: "rhythm",
|
||||
stability: "power",
|
||||
stability: "stability",
|
||||
};
|
||||
const unavailableTrainingIds = new Set(["stability"]);
|
||||
const unavailableTrainingIds = new Set();
|
||||
// 训练项目卡片右侧主图标。
|
||||
const trainingModeIconMap = {
|
||||
base_bow:
|
||||
|
||||
@@ -13,8 +13,11 @@ import BubbleTip from "./components/BubbleTip.vue";
|
||||
import audioManager, {
|
||||
getPrecisionShotAudioKeys,
|
||||
getRhythmShotAudioKeys,
|
||||
getStabilityShotAudioKeys,
|
||||
getTrainingStartAudioKey,
|
||||
RHYTHM_SHOOT_WINDOW_AUDIO_KEY,
|
||||
STABILITY_ENERGY_50_AUDIO_KEY,
|
||||
STABILITY_ENERGY_70_AUDIO_KEY,
|
||||
} from "@/audioManager";
|
||||
|
||||
import {
|
||||
@@ -265,6 +268,9 @@ const loadNextDifficultyState = (result = {}) => {
|
||||
const timeLimit = computed(() => getPositiveInteger(practiceInfo.value.timeLimit));
|
||||
const hasTimeLimit = computed(() => timeLimit.value > 0);
|
||||
const isRhythmTraining = computed(() => trainingType.value === "rhythm");
|
||||
const isStabilityTraining = computed(
|
||||
() => trainingType.value === "stability"
|
||||
);
|
||||
const rhythmRoundTime = computed(() =>
|
||||
getPositiveInteger(practiceInfo.value.roundTime) ||
|
||||
getPositiveInteger(trainingParams.value.roundTime)
|
||||
@@ -297,6 +303,53 @@ const rhythmHitReq = computed(
|
||||
getPositiveInteger(trainingParams.value.hitReq)
|
||||
);
|
||||
|
||||
const stabilityEnergyPerHit = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
getPracticeNumber(
|
||||
practiceInfo.value.energyPerHit,
|
||||
trainingParams.value.energyPerHit
|
||||
)
|
||||
)
|
||||
);
|
||||
const stabilityEnergyReqPercent = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
100,
|
||||
getPracticeNumber(
|
||||
practiceInfo.value.energyReqPercent,
|
||||
trainingParams.value.energyReqPercent
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
const stabilityScoreSlot = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
getPracticeNumber(
|
||||
practiceInfo.value.scoreSlot,
|
||||
trainingParams.value.scoreSlot
|
||||
)
|
||||
)
|
||||
);
|
||||
const stabilityCurrentEnergy = computed(() => {
|
||||
const currentEnergy = Math.max(
|
||||
0,
|
||||
getPracticeNumber(practiceInfo.value.currentEnergy)
|
||||
);
|
||||
return stabilityScoreSlot.value > 0
|
||||
? Math.min(stabilityScoreSlot.value, currentEnergy)
|
||||
: currentEnergy;
|
||||
});
|
||||
const stabilityEnergyPercent = computed(() => {
|
||||
if (stabilityScoreSlot.value <= 0) return 0;
|
||||
return Math.min(
|
||||
100,
|
||||
(stabilityCurrentEnergy.value / stabilityScoreSlot.value) * 100
|
||||
);
|
||||
});
|
||||
|
||||
const initializeRhythmFirstRoundCountdown = () => {
|
||||
rhythmHasActiveServerAnchor.value = false;
|
||||
rhythmFallbackWindowStart.value = 0;
|
||||
@@ -444,6 +497,27 @@ const trainingCopy = computed(() => {
|
||||
};
|
||||
}
|
||||
|
||||
if (trainingType.value === "stability") {
|
||||
const hitReq = getPracticeNumber(
|
||||
practiceInfo.value.hitReq,
|
||||
trainingParams.value.hitReq
|
||||
);
|
||||
|
||||
return {
|
||||
inline: true,
|
||||
details: [
|
||||
{ text: "射箭命中" },
|
||||
{ text: `${hitReq}环及以上`, highlight: true },
|
||||
{ text: `每箭可获得${stabilityEnergyPerHit.value}点能量,` },
|
||||
{ text: "计时结束时能量需达到" },
|
||||
{
|
||||
text: `${stabilityEnergyReqPercent.value}%`,
|
||||
highlight: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -481,6 +555,18 @@ const practiceInfoFields = [
|
||||
"shootTime",
|
||||
"shootWindowStart",
|
||||
"inShootWindow",
|
||||
"scoreSlot",
|
||||
"currentEnergy",
|
||||
"energyCostPerSec",
|
||||
"energyPerHit",
|
||||
"energyReqPercent",
|
||||
"deltaCurrentEnergy",
|
||||
"maxEnergyPercent",
|
||||
"qualifiedArrows",
|
||||
"qualifiedRate",
|
||||
"deltaMaxEnergyPercent",
|
||||
"deltaQualifiedArrows",
|
||||
"deltaQualifiedRate",
|
||||
"timeLimit",
|
||||
"completed",
|
||||
"totalArrows",
|
||||
@@ -519,6 +605,18 @@ const practiceResultFields = [
|
||||
"maxCombo",
|
||||
"totalHits",
|
||||
"currentRings",
|
||||
"scoreSlot",
|
||||
"currentEnergy",
|
||||
"energyCostPerSec",
|
||||
"energyPerHit",
|
||||
"energyReqPercent",
|
||||
"deltaCurrentEnergy",
|
||||
"maxEnergyPercent",
|
||||
"qualifiedArrows",
|
||||
"qualifiedRate",
|
||||
"deltaMaxEnergyPercent",
|
||||
"deltaQualifiedArrows",
|
||||
"deltaQualifiedRate",
|
||||
"deltaTotalHits",
|
||||
"deltaDuration",
|
||||
"deltaMaxCombo",
|
||||
@@ -600,6 +698,9 @@ const cacheRhythmTargetArrows = (message = {}) => {
|
||||
const syncPracticeInfo = (message = {}) => {
|
||||
cacheRhythmTrainingConfig(message);
|
||||
cacheRhythmTargetArrows(message);
|
||||
const messageTrainingType = String(
|
||||
message.trainingType ?? message.training_type ?? trainingType.value
|
||||
).trim();
|
||||
const nextInfo = practiceInfoFields.reduce((result, field) => {
|
||||
if (Object.prototype.hasOwnProperty.call(message, field)) {
|
||||
result[field] = message[field];
|
||||
@@ -607,6 +708,24 @@ const syncPracticeInfo = (message = {}) => {
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
const isStabilityEnergySnapshot =
|
||||
messageTrainingType === "stability" &&
|
||||
(Object.prototype.hasOwnProperty.call(message, "currentEnergy") ||
|
||||
message.type === undefined ||
|
||||
[
|
||||
MESSAGETYPESV2.BattleStart,
|
||||
MESSAGETYPESV2.ShootResult,
|
||||
MESSAGETYPESV2.BattleEnd,
|
||||
].includes(message.type));
|
||||
|
||||
if (isStabilityEnergySnapshot) {
|
||||
// BattleStart/ShootResult/PracticeEnd/同步消息都是稳定训练完整快照。
|
||||
// current_energy 为 0 时 protobuf 不编码,不能沿用上一份非零能量。
|
||||
if (!Object.prototype.hasOwnProperty.call(nextInfo, "currentEnergy")) {
|
||||
nextInfo.currentEnergy = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const isPrecisionSnapshot =
|
||||
(message.type === MESSAGETYPESV2.BattleStart ||
|
||||
message.type === MESSAGETYPESV2.ShootResult) &&
|
||||
@@ -1216,7 +1335,10 @@ onLoad((options = {}) => {
|
||||
toRouteNumber(trainingContext.difficultyLevel)
|
||||
),
|
||||
recordId: options.recordId || "",
|
||||
hitReq: toRouteNumber(options.hitReq),
|
||||
hitReq: toRouteNumber(
|
||||
options.hitReq,
|
||||
toRouteNumber(trainingContext.hitReq)
|
||||
),
|
||||
totalReq: toRouteNumber(options.totalReq),
|
||||
blocks: toRouteNumber(options.blocks),
|
||||
mode: toRouteNumber(options.mode),
|
||||
@@ -1228,6 +1350,22 @@ onLoad((options = {}) => {
|
||||
options.shootTime,
|
||||
toRouteNumber(trainingContext.shootTime)
|
||||
),
|
||||
energyPerHit: toRouteNumber(
|
||||
options.energyPerHit,
|
||||
toRouteNumber(trainingContext.energyPerHit)
|
||||
),
|
||||
energyCostPerSec: toRouteNumber(
|
||||
options.energyCostPerSec,
|
||||
toRouteNumber(trainingContext.energyCostPerSec)
|
||||
),
|
||||
energyReqPercent: toRouteNumber(
|
||||
options.energyReqPercent,
|
||||
toRouteNumber(trainingContext.energyReqPercent)
|
||||
),
|
||||
scoreSlot: toRouteNumber(
|
||||
options.scoreSlot,
|
||||
toRouteNumber(trainingContext.scoreSlot)
|
||||
),
|
||||
};
|
||||
practiseId.value = trainingContext.practiceId || "";
|
||||
serverAddr.value = trainingContext.serverAddr || "";
|
||||
@@ -1238,6 +1376,11 @@ onLoad((options = {}) => {
|
||||
warmupKeys.push("Bingo命中目标", "未命中");
|
||||
} else if (trainingParams.value.type === "rhythm") {
|
||||
warmupKeys.push(RHYTHM_SHOOT_WINDOW_AUDIO_KEY, "Perfect", "miss");
|
||||
} else if (trainingParams.value.type === "stability") {
|
||||
warmupKeys.push(
|
||||
STABILITY_ENERGY_50_AUDIO_KEY,
|
||||
STABILITY_ENERGY_70_AUDIO_KEY
|
||||
);
|
||||
}
|
||||
void audioManager.warmKeys(warmupKeys.filter(Boolean));
|
||||
});
|
||||
@@ -1406,6 +1549,30 @@ async function onReceiveMessage(msg) {
|
||||
if (hasNewShot) {
|
||||
shotEffectToken.value += 1;
|
||||
}
|
||||
} else if (trainingType.value === "stability") {
|
||||
const hasStabilityShot =
|
||||
msg.stabilityHasNewShot === true || hasNewShot;
|
||||
const latestDetail =
|
||||
Array.isArray(msg.details) && msg.details.length > 0
|
||||
? msg.details[msg.details.length - 1]
|
||||
: null;
|
||||
const arrow = msg.shootData || latestDetail;
|
||||
const audioKeys = hasStabilityShot
|
||||
? getStabilityShotAudioKeys(arrow)
|
||||
: [];
|
||||
const milestoneAudioKey = String(
|
||||
msg.stabilityMilestoneAudioKey || ""
|
||||
).trim();
|
||||
|
||||
// WebSocket 管理器负责阈值判定并把同一语音 key 用于 ACK;这里按
|
||||
// 环数/最高跨越阈值的顺序一次入队。
|
||||
if (milestoneAudioKey) audioKeys.push(milestoneAudioKey);
|
||||
if (audioKeys.length > 0) {
|
||||
audioManager.play(audioKeys, false);
|
||||
}
|
||||
if (hasStabilityShot) {
|
||||
shotEffectToken.value += 1;
|
||||
}
|
||||
} else if (hasNewShot) {
|
||||
shotEffectToken.value += 1;
|
||||
}
|
||||
@@ -1579,9 +1746,19 @@ onBeforeUnmount(() => {
|
||||
:showBottom="isDistanceStage"
|
||||
:scroll="!isShootingStage"
|
||||
:onBack="exitPractice"
|
||||
:title="isShootingStage && isRhythmTraining ? '节奏训练' : ''"
|
||||
:title="
|
||||
isShootingStage
|
||||
? isRhythmTraining
|
||||
? '节奏训练'
|
||||
: isStabilityTraining
|
||||
? '稳定训练'
|
||||
: ''
|
||||
: ''
|
||||
"
|
||||
:titleStyle="
|
||||
isShootingStage && isRhythmTraining ? rhythmHeaderTitleStyle : {}
|
||||
isShootingStage && (isRhythmTraining || isStabilityTraining)
|
||||
? rhythmHeaderTitleStyle
|
||||
: {}
|
||||
"
|
||||
>
|
||||
<view class="practise-content">
|
||||
@@ -1604,10 +1781,14 @@ onBeforeUnmount(() => {
|
||||
:inShootWindow="rhythmInShootWindow"
|
||||
:serverTimestamp="rhythmCountdownTimestamp"
|
||||
:hitReq="rhythmHitReq"
|
||||
:energyPercent="stabilityEnergyPercent"
|
||||
:energyReqPercent="stabilityEnergyReqPercent"
|
||||
:isVip="isVip"
|
||||
:isSvip="isSvip"
|
||||
:externalShootResultAudio="
|
||||
trainingType === 'precision' || trainingType === 'rhythm'
|
||||
trainingType === 'precision' ||
|
||||
trainingType === 'rhythm' ||
|
||||
trainingType === 'stability'
|
||||
"
|
||||
:onStop="onTimeLimitReached"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user