@@ -6,6 +6,9 @@ export type QuestionType = 'multiple_choice' | 'true_false' | 'open_ended'
66export type Difficulty = 'easy' | 'medium' | 'hard' | string
77export type GameStatus = 'waiting' | 'active' | 'paused' | 'ended'
88export type TransportMode = 'auto' | 'peer' | 'gun'
9+ export type GmDecision = 'Correct' | 'Incorrect' | 'Skip'
10+ export type BuzzDeduplication = 'firstOnly' | 'all'
11+ export type TiebreakerMode = 'serverOrder'
912export type WidgetType =
1013 | 'buzzer'
1114 | 'question'
@@ -68,17 +71,27 @@ export interface Game {
6871 transportMode : TransportMode
6972 roomId : string | null
7073 passphrase : string | null // Gun.js SEA encryption
71- scoringEnabled : boolean
74+ // Visibility
7275 showQuestion : boolean
7376 showAnswers : boolean
7477 showMedia : boolean
78+ // Players / teams
7579 maxTeams : number // 0 = unlimited
7680 maxPerTeam : number // 0 = unlimited
7781 allowIndividual : boolean
82+ // Rounds / navigation
7883 roundIds : string [ ]
7984 currentRoundIdx : number
8085 currentQuestionIdx : number
86+ // Buzzer state
8187 buzzerLocked : boolean
88+ // Buzzer configuration
89+ scoringEnabled : boolean
90+ autoLockOnFirstCorrect : boolean
91+ allowFalseStarts : boolean
92+ buzzDeduplication : BuzzDeduplication
93+ tiebreakerMode : TiebreakerMode
94+ // Timestamps
8295 createdAt : number
8396 updatedAt : number
8497}
@@ -105,10 +118,16 @@ export interface Player {
105118export interface BuzzEvent {
106119 id : string
107120 gameId : string
108- playerId : string
109121 questionId : string
110- timestamp : number // microsecond precision
111- correct : boolean | null
122+ playerId : string
123+ playerName : string
124+ teamId : string | null
125+ /** microsecond precision via performance.now() offset from Date.now() */
126+ timestamp : number
127+ /** true when buzz arrived before buzzerLocked was cleared */
128+ isFalseStart : boolean
129+ gmDecision : GmDecision | null
130+ decidedAt : number | null
112131}
113132
114133export interface Layout {
@@ -222,6 +241,45 @@ class ViktoraniDB extends Dexie {
222241 delete q [ 'categoryId' ]
223242 } )
224243 } )
244+
245+ // v4: full BuzzEvent schema + buzzer config back-fill on Game records
246+ this . version ( 4 )
247+ . stores ( {
248+ difficulties : 'id, name, order' ,
249+ tags : 'id, name' ,
250+ questions : 'id, difficulty, type, createdAt' ,
251+ rounds : 'id, createdAt' ,
252+ games : 'id, status, createdAt' ,
253+ teams : 'id, gameId' ,
254+ players : 'id, gameId, teamId, deviceId' ,
255+ buzzEvents : 'id, gameId, playerId, questionId, timestamp' ,
256+ layouts : 'id, gameId, target' ,
257+ widgets : 'id, layoutId, order' ,
258+ notes : 'id, name, createdAt, updatedAt' ,
259+ timers : 'id, gameId' ,
260+ gameQuestions : 'id, gameId, roundId, order' ,
261+ } )
262+ . upgrade ( async tx => {
263+ await tx
264+ . table ( 'games' )
265+ . toCollection ( )
266+ . modify ( ( g : Record < string , unknown > ) => {
267+ if ( g [ 'autoLockOnFirstCorrect' ] === undefined ) g [ 'autoLockOnFirstCorrect' ] = false
268+ if ( g [ 'allowFalseStarts' ] === undefined ) g [ 'allowFalseStarts' ] = false
269+ if ( g [ 'buzzDeduplication' ] === undefined ) g [ 'buzzDeduplication' ] = 'firstOnly'
270+ if ( g [ 'tiebreakerMode' ] === undefined ) g [ 'tiebreakerMode' ] = 'serverOrder'
271+ } )
272+ await tx
273+ . table ( 'buzzEvents' )
274+ . toCollection ( )
275+ . modify ( ( b : Record < string , unknown > ) => {
276+ if ( b [ 'playerName' ] === undefined ) b [ 'playerName' ] = 'Unknown'
277+ if ( b [ 'teamId' ] === undefined ) b [ 'teamId' ] = null
278+ if ( b [ 'isFalseStart' ] === undefined ) b [ 'isFalseStart' ] = false
279+ if ( b [ 'gmDecision' ] === undefined ) b [ 'gmDecision' ] = null
280+ if ( b [ 'decidedAt' ] === undefined ) b [ 'decidedAt' ] = null
281+ } )
282+ } )
225283 }
226284}
227285
0 commit comments