Commit f807008f authored by 董政锦's avatar 董政锦

style: game3 的 h5 样式马位置、道具位置、道具碰到位置、苹果蒙层宽度调整;

parent f8b7ee46
...@@ -193,14 +193,18 @@ defineExpose({ ...@@ -193,14 +193,18 @@ defineExpose({
} }
.modal-container { .modal-container {
position: absolute; /* position:fixed + 显式 100vw/100vh 确保在 iOS Safari 中全屏蒙层,
inset: 0; 避免因父级 overflow:hidden / transform:scale 导致 inset:0 计算异常 */
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10; z-index: 10;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: rgba(0, 0, 0, 0.6); background-color: rgba(0, 0, 0, 0.6);
/* pointer-events: none; */
} }
.game-rule-fade-enter-active, .game-rule-fade-enter-active,
......
<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 { useGameSocket, sendGameMessage, userJoinStatus, joinSharedRoom } from '@/composables/useGameSocket' import {
import LoadingView from './views/LoadingView.vue' useGameSocket,
import PlayingView from './views/PlayingView.vue' sendGameMessage,
import { $getWechat, $toast, $is_run_local } from '@/commons/utils.ts' userJoinStatus,
import { playGame3Music, stopAllMusic } from '@/commons/music'; joinSharedRoom,
} from "@/composables/useGameSocket";
type GameView = 'loading' | 'playing' import LoadingView from "./views/LoadingView.vue";
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 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 list[index] ?? { return (
rank: index + 1, list[index] ?? {
nickname: '-', rank: index + 1,
avatar: '', nickname: "-",
score: '-', avatar: "",
} 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_run_local() const isLocalMode = !IS_PRODUCTION && $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 (save) { if (!wechat && !isLocalMode) return;
sendGameMessage('submit_score_save', { if (save) {
score: currentTick, if (isLocalMode && !wechat) {
wechat: wechat.token_origin, console.log("[game3 本地模式] 提交最终分数:", currentTick);
item_num: 3, return;
nickname: wechat.nickname, }
avatar: wechat.avatar, sendGameMessage("submit_score_save", {
rank: rank.value, score: currentTick,
}) wechat: wechat.token_origin,
} else { item_num: 3,
sendGameMessage('submit_score', { score: currentTick }) nickname: wechat.nickname,
avatar: wechat.avatar,
rank: rank.value,
});
} else {
if (isLocalMode && !wechat) {
console.log("[game3 本地模式] 实时分数:", currentTick);
return;
} }
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) gameCountdownTimer = window.setTimeout(tickCountdown, 1000);
};
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 isGuPlaying.value = false;
isGameOver.value = false isGameOver.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 isGameOver.value = false;
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;
} }
const scoreChangeHandler = (delta: number) => { const scoreChangeHandler = (delta: number) => {
if (delta > 0) { if (delta > 0) {
playGame3Music(1); playGame3Music(1);
} else { } else {
playGame3Music(2); playGame3Music(2);
} }
let nextTick = score.value + delta let nextTick = score.value + delta;
if (nextTick < 0) nextTick = 0 if (nextTick < 0) nextTick = 0;
score.value = nextTick score.value = nextTick;
submitScore(false, 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(nickname.value, token.value, avatar.value, gameId) const joined = joinSharedRoom(
}, 200) nickname.value,
}, token.value,
onGameStart: async () => { avatar.value,
showGameRule.value = false; gameId,
await mobileStageRef.value?.startCountdown() );
startGameView() }, 200);
}, },
onScoreSubmitted: (is_save, data) => { onGameStart: async () => {
if (!is_save) {//非保存状态下 showGameRule.value = false;
const nextRank = Number(typeof data === 'object' ? data?.rank : data) await mobileStageRef.value?.startCountdown();
if (Number.isFinite(nextRank) && nextRank > 0) { startGameView();
rank.value = nextRank },
} onScoreSubmitted: (is_save, data) => {
} else if (typeof data === 'object' && data) { if (!is_save) {
if (Array.isArray(data.list)) { //非保存状态下
rankList.value = data.list.map((item: any, index: number) => ({ const nextRank = Number(typeof data === "object" ? data?.rank : data);
rank: Number(item?.rank ?? index + 1), if (Number.isFinite(nextRank) && nextRank > 0) {
nickname: String(item?.nickname ?? ''), rank.value = nextRank;
avatar: String(item?.avatar ?? ''), }
score: Number(item?.score ?? 0), } else if (typeof data === "object" && data) {
})) if (Array.isArray(data.list)) {
} rankList.value = data.list.map((item: any, index: number) => ({
rank: Number(item?.rank ?? index + 1),
const nextRank = Number(data.rank) nickname: String(item?.nickname ?? ""),
if (Number.isFinite(nextRank) && nextRank > 0) { avatar: String(item?.avatar ?? ""),
rank.value = nextRank score: Number(item?.score ?? 0),
} }));
} }
},
onRescoreSubmitted: (_recount) => { const nextRank = Number(data.rank);
submitScore(true); if (Number.isFinite(nextRank) && nextRank > 0) {
}, rank.value = nextRank;
onGameRecover: (data) => { }
const recoveredRank = Number(data?.rank) }
const recoveredScore = Number(data?.score) },
const recoveredSeconds = Number(data?.remainingSeconds) onRescoreSubmitted: (_recount) => {
submitScore(true);
rank.value = Number.isFinite(recoveredRank) && recoveredRank > 0 ? recoveredRank : 0 },
score.value = Number.isFinite(recoveredScore) && recoveredScore > 0 ? recoveredScore : 0 onGameRecover: (data) => {
showGameRule.value = false const recoveredRank = Number(data?.rank);
showGameRank.value = false const recoveredScore = Number(data?.score);
isGameOver.value = false const recoveredSeconds = Number(data?.remainingSeconds);
currentView.value = 'playing'
startGameCountdown( rank.value =
Number.isFinite(recoveredSeconds) && recoveredSeconds > 0 ? recoveredSeconds : 0, Number.isFinite(recoveredRank) && recoveredRank > 0 ? recoveredRank : 0;
) score.value =
}, Number.isFinite(recoveredScore) && recoveredScore > 0
onRoomClosed: () => { ? recoveredScore
stopGameCountdown() : 0;
currentView.value = 'loading' showGameRule.value = false;
setDivDescVisible(false) showGameRank.value = false;
$toast('管理员关闭了游戏房间') isGameOver.value = false;
}, currentView.value = "playing";
onRoomBack: () => { startGameCountdown(
}, Number.isFinite(recoveredSeconds) && recoveredSeconds > 0
}) ? recoveredSeconds
_offSocketMessage.value = offSocketMessage : 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 时生效)
if (isLocalMode) {
showGameRule.value = false;
currentView.value = "playing";
startGameCountdown();
}
});
onBeforeUnmount(() => { onBeforeUnmount(() => {
_offSocketMessage.value?.() _offSocketMessage.value?.();
stopAllMusic() stopAllMusic();
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 isGameOver.value = false;
resetToLoadingView() resetToLoadingView();
} };
</script> </script>
<template> <template>
<MobileStage v-if="token" :showGameRule="showGameRule" :showGameRank="showGameRank" ref="mobileStageRef" <MobileStage
:background="`${renderBG()}`"> v-if="token || isLocalMode"
<template #gameRule> :showGameRule="showGameRule"
<div class="rule-container"> :showGameRank="showGameRank"
<div class="rule-txt-container"> ref="mobileStageRef"
<div class="rule-title">游戏规则</div> :background="`${renderBG()}`"
<div class="rule-txt"> >
1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2 分;<br /> <template #gameRule>
2. 限时闯关比拼得分,结束后按积分排名;<br /> <div class="rule-container">
3. 高分用户有机会领取新疆福彩定制周边;<br /> <div class="rule-txt-container">
4. 禁止外挂作弊,违规账号取消成绩与领奖资格;<br /><br /> <div class="rule-title">游戏规则</div>
祝您游戏愉快! <div class="rule-txt">
</div> 1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2
<div class="rule-close" @click="showGameRuleHandler"></div> 分;<br />
</div> 2. 限时闯关比拼得分,结束后按积分排名;<br />
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>
</template> </div>
<template #gameRank> <div class="row row-3">
<div class="ranklist-container"> <div class="col-1">{{ rank > 0 ? rank : "未上榜" }}</div>
<div class="txt-title">排行</div> <div class="col-2">
<div class="list-container"> <div
<div class="row row-1"> class="avatar"
<div class="col-1">排名</div> :style="{ backgroundImage: `url(${avatar})` }"
<div class="col-2">用户</div> ></div>
<div class="col-3">成绩</div> <div>{{ nickname }}</div>
</div> </div>
<div class="row-2"> <div class="col-3">{{ rankScore }}</div>
<div class="row rank-row" v-for="(item, index) in displayRankList" </div>
:key="`${item.rank}-${index}`"> </div>
<div class="col-1"> <div class="img-close" @click="rankCloseHandler"></div>
<div>{{ item.rank }}</div> </div>
</div> </template>
<div class="col-2"> <LoadingView
<div class="avatar" v-if="currentView === 'loading'"
:style="item.avatar ? { backgroundImage: `url(${item.avatar})` } : {}"></div> :image-urls="imageUrls"
<div>{{ item.nickname }}</div> :user-join-status="userJoinStatus"
</div> @touchGameRule="showGameRuleHandler"
<div class="col-3">{{ item.score }}</div> />
</div> <PlayingView
</div> v-else
<div class="row row-3"> :image-urls="imageUrls"
<div class="col-1">{{ rank > 0 ? rank : '未上榜' }}</div> :tick="score"
<div class="col-2"> :rank="rank"
<div class="avatar" :style="{ backgroundImage: `url(${avatar})` }"></div> :countdown-interval="countdownInterval"
<div>{{ nickname }}</div> :is-div-desc-visible="isDivDescVisible"
</div> @score-change="scoreChangeHandler"
<div class="col-3">{{ rankScore }}</div> @rank-close="rankCloseHandler"
</div> />
</div>
<div class="img-close" @click="rankCloseHandler"></div> <!-- <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
</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" :is-game-over="isGameOver"
:rank-list="displayRankList" :user-rank="rank" :user-score="rankScore" :rank-list="displayRankList" :user-rank="rank" :user-score="rankScore"
:user-nickname="nickname" :user-avatar="avatar" :user-nickname="nickname" :user-avatar="avatar"
@touch="touchHandler" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" /> --> @touch="touchHandler" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" /> -->
</MobileStage> </MobileStage>
<div v-else style="text-align: center; width: 100vw; height: 100vh; line-height: 30; font-size: 20px;"> <div
请使用微信扫码进入游戏 v-else
</div> 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 {
.rule-txt-container { .rule-txt-container {
background: v-bind('imageUrls.rule') center / cover no-repeat; background: v-bind("imageUrls.rule") center / cover no-repeat;
width: 692px; width: 692px;
height: 797px; height: 797px;
margin-top: -20%; margin-top: -20%;
transform: scale(0.5); transform: scale(0.5);
.rule-title { .rule-title {
text-align: center; text-align: center;
color: white; color: white;
font-weight: bold; font-weight: bold;
font-size: 26pt; font-size: 26pt;
line-height: 50pt; line-height: 50pt;
letter-spacing: 4px; letter-spacing: 4px;
}
.rule-txt {
font-size: 26pt;
padding: 40px;
margin-top: 10px;
color: #AA0000;
}
} }
.rule-close { .rule-txt {
width: 57px; font-size: 26pt;
height: 57px; padding: 40px;
background: v-bind('imageUrls.close') center / cover no-repeat; margin-top: 10px;
transform: translateX(-50%) scale(1.4); color: #aa0000;
position: absolute;
bottom: -75px;
left: 50%;
} }
}
.rule-close {
width: 57px;
height: 57px;
background: v-bind("imageUrls.close") center / cover no-repeat;
transform: translateX(-50%) scale(1.4);
position: absolute;
bottom: -75px;
left: 50%;
}
} }
.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;
.txt-title { top: 140px;
width: 100%; text-align: center;
text-align: center; height: 73px;
font-size: 50px; line-height: 73px;
margin-top: 35px; font-size: 32px;
font-weight: bold;
color: #AA0000; .row {
letter-spacing: 2px; display: grid;
grid-template-columns: 150px auto 150px;
align-items: center;
} }
.list-container { .row-1 {
width: calc(100% - 90px); color: #aa0000;
height: calc(100% - 210px); font-weight: bold;
position: absolute; background: linear-gradient(
left: 40px; -84deg,
top: 140px; #ffd396 0%,
text-align: center; #fff3d1 53%,
height: 73px; #ffd59c 100%
line-height: 73px; );
font-size: 32px; border-radius: 20px 20px 0px 0px;
}
.row {
display: grid;
grid-template-columns: 150px auto 150px;
align-items: center;
}
.row-1 {
color: #AA0000;
font-weight: bold;
background: linear-gradient(-84deg, #FFD396 0%, #FFF3D1 53%, #FFD59C 100%);
border-radius: 20px 20px 0px 0px;
}
.row-2 { .row-2 {
color: #633911; color: #633911;
.rank-row:nth-child(odd) { .rank-row:nth-child(odd) {
background: #F2E2AE; background: #f2e2ae;
} }
.rank-row:nth-child(even) { .rank-row:nth-child(even) {
background: #FCEEBF; background: #fceebf;
} }
.col-1 { .col-1 {
position: relative; position: relative;
} }
} }
.row-3 { .row-3 {
background: linear-gradient(0deg, #F4B64C 0%, #F7C96F 100%); background: linear-gradient(0deg, #f4b64c 0%, #f7c96f 100%);
border-radius: 0px 0px 20px 20px; border-radius: 0px 0px 20px 20px;
} }
.col-2 { .col-2 {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 18px; gap: 18px;
.avatar { .avatar {
width: 48px; width: 48px;
height: 48px; height: 48px;
flex: 0 0 auto; flex: 0 0 auto;
border-radius: 50%; border-radius: 50%;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
} }
}
} }
}
} }
.img-close { .img-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.4); transform: translateX(-50%) scale(1.4);
position: absolute; position: absolute;
top: 0; top: 0;
left: 100%; left: 100%;
} }
</style> </style>
...@@ -26,8 +26,26 @@ const HORSE_BOTTOM_PCT = 19.11; ...@@ -26,8 +26,26 @@ const HORSE_BOTTOM_PCT = 19.11;
const ITEM_SIZE = 80; const ITEM_SIZE = 80;
const GAME_DURATION = 60; const GAME_DURATION = 60;
const LEFT_LANE_X = 170; // 设计基准跑道 X 位置(750px 设计稿下的值,窄屏设备上会自动居中偏移)
const RIGHT_LANE_X = 470; const DESIGN_LEFT_LANE_X = 150;
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;
...@@ -63,20 +81,20 @@ preloadFrames(bomFrames); ...@@ -63,20 +81,20 @@ 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(LEFT_LANE_X); const horseX = ref(leftLaneX.value);
const horseTargetX = ref(LEFT_LANE_X); const horseTargetX = ref(leftLaneX.value);
const switchToLeft = () => { const switchToLeft = () => {
if (horseLane.value !== 'left') { if (horseLane.value !== 'left') {
horseLane.value = 'left'; horseLane.value = 'left';
horseTargetX.value = LEFT_LANE_X; horseTargetX.value = leftLaneX.value;
} }
}; };
const switchToRight = () => { const switchToRight = () => {
if (horseLane.value !== 'right') { if (horseLane.value !== 'right') {
horseLane.value = 'right'; horseLane.value = 'right';
horseTargetX.value = RIGHT_LANE_X; horseTargetX.value = rightLaneX.value;
} }
}; };
...@@ -183,7 +201,7 @@ function generateNewItem() { ...@@ -183,7 +201,7 @@ 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.55 ? 'coin' : 'bomb'; const kind: ItemKind = seededRandom(seed) < 0.75 ? 'coin' : 'bomb';
// 概率分配车道,但避免与前一个同车道道具太近 // 概率分配车道,但避免与前一个同车道道具太近
let lane: 'left' | 'right' = seededRandom(seed + 1000) < 0.5 ? 'left' : 'right'; let lane: 'left' | 'right' = seededRandom(seed + 1000) < 0.5 ? 'left' : 'right';
...@@ -244,7 +262,7 @@ const checkCollisions = () => { ...@@ -244,7 +262,7 @@ const checkCollisions = () => {
for (const item of roadItems.value) { for (const item of roadItems.value) {
if (item.collected || item.colliding) continue; if (item.collected || item.colliding) continue;
const itemX = item.lane === 'left' ? LEFT_LANE_X : RIGHT_LANE_X; const itemX = item.lane === 'left' ? leftLaneX.value : rightLaneX.value;
// item.y 已经是屏幕坐标(content-bg 内),无需加 roadOffset // item.y 已经是屏幕坐标(content-bg 内),无需加 roadOffset
const itemCenterX = itemX + ITEM_SIZE / 2; const itemCenterX = itemX + ITEM_SIZE / 2;
const itemCenterY = item.y + ITEM_SIZE / 2; const itemCenterY = item.y + ITEM_SIZE / 2;
...@@ -278,7 +296,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => { ...@@ -278,7 +296,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
const frameDuration = 1000 / totalFrames; const frameDuration = 1000 / totalFrames;
// 碰撞动画(大幅放大尺寸,x 往右偏移让动画居中于视觉碰撞点) // 碰撞动画(大幅放大尺寸,x 往右偏移让动画居中于视觉碰撞点)
effectAnims.value.push({ id: animId, kind: item.kind, frame: 0, x: x + 40, 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 = () => {
...@@ -297,7 +315,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => { ...@@ -297,7 +315,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
const effectId = ++scoreEffectIdSeq; const effectId = ++scoreEffectIdSeq;
scoreEffects.value.push({ scoreEffects.value.push({
id: effectId, id: effectId,
x: x + ITEM_SIZE / 2, x: x + ITEM_SIZE / 2 + 25,
y: y, y: y,
score: delta, score: delta,
}); });
...@@ -377,6 +395,12 @@ const stopMainLoop = () => { ...@@ -377,6 +395,12 @@ 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();
...@@ -386,6 +410,8 @@ onMounted(() => { ...@@ -386,6 +410,8 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeydown); window.removeEventListener('keydown', handleKeydown);
window.removeEventListener('resize', updateLanePositions);
window.visualViewport?.removeEventListener('resize', updateLanePositions);
stopRoadScroll(); stopRoadScroll();
stopMainLoop(); stopMainLoop();
stopItemGen(); stopItemGen();
...@@ -405,6 +431,9 @@ watch(() => props.isDivDescVisible, (visible) => { ...@@ -405,6 +431,9 @@ watch(() => props.isDivDescVisible, (visible) => {
scoreEffects.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(); scheduleNextItem();
...@@ -582,18 +611,21 @@ watch(() => props.isDivDescVisible, (visible) => { ...@@ -582,18 +611,21 @@ 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 {
left: 170px; /* 当 content-bg 宽于 750px 设计稿时,自动居中 750px 游戏区域 */
left: calc((var(--stage-viewport-width, 750px) - 750px) / 2 + 170px);
} }
.item-lane-right { .item-lane-right {
left: 470px; left: calc((var(--stage-viewport-width, 750px) - 750px) / 2 + 520px);
} }
.effect-anim { .effect-anim {
......
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