From 903298fe761525b24155e163c461c49c6812f234 Mon Sep 17 00:00:00 2001 From: MalkizH3 Date: Mon, 19 Oct 2026 22:05:07 +0200 Subject: [PATCH 1/5] feat(dramaafera): rozbuduj podsumowanie spotkan i glosowan Wydziel sekcje spotkan do MeetingsSection i zastap dotychczasowa liste responsywnym rozkladem glosow w trzech kolumnach. Sortuj graczy od najwiekszej liczby glosow do zera, wyrownuj wysokosc kart w rzedzie oraz pokazuj pominiecia i brak oddanego glosu w dwoch osobnych panelach pod kandydatami. Dodaj wieksze avatary kandydatow, nicki oraz historie rol z ikonami i kolorami odpowiednimi dla sezonu gry. Glosujacych pokazuj jako kompaktowe avatary; nick i rola wysuwaja sie jako animowana nakladka po najechaniu lub zaznaczeniu klawiatura. Nakladka nie przesuwa pozostalych elementow i nie przedluza obszaru aktywnego kursora. Zachowaj linki do profili i oznaczenia wielokrotnych glosow jednej osoby. Przenies zgony nad rozklad glosow do wspolnej czerwonej ramki. Oznacz uciszonych ikona BlackmailLetter oraz czerwona polprzezroczysta nakladka, a uwiezionych przezroczysta grafika InJail na calym avatarze. Na karcie kandydata stosuj pojedyncza nakladke calego panelu zamiast nakladania jej ponownie na jego kafelek z nickiem. Udostepnij zapisany wynik wyrzucenia i odroznij brak wyniku w starszych grach. Odtwarzaj uczestnikow ostatniego spotkania bez glosowania na podstawie skladu gry, zapisanych zgonow, wyrzucen i pozniejszych uczestnictw. Pokazuj ich z zerem glosow. Rozpoznawaj zgony podczas spotkan z akcji zgadywania i egzekucji, przypisuj je na podstawie podsumowan zgonow kolejnych spotkan i dodaj notke Zginal na spotkaniu. Ogranicz pobieranie relacji glosujacych i celow do nazw graczy. Zachowaj aktualny mechanizm osi czasu z main oraz brak odczytu game_events dla v1. Zmiana dotyczy widoku gry, jego danych pomocniczych, ikon i testow; nie obejmuje ankiet cesarza, zapisow ani avatarow Discord. Walidacja: testy uczestnikow spotkan, zgonow podczas spotkan i zliczania glosow; kontrola TypeScript oraz git diff --check. --- .../images/mira/abilities/BlackmailLetter.png | Bin 0 -> 3313 bytes public/images/mira/abilities/InJail.png | Bin 0 -> 2287 bytes .../_services/games/_meetingDeaths.test.ts | 21 ++ .../_services/games/_meetingDeaths.ts | 25 ++ .../dramaafera/_services/games/getGameData.ts | 9 +- src/app/dramaafera/_services/games/types.ts | 3 + .../_utils/meetingParticipants.test.ts | 20 ++ .../dramaafera/_utils/meetingParticipants.ts | 24 ++ .../[date]/[gameId]/GameDetailContent.tsx | 216 +----------------- .../[date]/[gameId]/MeetingsSection.tsx | 166 ++++++++++++++ 10 files changed, 268 insertions(+), 216 deletions(-) create mode 100644 public/images/mira/abilities/BlackmailLetter.png create mode 100644 public/images/mira/abilities/InJail.png create mode 100644 src/app/dramaafera/_services/games/_meetingDeaths.test.ts create mode 100644 src/app/dramaafera/_services/games/_meetingDeaths.ts create mode 100644 src/app/dramaafera/_utils/meetingParticipants.test.ts create mode 100644 src/app/dramaafera/_utils/meetingParticipants.ts create mode 100644 src/app/dramaafera/historia-gier/[date]/[gameId]/MeetingsSection.tsx diff --git a/public/images/mira/abilities/BlackmailLetter.png b/public/images/mira/abilities/BlackmailLetter.png new file mode 100644 index 0000000000000000000000000000000000000000..76b15c6cfcefaed3554da5d5161008920baa23eb GIT binary patch literal 3313 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 literal 0 HcmV?d00001 diff --git a/public/images/mira/abilities/InJail.png b/public/images/mira/abilities/InJail.png new file mode 100644 index 0000000000000000000000000000000000000000..f95d773c82a5c8f14653691d639221dfa72419a2 GIT binary patch literal 2287 zcmcIm`9IT-AAci9bDxcbB2r7jm?K9yw#nRVjtp}y#zL-~MJOsa4N=Zm3&WgQSyH-O zA!o_)5k+qHoj>CHdOV)b=j-wO`SpB0UMW^6Gk%a12mkCztr3(NMstN#$SOCz;1At?MM{aLS z0f5IDVQgdv4*PahvcRP- zckHn#cL2KK(P~Y5zB+bpyKct6>JJN5=n^rB>&1hwR9=3T{d4an9SW zhGUrc+FaSy{^XM3c++&KL4U%~>aWU!jD$b?!tvIdvq-jg3x5wXs!v|p?@EaMJT%nv zDv{gI&(CNB(uhzt(JcHq_o49Ky?ekXLFqF%%4V_SfS{nbt28VqE}>@*IXi!Q>BUP8 zD9iF75+C*UA_6Bn+OOw0WU56*Mvk;cX}P+(Dr6$EY~FwTxHkV;UQtmIEG1PP>0M!? zjCu?+a&|sms6`O4{kpXU6BQM8>5L&&RaL$1TrLaOy}Z3xgKIv(%e%~T+nj5st*1rn z$tXR9W5HbIevjY^{-}%PnD9`^v^Pi(ryTL}?gF>Q9ST3aq^c?f4UwQ~-95gCIv;$# z4>aaC7S{X%UG-sweKO97GxquQZ#Dn6Gm*g}JNiijLaUss2u%8bQ!XSvu{iDc-<}&X zi!-2$iQre>Wa+=h#Y50`*BQaxA@eX4xD^%y4G#JTMdH2XqjuXUnlE15i2=U)H2*$rhJTzV06XM|aK z@wa8X{Hi*LDJWUx{mq%r3$z7()&calbzT0kQV|&y>li(o7#~mSTDs8Or5#b-bQ5#> z0E;c*%)9k!#K2&1Y`61evzgnA51uuz?$(_wy|4*Q3yBAWj|v@j0=uz-;gP%WI3A&g z|8EP!(SBBd*wgpBAu_~8sQwzoP4W6{ANUo1JMJfCQGD0SD566-&t_7wn?Oc}^6?2= ztGu!sdr1|BCU-=%rc>}h>43B0q{Y^p(CX8k3wiAQ^lwllYZ$54tYVKUKw+h^XO(wve>xs$b5_N`D&o(aF@4<>!qJK0H)X|qP4Dk^^j7^64Ofo=h%GLI zq{fB@F{wH+#`~W{-&%~5Q*}hh+**V*fOOlZIoHz_Vn~b=ZJ^l^oy0U zk(8S&*YrNtysPv3P74Jm8R?J|GkdWG-aMl(AshdFqmPeEC%I3-1j@?F-uL%Q5G8`@ zu(UNyZ#0@3X|-r+kdP_6otrFh!ee)1`SiK_9+s6;CsS(^*$YekzaoO)&+In#Kr*ZY zW2K@UYIJ=m7TDE2>wO`^qJn~%FVD<@Hm*H>eUqi(BCRXP@lsX=BW-O(NhFecQPv0d zP$(BRGLrV1ykt7v(DW!v{KhHJO+0?7AkTEjHa-?B-%AZ(mEA%J4L7&jC)*?`Dc~^fj~4~ zLyj)K?e6X#EfhT#Yb5_p!$R-l?7{vnzc#vb!-s||eJ=0=p&K30)5$Y5(l}a_S~S=+ z!?sKS+L3WCzvIc=D5_TAICiq*7EUd{`a_h-NAHv++BmDY+L|6|2O}s$PG!@_hI!SI z={hpS2}42UZOmK_Wy6+mlum6R!xE%=P;=5@V)HCfvSvWBBo=&arDEzLDZSdT^quB& z!}{WK6o=WAz^96-;=T##=QOg_VwP+L_XybjwsCg0F!nQ9@=Dh(=y7Ee@1p&-+j@;L vYQS)Z3Gin4NST { + expect(getDeathsDuringMeetings(meetings, [{ type: 'kill', target: { name: 'A' }, detail: '{"isGuess":true}' }]).get(1)).toEqual(['A']); +}); + +test('includes final meeting executions even without a following summary', () => { + expect(getDeathsDuringMeetings(meetings, [{ type: 'kill', target: { name: 'B' }, detail: '{"causeOfDeath":"Jailor"}' }]).get(2)).toEqual(['B']); +}); + +test('ignores ordinary kills and malformed details', () => { + expect(getDeathsDuringMeetings(meetings, [ + { type: 'kill', target: { name: 'A' }, detail: null }, + { type: 'kill', target: { name: 'A' }, detail: 'invalid' }, + ]).size).toBe(0); +}); diff --git a/src/app/dramaafera/_services/games/_meetingDeaths.ts b/src/app/dramaafera/_services/games/_meetingDeaths.ts new file mode 100644 index 00000000..a03934da --- /dev/null +++ b/src/app/dramaafera/_services/games/_meetingDeaths.ts @@ -0,0 +1,25 @@ +type Meeting = { meetingNumber: number; deathsSinceLastMeeting: string[] }; +type Action = { type: string; detail: string | null; target: { name: string } | null }; + +export function getDeathsDuringMeetings(meetings: Meeting[], actions: Action[]): Map { + const result = new Map(); + for (const action of actions) { + if (action.type !== 'kill' || !action.target || !action.detail) continue; + let detail: unknown; + try { detail = JSON.parse(action.detail); } catch { continue; } + if (!detail || typeof detail !== 'object') continue; + const isMeetingDeath = ('isGuess' in detail && detail.isGuess === true) || + ('causeOfDeath' in detail && ['Jailor', 'Prosecutor', 'Guess', 'Misguess'].includes(String(detail.causeOfDeath))); + if (!isMeetingDeath) continue; + const name = action.target.name; + // Meeting deaths appear in the following meeting's death summary. The final + // meeting has no following summary, so its guess/execution victims need actions. + const nextIndex = meetings.findIndex(meeting => meeting.deathsSinceLastMeeting.some(death => death.split(' (')[0].trim() === name)); + const meeting = nextIndex > 0 ? meetings[nextIndex - 1] : nextIndex === -1 ? meetings[meetings.length - 1] : undefined; + if (!meeting) continue; + const deaths = result.get(meeting.meetingNumber) ?? []; + if (!deaths.includes(name)) deaths.push(name); + result.set(meeting.meetingNumber, deaths); + } + return result; +} diff --git a/src/app/dramaafera/_services/games/getGameData.ts b/src/app/dramaafera/_services/games/getGameData.ts index c8822d59..9e2a42b8 100644 --- a/src/app/dramaafera/_services/games/getGameData.ts +++ b/src/app/dramaafera/_services/games/getGameData.ts @@ -11,6 +11,7 @@ import { calculateWinnerFromStats } from './winCalculator'; import { buildPlayerStats } from './_buildPlayerStats'; import { countVotes } from './_countVotes'; import { buildGameTimeline } from './_buildGameTimeline'; +import { getDeathsDuringMeetings } from './_meetingDeaths'; // Fetch detailed game data // Player relations are selected down to `name` on purpose. Identity columns @@ -46,8 +47,8 @@ export async function getGameData(gameId: string): Promise { meetingVotes: { where: { target: withoutDeleted, voter: withoutDeleted }, include: { - target: true, - voter: true + target: { select: { name: true } }, + voter: { select: { name: true } } } }, skipVotes: { @@ -140,6 +141,7 @@ export async function getGameData(gameId: string): Promise { const deathsSinceLastMeeting = JSON.parse(meeting.deathsSinceLastMeeting || '[]'); return { + exiledPlayer: game.gameActions.length > 0 ? meeting.exiledPlayer : undefined, meetingNumber: meeting.meetingNumber, deathsSinceLastMeeting, votes, @@ -152,6 +154,9 @@ export async function getGameData(gameId: string): Promise { }; }); + const meetingDeaths = getDeathsDuringMeetings(meetings, game.gameActions); + meetings.forEach(meeting => { meeting.deathsDuringMeeting = meetingDeaths.get(meeting.meetingNumber) ?? []; }); + const timeline = buildGameTimeline(game.gameActions, game.season); const winnerInfo = calculateWinnerFromStats(game.gamePlayerStatistics, game.season); diff --git a/src/app/dramaafera/_services/games/types.ts b/src/app/dramaafera/_services/games/types.ts index 129b47be..fb7e5bb2 100644 --- a/src/app/dramaafera/_services/games/types.ts +++ b/src/app/dramaafera/_services/games/types.ts @@ -122,6 +122,9 @@ export interface UIGameEvent { } export interface UIMeetingData { + deathsDuringMeeting?: string[]; + /** Undefined for legacy games where the outcome was not recorded explicitly. */ + exiledPlayer?: string | null; meetingNumber: number; deathsSinceLastMeeting: string[]; votes: { [playerName: string]: string[] }; diff --git a/src/app/dramaafera/_utils/meetingParticipants.test.ts b/src/app/dramaafera/_utils/meetingParticipants.test.ts new file mode 100644 index 00000000..97f4e49d --- /dev/null +++ b/src/app/dramaafera/_utils/meetingParticipants.test.ts @@ -0,0 +1,20 @@ +import { getMeetingParticipants } from './meetingParticipants'; +import type { UIMeetingData } from '../_services/games/types'; + +const meeting = (overrides: Partial): UIMeetingData => ({ + meetingNumber: 1, deathsSinceLastMeeting: [], votes: {}, skipVotes: [], noVotes: [], wasTie: false, wasBlessed: false, ...overrides, +}); + +test('includes living players when the final meeting ends before voting', () => { + const meetings = [meeting({ votes: { B: ['A'] }, exiledPlayer: 'B' }), meeting({ meetingNumber: 2, deathsSinceLastMeeting: ['C (zabity)'] })]; + expect(getMeetingParticipants(meetings, ['A', 'B', 'C', 'D']).get(2)).toEqual(['A', 'D']); +}); + +test('does not add the full roster to a completed voting meeting', () => { + expect(getMeetingParticipants([meeting({ votes: { B: ['A'] } })], ['A', 'B', 'C']).get(1)).toEqual(['B', 'A']); +}); + +test('restores players recorded as participants after an earlier death', () => { + const meetings = [meeting({ deathsSinceLastMeeting: ['B'] }), meeting({ meetingNumber: 2, noVotes: ['B'] })]; + expect(getMeetingParticipants(meetings, ['A', 'B']).get(2)).toEqual(['A', 'B']); +}); diff --git a/src/app/dramaafera/_utils/meetingParticipants.ts b/src/app/dramaafera/_utils/meetingParticipants.ts new file mode 100644 index 00000000..203f1ffb --- /dev/null +++ b/src/app/dramaafera/_utils/meetingParticipants.ts @@ -0,0 +1,24 @@ +import type { UIMeetingData } from '../_services/games/types'; + +/** Recover the final unvoted meeting from the roster and recorded deaths/exiles. */ +export function getMeetingParticipants(meetings: UIMeetingData[], roster: string[]): Map { + const alive = new Set(roster); + const result = new Map(); + meetings.forEach((meeting, index) => { + meeting.deathsSinceLastMeeting.forEach(death => alive.delete(death.split(' (')[0].trim())); + const recorded = new Set([ + ...Object.keys(meeting.votes), + ...Object.values(meeting.votes).flat(), + ...meeting.skipVotes, + ...meeting.noVotes, + ...(meeting.deathsDuringMeeting ?? []), + ]); + // A recorded participant may have been revived since an earlier meeting. + recorded.forEach(name => alive.add(name)); + const hasBallots = Object.values(meeting.votes).some(voters => voters.length > 0) || meeting.skipVotes.length > 0; + result.set(meeting.meetingNumber, index === meetings.length - 1 && !hasBallots ? Array.from(alive) : Array.from(recorded)); + if (meeting.exiledPlayer) alive.delete(meeting.exiledPlayer); + meeting.deathsDuringMeeting?.forEach(name => alive.delete(name)); + }); + return result; +} diff --git a/src/app/dramaafera/historia-gier/[date]/[gameId]/GameDetailContent.tsx b/src/app/dramaafera/historia-gier/[date]/[gameId]/GameDetailContent.tsx index c0da2e83..d7f366ae 100644 --- a/src/app/dramaafera/historia-gier/[date]/[gameId]/GameDetailContent.tsx +++ b/src/app/dramaafera/historia-gier/[date]/[gameId]/GameDetailContent.tsx @@ -7,6 +7,7 @@ import { TeamColors } from "@/constants/teams"; import { notFound } from "next/navigation"; import { PlayerStatsSection } from "./PlayerStatsSection"; import { GameTimeline } from "./GameTimeline"; +import { MeetingsSection } from "./MeetingsSection"; interface GameDetailContentProps { date: string; @@ -131,220 +132,7 @@ export async function GameDetailContent({ date, gameId, seasonId }: GameDetailCo {gameData.detailedStats.hasTimeline && } - {/* Dane spotkań */} -
-

🗳️ Spotkania i Głosowania

-
- {gameData.detailedStats.meetings.map((meeting, index) => ( -
-

- Spotkanie #{meeting.meetingNumber} - {meeting.wasTie && (Tie)} - {meeting.wasBlessed && (Blessed)} -

- - {meeting.deathsSinceLastMeeting.length > 0 && ( -
-

Zgony od ostatniego spotkania:

-
- {meeting.deathsSinceLastMeeting.map((death, i) => { - // Wyciągnij nick zmarłego (do pierwszego nawiasu) - const deadPlayerName = death.split(' (')[0]; - return ( -
-
- {`Avatar - {deadPlayerName} -
- {/* Tooltip z pełnym opisem */} -
- {death} -
-
- ); - })} -
-
- )} - -
-
-

Głosowania:

-
- {Object.entries(meeting.votes).map(([target, voters]) => ( -
- {/* Średni avatar i nick osoby otrzymującej głosy */} -
- {`Avatar - {target} -
- - {/* Strzałka */} - ← - - {/* Małe avatary głosujących */} -
- {voters.map((voter, index) => ( -
- {`Avatar - {/* Tooltip z nickiem */} -
- {voter} -
-
- ))} -
- - {/* Liczba głosów */} - - ({voters.length} głos{voters.length === 1 ? '' : voters.length < 5 ? 'y' : 'ów'}) - -
- ))} - - {/* Pominięcia na dole sekcji głosowań */} - {meeting.skipVotes.length > 0 && ( -
- {/* Napis SKIP zamiast avatara */} -
-
- SKIP -
- Skip -
- - {/* Strzałka */} - ← - - {/* Małe avatary głosujących na skip */} -
- {meeting.skipVotes.map((voter, index) => ( -
- {`Avatar - {/* Tooltip z nickiem */} -
- {voter} -
-
- ))} -
- - {/* Liczba głosów */} - - ({meeting.skipVotes.length} głos{meeting.skipVotes.length === 1 ? '' : meeting.skipVotes.length < 5 ? 'y' : 'ów'}) - -
- )} - - {/* Brak głosu na dole sekcji głosowań */} - {meeting.noVotes.length > 0 && ( -
- {/* Szary napis zamiast avatara */} -
- Brak głosu: -
- - - {/* Małe avatary graczy którzy nie głosowali */} -
- {meeting.noVotes.map((voter, index) => ( -
- {`Avatar - {/* Tooltip z nickiem */} -
- {voter} -
-
- ))} -
- - {/* Liczba graczy */} - - ({meeting.noVotes.length} {meeting.noVotes.length === 1 ? 'gracz' : meeting.noVotes.length < 5 ? 'graczy' : 'graczy'}) - -
- )} -
-
- -
- {meeting.blackmailedPlayers && meeting.blackmailedPlayers.length > 0 && ( -
-

🤐 Blackmailed:

-
- {meeting.blackmailedPlayers.map((playerName, index) => ( -
- {`Avatar - {playerName} -
- ))} -
-
- )} - - {meeting.jailedPlayers && meeting.jailedPlayers.length > 0 && ( -
-

🔒 Jailed:

-
- {meeting.jailedPlayers.map((playerName, index) => ( -
- {`Avatar - {playerName} -
- ))} -
-
- )} -
-
-
- ))} -
-
+
1 ? `, ${count} głosy` : ""}` : undefined} + className={`group relative inline-flex max-w-full items-center rounded-lg border border-white/10 bg-black/20 hover:bg-white/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-cyan-400 ${avatarOnly ? 'shrink-0 gap-0 p-1 text-xs hover:z-30 focus:z-30' : compact ? 'overflow-hidden gap-2 px-2 py-1 text-xs' : 'overflow-hidden gap-3 px-3 py-2 text-sm'}`}> + + + {blackmailed && Uciszony przez Blackmailera} + {jailed && Uwięziony} + + + + {roles.length ? roles.map((role, index) => ( + + {index > 0 && →} + + {convertRoleNameForDisplay(role)} + + )) : Nieznana rola} + + {name} + + {count > 1 && ×{count}} + {blackmailed && !suppressBlackmailOverlay &&