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

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

parent 2499dbb9
......@@ -66,12 +66,12 @@ offHorseBoost = onSocketMessage((evt, payload) => {
data.forEach((item: any) => {
const id = item.userid || item.wechat_id || "";
if (!id) return;
// 尝试多种字段名: boost / acceleration / speed / progress
const raw = item.boost ?? item.acceleration ?? item.speed ?? item.progress;
// 尝试多种字段名: score / boost / acceleration / speed / progress
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;
// boost 范围 0~N,0 表示无加速(基准60秒跑完),>0 越快
// score/boost 范围 0~N,0 表示无加速,越大越快
boosts[id] = Math.max(0, val);
hasBoost = true;
});
......@@ -145,7 +145,6 @@ function handleRoomState(data: any) {
const { startGame, createRoom, requestRoomState } = useAdminGameSocket({
gameId: "game4",
// loginRedirectPath: '/game4',
onGameStartRejected: gotoLoading,
onRoomState: handleRoomState,
onRoomJoin: (data) => {
renderPlayers(data);
......
......@@ -39,10 +39,51 @@ const imageUrls = {
avatarAnimation: cssAssetUrl("avatar_animation.png"),
};
// ========== 速度基准: 60秒从屏幕左侧跑到右侧 ==========
// ========== 速度基准: 90秒从屏幕左侧跑到右侧(调低初始速度) ==========
const TOTAL_DISTANCE_PX = 2160; // -120px ~ 2040px
const BASE_DURATION_SEC = 60;
const BASE_SPEED_PERCENT_PER_SEC = 100 / BASE_DURATION_SEC; // ≈ 1.667%/s 基准速度
const BASE_DURATION_SEC = 90;
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 {
id: string;
......@@ -66,6 +107,8 @@ const horses = ref<Horse[]>([]);
const isGameRunning = ref(false);
const showResult = ref(false);
const finalRankList = ref<RankItem[]>([]);
/** 倒计时结束后才为 true,控制背景动画和马匹渲染 */
const gameStarted = ref(false);
const LANE_POSITIONS = [42, 160, 270, 380, 490];
......@@ -109,6 +152,8 @@ watch(
(players) => {
if (players && players.length > 0) {
updateHorses(players);
// 检测积分增长 → 触发瞬时加速脉冲
detectScoreChanges(players);
}
},
{ immediate: true, deep: true },
......@@ -119,24 +164,31 @@ const horseTranslateX = (xPercent: number) => {
return -120 + (xPercent / 100) * TOTAL_DISTANCE_PX;
};
// ========== 接收 H5 传来的加速度 ==========
// ========== 接收 H5 传来的 acceleration / boost 数据 ==========
// 注意:rankList 的 score 增长检测是主通道,此处作为兜底
watch(
() => props.horseBoosts,
(boosts) => {
if (!boosts || Object.keys(boosts).length === 0) return;
console.log(
`[Rank1View] 收到加速度数据 (${new Date().toLocaleTimeString()}):`,
JSON.parse(JSON.stringify(boosts)),
);
horses.value.forEach((horse) => {
const boost = boosts[horse.id];
if (boost !== undefined) {
horse.boost = boost;
console.log(` 🐴 ${horse.nickname} (${horse.id}): boost=${boost}`);
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(
`[Rank1View] horseBoosts 触发瞬时加速 (${new Date().toLocaleTimeString()}):`,
JSON.parse(JSON.stringify(transientBoosts.value)),
);
}
},
{ deep: true },
);
......@@ -152,18 +204,34 @@ const startGameLoop = () => {
if (!isGameRunning.value) return;
let allFinished = true;
const newTransientBoosts: Record<string, number> = {};
horses.value.forEach((horse) => {
if (horse.xPercent >= 100) return;
// 实际速度 = 基准速度 × (1 + boost)
// 瞬时加速值(来自积分增长脉冲,每帧衰减中)
const boost = transientBoosts.value[horse.id] || 0;
horse.boost = boost;
// 实际速度 = 基准速度 × (1 + 瞬时boost)
horse.xPercent = Math.min(
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;
});
// 更新衰减后的加速值
transientBoosts.value = newTransientBoosts;
if (allFinished && horses.value.length > 0) {
stopGame();
}
......@@ -206,13 +274,13 @@ const computeFinalRank = (): RankItem[] => {
if (valid.length > 0) {
// 优先用 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) => ({
rank: i + 1,
userid: p.userid || p.wechat_id || `unknown-${i}`,
nickname: p.nickname,
avatar: p.avatar || "",
score: p.score ?? 0,
score: Number(p.score ?? 0),
}));
}
......@@ -255,15 +323,39 @@ const startIntro = async () => {
if ($is_run_local()) {
mockHorses();
}
isGameRunning.value = true;
showResult.value = false;
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();
if ($is_run_local()) {
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();
};
......@@ -301,6 +393,7 @@ const podiumPlayers = (): RankItem[] => {
onMounted(() => {
if ($is_run_local()) {
mockHorses();
gameStarted.value = true;
isGameRunning.value = true;
startGameLoop();
startMockBoost();
......@@ -318,7 +411,7 @@ defineExpose({
<template>
<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-title-bg"></div>
<div class="img-big-brum"></div>
......@@ -330,21 +423,24 @@ defineExpose({
<div class="bg-bottom-inner"></div>
<div class="bg-bottom-inner"></div>
</div>
<div
v-for="horse in horses"
:key="horse.id"
class="horse-wrapper"
:style="{
bottom: `${horse.startY}px`,
transform: `translateX(${horseTranslateX(horse.xPercent)}px)`,
}"
>
<div class="horse"></div>
<div class="horse-username">
<span class="username-text">{{ horse.nickname }}</span>
<!-- 倒计时结束后才显示马匹 -->
<template v-if="gameStarted">
<div
v-for="horse in horses"
:key="horse.id"
class="horse-wrapper"
:style="{
bottom: `${horse.startY}px`,
transform: `translateX(${horseTranslateX(horse.xPercent)}px)`,
}"
>
<div class="horse"></div>
<div class="horse-username">
<span class="username-text">{{ horse.nickname }}</span>
</div>
<div class="horse-avatar" :style="horseAvatarStyle(horse)"></div>
</div>
<div class="horse-avatar" :style="horseAvatarStyle(horse)"></div>
</div>
</template>
<div v-if="showResult" class="result-overlay">
<div class="result-mask"></div>
......
......@@ -14,7 +14,7 @@ const emit = defineEmits<{
const FINAL_RANK_LIMIT = 10;
const imageUrls = {
bg: cssAssetUrl("game4/bg.png"),
bg: cssAssetUrl("game4/bg.svg"),
logo: cssAssetUrl("game1/logo.png"),
title: cssAssetUrl("game1/title3.png"),
title2: cssAssetUrl("game1/title4.png"),
......@@ -68,21 +68,30 @@ watch(
<div class="podium">
<div class="rank-no1">
<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="guan"></div>
<div class="pai"></div>
</div>
<div class="rank-no2">
<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="guan"></div>
<div class="pai"></div>
</div>
<div class="rank-no3">
<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="guan"></div>
<div class="pai"></div>
......@@ -97,7 +106,10 @@ watch(
<div class="no-icon">{{ index + 4 }}</div>
<div>
<div class="img-avatar-container">
<div class="img-avatar"></div>
<div
class="img-avatar"
:style="{ backgroundImage: `url(${item.avatar || imageUrls.avatar})` }"
></div>
</div>
<div class="txt-nickname">{{ item.nickname }}</div>
</div>
......
......@@ -2,8 +2,8 @@ import { createRouter, createWebHashHistory } from 'vue-router'
import { $read } from '@/commons/utils'
import Login from '@/pages/Login.vue'
import Game1 from '@/pages/game1/Game.vue'
import Game6 from '@/pages/game6/Game.vue'
import Game4 from '@/pages/game4/Game4.vue'
import Game6 from '@/pages/game6/Game.vue'
const router = createRouter({
history: createWebHashHistory(),
......@@ -28,7 +28,7 @@ const router = createRouter({
{
path: '/game4',
name: 'Game4',
component: Game1,
component: Game4,
meta: {
requiresAuth: true,
},
......@@ -36,7 +36,7 @@ const router = createRouter({
{
path: '/game6',
name: 'Game6',
component: Game1,
component: Game6,
meta: {
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