Compare commits

..

No commits in common. "7290f02dad57e4bb5fc86d01155b49adbb25e1e4" and "4154509fdd4596f6d272835ffddcbf07e27d6c04" have entirely different histories.

16 changed files with 47 additions and 411 deletions

View file

@ -1,13 +0,0 @@
// Réglage de difficulté des bots : probabilité de donner la bonne réponse.
import type { BotDifficulty } from "@nerdware/shared"
const CHANCE: Record<BotDifficulty, number> = {
easy: 0.3,
normal: 0.55,
hard: 0.85,
}
export function botCorrectChance(difficulty: BotDifficulty): number {
return CHANCE[difficulty] ?? CHANCE.normal
}

View file

@ -47,7 +47,6 @@ const dummyRound: GameRound = {
return deltas
},
recap: () => ({ label: "2+2 ?", answer: "4", answers: [] }),
botAnswer: () => ({ choiceIndex: 0 }),
}
function setup(roundDurationSec: number): {

View file

@ -195,8 +195,6 @@ export class GameEngine implements RoomGameController {
this.io.to(this.room.code).emit("round:start", base)
}
this.scheduleBotVotes()
// Attend la première condition de fin : timer écoulé OU tous votés.
await new Promise<void>((resolve) => {
this.resolveRoundEnd = resolve
@ -224,44 +222,6 @@ export class GameEngine implements RoomGameController {
this.runtime = null
}
/** Programme le vote automatique des bots (jamais DJ ni contributeur). */
private scheduleBotVotes(): void {
if (!this.runtime) {
return
}
const { djId, secretPlayerId, endsAt, startedAt } = this.runtime
const window = Math.max(0, endsAt - startedAt)
const bots = [...this.room.players.values()].filter(
(p) => p.isBot && p.id !== djId && p.id !== secretPlayerId
)
for (const bot of bots) {
// Vote après l'ouverture des réponses, réparti dans la première moitié.
const delay = this.leadMs + 700 + Math.random() * Math.min(window * 0.5, 3500)
setTimeout(() => this.botVote(bot.id), delay)
}
}
/** Exécute le vote d'un bot (no-op si la manche est finie ou s'il a déjà voté). */
private botVote(botId: string): void {
if (!this.runtime || this.room.status !== "in_round") {
return
}
if (this.runtime.votes.has(botId)) {
return
}
const ctx = this.context(this.runtime)
this.runtime.round.submitAnswer(ctx, botId, this.runtime.round.botAnswer(ctx, botId))
const total = this.eligibleVoters().length
this.io.to(this.room.code).emit("round:voteAck", {
count: this.runtime.votes.size,
total,
voted: [...this.runtime.votes.keys()],
})
if (total > 0 && this.runtime.votes.size >= total) {
this.endRound()
}
}
/** Termine la manche en cours (idempotent : timer et "tous votés" peuvent se croiser). */
private endRound(): void {
if (this.timer) {

View file

@ -19,7 +19,6 @@ import type {
} from "../../round"
import type { BlindtestTrack, ServerRoom } from "../../../rooms"
import { fuzzyMatch } from "../../match"
import { botCorrectChance } from "../../bot"
import { takeTrack } from "./pool"
const TITLE_POINTS = 60
@ -32,10 +31,10 @@ interface BlindtestRoundData {
track: BlindtestTrack
}
/** Tire un DJ neutre : un humain connecté qui n'a pas soumis ce titre. */
/** Tire un DJ neutre : un joueur connecté qui n'a pas soumis ce titre. */
function pickDj(room: ServerRoom, submittedBy: string): string | null {
const eligible = [...room.players.values()].filter(
(p) => p.connected && !p.isBot && p.id !== submittedBy
(p) => p.connected && p.id !== submittedBy
)
if (eligible.length === 0) {
return null
@ -99,30 +98,6 @@ export class BlindtestRound implements GameRound {
ctx.votes.set(playerId, answer)
}
botAnswer(ctx: RoundContext, playerId: string): Answer {
const { track } = ctx.data as BlindtestRoundData
const mode = ctx.room.settings.blindtestMode
const chance = botCorrectChance(ctx.room.settings.botDifficulty)
// Selon le niveau : devine le bon contributeur, sinon un joueur au hasard.
const others = [...ctx.room.players.values()]
.filter((p) => p.id !== playerId)
.map((p) => p.id)
const guessedPlayerId =
Math.random() < chance
? track.submittedBy
: (others[Math.floor(Math.random() * others.length)] ?? playerId)
if (mode === "who_added") {
return { guessedPlayerId }
}
const right = Math.random() < chance
const title = right ? track.title : "?"
const artist = right ? track.artist : "?"
if (mode === "title_artist") {
return { title, artist }
}
return { title, artist, guessedPlayerId }
}
/** Résultat par joueur (votants + bonus de tromperie du contributeur). */
private buildResults(ctx: RoundContext): BlindtestPerPlayerResult {
const { track } = ctx.data as BlindtestRoundData

View file

@ -9,7 +9,6 @@ import type {
ScoreDelta,
} from "@nerdware/shared"
import { hasDb } from "../../../db"
import { botCorrectChance } from "../../bot"
import { markQuestionPlayed } from "../../../db/quiz-repo"
import { fuzzyMatch } from "../../match"
import type {
@ -148,22 +147,6 @@ export class QuizRound implements GameRound {
return deltas
}
botAnswer(ctx: RoundContext): Answer {
const { question } = ctx.data as QuizRoundData
// Le bot répond juste selon le niveau choisi, sinon une réponse plausible.
const rightish = Math.random() < botCorrectChance(ctx.room.settings.botDifficulty)
if (isTextFormat(question)) {
return {
text: rightish ? (question.acceptedAnswers?.[0] ?? "?") : "euh…",
}
}
const count = question.choices?.length ?? 1
if (rightish && typeof question.correctIndex === "number") {
return { choiceIndex: question.correctIndex }
}
return { choiceIndex: Math.floor(Math.random() * count) }
}
recap(ctx: RoundContext): RoundRecapInfo {
const { question } = ctx.data as QuizRoundData
const answer = isTextFormat(question)

View file

@ -63,9 +63,6 @@ export interface GameRound {
/** Renvoie les variations de score à appliquer (le moteur les applique). */
score(ctx: RoundContext): ScoreDelta[]
/** Réponse automatique d'un bot (vote plausible, parfois juste). */
botAnswer(ctx: RoundContext, playerId: string): Answer
/** Résumé de la manche pour le récap de fin de partie (intitulé + réponse + média). */
recap(ctx: RoundContext): RoundRecapInfo
}

View file

@ -144,33 +144,11 @@ export function registerRoomHandlers(
roundDuration: payload.roundDuration,
tracksPerPlayer: payload.tracksPerPlayer,
categories: payload.categories,
botDifficulty: payload.botDifficulty,
rounds: payload.rounds,
}
broadcastState(room)
})
socket.on("lobby:addBot", () => {
const code = socket.data.roomCode
const room = code ? rooms.get(code) : undefined
if (!room || room.hostId !== socket.data.playerId || room.status !== "lobby") {
return
}
if (rooms.addBot(room)) {
broadcastState(room)
}
})
socket.on("lobby:removeBot", () => {
const code = socket.data.roomCode
const room = code ? rooms.get(code) : undefined
if (!room || room.hostId !== socket.data.playerId) {
return
}
rooms.removeBot(room)
broadcastState(room)
})
socket.on("game:start", (ack) => {
const code = socket.data.roomCode
const room = code ? rooms.get(code) : undefined
@ -195,17 +173,15 @@ export function registerRoomHandlers(
return
}
const hasBlindtest = room.settings.rounds.some((r) => r.type === "blindtest")
const active = [...room.players.values()].filter(
const connected = [...room.players.values()].filter(
(p) => p.connected && p.name !== ""
)
const connected = active.length
const humans = active.filter((p) => !p.isBot).length
if (hasBlindtest && (connected < 3 || humans < 2)) {
).length
if (hasBlindtest && connected < 3) {
ack({
ok: false,
error: {
code: "NEED_THREE",
message: "Le blindtest nécessite au moins 3 joueurs dont 2 réels.",
message: "Le blindtest nécessite au moins 3 joueurs.",
},
})
return
@ -216,7 +192,6 @@ export function registerRoomHandlers(
(p) =>
p.connected &&
p.name !== "" &&
!p.isBot &&
room.blindtestTracks.filter((t) => t.submittedBy === p.id).length < tpp
)
if (pending) {

View file

@ -18,22 +18,8 @@ export interface ServerPlayer {
socketId: string | null
/** Secret de reconnexion (refresh/coupure). */
reconnectToken: string
/** Joueur virtuel (vote automatiquement, jamais DJ). */
isBot: boolean
}
const BOT_NAMES = [
"Bot BMO",
"Bot R2-D2",
"Bot GLaDOS",
"Bot Clank",
"Bot Wall-E",
"Bot K-2SO",
"Bot HAL",
"Bot Bender",
]
const MAX_PLAYERS = 8
/** Titre soumis pour le blindtest. `submittedBy` reste secret jusqu'au reveal. */
export interface BlindtestTrack {
id: string
@ -108,37 +94,6 @@ export class RoomManager {
return this.rooms.get(code)
}
/** Ajoute un bot (joueur virtuel) à la room. Renvoie false si plein. */
addBot(room: ServerRoom): boolean {
if (room.players.size >= MAX_PLAYERS) {
return false
}
const used = new Set([...room.players.values()].map((p) => p.name))
const name =
BOT_NAMES.find((n) => !used.has(n)) ?? `Bot ${room.players.size + 1}`
const bot: ServerPlayer = {
id: randomUUID(),
name,
connected: true,
socketId: null,
reconnectToken: "",
isBot: true,
}
room.players.set(bot.id, bot)
room.scores.set(bot.id, 0)
return true
}
/** Retire un bot (le dernier ajouté). */
removeBot(room: ServerRoom): void {
const bots = [...room.players.values()].filter((p) => p.isBot)
const last = bots[bots.length - 1]
if (last) {
room.players.delete(last.id)
room.scores.delete(last.id)
}
}
/**
* Retire un joueur d'une room (départ volontaire). Réassigne l'hôte si besoin,
* supprime la room si plus personne. Renvoie la room (ou null si supprimée).
@ -191,7 +146,6 @@ export class RoomManager {
id: p.id,
name: p.name,
connected: p.connected,
bot: p.isBot,
})),
settings: room.settings,
scores: named.map((p) => ({
@ -214,7 +168,6 @@ export class RoomManager {
connected: true,
socketId,
reconnectToken: randomUUID(),
isBot: false,
}
}

View file

@ -178,8 +178,6 @@ export function GameEndView({ snapshot }: { snapshot: RoomSnapshot }) {
const [first, second, third] = ranked
const rest = ranked.slice(3)
const hasTracks = !!gameTracks && gameTracks.length > 0
const hasRecap = !!gameRecap && gameRecap.length > 0
return (
<div className="flex w-full flex-col items-center gap-6">
@ -258,19 +256,15 @@ export function GameEndView({ snapshot }: { snapshot: RoomSnapshot }) {
)}
</div>
{/* Détails : musiques + récap côte à côte sur desktop s'il y a les deux,
sinon une seule colonne centrée (sinon le récap se calait à gauche). */}
{(hasTracks || hasRecap) && (
<div
className={
hasTracks && hasRecap
? "grid w-full gap-6 md:grid-cols-2 md:items-start"
: "flex w-full max-w-xl flex-col gap-6"
}
>
{hasTracks && <TracksRecap tracks={gameTracks!} />}
{hasRecap && (
<RoundsRecap recap={gameRecap!} nameOf={nameOf} playerId={playerId} />
{/* Détails (musiques + récap) côte à côte sur desktop. */}
{((gameTracks && gameTracks.length > 0) ||
(gameRecap && gameRecap.length > 0)) && (
<div className="grid w-full gap-6 md:grid-cols-2 md:items-start">
{gameTracks && gameTracks.length > 0 && (
<TracksRecap tracks={gameTracks} />
)}
{gameRecap && gameRecap.length > 0 && (
<RoundsRecap recap={gameRecap} nameOf={nameOf} playerId={playerId} />
)}
</div>
)}

View file

@ -31,16 +31,9 @@ const MIX_LABELS: Record<MixMode, string> = {
blindtest: "Blindtest",
}
import { Button } from "@workspace/ui/components/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@workspace/ui/components/tooltip"
import { useRoomStore } from "@/store/room"
import { Avatar } from "@/components/avatar"
const BLINDTEST_LOCK_HINT = "3 joueurs dont 2 réels (un bot ne peut pas être DJ)"
const MAX_TRACKS = 10
const GAME_TYPES: {
value: GameType
@ -58,12 +51,6 @@ const MODE_LABELS: Record<BlindtestMode, string> = {
who_added: "Qui l'a ajouté ?",
mixed: "Mixte",
}
const BOT_DIFFICULTIES = ["easy", "normal", "hard"] as const
const BOT_DIFF_LABELS: Record<(typeof BOT_DIFFICULTIES)[number], string> = {
easy: "Facile",
normal: "Normal",
hard: "Difficile",
}
const inputClass =
"border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none disabled:opacity-50"
@ -149,18 +136,9 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
const playerId = useRoomStore((s) => s.playerId)
const updateSettings = useRoomStore((s) => s.updateSettings)
const startGame = useRoomStore((s) => s.startGame)
const addBot = useRoomStore((s) => s.addBot)
const removeBot = useRoomStore((s) => s.removeBot)
const isHost = snapshot.hostId === playerId
const botCount = snapshot.players.filter((p) => p.bot).length
const {
gameType,
mixedModes,
blindtestMode,
tracksPerPlayer,
categories,
botDifficulty,
} = snapshot.settings
const { gameType, mixedModes, blindtestMode, tracksPerPlayer, categories } =
snapshot.settings
const allCategories =
useQuery({ queryKey: ["categories"], queryFn: fetchCategories }).data ?? []
const history =
@ -178,13 +156,9 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
const myCount =
snapshot.submissions.find((s) => s.playerId === playerId)?.count ?? 0
// Le blindtest exige 3 joueurs (DJ + 2 devineurs) DONT au moins 2 réels :
// un bot ne peut pas être DJ, donc il faut un humain DJ + un humain contributeur.
// Le blindtest exige au moins 3 joueurs connectés (DJ + 2 devineurs).
const connectedCount = snapshot.players.filter((p) => p.connected).length
const humanCount = snapshot.players.filter(
(p) => p.connected && !p.bot
).length
const blindtestAvailable = connectedCount >= 3 && humanCount >= 2
const blindtestAvailable = connectedCount >= 3
// Seul le blindtest "seul" retombe sur du quiz à <3 ; le mixte reste mixte
// (son sous-mode blindtest est juste désactivé tant qu'on n'est pas 3).
const effectiveType: GameType =
@ -206,13 +180,11 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
tracks: showBlindtest ? totalTracks : 0,
shuffleOrder: isMixed,
})
// Blindtest : tous les HUMAINS doivent avoir soumis leur quota (les bots non).
const botIds = new Set(snapshot.players.filter((p) => p.bot).map((p) => p.id))
const humanSubs = snapshot.submissions.filter((s) => !botIds.has(s.playerId))
// Blindtest : tout le monde doit avoir soumis son quota de titres.
const allSubmitted =
!showBlindtest ||
(humanSubs.length > 0 &&
humanSubs.every((s) => s.count >= tracksPerPlayer))
(snapshot.submissions.length > 0 &&
snapshot.submissions.every((s) => s.count >= tracksPerPlayer))
const canStart = rounds.length > 0 && allSubmitted
function toggleMix(mode: MixMode) {
@ -269,37 +241,12 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
)}
</span>
<span className="text-muted-foreground text-xs">
{p.bot ? "🤖 Bot" : p.id === snapshot.hostId ? "Hôte" : ""}
{!p.bot && !p.connected && " · hors ligne"}
{p.id === snapshot.hostId ? "Hôte" : ""}
{!p.connected && " · hors ligne"}
</span>
</li>
))}
</ul>
{isHost && (
<div className="flex items-center justify-between gap-2 pt-1">
<span className="text-muted-foreground text-xs">
Bots : {botCount}
</span>
<div className="flex gap-2">
<Button
size="sm"
variant="secondary"
disabled={botCount === 0}
onClick={removeBot}
>
<Minus /> Bot
</Button>
<Button
size="sm"
variant="secondary"
disabled={snapshot.players.length >= 8}
onClick={addBot}
>
<Plus /> Bot
</Button>
</div>
</div>
)}
</section>
<div className="flex flex-col gap-6">
@ -316,15 +263,13 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
{GAME_TYPES.map(({ value, label, Icon, needsThree }) => {
const locked = needsThree && !blindtestAvailable
const active = effectiveType === value
const tile = (
return (
<button
key={value}
aria-disabled={locked}
onClick={() => {
if (locked) return
updateSettings({ gameType: value })
}}
className={`flex flex-col items-center gap-1.5 rounded-lg border p-2.5 text-xs font-medium transition-colors aria-disabled:cursor-not-allowed aria-disabled:opacity-40 ${
disabled={locked}
title={locked ? "3 joueurs minimum" : undefined}
onClick={() => updateSettings({ gameType: value })}
className={`flex flex-col items-center gap-1.5 rounded-lg border p-2.5 text-xs font-medium transition-colors disabled:opacity-40 ${
active
? "border-primary bg-primary/10"
: "border-input hover:bg-muted/60"
@ -334,20 +279,11 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
{label}
</button>
)
return locked ? (
<Tooltip key={value}>
<TooltipTrigger asChild>{tile}</TooltipTrigger>
<TooltipContent>{BLINDTEST_LOCK_HINT}</TooltipContent>
</Tooltip>
) : (
tile
)
})}
</div>
{!blindtestAvailable && (
<p className="text-muted-foreground text-xs">
Le blindtest se débloque à 3 joueurs dont 2 réels (un bot ne peut
pas être DJ).
Le blindtest se débloque à 3 joueurs.
</p>
)}
</div>
@ -360,15 +296,13 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
{(["quiz", "image", "blindtest"] as const).map((m) => {
const locked = m === "blindtest" && !blindtestAvailable
const on = mixedModes.includes(m) && !locked
const tile = (
return (
<button
key={m}
aria-disabled={locked}
onClick={() => {
if (locked) return
toggleMix(m)
}}
className={`flex-1 rounded-lg border px-2 py-1.5 text-xs font-medium transition-colors aria-disabled:cursor-not-allowed aria-disabled:opacity-40 ${
disabled={locked}
title={locked ? "3 joueurs minimum" : undefined}
onClick={() => toggleMix(m)}
className={`flex-1 rounded-lg border px-2 py-1.5 text-xs font-medium transition-colors disabled:opacity-40 ${
on
? "border-primary bg-primary/10"
: "border-input hover:bg-muted/60"
@ -377,14 +311,6 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
{MIX_LABELS[m]}
</button>
)
return locked ? (
<Tooltip key={m}>
<TooltipTrigger asChild>{tile}</TooltipTrigger>
<TooltipContent>{BLINDTEST_LOCK_HINT}</TooltipContent>
</Tooltip>
) : (
tile
)
})}
</div>
</div>
@ -454,26 +380,6 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
</div>
)}
{/* Niveau des bots (si au moins un bot dans la partie) */}
{botCount > 0 && (
<div className="flex flex-col gap-1.5">
<span className="text-sm font-medium">🤖 Niveau des bots</span>
<div className="flex gap-1">
{BOT_DIFFICULTIES.map((d) => (
<Button
key={d}
size="sm"
className="flex-1"
variant={botDifficulty === d ? "default" : "secondary"}
onClick={() => updateSettings({ botDifficulty: d })}
>
{BOT_DIFF_LABELS[d]}
</Button>
))}
</div>
</div>
)}
{/* Réglages blindtest */}
{showBlindtest && (
<div className="flex flex-col gap-3 border-t pt-3">

View file

@ -4,11 +4,6 @@ import { Link } from "wouter"
import { Eye, EyeOff, Trash2, Upload } from "lucide-react"
import type { QuizFormat } from "@nerdware/shared"
import { Button } from "@workspace/ui/components/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@workspace/ui/components/tooltip"
import {
adminApi,
assetUrl,
@ -192,14 +187,14 @@ function QuestionRow({
</p>
)}
</div>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon-sm" variant="secondary" onClick={onToggle}>
<Button
size="icon-sm"
variant="secondary"
onClick={onToggle}
title={q.active ? "Désactiver" : "Activer"}
>
{q.active ? <Eye /> : <EyeOff />}
</Button>
</TooltipTrigger>
<TooltipContent>{q.active ? "Désactiver" : "Activer"}</TooltipContent>
</Tooltip>
<Button
size="icon-sm"
variant="secondary"

View file

@ -2,11 +2,6 @@ import { Link, useLocation } from "wouter"
import { AnimatePresence, motion } from "framer-motion"
import { LogOut } from "lucide-react"
import { Button } from "@workspace/ui/components/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@workspace/ui/components/tooltip"
import { useRoomStore } from "@/store/room"
import { LobbyView } from "@/components/lobby-view"
import { QuizView } from "@/components/quiz-view"
@ -89,16 +84,10 @@ export function RoomPage({ code }: { code: string }) {
<header className="flex items-center justify-between">
<RoomCode code={snapshot.code} />
<div className="flex items-center gap-3">
<Tooltip>
<TooltipTrigger asChild>
<span
className={`size-2.5 rounded-full ${connected ? "bg-green-500" : "bg-red-500"}`}
title={connected ? "Connecté" : "Déconnecté"}
/>
</TooltipTrigger>
<TooltipContent>
{connected ? "Connecté" : "Déconnecté"}
</TooltipContent>
</Tooltip>
<Button
size="sm"
variant="secondary"

View file

@ -78,8 +78,6 @@ interface RoomState {
removeTrack: (trackId: string) => Promise<boolean>
returnToLobby: () => void
leaveRoom: () => void
addBot: () => void
removeBot: () => void
vote: (choiceIndex: number) => void
voteText: (text: string) => void
voteBlindtest: (answer: BlindtestAnswer) => void
@ -164,7 +162,6 @@ export const useRoomStore = create<RoomState>((set, get) => ({
roundDuration: partial.roundDuration ?? current.roundDuration,
tracksPerPlayer: partial.tracksPerPlayer ?? current.tracksPerPlayer,
categories: partial.categories ?? current.categories,
botDifficulty: partial.botDifficulty ?? current.botDifficulty,
rounds: partial.rounds ?? current.rounds,
})
},
@ -201,9 +198,6 @@ export const useRoomStore = create<RoomState>((set, get) => ({
get().reset()
},
addBot: () => socket.emit("lobby:addBot"),
removeBot: () => socket.emit("lobby:removeBot"),
vote: (choiceIndex) => {
if (get().hasVoted) {
return

View file

@ -16,9 +16,6 @@ export type MixMode = "quiz" | "image" | "blindtest"
/** Modes de blindtest, déterminent la forme du vote et le barème. */
export type BlindtestMode = "title_artist" | "who_added" | "mixed"
/** Niveau des bots : pilote leur probabilité de répondre juste. */
export type BotDifficulty = "easy" | "normal" | "hard"
/** Formats de question quiz. */
export type QuizFormat = "mcq" | "truefalse" | "free" | "image_reveal"
@ -27,8 +24,6 @@ export interface Player {
id: string
name: string
connected: boolean
/** Joueur virtuel (bot) géré par le serveur. */
bot?: boolean
}
/** Configuration d'une épreuve planifiée dans la séquence de la partie. */
@ -51,8 +46,6 @@ export interface RoomSettings {
tracksPerPlayer: number
/** Catégories de quiz autorisées (vide = toutes). */
categories: string[]
/** Niveau des bots ajoutés à la partie. */
botDifficulty: BotDifficulty
/** Séquence d'épreuves de la partie. */
rounds: RoundConfig[]
}
@ -65,7 +58,6 @@ export const DEFAULT_ROOM_SETTINGS: RoomSettings = {
roundDuration: 60,
tracksPerPlayer: 2,
categories: [],
botDifficulty: "normal",
rounds: [],
}

View file

@ -4,7 +4,6 @@
import type {
Answer,
BlindtestMode,
BotDifficulty,
GameType,
MixMode,
PlayerScore,
@ -50,7 +49,6 @@ export interface UpdateSettingsPayload {
roundDuration: number
tracksPerPlayer: number
categories: string[]
botDifficulty: BotDifficulty
rounds: RoundConfig[]
}
@ -152,8 +150,6 @@ export interface ClientToServerEvents {
"room:rejoin": (payload: RejoinPayload, ack: Ack<RoomCreatedPayload>) => void
"player:setName": (payload: SetNamePayload) => void
"room:leave": () => void
"lobby:addBot": () => void
"lobby:removeBot": () => void
"lobby:updateSettings": (payload: UpdateSettingsPayload) => void
"lobby:return": () => void
"game:start": (ack: Ack<null>) => void

View file

@ -1,59 +0,0 @@
import * as React from "react"
import { Tooltip as TooltipPrimitive } from "radix-ui"
import { cn } from "@workspace/ui/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }