9 Commits

9 changed files with 483 additions and 143 deletions

View File

@@ -34,6 +34,18 @@ const props = defineProps({
type: Boolean,
default: false,
},
targetRadius: {
type: Number,
default: 20,
},
hitRadiusPx: {
type: Number,
default: 2,
},
zoomHitRadiusPx: {
type: Number,
default: 5,
},
});
const pMode = ref(true);
@@ -45,6 +57,8 @@ const timer = ref(null);
const dirTimer = ref(null);
const angle = ref(null);
const circleColor = ref("");
const ROUND_TIP_OFFSET_Y = -32;
const EXPERIENCE_TIP_OFFSET_Y = -68;
watch(
() => props.scores,
@@ -80,13 +94,92 @@ watch(
}
);
function calcRealX(num, offset = 3.4) {
const len = 20.4 + num;
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
const safeTargetRadius = computed(() => {
const radius = Number(props.targetRadius);
return Number.isFinite(radius) && radius > 0 ? radius : 20;
});
const currentHitRadiusPx = computed(() => {
const radius = Number(
pMode.value ? props.zoomHitRadiusPx : props.hitRadiusPx
);
return Number.isFinite(radius) && radius >= 0 ? radius : 0;
});
function getShotPoint(shot, fallbackCenter = false) {
const x = Number(shot?.x);
const y = Number(shot?.y);
if (Number.isFinite(x) && Number.isFinite(y)) return { x, y };
return fallbackCenter ? { x: 0, y: 0 } : null;
}
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}%)`;
function getPointDirection(point) {
if (!point) return null;
const distance = Math.sqrt(point.x * point.x + point.y * point.y);
if (distance === 0) return null;
return {
x: point.x / distance,
y: point.y / distance,
};
}
function formatPxOffset(value) {
if (!value) return "";
const operator = value > 0 ? "+" : "-";
return ` ${operator} ${Math.abs(value)}px`;
}
function formatTargetPosition(percent, offset) {
const pxOffset = formatPxOffset(offset);
return pxOffset ? `calc(${percent}%${pxOffset})` : `${percent}%`;
}
function getTargetPositionStyle(point, offsetPx = 0, extraOffset = {}) {
if (!point) return { display: "none" };
const radius = safeTargetRadius.value;
const diameter = radius * 2;
const direction = getPointDirection(point);
const xOffset = (direction ? direction.x * offsetPx : 0) + (extraOffset.x || 0);
const yOffset = (direction ? -direction.y * offsetPx : 0) + (extraOffset.y || 0);
const leftPercent = ((point.x + radius) / diameter) * 100;
const topPercent = ((radius - point.y) / diameter) * 100;
return {
left: formatTargetPosition(leftPercent, xOffset),
top: formatTargetPosition(topPercent, yOffset),
transform: "translate(-50%, -50%)",
};
}
function getHitStyle(shot) {
const radius = currentHitRadiusPx.value;
const point = getShotPoint(shot);
return {
...getTargetPositionStyle(point, radius),
width: `${radius * 2}px`,
height: `${radius * 2}px`,
};
}
function getRoundTipStyle(shot) {
const point = getShotPoint(shot, true);
return getTargetPositionStyle(
point,
shot?.ring ? currentHitRadiusPx.value : 0,
{ y: ROUND_TIP_OFFSET_Y }
);
}
function getExperienceTipStyle(shot) {
const point = getShotPoint(shot, true);
return getTargetPositionStyle(
point,
shot?.ring ? currentHitRadiusPx.value : 0,
{ y: EXPERIENCE_TIP_OFFSET_Y }
);
}
const simulShoot = async () => {
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
@@ -169,20 +262,14 @@ onBeforeUnmount(() => {
<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),
}"
:style="getExperienceTipStyle(latestOne)"
>
经验 +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),
}"
:style="getRoundTipStyle(latestOne)"
>{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
}}<text v-if="latestOne.ring"></text>
</view>
@@ -193,20 +280,14 @@ onBeforeUnmount(() => {
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),
}"
:style="getExperienceTipStyle(bluelatestOne)"
>
经验 +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),
}"
:style="getRoundTipStyle(bluelatestOne)"
>{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
}}<text v-if="bluelatestOne.ring">环</text></view
>
@@ -217,8 +298,7 @@ onBeforeUnmount(() => {
index === scores.length - 1 && latestOne ? 'pump-in' : ''
}`"
:style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
...getHitStyle(bow),
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
}"
><text v-if="pMode">{{ index + 1 }}</text></view
@@ -231,8 +311,7 @@ onBeforeUnmount(() => {
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
}`"
:style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
...getHitStyle(bow),
backgroundColor: '#1840FF',
}"
>
@@ -292,6 +371,31 @@ onBeforeUnmount(() => {
font-size: 24px;
margin-left: 5px;
}
@keyframes target-tip-fade-in-out {
0% {
transform: translate(-50%, -50%) translateY(20px);
opacity: 0;
}
30% {
transform: translate(-50%, -50%);
opacity: 1;
}
80% {
transform: translate(-50%, -50%);
opacity: 1;
}
100% {
transform: translate(-50%, -50%);
opacity: 0;
}
}
.round-tip.fade-in-out,
.e-value.fade-in-out {
animation: target-tip-fade-in-out 1.2s ease forwards;
}
.target > image:last-child {
width: 100%;
height: 100%;
@@ -302,21 +406,11 @@ onBeforeUnmount(() => {
z-index: 1;
color: #fff;
transition: all 0.3s ease;
}
.s-point {
width: 4px;
height: 4px;
min-width: 4px;
min-height: 4px;
box-sizing: border-box;
}
.b-point {
width: 10px;
height: 10px;
min-width: 10px;
min-height: 10px;
border: 1px solid #fff;
z-index: 1;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
@@ -332,6 +426,19 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%);*/
margin-top: 2rpx;
}
@keyframes target-pump-in {
from {
transform: translate(-50%, -50%) scale(2);
}
to {
transform: translate(-50%, -50%) scale(1);
}
}
.hit.pump-in {
animation: target-pump-in 0.3s ease-out forwards;
transform-origin: center center;
}
.header {
width: 100%;
display: flex;

View File

@@ -142,8 +142,8 @@ onBeforeUnmount(() => {
</view>
<block
v-if="
'-凹造型-感知距离-小试牛刀'.indexOf(title) === -1 ||
'-凹造型-感知距离-小试牛刀'.indexOf(title) === 10
'-箭前准备-感知距离-小试牛刀'.indexOf(title) === -1 ||
'-箭前准备-感知距离-小试牛刀'.indexOf(title) === 11
"
>
<text>{{ title }}</text>
@@ -151,12 +151,12 @@ onBeforeUnmount(() => {
<block
v-if="
title &&
'-凹造型-感知距离-小试牛刀'.indexOf(title) !== -1 &&
'-凹造型-感知距离-小试牛刀'.indexOf(title) !== 10
'-箭前准备-感知距离-小试牛刀'.indexOf(title) !== -1 &&
'-箭前准备-感知距离-小试牛刀'.indexOf(title) !== 11
"
>
<view class="first-try-steps">
<text :class="title === '-凹造型' ? 'current-step' : ''">凹造型</text>
<text :class="title === '-箭前准备' ? 'current-step' : ''">箭前准备</text>
<text>-</text>
<text :class="title === '-感知距离' ? 'current-step' : ''"
>感知距离</text

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref } from "vue";
import { ref, watch } from "vue";
const props = defineProps({
interval: {
@@ -14,13 +14,24 @@ const props = defineProps({
type: Array,
default: () => [],
},
current: {
type: Number,
default: 0,
},
onChange: {
type: Function,
default: (index) => {},
},
});
const currentIndex = ref(0);
const currentIndex = ref(props.current);
watch(
() => props.current,
(index) => {
currentIndex.value = index;
}
);
const handleChange = (e) => {
currentIndex.value = e.detail.current;
@@ -75,7 +86,7 @@ const handleChange = (e) => {
.dots {
position: absolute;
bottom: 5%;
bottom: 2%;
left: 50%;
transform: translateX(-50%);
display: flex;

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
import Guide from "@/components/Guide.vue";
import SButton from "@/components/SButton.vue";
import Swiper from "@/components/Swiper.vue";
@@ -18,6 +18,8 @@ import {
startPractiseAPI,
endPractiseAPI,
getPractiseAPI,
laserAimAPI,
laserCloseAPI,
} from "@/apis";
import { sharePractiseData } from "@/canvas";
import { wxShare, debounce } from "@/util";
@@ -32,6 +34,7 @@ const total = 12;
const stepButtonTexts = [
"开始",
"进入下一个任务",
"我已校准",
"进入下一个任务",
"我准备好了,开始",
"",
@@ -43,8 +46,11 @@ const practiseResult = ref({});
const btnDisabled = ref(false);
const practiseId = ref("");
const showGuide = ref(false);
const laserActive = ref(false);
const guideSwiperIndex = ref(0);
const guideImages = [
"https://static.shelingxingqiu.com/shootmini/static/target.png",
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wwdpgwt9e6du.png",
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wvv9sw4zioqk.png",
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0ww3khaycallu.png",
@@ -54,10 +60,45 @@ const guideImages = [
"https://static.shelingxingqiu.com/attachment/2026-02-08/dg9ev0wwr6hfjhyfn5.png",
];
const calibrationGuides = [
{
title: "箭头面向靶子",
src: "https://static.shelingxingqiu.com/attachment/2025-10-30/ddv9p5fk5wscg7hrfo.png",
},
{
title: "摆出拉弓姿势",
src: "https://static.shelingxingqiu.com/attachment/2025-10-30/ddv9p5fk5b7ljrhx3o.png",
},
{
title: "调整瞄准器",
src: "https://static.shelingxingqiu.com/attachment/2025-10-29/dduexjgrcxf9wjaiv4.png",
},
];
const onSwiperIndexChange = (index) => {
if (index + 1 === guideImages.length) {
showGuide.value = true;
}
guideSwiperIndex.value = index;
showGuide.value = index + 1 === guideImages.length;
};
const isGuideLastImage = computed(
() => guideSwiperIndex.value + 1 === guideImages.length
);
const currentStepButtonText = computed(() => {
if (step.value === 1 && isGuideLastImage.value) return "去校准智能弓";
return stepButtonTexts[step.value];
});
const openCalibrationLaser = async () => {
if (laserActive.value) return;
await laserAimAPI();
laserActive.value = true;
};
const closeCalibrationLaser = async () => {
if (!laserActive.value) return;
await laserCloseAPI();
laserActive.value = false;
};
const createPractise = async (arrows) => {
@@ -75,7 +116,7 @@ async function onReceiveMessage(msg) {
scores.value = msg.details;
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(onOver, 1500);
} else if (msg.type === MESSAGETYPESV2.TestDistance) {
} else if (msg.type === MESSAGETYPESV2.TestDistance && step.value === 3) {
if (msg.shootData.distance / 100 >= 5) {
audioManager.play("距离合格");
btnDisabled.value = false;
@@ -110,12 +151,13 @@ onMounted(() => {
uni.$on("share-image", onClickShare);
});
onBeforeUnmount(() => {
onBeforeUnmount(async () => {
uni.setKeepScreenOn({
keepScreenOn: false,
});
uni.$off("socket-inbox", onReceiveMessage);
uni.$off("share-image", onClickShare);
await closeCalibrationLaser();
audioManager.stopAll();
endPractiseAPI();
});
@@ -123,28 +165,39 @@ onBeforeUnmount(() => {
const nextStep = async () => {
if (step.value === 0) {
step.value = 1;
title.value = "-凹造型";
title.value = "-箭前准备";
} else if (step.value === 1) {
if (!isGuideLastImage.value) {
guideSwiperIndex.value += 1;
showGuide.value = guideSwiperIndex.value + 1 === guideImages.length;
return;
}
showGuide.value = false;
step.value = 2;
// title.value = "-校准智能弓";
await openCalibrationLaser();
} else if (step.value === 2) {
await closeCalibrationLaser();
showGuide.value = false;
btnDisabled.value = true;
step.value = 2;
step.value = 3;
title.value = "-感知距离";
const result = await createPractiseAPI(total, 120);
if (result) practiseId.value = result.id;
} else if (step.value === 2) {
showGuide.value = false;
step.value = 3;
title.value = "-小试牛刀";
} else if (step.value === 3) {
showGuide.value = false;
step.value = 4;
title.value = "-小试牛刀";
} else if (step.value === 4) {
title.value = "小试牛刀";
await startPractiseAPI();
scores.value = [];
step.value = 4;
step.value = 5;
start.value = true;
setTimeout(() => {
uni.$emit("play-sound", "请开始射击");
}, 300);
} else if (step.value === 5) {
} else if (step.value === 6) {
uni.navigateBack({
delta: 1,
});
@@ -159,13 +212,13 @@ const onClose = async () => {
setTimeout(() => {
practiseResult.value = {};
showGuide.value = false;
step.value = 5;
step.value = 6;
}, 500);
} else {
practiseResult.value = {};
start.value = false;
scores.value = [];
step.value = 3;
step.value = 4;
const result = await createPractiseAPI(total, 120);
if (result) practiseId.value = result.id;
}
@@ -173,14 +226,14 @@ const onClose = async () => {
</script>
<template>
<Container :bgType="1" :title="title" :showBottom="step !== 4">
<Container :bgType="1" :title="title" :showBottom="step !== 5">
<view class="container">
<Guide
v-if="step !== 4"
v-if="step !== 5"
:type="
step === 2
step === 3
? 2
: step === 5 || (step === 0 && user.nickName.length > 6)
: step === 6 || (step === 0 && user.nickName.length > 6)
? 1
: 0
"
@@ -196,25 +249,28 @@ const onClose = async () => {
这是新人必刷小任务0基础小白也能快速掌握弓箭技巧和游戏规则哦~
</text>
<text v-if="step === 1" :style="{ fontSize: '28rpx' }"
>这是我们人帅技高的高教练首先请按教练示范尝试自己去做这些动作和手势吧</text
>位就是人帅技高的高教练接下来请跟随教练指引做好射箭前期准备</text
>
<text v-if="step === 2" :style="{ fontSize: '28rpx' }"
>请按下方步骤完成智能弓校准让瞄准器和靶子保持对齐</text
>
<view
class="guide-tips"
:style="{ marginTop: '8rpx' }"
v-if="step === 2"
v-if="step === 3"
>
<text>你知道5米射程有多远吗</text>
<text>
在我们的排位赛中射程小于5米的成绩无效建议平时练习距离至少5米现在来边射箭边调整你的站位点吧
</text>
</view>
<view class="guide-tips" v-if="step === 3">
<view class="guide-tips" v-if="step === 4">
<text>一切准备就绪</text>
<text :style="{ fontSize: '28rpx' }"
>试着完成一个真正的弓箭手任务吧</text
>
</view>
<view class="guide-tips" v-if="step === 5">
<view class="guide-tips" v-if="step === 6">
<text>新手试炼场通关啦优秀</text>
<text :style="{ fontSize: '28rpx' }"
>反曲弓运动基本知识和射灵世界系统规则你已Get是不是挺容易呀</text
@@ -231,35 +287,53 @@ const onClose = async () => {
src="https://static.shelingxingqiu.com/attachment/2025-11-17/deas80ef1sf9td0leq.png"
class="try-tip"
mode="widthFix"
v-if="step === 3"
v-if="step === 4"
/>
<image
src="https://static.shelingxingqiu.com/attachment/2025-07-01/db0ehpz9lav58g5drl.png"
class="try-tip"
mode="widthFix"
v-if="step === 5"
v-if="step === 6"
/>
<view style="height: 570px" v-if="step === 1">
<Swiper :onChange="onSwiperIndexChange" :data="guideImages" />
<Swiper
:current="guideSwiperIndex"
:onChange="onSwiperIndexChange"
:data="guideImages"
/>
</view>
<ShootProgress v-if="step === 4" tips="请开始连续射箭" :start="start" />
<TestDistance v-if="step === 2" :guide="false" />
<view class="calibration-container" v-if="step === 2">
<view
v-for="(guide, index) in calibrationGuides"
:key="guide.title"
class="calibration-guide"
>
<view>
<text>{{ index + 1 }}</text>
<text>{{ guide.title }}</text>
</view>
<image :src="guide.src" mode="widthFix" />
</view>
<text>请完成以上步骤校准智能弓</text>
</view>
<ShootProgress v-if="step === 5" tips="请开始连续射箭" :start="start" />
<TestDistance v-if="step === 3" :guide="false" />
<view
class="user-row"
v-if="step === 4"
:style="{ marginBottom: step === 2 ? '40px' : '0' }"
v-if="step === 5"
:style="{ marginBottom: '0' }"
>
<Avatar :src="user.avatar" :size="35" />
<BowPower />
</view>
<BowTarget
v-if="step === 4"
:currentRound="step === 4 ? scores.length : 0"
:totalRound="step === 4 ? total : 0"
v-if="step === 5"
:currentRound="step === 5 ? scores.length : 0"
:totalRound="step === 5 ? total : 0"
:scores="scores"
/>
<ScorePanel
v-if="step === 4"
v-if="step === 5"
:total="total"
:rowCount="6"
:arrows="scores"
@@ -287,7 +361,7 @@ const onClose = async () => {
step === 1 ? "学会了,我摆得比教练还帅" : "我找到合适的点位了"
}}</text>
</BubbleTip>
{{ stepButtonTexts[step] }}
{{ currentStepButtonText }}
</SButton>
</template>
</Container>
@@ -301,4 +375,43 @@ const onClose = async () => {
width: calc(100% - 20px);
margin: 0 10px;
}
.calibration-container {
display: flex;
flex-direction: column;
align-items: center;
}
.calibration-guide {
display: flex;
flex-direction: column;
align-items: center;
font-size: 26rpx;
color: #ffffff;
margin-bottom: 15rpx;
}
.calibration-guide > view {
width: 100%;
margin: 25rpx 0;
display: flex;
align-items: center;
}
.calibration-guide > view > text:first-child {
font-size: 24rpx;
background: #e89024;
border-radius: 50%;
width: 32rpx;
height: 32rpx;
line-height: 32rpx;
display: block;
text-align: center;
margin-right: 15rpx;
}
.calibration-guide > image {
width: 630rpx;
height: 250rpx;
}
.calibration-container > text {
font-size: 24rpx;
color: #fff9;
margin: 30rpx;
}
</style>

View File

@@ -212,33 +212,35 @@ onShow(() => {
<text>{{ user.nickName }}</text>
</view>
</view>
<block v-if="calibration">
<SButton :onClick="toFristTryPage" width="60vw" :rounded="40"
>进入新手试炼</SButton
>
<view :style="{ marginTop: '15px' }">
<SButton
:onClick="backToHome"
backgroundColor="#fff3"
color="#fff"
width="60vw"
:rounded="40"
>返回首页</SButton
>
</view>
</block>
<block v-else>
<!-- <block v-if="calibration"> -->
<view>
<text>恭喜你的弓箭和账号已成功绑定</text>
<text :style="{ color: '#fed847' }">已赠送6个月射灵世界会员</text>
</view>
<SButton :onClick="goCalibration" width="60vw" :rounded="40">
<!-- <SButton :onClick="goCalibration" width="60vw" :rounded="40">
开启智能弓进行校准
</SButton>
<text :style="{ marginTop: '20rpx', fontSize: '24rpx', color: '#fff9' }"
>校准时弓箭激光将开启请勿直视激光</text
>
</block>
> -->
<view>
<SButton
:onClick="backToHome"
backgroundColor="#fff3"
color="#fff"
width="60vw"
:rounded="40"
>返回首页</SButton
>
</view>
<view :style="{ marginTop: '15px' }">
<SButton :onClick="toFristTryPage" width="60vw" :rounded="40">进入新手试炼</SButton>
</view>
<!-- </block> -->
<!-- <block v-else>
</block> -->
</view>
<view v-if="device.deviceId && !justBind" class="has-device">
<view class="device-binded">
@@ -394,7 +396,7 @@ onShow(() => {
width: 140rpx;
height: 140rpx;
margin-bottom: 5px;
border-radius: 10px;
border-radius: 12px;
}
.device-binded > view > text {
width: 120px;

View File

@@ -38,6 +38,18 @@ const props = defineProps({
type: Boolean,
default: false,
},
targetRadius: {
type: Number,
default: 20,
},
hitRadiusPx: {
type: Number,
default: 2,
},
zoomHitRadiusPx: {
type: Number,
default: 5,
},
});
const pMode = ref(true);
@@ -47,6 +59,8 @@ const timer = ref(null);
const dirTimer = ref(null);
const angle = ref(null);
const circleColor = ref("");
const ROUND_TIP_OFFSET_Y = -32;
const EXPERIENCE_TIP_OFFSET_Y = -68;
function showShotFlash(flash) {
const shootData = flash?.shootData;
@@ -75,13 +89,92 @@ watch(
{ immediate: true }
);
function calcRealX(num, offset = 3.4) {
const len = 20.4 + num;
return `calc(${(len / 40.8) * 100 - offset / 2}%)`;
const safeTargetRadius = computed(() => {
const radius = Number(props.targetRadius);
return Number.isFinite(radius) && radius > 0 ? radius : 20;
});
const currentHitRadiusPx = computed(() => {
const radius = Number(
pMode.value ? props.zoomHitRadiusPx : props.hitRadiusPx
);
return Number.isFinite(radius) && radius >= 0 ? radius : 0;
});
function getShotPoint(shot, fallbackCenter = false) {
const x = Number(shot?.x);
const y = Number(shot?.y);
if (Number.isFinite(x) && Number.isFinite(y)) return { x, y };
return fallbackCenter ? { x: 0, y: 0 } : null;
}
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}%)`;
function getPointDirection(point) {
if (!point) return null;
const distance = Math.sqrt(point.x * point.x + point.y * point.y);
if (distance === 0) return null;
return {
x: point.x / distance,
y: point.y / distance,
};
}
function formatPxOffset(value) {
if (!value) return "";
const operator = value > 0 ? "+" : "-";
return ` ${operator} ${Math.abs(value)}px`;
}
function formatTargetPosition(percent, offset) {
const pxOffset = formatPxOffset(offset);
return pxOffset ? `calc(${percent}%${pxOffset})` : `${percent}%`;
}
function getTargetPositionStyle(point, offsetPx = 0, extraOffset = {}) {
if (!point) return { display: "none" };
const radius = safeTargetRadius.value;
const diameter = radius * 2;
const direction = getPointDirection(point);
const xOffset = (direction ? direction.x * offsetPx : 0) + (extraOffset.x || 0);
const yOffset = (direction ? -direction.y * offsetPx : 0) + (extraOffset.y || 0);
const leftPercent = ((point.x + radius) / diameter) * 100;
const topPercent = ((radius - point.y) / diameter) * 100;
return {
left: formatTargetPosition(leftPercent, xOffset),
top: formatTargetPosition(topPercent, yOffset),
transform: "translate(-50%, -50%)",
};
}
function getHitStyle(shot) {
const radius = currentHitRadiusPx.value;
const point = getShotPoint(shot);
return {
...getTargetPositionStyle(point, radius),
width: `${radius * 2}px`,
height: `${radius * 2}px`,
};
}
function getRoundTipStyle(shot) {
const point = getShotPoint(shot, true);
return getTargetPositionStyle(
point,
shot?.ring ? currentHitRadiusPx.value : 0,
{ y: ROUND_TIP_OFFSET_Y }
);
}
function getExperienceTipStyle(shot) {
const point = getShotPoint(shot, true);
return getTargetPositionStyle(
point,
shot?.ring ? currentHitRadiusPx.value : 0,
{ y: EXPERIENCE_TIP_OFFSET_Y }
);
}
const simulShoot = async () => {
if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
@@ -164,20 +257,14 @@ onBeforeUnmount(() => {
<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),
}"
:style="getExperienceTipStyle(latestOne)"
>
经验 +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),
}"
:style="getRoundTipStyle(latestOne)"
>{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
}}<text v-if="latestOne.ring"></text>
</view>
@@ -188,20 +275,14 @@ onBeforeUnmount(() => {
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),
}"
:style="getExperienceTipStyle(bluelatestOne)"
>
经验 +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),
}"
:style="getRoundTipStyle(bluelatestOne)"
>{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
}}<text v-if="bluelatestOne.ring">环</text></view
>
@@ -212,8 +293,7 @@ onBeforeUnmount(() => {
index === scores.length - 1 && latestOne ? 'pump-in' : ''
}`"
:style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
...getHitStyle(bow),
backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
}"
><text v-if="pMode">{{ index + 1 }}</text></view
@@ -226,8 +306,7 @@ onBeforeUnmount(() => {
index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
}`"
:style="{
left: calcRealX(bow.x, pMode ? '3.4' : '2'),
top: calcRealY(bow.y, pMode ? '3.4' : '2'),
...getHitStyle(bow),
backgroundColor: '#1840FF',
}"
>
@@ -287,6 +366,31 @@ onBeforeUnmount(() => {
font-size: 24px;
margin-left: 5px;
}
@keyframes target-tip-fade-in-out {
0% {
transform: translate(-50%, -50%) translateY(20px);
opacity: 0;
}
30% {
transform: translate(-50%, -50%);
opacity: 1;
}
80% {
transform: translate(-50%, -50%);
opacity: 1;
}
100% {
transform: translate(-50%, -50%);
opacity: 0;
}
}
.round-tip.fade-in-out,
.e-value.fade-in-out {
animation: target-tip-fade-in-out 1.2s ease forwards;
}
.target > image:last-child {
width: 100%;
height: 100%;
@@ -297,21 +401,11 @@ onBeforeUnmount(() => {
z-index: 1;
color: #fff;
transition: all 0.3s ease;
}
.s-point {
width: 4px;
height: 4px;
min-width: 4px;
min-height: 4px;
box-sizing: border-box;
}
.b-point {
width: 10px;
height: 10px;
min-width: 10px;
min-height: 10px;
border: 1px solid #fff;
z-index: 1;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
@@ -327,6 +421,19 @@ onBeforeUnmount(() => {
transform: translate(-50%, -50%);*/
margin-top: 2rpx;
}
@keyframes target-pump-in {
from {
transform: translate(-50%, -50%) scale(2);
}
to {
transform: translate(-50%, -50%) scale(1);
}
}
.hit.pump-in {
animation: target-pump-in 0.3s ease-out forwards;
transform-origin: center center;
}
.header {
width: 100%;
display: flex;

View File

@@ -142,8 +142,8 @@ onBeforeUnmount(() => {
</view>
<block
v-if="
'-凹造型-感知距离-小试牛刀'.indexOf(title) === -1 ||
'-凹造型-感知距离-小试牛刀'.indexOf(title) === 10
'-箭前准备-感知距离-小试牛刀'.indexOf(title) === -1 ||
'-箭前准备-感知距离-小试牛刀'.indexOf(title) === 11
"
>
<text>{{ title }}</text>
@@ -151,12 +151,12 @@ onBeforeUnmount(() => {
<block
v-if="
title &&
'-凹造型-感知距离-小试牛刀'.indexOf(title) !== -1 &&
'-凹造型-感知距离-小试牛刀'.indexOf(title) !== 10
'-箭前准备-感知距离-小试牛刀'.indexOf(title) !== -1 &&
'-箭前准备-感知距离-小试牛刀'.indexOf(title) !== 11
"
>
<view class="first-try-steps">
<text :class="title === '-凹造型' ? 'current-step' : ''">凹造型</text>
<text :class="title === '-箭前准备' ? 'current-step' : ''">箭前准备</text>
<text>-</text>
<text :class="title === '-感知距离' ? 'current-step' : ''"
>感知距离</text

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 12 KiB