Skip to content

Commit 5ea57e3

Browse files
committed
колесо готово но с БАГАМИ
1 parent c5b27f4 commit 5ea57e3

19 files changed

Lines changed: 527 additions & 166 deletions

File tree

artifacts/api-server/src/quiz-nav.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ const CHAT_HISTORY_MAX = 50;
2828
interface ChatEntry { id: string; nick: string; role: "host" | "spectator"; text: string; }
2929
const chatHistory: ChatEntry[] = [];
3030

31+
/** Квиз перешёл на «Колесо адептов» — куда вернуться по кнопке ведущего. */
32+
let adeptsWheelActive = false;
33+
let adeptsWheelReturnHref: string | null = null;
34+
let adeptsWheelCurrentTurnSeat = 0;
35+
3136
function lobbyPayload(): {
3237
gameStarted: boolean;
3338
boardIndex: number;
@@ -100,6 +105,44 @@ export function setupQuizNav(io: Server) {
100105
);
101106
});
102107

108+
socket.on("hostAdeptsWheelOpen", (payload: unknown) => {
109+
const po = payload && typeof payload === "object" ? (payload as Record<string, unknown>) : {};
110+
const hrefRaw = po["returnHref"];
111+
const returnHref =
112+
typeof hrefRaw === "string" && hrefRaw.length > 0 && hrefRaw.length < 2048 ? hrefRaw : null;
113+
const rawSeat = po["currentTurnSeat"];
114+
const seatNum = typeof rawSeat === "number" ? rawSeat : Number(rawSeat);
115+
const currentTurnSeat =
116+
Number.isInteger(seatNum) && seatNum >= 0 && seatNum <= 4 ? seatNum : 0;
117+
if (!returnHref) return;
118+
adeptsWheelActive = true;
119+
adeptsWheelReturnHref = returnHref;
120+
adeptsWheelCurrentTurnSeat = currentTurnSeat;
121+
ns.emit("adeptsWheelOpened", {
122+
returnHref,
123+
currentTurnSeat,
124+
});
125+
logger.info({ returnHref, currentTurnSeat }, "Quiz adepts wheel opened");
126+
});
127+
128+
socket.on("hostAdeptsWheelReturn", () => {
129+
if (!adeptsWheelActive || !adeptsWheelReturnHref) return;
130+
const href = adeptsWheelReturnHref;
131+
adeptsWheelActive = false;
132+
adeptsWheelReturnHref = null;
133+
ns.emit("adeptsWheelReturn", { returnHref: href });
134+
logger.info({ returnHref: href }, "Quiz adepts wheel return");
135+
});
136+
137+
socket.on("requestAdeptsWheelState", () => {
138+
if (adeptsWheelActive && adeptsWheelReturnHref) {
139+
socket.emit("adeptsWheelOpened", {
140+
returnHref: adeptsWheelReturnHref,
141+
currentTurnSeat: adeptsWheelCurrentTurnSeat,
142+
});
143+
}
144+
});
145+
103146
socket.on("hostNavigate", (payload: { boardIndex?: unknown }) => {
104147
if (!gameStarted) return;
105148
const raw = payload?.boardIndex;
@@ -124,6 +167,8 @@ export function setupQuizNav(io: Server) {
124167
lastBoardIndex = null;
125168
seatPlayerNicks = [];
126169
lobbyEmojiLineIndex = -1;
170+
adeptsWheelActive = false;
171+
adeptsWheelReturnHref = null;
127172
clearQuizPlayers();
128173
ns.emit("returnToLogin", {});
129174
ns.emit("lobbyState", lobbyPayload());

artifacts/api-server/src/wheel.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,6 @@ export function setupWheel(io: Server) {
4141
spinDurationMs: SPIN_DURATION_MS,
4242
};
4343

44-
// Track which socket is the host (first non-viewer connection)
45-
let hostSocketId: string | null = null;
46-
4744
wheelNs.on("connection", (socket: Socket) => {
4845
const isViewer = socket.handshake.query.viewer === "1";
4946
logger.info({ socketId: socket.id, isViewer }, "Wheel connection");
@@ -60,12 +57,7 @@ export function setupWheel(io: Server) {
6057
});
6158

6259
if (!isViewer) {
63-
// First non-viewer becomes the host
64-
if (!hostSocketId) hostSocketId = socket.id;
65-
6660
socket.on("wheelSpin", () => {
67-
// Only the designated host can spin
68-
if (socket.id !== hostSocketId) return;
6961
if (state.isSpinning) return;
7062

7163
state.isSpinning = true;
@@ -103,9 +95,12 @@ export function setupWheel(io: Server) {
10395
}, SPIN_DURATION_MS + 200);
10496
});
10597

98+
socket.on("wheelResultDismiss", () => {
99+
wheelNs.emit("wheelResultDismissed", {});
100+
});
101+
106102
socket.on("disconnect", () => {
107-
logger.info({ socketId: socket.id }, "Wheel host disconnected");
108-
if (hostSocketId === socket.id) hostSocketId = null;
103+
logger.info({ socketId: socket.id }, "Wheel spinner disconnected");
109104
});
110105
} else {
111106
socket.on("disconnect", () => {

artifacts/game-client/src/App.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useEffect } from "react";
12
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
23
import { Route, Router, Switch } from "wouter";
34
import { Toaster } from "@/components/ui/toaster";
@@ -7,7 +8,7 @@ import { ViewerPage } from "@/pages/ViewerPage";
78
import { GamePage } from "@/pages/GamePage";
89
import { SpectatorPage } from "@/pages/SpectatorPage";
910
import { AdminGuard } from "@/pages/AdminGuard";
10-
import { AdeptsHostPage } from "@/pages/AdeptsHostPage";
11+
import { QuizAdeptsWheelPage } from "@/pages/QuizAdeptsWheelPage";
1112

1213
import Adepts1Home from "@/apps/adepts-game/pages/Home";
1314
import Adepts1NotFound from "@/apps/adepts-game/pages/not-found";
@@ -17,6 +18,7 @@ import Adepts3Home from "@/apps/adepts-game-3/pages/Home";
1718
import Adepts3NotFound from "@/apps/adepts-game-3/pages/not-found";
1819
import { GamePhaseArrows } from "@/components/GamePhaseArrows";
1920
import { QuizNavSync } from "@/components/QuizNavSync";
21+
import { QuizAdeptsWheelSync } from "@/components/QuizAdeptsWheelSync";
2022
import { QuizReturnToLoginSync } from "@/components/QuizReturnToLoginSync";
2123
import { AdeptsQuizBoardGuard } from "@/components/AdeptsQuizBoardGuard";
2224
import { RequireLogin } from "@/components/RequireLogin";
@@ -26,6 +28,21 @@ const queryClient = new QueryClient();
2628

2729
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
2830

31+
function AdeptsWheelLegacyRedirect() {
32+
useEffect(() => {
33+
window.location.replace(`${base}/adepts/watch`);
34+
}, []);
35+
return null;
36+
}
37+
38+
function AdeptsSpinRoute() {
39+
return <QuizAdeptsWheelPage viewerMode={false} />;
40+
}
41+
42+
function AdeptsWatchRoute() {
43+
return <QuizAdeptsWheelPage viewerMode={true} />;
44+
}
45+
2946
function App() {
3047
return (
3148
<QueryClientProvider client={queryClient}>
@@ -34,7 +51,9 @@ function App() {
3451
<>
3552
<Switch>
3653
<Route path="/admin" component={AdminGuard} />
37-
<Route path="/adepts" component={AdeptsHostPage} />
54+
<Route path="/adepts/spin" component={AdeptsSpinRoute} />
55+
<Route path="/adepts/watch" component={AdeptsWatchRoute} />
56+
<Route path="/adepts" component={AdeptsWheelLegacyRedirect} />
3857
<Route path="/watch" component={ViewerPage} />
3958
<Route path="/spectate" component={SpectatorPage} />
4059
<Route path="/game" component={GamePage} />
@@ -79,6 +98,7 @@ function App() {
7998
</Switch>
8099
<GamePhaseArrows />
81100
<QuizNavSync />
101+
<QuizAdeptsWheelSync />
82102
<QuizReturnToLoginSync />
83103
</>
84104
</Router>

artifacts/game-client/src/apps/adepts-game-2/hooks/useGameState.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { io, Socket } from "socket.io-client";
33
import type { Player, Question } from "@/lib/adepts-quiz-types";
44
import type { QuizBoardHoverCell } from "@/lib/quizBoardHover";
55
import { mergeSeatRosterIntoQuizPlayers } from "@/lib/quizLobbyClientAssignments";
6+
import { consumeAdeptsWheelReturnCloseQuizCardFlag } from "@/lib/quizAdeptsWheelClient";
67

78
export type { Player, Question };
89

@@ -439,11 +440,17 @@ export function useGameState() {
439440
}))
440441
),
441442
};
443+
const closeAfterWheel = consumeAdeptsWheelReturnCloseQuizCardFlag();
444+
const stateToApply = closeAfterWheel
445+
? { ...nextState, activeQuizCard: null, quizBoardHoverCell: null }
446+
: nextState;
447+
const rebroadcast = closeAfterWheel || hadRoster;
442448
skipEmitRef.current = true;
443-
setState(nextState);
444-
if (hadRoster) {
445-
skipEmitRef.current = false;
446-
queueMicrotask(() => socketRef.current?.emit("update", { ...nextState, boardRoom: ROOM }));
449+
setState(stateToApply);
450+
if (rebroadcast) {
451+
queueMicrotask(() => {
452+
socketRef.current?.emit("update", { ...stateToApply, boardRoom: ROOM });
453+
});
447454
}
448455
});
449456

artifacts/game-client/src/apps/adepts-game-2/pages/Home.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { GamePhaseNav } from "@/components/GamePhaseArrows";
88
import { QuizBoardReloadButton } from "@/components/QuizBoardReloadButton";
99
import { useRole } from "@/hooks/useRole";
1010
import { ChatPanel } from "@/components/ChatPanel";
11+
import { getQuizNavSocket } from "@/hooks/quizNavSocket";
1112

1213
function resolveUrl(url: string): string {
1314
if (!url) return url;
@@ -211,6 +212,13 @@ export default function Home() {
211212
updateQuestion(openCard.themeIndex, openCard.questionIndex, data)
212213
}
213214
onAwardPoints={handleAwardPoints}
215+
onHostBroadcastAdeptsWheel={
216+
isHost
217+
? (payload) => {
218+
getQuizNavSocket().emit("hostAdeptsWheelOpen", payload);
219+
}
220+
: undefined
221+
}
214222
/>
215223
)}
216224
</div>

artifacts/game-client/src/apps/adepts-game-3/hooks/useGameState.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { io, Socket } from "socket.io-client";
33
import type { Player, Question } from "@/lib/adepts-quiz-types";
44
import type { QuizBoardHoverCell } from "@/lib/quizBoardHover";
55
import { mergeSeatRosterIntoQuizPlayers } from "@/lib/quizLobbyClientAssignments";
6+
import { consumeAdeptsWheelReturnCloseQuizCardFlag } from "@/lib/quizAdeptsWheelClient";
67

78
export type { Player, Question };
89

@@ -596,11 +597,17 @@ export function useGameState() {
596597
}))
597598
),
598599
};
600+
const closeAfterWheel = consumeAdeptsWheelReturnCloseQuizCardFlag();
601+
const stateToApply = closeAfterWheel
602+
? { ...nextState, activeQuizCard: null, quizBoardHoverCell: null }
603+
: nextState;
604+
const rebroadcast = closeAfterWheel || hadRoster;
599605
skipEmitRef.current = true;
600-
setState(nextState);
601-
if (hadRoster) {
602-
skipEmitRef.current = false;
603-
queueMicrotask(() => socketRef.current?.emit("update", { ...nextState, boardRoom: ROOM }));
606+
setState(stateToApply);
607+
if (rebroadcast) {
608+
queueMicrotask(() => {
609+
socketRef.current?.emit("update", { ...stateToApply, boardRoom: ROOM });
610+
});
604611
}
605612
});
606613

