"use client"; import { useEffect, useRef, useState } from "react"; import { getSocket } from "../lib/socket-client"; import { useGame } from "../lib/store"; import ChatBox from "./ChatBox"; import PlayerList from "./PlayerList"; const PALETTE = [ "#2D2D2D","#FFFFFF","#FF5C5C","#FFD23F","#4ECDC4","#A593E0","#5BCEFA", "#1F8FFF","#1AAE56","#A35924","#FF8FB1","#7B3F00","#888888" ]; const SIZES = [3, 6, 12]; export default function SkribblGame() { const room = useGame((s) => s.room); const myId = useGame((s) => s.myId); const [choices, setChoices] = useState(null); const [endsAt, setEndsAt] = useState(0); const [now, setNow] = useState(Date.now()); const [color, setColor] = useState("#2D2D2D"); const [size, setSize] = useState(6); const [tool, setTool] = useState<"brush"|"eraser">("brush"); const canvasRef = useRef(null); const stageRef = useRef(null); const drawingRef = useRef(false); const lastRef = useRef<{x:number;y:number}|null>(null); const broadcastBufRef = useRef([]); const lastBroadcastRef = useRef(0); // Listen for skribbl-specific events useEffect(() => { const socket = getSocket(); const onChoices = (c: string[]) => setChoices(c); const onRoundStart = (d: any) => { setChoices(null); setEndsAt(Date.now() + d.durationMs); clearLocalCanvas(); }; const onHint = () => {/* server pushes new room state */}; const onRoundEnd = () => { setEndsAt(0); }; const onStroke = (s: any) => { drawSegment(s.prevX, s.prevY, s.x, s.y, s.color, s.size, s.tool); }; const onClear = () => clearLocalCanvas(); socket.on("skribbl:wordChoices", onChoices); socket.on("skribbl:roundStart", onRoundStart); socket.on("skribbl:hint", onHint); socket.on("skribbl:roundEnd", onRoundEnd); socket.on("skribbl:stroke", onStroke); socket.on("skribbl:clear", onClear); return () => { socket.off("skribbl:wordChoices", onChoices); socket.off("skribbl:roundStart", onRoundStart); socket.off("skribbl:hint", onHint); socket.off("skribbl:roundEnd", onRoundEnd); socket.off("skribbl:stroke", onStroke); socket.off("skribbl:clear", onClear); }; }, []); // Use endsAt from room.skribbl too, in case we joined mid-round useEffect(() => { if (room?.skribbl?.endsAt) setEndsAt(room.skribbl.endsAt); }, [room?.skribbl?.endsAt]); // Tick useEffect(() => { const t = setInterval(() => setNow(Date.now()), 250); return () => clearInterval(t); }, []); // Resize canvas useEffect(() => { const resize = () => { const canvas = canvasRef.current; const stage = stageRef.current; if (!canvas || !stage) return; const rect = stage.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; canvas.width = Math.max(100, rect.width * dpr); canvas.height = Math.max(100, rect.height * dpr); canvas.style.width = rect.width + "px"; canvas.style.height = rect.height + "px"; const ctx = canvas.getContext("2d"); if (ctx) ctx.scale(dpr, dpr); }; resize(); window.addEventListener("resize", resize); return () => window.removeEventListener("resize", resize); }, []); const drawSegment = (px:number, py:number, x:number, y:number, c:string, sz:number, tl:string) => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.strokeStyle = tl === "eraser" ? "#FFF8E7" : c; ctx.lineWidth = sz; ctx.beginPath(); ctx.moveTo(px, py); ctx.lineTo(x, y); ctx.stroke(); }; const clearLocalCanvas = () => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; ctx.clearRect(0, 0, canvas.width, canvas.height); }; const sk = room?.skribbl; const isDrawer = sk?.drawerId === myId; const drawerName = room?.players.find(p => p.id === sk?.drawerId)?.nickname || "?"; const getPos = (e: React.PointerEvent) => { const canvas = canvasRef.current!; const rect = canvas.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; }; const onDown = (e: React.PointerEvent) => { if (!isDrawer || sk?.phase !== "drawing") return; e.preventDefault(); drawingRef.current = true; const p = getPos(e); lastRef.current = p; drawSegment(p.x, p.y, p.x, p.y, color, size, tool); queueStroke({ prevX: p.x, prevY: p.y, x: p.x, y: p.y, color, size, tool }); }; const onMove = (e: React.PointerEvent) => { if (!drawingRef.current || !isDrawer) return; const p = getPos(e); const last = lastRef.current!; drawSegment(last.x, last.y, p.x, p.y, color, size, tool); queueStroke({ prevX: last.x, prevY: last.y, x: p.x, y: p.y, color, size, tool }); lastRef.current = p; }; const onUp = () => { drawingRef.current = false; lastRef.current = null; flushStrokes(); }; const queueStroke = (s: any) => { broadcastBufRef.current.push(s); const now = Date.now(); if (now - lastBroadcastRef.current > 33) { flushStrokes(); } }; const flushStrokes = () => { const buf = broadcastBufRef.current; if (!buf.length) return; const socket = getSocket(); for (const s of buf) socket.emit("skribbl:stroke", s); broadcastBufRef.current = []; lastBroadcastRef.current = Date.now(); }; const pickWord = (word: string) => { getSocket().emit("skribbl:pickWord", { word }, () => {}); }; const clearAll = () => { if (!isDrawer) return; clearLocalCanvas(); getSocket().emit("skribbl:clear"); }; const remaining = Math.max(0, Math.ceil((endsAt - now) / 1000)); const totalSec = (room?.settings?.drawTimeSec || 80) as number; const ringPct = endsAt ? Math.max(0, Math.min(1, (endsAt - now) / (totalSec * 1000))) : 0; const dashTotal = 165; const dashOff = dashTotal * (1 - ringPct); const wordMask = sk?.wordMask || ""; return (
{remaining}
{sk?.phase === "choosing" ? (isDrawer ? "Pick a word!" : `${drawerName} is choosing a word…`) : (isDrawer ? "Your secret word" : `${drawerName} is drawing`)}
{sk?.phase === "drawing" ? (isDrawer ? ((sk as any)?.word || wordMask) : (wordMask || "_____")) : ""}
Round {sk?.roundIndex || 0}/{sk?.totalRounds || 0}
{isDrawer &&
You're drawing
}
{isDrawer &&
{PALETTE.map(c => (
{SIZES.map((sz, i) => ( ))}
}
{choices && (

Pick a word to draw

Choose one of the three. 15s timer.

{choices.map(w => ( ))}
)}
); } function Modal({ children }: { children: React.ReactNode }) { return (
{children}
); }