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

feat: game4 PC端马的进度(基础速度+得分加速度);

parent cb184464
......@@ -262,7 +262,7 @@ const startGameWithMusic = () => {
console.log('[Game4] startGameWithMusic called, local mode:', $is_local_mode(), 'playerCount:', playerCount.value)
// 确保背景音乐播放(参考 game2 的 watch 模式,但直接在点击时调用更可靠)
playGame4Music()
// 本地模拟已关闭,如需开启请取消以下注释
// // 本地模拟已关闭,如需开启请取消以下注释
// if ($is_local_mode()) {
// console.log('[Game4] 本地模式,直接进入赛马画面')
// gotoRank1WithIntro()
......@@ -279,7 +279,7 @@ const startGameWithMusic = () => {
const nextRound = () => {
// 立刻停止喝彩音乐并移除指针事件监听,防止点击按钮时重新触发 playApplauseMusic
stopApplauseMusic()
// 本地模拟已关闭,如需开启请取消以下注释
// // 本地模拟已关闭,如需开启请取消以下注释
// if ($is_local_mode()) {
// gotoLoading(true)
// return
......@@ -299,7 +299,7 @@ const nextRoundWithMusic = () => {
const backHandler = async() => {
// 立刻停止喝彩音乐并移除指针事件监听,防止点击按钮时重新触发 playApplauseMusic
stopApplauseMusic()
// 本地模拟已关闭,如需开启请取消以下注释
// // 本地模拟已关闭,如需开启请取消以下注释
// if ($is_local_mode()) {
// stopAllMusic()
// router.replace('/main')
......
......@@ -45,12 +45,16 @@ const imageUrls = {
avatarAnimation: cssAssetUrl("avatar_animation.png"),
};
// ========== 速度基准 ==========
// 60s 游戏 × 基准速度(0.455%/s) = 27.3% → 不加速跑约 1/4 路程
// 60s 游戏 × 最大3.5x(1.59%/s) = 95.5% → 猛摇才能接近终点
// ========== 速度基准(满速马在游戏结束前 1~2 秒刚好跑到右边缘)==========
// MAX_PULSE=2.5 → 最大倍率 = 1+2.5 = 3.5x
// BASE_DURATION_SEC = 240 → 基准速度 0.417%/s
// 全程最大 boost: 0.417*3.5*60 = 87.5%(理论),实测约 58 秒到右边缘
// 不加速(boost=0): 0.417*60 = 25% → 时间地板推到 55%
// 一般活动(boost≈1): 0.417*2*60 = 50% → 地板 55%
// 活跃玩家(boost≈2): 0.417*3*60 = 75% → 大幅领先
const TOTAL_DISTANCE_PX = 2160; // -120px ~ 2040px
const BASE_DURATION_SEC = 220;
const BASE_SPEED_PERCENT_PER_SEC = 100 / BASE_DURATION_SEC; // ≈ 0.455%/s 基准速度
const BASE_DURATION_SEC = 240;
const BASE_SPEED_PERCENT_PER_SEC = 100 / BASE_DURATION_SEC; // ≈ 0.417%/s 基准速度
// 马匹帧配置(使用 game3 的马匹)
const HORSE_SIZE = 280;
......@@ -73,48 +77,59 @@ for (const config of horseFrameConfigs) {
const HORSE_FRAME_TICK_MS = 24; // ~40fps
// ========== 瞬时加速脉冲机制 ==========
// H5 每次摇一摇发送累计积分,PC 端检测 score 增长 → 触发短暂加速脉冲 → 迅速衰减
// 快速连摇时脉冲可叠加(每次 +1.0),最高不超过 4.5(即 5.5x 基准速度)
const BOOST_PULSE = 1.0; // 每次操作的加速幅度(+100% 额外速度,即 2x 基准
const MAX_PULSE = 4.5; // 最大叠加脉冲上限(即 5.5x 基准速度)
const BOOST_DECAY = 0.992; // 每帧衰减系数(60fps 下约 3.6s 衰减到 60%,约 9s 衰减归零
// ========== 速度/加速机制 ==========
// H5 每次摇一摇 → WebSocket 推送累计总分 → 按总分比例直接计算 boost
// 高分跑快、低分跑慢:boost = (自身分数 / 当前最高分) × MAX_PULSE
// 最高分玩家 boost=MAX_PULSE=2.5(即 3.5x 基准速度 ≈ 58 秒到右边缘,对标 mock 满速马
const MAX_PULSE = 2.5; // 最大加速倍率上限
const BOOST_DECAY = 0.994; // 每帧衰减系数(停止摇一摇后逐渐减速,半衰期约 1.9s
/** 瞬时加速值: { [userid]: currentBoost },每帧衰减 */
/** 瞬时加速值: { [userid]: currentBoost },每帧衰减(真实 websocket 数据专用) */
const transientBoosts = ref<Record<string, number>>({});
/** 上一轮各玩家累计积分,用于检测增长 */
const prevScores: Record<string, number> = {};
/** 检测 rankList 中哪些玩家 score 增长了 → 触发瞬时加速脉冲 */
const detectScoreChanges = (players: Player[]) => {
/** mock 模式下每匹马的固定全程 boost 值(不衰减),null 表示非 mock 模式 */
const mockFixedBoosts = ref<Record<string, number> | null>(null);
/**
* 根据 rankList 中的累计总分数,按比例直接计算每匹马的 boost(不再使用增量)
* - 找出所有有效玩家中的最高分
* - 每匹马的 boost = (自身分数 / 最高分) × MAX_PULSE
* - 最高分玩家享受 boost=2.5 满速,低分按比例减速
*/
const recalculateBoostsFromScores = (players: Player[]) => {
if (!players || players.length === 0) return;
if (mockFixedBoosts.value) return; // mock 模式下不干预
let hasNewBoost = false;
const updated = { ...transientBoosts.value };
// 收集有效玩家及其分数、ID
const entries: { id: string; score: number; nickname: string }[] = [];
players.forEach((p, index) => {
// WebSocket 数据无 userid/wechat_id,用 avatar 作为稳定标识
const id = p.userid || p.wechat_id || p.avatar || `player-${index}`;
if (!id || id.startsWith("empty-")) return;
const newScore = Number(p.score ?? 0);
const prev = prevScores[id] ?? 0;
if (newScore > prev) {
// 积分增长 = 玩家又摇了一次 → 叠加瞬时加速脉冲(连摇可累积到 MAX_PULSE)
const current = updated[id] || 0;
updated[id] = Math.min(current + BOOST_PULSE, MAX_PULSE);
hasNewBoost = true;
console.log(
`[Rank1View] 🚀 ${p.nickname} score ${prev}${newScore}, boost: ${current.toFixed(1)}+${BOOST_PULSE}=${updated[id].toFixed(1)}`,
);
}
if (!p.nickname || p.nickname === "虚位以待" || p.nickname === "-") return;
if (p.wechat_id && String(p.wechat_id).startsWith("empty-")) return;
const id = p.userid || p.wechat_id || (p.avatar ? `avt:${p.avatar}` : `horse-${index}`);
const score = Number(p.score ?? 0);
if (isNaN(score) || score <= 0) return;
entries.push({ id, score, nickname: p.nickname || "" });
});
if (entries.length === 0) return;
// 当前最高分作为满速基准
const maxScore = Math.max(...entries.map(e => e.score));
if (maxScore <= 0) return;
const updated: Record<string, number> = {};
let logLines: string[] = [];
prevScores[id] = newScore;
entries.forEach(({ id, score, nickname }) => {
// 按比例计算:最高分→2.5,其他人等比缩放
const boost = parseFloat(((score / maxScore) * MAX_PULSE).toFixed(2));
updated[id] = boost;
logLines.push(`${nickname}: score=${score} → boost=${boost.toFixed(2)}`);
});
if (hasNewBoost) {
transientBoosts.value = updated;
}
console.log(
`[Rank1View] 🏇 按总分比例计算 boost(最高分=${maxScore}, MAX_PULSE=${MAX_PULSE}):\n ${logLines.join("\n ")}`,
);
};
interface Horse {
......@@ -161,6 +176,11 @@ const fullRankList = computed<RankItem[]>(() => {
/** 倒计时结束后才为 true,控制背景动画和马匹渲染 */
const gameStarted = ref(false);
/** 游戏已运行时间(ms),用于时间比例最小进度保障(参考 game2) */
const gameElapsedMs = ref(0);
/** 时间比例最小进度百分比:确保无加速时马匹也随倒计时自然推进,但不会喧宾夺主 */
const MIN_PROGRESS_RATIO = 55; // 55% - 纯时间推进确保不落后太多,留给 boost 足够的竞争空间
// const LANE_POSITIONS = [7, 115, 235, 345, 455];
const LANE_POSITIONS = [0, 108, 228, 338, 448];
......@@ -242,8 +262,8 @@ watch(
console.log("[Rank1View] watch rankList 触发,players:", players?.length, JSON.stringify(players?.slice(0, 2)));
if (players && players.length > 0) {
updateHorses(players);
// 检测积分增长 → 触发瞬时加速脉冲
detectScoreChanges(players);
// 按总分比例直接计算 boost(高分快、低分慢)
recalculateBoostsFromScores(players);
}
},
{ immediate: true, deep: true },
......@@ -254,43 +274,21 @@ const horseTranslateX = (xPercent: number) => {
return -120 + (xPercent / 100) * TOTAL_DISTANCE_PX;
};
// ========== 接收 H5 传来的 acceleration / boost 数据 ==========
// 注意:rankList 的 score 增长检测是主通道,此处作为兜底
watch(
() => props.horseBoosts,
(boosts) => {
if (!boosts || Object.keys(boosts).length === 0) return;
let hasNew = false;
const updated = { ...transientBoosts.value };
Object.keys(boosts).forEach((id) => {
const val = Number(boosts[id] ?? 0);
if (val > 0) {
// 每次收到有效数据都叠加脉冲(连摇可累积到 MAX_PULSE)
const current = updated[id] || 0;
updated[id] = Math.min(current + BOOST_PULSE, MAX_PULSE);
hasNew = true;
}
});
if (hasNew) {
transientBoosts.value = updated;
console.log(
`[Rank1View] horseBoosts 触发瞬时加速 (${new Date().toLocaleTimeString()}):`,
JSON.parse(JSON.stringify(transientBoosts.value)),
);
}
},
{ deep: true },
);
// ========== 接收 H5 传来的 horseBoosts 数据(由 Game4.vue onAny 解析 WebSocket 提供) ==========
// 注意:加速计算已统一由 rankList watch → recalculateBoostsFromScores 处理
// horseBoosts prop 保留用于日后扩展(如直接传递 boost 值),当前不再单独 watch
// ========== 游戏主循环 (requestAnimationFrame 驱动位置 + 帧动画) ==========
let gameRafId: number | undefined;
let gameLastTs = 0;
let boostRecalcTimerMs = 0;
const BOOST_RECALC_INTERVAL_MS = 3000; // 每 3 秒用最新 rankList 重算 boost(防 WebSocket 断流导致 boost 过时)
const startGameLoop = () => {
stopGameLoop();
gameLastTs = 0;
gameElapsedMs.value = 0;
boostRecalcTimerMs = 0;
const tick = (ts: number) => {
if (!isGameRunning.value) {
......@@ -301,12 +299,35 @@ const startGameLoop = () => {
const dt = Math.min(ts - gameLastTs, 200);
gameLastTs = ts;
// 累计游戏运行时间(用于时间比例最小进度保障,参考 game2)
gameElapsedMs.value += dt;
const GAME_TOTAL_MS = 60_000; // 总游戏时间 60s
const elapsedRatio = Math.min(gameElapsedMs.value / GAME_TOTAL_MS, 1);
// 时间比例最小进度:确保即使无任何加速数据,马匹也会随倒计时推进
const minProgress = elapsedRatio * MIN_PROGRESS_RATIO;
// 每 3 秒用最新 rankList 重算 boost(兜底:防止 WebSocket 断流导致 boost 过时衰减至 0)
boostRecalcTimerMs += dt;
if (boostRecalcTimerMs >= BOOST_RECALC_INTERVAL_MS) {
boostRecalcTimerMs = 0;
recalculateBoostsFromScores(props.rankList);
}
const newTransientBoosts: Record<string, number> = {};
const fixedBoosts = mockFixedBoosts.value; // mock 模式下的固定 boost(null = 非 mock)
horses.value.forEach((horse) => {
// 时间比例最小进度保障:马匹位置不低于 elapsedRatio * MIN_PROGRESS_RATIO%
if (horse.xPercent < minProgress) {
horse.xPercent = minProgress;
}
if (horse.xPercent >= 100) return;
const boost = transientBoosts.value[horse.id] || 0;
// mock 模式:使用固定 boost,不参与衰减;真实模式:使用 transientBoosts 并逐帧衰减
const isMock = fixedBoosts && horse.id in fixedBoosts;
const boost = isMock
? (fixedBoosts[horse.id] ?? 0)
: (transientBoosts.value[horse.id] || 0);
horse.boost = boost;
horse.xPercent = Math.min(
......@@ -322,10 +343,13 @@ const startGameLoop = () => {
horse.frameIndex = (horse.frameIndex + 1) % fc;
}
// mock 马匹 boost 不衰减,真实马匹正常衰减
if (!isMock) {
const decayed = boost * Math.pow(BOOST_DECAY, dt / (1000 / 60));
if (decayed > 0.005) {
newTransientBoosts[horse.id] = decayed;
}
}
});
transientBoosts.value = newTransientBoosts;
......@@ -342,30 +366,30 @@ const stopGameLoop = () => {
}
};
// ========== 本地 mock 加速 ==========
// ========== 本地 mock 加速(每匹马分配不同的固定全程 boost,均匀分布 0~MAX_PULSE) ==========
let mockBoostTimer: ReturnType<typeof setInterval> | undefined;
const startMockBoost = () => {
stopMockBoost();
// 基于马匹顺序计算速度系数:排名靠前的马(小索引)基础快,末尾的慢
const horseRates: Record<string, number> = {};
horses.value.forEach((h, i) => {
horseRates[h.id] = 0.3 + (i / Math.max(horses.value.length - 1, 1)) * 0.7;
const totalHorses = horses.value.length;
const fixedBoosts: Record<string, number> = {};
// 按索引均匀分配 boost:第一批 (i=0) boost=0 不加速,最后一批 (i=n-1) boost=MAX_PULSE 满加速
// 满加速马 60s 刚好到 100%,便于观察不同 boost 等级对应的终点位置
horses.value.forEach((horse, i) => {
fixedBoosts[horse.id] =
totalHorses <= 1
? MAX_PULSE / 2
: parseFloat(((i / (totalHorses - 1)) * MAX_PULSE).toFixed(2));
});
mockFixedBoosts.value = fixedBoosts;
mockBoostTimer = setInterval(() => {
if (!isGameRunning.value) return;
const updated = { ...transientBoosts.value };
horses.value.forEach((horse) => {
const rate = horseRates[horse.id] ?? (0.3 + Math.random() * 0.7);
const current = updated[horse.id] || 0;
// 偶尔减速制造节奏变化
const decay = Math.random() > 0.75 ? -0.4 : 0;
updated[horse.id] = Math.max(0, Math.min(current + BOOST_PULSE * rate * 0.3 + decay, MAX_PULSE));
});
transientBoosts.value = updated;
}, 1500);
console.log(
'[Rank1View] 🎯 固定 mock boost 分配(0 ~ MAX_PULSE 均匀分布):',
horses.value.map(h => `${h.nickname}: boost=${fixedBoosts[h.id]?.toFixed(2)} 60s 预计进度=${(BASE_SPEED_PERCENT_PER_SEC * (1 + (fixedBoosts[h.id] ?? 0)) * 60).toFixed(0)}%`).join('\n '),
);
};
const stopMockBoost = () => {
mockFixedBoosts.value = null;
if (mockBoostTimer) {
clearInterval(mockBoostTimer);
mockBoostTimer = undefined;
......@@ -435,7 +459,7 @@ const startIntro = async () => {
console.log("[Rank1View] startIntro 开始,当前 rankList:", props.rankList?.length, JSON.stringify(props.rankList?.slice(0, 2)));
console.log("[Rank1View] 当前 horses 数量:", horses.value.length);
// 本地模拟已关闭,如需开启请取消以下注释
// // 本地模拟已关闭,如需开启请取消以下注释
// if ($is_local_mode()) {
// mockHorses();
// }
......@@ -445,7 +469,7 @@ const startIntro = async () => {
isGameRunning.value = false;
stopApplauseMusic();
transientBoosts.value = {};
Object.keys(prevScores).forEach((k) => delete prevScores[k]);
mockFixedBoosts.value = null;
const countdownPromise = designStage.value?.startCountdown(3, 60);
await new Promise((r) => setTimeout(r, 3500));
......@@ -469,9 +493,9 @@ const startIntro = async () => {
// }
startGameLoop();
if ($is_local_mode()) {
startMockBoost();
}
// if ($is_local_mode()) {
// startMockBoost();
// }
await countdownPromise;
stopGame();
......@@ -666,7 +690,7 @@ defineExpose({
top: 0;
left: 0;
display: flex;
animation: bg-scroll-left 20s linear infinite;
animation: bg-scroll-left 22s linear infinite;
z-index: 1;
}
......@@ -685,7 +709,7 @@ defineExpose({
bottom: 0;
left: 0;
display: flex;
animation: bg-scroll-left 20s linear infinite;
animation: bg-scroll-left 22s linear infinite;
z-index: 2;
}
......
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