This is an old revision of the document!
Table of Contents
JavaScript Terminal v2
- For the basic terminal please see JavaScript Terminal
About
This is the Season 2, version 2 of JavaScript Terminal. It is more advanced than the v1 and is intended as a platform for larger and more advanced projects than simple one-scene games.
Progression
Students should move to the v2 after three to five games are made using the original terminal.
Main Differences
The primary difference is the creation of terminal mode and command mode.
By setting gameState = 'terminal', the program solely acts as a terminal emulator. In this mode, when enter is pressed on a line, the entirely of that line is trim()'d and sent to the console (i.e. the command queue). Then when in command mode, the program does not allow the user to control or modify the terminal directly but instead begins processing events from the queue.
Secondly, a framework for mouse (i.e. touch, on a phone or tablet) has been added. In command mode, you can move the cursor around by clicking or tapping on the canvas.
Commands can be processed in terminal mode (ex. HELP) or in command mode (try typing something like 10 PRINT “HELLO”. open the console and press escape – the command processor will take the command and process it).
This separation of terminal mode and command move is central to the idea of a proper terminal simulator, because terminal mode and entering commands with ENTER will later probve fundamental to the illusion of simulating string input.
Code Commentary
main.js
// Set up Canvas and get ctx (context) for drawing. let canvas = document.getElementById('fsc'); let ctx = canvas.getContext('2d'); // Set up Terminal maxtrows = 10 maxtcols = 10 terminal = new Terminal(10, 10) // load font fontLoader = new FontLoader('myvga', 'PxPlus_IBM_VGA_9x16.ttf'); // Put up a cute message. function system_message() { terminal.clearline(0); terminal.clearline(1); terminal.clearline(2); terminal.clearline(3); terminal.clearline(4); terminal.clearline(5); msgline1 = "**** JavaScript Terminal Technical Demo v2.0 ****"; msgline2 = maxTcols + "x" + maxTrows + " " + ctx.font; msgline3 = 'READY.'; terminal.cx = Math.floor((terminal.cols - msgline1.length) / 2); terminal.cy = 0; terminal.puts(msgline1); terminal.cx = Math.floor((terminal.cols - msgline2.length) / 2); terminal.cy = 1; terminal.puts(msgline2); terminal.cx = 0; terminal.cy = 3; terminal.puts(msgline3); terminal.cx = 0; terminal.cy = 4; } // Function to resize and recreate the canvas function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; maxTcols = Math.floor(canvas.width / terminal.charWidth) maxTrows = Math.floor(canvas.height / terminal.charHeight); t = new Terminal(maxTcols, maxTrows); terminal.copyContentTo(t); terminal = t; ctx.font = '32px myvga'; // this seems to be needed here. //console.log("Terminal Resize Event") //console.log("Available Pixel Dimensions: " + canvas.width + "x" + canvas.height); //console.log("Calculating Maximum Terminal Size: " + maxTcols + "x" + maxTrows); //console.log("Actual Pixel Dimensions: " + maxTcols * terminal.charWidth + "x" + maxTrows * terminal.charHeight); system_message(); } // Resize and recreate the canvas window.addEventListener('resize', resizeCanvas); // Attach the click event listener to the canvas canvas.addEventListener('click', function (event) { // do not process click during terminal mode. if (gameState === 'terminal') { return; } // Get the mouse coordinates relative to the canvas var x = event.clientX - canvas.getBoundingClientRect().left; var y = event.clientY - canvas.getBoundingClientRect().top; // Calculate Canvas Areas // 1. Left Side leftx = canvas.width * 0.363380227632; // * (1 - (2/pi)) makes the center a bit smaller than /3. rightx = canvas.width - leftx; topy = canvas.height * 0.363380227632; bottomy = canvas.height - topy; // Check if the click is within a specific region (e.g., a rectangle) if (x <= leftx) { if (y <= topy) { //console.log("top left") terminal.arrowleft(); terminal.arrowup(); } if (y >= bottomy) { //console.log("bottom left"); terminal.arrowleft(); terminal.arrowdown(); } if ((y < bottomy) && (y > topy)) { //console.log("middle left"); terminal.arrowleft(); } } if (x >= rightx) { if (y <= topy) { // console.log("top right") terminal.arrowright(); terminal.arrowup(); } if (y >= bottomy) { //console.log("bottom right"); terminal.arrowright(); terminal.arrowdown(); } if ((y < bottomy) && (y > topy)) { //console.log("middle right"); terminal.arrowright(); } } if ((x > leftx) && (x < rightx)) { if (y <= topy) { //console.log("top middle") terminal.arrowup(); } if (y >= bottomy) { //console.log("bottom middle"); terminal.arrowdown(); } if ((y < bottomy) && (y > topy)) { //console.log("middle middle"); let mx = getRandomInt(0, terminal.cols - 1); let my = getRandomInt(0, terminal.rows - 1); let mch = getRandomLetter(); let mcolor = Color.getColor(getRandomInt(0, 15)); let mbackground = Color.black; terminal.setch(mx, my, mch, mcolor, mbackground); } } }); // Keyboard listener gameState = 'terminal' document.addEventListener('keydown', function (event) { switch (gameState) { case "terminal": // log any unprintable characters. if (event.key.length > 1) { console.log(event.key) } // Put characters on terminal. if (event.key === 'Escape') { console.log("Switching to command queue mode."); gameState = 'command'; } else { terminal.type(event.key); } break; case "command": // Enter keystrokes as console commands into the command queue. event_queue.push("KEY " + event.key); break; default: console.log("unknown game mode for key input"); break; } }); var event_queue = []; function check_events() { // While there are events to process, while (event_queue.length > 0) { let cmd = event_queue.shift().trim(); let para = ''; if (cmd.indexOf(' ') >= 0) { // contains internal spaces, determine parameters. para = cmd.substring(cmd.indexOf(' ') + 1); cmd = cmd.substring(0, cmd.indexOf(' ')); } cmd = cmd.toUpperCase().trim(); if (cmd.length == 0) { return; } // Now, cmd is the event/command and para contains any parameters to it. switch (cmd) { case 'KEY': console.log("fount KEY event: [" + para + "]"); if (para === 'Escape') { console.log("Switching to terminal mode."); gameState = 'terminal' } break; default: console.log("found unknown event: " + cmd + " " + para); break; } // switch } // while has events } // check_events // Animation logic, timed events, etc. // I don't plan for this to be used much in a roguelike. // but it is useful for (ex.) a static demo of the display engine. function update() { switch (gameState) { case "terminal": // Characters are sent to a terminal function and dumped to screen buffer. // We don't do any special processing here. break; case "command": // Command mode. Process events on the event stack. check_events(); break; default: // do nothing break; } var demo = false; if (demo) { n = getRandomInt(1, 100); if (n === 1) { x = getRandomInt(0, terminal.cols - 1) y = getRandomInt(0, terminal.rows - 1) ch = getRandomLetter() color = Color.getColor(getRandomInt(0, 15)) background = Color.black terminal.setch(x, y, ch, color, background) //console.log("[" + ch + "], " + x + "," + y + " " + color); } } } // All we really need to do is draw the terminal. // What is on the terminal is done using (ex.) Terminal.setch() function render() { // clear screen ctx.fillStyle = 'black'; ctx.fillRect(0, 0, canvas.width, canvas.height); // draw characters on terminal terminal.draw(ctx); } let lastTime = 0; function gameLoop(timestamp) { // Update game logic here update() // Render the game state render(); // Request the next frame requestAnimationFrame(gameLoop); } // Initial canvas setup resizeCanvas(); // Test procedure //test_screen(terminal); terminal.cc = true; // Wait for the font to load before starting the update/render loop. let waitTimerId; // Variable to store the interval identifier let loading_div; function waitForFonts() { waitTimerId = setInterval(() => { // Check the condition (replace this with your actual condition) if (fontLoader.loaded) { // Clear the interval clearInterval(waitTimerId); // Start Game Loop gameLoop(); } }, 100); // Set the interval time in milliseconds (e.g., 1000 ms = 1 second) } waitForFonts()
One major addition is the concept of the FontLoader class, which is intended to clean up main.js. With this in mind, 'waitForFonts()' should probably be moved to that class – maybe in a v3.
var gameState
The initial move is to create gameState, which will hold either 'terminal' or 'command', for the two different modes of operation. We will discuss the modes of operation in terms of practical analysis of update() and the keydown listener.
function update()
update() switches on var gameState. If gameState is 'terminal' then update does nothing. If gameState is 'command', the check_events() function will cycle through a list of events, in the same manner that a cpu executes an instruction cycle, until there are no more events (or a timer pauses the process until next frame).
keydown handler
In the same way that update() switches, the keydown handler also switches on gameState. If gameState is 'terminal', all event.key values are sent to terminal.type() which is the function that processes them as if they were typed on a terminal. Some special processing occurrs here for arrows, enter, and so forth – this could (and probably should) be moved to the terminal class to keep main.js concise.
function check_events()
check_events() acts to perform instruction cycling like how a cpu will load an instruction from memory and attempt to execute it. check_events() takes the first event in the event_queue array and switches on it to handle that event. In command mode, the keyboard handler creates events called KEY events, so that the check_events() function can deal with them as game commands. In terminal mode, the terminal will add events to this queue when the user presses enter.
So, in terminal mode, if the user types 'help', it will be processed by the terminal as a special system command. This is kind of like an immediate mode which simulates string input, firing on the ENTER key. However, if the user types anything else (such as 10 print “hello”) it is created as an event and processed when terminal mode is exited. This event would show up as 'ENTER 10 print “hello”'.
However, in command mode, any key pressed would cause the keyboard handler to create a KEY event such as “KEY A” which in many games is interpreted as 'move left' or 'strafe left'.
When check_events() sees any of these events it tries to do whatever it takes to 'handle' or 'process' the events. What it should do is just call a handler function, but this can be refactored in later.
Handling Click/Touch events
There is also a click (or, touch) handler, as an example framework for future projects. It is off in terminal mode, but in command (play) mode, pressing on the sides of the screen will move the cursor. Theoretically it would be designed to move a player. Clicking or touching in the middle of the screen causes a separate event, which for now causes a character to appear with a random color and location. But it could more practically be used to open a menu screen for additional commands – such as inventory, up/down, pick up, look, save, quit, etc.
Quality of Life
Some quality of life features have been added, such as the HELP command, and various color options (see: HELP in-game).