update:优化新版个人训练
This commit is contained in:
@@ -400,7 +400,8 @@ function main() {
|
|||||||
throw new Error(`未知参数:${unknownArgs.join(", ")}`);
|
throw new Error(`未知参数:${unknownArgs.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = readFileSync(sourcePath, "utf8");
|
// 统一换行符,避免 Windows 的 CRLF 让未变更的协议产生不同哈希。
|
||||||
|
const source = readFileSync(sourcePath, "utf8").replace(/\r\n?/g, "\n");
|
||||||
const { generatedBlock, messageCount, fieldCount } = createGeneratedSource(source);
|
const { generatedBlock, messageCount, fieldCount } = createGeneratedSource(source);
|
||||||
const runtimeSource = readFileSync(runtimePath, "utf8");
|
const runtimeSource = readFileSync(runtimePath, "utf8");
|
||||||
const startIndex = runtimeSource.indexOf(generatedStartMarker);
|
const startIndex = runtimeSource.indexOf(generatedStartMarker);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
import AppBackground from "@/components/AppBackground.vue";
|
import AppBackground from "@/components/AppBackground.vue";
|
||||||
import Avatar from "@/components/Avatar.vue";
|
import Avatar from "@/components/Avatar.vue";
|
||||||
import BowTarget from "./BowTarget.vue";
|
import BowTarget from "./BowTarget.vue";
|
||||||
@@ -7,6 +8,8 @@ import useStore from "@/store";
|
|||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { user } = storeToRefs(store);
|
const { user } = storeToRefs(store);
|
||||||
|
const isSvip = computed(() => user.value.sVip === true);
|
||||||
|
const isVip = computed(() => user.value.vip === true && !isSvip.value);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
@@ -35,8 +38,20 @@ const props = defineProps({
|
|||||||
<view>
|
<view>
|
||||||
<Avatar :src="user.avatar" :rankLvl="user.rankLvl" :size="45" />
|
<Avatar :src="user.avatar" :rankLvl="user.rankLvl" :size="45" />
|
||||||
<view>
|
<view>
|
||||||
<text>{{ user.nickName }}</text>
|
<view
|
||||||
<!-- <text>{{ user.lvlName }}</text> -->
|
:class="[
|
||||||
|
'header-nickname',
|
||||||
|
'member-nickname',
|
||||||
|
isVip ? 'member-nickname--vip' : '',
|
||||||
|
isSvip ? 'member-nickname--svip' : '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<text class="member-nickname__text">{{ user.nickName }}</text>
|
||||||
|
<text v-if="isSvip" class="member-nickname__shine">
|
||||||
|
{{ user.nickName }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<text>{{ user.lvlName }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view @click="onClose">
|
<view @click="onClose">
|
||||||
@@ -95,9 +110,12 @@ const props = defineProps({
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
.header-nickname {
|
||||||
|
max-width: 240rpx;
|
||||||
|
}
|
||||||
.header > view:first-child > view:last-child > text:last-child {
|
.header > view:first-child > view:last-child > text:last-child {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
background-color: #5f51ff;
|
/* background-color: #5f51ff; */
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["shot-effect-complete"]);
|
||||||
|
|
||||||
const pMode = ref(true);
|
const pMode = ref(true);
|
||||||
const latestOne = ref(null);
|
const latestOne = ref(null);
|
||||||
const bluelatestOne = ref(null);
|
const bluelatestOne = ref(null);
|
||||||
@@ -248,16 +250,25 @@ function triggerShotEffect(shot, index) {
|
|||||||
clearTipTimer();
|
clearTipTimer();
|
||||||
latestOne.value = null;
|
latestOne.value = null;
|
||||||
hiddenLatestKey.value = key;
|
hiddenLatestKey.value = key;
|
||||||
shotEffect.value = { key, shot };
|
shotEffect.value = {
|
||||||
|
key,
|
||||||
|
shot,
|
||||||
|
token: props.shotEffectToken,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function completeShotEffect(key) {
|
function completeShotEffect(key) {
|
||||||
if (!shotEffect.value || shotEffect.value.key !== key) return;
|
if (!shotEffect.value || shotEffect.value.key !== key) return;
|
||||||
|
|
||||||
const shot = shotEffect.value.shot;
|
const completedEffect = shotEffect.value;
|
||||||
|
const shot = completedEffect.shot;
|
||||||
hiddenLatestKey.value = "";
|
hiddenLatestKey.value = "";
|
||||||
shotEffect.value = null;
|
shotEffect.value = null;
|
||||||
showShotTip(shot);
|
showShotTip(shot);
|
||||||
|
emit("shot-effect-complete", {
|
||||||
|
key,
|
||||||
|
token: completedEffect.token,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldHideLatestHit(index) {
|
function shouldHideLatestHit(index) {
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: "precision",
|
default: "precision",
|
||||||
},
|
},
|
||||||
|
isVip: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isSvip: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
externalShootResultAudio: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
currentRound: {
|
currentRound: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
@@ -195,6 +207,8 @@ async function onReceiveMessage(msg) {
|
|||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
audioManager.play("比赛结束", false);
|
audioManager.play("比赛结束", false);
|
||||||
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||||
|
// 精准训练由页面统一等待语音和飞箭结束,其他训练保持原播放链路。
|
||||||
|
if (props.externalShootResultAudio) return;
|
||||||
const latestDetail =
|
const latestDetail =
|
||||||
Array.isArray(msg.details) && msg.details.length > 0
|
Array.isArray(msg.details) && msg.details.length > 0
|
||||||
? msg.details[msg.details.length - 1]
|
? msg.details[msg.details.length - 1]
|
||||||
@@ -261,7 +275,19 @@ onBeforeUnmount(() => {
|
|||||||
image-mode="aspectFill"
|
image-mode="aspectFill"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
<text class="progress-card__name">{{ displayName }}</text>
|
<view
|
||||||
|
:class="[
|
||||||
|
'progress-card__name',
|
||||||
|
'member-nickname',
|
||||||
|
isVip && !isSvip ? 'member-nickname--vip' : '',
|
||||||
|
isSvip ? 'member-nickname--svip' : '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<text class="member-nickname__text">{{ displayName }}</text>
|
||||||
|
<text v-if="isSvip" class="member-nickname__shine">
|
||||||
|
{{ displayName }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- <button class="progress-card__sound" hover-class="none" @click="updateSound">
|
<!-- <button class="progress-card__sound" hover-class="none" @click="updateSound">
|
||||||
@@ -338,9 +364,10 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
.progress-card__name {
|
.progress-card__name {
|
||||||
width: 86rpx;
|
width: 86rpx;
|
||||||
color: #E7BA80;
|
color: #fff;
|
||||||
font-size: 18rpx;
|
font-size: 18rpx;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
justify-content: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const trainingModeIconMap = {
|
|||||||
};
|
};
|
||||||
// 训练项目卡片标题图,按接口 id 映射 CDN 资源。
|
// 训练项目卡片标题图,按接口 id 映射 CDN 资源。
|
||||||
const trainingModeTitleImageMap = {
|
const trainingModeTitleImageMap = {
|
||||||
|
base:
|
||||||
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/jichuxunlian.png",
|
||||||
endurance:
|
endurance:
|
||||||
"https://static.shelingxingqiu.com/shootmini/static/training-home/nailixunlian.png",
|
"https://static.shelingxingqiu.com/shootmini/static/training-home/nailixunlian.png",
|
||||||
precision:
|
precision:
|
||||||
@@ -515,7 +517,6 @@ onShow(async () => {
|
|||||||
/>
|
/>
|
||||||
<view class="featured-card-mask"></view>
|
<view class="featured-card-mask"></view>
|
||||||
<view class="featured-card-copy">
|
<view class="featured-card-copy">
|
||||||
<text class="featured-card-title">常规训练</text>
|
|
||||||
<text class="featured-card-subtitle">12箭练习</text>
|
<text class="featured-card-subtitle">12箭练习</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -857,13 +858,12 @@ onShow(async () => {
|
|||||||
top: 0;
|
top: 0;
|
||||||
width: 278rpx;
|
width: 278rpx;
|
||||||
height: 150rpx;
|
height: 150rpx;
|
||||||
background: linear-gradient(90deg, #ffdaa0 0%, #f5c580 74%, rgba(245, 197, 128, 0) 100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.featured-card-copy {
|
.featured-card-copy {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 30rpx;
|
left: 174rpx;
|
||||||
top: 34rpx;
|
top: 58rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -883,7 +883,6 @@ onShow(async () => {
|
|||||||
color: #895409;
|
color: #895409;
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
line-height: 32rpx;
|
line-height: 32rpx;
|
||||||
opacity: 0.72;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-grid {
|
.mode-grid {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
MATCH_WS_STATE_EVENT,
|
MATCH_WS_STATE_EVENT,
|
||||||
} from "@/matchWebsocket";
|
} from "@/matchWebsocket";
|
||||||
import { sharePractiseData } from "@/canvas";
|
import { sharePractiseData } from "@/canvas";
|
||||||
import { wxShare, debounce } from "@/util";
|
import { wxShare, debounce, getDirectionText } from "@/util";
|
||||||
import { MESSAGETYPESV2, roundsName } from "@/constants";
|
import { MESSAGETYPESV2, roundsName } from "@/constants";
|
||||||
|
|
||||||
import useStore from "@/store";
|
import useStore from "@/store";
|
||||||
@@ -56,6 +56,11 @@ const tips = ref("");
|
|||||||
const targetType = ref(defaultTargetType);
|
const targetType = ref(defaultTargetType);
|
||||||
const trainingParams = ref({});
|
const trainingParams = ref({});
|
||||||
const practiceInfo = ref({});
|
const practiceInfo = ref({});
|
||||||
|
// 服务端状态立即落到 practiceInfo,精准训练的目标区域单独延迟展示。
|
||||||
|
const visiblePrecisionTarget = ref({
|
||||||
|
randomBlock: 0,
|
||||||
|
randomRingArea: 0,
|
||||||
|
});
|
||||||
const trainingDifficultyStorageKey = "training-selection";
|
const trainingDifficultyStorageKey = "training-selection";
|
||||||
const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
|
const trainingDifficultyRefreshEvent = "training-difficulty-refresh";
|
||||||
const useHighlightTest = ref(false);
|
const useHighlightTest = ref(false);
|
||||||
@@ -75,7 +80,14 @@ const connectionClosed = ref(true);
|
|||||||
let stopPracticeTask = null;
|
let stopPracticeTask = null;
|
||||||
let practiceSyncTimer = null;
|
let practiceSyncTimer = null;
|
||||||
let waitingPracticeSync = false;
|
let waitingPracticeSync = false;
|
||||||
|
let shotPresentationGeneration = 0;
|
||||||
|
const audioWaiters = new Set();
|
||||||
|
const shotEffectWaiters = new Map();
|
||||||
const PRACTICE_SYNC_TIMEOUT_MS = 5000;
|
const PRACTICE_SYNC_TIMEOUT_MS = 5000;
|
||||||
|
const SHOT_EFFECT_WAIT_TIMEOUT_MS = 1200;
|
||||||
|
const AUDIO_TIMEOUT_BASE = 3500;
|
||||||
|
const AUDIO_TIMEOUT_PER_KEY = 2600;
|
||||||
|
const AUDIO_TIMEOUT_MAX = 12000;
|
||||||
|
|
||||||
const env = computed(() => {
|
const env = computed(() => {
|
||||||
try {
|
try {
|
||||||
@@ -92,6 +104,9 @@ const showResult = computed(
|
|||||||
() => pageStage.value === pageStages.RESULT && hasPractiseResult.value
|
() => pageStage.value === pageStages.RESULT && hasPractiseResult.value
|
||||||
);
|
);
|
||||||
const isSvip = computed(() => practiceInfo.value.sVip === true);
|
const isSvip = computed(() => practiceInfo.value.sVip === true);
|
||||||
|
const isVip = computed(
|
||||||
|
() => practiceInfo.value.vip === true && !isSvip.value
|
||||||
|
);
|
||||||
|
|
||||||
const trainingType = computed(
|
const trainingType = computed(
|
||||||
() => practiceInfo.value.trainingType || trainingParams.value.type || ""
|
() => practiceInfo.value.trainingType || trainingParams.value.type || ""
|
||||||
@@ -108,6 +123,15 @@ const getPositiveInteger = (value) => {
|
|||||||
return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0;
|
return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPrecisionTargetSnapshot = (source = {}) => ({
|
||||||
|
randomBlock: getPositiveInteger(source.randomBlock),
|
||||||
|
randomRingArea: getPositiveInteger(source.randomRingArea),
|
||||||
|
});
|
||||||
|
|
||||||
|
const applyVisiblePrecisionTarget = (source = {}) => {
|
||||||
|
visiblePrecisionTarget.value = getPrecisionTargetSnapshot(source);
|
||||||
|
};
|
||||||
|
|
||||||
const currentDifficultyLevel = computed(
|
const currentDifficultyLevel = computed(
|
||||||
() =>
|
() =>
|
||||||
getPositiveInteger(practiseResult.value.difficultyLevel) ||
|
getPositiveInteger(practiseResult.value.difficultyLevel) ||
|
||||||
@@ -136,7 +160,7 @@ const precisionRandomBlock = computed(() => {
|
|||||||
const block = getPositiveInteger(
|
const block = getPositiveInteger(
|
||||||
useHighlightTest.value
|
useHighlightTest.value
|
||||||
? highlightTestState.value.randomBlock
|
? highlightTestState.value.randomBlock
|
||||||
: practiceInfo.value.randomBlock
|
: visiblePrecisionTarget.value.randomBlock
|
||||||
);
|
);
|
||||||
return block <= precisionBlocks.value ? block : 0;
|
return block <= precisionBlocks.value ? block : 0;
|
||||||
});
|
});
|
||||||
@@ -145,7 +169,7 @@ const precisionRandomRingArea = computed(() => {
|
|||||||
const ring = getPositiveInteger(
|
const ring = getPositiveInteger(
|
||||||
useHighlightTest.value
|
useHighlightTest.value
|
||||||
? highlightTestState.value.randomRingArea
|
? highlightTestState.value.randomRingArea
|
||||||
: practiceInfo.value.randomRingArea
|
: visiblePrecisionTarget.value.randomRingArea
|
||||||
);
|
);
|
||||||
return ring >= 1 && ring <= 10 ? ring : 0;
|
return ring >= 1 && ring <= 10 ? ring : 0;
|
||||||
});
|
});
|
||||||
@@ -239,6 +263,7 @@ const practiceInfoFields = [
|
|||||||
"statusText",
|
"statusText",
|
||||||
"startTime",
|
"startTime",
|
||||||
"targetType",
|
"targetType",
|
||||||
|
"vip",
|
||||||
"sVip",
|
"sVip",
|
||||||
"trainingType",
|
"trainingType",
|
||||||
"difficultyLevel",
|
"difficultyLevel",
|
||||||
@@ -332,6 +357,143 @@ const syncPracticeInfo = (message = {}) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildShootResultAudioKeys = (message = {}) => {
|
||||||
|
const latestDetail =
|
||||||
|
Array.isArray(message.details) && message.details.length > 0
|
||||||
|
? message.details[message.details.length - 1]
|
||||||
|
: null;
|
||||||
|
// 与原 ShootProgress 保持一致:优先使用当前箭,details 仅作兼容兜底。
|
||||||
|
const arrow = message.shootData || latestDetail;
|
||||||
|
if (!arrow) return [];
|
||||||
|
if (
|
||||||
|
arrow.playerId !== undefined &&
|
||||||
|
arrow.playerId !== null &&
|
||||||
|
String(arrow.playerId) !== String(user.value?.id)
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = [
|
||||||
|
arrow.ring ? `${arrow.ringX ? "X" : arrow.ring}环` : "未上靶",
|
||||||
|
];
|
||||||
|
if (arrow.angle !== null && arrow.angle !== undefined) {
|
||||||
|
keys.push(`向${getDirectionText(arrow.angle)}调整`);
|
||||||
|
}
|
||||||
|
if (arrow.threeConsecutive10Rings === true) {
|
||||||
|
keys.push("tententen");
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
|
||||||
|
const playAudioKeysAndWait = (keys) => {
|
||||||
|
const audioKeys = (Array.isArray(keys) ? keys : [keys]).filter(Boolean);
|
||||||
|
if (audioKeys.length === 0) return Promise.resolve();
|
||||||
|
|
||||||
|
const expectedKey = audioKeys[audioKeys.length - 1];
|
||||||
|
const waitTime = Math.min(
|
||||||
|
AUDIO_TIMEOUT_MAX,
|
||||||
|
Math.max(AUDIO_TIMEOUT_BASE, audioKeys.length * AUDIO_TIMEOUT_PER_KEY)
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
let timer = null;
|
||||||
|
const waiter = {
|
||||||
|
expectedKey,
|
||||||
|
done: () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
audioWaiters.delete(waiter);
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
if (typeof audioManager.recoverIfStale === "function") {
|
||||||
|
audioManager.recoverIfStale(expectedKey);
|
||||||
|
}
|
||||||
|
waiter.done();
|
||||||
|
}, waitTime);
|
||||||
|
audioWaiters.add(waiter);
|
||||||
|
|
||||||
|
try {
|
||||||
|
audioManager.play(audioKeys, false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("training shoot result audio failed", error);
|
||||||
|
waiter.done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldWaitForShotEffect = (shot) => {
|
||||||
|
const x = Number(shot?.x);
|
||||||
|
const y = Number(shot?.y);
|
||||||
|
return (
|
||||||
|
isSvip.value &&
|
||||||
|
Number(shot?.ring) > 0 &&
|
||||||
|
Number.isFinite(x) &&
|
||||||
|
Number.isFinite(y)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const waitForShotEffect = (token, shouldWait) => {
|
||||||
|
if (!shouldWait) return Promise.resolve();
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
let timer = null;
|
||||||
|
const waiter = {
|
||||||
|
done: () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
if (shotEffectWaiters.get(token) === waiter) {
|
||||||
|
shotEffectWaiters.delete(token);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
timer = setTimeout(waiter.done, SHOT_EFFECT_WAIT_TIMEOUT_MS);
|
||||||
|
shotEffectWaiters.set(token, waiter);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const invalidateShotPresentations = ({ resetVisible = false } = {}) => {
|
||||||
|
shotPresentationGeneration += 1;
|
||||||
|
Array.from(audioWaiters).forEach((waiter) => waiter.done());
|
||||||
|
Array.from(shotEffectWaiters.values()).forEach((waiter) => waiter.done());
|
||||||
|
if (resetVisible) {
|
||||||
|
applyVisiblePrecisionTarget();
|
||||||
|
}
|
||||||
|
return shotPresentationGeneration;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onShotEffectComplete = (payload = {}) => {
|
||||||
|
const token = Number(payload?.token ?? payload);
|
||||||
|
if (!Number.isFinite(token)) return;
|
||||||
|
shotEffectWaiters.get(token)?.done();
|
||||||
|
};
|
||||||
|
|
||||||
|
const commitPrecisionTargetAfterPresentation = async ({
|
||||||
|
generation,
|
||||||
|
target,
|
||||||
|
audioPromise,
|
||||||
|
effectPromise,
|
||||||
|
}) => {
|
||||||
|
await Promise.all([audioPromise, effectPromise]);
|
||||||
|
if (
|
||||||
|
generation !== shotPresentationGeneration ||
|
||||||
|
practiceEnded.value ||
|
||||||
|
!isShootingStage.value ||
|
||||||
|
trainingType.value !== "precision"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
visiblePrecisionTarget.value = target;
|
||||||
|
};
|
||||||
|
|
||||||
const createPracticeEndSnapshot = (message = {}) => {
|
const createPracticeEndSnapshot = (message = {}) => {
|
||||||
const source = {
|
const source = {
|
||||||
...practiceInfo.value,
|
...practiceInfo.value,
|
||||||
@@ -436,11 +598,13 @@ const onPracticeInfoSync = (payload = {}) => {
|
|||||||
const shouldShowDistance =
|
const shouldShowDistance =
|
||||||
waitingPracticeSync && pageStage.value === pageStages.LOADING;
|
waitingPracticeSync && pageStage.value === pageStages.LOADING;
|
||||||
cancelPracticeSyncWait();
|
cancelPracticeSyncWait();
|
||||||
|
invalidateShotPresentations();
|
||||||
|
|
||||||
// 14 是完整快照,先清空旧值,避免 proto3 省略的 0 沿用上一份状态。
|
// 14 是完整快照,先清空旧值,避免 proto3 省略的 0 沿用上一份状态。
|
||||||
practiceInfo.value = {};
|
practiceInfo.value = {};
|
||||||
practiceEndSnapshot.value = {};
|
practiceEndSnapshot.value = {};
|
||||||
syncPracticeInfo(snapshot);
|
syncPracticeInfo(snapshot);
|
||||||
|
applyVisiblePrecisionTarget(practiceInfo.value);
|
||||||
scores.value = Array.isArray(snapshot.details) ? snapshot.details : [];
|
scores.value = Array.isArray(snapshot.details) ? snapshot.details : [];
|
||||||
|
|
||||||
if (shouldShowDistance) {
|
if (shouldShowDistance) {
|
||||||
@@ -667,6 +831,7 @@ const onReady = async () => {
|
|||||||
clearHighlightTestTimer();
|
clearHighlightTestTimer();
|
||||||
useHighlightTest.value = false;
|
useHighlightTest.value = false;
|
||||||
practiceEndSnapshot.value = {};
|
practiceEndSnapshot.value = {};
|
||||||
|
invalidateShotPresentations();
|
||||||
try {
|
try {
|
||||||
await startPractiseAPI(practiseId.value);
|
await startPractiseAPI(practiseId.value);
|
||||||
practiseResult.value = {};
|
practiseResult.value = {};
|
||||||
@@ -694,6 +859,7 @@ const enterPracticeResult = (result = {}) => {
|
|||||||
|
|
||||||
// 正常结算不调用 stop,只清理上下文并断开比赛服连接。
|
// 正常结算不调用 stop,只清理上下文并断开比赛服连接。
|
||||||
practiceEnded.value = true;
|
practiceEnded.value = true;
|
||||||
|
invalidateShotPresentations();
|
||||||
clearPracticeRuntimeContext();
|
clearPracticeRuntimeContext();
|
||||||
closePracticeConnection("training-practice-result");
|
closePracticeConnection("training-practice-result");
|
||||||
pageStage.value = pageStages.RESULT;
|
pageStage.value = pageStages.RESULT;
|
||||||
@@ -724,15 +890,47 @@ const onOver = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function onReceiveMessage(msg) {
|
async function onReceiveMessage(msg) {
|
||||||
|
const previousScoreLength = scores.value.length;
|
||||||
syncPracticeInfo(msg);
|
syncPracticeInfo(msg);
|
||||||
|
|
||||||
if (msg.type === MESSAGETYPESV2.ShootResult && isShootingStage.value) {
|
if (msg.type === MESSAGETYPESV2.BattleStart) {
|
||||||
|
invalidateShotPresentations();
|
||||||
|
applyVisiblePrecisionTarget(practiceInfo.value);
|
||||||
|
} else if (
|
||||||
|
msg.type === MESSAGETYPESV2.ShootResult &&
|
||||||
|
isShootingStage.value
|
||||||
|
) {
|
||||||
|
let hasNewShot = false;
|
||||||
|
let latestShot = null;
|
||||||
if (Array.isArray(msg.details)) {
|
if (Array.isArray(msg.details)) {
|
||||||
const previousScoreLength = scores.value.length;
|
|
||||||
scores.value = msg.details;
|
scores.value = msg.details;
|
||||||
if (msg.details.length === previousScoreLength + 1) {
|
hasNewShot = msg.details.length === previousScoreLength + 1;
|
||||||
shotEffectToken.value += 1;
|
latestShot = hasNewShot ? msg.details[msg.details.length - 1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trainingType.value === "precision") {
|
||||||
|
const generation = invalidateShotPresentations();
|
||||||
|
const target = getPrecisionTargetSnapshot(practiceInfo.value);
|
||||||
|
const nextEffectToken = hasNewShot ? shotEffectToken.value + 1 : 0;
|
||||||
|
const effectPromise = waitForShotEffect(
|
||||||
|
nextEffectToken,
|
||||||
|
hasNewShot && shouldWaitForShotEffect(latestShot)
|
||||||
|
);
|
||||||
|
const audioPromise = playAudioKeysAndWait(
|
||||||
|
buildShootResultAudioKeys(msg)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasNewShot) {
|
||||||
|
shotEffectToken.value = nextEffectToken;
|
||||||
}
|
}
|
||||||
|
void commitPrecisionTargetAfterPresentation({
|
||||||
|
generation,
|
||||||
|
target,
|
||||||
|
audioPromise,
|
||||||
|
effectPromise,
|
||||||
|
});
|
||||||
|
} else if (hasNewShot) {
|
||||||
|
shotEffectToken.value += 1;
|
||||||
}
|
}
|
||||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||||
practiceEndSnapshot.value = createPracticeEndSnapshot(msg);
|
practiceEndSnapshot.value = createPracticeEndSnapshot(msg);
|
||||||
@@ -747,6 +945,7 @@ async function onReceiveMessage(msg) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
practiceEnded.value = true;
|
practiceEnded.value = true;
|
||||||
|
invalidateShotPresentations();
|
||||||
clearPracticeRuntimeContext();
|
clearPracticeRuntimeContext();
|
||||||
// setTimeout(onOver, 1500);
|
// setTimeout(onOver, 1500);
|
||||||
}
|
}
|
||||||
@@ -756,6 +955,7 @@ function onComplete() {
|
|||||||
pageStage.value = pageStages.LOADING;
|
pageStage.value = pageStages.LOADING;
|
||||||
start.value = false;
|
start.value = false;
|
||||||
practiceEnded.value = true;
|
practiceEnded.value = true;
|
||||||
|
invalidateShotPresentations();
|
||||||
clearPracticeRuntimeContext();
|
clearPracticeRuntimeContext();
|
||||||
closePracticeConnection("training-practice-complete");
|
closePracticeConnection("training-practice-complete");
|
||||||
uni.$emit(trainingDifficultyRefreshEvent);
|
uni.$emit(trainingDifficultyRefreshEvent);
|
||||||
@@ -771,6 +971,7 @@ async function onRetry() {
|
|||||||
practiseResult.value = {};
|
practiseResult.value = {};
|
||||||
practiceEndSnapshot.value = {};
|
practiceEndSnapshot.value = {};
|
||||||
practiceInfo.value = {};
|
practiceInfo.value = {};
|
||||||
|
invalidateShotPresentations({ resetVisible: true });
|
||||||
start.value = false;
|
start.value = false;
|
||||||
scores.value = [];
|
scores.value = [];
|
||||||
shotEffectToken.value = 0;
|
shotEffectToken.value = 0;
|
||||||
@@ -788,9 +989,12 @@ const onClickShare = debounce(async () => {
|
|||||||
await wxShare("shareCanvas");
|
await wxShare("shareCanvas");
|
||||||
});
|
});
|
||||||
|
|
||||||
function onAudioEnded(s) {
|
function onAudioEnded(key) {
|
||||||
if (s.indexOf("比赛结束") >= 0) {
|
Array.from(audioWaiters).forEach((waiter) => {
|
||||||
onOver()
|
if (waiter.expectedKey === key) waiter.done();
|
||||||
|
});
|
||||||
|
if (String(key || "").includes("比赛结束")) {
|
||||||
|
void onOver();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -802,6 +1006,7 @@ const updateSound = () => {
|
|||||||
const exitPractice = async () => {
|
const exitPractice = async () => {
|
||||||
if (exiting.value) return;
|
if (exiting.value) return;
|
||||||
exiting.value = true;
|
exiting.value = true;
|
||||||
|
invalidateShotPresentations();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await stopCurrentPractice();
|
await stopCurrentPractice();
|
||||||
@@ -813,6 +1018,7 @@ const exitPractice = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onHide(() => {
|
onHide(() => {
|
||||||
|
invalidateShotPresentations();
|
||||||
// 小程序被切到后台时尽早通知后端,作为杀进程前的尽力兜底。
|
// 小程序被切到后台时尽早通知后端,作为杀进程前的尽力兜底。
|
||||||
if (
|
if (
|
||||||
!exiting.value &&
|
!exiting.value &&
|
||||||
@@ -842,6 +1048,7 @@ onShow(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnload(() => {
|
onUnload(() => {
|
||||||
|
invalidateShotPresentations();
|
||||||
clearPracticeRuntimeContext();
|
clearPracticeRuntimeContext();
|
||||||
void stopCurrentPractice();
|
void stopCurrentPractice();
|
||||||
closePracticeConnection("training-practice-unload");
|
closePracticeConnection("training-practice-unload");
|
||||||
@@ -869,6 +1076,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
invalidateShotPresentations();
|
||||||
clearPracticeRuntimeContext();
|
clearPracticeRuntimeContext();
|
||||||
void stopCurrentPractice();
|
void stopCurrentPractice();
|
||||||
uni.setKeepScreenOn({
|
uni.setKeepScreenOn({
|
||||||
@@ -904,6 +1112,9 @@ onBeforeUnmount(() => {
|
|||||||
:total="timeLimit"
|
:total="timeLimit"
|
||||||
:countdownEnabled="hasTimeLimit"
|
:countdownEnabled="hasTimeLimit"
|
||||||
:trainingType="trainingType"
|
:trainingType="trainingType"
|
||||||
|
:isVip="isVip"
|
||||||
|
:isSvip="isSvip"
|
||||||
|
:externalShootResultAudio="trainingType === 'precision'"
|
||||||
:onStop="onTimeLimitReached"
|
:onStop="onTimeLimitReached"
|
||||||
/>
|
/>
|
||||||
<view class="user-row">
|
<view class="user-row">
|
||||||
@@ -925,8 +1136,9 @@ onBeforeUnmount(() => {
|
|||||||
:activeSector="precisionRandomBlock"
|
:activeSector="precisionRandomBlock"
|
||||||
:activeRing="precisionRandomRingArea"
|
:activeRing="precisionRandomRingArea"
|
||||||
:showSectorLabels="precisionBlocks > 0"
|
:showSectorLabels="precisionBlocks > 0"
|
||||||
|
@shot-effect-complete="onShotEffectComplete"
|
||||||
/>
|
/>
|
||||||
<view v-if="env !== 'release'" class="highlight-test-actions">
|
<!-- <view v-if="env !== 'release'" class="highlight-test-actions">
|
||||||
<button
|
<button
|
||||||
class="highlight-test-btn"
|
class="highlight-test-btn"
|
||||||
hover-class="none"
|
hover-class="none"
|
||||||
@@ -941,7 +1153,7 @@ onBeforeUnmount(() => {
|
|||||||
>
|
>
|
||||||
重置高亮
|
重置高亮
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view> -->
|
||||||
<view class="sound-text-box">
|
<view class="sound-text-box">
|
||||||
<button class="sound-btn" hover-class="none" @click="updateSound">
|
<button class="sound-btn" hover-class="none" @click="updateSound">
|
||||||
<image
|
<image
|
||||||
|
|||||||
Reference in New Issue
Block a user