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
82fe3ea6
Commit
82fe3ea6
authored
Jun 12, 2026
by
董政锦
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'fix--game3-PC马的渲染' into 'main'
Fix game3 pc马的渲染 See merge request
!13
parents
c29b01c5
25744edd
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
777 additions
and
521 deletions
+777
-521
MobileStage.vue
frontend-h5/src/components/MobileStage.vue
+7
-3
Game3.vue
frontend-h5/src/pages/game3/Game3.vue
+479
-404
PlayingView.vue
frontend-h5/src/pages/game3/views/PlayingView.vue
+46
-14
Game3.vue
frontend-large/src/pages/game3/Game3.vue
+102
-46
Rank1View.vue
frontend-large/src/pages/game3/views/Rank1View.vue
+143
-54
No files found.
frontend-h5/src/components/MobileStage.vue
View file @
82fe3ea6
...
@@ -193,14 +193,18 @@ defineExpose({
...
@@ -193,14 +193,18 @@ defineExpose({
}
}
.modal-container
{
.modal-container
{
position
:
absolute
;
/* position:fixed + 显式 100vw/100vh 确保在 iOS Safari 中全屏蒙层,
inset
:
0
;
避免因父级 overflow:hidden / transform:scale 导致 inset:0 计算异常 */
position
:
fixed
;
top
:
0
;
left
:
0
;
width
:
100vw
;
height
:
100vh
;
z-index
:
10
;
z-index
:
10
;
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
justify-content
:
center
;
justify-content
:
center
;
background-color
:
rgba
(
0
,
0
,
0
,
0.6
);
background-color
:
rgba
(
0
,
0
,
0
,
0.6
);
/* pointer-events: none; */
}
}
.game-rule-fade-enter-active
,
.game-rule-fade-enter-active
,
...
...
frontend-h5/src/pages/game3/Game3.vue
View file @
82fe3ea6
<
script
setup
lang=
"ts"
>
<
script
setup
lang=
"ts"
>
import
{
computed
,
onBeforeUnmount
,
onMounted
,
ref
}
from
'vue'
import
{
computed
,
onBeforeUnmount
,
onMounted
,
ref
}
from
"vue"
;
import
{
cssAssetUrl
}
from
'@/commons/assets.ts'
import
{
cssAssetUrl
}
from
"@/commons/assets.ts"
;
import
MobileStage
from
'@/components/MobileStage.vue'
import
MobileStage
from
"@/components/MobileStage.vue"
;
import
{
useGameSocket
,
sendGameMessage
,
userJoinStatus
,
joinSharedRoom
}
from
'@/composables/useGameSocket'
import
{
import
LoadingView
from
'./views/LoadingView.vue'
useGameSocket
,
import
PlayingView
from
'./views/PlayingView.vue'
sendGameMessage
,
import
{
$getWechat
,
$toast
,
$is_run_local
}
from
'@/commons/utils.ts'
userJoinStatus
,
import
{
playGame3Music
,
stopAllMusic
}
from
'@/commons/music'
;
joinSharedRoom
,
}
from
"@/composables/useGameSocket"
;
type
GameView
=
'loading'
|
'playing'
import
LoadingView
from
"./views/LoadingView.vue"
;
import
PlayingView
from
"./views/PlayingView.vue"
;
import
{
$getWechat
,
$toast
,
$is_run_local
}
from
"@/commons/utils.ts"
;
import
{
playGame3Music
,
stopAllMusic
}
from
"@/commons/music"
;
// ⚠ 发布上线时设为 true,本地测试时设为 false
const
IS_PRODUCTION
=
true
;
type
GameView
=
"loading"
|
"playing"
;
type
RankPlayer
=
{
type
RankPlayer
=
{
rank
:
number
rank
:
number
;
nickname
:
string
nickname
:
string
;
avatar
:
string
avatar
:
string
;
score
:
number
|
string
score
:
number
|
string
;
}
}
;
const
imageUrls
:
Record
<
string
,
string
>
=
{
const
imageUrls
:
Record
<
string
,
string
>
=
{
bg
:
cssAssetUrl
(
'game3/bg.webp'
),
bg
:
cssAssetUrl
(
"game3/bg.webp"
),
bg3
:
cssAssetUrl
(
'game3/bg3.webp'
),
bg3
:
cssAssetUrl
(
"game3/bg3.webp"
),
left
:
cssAssetUrl
(
'game3/icon_zuo.webp'
),
left
:
cssAssetUrl
(
"game3/icon_zuo.webp"
),
right
:
cssAssetUrl
(
'game3/icon_you.webp'
),
right
:
cssAssetUrl
(
"game3/icon_you.webp"
),
bg4
:
cssAssetUrl
(
'game3/pic_paodao.webp'
),
bg4
:
cssAssetUrl
(
"game3/pic_paodao.webp"
),
logo
:
cssAssetUrl
(
'logo.png'
),
logo
:
cssAssetUrl
(
"logo.png"
),
loadingBg1
:
cssAssetUrl
(
'game6/loadingbg1.png'
),
loadingBg1
:
cssAssetUrl
(
"game6/loadingbg1.png"
),
loadingBg2
:
cssAssetUrl
(
'game6/loadingbg2.png'
),
loadingBg2
:
cssAssetUrl
(
"game6/loadingbg2.png"
),
close
:
cssAssetUrl
(
'game1/close.png'
),
close
:
cssAssetUrl
(
"game1/close.png"
),
clock
:
cssAssetUrl
(
'game1/clock.png'
),
clock
:
cssAssetUrl
(
"game1/clock.png"
),
bg2
:
cssAssetUrl
(
'game6/bg2.png'
),
bg2
:
cssAssetUrl
(
"game6/bg2.png"
),
hydt
:
cssAssetUrl
(
'game3/title3.webp'
),
hydt
:
cssAssetUrl
(
"game3/title3.webp"
),
rank
:
cssAssetUrl
(
'game6/rank.png'
),
rank
:
cssAssetUrl
(
"game6/rank.png"
),
moleShowSprite
:
cssAssetUrl
(
'game6/mole_show.png'
),
moleShowSprite
:
cssAssetUrl
(
"game6/mole_show.png"
),
moleHideSprite
:
cssAssetUrl
(
'game6/mole_hide.png'
),
moleHideSprite
:
cssAssetUrl
(
"game6/mole_hide.png"
),
moleHideprite
:
cssAssetUrl
(
'game6/mole_hit.png'
),
moleHideprite
:
cssAssetUrl
(
"game6/mole_hit.png"
),
rabbitFaintprite
:
cssAssetUrl
(
'game6/rabbit_faint.png'
),
rabbitFaintprite
:
cssAssetUrl
(
"game6/rabbit_faint.png"
),
rabbitHideprite
:
cssAssetUrl
(
'game6/rabbit_hide.png'
),
rabbitHideprite
:
cssAssetUrl
(
"game6/rabbit_hide.png"
),
rabbitshowprite
:
cssAssetUrl
(
'game6/rabbit_show.png'
),
rabbitshowprite
:
cssAssetUrl
(
"game6/rabbit_show.png"
),
land
:
cssAssetUrl
(
'game6/land.png'
),
land
:
cssAssetUrl
(
"game6/land.png"
),
hammer
:
cssAssetUrl
(
'game6/hammer.png'
),
hammer
:
cssAssetUrl
(
"game6/hammer.png"
),
ranklist
:
cssAssetUrl
(
'game6/ranklist.png'
),
ranklist
:
cssAssetUrl
(
"game6/ranklist.png"
),
rule
:
cssAssetUrl
(
'game1/rule.png'
)
rule
:
cssAssetUrl
(
"game1/rule.png"
),
}
}
;
const
wechat
=
$getWechat
()
const
wechat
=
$getWechat
()
;
const
currentView
=
ref
<
GameView
>
(
'loading'
)
const
currentView
=
ref
<
GameView
>
(
"loading"
);
const
countdownInterval
=
ref
(
60
)
const
countdownInterval
=
ref
(
60
)
;
const
isDivDescVisible
=
ref
(
false
)
const
isDivDescVisible
=
ref
(
false
)
;
const
isGameOver
=
ref
(
false
)
const
isGameOver
=
ref
(
false
)
;
const
score
=
ref
(
0
)
const
score
=
ref
(
0
)
;
const
rank
=
ref
(
0
)
//排名
const
rank
=
ref
(
0
)
;
//排名
const
rankList
=
ref
<
RankPlayer
[]
>
([])
const
rankList
=
ref
<
RankPlayer
[]
>
([])
;
const
displayRankList
=
computed
<
RankPlayer
[]
>
(()
=>
{
const
displayRankList
=
computed
<
RankPlayer
[]
>
(()
=>
{
const
list
=
rankList
.
value
.
slice
(
0
,
10
)
const
list
=
rankList
.
value
.
slice
(
0
,
10
);
return
Array
.
from
({
length
:
10
},
(
_
,
index
)
=>
{
return
Array
.
from
({
length
:
10
},
(
_
,
index
)
=>
{
return
list
[
index
]
??
{
return
(
rank
:
index
+
1
,
list
[
index
]
??
{
nickname
:
'-'
,
rank
:
index
+
1
,
avatar
:
''
,
nickname
:
"-"
,
score
:
'-'
,
avatar
:
""
,
}
score
:
"-"
,
})
}
})
);
});
});
const
rankScore
=
computed
<
number
|
string
>
(()
=>
{
const
rankScore
=
computed
<
number
|
string
>
(()
=>
{
const
currentPlayer
=
rankList
.
value
const
currentPlayer
=
rankList
.
value
.
slice
(
0
,
10
)
.
slice
(
0
,
10
)
.
find
(
item
=>
item
.
rank
===
rank
.
value
)
.
find
((
item
)
=>
item
.
rank
===
rank
.
value
);
return
currentPlayer
?.
score
??
score
.
value
return
currentPlayer
?.
score
??
score
.
value
;
})
})
;
// const guFrames: [string, string, string] = [imageUrls.gu01, imageUrls.gu02, imageUrls.gu03]
// const guFrames: [string, string, string] = [imageUrls.gu01, imageUrls.gu02, imageUrls.gu03]
// const currentGu = ref(imageUrls.gu01)
// const currentGu = ref(imageUrls.gu01)
const
isGuPlaying
=
ref
(
false
)
const
isGuPlaying
=
ref
(
false
)
;
const
token
=
ref
(
wechat
?.
token
??
''
)
const
token
=
ref
(
wechat
?.
token
??
""
);
const
nickname
=
ref
(
wechat
?.
nickname
??
''
)
const
nickname
=
ref
(
wechat
?.
nickname
??
""
);
const
avatar
=
ref
(
wechat
?.
avatar
??
''
)
const
avatar
=
ref
(
wechat
?.
avatar
??
""
);
const
showGameRule
=
ref
(
true
)
const
showGameRule
=
ref
(
true
)
;
const
showGameRank
=
ref
(
false
)
const
showGameRank
=
ref
(
false
)
;
const
isLocalMode
=
$is_run_local
()
const
isLocalMode
=
!
IS_PRODUCTION
&&
$is_run_local
();
const
mobileStageRef
=
ref
<
InstanceType
<
typeof
MobileStage
>
|
null
>
(
null
)
const
mobileStageRef
=
ref
<
InstanceType
<
typeof
MobileStage
>
|
null
>
(
null
)
;
const
_offSocketMessage
=
ref
<
(()
=>
void
)
|
undefined
>
()
const
_offSocketMessage
=
ref
<
(()
=>
void
)
|
undefined
>
()
;
let
guFrameTimer
:
ReturnType
<
typeof
window
.
setTimeout
>
|
undefined
let
guFrameTimer
:
ReturnType
<
typeof
window
.
setTimeout
>
|
undefined
;
let
gameCountdownTimer
:
ReturnType
<
typeof
window
.
setTimeout
>
|
undefined
let
gameCountdownTimer
:
ReturnType
<
typeof
window
.
setTimeout
>
|
undefined
;
const
confirmRefresh
=
(
event
:
BeforeUnloadEvent
)
=>
{
const
confirmRefresh
=
(
event
:
BeforeUnloadEvent
)
=>
{
event
.
preventDefault
()
event
.
preventDefault
();
event
.
returnValue
=
''
event
.
returnValue
=
""
;
}
}
;
function
setDivDescVisible
(
visible
:
boolean
)
{
function
setDivDescVisible
(
visible
:
boolean
)
{
isDivDescVisible
.
value
=
visible
isDivDescVisible
.
value
=
visible
;
}
}
function
stopGameCountdown
()
{
function
stopGameCountdown
()
{
if
(
gameCountdownTimer
)
{
if
(
gameCountdownTimer
)
{
window
.
clearTimeout
(
gameCountdownTimer
)
window
.
clearTimeout
(
gameCountdownTimer
);
gameCountdownTimer
=
undefined
gameCountdownTimer
=
undefined
;
}
}
}
}
function
submitScore
(
save
:
boolean
=
false
,
currentTick
=
score
.
value
)
{
function
submitScore
(
save
:
boolean
=
false
,
currentTick
=
score
.
value
)
{
if
(
save
)
{
if
(
!
wechat
&&
!
isLocalMode
)
return
;
sendGameMessage
(
'submit_score_save'
,
{
if
(
save
)
{
score
:
currentTick
,
if
(
isLocalMode
&&
!
wechat
)
{
wechat
:
wechat
.
token_origin
,
console
.
log
(
"[game3 本地模式] 提交最终分数:"
,
currentTick
);
item_num
:
3
,
return
;
nickname
:
wechat
.
nickname
,
avatar
:
wechat
.
avatar
,
rank
:
rank
.
value
,
})
}
else
{
sendGameMessage
(
'submit_score'
,
{
score
:
currentTick
})
}
}
sendGameMessage
(
"submit_score_save"
,
{
score
:
currentTick
,
wechat
:
wechat
.
token_origin
,
item_num
:
3
,
nickname
:
wechat
.
nickname
,
avatar
:
wechat
.
avatar
,
rank
:
rank
.
value
,
});
}
else
{
if
(
isLocalMode
&&
!
wechat
)
{
console
.
log
(
"[game3 本地模式] 实时分数:"
,
currentTick
);
return
;
}
sendGameMessage
(
"submit_score"
,
{
score
:
currentTick
});
}
}
}
function
startGameCountdown
(
seconds
=
60
)
{
function
startGameCountdown
(
seconds
=
60
)
{
stopGameCountdown
()
stopGameCountdown
();
countdownInterval
.
value
=
seconds
countdownInterval
.
value
=
seconds
;
setDivDescVisible
(
true
)
setDivDescVisible
(
true
);
const
tickCountdown
=
()
=>
{
const
tickCountdown
=
()
=>
{
countdownInterval
.
value
-=
1
countdownInterval
.
value
-=
1
;
if
(
countdownInterval
.
value
<=
0
)
{
if
(
countdownInterval
.
value
<=
0
)
{
countdownInterval
.
value
=
0
countdownInterval
.
value
=
0
;
gameCountdownTimer
=
undefined
gameCountdownTimer
=
undefined
;
setDivDescVisible
(
false
)
setDivDescVisible
(
false
);
submitScore
(
true
)
submitScore
(
true
);
showGameOverRank
()
showGameOverRank
();
return
return
;
}
gameCountdownTimer
=
window
.
setTimeout
(
tickCountdown
,
1000
)
}
}
gameCountdownTimer
=
window
.
setTimeout
(
tickCountdown
,
1000
)
gameCountdownTimer
=
window
.
setTimeout
(
tickCountdown
,
1000
);
};
gameCountdownTimer
=
window
.
setTimeout
(
tickCountdown
,
1000
);
}
}
function
resetToLoadingView
()
{
function
resetToLoadingView
()
{
stopGameCountdown
()
stopGameCountdown
();
if
(
guFrameTimer
)
{
if
(
guFrameTimer
)
{
window
.
clearTimeout
(
guFrameTimer
)
window
.
clearTimeout
(
guFrameTimer
);
guFrameTimer
=
undefined
guFrameTimer
=
undefined
;
}
}
score
.
value
=
0
score
.
value
=
0
;
rank
.
value
=
0
rank
.
value
=
0
;
rankList
.
value
=
[]
rankList
.
value
=
[];
countdownInterval
.
value
=
60
countdownInterval
.
value
=
60
;
isGuPlaying
.
value
=
false
isGuPlaying
.
value
=
false
;
isGameOver
.
value
=
false
isGameOver
.
value
=
false
;
userJoinStatus
.
value
=
false
userJoinStatus
.
value
=
false
;
showGameRank
.
value
=
false
showGameRank
.
value
=
false
;
currentView
.
value
=
'loading'
currentView
.
value
=
"loading"
;
setDivDescVisible
(
false
)
setDivDescVisible
(
false
);
}
}
function
startGameView
()
{
function
startGameView
()
{
stopGameCountdown
()
stopGameCountdown
();
score
.
value
=
0
score
.
value
=
0
;
rank
.
value
=
0
rank
.
value
=
0
;
rankList
.
value
=
[]
rankList
.
value
=
[];
isGameOver
.
value
=
false
isGameOver
.
value
=
false
;
showGameRank
.
value
=
false
showGameRank
.
value
=
false
;
currentView
.
value
=
'playing'
currentView
.
value
=
"playing"
;
startGameCountdown
()
startGameCountdown
();
}
}
function
showGameOverRank
()
{
function
showGameOverRank
()
{
stopGameCountdown
()
stopGameCountdown
();
setDivDescVisible
(
false
)
setDivDescVisible
(
false
);
currentView
.
value
=
'loading'
currentView
.
value
=
"loading"
;
showGameRank
.
value
=
true
showGameRank
.
value
=
true
;
userJoinStatus
.
value
=
false
;
userJoinStatus
.
value
=
false
;
}
}
const
scoreChangeHandler
=
(
delta
:
number
)
=>
{
const
scoreChangeHandler
=
(
delta
:
number
)
=>
{
if
(
delta
>
0
)
{
if
(
delta
>
0
)
{
playGame3Music
(
1
);
playGame3Music
(
1
);
}
else
{
}
else
{
playGame3Music
(
2
);
playGame3Music
(
2
);
}
}
let
nextTick
=
score
.
value
+
delta
let
nextTick
=
score
.
value
+
delta
;
if
(
nextTick
<
0
)
nextTick
=
0
if
(
nextTick
<
0
)
nextTick
=
0
;
score
.
value
=
nextTick
score
.
value
=
nextTick
;
submitScore
(
false
,
nextTick
)
submitScore
(
false
,
nextTick
);
}
}
;
if
(
wechat
)
{
if
(
wechat
)
{
const
gameId
=
'game3'
;
const
gameId
=
"game3"
;
const
{
offSocketMessage
}
=
useGameSocket
({
const
{
offSocketMessage
}
=
useGameSocket
({
gameId
,
gameId
,
auth
:
{
auth
:
{
userid
:
wechat
.
token
,
userid
:
wechat
.
token
,
},
},
onConnect
:
()
=>
{
onConnect
:
()
=>
{
window
.
setTimeout
(()
=>
{
window
.
setTimeout
(()
=>
{
const
joined
=
joinSharedRoom
(
nickname
.
value
,
token
.
value
,
avatar
.
value
,
gameId
)
const
joined
=
joinSharedRoom
(
},
200
)
nickname
.
value
,
},
token
.
value
,
onGameStart
:
async
()
=>
{
avatar
.
value
,
showGameRule
.
value
=
false
;
gameId
,
await
mobileStageRef
.
value
?.
startCountdown
()
);
startGameView
()
},
200
);
},
},
onScoreSubmitted
:
(
is_save
,
data
)
=>
{
onGameStart
:
async
()
=>
{
if
(
!
is_save
)
{
//非保存状态下
showGameRule
.
value
=
false
;
const
nextScore
=
Number
(
typeof
data
===
'object'
?
data
?.
score
:
NaN
)
await
mobileStageRef
.
value
?.
startCountdown
();
const
nextRank
=
Number
(
typeof
data
===
'object'
?
data
?.
rank
:
data
)
startGameView
();
const
remainingSeconds
=
Number
(
typeof
data
===
'object'
?
data
?.
remainingSeconds
:
NaN
)
},
if
(
Number
.
isFinite
(
nextScore
)
&&
nextScore
>=
0
)
{
onScoreSubmitted
:
(
is_save
,
data
)
=>
{
score
.
value
=
nextScore
if
(
!
is_save
)
{
}
//非保存状态下
if
(
Number
.
isFinite
(
nextRank
)
&&
nextRank
>
0
)
{
const
nextRank
=
Number
(
typeof
data
===
"object"
?
data
?.
rank
:
data
);
rank
.
value
=
nextRank
if
(
Number
.
isFinite
(
nextRank
)
&&
nextRank
>
0
)
{
}
rank
.
value
=
nextRank
;
if
(
Number
.
isFinite
(
remainingSeconds
)
&&
remainingSeconds
>=
0
)
{
}
countdownInterval
.
value
=
remainingSeconds
}
else
if
(
typeof
data
===
"object"
&&
data
)
{
}
if
(
Array
.
isArray
(
data
.
list
))
{
}
else
if
(
typeof
data
===
'object'
&&
data
)
{
rankList
.
value
=
data
.
list
.
map
((
item
:
any
,
index
:
number
)
=>
({
if
(
Array
.
isArray
(
data
.
list
))
{
rank
:
Number
(
item
?.
rank
??
index
+
1
),
rankList
.
value
=
data
.
list
.
map
((
item
:
any
,
index
:
number
)
=>
({
nickname
:
String
(
item
?.
nickname
??
""
),
rank
:
Number
(
item
?.
rank
??
index
+
1
),
avatar
:
String
(
item
?.
avatar
??
""
),
nickname
:
String
(
item
?.
nickname
??
''
),
score
:
Number
(
item
?.
score
??
0
),
avatar
:
String
(
item
?.
avatar
??
''
),
}));
score
:
Number
(
item
?.
score
??
0
),
}
}))
}
const
nextRank
=
Number
(
data
.
rank
);
if
(
Number
.
isFinite
(
nextRank
)
&&
nextRank
>
0
)
{
const
nextRank
=
Number
(
data
.
rank
)
rank
.
value
=
nextRank
;
if
(
Number
.
isFinite
(
nextRank
)
&&
nextRank
>
0
)
{
}
rank
.
value
=
nextRank
}
}
},
}
onRescoreSubmitted
:
(
_recount
)
=>
{
return
{
code
:
'MSYF'
,
score
:
score
.
value
}
submitScore
(
true
);
},
},
onRescoreSubmitted
:
(
_recount
)
=>
{
onGameRecover
:
(
data
)
=>
{
submitScore
(
true
);
const
recoveredRank
=
Number
(
data
?.
rank
);
},
const
recoveredScore
=
Number
(
data
?.
score
);
onGameRecover
:
(
data
)
=>
{
const
recoveredSeconds
=
Number
(
data
?.
remainingSeconds
);
const
recoveredRank
=
Number
(
data
?.
rank
)
const
recoveredScore
=
Number
(
data
?.
score
)
rank
.
value
=
const
recoveredSeconds
=
Number
(
data
?.
remainingSeconds
)
Number
.
isFinite
(
recoveredRank
)
&&
recoveredRank
>
0
?
recoveredRank
:
0
;
score
.
value
=
rank
.
value
=
Number
.
isFinite
(
recoveredRank
)
&&
recoveredRank
>
0
?
recoveredRank
:
0
Number
.
isFinite
(
recoveredScore
)
&&
recoveredScore
>
0
score
.
value
=
Number
.
isFinite
(
recoveredScore
)
&&
recoveredScore
>
0
?
recoveredScore
:
0
?
recoveredScore
showGameRule
.
value
=
false
:
0
;
showGameRank
.
value
=
false
showGameRule
.
value
=
false
;
isGameOver
.
value
=
false
showGameRank
.
value
=
false
;
currentView
.
value
=
'playing'
isGameOver
.
value
=
false
;
startGameCountdown
(
currentView
.
value
=
"playing"
;
Number
.
isFinite
(
recoveredSeconds
)
&&
recoveredSeconds
>
0
?
recoveredSeconds
:
0
,
startGameCountdown
(
)
Number
.
isFinite
(
recoveredSeconds
)
&&
recoveredSeconds
>
0
},
?
recoveredSeconds
onRoomClosed
:
()
=>
{
:
0
,
stopGameCountdown
()
);
currentView
.
value
=
'loading'
},
setDivDescVisible
(
false
)
onRoomClosed
:
()
=>
{
$toast
(
'管理员关闭了游戏房间'
)
stopGameCountdown
();
},
currentView
.
value
=
"loading"
;
onRoomBack
:
()
=>
{
setDivDescVisible
(
false
);
},
$toast
(
"管理员关闭了游戏房间"
);
})
},
_offSocketMessage
.
value
=
offSocketMessage
onRoomBack
:
()
=>
{},
});
_offSocketMessage
.
value
=
offSocketMessage
;
}
}
onMounted
(()
=>
{
onMounted
(()
=>
{
document
.
title
=
'互动游戏 - 马上有福'
document
.
title
=
"互动游戏 - 马上有福"
;
if
(
token
.
value
)
{
if
(
token
.
value
)
{
window
.
addEventListener
(
'beforeunload'
,
confirmRefresh
)
window
.
addEventListener
(
"beforeunload"
,
confirmRefresh
);
}
}
})
// 本地测试模式:自动模拟游戏开始(IS_PRODUCTION=false 时生效)
if
(
isLocalMode
)
{
showGameRule
.
value
=
false
;
currentView
.
value
=
"playing"
;
startGameCountdown
();
}
});
onBeforeUnmount
(()
=>
{
onBeforeUnmount
(()
=>
{
_offSocketMessage
.
value
?.()
_offSocketMessage
.
value
?.();
stopAllMusic
()
stopAllMusic
();
if
(
guFrameTimer
)
{
if
(
guFrameTimer
)
{
window
.
clearTimeout
(
guFrameTimer
)
window
.
clearTimeout
(
guFrameTimer
);
}
}
stopGameCountdown
()
stopGameCountdown
();
if
(
token
.
value
)
{
if
(
token
.
value
)
{
window
.
removeEventListener
(
'beforeunload'
,
confirmRefresh
)
window
.
removeEventListener
(
"beforeunload"
,
confirmRefresh
);
}
}
})
})
;
const
showGameRuleHandler
=
()
=>
{
const
showGameRuleHandler
=
()
=>
{
showGameRule
.
value
=
!
showGameRule
.
value
;
showGameRule
.
value
=
!
showGameRule
.
value
;
}
}
;
const
renderBG
=
()
=>
{
const
renderBG
=
()
=>
{
if
(
currentView
.
value
==
'playing'
)
{
if
(
currentView
.
value
==
"playing"
)
{
return
`
${
imageUrls
.
bg2
}
center/cover`
return
`
${
imageUrls
.
bg2
}
center/cover`
;
}
}
return
`
${
imageUrls
.
bg
}
center/cover`
;
return
`
${
imageUrls
.
bg
}
center/cover`
;
}
}
;
const
rankCloseHandler
=
()
=>
{
const
rankCloseHandler
=
()
=>
{
showGameRank
.
value
=
false
showGameRank
.
value
=
false
;
isGameOver
.
value
=
false
isGameOver
.
value
=
false
;
resetToLoadingView
()
resetToLoadingView
();
}
}
;
</
script
>
</
script
>
<
template
>
<
template
>
<MobileStage
v-if=
"token"
:showGameRule=
"showGameRule"
:showGameRank=
"showGameRank"
ref=
"mobileStageRef"
<MobileStage
:background=
"`$
{renderBG()}`">
v-if=
"token || isLocalMode"
<template
#
gameRule
>
:showGameRule=
"showGameRule"
<div
class=
"rule-container"
>
:showGameRank=
"showGameRank"
<div
class=
"rule-txt-container"
>
ref=
"mobileStageRef"
<div
class=
"rule-title"
>
游戏规则
</div>
:background=
"`$
{renderBG()}`"
<div
class=
"rule-txt"
>
>
1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2 分;
<br
/>
<template
#
gameRule
>
2. 限时闯关比拼得分,结束后按积分排名;
<br
/>
<div
class=
"rule-container"
>
3. 高分用户有机会领取新疆福彩定制周边;
<br
/>
<div
class=
"rule-txt-container"
>
4. 禁止外挂作弊,违规账号取消成绩与领奖资格;
<br
/><br
/>
<div
class=
"rule-title"
>
游戏规则
</div>
祝您游戏愉快!
<div
class=
"rule-txt"
>
</div>
1. 开赛左右操控小马,接福袋 + 5 分、碰地雷 - 3 分,每秒行进 + 2
<div
class=
"rule-close"
@
click=
"showGameRuleHandler"
></div>
分;
<br
/>
</div>
2. 限时闯关比拼得分,结束后按积分排名;
<br
/>
3. 高分用户有机会领取新疆福彩定制周边;
<br
/>
4. 禁止外挂作弊,违规账号取消成绩与领奖资格;
<br
/><br
/>
祝您游戏愉快!
</div>
<div
class=
"rule-close"
@
click=
"showGameRuleHandler"
></div>
</div>
</div>
</
template
>
<
template
#
gameRank
>
<div
class=
"ranklist-container"
>
<div
class=
"txt-title"
>
排行
</div>
<div
class=
"list-container"
>
<div
class=
"row row-1"
>
<div
class=
"col-1"
>
排名
</div>
<div
class=
"col-2"
>
用户
</div>
<div
class=
"col-3"
>
成绩
</div>
</div>
<div
class=
"row-2"
>
<div
class=
"row rank-row"
v-for=
"(item, index) in displayRankList"
:key=
"`$
{item.rank}-${index}`"
>
<div
class=
"col-1"
>
<div>
{{
item
.
rank
}}
</div>
</div>
<div
class=
"col-2"
>
<div
class=
"avatar"
:style=
"
item.avatar
?
{ backgroundImage: `url(${item.avatar})` }
: {}
"
>
</div>
<div>
{{
item
.
nickname
}}
</div>
</div>
<div
class=
"col-3"
>
{{
item
.
score
}}
</div>
</div>
</div>
</
template
>
</div>
<
template
#
gameRank
>
<div
class=
"row row-3"
>
<div
class=
"ranklist-container"
>
<div
class=
"col-1"
>
{{
rank
>
0
?
rank
:
"未上榜"
}}
</div>
<div
class=
"txt-title"
>
排行
</div>
<div
class=
"col-2"
>
<div
class=
"list-container"
>
<div
<div
class=
"row row-1"
>
class=
"avatar"
<div
class=
"col-1"
>
排名
</div>
:style=
"
{ backgroundImage: `url(${avatar})` }"
<div
class=
"col-2"
>
用户
</div>
>
</div>
<div
class=
"col-3"
>
成绩
</div>
<div>
{{
nickname
}}
</div>
</div>
</div>
<div
class=
"row-2"
>
<div
class=
"col-3"
>
{{
rankScore
}}
</div>
<div
class=
"row rank-row"
v-for=
"(item, index) in displayRankList"
</div>
:key=
"`$
{item.rank}-${index}`">
</div>
<div
class=
"col-1"
>
<div
class=
"img-close"
@
click=
"rankCloseHandler"
></div>
<div>
{{
item
.
rank
}}
</div>
</div>
</div>
</
template
>
<div
class=
"col-2"
>
<LoadingView
<div
class=
"avatar"
v-if=
"currentView === 'loading'"
:style=
"item.avatar ?
{ backgroundImage: `url(${item.avatar})` } : {}">
</div>
:image-urls=
"imageUrls"
<div>
{{
item
.
nickname
}}
</div>
:user-join-status=
"userJoinStatus"
</div>
@
touchGameRule=
"showGameRuleHandler"
<div
class=
"col-3"
>
{{
item
.
score
}}
</div>
/>
</div>
<PlayingView
</div>
v-else
<div
class=
"row row-3"
>
:image-urls=
"imageUrls"
<div
class=
"col-1"
>
{{
rank
>
0
?
rank
:
'未上榜'
}}
</div>
:tick=
"score"
<div
class=
"col-2"
>
:rank=
"rank"
<div
class=
"avatar"
:style=
"
{ backgroundImage: `url(${avatar})` }">
</div>
:countdown-interval=
"countdownInterval"
<div>
{{
nickname
}}
</div>
:is-div-desc-visible=
"isDivDescVisible"
</div>
@
score-change=
"scoreChangeHandler"
<div
class=
"col-3"
>
{{
rankScore
}}
</div>
@
rank-close=
"rankCloseHandler"
</div>
/>
</div>
<div
class=
"img-close"
@
click=
"rankCloseHandler"
></div>
<!-- <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
</div>
</
template
>
<LoadingView
v-if=
"currentView === 'loading'"
:image-urls=
"imageUrls"
:user-join-status=
"userJoinStatus"
@
touchGameRule=
"showGameRuleHandler"
/>
<PlayingView
v-else
:image-urls=
"imageUrls"
:tick=
"score"
:rank=
"rank"
:countdown-interval=
"countdownInterval"
:is-div-desc-visible=
"isDivDescVisible"
@
score-change=
"scoreChangeHandler"
@
rank-close=
"rankCloseHandler"
/>
<!-- <LoadingView v-if="currentView === 'loading'" :image-urls="imageUrls" :user-join-status="userJoinStatus"
@touchGameRule="showGameRuleHandler" />
@touchGameRule="showGameRuleHandler" />
<PlayingView v-else :image-urls="imageUrls" :tick="score" :rank="rank" :countdown-interval="countdownInterval"
<PlayingView v-else :image-urls="imageUrls" :tick="score" :rank="rank" :countdown-interval="countdownInterval"
:is-div-desc-visible="isDivDescVisible" :is-game-over="isGameOver"
:is-div-desc-visible="isDivDescVisible" :is-game-over="isGameOver"
:rank-list="displayRankList" :user-rank="rank" :user-score="rankScore"
:rank-list="displayRankList" :user-rank="rank" :user-score="rankScore"
:user-nickname="nickname" :user-avatar="avatar"
:user-nickname="nickname" :user-avatar="avatar"
@touch="touchHandler" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" /> -->
@touch="touchHandler" @score-change="scoreChangeHandler" @rank-close="rankCloseHandler" /> -->
</MobileStage>
</MobileStage>
<div
v-else
style=
"text-align: center; width: 100vw; height: 100vh; line-height: 30; font-size: 20px;"
>
<div
请使用微信扫码进入游戏
v-else
</div>
style=
"
text-align: center;
width: 100vw;
height: 100vh;
line-height: 30;
font-size: 20px;
"
>
请使用微信扫码进入游戏
</div>
</template>
</template>
<
style
scoped
>
<
style
scoped
>
.rule-container
{
.rule-container
{
.rule-txt-container
{
.rule-txt-container
{
background
:
v-bind
(
'imageUrls.rule'
)
center
/
cover
no-repeat
;
background
:
v-bind
(
"imageUrls.rule"
)
center
/
cover
no-repeat
;
width
:
692px
;
width
:
692px
;
height
:
797px
;
height
:
797px
;
margin-top
:
-20%
;
margin-top
:
-20%
;
transform
:
scale
(
0.5
);
transform
:
scale
(
0.5
);
.rule-title
{
.rule-title
{
text-align
:
center
;
text-align
:
center
;
color
:
white
;
color
:
white
;
font-weight
:
bold
;
font-weight
:
bold
;
font-size
:
26pt
;
font-size
:
26pt
;
line-height
:
50pt
;
line-height
:
50pt
;
letter-spacing
:
4px
;
letter-spacing
:
4px
;
}
.rule-txt
{
font-size
:
26pt
;
padding
:
40px
;
margin-top
:
10px
;
color
:
#AA0000
;
}
}
}
.rule-close
{
.rule-close
{
...
@@ -407,88 +466,104 @@ const rankCloseHandler = () => {
...
@@ -407,88 +466,104 @@ const rankCloseHandler = () => {
left
:
100%
;
left
:
100%
;
margin-left
:
-48px
;
margin-left
:
-48px
;
}
}
}
.rule-close
{
width
:
57px
;
height
:
57px
;
background
:
v-bind
(
"imageUrls.close"
)
center
/
cover
no-repeat
;
transform
:
translateX
(
-50%
)
scale
(
1.4
);
position
:
absolute
;
bottom
:
-75px
;
left
:
50%
;
}
}
}
.ranklist-container
{
.ranklist-container
{
width
:
747px
;
width
:
747px
;
height
:
1084px
;
height
:
1084px
;
background
:
v-bind
(
'imageUrls.ranklist'
);
background
:
v-bind
(
"imageUrls.ranklist"
);
transform
:
translate
(
-50%
,
-50%
)
scale
(
0.45
);
transform
:
translate
(
-50%
,
-50%
)
scale
(
0.45
);
left
:
50%
;
left
:
50%
;
top
:
50%
;
top
:
50%
;
position
:
absolute
;
.txt-title
{
width
:
100%
;
text-align
:
center
;
font-size
:
50px
;
margin-top
:
35px
;
font-weight
:
bold
;
color
:
#aa0000
;
letter-spacing
:
2px
;
}
.list-container
{
width
:
calc
(
100%
-
90px
);
height
:
calc
(
100%
-
210px
);
position
:
absolute
;
position
:
absolute
;
left
:
40px
;
.txt-title
{
top
:
140px
;
width
:
100%
;
text-align
:
center
;
text-align
:
center
;
height
:
73px
;
font-size
:
50px
;
line-height
:
73px
;
margin-top
:
35px
;
font-size
:
32px
;
font-weight
:
bold
;
color
:
#AA0000
;
.row
{
letter-spacing
:
2px
;
display
:
grid
;
grid-template-columns
:
150px
auto
150px
;
align-items
:
center
;
}
}
.list-container
{
.row-1
{
width
:
calc
(
100%
-
90px
);
color
:
#aa0000
;
height
:
calc
(
100%
-
210px
);
font-weight
:
bold
;
position
:
absolute
;
background
:
linear-gradient
(
left
:
40px
;
-84deg
,
top
:
140px
;
#ffd396
0%
,
text-align
:
center
;
#fff3d1
53%
,
height
:
73px
;
#ffd59c
100%
line-height
:
73px
;
);
font-size
:
32px
;
border-radius
:
20px
20px
0px
0px
;
}
.row
{
display
:
grid
;
grid-template-columns
:
150px
auto
150px
;
align-items
:
center
;
}
.row-1
{
color
:
#AA0000
;
font-weight
:
bold
;
background
:
linear-gradient
(
-84deg
,
#FFD396
0%
,
#FFF3D1
53%
,
#FFD59C
100%
);
border-radius
:
20px
20px
0px
0px
;
}
.row-2
{
.row-2
{
color
:
#633911
;
color
:
#633911
;
.
rank-row
:
nth-child
(
odd
)
{
.
rank-row
:
nth-child
(
odd
)
{
background
:
#F2E2AE
;
background
:
#f2e2ae
;
}
}
.rank-row
:nth-child
(
even
)
{
.rank-row
:nth-child
(
even
)
{
background
:
#FCEEBF
;
background
:
#fceebf
;
}
}
.col-1
{
.col-1
{
position
:
relative
;
position
:
relative
;
}
}
}
}
.row-3
{
.row-3
{
background
:
linear-gradient
(
0deg
,
#F4B64C
0%
,
#F7C96F
100%
);
background
:
linear-gradient
(
0deg
,
#f4b64c
0%
,
#f7c96f
100%
);
border-radius
:
0px
0px
20px
20px
;
border-radius
:
0px
0px
20px
20px
;
}
}
.col-2
{
.col-2
{
display
:
flex
;
display
:
flex
;
align-items
:
center
;
align-items
:
center
;
justify-content
:
center
;
justify-content
:
center
;
gap
:
18px
;
gap
:
18px
;
.avatar
{
.avatar
{
width
:
48px
;
width
:
48px
;
height
:
48px
;
height
:
48px
;
flex
:
0
0
auto
;
flex
:
0
0
auto
;
border-radius
:
50%
;
border-radius
:
50%
;
background-position
:
center
;
background-position
:
center
;
background-size
:
cover
;
background-size
:
cover
;
}
}
}
}
}
}
}
}
.img-close
{
.img-close
{
...
...
frontend-h5/src/pages/game3/views/PlayingView.vue
View file @
82fe3ea6
...
@@ -26,8 +26,26 @@ const HORSE_BOTTOM_PCT = 19.11;
...
@@ -26,8 +26,26 @@ const HORSE_BOTTOM_PCT = 19.11;
const
ITEM_SIZE
=
80
;
const
ITEM_SIZE
=
80
;
const
GAME_DURATION
=
60
;
const
GAME_DURATION
=
60
;
const
LEFT_LANE_X
=
170
;
// 设计基准跑道 X 位置(750px 设计稿下的值,窄屏设备上会自动居中偏移)
const
RIGHT_LANE_X
=
470
;
const
DESIGN_LEFT_LANE_X
=
150
;
const
DESIGN_RIGHT_LANE_X
=
500
;
// 运行时根据视口计算的真实 X 位置(适配不同屏幕宽度)
const
leftLaneX
=
ref
(
DESIGN_LEFT_LANE_X
);
const
rightLaneX
=
ref
(
DESIGN_RIGHT_LANE_X
);
/** 按视口宽度计算 750px 设计区域在 content-bg 中的居中偏移 */
const
updateLanePositions
=
()
=>
{
const
vw
=
window
.
visualViewport
?.
width
??
window
.
innerWidth
;
const
vh
=
window
.
visualViewport
?.
height
??
window
.
innerHeight
;
// MobileStage contain 模式下 scale = min(scaleX, scaleY),手机通常由高度决定
const
scaleY
=
vh
/
1624
;
if
(
!
scaleY
||
scaleY
<=
0
)
return
;
const
stageViewportWidth
=
vw
/
scaleY
;
const
offset
=
Math
.
max
(
0
,
(
stageViewportWidth
-
750
)
/
2
);
leftLaneX
.
value
=
DESIGN_LEFT_LANE_X
+
offset
;
rightLaneX
.
value
=
DESIGN_RIGHT_LANE_X
+
offset
;
};
// ========== 马帧 ==========
// ========== 马帧 ==========
const
HORSE_FRAME_COUNT
=
21
;
const
HORSE_FRAME_COUNT
=
21
;
...
@@ -63,20 +81,20 @@ preloadFrames(bomFrames);
...
@@ -63,20 +81,20 @@ preloadFrames(bomFrames);
const
horseLane
=
ref
<
'left'
|
'right'
>
(
'left'
);
const
horseLane
=
ref
<
'left'
|
'right'
>
(
'left'
);
const
horseFrameIndex
=
ref
(
0
);
const
horseFrameIndex
=
ref
(
0
);
const
horseX
=
ref
(
LEFT_LANE_X
);
const
horseX
=
ref
(
leftLaneX
.
value
);
const
horseTargetX
=
ref
(
LEFT_LANE_X
);
const
horseTargetX
=
ref
(
leftLaneX
.
value
);
const
switchToLeft
=
()
=>
{
const
switchToLeft
=
()
=>
{
if
(
horseLane
.
value
!==
'left'
)
{
if
(
horseLane
.
value
!==
'left'
)
{
horseLane
.
value
=
'left'
;
horseLane
.
value
=
'left'
;
horseTargetX
.
value
=
LEFT_LANE_X
;
horseTargetX
.
value
=
leftLaneX
.
value
;
}
}
};
};
const
switchToRight
=
()
=>
{
const
switchToRight
=
()
=>
{
if
(
horseLane
.
value
!==
'right'
)
{
if
(
horseLane
.
value
!==
'right'
)
{
horseLane
.
value
=
'right'
;
horseLane
.
value
=
'right'
;
horseTargetX
.
value
=
RIGHT_LANE_X
;
horseTargetX
.
value
=
rightLaneX
.
value
;
}
}
};
};
...
@@ -183,7 +201,7 @@ function generateNewItem() {
...
@@ -183,7 +201,7 @@ function generateNewItem() {
if
(
!
props
.
isDivDescVisible
)
return
;
if
(
!
props
.
isDivDescVisible
)
return
;
const
seed
=
Date
.
now
()
+
itemIdCounter
;
const
seed
=
Date
.
now
()
+
itemIdCounter
;
const
kind
:
ItemKind
=
seededRandom
(
seed
)
<
0.
5
5
?
'coin'
:
'bomb'
;
const
kind
:
ItemKind
=
seededRandom
(
seed
)
<
0.
7
5
?
'coin'
:
'bomb'
;
// 概率分配车道,但避免与前一个同车道道具太近
// 概率分配车道,但避免与前一个同车道道具太近
let
lane
:
'left'
|
'right'
=
seededRandom
(
seed
+
1000
)
<
0.5
?
'left'
:
'right'
;
let
lane
:
'left'
|
'right'
=
seededRandom
(
seed
+
1000
)
<
0.5
?
'left'
:
'right'
;
...
@@ -244,7 +262,7 @@ const checkCollisions = () => {
...
@@ -244,7 +262,7 @@ const checkCollisions = () => {
for
(
const
item
of
roadItems
.
value
)
{
for
(
const
item
of
roadItems
.
value
)
{
if
(
item
.
collected
||
item
.
colliding
)
continue
;
if
(
item
.
collected
||
item
.
colliding
)
continue
;
const
itemX
=
item
.
lane
===
'left'
?
LEFT_LANE_X
:
RIGHT_LANE_X
;
const
itemX
=
item
.
lane
===
'left'
?
leftLaneX
.
value
:
rightLaneX
.
value
;
// item.y 已经是屏幕坐标(content-bg 内),无需加 roadOffset
// item.y 已经是屏幕坐标(content-bg 内),无需加 roadOffset
const
itemCenterX
=
itemX
+
ITEM_SIZE
/
2
;
const
itemCenterX
=
itemX
+
ITEM_SIZE
/
2
;
const
itemCenterY
=
item
.
y
+
ITEM_SIZE
/
2
;
const
itemCenterY
=
item
.
y
+
ITEM_SIZE
/
2
;
...
@@ -278,7 +296,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
...
@@ -278,7 +296,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
const
frameDuration
=
1000
/
totalFrames
;
const
frameDuration
=
1000
/
totalFrames
;
// 碰撞动画(大幅放大尺寸,x 往右偏移让动画居中于视觉碰撞点)
// 碰撞动画(大幅放大尺寸,x 往右偏移让动画居中于视觉碰撞点)
effectAnims
.
value
.
push
({
id
:
animId
,
kind
:
item
.
kind
,
frame
:
0
,
x
:
x
+
40
,
y
});
effectAnims
.
value
.
push
({
id
:
animId
,
kind
:
item
.
kind
,
frame
:
0
,
x
:
x
+
65
,
y
});
let
frameIdx
=
0
;
let
frameIdx
=
0
;
const
tick
=
()
=>
{
const
tick
=
()
=>
{
...
@@ -297,7 +315,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
...
@@ -297,7 +315,7 @@ const playCollectEffect = (item: RoadItem, x: number, y: number) => {
const
effectId
=
++
scoreEffectIdSeq
;
const
effectId
=
++
scoreEffectIdSeq
;
scoreEffects
.
value
.
push
({
scoreEffects
.
value
.
push
({
id
:
effectId
,
id
:
effectId
,
x
:
x
+
ITEM_SIZE
/
2
,
x
:
x
+
ITEM_SIZE
/
2
+
25
,
y
:
y
,
y
:
y
,
score
:
delta
,
score
:
delta
,
});
});
...
@@ -377,6 +395,12 @@ const stopMainLoop = () => {
...
@@ -377,6 +395,12 @@ const stopMainLoop = () => {
// ========== 生命周期 ==========
// ========== 生命周期 ==========
onMounted
(()
=>
{
onMounted
(()
=>
{
window
.
addEventListener
(
'keydown'
,
handleKeydown
);
window
.
addEventListener
(
'keydown'
,
handleKeydown
);
window
.
addEventListener
(
'resize'
,
updateLanePositions
);
window
.
visualViewport
?.
addEventListener
(
'resize'
,
updateLanePositions
);
updateLanePositions
();
// 初始化马匹在左侧跑道(使用计算后的实际 X 位置)
horseX
.
value
=
leftLaneX
.
value
;
horseTargetX
.
value
=
leftLaneX
.
value
;
nextTick
(()
=>
{
nextTick
(()
=>
{
startRoadScroll
();
startRoadScroll
();
startMainLoop
();
startMainLoop
();
...
@@ -386,6 +410,8 @@ onMounted(() => {
...
@@ -386,6 +410,8 @@ onMounted(() => {
onBeforeUnmount
(()
=>
{
onBeforeUnmount
(()
=>
{
window
.
removeEventListener
(
'keydown'
,
handleKeydown
);
window
.
removeEventListener
(
'keydown'
,
handleKeydown
);
window
.
removeEventListener
(
'resize'
,
updateLanePositions
);
window
.
visualViewport
?.
removeEventListener
(
'resize'
,
updateLanePositions
);
stopRoadScroll
();
stopRoadScroll
();
stopMainLoop
();
stopMainLoop
();
stopItemGen
();
stopItemGen
();
...
@@ -405,6 +431,9 @@ watch(() => props.isDivDescVisible, (visible) => {
...
@@ -405,6 +431,9 @@ watch(() => props.isDivDescVisible, (visible) => {
scoreEffects
.
value
=
[];
scoreEffects
.
value
=
[];
roadOffset
.
value
=
0
;
roadOffset
.
value
=
0
;
gameElapsed
.
value
=
0
;
gameElapsed
.
value
=
0
;
updateLanePositions
();
horseX
.
value
=
leftLaneX
.
value
;
horseTargetX
.
value
=
leftLaneX
.
value
;
startRoadScroll
();
startRoadScroll
();
startMainLoop
();
startMainLoop
();
scheduleNextItem
();
scheduleNextItem
();
...
@@ -582,18 +611,21 @@ watch(() => props.isDivDescVisible, (visible) => {
...
@@ -582,18 +611,21 @@ watch(() => props.isDivDescVisible, (visible) => {
.road-item
{
.road-item
{
position
:
absolute
;
position
:
absolute
;
width
:
80px
;
/* width: 80px;
height
:
80px
;
height: 80px; */
width
:
77px
;
height
:
96px
;
z-index
:
5
;
z-index
:
5
;
pointer-events
:
none
;
pointer-events
:
none
;
}
}
.item-lane-left
{
.item-lane-left
{
left
:
170px
;
/* 当 content-bg 宽于 750px 设计稿时,自动居中 750px 游戏区域 */
left
:
calc
((
var
(
--stage-viewport-width
,
750px
)
-
750px
)
/
2
+
170px
);
}
}
.item-lane-right
{
.item-lane-right
{
left
:
470px
;
left
:
calc
((
var
(
--stage-viewport-width
,
750px
)
-
750px
)
/
2
+
520px
)
;
}
}
.effect-anim
{
.effect-anim
{
...
...
frontend-large/src/pages/game3/Game3.vue
View file @
82fe3ea6
...
@@ -55,36 +55,48 @@ const renderPlayers = (data: any) => {
...
@@ -55,36 +55,48 @@ const renderPlayers = (data: any) => {
}
}
const
updateRankPlayers
=
(
data
:
any
)
=>
{
const
updateRankPlayers
=
(
data
:
any
)
=>
{
if
(
!
Array
.
isArray
(
data
))
return
// 兼容多种数据格式:纯数组 / {list:[]} / {players:[]} / {data:[]}
let
arr
:
any
[]
|
null
=
null
;
if
(
Array
.
isArray
(
data
))
{
arr
=
data
;
}
else
if
(
Array
.
isArray
(
data
?.
list
))
{
arr
=
data
.
list
;
}
else
if
(
Array
.
isArray
(
data
?.
players
))
{
arr
=
data
.
players
;
}
else
if
(
Array
.
isArray
(
data
?.
data
))
{
arr
=
data
.
data
;
}
rankPlayers
.
value
=
Array
.
from
(
if
(
!
arr
||
arr
.
length
===
0
)
{
{
length
:
RANK_LIMIT
},
console
.
warn
(
'[game3] updateRankPlayers: 无法解析排行数据'
,
data
);
(
_
,
index
)
=>
data
[
index
]
??
emptyRankPlayer
(
index
),
return
;
)
}
}
const
playerToRankPlayer
=
(
player
:
any
,
index
:
number
):
Player
=>
({
// 打印第一条原始数据的所有字段名,用于排查字段不匹配问题
telephone
:
player
?.
telephone
??
''
,
console
.
log
(
'[game3] updateRankPlayers: 收到排行数据'
,
arr
.
length
,
'条, 第1条字段:'
,
Object
.
keys
(
arr
[
0
]
||
{}),
', 第1条值:'
,
JSON
.
parse
(
JSON
.
stringify
(
arr
[
0
])));
userid
:
player
?.
userid
??
''
,
wechat_id
:
player
?.
wechat_id
||
`empty-
${
index
}
`
,
// 将 WS 数据标准化为 Player 格式(同时保留 userid / wechat_id)
nickname
:
player
?.
nickname
||
'虚位以待'
,
// ⚠️ 当 WS 返回的数据没有 userid/openid/wechat_id 等ID字段时,用 avatar 作为唯一标识
avatar
:
player
?.
avatar
||
''
,
const
normalized
=
arr
.
map
((
item
:
any
,
i
:
number
)
=>
{
score
:
player
?.
score
??
'-'
,
const
hasId
=
item
.
userid
||
item
.
openid
||
item
.
user_id
||
item
.
wechat_id
;
})
const
avatar
=
item
.
avatar
||
item
.
headimgurl
||
''
;
const
nickname
=
item
.
nickname
||
item
.
name
||
'虚位以待'
;
const
isRealPlayer
=
nickname
&&
nickname
!==
'-'
&&
nickname
!==
'虚位以待'
;
const
wechat_id
=
hasId
||
(
isRealPlayer
&&
avatar
?
`avt:
${
avatar
}
`
:
`empty-
${
i
}
`
);
return
{
...
item
,
userid
:
hasId
||
(
isRealPlayer
&&
avatar
?
`avt:
${
avatar
}
`
:
''
),
wechat_id
,
nickname
,
avatar
,
score
:
item
.
score
??
0
,
};
});
const
seedRankPlayersFromLoading
=
()
=>
{
rankPlayers
.
value
=
Array
.
from
(
rankPlayers
.
value
=
Array
.
from
(
{
length
:
RANK_LIMIT
},
{
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
{
(
_
,
index
)
=>
normalized
[
index
]
??
emptyRankPlayer
(
index
),
const
player
=
loadingPlayers
.
value
[
index
]
);
if
(
!
player
?.
userid
&&
!
player
?.
wechat_id
)
{
return
emptyRankPlayer
(
index
)
}
return
playerToRankPlayer
(
player
,
index
)
},
)
}
}
const
resetRoundState
=
()
=>
{
const
resetRoundState
=
()
=>
{
...
@@ -107,7 +119,7 @@ const gotoRank1WithIntro = async () => {
...
@@ -107,7 +119,7 @@ const gotoRank1WithIntro = async () => {
if
(
isPlayingStartAnimation
)
return
if
(
isPlayingStartAnimation
)
return
isPlayingStartAnimation
=
true
isPlayingStartAnimation
=
true
seedRankPlayersFromLoading
()
// ⚠️ 不要用 seedRankPlayersFromLoading,由 WS room_rank_result 驱动马匹数据
screen
.
value
=
'rank1'
screen
.
value
=
'rank1'
await
nextTick
()
await
nextTick
()
await
rank1Ref
.
value
?.
startIntro
()
await
rank1Ref
.
value
?.
startIntro
()
...
@@ -129,8 +141,16 @@ function handleRoomState(data: any) {
...
@@ -129,8 +141,16 @@ function handleRoomState(data: any) {
gotoLoading
()
gotoLoading
()
break
break
case
2
:
case
2
:
seedRankPlayersFromLoading
()
// 重连/中途进房时游戏已在运行,直接进入赛马画面
if
(
isPlayingStartAnimation
)
break
;
isPlayingStartAnimation
=
true
;
screen
.
value
=
'rank1'
screen
.
value
=
'rank1'
console
.
log
(
'[game3] handleRoomState: status=2, 直接启动赛马动画'
);
nextTick
(()
=>
{
rank1Ref
.
value
?.
startIntro
().
finally
(()
=>
{
isPlayingStartAnimation
=
false
;
});
});
break
break
case
3
:
case
3
:
screen
.
value
=
'rank2'
screen
.
value
=
'rank2'
...
@@ -143,24 +163,58 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
...
@@ -143,24 +163,58 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
onAny
:
(
evt
,
payload
)
=>
{
onAny
:
(
evt
,
payload
)
=>
{
// 兜底处理所有非标准事件,解析玩家实时分数(对齐 game4)
// 兜底处理所有非标准事件,解析玩家实时分数(对齐 game4)
const
data
=
payload
?.
data
??
payload
;
const
data
=
payload
?.
data
??
payload
;
if
(
!
Array
.
isArray
(
data
))
return
;
// 尝试从 data 中提取数组
const
boosts
:
Record
<
string
,
number
>
=
{};
let
arr
:
any
[]
|
null
=
null
;
let
hasBoost
=
false
;
if
(
Array
.
isArray
(
data
))
{
arr
=
data
;
data
.
forEach
((
item
:
any
)
=>
{
}
else
if
(
Array
.
isArray
(
data
?.
list
))
{
const
id
=
item
.
userid
||
item
.
wechat_id
||
item
.
avatar
||
""
;
arr
=
data
.
list
;
if
(
!
id
)
return
;
}
else
if
(
Array
.
isArray
(
data
?.
data
))
{
const
raw
=
item
.
score
??
item
.
boost
??
item
.
acceleration
??
item
.
speed
??
item
.
progress
;
arr
=
data
.
data
;
if
(
raw
===
undefined
||
raw
===
null
)
return
;
}
const
val
=
Number
(
raw
);
if
(
isNaN
(
val
))
return
;
if
(
arr
&&
arr
.
length
>
0
)
{
boosts
[
id
]
=
Math
.
max
(
0
,
val
);
const
boosts
:
Record
<
string
,
number
>
=
{};
hasBoost
=
true
;
const
normalized
=
arr
.
map
((
item
:
any
,
i
:
number
)
=>
{
});
const
hasId
=
item
.
userid
||
item
.
openid
||
item
.
user_id
||
item
.
wechat_id
;
const
avatar
=
item
.
avatar
||
item
.
headimgurl
||
''
;
if
(
hasBoost
)
{
const
nickname
=
item
.
nickname
||
item
.
name
||
'虚位以待'
;
horseBoosts
.
value
=
{
...
boosts
};
const
isRealPlayer
=
nickname
&&
nickname
!==
'-'
&&
nickname
!==
'虚位以待'
;
const
wechat_id
=
hasId
||
(
isRealPlayer
&&
avatar
?
`avt:
${
avatar
}
`
:
`empty-
${
i
}
`
);
return
{
...
item
,
userid
:
hasId
||
(
isRealPlayer
&&
avatar
?
`avt:
${
avatar
}
`
:
''
),
wechat_id
,
};
});
const
hasAnyPlayer
=
normalized
.
some
((
item
:
any
)
=>
{
const
id
=
item
.
userid
||
item
.
wechat_id
||
''
;
return
!!
id
&&
!
id
.
startsWith
(
'empty-'
);
});
if
(
hasAnyPlayer
)
{
// 同时更新 rankPlayers(确保马匹列表有真实玩家)
rankPlayers
.
value
=
Array
.
from
(
{
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
normalized
[
index
]
??
emptyRankPlayer
(
index
),
);
}
normalized
.
forEach
((
item
:
any
)
=>
{
const
id
=
item
.
userid
||
item
.
wechat_id
||
''
;
if
(
!
id
||
id
.
startsWith
(
'empty-'
))
return
;
const
raw
=
item
.
score
??
item
.
boost
??
item
.
acceleration
??
item
.
speed
??
item
.
progress
;
if
(
raw
===
undefined
||
raw
===
null
)
return
;
const
val
=
Number
(
raw
);
if
(
isNaN
(
val
))
return
;
boosts
[
id
]
=
Math
.
max
(
0
,
val
);
});
if
(
Object
.
keys
(
boosts
).
length
>
0
)
{
horseBoosts
.
value
=
{
...
horseBoosts
.
value
,
...
boosts
};
}
}
}
},
},
onRoomState
:
handleRoomState
,
onRoomState
:
handleRoomState
,
...
@@ -232,5 +286,7 @@ watch(screen, (value) => {
...
@@ -232,5 +286,7 @@ watch(screen, (value) => {
<
template
>
<
template
>
<Loading
v-if=
"screen === 'loading'"
:players=
"loadingPlayers"
:player-count=
"playerCount"
<Loading
v-if=
"screen === 'loading'"
:players=
"loadingPlayers"
:player-count=
"playerCount"
@
start=
"startGameWithMusic"
@
back=
"backHandler"
/>
@
start=
"startGameWithMusic"
@
back=
"backHandler"
/>
<Rank1
v-else
ref=
"rank1Ref"
:rank-list=
"rankPlayers"
:horse-boosts=
"horseBoosts"
@
start=
"startGameWithMusic"
@
next=
"nextRoundWithMusic"
@
back=
"backHandler"
/>
<Rank1
v-if=
"screen === 'rank1'"
ref=
"rank1Ref"
:rank-list=
"rankPlayers"
:horse-boosts=
"horseBoosts"
@
start=
"startGameWithMusic"
@
next=
"nextRoundWithMusic"
@
back=
"backHandler"
/>
<Rank2
v-if=
"screen === 'rank2'"
:rank-list=
"rankPlayers"
@
next=
"nextRoundWithMusic"
@
back=
"backHandler"
/>
</
template
>
</
template
>
frontend-large/src/pages/game3/views/Rank1View.vue
View file @
82fe3ea6
...
@@ -44,6 +44,7 @@ const imageUrls = {
...
@@ -44,6 +44,7 @@ const imageUrls = {
const
HORSE_SIZE
=
280
;
const
HORSE_SIZE
=
280
;
const
HORSE_START_X
=
-
HORSE_SIZE
;
const
HORSE_START_X
=
-
HORSE_SIZE
;
const
HORSE_END_X
=
1920
;
const
HORSE_END_X
=
1920
;
const
HORSE_Y_OFFSET
=
80
;
// 马匹整体向下偏移,补偿帧图片中马匹偏上的空白区域
const
horseFrameConfigs
=
[
const
horseFrameConfigs
=
[
{
type
:
1
,
dir
:
'game3/ma1'
,
prefix
:
'1ma_'
,
startIndex
:
24
,
count
:
12
},
{
type
:
1
,
dir
:
'game3/ma1'
,
prefix
:
'1ma_'
,
startIndex
:
24
,
count
:
12
},
...
@@ -68,11 +69,13 @@ interface Horse {
...
@@ -68,11 +69,13 @@ interface Horse {
lane
:
number
;
lane
:
number
;
frameIndex
:
number
;
frameIndex
:
number
;
frameTimer
:
number
;
frameTimer
:
number
;
x
:
number
;
x
:
number
;
// 像素位置(用于渲染 translateX)
targetX
:
number
;
xPercent
:
number
;
// 进度百分比 0~100(对齐 game4 时间驱动模型)
boost
:
number
;
// 瞬时加速值,每帧衰减
nickname
:
string
;
nickname
:
string
;
avatar
:
string
;
avatar
:
string
;
wechat_id
:
string
;
// 用于匹配 WebSocket 实时排名数据
/** 稳定标识,优先 wechat_id 再 userid,用于匹配 WebSocket 数据 */
playerKey
:
string
;
}
}
const
horses
=
ref
<
Horse
[]
>
([]);
const
horses
=
ref
<
Horse
[]
>
([]);
...
@@ -87,21 +90,63 @@ const emptyPlayer = (index: number): Player => ({
...
@@ -87,21 +90,63 @@ const emptyPlayer = (index: number): Player => ({
});
});
const
list
=
ref
<
Player
[]
>
(
Array
.
from
({
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
emptyPlayer
(
index
)));
const
list
=
ref
<
Player
[]
>
(
Array
.
from
({
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
emptyPlayer
(
index
)));
const
playerKey
=
(
player
:
Player
,
index
=
0
)
=>
player
.
userid
||
player
.
wechat_id
||
`empty-
${
index
}
`
;
const
updateRankList
=
(
rankList
:
Player
[])
=>
{
const
updateRankList
=
(
rankList
:
Player
[])
=>
{
const
next
=
Array
.
from
({
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
rankList
[
index
]
??
emptyPlayer
(
index
));
const
next
=
Array
.
from
({
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
rankList
[
index
]
??
emptyPlayer
(
index
));
list
.
value
=
next
;
list
.
value
=
next
;
};
};
// 玩家实时分数(合并 rankList + horseBoosts 双通道,对齐 game4 模式)
// ========== 瞬时加速脉冲(对齐 game4 detectScoreChanges) ==========
const
liveScores
=
ref
<
Record
<
string
,
number
>>
({});
const
transientBoosts
=
ref
<
Record
<
string
,
number
>>
({});
const
prevScores
:
Record
<
string
,
number
>
=
{};
/** 获取玩家的稳定标识(优先 wechat_id,其次 userid,最后用 avatar 回退) */
const
getPlayerKey
=
(
player
:
Player
,
index
=
0
):
string
=>
{
const
key
=
player
?.
wechat_id
||
player
?.
userid
||
''
;
if
(
key
&&
!
key
.
startsWith
(
'empty-'
))
return
key
;
// 如果 WS 数据没有ID字段,用 avatar URL 作为唯一标识(每个用户头像各不相同)
if
(
player
?.
nickname
&&
player
.
nickname
!==
'-'
&&
player
.
nickname
!==
'虚位以待'
&&
player
?.
avatar
)
{
return
`avt:
${
player
.
avatar
}
`
;
}
return
`empty-
${
index
}
`
;
};
/** 检测 rankList 中哪些玩家 score 增长了 → 触发瞬时加速脉冲 */
const
detectScoreChanges
=
(
players
:
Player
[])
=>
{
if
(
!
players
||
players
.
length
===
0
)
return
;
let
hasNewBoost
=
false
;
const
updated
=
{
...
transientBoosts
.
value
};
players
.
forEach
((
p
,
index
)
=>
{
const
key
=
getPlayerKey
(
p
,
index
);
if
(
key
.
startsWith
(
'empty-'
))
return
;
const
newScore
=
Number
(
p
.
score
??
0
);
if
(
isNaN
(
newScore
))
return
;
const
prev
=
prevScores
[
key
]
??
0
;
if
(
newScore
>
prev
)
{
const
current
=
updated
[
key
]
||
0
;
updated
[
key
]
=
Math
.
min
(
current
+
BOOST_PULSE
,
MAX_BOOST
);
hasNewBoost
=
true
;
console
.
log
(
`[game3 Rank1View] 🚀
${
p
.
nickname
}
score
${
prev
}
→
${
newScore
}
, boost:
${
updated
[
key
].
toFixed
(
1
)}
`
);
}
prevScores
[
key
]
=
newScore
;
});
if
(
hasNewBoost
)
{
transientBoosts
.
value
=
updated
;
}
};
watch
(
watch
(
()
=>
props
.
rankList
,
()
=>
props
.
rankList
,
(
rankList
)
=>
{
(
rankList
)
=>
{
console
.
log
(
'[game3 Rank1View] websocket rankList 数据:'
,
JSON
.
parse
(
JSON
.
stringify
(
rankList
)));
console
.
log
(
'[game3 Rank1View] websocket rankList 数据:'
,
JSON
.
parse
(
JSON
.
stringify
(
rankList
?.
slice
(
0
,
3
)
)));
updateRankList
(
rankList
);
updateRankList
(
rankList
);
// 检测积分增长 → 触发瞬时加速脉冲(对齐 game4)
detectScoreChanges
(
rankList
);
// 游戏进行中,实时更新马匹玩家列表(处理加入/离开)
// 游戏进行中,实时更新马匹玩家列表(处理加入/离开)
if
(
isGameRunning
.
value
)
{
if
(
isGameRunning
.
value
)
{
updateHorses
(
rankList
);
updateHorses
(
rankList
);
...
@@ -110,32 +155,63 @@ watch(
...
@@ -110,32 +155,63 @@ watch(
{
immediate
:
true
,
deep
:
true
},
{
immediate
:
true
,
deep
:
true
},
);
);
// 直接监听 list,确保游戏运行时任何 list 变化都触发马匹刷新
watch
(
list
,
(
newList
)
=>
{
const
validCount
=
newList
.
filter
(
p
=>
getPlayerKey
(
p
).
length
>
0
&&
!
getPlayerKey
(
p
).
startsWith
(
'empty-'
)).
length
;
console
.
log
(
'[game3 Rank1View] list 变化, 有效玩家数:'
,
validCount
,
'isGameRunning:'
,
isGameRunning
.
value
);
if
(
isGameRunning
.
value
&&
validCount
>
0
)
{
updateHorses
(
newList
);
}
},
{
deep
:
true
},
);
watch
(
watch
(
()
=>
props
.
horseBoosts
,
()
=>
props
.
horseBoosts
,
(
boosts
)
=>
{
(
boosts
)
=>
{
if
(
!
boosts
||
Object
.
keys
(
boosts
).
length
===
0
)
return
;
if
(
!
boosts
||
Object
.
keys
(
boosts
).
length
===
0
)
return
;
const
merged
=
{
...
liveScores
.
value
};
for
(
const
[
id
,
score
]
of
Object
.
entries
(
boosts
))
{
let
hasNew
=
false
;
// 取较大的值,因为不同通道可能收到不同时间点的数据
const
updated
=
{
...
transientBoosts
.
value
};
if
(
score
>
(
merged
[
id
]
??
0
))
{
Object
.
entries
(
boosts
).
forEach
(([
id
,
val
])
=>
{
merged
[
id
]
=
score
;
const
score
=
Number
(
val
??
0
);
if
(
score
>
0
)
{
const
current
=
updated
[
id
]
||
0
;
updated
[
id
]
=
Math
.
min
(
current
+
BOOST_PULSE
,
MAX_BOOST
);
hasNew
=
true
;
}
}
});
if
(
hasNew
)
{
transientBoosts
.
value
=
updated
;
console
.
log
(
'[game3 Rank1View] horseBoosts 触发加速:'
,
JSON
.
parse
(
JSON
.
stringify
(
transientBoosts
.
value
)));
}
}
liveScores
.
value
=
merged
;
},
},
{
deep
:
true
},
{
deep
:
true
},
);
);
// ========== 马匹动画循环 (RAF 驱动帧切换 + 分数驱动位移) ==========
// ========== 速度基准(对齐 game4 时间驱动模型) ==========
// 基础速度大幅下调,确保马匹在 60s 游戏时间内不会过早跑出屏幕
const
TOTAL_DISTANCE_PX
=
HORSE_END_X
-
HORSE_START_X
;
// 2200px
const
BASE_DURATION_SEC
=
150
;
// 基准时长 150s,即无加速时跑完全程需 150s
const
BASE_SPEED_PX_PER_SEC
=
TOTAL_DISTANCE_PX
/
BASE_DURATION_SEC
;
// ≈ 14.7px/s
// 瞬时加速脉冲机制(对齐 game4)
const
BOOST_PULSE
=
0.3
;
// 每次 score 增长对应的额外速度倍率
const
MAX_BOOST
=
3.0
;
// 最大加速倍率
const
BOOST_DECAY
=
0.992
;
// 每帧衰减系数(60fps 约 3.6s 到 60%)
// ========== 马匹动画循环 (RAF 驱动:时间驱动位移 + 帧动画) ==========
let
horseRafId
:
number
|
undefined
;
let
horseRafId
:
number
|
undefined
;
let
horseLastTs
=
0
;
let
horseLastTs
=
0
;
const
HORSE_FRAME_TICK_MS
=
48
;
const
HORSE_FRAME_TICK_MS
=
48
;
// 慢速 lerp,分数变化时马匹平滑过渡(约 2.5s 拉到位)
const
HORSE_LERP_SPEED
=
2.5
;
const
startHorseLoop
=
()
=>
{
const
startHorseLoop
=
()
=>
{
stopHorseLoop
();
stopHorseLoop
();
horseLastTs
=
0
;
horseLastTs
=
0
;
let
logCounter
=
0
;
const
tick
=
(
ts
:
number
)
=>
{
const
tick
=
(
ts
:
number
)
=>
{
if
(
!
isGameRunning
.
value
)
{
if
(
!
isGameRunning
.
value
)
{
...
@@ -147,36 +223,29 @@ const startHorseLoop = () => {
...
@@ -147,36 +223,29 @@ const startHorseLoop = () => {
horseLastTs
=
ts
;
horseLastTs
=
ts
;
if
(
horses
.
value
.
length
===
0
)
{
if
(
horses
.
value
.
length
===
0
)
{
if
(
logCounter
++
%
60
===
0
)
{
console
.
log
(
'[game3 Rank1View] RAF: horses 为空, gameStarted:'
,
gameStarted
.
value
,
'isGameRunning:'
,
isGameRunning
.
value
);
}
horseRafId
=
requestAnimationFrame
(
tick
);
horseRafId
=
requestAnimationFrame
(
tick
);
return
;
return
;
}
}
// 优先用 liveScores(onAny 实时数据),回退到 list 分数
const
newBoosts
:
Record
<
string
,
number
>
=
{};
const
getScore
=
(
wechatId
:
string
):
number
=>
{
const
liveScore
=
liveScores
.
value
[
wechatId
];
if
(
typeof
liveScore
===
'number'
&&
liveScore
>
0
)
return
liveScore
;
const
player
=
list
.
value
.
find
(
p
=>
p
.
wechat_id
===
wechatId
);
return
typeof
player
?.
score
===
'number'
?
player
.
score
:
0
;
};
// 计算当前最高分
let
maxScore
=
0
;
for
(
const
horse
of
horses
.
value
)
{
for
(
const
horse
of
horses
.
value
)
{
const
s
=
getScore
(
horse
.
wechat_id
);
if
(
horse
.
xPercent
>=
100
)
continue
;
if
(
s
>
maxScore
)
maxScore
=
s
;
}
if
(
maxScore
<=
0
)
maxScore
=
1
;
for
(
const
horse
of
horses
.
value
)
{
// 从 transientBoosts 读取实时加速值
const
score
=
getScore
(
horse
.
wechat_id
);
const
boost
=
transientBoosts
.
value
[
horse
.
playerKey
]
||
0
;
horse
.
boost
=
boost
;
// 进度 = 最小 5% + 根据分数占比最大 95%
// 基础速度 + 加速 → 推进百分比
const
progress
=
0.05
+
(
score
/
maxScore
)
*
0.95
;
const
effectiveSpeed
=
BASE_SPEED_PX_PER_SEC
*
(
1
+
boost
);
horse
.
targetX
=
HORSE_START_X
+
(
HORSE_END_X
-
HORSE_START_X
)
*
progress
;
const
deltaPx
=
effectiveSpeed
*
(
dt
/
1000
);
horse
.
xPercent
=
Math
.
min
(
100
,
horse
.
xPercent
+
(
deltaPx
/
TOTAL_DISTANCE_PX
)
*
100
);
// 平滑 lerp,分数变化不跳变
// 像素位置 = 起始位置 + 百分比 * 总距离
const
lerpFactor
=
1
-
Math
.
pow
(
0.001
,
dt
/
(
1000
*
HORSE_LERP_SPEED
));
horse
.
x
=
HORSE_START_X
+
(
horse
.
xPercent
/
100
)
*
TOTAL_DISTANCE_PX
;
horse
.
x
+=
(
horse
.
targetX
-
horse
.
x
)
*
lerpFactor
;
// 帧动画
// 帧动画
const
fc
=
horseFrameCount
[
horse
.
horseType
]
??
12
;
const
fc
=
horseFrameCount
[
horse
.
horseType
]
??
12
;
...
@@ -185,8 +254,15 @@ const startHorseLoop = () => {
...
@@ -185,8 +254,15 @@ const startHorseLoop = () => {
horse
.
frameTimer
-=
HORSE_FRAME_TICK_MS
;
horse
.
frameTimer
-=
HORSE_FRAME_TICK_MS
;
horse
.
frameIndex
=
(
horse
.
frameIndex
+
1
)
%
fc
;
horse
.
frameIndex
=
(
horse
.
frameIndex
+
1
)
%
fc
;
}
}
// 加速衰减
const
decayed
=
boost
*
Math
.
pow
(
BOOST_DECAY
,
dt
/
(
1000
/
60
));
if
(
decayed
>
0.001
)
{
newBoosts
[
horse
.
playerKey
]
=
decayed
;
}
}
}
transientBoosts
.
value
=
newBoosts
;
horseRafId
=
requestAnimationFrame
(
tick
);
horseRafId
=
requestAnimationFrame
(
tick
);
};
};
horseRafId
=
requestAnimationFrame
(
tick
);
horseRafId
=
requestAnimationFrame
(
tick
);
...
@@ -204,23 +280,27 @@ const LANE_TOP_POSITIONS = [480, 640, 800];
...
@@ -204,23 +280,27 @@ const LANE_TOP_POSITIONS = [480, 640, 800];
const
updateHorses
=
(
players
:
Player
[])
=>
{
const
updateHorses
=
(
players
:
Player
[])
=>
{
const
validPlayers
=
players
.
filter
(
p
=>
{
const
validPlayers
=
players
.
filter
(
p
=>
{
if
(
!
p
||
!
p
.
wechat_id
)
return
false
;
const
key
=
getPlayerKey
(
p
)
;
if
(
p
.
wechat_id
.
startsWith
(
'empty-'
))
return
false
;
if
(
!
key
||
key
.
startsWith
(
'empty-'
))
return
false
;
if
(
p
.
nickname
===
'-'
||
p
.
nickname
===
'虚位以待'
)
return
false
;
if
(
p
.
nickname
===
'-'
||
p
.
nickname
===
'虚位以待'
)
return
false
;
return
true
;
return
true
;
});
});
console
.
log
(
'[game3 Rank1View] updateHorses: 输入'
,
players
.
length
,
'条, 有效'
,
validPlayers
.
length
,
'条'
,
validPlayers
.
slice
(
0
,
3
).
map
(
p
=>
({
key
:
getPlayerKey
(
p
),
name
:
p
.
nickname
,
score
:
p
.
score
})));
if
(
validPlayers
.
length
===
0
)
{
if
(
validPlayers
.
length
===
0
)
{
console
.
warn
(
'[game3 Rank1View] updateHorses: 无有效玩家,horses 清空'
);
horses
.
value
=
[];
horses
.
value
=
[];
return
;
return
;
}
}
// 保留已有马匹的 x
位置
,防止重新生成时位置跳变
// 保留已有马匹的 x
/xPercent/boost
,防止重新生成时位置跳变
const
existingMap
=
new
Map
(
horses
.
value
.
map
(
h
=>
[
h
.
wechat_id
,
h
]));
const
existingMap
=
new
Map
(
horses
.
value
.
map
(
h
=>
[
h
.
playerKey
,
h
]));
horses
.
value
=
validPlayers
.
map
((
player
,
index
)
=>
{
horses
.
value
=
validPlayers
.
map
((
player
,
index
)
=>
{
const
wid
=
player
.
wechat_id
||
''
;
const
key
=
getPlayerKey
(
player
,
index
)
;
const
existing
=
existingMap
.
get
(
wid
);
const
existing
=
existingMap
.
get
(
key
);
return
{
return
{
id
:
existing
?.
id
||
`horse-
${
++
horseIdCounter
}
`
,
id
:
existing
?.
id
||
`horse-
${
++
horseIdCounter
}
`
,
horseType
:
HORSE_TYPES
[
index
%
HORSE_TYPES
.
length
]
!
,
horseType
:
HORSE_TYPES
[
index
%
HORSE_TYPES
.
length
]
!
,
...
@@ -228,12 +308,15 @@ const updateHorses = (players: Player[]) => {
...
@@ -228,12 +308,15 @@ const updateHorses = (players: Player[]) => {
frameIndex
:
existing
?.
frameIndex
??
Math
.
floor
(
Math
.
random
()
*
(
horseFrameCount
[
HORSE_TYPES
[
index
%
HORSE_TYPES
.
length
]
!
]
??
12
)),
frameIndex
:
existing
?.
frameIndex
??
Math
.
floor
(
Math
.
random
()
*
(
horseFrameCount
[
HORSE_TYPES
[
index
%
HORSE_TYPES
.
length
]
!
]
??
12
)),
frameTimer
:
existing
?.
frameTimer
??
0
,
frameTimer
:
existing
?.
frameTimer
??
0
,
x
:
existing
?.
x
??
HORSE_START_X
,
x
:
existing
?.
x
??
HORSE_START_X
,
targetX
:
existing
?.
targetX
??
HORSE_START_X
,
xPercent
:
existing
?.
xPercent
??
0
,
boost
:
existing
?.
boost
??
0
,
nickname
:
player
.
nickname
||
''
,
nickname
:
player
.
nickname
||
''
,
avatar
:
player
.
avatar
||
''
,
avatar
:
player
.
avatar
||
''
,
wechat_id
:
wid
,
playerKey
:
key
,
};
};
});
});
console
.
log
(
'[game3 Rank1View] updateHorses: 生成'
,
horses
.
value
.
length
,
'匹马'
);
};
};
// ========== 预加载马匹帧 ==========
// ========== 预加载马匹帧 ==========
...
@@ -248,17 +331,23 @@ const preloadHorseFrames = () => {
...
@@ -248,17 +331,23 @@ const preloadHorseFrames = () => {
// ========== 游戏流程 ==========
// ========== 游戏流程 ==========
const
startIntro
=
async
()
=>
{
const
startIntro
=
async
()
=>
{
console
.
log
(
'[game3 Rank1View] startIntro 开始'
);
showResult
.
value
=
false
;
showResult
.
value
=
false
;
gameStarted
.
value
=
false
;
gameStarted
.
value
=
false
;
isGameRunning
.
value
=
false
;
isGameRunning
.
value
=
false
;
horses
.
value
=
[];
horses
.
value
=
[];
horseIdCounter
=
0
;
horseIdCounter
=
0
;
// 重置加速状态
transientBoosts
.
value
=
{};
Object
.
keys
(
prevScores
).
forEach
(
k
=>
delete
prevScores
[
k
]);
stopApplauseMusic
();
stopApplauseMusic
();
preloadHorseFrames
();
preloadHorseFrames
();
const
countdownPromise
=
designStage
.
value
?.
startCountdown
(
3
,
60
);
const
countdownPromise
=
designStage
.
value
?.
startCountdown
(
3
,
60
);
await
new
Promise
((
r
)
=>
setTimeout
(
r
,
3500
));
await
new
Promise
((
r
)
=>
setTimeout
(
r
,
3500
));
const
validCount
=
list
.
value
.
filter
(
p
=>
getPlayerKey
(
p
).
length
>
0
&&
!
getPlayerKey
(
p
).
startsWith
(
'empty-'
)).
length
;
console
.
log
(
'[game3 Rank1View] startIntro: 3.5s 后, list 有效玩家数:'
,
validCount
);
gameStarted
.
value
=
true
;
gameStarted
.
value
=
true
;
isGameRunning
.
value
=
true
;
isGameRunning
.
value
=
true
;
updateHorses
(
list
.
value
);
updateHorses
(
list
.
value
);
...
@@ -300,27 +389,27 @@ onMounted(() => {
...
@@ -300,27 +389,27 @@ onMounted(() => {
if
(
$is_run_local
())
{
if
(
$is_run_local
())
{
// 本地模式:用模拟玩家填充 list,驱动马匹渲染
// 本地模式:用模拟玩家填充 list,驱动马匹渲染
list
.
value
=
[
list
.
value
=
[
{
wechat_
id
:
'local-1'
,
nickname
:
'骑手一'
,
avatar
:
''
,
score
:
45
},
{
user
id
:
'local-1'
,
nickname
:
'骑手一'
,
avatar
:
''
,
score
:
45
},
{
wechat_
id
:
'local-2'
,
nickname
:
'骑手二'
,
avatar
:
''
,
score
:
38
},
{
user
id
:
'local-2'
,
nickname
:
'骑手二'
,
avatar
:
''
,
score
:
38
},
{
wechat_
id
:
'local-3'
,
nickname
:
'骑手三'
,
avatar
:
''
,
score
:
30
},
{
user
id
:
'local-3'
,
nickname
:
'骑手三'
,
avatar
:
''
,
score
:
30
},
{
wechat_
id
:
'local-4'
,
nickname
:
'骑手四'
,
avatar
:
''
,
score
:
22
},
{
user
id
:
'local-4'
,
nickname
:
'骑手四'
,
avatar
:
''
,
score
:
22
},
{
wechat_
id
:
'local-5'
,
nickname
:
'骑手五'
,
avatar
:
''
,
score
:
15
},
{
user
id
:
'local-5'
,
nickname
:
'骑手五'
,
avatar
:
''
,
score
:
15
},
];
];
gameStarted
.
value
=
true
;
gameStarted
.
value
=
true
;
isGameRunning
.
value
=
true
;
isGameRunning
.
value
=
true
;
updateHorses
(
list
.
value
);
updateHorses
(
list
.
value
);
startHorseLoop
();
startHorseLoop
();
// 模拟分数变化
// 模拟分数变化
→ 驱动 detectScoreChanges
let
simTick
=
0
;
let
simTick
=
0
;
const
simInterval
=
setInterval
(()
=>
{
const
simInterval
=
setInterval
(()
=>
{
if
(
!
isGameRunning
.
value
)
{
clearInterval
(
simInterval
);
return
;
}
if
(
!
isGameRunning
.
value
)
{
clearInterval
(
simInterval
);
return
;
}
simTick
++
;
simTick
++
;
// 随机涨分
for
(
const
p
of
list
.
value
)
{
for
(
const
p
of
list
.
value
)
{
if
(
typeof
p
.
score
===
'number'
)
{
if
(
typeof
p
.
score
===
'number'
)
{
p
.
score
+=
Math
.
floor
(
Math
.
random
()
*
3
);
p
.
score
+=
Math
.
floor
(
Math
.
random
()
*
3
);
}
}
}
}
detectScoreChanges
(
list
.
value
);
},
1500
);
},
1500
);
}
}
});
});
...
@@ -351,7 +440,7 @@ onUnmounted(() => {
...
@@ -351,7 +440,7 @@ onUnmounted(() => {
:key=
"horse.id"
:key=
"horse.id"
class=
"horse-wrapper"
class=
"horse-wrapper"
:style=
"
{
:style=
"
{
top: `${
LANE_TOP_POSITIONS[horse.lane]
}px`,
top: `${
(LANE_TOP_POSITIONS[horse.lane] ?? 560) + HORSE_Y_OFFSET
}px`,
transform: `translateX(${horse.x}px)`,
transform: `translateX(${horse.x}px)`,
}"
}"
>
>
...
@@ -556,7 +645,7 @@ onUnmounted(() => {
...
@@ -556,7 +645,7 @@ onUnmounted(() => {
overflow
:
hidden
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
white-space
:
nowrap
;
padding-left
:
4
px
;
padding-left
:
6
px
;
box-sizing
:
border-box
;
box-sizing
:
border-box
;
}
}
...
...
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