Skip to content

Commit f8ae51f

Browse files
committed
добавлена табличка с пожертвованиями игроков
1 parent 1beaf25 commit f8ae51f

7 files changed

Lines changed: 137 additions & 2 deletions

File tree

artifacts/api-server/src/lib/adepts-quiz-relay-types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export type AdeptsQuizRelayPayload = {
2727
themes?: string[];
2828
questions?: unknown[][];
2929
dataVersion?: number;
30+
/** Пожертвования по 5 местам; общие для сессии. */
31+
donations?: (number | null)[];
3032
};
3133

3234
const THEME_COUNT = 8;
@@ -49,6 +51,7 @@ export function defaultQuizRelayPayload(sessionId: string): AdeptsQuizRelayPaylo
4951
quizBoardHoverCell: null,
5052
questionUsedGrid,
5153
dataVersion: 59,
54+
donations: [null, null, null, null, null],
5255
};
5356
}
5457

artifacts/api-server/src/lib/adepts-quiz-room-store.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export function getQuizRelayOrDefault(sessionId: string): AdeptsQuizRelayPayload
4343
s = defaultQuizRelayPayload(sessionId);
4444
rooms.set(sessionId, s);
4545
}
46+
if (!s.donations || s.donations.length !== 5) {
47+
s.donations = [null, null, null, null, null];
48+
}
4649
return s;
4750
}
4851

@@ -68,6 +71,18 @@ export function setQuizRelayFull(sessionId: string, payload: AdeptsQuizRelayPayl
6871
base.questionUsedGrid = structuredClone(payload.questionUsedGrid);
6972
if (payload.dataVersion !== undefined) base.dataVersion = payload.dataVersion;
7073

74+
const rawDon = (payload as Record<string, unknown>)["donations"];
75+
if (Array.isArray(rawDon) && rawDon.length === 5) {
76+
base.donations = rawDon.map((v) => {
77+
if (v === null || v === undefined) return null;
78+
if (typeof v === "number" && Number.isFinite(v)) return Math.round(v);
79+
const n = Number(v);
80+
return Number.isFinite(n) ? Math.round(n) : null;
81+
}) as (number | null)[];
82+
} else if (!base.donations || base.donations.length !== 5) {
83+
base.donations = [null, null, null, null, null];
84+
}
85+
7186
if (payload.catalogIncluded && payload.themes && payload.questions) {
7287
base.catalogIncluded = true;
7388
base.themes = structuredClone(payload.themes);

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import {
1616
buildAdeptsQuizRelayPayload,
1717
deriveThemesAndQuestionsFromQuizIncoming,
18+
normalizeQuizDonations,
1819
} from "@/lib/adeptsQuizSocketRelay";
1920
import {
2021
getAdeptsCommandSocket,
@@ -45,6 +46,8 @@ export type GameState = {
4546
quizBoardHoverCell?: QuizBoardHoverCell;
4647
dataVersion?: number;
4748
boardRoom?: string;
49+
/** Пожертвования по 5 местам; общие для всех раундов квиза. */
50+
donations: (number | null)[];
4851
};
4952

5053
const DEFAULT_PLAYERS: Player[] = Array.from({ length: 5 }, (_, i) => ({
@@ -69,16 +72,31 @@ function emptyGrid(): Pick<GameState, "themes" | "questions"> {
6972
};
7073
}
7174

75+
const DEFAULT_DONATIONS: (number | null)[] = [null, null, null, null, null];
76+
7277
const DEFAULT_CORE: Omit<GameState, "themes" | "questions"> = {
7378
activeQuizCard: null,
7479
currentTurnSeat: 0,
7580
quizBoardHoverCell: null,
7681
players: DEFAULT_PLAYERS,
7782
dataVersion: undefined,
7883
boardRoom: undefined,
84+
donations: [...DEFAULT_DONATIONS],
7985
};
8086

8187
const PLAYERS_KEY = "adepts-shared-players";
88+
const DONATIONS_KEY = "adepts-shared-donations";
89+
90+
function loadSharedDonations(): (number | null)[] {
91+
try {
92+
const s = localStorage.getItem(DONATIONS_KEY);
93+
if (!s) return [...DEFAULT_DONATIONS];
94+
const parsed = JSON.parse(s) as unknown;
95+
return normalizeQuizDonations(parsed) ?? [...DEFAULT_DONATIONS];
96+
} catch {
97+
return [...DEFAULT_DONATIONS];
98+
}
99+
}
82100

83101
type BoardRuntime = {
84102
storageKey: string;
@@ -327,6 +345,7 @@ function loadInitialState(boardId: AdeptsBoardId): GameState {
327345
...DEFAULT_CORE,
328346
...emptyGrid(),
329347
players: rosterPlayers,
348+
donations: loadSharedDonations(),
330349
});
331350
}
332351
}
@@ -340,11 +359,13 @@ function loadInitialState(boardId: AdeptsBoardId): GameState {
340359
...emptyGrid(),
341360
players: rosterPlayers,
342361
dataVersion: rt.dataVersion,
362+
donations: loadSharedDonations(),
343363
});
344364
}
345365
return migrateCatalog(boardId, {
346366
...parsed,
347367
players: rosterPlayers,
368+
donations: normalizeQuizDonations(parsed.donations) ?? loadSharedDonations(),
348369
activeQuizCard: parsed.activeQuizCard ?? null,
349370
currentTurnSeat: Number.isInteger(parsed.currentTurnSeat)
350371
? ((Number(parsed.currentTurnSeat) % 5) + 5) % 5
@@ -366,12 +387,14 @@ function loadInitialState(boardId: AdeptsBoardId): GameState {
366387
...emptyGrid(),
367388
players: rosterPlayers,
368389
dataVersion: rt.dataVersion,
390+
donations: loadSharedDonations(),
369391
});
370392
}
371393
return migrateCatalog(boardId, {
372394
...DEFAULT_CORE,
373395
...emptyGrid(),
374396
players: rosterPlayers,
397+
donations: loadSharedDonations(),
375398
});
376399
}
377400