artifacts/game-client/src/apps/adepts-game-3/pages/Home.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { GamePhaseNav } from "@/components/GamePhaseArrows";
88
import { QuizBoardReloadButton } from "@/components/QuizBoardReloadButton";
99
import { useRole } from "@/hooks/useRole";
1010
import { ChatPanel } from "@/components/ChatPanel";
11+
import { getQuizNavSocket } from "@/hooks/quizNavSocket";
1112

1213
function resolveUrl(url: string): string {
1314
if (!url) return url;
@@ -211,6 +212,13 @@ export default function Home() {
211212
updateQuestion(openCard.themeIndex, openCard.questionIndex, data)
212213
}
213214
onAwardPoints={handleAwardPoints}
215+
onHostBroadcastAdeptsWheel={
216+
isHost
217+
? (payload) => {
218+
getQuizNavSocket().emit("hostAdeptsWheelOpen", payload);
219+
}
220+
: undefined
221+
}
214222
/>
215223
)}
216224
</div>

artifacts/game-client/src/apps/adepts-game/hooks/useGameState.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { io, Socket } from "socket.io-client";
33
import type { Player, Question } from "@/lib/adepts-quiz-types";
44
import type { QuizBoardHoverCell } from "@/lib/quizBoardHover";
55
import { mergeSeatRosterIntoQuizPlayers } from "@/lib/quizLobbyClientAssignments";
6+
import { consumeAdeptsWheelReturnCloseQuizCardFlag } from "@/lib/quizAdeptsWheelClient";
67

