2025-07-21 14:03:08 +08:00

1293 lines
38 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 lang="ts">
import { computed, onMounted, ref, useTemplateRef, watch, type Ref } from 'vue';
import assets from '../assets';
import AniEle from '../components/AniEle.vue';
const shootNotice = useTemplateRef('shoot-notice');
defineExpose({
init: () => {
shootNotice.value?.jumpTo('蓄力');
}
})
const props = defineProps<{
userdata: {
region: string;
store: string;
username: string;
}
}>();
const sunEle = useTemplateRef('sun-ani');
const sunEndEle = useTemplateRef('sun-ani-end');
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const emit = defineEmits(['cloudUp', 'cloudDown', 'restart']);
async function shoot() {
//sunEle.value?.jumpTo('天上飞')
sunEle.value?.jumpTo('蓄力飞')
await document.querySelector('.sun-ani-wrapper')?.animate([
{ transform: 'translateY(0) translateX(-50%)' },
{ transform: 'translateY(8%) translateX(-50%)' },
], {
duration: 900,
easing: 'linear',
fill: 'forwards',
}).finished
document.querySelector('.arrow')?.animate([
{ opacity: 1 },
{ opacity: 0 },
], {
duration: 500,
easing: 'ease-in',
fill: 'forwards',
}).finished.then(() => {
document.querySelector('.arrow')?.remove();
});
emit('cloudUp');
document.querySelector('.bed')?.animate([
{ transform: 'translateY(0)' },
{ transform: 'translateY(150%)' },
], {
duration: 500,
delay: 200,
easing: 'cubic-bezier(0.4, 0, 1, 0.5)',
fill: 'forwards',
});
await document.querySelector('.sun-ani-wrapper')?.animate([
{ transform: 'translateY(8%) translateX(-50%)', width: '60vw' },
{ transform: 'translateY(-180%) translateX(-50%)', width: '50vw' },
], {
duration: 3200,
easing: 'cubic-bezier(0.99, 0.13, 0.35, 0.74)',
fill: 'forwards',
}).finished
emit('cloudDown');
await document.querySelector('.sun-ani-wrapper')?.animate([
{ transform: 'translateY(-180%) translateX(-50%)' },
{ transform: 'translateY(-70%) translateX(-50%)' },
], {
duration: 1200,
easing: 'cubic-bezier(0.22, 0.61, 0.36, 1)',
fill: 'forwards',
}).finished
/* await wait(1000) */
canAction.value = true;
gameLoop();
}
const gPos: Ref<'left' | 'center' | 'right'> = ref('center');
const gDeg = computed(() => ({
left: 294 - 360,
center: 326 - 360,
right: 8
})[gPos.value]);
const SPEED = 1.8;
const transitionOn = ref(true);
let lastTime = 0;
const calcGiftDis = () => {
if (giftPos.value[0] !== null && giftPos.value[1] !== null) {
const sunX = sunPos.value[0];
const sunY = sunPos.value[1];
const giftX = giftPos.value[0];
const giftY = giftPos.value[1];
// 计算太阳和礼物之间的距离
const distance = Math.sqrt(Math.pow(sunX - giftX, 2) + Math.pow(sunY - giftY, 2));
return distance;
} else return 0;
}
const collisionThreshold = 10; // vw 单位
const isPlayingDropAni = ref(false);
function gameLoop(currentTime = 0) {
let pauseFlag = false;
if (giftShow.value) {
giftProgress.value++
giftPos.value = [null, null];
giftShow.value = false;
lastTime = currentTime;
if (giftProgress.value >= giftList.length) {
// 游戏结束逻辑
console.log('游戏结束!耗时:', timeSpent.value / 1000, '秒');
gameEnd()
return;
}
}
if (lastTime === 0) lastTime = currentTime;
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
timeSpent.value += deltaTime
// 移动距离 = 速度 * 时间间隔 / 16.67 (目标帧率约60fps)
if (sunPos.value[1] < -50) {
transitionOn.value = false;
if (!isPlayingDropAni.value)
(async () => {
isPlayingDropAni.value = true;
// sunPos.y to -150, than y to 174 and drop to 124
transitionOn.value = true;
while (sunPos.value[1] > -150) {
// 逐渐加速
sunPos.value[1] -= SPEED * (deltaTime / 16.67) * 4;
await wait(16.67 / 2);
}
sunPos.value[1] = -150;
// 等待0.5秒
transitionOn.value = false;
await wait(500);
sunPos.value[1] = 124;
await wait(0);
transitionOn.value = true;
while (sunPos.value[1] > 66) {
sunPos.value[1] -= SPEED * (deltaTime / 16.67) * 6;
await wait(16.67 / 2);
}
// 最终位置 y = 66
sunPos.value[1] = 66;
isPlayingDropAni.value = false;
})();
} else if (!isPlayingDropAni.value) {
transitionOn.value = true;
sunPos.value[1] -= SPEED * 0.3 * (deltaTime / 16.67);
}
const moveGift = () => {
if (giftPos.value[0] === null) {
// 一直生成礼物,直到距离大于 collisionThreshold * 1.5
while (true) {
// 生成起始点
giftStartPos.value[0] = Math.random() * 60 - 30;
giftStartPos.value[1] = Math.random() * 80 - 20;
// 生成终点
giftEndPos.value[0] = Math.random() * 60 - 30;
giftEndPos.value[1] = Math.random() * 80 - 20;
// 初始位置为起始点
giftPos.value[0] = giftStartPos.value[0];
giftPos.value[1] = giftStartPos.value[1];
// 重置移动进度和方向
giftMoveProgress.value = 0;
giftMoveDirection.value = 1;
if (calcGiftDis() > collisionThreshold * 1.5) {
break; // 距离足够远,退出循环
}
}
}
// gift move between two points
if (giftPos.value[0] !== null && giftPos.value[1] !== null) {
const giftSpeed = 0.8 * (deltaTime / 16.67); // 速度加快 (原来是0.3)
// 更新移动进度
giftMoveProgress.value += giftSpeed * giftMoveDirection.value * 0.01;
// 检查是否到达端点,如果是则反转方向
if (giftMoveProgress.value >= 1) {
giftMoveProgress.value = 1;
giftMoveDirection.value = -1;
} else if (giftMoveProgress.value <= 0) {
giftMoveProgress.value = 0;
giftMoveDirection.value = 1;
}
// 使用线性插值计算当前位置
giftPos.value[0] = giftStartPos.value[0] + (giftEndPos.value[0] - giftStartPos.value[0]) * giftMoveProgress.value;
giftPos.value[1] = giftStartPos.value[1] + (giftEndPos.value[1] - giftStartPos.value[1]) * giftMoveProgress.value;
}
}
moveGift();
// 计算太阳和礼物之间的距离
const distance = calcGiftDis()
// 碰撞检测阈值(可以根据实际元素大小调整)
if (distance < collisionThreshold) {
// 太阳和礼物碰撞了
console.log('太阳收集到礼物!', { sunPos: sunPos.value, giftPos: giftPos.value, distance });
pauseFlag = true; // 暂停游戏循环
giftShow.value = true; // 显示展示礼物界面
document.querySelector('.gift-item.show')?.animate([
{ transform: 'scale(1)' },
{ transform: 'scale(0)' },
], {
duration: 900,
easing: 'cubic-bezier(0.42,-0.98, 0.4, 1)',
fill: 'forwards',
}).finished.then(() => {
giftPos.value = [null, null]; // 重置游戏中礼物
});
}
if (!pauseFlag) requestAnimationFrame(gameLoop);
}
function gToggle(pos: 'left' | 'center' | 'right') {
gPos.value = pos;
document.querySelector('.p-machine')?.animate([
{ scale: 1, },
{ scale: 1.2, },
{ scale: 1, },
], {
duration: 200,
easing: 'ease-in-out',
fill: 'forwards',
});
if (isPlayingDropAni.value) {
// 如果正在下落动画中,直接返回
return;
}
if (sunPos.value[0] < -36
|| sunPos.value[0] > 36
|| sunPos.value[1] > 70) {
// 太阳位置不在范围内,直接返回
return;
}
const windAniMap: Record<'left' | 'center' | 'right', [number, [number, number], [number, number]]> = {
left: [
-60,
[70, 0],
[0, -60],
],
center: [
-30,
[0, 0],
[0, -60],
],
right: [
0,
[-70, 0],
[30, -60],
],
};
const windAni = windAniMap[pos];
const windEl = document.querySelector('.wind') as HTMLElement | null;
if (windEl) {
windEl.style.rotate = `${windAni[0]}deg`;
}
windEl?.animate([
{ left: windAni[1][0] + 'vw', top: windAni[1][1] + 'vw', opacity: 0 },
{ opacity: 1 },
{ left: windAni[2][0] + 'vw', top: windAni[2][1] + 'vw', opacity: 0 },
], {
duration: 500,
easing: 'ease-in-out',
fill: 'forwards',
});
({
'left': [-2, 4],
'center': [0, 6],
'right': [2, 4],
})[pos].forEach((v, i) => {
sunPos.value[i] += v * SPEED * 2;
});
if (sunPos.value[0] < -36) {
sunPos.value[0] = -36;
} else if (sunPos.value[0] > 36) {
sunPos.value[0] = 36;
}
}
const giftShow = ref(false);
const canAction = ref(false);
const sunPos = ref([0, 0]) // x, y
const giftPos: Ref<[number | null, number | null]> = ref([null, null]); // x, y
const giftStartPos: Ref<[number, number]> = ref([0, 0]); // 起始点
const giftEndPos: Ref<[number, number]> = ref([0, 0]); // 终点
const giftMoveProgress = ref(0); // 移动进度 0-1
const giftMoveDirection = ref(1); // 移动方向 1正向 -1反向
const giftList = assets.icons;
const giftProgress = ref(0); // max = giftList.length - 1
const isGameEnd = ref(false);
const timeSpent = ref(0)
// 生成图标位置确保之间距离不小于16
const generateIconPositions = () => {
const icons = [...giftList, ...assets.metal];
const positions: Array<{ src: string; left: number; top: number; delay: number }> = [];
const minDistance = 16; // 最小距离
const maxAttempts = 100; // 防止无限循环的最大尝试次数
// 计算两点之间的距离
const getDistance = (pos1: { left: number; top: number }, pos2: { left: number; top: number }) => {
return Math.sqrt(Math.pow(pos1.left - pos2.left, 2) + Math.pow(pos1.top - pos2.top, 2));
};
// 检查位置是否与已有位置冲突
const isPositionValid = (newPos: { left: number; top: number }) => {
return positions.every(existingPos => getDistance(newPos, existingPos) >= minDistance);
};
icons.forEach((icon, index) => {
let attempts = 0;
let validPosition = false;
let newPosition: { left: number; top: number };
// 尝试生成不冲突的位置
while (!validPosition && attempts < maxAttempts) {
newPosition = {
left: Math.random() * 100 - 50, // 扩大范围以增加成功概率
top: Math.random() * 100 - 50
};
if (positions.length === 0 || isPositionValid(newPosition)) {
validPosition = true;
positions.push({
src: icon[0],
left: newPosition.left,
top: newPosition.top,
delay: index * 50
});
}
attempts++;
}
// 如果无法找到不冲突的位置,使用稍微宽松的条件
if (!validPosition) {
console.warn(`Could not find non-conflicting position for icon ${index}, using fallback position`);
positions.push({
src: icon[0],
left: Math.random() * 100 - 50,
top: Math.random() * 100 - 50,
delay: Math.random() * 5 + index * 0.5
});
}
});
return positions;
};
const floatIcons = ref(generateIconPositions());
// 图标聚集模拟函数 - 返回最终位置而不实际移动
const simulateGatherIcons = () => {
const moveSpeed = 0.5; // 移动速度
const minDistance = 16; // 最小距离
const centerTarget = { left: 0, top: 0 }; // 目标中心点
const threshold = 0.1; // 停止移动的阈值
const maxIterations = 1000; // 防止无限循环的最大迭代次数
// 创建位置副本用于模拟
const simulatedPositions = floatIcons.value.map(icon => ({
src: icon.src,
left: icon.left,
top: icon.top,
delay: icon.delay
}));
// 计算两点之间的距离
const getDistance = (pos1: { left: number; top: number }, pos2: { left: number; top: number }) => {
return Math.sqrt(Math.pow(pos1.left - pos2.left, 2) + Math.pow(pos1.top - pos2.top, 2));
};
// 检查位置是否与其他图标冲突
const isPositionValid = (newPos: { left: number; top: number }, currentIndex: number, positions: typeof simulatedPositions) => {
return positions.every((icon, index) => {
if (index === currentIndex) return true;
return getDistance(newPos, icon) >= minDistance;
});
};
// 向目标点移动,但避免碰撞
const moveTowardsTarget = (currentPos: { left: number; top: number }, targetPos: { left: number; top: number }, iconIndex: number, positions: typeof simulatedPositions) => {
const dx = targetPos.left - currentPos.left;
const dy = targetPos.top - currentPos.top;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < threshold) {
return currentPos; // 已经足够接近目标
}
// 计算移动方向的单位向量
const unitX = dx / distance;
const unitY = dy / distance;
// 计算新位置
const newPos = {
left: currentPos.left + unitX * moveSpeed,
top: currentPos.top + unitY * moveSpeed
};
// 检查新位置是否会产生碰撞
if (isPositionValid(newPos, iconIndex, positions)) {
return newPos;
} else {
// 如果会碰撞,尝试绕行
const angles = [Math.PI / 4, -Math.PI / 4, Math.PI / 2, -Math.PI / 2, 3 * Math.PI / 4, -3 * Math.PI / 4];
for (const angle of angles) {
const rotatedX = unitX * Math.cos(angle) - unitY * Math.sin(angle);
const rotatedY = unitX * Math.sin(angle) + unitY * Math.cos(angle);
const alternativePos = {
left: currentPos.left + rotatedX * moveSpeed * 0.5,
top: currentPos.top + rotatedY * moveSpeed * 0.5
};
if (isPositionValid(alternativePos, iconIndex, positions)) {
return alternativePos;
}
}
// 如果所有方向都被阻挡,保持原位置
return currentPos;
}
};
// 模拟聚集过程
let iterations = 0;
while (iterations < maxIterations) {
let hasMovement = false;
// 创建新的位置数组
const newPositions = simulatedPositions.map((icon, index) => {
const oldPos = { left: icon.left, top: icon.top };
const newPos = moveTowardsTarget(oldPos, centerTarget, index, simulatedPositions);
// 检查是否有移动
if (Math.abs(newPos.left - oldPos.left) > threshold || Math.abs(newPos.top - oldPos.top) > threshold) {
hasMovement = true;
}
return {
...icon,
left: newPos.left,
top: newPos.top
};
});
// 更新模拟位置
simulatedPositions.splice(0, simulatedPositions.length, ...newPositions);
// 如果没有移动,说明已经收敛
if (!hasMovement) {
console.log(`图标聚集模拟完成!迭代次数: ${iterations}`);
break;
}
iterations++;
}
if (iterations >= maxIterations) {
console.warn('图标聚集模拟达到最大迭代次数限制');
}
// 返回最终位置
return simulatedPositions;
};
const showEndAni = ref(false);
async function gameEnd() {
fetch('/api/score', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
region: props.userdata.region,
store: props.userdata.store,
name: props.userdata.username,
time: timeSpent.value / 1000,
})
}).then(_ => {
fetch('/api/score', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(data => {
leaderBoard.value = data;
});
})
document.querySelector('.bed')?.animate([
{ transform: 'translateY(150%)' },
{ transform: 'translateY(0)' },
], {
duration: 600,
easing: 'cubic-bezier(0.4, 0, 1, 0.5)',
fill: 'forwards',
});
sunEle.value?.jumpTo('落下');
; (document.querySelector('.p-machine') as HTMLDivElement).style.animationDirection = 'reverse';
(document.querySelectorAll('.action img') as NodeListOf<HTMLImageElement>).forEach(img => {
img.style.animationDirection = 'reverse';
});
document.querySelector('.sun-ani-wrapper')?.animate([
{ transform: 'translateY(8%) translateX(-50%)', left: '50%', bottom: '23%', width: '60vw' },
], {
duration: 1100,
easing: 'ease-in-out',
fill: 'forwards',
}).finished
isGameEnd.value = true;
// 模拟图标聚集并获取最终位置
const finalPositions = simulateGatherIcons();
await wait(0)
floatIcons.value = finalPositions;
await wait(1800)
showEndAni.value = true;
sunEndEle.value?.jumpTo('all');
await wait(3400);
showScore.value = true;
document.querySelector('.bed')?.animate([
{ transform: 'translateY(0)' },
{ transform: 'translateY(150%)' },
], {
duration: 600,
easing: 'cubic-bezier(0.4, 0, 1, 0.5)',
fill: 'forwards',
});
}
const leaderBoard = ref([
{
"name": "测试用户",
"time": 1.7116000000000005,
"region": "奥莱",
"store": "北京斯普瑞斯"
},
{
"name": "阿迪斯",
"time": 1.9645,
"region": "奥莱",
"store": "北京斯普瑞斯"
},
{
"name": "Mike",
"time": 45.5,
"region": "大北区",
"store": "大连恒隆广场"
},
{
"name": "小白",
"time": 83.2,
"region": "大西区",
"store": "太原万象城"
},
{
"name": "小明",
"time": 98.2,
"region": "大西区",
"store": "太原万象城"
},
{
"name": "Elena",
"time": 122.2,
"region": "大南区",
"store": "深圳湾万象城"
}
])
const fmtTime = (time: number) => {
time = Math.round(time);
// 将时间格式化为 mm:ss
const minutes = Math.floor(time / 60);
const seconds = time % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
const showScore = ref(false)
const showLastPage = ref(false);
const sunAniEndRules = ref([
{ name: 'all', frame: 164, loop: 1, pauseAfter: false, duration: 33 },
{ name: '左右跳', frame: 66, loop: 0, pauseAfter: true, duration: 33 },
{ name: '落下', frame: 32, loop: 1, pauseAfter: false, duration: 33 },
{ name: 'loop', frame: 33, loop: 0, pauseAfter: true, duration: 33 },
])
import QRCode from 'qrcode';
const qrcodeUrl = ref('');
QRCode.toDataURL(`https://arcteryx-game-demo.xn--876a.net`, {
width: 256,
}).then(url => {
qrcodeUrl.value = url;
}).catch(err => {
console.error('生成二维码失败:', err);
});
async function finishCollect() {
document.querySelector('.finish-collect')?.animate([
{ transform: 'scale(1)' },
{ transform: 'scale(0)' },
], {
duration: 500,
easing: 'cubic-bezier(0.53,-0.7, 0.25, 1)',
fill: 'forwards',
})
document.querySelector('.wecare-title')?.animate([
{ transform: 'scale(1)' },
{ transform: 'scale(0)' },
], {
duration: 500,
easing: 'ease-in-out',
fill: 'forwards',
})
await document.querySelector('.lines')?.animate([
{ transform: 'translateX(150%)' }], {
duration: 500,
easing: 'ease-in-out',
fill: 'forwards',
}).finished
await sunEndEle.value?.jumpToSoftly('落下');
await document.querySelector('.bar-container')?.animate([
{ transform: 'translateY(300%)' }], {
duration: 500,
easing: 'ease-in-out',
fill: 'forwards',
}).finished
//@ts-ignore
document.querySelector('.lines').style.display = 'none';
//@ts-ignore
document.querySelector('.bar-container').style.display = 'none';
showLastPage.value = true;
setTimeout(() => {
if (!posterHasChangedToPage2.value) posterHasChangedToPage2.value = true;
}, 8000);
}
// 重置游戏状态的函数
function resetGame() {
emit('restart')
}
const posterHasChangedToPage2 = ref(false);
const posterAni = useTemplateRef('gui-ani-end-post');
watch(posterHasChangedToPage2, (newVal) => {
if (newVal) {
posterAni.value?.jumpTo('p2');
}
});
let hasSwapX = 0
function posterSwap(e: PointerEvent) {
if (posterHasChangedToPage2.value) return;
if (e.buttons === 1) {
hasSwapX += e.offsetX
if (Math.abs(hasSwapX) > 100) {
posterHasChangedToPage2.value = true;
}
}
}
</script>
<template>
<div class="page game">
<div class="public">
<img src="../assets/game/床.png" alt="" class="abs bed"
style="width: 66vw;bottom: 12%;left: 17%;">
<AniEle :url="assets.ani.下拉蓄力提示" ref="shoot-notice" :height="334"
:width="337" :rules="[
{ name: '蓄力', frame: 241, loop: 2 },
]" class="abs arrow" style="bottom: 7%;width: 30vw;left: 35%;"
@pointerdown.once="shoot" />
<div class="sun-ani-wrapper abs" v-if="!showEndAni" :style="{
width: '60vw',
zIndex: 99,
pointerEvents: 'none',
bottom: `calc(21% + ${sunPos[1]}vw)`,
left: `calc(49% + ${sunPos[0]}vw)`,
transform: `translateX(-50%)`,
transition: transitionOn ? 'all 0.1s ease' : undefined
}">
<!-- AniEle 组件现在只关心自己的内容不再有复杂的外部样式 -->
<AniEle :url="assets.ani.小太阳总" ref="sun-ani" class="sun-ani"
:height="1800" :width="1600" :rules="[
{ name: '起飞前', frame: 90, loop: 0, pauseAfter: true, duration: 33 },
{ name: '蓄力飞', frame: 181, loop: 1, pauseAfter: false, duration: 33 },
{ name: '天上飞', frame: 90, loop: 0, pauseAfter: true, duration: 33 },
{ name: '落下', frame: 382, loop: 0, pauseAfter: false, duration: 33 },
]" style="width: 100%; height: auto;" />
</div>
</div>
<div v-if="canAction && !showLastPage">
<img src="../assets/game/鼓风机.png" class="abs p-machine"
style="bottom: -40%; width: 66vw; left: 17%; transform-origin: 50% 63%; transition: all 0.2s;"
:style="{ transform: `rotate(${gDeg}deg)` }" />
<div class="action">
<img draggable="false" :style="{ animationDelay: '0.1s' }"
src="../assets/game/左.png" alt="" @click="gToggle('left')">
<img draggable="false" :style="{ animationDelay: '0.2s' }"
src="../assets/game/中.png" alt=""
@click="gToggle('center')">
<img draggable="false" :style="{ animationDelay: '0.0s' }"
src="../assets/game/右.png" alt="" @click="gToggle('right')">
</div>
<img class="abs gift-item" v-for="(gift, index) in giftList"
:src="gift[0]" style="width: 20vw;" v-if="!isGameEnd" :style="{
display: giftProgress == index ? 'block' : 'none',
bottom: `calc(45% + ${giftPos[1] ?? 0}vw)`, left: `calc(39.5% + ${giftPos[0] ?? 0}vw)`, transition: transitionOn ? `all 0.1s ease` : undefined
}" alt=""
:class="{ show: giftProgress == index, hide: giftProgress != index }">
<img src="../assets//game/操作提示.png" class="abs prompt" alt=""
style="width: 31%; bottom: 18%; left: 34%; ">
<img src="../assets/game/风.png" alt="" class="abs wind">
<img src="../assets/game/wecare.png" alt="" class="abs wecare"
style="width: 24vw;
left: 50%; top: 6%; transform: translateX(-50%);">
<div class="dot" v-if="!isGameEnd">
<div v-for="(_, index) in giftList" :key="index"
class="dot-item" :class="{ finished: index < giftProgress }"
@click="giftProgress = index">
</div>
</div>
</div>
<div class="game-end abs" v-if="isGameEnd"
style="z-index: 2; inset: 0; height: 100%; width: 100%;pointer-events: none;">
<div class="icon-cloud"
style="position: absolute; left: 50%; top: 20%;">
<img v-for="icon in floatIcons" :src="icon.src" alt="" :style="{
top: `${icon.top}vw`,
left: `${icon.left}vw`,
animationDelay: `${icon.delay}ms`,
}">
</div>
<img src="../assets/game/wecare.png" alt="" class="abs wecare"
style="width: 24vw;
left: 50%; top: 6%; transform: translateX(-50%);">
<img src="../assets/game/wecare标题.png" alt=""
class="abs wecare-title" style="width: 72%;left: 14%;top: 12%;">
<div class="scoreboard" v-if="showScore">
<div class="bar-container">
<div class="bar"
v-for="(item, index) in [leaderBoard[1], leaderBoard[0], leaderBoard[2]]"
:key="index" :class="{
'first': index === 1, 'second': index === 0, 'third': index === 2
}">
<div class="name">{{ item.name }}</div>
<div class="time">{{ fmtTime(item.time) }}</div>
<div class="region">{{ item.region }}</div>
<div class="store">{{ item.store }}</div>
</div>
</div>
<div class="lines">
<div class="line"
v-for="(item, index) in leaderBoard.slice(3, 7)"
:key="index"
:style="{ animationDelay: index * 100 + 400 + 'ms' }">
<div class="rank">{{ index + 4 }}</div>
<div class="name">{{ item.name }}</div>
<div class="time">{{ fmtTime(item.time) }}</div>
<div class="region">{{ item.region }}</div>
</div>
</div>
</div>
<div class="sun-ani-end-wrapper abs"
style="pointer-events: none; inset: 0; width: 100%;z-index: 2;"
:style="{ display: showEndAni ? 'block' : 'none' }">
<!-- AniEle 组件现在只关心自己的内容不再有复杂的外部样式 -->
<AniEle :url="assets.ani.后端效果" ref="sun-ani-end"
class="sun-ani-end" :height="2462" :width="1179"
:rules="sunAniEndRules"
style="width: 100%; height: auto;" />
</div>
<img src="../assets/game/完成收集.png" alt="" v-if="showScore"
class="finish-collect abs" @click.once="finishCollect"
style="bottom: 8%;left: 33%;width: 34vw;animation: scale-in 0.3s ease-out;pointer-events: all;">
</div>
<div class="last-page" v-if="showLastPage"
style="z-index: 0;position: absolute; inset: 0;height: 100%; width: 100%; pointer-events: none;">
<AniEle :url="assets.ani.结尾主标" :width="925" :height="340"
:rules="[{ name: 'main', frame: 41, loop: 1, pauseAfter: true, duration: 33, reverse: false }]"
style="position: absolute;top: 13%;width: 80%;left: 11%; pointer-events:none;"
ref="main-logo" />
<div class="poster-container" @pointermove="posterSwap"
style="position: absolute;top: 29.3%;left: 37.2%;width: 57.3%; height: 38%; overflow: hidden;pointer-events: all;">
<AniEle :url="assets.ani.海报" ref="gui-ani-end-post"
style="width: 100%;" class="gui-ani-end-post" :height="940"
:width="671" :rules="[
{ name: 'p1', frame: 32, loop: 1, pauseAfter: true, duration: 33 },
{ name: 'p2', frame: 46, loop: 1, pauseAfter: true, duration: 33 },
]" />
</div>
<AniEle :url="assets.ani.线" ref="gui-ani-end" class="gui-ani-end"
:height="2462" :width="1179" :rules="[
{ name: 'all', frame: 54, loop: 1, pauseAfter: false, duration: 16 },
]"
style="width: 100%; height: auto;position: absolute;inset: 0;pointer-events: none;" />
<img src="../assets/game/床.png" alt="" class="abs last-bed"
style="width: 66vw;bottom: -19%;left: 17%;">
<img src="../assets/game/分享海报.png" alt="" class="abs"
style="width: 34vw;bottom: -19%;left: 15%;animation: last-btn-in 0.3s ease-out forwards; cursor: pointer; pointer-events: all;">
<img src="../assets/game/再玩一次.png" alt="" class="abs"
@click="resetGame"
style="width: 34vw;bottom: -19%;left: 51%;animation: last-btn-in 0.3s ease-out forwards; cursor: pointer; animation-delay: 200ms; pointer-events: all;">
<div class="time-spent abs" style="left: 10%; top:44%;width: 24%;height: 23%; display: flex;
align-items: center; flex-direction: column;">
<div class="line-1"
style="display: flex; align-items: center; gap: 1vw; animation: line-in 0.5s ease-out 0.6s forwards;transform: translateX(-210%);">
<div class="username" style="font-size: 2.9vw;">{{
userdata.username }}</div>
<div class="cost-info"
style="font-size: 2.9vw; color: white; display: flex;align-items: center;justify-content: center;background-color: black;padding: 0 1.2vw; height: 4.5vw; border-radius: 4vw;">
用时</div>
</div>
<div class="time"
style="font-size: 10vw;animation: line-in 0.5s ease-out 0.8s forwards;transform: translateX(-210%);">
{{ fmtTime(timeSpent / 1000) }}</div>
<img :src="qrcodeUrl" alt=""
style="width: 100%;bottom:7%;position: absolute;animation: line-in 0.5s ease-out 1s forwards;transform: translateX(-210%);">
</div>
<div class="region-area abs"
style="left: 10%; top:30%;width: 24%;height: 10%;gap:2vw; display: flex;animation: line-in 0.5s ease-out 0.4s forwards;transform: translateX(-210%);
align-items: center; flex-direction: column;justify-content: center;">
<div class="region"
style="width: 20vw; background-color: white;font-size: 3vw;text-align: center;height: 5.2vw;line-height: 5.2vw; border-radius: 4.5vw;">
{{ userdata.region }}</div>
<div class="store" style="font-size: 2.9vw; color: gray;">{{
userdata.store }}</div>
</div>
</div>
<div v-if="giftShow" class="gift-popup">
<img :src="giftList[giftProgress][1]" alt="" class="abs"
style="width: 82%; left: 9%; top: 11%;">
<img src="../assets/game/收下福利.png" alt="" class="abs"
style="width: 14%; left: 43%; top: 54%;" @click="gameLoop()">
</div>
</div>
</template>
<style lang="scss">
@keyframes last-btn-in {
0% {
bottom: -19%;
}
100% {
bottom: 8%;
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes last-bed-in {
0% {
bottom: -19%;
scale: 1;
}
100% {
bottom: 1.5%;
scale: 0.55;
}
}
.last-bed {
z-index: -1;
animation: last-bed-in 0.5s ease-in-out forwards;
}
@keyframes scale-out {
0% {
scale: 1;
}
100% {
scale: 0;
}
}
@keyframes bar-in {
from {
transform: translateY(300%);
}
to {
transform: translateY(0%);
}
}
@keyframes line-in {
from {
transform: translateX(-210%);
}
to {
transform: translateX(-0%);
}
}
.scoreboard {
position: absolute;
top: 44%;
width: 100%;
.bar-container {
width: 100%;
justify-content: center;
align-items: center;
display: flex;
gap: 2vw;
}
.bar {
position: relative;
height: 60vw;
width: 26vw;
background-size: contain;
background-repeat: no-repeat;
mask-image: linear-gradient(to bottom, black, black, transparent);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.5vw;
animation: bar-in 0.7s ease-out forwards;
transform: translateY(150%);
.name {
font-size: 4.5vw;
}
.time {
font-size: 3vw;
}
.region {
font-size: 3vw;
color: white;
background-color: #7EA0CA;
padding: 0 2vw;
border-radius: 2vw;
}
.store {
max-width: 13vw;
text-align: center;
font-size: 2.4vw;
color: gray;
}
&.first {
top: 3vw;
background-image: url('../assets/game/first.png');
animation-delay: 0ms;
}
&.second {
background-image: url('../assets/game/second.png');
animation-delay: 250ms;
top: 9vw;
}
&.third {
background-image: url('../assets/game/third.png');
animation-delay: 500ms;
top: 9vw;
}
}
.lines {
display: flex;
position: relative;
top: -5vw;
flex-direction: column;
align-items: center;
gap: 1vw;
.line {
display: flex;
align-items: center;
justify-content: space-between;
width: 80%;
font-size: 3.2vw;
background-color: #FFFFFF;
padding: 0 2vw;
height: 8vw;
border-radius: 8vw;
animation: line-in 0.5s ease-out forwards;
transform: translateX(-150%);
.rank {
// italic
font-style: italic;
flex: 1;
font-size: 4vw;
text-align: center;
color: #7EA0CA;
}
.name {
flex: 2;
text-align: center;
border-right: .35vw solid black;
}
.time {
flex: 2;
text-align: center;
border-right: .35vw solid black;
}
.region {
flex: 2;
color: gray;
text-align: center;
}
}
}
}
.wecare-title {
animation: scale-in 0.4s ease-in-out forwards;
animation-delay: 3.9s;
scale: 0;
}
.icon-cloud {
animation: scale-out 0.4s ease-in-out forwards;
animation-delay: 3.5s;
img {
position: absolute;
width: 16vw;
transition: all 4s ease;
animation: scale-in 0.4s ease-in-out forwards;
transform: translate(-50%, -50%);
transform-origin: left top;
scale: 0;
}
}
@keyframes gift-popup-bg {
0% {
backdrop-filter: blur(0px);
opacity: 0;
}
100% {
backdrop-filter: blur(16px);
opacity: 1;
}
}
.gift-popup {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
z-index: 1000;
backdrop-filter: blur(0px);
opacity: 0;
animation: gift-popup-bg 1.2s ease-in-out 0.7s forwards;
}
.dot {
position: absolute;
top: 10%;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 2vw;
.dot-item {
width: 2vw;
height: 2vw;
border-radius: 50%;
background-color: #fff;
transition: all 0.2s ease-in-out;
}
.dot-item.finished {
background-color: #7EA0CA;
}
}
@keyframes scale-in {
0% {
scale: 0;
}
100% {
scale: 1;
}
}
@keyframes warp-in {
0% {
bottom: -40%
}
100% {
bottom: -13%;
}
}
@keyframes prompt-in {
0% {
rotate: 20deg;
scale: 0;
}
100% {
rotate: 0;
scale: 1;
}
}
@keyframes prompt-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.prompt {
transform-origin: 21% 99%;
animation: prompt-in 0.8s cubic-bezier(0.71, 0.13, 0.43, 2.5) forwards,
prompt-out 0.8s cubic-bezier(0.71, 0.13, 0.43, 2.5) 2s forwards;
}
.p-machine {
animation: warp-in 0.5s ease-out forwards;
}
.abs {
position: absolute;
}
.wind {
z-index: 11;
pointer-events: none;
opacity: 0;
}
.action {
display: flex;
position: absolute;
bottom: 19vw;
left: 50%;
transform: translateX(-50%);
gap: 2vw;
img {
flex: 1;
width: 28vw;
scale: 0;
animation: scale-in 0.4s ease-in-out forwards;
}
}
</style>