diff --git a/assets/pregame.js b/assets/pregame.js new file mode 100644 index 0000000..bbea719 --- /dev/null +++ b/assets/pregame.js @@ -0,0 +1,349 @@ +/* Pregame (commandline tutorial) entry point built with Webpack Encore */ +import './styles/pregame.css'; + +function appendMessage(container, text, extraClass = '') { + const el = document.createElement('div'); + el.className = ('message ' + extraClass).trim(); + el.textContent = text; + container.appendChild(el); + window.scrollTo(0, document.body.scrollHeight); +} + +document.addEventListener('DOMContentLoaded', () => { + console.log('Pregame bundle loaded'); + + const messageContainer = document.getElementById('message-container'); + const inputField = document.getElementById('input-message'); + + if (!messageContainer || !inputField) { + return; + } + + const bootMessages = [ + ['Booting tutorial terminal...', 500], + ['Loading command line basics...', 800], + ['"Welcome agent. Lets go through the basics together.', 2000], + ['Let us start with the help command. Type help in the terminal (the input field at the bottom of the screen and press enter.', 500], + ]; + + // Same command help text as GameResponseService::getHelpCommand(), minus "scan". + const helpMessages = [ + 'Help function:', + '', + 'pwd', + ' This message will let you know what your current location is.', + ' It will show you the folder you are in so you can continue navigating the server.', + ' USAGE: pwd', + '', + 'cd', + ' Use cd to move to a different directory.', + ' You can go into a folder by using cd {foldername}, or a folder up by using "cd ..".', + ' Using cd / moves you to the root directory.', + ' USAGE: cd {directory}', + '', + 'ls', + ' To show all the files in the current directory, use ls.', + ' This will print the full list of directories and files of the current location on your screen.', + ' USAGE: ls', + '', + 'cat', + ' To read a file, use cat {filename}.', + ' This will print the full content of the file on the screen.', + ' USAGE: cat {filename}', + '', + 'rm', + ' Use rm to delete a file.', + ' Be careful with this command. It can not be undone and we do not want to lose any valuable data.', + ' USAGE: rm {filename}', + '', + 'sudo', + ' If you do not have enough rights to execute a command, you can use sudo to execute it as root.', + ' This is only possible for verified users. To verify yourself, use the /verify command.', + ' USAGE: sudo {command}', + '', + ]; + + // Narrative shown after the help output finishes, introducing the next step. + // Each entry is [delayBeforeShowing, text, cssClass]. + const helpFollowUp = [ + [2000, 'Well done, you now have a complete overview of the terminal commands this game uses.', 'message-mainframe'], + [500, 'Not all commands will be available immediately. But lets go through them one by one.', 'message-mainframe'], + [1000, 'Lets start with the command pwd. Enter this in the terminal (input field) now and press enter.', 'message-mainframe'], + ]; + + const pwdFollowUp = [ + [1000, 'You now know the location you are in on the terminal. /var/home/agent.', 'message-mainframe'], + [500, 'You can change directories with the cd command.', 'message-mainframe'], + [500, "If you type 'cd ..' in the terminal, you will go one directory up to /var/home", 'message-mainframe'], + [500, 'Try this now.', 'message-mainframe'], + ]; + + const cdFollowUp = [ + [1000, 'Good. Please check the directory you are in now.', 'message-mainframe'], + ]; + + const pwd2FollowUp = [ + [1000, 'As you can see, you are now in the /var/home directory', 'message-mainframe'], + [500, 'You can move between directories like this.', 'message-mainframe'], + ]; + + const wrongPwdMessage = "That is not the command which gives you the location you are in. You can check your current folder with the pwd command."; + + const lsIntro = [ + [1000, 'Now lets see what is actually in this directory. The ls command lists all files and folders in your current location.', 'message-mainframe'], + [500, 'Type ls now.', 'message-mainframe'], + ]; + + const lsListing = [ + 'agent,dir', + 'peter,dir', + 'james,dir', + 'william,dir', + 'system,dir', + ]; + + const lsFollowUp = [ + [1000, 'You can see several folders here, including your own: agent.', 'message-mainframe'], + [500, 'Lets go back to your own directory. Use cd agent to enter it.', 'message-mainframe'], + [250, 'Remember, cd /var/home/agent also works to get there directly from anywhere on the server.', 'message-mainframe'], + ]; + + const cdAgentFollowUp = [ + [1000, 'Lets check if you now really are in your own home folder.', 'message-mainframe'], + ]; + + const verifyAgentFollowUp = [ + [1000, "Well done, you're right back where you started.", 'message-mainframe'], + [500, 'Now lets move to the next command: cat.', 'message-mainframe'], + [500, 'First, lets see what is inside this folder.', 'message-mainframe'], + [500, 'Type ls to check the content of /var/home/agent.', 'message-mainframe'], + ]; + + const lsAgentListing = [ + 'HelloWorld.txt,file', + 'notes.txt,file', + 'schedule.txt,file', + 'contacts.db,file', + 'keycard.dat,file', + 'backup.zip,file', + ]; + + const lsAgentFinalListing = [ + 'notes.txt,file', + 'schedule.txt,file', + 'contacts.db,file', + 'keycard.dat,file', + 'backup.zip,file', + ]; + + const lsAgentFollowUp = [ + [1000, 'There it is: HelloWorld.txt, right there in the list.', 'message-mainframe'], + [500, 'Lets read what is inside. Type cat HelloWorld.txt to open the file.', 'message-mainframe'], + ]; + + const helloWorldContent = [ + 'Day 1 at the agency. My mentor says curiosity keeps you sharp -- and alive.', + "- I keep a spare set of shoelaces in my desk. You'd be surprised how handy that is.", + '- The coffee machine on this floor is cursed. Never trust it before 9am.', + '- Jared Williams owes me a favor after the incident with the elevator. He should not have forgotten that.', + '', + 'If you are reading this, agent: welcome to the team. Stay sharp out there.', + ]; + + const catFollowUp = [ + [1000, 'Nice work. cat is perfect for quickly reading any file on the server.', 'message-mainframe'], + ]; + + const rmIntro = [ + [1000, "There's one more command left in the basics: rm.", 'message-mainframe'], + [500, 'This command permanently deletes a file, so use it with caution.', 'message-mainframe'], + [500, 'Try removing HelloWorld.txt now. Type rm HelloWorld.txt.', 'message-mainframe'], + ]; + + const rmDeniedMessages = [ + 'Permission denied: HelloWorld.txt is protected against removal.', + 'You need sudo rights to remove this file.', + ]; + + const rmFollowUp = [ + [1000, 'sudo can technically be used for any command, at any moment.', 'message-mainframe'], + [500, 'But from a security standpoint, it is better not to use it unless it is truly necessary.', 'message-mainframe'], + [500, 'Only reach for sudo in extreme cases -- like this one, where a file is actively protected.', 'message-mainframe'], + [500, 'Try it now. Type sudo rm HelloWorld.txt.', 'message-mainframe'], + ]; + + const sudoRmFollowUp = [ + [1000, 'Well done. Since you used sudo, the protection was bypassed and the file was deleted.', 'message-mainframe'], + [500, 'Lets check the folder once more to confirm it is really gone. Type ls now.', 'message-mainframe'], + ]; + + const tutorialEndFollowUp = [ + [1000, 'As you can see, HelloWorld.txt is no longer in the list. It has been permanently removed.', 'message-mainframe'], + [500, 'That concludes the basics of the terminal, agent. You are ready for the real mission.', 'message-mainframe'], + [1000, 'Please be aware more commands can exist to help this mission succeed. These commands will show up in the help command. Good luck!', 'message-mainframe'] + ]; + + function playSequence(items, container, onDone) { + let i = 0; + const step = () => { + if (i >= items.length) { + if (onDone) onDone(); + return; + } + const [delay, text, cls] = items[i]; + i++; + setTimeout(() => { + appendMessage(container, text, cls); + step(); + }, delay); + }; + step(); + } + + // Tracks which command the tutorial is currently guiding the player towards. + let tutorialStage = 'help'; + + let currentMessageIndex = 0; + + const printNextMessage = () => { + if (currentMessageIndex < bootMessages.length) { + const [text, delay] = bootMessages[currentMessageIndex]; + appendMessage(messageContainer, text, 'message-mainframe'); + currentMessageIndex++; + setTimeout(printNextMessage, delay); + return; + } + + inputField.disabled = false; + inputField.focus(); + + inputField.addEventListener('keypress', (e) => { + if (e.key !== 'Enter') { + return; + } + + const value = inputField.value.trim(); + inputField.value = ''; + if (!value) { + return; + } + + appendMessage(messageContainer, value); + + if (value.toLowerCase() === 'help' || value.toLowerCase() === '/help') { + // No extraClass -> default "message" color, same as the real game's help output. + helpMessages.forEach((line) => appendMessage(messageContainer, line)); + + if (tutorialStage === 'help') { + playSequence(helpFollowUp, messageContainer, () => { + tutorialStage = 'pwd'; + }); + } + return; + } + + if (tutorialStage === 'pwd' && value.toLowerCase() === 'pwd') { + appendMessage(messageContainer, '/var/home/agent'); + playSequence(pwdFollowUp, messageContainer, () => { + tutorialStage = 'cd'; + }); + return; + } + + if (tutorialStage === 'cd' && value.toLowerCase() === 'cd ..') { + playSequence(cdFollowUp, messageContainer, () => { + tutorialStage = 'pwd2'; + }); + return; + } + + if (tutorialStage === 'pwd2') { + if (value.toLowerCase() === 'pwd') { + appendMessage(messageContainer, '/var/home'); + playSequence(pwd2FollowUp, messageContainer, () => { + playSequence(lsIntro, messageContainer, () => { + tutorialStage = 'ls'; + }); + }); + } else { + appendMessage(messageContainer, wrongPwdMessage, 'message-hint'); + } + return; + } + + if (tutorialStage === 'ls' && value.toLowerCase() === 'ls') { + lsListing.forEach((name) => appendMessage(messageContainer, name)); + playSequence(lsFollowUp, messageContainer, () => { + tutorialStage = 'cd-agent'; + }); + return; + } + + if ( + tutorialStage === 'cd-agent' && + (value.toLowerCase() === 'cd agent' || value.toLowerCase() === 'cd /var/home/agent') + ) { + playSequence(cdAgentFollowUp, messageContainer, () => { + tutorialStage = 'verify-agent'; + }); + return; + } + + if (tutorialStage === 'verify-agent' && value.toLowerCase() === 'pwd') { + appendMessage(messageContainer, '/var/home/agent'); + playSequence(verifyAgentFollowUp, messageContainer, () => { + tutorialStage = 'ls-agent'; + }); + return; + } + + if (tutorialStage === 'ls-agent' && value.toLowerCase() === 'ls') { + lsAgentListing.forEach((name) => appendMessage(messageContainer, name)); + playSequence(lsAgentFollowUp, messageContainer, () => { + tutorialStage = 'cat-file'; + }); + return; + } + + if (tutorialStage === 'cat-file' && value.toLowerCase() === 'cat helloworld.txt') { + helloWorldContent.forEach((line) => appendMessage(messageContainer, line)); + playSequence(catFollowUp, messageContainer, () => { + playSequence(rmIntro, messageContainer, () => { + tutorialStage = 'rm'; + }); + }); + return; + } + + if (tutorialStage === 'rm' && value.toLowerCase() === 'rm helloworld.txt') { + rmDeniedMessages.forEach((line) => appendMessage(messageContainer, line, 'message-hint')); + playSequence(rmFollowUp, messageContainer, () => { + tutorialStage = 'sudo-rm'; + }); + return; + } + + if (tutorialStage === 'sudo-rm' && value.toLowerCase() === 'sudo rm helloworld.txt') { + appendMessage(messageContainer, 'File removed: HelloWorld.txt'); + playSequence(sudoRmFollowUp, messageContainer, () => { + tutorialStage = 'ls-confirm'; + }); + return; + } + + if (tutorialStage === 'ls-confirm' && value.toLowerCase() === 'ls') { + lsAgentFinalListing.forEach((name) => appendMessage(messageContainer, name)); + playSequence(tutorialEndFollowUp, messageContainer, () => { + tutorialStage = 'complete'; + inputField.disabled = true; + }); + return; + } + + // Placeholder response until the rest of the tutorial content is defined. + appendMessage(messageContainer, 'Command not recognized yet.', 'message-hint'); + }); + }; + + printNextMessage(); +}); diff --git a/assets/styles/pregame.css b/assets/styles/pregame.css new file mode 100644 index 0000000..3245f7e --- /dev/null +++ b/assets/styles/pregame.css @@ -0,0 +1,71 @@ +/* Styles for the pregame (commandline tutorial) terminal, mirroring game1's look */ + +html::-webkit-scrollbar, +body::-webkit-scrollbar { + width: 1px; +} + +html::-webkit-scrollbar-track, +body::-webkit-scrollbar-track { + background: #000; +} + +html::-webkit-scrollbar-thumb, +body::-webkit-scrollbar-thumb { + background: #F00; +} + +body { + background-color: #000; + min-height: 100vh; + font-family: monospace; + margin: 0; + scrollbar-width: thin; + scrollbar-color: #F00 #000; +} + +div#message-container { + padding: 20px; + display: flex; + flex-direction: column; + justify-content: flex-end; + min-height: calc(100vh - 80px); + box-sizing: border-box; + font-size: 14px; +} + +div.message { + color: #C0C0C0; + white-space: pre-wrap; + line-height: 1.35; + margin-bottom: 2px; +} + +div.message-mainframe { + color: #0F0; +} + +div.message-virus { + color: #F00; + font-weight: bold; +} + +div.message-hint { + color: #FF0; + font-weight: bold; +} + +div#input { + padding: 20px; +} + +input#input-message { + width: 100%; + padding: 6px 10px; + background: #111; + border: 1px solid #A00000; + color: #C0C0C0; + font-size: 14px; + box-sizing: border-box; + font-family: monospace; +} diff --git a/src/Game/Controller/PreGameController.php b/src/Game/Controller/PreGameController.php new file mode 100644 index 0000000..85bec58 --- /dev/null +++ b/src/Game/Controller/PreGameController.php @@ -0,0 +1,37 @@ +getGame()?->getId(); + $template = sprintf('pregame/game%d.html.twig', $gameId ?? 0); + + if (!$this->twig->getLoader()->exists($template)) { + $template = 'pregame/default.html.twig'; + } + + return $this->render($template, [ + 'session' => $session, + ]); + } +} diff --git a/templates/game/lobby.html.twig b/templates/game/lobby.html.twig index 25f7d28..8c09b22 100644 --- a/templates/game/lobby.html.twig +++ b/templates/game/lobby.html.twig @@ -27,6 +27,13 @@ {% endfor %} +