78
export type { Player, Question };
89

@@ -250,11 +251,17 @@ export function useGameState() {
250251
}))
251252
),
252253
});
254+
const closeAfterWheel = consumeAdeptsWheelReturnCloseQuizCardFlag();
255+
const stateToApply = closeAfterWheel
256+
? { ...nextState, activeQuizCard: null, quizBoardHoverCell: null }
257+
: nextState;
258+
const rebroadcast = closeAfterWheel || hadRoster;
253259
skipEmitRef.current = true;
254-
setState(nextState);
255-
if (hadRoster) {
256-
skipEmitRef.current = false;
257-
queueMicrotask(() => socketRef.current?.emit("update", { ...nextState, boardRoom: ROOM }));
260+
setState(stateToApply);
261+
if (rebroadcast) {
262+
queueMicrotask(() => {
263+
socketRef.current?.emit("update", { ...stateToApply, boardRoom: ROOM });
264+
});
258265
}
259266
});
260267

artifacts/game-client/src/apps/adepts-game/pages/Home.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { GamePhaseNav } from "@/components/GamePhaseArrows";
88
import { QuizBoardReloadButton } from "@/components/QuizBoardReloadButton";
99
import { useRole } from "@/hooks/useRole";
1010
import { ChatPanel } from "@/components/ChatPanel";
11+
import { getQuizNavSocket } from "@/hooks/quizNavSocket";
1112

