Skip to content

Commit

Permalink
Merge pull request #16 from libondev/dev
Browse files Browse the repository at this point in the history
feat: 支持游戏不可见时暂停预览模式&调整 UI
  • Loading branch information
libondev authored Apr 6, 2024
2 parents fe97cf3 + 76f3aa7 commit 88d1c08
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 44 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,8 @@
一些想做但不确定会不会做的功能
- [ ] 排行榜
- [ ] 联机模式

## 补充
- 图标来自于:[iconify](https://iconify.design/),同时使用了 [icones](https://icones.js.org/) 网站来预览图
- 有趣的游戏音效来自:[Duolingo](https://www.duolingo.com/),因为我也是个Duolingo软件的忠实用户,很喜欢TA的音效设计。PS:请原谅我未经允许的使用😜
- 项目技术栈为:Vue3 + Vite5 + TypeScript + TailwindCSS + VueRouter + VueUse + Pinia
13 changes: 10 additions & 3 deletions src/composables/use-countdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,21 @@ interface Options {
/**
* 当倒计时结束时
*/
onEnd?: () => void
onFinished?: () => void

/**
* 当读秒改变时
* @param second {number}
*/
// onChange?: (second: number) => void
}

export function useCountdown({
times = 60,
interval = 1,
immediate = false,
onEnd = () => { },
onFinished = () => { },
// onChange = (second: number) => { },
} = {} as Options) {
let timeoutId: number

Expand All @@ -44,7 +51,7 @@ export function useCountdown({
timeoutId = window.setTimeout(() => {
remainder.value -= interval

remainder.value ? start(false) : onEnd()
remainder.value ? start(false) : onFinished()
}, interval * 1000)
}

Expand Down
18 changes: 1 addition & 17 deletions src/composables/use-game-score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function useGameScore(
timestamp.value = lastTime
lastTimestamp = performance.now()

stopRecording()
timestampId = window.setInterval(() => {
timestamp.value += 1
}, 1000)
Expand All @@ -52,23 +53,6 @@ export function useGameScore(
showDeltaScore.value = false
}

// 当页面不可见时停止计时
function onVisibilityChange() {
if (document.visibilityState === 'hidden') {
stopRecording()
} else {
startRecording(timestamp.value)
}
}

document.addEventListener('visibilitychange', onVisibilityChange)

onBeforeUnmount(() => {
stopRecording()

document.removeEventListener('visibilitychange', onVisibilityChange)
})

return {
timestamp,
gameScore,
Expand Down
66 changes: 42 additions & 24 deletions src/views/game/[level].vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,24 @@ const {
const {
value: countdown,
start: startCountdown,
reset: resetCountdown,
} = useCountdown({ times: levelConfig.internal })
start: startPreviewCountdown,
reset: resetPrewiewCountdown,
pause: pausePreviewCountdown,
} = useCountdown({
times: levelConfig.internal,
// 当倒计时结束时开始游戏并
onFinished: onFinishedPreviewCountdown,
})
let startTimeoutId = -1
// 当预览倒计时结束时开始游戏计分, 取消所有预览块, 并设置游戏状态
function onFinishedPreviewCountdown() {
// 开始计分计时
startRecording()
uncheckAllBlocks()
setGameStatus('playing')
}
async function startGame() {
resetCountdown()
startCountdown()
updateHighestScoreStatus()
generateRandomTargetBlock()
Expand All @@ -89,14 +97,9 @@ async function startGame() {
// 设置游戏状态为预览模式
setGameStatus('previewing')
clearTimeout(startTimeoutId)
// 延迟关闭预览模式
startTimeoutId = window.setTimeout(() => {
// 开始计分计时
startRecording()
uncheckAllBlocks()
setGameStatus('playing')
}, levelConfig.internal * 1000)
// 重置读秒并重新开始倒计时
resetPrewiewCountdown()
startPreviewCountdown()
}
function onCheckResult() {
Expand Down Expand Up @@ -186,12 +189,28 @@ function formatScore(score: number) {
return numStr.replace(reg, ',')
}
// 当浏览器游戏标签不可见时暂停计时
function onGameTabVisibilityChange() {
if (document.visibilityState === 'hidden') {
stopRecording()
pausePreviewCountdown()
return
}
// 可见时再重新开始读秒和预览模式的计时
startRecording(timestamp.value)
startPreviewCountdown()
}
onMounted(() => {
startGame()
document.addEventListener('visibilitychange', onGameTabVisibilityChange)
})
onBeforeUnmount(() => {
clearTimeout(startTimeoutId)
stopRecording()
document.removeEventListener('visibilitychange', onGameTabVisibilityChange)
})
</script>

Expand All @@ -204,7 +223,7 @@ onBeforeUnmount(() => {
</template>
</h2>

<div class="mt-6 font-mono flex items-center justify-between leading-none gap-4 min-w-60">
<div class="my-6 font-mono flex flex-wrap items-center justify-center leading-none gap-4 max-w-96">
<div class="flex items-center h-8 px-2 rounded-full border border-input bg-slate-100 dark:bg-slate-800 min-w-[75px]">
<i class="i-solar-stop-bold text-lg mr-1 text-emerald-500" />
<span class="flex-1 text-center">{{ checkedNumber }}/{{ targetBlocks.size }}</span>
Expand All @@ -215,7 +234,7 @@ onBeforeUnmount(() => {
<span class="flex-1 text-center">{{ timestamp }}s</span>
</div>

<div class="flex items-center h-8 px-2 rounded-full border border-input bg-slate-100 dark:bg-slate-800">
<div class="flex items-center h-8 px-2 pt-0.5 rounded-full border border-input bg-slate-100 dark:bg-slate-800">
<i class="i-solar-health-bold text-lg mr-1 text-red-500" />
<div v-if="gameHealth <= 5" class="w-4 h-4 mx-auto overflow-hidden">
<div
Expand All @@ -233,17 +252,16 @@ onBeforeUnmount(() => {
</div>
<span v-else class="flex-1 text-center">{{ gameHealth >= 99 ? '99+' : gameHealth }}</span>
</div>
</div>

<div class="mt-2 font-mono flex items-center justify-between leading-none gap-4 w-60">
<!-- 练习模式不展示最高分字段 -->
<div v-if="level !== 'custom'" class="flex items-center h-8 px-2 rounded-full border border-input bg-slate-100 dark:bg-slate-800 min-w-[75px]">
<i class="i-solar-ranking-bold-duotone text-lg translate-y-[-1.5px] mr-1 opacity-70" />
<span class="flex-1 text-center">{{ highestScore }}</span>
<i class="i-solar-ranking-bold-duotone text-lg translate-y-[-1.5px] mr-1 text-yellow-400" />
<span class="flex-1 text-center max-w-80 truncate">{{ highestScore }}</span>
</div>
</div>

<div class="relative mb-4 text-4xl text-center">
<span class="z-10 font-medium">{{ formatScore(gameScore) }}</span>
<div class="relative mb-6 text-5xl text-center">
<span class="z-10 font-mono font-medium">{{ formatScore(gameScore) }}</span>

<span v-if="showHighestScoreBadge" class="absolute -translate-x-2 text-xs rotate-45 inline-block font-bold px-2 rounded-full border-2 border-red-500 text-red-500">BEST</span>

Expand Down

0 comments on commit 88d1c08

Please sign in to comment.