Commit 2f5cc1aa authored by 董政锦's avatar 董政锦

feat: game3 第一页完成;

parent 8f17f3eb
<script setup lang="ts">
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import Loading from './views/LoadingView.vue'
import Rank1 from './views/Rank1View.vue'
import Rank2 from './views/Rank2View.vue'
import { useAdminGameSocket } from '@/composables/useAdminGameSocket'
import type Player from '@/commons/player.ts'
import { playApplauseMusic, playGame6Music, stopAllMusic, stopGame6Music } from '@/commons/music'
import { $confirm, $remove_socket_storage, $toast } from '@/commons/utils.ts'
const router = useRouter()
type GameScreen = 'loading' | 'rank1' | 'rank2'
const PLAYER_LIMIT = 20
const RANK_LIMIT = 10
const emptyLoadingPlayer = () => ({
score: '-',
telephone: '',
userid: '',
wechat_id: '',
nickname: '虚位以待',
avatar: '',
})
const emptyRankPlayer = (index: number): Player => ({
wechat_id: `empty-${index}`,
nickname: '虚位以待',
avatar: '',
score: '-',
})
const screen = ref<GameScreen>('loading')
const loadingPlayers = ref<any[]>(Array.from({ length: PLAYER_LIMIT }, emptyLoadingPlayer))
const playerCount = ref(0)
const rankPlayers = ref<Player[]>(Array.from({ length: RANK_LIMIT }, (_, index) => emptyRankPlayer(index)))
const rank1Ref = ref<InstanceType<typeof Rank1> | null>(null)
let isPlayingStartAnimation = false
const confirmRefresh = (event: BeforeUnloadEvent) => {
event.preventDefault()
event.returnValue = ''
}
const renderPlayers = (data: any) => {
const players = Array.isArray(data?.list) ? data.list : []
playerCount.value = Number(data?.count ?? players.length)
loadingPlayers.value = Array.from(
{ length: PLAYER_LIMIT },
(_, index) => players[index] ?? emptyLoadingPlayer(),
)
}
const updateRankPlayers = (data: any) => {
if (!Array.isArray(data)) return
rankPlayers.value = Array.from(
{ length: RANK_LIMIT },
(_, index) => data[index] ?? emptyRankPlayer(index),
)
}
const playerToRankPlayer = (player: any, index: number): Player => ({
telephone: player?.telephone ?? '',
userid: player?.userid ?? '',
wechat_id: player?.wechat_id || `empty-${index}`,
nickname: player?.nickname || '虚位以待',
avatar: player?.avatar || '',
score: player?.score ?? '-',
})
const seedRankPlayersFromLoading = () => {
rankPlayers.value = Array.from(
{ length: RANK_LIMIT },
(_, index) => {
const player = loadingPlayers.value[index]
if (!player?.userid && !player?.wechat_id) {
return emptyRankPlayer(index)
}
return playerToRankPlayer(player, index)
},
)
}
const resetRoundState = () => {
playerCount.value = 0
loadingPlayers.value = Array.from({ length: PLAYER_LIMIT }, emptyLoadingPlayer)
rankPlayers.value = Array.from({ length: RANK_LIMIT }, (_, index) => emptyRankPlayer(index))
isPlayingStartAnimation = false
}
const gotoLoading = (reset = false) => {
if (reset) {
resetRoundState()
}
screen.value = 'loading'
}
const gotoRank1WithIntro = async () => {
if (isPlayingStartAnimation) return
isPlayingStartAnimation = true
seedRankPlayersFromLoading()
screen.value = 'rank1'
await nextTick()
await rank1Ref.value?.startIntro()
screen.value = 'rank2'
isPlayingStartAnimation = false
}
function handleRoomState(data: any) {
if (Array.isArray(data?.list)) {
renderPlayers(data)
}
switch (data?.status) {
case 0:
gotoLoading(true)
createRoom(6)
break
case 1:
gotoLoading()
break
case 2:
seedRankPlayersFromLoading()
screen.value = 'rank1'
break
case 3:
screen.value = 'rank2'
break
}
}
const gameId='game6'
const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
gameId,
onRoomState: handleRoomState,
onRoomJoin: (data) => {
renderPlayers(data)
gotoLoading()
},
onGameStart: gotoRank1WithIntro,
onRoomRank: updateRankPlayers,
onRelogin: async () => {
$remove_socket_storage();
await router.replace('/')
}
})
const startGameWithMusic = () => {
if (playerCount.value == 0) {
// alert('房间中没有玩家')
$toast('房间中没有玩家');
return;
}
startGame()
}
const nextRound = () => {
gotoLoading(true)
//重开房间
backRoom(6)
}
const nextRoundWithMusic = () => {
nextRound()
}
const backHandler = async() => {
const ok = await $confirm({
title: '提示',
message: '是否关闭当前房间回到首页',
confirmText: '确定',
cancelText: '取消',
})
if (ok) {
closeRoom();
stopAllMusic()
router.replace('/main');
}
}
onMounted(() => {
window.addEventListener('beforeunload', confirmRefresh)
playGame6Music()
})
onUnmounted(() => {
stopAllMusic();
window.removeEventListener('beforeunload', confirmRefresh)
})
watch(screen, (value) => {
// console.log(value);
if(value == 'rank2'){
playApplauseMusic();
}else if(value == 'loading'){
playGame6Music();
}
})
</script>
<template>
<Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount"
@start="startGameWithMusic" @back="backHandler" />
<Rank1 v-else-if="screen === 'rank1'" ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic" />
<Rank2 v-else :rank-list="rankPlayers" @next="nextRoundWithMusic" @back="backHandler" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import router from '@/router';
import { $format_str } from '@/commons/utils.ts';
import DesignStage from '@/components/DesignStage.vue'
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
defineProps<{
players: any[];
playerCount: number;
}>();
const emit = defineEmits<{
start: [];
back:[];
}>();
const imageUrls = {
bg: cssAssetUrl('game3/bg.webp'),
logo: cssAssetUrl('game1/logo.png'),
title1: cssAssetUrl('game3/title.webp'),
title2: cssAssetUrl('game1/title2.png'),
qrcode: assetUrl('game3/qrcode.png'),
// gu: cssAssetUrl('game1/gu.png'),
// bg: cssAssetUrl('game1/bg.png'),
// logo: cssAssetUrl('game1/logo.png'),
// title1: cssAssetUrl('game1/title1.png'),
// title2: cssAssetUrl('game1/title2.png'),
// qrcode: assetUrl('game1/qrcode.png'),
// gu: cssAssetUrl('game1/gu.png'),
imgMole: cssAssetUrl('game3/left.webp'),
imgRabbit: cssAssetUrl('game3/right.webp'),
bottomLayers: cssAssetUrl('game1/bottom-layers.png'),
avatarAnimation: cssAssetUrl('avatar-red_animation.png'),
avatar: cssAssetUrl('avatar-red.png'),
line: cssAssetUrl('game1/line.png'),
// start1: cssAssetUrl('game1/start-1.png'),
// start2: cssAssetUrl('game1/start-2.png'),
start: cssAssetUrl('game1/start.png'),
back: cssAssetUrl('game1/back.png'),
};
const isStartPressed = ref(false);
const pressStartButton = () => {
isStartPressed.value = true;
emit('start');
};
const releaseStartButton = () => {
isStartPressed.value = false;
};
const renderAvatar = (item: any | null) => {
if (item && item.avatar) {
return `background: url("${String(item.avatar).replace(/"/g, '\\"')}") center / cover no-repeat;`
}
return `background: ${imageUrls.avatar} center / cover no-repeat;`
}
</script>
<template>
<DesignStage>
<div class="bg">
<div class="img-logo"></div>
<div class="img-title1"></div>
<!-- <div class="img-title2"></div> -->
<div class="img-qrcode-container">
<div class="img-qrcode"><img :src="imageUrls.qrcode" /></div>
<div class="txt-qrcode">微信扫码参与</div>
</div>
<div class="img-mole"></div>
<div class="img-rabbit"></div>
<div class="list-container">
<div class="txt-count">当前在线人数 <span>{{ playerCount }}</span></div>
<div class="list-container-left">
<div v-for="(item, index) in players" :key="item.userid || item.wechat_id || `empty-${index}`">
<div>
<div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div>
<div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div>
</div>
</div>
</div>
<div class="list-container-line"></div>
<div class="list-container-right">
<!-- <div class="btn-start" :class="{ pressed: isStartPressed }" @pointerdown="pressStartButton"
@pointerup="releaseStartButton" @pointerleave="releaseStartButton"
@pointercancel="releaseStartButton"></div> -->
<div class="btn-start" @click="pressStartButton"></div>
<div class="btn-back" @click="emit('back')"></div>
</div>
</div>
</div>
</DesignStage>
</template>
<style scoped>
.bg {
width: 1920px;
height: 1080px;
background: v-bind('imageUrls.bg') center / cover no-repeat;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.img-logo {
background: v-bind('imageUrls.logo');
background-size: cover;
width: 428px;
height: 77px;
position: absolute;
left: 37px;
top: 82px;
}
.img-title1 {
background: v-bind('imageUrls.title1');
width: 910px;
height: 437px;
/* margin: 0 auto; */
position: absolute;
left: 511px;
top:0;
}
.img-title2 {
background: v-bind('imageUrls.title2');
width: 643px;
height: 140px;
margin: 0 auto;
position: relative;
top: 80px;
}
.img-qrcode-container {
width: 144px;
height: 177px;
position: absolute;
z-index: 1;
top: 40px;
right: 40px;
.img-qrcode {
width: 100%;
height: 144px;
border-radius: 10px;
img {
width: 147px;
height: 147px;
}
}
.txt-qrcode {
font-size: 24px;
color: white;
}
}
.img-mole {
background: v-bind('imageUrls.imgMole') center / cover no-repeat;
width: 344px;
height: 354px;
position: absolute;
top: 311px;
left: 144px;
}
.img-rabbit {
background: v-bind('imageUrls.imgRabbit') center / cover no-repeat;
width: 633px;
height: 367px;
position: absolute;
top: 293px;
left: 1260px;
}
.list-container {
background: v-bind('imageUrls.bottomLayers');
width: 1800px;
height: 397px;
position: absolute;
left: 60px;
bottom: 60px;
border: 3px solid #AA0000;
border-radius: 10px;
.txt-count {
font-size: 36px;
margin-left: 30px;
margin-top: 20px;
color: #AA0000;
font-weight: bold;
>span {
margin-left: 20px;
font-size: 48px;
position: relative;
top: 5px;
}
}
.list-container-left {
float: left;
display: grid;
grid-template-columns: repeat(10, 1fr);
row-gap: 20px;
width: 73%;
margin-top: 30px;
height: 250px;
align-content: center;
.img-avatar-container {
position: relative;
margin: 0 auto;
width: 72px;
height: 72px;
border-radius: 36px;
}
.img-avatar-container::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: v-bind('imageUrls.avatarAnimation') center / cover no-repeat;
animation: avatar-rotate 10s linear infinite;
}
.img-avatar {
position: relative;
top: 6px;
z-index: 1;
margin: 0 auto;
width: 60px;
height: 60px;
border-radius: 30px;
/* background: v-bind('imageUrls.avatar'); */
}
.txt-nickname {
color: #666;
font-size: 20px;
text-align: center;
}
}
.list-container-line {
float: left;
width: 1px;
height: 281px;
margin-left: 50px;
background: v-bind('imageUrls.line') center no-repeat;
}
.list-container-right {
float: left;
.btn-start {
width: 316px;
height: 105px;
margin-left: 60px;
margin-top: 10px;
background: v-bind('imageUrls.start');
cursor: pointer;
}
/* .btn-start.pressed {
background: v-bind('imageUrls.start2');
} */
.btn-back {
width: 316px;
height: 105px;
margin-left: 60px;
margin-top: 30px;
background: v-bind('imageUrls.back');
cursor: pointer;
}
}
}
</style>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import DesignStage from '../../../components/DesignStage.vue'
import type Player from '../../../commons/player.ts'
import { $format_str, $is_run_local } from '@/commons/utils.ts';
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
const props = defineProps<{
rankList: Player[];
}>();
const emit = defineEmits<{
start: [];
}>();
const RANK_LIMIT = 10;
const START_COUNTDOWN_SECONDS = 3;
const GU_FRAME_DURATION = 300;
const GU_LOOP_DELAY = 1000;
const MOLE_CELL_COUNT = 9;
const MOLE_LAND_DURATION = 5000;
const MOLE_SHOW_DURATION = 1200;
const MOLE_HIDE_DURATION = 650;
const designStage = ref<InstanceType<typeof DesignStage> | null>(null);
const rankEffects = ref<Record<string, "up" | "down" | "new">>({});
const guFrameIndex = ref(0);
const guBurstVisible = ref(false);
const guBurstKey = ref(0);
let guFramesReady: Promise<void> | undefined;
let guTimer: ReturnType<typeof setTimeout> | undefined;
let guBurstTimer: ReturnType<typeof setTimeout> | undefined;
// const loadGuFrames = (dir: string) => Array.from(
// { length: 10 },
// (_, index) => assetUrl(`${dir}/${String(index + 1).padStart(2, '0')}.png`),
// );
// const guFrames = [
// loadGuFrames('game1/gu'),
// loadGuFrames('game1/gu2'),
// ];
const imageUrls = {
bg: cssAssetUrl('game6/bg2.png'),
logo: cssAssetUrl('game1/logo.png'),
title: cssAssetUrl('game6/title2.png'),
fireworks: cssAssetUrl('game1/fireworks.png'),
// fireworks: cssAssetUrl('game1/fireworks.png'),
// titleRank: cssAssetUrl('game1/title-rank.png'),
rankNo4: cssAssetUrl('game6/no4.png'),
rankNo1: cssAssetUrl('game6/no1.png'),
rankNo2: cssAssetUrl('game6/no2.png'),
rankNo3: cssAssetUrl('game6/no3.png'),
// rankIcon: cssAssetUrl('game1/no-icon.png'),
// rankNo1Icon: cssAssetUrl('game1/no1-icon.png'),
// rankNo2Icon: cssAssetUrl('game1/no2-icon.png'),
// rankNo3Icon: cssAssetUrl('game1/no3-icon.png'),
avatar: cssAssetUrl('avatar-yellow.png'),
avatarAnimation: cssAssetUrl('avatar-yellow_animation.png'),
mole2: cssAssetUrl('game6/mole2.png'),
rankContainer: cssAssetUrl('game6/rank-container.png'),
land: cssAssetUrl('game6/land.png'),
moleShow: cssAssetUrl('game6/mole_show.png'),
moleHide: cssAssetUrl('game6/mole_hide.png'),
rabbitShow: cssAssetUrl('game6/rabbit_show.png'),
rabbitHide: cssAssetUrl('game6/rabbit_hide.png'),
};
const emptyPlayer = (index: number): Player => ({
wechat_id: `empty-${index}`,
nickname: '-',
avatar: '',
score: 0,
});
const list = ref<Player[]>(Array.from({ length: RANK_LIMIT }, (_, index) => emptyPlayer(index)));
const playerKey = (player: Player, index = 0) => player.userid || player.wechat_id || `empty-${index}`;
const mockRankStep = ref(0);
type MoleKind = "land" | "mole" | "rabbit";
type MolePhase = "land" | "show" | "hide";
type MoleCell = {
kind: MoleKind;
phase: MolePhase;
animationKey: number;
};
const moleCells = ref<MoleCell[]>(
Array.from({ length: MOLE_CELL_COUNT }, () => ({
kind: "land",
phase: "land",
animationKey: 0,
})),
);
const moleTimers: Array<ReturnType<typeof setTimeout> | undefined> = [];
const makeMockPlayer = (id: number, score: number): Player => ({
wechat_id: `mock-${id}`,
nickname: `Player ${id}`,
avatar: '',
score,
});
const mockRankRounds: Player[][] = [
[
makeMockPlayer(1, 980),
makeMockPlayer(2, 900),
makeMockPlayer(3, 860),
makeMockPlayer(4, 820),
makeMockPlayer(5, 780),
makeMockPlayer(6, 720),
makeMockPlayer(7, 680),
makeMockPlayer(8, 640),
makeMockPlayer(9, 600),
makeMockPlayer(10, 560),
],
[
makeMockPlayer(4, 1040),
makeMockPlayer(1, 1010),
makeMockPlayer(7, 960),
makeMockPlayer(2, 940),
makeMockPlayer(11, 880),
makeMockPlayer(3, 850),
makeMockPlayer(5, 830),
makeMockPlayer(8, 790),
makeMockPlayer(6, 760),
makeMockPlayer(10, 700),
],
[
makeMockPlayer(7, 1180),
makeMockPlayer(11, 1090),
makeMockPlayer(4, 1060),
makeMockPlayer(12, 1000),
makeMockPlayer(1, 980),
makeMockPlayer(8, 930),
makeMockPlayer(2, 890),
makeMockPlayer(5, 850),
makeMockPlayer(13, 810),
makeMockPlayer(3, 780),
],
];
const updateRankList = (rankList: Player[]) => {
const oldRanks = new Map(list.value.map((player, index) => [playerKey(player, index), index]));
const next = Array.from({ length: RANK_LIMIT }, (_, index) => rankList[index] ?? emptyPlayer(index));
const effects: Record<string, "up" | "down" | "new"> = {};
next.forEach((player, index) => {
const key = playerKey(player, index);
if (!key || key.startsWith("empty-")) return;
const oldIndex = oldRanks.get(key);
if (oldIndex === undefined) effects[key] = "new";
else if (oldIndex > index) effects[key] = "up";
else if (oldIndex < index) effects[key] = "down";
});
list.value = next;
rankEffects.value = {};
requestAnimationFrame(() => {
rankEffects.value = effects;
});
};
watch(
() => props.rankList,
(rankList) => updateRankList(rankList),
{ immediate: true, deep: true },
);
const playMockRankAnimation = () => {
if ($is_run_local()) {
updateRankList(mockRankRounds[mockRankStep.value] ?? mockRankRounds[0] ?? []);
mockRankStep.value = (mockRankStep.value + 1) % mockRankRounds.length;
}
};
const startGame = () => {
if ($is_run_local()) {
emit('start');
}
};
const randomMoleKind = (includeLand = true): MoleKind => {
const kinds: MoleKind[] = includeLand ? ["land", "mole", "rabbit"] : ["mole", "rabbit"];
return kinds[Math.floor(Math.random() * kinds.length)] ?? "land";
};
const clearMoleTimer = (index: number) => {
const timer = moleTimers[index];
if (timer) {
clearTimeout(timer);
moleTimers[index] = undefined;
}
};
const setMoleCell = (index: number, kind: MoleKind, phase: MolePhase) => {
const next = [...moleCells.value];
const current = next[index];
next[index] = {
kind,
phase,
animationKey: (current?.animationKey ?? 0) + 1,
};
moleCells.value = next;
};
const scheduleMoleCell = (index: number, kind = randomMoleKind()) => {
clearMoleTimer(index);
if (kind === "land") {
setMoleCell(index, "land", "land");
moleTimers[index] = setTimeout(() => scheduleMoleCell(index, randomMoleKind(false)), MOLE_LAND_DURATION);
return;
}
setMoleCell(index, kind, "show");
moleTimers[index] = setTimeout(() => {
setMoleCell(index, kind, "hide");
moleTimers[index] = setTimeout(() => scheduleMoleCell(index, "land"), MOLE_HIDE_DURATION);
}, MOLE_SHOW_DURATION);
};
const moleCellClass = (cell: MoleCell) => [
`mole-${cell.kind}`,
`mole-${cell.phase}`,
];
onMounted(() => {
moleCells.value.forEach((_, index) => {
moleTimers[index] = setTimeout(
() => scheduleMoleCell(index, randomMoleKind()),
Math.floor(Math.random() * MOLE_LAND_DURATION),
);
});
});
onBeforeUnmount(() => {
moleTimers.forEach((_, index) => clearMoleTimer(index));
});
// const stopPlayerGu = () => {
// if (guTimer) {
// clearTimeout(guTimer);
// guTimer = undefined;
// }
// if (guBurstTimer) {
// clearTimeout(guBurstTimer);
// guBurstTimer = undefined;
// }
// guBurstVisible.value = false;
// guFrameIndex.value = 0;
// }
// const preloadGuFrames = () => {
// if (!guFramesReady) {
// guFramesReady = Promise.all(guFrames.flat().map((src) => {
// const img = new Image();
// img.src = src;
// return img.decode
// ? img.decode().catch(() => undefined)
// : new Promise<void>((resolve) => {
// img.onload = () => resolve();
// img.onerror = () => resolve();
// });
// })).then(() => undefined);
// }
// return guFramesReady;
// }
// const playGuLoopDelayAnimation = () => {
// if (guBurstTimer) {
// clearTimeout(guBurstTimer);
// guBurstTimer = undefined;
// }
// guBurstVisible.value = false;
// requestAnimationFrame(() => {
// guBurstKey.value += 1;
// guBurstVisible.value = true;
// guBurstTimer = setTimeout(() => {
// guBurstVisible.value = false;
// guBurstTimer = undefined;
// }, GU_LOOP_DELAY);
// });
// }
// const playerGu = async () => {
// stopPlayerGu();
// const frameCount = Math.min(...guFrames.map((frames) => frames.length));
// if (frameCount <= 0) return;
// await preloadGuFrames();
// let frameIndex = 0;
// const tick = () => {
// guFrameIndex.value = frameIndex;
// if (frameIndex >= frameCount - 1) {
// frameIndex = 0;
// playGuLoopDelayAnimation();
// guTimer = setTimeout(tick, GU_LOOP_DELAY);
// return;
// }
// frameIndex += 1;
// guTimer = setTimeout(tick, GU_FRAME_DURATION);
// }
// tick();
// }
const startIntro = async () => {
// playerGu();
await designStage.value?.startCountdown(START_COUNTDOWN_SECONDS, 60);
// stopPlayerGu();
};
defineExpose({
startIntro,
});
const renderAvatar = (item: any | null) => {
if (item && item.avatar) {
return `background: url("${String(item.avatar).replace(/"/g, '\\"')}") center / cover no-repeat;`
}
return `background: ${imageUrls.avatar} center / cover no-repeat;`
}
const rankIcon = (index: number) => {
const icons = [
imageUrls.rankNo1,
imageUrls.rankNo2,
imageUrls.rankNo3,
];
let w = 42;
let h = 47;
let l = -20;
let t = -20;
if (index <= 2) {
w = 65;
h = 48;
l = -30;
t = -20;
}
return {
background: `${icons[index] ?? imageUrls.rankNo4} center / cover no-repeat`,
width: w + 'px',
height: h + 'px',
left: l + 'px',
top: t + 'px',
lineHeight: h + 'px',
color: index <= 2 ? 'white' : '#CC7F36',
};
}
</script>
<template>
<DesignStage ref="designStage">
<div class="bg">
<div class="img-logo"></div>
<div class="img-title"></div>
<div class="img-mole"></div>
<div class="rank-container">
<div v-for="(item, index) in list" :key="playerKey(item, index)">
<div>
<div class="rank-icon" :style="rankIcon(index)">{{ index + 1 }}</div>
<div class="rank-item">
<div>
<div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div>
<div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div>
<div class="txt-score">{{ item.score }}</div>
</div>
</div>
</div>
</div>
</div>
<div class="mole-container">
<div v-for="(cell, index) in moleCells" :key="index" class="mole-hole" :class="moleCellClass(cell)">
<div :key="cell.animationKey" class="mole-sprite"></div>
</div>
</div>
</div>
</DesignStage>
</template>
<style scoped>
.bg {
width: 1920px;
height: 1080px;
background: v-bind('imageUrls.bg') center / cover no-repeat;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.img-logo {
background: v-bind('imageUrls.logo');
background-size: cover;
width: 428px;
height: 77px;
position: absolute;
left: 37px;
top: 82px;
}
.img-title {
background: v-bind('imageUrls.title') center / cover no-repeat;
width: 675px;
height: 281px;
margin: 0 auto;
margin-top: 37px;
}
.img-mole {
background: v-bind('imageUrls.mole2') center / cover no-repeat;
width: 189px;
height: 228px;
position: absolute;
left: 144px;
top: 141px;
}
.rank-container {
background: v-bind('imageUrls.rankContainer') center / cover no-repeat;
display: grid;
grid-template-columns: repeat(10, minmax(0, 1fr));
align-items: center;
column-gap: 42px;
box-sizing: border-box;
padding: 30px;
width: 1832px;
height: 244px;
position: absolute;
left: 44px;
top: 348px;
>div {
box-sizing: border-box;
text-align: center;
width: 100%;
height: 100%;
>div {
box-sizing: border-box;
width: 100%;
height: 100%;
background: #A10A06;
border-radius: 5px;
position: relative;
.rank-icon {
position: absolute;
color: #CC7F36;
font-weight: bold;
}
.rank-icon:nth-child(1),
.rank-icon:nth-child(2),
.rank-icon:nth-child(3) {
color: white;
}
}
}
.img-avatar-container {
position: relative;
margin: 0 auto;
width: 90px;
height: 90px;
border-radius: 36px;
/* transform: scale(1.3); */
top: 18px
}
.img-avatar-container::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: v-bind('imageUrls.avatarAnimation');
animation: avatar-rotate 10s linear infinite;
}
.img-avatar {
width: 80px;
height: 80px;
border-radius: 40px;
position: absolute;
left: 6px;
top: 5px;
}
.txt-nickname {
position: absolute;
bottom: 40px;
text-align: center;
width: 100%;
color: #F9EBCB;
}
.txt-score {
position: absolute;
bottom: 5px;
text-align: center;
width: 100%;
color: #F9EBCB;
}
}
.mole-container {
position: absolute;
top: 200px;
/* inset: 0; */
pointer-events: none;
.mole-hole {
position: absolute;
width: 500px;
height: 500px;
transform: translate(-125px, -125px);
overflow: hidden;
.mole-sprite {
width: 500px;
height: 500px;
background-repeat: no-repeat;
background-position: 0 0;
background-size: auto 500px;
}
&.mole-land .mole-sprite {
background: v-bind('imageUrls.land') center / cover no-repeat;
}
&.mole-mole.mole-show .mole-sprite {
background-image: v-bind('imageUrls.moleShow');
animation: mole-show 1.2s steps(30) both;
}
&.mole-mole.mole-hide .mole-sprite {
background-image: v-bind('imageUrls.moleHide');
animation: mole-hide 0.65s steps(15) both;
}
&.mole-rabbit.mole-show .mole-sprite {
background-image: v-bind('imageUrls.rabbitShow');
animation: mole-show 1.2s steps(30) both;
}
&.mole-rabbit.mole-hide .mole-sprite {
background-image: v-bind('imageUrls.rabbitHide');
animation: mole-hide 0.65s steps(15) both;
}
&:nth-child(1) {
left: 247px;
top: 312px;
}
&:nth-child(2) {
left: 547px;
top: 300px;
}
&:nth-child(3) {
left: 847px;
top: 312px;
}
&:nth-child(4) {
left: 1147px;
top: 300px;
}
&:nth-child(5) {
left: 1447px;
top: 312px;
}
&:nth-child(6) {
left: 397px;
top: 480px;
}
&:nth-child(7) {
left: 697px;
top: 460px;
}
&:nth-child(8) {
left: 997px;
top: 460px;
}
&:nth-child(9) {
left: 1297px;
top: 480px;
}
}
}
@keyframes mole-show {
from {
background-position-x: 0;
}
to {
background-position-x: -15000px;
}
}
@keyframes mole-hide {
from {
background-position-x: 0;
}
to {
background-position-x: -7500px;
}
}
</style>
<script setup lang="ts">
import { ref, watch } from 'vue'
import DesignStage from '../../../components/DesignStage.vue'
import type Player from '../../../commons/player.ts'
import { cssAssetUrl } from '@/commons/assets.ts';
const props = defineProps<{
rankList: Player[];
}>();
const emit = defineEmits<{
next: [];
back: [];
}>();
const FINAL_RANK_LIMIT = 10;
const imageUrls = {
bg: cssAssetUrl('game6/bg3.png'),
logo: cssAssetUrl('game1/logo.png'),
title: cssAssetUrl('game6/title3.png'),
podium: cssAssetUrl('game6/podium.png'),
vip: cssAssetUrl('game6/vip.png'),
avatar: cssAssetUrl('avatar-yellow.png'),
avatarAnimation: cssAssetUrl('avatar-yellow_animation.png'),
rankIcon: cssAssetUrl('game6/no4.png'),
next: cssAssetUrl('game1/next.png'),
back: cssAssetUrl('game1/back.png'),
};
const emptyPlayer = (index: number): Player => ({
active: false,
wechat_id: `empty-${index}`,
nickname: '虚位以待',
avatar: '',
score: 0,
});
const list = ref<Player[]>(Array.from({ length: FINAL_RANK_LIMIT }, (_, index) => emptyPlayer(index)));
const updateFinalRank = (rankList: Player[]) => {
list.value = Array.from(
{ length: FINAL_RANK_LIMIT },
(_, index) => rankList[index] ?? emptyPlayer(index),
);
};
watch(
() => props.rankList,
(rankList) => updateFinalRank(rankList),
{ immediate: true, deep: true },
);
const renderAvatar = (item: any | null) => {
if (item && item.avatar) {
return `background: url('${item.avatar}') center / cover no-repeat;`
}
return `background: ${imageUrls.avatar} center / cover no-repeat;`
}
</script>
<template>
<DesignStage>
<div class="bg">
<div class="img-logo"></div>
<div class="img-title"></div>
<div class="img-title2"></div>
<div class="podium">
<div class="img-vip"></div>
<div class="rank-no1">
<div class="avatar" :style="`${renderAvatar(list[0])}`">
</div>
<div class="nickname">{{ list[0]?.nickname }}</div>
</div>
<div class="rank-no2">
<div class="avatar" :style="`${renderAvatar(list[1])}`">
</div>
<div class="nickname">{{ list[1]?.nickname }}</div>
</div>
<div class="rank-no3">
<div class="avatar" :style="`${renderAvatar(list[2])}`">
</div>
<div class="nickname">{{ list[2]?.nickname }}</div>
</div>
</div>
<div class="rank-container">
<div v-for="(item, index) in list.slice(3)" :key="item.userid || item.wechat_id || `rank-${index}`">
<div class="no-icon">{{ index + 4 }}</div>
<div>
<div class="img-avatar-container">
<div class="img-avatar" :style="`${renderAvatar(list[index + 3])}`"></div>
</div>
<div class="txt-nickname">{{ item.nickname }}</div>
</div>
</div>
</div>
<div class="btn-next" @click="emit('next')"></div>
<div class="btn-back" @click="emit('back')"></div>
</div>
</DesignStage>
</template>
<style scoped>
.bg {
width: 1920px;
height: 1080px;
background: v-bind('imageUrls.bg') center / cover no-repeat;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
.img-logo {
background: v-bind('imageUrls.logo');
background-size: cover;
width: 428px;
height: 77px;
position: absolute;
left: 37px;
top: 82px;
}
.img-title {
background: v-bind('imageUrls.title') center / cover no-repeat;
width: 831px;
height: 120px;
margin: 0 auto;
margin-top: 64px;
}
/* .img-title2 {
background: v-bind('imageUrls.title2') no-repeat center;
width: 457px;
height: 57px;
margin: 0 auto;
margin-top: 37px;
} */
.podium {
background: v-bind('imageUrls.podium') no-repeat center;
width: 1920px;
height: 740px;
position: absolute;
bottom: 0;
left: 0;
.img-vip {
width: 969px;
height: 238px;
position: absolute;
left: 480px;
top: -60px;
background: v-bind('imageUrls.vip') center / cover no-repeat;
}
.rank-no1 {
position: relative;
.avatar {
width: 160px;
height: 160px;
border-radius: 80px;
position: absolute;
left: 884px;
top: -7px;
}
.nickname {
width: 220px;
height: 52px;
background: linear-gradient(0deg, #FFBF5A 0%, #FFE47A 100%);
border: 1px solid #FFF9DC;
text-align: center;
line-height: 46px;
position: absolute;
left: 850px;
top: 136px;
z-index: 1;
border-radius: 26px;
font-size: 30px;
color: #4B1A14;
}
}
.rank-no2 {
position: relative;
.avatar {
width: 144px;
height: 144px;
border-radius: 72px;
position: absolute;
left: 488px;
top: 27px;
/* border: 1px solid red; */
}
.nickname {
width: 220px;
height: 52px;
background: linear-gradient(0deg, #AEA6A7 0%, #EAE4DF 100%);
border: 1px solid white;
color: #383636;
text-align: center;
line-height: 52px;
position: absolute;
left: 447px;
top: 160px;
z-index: 1;
border-radius: 23px;
font-size: 30px;
}
}
.rank-no3 {
position: relative;
.avatar {
width: 144px;
height: 144px;
border-radius: 72px;
position: absolute;
left: 1297px;
top: 26px;
}
.nickname {
width: 220px;
height: 52px;
background: linear-gradient(0deg, #F3AE76 0%, #F9CDA3 100%);
color: #6F3A0E;
text-align: center;
line-height: 46px;
position: absolute;
left: 1255px;
top: 160px;
z-index: 1;
border-radius: 26px;
font-size: 30px;
border: 1px solid #FEFEFD;
}
}
}
.rank-container {
display: flex;
align-items: center;
width: 1570px;
height: 115px;
position: absolute;
bottom: 263px;
left: 200px;
>div {
flex: 1;
display: grid;
width: 100px;
height: 100%;
.no-icon {
background: v-bind('imageUrls.rankIcon') no-repeat center;
width: 42px;
height: 47px;
position: absolute;
text-align: center;
line-height: 42px;
color: #CC7F36;
font-weight: bold;
top: 50px;
}
.img-avatar-container {
position: relative;
margin: 0 auto;
margin-top: 10px;
width: 72px;
height: 72px;
border-radius: 36px;
transform: scale(1.6);
top: 50px;
}
.img-avatar-container::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: v-bind('imageUrls.avatarAnimation') center / cover no-repeat;
animation: avatar-rotate 10s linear infinite;
}
.img-avatar {
position: relative;
top: 6px;
z-index: 1;
margin: 0 auto;
width: 60px;
height: 60px;
border-radius: 30px;
background: v-bind('imageUrls.avatar') center / cover no-repeat;
}
.txt-nickname {
color: #F9EBCB;
font-size: 22px;
text-align: center;
position: absolute;
top: 170px;
text-align: center;
/* left:10px; */
margin-left: 68px;
}
}
}
.btn-next {
position: absolute;
bottom: 50px;
left: 620px;
width: 316px;
height: 105px;
cursor: pointer;
background: v-bind('imageUrls.next') center / cover no-repeat;
}
.btn-back {
position: absolute;
bottom: 50px;
left: 1020px;
width: 316px;
height: 105px;
cursor: pointer;
background: v-bind('imageUrls.back') center / cover no-repeat;
}
</style>
...@@ -4,6 +4,7 @@ import { $read } from '@/commons/utils' ...@@ -4,6 +4,7 @@ import { $read } from '@/commons/utils'
import Login from '@/pages/Login.vue' import Login from '@/pages/Login.vue'
import Main from '@/pages/Main.vue' import Main from '@/pages/Main.vue'
import Game1 from '@/pages/game1/Game.vue' import Game1 from '@/pages/game1/Game.vue'
import Game3 from '@/pages/game3/Game3.vue'
import Game4 from '@/pages/game4/Game4.vue' import Game4 from '@/pages/game4/Game4.vue'
import Game5 from '@/pages/game5/Game5.vue' import Game5 from '@/pages/game5/Game5.vue'
import Game6 from '@/pages/game6/Game.vue' import Game6 from '@/pages/game6/Game.vue'
...@@ -37,6 +38,14 @@ const router = createRouter({ ...@@ -37,6 +38,14 @@ const router = createRouter({
}, },
}, },
{ {
path: '/game3',
name: 'Game3',
component: Game3,
meta: {
requiresAuth: true,
},
},
{
path: '/game4', path: '/game4',
name: 'Game4', name: 'Game4',
component: Game4, component: Game4,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment