Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4abb0de1e7 | |||
| 7591058786 | |||
| 045827cb33 | |||
| 69ed5bda1b | |||
| 5bab30d2e4 | |||
| 8b8366f30f | |||
| 8b2ea24f38 | |||
| c30aa45e5b | |||
| 5cf243d187 | |||
| bfdd40ec93 |
10
src/App.vue
10
src/App.vue
@@ -22,7 +22,8 @@
|
||||
const {
|
||||
updateUser,
|
||||
updateOnline,
|
||||
clearSessionState
|
||||
clearSessionState,
|
||||
clearDevice
|
||||
} = store;
|
||||
|
||||
watch(
|
||||
@@ -63,6 +64,11 @@
|
||||
updateOnline(data.online);
|
||||
}
|
||||
|
||||
function onDeviceBindInvalid() {
|
||||
clearDevice();
|
||||
uni.setStorageSync("calibration", false);
|
||||
}
|
||||
|
||||
function onDeviceShoot() {
|
||||
// audioManager.play("射箭声音")
|
||||
}
|
||||
@@ -78,6 +84,7 @@
|
||||
uni.$on("update-user", emitUpdateUser);
|
||||
uni.$on("update-online", emitUpdateOnline);
|
||||
uni.$on("session-kicked-out", onSessionKickedOut);
|
||||
uni.$on("device-bind-invalid", onDeviceBindInvalid);
|
||||
const token = uni.getStorageSync(
|
||||
`${uni.getAccountInfoSync().miniProgram.envVersion}_token`
|
||||
);
|
||||
@@ -91,6 +98,7 @@
|
||||
uni.$off("update-user", emitUpdateUser);
|
||||
uni.$off("update-online", emitUpdateOnline);
|
||||
uni.$off("session-kicked-out", onSessionKickedOut);
|
||||
uni.$off("device-bind-invalid", onDeviceBindInvalid);
|
||||
websocket.closeWebSocket();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -70,6 +70,15 @@ function request(method, url, data = {}) {
|
||||
resolve({binded: true});
|
||||
return;
|
||||
}
|
||||
if (message === "BIND_FAILD") {
|
||||
uni.$emit("device-bind-invalid");
|
||||
uni.showToast({
|
||||
title: "设备绑定状态已失效,请重新绑定",
|
||||
icon: "none",
|
||||
});
|
||||
reject({type: "DEVICE_BIND_INVALID", message});
|
||||
return;
|
||||
}
|
||||
if (message === "ERROR_ORDER_UNPAY") {
|
||||
uni.showToast({
|
||||
title: "当前有未支付订单",
|
||||
|
||||
@@ -131,6 +131,10 @@ class AudioManager {
|
||||
this.lastPlayKey = null;
|
||||
this.lastPlayAt = 0;
|
||||
this.isInterrupted = false;
|
||||
this.interruptedAt = 0;
|
||||
this.interruptionFallbackMs = 5000;
|
||||
this.playWatchdogMs = 8000;
|
||||
this.playWatchdogTimers = new Map();
|
||||
|
||||
// 静音开关
|
||||
this.isMuted = false;
|
||||
@@ -157,6 +161,7 @@ class AudioManager {
|
||||
const begin = () => {
|
||||
if (this.isInterrupted) return;
|
||||
this.isInterrupted = true;
|
||||
this.interruptedAt = Date.now();
|
||||
this.stopAll();
|
||||
this.isSequenceRunning = false;
|
||||
this.sequenceQueue = [];
|
||||
@@ -168,6 +173,7 @@ class AudioManager {
|
||||
const end = () => {
|
||||
if (!this.isInterrupted) return;
|
||||
this.isInterrupted = false;
|
||||
this.interruptedAt = 0;
|
||||
uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
|
||||
void this.reloadAll();
|
||||
};
|
||||
@@ -350,9 +356,14 @@ class AudioManager {
|
||||
const loadTimeout = setTimeout(() => {
|
||||
debugLog(`音频 ${key} 加载超时`);
|
||||
this.recordLoadFailure(key);
|
||||
this.audioMap.delete(key);
|
||||
try {
|
||||
audio.destroy();
|
||||
} catch (_) {}
|
||||
this.finishPlayback(key, {
|
||||
advanceSequence: true,
|
||||
emitEnded: true,
|
||||
});
|
||||
if (callback) callback();
|
||||
}, 10000);
|
||||
|
||||
@@ -386,7 +397,13 @@ class AudioManager {
|
||||
}
|
||||
this.recordLoadFailure(key);
|
||||
this.audioMap.delete(key);
|
||||
audio.destroy();
|
||||
try {
|
||||
audio.destroy();
|
||||
} catch (_) {}
|
||||
this.finishPlayback(key, {
|
||||
advanceSequence: true,
|
||||
emitEnded: true,
|
||||
});
|
||||
if (this.readyMap.get(key)) {
|
||||
// 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载
|
||||
// this.readyMap.set(key, false);
|
||||
@@ -396,19 +413,14 @@ class AudioManager {
|
||||
});
|
||||
|
||||
audio.onEnded(() => {
|
||||
if (this.currentPlayingKey === key) {
|
||||
this.currentPlayingKey = null;
|
||||
}
|
||||
this.allowPlayMap.set(key, false);
|
||||
this.onAudioEnded(key);
|
||||
uni.$emit('audioEnded', key);
|
||||
this.finishPlayback(key, {
|
||||
advanceSequence: true,
|
||||
emitEnded: true,
|
||||
});
|
||||
});
|
||||
|
||||
audio.onStop(() => {
|
||||
if (this.currentPlayingKey === key) {
|
||||
this.currentPlayingKey = null;
|
||||
}
|
||||
this.allowPlayMap.set(key, false);
|
||||
this.finishPlayback(key);
|
||||
});
|
||||
|
||||
this.audioMap.set(key, audio);
|
||||
@@ -446,11 +458,19 @@ class AudioManager {
|
||||
});
|
||||
} else {
|
||||
this.recordLoadFailure(key);
|
||||
this.finishPlayback(key, {
|
||||
advanceSequence: true,
|
||||
emitEnded: true,
|
||||
});
|
||||
if (callback) callback();
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
this.recordLoadFailure(key);
|
||||
this.finishPlayback(key, {
|
||||
advanceSequence: true,
|
||||
emitEnded: true,
|
||||
});
|
||||
if (callback) callback();
|
||||
},
|
||||
});
|
||||
@@ -487,15 +507,137 @@ class AudioManager {
|
||||
this.failedLoadKeys.add(key);
|
||||
}
|
||||
|
||||
clearPlayWatchdog(key) {
|
||||
const timer = this.playWatchdogTimers.get(key);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
this.playWatchdogTimers.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
clearAllPlayWatchdogs() {
|
||||
for (const timer of this.playWatchdogTimers.values()) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
this.playWatchdogTimers.clear();
|
||||
}
|
||||
|
||||
startPlayWatchdog(key) {
|
||||
this.clearPlayWatchdog(key);
|
||||
const timer = setTimeout(() => {
|
||||
if (this.currentPlayingKey !== key) return;
|
||||
debugLog(`音频 ${key} 播放超时,跳过当前音频并继续队列`);
|
||||
this.finishPlayback(key, {
|
||||
advanceSequence: true,
|
||||
emitEnded: true,
|
||||
force: true,
|
||||
});
|
||||
this.reloadAudioKey(key);
|
||||
}, this.playWatchdogMs);
|
||||
this.playWatchdogTimers.set(key, timer);
|
||||
}
|
||||
|
||||
finishPlayback(key, { advanceSequence = false, emitEnded = false, force = false } = {}) {
|
||||
const wasCurrent = this.currentPlayingKey === key;
|
||||
const isSequenceCurrent =
|
||||
this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key;
|
||||
|
||||
this.clearPlayWatchdog(key);
|
||||
this.allowPlayMap.set(key, false);
|
||||
|
||||
if (!force && !wasCurrent && !isSequenceCurrent) return false;
|
||||
|
||||
if (wasCurrent) {
|
||||
this.currentPlayingKey = null;
|
||||
}
|
||||
|
||||
if (advanceSequence && isSequenceCurrent) {
|
||||
this.onAudioEnded(key);
|
||||
}
|
||||
|
||||
if (emitEnded) {
|
||||
uni.$emit("audioEnded", key);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
recoverFromInterruptionIfStale(force = false) {
|
||||
if (!this.isInterrupted) return false;
|
||||
const interruptedFor = Date.now() - (this.interruptedAt || Date.now());
|
||||
if (!force && interruptedFor < this.interruptionFallbackMs) return false;
|
||||
|
||||
debugLog("音频中断状态超时,执行兜底恢复");
|
||||
this.isInterrupted = false;
|
||||
this.interruptedAt = 0;
|
||||
uni.$emit(AUDIO_INTERRUPTION_END_EVENT);
|
||||
void this.reloadAll();
|
||||
return true;
|
||||
}
|
||||
|
||||
recoverIfStale(expectedKey) {
|
||||
if (this.recoverFromInterruptionIfStale(true)) return;
|
||||
|
||||
const key =
|
||||
expectedKey || this.currentPlayingKey || this.sequenceQueue[this.sequenceIndex];
|
||||
if (!key) {
|
||||
if (this.isSequenceRunning) {
|
||||
this.sequenceQueue = [];
|
||||
this.sequenceIndex = 0;
|
||||
this.isSequenceRunning = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isStaleCurrent =
|
||||
this.currentPlayingKey === key ||
|
||||
(this.isSequenceRunning && this.sequenceQueue[this.sequenceIndex] === key);
|
||||
if (!isStaleCurrent) return;
|
||||
|
||||
debugLog(`音频 ${key} 等待超时,执行轻量恢复`);
|
||||
const audio = this.audioMap.get(key);
|
||||
if (audio) {
|
||||
try {
|
||||
audio.stop();
|
||||
} catch (_) {}
|
||||
}
|
||||
this.finishPlayback(key, {
|
||||
advanceSequence: true,
|
||||
emitEnded: true,
|
||||
force: true,
|
||||
});
|
||||
this.reloadAudioKey(key);
|
||||
}
|
||||
|
||||
reloadAudioKey(key) {
|
||||
const audio = this.audioMap.get(key);
|
||||
if (audio) {
|
||||
try {
|
||||
audio.destroy();
|
||||
} catch (_) {}
|
||||
this.audioMap.delete(key);
|
||||
}
|
||||
this.readyMap.set(key, false);
|
||||
this.retryLoadAudio(key);
|
||||
}
|
||||
|
||||
// 重新加载音频
|
||||
retryLoadAudio(key) {
|
||||
this.clearPlayWatchdog(key);
|
||||
const oldAudio = this.audioMap.get(key);
|
||||
if (oldAudio) oldAudio.destroy();
|
||||
if (oldAudio) {
|
||||
try {
|
||||
oldAudio.destroy();
|
||||
} catch (_) {}
|
||||
}
|
||||
this.createAudio(key);
|
||||
}
|
||||
|
||||
// 播放指定音频或音频数组(数组则按顺序连续播放)
|
||||
play(input, interrupt = true) {
|
||||
if (this.isInterrupted) {
|
||||
this.recoverFromInterruptionIfStale();
|
||||
}
|
||||
if (this.isInterrupted) {
|
||||
debugLog("音频处理中断状态,忽略播放请求");
|
||||
return;
|
||||
@@ -553,6 +695,9 @@ class AudioManager {
|
||||
|
||||
// 内部方法:播放单个 key
|
||||
_playSingle(key, forceStopAll = false) {
|
||||
if (this.isInterrupted) {
|
||||
this.recoverFromInterruptionIfStale();
|
||||
}
|
||||
if (this.isInterrupted) {
|
||||
debugLog(`音频处理中断状态,跳过播放: ${key}`);
|
||||
return;
|
||||
@@ -561,6 +706,11 @@ class AudioManager {
|
||||
const now = Date.now();
|
||||
if (this.lastPlayKey === key && now - this.lastPlayAt < 250) {
|
||||
debugLog(`忽略快速重复播放: ${key}`);
|
||||
this.finishPlayback(key, {
|
||||
advanceSequence: true,
|
||||
emitEnded: true,
|
||||
force: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -603,21 +753,34 @@ class AudioManager {
|
||||
try {
|
||||
audio.play();
|
||||
} catch (err) {
|
||||
this.allowPlayMap.set(key, false);
|
||||
this.finishPlayback(key, {
|
||||
advanceSequence: true,
|
||||
emitEnded: true,
|
||||
force: true,
|
||||
});
|
||||
debugLog(`音频 ${key} 播放调用失败`, err?.errMsg || err);
|
||||
return;
|
||||
}
|
||||
this.currentPlayingKey = key;
|
||||
this.lastPlayKey = key;
|
||||
this.lastPlayAt = Date.now();
|
||||
this.startPlayWatchdog(key);
|
||||
} else {
|
||||
debugLog(`音频 ${key} 不存在,尝试重新加载...`);
|
||||
this.retryLoadAudio(key);
|
||||
let loadWaitTimer = null;
|
||||
const cleanup = () => {
|
||||
try {
|
||||
uni.$off("audioLoaded", handler);
|
||||
} catch (_) {}
|
||||
if (loadWaitTimer) {
|
||||
clearTimeout(loadWaitTimer);
|
||||
loadWaitTimer = null;
|
||||
}
|
||||
};
|
||||
const handler = (loadedKey) => {
|
||||
if (loadedKey === key) {
|
||||
try {
|
||||
uni.$off("audioLoaded", handler);
|
||||
} catch (_) {}
|
||||
cleanup();
|
||||
// 再次校验是否存在且就绪
|
||||
const a = this.audioMap.get(key);
|
||||
if (a && this.readyMap.get(key)) {
|
||||
@@ -628,6 +791,7 @@ class AudioManager {
|
||||
try {
|
||||
uni.$on("audioLoaded", handler);
|
||||
} catch (_) {}
|
||||
loadWaitTimer = setTimeout(cleanup, 12000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,6 +817,7 @@ class AudioManager {
|
||||
// 停止指定音频
|
||||
stop(key) {
|
||||
const audio = this.audioMap.get(key);
|
||||
this.clearPlayWatchdog(key);
|
||||
if (audio) {
|
||||
audio.stop();
|
||||
this.allowPlayMap.set(key, false);
|
||||
@@ -664,6 +829,7 @@ class AudioManager {
|
||||
|
||||
// 停止所有音频
|
||||
stopAll() {
|
||||
this.clearAllPlayWatchdogs();
|
||||
for (const [k, audio] of this.audioMap.entries()) {
|
||||
try {
|
||||
audio.stop();
|
||||
@@ -737,6 +903,7 @@ class AudioManager {
|
||||
this.readyMap.clear();
|
||||
this.failedLoadKeys.clear();
|
||||
this.allowPlayMap.clear();
|
||||
this.clearAllPlayWatchdogs();
|
||||
this.currentPlayingKey = null;
|
||||
this.sequenceQueue = [];
|
||||
this.sequenceIndex = 0;
|
||||
|
||||
@@ -16,6 +16,11 @@ const props = defineProps({
|
||||
|
||||
const rowCount = new Array(6).fill(0);
|
||||
|
||||
const getRingText = (arrow) => {
|
||||
if (!arrow) return "-";
|
||||
if (arrow.ringX && arrow.ring) return "X环";
|
||||
return arrow.ring ? `${arrow.ring}环` : "-";
|
||||
};
|
||||
const isMember = (player = {}) => player.vip === true || player.sVip === true;
|
||||
|
||||
const getMemberNicknameClass = (player = {}) => [
|
||||
@@ -52,23 +57,19 @@ const getMemberNicknameClass = (player = {}) => [
|
||||
<view>
|
||||
<view>
|
||||
<view v-for="(_, index) in rowCount" :key="index">
|
||||
<text>{{
|
||||
scores[0] && scores[0][index] ? `${scores[0][index].ring}环` : "-"
|
||||
}}</text>
|
||||
<text>{{ getRingText(scores[0]?.[index]) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<view v-for="(_, index) in rowCount" :key="index">
|
||||
<text>{{
|
||||
scores[1] && scores[1][index] ? `${scores[1][index].ring}环` : "-"
|
||||
}}</text>
|
||||
<text>{{ getRingText(scores[1]?.[index]) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text
|
||||
>{{
|
||||
scores
|
||||
.map((s) => s.reduce((last, next) => last + next.ring, 0))
|
||||
.map((s) => (s || []).reduce((last, next) => last + next.ring, 0))
|
||||
.reduce((last, next) => last + next, 0)
|
||||
}}环</text
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
|
||||
import useStore from "@/store";
|
||||
const store = useStore();
|
||||
const { updateUser, updateDevice, updateOnline } = store;
|
||||
const { updateUser, updateDevice, updateOnline, clearDevice } = store;
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
@@ -122,6 +122,8 @@ async function doLogin() {
|
||||
);
|
||||
const data = await getDeviceBatteryAPI();
|
||||
updateOnline(data.online);
|
||||
} else {
|
||||
clearDevice();
|
||||
}
|
||||
props.onClose();
|
||||
} catch (error) {
|
||||
|
||||
@@ -315,7 +315,7 @@ function goBack() {
|
||||
<Container
|
||||
:bgType="data.mode > 3 ? -1 : 0"
|
||||
bgColor="#000000"
|
||||
:onBack="goBack"
|
||||
:onBack="exit"
|
||||
>
|
||||
|
||||
<!-- ----- Banner 区:game 胜负展示图(仅 NvN 对抗模式)----- -->
|
||||
|
||||
@@ -26,6 +26,7 @@ const {
|
||||
updateConfig,
|
||||
updateUser,
|
||||
updateDevice,
|
||||
clearDevice,
|
||||
getLvlName,
|
||||
getLvlNameByScore,
|
||||
updateOnline,
|
||||
@@ -127,6 +128,8 @@ onShow(async () => {
|
||||
);
|
||||
const data = await getDeviceBatteryAPI();
|
||||
updateOnline(data.online);
|
||||
} else {
|
||||
clearDevice();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,64 @@ const playersSorted = ref([]);
|
||||
const playersScores = ref([]);
|
||||
const halfTimeTip = ref(false);
|
||||
const halfRest = ref(false);
|
||||
const HALF_REST_SECONDS = 20;
|
||||
const halfRestRemain = ref(HALF_REST_SECONDS);
|
||||
let halfRestTimer = null;
|
||||
/** 控制设备离线提示弹窗的显示状态 */
|
||||
const showOfflineModal = ref(false);
|
||||
/** 记录每位玩家当前半场连续 X 环数,key 为 playerId,用于触发 tententen 音效 */
|
||||
/** 记录每位玩家当前半场连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
|
||||
const xRingStreaks = ref({});
|
||||
|
||||
function clearHalfRestCountdown() {
|
||||
if (halfRestTimer) {
|
||||
clearInterval(halfRestTimer);
|
||||
halfRestTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function getHalfRestSeconds(battleInfo) {
|
||||
const remainCandidates = [
|
||||
battleInfo?.halfRestRemain,
|
||||
battleInfo?.halfRestRemainSeconds,
|
||||
battleInfo?.restRemain,
|
||||
battleInfo?.restRemainSeconds,
|
||||
];
|
||||
|
||||
for (const item of remainCandidates) {
|
||||
const remain = Number(item);
|
||||
if (Number.isFinite(remain) && remain > 0 && remain <= HALF_REST_SECONDS) {
|
||||
return Math.ceil(remain);
|
||||
}
|
||||
}
|
||||
|
||||
const endTime = Number(battleInfo?.halfRestEndTime ?? battleInfo?.restEndTime);
|
||||
if (!Number.isFinite(endTime) || endTime <= 0) return HALF_REST_SECONDS;
|
||||
|
||||
const timestamp = endTime < 1e12 ? endTime * 1000 : endTime;
|
||||
const diffSeconds = (timestamp - Date.now()) / 1000;
|
||||
if (diffSeconds > 0 && diffSeconds <= HALF_REST_SECONDS) {
|
||||
return Math.ceil(diffSeconds);
|
||||
}
|
||||
|
||||
return HALF_REST_SECONDS;
|
||||
}
|
||||
|
||||
function startHalfRestCountdown(seconds = HALF_REST_SECONDS) {
|
||||
clearHalfRestCountdown();
|
||||
halfRestRemain.value = Math.max(0, Math.ceil(Number(seconds) || HALF_REST_SECONDS));
|
||||
|
||||
if (halfRestRemain.value <= 0) return;
|
||||
|
||||
halfRestTimer = setInterval(() => {
|
||||
if (halfRestRemain.value <= 1) {
|
||||
halfRestRemain.value = 0;
|
||||
clearHalfRestCountdown();
|
||||
return;
|
||||
}
|
||||
|
||||
halfRestRemain.value -= 1;
|
||||
}, 1000);
|
||||
}
|
||||
const currentPlayer = computed(() =>
|
||||
players.value.find((player) => String(player?.id) === String(user.value.id))
|
||||
);
|
||||
@@ -96,8 +149,7 @@ function recoverData(battleInfo, { force = false } = {}) {
|
||||
halfTimeTip.value = true;
|
||||
halfRest.value = true;
|
||||
tips.value = "准备下半场";
|
||||
// 剩余休息时间
|
||||
// const remain = (Date.now() - battleInfo.timeoutTime) / 1000;
|
||||
startHalfRestCountdown(getHalfRestSeconds(battleInfo));
|
||||
setTimeout(() => {
|
||||
uni.$emit("update-remain", 0);
|
||||
}, 200);
|
||||
@@ -128,23 +180,27 @@ onLoad(async (options) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 检测指定玩家连续 X 环是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
||||
* 检测指定玩家连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
||||
* @param {number|string} playerId - 本次射手的 ID(大乱斗中 ShootResult 保留 playerId)
|
||||
* @param {boolean} isXRing - 本次射击是否为 X 环
|
||||
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
|
||||
*/
|
||||
function checkAndPlayTententen(playerId, isXRing) {
|
||||
function isTenPlusRing(shot) {
|
||||
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||
}
|
||||
|
||||
function checkAndPlayTententen(playerId, isTenPlusRingShot) {
|
||||
if (!playerId) return;
|
||||
const id = parseInt(playerId);
|
||||
if (isXRing) {
|
||||
if (isTenPlusRingShot) {
|
||||
xRingStreaks.value[id] = (xRingStreaks.value[id] || 0) + 1;
|
||||
// 同一玩家连续 3 箭均为 X 环,追加到环数音效队列尾部播放
|
||||
// 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
|
||||
if (xRingStreaks.value[id] >= 3) {
|
||||
xRingStreaks.value[id] = 0;
|
||||
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
|
||||
nextTick(() => audioManager.play("tententen", false));
|
||||
}
|
||||
} else {
|
||||
// 非 X 环则重置该玩家的连续计数
|
||||
// 低于 10 环或未上靶则重置该玩家的连续计数
|
||||
xRingStreaks.value[id] = 0;
|
||||
}
|
||||
}
|
||||
@@ -152,6 +208,7 @@ function checkAndPlayTententen(playerId, isXRing) {
|
||||
async function onReceiveMessage(msg) {
|
||||
if (Array.isArray(msg)) return;
|
||||
if (msg.type === MESSAGETYPESV2.BattleStart) {
|
||||
clearHalfRestCountdown();
|
||||
halfTimeTip.value = false;
|
||||
halfRest.value = false;
|
||||
recoverData(msg);
|
||||
@@ -166,22 +223,23 @@ async function onReceiveMessage(msg) {
|
||||
// 对比更新后数据找出箭数增加的玩家(即本次射手),并读取其最新箭的 ring 数据
|
||||
const newRound = playersScores.value[playersScores.value.length - 1] || {};
|
||||
let shooterId = null;
|
||||
let isXRing = false;
|
||||
let isTenPlusRingShot = false;
|
||||
for (const pid of Object.keys(newRound)) {
|
||||
const newLen = (newRound[pid] || []).length;
|
||||
if (newLen > (prevCounts[pid] || 0)) {
|
||||
shooterId = parseInt(pid);
|
||||
const shot = newRound[pid][newLen - 1];
|
||||
isXRing = !!(shot?.ringX && shot?.ring);
|
||||
isTenPlusRingShot = isTenPlusRing(shot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 检测同一玩家三箭全 X 环,触发 tententen 音效
|
||||
checkAndPlayTententen(shooterId, isXRing);
|
||||
// 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
|
||||
checkAndPlayTententen(shooterId, isTenPlusRingShot);
|
||||
} else if (msg.type === MESSAGETYPESV2.HalfRest) {
|
||||
halfTimeTip.value = true;
|
||||
halfRest.value = true;
|
||||
tips.value = "准备下半场";
|
||||
startHalfRestCountdown();
|
||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||
setTimeout(() => {
|
||||
// 全部跳转到新结算页
|
||||
@@ -202,6 +260,7 @@ onBeforeUnmount(() => {
|
||||
uni.setKeepScreenOn({
|
||||
keepScreenOn: false,
|
||||
});
|
||||
clearHalfRestCountdown();
|
||||
uni.$off("socket-inbox", onReceiveMessage);
|
||||
audioManager.stopAll();
|
||||
});
|
||||
@@ -267,7 +326,7 @@ onShow(async () => {
|
||||
>
|
||||
<view class="half-time-tip">
|
||||
<text>上半场结束,休息一下吧:)</text>
|
||||
<text>20秒后开始下半场</text>
|
||||
<text>{{ halfRestRemain }}秒后开始下半场</text>
|
||||
</view>
|
||||
</ScreenHint>
|
||||
<!-- 设备离线提示弹窗 -->
|
||||
|
||||
@@ -16,7 +16,7 @@ const showTip = ref(false);
|
||||
const confirmBindTip = ref(false);
|
||||
const addDevice = ref();
|
||||
const store = useStore();
|
||||
const { updateDevice } = store;
|
||||
const { updateDevice, clearDevice } = store;
|
||||
const { user, device } = storeToRefs(store);
|
||||
const justBind = ref(false);
|
||||
const calibration = ref(false);
|
||||
@@ -86,13 +86,21 @@ const toFristTryPage = () => {
|
||||
};
|
||||
|
||||
const unbindDevice = async () => {
|
||||
await unbindDeviceAPI(device.value.deviceId);
|
||||
try {
|
||||
await unbindDeviceAPI(device.value.deviceId);
|
||||
} catch (error) {
|
||||
if (error?.type === "DEVICE_BIND_INVALID") {
|
||||
uni.setStorageSync("calibration", false);
|
||||
clearDevice();
|
||||
}
|
||||
return;
|
||||
}
|
||||
uni.setStorageSync("calibration", false);
|
||||
uni.showToast({
|
||||
title: "解绑成功",
|
||||
icon: "success",
|
||||
});
|
||||
device.value = {};
|
||||
clearDevice();
|
||||
};
|
||||
|
||||
const toDeviceIntroPage = () => {
|
||||
@@ -124,8 +132,23 @@ const goCalibration = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
const syncDeviceBinding = async () => {
|
||||
if (!user.value.id) return;
|
||||
try {
|
||||
const devices = await getMyDevicesAPI();
|
||||
if (devices.bindings && devices.bindings.length) {
|
||||
updateDevice(devices.bindings[0].deviceId, devices.bindings[0].deviceName);
|
||||
} else {
|
||||
clearDevice();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("sync device binding error", error);
|
||||
}
|
||||
};
|
||||
|
||||
onShow(async () => {
|
||||
calibration.value = uni.getStorageSync("calibration");
|
||||
await syncDeviceBinding();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ const start = ref(false);
|
||||
const scores = ref([]);
|
||||
const isSvip = ref(false);
|
||||
const total = 12;
|
||||
/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */
|
||||
/** 当前练习中连续 10 环及以上计数,用于触发 tententen 音效 */
|
||||
const xRingStreak = ref(0);
|
||||
const practiseResult = ref({});
|
||||
const practiseId = ref("");
|
||||
@@ -61,19 +61,23 @@ const onOver = async () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效
|
||||
* @param {boolean} isXRing - 本次射击是否为 X 环
|
||||
* 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效
|
||||
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
|
||||
*/
|
||||
function checkAndPlayTententen(isXRing) {
|
||||
if (isXRing) {
|
||||
function isTenPlusRing(shot) {
|
||||
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||
}
|
||||
|
||||
function checkAndPlayTententen(isTenPlusRingShot) {
|
||||
if (isTenPlusRingShot) {
|
||||
xRingStreak.value += 1;
|
||||
// 连续 3 箭均为 X 环,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
||||
// 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
||||
if (xRingStreak.value >= 3) {
|
||||
xRingStreak.value = 0;
|
||||
nextTick(() => audioManager.play("tententen", false));
|
||||
}
|
||||
} else {
|
||||
// 非 X 环则重置连续计数
|
||||
// 低于 10 环或未上靶则重置连续计数
|
||||
xRingStreak.value = 0;
|
||||
}
|
||||
}
|
||||
@@ -83,10 +87,10 @@ async function onReceiveMessage(msg) {
|
||||
const prevLen = scores.value.length;
|
||||
isSvip.value = msg.sVip === true;
|
||||
scores.value = msg.details;
|
||||
// 有新箭时取最后一箭判断是否 X 环并检测连续计数
|
||||
// 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数
|
||||
if (scores.value.length > prevLen) {
|
||||
const latestArrow = scores.value[scores.value.length - 1];
|
||||
checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring));
|
||||
checkAndPlayTententen(isTenPlusRing(latestArrow));
|
||||
}
|
||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||
// setTimeout(onOver, 1500);
|
||||
|
||||
@@ -32,7 +32,7 @@ const start = ref(false);
|
||||
const scores = ref([]);
|
||||
const isSvip = ref(false);
|
||||
const total = 36;
|
||||
/** 当前练习中连续 X 环计数,用于触发 tententen 音效 */
|
||||
/** 当前练习中连续 10 环及以上计数,用于触发 tententen 音效 */
|
||||
const xRingStreak = ref(0);
|
||||
const practiseResult = ref({});
|
||||
const practiseId = ref("");
|
||||
@@ -60,19 +60,23 @@ const onOver = async () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测连续 X 环是否达到 3 箭,达到则播放 tententen 音效
|
||||
* @param {boolean} isXRing - 本次射击是否为 X 环
|
||||
* 检测连续 10 环及以上是否达到 3 箭,达到则播放 tententen 音效
|
||||
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
|
||||
*/
|
||||
function checkAndPlayTententen(isXRing) {
|
||||
if (isXRing) {
|
||||
function isTenPlusRing(shot) {
|
||||
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||
}
|
||||
|
||||
function checkAndPlayTententen(isTenPlusRingShot) {
|
||||
if (isTenPlusRingShot) {
|
||||
xRingStreak.value += 1;
|
||||
// 连续 3 箭均为 X 环,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
||||
// 连续 3 箭均为 10 环及以上,在环数播报入队后追加 tententen,避免播放顺序颠倒
|
||||
if (xRingStreak.value >= 3) {
|
||||
xRingStreak.value = 0;
|
||||
nextTick(() => audioManager.play("tententen", false));
|
||||
}
|
||||
} else {
|
||||
// 非 X 环则重置连续计数
|
||||
// 低于 10 环或未上靶则重置连续计数
|
||||
xRingStreak.value = 0;
|
||||
}
|
||||
}
|
||||
@@ -82,10 +86,10 @@ async function onReceiveMessage(msg) {
|
||||
const prevLen = scores.value.length;
|
||||
isSvip.value = msg.sVip === true;
|
||||
scores.value = msg.details;
|
||||
// 有新箭时取最后一箭判断是否 X 环并检测连续计数
|
||||
// 有新箭时取最后一箭判断是否 10 环及以上并检测连续计数
|
||||
if (scores.value.length > prevLen) {
|
||||
const latestArrow = scores.value[scores.value.length - 1];
|
||||
checkAndPlayTententen(!!(latestArrow?.ringX && latestArrow?.ring));
|
||||
checkAndPlayTententen(isTenPlusRing(latestArrow));
|
||||
}
|
||||
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
|
||||
setTimeout(onOver, 1500);
|
||||
|
||||
@@ -49,7 +49,7 @@ const battleWay = ref(0);
|
||||
const lastToSomeoneShootKey = ref("");
|
||||
/** 控制设备离线提示弹窗的显示状态 */
|
||||
const showOfflineModal = ref(false);
|
||||
/** 记录每位玩家当前轮连续 X 环数,key 为 playerId,用于触发 tententen 音效 */
|
||||
/** 记录每位玩家当前轮连续 10 环及以上次数,key 为 playerId,用于触发 tententen 音效 */
|
||||
const xRingStreaks = ref({});
|
||||
|
||||
/**
|
||||
@@ -234,22 +234,26 @@ function onNewRound(msg, prevRound) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测指定射手连续 X 环是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
||||
* 检测指定射手连续 10 环及以上是否达到 3 箭,达到则在环数播报入队后追加 tententen 音效
|
||||
* @param {number} shooterId - 本次射手的 ID(取自 currentShooterId.value)
|
||||
* @param {boolean} isXRing - 本次射击是否为 X 环
|
||||
* @param {boolean} isTenPlusRingShot - 本次射击是否为 10 环及以上
|
||||
*/
|
||||
function checkAndPlayTententen(shooterId, isXRing) {
|
||||
function isTenPlusRing(shot) {
|
||||
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||
}
|
||||
|
||||
function checkAndPlayTententen(shooterId, isTenPlusRingShot) {
|
||||
if (!shooterId) return;
|
||||
if (isXRing) {
|
||||
if (isTenPlusRingShot) {
|
||||
xRingStreaks.value[shooterId] = (xRingStreaks.value[shooterId] || 0) + 1;
|
||||
// 同一玩家连续 3 箭均为 X 环,追加到环数音效队列尾部播放
|
||||
// 同一玩家连续 3 箭均为 10 环及以上,追加到环数音效队列尾部播放
|
||||
if (xRingStreaks.value[shooterId] >= 3) {
|
||||
xRingStreaks.value[shooterId] = 0;
|
||||
// nextTick 确保 HeaderProgress 的环数播报已入队后再追加 tententen,避免播放顺序颠倒
|
||||
nextTick(() => audioManager.play("tententen", false));
|
||||
}
|
||||
} else {
|
||||
// 非 X 环则重置该玩家的连续计数
|
||||
// 低于 10 环或未上靶则重置该玩家的连续计数
|
||||
xRingStreaks.value[shooterId] = 0;
|
||||
}
|
||||
}
|
||||
@@ -268,9 +272,9 @@ async function onReceiveMessage(msg) {
|
||||
} else if (msg.type === MESSAGETYPESV2.ShootResult) {
|
||||
showRoundTip.value = false;
|
||||
recoverData(msg, {arrowOnly: true});
|
||||
// 检测同一玩家三箭全 X 环,触发 tententen 音效
|
||||
// 检测同一玩家连续三箭 10 环及以上,触发 tententen 音效
|
||||
// currentShooterId 在 ToSomeoneShoot 时写入,ShootResult 不会覆盖,可靠识别本次射手
|
||||
checkAndPlayTententen(currentShooterId.value, !!(msg.shootData?.ringX && msg.shootData?.ring));
|
||||
checkAndPlayTententen(currentShooterId.value, isTenPlusRing(msg.shootData));
|
||||
} else if (msg.type === MESSAGETYPESV2.NewRound) {
|
||||
// 在进入延迟前先捕获当前轮次,供 onNewRound 使用,防止 800ms 内 ToSomeoneShoot 提前更新 currentRound 造成 Tip 展示错轮
|
||||
const prevRound = currentRound.value;
|
||||
|
||||
@@ -432,7 +432,10 @@ function playAudioKeys(keys, { interrupt = false, timeout } = {}) {
|
||||
resolve();
|
||||
},
|
||||
};
|
||||
const timer = setTimeout(waiter.done, waitTime);
|
||||
const timer = setTimeout(() => {
|
||||
audioManager.recoverIfStale(expectedKey);
|
||||
waiter.done();
|
||||
}, waitTime);
|
||||
audioWaiters.add(waiter);
|
||||
audioManager.play(audioKeys, interrupt);
|
||||
});
|
||||
@@ -473,17 +476,14 @@ function updateTeams(battleInfo) {
|
||||
}
|
||||
|
||||
function updateGoldenRound(battleInfo) {
|
||||
const rounds = Array.isArray(battleInfo?.rounds) ? battleInfo.rounds : [];
|
||||
const currentRoundNo = Number(battleInfo?.current?.round || 0);
|
||||
const currentRoundInfo = rounds.find((round) => Number(round?.round) === currentRoundNo);
|
||||
const activeGoldRoundInfo = rounds.find(
|
||||
(round) => Number(round?.goldRound || 0) > 0 && round?.status === 1
|
||||
);
|
||||
const roundGoldRound = Number(currentRoundInfo?.goldRound || 0);
|
||||
const activeGoldRound = Number(activeGoldRoundInfo?.goldRound || 0);
|
||||
const currentGoldRound = Number(battleInfo?.current?.goldRound || 0);
|
||||
const nextGoldRound = roundGoldRound || activeGoldRound || currentGoldRound;
|
||||
goldenRound.value = nextGoldRound > 0 ? nextGoldRound : 0;
|
||||
if (!battleInfo?.current?.goldRound) {
|
||||
goldenRound.value = 0;
|
||||
return;
|
||||
}
|
||||
const rounds = Array.isArray(battleInfo.rounds) ? battleInfo.rounds : [];
|
||||
const finishedGoldCount = rounds.filter((round) => !!round?.ifGold).length;
|
||||
// goldenRound.value = Math.max(1, finishedGoldCount + (battleInfo.current?.playerId ? 1 : 0));
|
||||
goldenRound.value = Math.max(1, finishedGoldCount);
|
||||
}
|
||||
|
||||
// Restore an info snapshot whose eventType points at the NewRound phase.
|
||||
@@ -861,10 +861,14 @@ async function runToSomeoneShootTask(task, runId) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateXRingStreak(shooterId, isXRing) {
|
||||
function isTenPlusRing(shot) {
|
||||
return !!(shot?.ringX || Number(shot?.ring) >= 10);
|
||||
}
|
||||
|
||||
function updateXRingStreak(shooterId, isTenPlusRingShot) {
|
||||
if (!shooterId) return false;
|
||||
const id = String(shooterId);
|
||||
if (!isXRing) {
|
||||
if (!isTenPlusRingShot) {
|
||||
xRingStreaks.value[id] = 0;
|
||||
saveXRingStreaks();
|
||||
return false;
|
||||
@@ -909,7 +913,7 @@ async function runShootResultTask(task) {
|
||||
|
||||
const isTententen = updateXRingStreak(
|
||||
currentShooterId.value,
|
||||
!!(battleInfo.shootData?.ringX && battleInfo.shootData?.ring)
|
||||
isTenPlusRing(battleInfo.shootData)
|
||||
);
|
||||
const audioKeys = buildShootResultAudioKeys(battleInfo.shootData);
|
||||
if (isTententen) audioKeys.push("tententen");
|
||||
@@ -1255,7 +1259,7 @@ onShow(() => {
|
||||
<view class="offline-modal">
|
||||
<text class="offline-title">设备已离线</text>
|
||||
<text class="offline-desc">检测到设备已断开连接,请检查设备后继续比赛</text>
|
||||
<SButton @click="showOfflineModal = false">我知道了</SButton>
|
||||
<SButton :onClick="() => (showOfflineModal = false)">我知道了</SButton>
|
||||
</view>
|
||||
</SModal>
|
||||
</view>
|
||||
|
||||
@@ -137,6 +137,10 @@ export default defineStore("store", {
|
||||
this.device.deviceId = deviceId;
|
||||
this.device.deviceName = deviceName;
|
||||
},
|
||||
clearDevice() {
|
||||
this.device = getDefaultDevice();
|
||||
this.online = false;
|
||||
},
|
||||
async updateConfig(config) {
|
||||
this.config = config;
|
||||
if (this.user.scores !== undefined) {
|
||||
|
||||
Reference in New Issue
Block a user