1213
function resolveUrl(url: string): string {
1314
if (!url) return url;
@@ -211,6 +212,13 @@ export default function Home() {
211212
updateQuestion(openCard.themeIndex, openCard.questionIndex, data)
212213
}
213214
onAwardPoints={handleAwardPoints}
215+
onHostBroadcastAdeptsWheel={
216+
isHost
217+
? (payload) => {
218+
getQuizNavSocket().emit("hostAdeptsWheelOpen", payload);
219+
}
220+
: undefined
221+
}
214222
/>
215223
)}
216224
</div>

artifacts/game-client/src/components/AdeptsQuizBoardGuard.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, type ReactNode } from "react";
22
import { useQuizLobbyState } from "@/hooks/useQuizLobbyState";
3+
import { getQuizNavSocket } from "@/hooks/quizNavSocket";
34

45
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
56

@@ -14,6 +15,12 @@ export function AdeptsQuizBoardGuard({ children }: { children: ReactNode }) {
1415
}
1516
}, [lobbyState]);
1617

18+
const gameStarted = lobbyState?.gameStarted === true;
19+
useEffect(() => {
20+
if (!gameStarted) return;
21+
getQuizNavSocket().emit("requestAdeptsWheelState");
22+
}, [gameStarted]);
23+
1724
if (lobbyState == null) {
1825
return (
1926
<div className="adepts-quiz-theme flex min-h-screen items-center justify-center text-muted-foreground">

0 commit comments

Comments
 (0)