Commit 7405af15 authored by 陈冲's avatar 陈冲
parents 28863a1a 82fe3ea6
...@@ -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,
......
This diff is collapsed.
...@@ -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 {
......
...@@ -55,36 +55,48 @@ const renderPlayers = (data: any) => { ...@@ -55,36 +55,48 @@ const renderPlayers = (data: any) => {
} }
const updateRankPlayers = (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( if (!arr || arr.length === 0) {
{ length: RANK_LIMIT }, console.warn('[game3] updateRankPlayers: 无法解析排行数据', data);
(_, index) => data[index] ?? emptyRankPlayer(index), return;
) }
}
const playerToRankPlayer = (player: any, index: number): Player => ({ // 打印第一条原始数据的所有字段名,用于排查字段不匹配问题
telephone: player?.telephone ?? '', console.log('[game3] updateRankPlayers: 收到排行数据', arr.length, '条, 第1条字段:', Object.keys(arr[0] || {}), ', 第1条值:', JSON.parse(JSON.stringify(arr[0])));
userid: player?.userid ?? '',
wechat_id: player?.wechat_id || `empty-${index}`, // 将 WS 数据标准化为 Player 格式(同时保留 userid / wechat_id)
nickname: player?.nickname || '虚位以待', // ⚠️ 当 WS 返回的数据没有 userid/openid/wechat_id 等ID字段时,用 avatar 作为唯一标识
avatar: player?.avatar || '', const normalized = arr.map((item: any, i: number) => {
score: player?.score ?? '-', 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( rankPlayers.value = Array.from(
{ length: RANK_LIMIT }, { length: RANK_LIMIT },
(_, index) => { (_, index) => normalized[index] ?? emptyRankPlayer(index),
const player = loadingPlayers.value[index] );
if (!player?.userid && !player?.wechat_id) {
return emptyRankPlayer(index)
}
return playerToRankPlayer(player, index)
},
)
} }
const resetRoundState = () => { const resetRoundState = () => {
...@@ -107,7 +119,7 @@ const gotoRank1WithIntro = async () => { ...@@ -107,7 +119,7 @@ const gotoRank1WithIntro = async () => {
if (isPlayingStartAnimation) return if (isPlayingStartAnimation) return
isPlayingStartAnimation = true isPlayingStartAnimation = true
seedRankPlayersFromLoading() // ⚠️ 不要用 seedRankPlayersFromLoading,由 WS room_rank_result 驱动马匹数据
screen.value = 'rank1' screen.value = 'rank1'
await nextTick() await nextTick()
await rank1Ref.value?.startIntro() await rank1Ref.value?.startIntro()
...@@ -129,8 +141,16 @@ function handleRoomState(data: any) { ...@@ -129,8 +141,16 @@ function handleRoomState(data: any) {
gotoLoading() gotoLoading()
break break
case 2: case 2:
seedRankPlayersFromLoading() // 重连/中途进房时游戏已在运行,直接进入赛马画面
if (isPlayingStartAnimation) break;
isPlayingStartAnimation = true;
screen.value = 'rank1' screen.value = 'rank1'
console.log('[game3] handleRoomState: status=2, 直接启动赛马动画');
nextTick(() => {
rank1Ref.value?.startIntro().finally(() => {
isPlayingStartAnimation = false;
});
});
break break
case 3: case 3:
screen.value = 'rank2' screen.value = 'rank2'
...@@ -143,24 +163,58 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({ ...@@ -143,24 +163,58 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
onAny: (evt, payload) => { onAny: (evt, payload) => {
// 兜底处理所有非标准事件,解析玩家实时分数(对齐 game4) // 兜底处理所有非标准事件,解析玩家实时分数(对齐 game4)
const data = payload?.data ?? payload; const data = payload?.data ?? payload;
if (!Array.isArray(data)) return;
// 尝试从 data 中提取数组
const boosts: Record<string, number> = {}; let arr: any[] | null = null;
let hasBoost = false; if (Array.isArray(data)) {
arr = data;
data.forEach((item: any) => { } else if (Array.isArray(data?.list)) {
const id = item.userid || item.wechat_id || item.avatar || ""; arr = data.list;
if (!id) return; } else if (Array.isArray(data?.data)) {
const raw = item.score ?? item.boost ?? item.acceleration ?? item.speed ?? item.progress; arr = data.data;
if (raw === undefined || raw === null) return; }
const val = Number(raw);
if (isNaN(val)) return; if (arr && arr.length > 0) {
boosts[id] = Math.max(0, val); const boosts: Record<string, number> = {};
hasBoost = true; 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 || '';
if (hasBoost) { const nickname = item.nickname || item.name || '虚位以待';
horseBoosts.value = { ...boosts }; 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,
};
});
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);
});
if (Object.keys(boosts).length > 0) {
horseBoosts.value = { ...horseBoosts.value, ...boosts };
}
} }
}, },
onRoomState: handleRoomState, onRoomState: handleRoomState,
...@@ -232,5 +286,7 @@ watch(screen, (value) => { ...@@ -232,5 +286,7 @@ watch(screen, (value) => {
<template> <template>
<Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount" <Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount"
@start="startGameWithMusic" @back="backHandler" /> @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> </template>
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