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
c1dbe1ba
Commit
c1dbe1ba
authored
Jun 06, 2026
by
陈冲
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'main' of
http://git.ruanyiit.com/chenchong/fc-minigame
parents
f4049020
7c0c20c7
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
801 additions
and
74 deletions
+801
-74
Game4.vue
frontend-h5/src/pages/game4/Game4.vue
+8
-6
PlayingView.vue
frontend-h5/src/pages/game4/views/PlayingView.vue
+162
-6
Game5.vue
frontend-h5/src/pages/game5/Game5.vue
+13
-32
PlayingView.vue
frontend-h5/src/pages/game5/views/PlayingView.vue
+592
-24
LoadingView.vue
frontend-large/src/pages/game4/views/LoadingView.vue
+8
-2
Game5.vue
frontend-large/src/pages/game5/Game5.vue
+14
-4
Rank1View.vue
frontend-large/src/pages/game5/views/Rank1View.vue
+4
-0
No files found.
frontend-h5/src/pages/game4/Game4.vue
View file @
c1dbe1ba
...
...
@@ -166,7 +166,7 @@ function handleRoomBack() {
}
const
touchHandler
=
()
=>
{
tick
.
value
+=
1
tick
.
value
+=
5
submitScore
()
}
...
...
@@ -232,11 +232,12 @@ onBeforeUnmount(() => {
<div
class=
"rule-title"
>
游戏规则
</div>
<div
class=
"rule-txt"
>
<!-- 微信用户通过扫描大屏幕二维码参与,可容纳多人同时进行,摇晃手机控制赛马奔跑,摇晃越快赛马跑得越快,游戏结束按分值高低先后排名,有机会获得新疆福彩定制周边礼品一份。 -->
1. 主持人开赛即可摇手机,单次摇晃得 5 分;
2. 得分越高大屏幕上面的马跑得越快;
3. 限时比拼摇速,结束后按积分排名;
4. 高分用户有机会领取新疆福彩定制周边;
5. 禁止外挂作弊,违规账号取消成绩与领奖资格;
1. 主持人开赛即可摇手机,单次摇晃得 5 分;
<br
/>
2. 得分越高大屏幕上面的马跑得越快;
<br
/>
3. 限时比拼摇速,结束后按积分排名;
<br
/>
4. 高分用户有机会领取新疆福彩定制周边;
<br
/>
5. 禁止外挂作弊,违规账号取消成绩与领奖资格;
<br
/><br
/>
祝您游戏愉快!
</div>
</div>
<div
class=
"rule-close"
>
...
...
@@ -246,6 +247,7 @@ onBeforeUnmount(() => {
</
template
>
<LoadingView
v-if=
"currentView === 'loading'"
:image-urls=
"imageUrls"
:user-join-status=
"userJoinStatus"
@
touchGameRule=
"showGameRuleHandler"
/>
<!-- <PlayingView v-else :image-urls="imageUrls" :tick="tick" :rank="rank" -->
<PlayingView
v-else-if=
"currentView === 'playing'"
:image-urls=
"imageUrls"
:tick=
"tick"
:rank=
"rank"
:currentHorse=
"currentHorse"
:currentYaoyiyao=
"currentYaoyiyao"
:countdown-interval=
"countdownInterval"
:is-div-desc-visible=
"isDivDescVisible"
@
touch=
"touchHandler"
/>
...
...
frontend-h5/src/pages/game4/views/PlayingView.vue
View file @
c1dbe1ba
<
script
setup
lang=
"ts"
>
import
{
onBeforeUnmount
,
onMounted
}
from
'vue'
import
{
onBeforeUnmount
,
onMounted
,
ref
}
from
'vue'
import
shakeSound
from
'@/assets/images/game4/yaoyiyao.mp3'
const
props
=
defineProps
<
{
...
...
@@ -16,6 +16,10 @@ const emit = defineEmits<{
touch
:
[]
}
>
()
// ========== iOS 设备检测 ==========
const
isIOS
=
typeof
(
DeviceMotionEvent
as
any
).
requestPermission
===
'function'
const
showIOSHint
=
ref
(
isIOS
)
// ========== 摇一摇 ==========
const
shakeCooldown
=
300
let
lastShakeTime
=
0
...
...
@@ -33,6 +37,37 @@ let wasShaking = false
let
audioContext
:
AudioContext
|
null
=
null
let
shakeAudioBuffer
:
AudioBuffer
|
null
=
null
// iOS 13+/iPadOS 需要主动请求 DeviceMotion 权限
let
motionPermissionGranted
=
false
// 摇一摇图片抖动动画状态
const
isYaoyiyaoShaking
=
ref
(
false
)
let
yaoyiyaoShakeTimer
:
ReturnType
<
typeof
setTimeout
>
|
null
=
null
const
requestMotionPermission
=
async
():
Promise
<
boolean
>
=>
{
// 检查是否需要权限请求(iOS 12.2+)
if
(
typeof
(
DeviceMotionEvent
as
any
).
requestPermission
===
'function'
)
{
try
{
const
state
=
await
(
DeviceMotionEvent
as
any
).
requestPermission
()
if
(
state
===
'granted'
)
{
console
.
log
(
'[摇一摇] iOS DeviceMotion 权限已授予'
)
motionPermissionGranted
=
true
// 权限拿到后再注册监听
window
.
addEventListener
(
'devicemotion'
,
handleDeviceMotion
)
return
true
}
console
.
warn
(
'[摇一摇] iOS DeviceMotion 权限被拒绝:'
,
state
)
return
false
}
catch
(
err
)
{
console
.
error
(
'[摇一摇] iOS 权限请求失败:'
,
err
)
return
false
}
}
// Android / 旧版 iOS:无需额外权限
motionPermissionGranted
=
true
return
true
}
const
initAudio
=
async
()
=>
{
try
{
audioContext
=
new
(
window
.
AudioContext
||
(
window
as
any
).
webkitAudioContext
)()
...
...
@@ -82,6 +117,13 @@ const handleDeviceMotion = (ev: DeviceMotionEvent) => {
if
(
now
-
lastShakeTime
<
shakeCooldown
)
return
lastShakeTime
=
now
// 摇一摇图片抖动动画
isYaoyiyaoShaking
.
value
=
true
if
(
yaoyiyaoShakeTimer
)
clearTimeout
(
yaoyiyaoShakeTimer
)
yaoyiyaoShakeTimer
=
setTimeout
(()
=>
{
isYaoyiyaoShaking
.
value
=
false
},
320
)
playShakeSound
()
// 摇晃时打印日志:输出将要通过 websocket 提交的数据
...
...
@@ -105,8 +147,6 @@ const handleDeviceMotion = (ev: DeviceMotionEvent) => {
}
}
window
.
addEventListener
(
'devicemotion'
,
handleDeviceMotion
)
// ========== 生命周期 ==========
onMounted
(
async
()
=>
{
...
...
@@ -115,6 +155,15 @@ onMounted(async () => {
}
catch
(
error
)
{
console
.
error
(
'初始化失败:'
,
error
)
}
// 非 iOS 设备直接注册监听;iOS 需等用户点击后通过 requestMotionPermission 注册
if
(
typeof
(
DeviceMotionEvent
as
any
).
requestPermission
!==
'function'
)
{
window
.
addEventListener
(
'devicemotion'
,
handleDeviceMotion
)
motionPermissionGranted
=
true
console
.
log
(
'[摇一摇] DeviceMotion 监听已启动(Android/旧版iOS)'
)
}
else
{
console
.
log
(
'[摇一摇] 等待用户点击以请求 iOS DeviceMotion 权限…'
)
}
})
onBeforeUnmount
(()
=>
{
...
...
@@ -122,7 +171,20 @@ onBeforeUnmount(() => {
if
(
audioContext
)
{
audioContext
.
close
()
}
if
(
yaoyiyaoShakeTimer
)
{
clearTimeout
(
yaoyiyaoShakeTimer
)
}
})
// 点击马触发:iOS 先请求权限 + 触发射分
const
onTapHorse
=
async
()
=>
{
if
(
!
motionPermissionGranted
)
{
await
requestMotionPermission
()
// 权限请求完成(无论结果)隐藏 iOS 提示
showIOSHint
.
value
=
false
}
emit
(
'touch'
)
}
</
script
>
<
template
>
...
...
@@ -141,9 +203,19 @@ onBeforeUnmount(() => {
<div>
{{
tick
}}
</div>
<div>
当前排名:
第
<label>
{{
rank
}}
</label>
名
</div>
</div>
<!-- iOS 权限提示蒙层 -->
<Transition
name=
"ios-hint-fade"
>
<div
v-if=
"showIOSHint"
class=
"ios-hint-overlay"
@
click=
"onTapHorse"
>
<div
class=
"ios-hint-content"
>
<div
class=
"ios-hint-title"
>
👇点击下方赛马
</div>
<div
class=
"ios-hint-desc"
>
启用摇一摇功能开始游戏
</div>
<div
class=
"ios-hint-arrow"
>
⬇
</div>
</div>
</div>
</Transition>
<!--
<div
class=
"bg-bottom"
></div>
-->
<div
class=
"bg-yaoyiyao"
:style=
"
{ background: `${currentYaoyiyao} center/cover` }">
</div>
<div
class=
"play-horse"
:
style=
"
{ background: `${currentHorse} center/cover` }" @click="$emit('touch')
">
</div>
<div
class=
"bg-yaoyiyao"
:
class=
"
{ 'is-shaking': isYaoyiyaoShaking }" :
style="{ background: `${currentYaoyiyao} center/cover` }">
</div>
<div
class=
"play-horse"
:
class=
"
{ 'horse-clickable': showIOSHint }" :style="{ background: `${currentHorse} center/cover` }" @click="onTapHorse
">
</div>
</div>
</
template
>
...
...
@@ -297,14 +369,98 @@ onBeforeUnmount(() => {
width
:
514.51px
;
height
:
229px
;
transform
:
translateX
(
-50%
);
transition
:
transform
0.08s
ease-out
;
}
/* 摇一摇图片抖动动画 */
.bg-yaoyiyao.is-shaking
{
animation
:
yaoyiyao-shake
0.32s
ease-in-out
;
}
@keyframes
yaoyiyao-shake
{
0
%
{
transform
:
translateX
(
-50%
)
rotate
(
0deg
);
}
15
%
{
transform
:
translateX
(
-50%
)
rotate
(
-8deg
);
}
30
%
{
transform
:
translateX
(
-50%
)
rotate
(
8deg
);
}
45
%
{
transform
:
translateX
(
-50%
)
rotate
(
-6deg
);
}
60
%
{
transform
:
translateX
(
-50%
)
rotate
(
6deg
);
}
75
%
{
transform
:
translateX
(
-50%
)
rotate
(
-3deg
);
}
90
%
{
transform
:
translateX
(
-50%
)
rotate
(
3deg
);
}
100
%
{
transform
:
translateX
(
-50%
)
rotate
(
0deg
);
}
}
.play-horse
{
position
:
absolute
;
bottom
:
164px
;
left
:
50%
;
width
:
597px
;
height
:
679px
;
/* transform: translateX(-50%) scale(2.4); */
transform
:
translateX
(
-50%
);
}
/* 马匹点击引导动画 - 脉冲呼吸效果 */
.horse-clickable
{
animation
:
horse-pulse
1.2s
ease-in-out
infinite
;
}
@keyframes
horse-pulse
{
0
%,
100
%
{
filter
:
brightness
(
1
)
drop-shadow
(
0
0
8px
rgba
(
255
,
215
,
0
,
0
));
}
50
%
{
filter
:
brightness
(
1.12
)
drop-shadow
(
0
0
24px
rgba
(
255
,
215
,
0
,
0.6
));
}
}
/* ===== iOS 权限提示蒙层 ===== */
.ios-hint-overlay
{
position
:
absolute
;
top
:
0
;
bottom
:
0
;
left
:
var
(
--stage-viewport-left
,
0
);
width
:
var
(
--stage-viewport-width
,
750px
);
z-index
:
10
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background
:
rgba
(
0
,
0
,
0
,
0.55
);
cursor
:
pointer
;
-webkit-tap-highlight-color
:
transparent
;
}
.ios-hint-content
{
text-align
:
center
;
color
:
#fff
;
animation
:
ios-hint-bounce
1.6s
ease-in-out
infinite
;
}
.ios-hint-title
{
font-size
:
36pt
;
font-weight
:
bold
;
margin-bottom
:
16px
;
text-shadow
:
0
2px
8px
rgba
(
0
,
0
,
0
,
0.5
);
}
.ios-hint-desc
{
font-size
:
22pt
;
opacity
:
0.85
;
margin-bottom
:
20px
;
text-shadow
:
0
1px
4px
rgba
(
0
,
0
,
0
,
0.5
);
}
.ios-hint-arrow
{
font-size
:
40pt
;
animation
:
ios-arrow-bounce
0.8s
ease-in-out
infinite
alternate
;
}
@keyframes
ios-hint-bounce
{
0
%,
100
%
{
transform
:
translateY
(
0
);
}
50
%
{
transform
:
translateY
(
-12px
);
}
}
@keyframes
ios-arrow-bounce
{
from
{
transform
:
translateY
(
0
);
opacity
:
1
;
}
to
{
transform
:
translateY
(
16px
);
opacity
:
0.3
;
}
}
.ios-hint-fade-enter-active
{
transition
:
opacity
0.4s
ease
;
}
.ios-hint-fade-leave-active
{
transition
:
opacity
0.3s
ease
;
}
.ios-hint-fade-enter-from
,
.ios-hint-fade-leave-to
{
opacity
:
0
;
}
</
style
>
frontend-h5/src/pages/game5/Game5.vue
View file @
c1dbe1ba
...
...
@@ -105,16 +105,20 @@ function submitScore(save: boolean = false) {
if
(
save
)
{
//item_num 游戏项目(1到6)
//wechat 原始token
sendGameMessage
(
'submit_score_save'
,
{
const
data
=
{
score
:
tick
.
value
,
wechat
:
wechat
.
token_origin
,
item_num
:
1
,
nickname
:
wechat
.
nickname
,
avatar
:
wechat
.
avatar
,
rank
:
rank
.
value
,
})
}
console
.
log
(
'[WebSocket] send submit_score_save:'
,
JSON
.
stringify
(
data
,
null
,
2
))
sendGameMessage
(
'submit_score_save'
,
data
)
}
else
{
sendGameMessage
(
'submit_score'
,
{
score
:
tick
.
value
})
const
data
=
{
score
:
tick
.
value
}
console
.
log
(
'[WebSocket] send submit_score:'
,
JSON
.
stringify
(
data
))
sendGameMessage
(
'submit_score'
,
data
)
}
}
...
...
@@ -191,34 +195,11 @@ function handleRoomBack() {
},
200
)
}
const
touchHandler
=
()
=>
{
if
(
isGuPlaying
.
value
)
{
return
}
tick
.
value
+=
5
const
hitHandler
=
(
score
:
number
)
=>
{
tick
.
value
+=
score
console
.
log
(
'[WebSocket] 圈中马!得分:'
,
score
,
'当前总分:'
,
tick
.
value
)
submitScore
()
playGame1Music
();
isGuPlaying
.
value
=
true
let
frameIndex
=
0
const
playNextFrame
=
()
=>
{
currentGu
.
value
=
guFrames
[
frameIndex
]
??
imageUrls
.
gu01
frameIndex
+=
1
if
(
frameIndex
>=
guFrames
.
length
)
{
guFrameTimer
=
window
.
setTimeout
(()
=>
{
currentGu
.
value
=
guFrames
[
0
]
isGuPlaying
.
value
=
false
guFrameTimer
=
undefined
},
200
)
return
}
guFrameTimer
=
window
.
setTimeout
(
playNextFrame
,
200
)
}
playNextFrame
()
playGame1Music
()
}
if
(
wechat
)
{
...
...
@@ -350,10 +331,10 @@ const rankCloseHandler = () => {
</
template
>
<LoadingView
v-if=
"currentView === 'loading'"
:image-urls=
"imageUrls"
:user-join-status=
"userJoinStatus"
@
touchGameRule=
"showGameRuleHandler"
/>
<!-- <PlayingView v-else :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"
@
touch=
"touchHandler"
/>
<!-- <PlayingView v-else :image-urls="imageUrls" :tick="tick" :rank="rank" -->
@
hit=
"hitHandler"
/>
<!-- <ScoreView v-else :image-urls="imageUrls" :rank="rank" :tick="tick" /> -->
</MobileStage>
<div
v-else
style=
"text-align: center; width: 100vw; height: 100vh; line-height: 30; font-size: 20px;"
>
...
...
frontend-h5/src/pages/game5/views/PlayingView.vue
View file @
c1dbe1ba
<
script
setup
lang=
"ts"
>
defineProps
<
{
import
{
assetUrl
}
from
'@/commons/assets.ts'
import
{
onBeforeUnmount
,
onMounted
,
ref
,
watch
}
from
'vue'
const
props
=
defineProps
<
{
imageUrls
:
Record
<
string
,
string
>
countdownInterval
:
number
isDivDescVisible
:
boolean
...
...
@@ -8,13 +11,409 @@ defineProps<{
currentGu
:
string
}
>
()
defineEmits
<
{
touch
:
[
]
const
emit
=
defineEmits
<
{
hit
:
[
score
:
number
]
}
>
()
// ── 圈圈丢出动画帧 (31帧, 原图325×733 → 渲染宽165) ──
const
circleAnimFrames
=
Array
.
from
({
length
:
31
},
(
_
,
i
)
=>
assetUrl
(
`game5/circle-animation/qcnf-quan_
${
String
(
i
).
padStart
(
2
,
'0'
)}
.png`
)
)
// ── 四种马的帧 (各21帧, 原图196×204 → 渲染180×160) ──
const
horseFrames
:
Record
<
number
,
string
[]
>
=
{}
for
(
let
h
=
1
;
h
<=
4
;
h
++
)
{
horseFrames
[
h
]
=
Array
.
from
({
length
:
21
},
(
_
,
i
)
=>
assetUrl
(
`game5/horse
${
h
}
/no-circle/qcnf-ma
${
h
}
_
${
String
(
i
).
padStart
(
2
,
'0'
)}
.png`
)
)
}
// ── 布局常量 ──
const
STAGE_WIDTH
=
750
const
STAGE_HEIGHT
=
1624
const
RUNWAY_TOP
=
696
const
RUNWAY_HEIGHT
=
306
const
RUNWAY_HALF_Y
=
RUNWAY_TOP
+
RUNWAY_HEIGHT
/
2
// 849
const
HORSE_WIDTH
=
180
const
HORSE_HEIGHT
=
200
// 原图196×204→渲染180×200,留足跳跃帧头部空间
const
HORSE_Y
=
RUNWAY_TOP
+
(
RUNWAY_HEIGHT
-
HORSE_HEIGHT
)
/
2
// 749
const
CIRCLE_THROW_MS
=
1200
// 圈圈飞行时长
const
HIT_FADE_MS
=
400
// 被套中后马消失延迟
// ── 难度递增参数:随游戏时间从 0→60s 指数曲线变化 ──
const
HORSE_SPEED_MIN
=
40
// px/s 初始速度
const
HORSE_SPEED_MAX
=
280
// px/s 最大速度(60s 时,7倍提升)
const
SPAWN_MIN_START
=
3000
// ms 初始最短生成间隔
const
SPAWN_MIN_END
=
800
// ms 最终最短生成间隔
const
SPAWN_MAX_START
=
6000
// ms 初始最长生成间隔
const
SPAWN_MAX_END
=
1800
// ms 最终最长生成间隔
const
MAX_HORSES_ON_SCREEN
=
8
// 场上最多马匹数
// 马动画帧率控制(参考 Rank1View:每 N 个 raf tick 切一帧 → ~20fps)
const
HORSE_FRAME_TICK
=
3
// circle-outer: bottom:72, height:345 → outerTop = 1624 - 72 - 345 = 1207
// circle-inner 圆心 Y = outerTop + 345/2 = 1379.5
const
CIRCLE_CENTER_X
=
375
const
CIRCLE_CENTER_Y
=
STAGE_HEIGHT
-
72
-
345
+
345
/
2
// 1379.5
const
CIRCLE_ANIM_WIDTH
=
165
const
CIRCLE_ANIM_HEIGHT
=
Math
.
round
(
733
*
(
CIRCLE_ANIM_WIDTH
/
325
))
// ≈ 372
// ── 类型 ──
interface
Horse
{
id
:
number
type
:
number
x
:
number
frameIdx
:
number
frameTimer
:
number
// 帧切换滴答计数器(每帧+1,满 HORSE_FRAME_TICK 切帧)
alive
:
boolean
dying
:
boolean
dieAt
:
number
}
interface
CircleThrow
{
y
:
number
frameIdx
:
number
startY
:
number
startTime
:
number
}
interface
HitEffect
{
id
:
number
x
:
number
y
:
number
score
:
number
}
// ── 响应式状态 ──
const
horses
=
ref
<
Horse
[]
>
([])
const
circleThrow
=
ref
<
CircleThrow
|
null
>
(
null
)
const
isCircleFlying
=
ref
(
false
)
const
hitEffects
=
ref
<
HitEffect
[]
>
([])
// ── 内部变量 ──
let
horseIdSeq
=
0
let
hitEffectIdSeq
=
0
let
raf
=
0
let
prevTs
=
0
let
spawnAccum
=
0
let
nextSpawn
=
2000
let
gameElapsed
=
0
// 游戏已进行时间 ms(用于难度递增)
const
HIT_EFFECT_MS
=
800
// "+10" 动画持续时长
// ── 游戏状态 ──
const
gameStarted
=
ref
(
false
)
// 3-2-1 倒计时完成标志,控制动画启停
const
gameActive
=
ref
(
true
)
// 游戏进行中(倒计时到 0 后变 false)
const
preCountdown
=
ref
(
3
)
// 开场倒计时 3/2/1
const
preCountdownKey
=
ref
(
0
)
// 强制动画重触发
const
preCountdownVisible
=
ref
(
false
)
// 开场倒计时可见
let
preCountdownTimer
:
ReturnType
<
typeof
setTimeout
>
|
undefined
// ── Web Audio API 音效 ──
let
audioContext
:
AudioContext
|
null
=
null
function
getAudioContext
():
AudioContext
{
if
(
!
audioContext
)
{
audioContext
=
new
AudioContext
()
}
if
(
audioContext
.
state
===
'suspended'
)
{
audioContext
.
resume
()
}
return
audioContext
}
/** 丢圈"咻"声 — 短促上升频率扫频 */
function
playThrowSound
()
{
try
{
const
ctx
=
getAudioContext
()
const
now
=
ctx
.
currentTime
const
osc
=
ctx
.
createOscillator
()
const
gain
=
ctx
.
createGain
()
osc
.
connect
(
gain
)
gain
.
connect
(
ctx
.
destination
)
osc
.
type
=
'sine'
osc
.
frequency
.
setValueAtTime
(
400
,
now
)
osc
.
frequency
.
exponentialRampToValueAtTime
(
800
,
now
+
0.08
)
osc
.
frequency
.
exponentialRampToValueAtTime
(
200
,
now
+
0.2
)
gain
.
gain
.
setValueAtTime
(
0.25
,
now
)
gain
.
gain
.
exponentialRampToValueAtTime
(
0.01
,
now
+
0.2
)
osc
.
start
(
now
)
osc
.
stop
(
now
+
0.2
)
}
catch
{
/* ignore audio errors */
}
}
/** 套中"叮"声 — 两个简短的上升音 */
function
playHitSound
()
{
try
{
const
ctx
=
getAudioContext
()
const
now
=
ctx
.
currentTime
// 第一音 880 Hz
const
o1
=
ctx
.
createOscillator
()
const
g1
=
ctx
.
createGain
()
o1
.
connect
(
g1
)
g1
.
connect
(
ctx
.
destination
)
o1
.
type
=
'sine'
o1
.
frequency
.
setValueAtTime
(
880
,
now
)
g1
.
gain
.
setValueAtTime
(
0.3
,
now
)
g1
.
gain
.
exponentialRampToValueAtTime
(
0.01
,
now
+
0.25
)
o1
.
start
(
now
)
o1
.
stop
(
now
+
0.25
)
// 第二音 1320 Hz 延时 0.08s
const
o2
=
ctx
.
createOscillator
()
const
g2
=
ctx
.
createGain
()
o2
.
connect
(
g2
)
g2
.
connect
(
ctx
.
destination
)
o2
.
type
=
'sine'
o2
.
frequency
.
setValueAtTime
(
1320
,
now
+
0.08
)
g2
.
gain
.
setValueAtTime
(
0.22
,
now
+
0.08
)
g2
.
gain
.
exponentialRampToValueAtTime
(
0.01
,
now
+
0.35
)
o2
.
start
(
now
+
0.08
)
o2
.
stop
(
now
+
0.35
)
}
catch
{
/* ignore audio errors */
}
}
// ── 清理全部音效(组件卸载时) ──
function
disposeAudioContext
()
{
if
(
audioContext
)
{
audioContext
.
close
().
catch
(()
=>
{})
audioContext
=
null
}
}
// ── 难度系数(0~1,游戏 60s,指数曲线让后期明显加速) ──
function
difficultyFactor
():
number
{
return
Math
.
min
(
gameElapsed
/
60000
,
1
)
}
// ── 当前马速度(二次曲线:前期缓、后期猛) ──
function
currentSpeed
():
number
{
const
t
=
difficultyFactor
()
// 二次曲线:t² 让前期平缓、30s后加速明显
return
HORSE_SPEED_MIN
+
(
HORSE_SPEED_MAX
-
HORSE_SPEED_MIN
)
*
t
*
t
}
// ── 生成一匹马 ──
function
spawnHorse
()
{
const
type
=
Math
.
floor
(
Math
.
random
()
*
4
)
+
1
horses
.
value
.
push
({
id
:
++
horseIdSeq
,
type
,
x
:
-
HORSE_WIDTH
-
Math
.
random
()
*
200
,
frameIdx
:
Math
.
floor
(
Math
.
random
()
*
21
),
// 随机起始帧,避免多马同帧
frameTimer
:
0
,
alive
:
true
,
dying
:
false
,
dieAt
:
0
,
})
}
// ── 点击丢出圈圈 ──
function
throwCircle
()
{
if
(
!
gameActive
.
value
||
!
gameStarted
.
value
||
isCircleFlying
.
value
)
return
isCircleFlying
.
value
=
true
playThrowSound
()
circleThrow
.
value
=
{
y
:
CIRCLE_CENTER_Y
,
frameIdx
:
0
,
startY
:
CIRCLE_CENTER_Y
,
startTime
:
performance
.
now
(),
}
}
// ── 碰撞检测(收紧:圈心必须落在马身体中间区域) ──
function
checkHit
(
ts
:
number
):
boolean
{
if
(
!
circleThrow
.
value
)
return
false
const
ct
=
circleThrow
.
value
const
cx
=
CIRCLE_CENTER_X
const
cy
=
ct
.
y
// 命中区域:马身体宽度 20%~80%、高度 20%~80%(中间 60% 区域)
const
hitMarginX
=
HORSE_WIDTH
*
0.2
const
hitMarginY
=
HORSE_HEIGHT
*
0.2
const
halfHitW
=
HORSE_WIDTH
/
2
-
hitMarginX
const
halfHitH
=
HORSE_HEIGHT
/
2
-
hitMarginY
for
(
const
h
of
horses
.
value
)
{
if
(
!
h
.
alive
||
h
.
dying
)
continue
const
horseCenterX
=
h
.
x
+
HORSE_WIDTH
/
2
const
horseCenterY
=
HORSE_Y
+
HORSE_HEIGHT
/
2
if
(
Math
.
abs
(
cx
-
horseCenterX
)
<=
halfHitW
&&
Math
.
abs
(
cy
-
horseCenterY
)
<=
halfHitH
)
{
h
.
dying
=
true
h
.
dieAt
=
ts
circleThrow
.
value
=
null
isCircleFlying
.
value
=
false
// 在马头顶生成 "+10" 飘字效果
const
effectId
=
++
hitEffectIdSeq
hitEffects
.
value
.
push
({
id
:
effectId
,
x
:
horseCenterX
,
y
:
HORSE_Y
,
score
:
10
,
})
setTimeout
(()
=>
{
const
idx
=
hitEffects
.
value
.
findIndex
(
e
=>
e
.
id
===
effectId
)
if
(
idx
>=
0
)
hitEffects
.
value
.
splice
(
idx
,
1
)
},
HIT_EFFECT_MS
)
playHitSound
()
emit
(
'hit'
,
10
)
return
true
}
}
return
false
}
// ── 主循环 ──
function
loop
(
ts
:
number
)
{
if
(
!
gameActive
.
value
)
{
// 游戏结束:保持最后一帧,不再更新动画
raf
=
requestAnimationFrame
(
loop
)
return
}
if
(
!
prevTs
)
prevTs
=
ts
const
dt
=
Math
.
min
(
ts
-
prevTs
,
50
)
prevTs
=
ts
gameElapsed
+=
dt
// 1. 更新飞行中的圈圈
if
(
circleThrow
.
value
)
{
const
ct
=
circleThrow
.
value
const
elapsed
=
ts
-
ct
.
startTime
const
progress
=
Math
.
min
(
elapsed
/
CIRCLE_THROW_MS
,
1
)
ct
.
y
=
ct
.
startY
-
(
ct
.
startY
-
RUNWAY_HALF_Y
)
*
progress
ct
.
frameIdx
=
Math
.
min
(
Math
.
floor
(
progress
*
31
),
30
)
if
(
checkHit
(
ts
))
{
// 命中:已处理
}
else
if
(
progress
>=
1
)
{
// 落空
circleThrow
.
value
=
null
isCircleFlying
.
value
=
false
}
}
// 2. 更新马匹位置(速度随难度递增,帧率控制防闪烁)
const
speed
=
currentSpeed
()
for
(
let
i
=
horses
.
value
.
length
-
1
;
i
>=
0
;
i
--
)
{
const
h
=
horses
.
value
[
i
]
!
if
(
h
.
dying
)
{
if
(
ts
-
h
.
dieAt
>=
HIT_FADE_MS
)
{
horses
.
value
.
splice
(
i
,
1
)
}
continue
}
h
.
x
+=
speed
*
(
dt
/
1000
)
// 帧率控制:每 HORSE_FRAME_TICK 次循环切换一帧(约 20fps)
h
.
frameTimer
++
if
(
h
.
frameTimer
>=
HORSE_FRAME_TICK
)
{
h
.
frameTimer
=
0
h
.
frameIdx
=
(
h
.
frameIdx
+
1
)
%
21
}
if
(
h
.
x
>
STAGE_WIDTH
+
120
)
{
horses
.
value
.
splice
(
i
,
1
)
}
}
// 3. 生成新马(难度递增:间隔越来越短、数量受上限约束)
spawnAccum
+=
dt
if
(
spawnAccum
>=
nextSpawn
)
{
spawnAccum
=
0
const
t
=
difficultyFactor
()
const
t2
=
t
*
t
// 二次曲线与速度同步
const
minSpawn
=
SPAWN_MIN_START
+
(
SPAWN_MIN_END
-
SPAWN_MIN_START
)
*
t2
const
maxSpawn
=
SPAWN_MAX_START
+
(
SPAWN_MAX_END
-
SPAWN_MAX_START
)
*
t2
nextSpawn
=
minSpawn
+
Math
.
random
()
*
(
maxSpawn
-
minSpawn
)
if
(
horses
.
value
.
length
<
MAX_HORSES_ON_SCREEN
)
{
spawnHorse
()
}
}
raf
=
requestAnimationFrame
(
loop
)
}
// ── 预加载所有帧图片,避免运行时切换帧的闪烁 ──
function
preloadFrames
()
{
// 圈圈动画 31 帧
for
(
const
url
of
circleAnimFrames
)
{
const
img
=
new
Image
()
img
.
src
=
url
}
// 四种马各 21 帧
for
(
const
frames
of
Object
.
values
(
horseFrames
))
{
for
(
const
url
of
frames
)
{
const
img
=
new
Image
()
img
.
src
=
url
}
}
}
// ── 开场 3-2-1 倒计时 ──
function
startPreGameCountdown
()
{
preCountdown
.
value
=
3
preCountdownVisible
.
value
=
true
const
tick
=
()
=>
{
preCountdownKey
.
value
++
if
(
preCountdown
.
value
<=
1
)
{
preCountdownVisible
.
value
=
false
preCountdownTimer
=
undefined
// 倒计时结束 → 启动游戏
gameStarted
.
value
=
true
gameActive
.
value
=
true
prevTs
=
performance
.
now
()
spawnHorse
()
setTimeout
(
spawnHorse
,
800
)
setTimeout
(
spawnHorse
,
1600
)
raf
=
requestAnimationFrame
(
loop
)
return
}
preCountdown
.
value
--
preCountdownTimer
=
setTimeout
(
tick
,
1000
)
}
preCountdownTimer
=
setTimeout
(
tick
,
1000
)
}
function
stopPreCountdown
()
{
if
(
preCountdownTimer
)
{
clearTimeout
(
preCountdownTimer
)
preCountdownTimer
=
undefined
}
}
// ── 监听父组件倒计时,到 0 时暂停游戏 ──
watch
(()
=>
props
.
countdownInterval
,
(
val
)
=>
{
if
(
val
<=
0
&&
gameActive
.
value
)
{
gameActive
.
value
=
false
isCircleFlying
.
value
=
false
circleThrow
.
value
=
null
}
})
onMounted
(()
=>
{
preloadFrames
()
// 先启动音频上下文(需用户首次交互后 resume,此处尝试提前激活)
try
{
getAudioContext
()
}
catch
{
/* ignore */
}
// 开场 3-2-1 倒计时
startPreGameCountdown
()
})
onBeforeUnmount
(()
=>
{
cancelAnimationFrame
(
raf
)
stopPreCountdown
()
disposeAudioContext
()
})
</
script
>
<
template
>
<div
class=
"h5-page game-stage"
>
<div
class=
"game-stage"
>
<!-- 开场 3-2-1 倒计时遮罩 -->
<div
v-if=
"preCountdownVisible"
class=
"pre-countdown-overlay"
>
<div
:key=
"preCountdownKey"
class=
"pre-countdown-number"
>
{{
preCountdown
}}
</div>
</div>
<Transition
name=
"div-desc-slide"
appear
>
<div
v-if=
"isDivDescVisible"
class=
"div-desc"
>
<span
class=
"desc-clock"
aria-hidden=
"true"
></span>
...
...
@@ -33,23 +432,98 @@ defineEmits<{
<div
class=
"txt-bold"
>
{{
tick
}}
</div>
</div>
</div>
<!--
<div
class=
"bg-bottom"
></div>
-->
<div
class=
"paodao-runway"
></div>
<div
class=
"circle-outer"
@
click=
"$emit('touch')"
>
<div
class=
"paodao-runway-container"
>
<div
class=
"paodao-runway"
:class=
"
{ 'paodao-paused': !gameActive }">
</div>
</div>
<!-- 奔跑的马匹(改用
<img>
防闪烁,参考 Rank1View) -->
<template
v-if=
"gameStarted"
>
<div
v-for=
"horse in horses"
:key=
"horse.id"
class=
"horse"
:class=
"
{ 'horse-dying': horse.dying }"
:style="{ transform: `translateX(${horse.x}px)` }"
>
<img
class=
"horse-img"
:src=
"horseFrames[horse.type]?.[horse.frameIdx] ?? ''"
alt=
"horse"
/>
<div
class=
"horse-shadow"
:class=
"
{ 'shadow-dying': horse.dying }">
</div>
</div>
</
template
>
<!-- 套中加分飘字效果 -->
<div
v-for=
"effect in hitEffects"
:key=
"effect.id"
class=
"hit-effect"
:style=
"{ left: effect.x + 'px', top: effect.y + 'px' }"
>
+{{ effect.score }}
</div>
<!-- 飞行中的圈圈动画 -->
<div
v-if=
"gameStarted && circleThrow"
class=
"circle-throw"
:style=
"{
transform: `translate(${CIRCLE_CENTER_X - CIRCLE_ANIM_WIDTH / 2}px, ${circleThrow.y - CIRCLE_ANIM_HEIGHT / 2}px)`,
width: CIRCLE_ANIM_WIDTH + 'px',
height: CIRCLE_ANIM_HEIGHT + 'px',
backgroundImage: `url(${circleAnimFrames[circleThrow.frameIdx]})`,
}"
></div>
<div
class=
"circle-outer"
:class=
"{ 'circle-disabled': !gameActive || !gameStarted, 'circle-paused': !gameActive }"
@
click=
"throwCircle"
>
<div
class=
"circle-inner"
></div>
</div>
<div
class=
"play-click"
>
点击按钮,开始套圈
</div>
<div
class=
"play-click"
>
{{ !gameStarted ? '准备开始…' : !gameActive ? '游戏结束' : '点击按钮,开始套圈' }}
</div>
</div>
</template>
<
style
scoped
>
.game-stage
{
position
:
absolute
;
left
:
0
;
top
:
0
;
width
:
750px
;
height
:
1624px
;
margin
:
0
;
padding
:
0
;
}
/* ── 开场 3-2-1 倒计时遮罩 ── */
/* 必须使用 MobileStage 的 viewport CSS 变量,才能覆盖缩放产生的侧边空白 */
.pre-countdown-overlay
{
position
:
absolute
;
left
:
var
(
--stage-viewport-left
);
top
:
var
(
--stage-viewport-top
);
width
:
var
(
--stage-viewport-width
);
height
:
var
(
--stage-viewport-height
);
z-index
:
50
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background
:
rgba
(
0
,
0
,
0
,
0.45
);
pointer-events
:
none
;
}
.pre-countdown-number
{
color
:
#FFD700
;
font-size
:
200px
;
font-weight
:
900
;
line-height
:
1
;
text-shadow
:
0
10px
30px
rgba
(
0
,
0
,
0
,
0.4
);
animation
:
countdown-pop
0.9s
ease
both
;
}
@keyframes
countdown-pop
{
0
%
{
opacity
:
0
;
transform
:
scale
(
0.3
);
}
25
%
{
opacity
:
1
;
transform
:
scale
(
1.15
);
}
55
%
{
opacity
:
1
;
transform
:
scale
(
1
);
}
100
%
{
opacity
:
0
;
transform
:
scale
(
0.8
);
}
}
.div-desc
{
position
:
absolute
;
top
:
150px
;
...
...
@@ -108,16 +582,6 @@ defineEmits<{
transform
:
translateX
(
0
);
}
.bg-bottom
{
position
:
absolute
;
bottom
:
0
;
left
:
var
(
--stage-viewport-left
,
0
);
width
:
var
(
--stage-viewport-width
,
750px
);
height
:
479px
;
background
:
v-bind
(
'imageUrls.bg2'
)
center
/
cover
;
opacity
:
0.6
;
}
.img-logo
{
position
:
absolute
;
top
:
66px
;
...
...
@@ -150,6 +614,7 @@ defineEmits<{
transform
:
translate
(
-50%
,
-50%
);
font-size
:
24px
;
color
:
#A10A06
;
z-index
:
2
;
.txt-bold
{
font-size
:
40px
;
...
...
@@ -166,27 +631,119 @@ defineEmits<{
}
}
.paodao-runway
{
/* ── 跑道容器:撑满 viewport 宽度并裁剪溢出 ── */
.paodao-runway-container
{
position
:
absolute
;
left
:
0
;
left
:
var
(
--stage-viewport-left
,
0
)
;
top
:
696px
;
width
:
750px
;
width
:
var
(
--stage-viewport-width
,
750px
)
;
height
:
306px
;
overflow
:
hidden
;
z-index
:
2
;
}
/* ── 跑道内层:2x 宽度通过 transform 滚动,适配任意 viewport 宽 ── */
.paodao-runway
{
width
:
200%
;
height
:
100%
;
background
:
v-bind
(
'imageUrls.paodao'
)
repeat-x
;
background-size
:
auto
100%
;
animation
:
runway-scroll
8s
linear
infinite
;
}
@keyframes
runway-scroll
{
from
{
transform
:
translateX
(
0
);
}
to
{
transform
:
translateX
(
-50%
);
}
}
/* ── 跑道暂停 ── */
.paodao-paused
{
animation-play-state
:
paused
;
}
/* ── 马匹 ── */
.horse
{
position
:
absolute
;
left
:
0
;
top
:
v-bind
(
'HORSE_Y + "px"'
);
width
:
180px
;
height
:
200px
;
z-index
:
3
;
will-change
:
transform
;
}
.horse-img
{
width
:
100%
;
height
:
100%
;
object-fit
:
cover
;
}
.horse-dying
{
opacity
:
0
;
transition
:
opacity
0.3s
ease
,
transform
0.3s
ease
;
}
/* ── 马匹脚底阴影 ── */
.horse-shadow
{
position
:
absolute
;
bottom
:
-10px
;
left
:
50%
;
width
:
110px
;
height
:
18px
;
transform
:
translateX
(
-50%
);
background
:
radial-gradient
(
ellipse
at
center
,
rgba
(
0
,
0
,
0
,
0.35
)
0%
,
rgba
(
0
,
0
,
0
,
0.10
)
40%
,
rgba
(
0
,
0
,
0
,
0
)
72%
);
border-radius
:
50%
;
pointer-events
:
none
;
}
.shadow-dying
{
opacity
:
0
;
transition
:
opacity
0.3s
ease
;
}
/* ── 套中加分飘字 ── */
.hit-effect
{
position
:
absolute
;
z-index
:
20
;
color
:
#FFD700
;
font-size
:
40px
;
font-weight
:
900
;
pointer-events
:
none
;
text-shadow
:
0
3px
6px
rgba
(
0
,
0
,
0
,
0.6
);
animation
:
score-float
0.8s
ease-out
forwards
;
white-space
:
nowrap
;
transform
:
translate
(
-50%
,
-100%
);
}
@keyframes
score-float
{
0
%
{
background-position-x
:
0
;
opacity
:
1
;
transform
:
translate
(
-50%
,
-100%
)
scale
(
0.5
);
}
30
%
{
opacity
:
1
;
transform
:
translate
(
-50%
,
-130%
)
scale
(
1.1
);
}
100
%
{
background-position-x
:
-750px
;
opacity
:
0
;
transform
:
translate
(
-50%
,
-220%
)
scale
(
0.8
);
}
}
/* ── 飞行中的圈圈 ── */
.circle-throw
{
position
:
absolute
;
left
:
0
;
top
:
0
;
background-position
:
center
;
background-size
:
contain
;
background-repeat
:
no-repeat
;
z-index
:
10
;
pointer-events
:
none
;
will-change
:
transform
,
background-image
;
image-rendering
:
auto
;
}
.circle-outer
{
position
:
absolute
;
left
:
50%
;
...
...
@@ -196,6 +753,16 @@ defineEmits<{
background
:
v-bind
(
'imageUrls.circleBg'
)
center
/
cover
;
transform
:
translateX
(
-50%
);
animation
:
circle-spin
3s
linear
infinite
;
z-index
:
4
;
}
.circle-disabled
{
pointer-events
:
none
;
opacity
:
0.5
;
}
.circle-paused
{
animation-play-state
:
paused
;
}
@keyframes
circle-spin
{
...
...
@@ -227,5 +794,6 @@ defineEmits<{
font-weight
:
400
;
font-size
:
26px
;
color
:
#FFFFFF
;
z-index
:
5
;
}
</
style
>
frontend-large/src/pages/game4/views/LoadingView.vue
View file @
c1dbe1ba
...
...
@@ -32,6 +32,13 @@ const imageUrls = {
const
isStartPressed
=
ref
(
false
);
const
renderAvatar
=
(
item
:
any
|
null
)
=>
{
if
(
item
&&
item
.
avatar
)
{
return
`background: url("
${
String
(
item
.
avatar
).
replace
(
/"/g
,
'
\\
"'
)}
") center / cover no-repeat;`
}
return
`background:
${
imageUrls
.
avatar
}
center / cover no-repeat;`
}
const
pressStartButton
=
()
=>
{
isStartPressed
.
value
=
true
;
emit
(
"start"
);
...
...
@@ -66,7 +73,7 @@ const releaseStartButton = () => {
>
<div>
<div
class=
"img-avatar-container"
>
<div
class=
"img-avatar"
:style=
"`
background:url('$
{item.avatar}') center / cover no-repeat;
`">
</div>
<div
class=
"img-avatar"
:style=
"`
$
{renderAvatar(item)}
`">
</div>
</div>
<div
class=
"txt-nickname"
>
{{
$format_str
(
item
.
nickname
,
12
)
}}
...
...
@@ -235,7 +242,6 @@ const releaseStartButton = () => {
width
:
60px
;
height
:
60px
;
border-radius
:
30px
;
background
:
v-bind
(
"imageUrls.avatar"
);
}
.txt-nickname
{
...
...
frontend-large/src/pages/game5/Game5.vue
View file @
c1dbe1ba
...
...
@@ -46,7 +46,9 @@ const confirmRefresh = (event: BeforeUnloadEvent) => {
const
renderPlayers
=
(
data
:
any
)
=>
{
const
players
=
Array
.
isArray
(
data
?.
list
)
?
data
.
list
:
[]
playerCount
.
value
=
Number
(
data
?.
count
??
players
.
length
)
const
count
=
Number
(
data
?.
count
??
players
.
length
)
console
.
log
(
'[WebSocket] 玩家列表更新 - 在线人数:'
,
count
,
'玩家数:'
,
players
.
length
,
'数据:'
,
JSON
.
stringify
(
players
.
map
((
p
:
any
)
=>
({
nickname
:
p
.
nickname
,
userid
:
p
.
userid
??
p
.
wechat_id
}))))
playerCount
.
value
=
count
loadingPlayers
.
value
=
Array
.
from
(
{
length
:
PLAYER_LIMIT
},
(
_
,
index
)
=>
players
[
index
]
??
emptyLoadingPlayer
(),
...
...
@@ -114,6 +116,7 @@ const gotoRank1WithIntro = async () => {
}
function
handleRoomState
(
data
:
any
)
{
console
.
log
(
'[WebSocket] 房间状态更新 - status:'
,
data
?.
status
,
'数据:'
,
JSON
.
stringify
(
data
))
if
(
Array
.
isArray
(
data
?.
list
))
{
renderPlayers
(
data
)
}
...
...
@@ -137,14 +140,21 @@ function handleRoomState(data: any) {
}
const
{
startGame
,
createRoom
,
backRoom
}
=
useAdminGameSocket
({
gameId
:
'game
1
'
,
gameId
:
'game
5
'
,
onRoomState
:
handleRoomState
,
onRoomJoin
:
(
data
)
=>
{
console
.
log
(
'[WebSocket] 玩家加入房间 - 数据:'
,
JSON
.
stringify
(
data
))
renderPlayers
(
data
)
gotoLoading
()
},
onGameStart
:
gotoRank1WithIntro
,
onRoomRank
:
updateRankPlayers
,
onGameStart
:
()
=>
{
console
.
log
(
'[WebSocket] 游戏开始!'
)
gotoRank1WithIntro
()
},
onRoomRank
:
(
data
)
=>
{
console
.
log
(
'[WebSocket] 排名更新 - 数据:'
,
JSON
.
stringify
(
data
))
updateRankPlayers
(
data
)
},
onRelogin
:
async
()
=>
{
$remove_socket_storage
();
await
router
.
replace
(
'/'
)
...
...
frontend-large/src/pages/game5/views/Rank1View.vue
View file @
c1dbe1ba
...
...
@@ -107,6 +107,7 @@ watch(
()
=>
props
.
rankList
,
(
rankList
)
=>
{
if
(
rankList
&&
rankList
.
length
>
0
)
{
console
.
log
(
'[Rank1View] 排名列表更新 - 数据:'
,
JSON
.
stringify
(
rankList
.
map
(
p
=>
({
nickname
:
p
.
nickname
,
score
:
p
.
score
}))))
updateRankList
(
rankList
);
}
},
...
...
@@ -121,11 +122,13 @@ const startIntro = async () => {
horses
.
value
=
[];
horseIdCounter
=
0
;
console
.
log
(
'[Rank1View] 游戏流程启动 - 开始 3s 倒计时 + 60s 游戏'
)
// 启动完整倒计时:3s 开场 + 60s 游戏
const
countdownPromise
=
designStage
.
value
?.
startCountdown
(
3
,
60
);
// 等待 3-2-1 开场倒计时结束(~3.5s 后安全触发)
await
new
Promise
((
r
)
=>
setTimeout
(
r
,
3500
));
console
.
log
(
'[Rank1View] 倒计时结束,马匹开始奔跑'
)
// Phase 2: 倒计时结束 → 游戏正式开始,背景动画+马匹启动
gameStarted
.
value
=
true
;
...
...
@@ -139,6 +142,7 @@ const startIntro = async () => {
};
const
stopGame
=
()
=>
{
console
.
log
(
'[Rank1View] 游戏结束 - 60s 倒计时到'
)
isGameRunning
.
value
=
false
;
showResult
.
value
=
true
;
stopHorseLoop
();
...
...
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