Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7257c51bdf | ||
|
|
641573842c | ||
|
|
abc3712d97 | ||
|
|
66cb356955 | ||
|
|
ffd20f5535 | ||
|
|
1b52f80448 | ||
|
|
b965f0f085 | ||
|
|
c4c989db4c | ||
|
|
37507bd169 | ||
|
|
732148a533 | ||
|
|
50d7ce745c | ||
|
|
a475c1a268 | ||
|
|
1ae0f651d8 | ||
|
|
8e933631b2 | ||
|
|
f8746207e6 | ||
|
|
e42966e618 | ||
|
|
e4976e51fa | ||
|
|
65070688ec | ||
|
|
e54c870f05 | ||
|
|
f5aabafcc5 | ||
|
|
de4b7bca6a | ||
|
|
c6adb00219 | ||
|
|
5f6ea89179 | ||
|
|
c384fc3dd5 | ||
|
|
78b570bd75 | ||
|
|
74f52db002 | ||
|
|
56590a901f | ||
|
|
49045bc696 | ||
|
|
28ee969c29 | ||
|
|
7230520551 | ||
|
|
59c1d97d84 | ||
|
|
3de1e907f2 | ||
|
|
91c0c3e6d1 | ||
|
|
8e3807e079 | ||
|
|
10c3dbc066 | ||
|
|
af13be2196 | ||
|
|
c0aa2ad44e | ||
|
|
d3cff2ef58 | ||
|
|
24b76f9700 | ||
|
|
03994dd54e | ||
|
|
499e699dbd | ||
|
|
c8b0a6e966 | ||
|
|
5b6bfaf5ad | ||
|
|
af61a3b920 | ||
|
|
0d6628e7c9 | ||
|
|
534175efb3 |
@@ -0,0 +1,73 @@
|
|||||||
|
# In all environments, the following files are loaded if they exist,
|
||||||
|
# the latter taking precedence over the former:
|
||||||
|
#
|
||||||
|
# * .env contains default values for the environment variables needed by the app
|
||||||
|
# * .env.local uncommitted file with local overrides
|
||||||
|
# * .env.$APP_ENV committed environment-specific defaults
|
||||||
|
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||||
|
#
|
||||||
|
# Real environment variables win over .env files.
|
||||||
|
#
|
||||||
|
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||||
|
# https://symfony.com/doc/current/configuration/secrets.html
|
||||||
|
#
|
||||||
|
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||||
|
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||||
|
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||||
|
#
|
||||||
|
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
||||||
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||||
|
DB_DRIVER=pdo_mysql
|
||||||
|
DB_SERVER_VERSION=8.0.32
|
||||||
|
DB_CHARSET=utf8mb4
|
||||||
|
DB_USER=escapepage
|
||||||
|
DB_PASSWORD="b.0nqrxJ/D*Luf9N"
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_NAME=escapepage
|
||||||
|
DATABASE_URL="${DB_DRIVER}://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?serverVersion=${DB_SERVER_VERSION}&charset=${DB_CHARSET}"
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
###> symfony/messenger ###
|
||||||
|
# Choose one of the transports below
|
||||||
|
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||||
|
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||||
|
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||||
|
###< symfony/messenger ###
|
||||||
|
|
||||||
|
###> symfony/mailer ###
|
||||||
|
# Development: use Mailpit (docker compose override provides service `mailer` on port 1025)
|
||||||
|
MAILER_DSN=smtp://mailer:1025
|
||||||
|
# Production/Stage (uncomment and set SENDGRID_API_KEY in real env or secrets):
|
||||||
|
# MAILER_DSN=sendgrid+api://%env(SENDGRID_API_KEY)%
|
||||||
|
# Alternatively, via SMTP (no extra package needed):
|
||||||
|
# MAILER_DSN="smtp://apikey:%env(SENDGRID_API_KEY)%@smtp.sendgrid.net:587?encryption=tls"
|
||||||
|
# Optional default sender (used by test command if --from not passed):
|
||||||
|
# MAILER_FROM=no-reply@your-domain.tld
|
||||||
|
# SENDGRID_API_KEY=your_real_key_goes_here # Do NOT commit this; set in .env.local or deployment env
|
||||||
|
###< symfony/mailer ###
|
||||||
|
|
||||||
|
###> symfony/sendgrid-mailer ###
|
||||||
|
# MAILER_DSN=sendgrid://KEY@default
|
||||||
|
###< symfony/sendgrid-mailer ###
|
||||||
|
|
||||||
|
###> mercure ###
|
||||||
|
# Internal hub URL used by the PHP app (reachable from the php container)
|
||||||
|
MERCURE_URL=http://mercure/.well-known/mercure
|
||||||
|
# Public hub URL used by browsers
|
||||||
|
MERCURE_PUBLIC_URL=http://localhost:8090/.well-known/mercure
|
||||||
|
# Shared secret for signing JWTs (dev only). In prod, set via real env/secrets.
|
||||||
|
MERCURE_JWT_SECRET=!ChangeThisMercureJWTSignedBySymfonySecretKey!
|
||||||
|
# CORS allowed origins (default)
|
||||||
|
MERCURE_CORS_ALLOWED_ORIGINS=http://localhost:8080
|
||||||
|
# Base URL for Mercure topics.
|
||||||
|
MERCURE_TOPIC_BASE=https://escapepage.dev
|
||||||
|
###< mercure ###
|
||||||
@@ -131,6 +131,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
const screen = cfgEl.dataset.screen;
|
const screen = cfgEl.dataset.screen;
|
||||||
const apiPingUrl = cfgEl.dataset.apiPingUrl;
|
const apiPingUrl = cfgEl.dataset.apiPingUrl;
|
||||||
const apiEchoUrl = cfgEl.dataset.apiEchoUrl;
|
const apiEchoUrl = cfgEl.dataset.apiEchoUrl;
|
||||||
|
const apiCheckFinishedUrl = cfgEl.dataset.apiCheckFinishedUrl;
|
||||||
|
const lostUrl = cfgEl.dataset.lostUrl;
|
||||||
|
|
||||||
if (mercurePublicUrl && topic) {
|
if (mercurePublicUrl && topic) {
|
||||||
subscribeToMercure(mercurePublicUrl, topic, screen);
|
subscribeToMercure(mercurePublicUrl, topic, screen);
|
||||||
@@ -138,6 +140,52 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
console.warn('[Mercure][game1] Missing data attributes on #mercure-config');
|
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
|
// Demo API calls
|
||||||
try {
|
try {
|
||||||
if (apiPingUrl) {
|
if (apiPingUrl) {
|
||||||
|
|||||||
+3
-3
@@ -7,11 +7,11 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
# Uncomment the following line to disable HTTPS,
|
# Uncomment the following line to disable HTTPS,
|
||||||
#SERVER_NAME: ':80'
|
#SERVER_NAME: ':80'
|
||||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureJWTSignedBySymfonySecretKey!'
|
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET}
|
||||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureJWTSignedBySymfonySecretKey!'
|
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET}
|
||||||
# Set the URL of your Symfony project (without trailing slash!) as value of the cors_origins directive
|
# Set the URL of your Symfony project (without trailing slash!) as value of the cors_origins directive
|
||||||
MERCURE_EXTRA_DIRECTIVES: |
|
MERCURE_EXTRA_DIRECTIVES: |
|
||||||
cors_origins http://localhost:8080
|
cors_origins ${MERCURE_CORS_ALLOWED_ORIGINS:-http://localhost:8080}
|
||||||
# Comment the following line to disable the development mode
|
# Comment the following line to disable the development mode
|
||||||
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
|
command: /usr/bin/caddy run --config /etc/caddy/dev.Caddyfile
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
doctrine:
|
doctrine:
|
||||||
dbal:
|
dbal:
|
||||||
url: '%env(resolve:DATABASE_URL)%'
|
# url: '%env(resolve:DATABASE_URL)%'
|
||||||
|
driver: '%env(DB_DRIVER)%'
|
||||||
|
server_version: '%env(DB_SERVER_VERSION)%'
|
||||||
|
host: '%env(DB_HOST)%'
|
||||||
|
port: '%env(DB_PORT)%'
|
||||||
|
user: '%env(DB_USER)%'
|
||||||
|
password: '%env(DB_PASSWORD)%'
|
||||||
|
dbname: '%env(DB_NAME)%'
|
||||||
|
charset: '%env(DB_CHARSET)%'
|
||||||
|
|
||||||
# IMPORTANT: You MUST configure your server version,
|
# IMPORTANT: You MUST configure your server version,
|
||||||
# either here or in the DATABASE_URL env var (see .env file)
|
# either here or in the DATABASE_URL env var (see .env file)
|
||||||
|
|||||||
+8
-8
@@ -63,12 +63,12 @@ services:
|
|||||||
container_name: escapepage-mercure
|
container_name: escapepage-mercure
|
||||||
environment:
|
environment:
|
||||||
SERVER_NAME: ":80"
|
SERVER_NAME: ":80"
|
||||||
MERCURE_PUBLISHER_JWT_KEY: '!ChangeThisMercureJWTSignedBySymfonySecretKey!'
|
MERCURE_PUBLISHER_JWT_KEY: ${MERCURE_JWT_SECRET}
|
||||||
MERCURE_SUBSCRIBER_JWT_KEY: '!ChangeThisMercureJWTSignedBySymfonySecretKey!'
|
MERCURE_SUBSCRIBER_JWT_KEY: ${MERCURE_JWT_SECRET}
|
||||||
MERCURE_CORS_ALLOWED_ORIGINS: http://localhost:8080
|
MERCURE_CORS_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS:-http://localhost:8080}
|
||||||
MERCURE_PUBLISH_ALLOWED_ORIGINS: http://localhost:8080
|
MERCURE_PUBLISH_ALLOWED_ORIGINS: ${MERCURE_CORS_ALLOWED_ORIGINS:-http://localhost:8080}
|
||||||
MERCURE_EXTRA_DIRECTIVES: |
|
MERCURE_EXTRA_DIRECTIVES: |
|
||||||
cors_origins http://localhost:8080
|
cors_origins ${MERCURE_CORS_ALLOWED_ORIGINS:-http://localhost:8080}
|
||||||
# Allow anonymous subscribers in dev only
|
# Allow anonymous subscribers in dev only
|
||||||
anonymous
|
anonymous
|
||||||
ports:
|
ports:
|
||||||
@@ -82,9 +82,9 @@ services:
|
|||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
container_name: escapepage-db
|
container_name: escapepage-db
|
||||||
environment:
|
environment:
|
||||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-app}
|
MYSQL_DATABASE: ${DB_NAME:-app}
|
||||||
MYSQL_USER: ${MYSQL_USER:-app}
|
MYSQL_USER: ${DB_USER:-app}
|
||||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-!ChangeMe!}
|
MYSQL_PASSWORD: ${DB_PASSWORD:-!ChangeMe!}
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}"]
|
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p${MYSQL_ROOT_PASSWORD:-root}"]
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Game\Controller;
|
namespace App\Game\Controller;
|
||||||
|
|
||||||
|
use App\Game\Entity\Session;
|
||||||
|
use App\Game\Enum\SessionStatus;
|
||||||
use App\Game\Service\GameResponseService;
|
use App\Game\Service\GameResponseService;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -15,7 +18,9 @@ final class GameApiController extends AbstractController
|
|||||||
{
|
{
|
||||||
|
|
||||||
public function __construct(
|
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'])]
|
#[Route('/message', name: 'message', methods: ['POST'])]
|
||||||
public function message(Request $request): JsonResponse
|
public function message(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Game\Controller;
|
namespace App\Game\Controller;
|
||||||
|
|
||||||
|
use App\Game\Entity\Player;
|
||||||
use App\Game\Entity\Session;
|
use App\Game\Entity\Session;
|
||||||
use App\Game\Entity\SessionSetting;
|
use App\Game\Entity\SessionSetting;
|
||||||
use App\Game\Enum\SessionSettingType;
|
use App\Game\Enum\SessionSettingType;
|
||||||
@@ -12,6 +13,7 @@ use App\Game\Repository\PlayerRepository;
|
|||||||
use App\Game\Repository\SessionRepository;
|
use App\Game\Repository\SessionRepository;
|
||||||
use App\Game\Service\GameDashboardService;
|
use App\Game\Service\GameDashboardService;
|
||||||
use App\Tech\Entity\User;
|
use App\Tech\Entity\User;
|
||||||
|
use App\Game\Service\PlayerService;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@@ -25,7 +27,8 @@ final class GameController extends AbstractController
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
#[Autowire('%env(MERCURE_PUBLIC_URL)%')]
|
#[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;
|
$screen = $player ? $player->getScreen() : 0;
|
||||||
|
$session_id = $session->getId();
|
||||||
|
|
||||||
return $this->render('game/index.html.twig', [
|
return $this->render('game/index.html.twig', [
|
||||||
'session' => $session,
|
'session' => $session,
|
||||||
'screen' => $screen,
|
'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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,4 +70,8 @@ enum SessionSettingType: string
|
|||||||
case READY_AT_FOR_PLAYER8 = 'ReadyAtForPlayer8';
|
case READY_AT_FOR_PLAYER8 = 'ReadyAtForPlayer8';
|
||||||
case READY_AT_FOR_PLAYER9 = 'ReadyAtForPlayer9';
|
case READY_AT_FOR_PLAYER9 = 'ReadyAtForPlayer9';
|
||||||
case READY_AT_FOR_PLAYER10 = 'ReadyAtForPlayer10';
|
case READY_AT_FOR_PLAYER10 = 'ReadyAtForPlayer10';
|
||||||
|
case FEEDBACK_DIFFICULTY = 'FeedbackDifficulty';
|
||||||
|
case FEEDBACK_ENTERTAINING = 'FeedbackEntertaining';
|
||||||
|
case FEEDBACK_THEME = 'FeedbackTheme';
|
||||||
|
case FEEDBACK_TEXT = 'FeedbackText';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -391,6 +391,16 @@ final class GameDashboardService
|
|||||||
|
|
||||||
if ($readyPlayersCount === $numPlayers) {
|
if ($readyPlayersCount === $numPlayers) {
|
||||||
$session->setStatus(SessionStatus::PLAYING);
|
$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);
|
$this->entityManager->persist($session);
|
||||||
|
|
||||||
// Clean up ready settings
|
// Clean up ready settings
|
||||||
|
|||||||
@@ -18,12 +18,14 @@
|
|||||||
data-topic="{{ (mercure_topic_base ~ '/game/hub-' ~ session.id)|e('html_attr') }}"
|
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-ping-url="{{ path('game_api_ping')|e('html_attr') }}"
|
||||||
data-api-echo-url="{{ path('game_api_message')|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') }}"
|
data-screen="{{ screen|e('html_attr') }}"
|
||||||
style="display:none">
|
style="display:none">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="game-timer">
|
<div id="game-timer" data-end-time="{{ session.timer }}">
|
||||||
00:30:00
|
--:--:--
|
||||||
</div>
|
</div>
|
||||||
<div id="message-container">
|
<div id="message-container">
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Game Lost - {{ session.game.name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
{{ parent() }}
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-color: #444;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
.form-label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.story-container {
|
||||||
|
font-style: italic;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.donation-section {
|
||||||
|
background-color: #333;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.form-range::-webkit-slider-runnable-track {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
.form-range::-moz-range-track {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
.form-range::-webkit-slider-thumb {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
.form-range::-moz-range-thumb {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div class="container mt-5 mb-5">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card shadow-lg">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h3 class="card-title mb-0">Game Over - Time's Up!</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h4>{{ session.game.name }}</h4>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="story-container">
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="donation-section">
|
||||||
|
<h5>Support the Developer</h5>
|
||||||
|
<p>If you enjoyed the experience (even if you lost!), please consider a small donation to help me create more games.</p>
|
||||||
|
<a href="https://www.paypal.com/donate?hosted_button_id=X9X8KB6R6GMRU" target="_blank" class="btn btn-primary">
|
||||||
|
<i class="bi bi-paypal"></i> Donate via PayPal
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="feedback-form mt-4">
|
||||||
|
<h5>Feedback</h5>
|
||||||
|
<form method="post">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="difficulty" class="form-label">How would you rate the difficulty? (<span id="difficulty-val">5</span>/10)</label>
|
||||||
|
<input type="range" class="form-range" min="1" max="10" step="1" id="difficulty" name="difficulty" value="5" oninput="document.getElementById('difficulty-val').innerText = this.value">
|
||||||
|
<div class="d-flex justify-content-between mt-1">
|
||||||
|
<small class="text-muted">Absolutely not</small>
|
||||||
|
-
|
||||||
|
<small class="text-muted">Absolutely</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="entertaining" class="form-label">How entertaining was it? (<span id="entertaining-val">5</span>/10)</label>
|
||||||
|
<input type="range" class="form-range" min="1" max="10" step="1" id="entertaining" name="entertaining" value="5" oninput="document.getElementById('entertaining-val').innerText = this.value">
|
||||||
|
<div class="d-flex justify-content-between mt-1">
|
||||||
|
<small class="text-muted">Absolutely not</small>
|
||||||
|
-
|
||||||
|
<small class="text-muted">Absolutely</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="theme" class="form-label">How was the theme? (<span id="theme-val">5</span>/10)</label>
|
||||||
|
<input type="range" class="form-range" min="1" max="10" step="1" id="theme" name="theme" value="5" oninput="document.getElementById('theme-val').innerText = this.value">
|
||||||
|
<div class="d-flex justify-content-between mt-1">
|
||||||
|
<small class="text-muted">Absolutely not</small>
|
||||||
|
-
|
||||||
|
<small class="text-muted">Absolutely</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="feedback" class="form-label">Additional Feedback</label>
|
||||||
|
<textarea class="form-control" id="feedback" name="feedback" rows="4" placeholder="Tell us more about your experience..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
<button type="submit" class="btn btn-success">Submit Feedback & Return to Dashboard</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user