diff --git a/.env.dev b/.env.dev deleted file mode 100644 index dbdaa3f..0000000 --- a/.env.dev +++ /dev/null @@ -1,4 +0,0 @@ - -###> symfony/framework-bundle ### -APP_SECRET=620e9ce5f88a714b636179eb39d5be4f -###< symfony/framework-bundle ### diff --git a/.env.prod b/.env.prod deleted file mode 100644 index 7a524a9..0000000 --- a/.env.prod +++ /dev/null @@ -1,9 +0,0 @@ -### Compiled or real environment variables should be used in production. -### Configure MAILER_DSN to use SendGrid API transport. -### Prefer storing SENDGRID_API_KEY using Symfony Secrets or real env vars. - -###> symfony/mailer ### -# Example using SendGrid API key (replace with real secret via vault/secrets): -# SENDGRID_API_KEY=SG.xxxxx -MAILER_DSN=sendgrid+api://%env(resolve:SENDGRID_API_KEY)%@default -###< symfony/mailer ### diff --git a/.env.test b/.env.test deleted file mode 100644 index 64bd111..0000000 --- a/.env.test +++ /dev/null @@ -1,3 +0,0 @@ -# define your env variables for the test env here -KERNEL_CLASS='App\Kernel' -APP_SECRET='$ecretf0rt3st' diff --git a/.gitignore b/.gitignore index 2182434..d7d6746 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ /.env.local /.env.local.php /.env.*.local +/.env.dev +/.env.prod +/.env.test /config/secrets/prod/prod.decrypt.private.php /public/bundles/ /var/ diff --git a/assets/game1.js b/assets/game1.js index a69635a..b891b30 100644 --- a/assets/game1.js +++ b/assets/game1.js @@ -131,6 +131,8 @@ document.addEventListener('DOMContentLoaded', async () => { const screen = cfgEl.dataset.screen; const apiPingUrl = cfgEl.dataset.apiPingUrl; const apiEchoUrl = cfgEl.dataset.apiEchoUrl; + const apiCheckFinishedUrl = cfgEl.dataset.apiCheckFinishedUrl; + const lostUrl = cfgEl.dataset.lostUrl; if (mercurePublicUrl && topic) { subscribeToMercure(mercurePublicUrl, topic, screen); @@ -138,6 +140,52 @@ document.addEventListener('DOMContentLoaded', async () => { console.warn('[Mercure][game1] Missing data attributes on #mercure-config'); } + // Timer logic + const timerEl = document.getElementById('game-timer'); + if (timerEl && timerEl.dataset.endTime) { + const endTime = parseInt(timerEl.dataset.endTime) * 1000; + + const updateTimer = async () => { + const now = Date.now(); + const diff = endTime - now; + + if (diff <= 0) { + timerEl.textContent = '00:00:00'; + + // Timer reached zero, check with server + if (apiCheckFinishedUrl && lostUrl) { + try { + const response = await fetchJson(apiCheckFinishedUrl, { method: 'POST' }); + if (response && response.finished) { + window.location.href = lostUrl; + return; // Stop the timer loop + } + } catch (e) { + console.error('[API][game1] Failed to check finished status:', e); + } + } + + // Even if check failed or not finished, stop the loop if diff <= 0 + // (though technically if the server says not finished, we might want to keep checking, + // but 00:00:00 usually means it's over). + return; + } + + const hours = Math.floor(diff / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diff % (1000 * 60)) / 1000); + + const hDisplay = hours.toString().padStart(2, '0'); + const mDisplay = minutes.toString().padStart(2, '0'); + const sDisplay = seconds.toString().padStart(2, '0'); + + timerEl.textContent = `${hDisplay}:${mDisplay}:${sDisplay}`; + setTimeout(updateTimer, 1000); + }; + + updateTimer(); + } + // Demo API calls try { if (apiPingUrl) { diff --git a/src/Game/Controller/GameApiController.php b/src/Game/Controller/GameApiController.php index 57ba364..d37fdc6 100644 --- a/src/Game/Controller/GameApiController.php +++ b/src/Game/Controller/GameApiController.php @@ -3,7 +3,10 @@ declare(strict_types=1); namespace App\Game\Controller; +use App\Game\Entity\Session; +use App\Game\Enum\SessionStatus; use App\Game\Service\GameResponseService; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -15,7 +18,9 @@ final class GameApiController extends AbstractController { public function __construct( - protected GameResponseService $gameResponseService) { + protected GameResponseService $gameResponseService, + private EntityManagerInterface $entityManager + ) { } @@ -29,6 +34,30 @@ final class GameApiController extends AbstractController ]); } + #[Route('/check-finished/{session}', name: 'check_finished', methods: ['POST'])] + public function checkFinished(Session $session): JsonResponse + { + $now = (new \DateTime())->getTimestamp(); + $isFinished = false; + + if ($session->getStatus() === SessionStatus::PLAYING) { + if ($session->getTimer() !== null && $now >= $session->getTimer()) { + $session->setStatus(SessionStatus::LOST); + $this->entityManager->persist($session); + $this->entityManager->flush(); + $isFinished = true; + } + } elseif ($session->getStatus() === SessionStatus::LOST || $session->getStatus() === SessionStatus::WON) { + $isFinished = true; + } + + return $this->json([ + 'ok' => true, + 'finished' => $isFinished, + 'status' => $session->getStatus()->value, + ]); + } + #[Route('/message', name: 'message', methods: ['POST'])] public function message(Request $request): JsonResponse { diff --git a/src/Game/Controller/GameController.php b/src/Game/Controller/GameController.php index 1da07e3..b2edde8 100644 --- a/src/Game/Controller/GameController.php +++ b/src/Game/Controller/GameController.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Game\Controller; +use App\Game\Entity\Player; use App\Game\Entity\Session; use App\Game\Entity\SessionSetting; use App\Game\Enum\SessionSettingType; @@ -12,6 +13,7 @@ use App\Game\Repository\PlayerRepository; use App\Game\Repository\SessionRepository; use App\Game\Service\GameDashboardService; use App\Tech\Entity\User; +use App\Game\Service\PlayerService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\Request; @@ -25,7 +27,8 @@ final class GameController extends AbstractController { public function __construct( #[Autowire('%env(MERCURE_PUBLIC_URL)%')] - private string $mercurePublicUrl + private string $mercurePublicUrl, + private \Doctrine\ORM\EntityManagerInterface $entityManager ) { } @@ -153,10 +156,76 @@ final class GameController extends AbstractController } $screen = $player ? $player->getScreen() : 0; + $session_id = $session->getId(); return $this->render('game/index.html.twig', [ 'session' => $session, 'screen' => $screen, + 'session_id' => $session_id, ]); } + + #[Route(path: '/lost/{session}', name: 'game_lost', methods: ['GET', 'POST'])] + #[IsGranted(new Expression("is_granted('ROLE_PLAYER') or is_granted('ROLE_ADMIN')"))] + #[IsGranted('SESSION_VIEW', subject: 'session')] + public function lost( + Session $session, + Request $request, + Security $security, + PlayerService $playerService, + GameDashboardService $dashboardService + ): Response { + /** @var User $user */ + $user = $security->getUser(); + $player = $playerService->GetCurrentlyActiveAsPlayer($user); + + if ($request->isMethod('POST')) { + $difficulty = $request->request->get('difficulty'); + $entertaining = $request->request->get('entertaining'); + $theme = $request->request->get('theme'); + $feedback = $request->request->get('feedback'); + + // Save feedback + if ($player) { + $this->saveFeedback($session, $player, $difficulty, $entertaining, $theme, $feedback); + $this->addFlash('success', 'Thank you for your feedback!'); + return $this->redirectToRoute('game_dashboard'); + } + } + + return $this->render('game/lost.html.twig', [ + 'session' => $session, + ]); + } + + private function saveFeedback(Session $session, Player $player, $difficulty, $entertaining, $theme, $feedback): void + { + $settings = [ + SessionSettingType::FEEDBACK_DIFFICULTY, + SessionSettingType::FEEDBACK_ENTERTAINING, + SessionSettingType::FEEDBACK_THEME, + SessionSettingType::FEEDBACK_TEXT, + ]; + + $values = [ + $difficulty, + $entertaining, + $theme, + $feedback, + ]; + + foreach ($settings as $index => $type) { + $value = $values[$index]; + if ($value === null || $value === '') continue; + + $setting = new SessionSetting(); + $setting->setSession($session); + $setting->setPlayer($player); + $setting->setName($type); + $setting->setValue((string)$value); + $this->entityManager->persist($setting); + } + + $this->entityManager->flush(); + } } diff --git a/src/Game/Enum/SessionSettingType.php b/src/Game/Enum/SessionSettingType.php index 7788245..c700b91 100644 --- a/src/Game/Enum/SessionSettingType.php +++ b/src/Game/Enum/SessionSettingType.php @@ -70,4 +70,8 @@ enum SessionSettingType: string case READY_AT_FOR_PLAYER8 = 'ReadyAtForPlayer8'; case READY_AT_FOR_PLAYER9 = 'ReadyAtForPlayer9'; case READY_AT_FOR_PLAYER10 = 'ReadyAtForPlayer10'; + case FEEDBACK_DIFFICULTY = 'FeedbackDifficulty'; + case FEEDBACK_ENTERTAINING = 'FeedbackEntertaining'; + case FEEDBACK_THEME = 'FeedbackTheme'; + case FEEDBACK_TEXT = 'FeedbackText'; } diff --git a/src/Game/Service/GameDashboardService.php b/src/Game/Service/GameDashboardService.php index db8c7b1..0a7fe19 100644 --- a/src/Game/Service/GameDashboardService.php +++ b/src/Game/Service/GameDashboardService.php @@ -391,6 +391,16 @@ final class GameDashboardService if ($readyPlayersCount === $numPlayers) { $session->setStatus(SessionStatus::PLAYING); + + // Set the end timer + $game = $session->getGame(); + /** @var \App\Game\Repository\GameSettingRepository $gameSettingRepo */ + $gameSettingRepo = $this->entityManager->getRepository(\App\Game\Entity\GameSetting::class); + $totalTimeSetting = $gameSettingRepo->getSetting($game, \App\Game\Enum\GameSettingType::TOTAL_TIME); + $totalTime = $totalTimeSetting ? (int)$totalTimeSetting->getValue() : 3600; // Default to 60 minutes if not set + + $session->setTimer((new \DateTime())->getTimestamp() + $totalTime); + $this->entityManager->persist($session); // Clean up ready settings diff --git a/templates/game/index.html.twig b/templates/game/index.html.twig index 6c098cb..15a6ee9 100644 --- a/templates/game/index.html.twig +++ b/templates/game/index.html.twig @@ -18,12 +18,14 @@ data-topic="{{ (mercure_topic_base ~ '/game/hub-' ~ session.id)|e('html_attr') }}" data-api-ping-url="{{ path('game_api_ping')|e('html_attr') }}" data-api-echo-url="{{ path('game_api_message')|e('html_attr') }}" + data-api-check-finished-url="{{ path('game_api_check_finished', {session: session.id})|e('html_attr') }}" + data-lost-url="{{ path('game_lost', {session: session.id})|e('html_attr') }}" data-screen="{{ screen|e('html_attr') }}" style="display:none"> -
- 00:30:00 +
+ --:--:--
diff --git a/templates/game/lost.html.twig b/templates/game/lost.html.twig new file mode 100644 index 0000000..f705eb8 --- /dev/null +++ b/templates/game/lost.html.twig @@ -0,0 +1,127 @@ +{% extends 'base.html.twig' %} + +{% block title %}Game Lost - {{ session.game.name }}{% endblock %} + +{% block stylesheets %} + {{ parent() }} + +{% endblock %} + +{% block body %} +
+
+
+
+
+

Game Over - Time's Up!

+
+
+

{{ session.game.name }}

+
+ +
+

+ The screens flickered one last time before going completely dark. The hum of the servers ceased, replaced by an eerie silence. + The server seems to have shut itself down before the AI virus could complete its task in decoding the undercover agents. +

+

+ In the days after some people are arrested for sabotaging the agency, but in the end nobody is convicted for the actions. + Lets hope the agents which were undercover are safe and the information needed is still saved to put the other criminals behind bars. +

+
+ +
+
Support the Developer
+

If you enjoyed the experience (even if you lost!), please consider a small donation to help me create more games.

+ + Donate via PayPal + +
+ + +
+
+
+
+
+{% endblock %}