@@ -10,7 +10,11 @@ import {
1010} from "./quiz-players-registry" ;
1111import { applySeatNickRosterToQuizRelay , resetQuizRoomForSession } from "./lib/adepts-quiz-room-store" ;
1212import { broadcastAdeptsQuizSync } from "./adepts" ;
13- import { seedPandoraFromQuiz } from "./game" ;
13+ import {
14+ applyPandoraLottoWinnerReplace ,
15+ peekPandoraEliminatedSeatIndex ,
16+ seedPandoraFromQuiz ,
17+ } from "./game" ;
1418
1519function queryAdeptsRoleLower ( socket : Socket ) : string {
1620 const q = socket . handshake . query [ "adeptsRole" ] ;
@@ -297,11 +301,16 @@ export function setupQuizNav(io: Server) {
297301 const n = getNav ( sessionId ) ;
298302 n . pandoraLottoActive = true ;
299303 n . pandoraLottoPublic = { ...DEFAULT_PANDORA_LOTTO_PUBLIC } ;
300- ns . to ( sessionId ) . emit ( "pandoraLottoOpened" , { } ) ;
301- socket . emit ( "pandoraLottoOpened" , { } ) ;
304+ const boardIndex =
305+ n . lastBoardIndex !== null && n . lastBoardIndex >= 0 && n . lastBoardIndex <= MAX_BOARD
306+ ? n . lastBoardIndex
307+ : 0 ;
308+ const openPayload = { boardIndex, broadcastSession : true as const } ;
309+ ns . to ( sessionId ) . emit ( "pandoraLottoOpened" , openPayload ) ;
310+ socket . emit ( "pandoraLottoOpened" , openPayload ) ;
302311 ns . to ( sessionId ) . emit ( "pandoraLottoPublicState" , n . pandoraLottoPublic ) ;
303312 socket . emit ( "pandoraLottoPublicState" , n . pandoraLottoPublic ) ;
304- logger . info ( { sessionId } , "Quiz Pandora lotto opened" ) ;
313+ logger . info ( { sessionId, boardIndex } , "Quiz Pandora lotto opened" ) ;
305314 } ) ;
306315
307316 socket . on ( "hostPandoraLottoPublicSync" , ( payload : unknown ) => {
@@ -314,6 +323,68 @@ export function setupQuizNav(io: Server) {
314323 socket . emit ( "pandoraLottoPublicState" , next ) ;
315324 } ) ;
316325
326+ socket . on ( "hostPandoraLottoConfirmReplace" , ( payload : unknown ) => {
327+ if ( ! isQuizNavLobbyHost ( socket ) ) return ;
328+ const n = getNav ( sessionId ) ;
329+ if ( ! n . pandoraLottoActive ) return ;
330+
331+ const po = payload && typeof payload === "object" ? ( payload as Record < string , unknown > ) : { } ;
332+ const winnerNick =
333+ typeof po [ "winnerNick" ] === "string" ? po [ "winnerNick" ] . trim ( ) . slice ( 0 , 64 ) : "" ;
334+ if ( ! winnerNick ) return ;
335+
336+ const elimPeek = peekPandoraEliminatedSeatIndex ( sessionId ) ;
337+ if ( elimPeek === null ) {
338+ logger . warn ( { sessionId } , "hostPandoraLottoConfirmReplace no eliminated seat" ) ;
339+ return ;
340+ }
341+
342+ const row = [ ...n . seatPlayerNicks ] ;
343+ while ( row . length < 5 ) row . push ( "" ) ;
344+ const normalized = row . map ( ( x ) => String ( x ?? "" ) . trim ( ) . slice ( 0 , 64 ) ) ;
345+ while ( normalized . length < 5 ) normalized . push ( "" ) ;
346+ const wKey = winnerNick . trim ( ) . toLowerCase ( ) ;
347+ for ( let i = 0 ; i < 5 ; i += 1 ) {
348+ if ( i === elimPeek ) continue ;
349+ const nk = ( normalized [ i ] ?? "" ) . trim ( ) . toLowerCase ( ) ;
350+ if ( nk . length > 0 && nk === wKey ) {
351+ logger . warn ( { sessionId, winnerNick } , "hostPandoraLottoConfirmReplace duplicate nick on roster" ) ;
352+ return ;
353+ }
354+ }
355+
356+ const result = applyPandoraLottoWinnerReplace ( io , sessionId , winnerNick ) ;
357+ if ( ! result . ok ) {
358+ logger . warn (
359+ { sessionId, reason : result . reason } ,
360+ "hostPandoraLottoConfirmReplace rejected" ,
361+ ) ;
362+ return ;
363+ }
364+
365+ const elimIdx = result . eliminatedSeatIndex ;
366+ normalized [ elimIdx ] = winnerNick . trim ( ) . slice ( 0 , 64 ) ;
367+ n . seatPlayerNicks = normalized . slice ( 0 , 5 ) ;
368+ n . pandoraRoulettePlayerNames = [ ...n . seatPlayerNicks ] ;
369+
370+ applySeatNickRosterToQuizRelay ( sessionId , n . seatPlayerNicks ) ;
371+ broadcastAdeptsQuizSync ( io , sessionId , socket ) ;
372+
373+ const out = lobbyPayload ( sessionId ) ;
374+ ns . to ( sessionId ) . emit ( "lobbyState" , out ) ;
375+ socket . emit ( "lobbyState" , out ) ;
376+
377+ n . pandoraLottoActive = false ;
378+ n . pandoraLottoPublic = null ;
379+
380+ const boardIndex =
381+ n . lastBoardIndex !== null && n . lastBoardIndex >= 0 && n . lastBoardIndex <= MAX_BOARD
382+ ? n . lastBoardIndex
383+ : 0 ;
384+ ns . to ( sessionId ) . emit ( "pandoraLottoReturn" , { toQuizBoard : true , boardIndex } ) ;
385+ logger . info ( { sessionId, elimIdx, winnerNick } , "Quiz Pandora lotto confirm replace" ) ;
386+ } ) ;
387+
317388 socket . on ( "hostPandoraLottoClose" , ( ) => {
318389 if ( ! isQuizNavLobbyHost ( socket ) ) return ;
319390 const n = getNav ( sessionId ) ;
@@ -341,7 +412,7 @@ export function setupQuizNav(io: Server) {
341412 socket . on ( "requestPandoraLottoState" , ( ) => {
342413 const nav = getNav ( sessionId ) ;
343414 if ( nav . pandoraLottoActive ) {
344- socket . emit ( " pandoraLottoOpened" , { } ) ;
415+ /** Не шлём ` pandoraLottoOpened` — иначе снова сработает редирект в `QuizPandoraLottoSync`. */
345416 const pub = nav . pandoraLottoPublic ?? { ...DEFAULT_PANDORA_LOTTO_PUBLIC } ;
346417 nav . pandoraLottoPublic = pub ;
347418 socket . emit ( "pandoraLottoPublicState" , pub ) ;
@@ -376,11 +447,15 @@ export function setupQuizNav(io: Server) {
376447
377448 socket . on ( "requestPandoraRouletteState" , ( ) => {
378449 const n = getNav ( sessionId ) ;
450+ /** Пока открыт «Барабан Лото», не реплеить рулетку — иначе клиенты на доске получают `pandoraRouletteOpened` и улетают на `/game`/`/spectate`. */
451+ if ( n . pandoraLottoActive ) return ;
379452 if ( n . pandoraRouletteActive && n . pandoraRouletteReturnHref ) {
453+ /** Не путать с вещанием от `hostPandoraRouletteOpen`: клиент на доске не должен уезжать на рулетку при реплее. */
380454 socket . emit ( "pandoraRouletteOpened" , {
381455 returnHref : n . pandoraRouletteReturnHref ,
382456 currentTurnSeat : n . pandoraRouletteCurrentTurnSeat ,
383457 playerNames : [ ...n . pandoraRoulettePlayerNames ] ,
458+ stateReplay : true ,
384459 } ) ;
385460 }
386461 } ) ;
@@ -433,6 +508,15 @@ export function setupQuizNav(io: Server) {
433508 return ;
434509 }
435510 n . lastBoardIndex = boardIndex ;
511+ /**
512+ * После возврата с рулетки на доску флаг часто остаётся true; смена доски ведущим = работа с квизом,
513+ * реплей `requestPandoraRouletteState` не должен снова открывать рулетку.
514+ */
515+ if ( n . pandoraRouletteActive ) {
516+ n . pandoraRouletteActive = false ;
517+ n . pandoraRouletteReturnHref = null ;
518+ n . pandoraRoulettePlayerNames = [ ] ;
519+ }
436520 socket . to ( sessionId ) . emit ( "phase" , { boardIndex } ) ;
437521 /** Echo: `socket.to` excludes sender; host must receive `phase` for `QuizNavSync` (no client-side `assign` in `GamePhaseNav`). */
438522 socket . emit ( "phase" , { boardIndex } ) ;
0 commit comments