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

fix: 修复 game3 PC 端马的数据还是渲染模拟数据问题;

parent 415fbd0f
...@@ -55,6 +55,16 @@ const bomFrames = Array.from({ length: BOM_FRAME_COUNT }, (_, i) => ...@@ -55,6 +55,16 @@ const bomFrames = Array.from({ length: BOM_FRAME_COUNT }, (_, i) =>
const fudaiUrl = assetUrl('game3/icon_fudai.webp'); const fudaiUrl = assetUrl('game3/icon_fudai.webp');
const dileiUrl = assetUrl('game3/icon_dilei.webp'); const dileiUrl = assetUrl('game3/icon_dilei.webp');
// ========== 预加载碰撞动画帧(解决 4G 慢网下动画图片未加载的问题) ==========
const preloadFrames = (urls: string[]) => {
for (const url of urls) {
const img = new Image();
img.src = url;
}
};
preloadFrames(jinbiFrames);
preloadFrames(bomFrames);
// ========== 马状态 ========== // ========== 马状态 ==========
const horseLane = ref<'left' | 'right'>('left'); const horseLane = ref<'left' | 'right'>('left');
const horseFrameIndex = ref(0); const horseFrameIndex = ref(0);
...@@ -140,6 +150,7 @@ interface RoadItem { ...@@ -140,6 +150,7 @@ interface RoadItem {
lane: 'left' | 'right'; lane: 'left' | 'right';
y: number; y: number;
collected: boolean; collected: boolean;
colliding: boolean; // 防止同一帧内重复触发碰撞特效
animFrame: number; animFrame: number;
animTimer: ReturnType<typeof setTimeout> | undefined; animTimer: ReturnType<typeof setTimeout> | undefined;
} }
...@@ -198,6 +209,7 @@ function generateNewItem() { ...@@ -198,6 +209,7 @@ function generateNewItem() {
lane, lane,
y: -ITEM_SIZE, y: -ITEM_SIZE,
collected: false, collected: false,
colliding: false,
animFrame: 0, animFrame: 0,
animTimer: undefined, animTimer: undefined,
}; };
...@@ -236,7 +248,7 @@ const checkCollisions = () => { ...@@ -236,7 +248,7 @@ const checkCollisions = () => {
const horseRadius = HORSE_W * 0.35; const horseRadius = HORSE_W * 0.35;
for (const item of roadItems.value) { for (const item of roadItems.value) {
if (item.collected) continue; if (item.collected || item.colliding) continue;
const itemX = item.lane === 'left' ? LEFT_LANE_X : RIGHT_LANE_X; const itemX = item.lane === 'left' ? LEFT_LANE_X : RIGHT_LANE_X;
// item.y 已经是屏幕坐标(content-bg 内),无需加 roadOffset // item.y 已经是屏幕坐标(content-bg 内),无需加 roadOffset
...@@ -246,8 +258,13 @@ const checkCollisions = () => { ...@@ -246,8 +258,13 @@ const checkCollisions = () => {
const dist = Math.hypot(horseCenterX - itemCenterX, horseTopCenterY - itemCenterY); const dist = Math.hypot(horseCenterX - itemCenterX, horseTopCenterY - itemCenterY);
if (dist < horseRadius + ITEM_SIZE * 0.35) { if (dist < horseRadius + ITEM_SIZE * 0.35) {
item.collected = true; // 立即标记碰撞中,防止后续帧重复触发
item.colliding = true;
// 先启动碰撞特效动画,延迟隐藏道具让动画和道具短暂重叠过渡
playCollectEffect(item, itemX, item.y); playCollectEffect(item, itemX, item.y);
setTimeout(() => {
item.collected = true;
}, 100);
} }
} }
}; };
...@@ -268,8 +285,8 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => { ...@@ -268,8 +285,8 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
const totalFrames = isCoin ? JINBI_FRAME_COUNT : BOM_FRAME_COUNT; const totalFrames = isCoin ? JINBI_FRAME_COUNT : BOM_FRAME_COUNT;
const frameDuration = 1000 / totalFrames; const frameDuration = 1000 / totalFrames;
// 碰撞动画(大幅放大尺寸) // 碰撞动画(大幅放大尺寸,x 往右偏移让动画居中于视觉碰撞点
effectAnims.value.push({ id: animId, kind: item.kind, frame: 0, x, y }); effectAnims.value.push({ id: animId, kind: item.kind, frame: 0, x: x + 40, y });
let frameIdx = 0; let frameIdx = 0;
const tick = () => { const tick = () => {
......
...@@ -36,6 +36,7 @@ const screen = ref<GameScreen>('loading') ...@@ -36,6 +36,7 @@ const screen = ref<GameScreen>('loading')
const loadingPlayers = ref<any[]>(Array.from({ length: PLAYER_LIMIT }, emptyLoadingPlayer)) const loadingPlayers = ref<any[]>(Array.from({ length: PLAYER_LIMIT }, emptyLoadingPlayer))
const playerCount = ref(0) const playerCount = ref(0)
const rankPlayers = ref<Player[]>(Array.from({ length: RANK_LIMIT }, (_, index) => emptyRankPlayer(index))) const rankPlayers = ref<Player[]>(Array.from({ length: RANK_LIMIT }, (_, index) => emptyRankPlayer(index)))
const horseBoosts = ref<Record<string, number>>({});
const rank1Ref = ref<InstanceType<typeof Rank1> | null>(null) const rank1Ref = ref<InstanceType<typeof Rank1> | null>(null)
let isPlayingStartAnimation = false let isPlayingStartAnimation = false
...@@ -90,6 +91,7 @@ const resetRoundState = () => { ...@@ -90,6 +91,7 @@ const resetRoundState = () => {
playerCount.value = 0 playerCount.value = 0
loadingPlayers.value = Array.from({ length: PLAYER_LIMIT }, emptyLoadingPlayer) loadingPlayers.value = Array.from({ length: PLAYER_LIMIT }, emptyLoadingPlayer)
rankPlayers.value = Array.from({ length: RANK_LIMIT }, (_, index) => emptyRankPlayer(index)) rankPlayers.value = Array.from({ length: RANK_LIMIT }, (_, index) => emptyRankPlayer(index))
horseBoosts.value = {}
isPlayingStartAnimation = false isPlayingStartAnimation = false
} }
...@@ -138,6 +140,29 @@ function handleRoomState(data: any) { ...@@ -138,6 +140,29 @@ function handleRoomState(data: any) {
const gameId='game3' const gameId='game3'
const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
gameId, gameId,
onAny: (evt, payload) => {
// 兜底处理所有非标准事件,解析玩家实时分数(对齐 game4)
const data = payload?.data ?? payload;
if (!Array.isArray(data)) return;
const boosts: Record<string, number> = {};
let hasBoost = false;
data.forEach((item: any) => {
const id = item.userid || item.wechat_id || item.avatar || "";
if (!id) 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 };
}
},
onRoomState: handleRoomState, onRoomState: handleRoomState,
onRoomJoin: (data) => { onRoomJoin: (data) => {
renderPlayers(data) renderPlayers(data)
...@@ -207,5 +232,5 @@ watch(screen, (value) => { ...@@ -207,5 +232,5 @@ 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" @start="startGameWithMusic" @next="nextRoundWithMusic" @back="backHandler" /> <Rank1 v-else ref="rank1Ref" :rank-list="rankPlayers" :horse-boosts="horseBoosts" @start="startGameWithMusic" @next="nextRoundWithMusic" @back="backHandler" />
</template> </template>
...@@ -8,6 +8,7 @@ import { playApplauseMusic, stopApplauseMusic } from '@/commons/music' ...@@ -8,6 +8,7 @@ import { playApplauseMusic, stopApplauseMusic } from '@/commons/music'
const props = defineProps<{ const props = defineProps<{
rankList: Player[]; rankList: Player[];
horseBoosts: Record<string, number>;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
...@@ -67,11 +68,11 @@ interface Horse { ...@@ -67,11 +68,11 @@ interface Horse {
lane: number; lane: number;
frameIndex: number; frameIndex: number;
frameTimer: number; frameTimer: number;
startTime: number;
durationMs: number;
x: number; x: number;
targetX: number;
nickname: string; nickname: string;
avatar: string; avatar: string;
wechat_id: string; // 用于匹配 WebSocket 实时排名数据
} }
const horses = ref<Horse[]>([]); const horses = ref<Horse[]>([]);
...@@ -93,19 +94,44 @@ const updateRankList = (rankList: Player[]) => { ...@@ -93,19 +94,44 @@ const updateRankList = (rankList: Player[]) => {
list.value = next; list.value = next;
}; };
// 玩家实时分数(合并 rankList + horseBoosts 双通道,对齐 game4 模式)
const liveScores = ref<Record<string, number>>({});
watch( watch(
() => props.rankList, () => props.rankList,
(rankList) => { (rankList) => {
console.log('[game3 Rank1View] websocket rankList 数据:', JSON.parse(JSON.stringify(rankList))); console.log('[game3 Rank1View] websocket rankList 数据:', JSON.parse(JSON.stringify(rankList)));
updateRankList(rankList); updateRankList(rankList);
// 游戏进行中,实时更新马匹玩家列表(处理加入/离开)
if (isGameRunning.value) {
updateHorses(rankList);
}
}, },
{ immediate: true, deep: true }, { immediate: true, deep: true },
); );
// ========== 马匹动画循环 (RAF 驱动帧切换 + 位移) ========== 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;
}
}
liveScores.value = merged;
},
{ deep: true },
);
// ========== 马匹动画循环 (RAF 驱动帧切换 + 分数驱动位移) ==========
let horseRafId: number | undefined; let horseRafId: number | undefined;
let horseLastTs = 0; let horseLastTs = 0;
const HORSE_FRAME_TICK_MS = 48; const HORSE_FRAME_TICK_MS = 48;
// 慢速 lerp,分数变化时马匹平滑过渡(约 2.5s 拉到位)
const HORSE_LERP_SPEED = 2.5;
const startHorseLoop = () => { const startHorseLoop = () => {
stopHorseLoop(); stopHorseLoop();
...@@ -120,13 +146,39 @@ const startHorseLoop = () => { ...@@ -120,13 +146,39 @@ const startHorseLoop = () => {
const dt = Math.min(ts - horseLastTs, 200); const dt = Math.min(ts - horseLastTs, 200);
horseLastTs = ts; horseLastTs = ts;
if (horses.value.length === 0) {
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;
};
// 计算当前最高分
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;
for (const horse of horses.value) { for (const horse of horses.value) {
if (ts < horse.startTime) continue; const score = getScore(horse.wechat_id);
// 进度 = 最小 5% + 根据分数占比最大 95%
const progress = 0.05 + (score / maxScore) * 0.95;
horse.targetX = HORSE_START_X + (HORSE_END_X - HORSE_START_X) * progress;
const elapsed = ts - horse.startTime; // 平滑 lerp,分数变化不跳变
const progress = Math.min(elapsed / horse.durationMs, 1); const lerpFactor = 1 - Math.pow(0.001, dt / (1000 * HORSE_LERP_SPEED));
horse.x = HORSE_START_X + (HORSE_END_X - HORSE_START_X) * progress; horse.x += (horse.targetX - horse.x) * lerpFactor;
// 帧动画
const fc = horseFrameCount[horse.horseType] ?? 12; const fc = horseFrameCount[horse.horseType] ?? 12;
horse.frameTimer += dt; horse.frameTimer += dt;
while (horse.frameTimer >= HORSE_FRAME_TICK_MS) { while (horse.frameTimer >= HORSE_FRAME_TICK_MS) {
...@@ -147,36 +199,41 @@ const stopHorseLoop = () => { ...@@ -147,36 +199,41 @@ const stopHorseLoop = () => {
} }
}; };
// ========== 生成马匹 ========== // ========== 生成/更新马匹(对齐 game4 updateHorses 模式,保留已有马匹进度) ==========
const LANE_TOP_POSITIONS = [380, 530, 680]; const LANE_TOP_POSITIONS = [480, 640, 800];
const generateHorses = () => { const updateHorses = (players: Player[]) => {
const validPlayers = list.value.filter(p => !p.wechat_id?.startsWith('empty-') && p.nickname !== '-'); const validPlayers = players.filter(p => {
const count = Math.max(validPlayers.length, 3); if (!p || !p.wechat_id) return false;
const newHorses: Horse[] = []; if (p.wechat_id.startsWith('empty-')) return false;
const now = performance.now(); if (p.nickname === '-' || p.nickname === '虚位以待') return false;
return true;
for (let i = 0; i < count; i++) { });
const type = HORSE_TYPES[i % HORSE_TYPES.length]!;
const lane = i % LANE_TOP_POSITIONS.length; if (validPlayers.length === 0) {
const delayMs = i * 2500 + Math.random() * 1000; horses.value = [];
const durationMs = (30 + Math.random() * 30) * 1000; return;
const player = validPlayers[i] ?? { nickname: `骑手${i + 1}`, avatar: '' };
newHorses.push({
id: `horse-${++horseIdCounter}`,
horseType: type,
lane,
frameIndex: Math.floor(Math.random() * (horseFrameCount[type] ?? 12)),
frameTimer: 0,
startTime: now + delayMs,
durationMs,
x: HORSE_START_X,
nickname: player.nickname,
avatar: player.avatar || '',
});
} }
horses.value = newHorses; // 保留已有马匹的 x 位置,防止重新生成时位置跳变
const existingMap = new Map(horses.value.map(h => [h.wechat_id, h]));
horses.value = validPlayers.map((player, index) => {
const wid = player.wechat_id || '';
const existing = existingMap.get(wid);
return {
id: existing?.id || `horse-${++horseIdCounter}`,
horseType: HORSE_TYPES[index % HORSE_TYPES.length]!,
lane: index % LANE_TOP_POSITIONS.length,
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,
nickname: player.nickname || '',
avatar: player.avatar || '',
wechat_id: wid,
};
});
}; };
// ========== 预加载马匹帧 ========== // ========== 预加载马匹帧 ==========
...@@ -204,7 +261,7 @@ const startIntro = async () => { ...@@ -204,7 +261,7 @@ const startIntro = async () => {
gameStarted.value = true; gameStarted.value = true;
isGameRunning.value = true; isGameRunning.value = true;
generateHorses(); updateHorses(list.value);
startHorseLoop(); startHorseLoop();
await countdownPromise; await countdownPromise;
...@@ -241,10 +298,30 @@ const renderAvatar = (item: any | null) => { ...@@ -241,10 +298,30 @@ const renderAvatar = (item: any | null) => {
onMounted(() => { onMounted(() => {
preloadHorseFrames(); preloadHorseFrames();
if ($is_run_local()) { 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 },
];
gameStarted.value = true; gameStarted.value = true;
isGameRunning.value = true; isGameRunning.value = true;
generateHorses(); updateHorses(list.value);
startHorseLoop(); startHorseLoop();
// 模拟分数变化
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);
}
}
}, 1500);
} }
}); });
...@@ -430,7 +507,7 @@ onUnmounted(() => { ...@@ -430,7 +507,7 @@ onUnmounted(() => {
.horse-shadow { .horse-shadow {
position: absolute; position: absolute;
bottom: -4px; bottom: 20px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
width: 180px; width: 180px;
...@@ -447,10 +524,10 @@ onUnmounted(() => { ...@@ -447,10 +524,10 @@ onUnmounted(() => {
height: 50px; height: 50px;
border-radius: 50%; border-radius: 50%;
position: absolute; position: absolute;
top: 40px; top: 80px;
left: 10px; left: 60px;
border: 2px solid #FFCF44; border: 2px solid #FFCF44;
z-index: 2; z-index: 3;
} }
.horse-username { .horse-username {
...@@ -459,9 +536,10 @@ onUnmounted(() => { ...@@ -459,9 +536,10 @@ onUnmounted(() => {
background-repeat: no-repeat; background-repeat: no-repeat;
width: 120px; width: 120px;
height: 44px; height: 44px;
padding-left: 12px;
position: absolute; position: absolute;
top: 92px; top: 105px;
left: 0px; left: 60px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
......
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