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
f8b7ee46
Commit
f8b7ee46
authored
Jun 12, 2026
by
董政锦
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: 修复大屏马的生成数据和动画;
parent
2a8c14a9
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
245 additions
and
100 deletions
+245
-100
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-large/src/pages/game3/Game3.vue
View file @
f8b7ee46
...
...
@@ -55,36 +55,48 @@ const renderPlayers = (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
(
{
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
data
[
index
]
??
emptyRankPlayer
(
index
),
)
}
if
(
!
arr
||
arr
.
length
===
0
)
{
console
.
warn
(
'[game3] updateRankPlayers: 无法解析排行数据'
,
data
);
return
;
}
const
playerToRankPlayer
=
(
player
:
any
,
index
:
number
):
Player
=>
({
telephone
:
player
?.
telephone
??
''
,
userid
:
player
?.
userid
??
''
,
wechat_id
:
player
?.
wechat_id
||
`empty-
${
index
}
`
,
nickname
:
player
?.
nickname
||
'虚位以待'
,
avatar
:
player
?.
avatar
||
''
,
score
:
player
?.
score
??
'-'
,
})
// 打印第一条原始数据的所有字段名,用于排查字段不匹配问题
console
.
log
(
'[game3] updateRankPlayers: 收到排行数据'
,
arr
.
length
,
'条, 第1条字段:'
,
Object
.
keys
(
arr
[
0
]
||
{}),
', 第1条值:'
,
JSON
.
parse
(
JSON
.
stringify
(
arr
[
0
])));
// 将 WS 数据标准化为 Player 格式(同时保留 userid / wechat_id)
// ⚠️ 当 WS 返回的数据没有 userid/openid/wechat_id 等ID字段时,用 avatar 作为唯一标识
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
||
''
;
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
(
{
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
{
const
player
=
loadingPlayers
.
value
[
index
]
if
(
!
player
?.
userid
&&
!
player
?.
wechat_id
)
{
return
emptyRankPlayer
(
index
)
}
return
playerToRankPlayer
(
player
,
index
)
},
)
(
_
,
index
)
=>
normalized
[
index
]
??
emptyRankPlayer
(
index
),
);
}
const
resetRoundState
=
()
=>
{
...
...
@@ -107,7 +119,7 @@ const gotoRank1WithIntro = async () => {
if
(
isPlayingStartAnimation
)
return
isPlayingStartAnimation
=
true
seedRankPlayersFromLoading
()
// ⚠️ 不要用 seedRankPlayersFromLoading,由 WS room_rank_result 驱动马匹数据
screen
.
value
=
'rank1'
await
nextTick
()
await
rank1Ref
.
value
?.
startIntro
()
...
...
@@ -129,8 +141,16 @@ function handleRoomState(data: any) {
gotoLoading
()
break
case
2
:
seedRankPlayersFromLoading
()
// 重连/中途进房时游戏已在运行,直接进入赛马画面
if
(
isPlayingStartAnimation
)
break
;
isPlayingStartAnimation
=
true
;
screen
.
value
=
'rank1'
console
.
log
(
'[game3] handleRoomState: status=2, 直接启动赛马动画'
);
nextTick
(()
=>
{
rank1Ref
.
value
?.
startIntro
().
finally
(()
=>
{
isPlayingStartAnimation
=
false
;
});
});
break
case
3
:
screen
.
value
=
'rank2'
...
...
@@ -143,24 +163,58 @@ const { startGame, createRoom, backRoom, closeRoom } = useAdminGameSocket({
onAny
:
(
evt
,
payload
)
=>
{
// 兜底处理所有非标准事件,解析玩家实时分数(对齐 game4)
const
data
=
payload
?.
data
??
payload
;
if
(
!
Array
.
isArray
(
data
))
return
;
const
boosts
:
Record
<
string
,
number
>
=
{};
let
hasBoost
=
false
;
data
.
forEach
((
item
:
any
)
=>
{
const
id
=
item
.
userid
||
item
.
wechat_id
||
item
.
avatar
||
""
;
if
(
!
id
)
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
);
hasBoost
=
true
;
});
if
(
hasBoost
)
{
horseBoosts
.
value
=
{
...
boosts
};
// 尝试从 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
?.
data
))
{
arr
=
data
.
data
;
}
if
(
arr
&&
arr
.
length
>
0
)
{
const
boosts
:
Record
<
string
,
number
>
=
{};
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
||
''
;
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
,
};
});
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
,
...
...
@@ -232,5 +286,7 @@ watch(screen, (value) => {
<
template
>
<Loading
v-if=
"screen === 'loading'"
:players=
"loadingPlayers"
:player-count=
"playerCount"
@
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
>
frontend-large/src/pages/game3/views/Rank1View.vue
View file @
f8b7ee46
...
...
@@ -44,6 +44,7 @@ const imageUrls = {
const
HORSE_SIZE
=
280
;
const
HORSE_START_X
=
-
HORSE_SIZE
;
const
HORSE_END_X
=
1920
;
const
HORSE_Y_OFFSET
=
80
;
// 马匹整体向下偏移,补偿帧图片中马匹偏上的空白区域
const
horseFrameConfigs
=
[
{
type
:
1
,
dir
:
'game3/ma1'
,
prefix
:
'1ma_'
,
startIndex
:
24
,
count
:
12
},
...
...
@@ -68,11 +69,13 @@ interface Horse {
lane
:
number
;
frameIndex
:
number
;
frameTimer
:
number
;
x
:
number
;
targetX
:
number
;
x
:
number
;
// 像素位置(用于渲染 translateX)
xPercent
:
number
;
// 进度百分比 0~100(对齐 game4 时间驱动模型)
boost
:
number
;
// 瞬时加速值,每帧衰减
nickname
:
string
;
avatar
:
string
;
wechat_id
:
string
;
// 用于匹配 WebSocket 实时排名数据
/** 稳定标识,优先 wechat_id 再 userid,用于匹配 WebSocket 数据 */
playerKey
:
string
;
}
const
horses
=
ref
<
Horse
[]
>
([]);
...
...
@@ -87,21 +90,63 @@ const emptyPlayer = (index: number): Player => ({
});
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
next
=
Array
.
from
({
length
:
RANK_LIMIT
},
(
_
,
index
)
=>
rankList
[
index
]
??
emptyPlayer
(
index
));
list
.
value
=
next
;
};
// 玩家实时分数(合并 rankList + horseBoosts 双通道,对齐 game4 模式)
const
liveScores
=
ref
<
Record
<
string
,
number
>>
({});
// ========== 瞬时加速脉冲(对齐 game4 detectScoreChanges) ==========
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
(
()
=>
props
.
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
);
// 检测积分增长 → 触发瞬时加速脉冲(对齐 game4)
detectScoreChanges
(
rankList
);
// 游戏进行中,实时更新马匹玩家列表(处理加入/离开)
if
(
isGameRunning
.
value
)
{
updateHorses
(
rankList
);
...
...
@@ -110,32 +155,63 @@ watch(
{
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
(
()
=>
props
.
horseBoosts
,
(
boosts
)
=>
{
if
(
!
boosts
||
Object
.
keys
(
boosts
).
length
===
0
)
return
;
const
merged
=
{
...
liveScores
.
value
};
for
(
const
[
id
,
score
]
of
Object
.
entries
(
boosts
))
{
// 取较大的值,因为不同通道可能收到不同时间点的数据
if
(
score
>
(
merged
[
id
]
??
0
))
{
merged
[
id
]
=
score
;
let
hasNew
=
false
;
const
updated
=
{
...
transientBoosts
.
value
};
Object
.
entries
(
boosts
).
forEach
(([
id
,
val
])
=>
{
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
},
);
// ========== 马匹动画循环 (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
horseLastTs
=
0
;
const
HORSE_FRAME_TICK_MS
=
48
;
// 慢速 lerp,分数变化时马匹平滑过渡(约 2.5s 拉到位)
const
HORSE_LERP_SPEED
=
2.5
;
const
startHorseLoop
=
()
=>
{
stopHorseLoop
();
horseLastTs
=
0
;
let
logCounter
=
0
;
const
tick
=
(
ts
:
number
)
=>
{
if
(
!
isGameRunning
.
value
)
{
...
...
@@ -147,36 +223,29 @@ const startHorseLoop = () => {
horseLastTs
=
ts
;
if
(
horses
.
value
.
length
===
0
)
{
if
(
logCounter
++
%
60
===
0
)
{
console
.
log
(
'[game3 Rank1View] RAF: horses 为空, gameStarted:'
,
gameStarted
.
value
,
'isGameRunning:'
,
isGameRunning
.
value
);
}
horseRafId
=
requestAnimationFrame
(
tick
);
return
;
}
// 优先用 liveScores(onAny 实时数据),回退到 list 分数
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
;
};
const
newBoosts
:
Record
<
string
,
number
>
=
{};
// 计算当前最高分
let
maxScore
=
0
;
for
(
const
horse
of
horses
.
value
)
{
const
s
=
getScore
(
horse
.
wechat_id
);
if
(
s
>
maxScore
)
maxScore
=
s
;
}
if
(
maxScore
<=
0
)
maxScore
=
1
;
if
(
horse
.
xPercent
>=
100
)
continue
;
for
(
const
horse
of
horses
.
value
)
{
const
score
=
getScore
(
horse
.
wechat_id
);
// 从 transientBoosts 读取实时加速值
const
boost
=
transientBoosts
.
value
[
horse
.
playerKey
]
||
0
;
horse
.
boost
=
boost
;
// 进度 = 最小 5% + 根据分数占比最大 95%
const
progress
=
0.05
+
(
score
/
maxScore
)
*
0.95
;
horse
.
targetX
=
HORSE_START_X
+
(
HORSE_END_X
-
HORSE_START_X
)
*
progress
;
// 基础速度 + 加速 → 推进百分比
const
effectiveSpeed
=
BASE_SPEED_PX_PER_SEC
*
(
1
+
boost
);
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
.
targetX
-
horse
.
x
)
*
lerpFactor
;
// 像素位置 = 起始位置 + 百分比 * 总距离
horse
.
x
=
HORSE_START_X
+
(
horse
.
xPercent
/
100
)
*
TOTAL_DISTANCE_PX
;
// 帧动画
const
fc
=
horseFrameCount
[
horse
.
horseType
]
??
12
;
...
...
@@ -185,8 +254,15 @@ const startHorseLoop = () => {
horse
.
frameTimer
-=
HORSE_FRAME_TICK_MS
;
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
);
...
...
@@ -204,23 +280,27 @@ const LANE_TOP_POSITIONS = [480, 640, 800];
const
updateHorses
=
(
players
:
Player
[])
=>
{
const
validPlayers
=
players
.
filter
(
p
=>
{
if
(
!
p
||
!
p
.
wechat_id
)
return
false
;
if
(
p
.
wechat_id
.
startsWith
(
'empty-'
))
return
false
;
const
key
=
getPlayerKey
(
p
)
;
if
(
!
key
||
key
.
startsWith
(
'empty-'
))
return
false
;
if
(
p
.
nickname
===
'-'
||
p
.
nickname
===
'虚位以待'
)
return
false
;
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
)
{
console
.
warn
(
'[game3 Rank1View] updateHorses: 无有效玩家,horses 清空'
);
horses
.
value
=
[];
return
;
}
// 保留已有马匹的 x
位置
,防止重新生成时位置跳变
const
existingMap
=
new
Map
(
horses
.
value
.
map
(
h
=>
[
h
.
wechat_id
,
h
]));
// 保留已有马匹的 x
/xPercent/boost
,防止重新生成时位置跳变
const
existingMap
=
new
Map
(
horses
.
value
.
map
(
h
=>
[
h
.
playerKey
,
h
]));
horses
.
value
=
validPlayers
.
map
((
player
,
index
)
=>
{
const
wid
=
player
.
wechat_id
||
''
;
const
existing
=
existingMap
.
get
(
wid
);
const
key
=
getPlayerKey
(
player
,
index
)
;
const
existing
=
existingMap
.
get
(
key
);
return
{
id
:
existing
?.
id
||
`horse-
${
++
horseIdCounter
}
`
,
horseType
:
HORSE_TYPES
[
index
%
HORSE_TYPES
.
length
]
!
,
...
...
@@ -228,12 +308,15 @@ const updateHorses = (players: Player[]) => {
frameIndex
:
existing
?.
frameIndex
??
Math
.
floor
(
Math
.
random
()
*
(
horseFrameCount
[
HORSE_TYPES
[
index
%
HORSE_TYPES
.
length
]
!
]
??
12
)),
frameTimer
:
existing
?.
frameTimer
??
0
,
x
:
existing
?.
x
??
HORSE_START_X
,
targetX
:
existing
?.
targetX
??
HORSE_START_X
,
xPercent
:
existing
?.
xPercent
??
0
,
boost
:
existing
?.
boost
??
0
,
nickname
:
player
.
nickname
||
''
,
avatar
:
player
.
avatar
||
''
,
wechat_id
:
wid
,
playerKey
:
key
,
};
});
console
.
log
(
'[game3 Rank1View] updateHorses: 生成'
,
horses
.
value
.
length
,
'匹马'
);
};
// ========== 预加载马匹帧 ==========
...
...
@@ -248,17 +331,23 @@ const preloadHorseFrames = () => {
// ========== 游戏流程 ==========
const
startIntro
=
async
()
=>
{
console
.
log
(
'[game3 Rank1View] startIntro 开始'
);
showResult
.
value
=
false
;
gameStarted
.
value
=
false
;
isGameRunning
.
value
=
false
;
horses
.
value
=
[];
horseIdCounter
=
0
;
// 重置加速状态
transientBoosts
.
value
=
{};
Object
.
keys
(
prevScores
).
forEach
(
k
=>
delete
prevScores
[
k
]);
stopApplauseMusic
();
preloadHorseFrames
();
const
countdownPromise
=
designStage
.
value
?.
startCountdown
(
3
,
60
);
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
;
isGameRunning
.
value
=
true
;
updateHorses
(
list
.
value
);
...
...
@@ -300,27 +389,27 @@ onMounted(() => {
if
(
$is_run_local
())
{
// 本地模式:用模拟玩家填充 list,驱动马匹渲染
list
.
value
=
[
{
wechat_
id
:
'local-1'
,
nickname
:
'骑手一'
,
avatar
:
''
,
score
:
45
},
{
wechat_
id
:
'local-2'
,
nickname
:
'骑手二'
,
avatar
:
''
,
score
:
38
},
{
wechat_
id
:
'local-3'
,
nickname
:
'骑手三'
,
avatar
:
''
,
score
:
30
},
{
wechat_
id
:
'local-4'
,
nickname
:
'骑手四'
,
avatar
:
''
,
score
:
22
},
{
wechat_
id
:
'local-5'
,
nickname
:
'骑手五'
,
avatar
:
''
,
score
:
15
},
{
user
id
:
'local-1'
,
nickname
:
'骑手一'
,
avatar
:
''
,
score
:
45
},
{
user
id
:
'local-2'
,
nickname
:
'骑手二'
,
avatar
:
''
,
score
:
38
},
{
user
id
:
'local-3'
,
nickname
:
'骑手三'
,
avatar
:
''
,
score
:
30
},
{
user
id
:
'local-4'
,
nickname
:
'骑手四'
,
avatar
:
''
,
score
:
22
},
{
user
id
:
'local-5'
,
nickname
:
'骑手五'
,
avatar
:
''
,
score
:
15
},
];
gameStarted
.
value
=
true
;
isGameRunning
.
value
=
true
;
updateHorses
(
list
.
value
);
startHorseLoop
();
// 模拟分数变化
// 模拟分数变化
→ 驱动 detectScoreChanges
let
simTick
=
0
;
const
simInterval
=
setInterval
(()
=>
{
if
(
!
isGameRunning
.
value
)
{
clearInterval
(
simInterval
);
return
;
}
simTick
++
;
// 随机涨分
for
(
const
p
of
list
.
value
)
{
if
(
typeof
p
.
score
===
'number'
)
{
p
.
score
+=
Math
.
floor
(
Math
.
random
()
*
3
);
}
}
detectScoreChanges
(
list
.
value
);
},
1500
);
}
});
...
...
@@ -351,7 +440,7 @@ onUnmounted(() => {
:key=
"horse.id"
class=
"horse-wrapper"
:style=
"
{
top: `${
LANE_TOP_POSITIONS[horse.lane]
}px`,
top: `${
(LANE_TOP_POSITIONS[horse.lane] ?? 560) + HORSE_Y_OFFSET
}px`,
transform: `translateX(${horse.x}px)`,
}"
>
...
...
@@ -556,7 +645,7 @@ onUnmounted(() => {
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
padding-left
:
4
px
;
padding-left
:
6
px
;
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