From 96b64e41878a2dd0e7cdc0a86545fd2db5ffe8f2 Mon Sep 17 00:00:00 2001
From: zhangyibo95 <690096405@qq.com>
Date: Wed, 26 Aug 2026 15:23:02 +0800
Subject: [PATCH] =?UTF-8?q?update=EF=BC=9A=E6=96=B0=E5=A2=9E=E7=A8=B3?=
=?UTF-8?q?=E5=AE=9A=E8=AE=AD=E7=BB=83?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/apis.js | 4 +-
src/audioManager.js | 22 ++
src/components/TrainingScorePanel.vue | 5 +-
src/matchWebsocket.js | 194 ++++++++++++++++++
src/pages/mine-bow-data.vue | 1 +
src/pages/my-growth.vue | 1 +
src/pages/training/components/ScorePanel.vue | 4 +-
src/pages/training/components/ScoreResult.vue | 34 ++-
.../training/components/ShootProgress.vue | 114 +++++++++-
.../TrainingDifficultyPreviewCard.vue | 28 ++-
src/pages/training/difficulty.vue | 58 +++++-
src/pages/training/index.vue | 4 +-
src/pages/training/practise-one.vue | 189 ++++++++++++++++-
src/utils/match.min.js | 2 +-
src/utils/matchProtocol.js | 10 +-
15 files changed, 645 insertions(+), 25 deletions(-)
diff --git a/src/apis.js b/src/apis.js
index 7eb6abc..6bf5715 100644
--- a/src/apis.js
+++ b/src/apis.js
@@ -376,9 +376,11 @@ export const readyGameAPI = (battleId) => {
});
};
-export const simulShootAPI = (device_id, x, y) => {
+export const simulShootAPI = (device_id, x, y, targetType = 40) => {
const data = {
device_id,
+ // 模拟射箭仅支持 20cm、40cm 靶纸,未传或传入无效值时默认使用 40cm。
+ targetType: Number(targetType) === 20 ? 20 : 40,
};
if (x !== undefined && y !== undefined) {
data.x = x;
diff --git a/src/audioManager.js b/src/audioManager.js
index baeb623..fec9d59 100644
--- a/src/audioManager.js
+++ b/src/audioManager.js
@@ -1,12 +1,18 @@
export const AUDIO_INTERRUPTION_BEGIN_EVENT = "audio-interruption-begin";
export const AUDIO_INTERRUPTION_END_EVENT = "audio-interruption-end";
export const RHYTHM_SHOOT_WINDOW_AUDIO_KEY = "请射箭";
+export const STABILITY_START_AUDIO_KEY =
+ "请在计时结束时能量条达到标记区域";
+export const STABILITY_ENERGY_50_AUDIO_KEY = "加油!到达50%啦";
+export const STABILITY_ENERGY_70_AUDIO_KEY =
+ "70%啦!还差一点,胜利就在眼前";
const TRAINING_START_AUDIO_KEY_MAP = Object.freeze({
base: "请于计时前完成指定环数对应箭量",
endurance: "请于计时前完成累计环数和箭量",
precision: "请射箭命中高亮区域",
rhythm: "请在读条推进到标记区间内射箭",
+ stability: STABILITY_START_AUDIO_KEY,
});
export const getTrainingStartAudioKey = (trainingType, fallback = "") =>
@@ -41,6 +47,16 @@ export const getRhythmShotAudioKeys = (shootData) => {
return [ringAudioKey, shootData.ok === true ? "Perfect" : "miss"];
};
+export const getStabilityShotAudioKeys = (shootData) => {
+ if (!shootData || typeof shootData !== "object") return [];
+ const ring = Number(shootData.ring);
+ const ringAudioKey =
+ Number.isFinite(ring) && ring > 0
+ ? `${shootData.ringX ? "X" : ring}环`
+ : "未上靶";
+ return [ringAudioKey];
+};
+
export const audioFils = {
tententen: "https://static.shelingxingqiu.com/shootmini/static/audio/tententen.mp3",
点击按钮: "https://static.shelingxingqiu.com/shootmini/static/audio/%E7%82%B9%E5%87%BB%E6%8C%89%E9%92%AE.mp3",
@@ -159,6 +175,12 @@ export const audioFils = {
"https://static.shelingxingqiu.com/attachment/2025-11-13/de7kzzllq0futwynso.mp3",
练习开始:
"https://static.shelingxingqiu.com/attachment/2025-11-14/de88w0lmmt43nnfmoi.mp3",
+ [STABILITY_START_AUDIO_KEY]:
+ "https://static.shelingxingqiu.com/shootmini/static/audio0820/%E8%AF%B7%E5%9C%A8%E8%AE%A1%E6%97%B6%E7%BB%93%E6%9D%9F%E6%97%B6%E8%83%BD%E9%87%8F%E6%9D%A1%E8%BE%BE%E5%88%B0%E6%A0%87%E8%AE%B0%E5%8C%BA%E5%9F%9F.MP3",
+ [STABILITY_ENERGY_50_AUDIO_KEY]:
+ "https://static.shelingxingqiu.com/shootmini/static/audio0820/%E5%8A%A0%E6%B2%B9%EF%BC%81%E5%88%B0%E8%BE%BE50%25%E5%95%A6.MP3",
+ [STABILITY_ENERGY_70_AUDIO_KEY]:
+ "https://static.shelingxingqiu.com/shootmini/static/audio0820/70%25%E5%95%A6%EF%BC%81%E8%BF%98%E5%B7%AE%E4%B8%80%E7%82%B9%EF%BC%8C%E8%83%9C%E5%88%A9%E5%B0%B1%E5%9C%A8%E7%9C%BC%E5%89%8D.MP3",
请于计时前完成指定环数对应箭量:
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E8%AF%B7%E4%BA%8E%E8%AE%A1%E6%97%B6%E5%89%8D%E5%AE%8C%E6%88%90%E6%8C%87%E5%AE%9A%E7%8E%AF%E6%95%B0%E5%AF%B9%E5%BA%94%E7%AE%AD%E9%87%8F.MP3",
请于计时前完成累计环数和箭量:
diff --git a/src/components/TrainingScorePanel.vue b/src/components/TrainingScorePanel.vue
index b034ecc..90ac90f 100644
--- a/src/components/TrainingScorePanel.vue
+++ b/src/components/TrainingScorePanel.vue
@@ -24,7 +24,7 @@ const props = defineProps({
const getDisplayText = (arrow = {}) => {
if (!arrow) return "";
- if (!arrow.ring) return "0";
+ if (!arrow.ring) return props.trainingType === "stability" ? "-" : "0";
return arrow.ringX ? "X" : String(arrow.ring);
};
@@ -33,7 +33,8 @@ const isFailed = (arrow = {}) => {
if (
props.recordMode &&
props.trainingType !== "precision" &&
- props.trainingType !== "rhythm"
+ props.trainingType !== "rhythm" &&
+ props.trainingType !== "stability"
) {
return false;
}
diff --git a/src/matchWebsocket.js b/src/matchWebsocket.js
index 37d105b..09bb6fe 100644
--- a/src/matchWebsocket.js
+++ b/src/matchWebsocket.js
@@ -17,7 +17,10 @@ import {
import {
getPrecisionShotAudioKeys,
getRhythmShotAudioKeys,
+ getStabilityShotAudioKeys,
getTrainingStartAudioKey,
+ STABILITY_ENERGY_50_AUDIO_KEY,
+ STABILITY_ENERGY_70_AUDIO_KEY,
} from "@/audioManager";
import {
normalizeId,
@@ -220,6 +223,13 @@ function buildPracticeSyncMessage(message) {
if (currentContext && practiceInfo.trainingType) {
currentContext.trainingType = String(practiceInfo.trainingType).trim();
}
+ updateStabilityEnergyContext(practiceInfo, {
+ fullSnapshot: true,
+ detectCrossing: false,
+ });
+ updateStabilityShotContext(practiceInfo, {
+ detectShot: false,
+ });
const matchId = normalizeId(
pickField(message, "matchId", "match_id") ||
practiceInfo.id ||
@@ -378,12 +388,171 @@ function getShootResultAudioKeys(shootData) {
return keys;
}
+function normalizeScoreSlot(value) {
+ const scoreSlot = Number(value);
+ return Number.isFinite(scoreSlot) && scoreSlot > 0 ? scoreSlot : 0;
+}
+
+function clampEnergy(value, scoreSlot = 0) {
+ const energy = Number(value);
+ if (!Number.isFinite(energy)) return 0;
+ const maxEnergy = normalizeScoreSlot(scoreSlot);
+ return maxEnergy > 0
+ ? Math.min(maxEnergy, Math.max(0, energy))
+ : Math.max(0, energy);
+}
+
+function getEnergyPercent(energy, scoreSlot) {
+ const maxEnergy = normalizeScoreSlot(scoreSlot);
+ if (!maxEnergy) return 0;
+ return Math.min(100, (clampEnergy(energy, maxEnergy) / maxEnergy) * 100);
+}
+
+function getStabilityArrowCount(source = {}) {
+ const candidates = [
+ Array.isArray(source.details) ? source.details.length : NaN,
+ Number(source.currentArrows),
+ Number(source.totalArrows),
+ ].filter((value) => Number.isFinite(value) && value >= 0);
+ return candidates.length > 0 ? Math.max(...candidates) : null;
+}
+
+function updateStabilityShotContext(
+ source = {},
+ { detectShot = false } = {}
+) {
+ const trainingType = String(
+ source.trainingType || currentContext?.trainingType || ""
+ ).trim();
+ if (trainingType !== "stability" || !currentContext) return false;
+
+ const nextArrowCount = getStabilityArrowCount(source);
+ if (nextArrowCount === null) return false;
+
+ const storedArrowCount = Number(currentContext.stabilityArrowCount);
+ const previousArrowCount = Number.isFinite(storedArrowCount)
+ ? storedArrowCount
+ : 0;
+ const hasNewShot =
+ detectShot && nextArrowCount > previousArrowCount;
+
+ if (nextArrowCount >= previousArrowCount) {
+ currentContext.stabilityArrowCount = nextArrowCount;
+ }
+ return hasNewShot;
+}
+
+// 稳定训练的阈值语音必须和 ACK 使用同一份判定结果,避免页面播放了
+// 里程碑语音但比赛服提前收到 ACK。能量跌破阈值后再次向上跨越可重复触发。
+function updateStabilityEnergyContext(
+ source = {},
+ { fullSnapshot = false, detectCrossing = false } = {}
+) {
+ const trainingType = String(
+ source.trainingType || currentContext?.trainingType || ""
+ ).trim();
+ if (trainingType !== "stability" || !currentContext) return "";
+
+ const hasCurrentEnergy = Object.prototype.hasOwnProperty.call(
+ source,
+ "currentEnergy"
+ );
+ if (!hasCurrentEnergy && !fullSnapshot) return "";
+
+ const previousScoreSlot = normalizeScoreSlot(
+ currentContext.stabilityScoreSlot
+ );
+ const nextScoreSlot =
+ normalizeScoreSlot(source.scoreSlot) || previousScoreSlot;
+ const previousEnergy = clampEnergy(
+ currentContext.stabilityEnergy,
+ previousScoreSlot
+ );
+ const nextEnergy = clampEnergy(
+ hasCurrentEnergy ? source.currentEnergy : 0,
+ nextScoreSlot
+ );
+ const previousPercent = getEnergyPercent(
+ previousEnergy,
+ previousScoreSlot || nextScoreSlot
+ );
+ const nextPercent = getEnergyPercent(nextEnergy, nextScoreSlot);
+
+ currentContext.stabilityEnergy = nextEnergy;
+ currentContext.stabilityScoreSlot = nextScoreSlot;
+
+ if (!detectCrossing) {
+ currentContext.stabilityEnergy50Armed = nextPercent < 50;
+ currentContext.stabilityEnergy70Armed = nextPercent < 70;
+ return "";
+ }
+
+ let energy50Armed =
+ typeof currentContext.stabilityEnergy50Armed === "boolean"
+ ? currentContext.stabilityEnergy50Armed
+ : previousPercent < 50;
+ let energy70Armed =
+ typeof currentContext.stabilityEnergy70Armed === "boolean"
+ ? currentContext.stabilityEnergy70Armed
+ : previousPercent < 70;
+
+ if (nextPercent < 50) energy50Armed = true;
+ if (nextPercent < 70) energy70Armed = true;
+
+ let milestoneAudioKey = "";
+ if (energy70Armed && nextPercent >= 70) {
+ milestoneAudioKey = STABILITY_ENERGY_70_AUDIO_KEY;
+ energy70Armed = false;
+ // 同一次直接跨过两个阈值时只播70%,不能在后续补播50%。
+ energy50Armed = false;
+ } else if (energy50Armed && nextPercent >= 50) {
+ milestoneAudioKey = STABILITY_ENERGY_50_AUDIO_KEY;
+ energy50Armed = false;
+ }
+
+ currentContext.stabilityEnergy50Armed = energy50Armed;
+ currentContext.stabilityEnergy70Armed = energy70Armed;
+ return milestoneAudioKey;
+}
+
function getTestDistanceAudioKeys(shootData) {
if (!shootData) return [];
return [getDistanceCheckAudioKey(shootData)];
}
+function attachStabilityAudioContext(message, businessMessage) {
+ const fullStabilitySnapshot = [
+ ServerMessageType.SERVER_MSG_MATCH_START,
+ ServerMessageType.SERVER_MSG_SHOT,
+ ServerMessageType.SERVER_MSG_MATCH_END,
+ ServerMessageType.SERVER_MSG_TIMEOUT,
+ ServerMessageType.SERVER_MSG_PRACTICE_END,
+ ].includes(message.type);
+ const stabilityMilestoneAudioKey = updateStabilityEnergyContext(
+ businessMessage,
+ {
+ fullSnapshot: fullStabilitySnapshot,
+ detectCrossing: message.type === ServerMessageType.SERVER_MSG_SHOT,
+ }
+ );
+ const stabilityHasNewShot = updateStabilityShotContext(businessMessage, {
+ detectShot: message.type === ServerMessageType.SERVER_MSG_SHOT,
+ });
+ if (businessMessage && stabilityMilestoneAudioKey) {
+ businessMessage.stabilityMilestoneAudioKey = stabilityMilestoneAudioKey;
+ }
+ if (businessMessage) {
+ businessMessage.stabilityHasNewShot = stabilityHasNewShot;
+ }
+}
+
function getAckAudioKeys(message, businessMessage) {
+ const stabilityMilestoneAudioKey = String(
+ businessMessage?.stabilityMilestoneAudioKey || ""
+ ).trim();
+ const stabilityHasNewShot =
+ businessMessage?.stabilityHasNewShot === true;
+
switch (message.type) {
case ServerMessageType.SERVER_MSG_MATCH_START:
if (businessMessage?.isSecondHalfStart) return ["下半场开始"];
@@ -419,6 +588,14 @@ function getAckAudioKeys(message, businessMessage) {
if (trainingType === "rhythm") {
return getRhythmShotAudioKeys(businessMessage?.shootData);
}
+ if (trainingType === "stability") {
+ return [
+ ...(stabilityHasNewShot
+ ? getStabilityShotAudioKeys(businessMessage?.shootData)
+ : []),
+ stabilityMilestoneAudioKey,
+ ].filter(Boolean);
+ }
return getShootResultAudioKeys(businessMessage?.shootData);
case ServerMessageType.SERVER_MSG_MATCH_END:
return ["比赛结束"];
@@ -880,6 +1057,8 @@ function handleMessage(data) {
if (businessMessage?.matchId && currentContext) {
currentContext.matchId = businessMessage.matchId;
}
+ // 阈值事件必须先于 ACK 判断生成,确保无 sequence 的状态推送也能播放。
+ attachStabilityAudioContext(message, businessMessage);
queueAckAfterAudio(message, businessMessage);
emitBusinessMessage(businessMessage);
}
@@ -991,6 +1170,21 @@ export function connectMatchWebSocket(options = {}) {
trainingType:
normalizedTrainingType ||
(isSameContext ? currentContext?.trainingType || "" : ""),
+ stabilityEnergy: isSameContext
+ ? currentContext?.stabilityEnergy
+ : undefined,
+ stabilityScoreSlot: isSameContext
+ ? currentContext?.stabilityScoreSlot
+ : undefined,
+ stabilityEnergy50Armed: isSameContext
+ ? currentContext?.stabilityEnergy50Armed
+ : undefined,
+ stabilityEnergy70Armed: isSameContext
+ ? currentContext?.stabilityEnergy70Armed
+ : undefined,
+ stabilityArrowCount: isSameContext
+ ? currentContext?.stabilityArrowCount
+ : undefined,
meleeHalfRest: isSameContext
? currentContext?.meleeHalfRest === true
: false,
diff --git a/src/pages/mine-bow-data.vue b/src/pages/mine-bow-data.vue
index 1217819..92d6370 100644
--- a/src/pages/mine-bow-data.vue
+++ b/src/pages/mine-bow-data.vue
@@ -18,6 +18,7 @@ const trainingTypeNameMap = Object.freeze({
endurance: "耐力训练",
precision: "精准训练",
rhythm: "节奏训练",
+ stability: "稳定训练",
});
const trainingType = computed(() =>
diff --git a/src/pages/my-growth.vue b/src/pages/my-growth.vue
index 35f4e3d..7cd4b63 100644
--- a/src/pages/my-growth.vue
+++ b/src/pages/my-growth.vue
@@ -20,6 +20,7 @@ const trainingTypeNameMap = Object.freeze({
endurance: "耐力训练",
precision: "精准训练",
rhythm: "节奏训练",
+ stability: "稳定训练",
});
const getTrainingTypeName = (trainingType) => {
diff --git a/src/pages/training/components/ScorePanel.vue b/src/pages/training/components/ScorePanel.vue
index 9d81ab7..f995bd9 100644
--- a/src/pages/training/components/ScorePanel.vue
+++ b/src/pages/training/components/ScorePanel.vue
@@ -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;
diff --git a/src/pages/training/components/ScoreResult.vue b/src/pages/training/components/ScoreResult.vue
index 964b62c..4eeecc2 100644
--- a/src/pages/training/components/ScoreResult.vue
+++ b/src/pages/training/components/ScoreResult.vue
@@ -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(
--
-
+
消耗卡路里
diff --git a/src/pages/training/components/ShootProgress.vue b/src/pages/training/components/ShootProgress.vue
index ae01fa8..e2ce352 100644
--- a/src/pages/training/components/ShootProgress.vue
+++ b/src/pages/training/components/ShootProgress.vue
@@ -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(() => {
-
+
+ {{ remain }}秒
+
+
+
+
+ {{ Math.round(stabilityEnergyPercent) }}%
+
+
+
+
{{ rhythmTitle }}
{
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;
diff --git a/src/pages/training/components/TrainingDifficultyPreviewCard.vue b/src/pages/training/components/TrainingDifficultyPreviewCard.vue
index 9271672..534e072 100644
--- a/src/pages/training/components/TrainingDifficultyPreviewCard.vue
+++ b/src/pages/training/components/TrainingDifficultyPreviewCard.vue
@@ -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);
});
@@ -27,13 +40,16 @@ const previewLines = computed(() => {
{{ title }}
-
- {{ line }}
-
+ {{ part.text }}
+
@@ -86,6 +102,4 @@ const previewLines = computed(() => {
.difficulty-preview__line {
display: block;
}
-
-
diff --git a/src/pages/training/difficulty.vue b/src/pages/training/difficulty.vue
index 24b3ed7..a85d0e4 100644
--- a/src/pages/training/difficulty.vue
+++ b/src/pages/training/difficulty.vue
@@ -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,
diff --git a/src/pages/training/index.vue b/src/pages/training/index.vue
index 8db9420..8ad0d10 100644
--- a/src/pages/training/index.vue
+++ b/src/pages/training/index.vue
@@ -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:
diff --git a/src/pages/training/practise-one.vue b/src/pages/training/practise-one.vue
index 5e0dfc9..cf7f442 100644
--- a/src/pages/training/practise-one.vue
+++ b/src/pages/training/practise-one.vue
@@ -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
+ : {}
"
>
@@ -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"
/>
diff --git a/src/utils/match.min.js b/src/utils/match.min.js
index 96f5137..bccff43 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,"SERVER_MSG_SYNC_PRACTICE_INFO":14,"SERVER_MSG_SYNC_MATCH_INFO":15},"ClientMessageType":{"CLIENT_MSG_UNKNOWN":0,"CLIENT_MSG_HEARTBEAT_ACK":1,"CLIENT_MSG_SHOOT_DATA":2,"CLIENT_MSG_ACK":3,"CLIENT_MSG_LEAVE":4,"CLIENT_MSG_SYNC_PRACTICE_INFO":5,"CLIENT_MSG_SYNC_MATCH_INFO":6},"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},"three_consecutive_10_rings":{"type":"bool","id":9},"ok":{"type":"bool","id":10},"target_ok":{"type":"bool","id":11}}},"MatchShootList":{"fields":{"items":{"type":"MatchShoot","id":1,"rule":"repeated"}}},"RoundScore":{"fields":{"total_ring":{"type":"int32","id":1},"score":{"type":"int32","id":2},"if_win":{"type":"bool","id":3}}},"MatchRound":{"fields":{"shoots":{"type":"MatchShootList","id":1,"keytype":"int64"},"scores":{"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},"rank_lvl":{"type":"int32","id":16},"rank_name":{"type":"string","id":17},"rank_icon":{"type":"string","id":18}}},"TeamInfo":{"fields":{"players":{"type":"PlayerFull","id":1,"rule":"repeated"},"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":{"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},"three_consecutive_10_rings":{"type":"bool","id":9},"target_ok":{"type":"bool","id":10}}},"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":{"type":"MatchShoot","id":11,"rule":"repeated"},"training_type":{"type":"string","id":12},"difficulty_level":{"type":"int32","id":13},"hit_req":{"type":"int32","id":14},"arrows_left":{"type":"int32","id":15},"target_arrows":{"type":"int32","id":16},"target_rings":{"type":"int32","id":17},"current_arrows":{"type":"int32","id":18},"current_rings":{"type":"int32","id":19},"blocks":{"type":"int32","id":20},"random_block":{"type":"int32","id":21},"random_ring_area":{"type":"int32","id":22},"score_slot":{"type":"int32","id":44},"current_energy":{"type":"int32","id":45},"energy_cost_per_sec":{"type":"int32","id":46},"energy_per_hit":{"type":"int32","id":47},"energy_req_percent":{"type":"int32","id":48},"round_time":{"type":"int32","id":50},"shoot_time":{"type":"int32","id":53},"shoot_window_start":{"type":"int64","id":51},"in_shoot_window":{"type":"bool","id":52},"time_limit":{"type":"int32","id":23},"completed":{"type":"bool","id":24},"total_arrows":{"type":"int32","id":25},"duration":{"type":"int32","id":26},"average_ring":{"type":"float","id":27},"stability":{"type":"float","id":28},"max_combo":{"type":"int32","id":29},"total_hits":{"type":"int32","id":30},"delta_total_hits":{"type":"int32","id":31},"delta_duration":{"type":"int32","id":32},"delta_max_combo":{"type":"int32","id":33},"delta_total_rings":{"type":"int32","id":34},"delta_total_arrows":{"type":"int32","id":35},"delta_average_ring":{"type":"float","id":36},"delta_stability":{"type":"float","id":37},"delta_current_energy":{"type":"int32","id":49},"ten_ring_count":{"type":"int32","id":54},"delta_ten_ring_count":{"type":"int32","id":55},"before_exp":{"type":"int32","id":38},"before_level":{"type":"int32","id":39},"current_exp":{"type":"int32","id":40},"level":{"type":"int32","id":41},"upgrade_exp":{"type":"int32","id":42},"calories":{"type":"double","id":43}}},"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":{"type":"MatchRound","id":12,"rule":"repeated"},"teams":{"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":{"type":"PlayerMatchResult","id":20,"rule":"repeated"},"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},"shoot_data":{"type":"ShootData","id":5},"practice_info":{"type":"PracticeInfo","id":6},"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,"SERVER_MSG_SYNC_PRACTICE_INFO":14,"SERVER_MSG_SYNC_MATCH_INFO":15},"ClientMessageType":{"CLIENT_MSG_UNKNOWN":0,"CLIENT_MSG_HEARTBEAT_ACK":1,"CLIENT_MSG_SHOOT_DATA":2,"CLIENT_MSG_ACK":3,"CLIENT_MSG_LEAVE":4,"CLIENT_MSG_SYNC_PRACTICE_INFO":5,"CLIENT_MSG_SYNC_MATCH_INFO":6},"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},"three_consecutive_10_rings":{"type":"bool","id":9},"ok":{"type":"bool","id":10},"target_ok":{"type":"bool","id":11}}},"MatchShootList":{"fields":{"items":{"type":"MatchShoot","id":1,"rule":"repeated"}}},"RoundScore":{"fields":{"total_ring":{"type":"int32","id":1},"score":{"type":"int32","id":2},"if_win":{"type":"bool","id":3}}},"MatchRound":{"fields":{"shoots":{"type":"MatchShootList","id":1,"keytype":"int64"},"scores":{"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},"rank_lvl":{"type":"int32","id":16},"rank_name":{"type":"string","id":17},"rank_icon":{"type":"string","id":18}}},"TeamInfo":{"fields":{"players":{"type":"PlayerFull","id":1,"rule":"repeated"},"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":{"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},"three_consecutive_10_rings":{"type":"bool","id":9},"target_ok":{"type":"bool","id":10}}},"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":{"type":"MatchShoot","id":11,"rule":"repeated"},"training_type":{"type":"string","id":12},"difficulty_level":{"type":"int32","id":13},"hit_req":{"type":"int32","id":14},"arrows_left":{"type":"int32","id":15},"target_arrows":{"type":"int32","id":16},"target_rings":{"type":"int32","id":17},"current_arrows":{"type":"int32","id":18},"current_rings":{"type":"int32","id":19},"blocks":{"type":"int32","id":20},"random_block":{"type":"int32","id":21},"random_ring_area":{"type":"int32","id":22},"score_slot":{"type":"int32","id":44},"current_energy":{"type":"int32","id":45},"energy_cost_per_sec":{"type":"int32","id":46},"energy_per_hit":{"type":"int32","id":47},"energy_req_percent":{"type":"int32","id":48},"round_time":{"type":"int32","id":50},"shoot_time":{"type":"int32","id":53},"shoot_window_start":{"type":"int64","id":51},"in_shoot_window":{"type":"bool","id":52},"time_limit":{"type":"int32","id":23},"completed":{"type":"bool","id":24},"total_arrows":{"type":"int32","id":25},"duration":{"type":"int32","id":26},"average_ring":{"type":"float","id":27},"stability":{"type":"float","id":28},"max_combo":{"type":"int32","id":29},"total_hits":{"type":"int32","id":30},"delta_total_hits":{"type":"int32","id":31},"delta_duration":{"type":"int32","id":32},"delta_max_combo":{"type":"int32","id":33},"delta_total_rings":{"type":"int32","id":34},"delta_total_arrows":{"type":"int32","id":35},"delta_average_ring":{"type":"float","id":36},"delta_stability":{"type":"float","id":37},"delta_current_energy":{"type":"int32","id":49},"ten_ring_count":{"type":"int32","id":54},"delta_ten_ring_count":{"type":"int32","id":55},"max_energy_percent":{"type":"int32","id":56},"qualified_arrows":{"type":"int32","id":57},"qualified_rate":{"type":"int32","id":58},"delta_max_energy_percent":{"type":"int32","id":59},"delta_qualified_arrows":{"type":"int32","id":60},"delta_qualified_rate":{"type":"int32","id":61},"before_exp":{"type":"int32","id":38},"before_level":{"type":"int32","id":39},"current_exp":{"type":"int32","id":40},"level":{"type":"int32","id":41},"upgrade_exp":{"type":"int32","id":42},"calories":{"type":"double","id":43}}},"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":{"type":"MatchRound","id":12,"rule":"repeated"},"teams":{"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":{"type":"PlayerMatchResult","id":20,"rule":"repeated"},"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},"shoot_data":{"type":"ShootData","id":5},"practice_info":{"type":"PracticeInfo","id":6},"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 7f57cb8..a6921ff 100644
--- a/src/utils/matchProtocol.js
+++ b/src/utils/matchProtocol.js
@@ -7,8 +7,8 @@ const { Reader, Writer } = protobuf;
// 所以这里使用 minimal Reader/Writer 做静态字段解码和客户端消息编码。
//
// 此区块由 scripts/generate-match-schema.mjs 自动生成,请勿手动修改。
-// 来源:src/utils/match.min.js(sha256: fbf97fd3d08ae314)
-// 协议命名空间:rpc;消息数:12;字段数:155
+// 来源:src/utils/match.min.js(sha256: 0fe50dfa2644f750)
+// 协议命名空间:rpc;消息数:12;字段数:161
export const ServerMessageType = {
SERVER_MSG_UNKNOWN: 0,
@@ -198,6 +198,12 @@ const SCHEMAS = {
53: { name: "shoot_time", kind: "int32" },
54: { name: "ten_ring_count", kind: "int32" },
55: { name: "delta_ten_ring_count", kind: "int32" },
+ 56: { name: "max_energy_percent", kind: "int32" },
+ 57: { name: "qualified_arrows", kind: "int32" },
+ 58: { name: "qualified_rate", kind: "int32" },
+ 59: { name: "delta_max_energy_percent", kind: "int32" },
+ 60: { name: "delta_qualified_arrows", kind: "int32" },
+ 61: { name: "delta_qualified_rate", kind: "int32" },
},
MatchInfo: {
1: { name: "match_id", kind: "string" },