@@ -457,6 +480,7 @@ export function useGameState(boardId: AdeptsBoardId) {
457480
boardId: rec["boardId"],
458481
players: rec["players"],
459482
currentTurnSeat: rec["currentTurnSeat"],
483+
donations: rec["donations"],
460484
// Reset all board-specific fields to safe defaults
461485
activeQuizCard: null,
462486
quizBoardHoverCell: null,
@@ -501,13 +525,17 @@ export function useGameState(boardId: AdeptsBoardId) {
501525
hoverFromRelay = null;
502526
}
503527

528+
const incomingDonations = normalizeQuizDonations(recToUse["donations"]);
529+
const nextDonations = incomingDonations ?? prev.donations;
530+
504531
let nextState = withClosedActiveQuizIfCellUsed(
505532
migrateCatalog(boardId, {
506533
...prev,
507534
boardRoom: typeof recToUse["boardRoom"] === "string" ? recToUse["boardRoom"] : prev.boardRoom,
508535
themes,
509536
questions,
510537
players: merged,
538+
donations: nextDonations,
511539
activeQuizCard: rawCard,
512540
// Relay is authoritative: never let host+Roster merge stomp server `currentTurnSeat`.
513541
currentTurnSeat:
@@ -568,6 +596,7 @@ export function useGameState(boardId: AdeptsBoardId) {
568596
useEffect(() => {
569597
localStorage.setItem(rt.storageKey, JSON.stringify(state));
570598
localStorage.setItem(PLAYERS_KEY, JSON.stringify(state.players));
599+
localStorage.setItem(DONATIONS_KEY, JSON.stringify(state.donations));
571600

572601
/** Consume before `catalogReady` / `allowQuizPush` returns — otherwise skip stays true and a later `hostQuizRelay` can stomp relay (host pick then no modal). */
573602
const skipThisCommit = skipEmitRef.current;
@@ -601,6 +630,15 @@ export function useGameState(boardId: AdeptsBoardId) {
601630
setState((prev) => ({ ...prev, players }));
602631
} catch {}
603632
}
633+
if (e.key === DONATIONS_KEY && e.newValue) {
634+
try {
635+
const d = normalizeQuizDonations(JSON.parse(e.newValue));
636+
if (d) {
637+
skipEmitRef.current = true;
638+
setState((prev) => ({ ...prev, donations: d }));
639+
}
640+
} catch {}
641+
}
604642
};
605643
window.addEventListener("storage", handler);
606644
return () => window.removeEventListener("storage", handler);
@@ -768,6 +806,8 @@ export function useGameState(boardId: AdeptsBoardId) {
768806
players: DEFAULT_PLAYERS.map((p) => ({ ...p })),
769807
dataVersion: rt.dataVersion,
770808
boardRoom: getAdeptsSessionId(),
809+
donations:
810+
prev.donations?.length === 5 ? [...prev.donations] : [...DEFAULT_DONATIONS],
771811
})
772812
);
773813
}, [boardId, rt.dataVersion]);

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { QuizBoardReloadButton } from "@/components/QuizBoardReloadButton";
66
import { useRole } from "@/hooks/useRole";
77
import { ChatPanel } from "@/components/ChatPanel";
88
import { QuizBoardPandoraLottoOverlay } from "@/components/QuizBoardPandoraLottoOverlay";
9+
import { DonationsTable } from "@/components/DonationsTable";
910
import { getQuizNavSocket } from "@/hooks/quizNavSocket";
1011

1112
const BOARD_BG = "/funeral-board-bg.png";
@@ -146,7 +147,9 @@ export default function FuneralRoundPage() {
146147
aria-label="Сцена похорон"
147148
/>
148149
</main>
149-
<div className="min-h-0 min-w-0" aria-hidden="true" />
150+
<aside className="flex min-h-0 min-w-0 flex-col items-end p-2 pt-3">
151+
<DonationsTable players={state.players} donations={state.donations} />
152+
</aside>
150153
</div>
151154

152155
<div className="relative z-[110] grid shrink-0 grid-cols-[minmax(0,15%)_minmax(0,1fr)_minmax(0,15%)]">

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { QuizBoardReloadButton } from "@/components/QuizBoardReloadButton";
1010
import { useRole } from "@/hooks/useRole";
1111
import { ChatPanel } from "@/components/ChatPanel";
1212
import { QuizBoardPandoraLottoOverlay } from "@/components/QuizBoardPandoraLottoOverlay";
13+
import { DonationsTable } from "@/components/DonationsTable";
1314
import { getQuizNavSocket } from "@/hooks/quizNavSocket";
1415

1516
function resolveUrl(url: string): string {
@@ -173,7 +174,9 @@ export default function Home({ boardId }: { boardId: AdeptsBoardId }) {
173174
/>
174175
</div>
175176
</main>
176-
<div className="min-h-0 min-w-0" aria-hidden="true" />
177+
<aside className="flex min-h-0 min-w-0 flex-col items-end p-2 pt-3">
178+
<DonationsTable players={state.players} donations={state.donations} />
179+
</aside>
177180
</div>
178181

179182
<div className="grid shrink-0 grid-cols-[minmax(0,15%)_minmax(0,1fr)_minmax(0,15%)]">
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { Player } from "@/lib/adepts-quiz-types";
2+
3+
type Props = {
4+
players: Player[];
5+
donations: (number | null)[];
6+
};
7+
8+
/** Компактная таблица пожертвований — общая для всех фаз квиза (синхрон через relay). */
9+
export function DonationsTable({ players, donations }: Props) {
10+
const rows = players.slice(0, 5);
11+
const d =
12+
donations.length >= 5
13+
? donations
14+
: [...donations, ...Array(Math.max(0, 5 - donations.length)).fill(null)].slice(0, 5);
15+
16+
return (
17+
<div
18+
className="w-full max-w-[min(100%,14rem)] rounded-lg border-2 border-amber-400/75 bg-gradient-to-b from-amber-950/35 to-black/50 px-2.5 py-2 shadow-[0_0_22px_rgba(250,204,21,0.45),0_0_8px_rgba(251,191,36,0.25)_inset] backdrop-blur-sm"
19+
aria-label="Пожертвования игроков"
20+
>
21+
<table className="w-full border-collapse text-left font-display text-[11px] leading-tight sm:text-xs">
22+
<caption className="sr-only">Пожертвования по игрокам</caption>
23+
<thead>
24+
<tr className="border-b border-amber-500/50 text-amber-100/95">
25+
<th scope="col" className="pb-1.5 pr-2 font-semibold tracking-wide">
26+
Игрок
27+
</th>
28+
<th scope="col" className="pb-1.5 font-semibold tracking-wide">
29+
Пожертвования
30+
</th>
31+
</tr>
32+
</thead>
33+
<tbody>
34+
{rows.map((p, i) => (
35+
<tr key={p.id} className="border-b border-white/10 last:border-b-0">
36+
<td className="max-w-[5.5rem] truncate py-1 pr-2 text-foreground/95" title={p.name}>
37+
{p.name}
38+
</td>
39+
<td className="py-1 tabular-nums text-amber-50/95">
40+
{d[i] === null || d[i] === undefined ? "—" : d[i]}
41+
</td>
42+
</tr>
43+
))}
44+
</tbody>
45+
</table>
46+
</div>
47+
);
48+
}

artifacts/game-client/src/lib/adeptsQuizSocketRelay.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,29 @@ export type AdeptsQuizRelayPayload = {
3232
themes?: string[];
3333
questions?: Question[][];
3434
dataVersion?: number;
35+
/** Пожертвования по местам 1–5; общие для всей сессии квиза. */
36+
donations?: (number | null)[];
3537
};
3638

39+
export function normalizeQuizDonations(raw: unknown): (number | null)[] | null {
40+
if (!Array.isArray(raw) || raw.length !== 5) return null;
41+
const out: (number | null)[] = [];
42+
for (let i = 0; i < 5; i++) {
43+
const v = raw[i];
44+
if (v === null || v === undefined) {
45+
out.push(null);
46+
continue;
47+
}
48+
if (typeof v === "number" && Number.isFinite(v)) {
49+
out.push(Math.round(v));
50+
continue;
51+
}
52+
const n = Number(v);
53+
out.push(Number.isFinite(n) ? Math.round(n) : null);
54+
}
55+
return out;
56+
}
57+
3758
export function buildAdeptsQuizRelayPayload(
3859
slice: {
3960
players: Player[];
@@ -43,6 +64,7 @@ export function buildAdeptsQuizRelayPayload(
4364
currentTurnSeat: number;
4465
quizBoardHoverCell?: QuizBoardHoverCell | null;
4566
dataVersion?: number;
67+
donations: (number | null)[];
4668
},
4769
boardRoom: string,
4870
includeCatalog: boolean,
@@ -55,6 +77,7 @@ export function buildAdeptsQuizRelayPayload(
5577
currentTurnSeat: slice.currentTurnSeat,
5678
quizBoardHoverCell: slice.quizBoardHoverCell ?? null,
5779
questionUsedGrid: slice.questions.map((row) => row.map((q) => Boolean(q.used))),
80+
donations: normalizeQuizDonations(slice.donations) ?? [null, null, null, null, null],
5881
};
5982
if (boardId !== undefined) out.boardId = boardId;
6083
if (slice.dataVersion !== undefined) out.dataVersion = slice.dataVersion;

0 commit comments

Comments
 (0)