Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
F
fc-minigame
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
陈冲
fc-minigame
Commits
d0bec691
Commit
d0bec691
authored
Jun 12, 2026
by
董政锦
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: game4 背景映音乐
parent
83aad560
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
90 additions
and
36 deletions
+90
-36
Rank1View.vue
frontend-large/src/pages/game3/views/Rank1View.vue
+6
-6
Game4.vue
frontend-large/src/pages/game4/Game4.vue
+33
-2
Rank1View.vue
frontend-large/src/pages/game4/views/Rank1View.vue
+51
-28
No files found.
frontend-large/src/pages/game3/views/Rank1View.vue
View file @
d0bec691
...
@@ -197,15 +197,15 @@ watch(
...
@@ -197,15 +197,15 @@ watch(
);
);
// ========== 速度基准(对齐 game4 时间驱动模型) ==========
// ========== 速度基准(对齐 game4 时间驱动模型) ==========
//
基础速度大幅下调,确保马匹在 60s 游戏时间内不会过早跑出屏幕
//
60s 游戏内,即使满加速也最多跑 ~75% 路程,确保马匹始终可见
const
TOTAL_DISTANCE_PX
=
HORSE_END_X
-
HORSE_START_X
;
// 2200px
const
TOTAL_DISTANCE_PX
=
HORSE_END_X
-
HORSE_START_X
;
// 2200px
const
BASE_DURATION_SEC
=
150
;
// 基准时长 150s,即无加速时跑完全程需 15
0s
const
BASE_DURATION_SEC
=
200
;
// 基准时长 200s,即无加速时跑完全程需 20
0s
const
BASE_SPEED_PX_PER_SEC
=
TOTAL_DISTANCE_PX
/
BASE_DURATION_SEC
;
//
≈ 14.7
px/s
const
BASE_SPEED_PX_PER_SEC
=
TOTAL_DISTANCE_PX
/
BASE_DURATION_SEC
;
//
11.0
px/s
// 瞬时加速脉冲机制(对齐 game4)
// 瞬时加速脉冲机制(对齐 game4)
const
BOOST_PULSE
=
0.
3
;
// 每次 score 增长对应的额外速度倍率
const
BOOST_PULSE
=
0.
15
;
// 每次 score 增长对应的额外速度倍率(从 0.3 下调,减缓加速累积)
const
MAX_BOOST
=
3.0
;
// 最大加速倍率
const
MAX_BOOST
=
1.5
;
// 最大加速倍率(从 3.0 下调,确保满速时 80s 才能跑完 2200px)
const
BOOST_DECAY
=
0.99
2
;
// 每帧衰减系数(60fps 约 3.6s 到 60%
)
const
BOOST_DECAY
=
0.99
0
;
// 每帧衰减系数(微微加快衰减,避免 boost 持续高位
)
// ========== 马匹动画循环 (RAF 驱动:时间驱动位移 + 帧动画) ==========
// ========== 马匹动画循环 (RAF 驱动:时间驱动位移 + 帧动画) ==========
let
horseRafId
:
number
|
undefined
;
let
horseRafId
:
number
|
undefined
;
...
...
frontend-large/src/pages/game4/Game4.vue
View file @
d0bec691
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
nextTick
,
onMounted
,
onUnmounted
,
ref
,
watch
}
from
"vue"
;
import
{
nextTick
,
onMounted
,
onUnmounted
,
ref
,
watch
}
from
"vue"
;
import
{
playApplauseMusic
,
playGame4Music
,
stopAllMusic
,
stopGame4Music
}
from
'@/commons/music'
import
{
playApplauseMusic
,
playGame4Music
,
stopAllMusic
,
stop
ApplauseMusic
,
stop
Game4Music
}
from
'@/commons/music'
import
Loading
from
"./views/LoadingView.vue"
;
import
Loading
from
"./views/LoadingView.vue"
;
import
Rank1
from
"./views/Rank1View.vue"
;
import
Rank1
from
"./views/Rank1View.vue"
;
import
Rank2
from
"./views/Rank2View.vue"
;
import
Rank2
from
"./views/Rank2View.vue"
;
...
@@ -8,7 +8,7 @@ import { useAdminGameSocket } from "@/composables/useAdminGameSocket";
...
@@ -8,7 +8,7 @@ import { useAdminGameSocket } from "@/composables/useAdminGameSocket";
import
{
onSocketMessage
}
from
"@/commons/ws"
;
import
{
onSocketMessage
}
from
"@/commons/ws"
;
import
type
Player
from
"@/commons/player.ts"
;
import
type
Player
from
"@/commons/player.ts"
;
import
{
useRouter
}
from
'vue-router'
import
{
useRouter
}
from
'vue-router'
import
{
$confirm
,
$remove_socket_storage
,
$notify
,
$toast
}
from
"@/commons/utils.ts"
;
import
{
$confirm
,
$
is_local_mode
,
$
remove_socket_storage
,
$notify
,
$toast
}
from
"@/commons/utils.ts"
;
const
router
=
useRouter
()
const
router
=
useRouter
()
...
@@ -108,6 +108,9 @@ const gotoRank1WithIntro = async () => {
...
@@ -108,6 +108,9 @@ const gotoRank1WithIntro = async () => {
isPlayingStartAnimation
=
true
isPlayingStartAnimation
=
true
seedRankPlayersFromLoading
()
seedRankPlayersFromLoading
()
// 对齐 game3:进入赛马画面前确保背景音乐播放中
// 必须在 startIntro 之前调用,因为 startIntro 内部会 stopApplauseMusic() 误伤 BGM
playGame4Music
()
screen
.
value
=
'rank1'
screen
.
value
=
'rank1'
await
nextTick
()
await
nextTick
()
await
rank1Ref
.
value
?.
startIntro
()
await
rank1Ref
.
value
?.
startIntro
()
...
@@ -193,16 +196,37 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
...
@@ -193,16 +196,37 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
})
})
const
startGameWithMusic
=
()
=>
{
const
startGameWithMusic
=
()
=>
{
// 本地调试模式:跳过 WebSocket,直接进入赛马画面(对齐 game3)
// 注意:不在这里调 playGame4Music(),因为 onMounted 中已注册 pointerdown 监听器,
// 点击按钮时 pointerdown 事件先于 click 触发,会自动播放 BGM。
// 如果在此额外调用 playGame4Music(),会导致 stopAllMusic 销毁并重建 Audio,
// 在复杂时序下可能 play() 失败,需要再点一次页面才能听到音乐。
if
(
$is_local_mode
())
{
gotoRank1WithIntro
()
return
}
if
(
playerCount
.
value
==
0
)
{
if
(
playerCount
.
value
==
0
)
{
// alert('房间中没有玩家')
// alert('房间中没有玩家')
$toast
(
'房间中没有玩家'
);
$toast
(
'房间中没有玩家'
);
return
;
return
;
}
}
// 对齐 game3:点击"开始挑战"时确保背景音乐播放中
playGame4Music
()
startGame
()
startGame
()
}
}
const
nextRound
=
()
=>
{
const
nextRound
=
()
=>
{
// 立刻停止喝彩音乐并移除指针事件监听,防止点击按钮时重新触发 playApplauseMusic
stopApplauseMusic
()
if
(
$is_local_mode
())
{
// 本地模式:回到 loading 页,用户点击"开始挑战"后再进入赛马(对齐 game3)
gotoLoading
(
true
)
return
}
gotoLoading
(
true
)
gotoLoading
(
true
)
// 等待 Rank1View 卸载完成后再确保 BGM 播放(对齐 game3 的 watch 模式)
// 用 nextTick 确保在组件卸载、watch 触发的 BGM 之后再次保证
nextTick
().
then
(()
=>
playGame4Music
())
//重开房间
//重开房间
backRoom
(
4
)
backRoom
(
4
)
}
}
...
@@ -212,6 +236,13 @@ const nextRoundWithMusic = () => {
...
@@ -212,6 +236,13 @@ const nextRoundWithMusic = () => {
}
}
const
backHandler
=
async
()
=>
{
const
backHandler
=
async
()
=>
{
// 立刻停止喝彩音乐并移除指针事件监听,防止点击按钮时重新触发 playApplauseMusic
stopApplauseMusic
()
if
(
$is_local_mode
())
{
stopAllMusic
()
router
.
replace
(
'/main'
)
return
}
const
ok
=
await
$confirm
({
const
ok
=
await
$confirm
({
title
:
'提示'
,
title
:
'提示'
,
message
:
'是否关闭当前房间回到首页'
,
message
:
'是否关闭当前房间回到首页'
,
...
...
frontend-large/src/pages/game4/views/Rank1View.vue
View file @
d0bec691
...
@@ -2,7 +2,7 @@
...
@@ -2,7 +2,7 @@
import
{
ref
,
computed
,
watch
,
onMounted
,
onUnmounted
}
from
"vue"
;
import
{
ref
,
computed
,
watch
,
onMounted
,
onUnmounted
}
from
"vue"
;
import
DesignStage
from
"../../../components/DesignStage.vue"
;
import
DesignStage
from
"../../../components/DesignStage.vue"
;
import
type
Player
from
"@/commons/player.ts"
;
import
type
Player
from
"@/commons/player.ts"
;
import
{
$is_
run_local
}
from
"@/commons/utils.ts"
;
import
{
$is_
local_mode
}
from
"@/commons/utils.ts"
;
import
{
cssAssetUrl
,
assetUrl
}
from
"@/commons/assets.ts"
;
import
{
cssAssetUrl
,
assetUrl
}
from
"@/commons/assets.ts"
;
import
{
playApplauseMusic
,
stopApplauseMusic
}
from
"@/commons/music"
;
import
{
playApplauseMusic
,
stopApplauseMusic
}
from
"@/commons/music"
;
const
defaultAvatarUrl
=
assetUrl
(
"game4/default-avator.svg"
);
const
defaultAvatarUrl
=
assetUrl
(
"game4/default-avator.svg"
);
...
@@ -176,12 +176,26 @@ const updateHorses = (players: Player[]) => {
...
@@ -176,12 +176,26 @@ const updateHorses = (players: Player[]) => {
});
});
};
};
const
mockAvatars
=
[
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Felix'
,
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Aneka'
,
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Salem'
,
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Midnight'
,
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Lucky'
,
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Bandit'
,
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Misty'
,
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Pepper'
,
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Oreo'
,
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Tiger'
,
];
const
mockNicknames
=
[
'疾风骑士'
,
'闪电侠'
,
'追风者'
,
'千里马'
,
'飞毛腿'
,
'烈焰驹'
,
'暴风战士'
,
'神行者'
,
'银鞍客'
,
'踏雪无痕'
];
const
mockHorses
=
()
=>
{
const
mockHorses
=
()
=>
{
const
mockPlayers
:
Player
[]
=
Array
.
from
({
length
:
10
},
(
_
,
i
)
=>
({
const
mockPlayers
:
Player
[]
=
Array
.
from
({
length
:
10
},
(
_
,
i
)
=>
({
userid
:
`mock-
${
i
}
`
,
userid
:
`mock-
${
i
}
`
,
nickname
:
`玩家
${
i
+
1
}
`
,
nickname
:
mockNicknames
[
i
]
||
`玩家
${
i
+
1
}
`
,
avatar
:
""
,
avatar
:
mockAvatars
[
i
]
||
''
,
score
:
Math
.
floor
(
Math
.
random
()
*
1000
)
,
score
:
Math
.
floor
(
Math
.
random
()
*
60
)
+
10
,
}));
}));
updateHorses
(
mockPlayers
);
updateHorses
(
mockPlayers
);
};
};
...
@@ -294,15 +308,24 @@ const stopGameLoop = () => {
...
@@ -294,15 +308,24 @@ const stopGameLoop = () => {
let
mockBoostTimer
:
ReturnType
<
typeof
setInterval
>
|
undefined
;
let
mockBoostTimer
:
ReturnType
<
typeof
setInterval
>
|
undefined
;
const
startMockBoost
=
()
=>
{
const
startMockBoost
=
()
=>
{
stopMockBoost
();
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
;
});
mockBoostTimer
=
setInterval
(()
=>
{
mockBoostTimer
=
setInterval
(()
=>
{
if
(
!
isGameRunning
.
value
)
return
;
if
(
!
isGameRunning
.
value
)
return
;
// 随机选 1~3 匹马触发加速
const
updated
=
{
...
transientBoosts
.
value
};
const
count
=
1
+
Math
.
floor
(
Math
.
random
()
*
3
);
horses
.
value
.
forEach
((
horse
)
=>
{
const
shuffled
=
[...
horses
.
value
].
sort
(()
=>
Math
.
random
()
-
0.5
);
const
rate
=
horseRates
[
horse
.
id
]
??
(
0.3
+
Math
.
random
()
*
0.7
);
shuffled
.
slice
(
0
,
count
).
forEach
((
horse
)
=>
{
const
current
=
updated
[
horse
.
id
]
||
0
;
horse
.
boost
=
Math
.
min
(
3
,
horse
.
boost
+
0.3
+
Math
.
random
()
*
0.7
);
// 偶尔减速制造节奏变化
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
));
});
});
},
2000
);
transientBoosts
.
value
=
updated
;
},
1500
);
};
};
const
stopMockBoost
=
()
=>
{
const
stopMockBoost
=
()
=>
{
if
(
mockBoostTimer
)
{
if
(
mockBoostTimer
)
{
...
@@ -368,7 +391,7 @@ const stopGame = () => {
...
@@ -368,7 +391,7 @@ const stopGame = () => {
};
};
const
startIntro
=
async
()
=>
{
const
startIntro
=
async
()
=>
{
if
(
$is_
run_local
())
{
if
(
$is_
local_mode
())
{
mockHorses
();
mockHorses
();
}
}
showResult
.
value
=
false
;
showResult
.
value
=
false
;
...
@@ -380,8 +403,8 @@ const startIntro = async () => {
...
@@ -380,8 +403,8 @@ const startIntro = async () => {
transientBoosts
.
value
=
{};
transientBoosts
.
value
=
{};
Object
.
keys
(
prevScores
).
forEach
((
k
)
=>
delete
prevScores
[
k
]);
Object
.
keys
(
prevScores
).
forEach
((
k
)
=>
delete
prevScores
[
k
]);
// 启动完整倒计时:3s 开场 +
60
s 游戏(DesignStage 内部管理两个阶段的 UI)
// 启动完整倒计时:3s 开场 +
3
s 游戏(DesignStage 内部管理两个阶段的 UI)
const
countdownPromise
=
designStage
.
value
?.
startCountdown
(
3
,
60
);
const
countdownPromise
=
designStage
.
value
?.
startCountdown
(
3
,
3
);
// 等待 3-2-1 开场倒计时结束(~3.5s 后安全触发)
// 等待 3-2-1 开场倒计时结束(~3.5s 后安全触发)
await
new
Promise
((
r
)
=>
setTimeout
(
r
,
3500
));
await
new
Promise
((
r
)
=>
setTimeout
(
r
,
3500
));
...
@@ -390,7 +413,7 @@ const startIntro = async () => {
...
@@ -390,7 +413,7 @@ const startIntro = async () => {
gameStarted
.
value
=
true
;
gameStarted
.
value
=
true
;
isGameRunning
.
value
=
true
;
isGameRunning
.
value
=
true
;
startGameLoop
();
startGameLoop
();
if
(
$is_
run_local
())
{
if
(
$is_
local_mode
())
{
startMockBoost
();
startMockBoost
();
}
}
...
@@ -427,23 +450,23 @@ const renderAvatar = (item: any) => {
...
@@ -427,23 +450,23 @@ const renderAvatar = (item: any) => {
// 获取 podium 上三名选手(不足补空)
// 获取 podium 上三名选手(不足补空)
const
podiumPlayers
=
():
RankItem
[]
=>
{
const
podiumPlayers
=
():
RankItem
[]
=>
{
const
result
=
fullRankList
.
value
.
slice
(
0
,
3
);
const
result
=
fullRankList
.
value
.
slice
(
0
,
3
);
// 按 podium 视觉顺序:2 1 3
// 按 podium 视觉顺序:2 1 3
(第二名在左,第三名在右)
return
[
result
[
1
]
!
,
result
[
0
]
!
,
result
[
2
]
!
];
return
[
result
[
1
]
!
,
result
[
0
]
!
,
result
[
2
]
!
];
};
};
onMounted
(()
=>
{
onMounted
(()
=>
{
preloadHorseSprite
();
preloadHorseSprite
();
if
(
$is_run_local
())
{
// 本地模式:Game4.vue onMounted 会调用 startIntro() 来控制完整流程
mockHorses
();
// 此处不自动开始,避免与 startIntro 冲突
gameStarted
.
value
=
true
;
isGameRunning
.
value
=
true
;
startGameLoop
();
startMockBoost
();
}
});
});
onUnmounted
(()
=>
{
onUnmounted
(()
=>
{
stopGame
();
// 仅清理游戏资源,不操作音乐(音乐由 Game4.vue 统一管理)
// 注意:不能调 stopApplauseMusic(),因为 backgroundMusic 是全局共享的,
// 组件卸载时 Game4.vue 的 watch 可能已将其切换为 BGM,pause 会误伤 BGM
isGameRunning
.
value
=
false
;
stopGameLoop
();
stopMockBoost
();
});
});
defineExpose
({
defineExpose
({
...
@@ -493,7 +516,7 @@ defineExpose({
...
@@ -493,7 +516,7 @@ defineExpose({
<div
<div
v-for=
"(item, idx) in podiumPlayers()"
v-for=
"(item, idx) in podiumPlayers()"
:key=
"item.userid || `podium-${idx}`"
:key=
"item.userid || `podium-${idx}`"
:class=
"['rank-no', `rank-no${idx === 1 ? 1 : idx === 0 ?
2 : 3
}`]"
:class=
"['rank-no', `rank-no${idx === 1 ? 1 : idx === 0 ?
3 : 2
}`]"
>
>
<div
class=
"gu-burst"
></div>
<div
class=
"gu-burst"
></div>
<div
class=
"avatar"
:style=
"renderAvatar(item)"
></div>
<div
class=
"avatar"
:style=
"renderAvatar(item)"
></div>
...
@@ -778,7 +801,7 @@ defineExpose({
...
@@ -778,7 +801,7 @@ defineExpose({
}
}
}
}
/* 第2名
(原 rank-no3 样式)
*/
/* 第2名 */
.rank-no2
{
.rank-no2
{
.gu-burst
{
left
:
908px
;
top
:
-81px
;
}
.gu-burst
{
left
:
908px
;
top
:
-81px
;
}
.avatar
{
.avatar
{
...
@@ -787,7 +810,7 @@ defineExpose({
...
@@ -787,7 +810,7 @@ defineExpose({
}
}
.nickname
{
.nickname
{
width
:
167px
;
height
:
46px
;
width
:
167px
;
height
:
46px
;
background
:
#
E2E2E2
;
color
:
#AFAFAF
;
background
:
#
CA9600
;
color
:
white
;
text-align
:
center
;
line-height
:
46px
;
text-align
:
center
;
line-height
:
46px
;
position
:
absolute
;
left
:
897px
;
top
:
136px
;
position
:
absolute
;
left
:
897px
;
top
:
136px
;
z-index
:
1
;
border-radius
:
23px
;
z-index
:
1
;
border-radius
:
23px
;
...
@@ -804,7 +827,7 @@ defineExpose({
...
@@ -804,7 +827,7 @@ defineExpose({
}
}
}
}
/* 第3名
(原 rank-no2 样式)
*/
/* 第3名 */
.rank-no3
{
.rank-no3
{
.gu-burst
{
left
:
318px
;
top
:
-120px
;
}
.gu-burst
{
left
:
318px
;
top
:
-120px
;
}
.avatar
{
.avatar
{
...
@@ -813,7 +836,7 @@ defineExpose({
...
@@ -813,7 +836,7 @@ defineExpose({
}
}
.nickname
{
.nickname
{
width
:
167px
;
height
:
46px
;
width
:
167px
;
height
:
46px
;
background
:
#
CA9600
;
color
:
white
;
background
:
#
E2E2E2
;
color
:
#AFAFAF
;
text-align
:
center
;
line-height
:
46px
;
text-align
:
center
;
line-height
:
46px
;
position
:
absolute
;
top
:
95px
;
left
:
309px
;
position
:
absolute
;
top
:
95px
;
left
:
309px
;
z-index
:
1
;
border-radius
:
23px
;
z-index
:
1
;
border-radius
:
23px
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment