update: 修复飞行特效

This commit is contained in:
2026-08-04 16:18:30 +08:00
parent ab70c8c78e
commit 3c45ef78ac
8 changed files with 406 additions and 104 deletions
+88 -11
View File
@@ -34,6 +34,10 @@ const props = defineProps({
type: Number, type: Number,
default: 0, default: 0,
}, },
viewportMode: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(["complete", "impact"]); const emit = defineEmits(["complete", "impact"]);
@@ -43,6 +47,7 @@ const activePlayKey = ref("");
const animationKey = ref(""); const animationKey = ref("");
const impactEmitted = ref(false); const impactEmitted = ref(false);
const activeShot = ref(null); const activeShot = ref(null);
const activeLayout = ref(null);
let timers = []; let timers = [];
const isActive = computed(() => phase.value !== "idle"); const isActive = computed(() => phase.value !== "idle");
@@ -93,6 +98,17 @@ function hasShotPoint(shot) {
} }
const effectiveShot = computed(() => activeShot.value || props.shot); const effectiveShot = computed(() => activeShot.value || props.shot);
const effectiveTargetSize = computed(
() => activeLayout.value?.target || safeTargetSize.value
);
const effectiveWindowSize = computed(
() => activeLayout.value?.window || getWindowSize()
);
const isViewportMode = computed(() =>
activeLayout.value
? activeLayout.value.viewportMode
: props.viewportMode === true
);
const shotPoint = computed(() => { const shotPoint = computed(() => {
const x = Number(effectiveShot.value?.x); const x = Number(effectiveShot.value?.x);
@@ -137,8 +153,40 @@ const hitPercent = computed(() => {
}); });
const flightPath = computed(() => { const flightPath = computed(() => {
const size = safeTargetSize.value; const size = effectiveTargetSize.value;
const windowSize = getWindowSize(); const windowSize = effectiveWindowSize.value;
if (
isViewportMode.value &&
size.width > 0 &&
size.height > 0 &&
windowSize.width > 0 &&
windowSize.height > 0
) {
const startX = windowSize.width * 0.5;
const endX =
size.left +
size.width * (hitPercent.value.left / 100) +
hitOffset.value.x;
const endY =
size.top +
size.height * (hitPercent.value.top / 100) +
hitOffset.value.y;
// 常规情况下从视口底部进入;靶面局部超出视口时仍保证起点在命中点下方。
const startY = Math.max(windowSize.height + 16, endY + 80);
const dx = endX - startX;
const dy = endY - startY;
return {
startX,
startY,
endX,
endY,
translateX: dx,
translateY: dy,
angle: Math.atan2(dx, -dy) * (180 / Math.PI),
};
}
const hasScreenCoordinates = const hasScreenCoordinates =
size.width > 0 && size.width > 0 &&
@@ -161,6 +209,8 @@ const flightPath = computed(() => {
return { return {
startX, startX,
startY, startY,
endX,
endY,
translateX: dx, translateX: dx,
translateY: dy, translateY: dy,
angle: Math.atan2(dx, -dy) * (180 / Math.PI), angle: Math.atan2(dx, -dy) * (180 / Math.PI),
@@ -178,10 +228,19 @@ function formatTargetPosition(percent, offset) {
return pxOffset ? `calc(${percent}%${pxOffset})` : `${percent}%`; return pxOffset ? `calc(${percent}%${pxOffset})` : `${percent}%`;
} }
const crackStyle = computed(() => ({ const crackStyle = computed(() => {
left: formatTargetPosition(hitPercent.value.left, hitOffset.value.x), if (isViewportMode.value) {
top: formatTargetPosition(hitPercent.value.top, hitOffset.value.y), return {
})); left: `${flightPath.value.endX}px`,
top: `${flightPath.value.endY}px`,
};
}
return {
left: formatTargetPosition(hitPercent.value.left, hitOffset.value.x),
top: formatTargetPosition(hitPercent.value.top, hitOffset.value.y),
};
});
function getTargetTranslate(percent) { function getTargetTranslate(percent) {
const absPercent = Math.abs(percent); const absPercent = Math.abs(percent);
@@ -190,19 +249,21 @@ function getTargetTranslate(percent) {
} }
const arrowMoveStyle = computed(() => { const arrowMoveStyle = computed(() => {
const size = safeTargetSize.value; const size = effectiveTargetSize.value;
const path = flightPath.value; const path = flightPath.value;
let x = getTargetTranslate(hitPercent.value.left - 50); let x = getTargetTranslate(hitPercent.value.left - 50);
let y = getTargetTranslate(hitPercent.value.top - 114); let y = getTargetTranslate(hitPercent.value.top - 114);
if (size.width && size.height) { if (isViewportMode.value || (size.width && size.height)) {
x = `${path.translateX}px`; x = `${path.translateX}px`;
y = `${path.translateY}px`; y = `${path.translateY}px`;
} }
return { return {
"--shot-start-x": size.width ? `${path.startX}px` : "50%", "--shot-start-x":
"--shot-start-y": size.height ? `${path.startY}px` : "114%", isViewportMode.value || size.width ? `${path.startX}px` : "50%",
"--shot-start-y":
isViewportMode.value || size.height ? `${path.startY}px` : "114%",
"--shot-tx": x, "--shot-tx": x,
"--shot-ty": y, "--shot-ty": y,
"--shot-angle": `${path.angle}deg`, "--shot-angle": `${path.angle}deg`,
@@ -231,6 +292,7 @@ function finish(playKey) {
phase.value = "idle"; phase.value = "idle";
activePlayKey.value = ""; activePlayKey.value = "";
activeShot.value = null; activeShot.value = null;
activeLayout.value = null;
emit("complete", playKey); emit("complete", playKey);
} }
@@ -244,6 +306,11 @@ function play() {
animationKey.value = `${props.playKey}`; animationKey.value = `${props.playKey}`;
impactEmitted.value = false; impactEmitted.value = false;
activeShot.value = { ...props.shot }; activeShot.value = { ...props.shot };
activeLayout.value = {
target: { ...safeTargetSize.value },
window: getWindowSize(),
viewportMode: props.viewportMode === true,
};
phase.value = "playing"; phase.value = "playing";
queueTimer(() => { queueTimer(() => {
@@ -278,7 +345,11 @@ onBeforeUnmount(() => {
<template> <template>
<view <view
v-show="isActive" v-show="isActive"
:class="['shot-effect', `shot-effect--${phase}`]" :class="[
'shot-effect',
`shot-effect--${phase}`,
{ 'shot-effect--viewport': isViewportMode },
]"
:style="arrowMoveStyle" :style="arrowMoveStyle"
> >
<view <view
@@ -326,6 +397,12 @@ onBeforeUnmount(() => {
transform: translateZ(0); transform: translateZ(0);
} }
.shot-effect--viewport {
position: fixed;
width: 100vw;
height: 100vh;
}
.shot-arrow-track { .shot-arrow-track {
position: absolute; position: absolute;
left: var(--shot-start-x); left: var(--shot-start-x);
+108 -32
View File
@@ -63,6 +63,10 @@ const props = defineProps({
type: Number, type: Number,
default: 5, default: 5,
}, },
stableShotEffect: {
type: Boolean,
default: false,
},
}); });
const pMode = ref(true); const pMode = ref(true);
@@ -73,12 +77,14 @@ const dirTimer = ref(null);
const angle = ref(null); const angle = ref(null);
const circleColor = ref(""); const circleColor = ref("");
const shotEffect = ref(null); const shotEffect = ref(null);
const pendingShotEffect = ref(null);
const hiddenRedLatestKey = ref(""); const hiddenRedLatestKey = ref("");
const hiddenBlueLatestKey = ref(""); const hiddenBlueLatestKey = ref("");
const targetShaking = ref(false); const targetShaking = ref(false);
const targetRect = ref({ left: 0, top: 0, width: 0, height: 0 }); const targetRect = ref({ left: 0, top: 0, width: 0, height: 0 });
const shakeTimer = ref(null); const shakeTimer = ref(null);
const instance = getCurrentInstance(); const instance = getCurrentInstance();
let shotEffectRequestGeneration = 0;
const ROUND_TIP_OFFSET_Y = -32; const ROUND_TIP_OFFSET_Y = -32;
const EXPERIENCE_TIP_OFFSET_Y = -68; const EXPERIENCE_TIP_OFFSET_Y = -68;
@@ -135,7 +141,7 @@ function showShotTip(team, shot) {
}, 1000); }, 1000);
} }
function triggerShotEffect(team, shot, index) { function triggerShotEffect(team, shot, index, viewportMode = false) {
const key = buildShotEffectKey(team, shot, index); const key = buildShotEffectKey(team, shot, index);
if (shotEffect.value?.team === "red") hiddenRedLatestKey.value = ""; if (shotEffect.value?.team === "red") hiddenRedLatestKey.value = "";
@@ -149,7 +155,29 @@ function triggerShotEffect(team, shot, index) {
hiddenBlueLatestKey.value = key; hiddenBlueLatestKey.value = key;
} }
shotEffect.value = { key, team, shot }; shotEffect.value = { key, team, shot, viewportMode };
}
async function prepareShotEffect(team, shot, index) {
const requestGeneration = ++shotEffectRequestGeneration;
const key = buildShotEffectKey(team, shot, index);
pendingShotEffect.value = { generation: requestGeneration, team, key };
clearTipTimer();
if (team === "red") latestOne.value = null;
if (team === "blue") bluelatestOne.value = null;
const viewportMode = props.stableShotEffect
? await updateTargetRect()
: false;
if (requestGeneration !== shotEffectRequestGeneration) {
if (pendingShotEffect.value?.generation === requestGeneration) {
pendingShotEffect.value = null;
}
return;
}
pendingShotEffect.value = null;
triggerShotEffect(team, shot, index, viewportMode);
} }
function completeShotEffect(key) { function completeShotEffect(key) {
@@ -178,44 +206,67 @@ function shakeTarget() {
}); });
} }
function updateTargetRect() { function hasValidTargetRect(rect = targetRect.value) {
nextTick(() => { return (
const query = instance?.proxy Number.isFinite(Number(rect?.left)) &&
? uni.createSelectorQuery().in(instance.proxy) Number.isFinite(Number(rect?.top)) &&
: uni.createSelectorQuery(); Number(rect?.width) > 0 &&
Number(rect?.height) > 0
);
}
query async function updateTargetRect() {
.select(".target") await nextTick();
.boundingClientRect((rect) => {
const left = Number(rect?.left); return new Promise((resolve) => {
const top = Number(rect?.top); let settled = false;
const width = Number(rect?.width); const finish = (rect) => {
const height = Number(rect?.height); if (settled) return;
if ( settled = true;
!Number.isFinite(left) ||
!Number.isFinite(top) || const isValid = hasValidTargetRect(rect);
!Number.isFinite(width) || if (isValid) {
!Number.isFinite(height) targetRect.value = {
) { left: Number(rect.left),
return; top: Number(rect.top),
} width: Number(rect.width),
if (width <= 0 || height <= 0) return; height: Number(rect.height),
targetRect.value = { left, top, width, height }; };
}) }
.exec(); resolve(isValid);
};
try {
const query = instance?.proxy
? uni.createSelectorQuery().in(instance.proxy)
: uni.createSelectorQuery();
query
.select(".target")
.boundingClientRect()
.exec((result) => finish(Array.isArray(result) ? result[0] : null));
} catch {
finish(null);
}
}); });
} }
function handleWindowResize() { function handleWindowResize() {
updateTargetRect(); void updateTargetRect();
} }
function shouldHideRedHit(index) { function shouldHideRedHit(index) {
return !!hiddenRedLatestKey.value && index === props.scores.length - 1; return (
(!!hiddenRedLatestKey.value || pendingShotEffect.value?.team === "red") &&
index === props.scores.length - 1
);
} }
function shouldHideBlueHit(index) { function shouldHideBlueHit(index) {
return !!hiddenBlueLatestKey.value && index === props.blueScores.length - 1; return (
(!!hiddenBlueLatestKey.value || pendingShotEffect.value?.team === "blue") &&
index === props.blueScores.length - 1
);
} }
watch( watch(
@@ -224,14 +275,18 @@ watch(
if (newLen === oldLen + 1) { if (newLen === oldLen + 1) {
const latestShot = props.scores[newLen - 1]; const latestShot = props.scores[newLen - 1];
if (shouldPlayShotEffect(latestShot)) { if (shouldPlayShotEffect(latestShot)) {
triggerShotEffect("red", latestShot, newLen - 1); void prepareShotEffect("red", latestShot, newLen - 1);
} else { } else {
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
showShotTip("red", latestShot); showShotTip("red", latestShot);
} }
return; return;
} }
if (newLen < oldLen) { if (newLen < oldLen) {
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
latestOne.value = null; latestOne.value = null;
hiddenRedLatestKey.value = ""; hiddenRedLatestKey.value = "";
if (shotEffect.value?.team === "red") shotEffect.value = null; if (shotEffect.value?.team === "red") shotEffect.value = null;
@@ -245,14 +300,18 @@ watch(
if (newLen === oldLen + 1) { if (newLen === oldLen + 1) {
const latestShot = props.blueScores[newLen - 1]; const latestShot = props.blueScores[newLen - 1];
if (shouldPlayShotEffect(latestShot)) { if (shouldPlayShotEffect(latestShot)) {
triggerShotEffect("blue", latestShot, newLen - 1); void prepareShotEffect("blue", latestShot, newLen - 1);
} else { } else {
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
showShotTip("blue", latestShot); showShotTip("blue", latestShot);
} }
return; return;
} }
if (newLen < oldLen) { if (newLen < oldLen) {
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
bluelatestOne.value = null; bluelatestOne.value = null;
hiddenBlueLatestKey.value = ""; hiddenBlueLatestKey.value = "";
if (shotEffect.value?.team === "blue") shotEffect.value = null; if (shotEffect.value?.team === "blue") shotEffect.value = null;
@@ -411,11 +470,13 @@ async function onReceiveMessage(message) {
onMounted(() => { onMounted(() => {
uni.$on("socket-inbox", onReceiveMessage); uni.$on("socket-inbox", onReceiveMessage);
updateTargetRect(); void updateTargetRect();
if (uni.onWindowResize) uni.onWindowResize(handleWindowResize); if (uni.onWindowResize) uni.onWindowResize(handleWindowResize);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
if (timer.value) { if (timer.value) {
clearTimeout(timer.value); clearTimeout(timer.value);
timer.value = null; timer.value = null;
@@ -521,6 +582,7 @@ onBeforeUnmount(() => {
</view> </view>
</block> </block>
<BowShotEffect <BowShotEffect
v-if="!shotEffect || !shotEffect.viewportMode"
:shot="shotEffect && shotEffect.shot" :shot="shotEffect && shotEffect.shot"
:playKey="shotEffect ? shotEffect.key : ''" :playKey="shotEffect ? shotEffect.key : ''"
:targetRadius="safeTargetRadius" :targetRadius="safeTargetRadius"
@@ -534,6 +596,20 @@ onBeforeUnmount(() => {
/> />
<image src="https://static.shelingxingqiu.com/shootmini/static/bow-target.png" mode="widthFix" /> <image src="https://static.shelingxingqiu.com/shootmini/static/bow-target.png" mode="widthFix" />
</view> </view>
<BowShotEffect
v-if="shotEffect && shotEffect.viewportMode"
:shot="shotEffect.shot"
:playKey="shotEffect.key"
:targetRadius="safeTargetRadius"
:targetLeft="targetRect.left"
:targetTop="targetRect.top"
:targetWidth="targetRect.width"
:targetHeight="targetRect.height"
:hitOffsetPx="currentHitRadiusPx"
:viewportMode="true"
@impact="shakeTarget"
@complete="completeShotEffect"
/>
<view class="footer"> <view class="footer">
<PointSwitcher <PointSwitcher
:onChange="(val) => (pMode = val)" :onChange="(val) => (pMode = val)"
+1 -1
View File
@@ -489,8 +489,8 @@ onShow(() => {
:totalRound="12" :totalRound="12"
:scores="playersScores.map((r) => r[user.id]).flat()" :scores="playersScores.map((r) => r[user.id]).flat()"
:isSvip="isCurrentUserSvip" :isSvip="isCurrentUserSvip"
:missAsZero="true"
:stop="halfRest" :stop="halfRest"
stable-shot-effect
/> />
<view :style="{ paddingBottom: '20px' }"> <view :style="{ paddingBottom: '20px' }">
<PlayerScore <PlayerScore
+1
View File
@@ -466,6 +466,7 @@ onBeforeUnmount(() => {
:currentRound="scores.length % 3" :currentRound="scores.length % 3"
:scores="scores" :scores="scores"
:isSvip="isSvip" :isSvip="isSvip"
stable-shot-effect
/> />
<ScorePanel2 :arrows="scores" /> <ScorePanel2 :arrows="scores" />
<ScoreResult <ScoreResult
+107 -31
View File
@@ -67,6 +67,10 @@ const props = defineProps({
type: Number, type: Number,
default: 5, default: 5,
}, },
stableShotEffect: {
type: Boolean,
default: false,
},
}); });
const pMode = ref(true); const pMode = ref(true);
@@ -77,12 +81,14 @@ const dirTimer = ref(null);
const angle = ref(null); const angle = ref(null);
const circleColor = ref(""); const circleColor = ref("");
const shotEffect = ref(null); const shotEffect = ref(null);
const pendingShotEffect = ref(null);
const hiddenRedLatestKey = ref(""); const hiddenRedLatestKey = ref("");
const hiddenBlueLatestKey = ref(""); const hiddenBlueLatestKey = ref("");
const targetShaking = ref(false); const targetShaking = ref(false);
const targetRect = ref({ left: 0, top: 0, width: 0, height: 0 }); const targetRect = ref({ left: 0, top: 0, width: 0, height: 0 });
const shakeTimer = ref(null); const shakeTimer = ref(null);
const instance = getCurrentInstance(); const instance = getCurrentInstance();
let shotEffectRequestGeneration = 0;
const ROUND_TIP_OFFSET_Y = -32; const ROUND_TIP_OFFSET_Y = -32;
const EXPERIENCE_TIP_OFFSET_Y = -68; const EXPERIENCE_TIP_OFFSET_Y = -68;
@@ -137,7 +143,7 @@ function showShotTip(team, shootData) {
}, 1000); }, 1000);
} }
function triggerShotEffect(team, shot, fallbackKey = "") { function triggerShotEffect(team, shot, fallbackKey = "", viewportMode = false) {
const key = buildShotEffectKey(team, shot, fallbackKey); const key = buildShotEffectKey(team, shot, fallbackKey);
if (shotEffect.value?.team === "red") hiddenRedLatestKey.value = ""; if (shotEffect.value?.team === "red") hiddenRedLatestKey.value = "";
@@ -151,7 +157,29 @@ function triggerShotEffect(team, shot, fallbackKey = "") {
hiddenBlueLatestKey.value = key; hiddenBlueLatestKey.value = key;
} }
shotEffect.value = { key, team, shot }; shotEffect.value = { key, team, shot, viewportMode };
}
async function prepareShotEffect(team, shot, fallbackKey = "") {
const requestGeneration = ++shotEffectRequestGeneration;
const key = buildShotEffectKey(team, shot, fallbackKey);
pendingShotEffect.value = { generation: requestGeneration, team, key };
clearTipTimer();
if (team === "red") latestOne.value = null;
if (team === "blue") bluelatestOne.value = null;
const viewportMode = props.stableShotEffect
? await updateTargetRect()
: false;
if (requestGeneration !== shotEffectRequestGeneration) {
if (pendingShotEffect.value?.generation === requestGeneration) {
pendingShotEffect.value = null;
}
return;
}
pendingShotEffect.value = null;
triggerShotEffect(team, shot, fallbackKey, viewportMode);
} }
function completeShotEffect(key) { function completeShotEffect(key) {
@@ -180,49 +208,74 @@ function shakeTarget() {
}); });
} }
function updateTargetRect() { function hasValidTargetRect(rect = targetRect.value) {
nextTick(() => { return (
const query = instance?.proxy Number.isFinite(Number(rect?.left)) &&
? uni.createSelectorQuery().in(instance.proxy) Number.isFinite(Number(rect?.top)) &&
: uni.createSelectorQuery(); Number(rect?.width) > 0 &&
Number(rect?.height) > 0
);
}
query async function updateTargetRect() {
.select(".target") await nextTick();
.boundingClientRect((rect) => {
const left = Number(rect?.left); return new Promise((resolve) => {
const top = Number(rect?.top); let settled = false;
const width = Number(rect?.width); const finish = (rect) => {
const height = Number(rect?.height); if (settled) return;
if ( settled = true;
!Number.isFinite(left) ||
!Number.isFinite(top) || const isValid = hasValidTargetRect(rect);
!Number.isFinite(width) || if (isValid) {
!Number.isFinite(height) targetRect.value = {
) { left: Number(rect.left),
return; top: Number(rect.top),
} width: Number(rect.width),
if (width <= 0 || height <= 0) return; height: Number(rect.height),
targetRect.value = { left, top, width, height }; };
}) }
.exec(); resolve(isValid);
};
try {
const query = instance?.proxy
? uni.createSelectorQuery().in(instance.proxy)
: uni.createSelectorQuery();
query
.select(".target")
.boundingClientRect()
.exec((result) => finish(Array.isArray(result) ? result[0] : null));
} catch {
finish(null);
}
}); });
} }
function handleWindowResize() { function handleWindowResize() {
updateTargetRect(); void updateTargetRect();
} }
function shouldHideRedHit(index) { function shouldHideRedHit(index) {
return !!hiddenRedLatestKey.value && index === props.scores.length - 1; return (
(!!hiddenRedLatestKey.value || pendingShotEffect.value?.team === "red") &&
index === props.scores.length - 1
);
} }
function shouldHideBlueHit(index) { function shouldHideBlueHit(index) {
return !!hiddenBlueLatestKey.value && index === props.blueScores.length - 1; return (
(!!hiddenBlueLatestKey.value || pendingShotEffect.value?.team === "blue") &&
index === props.blueScores.length - 1
);
} }
function showShotFlash(flash) { function showShotFlash(flash) {
const shootData = flash?.shootData; const shootData = flash?.shootData;
if (!shootData) { if (!shootData) {
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
hiddenRedLatestKey.value = ""; hiddenRedLatestKey.value = "";
hiddenBlueLatestKey.value = ""; hiddenBlueLatestKey.value = "";
shotEffect.value = null; shotEffect.value = null;
@@ -231,10 +284,12 @@ function showShotFlash(flash) {
const team = flash.team === "red" ? "red" : "blue"; const team = flash.team === "red" ? "red" : "blue";
if (shouldPlayShotEffect(shootData, team)) { if (shouldPlayShotEffect(shootData, team)) {
triggerShotEffect(team, shootData, flash.key); void prepareShotEffect(team, shootData, flash.key);
return; return;
} }
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
showShotTip(team, shootData); showShotTip(team, shootData);
} }
@@ -250,6 +305,8 @@ watch(
() => props.scores.length, () => props.scores.length,
(newLen, oldLen) => { (newLen, oldLen) => {
if (newLen > oldLen) return; if (newLen > oldLen) return;
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
latestOne.value = null; latestOne.value = null;
hiddenRedLatestKey.value = ""; hiddenRedLatestKey.value = "";
if (shotEffect.value?.team === "red") shotEffect.value = null; if (shotEffect.value?.team === "red") shotEffect.value = null;
@@ -260,6 +317,8 @@ watch(
() => props.blueScores.length, () => props.blueScores.length,
(newLen, oldLen) => { (newLen, oldLen) => {
if (newLen > oldLen) return; if (newLen > oldLen) return;
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
bluelatestOne.value = null; bluelatestOne.value = null;
hiddenBlueLatestKey.value = ""; hiddenBlueLatestKey.value = "";
if (shotEffect.value?.team === "blue") shotEffect.value = null; if (shotEffect.value?.team === "blue") shotEffect.value = null;
@@ -417,11 +476,13 @@ async function onReceiveMessage(message) {
onMounted(() => { onMounted(() => {
uni.$on("socket-inbox", onReceiveMessage); uni.$on("socket-inbox", onReceiveMessage);
updateTargetRect(); void updateTargetRect();
if (uni.onWindowResize) uni.onWindowResize(handleWindowResize); if (uni.onWindowResize) uni.onWindowResize(handleWindowResize);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
if (timer.value) { if (timer.value) {
clearTimeout(timer.value); clearTimeout(timer.value);
timer.value = null; timer.value = null;
@@ -539,6 +600,7 @@ onBeforeUnmount(() => {
</view> </view>
</block> </block>
<BowShotEffect <BowShotEffect
v-if="!shotEffect || !shotEffect.viewportMode"
:shot="shotEffect && shotEffect.shot" :shot="shotEffect && shotEffect.shot"
:playKey="shotEffect ? shotEffect.key : ''" :playKey="shotEffect ? shotEffect.key : ''"
:targetRadius="safeTargetRadius" :targetRadius="safeTargetRadius"
@@ -552,6 +614,20 @@ onBeforeUnmount(() => {
/> />
<image src="https://static.shelingxingqiu.com/shootmini/static/bow-target.png" mode="widthFix" /> <image src="https://static.shelingxingqiu.com/shootmini/static/bow-target.png" mode="widthFix" />
</view> </view>
<BowShotEffect
v-if="shotEffect && shotEffect.viewportMode"
:shot="shotEffect.shot"
:playKey="shotEffect.key"
:targetRadius="safeTargetRadius"
:targetLeft="targetRect.left"
:targetTop="targetRect.top"
:targetWidth="targetRect.width"
:targetHeight="targetRect.height"
:hitOffsetPx="currentHitRadiusPx"
:viewportMode="true"
@impact="shakeTarget"
@complete="completeShotEffect"
/>
<view class="footer"> <view class="footer">
<PointSwitcher <PointSwitcher
:onChange="(val) => (pMode = val)" :onChange="(val) => (pMode = val)"
+1
View File
@@ -1417,6 +1417,7 @@ onShow(() => {
:latestShotFlash="latestShotFlash" :latestShotFlash="latestShotFlash"
:redTeam="redTeam" :redTeam="redTeam"
:blueTeam="blueTeam" :blueTeam="blueTeam"
stable-shot-effect
/> />
<BattleFooter <BattleFooter
v-if="start" v-if="start"
+99 -29
View File
@@ -84,6 +84,10 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
stableShotEffect: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(["shot-effect-complete"]); const emit = defineEmits(["shot-effect-complete"]);
@@ -98,11 +102,13 @@ const dirTimer = ref(null);
const angle = ref(null); const angle = ref(null);
const circleColor = ref(""); const circleColor = ref("");
const shotEffect = ref(null); const shotEffect = ref(null);
const pendingShotEffect = ref(null);
const hiddenLatestKey = ref(""); const hiddenLatestKey = ref("");
const targetShaking = ref(false); const targetShaking = ref(false);
const targetRect = ref({ left: 0, top: 0, width: 0, height: 0 }); const targetRect = ref({ left: 0, top: 0, width: 0, height: 0 });
const shakeTimer = ref(null); const shakeTimer = ref(null);
const instance = getCurrentInstance(); const instance = getCurrentInstance();
let shotEffectRequestGeneration = 0;
const ROUND_TIP_OFFSET_Y = -32; const ROUND_TIP_OFFSET_Y = -32;
const EXPERIENCE_TIP_OFFSET_Y = -68; const EXPERIENCE_TIP_OFFSET_Y = -68;
@@ -245,7 +251,7 @@ function buildShotEffectKey(shot, index) {
].join("-"); ].join("-");
} }
function triggerShotEffect(shot, index) { function triggerShotEffect(shot, index, viewportMode = false) {
const key = buildShotEffectKey(shot, index); const key = buildShotEffectKey(shot, index);
clearTipTimer(); clearTipTimer();
latestOne.value = null; latestOne.value = null;
@@ -254,9 +260,31 @@ function triggerShotEffect(shot, index) {
key, key,
shot, shot,
token: props.shotEffectToken, token: props.shotEffectToken,
viewportMode,
}; };
} }
async function prepareShotEffect(shot, index) {
const requestGeneration = ++shotEffectRequestGeneration;
const key = buildShotEffectKey(shot, index);
pendingShotEffect.value = { generation: requestGeneration, key };
clearTipTimer();
latestOne.value = null;
const viewportMode = props.stableShotEffect
? await updateTargetRect()
: false;
if (requestGeneration !== shotEffectRequestGeneration) {
if (pendingShotEffect.value?.generation === requestGeneration) {
pendingShotEffect.value = null;
}
return;
}
pendingShotEffect.value = null;
triggerShotEffect(shot, index, viewportMode);
}
function completeShotEffect(key) { function completeShotEffect(key) {
if (!shotEffect.value || shotEffect.value.key !== key) return; if (!shotEffect.value || shotEffect.value.key !== key) return;
@@ -272,7 +300,10 @@ function completeShotEffect(key) {
} }
function shouldHideLatestHit(index) { function shouldHideLatestHit(index) {
return !!hiddenLatestKey.value && index === props.scores.length - 1; return (
(!!hiddenLatestKey.value || !!pendingShotEffect.value) &&
index === props.scores.length - 1
);
} }
function shakeTarget() { function shakeTarget() {
@@ -291,36 +322,53 @@ function shakeTarget() {
}); });
} }
function updateTargetRect() { function hasValidTargetRect(rect = targetRect.value) {
nextTick(() => { return (
const query = instance?.proxy Number.isFinite(Number(rect?.left)) &&
? uni.createSelectorQuery().in(instance.proxy) Number.isFinite(Number(rect?.top)) &&
: uni.createSelectorQuery(); Number(rect?.width) > 0 &&
Number(rect?.height) > 0
);
}
query async function updateTargetRect() {
.select(".target") await nextTick();
.boundingClientRect((rect) => {
const left = Number(rect?.left); return new Promise((resolve) => {
const top = Number(rect?.top); let settled = false;
const width = Number(rect?.width); const finish = (rect) => {
const height = Number(rect?.height); if (settled) return;
if ( settled = true;
!Number.isFinite(left) ||
!Number.isFinite(top) || const isValid = hasValidTargetRect(rect);
!Number.isFinite(width) || if (isValid) {
!Number.isFinite(height) targetRect.value = {
) { left: Number(rect.left),
return; top: Number(rect.top),
} width: Number(rect.width),
if (width <= 0 || height <= 0) return; height: Number(rect.height),
targetRect.value = { left, top, width, height }; };
}) }
.exec(); resolve(isValid);
};
try {
const query = instance?.proxy
? uni.createSelectorQuery().in(instance.proxy)
: uni.createSelectorQuery();
query
.select(".target")
.boundingClientRect()
.exec((result) => finish(Array.isArray(result) ? result[0] : null));
} catch {
finish(null);
}
}); });
} }
function handleWindowResize() { function handleWindowResize() {
updateTargetRect(); void updateTargetRect();
} }
watch( watch(
@@ -329,6 +377,8 @@ watch(
if (newVal.length - prevScores.value.length === 1) { if (newVal.length - prevScores.value.length === 1) {
showShotTip(newVal[newVal.length - 1]); showShotTip(newVal[newVal.length - 1]);
} else if (newVal.length < prevScores.value.length) { } else if (newVal.length < prevScores.value.length) {
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
clearTipTimer(); clearTipTimer();
latestOne.value = null; latestOne.value = null;
hiddenLatestKey.value = ""; hiddenLatestKey.value = "";
@@ -349,7 +399,10 @@ watch(
const latestIndex = props.scores.length - 1; const latestIndex = props.scores.length - 1;
const latestShot = props.scores[latestIndex]; const latestShot = props.scores[latestIndex];
if (shouldPlayShotEffect(latestShot)) { if (shouldPlayShotEffect(latestShot)) {
triggerShotEffect(latestShot, latestIndex); void prepareShotEffect(latestShot, latestIndex);
} else {
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
} }
} }
); );
@@ -431,11 +484,13 @@ async function onReceiveMessage(message) {
onMounted(() => { onMounted(() => {
uni.$on("socket-inbox", onReceiveMessage); uni.$on("socket-inbox", onReceiveMessage);
updateTargetRect(); void updateTargetRect();
if (uni.onWindowResize) uni.onWindowResize(handleWindowResize); if (uni.onWindowResize) uni.onWindowResize(handleWindowResize);
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
shotEffectRequestGeneration += 1;
pendingShotEffect.value = null;
clearTipTimer(); clearTipTimer();
if (dirTimer.value) { if (dirTimer.value) {
clearTimeout(dirTimer.value); clearTimeout(dirTimer.value);
@@ -555,6 +610,7 @@ onBeforeUnmount(() => {
</view> </view>
</block> </block>
<BowShotEffect <BowShotEffect
v-if="!shotEffect || !shotEffect.viewportMode"
:shot="shotEffect && shotEffect.shot" :shot="shotEffect && shotEffect.shot"
:playKey="shotEffect ? shotEffect.key : ''" :playKey="shotEffect ? shotEffect.key : ''"
:targetRadius="safeTargetRadius" :targetRadius="safeTargetRadius"
@@ -567,6 +623,20 @@ onBeforeUnmount(() => {
@complete="completeShotEffect" @complete="completeShotEffect"
/> />
</view> </view>
<BowShotEffect
v-if="shotEffect && shotEffect.viewportMode"
:shot="shotEffect.shot"
:playKey="shotEffect.key"
:targetRadius="safeTargetRadius"
:targetLeft="targetRect.left"
:targetTop="targetRect.top"
:targetWidth="targetRect.width"
:targetHeight="targetRect.height"
:hitOffsetPx="currentHitRadiusPx"
:viewportMode="true"
@impact="shakeTarget"
@complete="completeShotEffect"
/>
<view class="footer"> <view class="footer">
<PointSwitcher <PointSwitcher
:onChange="(val) => (pMode = val)" :onChange="(val) => (pMode = val)"
+1
View File
@@ -1330,6 +1330,7 @@ onBeforeUnmount(() => {
:activeSector="precisionRandomBlock" :activeSector="precisionRandomBlock"
:activeRing="precisionRandomRingArea" :activeRing="precisionRandomRingArea"
:showSectorLabels="precisionBlocks > 0" :showSectorLabels="precisionBlocks > 0"
stable-shot-effect
@shot-effect-complete="onShotEffectComplete" @shot-effect-complete="onShotEffectComplete"
/> />
<!-- <view v-if="env !== 'release'" class="highlight-test-actions"> <!-- <view v-if="env !== 'release'" class="highlight-test-actions">