Commit 82fe3ea6 authored by 董政锦's avatar 董政锦

Merge branch 'fix--game3-PC马的渲染' into 'main'

Fix game3 pc马的渲染 See merge request !13
parents c29b01c5 25744edd
......@@ -193,14 +193,18 @@ defineExpose({
}
.modal-container {
position: absolute;
inset: 0;
/* position:fixed + 显式 100vw/100vh 确保在 iOS Safari 中全屏蒙层,
避免因父级 overflow:hidden / transform:scale 导致 inset:0 计算异常 */
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.6);
/* pointer-events: none; */
}
.game-rule-fade-enter-active,
......
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { cssAssetUrl } from '@/commons/assets.ts'
import MobileStage from '@/components/MobileStage.vue'
import { useGameSocket, sendGameMessage, userJoinStatus, joinSharedRoom } from '@/composables/useGameSocket'
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';
type GameView = 'loading' | 'playing'
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { cssAssetUrl } from "@/commons/assets.ts";
import MobileStage from "@/components/MobileStage.vue";
import {
useGameSocket,
sendGameMessage,
userJoinStatus,
joinSharedRoom,
} from "@/composables/useGameSocket";
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 = {
rank: number
nickname: string
avatar: string
score: number | string
}
rank: number;
nickname: string;
avatar: string;
score: number | string;
};
const imageUrls: Record<string, string> = {
bg: cssAssetUrl('game3/bg.webp'),
bg3: cssAssetUrl('game3/bg3.webp'),
left: cssAssetUrl('game3/icon_zuo.webp'),
right: cssAssetUrl('game3/icon_you.webp'),
bg4: cssAssetUrl('game3/pic_paodao.webp'),
logo: cssAssetUrl('logo.png'),
loadingBg1: cssAssetUrl('game6/loadingbg1.png'),
loadingBg2: cssAssetUrl('game6/loadingbg2.png'),
close: cssAssetUrl('game1/close.png'),
clock: cssAssetUrl('game1/clock.png'),
bg2: cssAssetUrl('game6/bg2.png'),
hydt: cssAssetUrl('game3/title3.webp'),
rank: cssAssetUrl('game6/rank.png'),
moleShowSprite: cssAssetUrl('game6/mole_show.png'),
moleHideSprite: cssAssetUrl('game6/mole_hide.png'),
moleHideprite: cssAssetUrl('game6/mole_hit.png'),
rabbitFaintprite: cssAssetUrl('game6/rabbit_faint.png'),
rabbitHideprite: cssAssetUrl('game6/rabbit_hide.png'),
rabbitshowprite: cssAssetUrl('game6/rabbit_show.png'),
land: cssAssetUrl('game6/land.png'),
hammer: cssAssetUrl('game6/hammer.png'),
ranklist: cssAssetUrl('game6/ranklist.png'),
rule: cssAssetUrl('game1/rule.png')
}
const wechat = $getWechat()
const currentView = ref<GameView>('loading')
const countdownInterval = ref(60)
const isDivDescVisible = ref(false)
const isGameOver = ref(false)
const score = ref(0)
const rank = ref(0) //排名
const rankList = ref<RankPlayer[]>([])
bg: cssAssetUrl("game3/bg.webp"),
bg3: cssAssetUrl("game3/bg3.webp"),
left: cssAssetUrl("game3/icon_zuo.webp"),
right: cssAssetUrl("game3/icon_you.webp"),
bg4: cssAssetUrl("game3/pic_paodao.webp"),
logo: cssAssetUrl("logo.png"),
loadingBg1: cssAssetUrl("game6/loadingbg1.png"),
loadingBg2: cssAssetUrl("game6/loadingbg2.png"),
close: cssAssetUrl("game1/close.png"),
clock: cssAssetUrl("game1/clock.png"),
bg2: cssAssetUrl("game6/bg2.png"),
hydt: cssAssetUrl("game3/title3.webp"),
rank: cssAssetUrl("game6/rank.png"),
moleShowSprite: cssAssetUrl("game6/mole_show.png"),
moleHideSprite: cssAssetUrl("game6/mole_hide.png"),
moleHideprite: cssAssetUrl("game6/mole_hit.png"),
rabbitFaintprite: cssAssetUrl("game6/rabbit_faint.png"),
rabbitHideprite: cssAssetUrl("game6/rabbit_hide.png"),
rabbitshowprite: cssAssetUrl("game6/rabbit_show.png"),
land: cssAssetUrl("game6/land.png"),
hammer: cssAssetUrl("game6/hammer.png"),
ranklist: cssAssetUrl("game6/ranklist.png"),
rule: cssAssetUrl("game1/rule.png"),
};
const wechat = $getWechat();
const currentView = ref<GameView>("loading");
const countdownInterval = ref(60);
const isDivDescVisible = ref(false);
const isGameOver = ref(false);
const score = ref(0);
const rank = ref(0); //排名
const rankList = ref<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 list[index] ?? {
return (
list[index] ?? {
rank: index + 1,
nickname: '-',
avatar: '',
score: '-',
nickname: "-",
avatar: "",
score: "-",
}
})
})
);
});
});
const rankScore = computed<number | string>(() => {
const currentPlayer = rankList.value
.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 currentGu = ref(imageUrls.gu01)
const isGuPlaying = ref(false)
const token = ref(wechat?.token ?? '')
const nickname = ref(wechat?.nickname ?? '')
const avatar = ref(wechat?.avatar ?? '')
const showGameRule = ref(true)
const showGameRank = ref(false)
const isLocalMode = $is_run_local()
const mobileStageRef = ref<InstanceType<typeof MobileStage> | null>(null)
const _offSocketMessage = ref<(() => void) | undefined>()
let guFrameTimer: ReturnType<typeof window.setTimeout> | undefined
let gameCountdownTimer: ReturnType<typeof window.setTimeout> | undefined
const isGuPlaying = ref(false);
const token = ref(wechat?.token ?? "");
const nickname = ref(wechat?.nickname ?? "");
const avatar = ref(wechat?.avatar ?? "");
const showGameRule = ref(true);
const showGameRank = ref(false);
const isLocalMode = !IS_PRODUCTION && $is_run_local();
const mobileStageRef = ref<InstanceType<typeof MobileStage> | null>(null);
const _offSocketMessage = ref<(() => void) | undefined>();
let guFrameTimer: ReturnType<typeof window.setTimeout> | undefined;
let gameCountdownTimer: ReturnType<typeof window.setTimeout> | undefined;
const confirmRefresh = (event: BeforeUnloadEvent) => {
event.preventDefault()
event.returnValue = ''
}
event.preventDefault();
event.returnValue = "";
};
function setDivDescVisible(visible: boolean) {
isDivDescVisible.value = visible
isDivDescVisible.value = visible;
}
function stopGameCountdown() {
if (gameCountdownTimer) {
window.clearTimeout(gameCountdownTimer)
gameCountdownTimer = undefined
window.clearTimeout(gameCountdownTimer);
gameCountdownTimer = undefined;
}
}
function submitScore(save: boolean = false, currentTick = score.value) {
if (!wechat && !isLocalMode) return;
if (save) {
sendGameMessage('submit_score_save', {
if (isLocalMode && !wechat) {
console.log("[game3 本地模式] 提交最终分数:", currentTick);
return;
}
sendGameMessage("submit_score_save", {
score: currentTick,
wechat: wechat.token_origin,
item_num: 3,
nickname: wechat.nickname,
avatar: wechat.avatar,
rank: rank.value,
})
});
} else {
sendGameMessage('submit_score', { score: currentTick })
if (isLocalMode && !wechat) {
console.log("[game3 本地模式] 实时分数:", currentTick);
return;
}
sendGameMessage("submit_score", { score: currentTick });
}
}
function startGameCountdown(seconds = 60) {
stopGameCountdown()
countdownInterval.value = seconds
setDivDescVisible(true)
stopGameCountdown();
countdownInterval.value = seconds;
setDivDescVisible(true);
const tickCountdown = () => {
countdownInterval.value -= 1
countdownInterval.value -= 1;
if (countdownInterval.value <= 0) {
countdownInterval.value = 0
gameCountdownTimer = undefined
setDivDescVisible(false)
submitScore(true)
showGameOverRank()
return
countdownInterval.value = 0;
gameCountdownTimer = undefined;
setDivDescVisible(false);
submitScore(true);
showGameOverRank();
return;
}
gameCountdownTimer = window.setTimeout(tickCountdown, 1000)
}
gameCountdownTimer = window.setTimeout(tickCountdown, 1000);
};
gameCountdownTimer = window.setTimeout(tickCountdown, 1000)
gameCountdownTimer = window.setTimeout(tickCountdown, 1000);
}
function resetToLoadingView() {
stopGameCountdown()
stopGameCountdown();
if (guFrameTimer) {
window.clearTimeout(guFrameTimer)
guFrameTimer = undefined
}
score.value = 0
rank.value = 0
rankList.value = []
countdownInterval.value = 60
isGuPlaying.value = false
isGameOver.value = false
userJoinStatus.value = false
showGameRank.value = false
currentView.value = 'loading'
setDivDescVisible(false)
window.clearTimeout(guFrameTimer);
guFrameTimer = undefined;
}
score.value = 0;
rank.value = 0;
rankList.value = [];
countdownInterval.value = 60;
isGuPlaying.value = false;
isGameOver.value = false;
userJoinStatus.value = false;
showGameRank.value = false;
currentView.value = "loading";
setDivDescVisible(false);
}
function startGameView() {
stopGameCountdown()
score.value = 0
rank.value = 0
rankList.value = []
isGameOver.value = false
showGameRank.value = false
currentView.value = 'playing'
startGameCountdown()
stopGameCountdown();
score.value = 0;
rank.value = 0;
rankList.value = [];
isGameOver.value = false;
showGameRank.value = false;
currentView.value = "playing";
startGameCountdown();
}
function showGameOverRank() {
stopGameCountdown()
setDivDescVisible(false)
currentView.value = 'loading'
showGameRank.value = true
stopGameCountdown();
setDivDescVisible(false);
currentView.value = "loading";
showGameRank.value = true;
userJoinStatus.value = false;
}
......@@ -181,14 +200,14 @@ const scoreChangeHandler = (delta: number) => {
} else {
playGame3Music(2);
}
let nextTick = score.value + delta
if (nextTick < 0) nextTick = 0
score.value = nextTick
submitScore(false, nextTick)
}
let nextTick = score.value + delta;
if (nextTick < 0) nextTick = 0;
score.value = nextTick;
submitScore(false, nextTick);
};
if (wechat) {
const gameId = 'game3';
const gameId = "game3";
const { offSocketMessage } = useGameSocket({
gameId,
auth: {
......@@ -196,120 +215,135 @@ if (wechat) {
},
onConnect: () => {
window.setTimeout(() => {
const joined = joinSharedRoom(nickname.value, token.value, avatar.value, gameId)
}, 200)
const joined = joinSharedRoom(
nickname.value,
token.value,
avatar.value,
gameId,
);
}, 200);
},
onGameStart: async () => {
showGameRule.value = false;
await mobileStageRef.value?.startCountdown()
startGameView()
await mobileStageRef.value?.startCountdown();
startGameView();
},
onScoreSubmitted: (is_save, data) => {
if (!is_save) {//非保存状态下
const nextScore = Number(typeof data === 'object' ? data?.score : NaN)
const nextRank = Number(typeof data === 'object' ? data?.rank : data)
const remainingSeconds = Number(typeof data === 'object' ? data?.remainingSeconds : NaN)
if (Number.isFinite(nextScore) && nextScore >= 0) {
score.value = nextScore
}
if (!is_save) {
//非保存状态下
const nextRank = Number(typeof data === "object" ? data?.rank : data);
if (Number.isFinite(nextRank) && nextRank > 0) {
rank.value = nextRank
rank.value = nextRank;
}
if (Number.isFinite(remainingSeconds) && remainingSeconds >= 0) {
countdownInterval.value = remainingSeconds
}
} else if (typeof data === 'object' && data) {
} 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),
nickname: String(item?.nickname ?? ''),
avatar: String(item?.avatar ?? ''),
nickname: String(item?.nickname ?? ""),
avatar: String(item?.avatar ?? ""),
score: Number(item?.score ?? 0),
}))
}));
}
const nextRank = Number(data.rank)
const nextRank = Number(data.rank);
if (Number.isFinite(nextRank) && nextRank > 0) {
rank.value = nextRank
rank.value = nextRank;
}
}
return { code: 'MSYF', score: score.value }
},
onRescoreSubmitted: (_recount) => {
submitScore(true);
},
onGameRecover: (data) => {
const recoveredRank = Number(data?.rank)
const recoveredScore = Number(data?.score)
const recoveredSeconds = Number(data?.remainingSeconds)
rank.value = Number.isFinite(recoveredRank) && recoveredRank > 0 ? recoveredRank : 0
score.value = Number.isFinite(recoveredScore) && recoveredScore > 0 ? recoveredScore : 0
showGameRule.value = false
showGameRank.value = false
isGameOver.value = false
currentView.value = 'playing'
const recoveredRank = Number(data?.rank);
const recoveredScore = Number(data?.score);
const recoveredSeconds = Number(data?.remainingSeconds);
rank.value =
Number.isFinite(recoveredRank) && recoveredRank > 0 ? recoveredRank : 0;
score.value =
Number.isFinite(recoveredScore) && recoveredScore > 0
? recoveredScore
: 0;
showGameRule.value = false;
showGameRank.value = false;
isGameOver.value = false;
currentView.value = "playing";
startGameCountdown(
Number.isFinite(recoveredSeconds) && recoveredSeconds > 0 ? recoveredSeconds : 0,
)
Number.isFinite(recoveredSeconds) && recoveredSeconds > 0
? recoveredSeconds
: 0,
);
},
onRoomClosed: () => {
stopGameCountdown()
currentView.value = 'loading'
setDivDescVisible(false)
$toast('管理员关闭了游戏房间')
stopGameCountdown();
currentView.value = "loading";
setDivDescVisible(false);
$toast("管理员关闭了游戏房间");
},
onRoomBack: () => {
},
})
_offSocketMessage.value = offSocketMessage
onRoomBack: () => {},
});
_offSocketMessage.value = offSocketMessage;
}
onMounted(() => {
document.title = '互动游戏 - 马上有福'
document.title = "互动游戏 - 马上有福";
if (token.value) {
window.addEventListener('beforeunload', confirmRefresh)
window.addEventListener("beforeunload", confirmRefresh);
}
// 本地测试模式:自动模拟游戏开始(IS_PRODUCTION=false 时生效)
if (isLocalMode) {
showGameRule.value = false;
currentView.value = "playing";
startGameCountdown();
}
})
});
onBeforeUnmount(() => {
_offSocketMessage.value?.()
stopAllMusic()
_offSocketMessage.value?.();
stopAllMusic();
if (guFrameTimer) {
window.clearTimeout(guFrameTimer)
window.clearTimeout(guFrameTimer);
}
stopGameCountdown()
stopGameCountdown();
if (token.value) {
window.removeEventListener('beforeunload', confirmRefresh)
window.removeEventListener("beforeunload", confirmRefresh);
}
})
});
const showGameRuleHandler = () => {
showGameRule.value = !showGameRule.value;
}
};
const renderBG = () => {
if (currentView.value == 'playing') {
return `${imageUrls.bg2} center/cover`
if (currentView.value == "playing") {
return `${imageUrls.bg2} center/cover`;
}
return `${imageUrls.bg} center/cover`;
}
};
const rankCloseHandler = () => {
showGameRank.value = false
isGameOver.value = false
resetToLoadingView()
}
showGameRank.value = false;
isGameOver.value = false;
resetToLoadingView();
};
</script>
<template>
<MobileStage v-if="token" :showGameRule="showGameRule" :showGameRank="showGameRank" ref="mobileStageRef"
:background="`${renderBG()}`">
<MobileStage
v-if="token || isLocalMode"
:showGameRule="showGameRule"
:showGameRank="showGameRank"
ref="mobileStageRef"
:background="`${renderBG()}`"
>
<template #gameRule>
<div class="rule-container">
<div class="rule-txt-container">
<div class="rule-title">游戏规则</div>
<div class="rule-txt">
1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2 分;<br />
1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2
分;<br />
2. 限时闯关比拼得分,结束后按积分排名;<br />
3. 高分用户有机会领取新疆福彩定制周边;<br />
4. 禁止外挂作弊,违规账号取消成绩与领奖资格;<br /><br />
......@@ -329,23 +363,35 @@ const rankCloseHandler = () => {
<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="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
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-1">{{ rank > 0 ? rank : "未上榜" }}</div>
<div class="col-2">
<div class="avatar" :style="{ backgroundImage: `url(${avatar})` }"></div>
<div
class="avatar"
:style="{ backgroundImage: `url(${avatar})` }"
></div>
<div>{{ nickname }}</div>
</div>
<div class="col-3">{{ rankScore }}</div>
......@@ -354,10 +400,22 @@ const rankCloseHandler = () => {
<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"
/>
<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" />
......@@ -367,14 +425,23 @@ const rankCloseHandler = () => {
:user-nickname="nickname" :user-avatar="avatar"
@touch="touchHandler" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" /> -->
</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>
</template>
<style scoped>
.rule-container {
.rule-txt-container {
background: v-bind('imageUrls.rule') center / cover no-repeat;
background: v-bind("imageUrls.rule") center / cover no-repeat;
width: 692px;
height: 797px;
margin-top: -20%;
......@@ -389,14 +456,6 @@ const rankCloseHandler = () => {
letter-spacing: 4px;
}
.rule-txt {
font-size: 26pt;
padding: 40px;
margin-top: 10px;
color: #AA0000;
}
}
.rule-close {
width: 57px;
height: 57px;
......@@ -407,12 +466,23 @@ const rankCloseHandler = () => {
left: 100%;
margin-left: -48px;
}
}
.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 {
width: 747px;
height: 1084px;
background: v-bind('imageUrls.ranklist');
background: v-bind("imageUrls.ranklist");
transform: translate(-50%, -50%) scale(0.45);
left: 50%;
top: 50%;
......@@ -424,7 +494,7 @@ const rankCloseHandler = () => {
font-size: 50px;
margin-top: 35px;
font-weight: bold;
color: #AA0000;
color: #aa0000;
letter-spacing: 2px;
}
......@@ -446,9 +516,14 @@ const rankCloseHandler = () => {
}
.row-1 {
color: #AA0000;
color: #aa0000;
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;
}
......@@ -456,11 +531,11 @@ const rankCloseHandler = () => {
color: #633911;
.rank-row:nth-child(odd) {
background: #F2E2AE;
background: #f2e2ae;
}
.rank-row:nth-child(even) {
background: #FCEEBF;
background: #fceebf;
}
.col-1 {
......@@ -469,7 +544,7 @@ const rankCloseHandler = () => {
}
.row-3 {
background: linear-gradient(0deg, #F4B64C 0%, #F7C96F 100%);
background: linear-gradient(0deg, #f4b64c 0%, #f7c96f 100%);
border-radius: 0px 0px 20px 20px;
}
......
......@@ -26,8 +26,26 @@ const HORSE_BOTTOM_PCT = 19.11;
const ITEM_SIZE = 80;
const GAME_DURATION = 60;
const LEFT_LANE_X = 170;
const RIGHT_LANE_X = 470;
// 设计基准跑道 X 位置(750px 设计稿下的值,窄屏设备上会自动居中偏移)
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;
......@@ -63,20 +81,20 @@ preloadFrames(bomFrames);
const horseLane = ref<'left' | 'right'>('left');
const horseFrameIndex = ref(0);
const horseX = ref(LEFT_LANE_X);
const horseTargetX = ref(LEFT_LANE_X);
const horseX = ref(leftLaneX.value);
const horseTargetX = ref(leftLaneX.value);
const switchToLeft = () => {
if (horseLane.value !== 'left') {
horseLane.value = 'left';
horseTargetX.value = LEFT_LANE_X;
horseTargetX.value = leftLaneX.value;
}
};
const switchToRight = () => {
if (horseLane.value !== 'right') {
horseLane.value = 'right';
horseTargetX.value = RIGHT_LANE_X;
horseTargetX.value = rightLaneX.value;
}
};
......@@ -183,7 +201,7 @@ function generateNewItem() {
if (!props.isDivDescVisible) return;
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';
......@@ -244,7 +262,7 @@ const checkCollisions = () => {
for (const item of roadItems.value) {
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
const itemCenterX = itemX + ITEM_SIZE / 2;
const itemCenterY = item.y + ITEM_SIZE / 2;
......@@ -278,7 +296,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
const frameDuration = 1000 / totalFrames;
// 碰撞动画(大幅放大尺寸,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;
const tick = () => {
......@@ -297,7 +315,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
const effectId = ++scoreEffectIdSeq;
scoreEffects.value.push({
id: effectId,
x: x + ITEM_SIZE / 2,
x: x + ITEM_SIZE / 2 + 25,
y: y,
score: delta,
});
......@@ -377,6 +395,12 @@ const stopMainLoop = () => {
// ========== 生命周期 ==========
onMounted(() => {
window.addEventListener('keydown', handleKeydown);
window.addEventListener('resize', updateLanePositions);
window.visualViewport?.addEventListener('resize', updateLanePositions);
updateLanePositions();
// 初始化马匹在左侧跑道(使用计算后的实际 X 位置)
horseX.value = leftLaneX.value;
horseTargetX.value = leftLaneX.value;
nextTick(() => {
startRoadScroll();
startMainLoop();
......@@ -386,6 +410,8 @@ onMounted(() => {
onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeydown);
window.removeEventListener('resize', updateLanePositions);
window.visualViewport?.removeEventListener('resize', updateLanePositions);
stopRoadScroll();
stopMainLoop();
stopItemGen();
......@@ -405,6 +431,9 @@ watch(() => props.isDivDescVisible, (visible) => {
scoreEffects.value = [];
roadOffset.value = 0;
gameElapsed.value = 0;
updateLanePositions();
horseX.value = leftLaneX.value;
horseTargetX.value = leftLaneX.value;
startRoadScroll();
startMainLoop();
scheduleNextItem();
......@@ -582,18 +611,21 @@ watch(() => props.isDivDescVisible, (visible) => {
.road-item {
position: absolute;
width: 80px;
height: 80px;
/* width: 80px;
height: 80px; */
width: 77px;
height: 96px;
z-index: 5;
pointer-events: none;
}
.item-lane-left {
left: 170px;
/* 当 content-bg 宽于 750px 设计稿时,自动居中 750px 游戏区域 */
left: calc((var(--stage-viewport-width, 750px) - 750px) / 2 + 170px);
}
.item-lane-right {
left: 470px;
left: calc((var(--stage-viewport-width, 750px) - 750px) / 2 + 520px);
}
.effect-anim {
......
......@@ -55,36 +55,48 @@ const renderPlayers = (data: any) => {
}
const updateRankPlayers = (data: any) => {
if (!Array.isArray(data)) return
// 兼容多种数据格式:纯数组 / {list:[]} / {players:[]} / {data:[]}
let arr: any[] | null = null;
if (Array.isArray(data)) {
arr = data;
} else if (Array.isArray(data?.list)) {
arr = data.list;
} else if (Array.isArray(data?.players)) {
arr = data.players;
} else if (Array.isArray(data?.data)) {
arr = data.data;
}
rankPlayers.value = Array.from(
{ length: RANK_LIMIT },
(_, index) => data[index] ?? emptyRankPlayer(index),
)
}
if (!arr || arr.length === 0) {
console.warn('[game3] updateRankPlayers: 无法解析排行数据', data);
return;
}
const playerToRankPlayer = (player: any, index: number): Player => ({
telephone: player?.telephone ?? '',
userid: player?.userid ?? '',
wechat_id: player?.wechat_id || `empty-${index}`,
nickname: player?.nickname || '虚位以待',
avatar: player?.avatar || '',
score: player?.score ?? '-',
})
// 打印第一条原始数据的所有字段名,用于排查字段不匹配问题
console.log('[game3] updateRankPlayers: 收到排行数据', arr.length, '条, 第1条字段:', Object.keys(arr[0] || {}), ', 第1条值:', JSON.parse(JSON.stringify(arr[0])));
// 将 WS 数据标准化为 Player 格式(同时保留 userid / wechat_id)
// ⚠️ 当 WS 返回的数据没有 userid/openid/wechat_id 等ID字段时,用 avatar 作为唯一标识
const normalized = arr.map((item: any, i: number) => {
const hasId = item.userid || item.openid || item.user_id || item.wechat_id;
const avatar = item.avatar || item.headimgurl || '';
const nickname = item.nickname || item.name || '虚位以待';
const isRealPlayer = nickname && nickname !== '-' && nickname !== '虚位以待';
const wechat_id = hasId || (isRealPlayer && avatar ? `avt:${avatar}` : `empty-${i}`);
return {
...item,
userid: hasId || (isRealPlayer && avatar ? `avt:${avatar}` : ''),
wechat_id,
nickname,
avatar,
score: item.score ?? 0,
};
});
const seedRankPlayersFromLoading = () => {
rankPlayers.value = Array.from(
{ length: RANK_LIMIT },
(_, index) => {
const player = loadingPlayers.value[index]
if (!player?.userid && !player?.wechat_id) {
return emptyRankPlayer(index)
}
return playerToRankPlayer(player, index)
},
)
(_, index) => normalized[index] ?? emptyRankPlayer(index),
);
}
const resetRoundState = () => {
......@@ -107,7 +119,7 @@ const gotoRank1WithIntro = async () => {
if (isPlayingStartAnimation) return
isPlayingStartAnimation = true
seedRankPlayersFromLoading()
// ⚠️ 不要用 seedRankPlayersFromLoading,由 WS room_rank_result 驱动马匹数据
screen.value = 'rank1'
await nextTick()
await rank1Ref.value?.startIntro()
......@@ -129,8 +141,16 @@ function handleRoomState(data: any) {
gotoLoading()
break
case 2:
seedRankPlayersFromLoading()
// 重连/中途进房时游戏已在运行,直接进入赛马画面
if (isPlayingStartAnimation) break;
isPlayingStartAnimation = true;
screen.value = 'rank1'
console.log('[game3] handleRoomState: status=2, 直接启动赛马动画');
nextTick(() => {
rank1Ref.value?.startIntro().finally(() => {
isPlayingStartAnimation = false;
});
});
break
case 3:
screen.value = 'rank2'
......@@ -143,24 +163,58 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
onAny: (evt, payload) => {
// 兜底处理所有非标准事件,解析玩家实时分数(对齐 game4)
const data = payload?.data ?? payload;
if (!Array.isArray(data)) return;
// 尝试从 data 中提取数组
let arr: any[] | null = null;
if (Array.isArray(data)) {
arr = data;
} else if (Array.isArray(data?.list)) {
arr = data.list;
} else if (Array.isArray(data?.data)) {
arr = data.data;
}
if (arr && arr.length > 0) {
const boosts: Record<string, number> = {};
let hasBoost = false;
const normalized = arr.map((item: any, i: number) => {
const hasId = item.userid || item.openid || item.user_id || item.wechat_id;
const avatar = item.avatar || item.headimgurl || '';
const nickname = item.nickname || item.name || '虚位以待';
const isRealPlayer = nickname && nickname !== '-' && nickname !== '虚位以待';
const wechat_id = hasId || (isRealPlayer && avatar ? `avt:${avatar}` : `empty-${i}`);
return {
...item,
userid: hasId || (isRealPlayer && avatar ? `avt:${avatar}` : ''),
wechat_id,
};
});
data.forEach((item: any) => {
const id = item.userid || item.wechat_id || item.avatar || "";
if (!id) return;
const hasAnyPlayer = normalized.some((item: any) => {
const id = item.userid || item.wechat_id || '';
return !!id && !id.startsWith('empty-');
});
if (hasAnyPlayer) {
// 同时更新 rankPlayers(确保马匹列表有真实玩家)
rankPlayers.value = Array.from(
{ length: RANK_LIMIT },
(_, index) => normalized[index] ?? emptyRankPlayer(index),
);
}
normalized.forEach((item: any) => {
const id = item.userid || item.wechat_id || '';
if (!id || id.startsWith('empty-')) return;
const raw = item.score ?? item.boost ?? item.acceleration ?? item.speed ?? item.progress;
if (raw === undefined || raw === null) return;
const val = Number(raw);
if (isNaN(val)) return;
boosts[id] = Math.max(0, val);
hasBoost = true;
});
if (hasBoost) {
horseBoosts.value = { ...boosts };
if (Object.keys(boosts).length > 0) {
horseBoosts.value = { ...horseBoosts.value, ...boosts };
}
}
},
onRoomState: handleRoomState,
......@@ -232,5 +286,7 @@ watch(screen, (value) => {
<template>
<Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount"
@start="startGameWithMusic" @back="backHandler" />
<Rank1 v-else ref="rank1Ref" :rank-list="rankPlayers" :horse-boosts="horseBoosts" @start="startGameWithMusic" @next="nextRoundWithMusic" @back="backHandler" />
<Rank1 v-if="screen === 'rank1'" ref="rank1Ref" :rank-list="rankPlayers" :horse-boosts="horseBoosts"
@start="startGameWithMusic" @next="nextRoundWithMusic" @back="backHandler" />
<Rank2 v-if="screen === 'rank2'" :rank-list="rankPlayers" @next="nextRoundWithMusic" @back="backHandler" />
</template>
......@@ -44,6 +44,7 @@ const imageUrls = {
const HORSE_SIZE = 280;
const HORSE_START_X = -HORSE_SIZE;
const HORSE_END_X = 1920;
const HORSE_Y_OFFSET = 80; // 马匹整体向下偏移,补偿帧图片中马匹偏上的空白区域
const horseFrameConfigs = [
{ type: 1, dir: 'game3/ma1', prefix: '1ma_', startIndex: 24, count: 12 },
......@@ -68,11 +69,13 @@ interface Horse {
lane: number;
frameIndex: number;
frameTimer: number;
x: number;
targetX: number;
x: number; // 像素位置(用于渲染 translateX)
xPercent: number; // 进度百分比 0~100(对齐 game4 时间驱动模型)
boost: number; // 瞬时加速值,每帧衰减
nickname: string;
avatar: string;
wechat_id: string; // 用于匹配 WebSocket 实时排名数据
/** 稳定标识,优先 wechat_id 再 userid,用于匹配 WebSocket 数据 */
playerKey: string;
}
const horses = ref<Horse[]>([]);
......@@ -87,21 +90,63 @@ const emptyPlayer = (index: number): Player => ({
});
const list = ref<Player[]>(Array.from({ length: RANK_LIMIT }, (_, index) => emptyPlayer(index)));
const playerKey = (player: Player, index = 0) => player.userid || player.wechat_id || `empty-${index}`;
const updateRankList = (rankList: Player[]) => {
const next = Array.from({ length: RANK_LIMIT }, (_, index) => rankList[index] ?? emptyPlayer(index));
list.value = next;
};
// 玩家实时分数(合并 rankList + horseBoosts 双通道,对齐 game4 模式)
const liveScores = ref<Record<string, number>>({});
// ========== 瞬时加速脉冲(对齐 game4 detectScoreChanges) ==========
const transientBoosts = ref<Record<string, number>>({});
const prevScores: Record<string, number> = {};
/** 获取玩家的稳定标识(优先 wechat_id,其次 userid,最后用 avatar 回退) */
const getPlayerKey = (player: Player, index = 0): string => {
const key = player?.wechat_id || player?.userid || '';
if (key && !key.startsWith('empty-')) return key;
// 如果 WS 数据没有ID字段,用 avatar URL 作为唯一标识(每个用户头像各不相同)
if (player?.nickname && player.nickname !== '-' && player.nickname !== '虚位以待' && player?.avatar) {
return `avt:${player.avatar}`;
}
return `empty-${index}`;
};
/** 检测 rankList 中哪些玩家 score 增长了 → 触发瞬时加速脉冲 */
const detectScoreChanges = (players: Player[]) => {
if (!players || players.length === 0) return;
let hasNewBoost = false;
const updated = { ...transientBoosts.value };
players.forEach((p, index) => {
const key = getPlayerKey(p, index);
if (key.startsWith('empty-')) return;
const newScore = Number(p.score ?? 0);
if (isNaN(newScore)) return;
const prev = prevScores[key] ?? 0;
if (newScore > prev) {
const current = updated[key] || 0;
updated[key] = Math.min(current + BOOST_PULSE, MAX_BOOST);
hasNewBoost = true;
console.log(`[game3 Rank1View] 🚀 ${p.nickname} score ${prev}${newScore}, boost: ${updated[key].toFixed(1)}`);
}
prevScores[key] = newScore;
});
if (hasNewBoost) {
transientBoosts.value = updated;
}
};
watch(
() => props.rankList,
(rankList) => {
console.log('[game3 Rank1View] websocket rankList 数据:', JSON.parse(JSON.stringify(rankList)));
console.log('[game3 Rank1View] websocket rankList 数据:', JSON.parse(JSON.stringify(rankList?.slice(0, 3))));
updateRankList(rankList);
// 检测积分增长 → 触发瞬时加速脉冲(对齐 game4)
detectScoreChanges(rankList);
// 游戏进行中,实时更新马匹玩家列表(处理加入/离开)
if (isGameRunning.value) {
updateHorses(rankList);
......@@ -110,32 +155,63 @@ watch(
{ immediate: true, deep: true },
);
// 直接监听 list,确保游戏运行时任何 list 变化都触发马匹刷新
watch(
list,
(newList) => {
const validCount = newList.filter(p => getPlayerKey(p).length > 0 && !getPlayerKey(p).startsWith('empty-')).length;
console.log('[game3 Rank1View] list 变化, 有效玩家数:', validCount, 'isGameRunning:', isGameRunning.value);
if (isGameRunning.value && validCount > 0) {
updateHorses(newList);
}
},
{ deep: true },
);
watch(
() => props.horseBoosts,
(boosts) => {
if (!boosts || Object.keys(boosts).length === 0) return;
const merged = { ...liveScores.value };
for (const [id, score] of Object.entries(boosts)) {
// 取较大的值,因为不同通道可能收到不同时间点的数据
if (score > (merged[id] ?? 0)) {
merged[id] = score;
let hasNew = false;
const updated = { ...transientBoosts.value };
Object.entries(boosts).forEach(([id, val]) => {
const score = Number(val ?? 0);
if (score > 0) {
const current = updated[id] || 0;
updated[id] = Math.min(current + BOOST_PULSE, MAX_BOOST);
hasNew = true;
}
});
if (hasNew) {
transientBoosts.value = updated;
console.log('[game3 Rank1View] horseBoosts 触发加速:', JSON.parse(JSON.stringify(transientBoosts.value)));
}
liveScores.value = merged;
},
{ deep: true },
);
// ========== 马匹动画循环 (RAF 驱动帧切换 + 分数驱动位移) ==========
// ========== 速度基准(对齐 game4 时间驱动模型) ==========
// 基础速度大幅下调,确保马匹在 60s 游戏时间内不会过早跑出屏幕
const TOTAL_DISTANCE_PX = HORSE_END_X - HORSE_START_X; // 2200px
const BASE_DURATION_SEC = 150; // 基准时长 150s,即无加速时跑完全程需 150s
const BASE_SPEED_PX_PER_SEC = TOTAL_DISTANCE_PX / BASE_DURATION_SEC; // ≈ 14.7px/s
// 瞬时加速脉冲机制(对齐 game4)
const BOOST_PULSE = 0.3; // 每次 score 增长对应的额外速度倍率
const MAX_BOOST = 3.0; // 最大加速倍率
const BOOST_DECAY = 0.992; // 每帧衰减系数(60fps 约 3.6s 到 60%)
// ========== 马匹动画循环 (RAF 驱动:时间驱动位移 + 帧动画) ==========
let horseRafId: number | undefined;
let horseLastTs = 0;
const HORSE_FRAME_TICK_MS = 48;
// 慢速 lerp,分数变化时马匹平滑过渡(约 2.5s 拉到位)
const HORSE_LERP_SPEED = 2.5;
const startHorseLoop = () => {
stopHorseLoop();
horseLastTs = 0;
let logCounter = 0;
const tick = (ts: number) => {
if (!isGameRunning.value) {
......@@ -147,36 +223,29 @@ const startHorseLoop = () => {
horseLastTs = ts;
if (horses.value.length === 0) {
if (logCounter++ % 60 === 0) {
console.log('[game3 Rank1View] RAF: horses 为空, gameStarted:', gameStarted.value, 'isGameRunning:', isGameRunning.value);
}
horseRafId = requestAnimationFrame(tick);
return;
}
// 优先用 liveScores(onAny 实时数据),回退到 list 分数
const getScore = (wechatId: string): number => {
const liveScore = liveScores.value[wechatId];
if (typeof liveScore === 'number' && liveScore > 0) return liveScore;
const player = list.value.find(p => p.wechat_id === wechatId);
return typeof player?.score === 'number' ? player.score : 0;
};
const newBoosts: Record<string, number> = {};
// 计算当前最高分
let maxScore = 0;
for (const horse of horses.value) {
const s = getScore(horse.wechat_id);
if (s > maxScore) maxScore = s;
}
if (maxScore <= 0) maxScore = 1;
if (horse.xPercent >= 100) continue;
for (const horse of horses.value) {
const score = getScore(horse.wechat_id);
// 从 transientBoosts 读取实时加速值
const boost = transientBoosts.value[horse.playerKey] || 0;
horse.boost = boost;
// 进度 = 最小 5% + 根据分数占比最大 95%
const progress = 0.05 + (score / maxScore) * 0.95;
horse.targetX = HORSE_START_X + (HORSE_END_X - HORSE_START_X) * progress;
// 基础速度 + 加速 → 推进百分比
const effectiveSpeed = BASE_SPEED_PX_PER_SEC * (1 + boost);
const deltaPx = effectiveSpeed * (dt / 1000);
horse.xPercent = Math.min(100, horse.xPercent + (deltaPx / TOTAL_DISTANCE_PX) * 100);
// 平滑 lerp,分数变化不跳变
const lerpFactor = 1 - Math.pow(0.001, dt / (1000 * HORSE_LERP_SPEED));
horse.x += (horse.targetX - horse.x) * lerpFactor;
// 像素位置 = 起始位置 + 百分比 * 总距离
horse.x = HORSE_START_X + (horse.xPercent / 100) * TOTAL_DISTANCE_PX;
// 帧动画
const fc = horseFrameCount[horse.horseType] ?? 12;
......@@ -185,8 +254,15 @@ const startHorseLoop = () => {
horse.frameTimer -= HORSE_FRAME_TICK_MS;
horse.frameIndex = (horse.frameIndex + 1) % fc;
}
// 加速衰减
const decayed = boost * Math.pow(BOOST_DECAY, dt / (1000 / 60));
if (decayed > 0.001) {
newBoosts[horse.playerKey] = decayed;
}
}
transientBoosts.value = newBoosts;
horseRafId = requestAnimationFrame(tick);
};
horseRafId = requestAnimationFrame(tick);
......@@ -204,23 +280,27 @@ const LANE_TOP_POSITIONS = [480, 640, 800];
const updateHorses = (players: Player[]) => {
const validPlayers = players.filter(p => {
if (!p || !p.wechat_id) return false;
if (p.wechat_id.startsWith('empty-')) return false;
const key = getPlayerKey(p);
if (!key || key.startsWith('empty-')) return false;
if (p.nickname === '-' || p.nickname === '虚位以待') return false;
return true;
});
console.log('[game3 Rank1View] updateHorses: 输入', players.length, '条, 有效', validPlayers.length, '条',
validPlayers.slice(0, 3).map(p => ({ key: getPlayerKey(p), name: p.nickname, score: p.score })));
if (validPlayers.length === 0) {
console.warn('[game3 Rank1View] updateHorses: 无有效玩家,horses 清空');
horses.value = [];
return;
}
// 保留已有马匹的 x 位置,防止重新生成时位置跳变
const existingMap = new Map(horses.value.map(h => [h.wechat_id, h]));
// 保留已有马匹的 x/xPercent/boost,防止重新生成时位置跳变
const existingMap = new Map(horses.value.map(h => [h.playerKey, h]));
horses.value = validPlayers.map((player, index) => {
const wid = player.wechat_id || '';
const existing = existingMap.get(wid);
const key = getPlayerKey(player, index);
const existing = existingMap.get(key);
return {
id: existing?.id || `horse-${++horseIdCounter}`,
horseType: HORSE_TYPES[index % HORSE_TYPES.length]!,
......@@ -228,12 +308,15 @@ const updateHorses = (players: Player[]) => {
frameIndex: existing?.frameIndex ?? Math.floor(Math.random() * (horseFrameCount[HORSE_TYPES[index % HORSE_TYPES.length]!] ?? 12)),
frameTimer: existing?.frameTimer ?? 0,
x: existing?.x ?? HORSE_START_X,
targetX: existing?.targetX ?? HORSE_START_X,
xPercent: existing?.xPercent ?? 0,
boost: existing?.boost ?? 0,
nickname: player.nickname || '',
avatar: player.avatar || '',
wechat_id: wid,
playerKey: key,
};
});
console.log('[game3 Rank1View] updateHorses: 生成', horses.value.length, '匹马');
};
// ========== 预加载马匹帧 ==========
......@@ -248,17 +331,23 @@ const preloadHorseFrames = () => {
// ========== 游戏流程 ==========
const startIntro = async () => {
console.log('[game3 Rank1View] startIntro 开始');
showResult.value = false;
gameStarted.value = false;
isGameRunning.value = false;
horses.value = [];
horseIdCounter = 0;
// 重置加速状态
transientBoosts.value = {};
Object.keys(prevScores).forEach(k => delete prevScores[k]);
stopApplauseMusic();
preloadHorseFrames();
const countdownPromise = designStage.value?.startCountdown(3, 60);
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] startIntro: 3.5s 后, list 有效玩家数:', validCount);
gameStarted.value = true;
isGameRunning.value = true;
updateHorses(list.value);
......@@ -300,27 +389,27 @@ onMounted(() => {
if ($is_run_local()) {
// 本地模式:用模拟玩家填充 list,驱动马匹渲染
list.value = [
{ wechat_id: 'local-1', nickname: '骑手一', avatar: '', score: 45 },
{ wechat_id: 'local-2', nickname: '骑手二', avatar: '', score: 38 },
{ wechat_id: 'local-3', nickname: '骑手三', avatar: '', score: 30 },
{ wechat_id: 'local-4', nickname: '骑手四', avatar: '', score: 22 },
{ wechat_id: 'local-5', nickname: '骑手五', avatar: '', score: 15 },
{ userid: 'local-1', nickname: '骑手一', avatar: '', score: 45 },
{ userid: 'local-2', nickname: '骑手二', avatar: '', score: 38 },
{ userid: 'local-3', nickname: '骑手三', avatar: '', score: 30 },
{ userid: 'local-4', nickname: '骑手四', avatar: '', score: 22 },
{ userid: 'local-5', nickname: '骑手五', avatar: '', score: 15 },
];
gameStarted.value = true;
isGameRunning.value = true;
updateHorses(list.value);
startHorseLoop();
// 模拟分数变化
// 模拟分数变化 → 驱动 detectScoreChanges
let simTick = 0;
const simInterval = setInterval(() => {
if (!isGameRunning.value) { clearInterval(simInterval); return; }
simTick++;
// 随机涨分
for (const p of list.value) {
if (typeof p.score === 'number') {
p.score += Math.floor(Math.random() * 3);
}
}
detectScoreChanges(list.value);
}, 1500);
}
});
......@@ -351,7 +440,7 @@ onUnmounted(() => {
:key="horse.id"
class="horse-wrapper"
:style="{
top: `${LANE_TOP_POSITIONS[horse.lane]}px`,
top: `${(LANE_TOP_POSITIONS[horse.lane] ?? 560) + HORSE_Y_OFFSET}px`,
transform: `translateX(${horse.x}px)`,
}"
>
......@@ -556,7 +645,7 @@ onUnmounted(() => {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: 4px;
padding-left: 6px;
box-sizing: border-box;
}
......
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