Commit 6d448527 authored by 董政锦's avatar 董政锦

fix: game4 修复马短暂脉冲加速;

parent 2499dbb9
...@@ -66,12 +66,12 @@ offHorseBoost = onSocketMessage((evt, payload) => { ...@@ -66,12 +66,12 @@ offHorseBoost = onSocketMessage((evt, payload) => {
data.forEach((item: any) => { data.forEach((item: any) => {
const id = item.userid || item.wechat_id || ""; const id = item.userid || item.wechat_id || "";
if (!id) return; if (!id) return;
// 尝试多种字段名: boost / acceleration / speed / progress // 尝试多种字段名: score / boost / acceleration / speed / progress
const raw = item.boost ?? item.acceleration ?? item.speed ?? item.progress; const raw = item.score ?? item.boost ?? item.acceleration ?? item.speed ?? item.progress;
if (raw === undefined || raw === null) return; if (raw === undefined || raw === null) return;
const val = Number(raw); const val = Number(raw);
if (isNaN(val)) return; if (isNaN(val)) return;
// boost 范围 0~N,0 表示无加速(基准60秒跑完),>0 越快 // score/boost 范围 0~N,0 表示无加速,越大越快
boosts[id] = Math.max(0, val); boosts[id] = Math.max(0, val);
hasBoost = true; hasBoost = true;
}); });
...@@ -145,7 +145,6 @@ function handleRoomState(data: any) { ...@@ -145,7 +145,6 @@ function handleRoomState(data: any) {
const { startGame, createRoom, requestRoomState } = useAdminGameSocket({ const { startGame, createRoom, requestRoomState } = useAdminGameSocket({
gameId: "game4", gameId: "game4",
// loginRedirectPath: '/game4', // loginRedirectPath: '/game4',
onGameStartRejected: gotoLoading,
onRoomState: handleRoomState, onRoomState: handleRoomState,
onRoomJoin: (data) => { onRoomJoin: (data) => {
renderPlayers(data); renderPlayers(data);
......
...@@ -39,10 +39,51 @@ const imageUrls = { ...@@ -39,10 +39,51 @@ const imageUrls = {
avatarAnimation: cssAssetUrl("avatar_animation.png"), avatarAnimation: cssAssetUrl("avatar_animation.png"),
}; };
// ========== 速度基准: 60秒从屏幕左侧跑到右侧 ========== // ========== 速度基准: 90秒从屏幕左侧跑到右侧(调低初始速度) ==========
const TOTAL_DISTANCE_PX = 2160; // -120px ~ 2040px const TOTAL_DISTANCE_PX = 2160; // -120px ~ 2040px
const BASE_DURATION_SEC = 60; const BASE_DURATION_SEC = 90;
const BASE_SPEED_PERCENT_PER_SEC = 100 / BASE_DURATION_SEC; // ≈ 1.667%/s 基准速度 const BASE_SPEED_PERCENT_PER_SEC = 100 / BASE_DURATION_SEC; // ≈ 1.111%/s 基准速度
// ========== 瞬时加速脉冲机制 ==========
// H5 每次摇一摇发送累计积分,PC 端检测 score 增长 → 触发短暂加速脉冲 → 迅速衰减
const BOOST_PULSE = 0.8; // 每次操作的加速幅度(+80% 额外速度)
const BOOST_DECAY = 0.97; // 每帧衰减系数(60fps 下约 1s 衰减到 16%,约 2s 衰减归零)
/** 瞬时加速值: { [userid]: currentBoost },每帧衰减 */
const transientBoosts = ref<Record<string, number>>({});
/** 上一轮各玩家累计积分,用于检测增长 */
const prevScores: Record<string, number> = {};
/** 检测 rankList 中哪些玩家 score 增长了 → 触发瞬时加速脉冲 */
const detectScoreChanges = (players: Player[]) => {
if (!players || players.length === 0) return;
let hasNewBoost = false;
const updated = { ...transientBoosts.value };
players.forEach((p) => {
const id = p.userid || p.wechat_id || "";
if (!id || id.startsWith("empty-")) return;
const newScore = Number(p.score ?? 0);
const prev = prevScores[id] ?? 0;
if (newScore > prev) {
// 积分增长 = 玩家又摇了一次 → 触发瞬时加速脉冲
updated[id] = BOOST_PULSE;
hasNewBoost = true;
}
prevScores[id] = newScore;
});
if (hasNewBoost) {
transientBoosts.value = updated;
console.log(
"[Rank1View] 瞬时加速触发:",
JSON.parse(JSON.stringify(transientBoosts.value)),
);
}
};
interface Horse { interface Horse {
id: string; id: string;
...@@ -66,6 +107,8 @@ const horses = ref<Horse[]>([]); ...@@ -66,6 +107,8 @@ const horses = ref<Horse[]>([]);
const isGameRunning = ref(false); const isGameRunning = ref(false);
const showResult = ref(false); const showResult = ref(false);
const finalRankList = ref<RankItem[]>([]); const finalRankList = ref<RankItem[]>([]);
/** 倒计时结束后才为 true,控制背景动画和马匹渲染 */
const gameStarted = ref(false);
const LANE_POSITIONS = [42, 160, 270, 380, 490]; const LANE_POSITIONS = [42, 160, 270, 380, 490];
...@@ -109,6 +152,8 @@ watch( ...@@ -109,6 +152,8 @@ watch(
(players) => { (players) => {
if (players && players.length > 0) { if (players && players.length > 0) {
updateHorses(players); updateHorses(players);
// 检测积分增长 → 触发瞬时加速脉冲
detectScoreChanges(players);
} }
}, },
{ immediate: true, deep: true }, { immediate: true, deep: true },
...@@ -119,24 +164,31 @@ const horseTranslateX = (xPercent: number) => { ...@@ -119,24 +164,31 @@ const horseTranslateX = (xPercent: number) => {
return -120 + (xPercent / 100) * TOTAL_DISTANCE_PX; return -120 + (xPercent / 100) * TOTAL_DISTANCE_PX;
}; };
// ========== 接收 H5 传来的加速度 ========== // ========== 接收 H5 传来的 acceleration / boost 数据 ==========
// 注意:rankList 的 score 增长检测是主通道,此处作为兜底
watch( watch(
() => props.horseBoosts, () => props.horseBoosts,
(boosts) => { (boosts) => {
if (!boosts || Object.keys(boosts).length === 0) return; 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) {
// 每次收到有效数据都刷新脉冲(叠加摇一摇的连续加速感)
updated[id] = BOOST_PULSE;
hasNew = true;
}
});
if (hasNew) {
transientBoosts.value = updated;
console.log( console.log(
`[Rank1View] 收到加速度数据 (${new Date().toLocaleTimeString()}):`, `[Rank1View] horseBoosts 触发瞬时加速 (${new Date().toLocaleTimeString()}):`,
JSON.parse(JSON.stringify(boosts)), JSON.parse(JSON.stringify(transientBoosts.value)),
); );
horses.value.forEach((horse) => {
const boost = boosts[horse.id];
if (boost !== undefined) {
horse.boost = boost;
console.log(` 🐴 ${horse.nickname} (${horse.id}): boost=${boost}`);
} }
});
}, },
{ deep: true }, { deep: true },
); );
...@@ -152,18 +204,34 @@ const startGameLoop = () => { ...@@ -152,18 +204,34 @@ const startGameLoop = () => {
if (!isGameRunning.value) return; if (!isGameRunning.value) return;
let allFinished = true; let allFinished = true;
const newTransientBoosts: Record<string, number> = {};
horses.value.forEach((horse) => { horses.value.forEach((horse) => {
if (horse.xPercent >= 100) return; if (horse.xPercent >= 100) return;
// 实际速度 = 基准速度 × (1 + boost) // 瞬时加速值(来自积分增长脉冲,每帧衰减中)
const boost = transientBoosts.value[horse.id] || 0;
horse.boost = boost;
// 实际速度 = 基准速度 × (1 + 瞬时boost)
horse.xPercent = Math.min( horse.xPercent = Math.min(
100, 100,
horse.xPercent + BASE_SPEED_PERCENT_PER_SEC * (1 + horse.boost) * TICK_SEC, horse.xPercent +
BASE_SPEED_PERCENT_PER_SEC * (1 + boost) * TICK_SEC,
); );
// 衰减瞬时加速:每帧 × 衰减系数
const decayed = boost * BOOST_DECAY;
if (decayed > 0.005) {
newTransientBoosts[horse.id] = decayed;
}
if (horse.xPercent < 100) allFinished = false; if (horse.xPercent < 100) allFinished = false;
}); });
// 更新衰减后的加速值
transientBoosts.value = newTransientBoosts;
if (allFinished && horses.value.length > 0) { if (allFinished && horses.value.length > 0) {
stopGame(); stopGame();
} }
...@@ -206,13 +274,13 @@ const computeFinalRank = (): RankItem[] => { ...@@ -206,13 +274,13 @@ const computeFinalRank = (): RankItem[] => {
if (valid.length > 0) { if (valid.length > 0) {
// 优先用 WebSocket 的 score 排序 // 优先用 WebSocket 的 score 排序
const sorted = [...valid].sort((a, b) => (b.score ?? 0) - (a.score ?? 0)); const sorted = [...valid].sort((a, b) => Number(b.score ?? 0) - Number(a.score ?? 0));
return sorted.map((p, i) => ({ return sorted.map((p, i) => ({
rank: i + 1, rank: i + 1,
userid: p.userid || p.wechat_id || `unknown-${i}`, userid: p.userid || p.wechat_id || `unknown-${i}`,
nickname: p.nickname, nickname: p.nickname,
avatar: p.avatar || "", avatar: p.avatar || "",
score: p.score ?? 0, score: Number(p.score ?? 0),
})); }));
} }
...@@ -255,15 +323,39 @@ const startIntro = async () => { ...@@ -255,15 +323,39 @@ const startIntro = async () => {
if ($is_run_local()) { if ($is_run_local()) {
mockHorses(); mockHorses();
} }
isGameRunning.value = true;
showResult.value = false; showResult.value = false;
finalRankList.value = []; finalRankList.value = [];
gameStarted.value = false;
isGameRunning.value = false;
// 重置瞬时加速和积分追踪
transientBoosts.value = {};
Object.keys(prevScores).forEach((k) => delete prevScores[k]);
// Phase 1: 3 秒开场倒计时(背景动画暂停、马匹不显示)
await designStage.value?.startCountdown(3, 0);
// Phase 2: 倒计时结束 → 游戏正式开始
gameStarted.value = true;
isGameRunning.value = true;
startGameLoop(); startGameLoop();
if ($is_run_local()) { if ($is_run_local()) {
startMockBoost(); startMockBoost();
} }
// DesignStage 倒计时: 开场3秒 + 游戏60秒
await designStage.value?.startCountdown(3, 60); // Phase 3: 等待游戏结束(最多 60 秒 或 所有马跑完)
const maxMs = 60000;
const gameStart = Date.now();
await new Promise<void>((resolve) => {
const check = setInterval(() => {
const allDone = horses.value.length > 0 && horses.value.every((h) => h.xPercent >= 100);
const timeUp = Date.now() - gameStart >= maxMs;
if (allDone || timeUp || !isGameRunning.value) {
clearInterval(check);
resolve();
}
}, 200);
});
stopGame(); stopGame();
}; };
...@@ -301,6 +393,7 @@ const podiumPlayers = (): RankItem[] => { ...@@ -301,6 +393,7 @@ const podiumPlayers = (): RankItem[] => {
onMounted(() => { onMounted(() => {
if ($is_run_local()) { if ($is_run_local()) {
mockHorses(); mockHorses();
gameStarted.value = true;
isGameRunning.value = true; isGameRunning.value = true;
startGameLoop(); startGameLoop();
startMockBoost(); startMockBoost();
...@@ -318,7 +411,7 @@ defineExpose({ ...@@ -318,7 +411,7 @@ defineExpose({
<template> <template>
<DesignStage ref="designStage"> <DesignStage ref="designStage">
<div class="screen-container" :class="{ paused: !isGameRunning }"> <div class="screen-container" :class="{ paused: !gameStarted }">
<div class="img-logo"></div> <div class="img-logo"></div>
<div class="img-title-bg"></div> <div class="img-title-bg"></div>
<div class="img-big-brum"></div> <div class="img-big-brum"></div>
...@@ -330,6 +423,8 @@ defineExpose({ ...@@ -330,6 +423,8 @@ defineExpose({
<div class="bg-bottom-inner"></div> <div class="bg-bottom-inner"></div>
<div class="bg-bottom-inner"></div> <div class="bg-bottom-inner"></div>
</div> </div>
<!-- 倒计时结束后才显示马匹 -->
<template v-if="gameStarted">
<div <div
v-for="horse in horses" v-for="horse in horses"
:key="horse.id" :key="horse.id"
...@@ -345,6 +440,7 @@ defineExpose({ ...@@ -345,6 +440,7 @@ defineExpose({
</div> </div>
<div class="horse-avatar" :style="horseAvatarStyle(horse)"></div> <div class="horse-avatar" :style="horseAvatarStyle(horse)"></div>
</div> </div>
</template>
<div v-if="showResult" class="result-overlay"> <div v-if="showResult" class="result-overlay">
<div class="result-mask"></div> <div class="result-mask"></div>
......
...@@ -14,7 +14,7 @@ const emit = defineEmits<{ ...@@ -14,7 +14,7 @@ const emit = defineEmits<{
const FINAL_RANK_LIMIT = 10; const FINAL_RANK_LIMIT = 10;
const imageUrls = { const imageUrls = {
bg: cssAssetUrl("game4/bg.png"), bg: cssAssetUrl("game4/bg.svg"),
logo: cssAssetUrl("game1/logo.png"), logo: cssAssetUrl("game1/logo.png"),
title: cssAssetUrl("game1/title3.png"), title: cssAssetUrl("game1/title3.png"),
title2: cssAssetUrl("game1/title4.png"), title2: cssAssetUrl("game1/title4.png"),
...@@ -68,21 +68,30 @@ watch( ...@@ -68,21 +68,30 @@ watch(
<div class="podium"> <div class="podium">
<div class="rank-no1"> <div class="rank-no1">
<div class="gu-burst"></div> <div class="gu-burst"></div>
<div class="avatar"></div> <div
class="avatar"
:style="{ backgroundImage: `url(${list[0]?.avatar || imageUrls.avatar})` }"
></div>
<div class="nickname">{{ list[0]?.nickname }}</div> <div class="nickname">{{ list[0]?.nickname }}</div>
<div class="guan"></div> <div class="guan"></div>
<div class="pai"></div> <div class="pai"></div>
</div> </div>
<div class="rank-no2"> <div class="rank-no2">
<div class="gu-burst"></div> <div class="gu-burst"></div>
<div class="avatar"></div> <div
class="avatar"
:style="{ backgroundImage: `url(${list[1]?.avatar || imageUrls.avatar})` }"
></div>
<div class="nickname">{{ list[1]?.nickname }}</div> <div class="nickname">{{ list[1]?.nickname }}</div>
<div class="guan"></div> <div class="guan"></div>
<div class="pai"></div> <div class="pai"></div>
</div> </div>
<div class="rank-no3"> <div class="rank-no3">
<div class="gu-burst"></div> <div class="gu-burst"></div>
<div class="avatar"></div> <div
class="avatar"
:style="{ backgroundImage: `url(${list[2]?.avatar || imageUrls.avatar})` }"
></div>
<div class="nickname">{{ list[2]?.nickname }}</div> <div class="nickname">{{ list[2]?.nickname }}</div>
<div class="guan"></div> <div class="guan"></div>
<div class="pai"></div> <div class="pai"></div>
...@@ -97,7 +106,10 @@ watch( ...@@ -97,7 +106,10 @@ watch(
<div class="no-icon">{{ index + 4 }}</div> <div class="no-icon">{{ index + 4 }}</div>
<div> <div>
<div class="img-avatar-container"> <div class="img-avatar-container">
<div class="img-avatar"></div> <div
class="img-avatar"
:style="{ backgroundImage: `url(${item.avatar || imageUrls.avatar})` }"
></div>
</div> </div>
<div class="txt-nickname">{{ item.nickname }}</div> <div class="txt-nickname">{{ item.nickname }}</div>
</div> </div>
......
...@@ -2,8 +2,8 @@ import { createRouter, createWebHashHistory } from 'vue-router' ...@@ -2,8 +2,8 @@ import { createRouter, createWebHashHistory } from 'vue-router'
import { $read } from '@/commons/utils' import { $read } from '@/commons/utils'
import Login from '@/pages/Login.vue' import Login from '@/pages/Login.vue'
import Game1 from '@/pages/game1/Game.vue' import Game1 from '@/pages/game1/Game.vue'
import Game6 from '@/pages/game6/Game.vue'
import Game4 from '@/pages/game4/Game4.vue' import Game4 from '@/pages/game4/Game4.vue'
import Game6 from '@/pages/game6/Game.vue'
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
...@@ -28,7 +28,7 @@ const router = createRouter({ ...@@ -28,7 +28,7 @@ const router = createRouter({
{ {
path: '/game4', path: '/game4',
name: 'Game4', name: 'Game4',
component: Game1, component: Game4,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
}, },
...@@ -36,7 +36,7 @@ const router = createRouter({ ...@@ -36,7 +36,7 @@ const router = createRouter({
{ {
path: '/game6', path: '/game6',
name: 'Game6', name: 'Game6',
component: Game1, component: Game6,
meta: { meta: {
requiresAuth: true, requiresAuth: true,
}, },
......
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