adding pause and resume in game state

This commit is contained in:
RandyJC
2025-12-09 14:02:52 +01:00
parent 3ac9d5ac39
commit ab7ddfed4b
6 changed files with 72 additions and 12 deletions

View File

@@ -35,6 +35,7 @@ export interface ServerToClientEvents {
"game:reset": (_message: string) => void "game:reset": (_message: string) => void
"game:updateQuestion": (_data: { current: number; total: number }) => void "game:updateQuestion": (_data: { current: number; total: number }) => void
"game:playerAnswer": (_count: number) => void "game:playerAnswer": (_count: number) => void
"game:break": (_active: boolean) => void
// Player events // Player events
"player:successReconnect": (_data: { "player:successReconnect": (_data: {
@@ -66,6 +67,7 @@ export interface ServerToClientEvents {
"manager:quizzLoaded": (_quizz: QuizzWithId) => void "manager:quizzLoaded": (_quizz: QuizzWithId) => void
"manager:quizzSaved": (_quizz: QuizzWithId) => void "manager:quizzSaved": (_quizz: QuizzWithId) => void
"manager:quizzDeleted": (_id: string) => void "manager:quizzDeleted": (_id: string) => void
"manager:break": (_active: boolean) => void
} }
export interface ClientToServerEvents { export interface ClientToServerEvents {
@@ -78,6 +80,7 @@ export interface ClientToServerEvents {
"manager:abortQuiz": (_message: MessageGameId) => void "manager:abortQuiz": (_message: MessageGameId) => void
"manager:pauseCooldown": (_message: MessageGameId) => void "manager:pauseCooldown": (_message: MessageGameId) => void
"manager:resumeCooldown": (_message: MessageGameId) => void "manager:resumeCooldown": (_message: MessageGameId) => void
"manager:setBreak": (_message: { gameId?: string; active: boolean }) => void
"manager:endGame": (_message: MessageGameId) => void "manager:endGame": (_message: MessageGameId) => void
"manager:skipQuestionIntro": (_message: MessageGameId) => void "manager:skipQuestionIntro": (_message: MessageGameId) => void
"manager:nextQuestion": (_message: MessageGameId) => void "manager:nextQuestion": (_message: MessageGameId) => void

View File

@@ -234,6 +234,10 @@ io.on("connection", (socket) => {
withGame(gameId, socket, (game) => game.resumeCooldown(socket)) withGame(gameId, socket, (game) => game.resumeCooldown(socket))
) )
socket.on("manager:setBreak", ({ gameId, active }) =>
withGame(gameId, socket, (game) => game.setBreak(socket, active))
)
socket.on("manager:endGame", ({ gameId }) => socket.on("manager:endGame", ({ gameId }) =>
withGame(gameId, socket, (game) => game.endGame(socket, registry)) withGame(gameId, socket, (game) => game.endGame(socket, registry))
) )

View File

@@ -46,6 +46,7 @@ class Game {
timer: NodeJS.Timeout | null timer: NodeJS.Timeout | null
resolve: (() => void) | null resolve: (() => void) | null
} }
breakActive: boolean
constructor(io: Server, socket: Socket, quizz: Quizz) { constructor(io: Server, socket: Socket, quizz: Quizz) {
if (!io) { if (!io) {
@@ -84,6 +85,7 @@ class Game {
timer: null, timer: null,
resolve: null, resolve: null,
} }
this.breakActive = false
const roomInvite = createInviteCode() const roomInvite = createInviteCode()
this.inviteCode = roomInvite this.inviteCode = roomInvite
@@ -137,6 +139,7 @@ class Game {
timer: null, timer: null,
resolve: null, resolve: null,
} }
game.breakActive = snapshot.breakActive || false
if (game.cooldown.active && game.cooldown.remaining > 0 && !game.cooldown.paused) { if (game.cooldown.active && game.cooldown.remaining > 0 && !game.cooldown.paused) {
game.startCooldown(game.cooldown.remaining) game.startCooldown(game.cooldown.remaining)
@@ -193,6 +196,7 @@ class Game {
paused: this.cooldown.paused, paused: this.cooldown.paused,
remaining: this.cooldown.remaining, remaining: this.cooldown.remaining,
}, },
breakActive: this.breakActive,
} }
} }
@@ -453,6 +457,27 @@ class Game {
this.persist() this.persist()
} }
setBreak(socket: Socket, active: boolean) {
if (this.manager.id !== socket.id) {
return
}
this.breakActive = active
if (this.cooldown.active) {
if (active) {
this.cooldown.paused = true
} else {
this.cooldown.paused = false
}
this.io.to(this.gameId).emit("game:cooldownPause", this.cooldown.paused)
}
this.io.to(this.gameId).emit("game:break", active)
this.io.to(this.manager.id).emit("manager:break", active)
this.persist()
}
skipQuestionIntro(socket: Socket) { skipQuestionIntro(socket: Socket) {
if (this.manager.id !== socket.id) { if (this.manager.id !== socket.id) {
return return

View File

@@ -41,6 +41,9 @@ const Manager = () => {
const handleCreate = (quizzId: string) => { const handleCreate = (quizzId: string) => {
socket?.emit("game:create", quizzId) socket?.emit("game:create", quizzId)
} }
const handleBreakToggle = (active: boolean) => {
socket?.emit("manager:setBreak", { gameId: null, active })
}
if (!isAuth) { if (!isAuth) {
return <ManagerPassword onSubmit={handleAuth} /> return <ManagerPassword onSubmit={handleAuth} />
@@ -52,6 +55,7 @@ const Manager = () => {
quizzList={quizzList} quizzList={quizzList}
onBack={() => setShowEditor(false)} onBack={() => setShowEditor(false)}
onListUpdate={setQuizzList} onListUpdate={setQuizzList}
onBreakToggle={handleBreakToggle}
/> />
) )
} }

View File

@@ -39,6 +39,7 @@ const GameWrapper = ({
const { questionStates, setQuestionStates } = useQuestionStore() const { questionStates, setQuestionStates } = useQuestionStore()
const { backgroundUrl, setBackground, setBrandName } = useThemeStore() const { backgroundUrl, setBackground, setBrandName } = useThemeStore()
const [isDisabled, setIsDisabled] = useState(false) const [isDisabled, setIsDisabled] = useState(false)
const [onBreak, setOnBreak] = useState(false)
const next = statusName ? MANAGER_SKIP_BTN[statusName] : null const next = statusName ? MANAGER_SKIP_BTN[statusName] : null
useEvent("game:updateQuestion", ({ current, total }) => { useEvent("game:updateQuestion", ({ current, total }) => {
@@ -52,6 +53,9 @@ const GameWrapper = ({
setIsDisabled(false) setIsDisabled(false)
}, [statusName]) }, [statusName])
useEvent("game:break", (active) => setOnBreak(active))
useEvent("manager:break", (active) => setOnBreak(active))
useEffect(() => { useEffect(() => {
const loadTheme = async () => { const loadTheme = async () => {
try { try {
@@ -171,6 +175,15 @@ const GameWrapper = ({
</div> </div>
</div> </div>
)} )}
{onBreak && (
<div className="pointer-events-none fixed inset-0 z-40 flex items-center justify-center bg-black/60">
<div className="rounded-md bg-white/90 px-6 py-4 text-center shadow-lg">
<p className="text-lg font-semibold text-gray-800">Game paused for a break</p>
<p className="text-sm text-gray-600">We&apos;ll resume from the same spot.</p>
</div>
</div>
)}
</> </>
)} )}
</section> </section>

View File

@@ -12,6 +12,7 @@ type Props = {
quizzList: QuizzWithId[] quizzList: QuizzWithId[]
onBack: () => void onBack: () => void
onListUpdate: (_quizz: QuizzWithId[]) => void onListUpdate: (_quizz: QuizzWithId[]) => void
onBreakToggle?: (_active: boolean) => void
} }
type EditableQuestion = QuizzWithId["questions"][number] type EditableQuestion = QuizzWithId["questions"][number]
@@ -55,7 +56,7 @@ const formatBytes = (bytes: number) => {
return `${value.toFixed(value >= 10 || value % 1 === 0 ? 0 : 1)} ${units[i]}` return `${value.toFixed(value >= 10 || value % 1 === 0 ? 0 : 1)} ${units[i]}`
} }
const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => { const QuizEditor = ({ quizzList, onBack, onListUpdate, onBreakToggle }: Props) => {
const { socket } = useSocket() const { socket } = useSocket()
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const [draft, setDraft] = useState<QuizzWithId | null>(null) const [draft, setDraft] = useState<QuizzWithId | null>(null)
@@ -462,19 +463,29 @@ const QuizEditor = ({ quizzList, onBack, onListUpdate }: Props) => {
return ( return (
<div className="mx-auto flex w-full max-w-[1280px] flex-col gap-7 rounded-md bg-white p-6 shadow-sm md:p-8"> <div className="mx-auto flex w-full max-w-[1280px] flex-col gap-7 rounded-md bg-white p-6 shadow-sm md:p-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button onClick={onBack} className="bg-gray-700"> <Button onClick={onBack} className="bg-gray-700">
Back Back
</Button>
<Button onClick={handleNew} className="bg-blue-600">
New quiz
</Button>
{selectedId && (
<Button className="bg-red-600" onClick={handleDeleteQuizz} disabled={saving}>
Delete quiz
</Button> </Button>
<Button onClick={handleNew} className="bg-blue-600"> )}
New quiz {onBreakToggle && (
</Button> <>
{selectedId && ( <Button className="bg-amber-500" onClick={() => onBreakToggle(true)}>
<Button className="bg-red-600" onClick={handleDeleteQuizz} disabled={saving}> Break
Delete quiz
</Button> </Button>
)} <Button className="bg-green-600" onClick={() => onBreakToggle(false)}>
</div> Resume
</Button>
</>
)}
</div>
<Button onClick={handleSave} disabled={saving || loading}> <Button onClick={handleSave} disabled={saving || loading}>
{saving ? "Saving..." : "Save quiz"} {saving ? "Saving..." : "Save quiz"}