Commit 4b927c24 authored by 董政锦's avatar 董政锦

Merge branch 'main' into 'game4-0604'

# Conflicts: # frontend-h5/src/components/MobileStage.vue
parents 62122a8e 4a6831ac
......@@ -278,6 +278,45 @@ fn broadcast_room_rank(ranking: Vec<Value>) {
broadcast_msgpack_all_admin("room_rank_result", &ok_resp(Some(json!(ranking))));
}
fn emit_msgpack_to_user(userid: &str, event: &str, payload: &Value) {
if userid.is_empty() {
return;
}
if let Some(socket) = CLIENT_PLAYER_MAP.get(userid) {
emit_msgpack(&socket.1, event, payload);
}
}
fn push_room_score_ranking() -> bool {
let (ranking, player_ranks) = if let Ok(state) = ROOM_STATE.lock() {
let full_ranking = state.full_score_ranking();
let ranking = RoomState::ranking_payload(
&full_ranking
.iter()
.take(ROOM_SCORE_RANK_LIMIT)
.cloned()
.collect::<Vec<_>>(),
);
let player_ranks = full_ranking
.into_iter()
.enumerate()
.map(|(index, player)| (player.wechat, json!(index + 1)))
.collect::<Vec<_>>();
(ranking, player_ranks)
} else {
return false;
};
broadcast_room_rank(ranking);
for (userid, rank) in player_ranks {
emit_msgpack_to_user(&userid, "submit_score_result", &ok_resp(Some(rank)));
}
true
}
async fn sleep_room_status(round_id: u64, secs: u64, expected_status: RoomStatus) -> bool {
for _ in 0..secs {
tokio::time::sleep(Duration::from_secs(1)).await;
......@@ -306,8 +345,23 @@ fn spawn_game_timeout(round_id: u64, secs: u64) {
return;
}
if !sleep_room_status(round_id, running_secs, RoomStatus::Running).await {
return;
for _ in 0..running_secs {
tokio::time::sleep(Duration::from_secs(1)).await;
if GAME_ROUND_ID.load(Ordering::SeqCst) != round_id {
return;
}
if let Ok(state) = ROOM_STATE.lock() {
if state.status != RoomStatus::Running {
return;
}
}
// 游戏正式计时期间,每秒统一给大屏和玩家推一次实时排名。
if !push_room_score_ranking() {
return;
}
}
if let Ok(mut state) = ROOM_STATE.lock() {
......@@ -525,14 +579,16 @@ async fn handle_room_command(
player.score = score;
// player.wechat = wechat.to_string();
player.online = true;
drop(state);
// 方案1:每有一名玩家提交时发送给所有玩家跟管理员,玩家过多时发送太频繁,
// 这里会选第2种方案, 游戏中时每秒发给所有玩家跟管理员 (push_room_score_ranking)
// 玩家分数排序
let ranking = RoomState::ranking_payload(&state.score_ranking());
let player_rank = state.score_result_payload(userid);
drop(state);
emit_msgpack(&socket, evt, &ok_resp(player_rank));
//通知管理员
broadcast_room_rank(ranking);
// let ranking = RoomState::ranking_payload(&state.score_ranking());
// let player_rank = state.score_result_payload(userid);
// drop(state);
// emit_msgpack(&socket, evt, &ok_resp(player_rank));
// broadcast_room_rank(ranking);
} else {
drop(state);
emit_msgpack(&socket, evt, &err_resp("提交异常", None));
......
......@@ -151,9 +151,11 @@ defineExpose({
<div v-if="countdownVisible" class="modal-container">
<div :key="countdownKey" class="countdown-number">{{ countdown }}</div>
</div>
<div v-else-if="showGameRule" class="modal-container">
<slot name="gameRule" ></slot>
</div>
<Transition name="game-rule-fade">
<div v-if="!countdownVisible && showGameRule" class="modal-container">
<slot name="gameRule"></slot>
</div>
</Transition>
</main>
</template>
......@@ -190,6 +192,16 @@ defineExpose({
/* pointer-events: none; */
}
.game-rule-fade-enter-active,
.game-rule-fade-leave-active {
transition: opacity 0.25s ease;
}
.game-rule-fade-enter-from,
.game-rule-fade-leave-to {
opacity: 0;
}
.countdown-number {
color: white;
font-size: 200pt;
......
......@@ -16,7 +16,7 @@ const imageUrls = {
bg2: cssAssetUrl('game1/bg2.png'),
logo: cssAssetUrl('logo.png'),
title: cssAssetUrl('game1/title.png'),
avatar: cssAssetUrl('avatar.png'),
avatar: cssAssetUrl('avatar-yellow.png'),
gu: cssAssetUrl('game1/gu.png'),
clock: cssAssetUrl('game1/clock.png'),
gu01: cssAssetUrl('game1/gu01.png'),
......@@ -26,6 +26,8 @@ const imageUrls = {
back: cssAssetUrl('game1/back.png'),
replay: cssAssetUrl('game1/replay.png'),
close: cssAssetUrl('game1/close.png'),
rule: cssAssetUrl('game1/rule.png')
}
const wechat = $getWechat()
......@@ -140,14 +142,6 @@ function showScoreView() {
currentView.value = 'score'
}
function replayGame() {
startGameView()
}
function backToWaiting() {
resetToLoadingView()
joinSharedRoom(nickname.value, token.value, avatar.value)
}
function handleRoomBack() {
resetToLoadingView()
......@@ -161,7 +155,7 @@ const touchHandler = () => {
return
}
tick.value += 1
tick.value += 5
submitScore()
playGame1Music();
isGuPlaying.value = true
......@@ -250,12 +244,14 @@ const showGameRuleHandler = () => {
<div class="rule-txt-container">
<div class="rule-title">游戏规则</div>
<div class="rule-txt">
微信用户通过扫描大屏幕二维码参与,可容纳多人同时进行且比拼敲鼓,谁敲的最多即为获胜,游戏结束按分值高低先后排名,有机会获得新疆福彩定制周边礼品一份。
1.主持人开赛即可点击鼓面,单次点击得 5 分;<br />
2.限时比拼手速,结束后按积分排名;<br />
3.高分用户有机会领取新疆福彩定制周边;<br />
4.禁止外挂,作弊,违规账号取消成绩与领奖资格;<br /><br />
祝您游戏愉快!
</div>
</div>
<div class="rule-close">
<div @click="showGameRuleHandler"></div>
</div>
<div class="rule-close" @click="showGameRuleHandler"></div>
</div>
</template>
<LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
......@@ -263,8 +259,7 @@ const showGameRuleHandler = () => {
<PlayingView v-else-if="currentView === 'playing'" :image-urls="imageUrls" :tick="tick" :rank="rank"
:current-gu="currentGu" :countdown-interval="countdownInterval" :is-div-desc-visible="isDivDescVisible"
@touch="touchHandler" />
<ScoreView v-else :image-urls="imageUrls" :rank="rank" :tick="tick" @replay="replayGame"
@back="backToWaiting" />
<ScoreView v-else :image-urls="imageUrls" :rank="rank" :tick="tick" />
</MobileStage>
<div v-else style="text-align: center; width: 100vw; height: 100vh; line-height: 30; font-size: 20px;">
请使用微信扫码进入游戏
......@@ -273,40 +268,39 @@ const showGameRuleHandler = () => {
<style scoped>
.rule-container {
.rule-txt-container {
background: #FFF9E4;
margin:0 40px;
border-radius: 5px;
padding-top: 20px;
padding-bottom: 10px;
position: absolute;
background: v-bind('imageUrls.rule') center / cover no-repeat;
width: 692px;
height: 797px;
left: -157px;
top: -75px;
transform: scale(0.5);
.rule-title {
text-align: center;
color: #850000;
color: white;
font-weight: bold;
font-size: 15pt;
font-size: 22pt;
margin-top: 10px;
letter-spacing: 4px;
}
.rule-txt {
font-size: 12pt;
padding: 10px;
font-size: 26pt;
padding: 40px;
margin-top: 10px;
color: #AA0000;
}
}
.rule-close {
width: 32px;
height: 130px;
width: 57px;
height: 57px;
margin: 0 auto;
margin-top: 450px;
background: v-bind('imageUrls.close') center/cover;
position: relative;
>div {
position: absolute;
bottom: 0;
width: 32px;
height: 32px;
border-radius: 16px;
}
transform: scale(0.8);
}
}
</style>
\ No newline at end of file
......@@ -20,12 +20,11 @@ onMounted(() => {
<div class="game-desc" @click="$emit('touchGameRule')">游戏规则</div>
<div class="img-logo"></div>
<div class="img-title"></div>
<div class="img-avatar" :style="`background: url(${avatar});border-radius:66px;width:132px;height:132px;`"></div>
<div class="txt-nickname">{{ nickname }}</div>
<div class="txt-loading">
<label v-if="userJoinStatus">您已成功加入游戏等待主持人开始</label>
<label v-else>正在加入游戏...</label>
<div class="img-avatar" :style="`background: url(${avatar});border-radius:66px;width:132px;height:132px;`">
</div>
<div class="txt-nickname">{{ nickname }}</div>
<div class="txt-loading">您已成功加入游戏等待主持人开始</div>
<div class="txt-loading2">游戏即将开始 敬请期待</div>
<div class="bg-bottom"></div>
<div class="loading-gu"></div>
</div>
......@@ -105,7 +104,7 @@ onMounted(() => {
top: 640px;
left: 50%;
color: #E00000;
font-size: 25pt;
font-size: 30pt;
font-weight: bold;
transform: translateX(-50%);
}
......@@ -115,29 +114,28 @@ onMounted(() => {
top: 710px;
left: 50%;
width: 288px;
color: #E00000;
color: #6F0000;
font-size: 25pt;
font-weight: bold;
text-align: center;
transform: translateX(-50%);
}
.txt-loading label {
display: inline-block;
transform-origin: center;
animation: loading-text-scale 1.2s ease-in-out infinite;
}
@keyframes loading-text-scale {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.08);
}
.txt-loading2 {
width: 500px;
height: 90px;
background: #E61903;
color: white;
position: absolute;
top:850px;
left: 50%;
transform: translate(-50%);
line-height: 90px;
text-align: center;
font-size: 36px;
font-weight: bold;
letter-spacing:5px;
border-radius: 10px;
}
.loading-gu {
......
......@@ -37,10 +37,6 @@ onMounted(() => {
<div><label>{{ rank }}</label></div>
</div>
</div>
<div class="btn-group">
<div class="btn-replay" @click="$emit('replay')"></div>
<div class="btn-back" @click="$emit('back')"></div>
</div>
</div>
</template>
......@@ -142,26 +138,4 @@ onMounted(() => {
margin: 0 10px;
}
.btn-group {
position: absolute;
top: 1320px;
left: 50%;
display: flex;
gap: 40px;
transform: translateX(-50%);
}
.btn-replay,
.btn-back {
width: 315px;
height: 100px;
}
.btn-replay {
background: v-bind('imageUrls.replay') center/cover;
}
.btn-back {
background: v-bind('imageUrls.back') center/cover;
}
</style>
......@@ -120,7 +120,7 @@ function handleTargetTouch(index: number) {
return
}
const scoreDelta = item.kind === 'mole' ? 1 : -1
const scoreDelta = item.kind === 'mole' ? 5 : -3
emit('touch', scoreDelta)
clearTargetTimer(item)
item.state = 'hit'
......
<script setup lang="ts">
import { cssAssetUrl } from '@/commons/assets.ts';
const imageUrls = {
bg1: cssAssetUrl('main-bg1.png'),
logo: cssAssetUrl('game1/logo.png'),
title1: cssAssetUrl('main-title.png'),
}
</script>
<template>
<div class="bg">
<div class="img-logo"></div>
<div class="img-title1"></div>
<div class="rule-container">
<div class="rule-title">游戏规则</div>
<div class="rule-text">
扫码即玩,大屏实时同步。每局结束大屏公布前10名。主持人邀请获奖用户,凭获奖界面作为凭证,上台领奖。,核对后发放实物奖品。奖品不折现、不替换,逾期作废。
</div>
</div>
</div>
</template>
<style scoped>
.bg {
width: 1920px;
height: 1080px;
background: v-bind('imageUrls.bg1') 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') center / cover no-repeat;
width: 1024px;
height: 143px;
margin: 0 auto;
position: relative;
top: 50px;
}
.rule-container {
box-sizing: border-box;
width: 638px;
min-height: 162px;
border-radius: 12px;
background:
linear-gradient(90deg, rgba(124, 77, 28, 0.74), rgba(136, 122, 76, 0.72)),
rgba(124, 91, 48, 0.52);
backdrop-filter: blur(18px) saturate(0.9);
-webkit-backdrop-filter: blur(18px) saturate(0.9);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
inset 0 -1px 0 rgba(0, 0, 0, 0.08);
color: white;
padding: 27px 34px 24px;
position: absolute;
left: 0;
top: 0;
font-weight: 700;
text-shadow:
0 2px 0 rgba(0, 0, 0, 0.65),
1px 0 0 rgba(0, 0, 0, 0.45),
-1px 0 0 rgba(0, 0, 0, 0.45),
0 -1px 0 rgba(0, 0, 0, 0.45);
}
.rule-title {
margin-bottom: 12px;
font-size: 22px;
line-height: 1.2;
text-align: center;
}
.rule-text {
font-size: 16px;
line-height: 1.58;
}
</style>
......@@ -64,7 +64,7 @@ const renderAvatar = (item: any | null) => {
<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.avatar)}`"></div>
<div class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div>
<div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div>
</div>
......
......@@ -182,9 +182,12 @@ onUnmounted(() => {
</script>
<template>
<Rank1 ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic" />
<!-- <Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount"
<!-- <Loading :players="loadingPlayers" :player-count="playerCount"
@start="startGameWithMusic" /> -->
<!-- <Rank1 ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic" /> -->
<!-- <Rank2 :rank-list="rankPlayers" @next="nextRoundWithMusic" /> -->
<Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount"
@start="startGameWithMusic" />
<Rank1 v-else-if="screen === 'rank1'" ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic" />
<Rank2 v-else :rank-list="rankPlayers" @next="nextRoundWithMusic" /> -->
<Rank2 v-else :rank-list="rankPlayers" @next="nextRoundWithMusic" />
</template>
......@@ -19,7 +19,7 @@ const imageUrls = {
title1: cssAssetUrl('game6/title.png'),
title2: cssAssetUrl('game1/title2.png'),
qrcode: assetUrl('game6/qrcode.png'),
gu: cssAssetUrl('game1/gu.png'),
// gu: cssAssetUrl('game1/gu.png'),
// bg: cssAssetUrl('game1/bg.png'),
// logo: cssAssetUrl('game1/logo.png'),
......@@ -27,12 +27,12 @@ const imageUrls = {
// title2: cssAssetUrl('game1/title2.png'),
// qrcode: assetUrl('game1/qrcode.png'),
// gu: cssAssetUrl('game1/gu.png'),
imgMole:cssAssetUrl('game6/mole.png'),
imgRabbit:cssAssetUrl('game6/rabbit.png'),
imgMole: cssAssetUrl('game6/mole.png'),
imgRabbit: cssAssetUrl('game6/rabbit.png'),
bottomLayers: cssAssetUrl('game1/bottom-layers.png'),
avatarAnimation: cssAssetUrl('avatar_animation.png'),
avatar: cssAssetUrl('avatar.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'),
......@@ -52,7 +52,7 @@ const releaseStartButton = () => {
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>
......@@ -75,7 +75,7 @@ const renderAvatar = (item: any | null) => {
<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 class="img-avatar" :style="`${renderAvatar(item)}`"></div>
</div>
<div class="txt-nickname">{{ $format_str(item.nickname, 12) }}</div>
</div>
......@@ -83,14 +83,9 @@ const renderAvatar = (item: any | null) => {
</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" :class="{ pressed: isStartPressed }" @pointerdown="pressStartButton"
@pointerup="releaseStartButton" @pointerleave="releaseStartButton"
@pointercancel="releaseStartButton"></div>
</div>
</div>
</div>
......@@ -164,6 +159,7 @@ const renderAvatar = (item: any | null) => {
top: 315px;
left: 150px;
}
.img-rabbit {
background: v-bind('imageUrls.imgRabbit') center / cover no-repeat;
width: 475px;
......@@ -180,7 +176,7 @@ const renderAvatar = (item: any | null) => {
position: absolute;
left: 60px;
bottom: 60px;
border:3px solid #AA0000;
border: 3px solid #AA0000;
border-radius: 10px;
.txt-count {
......@@ -221,7 +217,7 @@ const renderAvatar = (item: any | null) => {
position: absolute;
inset: 0;
border-radius: inherit;
background: v-bind('imageUrls.avatarAnimation');
background: v-bind('imageUrls.avatarAnimation') center / cover no-repeat;
animation: avatar-rotate 10s linear infinite;
}
......
<script setup lang="ts">
import { ref, watch } from 'vue'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import DesignStage from '../../../components/DesignStage.vue'
import type Player from '../../../commons/player.ts'
import { $is_run_local } from '@/commons/utils.ts';
import { $format_str, $is_run_local } from '@/commons/utils.ts';
import { assetUrl, cssAssetUrl } from '@/commons/assets.ts';
const props = defineProps<{
......@@ -17,6 +17,10 @@ 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);
......@@ -26,15 +30,15 @@ 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 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 guFrames = [
// loadGuFrames('game1/gu'),
// loadGuFrames('game1/gu2'),
// ];
const imageUrls = {
bg: cssAssetUrl('game6/bg2.png'),
......@@ -52,9 +56,15 @@ const imageUrls = {
// rankNo1Icon: cssAssetUrl('game1/no1-icon.png'),
// rankNo2Icon: cssAssetUrl('game1/no2-icon.png'),
// rankNo3Icon: cssAssetUrl('game1/no3-icon.png'),
avatar: assetUrl('avatar.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 => ({
......@@ -67,6 +77,21 @@ const emptyPlayer = (index: number): Player => ({
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}`,
......@@ -155,96 +180,153 @@ const startGame = () => {
}
};
const stopPlayerGu = () => {
if (guTimer) {
clearTimeout(guTimer);
guTimer = undefined;
}
if (guBurstTimer) {
clearTimeout(guBurstTimer);
guBurstTimer = undefined;
}
guBurstVisible.value = false;
guFrameIndex.value = 0;
}
const randomMoleKind = (includeLand = true): MoleKind => {
const kinds: MoleKind[] = includeLand ? ["land", "mole", "rabbit"] : ["mole", "rabbit"];
return kinds[Math.floor(Math.random() * kinds.length)] ?? "land";
};
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);
const clearMoleTimer = (index: number) => {
const timer = moleTimers[index];
if (timer) {
clearTimeout(timer);
moleTimers[index] = undefined;
}
};
return guFramesReady;
}
const playGuLoopDelayAnimation = () => {
if (guBurstTimer) {
clearTimeout(guBurstTimer);
guBurstTimer = 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;
};
guBurstVisible.value = false;
requestAnimationFrame(() => {
guBurstKey.value += 1;
guBurstVisible.value = true;
guBurstTimer = setTimeout(() => {
guBurstVisible.value = false;
guBurstTimer = undefined;
}, GU_LOOP_DELAY);
});
}
const scheduleMoleCell = (index: number, kind = randomMoleKind()) => {
clearMoleTimer(index);
const playerGu = async () => {
stopPlayerGu();
const frameCount = Math.min(...guFrames.map((frames) => frames.length));
if (frameCount <= 0) return;
if (kind === "land") {
setMoleCell(index, "land", "land");
moleTimers[index] = setTimeout(() => scheduleMoleCell(index, randomMoleKind(false)), MOLE_LAND_DURATION);
return;
}
await preloadGuFrames();
setMoleCell(index, kind, "show");
moleTimers[index] = setTimeout(() => {
setMoleCell(index, kind, "hide");
moleTimers[index] = setTimeout(() => scheduleMoleCell(index, "land"), MOLE_HIDE_DURATION);
}, MOLE_SHOW_DURATION);
};
let frameIndex = 0;
const tick = () => {
guFrameIndex.value = frameIndex;
const moleCellClass = (cell: MoleCell) => [
`mole-${cell.kind}`,
`mole-${cell.phase}`,
];
if (frameIndex >= frameCount - 1) {
frameIndex = 0;
playGuLoopDelayAnimation();
guTimer = setTimeout(tick, GU_LOOP_DELAY);
return;
}
onMounted(() => {
moleCells.value.forEach((_, index) => {
moleTimers[index] = setTimeout(
() => scheduleMoleCell(index, randomMoleKind()),
Math.floor(Math.random() * MOLE_LAND_DURATION),
);
});
});
frameIndex += 1;
guTimer = setTimeout(tick, GU_FRAME_DURATION);
}
onBeforeUnmount(() => {
moleTimers.forEach((_, index) => clearMoleTimer(index));
});
tick();
}
// 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();
// playerGu();
await designStage.value?.startCountdown(START_COUNTDOWN_SECONDS, 60);
stopPlayerGu();
// stopPlayerGu();
};
defineExpose({
startIntro,
});
const avatarStyle = (item: Player) => {
const avatar = item.avatar || imageUrls.avatar
return {
backgroundImage: `url("${avatar}")`,
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,
......@@ -266,7 +348,9 @@ const rankIcon = (index: number) => {
width: w + 'px',
height: h + 'px',
left: l + 'px',
top: t + 'px'
top: t + 'px',
lineHeight: h + 'px',
color: index <= 2 ? 'white' : '#CC7F36',
};
}
</script>
......@@ -278,13 +362,26 @@ const rankIcon = (index: number) => {
<div class="img-title"></div>
<div class="img-mole"></div>
<div class="rank-container">
<div v-for="(item, index) in list">
<div v-for="(item, index) in list" :key="playerKey(item, index)">
<div>
<div class="rank-icon" :style="rankIcon(index)"></div>
<div class="rank-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>
......@@ -334,7 +431,7 @@ const rankIcon = (index: number) => {
align-items: center;
column-gap: 42px;
box-sizing: border-box;
padding: 42px 42px;
padding: 30px;
width: 1832px;
height: 244px;
position: absolute;
......@@ -357,90 +454,174 @@ const rankIcon = (index: number) => {
.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-gu-group {
.img-gu {
width: 400px;
height: 400px;
transform: scale(1.6);
.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;
top: 400px;
inset: 0;
border-radius: inherit;
background: v-bind('imageUrls.avatarAnimation');
animation: avatar-rotate 10s linear infinite;
}
.img-gu:nth-child(1) {
left: 200px;
.img-avatar {
width: 80px;
height: 80px;
border-radius: 40px;
position: absolute;
left: 6px;
top: 5px;
}
.img-gu:nth-child(2) {
right: 200px;
.txt-nickname {
position: absolute;
bottom: 40px;
text-align: center;
width: 100%;
color: #F9EBCB;
}
.img-gu {
.gu-frame {
position: absolute;
inset: 0;
opacity: 0;
background-position: center;
.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-size: contain;
pointer-events: none;
will-change: opacity;
background-position: 0 0;
background-size: auto 500px;
}
.gu-frame.active {
opacity: 1;
&.mole-land .mole-sprite {
background: v-bind('imageUrls.land') center / cover no-repeat;
}
.gu-burst {
width: 200px;
height: 200px;
position: absolute;
right: 80px;
top: 30px;
opacity: 0;
transform: scale(0.6);
transform-origin: center center;
background: v-bind('imageUrls.fireworks') center / cover no-repeat;
pointer-events: none;
&.active {
animation: gu-burst-expand 1s ease-out both;
}
&.mole-mole.mole-show .mole-sprite {
background-image: v-bind('imageUrls.moleShow');
animation: mole-show 1.2s steps(30) both;
}
}
}
.rank-move {
transition: transform 0.45s ease;
}
&.mole-mole.mole-hide .mole-sprite {
background-image: v-bind('imageUrls.moleHide');
animation: mole-hide 0.65s steps(15) both;
}
.rank-enter-active,
.rank-leave-active {
transition: opacity 0.25s ease, transform 0.25s ease;
}
&.mole-rabbit.mole-show .mole-sprite {
background-image: v-bind('imageUrls.rabbitShow');
animation: mole-show 1.2s steps(30) both;
}
.rank-leave-active {
position: absolute;
left: 17px;
}
&.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;
}
.rank-enter-from,
.rank-leave-to {
opacity: 0;
transform: translateY(90px);
&:nth-child(8) {
left: 997px;
top: 460px;
}
&:nth-child(9) {
left: 1297px;
top: 480px;
}
}
}
.rank-item.up,
.rank-item.new {
animation: rank-up-flash 0.8s ease;
@keyframes mole-show {
from {
background-position-x: 0;
}
to {
background-position-x: -15000px;
}
}
.rank-item.down {
animation: rank-down-flash 0.8s ease;
@keyframes mole-hide {
from {
background-position-x: 0;
}
to {
background-position-x: -7500px;
}
}
</style>
......@@ -14,22 +14,15 @@ const emit = defineEmits<{
const FINAL_RANK_LIMIT = 10;
const imageUrls = {
bg: cssAssetUrl('game1/bg.png'),
bg: cssAssetUrl('game6/bg3.png'),
logo: cssAssetUrl('game1/logo.png'),
title: cssAssetUrl('game1/title3.png'),
title2: cssAssetUrl('game1/title4.png'),
podium: cssAssetUrl('game1/podium.png'),
fireworks: cssAssetUrl('game1/fireworks.png'),
avatar: cssAssetUrl('avatar.png'),
guan1: cssAssetUrl('game1/guan1.png'),
pai1: cssAssetUrl('game1/pai1.png'),
guan2: cssAssetUrl('game1/guan2.png'),
pai2: cssAssetUrl('game1/pai2.png'),
guan3: cssAssetUrl('game1/guan3.png'),
pai3: cssAssetUrl('game1/pai3.png'),
rankIcon: cssAssetUrl('game1/no-icon.png'),
avatarAnimation: cssAssetUrl('avatar_animation.png'),
next: cssAssetUrl('game1/next-1.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('game6/next-1.png'),
};
const emptyPlayer = (index: number): Player => ({
......@@ -58,8 +51,8 @@ watch(
const renderAvatar = (item: any | null) => {
if (item && item.avatar) {
return `background: url('${item.avatar}') center / cover no-repeat;`
}
return `background: v-bind('imageUrls.avatar') center / cover no-repeat;`
}
return `background: ${imageUrls.avatar} center / cover no-repeat;`
}
</script>
......@@ -71,8 +64,8 @@ const renderAvatar = (item: any | null) => {
<div class="img-title2"></div>
<div class="podium">
<div class="img-vip"></div>
<div class="rank-no1">
<div class="gu-burst"></div>
<div class="avatar" :style="`${renderAvatar(list[0])}`">
</div>
<div class="nickname">{{ list[0]?.nickname }}</div>
......@@ -80,7 +73,6 @@ const renderAvatar = (item: any | null) => {
<div class="pai"></div>
</div>
<div class="rank-no2">
<div class="gu-burst"></div>
<div class="avatar" :style="`${renderAvatar(list[1])}`">
</div>
<div class="nickname">{{ list[1]?.nickname }}</div>
......@@ -88,7 +80,6 @@ const renderAvatar = (item: any | null) => {
<div class="pai"></div>
</div>
<div class="rank-no3">
<div class="gu-burst"></div>
<div class="avatar" :style="`${renderAvatar(list[2])}`">
</div>
<div class="nickname">{{ list[2]?.nickname }}</div>
......@@ -136,11 +127,11 @@ const renderAvatar = (item: any | null) => {
}
.img-title {
background: v-bind('imageUrls.title');
width: 967px;
height: 122px;
background: v-bind('imageUrls.title') center / cover no-repeat;
width: 831px;
height: 120px;
margin: 0 auto;
margin-top: 37px;
margin-top: 64px;
}
.img-title2 {
......@@ -153,176 +144,106 @@ const renderAvatar = (item: any | null) => {
.podium {
background: v-bind('imageUrls.podium') no-repeat center;
width: 1347px;
height: 269px;
width: 1920px;
height: 740px;
position: absolute;
top: 427px;
left: 287px;
bottom: 0;
left: 0;
.gu-burst {
width: 200px;
height: 200px;
.img-vip {
width: 969px;
height: 238px;
position: absolute;
z-index: 1;
border-radius: 50%;
opacity: 1;
transform: scale(0.6);
transform-origin: center center;
background: v-bind('imageUrls.fireworks') center / cover no-repeat;
pointer-events: none;
animation: gu-burst-expand 1.2s ease-out infinite;
left: 480px;
top: -60px;
background: v-bind('imageUrls.vip') center / cover no-repeat;
}
.rank-no1 {
position: relative;
.gu-burst {
left: 580px;
top: -200px;
}
.avatar {
/* background: v-bind('imageUrls.avatar') center / cover no-repeat; */
width: 120px;
height: 120px;
border-radius: 60px;
width: 160px;
height: 160px;
border-radius: 80px;
position: absolute;
left: 610px;
top: -136px;
left: 884px;
top: -7px;
}
.nickname {
width: 167px;
height: 46px;
background: #FFCF44;
color: white;
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: 590px;
top: -25px;
left: 850px;
top: 136px;
z-index: 1;
border-radius: 23px;
}
.guan {
background: v-bind('imageUrls.guan1');
width: 64px;
height: 64px;
position: absolute;
left: 580px;
top: -153px
}
.pai {
background: v-bind('imageUrls.pai1');
width: 212px;
height: 200px;
position: absolute;
left: 570px;
top: 40px;
border-radius: 26px;
font-size: 30px;
color: #4B1A14;
}
}
.rank-no2 {
position: relative;
.gu-burst {
left: 140px;
top: -170px;
}
.avatar {
background: v-bind('imageUrls.avatar') center / cover no-repeat;
width: 120px;
height: 120px;
border-radius: 60px;
width: 144px;
height: 144px;
border-radius: 72px;
position: absolute;
left: 180px;
top: -110px;
left: 488px;
top: 27px;
/* border: 1px solid red; */
}
.nickname {
width: 167px;
height: 46px;
background: #CA9600;
color: white;
width: 220px;
height: 52px;
background: linear-gradient(0deg, #AEA6A7 0%, #EAE4DF 100%);
border: 1px solid white;
color: #383636;
text-align: center;
line-height: 46px;
line-height: 52px;
position: absolute;
left: 157px;
top: 2px;
left: 447px;
top: 160px;
z-index: 1;
border-radius: 23px;
}
.guan {
background: v-bind('imageUrls.guan2') no-repeat center;
width: 96px;
height: 96px;
position: absolute;
left: 134px;
top: -144px;
}
.pai {
background: v-bind('imageUrls.pai2') no-repeat center;
width: 212px;
height: 200px;
position: absolute;
left: 140px;
top: 50px;
font-size: 30px;
}
}
.rank-no3 {
position: relative;
.gu-burst {
left: 1020px;
top: -170px;
}
.avatar {
background: v-bind('imageUrls.avatar') center / cover no-repeat;
width: 120px;
height: 120px;
border-radius: 60px;
width: 144px;
height: 144px;
border-radius: 72px;
position: absolute;
left: 1046px;
top: -110px;
left: 1297px;
top: 26px;
}
.nickname {
width: 167px;
height: 46px;
background: #E2E2E2;
color: #AFAFAF;
width: 220px;
height: 52px;
background: linear-gradient(0deg, #F3AE76 0%, #F9CDA3 100%);
color: #6F3A0E;
text-align: center;
line-height: 46px;
position: absolute;
left: 1028px;
top: 2px;
left: 1255px;
top: 160px;
z-index: 1;
border-radius: 23px;
}
.guan {
background: v-bind('imageUrls.guan3') no-repeat center;
width: 96px;
height: 96px;
position: absolute;
left: 1003px;
top: -146px;
}
.pai {
background: v-bind('imageUrls.pai3') no-repeat center;
width: 212px;
height: 200px;
position: absolute;
right: 130px;
top: 50px;
border-radius: 26px;
font-size: 30px;
border: 1px solid #FEFEFD;
}
}
}
......@@ -330,11 +251,11 @@ const renderAvatar = (item: any | null) => {
.rank-container {
display: flex;
align-items: center;
width: 1270px;
width: 1570px;
height: 115px;
position: absolute;
bottom: 243px;
left: 325px;
bottom: 263px;
left: 200px;
>div {
flex: 1;
......@@ -344,13 +265,14 @@ const renderAvatar = (item: any | null) => {
.no-icon {
background: v-bind('imageUrls.rankIcon') no-repeat center;
width: 40px;
height: 40px;
width: 42px;
height: 47px;
position: absolute;
text-align: center;
line-height: 40px;
color: white;
line-height: 42px;
color: #CC7F36;
font-weight: bold;
top: 50px;
}
.img-avatar-container {
......@@ -360,6 +282,8 @@ const renderAvatar = (item: any | null) => {
width: 72px;
height: 72px;
border-radius: 36px;
transform: scale(1.6);
top: 50px;
}
.img-avatar-container::before {
......@@ -367,7 +291,7 @@ const renderAvatar = (item: any | null) => {
position: absolute;
inset: 0;
border-radius: inherit;
background: v-bind('imageUrls.avatarAnimation');
background: v-bind('imageUrls.avatarAnimation') center / cover no-repeat;
animation: avatar-rotate 10s linear infinite;
}
......@@ -379,23 +303,28 @@ const renderAvatar = (item: any | null) => {
width: 60px;
height: 60px;
border-radius: 30px;
background: v-bind('imageUrls.avatar');
background: v-bind('imageUrls.avatar') center / cover no-repeat;
}
.txt-nickname {
color: white;
font-size: 20px;
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: 100px;
bottom: 50px;
left: 820px;
width: 316px;
height: 105px;
width: 318px;
height: 95px;
cursor: pointer;
background: v-bind('imageUrls.next') center / cover no-repeat;
}
......
import { createRouter, createWebHashHistory } from 'vue-router'
import { $read } from '@/commons/utils'
import Main from '@/pages/Main.vue'
import Login from '@/pages/Login.vue'
import Game1 from '@/pages/game1/Game.vue'
import Game4 from '@/pages/game4/Game4.vue'
......@@ -10,7 +11,12 @@ const router = createRouter({
routes: [
{
path: '/',
redirect: () => ($read('token', '') ? '/game1' : '/login'),
redirect: () => ($read('token', '') ? '/game1' : '/main'),
},
{
path: '/main',
name: 'Main',
component: Main,
},
{
path: '/login',
......
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