Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/autotag.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Bump version
on:
pull_request:
types:
- closed
branches:
- main

jobs:
build:
if: |
github.event.pull_request.merged == true &&
!contains(github.event.pull_request.labels.*.name, 'github_actions')
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: '0'

- name: Bump version and push tag
uses: anothrNick/github-tag-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_PREFIX: v
DEFAULT_BUMP: patch
29 changes: 29 additions & 0 deletions migrations/Version20260117120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Migration: Add allow_spectators column to room table for spectator mode support
*/
final class Version20260117120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add allow_spectators column to room table for spectator mode support';
}

public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE room ADD allow_spectators BOOLEAN NOT NULL DEFAULT FALSE');
}

public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE room DROP COLUMN allow_spectators');
}
}
15 changes: 14 additions & 1 deletion src/Api/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Domain\KillerSerializerInterface;
use App\Domain\KillerValidatorInterface;
use App\Domain\Player\Enum\PlayerStatus;
use App\Domain\Player\PlayerRepository;
use App\Domain\Room\Entity\Room;
use App\Domain\Room\RoomRepository;
use App\Domain\Room\RoomWorkflowTransitionInterface;
Expand All @@ -33,6 +34,7 @@ class RoomController extends AbstractController

public function __construct(
private readonly RoomRepository $roomRepository,
private readonly PlayerRepository $playerRepository,
private readonly PersistenceAdapterInterface $persistenceAdapter,
private readonly RoomWorkflowTransitionInterface $roomStatusTransitionUseCase,
private readonly SseInterface $hub,
Expand Down Expand Up @@ -83,7 +85,18 @@ public function createRoom(Request $request): JsonResponse
#[IsGranted(RoomVoter::VIEW_ROOM, subject: 'room', message: 'KILLER_VIEW_ROOM_UNAUTHORIZED')]
public function getRoom(Room $room): JsonResponse
{
return $this->json($room, Response::HTTP_OK, [], [AbstractNormalizer::GROUPS => 'get-room']);
/** @var User|null $user */
$user = $this->getUser();
$currentPlayer = $user !== null ? $this->playerRepository->getCurrentUserPlayer($user) : null;

// Spectators get limited view (game masters see everything despite having SPECTATING status)
$isSpectator = $currentPlayer !== null
&& $currentPlayer->getStatus() === PlayerStatus::SPECTATING
&& !$currentPlayer->isMaster();

$groups = $isSpectator ? ['get-room-spectator'] : ['get-room'];

return $this->json($room, Response::HTTP_OK, [], [AbstractNormalizer::GROUPS => $groups]);
}

#[Route('/{id}', name: 'patch_room', methods: [Request::METHOD_PATCH])]
Expand Down
14 changes: 13 additions & 1 deletion src/Api/Controller/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

namespace App\Api\Controller;

use App\Api\Exception\KillerBadRequestHttpException;
use App\Application\UseCase\Player\CreatePlayerUseCase;
use App\Domain\KillerSerializerInterface;
use App\Domain\KillerValidatorInterface;
use App\Domain\Player\Enum\PlayerStatus;
use App\Domain\Player\Event\PlayerChangedRoomEvent;
use App\Domain\Player\PlayerRepository;
use App\Domain\Room\RoomRepository;
Expand Down Expand Up @@ -145,6 +147,7 @@ public function patchUser(Request $request): JsonResponse
// Handle room change
if (array_key_exists('room', $data)) {
$newRoomId = $data['room'];
$joinAsSpectator = filter_var($data['spectate'] ?? false, FILTER_VALIDATE_BOOLEAN);

if ($newRoomId !== null) {
$newRoom = $this->roomRepository->findOneBy(['id' => $newRoomId]);
Expand All @@ -153,12 +156,21 @@ public function patchUser(Request $request): JsonResponse
throw $this->createNotFoundException('ROOM_NOT_FOUND');
}

// Validate spectator access
if ($joinAsSpectator && !$newRoom->isAllowSpectators()) {
throw new KillerBadRequestHttpException('ROOM_SPECTATORS_NOT_ALLOWED');
}

$existingPlayer = $this->playerRepository->findPlayerByUserAndRoom($user, $newRoom);
$user->setRoom($newRoom);

// Create a new player for this user in the room if one doesn't exist
if ($existingPlayer === null) {
$this->createPlayerUseCase->execute($user, $newRoom);
$player = $this->createPlayerUseCase->execute($user, $newRoom);

if ($joinAsSpectator) {
$player->setStatus(PlayerStatus::SPECTATING);
}
}
}

Expand Down
31 changes: 24 additions & 7 deletions src/Domain/Player/Entity/Player.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,20 @@ class Player implements RecipientInterface
#[ORM\Id]
#[ORM\Column(type: 'integer', unique: true)]
#[ORM\GeneratedValue(strategy: 'SEQUENCE')]
#[Groups(['get-player', 'create-player', 'get-room', 'get-mission', 'me', 'publish-mercure'])]
#[Groups(['get-player', 'create-player', 'get-room', 'get-mission', 'me', 'publish-mercure', 'get-room-spectator'])]
private int $id;

#[ORM\Column(type: 'string', length: 255)]
#[Groups(['get-player', 'create-player', 'get-room', 'me', 'post-player', 'patch-player', 'publish-mercure'])]
#[Groups([
'get-player',
'create-player',
'get-room',
'me',
'post-player',
'patch-player',
'publish-mercure',
'get-room-spectator',
])]
#[Assert\Length(
min: 2,
max: 30,
Expand All @@ -53,7 +62,15 @@ class Player implements RecipientInterface
enumType: PlayerStatus::class,
options: ['default' => PlayerStatus::ALIVE],
)]
#[Groups(['get-player', 'create-player', 'get-room', 'me', 'patch-player', 'publish-mercure'])]
#[Groups([
'get-player',
'create-player',
'get-room',
'me',
'patch-player',
'publish-mercure',
'get-room-spectator',
])]
private PlayerStatus $status = PlayerStatus::ALIVE;

