455 lines
10 KiB
Vue
455 lines
10 KiB
Vue
<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>
|