update:会员特效

This commit is contained in:
2026-06-11 16:09:20 +08:00
parent 5581c117e2
commit 68f13910a3
6 changed files with 769 additions and 29 deletions

View File

@@ -1,6 +1,15 @@
<script setup>
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
import {
ref,
watch,
onMounted,
onBeforeUnmount,
computed,
nextTick,
getCurrentInstance,
} from "vue";
import PointSwitcher from "./PointSwitcher.vue";
import BowShotEffect from "@/components/BowShotEffect.vue";
import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
import { simulShootAPI } from "@/apis";
@@ -59,18 +68,47 @@ const timer = ref(null);
const dirTimer = ref(null);
const angle = ref(null);
const circleColor = ref("");
const shotEffect = ref(null);
const hiddenRedLatestKey = ref("");
const hiddenBlueLatestKey = ref("");
const targetShaking = ref(false);
const targetSize = ref({ width: 0, height: 0 });
const shakeTimer = ref(null);
const instance = getCurrentInstance();
const ROUND_TIP_OFFSET_Y = -32;
const EXPERIENCE_TIP_OFFSET_Y = -68;
function showShotFlash(flash) {
const shootData = flash?.shootData;
if (!shootData) return;
if (timer.value) clearTimeout(timer.value);
function buildShotEffectKey(team, shot, fallbackKey = "") {
return (
fallbackKey ||
[
team,
shot?.playerId ?? "",
shot?.x ?? "",
shot?.y ?? "",
shot?.ring ?? "",
shot?.ringX ? 1 : 0,
].join("-")
);
}
if (flash.team === "red") {
function shouldPlayShotEffect(shot) {
return !!shot && Number(shot.ring) > 0;
}
function clearTipTimer() {
if (timer.value) clearTimeout(timer.value);
timer.value = null;
}
function showShotTip(team, shootData) {
clearTipTimer();
if (team === "red") {
latestOne.value = shootData;
timer.value = setTimeout(() => {
latestOne.value = null;
timer.value = null;
}, 1000);
return;
}
@@ -78,9 +116,102 @@ function showShotFlash(flash) {
bluelatestOne.value = shootData;
timer.value = setTimeout(() => {
bluelatestOne.value = null;
timer.value = null;
}, 1000);
}
function triggerShotEffect(team, shot, fallbackKey = "") {
const key = buildShotEffectKey(team, shot, fallbackKey);
if (shotEffect.value?.team === "red") hiddenRedLatestKey.value = "";
if (shotEffect.value?.team === "blue") hiddenBlueLatestKey.value = "";
if (team === "red") {
latestOne.value = null;
hiddenRedLatestKey.value = key;
} else {
bluelatestOne.value = null;
hiddenBlueLatestKey.value = key;
}
shotEffect.value = { key, team, shot };
}
function completeShotEffect(key) {
if (!shotEffect.value || shotEffect.value.key !== key) return;
const { team, shot } = shotEffect.value;
if (team === "red") hiddenRedLatestKey.value = "";
if (team === "blue") hiddenBlueLatestKey.value = "";
shotEffect.value = null;
showShotTip(team, shot);
}
function shakeTarget() {
targetShaking.value = false;
if (shakeTimer.value) {
clearTimeout(shakeTimer.value);
shakeTimer.value = null;
}
nextTick(() => {
targetShaking.value = true;
shakeTimer.value = setTimeout(() => {
targetShaking.value = false;
shakeTimer.value = null;
}, 260);
});
}
function updateTargetSize() {
nextTick(() => {
const query = instance?.proxy
? uni.createSelectorQuery().in(instance.proxy)
: uni.createSelectorQuery();
query
.select(".target")
.boundingClientRect((rect) => {
const width = Number(rect?.width);
const height = Number(rect?.height);
if (!Number.isFinite(width) || !Number.isFinite(height)) return;
if (width <= 0 || height <= 0) return;
targetSize.value = { width, height };
})
.exec();
});
}
function handleWindowResize() {
updateTargetSize();
}
function shouldHideRedHit(index) {
return !!hiddenRedLatestKey.value && index === props.scores.length - 1;
}
function shouldHideBlueHit(index) {
return !!hiddenBlueLatestKey.value && index === props.blueScores.length - 1;
}
function showShotFlash(flash) {
const shootData = flash?.shootData;
if (!shootData) {
hiddenRedLatestKey.value = "";
hiddenBlueLatestKey.value = "";
shotEffect.value = null;
return;
}
const team = flash.team === "red" ? "red" : "blue";
if (shouldPlayShotEffect(shootData)) {
triggerShotEffect(team, shootData, flash.key);
return;
}
showShotTip(team, shootData);
}
watch(
() => props.latestShotFlash,
(newVal) => {
@@ -89,6 +220,26 @@ watch(
{ immediate: true }
);
watch(
() => props.scores.length,
(newLen, oldLen) => {
if (newLen > oldLen) return;
latestOne.value = null;
hiddenRedLatestKey.value = "";
if (shotEffect.value?.team === "red") shotEffect.value = null;
}
);
watch(
() => props.blueScores.length,
(newLen, oldLen) => {
if (newLen > oldLen) return;
bluelatestOne.value = null;
hiddenBlueLatestKey.value = "";
if (shotEffect.value?.team === "blue") shotEffect.value = null;
}
);
const safeTargetRadius = computed(() => {
const radius = Number(props.targetRadius);
return Number.isFinite(radius) && radius > 0 ? radius : 20;
@@ -223,6 +374,8 @@ async function onReceiveMessage(message) {
onMounted(() => {
uni.$on("socket-inbox", onReceiveMessage);
updateTargetSize();
if (uni.onWindowResize) uni.onWindowResize(handleWindowResize);
});
onBeforeUnmount(() => {
@@ -234,12 +387,17 @@ onBeforeUnmount(() => {
clearTimeout(dirTimer.value);
dirTimer.value = null;
}
if (shakeTimer.value) {
clearTimeout(shakeTimer.value);
shakeTimer.value = null;
}
uni.$off("socket-inbox", onReceiveMessage);
if (uni.offWindowResize) uni.offWindowResize(handleWindowResize);
});
</script>
<template>
<view class="container">
<view :class="['container', { 'container--effecting': shotEffect }]">
<view class="header" v-if="totalRound > 0">
<text v-if="totalRound > 0" class="round-count">{{
(currentRound > totalRound ? totalRound : currentRound) +
@@ -247,7 +405,7 @@ onBeforeUnmount(() => {
totalRound
}}</text>
</view>
<view class="target">
<view :class="['target', { 'target--shake': targetShaking }]">
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
<view :style="{ background: circleColor }">
<image src="../../../static/dot-circle.png" mode="widthFix" />
@@ -288,7 +446,7 @@ onBeforeUnmount(() => {
>
<block v-for="(bow, index) in scores" :key="index">
<view
v-if="bow.ring > 0"
v-if="bow.ring > 0 && !shouldHideRedHit(index)"
:class="`hit ${pMode ? 'b' : 's'}-point ${
index === scores.length - 1 && latestOne ? 'pump-in' : ''
}`"
@@ -301,7 +459,7 @@ onBeforeUnmount(() => {
</block>
<block v-for="(bow, index) in blueScores" :key="index">
<view
v-if="bow.ring > 0"
v-if="bow.ring > 0 && !shouldHideBlueHit(index)"
:class="`hit ${pMode ? 'b' : 's'}-point ${
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
}`"
@@ -313,6 +471,16 @@ onBeforeUnmount(() => {
<text v-if="pMode">{{ index + 1 }}</text>
</view>
</block>
<BowShotEffect
:shot="shotEffect && shotEffect.shot"
:playKey="shotEffect ? shotEffect.key : ''"
:targetRadius="safeTargetRadius"
:targetWidth="targetSize.width"
:targetHeight="targetSize.height"
:hitOffsetPx="currentHitRadiusPx"
@impact="shakeTarget"
@complete="completeShotEffect"
/>
<image src="../../../static/bow-target.png" mode="widthFix" />
</view>
<view class="footer">
@@ -334,13 +502,22 @@ onBeforeUnmount(() => {
height: calc(100vw - 30px);
padding: 0px 15px;
position: relative;
z-index: 3;
}
.container--effecting {
z-index: 10000;
}
.target {
position: relative;
margin: 10px;
width: calc(100% - 20px);
height: calc(100% - 20px);
z-index: -1;
z-index: 1;
pointer-events: none;
transform-origin: center center;
}
.target--shake {
animation: target-shake 0.26s ease-out;
}
.e-value {
position: absolute;
@@ -400,7 +577,7 @@ onBeforeUnmount(() => {
border-radius: 50%;
z-index: 1;
color: #fff;
transition: all 0.3s ease;
transition: transform 0.2s ease, opacity 0.2s ease;
box-sizing: border-box;
}
.b-point {
@@ -430,6 +607,29 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes target-shake {
0% {
transform: translate(0, 0);
}
14% {
transform: translate(-20rpx, 8rpx);
}
28% {
transform: translate(16rpx, -8rpx);
}
44% {
transform: translate(-12rpx, 6rpx);
}
64% {
transform: translate(8rpx, -4rpx);
}
82% {
transform: translate(-4rpx, 2rpx);
}
100% {
transform: translate(0, 0);
}
}
.hit.pump-in {
animation: target-pump-in 0.3s ease-out forwards;
transform-origin: center center;
@@ -457,6 +657,8 @@ onBeforeUnmount(() => {
display: flex;
margin-top: -40px;
justify-content: flex-end;
position: relative;
z-index: 999;
}
.footer > image {
width: 40px;