1293 lines
38 KiB
Vue
1293 lines
38 KiB
Vue
<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>
|