Files
Escapepage/assets/pregame.js
T
Frank 7c14ab3331 Add a back button to the tutorial that docks into the input row on completion
Lets players leave the tutorial at any time via a top-left link back to the
game session. Once the tutorial finishes and the input field is disabled,
the button relocates into the input row so it's the obvious next action.
2026-08-18 22:35:52 +02:00

363 lines
13 KiB
JavaScript

/* 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);
}
function dockBackButton(inputField) {
const backButton = document.getElementById('back-button');
const inputContainer = document.getElementById('input');
if (!backButton || !inputContainer) {
return;
}
inputField.style.display = 'none';
backButton.classList.add('docked');
inputContainer.appendChild(backButton);
}
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;
dockBackButton(inputField);
});
return;
}
// Placeholder response until the rest of the tutorial content is defined.
appendMessage(messageContainer, 'Command not recognized yet.', 'message-hint');
});
};
printNextMessage();
});