import { create } from "zustand" import { DEFAULT_ROOM_SETTINGS, type BlindtestAnswer, type BlindtestTrackInfo, type ErrorPayload, type MediaControlPayload, type MediaSyncPayload, type PlayerScore, type RoomCreatedPayload, type RoomSnapshot, type RoundConfig, type RoundStartPayload, type SubmitOkPayload, type UpdateSettingsPayload, type VoteAckPayload, } from "@nerdware/shared" import { socket } from "@/lib/socket" /** Manche en cours côté client (depuis round:start). */ export interface ActiveRound { type: RoundStartPayload["type"] djId?: string /** Blindtest : true si c'est MON titre (je dois faire deviner, pas voter). */ mine?: boolean startsAt: number endsAt: number payload: unknown } /** Reveal reçu (round:reveal), payloads typés par mode au moment de l'affichage. */ export interface RoundReveal { truth: unknown perPlayerResult: unknown } interface RoomState { connected: boolean playerId: string | null roomCode: string | null /** Le joueur local a-t-il choisi son pseudo dans la room ? */ pseudoSet: boolean snapshot: RoomSnapshot | null lastError: ErrorPayload | null // État de jeu (poussé par le moteur serveur). round: ActiveRound | null voteProgress: VoteAckPayload | null reveal: RoundReveal | null myChoiceIndex: number | null hasVoted: boolean finalScores: PlayerScore[] | null gameTracks: BlindtestTrackInfo[] | null mediaSync: MediaSyncPayload | null boomKey: number roundKey: number /** Le type d'épreuve a-t-il changé par rapport à la manche précédente ? */ roundModeChanged: boolean createRoom: () => Promise joinRoom: (roomCode: string) => Promise setName: (name: string) => void updateSettings: (partial: Partial) => void startGame: (rounds: RoundConfig[]) => Promise submitTrack: (youtubeUrl: string) => Promise removeTrack: (trackId: string) => Promise returnToLobby: () => void vote: (choiceIndex: number) => void voteText: (text: string) => void voteBlindtest: (answer: BlindtestAnswer) => void mediaControl: (action: MediaControlPayload["action"], positionSec: number) => void boom: () => void clearError: () => void reset: () => void } export const useRoomStore = create((set, get) => ({ connected: socket.connected, playerId: null, roomCode: null, pseudoSet: false, snapshot: null, lastError: null, round: null, voteProgress: null, reveal: null, myChoiceIndex: null, hasVoted: false, finalScores: null, gameTracks: null, mediaSync: null, boomKey: 0, roundKey: 0, roundModeChanged: false, createRoom: () => new Promise((resolve, reject) => { socket.emit("room:create", {}, (res) => { if (res.ok) { set({ playerId: res.data.playerId, roomCode: res.data.roomCode, pseudoSet: false, }) resolve(res.data) } else { set({ lastError: res.error }) reject(res.error) } }) }), joinRoom: (roomCode) => new Promise((resolve, reject) => { socket.emit("room:join", { roomCode }, (res) => { if (res.ok) { set({ playerId: res.data.playerId, roomCode: res.data.roomCode, pseudoSet: false, }) resolve(res.data) } else { set({ lastError: res.error }) reject(res.error) } }) }), setName: (name) => { if (name.trim().length === 0) { return } socket.emit("player:setName", { name: name.trim() }) set({ pseudoSet: true }) }, updateSettings: (partial) => { const current = get().snapshot?.settings ?? DEFAULT_ROOM_SETTINGS socket.emit("lobby:updateSettings", { gameType: partial.gameType ?? current.gameType, mixedModes: partial.mixedModes ?? current.mixedModes, blindtestMode: partial.blindtestMode ?? current.blindtestMode, roundDuration: partial.roundDuration ?? current.roundDuration, tracksPerPlayer: partial.tracksPerPlayer ?? current.tracksPerPlayer, rounds: partial.rounds ?? current.rounds, }) }, startGame: (rounds) => new Promise((resolve, reject) => { get().updateSettings({ rounds }) socket.emit("game:start", (res) => { if (res.ok) { resolve() } else { set({ lastError: res.error }) reject(res.error) } }) }), submitTrack: (youtubeUrl) => new Promise((resolve) => { socket.emit("blindtest:submitTrack", { youtubeUrl }, (res) => resolve(res)) }), removeTrack: (trackId) => new Promise((resolve) => { socket.emit("blindtest:removeTrack", { trackId }, (res) => resolve(res.ok)) }), returnToLobby: () => { socket.emit("lobby:return") }, vote: (choiceIndex) => { if (get().hasVoted) { return } set({ myChoiceIndex: choiceIndex, hasVoted: true }) socket.emit("round:vote", { answer: { choiceIndex } }) }, voteText: (text) => { if (get().hasVoted || text.trim().length === 0) { return } set({ hasVoted: true }) socket.emit("round:vote", { answer: { text: text.trim() } }) }, voteBlindtest: (answer) => { if (get().hasVoted) { return } set({ hasVoted: true }) socket.emit("round:vote", { answer }) }, mediaControl: (action, positionSec) => { socket.emit("media:control", { action, positionSec }) }, boom: () => set((s) => ({ boomKey: s.boomKey + 1 })), clearError: () => set({ lastError: null }), reset: () => set({ roomCode: null, pseudoSet: false, snapshot: null, lastError: null, round: null, voteProgress: null, reveal: null, myChoiceIndex: null, hasVoted: false, finalScores: null, gameTracks: null, mediaSync: null, boomKey: 0, roundKey: 0, roundModeChanged: false, }), })) // Listeners socket → on pousse l'état serveur dans le store (source de vérité). socket.on("connect", () => useRoomStore.setState({ connected: true })) socket.on("disconnect", () => useRoomStore.setState({ connected: false })) socket.on("room:state", (snapshot) => useRoomStore.setState( snapshot.status === "lobby" ? { snapshot, round: null, reveal: null, voteProgress: null, myChoiceIndex: null, hasVoted: false, finalScores: null, gameTracks: null, mediaSync: null, } : { snapshot } ) ) socket.on("error", (error) => useRoomStore.setState({ lastError: error })) socket.on("round:start", (payload) => useRoomStore.setState((s) => ({ round: { type: payload.type, djId: payload.djId, mine: payload.mine, startsAt: payload.startsAt, endsAt: payload.endsAt, payload: payload.payload, }, voteProgress: null, reveal: null, myChoiceIndex: null, hasVoted: false, mediaSync: null, roundKey: s.roundKey + 1, roundModeChanged: (s.round?.type ?? null) !== payload.type, })) ) socket.on("round:voteAck", (voteProgress) => useRoomStore.setState({ voteProgress }) ) socket.on("round:reveal", (reveal) => useRoomStore.setState({ reveal })) socket.on("media:sync", (mediaSync) => useRoomStore.setState({ mediaSync })) socket.on("game:end", ({ finalScores, tracks }) => useRoomStore.setState({ finalScores, gameTracks: tracks ?? null }) )