diff --git a/migrations/Version20260810120000.php b/migrations/Version20260810120000.php new file mode 100644 index 0000000..e8d6d4e --- /dev/null +++ b/migrations/Version20260810120000.php @@ -0,0 +1,30 @@ +addSql('CREATE TABLE lobby_message (id INT AUTO_INCREMENT NOT NULL, session_id INT NOT NULL, player_id INT NOT NULL, content VARCHAR(500) NOT NULL, created_at DATETIME NOT NULL, INDEX IDX_LOBBY_MESSAGE_SESSION (session_id), INDEX IDX_LOBBY_MESSAGE_PLAYER (player_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE lobby_message ADD CONSTRAINT FK_LOBBY_MESSAGE_SESSION FOREIGN KEY (session_id) REFERENCES session (id)'); + $this->addSql('ALTER TABLE lobby_message ADD CONSTRAINT FK_LOBBY_MESSAGE_PLAYER FOREIGN KEY (player_id) REFERENCES player (id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE lobby_message DROP FOREIGN KEY FK_LOBBY_MESSAGE_SESSION'); + $this->addSql('ALTER TABLE lobby_message DROP FOREIGN KEY FK_LOBBY_MESSAGE_PLAYER'); + $this->addSql('DROP TABLE lobby_message'); + } +} diff --git a/src/Game/Controller/GameController.php b/src/Game/Controller/GameController.php index bc504a5..d9929cc 100644 --- a/src/Game/Controller/GameController.php +++ b/src/Game/Controller/GameController.php @@ -141,12 +141,21 @@ final class GameController extends AbstractController return $this->redirectToRoute('game', ['session' => $session->getId()]); } + if ($request->isMethod('POST') && $request->request->has('send_message')) { + $dashboardService->postLobbyMessage($session, $user, (string) $request->request->get('content', '')); + return $this->redirectToRoute('game', ['session' => $session->getId()]); + } + // Lazily pick up readiness changes from other players since our last request $dashboardService->checkAllPlayersReady($session); if ($session->getStatus() === SessionStatus::CREATED) { - $this->addFlash('info', 'This session is still waiting for more players to join.'); - return $this->redirectToRoute('game_dashboard'); + return $this->render('game/lobby.html.twig', [ + 'session' => $session, + 'messages' => $dashboardService->getLobbyMessages($session), + 'player' => $player, + 'mercure_public_url' => $this->mercurePublicUrl, + ]); } if ($session->getStatus() === SessionStatus::WON) { diff --git a/src/Game/Entity/LobbyMessage.php b/src/Game/Entity/LobbyMessage.php new file mode 100644 index 0000000..7dedd7d --- /dev/null +++ b/src/Game/Entity/LobbyMessage.php @@ -0,0 +1,88 @@ +createdAt = new \DateTime(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getSession(): ?Session + { + return $this->session; + } + + public function setSession(?Session $session): static + { + $this->session = $session; + + return $this; + } + + public function getPlayer(): ?Player + { + return $this->player; + } + + public function setPlayer(?Player $player): static + { + $this->player = $player; + + return $this; + } + + public function getContent(): ?string + { + return $this->content; + } + + public function setContent(string $content): static + { + $this->content = $content; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeInterface + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeInterface $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } +} diff --git a/src/Game/Repository/LobbyMessageRepository.php b/src/Game/Repository/LobbyMessageRepository.php new file mode 100644 index 0000000..f2a51a7 --- /dev/null +++ b/src/Game/Repository/LobbyMessageRepository.php @@ -0,0 +1,33 @@ + + */ +class LobbyMessageRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, LobbyMessage::class); + } + + /** + * @return LobbyMessage[] + */ + public function findForSession(Session $session, int $limit = 100): array + { + return $this->createQueryBuilder('m') + ->andWhere('m.session = :session') + ->setParameter('session', $session) + ->orderBy('m.createdAt', 'ASC') + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } +} diff --git a/src/Game/Service/GameDashboardService.php b/src/Game/Service/GameDashboardService.php index 6da63d6..a0ebb4a 100644 --- a/src/Game/Service/GameDashboardService.php +++ b/src/Game/Service/GameDashboardService.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Game\Service; use App\Game\Entity\Game; +use App\Game\Entity\LobbyMessage; use App\Game\Entity\Player; use App\Game\Entity\Session; use App\Game\Entity\SessionSetting; @@ -11,6 +12,7 @@ use App\Game\Enum\GameStatus; use App\Game\Enum\SessionSettingType; use App\Game\Enum\SessionStatus; use App\Game\Repository\GameRepository; +use App\Game\Repository\LobbyMessageRepository; use App\Game\Repository\SessionRepository; use App\Tech\Entity\User; use Doctrine\ORM\EntityManagerInterface; @@ -21,10 +23,12 @@ use Symfony\Component\Mercure\Update; final class GameDashboardService { private const READY_TIMEOUT_SECONDS = 60; + private const LOBBY_MESSAGE_MAX_LENGTH = 500; public function __construct( private readonly GameRepository $gameRepository, private readonly SessionRepository $sessionRepository, + private readonly LobbyMessageRepository $lobbyMessageRepository, private readonly EntityManagerInterface $entityManager, private readonly HubInterface $hub, ) { @@ -122,6 +126,8 @@ final class GameDashboardService $this->entityManager->flush(); + $this->publishLobbyEvent($session, 'player_joined'); + if (count($session->getPlayers()) === $session->getGame()->getNumberOfPlayers()) { $this->startSession($session); } @@ -283,9 +289,60 @@ final class GameDashboardService $this->entityManager->persist($session); $this->entityManager->flush(); + $this->publishLobbyEvent($session, 'session_started'); + return true; } + /** + * @return LobbyMessage[] + */ + public function getLobbyMessages(Session $session): array + { + return $this->lobbyMessageRepository->findForSession($session); + } + + public function postLobbyMessage(Session $session, User $user, string $content): ?LobbyMessage + { + if ($session->getStatus() !== SessionStatus::CREATED) { + return null; + } + + $player = null; + foreach ($session->getPlayers() as $sessionPlayer) { + if ($sessionPlayer->getUser() === $user) { + $player = $sessionPlayer; + break; + } + } + + if (!$player) { + return null; + } + + $content = trim($content); + if ($content === '') { + return null; + } + $content = mb_substr($content, 0, self::LOBBY_MESSAGE_MAX_LENGTH); + + $message = new LobbyMessage(); + $message->setSession($session); + $message->setPlayer($player); + $message->setContent($content); + + $this->entityManager->persist($message); + $this->entityManager->flush(); + + $this->publishLobbyEvent($session, 'lobby_message', [ + 'username' => $user->getUserIdentifier(), + 'content' => $content, + 'createdAt' => $message->getCreatedAt()->format(DATE_ATOM), + ]); + + return $message; + } + public function toggleReady(Session $session, User $user): bool { if ($session->getStatus() !== SessionStatus::READY) { @@ -393,10 +450,15 @@ final class GameDashboardService } private function publishPlayerReady(Session $session, int $screen, bool $ready): void + { + $this->publishLobbyEvent($session, 'player_ready', ['player' => $screen, 'ready' => $ready]); + } + + private function publishLobbyEvent(Session $session, string $type, array $extra = []): void { try { $topic = '/game/hub/' . $session->getId(); - $this->hub->publish(new Update($topic, json_encode(['type' => 'player_ready', 'player' => $screen, 'ready' => $ready]))); + $this->hub->publish(new Update($topic, json_encode(array_merge(['type' => $type], $extra)))); } catch (\Exception $e) { // Mercure might be down, but we don't want to crash the game } diff --git a/templates/game/lobby.html.twig b/templates/game/lobby.html.twig new file mode 100644 index 0000000..427fd49 --- /dev/null +++ b/templates/game/lobby.html.twig @@ -0,0 +1,72 @@ +{% extends 'layout/site.html.twig' %} + +{% block title %}Waiting for players - {{ session.game.name }}{% endblock %} + +{% block body %} +
Share the invite code with your friends. Feel free to chat below while you wait — no need to reload the page.
+ +No messages yet. Say hi!
+ {% endfor %} +Only players in this session can chat.
+ {% endif %} +