#[ORM\ManyToOne(targetEntity: Room::class, inversedBy: 'players')]
Expand All @@ -78,27 +95,27 @@ enumType: PlayerStatus::class,
private ?Mission $assignedMission = null;

#[ORM\Column(type: 'string', options: ['default' => self::DEFAULT_AVATAR])]
#[Groups(['me', 'get-room', 'post-player', 'create-player', 'get-player', 'patch-player'])]
#[Groups(['me', 'get-room', 'post-player', 'create-player', 'get-player', 'patch-player', 'get-room-spectator'])]
private string $avatar = self::DEFAULT_AVATAR;

#[ORM\Column(type: 'string', options: ['default' => ''])]
#[Groups(['me', 'post-player', 'create-player', 'patch-player'])]
private string $expoPushToken = '';

#[ORM\Column(type: 'integer', options: ['default' => 0])]
#[Groups(['me', 'get-player', 'get-room'])]
#[Groups(['me', 'get-player', 'get-room', 'get-room-spectator'])]
private int $points = 0;

#[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['me'])]
private bool $missionSwitchUsed = false;

#[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['me', 'get-player', 'get-room'])]
#[Groups(['me', 'get-player', 'get-room', 'get-room-spectator'])]
private bool $isAdmin = false;

#[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['me', 'get-player', 'get-room'])]
#[Groups(['me', 'get-player', 'get-room', 'get-room-spectator'])]
private bool $isMaster = false;

public function __construct()
Expand Down
30 changes: 23 additions & 7 deletions src/Domain/Room/Entity/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,26 @@ class Room
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(RoomIdGenerator::class)]
#[Assert\Length(exactly: 5)]
#[Groups(['get-player', 'get-room', 'get-mission', 'me', 'publish-mercure', 'patch-player'])]
#[Groups(['get-player', 'get-room', 'get-mission', 'me', 'publish-mercure', 'patch-player', 'get-room-spectator'])]
private string $id;

#[ORM\Column(type: 'string', length: 255)]
#[Groups(['get-room', 'me', 'patch-room', 'publish-mercure'])]
#[Groups(['get-room', 'me', 'patch-room', 'publish-mercure', 'get-room-spectator'])]
#[Assert\Length(min: 2, max: 50, minMessage: 'TOO_SHORT_CONTENT', maxMessage: 'TOO_LONG_CONTENT')]
private string $name;

#[ORM\Column(type: 'string', length: 255, options: ['default' => self::PENDING])]
#[Groups(['get-room', 'me', 'publish-mercure'])]
#[Groups(['get-room', 'me', 'publish-mercure', 'get-room-spectator'])]
private string $status = self::PENDING;

/** @var Collection<int, Player> */
#[ORM\OneToMany(mappedBy: 'room', targetEntity: Player::class, fetch: 'EAGER')]
#[Assert\Unique]
#[Groups(['get-room', 'publish-mercure'])]
#[Groups(['get-room', 'publish-mercure', 'get-room-spectator'])]
private Collection $players;

#[ORM\OneToOne(targetEntity: Player::class)]
#[Groups(['get-room', 'publish-mercure'])]
#[Groups(['get-room', 'publish-mercure', 'get-room-spectator'])]
private ?Player $admin = null;

#[ORM\Column(type: 'datetime_immutable')]
Expand All @@ -69,13 +69,17 @@ class Room
private Collection $secondaryMissions;

#[ORM\ManyToOne(targetEntity: Player::class)]
#[Groups(['get-room', 'publish-mercure'])]
#[Groups(['get-room', 'publish-mercure', 'get-room-spectator'])]
private ?Player $winner = null;

#[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['get-room', 'publish-mercure', 'me'])]
#[Groups(['get-room', 'publish-mercure', 'me', 'get-room-spectator'])]
private bool $isGameMastered = false;

#[ORM\Column(type: 'boolean', options: ['default' => false])]
#[Groups(['get-room', 'patch-room', 'publish-mercure', 'get-room-spectator'])]
private bool $allowSpectators = false;

public function __construct()
{
$this->players = new ArrayCollection();
Expand Down Expand Up @@ -293,6 +297,18 @@ public function popSecondaryMission(): ?Mission
return $mission;
}

public function isAllowSpectators(): bool
{
return $this->allowSpectators;
}

public function setAllowSpectators(bool $allowSpectators): self
{
$this->allowSpectators = $allowSpectators;

return $this;
}

public function __toString(): string
{
return $this->getId();
Expand Down
Loading
Loading