javascript_terminal_v2
Differences
This shows you the differences between two versions of the page.
Next revision | Previous revision | ||
javascript_terminal_v2 [2023/11/21 15:43] – created appledog | javascript_terminal_v2 [2023/11/27 04:51] (current) – appledog | ||
---|---|---|---|
Line 4: | Line 4: | ||
== About | == 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. | 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 | == Main Differences | ||
- | The primary difference is the creation of terminal | + | The primary difference is that the engine has been split into two distinct halves; ' |
- | By setting gameState = ' | + | By setting gameState = ' |
- | Secondly, a framework for mouse (i.e. touch, on a phone or tablet) has been added. In command | + | The next mode to understand is 'run mode', or ' |
- | Commands can be processed in terminal | + | === Simulating String Input |
+ | Currently, immediate | ||
- | This separation of terminal mode and command move is central to the idea of a proper terminal simulator, because terminal mode and entering | + | When in immediate (terminal) mode, the user can control the cursor with the arrow keys. |
+ | In command (run) mode, click and touch actions will generate events which can be handled by the engine if desired. | ||
+ | |||
+ | This separation of terminal mode and command move is central to the goal of escaping the limitations of JavaScript. We do this by creating the nucleus | ||
+ | |||
+ | === the input() connundrum | ||
+ | After I published technical demo 2 (this) I added an input() function, which led to a realization that there was no need to use state to operate the game in two modes; what was really required were a series of flags such as ' | ||
+ | |||
+ | I did retroactively add input() to technical demo 2 (see commands | ||
+ | |||
+ | < | ||
+ | input(" | ||
+ | return; | ||
+ | </ | ||
+ | |||
+ | Then, when the user presses | ||
+ | |||
+ | < | ||
+ | SET_NAME Richard | ||
+ | </ | ||
+ | |||
+ | Then, the game engine will handle this event by changing the user's name. In theory, the game engine will set the name before the next user event is called, either by having a priority queue for state changes, or by assumption (i.e. fiat) that there is no command in the queue which relies on SET_NAME. If so, the system could be programmed to HALT until SET_NAME executes (SET_NAME would be added and the system would wait until it changed to SET NAME < | ||
+ | |||
+ | What if multiple contexts are required; such as, asking three questions in a row? Then, multiple events could be added to the queue: | ||
+ | |||
+ | < | ||
+ | INPUT SET_NAME What is your name? | ||
+ | INPUT SET_CLASS "What is your class (Fighter, Mage)? | ||
+ | INPUT SET_RACE "What is your race (Human, Elf, Hobbit)? | ||
+ | </ | ||
+ | |||
+ | The engine can then throw an INPUT command, and since event processing stops during INPUT, the next question will be asked once the previous one is answered. | ||
+ | |||
+ | What if you need to control state? then you can add queue commands such as: | ||
+ | |||
+ | < | ||
+ | SET_NAME | ||
+ | VAR NAME VALUE | ||
+ | </ | ||
+ | |||
+ | or if you want to be fancy you can index the value by string name; ex. variables[s] where s is the string value of something like | ||
+ | |||
+ | < | ||
+ | INPUT using A | ||
+ | VAR NAME is A | ||
+ | </ | ||
+ | |||
+ | The interpreter can read and write, even to a map (dictionary) if string | ||
+ | |||
+ | === The VM idea | ||
+ | The idea was that we would get around the limitations of JavaScript (or any modern game engine, or, single threaded environment) by creating a virtual machine that accepts commands, like a computer or cpu that pulls instructions and executes them in the context of some environment. This is what the original idea was, we would create an event after we processed the input in 'input mode' (such as SET_NAME). But then, it became obvious that the same system, taken a little further could also do variables, and even simulate BASIC (line numbers could be array indexes, for easy sorting and renumbering). | ||
+ | |||
+ | This led to the realization that having two modes wasn't really necessary. There should only be one mode, and when input is needed an input flag could occur which would put characters into a string buffer OR which would trigger a line read from a cursor position, or, on the current line, or context, as contextually implied by the environment. For example a command line has different parameters than a 'What is your name?' prompt in Nethack, or a simulated getkey. | ||
+ | |||
+ | Eventually it became obvious that the system was falling towards a CPU/System simulator. My mind jumped to ' | ||
+ | |||
+ | This topic became too large to fit inside the JavaScript Terminal or JavaScript Netwhack idea, so I will write about it somewhere else, maybe [[JavaScript Virtual Machine]]. | ||
== Code Commentary | == Code Commentary | ||
- | FIXME | + | === main.js |
+ | < | ||
+ | // Set up Canvas and get ctx (context) for drawing. | ||
+ | let canvas = document.getElementById(' | ||
+ | let ctx = canvas.getContext(' | ||
+ | |||
+ | // Set up Terminal | ||
+ | maxtrows = 10 | ||
+ | maxtcols = 10 | ||
+ | terminal = new Terminal(10, | ||
+ | |||
+ | // load font | ||
+ | fontLoader = new FontLoader(' | ||
+ | |||
+ | // 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 + " | ||
+ | msgline3 = ' | ||
+ | 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, | ||
+ | terminal.copyContentTo(t); | ||
+ | terminal = t; | ||
+ | |||
+ | ctx.font = '32px myvga'; | ||
+ | |||
+ | // | ||
+ | // | ||
+ | // | ||
+ | // | ||
+ | |||
+ | system_message(); | ||
+ | } | ||
+ | |||
+ | // Resize and recreate the canvas | ||
+ | window.addEventListener(' | ||
+ | |||
+ | |||
+ | // Attach the click event listener to the canvas | ||
+ | canvas.addEventListener(' | ||
+ | // do not process click during terminal mode. | ||
+ | if (gameState === ' | ||
+ | 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; | ||
+ | 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) { | ||
+ | // | ||
+ | terminal.arrowleft(); | ||
+ | terminal.arrowup(); | ||
+ | } | ||
+ | if (y >= bottomy) { | ||
+ | // | ||
+ | terminal.arrowleft(); | ||
+ | terminal.arrowdown(); | ||
+ | } | ||
+ | if ((y < bottomy) && (y > topy)) { | ||
+ | // | ||
+ | terminal.arrowleft(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | if (x >= rightx) { | ||
+ | if (y <= topy) { | ||
+ | // console.log(" | ||
+ | terminal.arrowright(); | ||
+ | terminal.arrowup(); | ||
+ | } | ||
+ | if (y >= bottomy) { | ||
+ | // | ||
+ | terminal.arrowright(); | ||
+ | terminal.arrowdown(); | ||
+ | } | ||
+ | if ((y < bottomy) && (y > topy)) { | ||
+ | // | ||
+ | terminal.arrowright(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | if ((x > leftx) && (x < rightx)) { | ||
+ | if (y <= topy) { | ||
+ | // | ||
+ | terminal.arrowup(); | ||
+ | } | ||
+ | if (y >= bottomy) { | ||
+ | // | ||
+ | terminal.arrowdown(); | ||
+ | } | ||
+ | if ((y < bottomy) && (y > topy)) { | ||
+ | // | ||
+ | let mx = getRandomInt(0, | ||
+ | let my = getRandomInt(0, | ||
+ | let mch = getRandomLetter(); | ||
+ | let mcolor = Color.getColor(getRandomInt(0, | ||
+ | let mbackground = Color.black; | ||
+ | terminal.setch(mx, | ||
+ | } | ||
+ | } | ||
+ | |||
+ | }); | ||
+ | |||
+ | // Keyboard listener | ||
+ | gameState = ' | ||
+ | document.addEventListener(' | ||
+ | switch (gameState) { | ||
+ | case " | ||
+ | // log any unprintable characters. | ||
+ | if (event.key.length > 1) { | ||
+ | console.log(event.key) | ||
+ | } | ||
+ | // Put characters on terminal. | ||
+ | if (event.key === ' | ||
+ | console.log(" | ||
+ | gameState = ' | ||
+ | } else { | ||
+ | terminal.type(event.key); | ||
+ | } | ||
+ | break; | ||
+ | case " | ||
+ | // Enter keystrokes as console commands into the command queue. | ||
+ | event_queue.push(" | ||
+ | break; | ||
+ | default: | ||
+ | console.log(" | ||
+ | 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(' | ||
+ | // contains internal spaces, determine parameters. | ||
+ | para = cmd.substring(cmd.indexOf(' | ||
+ | cmd = cmd.substring(0, | ||
+ | } | ||
+ | |||
+ | cmd = cmd.toUpperCase().trim(); | ||
+ | |||
+ | if (cmd.length == 0) { | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // Now, cmd is the event/ | ||
+ | switch (cmd) { | ||
+ | case ' | ||
+ | console.log(" | ||
+ | if (para === ' | ||
+ | console.log(" | ||
+ | gameState = ' | ||
+ | } | ||
+ | break; | ||
+ | default: | ||
+ | console.log(" | ||
+ | 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 " | ||
+ | // Characters are sent to a terminal function and dumped to screen buffer. | ||
+ | // We don't do any special processing here. | ||
+ | break; | ||
+ | case " | ||
+ | // Command mode. Process events on the event stack. | ||
+ | check_events(); | ||
+ | break; | ||
+ | default: | ||
+ | // do nothing | ||
+ | break; | ||
+ | } | ||
+ | |||
+ | var demo = false; | ||
+ | if (demo) { | ||
+ | n = getRandomInt(1, | ||
+ | if (n === 1) { | ||
+ | x = getRandomInt(0, | ||
+ | y = getRandomInt(0, | ||
+ | ch = getRandomLetter() | ||
+ | color = Color.getColor(getRandomInt(0, | ||
+ | background = Color.black | ||
+ | terminal.setch(x, | ||
+ | // | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // 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 = ' | ||
+ | ctx.fillRect(0, | ||
+ | |||
+ | // 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 | ||
+ | // | ||
+ | |||
+ | terminal.cc = true; | ||
+ | |||
+ | // Wait for the font to load before starting the update/ | ||
+ | let waitTimerId; | ||
+ | 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, ' | ||
+ | |||
+ | ==== var gameState | ||
+ | The initial move is to create gameState, which will hold either ' | ||
+ | |||
+ | === function update() | ||
+ | update() switches on var gameState. If gameState is ' | ||
+ | |||
+ | === keydown handler | ||
+ | In the same way that update() switches, the keydown handler also switches on gameState. If gameState is ' | ||
+ | |||
+ | === 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 ' | ||
+ | |||
+ | 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 ' | ||
+ | |||
+ | When check_events() sees any of these events it tries to do whatever it takes to ' | ||
+ | |||
+ | === 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). | ||
+ | |||
+ | == Terminal.js | ||
+ | A lot of major changes have occurred here. | ||
+ | |||
+ | < | ||
+ | class Terminal { | ||
+ | constructor(cols, | ||
+ | this.charWidth = 18; | ||
+ | this.charHeight = 32; | ||
+ | this.cols = cols; | ||
+ | this.rows = rows; | ||
+ | this.color = Color.ibm3270_green1; | ||
+ | this.background = Color.ibm3270_green2; | ||
+ | |||
+ | // Cursor metrics | ||
+ | this.cx = 0; | ||
+ | this.cy = 0; | ||
+ | this.interval = 535; // 535ms | ||
+ | this.cc = false; | ||
+ | this.cursor = true; | ||
+ | this.timerId = false; | ||
+ | this.startCursorTimer(); | ||
+ | |||
+ | this.buf = new Array(rows); | ||
+ | for (let y = 0; y < rows; y++) { | ||
+ | this.buf[y] = new Array(cols); | ||
+ | for (let x = 0; x < cols; x++) { | ||
+ | this.buf[y][x] = new Character(' | ||
+ | } | ||
+ | } | ||
+ | |||
+ | this.calcFontMetrics() | ||
+ | } | ||
+ | |||
+ | calcFontMetrics() { | ||
+ | // Measure the width of a single character (assuming monospaced font) | ||
+ | const measureTextResult = ctx.measureText(' | ||
+ | // | ||
+ | |||
+ | // Calculate the baseline offset | ||
+ | // If there is a problem, adjust by hand. | ||
+ | // PxPlus_IBM_VGA_9x16 seems to work well at 24... | ||
+ | this.font_yadj = Math.round(32 - measureTextResult.fontBoundingBoxAscent) + 1; | ||
+ | this.font_yadj = 24 | ||
+ | // | ||
+ | } | ||
+ | |||
+ | // Function to schedule the timer to repeat | ||
+ | startCursorTimer() { | ||
+ | if (this.timerId == false) { | ||
+ | this.timerId = setInterval(() => { | ||
+ | this.cc = !this.cc; | ||
+ | // | ||
+ | }, this.interval); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | stopCursorTimer() { | ||
+ | // Stopping the timer | ||
+ | if (this.timerId) { | ||
+ | clearInterval(this.timerId); | ||
+ | this.timerId = null; // Optional: Set to null to indicate that the timer is not active | ||
+ | } | ||
+ | } | ||
+ | |||
+ | setch(x, y, ch, color = this.color, background = this.background) { | ||
+ | this.buf[y][x].ch = ch; | ||
+ | this.buf[y][x].color = color; | ||
+ | this.buf[y][x].background = background; | ||
+ | } | ||
+ | |||
+ | // setColor(c) { | ||
+ | // this.color = c | ||
+ | // } | ||
+ | |||
+ | // setBkg(b) { | ||
+ | // this.background = background | ||
+ | // } | ||
+ | |||
+ | type(key) { | ||
+ | if (key.length === 1) { | ||
+ | this.putch(key); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // It's nonstandard. | ||
+ | if (key.length < 1) { | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | // Fallthrough: | ||
+ | if (key === " | ||
+ | let s = ""; | ||
+ | for (let i = 0; i < this.cols; i++) { | ||
+ | let c = this.buf[this.cy][i].ch; | ||
+ | s = s + c; | ||
+ | } | ||
+ | |||
+ | s = s.trim(); | ||
+ | s = s.toLowerCase(); | ||
+ | |||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | |||
+ | if (s === ' | ||
+ | this.puts(" | ||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | this.puts(" | ||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | this.puts(" | ||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | this.puts(" | ||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | this.puts(" | ||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | this.puts(" | ||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (s === ' | ||
+ | this.puts(" | ||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (s === ' | ||
+ | this.puts(" | ||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (s === ' | ||
+ | this.puts(" | ||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | this.puts(" | ||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | } | ||
+ | |||
+ | if (s === ' | ||
+ | console.log(" | ||
+ | this.setTheme(' | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (s === ' | ||
+ | console.log(" | ||
+ | this.setTheme(' | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if ((s === ' | ||
+ | console.log(" | ||
+ | this.setTheme(' | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | |||
+ | if (s === ' | ||
+ | console.log(" | ||
+ | this.setTheme(' | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (s ===' | ||
+ | console.log(" | ||
+ | this.setTheme('', | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | |||
+ | console.log(' | ||
+ | console.log(" | ||
+ | event_queue.push(" | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (key === " | ||
+ | this.arrowleft_jump(); | ||
+ | this.delete(); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (key === " | ||
+ | this.delete() | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (key === " | ||
+ | this.arrowleft(); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (key === " | ||
+ | this.arrowright(); | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (key === " | ||
+ | this.cy = this.cy - 1; | ||
+ | if (this.cy < 0) { | ||
+ | this.cy = 0; | ||
+ | } | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | if (key === " | ||
+ | this.cy = this.cy + 1; | ||
+ | if (this.cy >= this.rows) { | ||
+ | this.cy = this.rows - 1; | ||
+ | } | ||
+ | } | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | putch(ch, color = this.color, background = this.background) { | ||
+ | // | ||
+ | |||
+ | if ((this.cx < 0) || (this.cx >= this.cols)) { | ||
+ | console.log(" | ||
+ | return; | ||
+ | } | ||
+ | this.buf[this.cy][this.cx].ch = ch; | ||
+ | this.buf[this.cy][this.cx].color = color; | ||
+ | this.buf[this.cy][this.cx].background = background; | ||
+ | if (this.arrowright()) { | ||
+ | this.cr(); | ||
+ | this.lf(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | puts(s) { | ||
+ | for (let i = 0; i < s.length; i++) { | ||
+ | let c = s.charAt(i); | ||
+ | this.putch(c) | ||
+ | } | ||
+ | } | ||
+ | |||
+ | putsxy(s, x, y) { | ||
+ | this.cx = x; | ||
+ | this.cy = y; | ||
+ | this.puts(s); | ||
+ | } | ||
+ | |||
+ | delete() { | ||
+ | if (this.cx >= this.cols) { | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | for (let i = this.cx; i < this.cols; i++) { | ||
+ | // copy next into here; | ||
+ | if ((i + 1) < this.cols) { | ||
+ | this.buf[this.cy][i].ch = this.buf[this.cy][i + 1].ch; | ||
+ | this.buf[this.cy][i].color = this.buf[this.cy][i + 1].color; | ||
+ | this.buf[this.cy][i].background = this.buf[this.cy][i + 1].background; | ||
+ | this.buf[this.cy][i + 1].ch = ' '; | ||
+ | this.buf[this.cy][i + 1].color = this.color; | ||
+ | this.buf[this.cy][i + 1].background = this.background; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | backspace() { | ||
+ | this.putch(" | ||
+ | } | ||
+ | |||
+ | arrowleft_jump() { | ||
+ | var skip = this.arrowleft(); | ||
+ | |||
+ | while (skip) { | ||
+ | if (this.buf[this.cy][this.cx].ch == ' ') { | ||
+ | this.cx = this.cx - 1; | ||
+ | if (this.cx < 0) { | ||
+ | this.cx = 0; | ||
+ | skip = false; | ||
+ | } | ||
+ | } else { | ||
+ | skip = false; | ||
+ | this.cx = this.cx + 1; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | } | ||
+ | |||
+ | arrowleft() { | ||
+ | this.cx = this.cx - 1; | ||
+ | if (this.cx < 0) { | ||
+ | this.cx = 0; | ||
+ | if (this.cy > 0) { | ||
+ | this.cy = this.cy - 1; | ||
+ | this.cx = this.cols - 1; | ||
+ | return true; // y - 1. signal true for arrowleft_jump() to skip spaces. | ||
+ | } | ||
+ | } | ||
+ | return false; // didn't go up | ||
+ | } | ||
+ | |||
+ | arrowright() { | ||
+ | this.cx = this.cx + 1; | ||
+ | if (this.cx >= this.cols) { | ||
+ | this.cx = 0; | ||
+ | |||
+ | this.cy = this.cy + 1; | ||
+ | if (this.cy >= this.rows) { | ||
+ | this.cy = this.rows - 1; | ||
+ | return true; // signal that a cr/lf is needed to 'go down' | ||
+ | } else { | ||
+ | return false; // signal we handled the arrow in-terminal. | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | arrowup() { | ||
+ | this.cy = this.cy - 1; | ||
+ | if (this.cy < 0) { | ||
+ | this.cy = 0 | ||
+ | } | ||
+ | } | ||
+ | |||
+ | arrowdown() { | ||
+ | this.cy = this.cy + 1; | ||
+ | if (this.cy >= this.rows) { | ||
+ | this.cy = this.rows - 1; | ||
+ | } | ||
+ | // arrow downs on the bottom do nothing (they don't cause a lf). | ||
+ | } | ||
+ | |||
+ | cr() { | ||
+ | this.cx = 0; | ||
+ | } | ||
+ | |||
+ | lf() { | ||
+ | this.cy = this.cy + 1; | ||
+ | if (this.cy >= this.rows) { | ||
+ | this.cy = this.rows - 1; | ||
+ | this.hard_lf() | ||
+ | } | ||
+ | } | ||
+ | |||
+ | hard_lf() { | ||
+ | for (let y = 1; y < this.rows; y++) { | ||
+ | for (let x = 0; x < this.cols; x++) { | ||
+ | this.buf[y-1][x].ch = this.buf[y][x].ch; | ||
+ | this.buf[y-1][x].color = this.buf[y][x].color; | ||
+ | this.buf[y-1][x].background = this.buf[y][x].background; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | for (let x = 0; x < this.cols; x++) { | ||
+ | this.buf[this.rows-1][x].ch = ' '; | ||
+ | this.buf[this.rows-1][x].color = this.color; | ||
+ | this.buf[this.rows-1][x].background = this.background; | ||
+ | } | ||
+ | this.cx = 0; | ||
+ | this.cy = this.rows - 1; | ||
+ | } | ||
+ | |||
+ | clearline(y) { | ||
+ | if ((y < 0) || (y >= this.rows)) { | ||
+ | | ||
+ | | ||
+ | } | ||
+ | |||
+ | for (let x = 0; x < this.cols; x++) { | ||
+ | this.buf[y][x].ch = ' '; | ||
+ | this.buf[y][x].color = this.color; | ||
+ | this.buf[y][x].background = this.background; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | drawCharacter(ctx, | ||
+ | // It looks like we have to do this because of various factors. | ||
+ | ctx.font = '32px myvga'; | ||
+ | |||
+ | // Calculate the actual position on the canvas based on character width and height | ||
+ | const xPos = x * this.charWidth; | ||
+ | const yPos = (y * this.charHeight); | ||
+ | |||
+ | |||
+ | // Set the background | ||
+ | ctx.fillStyle = background; | ||
+ | ctx.fillRect(xPos, | ||
+ | |||
+ | // Draw the character on the canvas | ||
+ | ctx.fillStyle = color; | ||
+ | ctx.fillText(ch, | ||
+ | |||
+ | } | ||
+ | |||
+ | draw(ctx) { | ||
+ | for (let y = 0; y < this.rows; y++) { | ||
+ | for (let x = 0; x < this.cols; x++) { | ||
+ | // Get the character and color from the buf array | ||
+ | const ch = this.buf[y][x].ch; | ||
+ | const fg = this.buf[y][x].color; | ||
+ | const bg = this.buf[y][x].background; | ||
+ | this.drawCharacter(ctx, | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // draw the cursor | ||
+ | if (this.cursor && this.cc) { | ||
+ | const xPos = this.cx * this.charWidth; | ||
+ | const yPos = (this.cy * this.charHeight); | ||
+ | ctx.fillStyle = this.color; | ||
+ | ctx.fillText(" | ||
+ | } | ||
+ | } | ||
+ | |||
+ | copyContentTo(newTerminal) { | ||
+ | // Determine the number of rows and columns to copy | ||
+ | const rowsToCopy = Math.min(this.rows, | ||
+ | const colsToCopy = Math.min(this.cols, | ||
+ | |||
+ | // Copy content from the old terminal to the new terminal | ||
+ | for (let y = 0; y < rowsToCopy; y++) { | ||
+ | for (let x = 0; x < colsToCopy; x++) { | ||
+ | const nch = this.buf[y][x].ch; | ||
+ | const ncolor = this.buf[y][x].color; | ||
+ | const nbackground = this.buf[y][x].background; | ||
+ | |||
+ | newTerminal.setch(x, | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // Copy vital data | ||
+ | newTerminal.cx = this.cx | ||
+ | newTerminal.cy = this.cy | ||
+ | newTerminal.cc = this.cc | ||
+ | newTerminal.font_baseline = this.font_baseline; | ||
+ | |||
+ | if (this.timerId) { | ||
+ | this.stopCursorTimer() | ||
+ | newTerminal.startCursorTimer() | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | setTheme(name, | ||
+ | this.theme = name; | ||
+ | this.color = fg; | ||
+ | this.background = bg; | ||
+ | |||
+ | if (name.length == 0) { | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | for (let y = 0; y < this.rows; y++) { | ||
+ | for (let x = 0; x < this.cols; x++) { | ||
+ | this.buf[y][x].color = fg; | ||
+ | this.buf[y][x].background = bg; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | == Code Discussion | ||
+ | === Consistancy | ||
+ | From the top. this.color and this.background are used to provide consistancy. As an exercise to the reader, which will be implemented in v3, a character' | ||
+ | |||
+ | === type() | ||
+ | The entry point for raw typed keys is type(). This is because we don't know what the user will type and he might type ' | ||
+ | |||
+ | Since we have the choice to either process lines directy in the type() function or create event-commands for later processing (such as when entering '10 print " | ||
+ | |||
+ | |||
+ | === putch() | ||
+ | This is a real workhorse, which takes a character and puts it at the current cx,cy. It then handles the motion of the cursor, possibly executing a hard linefeed to scroll the screen up. This is the way in which we treat the screen as a lp device and just dump characters into it. | ||
+ | Note that this is not intended to produce cr and lf or any special control or non-printable characters. Those should be brought in with the type() command instead. Also note that most people will use setch() instead to set the value of a character on the screen. In a practical sense, putch() is used mainly by puts(). | ||
+ | |||
+ | === puts() and putsxy() | ||
+ | These functions go through a string and putch() them, allowing putch() to handle desired things like wrapping to the next line. The operation is quite difficult and can fail if the string is too long for the line -- a bug left as an exercise to the reader (but is intended to be fixed in v3). | ||
+ | |||
+ | === backspace() and delete() | ||
+ | These and similar functions are designed to make the terminal and character editing on the termal feel like on a real terminal or word processor. They are intended to be as simple and basic as possible and solely to facilitate users being able to correct typing mistakes or make changes. | ||
+ | |||
+ | === arrowleft() and arrowleft_jump() | ||
+ | These and other such commands as arrowup() control the motion of the cursor in a consistant way, the _jump() version skips spaces when wrapping up and to the far right, so it appears as if you cursor directly to the text. Most of the time this is the intended effect but we could also deny this and only call it if control is pressed during the arrowleft. There are a lot of UI/UX issues to consider, but we just want to keep things simple and small for now. | ||
+ | |||
+ | === hard_lf() | ||
+ | This is the function that scrolls the screen up. | ||
+ | On most terminals there' | ||
+ | |||
+ | === drawCharacter() | ||
+ | It appears as if we need to set ctx.font = '32px myvga'; | ||
+ | |||
+ | == Color.js | ||
+ | Several new colors have been added. Standard greens and ambers based on phosphor wavelength values, as well as look-and-feel approximations of greens and ambers from an IBM 3270. Some look and feel ambers, and some phosphor and/or look and feel consensus opinions on the AppleII and AppleIIc have been added. In a v3, we may also decide to include more colors (such as for VIC-20, C64 and C128) and special fonts to simulate these terminals. For now the extra colors are there to demonstrate how to use the immediate command mode in Terminal (ex. by typing ' | ||
+ | |||
+ | == Bugs | ||
+ | There were a few bugs. One early bug from v1 is that we have to keep setting the font when we draw tthe character, or for some reason it doesn' | ||
+ | |||
+ | Another bug is related to resizing the terminal. It may be better to simply regenerate the character array instead of creating a whole new terminal because a ' | ||
+ | |||
+ | There is a ' | ||
+ | |||
+ | There was, actually however, a bug in that system I did fix, if it is called with negative cx it doesn' | ||
+ | |||
+ | In that sense there aren't really any other serious bugs, it basicaly works as intended (I checked). If it's something I didn't check then you will be able to work around it in the game code. | ||
+ | |||
+ | == Closing Thoughts | ||
+ | This is an exciting platform to work with, once it is understood. To understand how to use this platform and how to extend it towards your game, I will discuss my plans for extending this code-base into JavaScript NetWhack. I will put this in a page [[JavaScript NetWhack]]. |
javascript_terminal_v2.1700581382.txt.gz · Last modified: 2023/11/21 15:43 by appledog