Files
shoot-miniprograms/src/components/BowTarget.vue
2025-12-05 15:24:14 +08:00

455 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
import StartCountdown from "@/components/StartCountdown.vue";
import { MESSAGETYPES } from "@/constants";
import { simulShootAPI } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, device } = storeToRefs(store);
const props = defineProps({
currentRound: {
type: Number,
default: 0,
},
totalRound: {
type: Number,
default: 0,
},
avatar: {
type: String,
default: "",
},
scores: {
type: Array,
default: () => [],
},
blueScores: {
type: Array,
default: () => [],
},
mode: {
type: String,
default: "solo", // solo 单排team 双排
},
// start: {
// type: Boolean,
// default: false,
// },
stop: {
type: Boolean,
default: false,
},
});
const latestOne = ref(null);
const bluelatestOne = ref(null);
const prevScores = ref([]);
const prevBlueScores = ref([]);
const timer = ref(null);
const dirTimer = ref(null);
const angle = ref(null);
const circleColor = ref("");
watch(
() => props.scores,
(newVal) => {
if (newVal.length - prevScores.value.length === 1) {
latestOne.value = newVal[newVal.length - 1];
if (timer.value) clearTimeout(timer.value);
timer.value = setTimeout(() => {
latestOne.value = null;
}, 1000);
}
prevScores.value = [...newVal];
},
{
deep: true,
}
);
watch(
() => props.blueScores,
(newVal) => {
if (newVal.length - prevBlueScores.value.length === 1) {
bluelatestOne.value = newVal[newVal.length - 1];
if (timer.value) clearTimeout(timer.value);
timer.value = setTimeout(() => {
bluelatestOne.value = null;
}, 1000);
}
prevBlueScores.value = [...newVal];
},
{
deep: true,
}
);
function calcRealX(num, offset = 3.4) {
const len = 20.4 + num;
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
}
function calcRealY(num, offset = 3.4) {
const len = num < 0 ? Math.abs(num) + 20.4 : 20.4 - num;
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
}
const simulShoot = async () => {
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
};
const simulShoot2 = async () => {
if (device.value.deviceId) await simulShootAPI(device.value.deviceId, 1, 1);
};
const env = computed(() => {
const accountInfo = uni.getAccountInfoSync();
return accountInfo.miniProgram.envVersion;
});
const arrowStyle = computed(() => {
return {
transform: `rotateX(180deg) translate(-50%, -50%) rotate(${
360 - angle.value
}deg) translateY(105%)`,
};
});
async function onReceiveMessage(messages = []) {
messages.forEach((msg) => {
if (
msg.constructor === MESSAGETYPES.ShootSyncMeArrowID ||
msg.constructor === MESSAGETYPES.ShootResult
) {
if (
msg.userId === user.value.id &&
!msg.target.ring &&
msg.target.angle >= 0
) {
angle.value = null;
setTimeout(() => {
if (props.scores[0]) {
circleColor.value =
msg.userId === props.scores[0].playerId ? "#ff4444" : "#1840FF";
}
angle.value = msg.target.angle;
}, 200);
}
}
});
}
onMounted(() => {
uni.$on("socket-inbox", onReceiveMessage);
});
onBeforeUnmount(() => {
if (timer.value) {
clearTimeout(timer.value);
timer.value = null;
}
if (dirTimer.value) {
clearTimeout(dirTimer.value);
dirTimer.value = null;
}
uni.$off("socket-inbox", onReceiveMessage);
});
</script>
<template>
<view class="container">
<view class="header" v-if="totalRound > 0">
<text v-if="totalRound > 0" class="round-count">{{
(currentRound > totalRound ? totalRound : currentRound) +
"/" +
totalRound
}}</text>
</view>
<view class="target">
<view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
<view :style="{ background: circleColor }">
<image src="../static/dot-circle.png" mode="widthFix" />
</view>
</view>
<view v-if="stop" class="stop-sign">中场休息</view>
<view
v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
class="e-value fade-in-out"
:style="{
left: calcRealX(latestOne.ring ? latestOne.x : 0, 20),
top: calcRealY(latestOne.ring ? latestOne.y : 0, 40),
}"
>
经验 +1
</view>
<view
v-if="latestOne"
class="round-tip fade-in-out"
:style="{
left: calcRealX(latestOne.ring ? latestOne.x : 0, 28),
top: calcRealY(latestOne.ring ? latestOne.y : 0, 28),
}"
>{{ latestOne.ring || "未上靶" }}<text v-if="latestOne.ring"></text>
</view>
<view
v-if="
bluelatestOne &&
bluelatestOne.ring &&
user.id === bluelatestOne.playerId
"
class="e-value fade-in-out"
:style="{
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 20),
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 40),
}"
>
经验 +1
</view>
<view
v-if="bluelatestOne"
class="round-tip fade-in-out"
:style="{
left: calcRealX(bluelatestOne.ring ? bluelatestOne.x : 0, 28),
top: calcRealY(bluelatestOne.ring ? bluelatestOne.y : 0, 28),
}"
>{{ bluelatestOne.ring || "未上靶"
}}<text v-if="bluelatestOne.ring">环</text></view
>
<block v-for="(bow, index) in scores" :key="index">
<view
v-if="bow.ring > 0"
:class="`hit ${
index === scores.length - 1 && latestOne ? 'pump-in' : ''
}`"
:style="{
left: calcRealX(bow.x),
top: calcRealY(bow.y),
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
}"
><text>{{ index + 1 }}</text></view
>
</block>
<block v-for="(bow, index) in blueScores" :key="index">
<view
v-if="bow.ring > 0"
:class="`hit ${
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
}`"
:style="{
left: calcRealX(bow.x),
top: calcRealY(bow.y),
backgroundColor: '#1840FF',
}"
>
<text>{{ index + 1 }}</text>
</view>
</block>
<image src="../static/bow-target.png" mode="widthFix" />
</view>
<view v-if="avatar" class="footer">
<image :src="avatar" mode="widthFix" />
</view>
<view class="simul" v-if="env !== 'release'">
<button @click="simulShoot">模拟</button>
<button @click="simulShoot2">射箭</button>
</view>
<!-- <text :style="{ color: '#fff', wordBreak: 'break-all' }">{{
scores.length ? scores[scores.length - 1] : ""
}}</text> -->
<!-- <StartCountdown :start="startCount" /> -->
</view>
</template>
<style scoped>
.container {
width: calc(100vw - 30px);
height: calc(100vw - 30px);
padding: 0px 15px;
position: relative;
}
.target {
position: relative;
margin: 10px;
width: calc(100% - 20px);
height: calc(100% - 20px);
}
.e-value {
position: absolute;
/* top: 30%;
left: 60%; */
background-color: #0006;
color: #fff;
font-size: 12px;
padding: 4px 7px;
border-radius: 5px;
z-index: 2;
width: 50px;
text-align: center;
}
.round-tip {
position: absolute;
/* top: 38%; */
/* left: 60%; */
color: #fff;
font-size: 30px;
font-weight: bold;
z-index: 2;
width: 100px;
text-align: center;
}
.round-tip > text {
font-size: 24px;
margin-left: 5px;
}
.target > image:last-child {
width: 100%;
height: 100%;
}
.hit {
position: absolute;
width: 10px;
height: 10px;
min-width: 10px;
min-height: 10px;
border-radius: 50%;
border: 1px solid #fff;
z-index: 1;
color: #fff;
box-sizing: border-box;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
display: flex;
justify-content: center;
align-items: center;
}
.hit > text {
font-size: 16rpx;
font-family: "DINCondensed", "PingFang SC", "Helvetica Neue", Arial,
sans-serif;
text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin-top: 1px;
}
.header {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: -40px;
}
.header > image:first-child {
width: 40px;
height: 40px;
}
.round-count {
font-size: 20px;
color: #fed847;
top: 75px;
font-weight: bold;
}
.footer {
width: calc(100% - 20px);
padding: 0 10px;
display: flex;
margin-top: -40px;
}
.footer > image {
width: 40px;
min-height: 40px;
max-height: 40px;
border-radius: 50%;
border: 1px solid #fff;
}
.simul {
position: absolute;
bottom: 40px;
right: 20px;
margin-left: 20px;
z-index: 999;
}
.simul > button {
color: #fff;
}
.stop-sign {
position: absolute;
font-size: 44px;
color: #fff9;
text-align: center;
width: 200px;
height: 60px;
left: calc(50% - 100px);
top: calc(50% - 30px);
z-index: 99;
font-weight: bold;
}
.arrow-dir {
position: absolute;
width: 100%;
height: 52%;
left: 50%;
bottom: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.arrow-dir > view {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
}
.arrow-dir > view > image {
width: 100rpx;
height: 100rpx;
transform: translate(-30%, -30%);
}
@keyframes spring-in {
0% {
transform: scale(2);
opacity: 0.4;
}
15% {
transform: scale(3);
opacity: 1;
}
30% {
transform: scale(2);
opacity: 0.4;
}
45% {
transform: scale(3);
opacity: 1;
}
60% {
transform: scale(2);
opacity: 0.4;
}
75% {
transform: scale(3);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
@keyframes disappear {
0% {
opacity: 1;
}
75% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.arrow-dir > view {
animation: disappear 3s ease forwards;
}
.arrow-dir > view > image {
animation: spring-in 3s ease forwards;
width: 100%;
}
</style>