feat: pick pseudo inside the room + shareable invite link
Restructure onboarding (option B): - home is just Create / Join (code) — no name field - name is chosen in the room: room:create/join no longer require a name; new player:setName event. Nameless players are hidden from the snapshot (players/scores/submissions) and excluded from eligible voters and the 3-player blindtest gate until they pick a pseudo. - PseudoScreen with live DiceBear avatar preview gates RoomPage until set - /join/:code auto-joins then redirects to the room (seamless invite links) - RoomCode gains a "copier le lien" button (origin/join/CODE) Verified e2e: nameless players excluded from the room until setName. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6251146c19
commit
1071a59b68
11 changed files with 223 additions and 69 deletions
|
|
@ -198,7 +198,7 @@ export class GameEngine implements RoomGameController {
|
||||||
private eligibleVoters(): string[] {
|
private eligibleVoters(): string[] {
|
||||||
const secret = this.runtime?.secretPlayerId
|
const secret = this.runtime?.secretPlayerId
|
||||||
return [...this.room.players.values()]
|
return [...this.room.players.values()]
|
||||||
.filter((p) => p.connected && p.id !== secret)
|
.filter((p) => p.connected && p.name !== "" && p.id !== secret)
|
||||||
.map((p) => p.id)
|
.map((p) => p.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,11 +53,8 @@ export function registerRoomHandlers(
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on("room:create", (payload, ack) => {
|
socket.on("room:create", (payload, ack) => {
|
||||||
const name = cleanName(payload?.playerName)
|
// Le pseudo est choisi dans la room (player:setName) : on crée sans nom.
|
||||||
if (!name) {
|
const name = cleanName(payload?.playerName) ?? ""
|
||||||
ack({ ok: false, error: { code: "INVALID_NAME", message: "Pseudo invalide." } })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const { room, player } = rooms.create(name, socket.id)
|
const { room, player } = rooms.create(name, socket.id)
|
||||||
socket.data.playerId = player.id
|
socket.data.playerId = player.id
|
||||||
|
|
@ -71,12 +68,8 @@ export function registerRoomHandlers(
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on("room:join", (payload, ack) => {
|
socket.on("room:join", (payload, ack) => {
|
||||||
const name = cleanName(payload?.playerName)
|
const name = cleanName(payload?.playerName) ?? ""
|
||||||
const code = typeof payload?.roomCode === "string" ? payload.roomCode.trim().toUpperCase() : ""
|
const code = typeof payload?.roomCode === "string" ? payload.roomCode.trim().toUpperCase() : ""
|
||||||
if (!name) {
|
|
||||||
ack({ ok: false, error: { code: "INVALID_NAME", message: "Pseudo invalide." } })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const { room, player } = rooms.join(code, name, socket.id)
|
const { room, player } = rooms.join(code, name, socket.id)
|
||||||
socket.data.playerId = player.id
|
socket.data.playerId = player.id
|
||||||
|
|
@ -89,6 +82,22 @@ export function registerRoomHandlers(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
socket.on("player:setName", (payload) => {
|
||||||
|
const code = socket.data.roomCode
|
||||||
|
const playerId = socket.data.playerId
|
||||||
|
const room = code ? rooms.get(code) : undefined
|
||||||
|
const name = cleanName(payload?.name)
|
||||||
|
if (!room || !playerId || !name) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const player = room.players.get(playerId)
|
||||||
|
if (!player) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
player.name = name
|
||||||
|
broadcastState(room)
|
||||||
|
})
|
||||||
|
|
||||||
socket.on("lobby:updateSettings", (payload) => {
|
socket.on("lobby:updateSettings", (payload) => {
|
||||||
const code = socket.data.roomCode
|
const code = socket.data.roomCode
|
||||||
const room = code ? rooms.get(code) : undefined
|
const room = code ? rooms.get(code) : undefined
|
||||||
|
|
@ -129,7 +138,9 @@ export function registerRoomHandlers(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const hasBlindtest = room.settings.rounds.some((r) => r.type === "blindtest")
|
const hasBlindtest = room.settings.rounds.some((r) => r.type === "blindtest")
|
||||||
const connected = [...room.players.values()].filter((p) => p.connected).length
|
const connected = [...room.players.values()].filter(
|
||||||
|
(p) => p.connected && p.name !== ""
|
||||||
|
).length
|
||||||
if (hasBlindtest && connected < 3) {
|
if (hasBlindtest && connected < 3) {
|
||||||
ack({
|
ack({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|
|
||||||
|
|
@ -112,23 +112,25 @@ export class RoomManager {
|
||||||
|
|
||||||
/** Projette la room vers la vue publique diffusée aux clients (sans secrets). */
|
/** Projette la room vers la vue publique diffusée aux clients (sans secrets). */
|
||||||
toSnapshot(room: ServerRoom): RoomSnapshot {
|
toSnapshot(room: ServerRoom): RoomSnapshot {
|
||||||
|
// Seuls les joueurs ayant choisi un pseudo sont visibles dans la room.
|
||||||
|
const named = [...room.players.values()].filter((p) => p.name !== "")
|
||||||
return {
|
return {
|
||||||
code: room.code,
|
code: room.code,
|
||||||
status: room.status,
|
status: room.status,
|
||||||
hostId: room.hostId,
|
hostId: room.hostId,
|
||||||
players: [...room.players.values()].map((p) => ({
|
players: named.map((p) => ({
|
||||||
id: p.id,
|
id: p.id,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
connected: p.connected,
|
connected: p.connected,
|
||||||
})),
|
})),
|
||||||
settings: room.settings,
|
settings: room.settings,
|
||||||
scores: [...room.scores.entries()].map(([playerId, score]) => ({
|
scores: named.map((p) => ({
|
||||||
playerId,
|
playerId: p.id,
|
||||||
score,
|
score: room.scores.get(p.id) ?? 0,
|
||||||
})),
|
})),
|
||||||
currentRound: room.currentRound,
|
currentRound: room.currentRound,
|
||||||
totalRounds: room.settings.rounds.length,
|
totalRounds: room.settings.rounds.length,
|
||||||
submissions: [...room.players.values()].map((p) => ({
|
submissions: named.map((p) => ({
|
||||||
playerId: p.id,
|
playerId: p.id,
|
||||||
count: room.blindtestTracks.filter((t) => t.submittedBy === p.id).length,
|
count: room.blindtestTracks.filter((t) => t.submittedBy === p.id).length,
|
||||||
})),
|
})),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import { Route, Switch } from "wouter"
|
import { Route, Switch } from "wouter"
|
||||||
import { HomePage } from "@/pages/home"
|
import { HomePage } from "@/pages/home"
|
||||||
|
import { JoinPage } from "@/pages/join"
|
||||||
import { RoomPage } from "@/pages/room"
|
import { RoomPage } from "@/pages/room"
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/" component={HomePage} />
|
<Route path="/" component={HomePage} />
|
||||||
|
<Route path="/join/:code">
|
||||||
|
{(params) => <JoinPage code={params.code.toUpperCase()} />}
|
||||||
|
</Route>
|
||||||
<Route path="/room/:code">
|
<Route path="/room/:code">
|
||||||
{(params) => <RoomPage code={params.code.toUpperCase()} />}
|
{(params) => <RoomPage code={params.code.toUpperCase()} />}
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
||||||
45
apps/web/src/components/pseudo-screen.tsx
Normal file
45
apps/web/src/components/pseudo-screen.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { useState } from "react"
|
||||||
|
import { Button } from "@workspace/ui/components/button"
|
||||||
|
import { useRoomStore } from "@/store/room"
|
||||||
|
import { Avatar } from "@/components/avatar"
|
||||||
|
|
||||||
|
const inputClass =
|
||||||
|
"border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-center text-sm focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none"
|
||||||
|
|
||||||
|
/** Choix du pseudo dans la room, avec aperçu de l'avatar en temps réel. */
|
||||||
|
export function PseudoScreen({ code }: { code: string }) {
|
||||||
|
const setName = useRoomStore((s) => s.setName)
|
||||||
|
const [name, setName_] = useState("")
|
||||||
|
const canSubmit = name.trim().length > 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-svh items-center justify-center p-6">
|
||||||
|
<div className="flex w-full max-w-xs flex-col items-center gap-5">
|
||||||
|
<p className="text-muted-foreground text-xs uppercase">
|
||||||
|
Room {code}
|
||||||
|
</p>
|
||||||
|
<Avatar
|
||||||
|
seed={name.trim() || "?"}
|
||||||
|
className="size-28 ring-2 ring-primary/30"
|
||||||
|
/>
|
||||||
|
<p className="font-heading text-lg font-bold">Choisis ton pseudo</p>
|
||||||
|
<input
|
||||||
|
className={inputClass}
|
||||||
|
placeholder="Ton pseudo"
|
||||||
|
value={name}
|
||||||
|
maxLength={24}
|
||||||
|
autoFocus
|
||||||
|
onChange={(e) => setName_(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && canSubmit && setName(name)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
disabled={!canSubmit}
|
||||||
|
onClick={() => setName(name)}
|
||||||
|
>
|
||||||
|
Entrer dans la room
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,39 +1,56 @@
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Check, Copy } from "lucide-react"
|
import { Check, Copy, Link2 } from "lucide-react"
|
||||||
|
import { Button } from "@workspace/ui/components/button"
|
||||||
|
|
||||||
/** Code de room cliquable : copie dans le presse-papier avec retour visuel. */
|
type Copied = "code" | "link" | null
|
||||||
|
|
||||||
|
/** Code de room copiable + bouton pour copier le lien d'invitation. */
|
||||||
export function RoomCode({ code }: { code: string }) {
|
export function RoomCode({ code }: { code: string }) {
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState<Copied>(null)
|
||||||
|
|
||||||
async function copy() {
|
async function copy(what: Copied, text: string) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(code)
|
await navigator.clipboard.writeText(text)
|
||||||
setCopied(true)
|
setCopied(what)
|
||||||
setTimeout(() => setCopied(false), 1500)
|
setTimeout(() => setCopied(null), 1500)
|
||||||
} catch {
|
} catch {
|
||||||
// presse-papier indisponible (http non sécurisé) : on ignore silencieusement
|
// presse-papier indisponible (http non sécurisé) : on ignore silencieusement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const link = `${window.location.origin}/join/${code}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={copy}
|
<button
|
||||||
title="Copier le code"
|
onClick={() => copy("code", code)}
|
||||||
className="group flex items-center gap-2 text-left"
|
title="Copier le code"
|
||||||
>
|
className="group flex items-center gap-2 text-left"
|
||||||
<span>
|
>
|
||||||
<span className="text-muted-foreground text-xs uppercase">
|
<span>
|
||||||
Code room
|
<span className="text-muted-foreground text-xs uppercase">
|
||||||
|
Code room
|
||||||
|
</span>
|
||||||
|
<span className="font-heading block text-2xl font-bold tracking-widest">
|
||||||
|
{code}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="font-heading block text-2xl font-bold tracking-widest">
|
{copied === "code" ? (
|
||||||
{code}
|
<Check className="size-4 text-green-500" />
|
||||||
</span>
|
) : (
|
||||||
</span>
|
<Copy className="text-muted-foreground group-hover:text-foreground size-4 transition-colors" />
|
||||||
{copied ? (
|
)}
|
||||||
<Check className="size-4 text-green-500" />
|
</button>
|
||||||
) : (
|
|
||||||
<Copy className="text-muted-foreground group-hover:text-foreground size-4 transition-colors" />
|
<Button
|
||||||
)}
|
size="sm"
|
||||||
</button>
|
variant="secondary"
|
||||||
|
onClick={() => copy("link", link)}
|
||||||
|
title="Copier le lien d'invitation"
|
||||||
|
>
|
||||||
|
{copied === "link" ? <Check className="text-green-500" /> : <Link2 />}
|
||||||
|
{copied === "link" ? "Copié" : "Lien"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,15 @@ export function HomePage() {
|
||||||
const joinRoom = useRoomStore((s) => s.joinRoom)
|
const joinRoom = useRoomStore((s) => s.joinRoom)
|
||||||
const connected = useRoomStore((s) => s.connected)
|
const connected = useRoomStore((s) => s.connected)
|
||||||
|
|
||||||
const [name, setName] = useState("")
|
|
||||||
const [code, setCode] = useState("")
|
const [code, setCode] = useState("")
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
|
|
||||||
const canSubmit = name.trim().length > 0 && connected && !busy
|
|
||||||
|
|
||||||
async function handleCreate() {
|
async function handleCreate() {
|
||||||
setError(null)
|
setError(null)
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
try {
|
try {
|
||||||
const { roomCode } = await createRoom(name.trim())
|
const { roomCode } = await createRoom()
|
||||||
navigate(`/room/${roomCode}`)
|
navigate(`/room/${roomCode}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError((err as { message?: string }).message ?? "Erreur")
|
setError((err as { message?: string }).message ?? "Erreur")
|
||||||
|
|
@ -36,7 +33,7 @@ export function HomePage() {
|
||||||
setError(null)
|
setError(null)
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
try {
|
try {
|
||||||
const { roomCode } = await joinRoom(code.trim().toUpperCase(), name.trim())
|
const { roomCode } = await joinRoom(code.trim().toUpperCase())
|
||||||
navigate(`/room/${roomCode}`)
|
navigate(`/room/${roomCode}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError((err as { message?: string }).message ?? "Erreur")
|
setError((err as { message?: string }).message ?? "Erreur")
|
||||||
|
|
@ -57,15 +54,7 @@ export function HomePage() {
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<input
|
<Button disabled={!connected || busy} onClick={handleCreate}>
|
||||||
className={inputClass}
|
|
||||||
placeholder="Ton pseudo"
|
|
||||||
value={name}
|
|
||||||
maxLength={24}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button disabled={!canSubmit} onClick={handleCreate}>
|
|
||||||
Créer une room
|
Créer une room
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|
@ -82,10 +71,11 @@ export function HomePage() {
|
||||||
value={code}
|
value={code}
|
||||||
maxLength={4}
|
maxLength={4}
|
||||||
onChange={(e) => setCode(e.target.value)}
|
onChange={(e) => setCode(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && code.trim() && handleJoin()}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={!canSubmit || code.trim().length === 0}
|
disabled={!connected || busy || code.trim().length === 0}
|
||||||
onClick={handleJoin}
|
onClick={handleJoin}
|
||||||
>
|
>
|
||||||
Rejoindre
|
Rejoindre
|
||||||
|
|
|
||||||
42
apps/web/src/pages/join.tsx
Normal file
42
apps/web/src/pages/join.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
import { Link, useLocation } from "wouter"
|
||||||
|
import { Button } from "@workspace/ui/components/button"
|
||||||
|
import { useRoomStore } from "@/store/room"
|
||||||
|
|
||||||
|
/** Lien d'invitation /join/:code → rejoint la room puis redirige vers le lobby. */
|
||||||
|
export function JoinPage({ code }: { code: string }) {
|
||||||
|
const [, navigate] = useLocation()
|
||||||
|
const joinRoom = useRoomStore((s) => s.joinRoom)
|
||||||
|
const connected = useRoomStore((s) => s.connected)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const tried = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!connected || tried.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tried.current = true
|
||||||
|
joinRoom(code)
|
||||||
|
.then(({ roomCode }) => navigate(`/room/${roomCode}`, { replace: true }))
|
||||||
|
.catch((err) =>
|
||||||
|
setError((err as { message?: string }).message ?? "Room introuvable.")
|
||||||
|
)
|
||||||
|
}, [connected, code, joinRoom, navigate])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-svh flex-col items-center justify-center gap-4 p-6 text-center">
|
||||||
|
{error ? (
|
||||||
|
<>
|
||||||
|
<p className="text-muted-foreground text-sm">{error}</p>
|
||||||
|
<Link href="/">
|
||||||
|
<Button variant="secondary">Retour à l'accueil</Button>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Connexion à la room {code}…
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -9,19 +9,22 @@ import { PlayerCards } from "@/components/player-cards"
|
||||||
import { RoomCode } from "@/components/room-code"
|
import { RoomCode } from "@/components/room-code"
|
||||||
import { Boom } from "@/components/boom"
|
import { Boom } from "@/components/boom"
|
||||||
import { RoundTransition } from "@/components/round-transition"
|
import { RoundTransition } from "@/components/round-transition"
|
||||||
|
import { PseudoScreen } from "@/components/pseudo-screen"
|
||||||
|
|
||||||
export function RoomPage({ code }: { code: string }) {
|
export function RoomPage({ code }: { code: string }) {
|
||||||
const snapshot = useRoomStore((s) => s.snapshot)
|
const snapshot = useRoomStore((s) => s.snapshot)
|
||||||
|
const roomCode = useRoomStore((s) => s.roomCode)
|
||||||
const playerId = useRoomStore((s) => s.playerId)
|
const playerId = useRoomStore((s) => s.playerId)
|
||||||
const connected = useRoomStore((s) => s.connected)
|
const connected = useRoomStore((s) => s.connected)
|
||||||
|
const pseudoSet = useRoomStore((s) => s.pseudoSet)
|
||||||
const boomKey = useRoomStore((s) => s.boomKey)
|
const boomKey = useRoomStore((s) => s.boomKey)
|
||||||
const roundKey = useRoomStore((s) => s.roundKey)
|
const roundKey = useRoomStore((s) => s.roundKey)
|
||||||
const voteProgress = useRoomStore((s) => s.voteProgress)
|
const voteProgress = useRoomStore((s) => s.voteProgress)
|
||||||
const round = useRoomStore((s) => s.round)
|
const round = useRoomStore((s) => s.round)
|
||||||
const roundModeChanged = useRoomStore((s) => s.roundModeChanged)
|
const roundModeChanged = useRoomStore((s) => s.roundModeChanged)
|
||||||
|
|
||||||
// Pas de snapshot pour ce code (accès direct / refresh) : retour à l'accueil.
|
// Pas dans cette room (accès direct sans rejoindre / refresh) : retour accueil.
|
||||||
if (!snapshot || snapshot.code !== code) {
|
if (roomCode !== code) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-svh flex-col items-center justify-center gap-4 p-6 text-center">
|
<div className="flex min-h-svh flex-col items-center justify-center gap-4 p-6 text-center">
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
|
|
@ -34,6 +37,20 @@ export function RoomPage({ code }: { code: string }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pseudo pas encore choisi : on l'affiche d'abord.
|
||||||
|
if (!pseudoSet) {
|
||||||
|
return <PseudoScreen code={code} />
|
||||||
|
}
|
||||||
|
|
||||||
|
// En attente du premier snapshot.
|
||||||
|
if (!snapshot || snapshot.code !== code) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-svh items-center justify-center p-6">
|
||||||
|
<p className="text-muted-foreground text-sm">Chargement…</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Cards de scores visibles en permanence pendant le jeu (suivi continu).
|
// Cards de scores visibles en permanence pendant le jeu (suivi continu).
|
||||||
const inGame =
|
const inGame =
|
||||||
snapshot.status === "in_round" ||
|
snapshot.status === "in_round" ||
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ interface RoomState {
|
||||||
connected: boolean
|
connected: boolean
|
||||||
playerId: string | null
|
playerId: string | null
|
||||||
roomCode: string | null
|
roomCode: string | null
|
||||||
|
/** Le joueur local a-t-il choisi son pseudo dans la room ? */
|
||||||
|
pseudoSet: boolean
|
||||||
snapshot: RoomSnapshot | null
|
snapshot: RoomSnapshot | null
|
||||||
lastError: ErrorPayload | null
|
lastError: ErrorPayload | null
|
||||||
|
|
||||||
|
|
@ -53,8 +55,9 @@ interface RoomState {
|
||||||
/** Le type d'épreuve a-t-il changé par rapport à la manche précédente ? */
|
/** Le type d'épreuve a-t-il changé par rapport à la manche précédente ? */
|
||||||
roundModeChanged: boolean
|
roundModeChanged: boolean
|
||||||
|
|
||||||
createRoom: (playerName: string) => Promise<RoomCreatedPayload>
|
createRoom: () => Promise<RoomCreatedPayload>
|
||||||
joinRoom: (roomCode: string, playerName: string) => Promise<RoomCreatedPayload>
|
joinRoom: (roomCode: string) => Promise<RoomCreatedPayload>
|
||||||
|
setName: (name: string) => void
|
||||||
updateSettings: (partial: Partial<UpdateSettingsPayload>) => void
|
updateSettings: (partial: Partial<UpdateSettingsPayload>) => void
|
||||||
startGame: (rounds: RoundConfig[]) => Promise<void>
|
startGame: (rounds: RoundConfig[]) => Promise<void>
|
||||||
submitTrack: (youtubeUrl: string) => Promise<SubmitOkPayload>
|
submitTrack: (youtubeUrl: string) => Promise<SubmitOkPayload>
|
||||||
|
|
@ -73,6 +76,7 @@ export const useRoomStore = create<RoomState>((set, get) => ({
|
||||||
connected: socket.connected,
|
connected: socket.connected,
|
||||||
playerId: null,
|
playerId: null,
|
||||||
roomCode: null,
|
roomCode: null,
|
||||||
|
pseudoSet: false,
|
||||||
snapshot: null,
|
snapshot: null,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
round: null,
|
round: null,
|
||||||
|
|
@ -86,11 +90,15 @@ export const useRoomStore = create<RoomState>((set, get) => ({
|
||||||
roundKey: 0,
|
roundKey: 0,
|
||||||
roundModeChanged: false,
|
roundModeChanged: false,
|
||||||
|
|
||||||
createRoom: (playerName) =>
|
createRoom: () =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
socket.emit("room:create", { playerName }, (res) => {
|
socket.emit("room:create", {}, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
set({ playerId: res.data.playerId, roomCode: res.data.roomCode })
|
set({
|
||||||
|
playerId: res.data.playerId,
|
||||||
|
roomCode: res.data.roomCode,
|
||||||
|
pseudoSet: false,
|
||||||
|
})
|
||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
} else {
|
} else {
|
||||||
set({ lastError: res.error })
|
set({ lastError: res.error })
|
||||||
|
|
@ -99,11 +107,15 @@ export const useRoomStore = create<RoomState>((set, get) => ({
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
joinRoom: (roomCode, playerName) =>
|
joinRoom: (roomCode) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
socket.emit("room:join", { roomCode, playerName }, (res) => {
|
socket.emit("room:join", { roomCode }, (res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
set({ playerId: res.data.playerId, roomCode: res.data.roomCode })
|
set({
|
||||||
|
playerId: res.data.playerId,
|
||||||
|
roomCode: res.data.roomCode,
|
||||||
|
pseudoSet: false,
|
||||||
|
})
|
||||||
resolve(res.data)
|
resolve(res.data)
|
||||||
} else {
|
} else {
|
||||||
set({ lastError: res.error })
|
set({ lastError: res.error })
|
||||||
|
|
@ -112,6 +124,14 @@ export const useRoomStore = create<RoomState>((set, get) => ({
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setName: (name) => {
|
||||||
|
if (name.trim().length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
socket.emit("player:setName", { name: name.trim() })
|
||||||
|
set({ pseudoSet: true })
|
||||||
|
},
|
||||||
|
|
||||||
updateSettings: (partial) => {
|
updateSettings: (partial) => {
|
||||||
const current = get().snapshot?.settings ?? DEFAULT_ROOM_SETTINGS
|
const current = get().snapshot?.settings ?? DEFAULT_ROOM_SETTINGS
|
||||||
socket.emit("lobby:updateSettings", {
|
socket.emit("lobby:updateSettings", {
|
||||||
|
|
@ -184,6 +204,7 @@ export const useRoomStore = create<RoomState>((set, get) => ({
|
||||||
reset: () =>
|
reset: () =>
|
||||||
set({
|
set({
|
||||||
roomCode: null,
|
roomCode: null,
|
||||||
|
pseudoSet: false,
|
||||||
snapshot: null,
|
snapshot: null,
|
||||||
lastError: null,
|
lastError: null,
|
||||||
round: null,
|
round: null,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import type {
|
||||||
// --- Payloads ------------------------------------------------------------
|
// --- Payloads ------------------------------------------------------------
|
||||||
|
|
||||||
export interface RoomCreatePayload {
|
export interface RoomCreatePayload {
|
||||||
playerName: string
|
playerName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomCreatedPayload {
|
export interface RoomCreatedPayload {
|
||||||
|
|
@ -24,7 +24,11 @@ export interface RoomCreatedPayload {
|
||||||
|
|
||||||
export interface RoomJoinPayload {
|
export interface RoomJoinPayload {
|
||||||
roomCode: string
|
roomCode: string
|
||||||
playerName: string
|
playerName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetNamePayload {
|
||||||
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateSettingsPayload {
|
export interface UpdateSettingsPayload {
|
||||||
|
|
@ -126,6 +130,7 @@ export type Ack<T> = (result: { ok: true; data: T } | { ok: false; error: ErrorP
|
||||||
export interface ClientToServerEvents {
|
export interface ClientToServerEvents {
|
||||||
"room:create": (payload: RoomCreatePayload, ack: Ack<RoomCreatedPayload>) => void
|
"room:create": (payload: RoomCreatePayload, ack: Ack<RoomCreatedPayload>) => void
|
||||||
"room:join": (payload: RoomJoinPayload, ack: Ack<RoomCreatedPayload>) => void
|
"room:join": (payload: RoomJoinPayload, ack: Ack<RoomCreatedPayload>) => void
|
||||||
|
"player:setName": (payload: SetNamePayload) => void
|
||||||
"lobby:updateSettings": (payload: UpdateSettingsPayload) => void
|
"lobby:updateSettings": (payload: UpdateSettingsPayload) => void
|
||||||
"lobby:return": () => void
|
"lobby:return": () => void
|
||||||
"game:start": (ack: Ack<null>) => void
|
"game:start": (ack: Ack<null>) => void
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue