Commit 9ac30b53 authored by 董政锦's avatar 董政锦

Merge branch 'fix-修复音乐通用js文件' into 'main'

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