Commit 5629f083 authored by 董政锦's avatar 董政锦

build: 图片压缩;

parent 4f1a0f8e
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from "vue"; import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from "@/commons/assets.ts"; import { cssAssetUrl } from '@/commons/assets.ts'
import MobileStage from "@/components/MobileStage.vue"; import MobileStage from '@/components/MobileStage.vue'
import { import { useGameSocket, joinSharedRoom, sendGameMessage, userJoinStatus } from '@/composables/useGameSocket'
useGameSocket, import LoadingView from './views/LoadingView.vue'
sendGameMessage, import PlayingView from './views/PlayingView.vue'
userJoinStatus, import { $getWechat, $toast, $is_run_local } from '@/commons/utils.ts'
joinSharedRoom, import { playGame6Music, stopGame6Music } from '@/commons/music';
} from "@/composables/useGameSocket";
import LoadingView from "./views/LoadingView.vue"; type GameView = 'loading' | 'playing'
import PlayingView from "./views/PlayingView.vue";
import { $getWechat, $toast, $is_run_local } from "@/commons/utils.ts";
import { playGame3Music, stopAllMusic } from "@/commons/music";
// ⚠ 发布上线时设为 true,本地测试时设为 false
const IS_PRODUCTION = true;
type GameView = "loading" | "playing";
type RankPlayer = { type RankPlayer = {
rank: number; rank: number
nickname: string; nickname: string
avatar: string; avatar: string
score: number | string; score: number | string
}; }
const imageUrls: Record<string, string> = { const imageUrls: Record<string, string> = {
bg: cssAssetUrl("game3/bg.webp"), bg: cssAssetUrl('game3/bg.webp'),
bg3: cssAssetUrl("game3/bg3.webp"), bg3: cssAssetUrl('game3/bg3.webp'),
left: cssAssetUrl("game3/icon_zuo.webp"), left: cssAssetUrl('game3/icon_zuo.webp'),
right: cssAssetUrl("game3/icon_you.webp"), right: cssAssetUrl('game3/icon_you.webp'),
bg4: cssAssetUrl("game3/pic_paodao.webp"), bg4: cssAssetUrl('game3/pic_paodao.webp'),
logo: cssAssetUrl("logo.png"), logo: cssAssetUrl('logo.png'),
loadingBg1: cssAssetUrl("game6/loadingbg1.png"), loadingBg1: cssAssetUrl('game6/loadingbg1.png'),
loadingBg2: cssAssetUrl("game6/loadingbg2.png"), loadingBg2: cssAssetUrl('game6/loadingbg2.png'),
close: cssAssetUrl("game1/close.png"), close: cssAssetUrl('game1/close.png'),
clock: cssAssetUrl("game1/clock.png"), clock: cssAssetUrl('game1/clock.png'),
bg2: cssAssetUrl("game6/bg2.png"), bg2: cssAssetUrl('game6/bg2.png'),
hydt: cssAssetUrl("game3/title3.webp"), hydt: cssAssetUrl('game3/title3.webp'),
rank: cssAssetUrl("game6/rank.png"), rank: cssAssetUrl('game6/rank.png'),
moleShowSprite: cssAssetUrl("game6/mole_show.png"), moleShowSprite: cssAssetUrl('game6/mole_show.png'),
moleHideSprite: cssAssetUrl("game6/mole_hide.png"), moleHideSprite: cssAssetUrl('game6/mole_hide.png'),
moleHideprite: cssAssetUrl("game6/mole_hit.png"), moleHideprite: cssAssetUrl('game6/mole_hit.png'),
rabbitFaintprite: cssAssetUrl("game6/rabbit_faint.png"), rabbitFaintprite: cssAssetUrl('game6/rabbit_faint.png'),
rabbitHideprite: cssAssetUrl("game6/rabbit_hide.png"), rabbitHideprite: cssAssetUrl('game6/rabbit_hide.png'),
rabbitshowprite: cssAssetUrl("game6/rabbit_show.png"), rabbitshowprite: cssAssetUrl('game6/rabbit_show.png'),
land: cssAssetUrl("game6/land.png"), land: cssAssetUrl('game6/land.png'),
hammer: cssAssetUrl("game6/hammer.png"), hammer: cssAssetUrl('game6/hammer.png'),
ranklist: cssAssetUrl("game6/ranklist.png"), ranklist: cssAssetUrl('game6/ranklist.png'),
rule: cssAssetUrl("game1/rule.png"), rule: cssAssetUrl('game1/rule.png')
}; }
const wechat = $getWechat(); const wechat = $getWechat()
const currentView = ref<GameView>("loading"); const currentView = ref<GameView>('loading')
const countdownInterval = ref(60); const countdownInterval = ref(60)
const isDivDescVisible = ref(false); const isDivDescVisible = ref(false)
const isGameOver = ref(false); const score = ref(0)
const score = ref(0); const rank = ref(0) //排名
const rank = ref(0); //排名 const rankList = ref<RankPlayer[]>([])
const rankList = ref<RankPlayer[]>([]);
const displayRankList = computed<RankPlayer[]>(() => { const displayRankList = computed<RankPlayer[]>(() => {
const list = rankList.value.slice(0, 10); const list = rankList.value.slice(0, 10)
return Array.from({ length: 10 }, (_, index) => { return Array.from({ length: 10 }, (_, index) => {
return ( return list[index] ?? {
list[index] ?? { rank: index + 1,
rank: index + 1, nickname: '-',
nickname: "-", avatar: '',
avatar: "", score: '-',
score: "-", }
} })
); })
});
});
const rankScore = computed<number | string>(() => { const rankScore = computed<number | string>(() => {
const currentPlayer = rankList.value const currentPlayer = rankList.value
.slice(0, 10) .slice(0, 10)
.find((item) => item.rank === rank.value); .find(item => item.rank === rank.value)
return currentPlayer?.score ?? score.value; return currentPlayer?.score ?? score.value
}); })
// const guFrames: [string, string, string] = [imageUrls.gu01, imageUrls.gu02, imageUrls.gu03] // const guFrames: [string, string, string] = [imageUrls.gu01, imageUrls.gu02, imageUrls.gu03]
// const currentGu = ref(imageUrls.gu01) // const currentGu = ref(imageUrls.gu01)
const isGuPlaying = ref(false); const isGuPlaying = ref(false)
const token = ref(wechat?.token ?? ""); const token = ref(wechat?.token ?? '')
const nickname = ref(wechat?.nickname ?? ""); const nickname = ref(wechat?.nickname ?? '')
const avatar = ref(wechat?.avatar ?? ""); const avatar = ref(wechat?.avatar ?? '')
const showGameRule = ref(true); const showGameRule = ref(true)
const showGameRank = ref(false); const showGameRank = ref(false)
const isLocalMode = !IS_PRODUCTION && $is_run_local(); const isLocalMode = $is_run_local()
const mobileStageRef = ref<InstanceType<typeof MobileStage> | null>(null); const mobileStageRef = ref<InstanceType<typeof MobileStage> | null>(null)
const _offSocketMessage = ref<(() => void) | undefined>(); const _offSocketMessage = ref<(() => void) | undefined>()
let guFrameTimer: ReturnType<typeof window.setTimeout> | undefined; let guFrameTimer: ReturnType<typeof window.setTimeout> | undefined
let gameCountdownTimer: ReturnType<typeof window.setTimeout> | undefined; let gameCountdownTimer: ReturnType<typeof window.setTimeout> | undefined
const confirmRefresh = (event: BeforeUnloadEvent) => { const confirmRefresh = (event: BeforeUnloadEvent) => {
event.preventDefault(); event.preventDefault()
event.returnValue = ""; event.returnValue = ''
}; }
function setDivDescVisible(visible: boolean) { function setDivDescVisible(visible: boolean) {
isDivDescVisible.value = visible; isDivDescVisible.value = visible
} }
function stopGameCountdown() { function stopGameCountdown() {
if (gameCountdownTimer) { if (gameCountdownTimer) {
window.clearTimeout(gameCountdownTimer); window.clearTimeout(gameCountdownTimer)
gameCountdownTimer = undefined; gameCountdownTimer = undefined
} }
} }
function submitScore(save: boolean = false, currentTick = score.value) { function submitScore(save: boolean = false, currentTick = score.value) {
if (!wechat && !isLocalMode) return; if (!wechat && !isLocalMode) return
if (save) { if (save) {
if (isLocalMode && !wechat) { if (isLocalMode && !wechat) {
console.log("[game3 本地模式] 提交最终分数:", currentTick); console.log('[game3 本地模式] 提交最终分数:', currentTick)
return; return
} }
sendGameMessage("submit_score_save", { sendGameMessage('submit_score_save', {
score: currentTick, score: currentTick,
wechat: wechat.token_origin, wechat: wechat.token_origin,
item_num: 3, item_num: 3,
nickname: wechat.nickname, nickname: wechat.nickname,
avatar: wechat.avatar, avatar: wechat.avatar,
rank: rank.value, rank: rank.value,
}); })
} else { } else {
if (isLocalMode && !wechat) { if (isLocalMode && !wechat) {
console.log("[game3 本地模式] 实时分数:", currentTick); console.log('[game3 本地模式] 实时分数:', currentTick)
return; return
}
sendGameMessage('submit_score', { score: currentTick })
} }
sendGameMessage("submit_score", { score: currentTick });
}
} }
function startGameCountdown(seconds = 60) { function startGameCountdown(seconds = 60) {
stopGameCountdown(); stopGameCountdown()
countdownInterval.value = seconds; countdownInterval.value = seconds
setDivDescVisible(true); setDivDescVisible(true)
const tickCountdown = () => { const tickCountdown = () => {
countdownInterval.value -= 1; countdownInterval.value -= 1
if (countdownInterval.value <= 0) { if (countdownInterval.value <= 0) {
countdownInterval.value = 0; countdownInterval.value = 0
gameCountdownTimer = undefined; gameCountdownTimer = undefined
setDivDescVisible(false); setDivDescVisible(false)
submitScore(true); submitScore(true)
showGameOverRank(); showGameOverRank()
return; return
} }
gameCountdownTimer = window.setTimeout(tickCountdown, 1000);
};
gameCountdownTimer = window.setTimeout(tickCountdown, 1000);
}
function syncGameCountdown(seconds: number) {
const syncedSeconds = Math.max(0, Math.floor(seconds));
if (countdownInterval.value - syncedSeconds <= 1) return;
if (syncedSeconds === 0) { gameCountdownTimer = window.setTimeout(tickCountdown, 1000)
countdownInterval.value = 0; }
return;
}
startGameCountdown(syncedSeconds); gameCountdownTimer = window.setTimeout(tickCountdown, 1000)
} }
function resetToLoadingView() { function resetToLoadingView() {
stopGameCountdown(); stopGameCountdown()
if (guFrameTimer) { if (guFrameTimer) {
window.clearTimeout(guFrameTimer); window.clearTimeout(guFrameTimer)
guFrameTimer = undefined; guFrameTimer = undefined
} }
score.value = 0; score.value = 0
rank.value = 0; rank.value = 0
rankList.value = []; rankList.value = []
countdownInterval.value = 60; countdownInterval.value = 60
isGuPlaying.value = false; // currentGu.value = guFrames[0]
isGameOver.value = false; isGuPlaying.value = false
userJoinStatus.value = false; userJoinStatus.value = false
showGameRank.value = false; showGameRank.value = false
currentView.value = "loading"; currentView.value = 'loading'
setDivDescVisible(false); setDivDescVisible(false)
} }
function startGameView() { function startGameView() {
stopGameCountdown(); stopGameCountdown()
score.value = 0; score.value = 0
rank.value = 0; rank.value = 0
rankList.value = []; rankList.value = []
isGameOver.value = false; // currentGu.value = guFrames[0]
showGameRank.value = false; showGameRank.value = false
currentView.value = "playing"; currentView.value = 'playing'
startGameCountdown(); startGameCountdown()
} }
function showGameOverRank() { function showGameOverRank() {
stopGameCountdown(); stopGameCountdown()
setDivDescVisible(false); setDivDescVisible(false)
currentView.value = "loading"; currentView.value = 'loading'
showGameRank.value = true; showGameRank.value = true
userJoinStatus.value = false; userJoinStatus.value = false
if (isLocalMode && !wechat) {
console.log('[game3 本地模式] 游戏结束,最终分数:', score.value)
}
}
const touchHandler = (mole: boolean) => {
let scoreDelta = mole ? 5 : -3;
let nextTick = score.value + scoreDelta
if (nextTick < 0) nextTick = 0
score.value = nextTick
if (mole) {
playGame6Music(1);
} else {
playGame6Music(2);
}
submitScore(false, nextTick)
} }
const scoreChangeHandler = (delta: number) => { const scoreChangeHandler = (delta: number) => {
if (delta > 0) { let nextTick = score.value + delta
playGame3Music(1); if (nextTick < 0) nextTick = 0
} else { score.value = nextTick
playGame3Music(2); submitScore(false, nextTick)
} }
let nextTick = score.value + delta;
if (nextTick < 0) nextTick = 0;
score.value = nextTick;
submitScore(false, nextTick);
};
if (wechat) { if (wechat) {
const gameId = "game3"; const gameId = 'game3';
const { offSocketMessage } = useGameSocket({ const { offSocketMessage } = useGameSocket({
gameId, gameId,
auth: { auth: {
userid: wechat.token, userid: wechat.token,
}, },
onConnect: () => { onConnect: () => {
window.setTimeout(() => { window.setTimeout(() => {
const joined = joinSharedRoom( const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
nickname.value, }, 200)
token.value, },
avatar.value, onGameStart: async () => {
gameId, showGameRule.value = false;
); await mobileStageRef.value?.startCountdown()
}, 200); startGameView()
}, },
onGameStart: async () => { onScoreSubmitted: (is_save, data) => {
showGameRule.value = false; if (!is_save) {//非保存状态下
await mobileStageRef.value?.startCountdown(); const nextRank = Number(typeof data === 'object' ? data?.rank : data)
startGameView(); if (Number.isFinite(nextRank) && nextRank > 0) {
}, rank.value = nextRank
onScoreSubmitted: (is_save, data) => { }
if (!is_save) { } else if (typeof data === 'object' && data) {
//非保存状态下 if (Array.isArray(data.list)) {
const nextScore = Number(typeof data === "object" ? data?.score : NaN); rankList.value = data.list.map((item: any, index: number) => ({
const nextRank = Number(typeof data === "object" ? data?.rank : data); rank: Number(item?.rank ?? index + 1),
const remainingSeconds = Number( nickname: String(item?.nickname ?? ''),
typeof data === "object" ? data?.remainingSeconds : NaN, avatar: String(item?.avatar ?? ''),
); score: Number(item?.score ?? 0),
if (Number.isFinite(nextScore) && nextScore >= 0) { }))
score.value = nextScore; }
}
if (Number.isFinite(nextRank) && nextRank > 0) { const nextRank = Number(data.rank)
rank.value = nextRank; if (Number.isFinite(nextRank) && nextRank > 0) {
} rank.value = nextRank
if (Number.isFinite(remainingSeconds) && remainingSeconds >= 0) { }
syncGameCountdown(remainingSeconds); }
} },
} else if (typeof data === "object" && data) { onRescoreSubmitted: (_recount) => {
if (Array.isArray(data.list)) { submitScore(true);
rankList.value = data.list.map((item: any, index: number) => ({ },
rank: Number(item?.rank ?? index + 1), onGameRecover: (data) => {
nickname: String(item?.nickname ?? ""), const recoveredRank = Number(data?.rank)
avatar: String(item?.avatar ?? ""), const recoveredScore = Number(data?.score)
score: Number(item?.score ?? 0), const recoveredSeconds = Number(data?.remainingSeconds)
}));
} rank.value = Number.isFinite(recoveredRank) && recoveredRank > 0 ? recoveredRank : 0
score.value = Number.isFinite(recoveredScore) && recoveredScore > 0 ? recoveredScore : 0
const nextRank = Number(data.rank); showGameRule.value = false
if (Number.isFinite(nextRank) && nextRank > 0) { showGameRank.value = false
rank.value = nextRank; currentView.value = 'playing'
} startGameCountdown(
} Number.isFinite(recoveredSeconds) && recoveredSeconds > 0 ? recoveredSeconds : 0,
return { code: "MSYF", score: score.value }; )
}, },
onRescoreSubmitted: (_recount) => { onRoomClosed: () => {
submitScore(true); stopGameCountdown()
}, currentView.value = 'loading'
onGameRecover: (data) => { setDivDescVisible(false)
const recoveredRank = Number(data?.rank); $toast('管理员关闭了该房间')
const recoveredScore = Number(data?.score); },
const recoveredSeconds = Number(data?.remainingSeconds); onRoomBack: () => {
resetToLoadingView()
rank.value = window.setTimeout(() => {
Number.isFinite(recoveredRank) && recoveredRank > 0 ? recoveredRank : 0; joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
score.value = }, 200)
Number.isFinite(recoveredScore) && recoveredScore > 0 },
? recoveredScore })
: 0; _offSocketMessage.value = offSocketMessage
showGameRule.value = false;
showGameRank.value = false;
isGameOver.value = false;
currentView.value = "playing";
startGameCountdown(
Number.isFinite(recoveredSeconds) && recoveredSeconds > 0
? recoveredSeconds
: 0,
);
},
onRoomClosed: () => {
stopGameCountdown();
currentView.value = "loading";
setDivDescVisible(false);
$toast("管理员关闭了游戏房间");
},
onRoomBack: () => {},
});
_offSocketMessage.value = offSocketMessage;
} }
onMounted(() => { onMounted(() => {
document.title = "互动游戏 - 马上有福"; document.title = '小游戏 - 福运当头'
if (token.value) { if (token.value) {
window.addEventListener("beforeunload", confirmRefresh); window.addEventListener('beforeunload', confirmRefresh)
} }
// 本地测试模式:自动模拟游戏开始(IS_PRODUCTION=false 时生效) // TODO_LOCAL: 本地模拟游戏开始,稍后部署上线前删除此段
if (isLocalMode) { if ($is_run_local()) {
showGameRule.value = false; showGameRule.value = false
currentView.value = "playing"; currentView.value = 'playing'
startGameCountdown(); startGameCountdown()
} }
}); })
onBeforeUnmount(() => { onBeforeUnmount(() => {
_offSocketMessage.value?.(); _offSocketMessage.value?.()
stopAllMusic(); stopGame6Music()
if (guFrameTimer) { if (guFrameTimer) {
window.clearTimeout(guFrameTimer); window.clearTimeout(guFrameTimer)
} }
stopGameCountdown(); stopGameCountdown()
if (token.value) { if (token.value) {
window.removeEventListener("beforeunload", confirmRefresh); window.removeEventListener('beforeunload', confirmRefresh)
} }
}); })
const showGameRuleHandler = () => { const showGameRuleHandler = () => {
showGameRule.value = !showGameRule.value; showGameRule.value = !showGameRule.value;
}; }
const renderBG = () => { const renderBG = () => {
if (currentView.value == "playing") { if (currentView.value == 'playing') {
return `${imageUrls.bg2} center/cover`; return `${imageUrls.bg2} center/cover`
} }
return `${imageUrls.bg} center/cover`; return `${imageUrls.bg} center/cover`;
}; }
const rankCloseHandler = () => { const rankCloseHandler = () => {
showGameRank.value = false; showGameRank.value = false
isGameOver.value = false; }
resetToLoadingView();
};
</script> </script>
<template> <template>
<MobileStage <MobileStage v-if="token || isLocalMode" :showGameRule="showGameRule" :showGameRank="showGameRank" ref="mobileStageRef"
v-if="token || isLocalMode" :background="`${renderBG()}`">
:showGameRule="showGameRule" <template #gameRule>
:showGameRank="showGameRank" <div class="rule-container">
ref="mobileStageRef" <div class="rule-txt-container">
:background="`${renderBG()}`" <div class="rule-title">游戏规则</div>
> <div class="rule-txt">
<template #gameRule> 1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2 分;<br />
<div class="rule-container"> 2. 限时闯关比拼得分,结束后按积分排名;<br />
<div class="rule-txt-container"> 3. 高分用户有机会领取新疆福彩定制周边;<br />
<div class="rule-title">游戏规则</div> 4. 禁止外挂作弊,违规账号取消成绩与领奖资格;<br /><br />
<div class="rule-txt"> 祝您游戏愉快!
1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2 </div>
分;<br /> <div class="rule-close" @click="showGameRuleHandler"></div>
2. 限时闯关比拼得分,结束后按积分排名;<br /> </div>
3. 高分用户有机会领取新疆福彩定制周边;<br />
4. 禁止外挂作弊,违规账号取消成绩与领奖资格;<br /><br />
祝您游戏愉快!
</div>
<div class="rule-close" @click="showGameRuleHandler"></div>
</div>
</div>
</template>
<template #gameRank>
<div class="ranklist-container">
<div class="txt-title">排行</div>
<div class="list-container">
<div class="row row-1">
<div class="col-1">排名</div>
<div class="col-2">用户</div>
<div class="col-3">成绩</div>
</div>
<div class="row-2">
<div
class="row rank-row"
v-for="(item, index) in displayRankList"
:key="`${item.rank}-${index}`"
>
<div class="col-1">
<div>{{ item.rank }}</div>
</div>
<div class="col-2">
<div
class="avatar"
:style="
item.avatar
? { backgroundImage: `url(${item.avatar})` }
: {}
"
></div>
<div>{{ item.nickname }}</div>
</div>
<div class="col-3">{{ item.score }}</div>
</div> </div>
</div> </template>
<div class="row row-3"> <template #gameRank>
<div class="col-1">{{ rank > 0 ? rank : "未上榜" }}</div> <div class="ranklist-container">
<div class="col-2"> <div class="txt-title">排行</div>
<div <div class="list-container">
class="avatar" <div class="row row-1">
:style="{ backgroundImage: `url(${avatar})` }" <div class="col-1">排名</div>
></div> <div class="col-2">用户</div>
<div>{{ nickname }}</div> <div class="col-3">成绩</div>
</div>
<div class="row-2">
<div class="row rank-row" v-for="(item, index) in displayRankList"
:key="`${item.rank}-${index}`">
<div class="col-1">
<div>{{ item.rank }}</div>
</div>
<div class="col-2">
<div class="avatar"
:style="item.avatar ? { backgroundImage: `url(${item.avatar})` } : {}"></div>
<div>{{ item.nickname }}</div>
</div>
<div class="col-3">{{ item.score }}</div>
</div>
</div>
<div class="row row-3">
<div class="col-1">{{ rank > 0 ? rank : '未上榜' }}</div>
<div class="col-2">
<div class="avatar" :style="{ backgroundImage: `url(${avatar})` }"></div>
<div>{{ nickname }}</div>
</div>
<div class="col-3">{{ rankScore }}</div>
</div>
</div>
</div> </div>
<div class="col-3">{{ rankScore }}</div> <div class="img-close" @click="rankCloseHandler"></div>
</div> </template>
</div> <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
<div class="img-close" @click="rankCloseHandler"></div>
</div>
</template>
<LoadingView
v-if="currentView === 'loading'"
:image-urls="imageUrls"
:user-join-status="userJoinStatus"
@touchGameRule="showGameRuleHandler"
/>
<PlayingView
v-else
:image-urls="imageUrls"
:tick="score"
:rank="rank"
:countdown-interval="countdownInterval"
:is-div-desc-visible="isDivDescVisible"
@score-change="scoreChangeHandler"
@rank-close="rankCloseHandler"
/>
<!-- <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
@touchGameRule="showGameRuleHandler" /> @touchGameRule="showGameRuleHandler" />
<PlayingView v-else :image-urls="imageUrls" :tick="score" :rank="rank" :countdown-interval="countdownInterval" <PlayingView v-else :image-urls="imageUrls" :tick="score" :rank="rank" :countdown-interval="countdownInterval"
:is-div-desc-visible="isDivDescVisible" :is-game-over="isGameOver" :is-div-desc-visible="isDivDescVisible" @touch="touchHandler" @score-change="scoreChangeHandler" />
:rank-list="displayRankList" :user-rank="rank" :user-score="rankScore" </MobileStage>
:user-nickname="nickname" :user-avatar="avatar" <div v-else style="text-align: center; width: 100vw; height: 100vh; line-height: 30; font-size: 20px;">
@touch="touchHandler" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" /> --> 请使用微信扫码进入游戏
</MobileStage> </div>
<div
v-else
style="
text-align: center;
width: 100vw;
height: 100vh;
line-height: 30;
font-size: 20px;
"
>
请使用微信扫码进入游戏
</div>
</template> </template>
<style scoped> <style scoped>
.rule-container { .rule-container {
...@@ -485,115 +405,107 @@ const rankCloseHandler = () => { ...@@ -485,115 +405,107 @@ const rankCloseHandler = () => {
margin-top: 10px; margin-top: 10px;
color: #AA0000; color: #AA0000;
} }
}
.rule-close { .rule-close {
width: 57px; width: 57px;
height: 57px; height: 57px;
background: v-bind('imageUrls.close') center / cover no-repeat; background: v-bind('imageUrls.close') center / cover no-repeat;
transform: translateX(-50%) scale(1.5); transform: translateX(-50%) scale(1.4);
position: absolute; position: absolute;
top: -73px; bottom: -75px;
left: 100%; left: 50%;
margin-left: -48px;
}
} }
} }
.ranklist-container { .ranklist-container {
width: 747px; width: 747px;
height: 1084px; height: 1084px;
background: v-bind("imageUrls.ranklist"); background: v-bind('imageUrls.ranklist');
transform: translate(-50%, -50%) scale(0.45); transform: translate(-50%, -50%) scale(0.45);
left: 50%; left: 50%;
top: 50%; top: 50%;
position: absolute;
.txt-title {
width: 100%;
text-align: center;
font-size: 50px;
margin-top: 35px;
font-weight: bold;
color: #aa0000;
letter-spacing: 2px;
}
.list-container {
width: calc(100% - 90px);
height: calc(100% - 210px);
position: absolute; position: absolute;
left: 40px;
top: 140px;
text-align: center;
height: 73px;
line-height: 73px;
font-size: 32px;
.row {
display: grid;
grid-template-columns: 150px auto 150px;
align-items: center;
}
.row-1 { .txt-title {
color: #aa0000; width: 100%;
font-weight: bold; text-align: center;
background: linear-gradient( font-size: 50px;
-84deg, margin-top: 35px;
#ffd396 0%, font-weight: bold;
#fff3d1 53%, color: #AA0000;
#ffd59c 100% letter-spacing: 2px;
);
border-radius: 20px 20px 0px 0px;
} }
.row-2 { .list-container {
color: #633911; width: calc(100% - 90px);
height: calc(100% - 210px);
position: absolute;
left: 40px;
top: 140px;
text-align: center;
height: 73px;
line-height: 73px;
font-size: 32px;
.row {
display: grid;
grid-template-columns: 150px auto 150px;
align-items: center;
}
.rank-row:nth-child(odd) { .row-1 {
background: #f2e2ae; color: #AA0000;
} font-weight: bold;
background: linear-gradient(-84deg, #FFD396 0%, #FFF3D1 53%, #FFD59C 100%);
border-radius: 20px 20px 0px 0px;
}
.rank-row:nth-child(even) { .row-2 {
background: #fceebf; color: #633911;
}
.col-1 { .rank-row:nth-child(odd) {
position: relative; background: #F2E2AE;
} }
}
.row-3 { .rank-row:nth-child(even) {
background: linear-gradient(0deg, #f4b64c 0%, #f7c96f 100%); background: #FCEEBF;
border-radius: 0px 0px 20px 20px; }
}
.col-2 { .col-1 {
display: flex; position: relative;
align-items: center; }
justify-content: center; }
gap: 18px;
.row-3 {
.avatar { background: linear-gradient(0deg, #F4B64C 0%, #F7C96F 100%);
width: 48px; border-radius: 0px 0px 20px 20px;
height: 48px; }
flex: 0 0 auto;
border-radius: 50%; .col-2 {
background-position: center; display: flex;
background-size: cover; align-items: center;
} justify-content: center;
gap: 18px;
.avatar {
width: 48px;
height: 48px;
flex: 0 0 auto;
border-radius: 50%;
background-position: center;
background-size: cover;
}
}
} }
}
} }
.img-close { .img-close {
width: 57px; width: 57px;
height: 57px; height: 57px;
background: v-bind('imageUrls.close') center / cover no-repeat; border-radius: 29px;
transform: translateX(-50%) scale(1.5); background: v-bind('imageUrls.close')center/cover;
position: absolute; transform: scale(0.7);
top: -35px; margin-top: 500px;
left: 100%;
margin-left: -58px;
} }
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch, nextTick, computed } from 'vue'; import { onBeforeUnmount, onMounted, ref, watch, nextTick, computed } from 'vue';
import { assetUrl } from '@/commons/assets.ts' import { assetUrl } from '@/commons/assets.ts'
// import { playGame6Music } from '@/commons/music' import { playGame6Music } from '@/commons/music'
const props = defineProps<{ const props = defineProps<{
imageUrls: Record<string, string> imageUrls: Record<string, string>
...@@ -12,9 +12,8 @@ const props = defineProps<{ ...@@ -12,9 +12,8 @@ const props = defineProps<{
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
// touch: [mole: boolean] touch: [mole: boolean]
scoreChange: [delta: number] scoreChange: [delta: number]
rankClose: []
}>(); }>();
// ========== 常量 ========== // ========== 常量 ==========
...@@ -26,26 +25,8 @@ const HORSE_BOTTOM_PCT = 19.11; ...@@ -26,26 +25,8 @@ const HORSE_BOTTOM_PCT = 19.11;
const ITEM_SIZE = 80; const ITEM_SIZE = 80;
const GAME_DURATION = 60; const GAME_DURATION = 60;
// 设计基准跑道 X 位置(750px 设计稿下的值,窄屏设备上会自动居中偏移) const LEFT_LANE_X = 170;
const DESIGN_LEFT_LANE_X = 150; const RIGHT_LANE_X = 470;
const DESIGN_RIGHT_LANE_X = 500;
// 运行时根据视口计算的真实 X 位置(适配不同屏幕宽度)
const leftLaneX = ref(DESIGN_LEFT_LANE_X);
const rightLaneX = ref(DESIGN_RIGHT_LANE_X);
/** 按视口宽度计算 750px 设计区域在 content-bg 中的居中偏移 */
const updateLanePositions = () => {
const vw = window.visualViewport?.width ?? window.innerWidth;
const vh = window.visualViewport?.height ?? window.innerHeight;
// MobileStage contain 模式下 scale = min(scaleX, scaleY),手机通常由高度决定
const scaleY = vh / 1624;
if (!scaleY || scaleY <= 0) return;
const stageViewportWidth = vw / scaleY;
const offset = Math.max(0, (stageViewportWidth - 750) / 2);
leftLaneX.value = DESIGN_LEFT_LANE_X + offset;
rightLaneX.value = DESIGN_RIGHT_LANE_X + offset;
};
// ========== 马帧 ========== // ========== 马帧 ==========
const HORSE_FRAME_COUNT = 21; const HORSE_FRAME_COUNT = 21;
...@@ -67,34 +48,24 @@ const bomFrames = Array.from({ length: BOM_FRAME_COUNT }, (_, i) => ...@@ -67,34 +48,24 @@ const bomFrames = Array.from({ length: BOM_FRAME_COUNT }, (_, i) =>
const fudaiUrl = assetUrl('game3/icon_fudai.webp'); const fudaiUrl = assetUrl('game3/icon_fudai.webp');
const dileiUrl = assetUrl('game3/icon_dilei.webp'); const dileiUrl = assetUrl('game3/icon_dilei.webp');
// ========== 预加载碰撞动画帧(解决 4G 慢网下动画图片未加载的问题) ==========
const preloadFrames = (urls: string[]) => {
for (const url of urls) {
const img = new Image();
img.src = url;
}
};
preloadFrames(jinbiFrames);
preloadFrames(bomFrames);
// ========== 马状态 ========== // ========== 马状态 ==========
const horseLane = ref<'left' | 'right'>('left'); const horseLane = ref<'left' | 'right'>('left');
const horseFrameIndex = ref(0); const horseFrameIndex = ref(0);
const horseX = ref(leftLaneX.value); const horseX = ref(LEFT_LANE_X);
const horseTargetX = ref(leftLaneX.value); const horseTargetX = ref(LEFT_LANE_X);
const switchToLeft = () => { const switchToLeft = () => {
if (horseLane.value !== 'left') { if (horseLane.value !== 'left') {
horseLane.value = 'left'; horseLane.value = 'left';
horseTargetX.value = leftLaneX.value; horseTargetX.value = LEFT_LANE_X;
} }
}; };
const switchToRight = () => { const switchToRight = () => {
if (horseLane.value !== 'right') { if (horseLane.value !== 'right') {
horseLane.value = 'right'; horseLane.value = 'right';
horseTargetX.value = rightLaneX.value; horseTargetX.value = RIGHT_LANE_X;
} }
}; };
...@@ -112,8 +83,8 @@ const handleKeydown = (e: KeyboardEvent) => { ...@@ -112,8 +83,8 @@ const handleKeydown = (e: KeyboardEvent) => {
const roadOffset = ref(0); const roadOffset = ref(0);
let roadRafId: number | undefined; let roadRafId: number | undefined;
let roadLastTs = 0; let roadLastTs = 0;
const BASE_ROAD_SPEED = 200; const BASE_ROAD_SPEED = 80;
const MAX_ROAD_SPEED = 700; const MAX_ROAD_SPEED = 400;
const gameElapsed = ref(0); const gameElapsed = ref(0);
const currentRoadSpeed = ref(BASE_ROAD_SPEED); const currentRoadSpeed = ref(BASE_ROAD_SPEED);
...@@ -137,9 +108,6 @@ const startRoadScroll = () => { ...@@ -137,9 +108,6 @@ const startRoadScroll = () => {
currentRoadSpeed.value = BASE_ROAD_SPEED + (MAX_ROAD_SPEED - BASE_ROAD_SPEED) * progress; currentRoadSpeed.value = BASE_ROAD_SPEED + (MAX_ROAD_SPEED - BASE_ROAD_SPEED) * progress;
roadOffset.value += currentRoadSpeed.value * dt / 1000; roadOffset.value += currentRoadSpeed.value * dt / 1000;
if (roadOffset.value >= ROAD_HEIGHT) {
roadOffset.value -= ROAD_HEIGHT;
}
roadRafId = requestAnimationFrame(tick); roadRafId = requestAnimationFrame(tick);
}; };
...@@ -162,7 +130,6 @@ interface RoadItem { ...@@ -162,7 +130,6 @@ interface RoadItem {
lane: 'left' | 'right'; lane: 'left' | 'right';
y: number; y: number;
collected: boolean; collected: boolean;
colliding: boolean; // 防止同一帧内重复触发碰撞特效
animFrame: number; animFrame: number;
animTimer: ReturnType<typeof setTimeout> | undefined; animTimer: ReturnType<typeof setTimeout> | undefined;
} }
...@@ -170,79 +137,47 @@ interface RoadItem { ...@@ -170,79 +137,47 @@ interface RoadItem {
const roadItems = ref<RoadItem[]>([]); const roadItems = ref<RoadItem[]>([]);
const effectAnims = ref<{ id: number; kind: ItemKind; frame: number; x: number; y: number }[]>([]); const effectAnims = ref<{ id: number; kind: ItemKind; frame: number; x: number; y: number }[]>([]);
interface ScoreEffect {
id: number
x: number
y: number
score: number // 正数表示加分,负数表示扣分
}
const scoreEffects = ref<ScoreEffect[]>([]);
let scoreEffectIdSeq = 0;
let itemIdCounter = 0; let itemIdCounter = 0;
let genTimerId: ReturnType<typeof setTimeout> | undefined; let lastItemY = 0;
const MIN_ITEM_SPACING = 250;
const MAX_ITEM_SPACING = 400;
const ITEM_GEN_INTERVAL = 1500;
function seededRandom(seed: number) { function seededRandom(seed: number) {
const x = Math.sin(seed * 12.9898 + 78.233) * 43758.5453; const x = Math.sin(seed * 12.9898 + 78.233) * 43758.5453;
return x - Math.floor(x); return x - Math.floor(x);
} }
function scheduleNextItem() {
if (!props.isDivDescVisible) return;
// 游戏越接近尾声,生成间隔越短(1800ms → 350ms),加上随机抖动
const progress = Math.min(gameElapsed.value / GAME_DURATION, 1);
const base = 1800 - 1450 * progress;
const jitter = (Math.random() - 0.5) * 800; // ±400ms 随机
const interval = Math.max(250, base + jitter);
genTimerId = setTimeout(generateNewItem, interval);
}
function generateNewItem() { function generateNewItem() {
if (!props.isDivDescVisible) return; if (!props.isDivDescVisible) return;
const seed = Date.now() + itemIdCounter; const seed = Date.now() + itemIdCounter;
const kind: ItemKind = seededRandom(seed) < 0.75 ? 'coin' : 'bomb'; const kind: ItemKind = seededRandom(seed) < 0.55 ? 'coin' : 'bomb';
const lane: 'left' | 'right' = seededRandom(seed + 1000) < 0.5 ? 'left' : 'right';
// 概率分配车道,但避免与前一个同车道道具太近
let lane: 'left' | 'right' = seededRandom(seed + 1000) < 0.5 ? 'left' : 'right';
// 找到该车道还在顶部附近(y < 200)的未收集道具 const spacing = MIN_ITEM_SPACING + seededRandom(seed + 2000) * (MAX_ITEM_SPACING - MIN_ITEM_SPACING);
const hasNearbyInLane = (ln: 'left' | 'right') => const startY = -ITEM_SIZE;
roadItems.value.some(it => !it.collected && it.lane === ln && it.y < 200);
if (hasNearbyInLane(lane) && !hasNearbyInLane(lane === 'left' ? 'right' : 'left')) {
// 首选车道已有最近道具,强制换到另一车道
lane = lane === 'left' ? 'right' : 'left';
}
const newItem: RoadItem = { const newItem: RoadItem = {
id: itemIdCounter++, id: itemIdCounter++,
kind, kind,
lane, lane,
y: -ITEM_SIZE, y: startY,
collected: false, collected: false,
colliding: false,
animFrame: 0, animFrame: 0,
animTimer: undefined, animTimer: undefined,
}; };
roadItems.value.push(newItem); roadItems.value.push(newItem);
scheduleNextItem(); setTimeout(generateNewItem, ITEM_GEN_INTERVAL);
}
function stopItemGen() {
if (genTimerId) {
clearTimeout(genTimerId);
genTimerId = undefined;
}
} }
// 道具 y 已是 screen-space(content-bg 坐标系),清理逻辑直接判断
function cleanupOffscreenItems() { function cleanupOffscreenItems() {
const screenBottom = roadOffset.value + ROAD_HEIGHT;
roadItems.value = roadItems.value.filter(item => { roadItems.value = roadItems.value.filter(item => {
// item.y 超出 content-bg 底部即删除,超出顶部也删除 const itemBottom = item.y + ITEM_SIZE;
return item.y < ROAD_HEIGHT && item.y > -ITEM_SIZE; return itemBottom > roadOffset.value - 100 && item.y < screenBottom + 100;
}); });
} }
...@@ -250,53 +185,45 @@ function cleanupOffscreenItems() { ...@@ -250,53 +185,45 @@ function cleanupOffscreenItems() {
// ========== 碰撞检测 ========== // ========== 碰撞检测 ==========
// 马匹在屏幕上的Y坐标(从 content-bg 顶部算起,horse 使用 bottom: 19.11% 定位) const horseBottomY = ROAD_HEIGHT * (HORSE_BOTTOM_PCT / 100);
const horseCSSBottom = ROAD_HEIGHT * (1 - HORSE_BOTTOM_PCT / 100); const horseTopY = horseBottomY - HORSE_H;
const horseCSSTop = horseCSSBottom - HORSE_H;
const checkCollisions = () => { const checkCollisions = () => {
const horseCenterX = horseX.value + HORSE_W / 2; const horseCenterX = horseX.value + HORSE_W / 2;
const horseTopCenterY = horseCSSTop + HORSE_H * 0.15; const horseTopCenterY = horseTopY + HORSE_H * 0.15;
const horseRadius = HORSE_W * 0.35; const horseRadius = HORSE_W * 0.35;
for (const item of roadItems.value) { for (const item of roadItems.value) {
if (item.collected || item.colliding) continue; if (item.collected) continue;
const itemX = item.lane === 'left' ? leftLaneX.value : rightLaneX.value; const itemX = item.lane === 'left' ? LEFT_LANE_X : RIGHT_LANE_X;
// item.y 已经是屏幕坐标(content-bg 内),无需加 roadOffset const itemVisualY = item.y;
const itemCenterX = itemX + ITEM_SIZE / 2; const itemCenterX = itemX + ITEM_SIZE / 2;
const itemCenterY = item.y + ITEM_SIZE / 2; const itemCenterY = itemVisualY + ITEM_SIZE / 2;
const dist = Math.hypot(horseCenterX - itemCenterX, horseTopCenterY - itemCenterY); const dist = Math.hypot(horseCenterX - itemCenterX, horseTopCenterY - itemCenterY);
if (dist < horseRadius + ITEM_SIZE * 0.35) { if (dist < horseRadius + ITEM_SIZE * 0.35) {
// 立即标记碰撞中,防止后续帧重复触发 item.collected = true;
item.colliding = true; playCollectEffect(item, itemX, itemVisualY);
// 先启动碰撞特效动画,延迟隐藏道具让动画和道具短暂重叠过渡
playCollectEffect(item, itemX, item.y);
setTimeout(() => {
item.collected = true;
}, 100);
} }
} }
}; };
const playCollectEffect = (item: RoadItem, x: number, y: number) => { const playCollectEffect = (item: RoadItem, x: number, y: number) => {
const isCoin = item.kind === 'coin'; if (item.kind === 'coin') {
const delta = isCoin ? 5 : -3;
if (isCoin) {
emit('scoreChange', 5); emit('scoreChange', 5);
playGame6Music(1);
} else { } else {
emit('scoreChange', -3); emit('scoreChange', -3);
playGame6Music(2);
} }
const animId = Date.now() + Math.random(); const animId = Date.now() + Math.random();
const totalFrames = isCoin ? JINBI_FRAME_COUNT : BOM_FRAME_COUNT; const totalFrames = item.kind === 'coin' ? JINBI_FRAME_COUNT : BOM_FRAME_COUNT;
const frameDuration = 1000 / totalFrames; const frameDuration = 1000 / totalFrames;
// 碰撞动画(大幅放大尺寸,x 往右偏移让动画居中于视觉碰撞点) effectAnims.value.push({ id: animId, kind: item.kind, frame: 0, x, y });
effectAnims.value.push({ id: animId, kind: item.kind, frame: 0, x: x + 65, y });
let frameIdx = 0; let frameIdx = 0;
const tick = () => { const tick = () => {
...@@ -310,19 +237,6 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => { ...@@ -310,19 +237,6 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
item.animTimer = setTimeout(tick, frameDuration); item.animTimer = setTimeout(tick, frameDuration);
}; };
item.animTimer = setTimeout(tick, frameDuration); item.animTimer = setTimeout(tick, frameDuration);
// 积分飘字效果(参考 game5 套中圈 "+10" 动画)
const effectId = ++scoreEffectIdSeq;
scoreEffects.value.push({
id: effectId,
x: x + ITEM_SIZE / 2 + 25,
y: y,
score: delta,
});
setTimeout(() => {
const idx = scoreEffects.value.findIndex(e => e.id === effectId);
if (idx >= 0) scoreEffects.value.splice(idx, 1);
}, 800);
}; };
// ========== 主循环 ========== // ========== 主循环 ==========
...@@ -347,28 +261,24 @@ const startMainLoop = () => { ...@@ -347,28 +261,24 @@ const startMainLoop = () => {
mainLastTs = ts; mainLastTs = ts;
horseFrameAccum += dt; horseFrameAccum += dt;
const horseInterval = 80 * BASE_ROAD_SPEED / currentRoadSpeed.value; while (horseFrameAccum >= 48) {
while (horseFrameAccum >= horseInterval) { horseFrameAccum -= 48;
horseFrameAccum -= horseInterval;
horseFrameIndex.value = (horseFrameIndex.value + 1) % HORSE_FRAME_COUNT; horseFrameIndex.value = (horseFrameIndex.value + 1) % HORSE_FRAME_COUNT;
} }
const lerpFactor = 1 - Math.pow(0.001, dt / 1000); const lerpFactor = 1 - Math.pow(0.001, dt / 1000);
horseX.value += (horseTargetX.value - horseX.value) * lerpFactor; horseX.value += (horseTargetX.value - horseX.value) * lerpFactor;
// 先移动所有道具 checkCollisions();
for (const item of roadItems.value) { for (const item of roadItems.value) {
if (!item.collected) { if (!item.collected) {
item.y += currentRoadSpeed.value * dt / 1000; item.y += currentRoadSpeed.value * dt / 1000;
} }
} }
// 清理已离开屏幕的道具,避免 roadOffset 回绕后旧道具闪现
cleanupOffscreenItems(); cleanupOffscreenItems();
// 最后再检测碰撞,确保只检测屏幕内确实可见的道具
checkCollisions();
mainRafId = requestAnimationFrame(tick); mainRafId = requestAnimationFrame(tick);
}; };
mainRafId = requestAnimationFrame(tick); mainRafId = requestAnimationFrame(tick);
...@@ -395,32 +305,21 @@ const stopMainLoop = () => { ...@@ -395,32 +305,21 @@ const stopMainLoop = () => {
// ========== 生命周期 ========== // ========== 生命周期 ==========
onMounted(() => { onMounted(() => {
window.addEventListener('keydown', handleKeydown); window.addEventListener('keydown', handleKeydown);
window.addEventListener('resize', updateLanePositions);
window.visualViewport?.addEventListener('resize', updateLanePositions);
updateLanePositions();
// 初始化马匹在左侧跑道(使用计算后的实际 X 位置)
horseX.value = leftLaneX.value;
horseTargetX.value = leftLaneX.value;
nextTick(() => { nextTick(() => {
startRoadScroll(); startRoadScroll();
startMainLoop(); startMainLoop();
scheduleNextItem(); setTimeout(generateNewItem, 1000);
}); });
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeydown); window.removeEventListener('keydown', handleKeydown);
window.removeEventListener('resize', updateLanePositions);
window.visualViewport?.removeEventListener('resize', updateLanePositions);
stopRoadScroll(); stopRoadScroll();
stopMainLoop(); stopMainLoop();
stopItemGen();
for (const item of roadItems.value) { for (const item of roadItems.value) {
if (item.animTimer) clearTimeout(item.animTimer); if (item.animTimer) clearTimeout(item.animTimer);
} }
roadItems.value = []; roadItems.value = [];
effectAnims.value = [];
scoreEffects.value = [];
}); });
watch(() => props.isDivDescVisible, (visible) => { watch(() => props.isDivDescVisible, (visible) => {
...@@ -428,50 +327,35 @@ watch(() => props.isDivDescVisible, (visible) => { ...@@ -428,50 +327,35 @@ watch(() => props.isDivDescVisible, (visible) => {
nextTick(() => { nextTick(() => {
roadItems.value = []; roadItems.value = [];
effectAnims.value = []; effectAnims.value = [];
scoreEffects.value = [];
roadOffset.value = 0; roadOffset.value = 0;
gameElapsed.value = 0; gameElapsed.value = 0;
updateLanePositions();
horseX.value = leftLaneX.value;
horseTargetX.value = leftLaneX.value;
startRoadScroll(); startRoadScroll();
startMainLoop(); startMainLoop();
scheduleNextItem(); setTimeout(generateNewItem, 1000);
}); });
} else { } else {
stopRoadScroll(); stopRoadScroll();
stopMainLoop(); stopMainLoop();
stopItemGen();
for (const item of roadItems.value) { for (const item of roadItems.value) {
if (item.animTimer) clearTimeout(item.animTimer); if (item.animTimer) clearTimeout(item.animTimer);
} }
roadItems.value = []; roadItems.value = [];
effectAnims.value = []; effectAnims.value = [];
scoreEffects.value = [];
} }
}, { immediate: true }); }, { immediate: true });
// 游戏结束:暂停全部动画,显示结果页
// watch(() => props.isGameOver, (over) => {
// if (over) {
// stopRoadScroll();
// stopMainLoop();
// stopItemGen();
// }
// });
</script> </script>
<template> <template>
<div class="h5-page game-stage"> <div class="h5-page game-stage">
<div class="only-bg"></div> <div class="only-bg"></div>
<div class="content-bg"> <div class="content-bg">
<div class="road-track" :style="{ transform: `translateY(${roadOffset}px)` }"> <div class="road-track" :style="{ transform: `translateY(${roadOffset % ROAD_HEIGHT}px)` }">
<div class="road-inner"></div> <div class="road-inner"></div>
<div class="road-inner"></div> <div class="road-inner"></div>
<div class="road-inner"></div> <div class="road-inner"></div>
</div> </div>
<!-- 道具(屏幕坐标系,独立于跑道滚动) --> <!-- 道具 -->
<template v-for="item in roadItems" :key="item.id"> <template v-for="item in roadItems" :key="item.id">
<img <img
v-if="!item.collected" v-if="!item.collected"
...@@ -479,7 +363,7 @@ watch(() => props.isDivDescVisible, (visible) => { ...@@ -479,7 +363,7 @@ watch(() => props.isDivDescVisible, (visible) => {
class="road-item" class="road-item"
:class="`item-lane-${item.lane}`" :class="`item-lane-${item.lane}`"
:style="{ :style="{
top: `${item.y}px`, top: `${item.y + roadOffset}px`,
}" }"
/> />
</template> </template>
...@@ -493,15 +377,6 @@ watch(() => props.isDivDescVisible, (visible) => { ...@@ -493,15 +377,6 @@ watch(() => props.isDivDescVisible, (visible) => {
/> />
</template> </template>
<!-- 积分飘字 -->
<div
v-for="ef in scoreEffects"
:key="ef.id"
class="score-float"
:class="ef.score > 0 ? 'score-plus' : 'score-minus'"
:style="{ left: ef.x + 'px', top: ef.y + 'px' }"
>{{ ef.score > 0 ? '+' : '' }}{{ ef.score }}</div>
<!-- 马 --> <!-- 马 -->
<div class="horse-wrapper" :style="{ left: `${horseX}px`, bottom: `${HORSE_BOTTOM_PCT}%` }"> <div class="horse-wrapper" :style="{ left: `${horseX}px`, bottom: `${HORSE_BOTTOM_PCT}%` }">
<img <img
...@@ -526,43 +401,6 @@ watch(() => props.isDivDescVisible, (visible) => { ...@@ -526,43 +401,6 @@ watch(() => props.isDivDescVisible, (visible) => {
<div class="img-hammer"></div> <div class="img-hammer"></div>
<div class="img-left" @click="switchToLeft"></div> <div class="img-left" @click="switchToLeft"></div>
<div class="img-right" @click="switchToRight"></div> <div class="img-right" @click="switchToRight"></div>
<!-- 游戏结束结果页覆盖层 -->
<!-- <Transition name="rank-fade">
<div v-if="isGameOver" class="rank-overlay">
<div class="ranklist-container">
<div class="txt-title">排行</div>
<div class="list-container">
<div class="row row-1">
<div class="col-1">排名</div>
<div class="col-2">用户</div>
<div class="col-3">成绩</div>
</div>
<div class="row-2">
<div class="row rank-row" v-for="(item, index) in rankList" :key="`${item.rank}-${index}`">
<div class="col-1">
<div>{{ item.rank }}</div>
</div>
<div class="col-2">
<div class="avatar" :style="item.avatar ? { backgroundImage: `url(${item.avatar})` } : {}"></div>
<div>{{ item.nickname }}</div>
</div>
<div class="col-3">{{ item.score }}</div>
</div>
</div>
<div class="row row-3">
<div class="col-1">{{ userRank > 0 ? userRank : '未上榜' }}</div>
<div class="col-2">
<div class="avatar" :style="{ backgroundImage: `url(${userAvatar})` }"></div>
<div>{{ userNickname }}</div>
</div>
<div class="col-3">{{ userScore }}</div>
</div>
</div>
</div>
<div class="img-rank-close" @click="$emit('rankClose')"></div>
</div>
</Transition> -->
</div> </div>
</template> </template>
...@@ -596,7 +434,7 @@ watch(() => props.isDivDescVisible, (visible) => { ...@@ -596,7 +434,7 @@ watch(() => props.isDivDescVisible, (visible) => {
.road-track { .road-track {
width: 100%; width: 100%;
position: absolute; position: absolute;
top: -1202.1px; top: 0;
left: 0; left: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -611,67 +449,26 @@ watch(() => props.isDivDescVisible, (visible) => { ...@@ -611,67 +449,26 @@ watch(() => props.isDivDescVisible, (visible) => {
.road-item { .road-item {
position: absolute; position: absolute;
/* width: 80px; width: 80px;
height: 80px; */ height: 80px;
width: 77px;
height: 96px;
z-index: 5; z-index: 5;
pointer-events: none; pointer-events: none;
} }
.item-lane-left { .item-lane-left {
/* 当 content-bg 宽于 750px 设计稿时,自动居中 750px 游戏区域 */ left: 170px;
left: calc((var(--stage-viewport-width, 750px) - 750px) / 2 + 170px);
} }
.item-lane-right { .item-lane-right {
left: calc((var(--stage-viewport-width, 750px) - 750px) / 2 + 520px); left: 470px;
} }
.effect-anim { .effect-anim {
position: absolute; position: absolute;
width: 250px; width: 120px;
height: 250px; height: 120px;
z-index: 10; z-index: 10;
pointer-events: none; pointer-events: none;
transform: translate(-50%, -50%);
}
/* 积分飘字动画 */
.score-float {
position: absolute;
z-index: 25;
font-size: 56px;
font-weight: 900;
pointer-events: none;
white-space: nowrap;
transform: translate(-50%, -100%);
animation: score-float-up 0.8s ease-out forwards;
}
.score-plus {
color: #FFD700;
text-shadow: 0 3px 8px rgba(0, 0, 0, 0.7), 0 0 20px rgba(255, 215, 0, 0.5);
}
.score-minus {
color: #FF4444;
text-shadow: 0 3px 8px rgba(0, 0, 0, 0.7), 0 0 20px rgba(255, 68, 68, 0.5);
}
@keyframes score-float-up {
0% {
opacity: 1;
transform: translate(-50%, -100%) scale(0.5);
}
25% {
opacity: 1;
transform: translate(-50%, -140%) scale(1.15);
}
100% {
opacity: 0;
transform: translate(-50%, -260%) scale(0.8);
}
} }
.horse-wrapper { .horse-wrapper {
...@@ -794,114 +591,6 @@ watch(() => props.isDivDescVisible, (visible) => { ...@@ -794,114 +591,6 @@ watch(() => props.isDivDescVisible, (visible) => {
transform: translateX(0); transform: translateX(0);
} }
/* 游戏结束结果页覆盖层 */
.rank-overlay {
position: absolute;
inset: 0;
z-index: 100;
background: rgba(0, 0, 0, 0.65);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.ranklist-container {
width: 550px;
background: v-bind('imageUrls.ranklist') center / 100% 100% no-repeat;
border-radius: 16px;
padding: 60px 30px 30px;
display: flex;
flex-direction: column;
align-items: center;
.txt-title {
font-size: 36px;
font-weight: bold;
color: #AA0000;
letter-spacing: 2px;
margin-bottom: 16px;
}
.list-container {
width: 100%;
text-align: center;
line-height: 56px;
font-size: 24px;
.row {
display: grid;
grid-template-columns: 100px auto 120px;
align-items: center;
}
.row-1 {
color: #AA0000;
font-weight: bold;
background: linear-gradient(-84deg, #FFD396 0%, #FFF3D1 53%, #FFD59C 100%);
border-radius: 12px 12px 0 0;
}
.row-2 {
color: #633911;
max-height: 440px;
overflow-y: auto;
.rank-row:nth-child(odd) {
background: #F2E2AE;
}
.rank-row:nth-child(even) {
background: #FCEEBF;
}
}
.row-3 {
background: linear-gradient(0deg, #F4B64C 0%, #F7C96F 100%);
border-radius: 0 0 12px 12px;
font-weight: bold;
}
.col-2 {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
.avatar {
width: 40px;
height: 40px;
flex: 0 0 auto;
border-radius: 50%;
background-position: center;
background-size: cover;
}
}
}
}
.img-rank-close {
width: 48px;
height: 48px;
margin-top: 24px;
border-radius: 50%;
background: v-bind('imageUrls.close') center / cover no-repeat;
cursor: pointer;
}
.rank-fade-enter-active {
transition: opacity 0.35s ease-out;
}
.rank-fade-leave-active {
transition: opacity 0.25s ease-in;
}
.rank-fade-enter-from,
.rank-fade-leave-to {
opacity: 0;
}
.img-logo { .img-logo {
position: absolute; position: absolute;
top: 46px; top: 46px;
......
...@@ -12,21 +12,21 @@ const gameId = 'game4'; ...@@ -12,21 +12,21 @@ const gameId = 'game4';
type GameView = 'loading' | 'playing' | 'score' type GameView = 'loading' | 'playing' | 'score'
const imageUrls = { const imageUrls = {
bg: cssAssetUrl('game4/h5-game4-bg.svg'), bg: cssAssetUrl('game4/h5-game4-bg.webp'),
bg2: cssAssetUrl('game1/bg2.png'), bg2: cssAssetUrl('game1/bg2.png'),
playBg: cssAssetUrl('game4/h5-game4-bg2.svg'), playBg: cssAssetUrl('game4/h5-game4-bg2.webp'),
logo: cssAssetUrl('game4/logo.svg'), logo: cssAssetUrl('game4/logo.webp'),
time: cssAssetUrl('game4/h5-game4-clock.svg'), time: cssAssetUrl('game4/h5-game4-clock.svg'),
title: cssAssetUrl('game4/h5-game4-title1.svg'), title: cssAssetUrl('game4/h5-game4-title1.webp'),
title2: cssAssetUrl('game4/h5-game4-title2.svg'), title2: cssAssetUrl('game4/h5-game4-title2.webp'),
// avatarBg: cssAssetUrl('game4/h5-game4-join.svg'), // avatarBg: cssAssetUrl('game4/h5-game4-join.svg'),
userBg: cssAssetUrl('game4/user-bg.svg'), userBg: cssAssetUrl('game4/user-bg.webpp'),
startBg: cssAssetUrl('game4/start-bg.svg'), startBg: cssAssetUrl('game4/start-bg.webp'),
gift: cssAssetUrl('game4/h5-game4-gift.svg'), gift: cssAssetUrl('game4/h5-game4-gift.webp'),
avatar: cssAssetUrl('game1/avatar.png'), avatar: cssAssetUrl('game1/avatar.png'),
gu: cssAssetUrl('game4/h5-game4-horse.svg'), gu: cssAssetUrl('game4/h5-game4-horse.webp'),
clock: cssAssetUrl('game1/clock.png'), clock: cssAssetUrl('game1/clock.png'),
horse: cssAssetUrl('game4/h5-game4-horse.svg'), horse: cssAssetUrl('game4/h5-game4-horse.webp'),
yaoyiyao: cssAssetUrl('game4/h5-game4-yaoyiyao.svg'), yaoyiyao: cssAssetUrl('game4/h5-game4-yaoyiyao.svg'),
scoreBg: cssAssetUrl('game1/score-bg.png'), scoreBg: cssAssetUrl('game1/score-bg.png'),
back: cssAssetUrl('game4/back.svg'), back: cssAssetUrl('game4/back.svg'),
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -11,7 +11,7 @@ const password = ref('') ...@@ -11,7 +11,7 @@ const password = ref('')
const imageUrls = { const imageUrls = {
bg1: cssAssetUrl('main-bg1.png'), bg1: cssAssetUrl('main-bg1.png'),
logo: cssAssetUrl('game1/logo.png'), logo: cssAssetUrl('game1/logo.png'),
title1: cssAssetUrl('main-title.png'), title1: cssAssetUrl('main-title.webp'),
} }
async function login() { async function login() {
......
...@@ -8,7 +8,7 @@ import { onMounted, onUnmounted, ref } from 'vue'; ...@@ -8,7 +8,7 @@ import { onMounted, onUnmounted, ref } from 'vue';
const imageUrls = { const imageUrls = {
bg1: cssAssetUrl('main-bg1.png'), bg1: cssAssetUrl('main-bg1.png'),
logo: cssAssetUrl('game1/logo.png'), logo: cssAssetUrl('game1/logo.png'),
title1: cssAssetUrl('main-title.png'), title1: cssAssetUrl('main-title.webp'),
icon1: cssAssetUrl('icon1.png'), icon1: cssAssetUrl('icon1.png'),
icon2: cssAssetUrl('icon2.png'), icon2: cssAssetUrl('icon2.png'),
......
...@@ -42,7 +42,7 @@ const imageUrls = { ...@@ -42,7 +42,7 @@ const imageUrls = {
// ========== 马匹帧配置(只用 ma1/ma2/ma3,不用 ma4) ========== // ========== 马匹帧配置(只用 ma1/ma2/ma3,不用 ma4) ==========
const HORSE_SIZE = 280; const HORSE_SIZE = 280;
const HORSE_START_X = -HORSE_SIZE; const HORSE_START_X = -100;
const HORSE_END_X = 1920; const HORSE_END_X = 1920;
const HORSE_Y_OFFSET = 50; // 马匹整体偏移 const HORSE_Y_OFFSET = 50; // 马匹整体偏移
...@@ -341,7 +341,6 @@ const startIntro = async () => { ...@@ -341,7 +341,6 @@ const startIntro = async () => {
isGameRunning.value = false; isGameRunning.value = false;
horses.value = []; horses.value = [];
horseIdCounter = 0; horseIdCounter = 0;
// 重置加速状态
transientBoosts.value = {}; transientBoosts.value = {};
Object.keys(prevScores).forEach(k => delete prevScores[k]); Object.keys(prevScores).forEach(k => delete prevScores[k]);
preloadHorseFrames(); preloadHorseFrames();
...@@ -349,11 +348,45 @@ const startIntro = async () => { ...@@ -349,11 +348,45 @@ const startIntro = async () => {
const countdownPromise = designStage.value?.startCountdown(3, 60); const countdownPromise = designStage.value?.startCountdown(3, 60);
await new Promise((r) => setTimeout(r, 3500)); await new Promise((r) => setTimeout(r, 3500));
const validCount = list.value.filter(p => getPlayerKey(p).length > 0 && !getPlayerKey(p).startsWith('empty-')).length; console.log('[game3 Rank1View] 倒计时结束,马匹开始奔跑');
console.log('[game3 Rank1View] startIntro: 3.5s 后, list 有效玩家数:', validCount);
gameStarted.value = true; gameStarted.value = true;
isGameRunning.value = true; isGameRunning.value = true;
updateHorses(list.value);
const validPlayers = list.value.filter(p => {
const key = getPlayerKey(p);
return key && !key.startsWith('empty-') && p.nickname !== '-' && p.nickname !== '虚位以待';
});
if (validPlayers.length > 0) {
updateHorses(list.value);
} else {
const count = 8 + Math.floor(Math.random() * 3);
const now = performance.now();
const newHorses: Horse[] = [];
for (let i = 0; i < count; i++) {
const type = HORSE_TYPES[i % HORSE_TYPES.length]!;
const lane = i % LANE_TOP_POSITIONS.length;
const delayMs = i * 50 + Math.random() * 50;
const durationMs = (60 + Math.random() * 40) * 1000;
newHorses.push({
id: `horse-${++horseIdCounter}`,
horseType: type,
lane,
frameIndex: Math.floor(Math.random() * (horseFrameCount[type] ?? 12)),
frameTimer: 0,
x: HORSE_START_X,
xPercent: 0,
boost: 0,
nickname: `马匹${i + 1}`,
avatar: '',
playerKey: `auto-${i}`,
});
}
horses.value = newHorses;
console.log('[game3 Rank1View] 生成', count, '匹默认马');
}
startHorseLoop(); startHorseLoop();
await countdownPromise; await countdownPromise;
......
...@@ -14,14 +14,14 @@ const emit = defineEmits<{ ...@@ -14,14 +14,14 @@ const emit = defineEmits<{
}>(); }>();
const imageUrls = { const imageUrls = {
bg: cssAssetUrl("game4/bg.svg"), bg: cssAssetUrl("game4/bg.webp"),
logo: cssAssetUrl("game1/logo.png"), logo: cssAssetUrl("game1/logo.png"),
title1: cssAssetUrl("game4/title1.svg"), title1: cssAssetUrl("game4/title1.webp"),
title2: cssAssetUrl("game4/title2.svg"), title2: cssAssetUrl("game4/title2.svg"),
qrcode: assetUrl("game4/qrcode.png"), qrcode: assetUrl("game4/qrcode.png"),
gift: cssAssetUrl("game4/gift.svg"), gift: cssAssetUrl("game4/gift.webp"),
gu: cssAssetUrl("game4/big-brum.svg"), gu: cssAssetUrl("game4/big-brum.webp"),
smallGu: cssAssetUrl("game4/small-brum.svg"), smallGu: cssAssetUrl("game4/small-brum.webp"),
bottomLayers: cssAssetUrl("game1/bottom-layers.png"), bottomLayers: cssAssetUrl("game1/bottom-layers.png"),
avatarAnimation: cssAssetUrl("avatar_animation.png"), avatarAnimation: cssAssetUrl("avatar_animation.png"),
avatar: cssAssetUrl("avatar.png"), avatar: cssAssetUrl("avatar.png"),
......
...@@ -5,7 +5,7 @@ import type Player from "@/commons/player.ts"; ...@@ -5,7 +5,7 @@ import type Player from "@/commons/player.ts";
import { $is_local_mode } from "@/commons/utils.ts"; import { $is_local_mode } from "@/commons/utils.ts";
import { cssAssetUrl, assetUrl } from "@/commons/assets.ts"; import { cssAssetUrl, assetUrl } from "@/commons/assets.ts";
import { playApplauseMusic, stopApplauseMusic } from "@/commons/music"; import { playApplauseMusic, stopApplauseMusic } from "@/commons/music";
const defaultAvatarUrl = assetUrl("game4/default-avator.svg"); const defaultAvatarUrl = assetUrl("game4/default-avator.webp");
const props = defineProps<{ const props = defineProps<{
rankList: Player[]; rankList: Player[];
...@@ -25,22 +25,22 @@ const imageUrls = { ...@@ -25,22 +25,22 @@ const imageUrls = {
bgTop: cssAssetUrl("game4/bg-top.webp"), bgTop: cssAssetUrl("game4/bg-top.webp"),
bgBottom: cssAssetUrl("game4/bg-bottom.webp"), bgBottom: cssAssetUrl("game4/bg-bottom.webp"),
logo: cssAssetUrl("game1/logo.png"), logo: cssAssetUrl("game1/logo.png"),
titleBg: cssAssetUrl("game4/horse-title-bg.svg"), titleBg: cssAssetUrl("game4/horse-title-bg.webp"),
bigBrum: cssAssetUrl("game4/big-brum.svg"), bigBrum: cssAssetUrl("game4/big-brum.webp"),
username: cssAssetUrl("game4/username.svg"), username: cssAssetUrl("game4/username.svg"),
avatar: cssAssetUrl("game4/default-avator.svg"), avatar: cssAssetUrl("game4/default-avator.webp"),
resultBg: cssAssetUrl("game4/resultBg.svg"), resultBg: cssAssetUrl("game4/resultbg.webp"),
next: cssAssetUrl("game1/next.png"), next: cssAssetUrl("game1/next.png"),
back: cssAssetUrl("game1/back.png"), back: cssAssetUrl("game1/back.png"),
// 结果弹窗 - podium 相关资源(复用 game1) // 结果弹窗 - podium 相关资源(复用 game1)
podium: cssAssetUrl("game4/podium.svg"), podium: cssAssetUrl("game4/podium.webp"),
fireworks: cssAssetUrl("game1/fireworks.png"), fireworks: cssAssetUrl("game1/fireworks.png"),
guan1: cssAssetUrl("game1/guan1.png"), guan1: cssAssetUrl("game1/guan1.png"),
pai1: cssAssetUrl("game4/pai1.svg"), pai1: cssAssetUrl("game4/pai1.webp"),
guan2: cssAssetUrl("game1/guan2.png"), guan2: cssAssetUrl("game1/guan2.png"),
pai2: cssAssetUrl("game4/pai2.svg"), pai2: cssAssetUrl("game4/pai2.webp"),
guan3: cssAssetUrl("game1/guan3.png"), guan3: cssAssetUrl("game1/guan3.png"),
pai3: cssAssetUrl("game4/pai3.svg"), pai3: cssAssetUrl("game4/pai3.webp"),
rankIcon: cssAssetUrl("game1/no-icon.png"), rankIcon: cssAssetUrl("game1/no-icon.png"),
avatarAnimation: cssAssetUrl("avatar_animation.png"), avatarAnimation: cssAssetUrl("avatar_animation.png"),
}; };
...@@ -52,12 +52,12 @@ const TOTAL_DISTANCE_PX = 2160; // -120px ~ 2040px ...@@ -52,12 +52,12 @@ const TOTAL_DISTANCE_PX = 2160; // -120px ~ 2040px
const BASE_DURATION_SEC = 220; const BASE_DURATION_SEC = 220;
const BASE_SPEED_PERCENT_PER_SEC = 100 / BASE_DURATION_SEC; // ≈ 0.455%/s 基准速度 const BASE_SPEED_PERCENT_PER_SEC = 100 / BASE_DURATION_SEC; // ≈ 0.455%/s 基准速度
// 马匹精灵图帧动画(horse.png 共 12 帧,改用 JS 切帧,避免 CSS steps 在部分浏览器不生效) // 马匹精灵图帧动画(horse.webp 共 12 帧,改用 JS 切帧,避免 CSS steps 在部分浏览器不生效)
const HORSE_FRAME_COUNT = 12; const HORSE_FRAME_COUNT = 12;
const HORSE_FRAME_TICK_MS = 48; // ~20fps const HORSE_FRAME_TICK_MS = 48; // ~20fps
const HORSE_SPRITE_OFFSET_X = -17; const HORSE_SPRITE_OFFSET_X = -17;
const HORSE_SPRITE_FRAME_WIDTH = 120; const HORSE_SPRITE_FRAME_WIDTH = 120;
const horseSpriteUrl = assetUrl("game4/horse.png"); const horseSpriteUrl = assetUrl("game4/horse.webp");
// ========== 瞬时加速脉冲机制 ========== // ========== 瞬时加速脉冲机制 ==========
// H5 每次摇一摇发送累计积分,PC 端检测 score 增长 → 触发短暂加速脉冲 → 迅速衰减 // H5 每次摇一摇发送累计积分,PC 端检测 score 增长 → 触发短暂加速脉冲 → 迅速衰减
...@@ -398,25 +398,32 @@ const startIntro = async () => { ...@@ -398,25 +398,32 @@ const startIntro = async () => {
gameStarted.value = false; gameStarted.value = false;
isGameRunning.value = false; isGameRunning.value = false;
stopApplauseMusic(); stopApplauseMusic();
// 重置瞬时加速和积分追踪
transientBoosts.value = {}; transientBoosts.value = {};
Object.keys(prevScores).forEach((k) => delete prevScores[k]); Object.keys(prevScores).forEach((k) => delete prevScores[k]);
// 启动完整倒计时:3s 开场 + 3s 游戏(DesignStage 内部管理两个阶段的 UI)
const countdownPromise = designStage.value?.startCountdown(3, 60); const countdownPromise = designStage.value?.startCountdown(3, 60);
// 等待 3-2-1 开场倒计时结束(~3.5s 后安全触发)
await new Promise((r) => setTimeout(r, 3500)); await new Promise((r) => setTimeout(r, 3500));
// Phase 2: 倒计时结束 → 游戏正式开始,背景动画+马匹启动
gameStarted.value = true; gameStarted.value = true;
isGameRunning.value = true; isGameRunning.value = true;
if (horses.value.length === 0) {
const count = 8 + Math.floor(Math.random() * 3);
const mockPlayers: Player[] = Array.from({ length: count }, (_, i) => ({
userid: `auto-${i}`,
nickname: mockNicknames[i] || `马匹${i + 1}`,
avatar: mockAvatars[i] || '',
score: Math.floor(Math.random() * 60) + 10,
}));
updateHorses(mockPlayers);
console.log('[game4 Rank1View] 生成', count, '匹默认马');
}
startGameLoop(); startGameLoop();
if ($is_local_mode()) { if ($is_local_mode()) {
startMockBoost(); startMockBoost();
} }
// 等待 60s 游戏倒计时完全结束 → 才出现结果页
await countdownPromise; await countdownPromise;
stopGame(); stopGame();
}; };
...@@ -442,7 +449,7 @@ const renderAvatar = (item: any) => { ...@@ -442,7 +449,7 @@ const renderAvatar = (item: any) => {
if (item && item.avatar) { if (item && item.avatar) {
return `background: url('${item.avatar}') center / cover no-repeat;`; return `background: url('${item.avatar}') center / cover no-repeat;`;
} }
// 无头像时用 default-avator.svg 占位(白底 + 居中 cover) // 无头像时用 default-avator 占位(白底 + 居中 cover)
return `background: #fff url('${defaultAvatarUrl}') center / cover no-repeat;`; return `background: #fff url('${defaultAvatarUrl}') center / cover no-repeat;`;
}; };
......
...@@ -14,7 +14,7 @@ const emit = defineEmits<{ ...@@ -14,7 +14,7 @@ const emit = defineEmits<{
const FINAL_RANK_LIMIT = 10; const FINAL_RANK_LIMIT = 10;
const imageUrls = { const imageUrls = {
bg: cssAssetUrl("game4/bg.svg"), bg: cssAssetUrl("game4/bg.webp"),
logo: cssAssetUrl("game1/logo.png"), logo: cssAssetUrl("game1/logo.png"),
title: cssAssetUrl("game1/title3.png"), title: cssAssetUrl("game1/title3.png"),
title2: cssAssetUrl("game1/title4.png"), title2: cssAssetUrl("game1/title4.png"),
......
...@@ -14,7 +14,7 @@ const emit = defineEmits<{ ...@@ -14,7 +14,7 @@ const emit = defineEmits<{
}>(); }>();
const imageUrls = { const imageUrls = {
bg: cssAssetUrl('game5/bg.svg'), bg: cssAssetUrl('game5/bg.webp'),
logo: cssAssetUrl('game1/logo.png'), logo: cssAssetUrl('game1/logo.png'),
title1: cssAssetUrl('game5/title1.webp'), title1: cssAssetUrl('game5/title1.webp'),
// title2: cssAssetUrl('game1/title2.png'), // title2: cssAssetUrl('game1/title2.png'),
......
...@@ -22,7 +22,7 @@ const showResult = ref(false); ...@@ -22,7 +22,7 @@ const showResult = ref(false);
const imageUrls = { const imageUrls = {
runway: cssAssetUrl("game5/paodao.webp"), runway: cssAssetUrl("game5/paodao.webp"),
bg: cssAssetUrl("game5/bg.svg"), bg: cssAssetUrl("game5/bg.webp"),
logo: cssAssetUrl("game1/logo.png"), logo: cssAssetUrl("game1/logo.png"),
title: cssAssetUrl("game5/title3.webp"), title: cssAssetUrl("game5/title3.webp"),
rankNo4: cssAssetUrl("game6/no4.png"), rankNo4: cssAssetUrl("game6/no4.png"),
......
...@@ -15,7 +15,7 @@ const emit = defineEmits<{ ...@@ -15,7 +15,7 @@ const emit = defineEmits<{
const FINAL_RANK_LIMIT = 10; const FINAL_RANK_LIMIT = 10;
const imageUrls = { const imageUrls = {
bg: cssAssetUrl('game5/bg.svg'), bg: cssAssetUrl('game5/bg.webp'),
logo: cssAssetUrl('game1/logo.png'), logo: cssAssetUrl('game1/logo.png'),
title: cssAssetUrl('game5/title.webp'), title: cssAssetUrl('game5/title.webp'),
title2: cssAssetUrl('game5/title2.webp'), title2: cssAssetUrl('game5/title2.webp'),
......
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