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
e223f4d5
Commit
e223f4d5
authored
Jun 06, 2026
by
董政锦
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: 修复 game5 跑道和倒计时蒙层在375px宽度问题;
parent
43ac0066
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
604 additions
and
59 deletions
+604
-59
Game4.vue
frontend-h5/src/pages/game4/Game4.vue
+6
-5
Game5.vue
frontend-h5/src/pages/game5/Game5.vue
+6
-30
PlayingView.vue
frontend-h5/src/pages/game5/views/PlayingView.vue
+592
-24
No files found.
frontend-h5/src/pages/game4/Game4.vue
View file @
e223f4d5
...
...
@@ -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"
>
...
...
frontend-h5/src/pages/game5/Game5.vue
View file @
e223f4d5
...
...
@@ -191,34 +191,10 @@ function handleRoomBack() {
},
200
)
}
const
touchHandler
=
()
=>
{
if
(
isGuPlaying
.
value
)
{
return
}
tick
.
value
+=
5
const
hitHandler
=
(
score
:
number
)
=>
{
tick
.
value
+=
score
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 +326,10 @@ const rankCloseHandler = () => {
</
template
>
<LoadingView
v-if=
"currentView === 'loading'"
:image-urls=
"imageUrls"
:user-join-status=
"userJoinStatus"
@
touchGameRule=
"showGameRuleHandler"
/>
<PlayingView
v-else-if=
"currentView === 'playing'"
: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"
: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 @
e223f4d5
<
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
:
12px
;
left
:
50%
;
width
:
110px
;
height
:
18px
;
transform
:
translateX
(
-50%
);
background
:
radial-gradient
(
ellipse
at
center
,
rgba
(
0
,
0
,
0
,
0.38
)
0%
,
rgba
(
0
,
0
,
0
,
0.12
)
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
>
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