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 (
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_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 (!wechat && !isLocalMode) return;
if (save) { if (save) {
sendGameMessage('submit_score_save', { if (isLocalMode && !wechat) {
console.log("[game3 本地模式] 提交最终分数:", currentTick);
return;
}
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 {
sendGameMessage('submit_score', { score: currentTick }) 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;
} }
...@@ -181,14 +200,14 @@ const scoreChangeHandler = (delta: number) => { ...@@ -181,14 +200,14 @@ const scoreChangeHandler = (delta: number) => {
} 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: {
...@@ -196,33 +215,39 @@ if (wechat) { ...@@ -196,33 +215,39 @@ if (wechat) {
}, },
onConnect: () => { onConnect: () => {
window.setTimeout(() => { window.setTimeout(() => {
const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId) const joined = joinSharedRoom(
}, 200) nickname.value,
token.value,
avatar.value,
gameId,
);
}, 200);
}, },
onGameStart: async () => { onGameStart: async () => {
showGameRule.value = false; showGameRule.value = false;
await mobileStageRef.value?.startCountdown() await mobileStageRef.value?.startCountdown();
startGameView() startGameView();
}, },
onScoreSubmitted: (is_save, data) => { onScoreSubmitted: (is_save, data) => {
if (!is_save) {//非保存状态下 if (!is_save) {
const nextRank = Number(typeof data === 'object' ? data?.rank : data) //非保存状态下
const nextRank = Number(typeof data === "object" ? data?.rank : data);
if (Number.isFinite(nextRank) && nextRank > 0) { if (Number.isFinite(nextRank) && nextRank > 0) {
rank.value = nextRank rank.value = nextRank;
} }
} else if (typeof data === 'object' && data) { } else if (typeof data === "object" && data) {
if (Array.isArray(data.list)) { if (Array.isArray(data.list)) {
rankList.value = data.list.map((item: any, index: number) => ({ rankList.value = data.list.map((item: any, index: number) => ({
rank: Number(item?.rank ?? index + 1), rank: Number(item?.rank ?? index + 1),
nickname: String(item?.nickname ?? ''), nickname: String(item?.nickname ?? ""),
avatar: String(item?.avatar ?? ''), avatar: String(item?.avatar ?? ""),
score: Number(item?.score ?? 0), score: Number(item?.score ?? 0),
})) }));
} }
const nextRank = Number(data.rank) const nextRank = Number(data.rank);
if (Number.isFinite(nextRank) && nextRank > 0) { if (Number.isFinite(nextRank) && nextRank > 0) {
rank.value = nextRank rank.value = nextRank;
} }
} }
}, },
...@@ -230,77 +255,95 @@ if (wechat) { ...@@ -230,77 +255,95 @@ if (wechat) {
submitScore(true); submitScore(true);
}, },
onGameRecover: (data) => { onGameRecover: (data) => {
const recoveredRank = Number(data?.rank) const recoveredRank = Number(data?.rank);
const recoveredScore = Number(data?.score) const recoveredScore = Number(data?.score);
const recoveredSeconds = Number(data?.remainingSeconds) const recoveredSeconds = Number(data?.remainingSeconds);
rank.value = Number.isFinite(recoveredRank) && recoveredRank > 0 ? recoveredRank : 0 rank.value =
score.value = Number.isFinite(recoveredScore) && recoveredScore > 0 ? recoveredScore : 0 Number.isFinite(recoveredRank) && recoveredRank > 0 ? recoveredRank : 0;
showGameRule.value = false score.value =
showGameRank.value = false Number.isFinite(recoveredScore) && recoveredScore > 0
isGameOver.value = false ? recoveredScore
currentView.value = 'playing' : 0;
showGameRule.value = false;
showGameRank.value = false;
isGameOver.value = false;
currentView.value = "playing";
startGameCountdown( startGameCountdown(
Number.isFinite(recoveredSeconds) && recoveredSeconds > 0 ? recoveredSeconds : 0, Number.isFinite(recoveredSeconds) && recoveredSeconds > 0
) ? recoveredSeconds
: 0,
);
}, },
onRoomClosed: () => { onRoomClosed: () => {
stopGameCountdown() stopGameCountdown();
currentView.value = 'loading' currentView.value = "loading";
setDivDescVisible(false) setDivDescVisible(false);
$toast('管理员关闭了游戏房间') $toast("管理员关闭了游戏房间");
},
onRoomBack: () => {
}, },
}) onRoomBack: () => {},
_offSocketMessage.value = offSocketMessage });
_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"
:showGameRule="showGameRule"
:showGameRank="showGameRank"
ref="mobileStageRef"
:background="`${renderBG()}`"
>
<template #gameRule> <template #gameRule>
<div class="rule-container"> <div class="rule-container">
<div class="rule-txt-container"> <div class="rule-txt-container">
<div class="rule-title">游戏规则</div> <div class="rule-title">游戏规则</div>
<div class="rule-txt"> <div class="rule-txt">
1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2 分;<br /> 1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2
分;<br />
2. 限时闯关比拼得分,结束后按积分排名;<br /> 2. 限时闯关比拼得分,结束后按积分排名;<br />
3. 高分用户有机会领取新疆福彩定制周边;<br /> 3. 高分用户有机会领取新疆福彩定制周边;<br />
4. 禁止外挂作弊,违规账号取消成绩与领奖资格;<br /><br /> 4. 禁止外挂作弊,违规账号取消成绩与领奖资格;<br /><br />
...@@ -320,23 +363,35 @@ const rankCloseHandler = () => { ...@@ -320,23 +363,35 @@ const rankCloseHandler = () => {
<div class="col-3">成绩</div> <div class="col-3">成绩</div>
</div> </div>
<div class="row-2"> <div class="row-2">
<div class="row rank-row" v-for="(item, index) in displayRankList" <div
:key="`${item.rank}-${index}`"> class="row rank-row"
v-for="(item, index) in displayRankList"
:key="`${item.rank}-${index}`"
>
<div class="col-1"> <div class="col-1">
<div>{{ item.rank }}</div> <div>{{ item.rank }}</div>
</div> </div>
<div class="col-2"> <div class="col-2">
<div class="avatar" <div
:style="item.avatar ? { backgroundImage: `url(${item.avatar})` } : {}"></div> class="avatar"
:style="
item.avatar
? { backgroundImage: `url(${item.avatar})` }
: {}
"
></div>
<div>{{ item.nickname }}</div> <div>{{ item.nickname }}</div>
</div> </div>
<div class="col-3">{{ item.score }}</div> <div class="col-3">{{ item.score }}</div>
</div> </div>
</div> </div>
<div class="row row-3"> <div class="row row-3">
<div class="col-1">{{ rank > 0 ? rank : '未上榜' }}</div> <div class="col-1">{{ rank > 0 ? rank : "未上榜" }}</div>
<div class="col-2"> <div class="col-2">
<div class="avatar" :style="{ backgroundImage: `url(${avatar})` }"></div> <div
class="avatar"
:style="{ backgroundImage: `url(${avatar})` }"
></div>
<div>{{ nickname }}</div> <div>{{ nickname }}</div>
</div> </div>
<div class="col-3">{{ rankScore }}</div> <div class="col-3">{{ rankScore }}</div>
...@@ -345,10 +400,22 @@ const rankCloseHandler = () => { ...@@ -345,10 +400,22 @@ const rankCloseHandler = () => {
<div class="img-close" @click="rankCloseHandler"></div> <div class="img-close" @click="rankCloseHandler"></div>
</div> </div>
</template> </template>
<LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus" <LoadingView
@touchGameRule="showGameRuleHandler" /> v-if="currentView === 'loading'"
<PlayingView v-else :image-urls="imageUrls" :tick="score" :rank="rank" :countdown-interval="countdownInterval" :image-urls="imageUrls"
:is-div-desc-visible="isDivDescVisible" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" /> :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" <!-- <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
@touchGameRule="showGameRuleHandler" /> @touchGameRule="showGameRuleHandler" />
...@@ -358,14 +425,23 @@ const rankCloseHandler = () => { ...@@ -358,14 +425,23 @@ const rankCloseHandler = () => {
: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
style="
text-align: center;
width: 100vw;
height: 100vh;
line-height: 30;
font-size: 20px;
"
>
请使用微信扫码进入游戏 请使用微信扫码进入游戏
</div> </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%;
...@@ -384,14 +460,14 @@ const rankCloseHandler = () => { ...@@ -384,14 +460,14 @@ const rankCloseHandler = () => {
font-size: 26pt; font-size: 26pt;
padding: 40px; padding: 40px;
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.4); transform: translateX(-50%) scale(1.4);
position: absolute; position: absolute;
bottom: -75px; bottom: -75px;
...@@ -402,7 +478,7 @@ const rankCloseHandler = () => { ...@@ -402,7 +478,7 @@ const rankCloseHandler = () => {
.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%;
...@@ -414,7 +490,7 @@ const rankCloseHandler = () => { ...@@ -414,7 +490,7 @@ const rankCloseHandler = () => {
font-size: 50px; font-size: 50px;
margin-top: 35px; margin-top: 35px;
font-weight: bold; font-weight: bold;
color: #AA0000; color: #aa0000;
letter-spacing: 2px; letter-spacing: 2px;
} }
...@@ -436,9 +512,14 @@ const rankCloseHandler = () => { ...@@ -436,9 +512,14 @@ const rankCloseHandler = () => {
} }
.row-1 { .row-1 {
color: #AA0000; color: #aa0000;
font-weight: bold; font-weight: bold;
background: linear-gradient(-84deg, #FFD396 0%, #FFF3D1 53%, #FFD59C 100%); background: linear-gradient(
-84deg,
#ffd396 0%,
#fff3d1 53%,
#ffd59c 100%
);
border-radius: 20px 20px 0px 0px; border-radius: 20px 20px 0px 0px;
} }
...@@ -446,11 +527,11 @@ const rankCloseHandler = () => { ...@@ -446,11 +527,11 @@ const rankCloseHandler = () => {
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 {
...@@ -459,7 +540,7 @@ const rankCloseHandler = () => { ...@@ -459,7 +540,7 @@ const rankCloseHandler = () => {
} }
.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;
} }
...@@ -484,7 +565,7 @@ const rankCloseHandler = () => { ...@@ -484,7 +565,7 @@ const rankCloseHandler = () => {
.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;
......
...@@ -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