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
00dea6cc
Commit
00dea6cc
authored
Jun 12, 2026
by
陈冲
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'main' of
http://git.ruanyiit.com/chenchong/fc-minigame
parents
e3a36f04
9ac30b53
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
155 additions
and
53 deletions
+155
-53
PlayingView.vue
frontend-h5/src/pages/game4/views/PlayingView.vue
+4
-3
Game5.vue
frontend-h5/src/pages/game5/Game5.vue
+4
-3
PlayingView.vue
frontend-h5/src/pages/game5/views/PlayingView.vue
+16
-11
music.ts
frontend-large/src/commons/music.ts
+31
-6
Game4.vue
frontend-large/src/pages/game4/Game4.vue
+6
-11
Rank1View.vue
frontend-large/src/pages/game4/views/Rank1View.vue
+5
-6
Game5.vue
frontend-large/src/pages/game5/Game5.vue
+53
-6
Rank1View.vue
frontend-large/src/pages/game5/views/Rank1View.vue
+36
-7
No files found.
frontend-h5/src/pages/game4/views/PlayingView.vue
View file @
00dea6cc
...
@@ -207,9 +207,10 @@ const onTapHorse = async () => {
...
@@ -207,9 +207,10 @@ const onTapHorse = async () => {
<Transition
name=
"ios-hint-fade"
>
<Transition
name=
"ios-hint-fade"
>
<div
v-if=
"showIOSHint"
class=
"ios-hint-overlay"
@
click=
"onTapHorse"
>
<div
v-if=
"showIOSHint"
class=
"ios-hint-overlay"
@
click=
"onTapHorse"
>
<div
class=
"ios-hint-content"
>
<div
class=
"ios-hint-content"
>
<div
class=
"ios-hint-title"
>
👇点击下方赛马
</div>
<!--
<div
class=
"ios-hint-title"
>
👇点击下方赛马
</div>
-->
<div
class=
"ios-hint-desc"
>
启用摇一摇功能开始游戏
</div>
<!--
<div
class=
"ios-hint-desc"
>
请先点击屏幕,启动摇一摇功能
</div>
-->
<div
class=
"ios-hint-arrow"
>
⬇
</div>
<div
class=
"ios-hint-title"
>
请先点击屏幕,启动摇一摇功能
</div>
<!--
<div
class=
"ios-hint-arrow"
>
⬇
</div>
-->
</div>
</div>
</div>
</div>
</Transition>
</Transition>
...
...
frontend-h5/src/pages/game5/Game5.vue
View file @
00dea6cc
...
@@ -241,7 +241,8 @@ const hitHandler = (score: number) => {
...
@@ -241,7 +241,8 @@ const hitHandler = (score: number) => {
tick
.
value
+=
score
tick
.
value
+=
score
console
.
log
(
'[WebSocket] 圈中马!得分:'
,
score
,
'当前总分:'
,
tick
.
value
)
console
.
log
(
'[WebSocket] 圈中马!得分:'
,
score
,
'当前总分:'
,
tick
.
value
)
submitScore
()
submitScore
()
playGame1Music
()
// console.log('[Audio] playGame1Music() 被调用 - 播放 game1.mp3 背景音乐')
// playGame1Music()
}
}
if
(
wechat
)
{
if
(
wechat
)
{
...
@@ -388,8 +389,8 @@ const rankCloseHandler = () => {
...
@@ -388,8 +389,8 @@ const rankCloseHandler = () => {
</
template
>
</
template
>
<LoadingView
v-if=
"currentView === 'loading'"
:image-urls=
"imageUrls"
:user-join-status=
"userJoinStatus"
<LoadingView
v-if=
"currentView === 'loading'"
:image-urls=
"imageUrls"
:user-join-status=
"userJoinStatus"
@
touchGameRule=
"showGameRuleHandler"
/>
@
touchGameRule=
"showGameRuleHandler"
/>
<!-- <PlayingView v-else :image-urls="imageUrls" :tick="tick" :rank="rank" -->
<!-- <PlayingView v-else :image-urls="imageUrls" :tick="tick" :rank="rank" -->
<PlayingView
v-else-if=
"currentView === 'playing'"
:image-urls=
"imageUrls"
:tick=
"tick"
:rank=
"rank"
<PlayingView
v-else-if=
"currentView === 'playing'"
:image-urls=
"imageUrls"
:tick=
"tick"
:rank=
"rank"
:current-gu=
"currentGu"
:countdown-interval=
"countdownInterval"
:is-div-desc-visible=
"isDivDescVisible"
:current-gu=
"currentGu"
:countdown-interval=
"countdownInterval"
:is-div-desc-visible=
"isDivDescVisible"
@
hit=
"hitHandler"
/>
@
hit=
"hitHandler"
/>
<!-- <ScoreView v-else :image-urls="imageUrls" :rank="rank" :tick="tick" /> -->
<!-- <ScoreView v-else :image-urls="imageUrls" :rank="rank" :tick="tick" /> -->
...
...
frontend-h5/src/pages/game5/views/PlayingView.vue
View file @
00dea6cc
...
@@ -39,13 +39,13 @@ const CIRCLE_THROW_MS = 1200 // 圈圈飞行时长
...
@@ -39,13 +39,13 @@ const CIRCLE_THROW_MS = 1200 // 圈圈飞行时长
const
HIT_FADE_MS
=
400
// 被套中后马消失延迟
const
HIT_FADE_MS
=
400
// 被套中后马消失延迟
// ── 难度递增参数:随游戏时间从 0→60s 指数曲线变化 ──
// ── 难度递增参数:随游戏时间从 0→60s 指数曲线变化 ──
const
HORSE_SPEED_MIN
=
80
// px/s 初始速度
const
HORSE_SPEED_MIN
=
120
// px/s 初始速度(加快)
const
HORSE_SPEED_MAX
=
2
00
// px/s 最大速度(60s 时,7倍提升
)
const
HORSE_SPEED_MAX
=
2
50
// px/s 最大速度(提升上限
)
const
SPAWN_MIN_START
=
3000
// ms 初始最短生成间隔
const
SPAWN_MIN_START
=
1500
// ms 初始最短生成间隔(缩短)
const
SPAWN_MIN_END
=
800
// ms 最终最短生成间隔
const
SPAWN_MIN_END
=
500
// ms 最终最短生成间隔(缩短)
const
SPAWN_MAX_START
=
6000
// ms 初始最长生成间隔
const
SPAWN_MAX_START
=
3000
// ms 初始最长生成间隔(缩短)
const
SPAWN_MAX_END
=
1
800
// ms 最终最长生成间隔
const
SPAWN_MAX_END
=
1
000
// ms 最终最长生成间隔(缩短)
const
MAX_HORSES_ON_SCREEN
=
8
// 场上最多马匹数
const
MAX_HORSES_ON_SCREEN
=
10
// 场上最多马匹数(增加)
// 马动画帧率控制(参考 Rank1View:每 N 个 raf tick 切一帧 → ~20fps)
// 马动画帧率控制(参考 Rank1View:每 N 个 raf tick 切一帧 → ~20fps)
const
HORSE_FRAME_TICK
=
3
const
HORSE_FRAME_TICK
=
3
...
@@ -125,6 +125,7 @@ function getAudioContext(): AudioContext {
...
@@ -125,6 +125,7 @@ function getAudioContext(): AudioContext {
/** 丢圈"咻"声 — 短促上升频率扫频 */
/** 丢圈"咻"声 — 短促上升频率扫频 */
function
playThrowSound
()
{
function
playThrowSound
()
{
console
.
log
(
'[Audio] playThrowSound - 丢圈音效'
)
try
{
try
{
const
ctx
=
getAudioContext
()
const
ctx
=
getAudioContext
()
const
now
=
ctx
.
currentTime
const
now
=
ctx
.
currentTime
...
@@ -140,11 +141,13 @@ function playThrowSound() {
...
@@ -140,11 +141,13 @@ function playThrowSound() {
gain
.
gain
.
exponentialRampToValueAtTime
(
0.01
,
now
+
0.2
)
gain
.
gain
.
exponentialRampToValueAtTime
(
0.01
,
now
+
0.2
)
osc
.
start
(
now
)
osc
.
start
(
now
)
osc
.
stop
(
now
+
0.2
)
osc
.
stop
(
now
+
0.2
)
}
catch
{
/* ignore audio errors */
}
console
.
log
(
'[Audio] playThrowSound - 音效开始播放, 持续0.2s'
)
}
catch
(
e
)
{
console
.
error
(
'[Audio] playThrowSound error'
,
e
);
}
}
}
/** 套中"叮"声 — 两个简短的上升音 */
/** 套中"叮"声 — 两个简短的上升音 */
function
playHitSound
()
{
function
playHitSound
()
{
console
.
log
(
'[Audio] playHitSound - 套中马音效(叮声)'
)
try
{
try
{
const
ctx
=
getAudioContext
()
const
ctx
=
getAudioContext
()
const
now
=
ctx
.
currentTime
const
now
=
ctx
.
currentTime
...
@@ -159,6 +162,7 @@ function playHitSound() {
...
@@ -159,6 +162,7 @@ function playHitSound() {
g1
.
gain
.
exponentialRampToValueAtTime
(
0.01
,
now
+
0.25
)
g1
.
gain
.
exponentialRampToValueAtTime
(
0.01
,
now
+
0.25
)
o1
.
start
(
now
)
o1
.
start
(
now
)
o1
.
stop
(
now
+
0.25
)
o1
.
stop
(
now
+
0.25
)
console
.
log
(
'[Audio] playHitSound - 第一音 880Hz 开始, 持续0.25s'
)
// 第二音 1320 Hz 延时 0.08s
// 第二音 1320 Hz 延时 0.08s
const
o2
=
ctx
.
createOscillator
()
const
o2
=
ctx
.
createOscillator
()
const
g2
=
ctx
.
createGain
()
const
g2
=
ctx
.
createGain
()
...
@@ -170,7 +174,8 @@ function playHitSound() {
...
@@ -170,7 +174,8 @@ function playHitSound() {
g2
.
gain
.
exponentialRampToValueAtTime
(
0.01
,
now
+
0.35
)
g2
.
gain
.
exponentialRampToValueAtTime
(
0.01
,
now
+
0.35
)
o2
.
start
(
now
+
0.08
)
o2
.
start
(
now
+
0.08
)
o2
.
stop
(
now
+
0.35
)
o2
.
stop
(
now
+
0.35
)
}
catch
{
/* ignore audio errors */
}
console
.
log
(
'[Audio] playHitSound - 第二音 1320Hz 开始, 持续0.35s (延时0.08s)'
)
}
catch
(
e
)
{
console
.
error
(
'[Audio] playHitSound error'
,
e
);
}
}
}
// ── 清理全部音效(组件卸载时) ──
// ── 清理全部音效(组件卸载时) ──
...
@@ -358,8 +363,8 @@ onMounted(() => {
...
@@ -358,8 +363,8 @@ onMounted(() => {
// MobileStage 已完成 3-2-1,游戏直接开始
// MobileStage 已完成 3-2-1,游戏直接开始
prevTs
=
performance
.
now
()
prevTs
=
performance
.
now
()
spawnHorse
()
spawnHorse
()
setTimeout
(
spawnHorse
,
800
)
setTimeout
(
spawnHorse
,
300
)
// 更早生成第二匹马
setTimeout
(
spawnHorse
,
1600
)
setTimeout
(
spawnHorse
,
600
)
// 更早生成第三匹马
raf
=
requestAnimationFrame
(
loop
)
raf
=
requestAnimationFrame
(
loop
)
})
})
...
...
frontend-large/src/commons/music.ts
View file @
00dea6cc
...
@@ -26,18 +26,25 @@ export function stopAllMusic() {
...
@@ -26,18 +26,25 @@ export function stopAllMusic() {
backgroundMusic
=
null
;
backgroundMusic
=
null
;
}
}
function
getBackgroundMusic
(
music
:
string
,
force
:
boolean
=
true
)
{
function
getBackgroundMusic
(
music
:
string
,
force
:
boolean
=
false
)
{
if
(
force
)
{
//
console
.
log
(
'[Music] getBackgroundMusic called, music:'
,
music
,
'force:'
,
force
,
'current backgroundMusic:'
,
backgroundMusic
?.
src
)
// 检查当前背景音乐是否是同一首(比较路径末尾,处理相对路径和完整URL的情况)
const
isSameMusic
=
backgroundMusic
&&
(
backgroundMusic
.
src
===
music
||
backgroundMusic
.
src
.
endsWith
(
music
));
// 只有在需要切换不同音乐或强制重建时才停止并重建
if
(
force
||
(
!
backgroundMusic
||
!
isSameMusic
))
{
console
.
log
(
'[Music] 重建背景音乐, 原因:'
,
force
?
'强制重建'
:
backgroundMusic
?
'音乐URL变更'
:
'无现有音乐'
)
if
(
backgroundMusic
)
{
if
(
backgroundMusic
)
{
stopAllMusic
();
stopAllMusic
();
}
}
}
if
(
!
backgroundMusic
)
{
backgroundMusic
=
new
Audio
(
music
)
backgroundMusic
=
new
Audio
(
music
)
backgroundMusic
.
loop
=
true
backgroundMusic
.
loop
=
true
backgroundMusic
.
preload
=
'auto'
backgroundMusic
.
preload
=
'auto'
backgroundMusic
.
volume
=
0.6
backgroundMusic
.
volume
=
0.6
backgroundMusic
.
load
()
backgroundMusic
.
load
()
console
.
log
(
'[Music] 新背景音乐创建完成:'
,
backgroundMusic
.
src
)
}
}
return
backgroundMusic
return
backgroundMusic
...
@@ -60,7 +67,13 @@ export async function playApplauseMusic() {
...
@@ -60,7 +67,13 @@ export async function playApplauseMusic() {
export
function
stopApplauseMusic
()
{
export
function
stopApplauseMusic
()
{
window
.
removeEventListener
(
'pointerdown'
,
playApplauseMusic
)
window
.
removeEventListener
(
'pointerdown'
,
playApplauseMusic
)
window
.
removeEventListener
(
'keydown'
,
playApplauseMusic
)
window
.
removeEventListener
(
'keydown'
,
playApplauseMusic
)
backgroundMusic
?.
pause
()
// 只有当当前播放的是喝彩音乐时才暂停,避免误伤背景音乐
if
(
backgroundMusic
&&
backgroundMusic
.
src
.
endsWith
(
applause
))
{
console
.
log
(
'[Music] 停止喝彩音乐'
)
backgroundMusic
.
pause
()
}
else
{
console
.
log
(
'[Music] stopApplauseMusic 被调用,但当前播放的不是喝彩音乐,不暂停'
)
}
}
}
...
@@ -103,13 +116,25 @@ export function stopGame2Music() {
...
@@ -103,13 +116,25 @@ export function stopGame2Music() {
}
}
export
async
function
playGame4Music
()
{
export
async
function
playGame4Music
()
{
console
.
log
(
'[Music] playGame4Music called'
)
const
audio
=
getBackgroundMusic
(
game4MusicUrl
)
const
audio
=
getBackgroundMusic
(
game4MusicUrl
)
console
.
log
(
'[Music] 播放音乐文件:'
,
game4MusicUrl
,
'完整路径:'
,
audio
.
src
)
console
.
log
(
'[Music] audio element readyState:'
,
audio
.
readyState
,
'paused:'
,
audio
.
paused
,
'currentTime:'
,
audio
.
currentTime
)
try
{
try
{
console
.
log
(
'[Music] 尝试播放音乐...'
)
await
audio
.
play
()
await
audio
.
play
()
console
.
log
(
'[Music] 音乐播放成功!文件:'
,
game4MusicUrl
,
'paused:'
,
audio
.
paused
,
'currentTime:'
,
audio
.
currentTime
,
'duration:'
,
audio
.
duration
)
// 添加延迟检查,看是否被其他代码暂停
setTimeout
(()
=>
{
console
.
log
(
'[Music] 100ms后检查 - 文件:'
,
game4MusicUrl
,
'paused:'
,
audio
.
paused
,
'currentTime:'
,
audio
.
currentTime
)
},
100
)
window
.
removeEventListener
(
'pointerdown'
,
playGame4Music
)
window
.
removeEventListener
(
'pointerdown'
,
playGame4Music
)
window
.
removeEventListener
(
'keydown'
,
playGame4Music
)
window
.
removeEventListener
(
'keydown'
,
playGame4Music
)
}
catch
{
}
catch
(
error
)
{
console
.
log
(
'[Music] 音乐播放失败,注册交互监听器:'
,
error
)
window
.
addEventListener
(
'pointerdown'
,
playGame4Music
,
{
once
:
true
})
window
.
addEventListener
(
'pointerdown'
,
playGame4Music
,
{
once
:
true
})
window
.
addEventListener
(
'keydown'
,
playGame4Music
,
{
once
:
true
})
window
.
addEventListener
(
'keydown'
,
playGame4Music
,
{
once
:
true
})
}
}
...
...
frontend-large/src/pages/game4/Game4.vue
View file @
00dea6cc
...
@@ -108,9 +108,7 @@ const gotoRank1WithIntro = async () => {
...
@@ -108,9 +108,7 @@ const gotoRank1WithIntro = async () => {
isPlayingStartAnimation
=
true
isPlayingStartAnimation
=
true
seedRankPlayersFromLoading
()
seedRankPlayersFromLoading
()
// 对齐 game3:进入赛马画面前确保背景音乐播放中
// 参考 game2:不在这里调用 playGame4Music(),依赖 watch 和 pointerdown 监听器触发
// 必须在 startIntro 之前调用,因为 startIntro 内部会 stopApplauseMusic() 误伤 BGM
playGame4Music
()
screen
.
value
=
'rank1'
screen
.
value
=
'rank1'
await
nextTick
()
await
nextTick
()
await
rank1Ref
.
value
?.
startIntro
()
await
rank1Ref
.
value
?.
startIntro
()
...
@@ -196,22 +194,19 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
...
@@ -196,22 +194,19 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
})
})
const
startGameWithMusic
=
()
=>
{
const
startGameWithMusic
=
()
=>
{
// 本地调试模式:跳过 WebSocket,直接进入赛马画面(对齐 game3)
console
.
log
(
'[Game4] startGameWithMusic called, local mode:'
,
$is_local_mode
(),
'playerCount:'
,
playerCount
.
value
)
// 注意:不在这里调 playGame4Music(),因为 onMounted 中已注册 pointerdown 监听器,
// 确保背景音乐播放(参考 game2 的 watch 模式,但直接在点击时调用更可靠)
// 点击按钮时 pointerdown 事件先于 click 触发,会自动播放 BGM。
playGame4Music
()
// 如果在此额外调用 playGame4Music(),会导致 stopAllMusic 销毁并重建 Audio,
// 在复杂时序下可能 play() 失败,需要再点一次页面才能听到音乐。
if
(
$is_local_mode
())
{
if
(
$is_local_mode
())
{
console
.
log
(
'[Game4] 本地模式,直接进入赛马画面'
)
gotoRank1WithIntro
()
gotoRank1WithIntro
()
return
return
}
}
if
(
playerCount
.
value
==
0
)
{
if
(
playerCount
.
value
==
0
)
{
// alert('房间中没有玩家')
$toast
(
'房间中没有玩家'
);
$toast
(
'房间中没有玩家'
);
return
;
return
;
}
}
// 对齐 game3:点击"开始挑战"时确保背景音乐播放中
console
.
log
(
'[Game4] 调用 startGame()'
)
playGame4Music
()
startGame
()
startGame
()
}
}
...
...
frontend-large/src/pages/game4/views/Rank1View.vue
View file @
00dea6cc
...
@@ -335,15 +335,14 @@ const stopMockBoost = () => {
...
@@ -335,15 +335,14 @@ const stopMockBoost = () => {
};
};
const
computeFinalRank
=
():
RankItem
[]
=>
{
const
computeFinalRank
=
():
RankItem
[]
=>
{
// 从 WebSocket rankList 中取有效玩家
// 从 WebSocket rankList 中取有效玩家
(不排序,直接使用 WebSocket 返回的顺序)
const
valid
=
(
props
.
rankList
||
[]).
filter
(
const
valid
=
(
props
.
rankList
||
[]).
filter
(
(
p
)
=>
p
.
nickname
!==
"虚位以待"
&&
!
(
p
.
wechat_id
||
""
).
startsWith
(
"empty-"
),
(
p
)
=>
p
.
nickname
!==
"虚位以待"
&&
!
(
p
.
wechat_id
||
""
).
startsWith
(
"empty-"
),
);
);
if
(
valid
.
length
>
0
)
{
if
(
valid
.
length
>
0
)
{
// 优先用 WebSocket 的 score 排序
// 直接使用 WebSocket 返回的顺序,不进行排序
const
sorted
=
[...
valid
].
sort
((
a
,
b
)
=>
Number
(
b
.
score
??
0
)
-
Number
(
a
.
score
??
0
));
return
valid
.
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
,
...
@@ -352,7 +351,7 @@ const computeFinalRank = (): RankItem[] => {
...
@@ -352,7 +351,7 @@ const computeFinalRank = (): RankItem[] => {
}));
}));
}
}
// 无 WebSocket 数据时降级:按 xPercent 降序
// 无 WebSocket 数据时降级:按 xPercent 降序
(本地模拟模式)
return
horses
.
value
return
horses
.
value
.
slice
()
.
slice
()
.
sort
((
a
,
b
)
=>
b
.
xPercent
-
a
.
xPercent
)
.
sort
((
a
,
b
)
=>
b
.
xPercent
-
a
.
xPercent
)
...
@@ -404,7 +403,7 @@ const startIntro = async () => {
...
@@ -404,7 +403,7 @@ const startIntro = async () => {
Object
.
keys
(
prevScores
).
forEach
((
k
)
=>
delete
prevScores
[
k
]);
Object
.
keys
(
prevScores
).
forEach
((
k
)
=>
delete
prevScores
[
k
]);
// 启动完整倒计时:3s 开场 + 3s 游戏(DesignStage 内部管理两个阶段的 UI)
// 启动完整倒计时:3s 开场 + 3s 游戏(DesignStage 内部管理两个阶段的 UI)
const
countdownPromise
=
designStage
.
value
?.
startCountdown
(
3
,
3
);
const
countdownPromise
=
designStage
.
value
?.
startCountdown
(
3
,
60
);
// 等待 3-2-1 开场倒计时结束(~3.5s 后安全触发)
// 等待 3-2-1 开场倒计时结束(~3.5s 后安全触发)
await
new
Promise
((
r
)
=>
setTimeout
(
r
,
3500
));
await
new
Promise
((
r
)
=>
setTimeout
(
r
,
3500
));
...
...
frontend-large/src/pages/game5/Game5.vue
View file @
00dea6cc
...
@@ -6,8 +6,8 @@ import Rank1 from './views/Rank1View.vue'
...
@@ -6,8 +6,8 @@ import Rank1 from './views/Rank1View.vue'
import
Rank2
from
'./views/Rank2View.vue'
import
Rank2
from
'./views/Rank2View.vue'
import
{
useAdminGameSocket
}
from
'@/composables/useAdminGameSocket'
import
{
useAdminGameSocket
}
from
'@/composables/useAdminGameSocket'
import
type
Player
from
'@/commons/player.ts'
import
type
Player
from
'@/commons/player.ts'
import
{
playApplauseMusic
,
playGame5Music
,
stopAllMusic
,
stopGame5Music
}
from
'@/commons/music'
import
{
playApplauseMusic
,
playGame5Music
,
stopAllMusic
,
stop
ApplauseMusic
,
stop
Game5Music
}
from
'@/commons/music'
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
()
...
@@ -33,7 +33,25 @@ const emptyRankPlayer = (index: number): Player => ({
...
@@ -33,7 +33,25 @@ const emptyRankPlayer = (index: number): Player => ({
})
})
const
screen
=
ref
<
GameScreen
>
(
'loading'
)
const
screen
=
ref
<
GameScreen
>
(
'loading'
)
const
loadingPlayers
=
ref
<
any
[]
>
(
Array
.
from
({
length
:
PLAYER_LIMIT
},
emptyLoadingPlayer
))
// 本地模式下使用模拟玩家数据
const
mockPlayers
=
[
{
userid
:
'local-1'
,
nickname
:
'疾风骑士'
,
avatar
:
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Felix'
,
score
:
45
},
{
userid
:
'local-2'
,
nickname
:
'闪电侠'
,
avatar
:
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Aneka'
,
score
:
38
},
{
userid
:
'local-3'
,
nickname
:
'追风者'
,
avatar
:
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Salem'
,
score
:
30
},
{
userid
:
'local-4'
,
nickname
:
'千里马'
,
avatar
:
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Midnight'
,
score
:
22
},
{
userid
:
'local-5'
,
nickname
:
'飞毛腿'
,
avatar
:
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Lucky'
,
score
:
15
},
{
userid
:
'local-6'
,
nickname
:
'烈焰驹'
,
avatar
:
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Bandit'
,
score
:
52
},
{
userid
:
'local-7'
,
nickname
:
'暴风战士'
,
avatar
:
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Misty'
,
score
:
41
},
{
userid
:
'local-8'
,
nickname
:
'神行者'
,
avatar
:
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Pepper'
,
score
:
35
},
{
userid
:
'local-9'
,
nickname
:
'银鞍客'
,
avatar
:
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Oreo'
,
score
:
28
},
{
userid
:
'local-10'
,
nickname
:
'踏雪无痕'
,
avatar
:
'https://api.dicebear.com/9.x/fun-emoji/svg?seed=Tiger'
,
score
:
18
},
];
const
loadingPlayers
=
ref
<
any
[]
>
(
$is_local_mode
()
?
mockPlayers
:
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
rank1Ref
=
ref
<
InstanceType
<
typeof
Rank1
>
|
null
>
(
null
)
const
rank1Ref
=
ref
<
InstanceType
<
typeof
Rank1
>
|
null
>
(
null
)
...
@@ -108,6 +126,8 @@ const gotoRank1WithIntro = async () => {
...
@@ -108,6 +126,8 @@ const gotoRank1WithIntro = async () => {
isPlayingStartAnimation
=
true
isPlayingStartAnimation
=
true
seedRankPlayersFromLoading
()
seedRankPlayersFromLoading
()
// 进入游戏画面前确保背景音乐播放中
playGame5Music
()
screen
.
value
=
'rank1'
screen
.
value
=
'rank1'
await
nextTick
()
await
nextTick
()
await
rank1Ref
.
value
?.
startIntro
()
await
rank1Ref
.
value
?.
startIntro
()
...
@@ -162,23 +182,41 @@ const { startGame, createRoom, closeRoom, backRoom } = useAdminGameSocket({
...
@@ -162,23 +182,41 @@ const { startGame, createRoom, closeRoom, backRoom } = useAdminGameSocket({
})
})
const
startGameWithMusic
=
()
=>
{
const
startGameWithMusic
=
()
=>
{
// 确保背景音乐播放
playGame5Music
()
// 本地调试模式:跳过 WebSocket,直接进入游戏画面
if
(
$is_local_mode
())
{
console
.
log
(
'[Game5] 本地模式,直接进入游戏画面'
)
gotoRank1WithIntro
()
return
}
if
(
playerCount
.
value
==
0
)
{
if
(
playerCount
.
value
==
0
)
{
// alert('房间中没有玩家')
$toast
(
'房间中没有玩家'
);
$toast
(
'房间中没有玩家'
);
return
;
return
;
}
}
playGame5Music
()
startGame
()
startGame
()
}
}
const
nextRound
=
()
=>
{
const
nextRound
=
()
=>
{
// 立刻停止喝彩音乐并移除指针事件监听,防止点击按钮时重新触发 playApplauseMusic
stopApplauseMusic
()
if
(
$is_local_mode
())
{
// 本地模式:回到 loading 页,用户点击"开始挑战"后再进入游戏
gotoLoading
(
true
)
return
}
gotoLoading
(
true
)
gotoLoading
(
true
)
// 等待组件卸载完成后再确保 BGM 播放
nextTick
().
then
(()
=>
playGame5Music
())
//重开房间
//重开房间
backRoom
(
5
)
backRoom
(
5
)
}
}
const
nextRoundWithMusic
=
()
=>
{
const
nextRoundWithMusic
=
()
=>
{
playGame5Music
()
nextRound
()
nextRound
()
}
}
...
@@ -193,6 +231,15 @@ onUnmounted(() => {
...
@@ -193,6 +231,15 @@ onUnmounted(() => {
})
})
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/game5/views/Rank1View.vue
View file @
00dea6cc
...
@@ -38,7 +38,7 @@ const imageUrls = {
...
@@ -38,7 +38,7 @@ const imageUrls = {
const
HORSE_TYPES
=
[
1
,
2
,
3
,
4
]
as
const
;
const
HORSE_TYPES
=
[
1
,
2
,
3
,
4
]
as
const
;
const
HORSE_FRAME_COUNT
=
21
;
// 每匹马 21 帧(00-20)
const
HORSE_FRAME_COUNT
=
21
;
// 每匹马 21 帧(00-20)
const
HORSE_WIDTH
=
200
;
const
HORSE_WIDTH
=
200
;
const
HORSE_START_X
=
-
330
;
// 左侧屏外起点
const
HORSE_START_X
=
-
100
;
// 左侧屏外起点,进一步右移让马更快进入屏幕
const
HORSE_END_X
=
1920
-
HORSE_WIDTH
;
// 右边缘贴齐屏幕右侧(1720px)
const
HORSE_END_X
=
1920
-
HORSE_WIDTH
;
// 右边缘贴齐屏幕右侧(1720px)
...
@@ -67,9 +67,9 @@ let horseIdCounter = 0;
...
@@ -67,9 +67,9 @@ let horseIdCounter = 0;
const
isGameRunning
=
ref
(
false
);
const
isGameRunning
=
ref
(
false
);
const
emptyPlayer
=
(
index
:
number
):
Player
=>
({
const
emptyPlayer
=
(
index
:
number
):
Player
=>
({
wechat_id
:
`empty-
${
index
}
`
,
wechat_id
:
`empty-
${
index
}
`
,
nickname
:
"
-
"
,
nickname
:
"
虚位以待
"
,
avatar
:
""
,
avatar
:
""
,
score
:
0
,
score
:
"-"
,
});
});
const
list
=
ref
<
Player
[]
>
(
const
list
=
ref
<
Player
[]
>
(
...
@@ -210,8 +210,8 @@ const generateRandomHorses = () => {
...
@@ -210,8 +210,8 @@ const generateRandomHorses = () => {
for
(
let
i
=
0
;
i
<
count
;
i
++
)
{
for
(
let
i
=
0
;
i
<
count
;
i
++
)
{
const
type
=
HORSE_TYPES
[
i
%
HORSE_TYPES
.
length
]
!
;
const
type
=
HORSE_TYPES
[
i
%
HORSE_TYPES
.
length
]
!
;
const
lane
=
lanes
[
i
%
2
]
!
;
const
lane
=
lanes
[
i
%
2
]
!
;
const
delayMs
=
i
*
2500
+
Math
.
random
()
*
1000
;
// 每匹马间隔2.5秒起步,加随机1秒
const
delayMs
=
i
*
50
+
Math
.
random
()
*
50
;
// 大幅减少起步延迟,第一匹马几乎立刻出现
const
durationMs
=
(
40
+
Math
.
random
()
*
40
)
*
1000
;
// 40-80秒,速度浮动更大
const
durationMs
=
(
60
+
Math
.
random
()
*
40
)
*
1000
;
// 60-100秒,降低速度,让马跑得更慢
newHorses
.
push
({
newHorses
.
push
({
id
:
`horse-
${
++
horseIdCounter
}
`
,
id
:
`horse-
${
++
horseIdCounter
}
`
,
horseType
:
type
,
horseType
:
type
,
...
@@ -227,6 +227,33 @@ const generateRandomHorses = () => {
...
@@ -227,6 +227,33 @@ const generateRandomHorses = () => {
horses
.
value
=
newHorses
;
horses
.
value
=
newHorses
;
};
};
// ========== 本地模拟数据(参考 game3)==========
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
mockPlayers
:
Player
[]
=
[
{
userid
:
'local-1'
,
nickname
:
'疾风骑士'
,
avatar
:
mockAvatars
[
0
],
score
:
45
},
{
userid
:
'local-2'
,
nickname
:
'闪电侠'
,
avatar
:
mockAvatars
[
1
],
score
:
38
},
{
userid
:
'local-3'
,
nickname
:
'追风者'
,
avatar
:
mockAvatars
[
2
],
score
:
30
},
{
userid
:
'local-4'
,
nickname
:
'千里马'
,
avatar
:
mockAvatars
[
3
],
score
:
22
},
{
userid
:
'local-5'
,
nickname
:
'飞毛腿'
,
avatar
:
mockAvatars
[
4
],
score
:
15
},
{
userid
:
'local-6'
,
nickname
:
'烈焰驹'
,
avatar
:
mockAvatars
[
5
],
score
:
52
},
{
userid
:
'local-7'
,
nickname
:
'暴风战士'
,
avatar
:
mockAvatars
[
6
],
score
:
41
},
{
userid
:
'local-8'
,
nickname
:
'神行者'
,
avatar
:
mockAvatars
[
7
],
score
:
35
},
{
userid
:
'local-9'
,
nickname
:
'银鞍客'
,
avatar
:
mockAvatars
[
8
],
score
:
28
},
{
userid
:
'local-10'
,
nickname
:
'踏雪无痕'
,
avatar
:
mockAvatars
[
9
],
score
:
18
},
];
// ========== 预加载所有马匹帧图片(防止切帧时闪烁/白屏)==========
// ========== 预加载所有马匹帧图片(防止切帧时闪烁/白屏)==========
const
preloadHorseFrames
=
()
=>
{
const
preloadHorseFrames
=
()
=>
{
for
(
const
frames
of
Object
.
values
(
horseFrames
))
{
for
(
const
frames
of
Object
.
values
(
horseFrames
))
{
...
@@ -240,6 +267,8 @@ const preloadHorseFrames = () => {
...
@@ -240,6 +267,8 @@ const preloadHorseFrames = () => {
onMounted
(()
=>
{
onMounted
(()
=>
{
preloadHorseFrames
();
preloadHorseFrames
();
if
(
$is_run_local
())
{
if
(
$is_run_local
())
{
// 使用模拟玩家数据更新排名列表
updateRankList
(
mockPlayers
);
gameStarted
.
value
=
true
;
gameStarted
.
value
=
true
;
isGameRunning
.
value
=
true
;
isGameRunning
.
value
=
true
;
generateRandomHorses
();
generateRandomHorses
();
...
@@ -525,11 +554,11 @@ const rankIcon = (index: number) => {
...
@@ -525,11 +554,11 @@ const rankIcon = (index: number) => {
}
}
.horse-wrapper.horse-lane-top
{
.horse-wrapper.horse-lane-top
{
bottom
:
3
50px
;
bottom
:
3
00px
;
/* 往下挪 50px */
}
}
.horse-wrapper.horse-lane-bottom
{
.horse-wrapper.horse-lane-bottom
{
bottom
:
60px
;
bottom
:
110px
;
/* 往上挪 50px */
}
}
...
...
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