bonus and timing logic a little bit better
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getRoom, buzz, startGame, loadQuestion, startTossupTimer, startBonusTimer, markTossupCorrect, markTossupWrong, markBonusCorrect, markBonusWrong, endGame } from '@/lib/rooms';
|
||||
import { getRandomQuestion } from '@/lib/scibowl-api';
|
||||
import { z, ZodError } from 'zod';
|
||||
import { getRoom, buzz, startGame, loadQuestion, startTossupTimer, startBonusTimer, markTossupCorrect, markTossupWrong, markBonusCorrect, markBonusWrong, moveOn, endGame } from '@/lib/rooms';
|
||||
|
||||
const gameActionSchema = z.object({
|
||||
action: z.enum(['start', 'load_question', 'start_timer', 'buzz', 'tossup_correct', 'tossup_wrong', 'bonus_start_timer', 'bonus_correct', 'bonus_wrong', 'end']),
|
||||
action: z.enum(['start', 'load_question', 'start_timer', 'buzz', 'tossup_correct', 'tossup_wrong', 'bonus_start_timer', 'bonus_correct', 'bonus_wrong', 'move_on', 'end']),
|
||||
moderatorId: z.string().optional(),
|
||||
playerId: z.string().optional(),
|
||||
});
|
||||
@@ -66,6 +67,11 @@ export async function POST(
|
||||
success = markBonusWrong(code, moderatorId);
|
||||
break;
|
||||
|
||||
case 'move_on':
|
||||
if (!moderatorId) return NextResponse.json({ error: 'Missing moderatorId' }, { status: 400 });
|
||||
success = moveOn(code, moderatorId);
|
||||
break;
|
||||
|
||||
case 'end':
|
||||
if (!moderatorId) return NextResponse.json({ error: 'Missing moderatorId' }, { status: 400 });
|
||||
success = endGame(code, moderatorId);
|
||||
|
||||
@@ -80,16 +80,61 @@ export default function HostPage() {
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
if (!roomCode) return;
|
||||
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (e.code === "Space") {
|
||||
e.preventDefault();
|
||||
const phase = gameState?.phase;
|
||||
|
||||
if (phase === "lobby" && players.length > 0) {
|
||||
gameAction("start");
|
||||
} else if (phase === "intermission") {
|
||||
gameAction("load_question");
|
||||
} else if (phase === "tossup_reading") {
|
||||
gameAction("start_timer");
|
||||
} else if (phase === "bonus" && !gameState?.bonusStartTime) {
|
||||
gameAction("bonus_start_timer");
|
||||
}
|
||||
}
|
||||
|
||||
// C for correct
|
||||
if (e.code === "KeyC") {
|
||||
const phase = gameState?.phase;
|
||||
if (phase === "tossup_buzzing" && gameState?.buzzedPlayer) {
|
||||
gameAction("tossup_correct");
|
||||
} else if (phase === "bonus" && gameState?.bonusStartTime) {
|
||||
gameAction("bonus_correct");
|
||||
}
|
||||
}
|
||||
|
||||
// W for wrong
|
||||
if (e.code === "KeyW") {
|
||||
const phase = gameState?.phase;
|
||||
if (phase === "tossup_buzzing" && gameState?.buzzedPlayer) {
|
||||
gameAction("tossup_wrong");
|
||||
} else if (phase === "bonus" && gameState?.bonusStartTime) {
|
||||
gameAction("bonus_wrong");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyPress);
|
||||
return () => window.removeEventListener("keydown", handleKeyPress);
|
||||
}, [roomCode, gameState, players]);
|
||||
|
||||
if (roomClosed) {
|
||||
return (
|
||||
<main className="min-h-screen flex flex-col items-center justify-center p-8">
|
||||
<div className="fade-in bg-[var(--ctp-surface0)] p-12 rounded-2xl border-2 border-[var(--ctp-surface2)] max-w-md">
|
||||
<h1 className="text-4xl font-bold mb-6 text-[var(--ctp-red)] text-center">
|
||||
<main className="min-h-screen flex items-center justify-center p-16">
|
||||
<div className="card fade-in p-20 max-w-2xl text-center">
|
||||
<h1 className="text-7xl font-bold mb-12 text-[var(--ctp-red)]">
|
||||
Room Closed
|
||||
</h1>
|
||||
<a
|
||||
href="/"
|
||||
className="btn inline-block w-full text-center px-8 py-4 bg-[var(--ctp-blue)] hover:bg-[var(--ctp-sapphire)] text-[var(--ctp-base)] rounded-lg transition-colors font-bold"
|
||||
className="btn inline-block px-20 py-8 bg-[var(--ctp-blue)] hover:bg-[var(--ctp-sapphire)] text-[var(--ctp-base)] text-3xl"
|
||||
>
|
||||
Go Home
|
||||
</a>
|
||||
@@ -100,45 +145,45 @@ export default function HostPage() {
|
||||
|
||||
if (!roomCode) {
|
||||
return (
|
||||
<main className="min-h-screen flex flex-col items-center justify-center p-8">
|
||||
<div className="fade-in bg-[var(--ctp-surface0)] p-12 rounded-2xl border-2 border-[var(--ctp-surface2)] max-w-lg w-full">
|
||||
<h1 className="text-5xl font-bold mb-12 text-center text-[var(--ctp-mauve)]">
|
||||
<main className="min-h-screen flex items-center justify-center p-16">
|
||||
<div className="card fade-in p-20 max-w-4xl w-full">
|
||||
<h1 className="text-8xl font-bold mb-20 text-center text-[var(--ctp-mauve)]">
|
||||
Create Room
|
||||
</h1>
|
||||
|
||||
<div className="space-y-6 mb-12">
|
||||
<label className="flex items-start p-6 bg-[var(--ctp-surface1)] rounded-xl cursor-pointer hover:bg-[var(--ctp-surface2)] transition-colors border-2 border-transparent hover:border-[var(--ctp-mauve)]">
|
||||
<div className="space-y-8 mb-20">
|
||||
<label className="flex items-start p-12 bg-[var(--ctp-surface1)] rounded-3xl cursor-pointer hover:bg-[var(--ctp-surface2)] transition-all border-4 border-transparent hover:border-[var(--ctp-mauve)]">
|
||||
<input
|
||||
type="radio"
|
||||
value="1"
|
||||
checked={teamMode === "1"}
|
||||
onChange={(e) => setTeamMode(e.target.value as "1" | "2")}
|
||||
className="mr-4 mt-1 w-5 h-5"
|
||||
className="mr-8 mt-2 w-8 h-8"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-bold text-lg text-[var(--ctp-text)] mb-2">
|
||||
<div className="font-bold text-3xl text-[var(--ctp-text)] mb-4">
|
||||
Individual Scoring
|
||||
</div>
|
||||
<div className="text-sm text-[var(--ctp-subtext0)]">
|
||||
Track each player's performance separately
|
||||
<div className="text-xl text-[var(--ctp-subtext0)]">
|
||||
Track each player separately
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-start p-6 bg-[var(--ctp-surface1)] rounded-xl cursor-pointer hover:bg-[var(--ctp-surface2)] transition-colors border-2 border-transparent hover:border-[var(--ctp-blue)]">
|
||||
<label className="flex items-start p-12 bg-[var(--ctp-surface1)] rounded-3xl cursor-pointer hover:bg-[var(--ctp-surface2)] transition-all border-4 border-transparent hover:border-[var(--ctp-blue)]">
|
||||
<input
|
||||
type="radio"
|
||||
value="2"
|
||||
checked={teamMode === "2"}
|
||||
onChange={(e) => setTeamMode(e.target.value as "1" | "2")}
|
||||
className="mr-4 mt-1 w-5 h-5"
|
||||
className="mr-8 mt-2 w-8 h-8"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-bold text-lg text-[var(--ctp-text)] mb-2">
|
||||
<div className="font-bold text-3xl text-[var(--ctp-text)] mb-4">
|
||||
Team Mode
|
||||
</div>
|
||||
<div className="text-sm text-[var(--ctp-subtext0)]">
|
||||
Two teams competing against each other
|
||||
<div className="text-xl text-[var(--ctp-subtext0)]">
|
||||
Two teams competing
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
@@ -146,7 +191,7 @@ export default function HostPage() {
|
||||
|
||||
<button
|
||||
onClick={createRoom}
|
||||
className="btn w-full px-12 py-6 bg-[var(--ctp-green)] hover:bg-[var(--ctp-teal)] text-[var(--ctp-base)] rounded-xl text-2xl font-bold transition-all"
|
||||
className="btn w-full px-16 py-10 bg-[var(--ctp-green)] hover:bg-[var(--ctp-teal)] text-[var(--ctp-base)] text-4xl font-bold"
|
||||
>
|
||||
Create Room
|
||||
</button>
|
||||
@@ -164,52 +209,51 @@ export default function HostPage() {
|
||||
const buzzedPlayer = players.find((p) => p.id === gameState?.buzzedPlayer);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen p-8">
|
||||
<div className="max-w-7xl mx-auto space-y-8">
|
||||
<main className="min-h-screen p-12">
|
||||
<div className="max-w-[2000px] mx-auto space-y-12">
|
||||
{/* Header */}
|
||||
<div className="bg-[var(--ctp-surface0)] p-8 rounded-2xl border-2 border-[var(--ctp-surface2)] flex justify-between items-center">
|
||||
<div className="card p-12 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-6xl font-bold text-[var(--ctp-mauve)] mb-3 tracking-wider">
|
||||
<h1 className="text-9xl font-bold text-[var(--ctp-mauve)] mb-6 tracking-wider">
|
||||
{roomCode}
|
||||
</h1>
|
||||
<p className="text-xl text-[var(--ctp-subtext0)]">
|
||||
{isIndividual ? "Individual Mode" : "Team Mode"} • Question{" "}
|
||||
{gameState?.questionNumber || 0} /{" "}
|
||||
{gameState?.totalQuestions || 20}
|
||||
<p className="text-3xl text-[var(--ctp-subtext0)]">
|
||||
{isIndividual ? "Individual" : "Team"} • Q
|
||||
{gameState?.questionNumber || 0}/{gameState?.totalQuestions || 20}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={closeRoom}
|
||||
className="btn px-8 py-4 bg-[var(--ctp-red)] hover:bg-[var(--ctp-maroon)] text-[var(--ctp-base)] rounded-xl font-bold text-lg transition-all"
|
||||
className="btn px-12 py-6 bg-[var(--ctp-red)] hover:bg-[var(--ctp-maroon)] text-[var(--ctp-base)] text-2xl font-bold"
|
||||
>
|
||||
Close Room
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Scores */}
|
||||
<div
|
||||
className={`grid ${isIndividual ? "grid-cols-1" : "grid-cols-2"} gap-8`}
|
||||
className={`grid ${isIndividual ? "grid-cols-1" : "grid-cols-2"} gap-12`}
|
||||
>
|
||||
<div className="bg-[var(--ctp-blue)]/10 border-3 border-[var(--ctp-blue)] p-8 rounded-2xl">
|
||||
<h2 className="text-3xl font-bold mb-6 text-[var(--ctp-blue)]">
|
||||
<div className="card bg-[var(--ctp-blue)]/10 border-[var(--ctp-blue)] border-4 p-12">
|
||||
<h2 className="text-5xl font-bold mb-10 text-[var(--ctp-blue)]">
|
||||
{isIndividual ? "Players" : "Team 1"}:{" "}
|
||||
{team1.reduce((sum, p) => sum + p.score, 0)}
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-5">
|
||||
{team1.length === 0 ? (
|
||||
<p className="text-[var(--ctp-overlay0)] italic py-4">
|
||||
No players yet...
|
||||
<p className="text-[var(--ctp-overlay0)] italic text-2xl py-8">
|
||||
Waiting...
|
||||
</p>
|
||||
) : (
|
||||
team1.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex justify-between items-center bg-[var(--ctp-surface0)] p-5 rounded-xl"
|
||||
className="flex justify-between items-center bg-[var(--ctp-surface0)] p-8 rounded-2xl"
|
||||
>
|
||||
<span className="text-[var(--ctp-text)] text-lg">
|
||||
<span className="text-[var(--ctp-text)] text-3xl">
|
||||
{p.name}
|
||||
</span>
|
||||
<span className="font-bold text-xl text-[var(--ctp-blue)]">
|
||||
<span className="font-bold text-4xl text-[var(--ctp-blue)]">
|
||||
{p.score}
|
||||
</span>
|
||||
</div>
|
||||
@@ -219,25 +263,25 @@ export default function HostPage() {
|
||||
</div>
|
||||
|
||||
{!isIndividual && (
|
||||
<div className="bg-[var(--ctp-red)]/10 border-3 border-[var(--ctp-red)] p-8 rounded-2xl">
|
||||
<h2 className="text-3xl font-bold mb-6 text-[var(--ctp-red)]">
|
||||
<div className="card bg-[var(--ctp-red)]/10 border-[var(--ctp-red)] border-4 p-12">
|
||||
<h2 className="text-5xl font-bold mb-10 text-[var(--ctp-red)]">
|
||||
Team 2: {team2.reduce((sum, p) => sum + p.score, 0)}
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-5">
|
||||
{team2.length === 0 ? (
|
||||
<p className="text-[var(--ctp-overlay0)] italic py-4">
|
||||
No players yet...
|
||||
<p className="text-[var(--ctp-overlay0)] italic text-2xl py-8">
|
||||
Waiting...
|
||||
</p>
|
||||
) : (
|
||||
team2.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex justify-between items-center bg-[var(--ctp-surface0)] p-5 rounded-xl"
|
||||
className="flex justify-between items-center bg-[var(--ctp-surface0)] p-8 rounded-2xl"
|
||||
>
|
||||
<span className="text-[var(--ctp-text)] text-lg">
|
||||
<span className="text-[var(--ctp-text)] text-3xl">
|
||||
{p.name}
|
||||
</span>
|
||||
<span className="font-bold text-xl text-[var(--ctp-red)]">
|
||||
<span className="font-bold text-4xl text-[var(--ctp-red)]">
|
||||
{p.score}
|
||||
</span>
|
||||
</div>
|
||||
@@ -249,203 +293,228 @@ export default function HostPage() {
|
||||
</div>
|
||||
|
||||
{/* Game State */}
|
||||
<div className="bg-[var(--ctp-surface0)] border-2 border-[var(--ctp-surface2)] p-12 rounded-2xl min-h-[500px] flex items-center justify-center">
|
||||
<div className="card p-20 min-h-[700px] flex items-center justify-center">
|
||||
{phase === "lobby" && (
|
||||
<div className="fade-in text-center w-full">
|
||||
<h2 className="text-5xl font-bold mb-8 text-[var(--ctp-yellow)]">
|
||||
Waiting for Players...
|
||||
<h2 className="text-7xl font-bold mb-12 text-[var(--ctp-yellow)]">
|
||||
Waiting...
|
||||
</h2>
|
||||
<p className="text-2xl mb-12 text-[var(--ctp-subtext1)]">
|
||||
{players.length} player{players.length !== 1 && "s"} joined
|
||||
<p className="text-4xl mb-20 text-[var(--ctp-subtext1)]">
|
||||
{players.length} player{players.length !== 1 && "s"}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => gameAction("start")}
|
||||
className="btn px-16 py-8 bg-[var(--ctp-green)] hover:bg-[var(--ctp-teal)] disabled:bg-[var(--ctp-surface1)] disabled:text-[var(--ctp-overlay0)] text-[var(--ctp-base)] rounded-xl text-3xl font-bold transition-all disabled:cursor-not-allowed"
|
||||
className="btn px-24 py-12 bg-[var(--ctp-green)] hover:bg-[var(--ctp-teal)] disabled:bg-[var(--ctp-surface1)] disabled:text-[var(--ctp-overlay0)] text-[var(--ctp-base)] text-5xl font-bold"
|
||||
disabled={players.length === 0}
|
||||
>
|
||||
Start Game
|
||||
</button>
|
||||
{players.length > 0 && (
|
||||
<p className="text-2xl text-[var(--ctp-subtext0)] mt-8">
|
||||
Press SPACE to start
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "intermission" && (
|
||||
<div className="fade-in text-center w-full space-y-8">
|
||||
<h2 className="text-5xl font-bold text-[var(--ctp-sky)] mb-12">
|
||||
Ready for Next Question
|
||||
<div className="fade-in text-center w-full space-y-16">
|
||||
<h2 className="text-7xl font-bold text-[var(--ctp-sky)]">
|
||||
Next Question
|
||||
</h2>
|
||||
<div className="flex gap-6 justify-center flex-wrap">
|
||||
<button
|
||||
onClick={() => gameAction("load_question")}
|
||||
className="btn px-12 py-6 bg-[var(--ctp-blue)] hover:bg-[var(--ctp-sapphire)] text-[var(--ctp-base)] rounded-xl text-2xl font-bold transition-all"
|
||||
className="btn px-24 py-12 bg-[var(--ctp-blue)] hover:bg-[var(--ctp-sapphire)] text-[var(--ctp-base)] text-5xl font-bold"
|
||||
>
|
||||
Load Question
|
||||
</button>
|
||||
<button
|
||||
onClick={() => gameAction("end")}
|
||||
className="btn px-12 py-6 bg-[var(--ctp-overlay0)] hover:bg-[var(--ctp-overlay1)] text-[var(--ctp-text)] rounded-xl text-2xl font-bold transition-all"
|
||||
>
|
||||
End Game
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-2xl text-[var(--ctp-subtext0)]">
|
||||
Press SPACE to load
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "tossup_reading" && question && (
|
||||
<div className="fade-in w-full space-y-8">
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<h2 className="text-5xl font-bold text-[var(--ctp-yellow)]">
|
||||
<div className="fade-in w-full space-y-12">
|
||||
<div className="flex justify-between items-start">
|
||||
<h2 className="text-7xl font-bold text-[var(--ctp-yellow)]">
|
||||
TOSSUP
|
||||
</h2>
|
||||
<span className="text-2xl px-6 py-3 bg-[var(--ctp-surface1)] rounded-xl text-[var(--ctp-yellow)] font-bold">
|
||||
<span className="text-3xl px-10 py-5 bg-[var(--ctp-surface1)] rounded-2xl text-[var(--ctp-yellow)] font-bold">
|
||||
{question.category}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-2xl mb-10 text-[var(--ctp-text)] leading-relaxed">
|
||||
<p className="text-4xl mb-16 text-[var(--ctp-text)] leading-relaxed">
|
||||
{question.tossup_question}
|
||||
</p>
|
||||
|
||||
<div className="p-6 bg-[var(--ctp-surface1)] rounded-xl mb-10">
|
||||
<span className="text-sm text-[var(--ctp-subtext0)] uppercase tracking-wide block mb-3">
|
||||
<div className="p-10 bg-[var(--ctp-surface1)] rounded-2xl mb-16">
|
||||
<span className="text-xl text-[var(--ctp-subtext0)] uppercase tracking-wide block mb-5">
|
||||
Answer:
|
||||
</span>
|
||||
<p className="text-2xl font-bold text-[var(--ctp-green)]">
|
||||
<p className="text-4xl font-bold text-[var(--ctp-green)]">
|
||||
{question.tossup_answer}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => gameAction("start_timer")}
|
||||
className="btn px-12 py-6 bg-[var(--ctp-green)] hover:bg-[var(--ctp-teal)] text-[var(--ctp-base)] rounded-xl text-2xl font-bold transition-all"
|
||||
className="btn px-20 py-10 bg-[var(--ctp-green)] hover:bg-[var(--ctp-teal)] text-[var(--ctp-base)] text-4xl font-bold"
|
||||
>
|
||||
Start Timer (20s)
|
||||
Start Timer (5s)
|
||||
</button>
|
||||
<p className="text-2xl text-[var(--ctp-subtext0)] mt-6">
|
||||
Press SPACE after reading
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "tossup_buzzing" && question && (
|
||||
<div className="fade-in w-full space-y-8">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h2 className="text-5xl font-bold text-[var(--ctp-green)]">
|
||||
BUZZING OPEN
|
||||
<div className="fade-in w-full space-y-12">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-7xl font-bold text-[var(--ctp-green)]">
|
||||
BUZZING
|
||||
</h2>
|
||||
<Timer startTime={gameState?.tossupStartTime} duration={20} />
|
||||
<Timer startTime={gameState?.tossupStartTime} duration={5} />
|
||||
</div>
|
||||
|
||||
{!buzzedPlayer ? (
|
||||
<p className="text-3xl text-[var(--ctp-text)] text-center py-20">
|
||||
Waiting for buzz...
|
||||
<>
|
||||
<p className="text-5xl text-[var(--ctp-text)] text-center py-32">
|
||||
Waiting...
|
||||
</p>
|
||||
{gameState?.tossupStartTime && (
|
||||
<button
|
||||
onClick={() => gameAction("move_on")}
|
||||
className="btn w-full px-20 py-10 bg-[var(--ctp-overlay0)] hover:bg-[var(--ctp-overlay1)] text-[var(--ctp-text)] text-4xl font-bold"
|
||||
>
|
||||
Move On (No Answer)
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-[var(--ctp-blue)]/20 border-3 border-[var(--ctp-blue)] p-8 rounded-2xl">
|
||||
<p className="text-3xl font-bold text-[var(--ctp-blue)] mb-3">
|
||||
{buzzedPlayer.name} (Team {buzzedPlayer.teamId}) buzzed!
|
||||
<div className="space-y-12">
|
||||
<div className="bg-[var(--ctp-blue)]/20 border-4 border-[var(--ctp-blue)] p-12 rounded-3xl">
|
||||
<p className="text-5xl font-bold text-[var(--ctp-blue)] mb-5">
|
||||
{buzzedPlayer.name} (Team {buzzedPlayer.teamId})
|
||||
</p>
|
||||
{gameState?.tossupInterrupted && (
|
||||
<p className="text-[var(--ctp-red)] font-bold text-2xl">
|
||||
<p className="text-[var(--ctp-red)] font-bold text-4xl">
|
||||
⚠️ INTERRUPTED
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-[var(--ctp-surface1)] rounded-xl">
|
||||
<span className="text-sm text-[var(--ctp-subtext0)] uppercase tracking-wide block mb-3">
|
||||
<div className="p-10 bg-[var(--ctp-surface1)] rounded-2xl">
|
||||
<span className="text-xl text-[var(--ctp-subtext0)] uppercase tracking-wide block mb-5">
|
||||
Answer:
|
||||
</span>
|
||||
<p className="text-2xl font-bold text-[var(--ctp-green)]">
|
||||
<p className="text-4xl font-bold text-[var(--ctp-green)]">
|
||||
{question.tossup_answer}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div className="grid grid-cols-2 gap-10">
|
||||
<button
|
||||
onClick={() => gameAction("tossup_correct")}
|
||||
className="btn px-10 py-8 bg-[var(--ctp-green)] hover:bg-[var(--ctp-teal)] text-[var(--ctp-base)] rounded-xl text-2xl font-bold transition-all"
|
||||
className="btn px-16 py-12 bg-[var(--ctp-green)] hover:bg-[var(--ctp-teal)] text-[var(--ctp-base)] text-4xl font-bold"
|
||||
>
|
||||
✓ Correct (+4)
|
||||
✓ Correct
|
||||
</button>
|
||||
<button
|
||||
onClick={() => gameAction("tossup_wrong")}
|
||||
className="btn px-10 py-8 bg-[var(--ctp-red)] hover:bg-[var(--ctp-maroon)] text-[var(--ctp-base)] rounded-xl text-2xl font-bold transition-all"
|
||||
className="btn px-16 py-12 bg-[var(--ctp-red)] hover:bg-[var(--ctp-maroon)] text-[var(--ctp-base)] text-4xl font-bold"
|
||||
>
|
||||
✗ Wrong{" "}
|
||||
{gameState?.tossupInterrupted
|
||||
? "(-4 / +4)"
|
||||
: "(no penalty)"}
|
||||
✗ Wrong
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-2xl text-[var(--ctp-subtext0)] text-center">
|
||||
Press C for Correct, W for Wrong
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "bonus" && question && (
|
||||
<div className="fade-in w-full space-y-8">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h2 className="text-5xl font-bold text-[var(--ctp-mauve)]">
|
||||
BONUS - Team {gameState?.currentTeam}
|
||||
<div className="fade-in w-full space-y-12">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-7xl font-bold text-[var(--ctp-mauve)]">
|
||||
BONUS (Team {gameState?.currentTeam})
|
||||
</h2>
|
||||
{gameState?.bonusStartTime && (
|
||||
<Timer startTime={gameState.bonusStartTime} duration={40} />
|
||||
<Timer startTime={gameState.bonusStartTime} duration={20} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-2xl mb-10 text-[var(--ctp-text)] leading-relaxed">
|
||||
<p className="text-4xl mb-16 text-[var(--ctp-text)] leading-relaxed">
|
||||
{question.bonus_question}
|
||||
</p>
|
||||
|
||||
<div className="p-6 bg-[var(--ctp-surface1)] rounded-xl mb-10">
|
||||
<span className="text-sm text-[var(--ctp-subtext0)] uppercase tracking-wide block mb-3">
|
||||
<div className="p-10 bg-[var(--ctp-surface1)] rounded-2xl mb-16">
|
||||
<span className="text-xl text-[var(--ctp-subtext0)] uppercase tracking-wide block mb-5">
|
||||
Answer:
|
||||
</span>
|
||||
<p className="text-2xl font-bold text-[var(--ctp-green)]">
|
||||
<p className="text-4xl font-bold text-[var(--ctp-green)]">
|
||||
{question.bonus_answer}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!gameState?.bonusStartTime ? (
|
||||
<>
|
||||
<button
|
||||
onClick={() => gameAction("bonus_start_timer")}
|
||||
className="btn w-full px-12 py-6 bg-[var(--ctp-yellow)] hover:bg-[var(--ctp-peach)] text-[var(--ctp-base)] rounded-xl text-2xl font-bold transition-all mb-6"
|
||||
className="btn w-full px-20 py-10 bg-[var(--ctp-yellow)] hover:bg-[var(--ctp-peach)] text-[var(--ctp-base)] text-4xl font-bold"
|
||||
>
|
||||
Start Timer (40s)
|
||||
Start Timer (20s)
|
||||
</button>
|
||||
<p className="text-2xl text-[var(--ctp-subtext0)] text-center mt-6">
|
||||
Press SPACE after reading
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-8">
|
||||
<button
|
||||
onClick={() => gameAction("bonus_correct")}
|
||||
className="btn px-10 py-8 bg-[var(--ctp-green)] hover:bg-[var(--ctp-teal)] text-[var(--ctp-base)] rounded-xl text-2xl font-bold transition-all"
|
||||
className="btn px-12 py-12 bg-[var(--ctp-green)] hover:bg-[var(--ctp-teal)] text-[var(--ctp-base)] text-4xl font-bold"
|
||||
>
|
||||
✓ Correct (+10)
|
||||
✓ Correct
|
||||
</button>
|
||||
<button
|
||||
onClick={() => gameAction("bonus_wrong")}
|
||||
className="btn px-10 py-8 bg-[var(--ctp-red)] hover:bg-[var(--ctp-maroon)] text-[var(--ctp-base)] rounded-xl text-2xl font-bold transition-all"
|
||||
className="btn px-12 py-12 bg-[var(--ctp-red)] hover:bg-[var(--ctp-maroon)] text-[var(--ctp-base)] text-4xl font-bold"
|
||||
>
|
||||
✗ Wrong
|
||||
</button>
|
||||
<button
|
||||
onClick={() => gameAction("move_on")}
|
||||
className="btn px-12 py-12 bg-[var(--ctp-overlay0)] hover:bg-[var(--ctp-overlay1)] text-[var(--ctp-text)] text-4xl font-bold"
|
||||
>
|
||||
Move On
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-2xl text-[var(--ctp-subtext0)] text-center">
|
||||
Press C for Correct, W for Wrong
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "finished" && (
|
||||
<div className="fade-in text-center w-full">
|
||||
<h2 className="text-6xl font-bold mb-12 text-[var(--ctp-green)]">
|
||||
<h2 className="text-8xl font-bold mb-20 text-[var(--ctp-green)]">
|
||||
Game Over!
|
||||
</h2>
|
||||
<div className="text-4xl mb-8">
|
||||
{isIndividual ? (
|
||||
<div className="space-y-6">
|
||||
<p className="text-[var(--ctp-subtext1)] text-2xl mb-8">
|
||||
Final Scores:
|
||||
</p>
|
||||
<div className="space-y-10">
|
||||
{[...team1]
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.map((p, i) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className={`text-3xl ${i === 0 ? "text-[var(--ctp-yellow)] text-5xl font-bold" : "text-[var(--ctp-text)]"}`}
|
||||
className={`text-5xl ${i === 0 ? "text-[var(--ctp-yellow)] text-7xl font-bold" : "text-[var(--ctp-text)]"}`}
|
||||
>
|
||||
{i === 0 && "🏆 "}
|
||||
{p.name}: {p.score}
|
||||
@@ -453,8 +522,8 @@ export default function HostPage() {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-center items-center gap-12 text-4xl">
|
||||
<div className="space-y-16">
|
||||
<div className="flex justify-center items-center gap-20 text-6xl">
|
||||
<span className="text-[var(--ctp-blue)]">
|
||||
Team 1: {team1.reduce((s, p) => s + p.score, 0)}
|
||||
</span>
|
||||
@@ -463,7 +532,7 @@ export default function HostPage() {
|
||||
Team 2: {team2.reduce((s, p) => s + p.score, 0)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-5xl font-bold text-[var(--ctp-yellow)]">
|
||||
<p className="text-7xl font-bold text-[var(--ctp-yellow)]">
|
||||
{team1.reduce((s, p) => s + p.score, 0) >
|
||||
team2.reduce((s, p) => s + p.score, 0)
|
||||
? "🏆 Team 1 Wins!"
|
||||
@@ -475,7 +544,6 @@ export default function HostPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,9 +53,21 @@ export default function PlayPage() {
|
||||
if (me) setMyScore(me.score);
|
||||
|
||||
const phase = data.gameState?.phase;
|
||||
|
||||
// Check if timer expired
|
||||
let timerExpired = false;
|
||||
if (data.gameState?.tossupStartTime) {
|
||||
const elapsed = Math.floor(
|
||||
(Date.now() - data.gameState.tossupStartTime) / 1000,
|
||||
);
|
||||
timerExpired = elapsed >= 5;
|
||||
}
|
||||
|
||||
// Can buzz during reading OR buzzing phase (but not if timer expired or already buzzed)
|
||||
setCanBuzz(
|
||||
(phase === "tossup_reading" || phase === "tossup_buzzing") &&
|
||||
!data.gameState?.buzzedPlayer,
|
||||
!data.gameState?.buzzedPlayer &&
|
||||
!timerExpired,
|
||||
);
|
||||
}, 500);
|
||||
|
||||
|
||||
@@ -16,10 +16,12 @@ export default function Timer({
|
||||
showWarning = true,
|
||||
}: TimerProps) {
|
||||
const [remaining, setRemaining] = useState(duration);
|
||||
const [hasExpired, setHasExpired] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!startTime) {
|
||||
setRemaining(duration);
|
||||
setHasExpired(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -28,35 +30,41 @@ export default function Timer({
|
||||
const left = Math.max(0, duration - elapsed);
|
||||
setRemaining(left);
|
||||
|
||||
if (left === 0 && onExpire) {
|
||||
if (left === 0 && !hasExpired) {
|
||||
setHasExpired(true);
|
||||
if (onExpire) {
|
||||
onExpire();
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [startTime, duration, onExpire]);
|
||||
}, [startTime, duration, onExpire, hasExpired]);
|
||||
|
||||
const percentage = (remaining / duration) * 100;
|
||||
const isWarning = remaining <= 10;
|
||||
const isCritical = remaining <= 5;
|
||||
const isExpired = remaining === 0;
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md scale-in">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`status-dot ${isCritical ? "status-error" : isWarning ? "status-waiting" : "status-active"}`}
|
||||
className={`status-dot ${isExpired ? "status-error" : isCritical ? "status-error" : isWarning ? "status-waiting" : "status-active"}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-3xl font-bold transition-all duration-300 ${
|
||||
isCritical
|
||||
isExpired
|
||||
? "text-[var(--ctp-red)] timer-warning neon-text"
|
||||
: isCritical
|
||||
? "text-[var(--ctp-red)] timer-warning neon-text"
|
||||
: isWarning
|
||||
? "text-[var(--ctp-yellow)]"
|
||||
: "text-[var(--ctp-green)]"
|
||||
}`}
|
||||
>
|
||||
{remaining}s
|
||||
{isExpired ? "TIME!" : `${remaining}s`}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-[var(--ctp-subtext0)] font-mono">
|
||||
@@ -68,7 +76,7 @@ export default function Timer({
|
||||
<div className="w-full h-4 bg-[var(--ctp-surface0)] rounded-full overflow-hidden shadow-inner">
|
||||
<div
|
||||
className={`timer-bar h-full ${
|
||||
isCritical
|
||||
isExpired || isCritical
|
||||
? "bg-gradient-to-r from-[var(--ctp-red)] to-[var(--ctp-maroon)] glow-red"
|
||||
: isWarning
|
||||
? "bg-gradient-to-r from-[var(--ctp-yellow)] to-[var(--ctp-peach)] glow-yellow"
|
||||
@@ -78,10 +86,10 @@ export default function Timer({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showWarning && isCritical && (
|
||||
{showWarning && isExpired && (
|
||||
<div className="absolute -top-12 left-1/2 transform -translate-x-1/2 scale-in">
|
||||
<div className="bg-[var(--ctp-red)] text-[var(--ctp-base)] px-4 py-2 rounded-lg font-bold shadow-lg glow-red">
|
||||
⚠️ TIME RUNNING OUT!
|
||||
⏰ TIME EXPIRED!
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -148,11 +148,12 @@ export function loadQuestion(code: string, moderatorId: string, question: any) {
|
||||
export function startTossupTimer(code: string, moderatorId: string) {
|
||||
const room = rooms.get(code);
|
||||
if (!room || room.moderatorId !== moderatorId) return false;
|
||||
if (room.gameState.phase !== 'tossup_reading') return false;
|
||||
|
||||
room.gameState.phase = 'tossup_buzzing';
|
||||
room.gameState.tossupStartTime = Date.now();
|
||||
|
||||
console.log('⏱️ Tossup timer started');
|
||||
console.log('⏱️ Tossup timer started (5s)');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -163,10 +164,9 @@ export function startBonusTimer(code: string, moderatorId: string) {
|
||||
|
||||
room.gameState.bonusStartTime = Date.now();
|
||||
|
||||
console.log('⏱️ Bonus timer started');
|
||||
console.log('⏱️ Bonus timer started (20s)');
|
||||
return true;
|
||||
}
|
||||
|
||||
export function buzz(code: string, playerId: string) {
|
||||
const room = rooms.get(code);
|
||||
|
||||
@@ -177,6 +177,15 @@ export function buzz(code: string, playerId: string) {
|
||||
|
||||
if (room.gameState.buzzedPlayer) return false; // Already buzzed
|
||||
|
||||
// Check if timer expired (5 seconds)
|
||||
if (room.gameState.tossupStartTime) {
|
||||
const elapsed = Math.floor((Date.now() - room.gameState.tossupStartTime) / 1000);
|
||||
if (elapsed >= 5) {
|
||||
console.log('⏰ Buzz rejected - timer expired');
|
||||
return false; // Timer expired, no more buzzing
|
||||
}
|
||||
}
|
||||
|
||||
const player = room.players.get(playerId);
|
||||
if (!player) return false;
|
||||
|
||||
@@ -263,6 +272,32 @@ export function markTossupWrong(code: string, moderatorId: string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function moveOn(code: string, moderatorId: string) {
|
||||
const room = rooms.get(code);
|
||||
if (!room || room.moderatorId !== moderatorId) return false;
|
||||
|
||||
const phase = room.gameState.phase;
|
||||
|
||||
// Move on from tossup buzzing (time expired, no one buzzed or wrong answer)
|
||||
if (phase === 'tossup_buzzing') {
|
||||
room.gameState.phase = 'intermission';
|
||||
room.gameState.buzzedPlayer = null;
|
||||
room.gameState.tossupStartTime = null;
|
||||
console.log('⏭️ Moving on from tossup');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Move on from bonus (time expired)
|
||||
if (phase === 'bonus') {
|
||||
room.gameState.phase = 'intermission';
|
||||
room.gameState.currentTeam = null;
|
||||
room.gameState.bonusStartTime = null;
|
||||
console.log('⏭️ Moving on from bonus');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function markBonusWrong(code: string, moderatorId: string) {
|
||||
const room = rooms.get(code);
|
||||
|
||||
Reference in New Issue
Block a user