Wire up decode puzzle chain and lock terminal on file removal
Connects the previously-disconnected /decode command to per-player messages: player 1 unlocks sudo for everyone, player 2 unlocks a scan command that reveals which files the AI virus has locked, player 3 learns those files should be removed. Also adds a retaliation mechanic: successfully removing a file locks the player's terminal. The AI virus locks it out in red immediately; the mainframe reveals a 12-character recovery code in green after 30 seconds, which can be submitted via /unlock to restore access early, otherwise the terminal auto-recovers after 45 seconds.
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Game\Service;
|
||||
use App\Game\Enum\DecodeMessage;
|
||||
use App\Game\Enum\SessionSettingType;
|
||||
use App\Game\Entity\Player;
|
||||
use App\Game\Entity\Session;
|
||||
use App\Game\Entity\SessionSetting;
|
||||
use App\Game\Repository\SessionSettingRepository;
|
||||
use App\Tech\Entity\User;
|
||||
@@ -15,6 +16,10 @@ use Symfony\Component\Mercure\Update;
|
||||
|
||||
class GameResponseService
|
||||
{
|
||||
private const LOCK_REVEAL_AFTER_SECONDS = 30;
|
||||
private const LOCK_DURATION_SECONDS = 45;
|
||||
private const LOCK_PASSCODE_LENGTH = 12;
|
||||
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private PlayerService $playerService,
|
||||
@@ -25,6 +30,25 @@ class GameResponseService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Public, code-free view of a player's current lock state, safe to expose on page load
|
||||
* (e.g. so the UI can restore the lock banner/countdown after a refresh).
|
||||
*/
|
||||
public function getPublicLockState(Player $player): ?array
|
||||
{
|
||||
$lock = $this->getLockState($player);
|
||||
|
||||
if ($lock === null || time() >= $lock['unlockAt']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'lockedAt' => $lock['lockedAt'],
|
||||
'revealAt' => $lock['revealAt'],
|
||||
'unlockAt' => $lock['unlockAt'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getGameResponse(string $raw) : array
|
||||
{
|
||||
$info = json_decode($raw, true);
|
||||
@@ -47,12 +71,14 @@ class GameResponseService
|
||||
|
||||
$this->logSessionActivity($player, 'PLAYER: ' . $message);
|
||||
|
||||
$data = [];
|
||||
$data = $this->handleLockedPlayer($message, $player);
|
||||
|
||||
if(str_starts_with($message, '/')) {
|
||||
$data = $this->checkGameCommando($message, $player);
|
||||
} else {
|
||||
$data = $this->checkConsoleCommando($message, $player);
|
||||
if ($data === null) {
|
||||
if (str_starts_with($message, '/')) {
|
||||
$data = $this->checkGameCommando($message, $player);
|
||||
} else {
|
||||
$data = $this->checkConsoleCommando($message, $player);
|
||||
}
|
||||
}
|
||||
|
||||
$responseLog = '';
|
||||
@@ -202,7 +228,19 @@ class GameResponseService
|
||||
return ['result' => ['You are not allowed to remove this file.']];
|
||||
|
||||
$this->playerService->addDeletedFileToSession($player, $fullPath);
|
||||
return ['result' => ['File removed: ' . $filename]];
|
||||
$lock = $this->triggerLock($player);
|
||||
|
||||
return [
|
||||
'result' => [
|
||||
'File removed: ' . $filename,
|
||||
'AI VIRUS: Intrusion detected. Locking down your terminal...',
|
||||
],
|
||||
'messageType' => 'virus',
|
||||
'locked' => true,
|
||||
'lockedAt' => $lock['lockedAt'],
|
||||
'revealAt' => $lock['revealAt'],
|
||||
'unlockAt' => $lock['unlockAt'],
|
||||
];
|
||||
case 'sudo':
|
||||
if(!in_array('sudo', $rechten))
|
||||
return ['result' => ['Unknown command']];
|
||||
@@ -211,6 +249,17 @@ class GameResponseService
|
||||
$message = implode(' ', $messagePart);
|
||||
|
||||
return $this->checkConsoleCommando($message, $player, true);
|
||||
case 'scan':
|
||||
if(!in_array('scan', $rechten))
|
||||
return ['result' => ['Unknown command']];
|
||||
|
||||
$result = ['Running quarantine scan...', 'Locked files detected:'];
|
||||
foreach ($this->getLockedFiles() as $lockedFile) {
|
||||
$result[] = ' ' . $lockedFile;
|
||||
}
|
||||
$result[] = 'These files are protected by the AI virus. Use sudo rm {file} to remove them.';
|
||||
|
||||
return ['result' => $result];
|
||||
default:
|
||||
return ['result' => ['Unknown command']];
|
||||
}
|
||||
@@ -287,6 +336,12 @@ class GameResponseService
|
||||
$messages[] = ' USAGE: sudo {command}';
|
||||
$messages[] = '';
|
||||
break;
|
||||
case 'scan':
|
||||
$messages[] = 'scan';
|
||||
$messages[] = ' Runs a quarantine scan that reveals which files are locked by the AI virus.';
|
||||
$messages[] = ' USAGE: scan';
|
||||
$messages[] = '';
|
||||
break;
|
||||
case 'verify':
|
||||
$messages[] = '/verify';
|
||||
$messages[] = ' You can verify yourself by using this command.';
|
||||
@@ -470,24 +525,212 @@ class GameResponseService
|
||||
}
|
||||
}
|
||||
|
||||
private function handleDecodeMessage(string $message, Player $player)
|
||||
private function handleDecodeMessage(string $message, Player $player): string
|
||||
{
|
||||
$userNumber = $player->getScreen();
|
||||
|
||||
preg_match('/\d+/', $message, $matches);
|
||||
preg_match('/\d/', $message, $matches);
|
||||
$num = $matches[0] ?? null;
|
||||
$randomString = $this->generateRandomString(250, 500);
|
||||
|
||||
if(is_null($num) || $num != $userNumber)
|
||||
if (is_null($num) || (int)$num !== $userNumber) {
|
||||
return $randomString;
|
||||
}
|
||||
|
||||
foreach (DecodeMessage::cases() as $decodeMessage) {
|
||||
if ($decodeMessage->name === $message) {
|
||||
return $decodeMessage->value;
|
||||
$decodeMessage = match ($userNumber) {
|
||||
1 => DecodeMessage::PLAYER_1,
|
||||
2 => DecodeMessage::PLAYER_2,
|
||||
3 => DecodeMessage::PLAYER_3,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($decodeMessage === null) {
|
||||
return $randomString;
|
||||
}
|
||||
|
||||
if ($decodeMessage === DecodeMessage::PLAYER_1) {
|
||||
$this->grantRightToAllPlayers($player->getSession(), 'sudo');
|
||||
}
|
||||
|
||||
if ($decodeMessage === DecodeMessage::PLAYER_2) {
|
||||
$this->grantRightToAllPlayers($player->getSession(), 'scan');
|
||||
}
|
||||
|
||||
return $decodeMessage->value;
|
||||
}
|
||||
|
||||
private function grantRightToAllPlayers(Session $session, string $right): void
|
||||
{
|
||||
$updated = false;
|
||||
|
||||
foreach ($session->getPlayers() as $sessionPlayer) {
|
||||
$rightsSettingName = SessionSettingType::tryFrom('RightsForPlayer' . $sessionPlayer->getScreen());
|
||||
if (!$rightsSettingName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$setting = $this->sessionSettingRepository->getSetting($session, $rightsSettingName, $sessionPlayer);
|
||||
if (!$setting) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rights = json_decode($setting->getValue() ?? '[]', true) ?? [];
|
||||
if (!in_array($right, $rights)) {
|
||||
$rights[] = $right;
|
||||
$setting->setValue(json_encode($rights));
|
||||
$this->entityManager->persist($setting);
|
||||
$updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $randomString;
|
||||
if ($updated) {
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if the player isn't locked (or their lock already expired), otherwise
|
||||
* a result array to short-circuit normal command handling.
|
||||
*/
|
||||
private function handleLockedPlayer(string $message, Player $player): ?array
|
||||
{
|
||||
$lock = $this->getLockState($player);
|
||||
|
||||
if ($lock === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
|
||||
if ($now >= $lock['unlockAt']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$trimmed = trim($message);
|
||||
|
||||
if (stripos($trimmed, '/unlock ') === 0) {
|
||||
$attempt = trim(substr($trimmed, 8));
|
||||
|
||||
if ($now >= $lock['revealAt'] && hash_equals($lock['code'], $attempt)) {
|
||||
$this->setLockUnlockAt($player, $now);
|
||||
|
||||
return [
|
||||
'result' => ['ACCESS RESTORED. Welcome back, agent.'],
|
||||
'messageType' => 'mainframe',
|
||||
'locked' => false,
|
||||
];
|
||||
}
|
||||
|
||||
return $this->lockStatusResponse('ACCESS DENIED: Incorrect passcode.', 'virus', $lock);
|
||||
}
|
||||
|
||||
if ($now >= $lock['revealAt']) {
|
||||
return $this->lockStatusResponse(
|
||||
'MAINFRAME: Recovery code acquired: ' . $lock['code'] . '. Use /unlock ' . $lock['code'] . ' to restore access.',
|
||||
'mainframe',
|
||||
$lock
|
||||
);
|
||||
}
|
||||
|
||||
return $this->lockStatusResponse(
|
||||
'AI VIRUS: SYSTEM LOCKED. Countermeasures engaged. Stand by for mainframe recovery protocol.',
|
||||
'virus',
|
||||
$lock
|
||||
);
|
||||
}
|
||||
|
||||
private function lockStatusResponse(string $text, string $messageType, array $lock): array
|
||||
{
|
||||
return [
|
||||
'result' => [$text],
|
||||
'messageType' => $messageType,
|
||||
'locked' => true,
|
||||
'lockedAt' => $lock['lockedAt'],
|
||||
'revealAt' => $lock['revealAt'],
|
||||
'unlockAt' => $lock['unlockAt'],
|
||||
];
|
||||
}
|
||||
|
||||
private function getLockState(Player $player): ?array
|
||||
{
|
||||
$settingName = SessionSettingType::tryFrom('LockForPlayer' . $player->getScreen());
|
||||
if (!$settingName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), $settingName, $player);
|
||||
if (!$setting || !$setting->getValue()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$state = json_decode($setting->getValue(), true);
|
||||
if (!is_array($state) || !isset($state['lockedAt'], $state['revealAt'], $state['unlockAt'], $state['code'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
private function triggerLock(Player $player): array
|
||||
{
|
||||
$now = time();
|
||||
$state = [
|
||||
'lockedAt' => $now,
|
||||
'revealAt' => $now + self::LOCK_REVEAL_AFTER_SECONDS,
|
||||
'unlockAt' => $now + self::LOCK_DURATION_SECONDS,
|
||||
'code' => $this->generatePasscode(),
|
||||
];
|
||||
|
||||
$settingName = SessionSettingType::tryFrom('LockForPlayer' . $player->getScreen());
|
||||
if ($settingName) {
|
||||
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), $settingName, $player);
|
||||
if (!$setting) {
|
||||
$setting = new SessionSetting();
|
||||
$setting->setSession($player->getSession());
|
||||
$setting->setPlayer($player);
|
||||
$setting->setName($settingName);
|
||||
}
|
||||
|
||||
$setting->setValue(json_encode($state));
|
||||
$this->entityManager->persist($setting);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
private function setLockUnlockAt(Player $player, int $unlockAt): void
|
||||
{
|
||||
$settingName = SessionSettingType::tryFrom('LockForPlayer' . $player->getScreen());
|
||||
if (!$settingName) {
|
||||
return;
|
||||
}
|
||||
|
||||
$setting = $this->sessionSettingRepository->getSetting($player->getSession(), $settingName, $player);
|
||||
if (!$setting || !$setting->getValue()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$state = json_decode($setting->getValue(), true);
|
||||
if (!is_array($state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$state['unlockAt'] = $unlockAt;
|
||||
$setting->setValue(json_encode($state));
|
||||
$this->entityManager->persist($setting);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
private function generatePasscode(): string
|
||||
{
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$charactersLength = strlen($characters);
|
||||
$code = '';
|
||||
for ($i = 0; $i < self::LOCK_PASSCODE_LENGTH; $i++) {
|
||||
$code .= $characters[random_int(0, $charactersLength - 1)];
|
||||
}
|
||||
return $code;
|
||||
}
|
||||
|
||||
private function generateRandomString(int $min, int $max): string
|
||||
@@ -770,13 +1013,16 @@ class GameResponseService
|
||||
if(in_array('sudo', $rights) || $sudo)
|
||||
return true;
|
||||
|
||||
$sudoFiles = [
|
||||
return !in_array($file, $this->getLockedFiles());
|
||||
}
|
||||
|
||||
private function getLockedFiles() : array
|
||||
{
|
||||
return [
|
||||
'/var/arrest/handle.sh',
|
||||
'/var/arrest/cell.sh',
|
||||
'/var/marriage/divorce.sh',
|
||||
];
|
||||
|
||||
return !in_array($file, $sudoFiles);
|
||||
}
|
||||
|
||||
private function fileExists(string $file, Player $player) : bool
|
||||
|
||||
Reference in New Issue
Block a user