├── LICENSE ├── README.md ├── audio.js ├── background.png ├── cp437.png ├── index.html ├── textconsole.js ├── worldloader.js ├── worlds ├── basetest.zzt ├── caves.zzt ├── city.zzt ├── demo.zzt ├── dungeons.zzt ├── index.json ├── tour.zzt └── town.zzt ├── zztboard.js ├── zztdialog.js ├── zztfilestream.js ├── zztgame.js └── zztobject.js /LICENSE: -------------------------------------------------------------------------------- 1 | The ZZT worlds are copyright (C) 1991 Epic Games. 2 | 3 | All other code is released under the MIT license: 4 | 5 | Copyright (C) 2013 Brenda Streiff 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to 9 | deal in the Software without restriction, including without limitation the 10 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 11 | sell copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 23 | IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ZZT.js 2 | ====== 3 | 4 | This is an attempt to recreate a ZZT engine in HTML5/JS. 5 | 6 | ZZT is a game developed in 1991 by Tim Sweeney of Potomac Computer Systems, 7 | which most people would probably recognize today as Epic Games. 8 | 9 | ZZT, despite the dated (even for 1991) textmode graphics, was notable for 10 | being one of the first games to have a built-in editor and a specialized 11 | scripting language (ZZT-OOP) that made it very easy for users to develop 12 | their own game worlds. A large community sprung up around ZZT development. 13 | 14 | ZZT was sufficiently popular to finance Sweeney's next game, Jill of the 15 | Jungle, and successive games which would eventually lead to the gaming 16 | powerhouse that Epic is today. Many members of the ZZT community would also 17 | move on to become professional game designers in their own right. 18 | 19 | In 1998, as the game became increasingly obsolete, Sweeney released ZZT 20 | (and the four shareware game worlds) for free. Unfortunately, the source 21 | code was long since lost, hampering any development past ZZT 3.2. However, 22 | the ZZT community has reverse-engineered many aspects of the game, allowing 23 | for alternative editors and alternative engines to exist. 24 | 25 | (As for my part, this project is an exercise in learning HTML5 and JavaScript 26 | while on vacation from work, where the programming I do is considerably more 27 | low-level. ZZT's engine is simple enough and the formats are well-known, so I 28 | expect it won't take too long to come up with something workable...) 29 | -------------------------------------------------------------------------------- /audio.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | 5 | http://zzt.org/fora/viewtopic.php?f=18&t=3345&start=15#p65718 6 | At #cycle 1, a sixteenth note has the length of 1 idle. 7 | 8 | 9 | http://zzt.org/fora/viewtopic.php?f=9&t=3124&p=62791&hilit=cycle+game+speed#p62791 10 | 11 | So i guess ZZT's frame rate ought to be 9.1032548384 frames/sec. Though I can think 12 | of advantages with picking 10 frames/sec (ideal numbers vs. subtle familiarity of speed). 13 | */ 14 | 15 | function ZZTAudio() 16 | { 17 | try 18 | { 19 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 20 | this.context = new AudioContext(); 21 | this.gain = this.context.createGain(); 22 | this.gain.gain.value = 0.5; 23 | } 24 | catch(e) 25 | { 26 | console.log("no audio context for you. :("); 27 | console.log(e); 28 | return; 29 | } 30 | } 31 | 32 | ZZTAudio.prototype.SFX_TORCH_DEAD = "tc-c-c"; 33 | ZZTAudio.prototype.SFX_PLAYER_DEAD = "s.-cd#g+c-ga#+dgfg#+cf----q.c"; 34 | ZZTAudio.prototype.SFX_ENERGIZER_DEAD = "s.-c-a#gf#fd#c"; 35 | ZZTAudio.prototype.SFX_TIME_RUNNING_OUT = "i.+cfc-f+cfq.c"; 36 | 37 | /* table of note frequencies */ 38 | ZZTAudio.prototype.NOTES = function() 39 | { 40 | var notes = new Array(128); 41 | for (var i = 0; i < 128; ++i) 42 | { 43 | notes[i] = 8.1758 * Math.pow(2.0, i / 12.0); 44 | } 45 | return notes; 46 | }(); 47 | 48 | ZZTAudio.prototype.setVolume = function(val) 49 | { 50 | if (this.gain) 51 | this.gain.gain.value = val; 52 | } 53 | 54 | ZZTAudio.prototype.play = function(str) 55 | { 56 | if (game.quiet) 57 | return; 58 | 59 | if (!this.context) 60 | return; 61 | 62 | var octave = 5; 63 | var noteDuration = 32; 64 | 65 | if (this.oscillator) 66 | { 67 | this.oscillator.disconnect(); 68 | } 69 | this.oscillator = this.context.createOscillator(); 70 | this.oscillator.type = "square"; 71 | this.oscillator.connect(this.gain); 72 | this.gain.connect(this.context.destination); 73 | 74 | var streamTime = 0; 75 | for (var i = 0; i < str.length; ++i) 76 | { 77 | var ch = str.charAt(i); 78 | var note = -1; 79 | 80 | /* TODO: doesn't handle the 1-2,4-9 'sound effects' */ 81 | switch (ch) 82 | { 83 | case "c": note = (octave * 12); break; 84 | case "d": note = (octave * 12) + 2; break; 85 | case "e": note = (octave * 12) + 4; break; 86 | case "f": note = (octave * 12) + 5; break; 87 | case "g": note = (octave * 12) + 7; break; 88 | case "a": note = (octave * 12) + 9; break; 89 | case "b": note = (octave * 12) + 11; break; 90 | case "+": octave++; break; 91 | case "-": octave--; break; 92 | 93 | /* Duration here is given in terms of division of a cycle. */ 94 | case "t": noteDuration = 32; break; /* 1/32th note */ 95 | case "s": noteDuration = 16; break; /* 1/16th note */ 96 | case "i": noteDuration = 8; break; /* 1/8th note */ 97 | case "q": noteDuration = 4; break; /* 1/4th note */ 98 | case "h": noteDuration = 2; break; /* 1/2th note */ 99 | case "w": noteDuration = 1; break; /* whole note */ 100 | case "3": noteDuration /= 3; break; 101 | case "x": note = 0; break; 102 | default: break; 103 | } 104 | 105 | /* If it's a note, it might be followed by a sharp or flat */ 106 | if (note >= 0 && (i+1 < str.length)) 107 | { 108 | if (str.charAt(i+1) == '#') 109 | note++; 110 | else if (str.charAt(i+1) == '!') 111 | note--; 112 | } 113 | 114 | if (note >= 0) 115 | { 116 | var frequency = this.NOTES[note]; 117 | /* A sixteenth note has the duration of one cycle. */ 118 | var noteTime = (1 / noteDuration) * (16 / game.fps); 119 | 120 | this.oscillator.frequency.setValueAtTime(frequency, this.context.currentTime + streamTime); 121 | streamTime += noteTime; 122 | } 123 | } 124 | 125 | this.oscillator.start(this.context.currentTime, 0, streamTime); 126 | this.oscillator.stop(this.context.currentTime + streamTime); 127 | } 128 | -------------------------------------------------------------------------------- /background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstreiff/zztjs/bf58ab7536d9dc1255676e0b6a492f0bd5e5bd42/background.png -------------------------------------------------------------------------------- /cp437.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstreiff/zztjs/bf58ab7536d9dc1255676e0b6a492f0bd5e5bd42/cp437.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ! 13 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /textconsole.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* "constants" */ 4 | var VGA = 5 | { 6 | COLOR: [ 7 | [ 0, 0, 0 ], 8 | [ 0, 0, 170 ], 9 | [ 0, 170, 0 ], 10 | [ 0, 170, 170 ], 11 | [ 170, 0, 0 ], 12 | [ 170, 0, 170 ], 13 | [ 170, 85, 0 ], 14 | [ 170, 170, 170 ], 15 | [ 85, 85, 85 ], 16 | [ 85, 85, 255 ], 17 | [ 85, 255, 85 ], 18 | [ 85, 255, 255 ], 19 | [ 255, 85, 85 ], 20 | [ 255, 85, 255 ], 21 | [ 255, 255, 85 ], 22 | [ 255, 255, 255 ] 23 | ], 24 | ATTR_FG_BLACK : 0x00, 25 | ATTR_FG_BLUE : 0x01, 26 | ATTR_FG_GREEN : 0x02, 27 | ATTR_FG_CYAN : 0x03, 28 | ATTR_FG_RED : 0x04, 29 | ATTR_FG_MAGENTA : 0x05, 30 | ATTR_FG_BROWN : 0x06, 31 | ATTR_FG_GRAY : 0x07, 32 | ATTR_FG_DARKGRAY: 0x08, 33 | ATTR_FG_BBLUE : 0x09, 34 | ATTR_FG_BGREEN : 0x0A, 35 | ATTR_FG_BCYAN : 0x0B, 36 | ATTR_FG_BRED : 0x0C, 37 | ATTR_FG_BMAGENTA: 0x0D, 38 | ATTR_FG_YELLOW : 0x0E, 39 | ATTR_FG_WHITE : 0x0F, 40 | ATTR_BG_BLACK : 0x00, 41 | ATTR_BG_BLUE : 0x10, 42 | ATTR_BG_GREEN : 0x20, 43 | ATTR_BG_CYAN : 0x30, 44 | ATTR_BG_RED : 0x40, 45 | ATTR_BG_MAGENTA : 0x50, 46 | ATTR_BG_BROWN : 0x60, 47 | ATTR_BG_GRAY : 0x70, 48 | ATTR_BLINK : 0x80, 49 | foregroundColorFromAttribute: function(attr) 50 | { 51 | return (attr & 0x0F); 52 | }, 53 | backgroundColorFromAttribute: function(attr) 54 | { 55 | return ((attr & 0x70) >> 4); 56 | } 57 | }; 58 | 59 | function TextConsole(canvas, width, height) 60 | { 61 | var self = this; 62 | 63 | this.canvas = canvas; 64 | this.width = width; 65 | this.height = height; 66 | this.screenText = new Uint8Array(width*height); 67 | this.screenAttr = new Uint8Array(width*height); 68 | this.fontImages = new Array(VGA.COLOR.length); 69 | this.characterWidth = 0; 70 | this.characterHeight = 0; 71 | 72 | this.onclick = function(event) {} 73 | 74 | this.canvas.addEventListener('click', 75 | function(event) { 76 | if (self.onclick) 77 | { 78 | /* compute the 'x' and 'y' cells */ 79 | var canvasX = event.pageX - event.target.offsetLeft; 80 | var canvasY = event.pageY - event.target.offsetTop; 81 | 82 | /* now divide by the width/height, and add as cellX/cellY */ 83 | event.cellX = Math.floor(canvasX * self.width / event.target.clientWidth); 84 | event.cellY = Math.floor(canvasY * self.height / event.target.clientHeight); 85 | 86 | self.onclick(event); 87 | } 88 | }, false); 89 | } 90 | 91 | TextConsole.prototype.getSpriteCoords = function(ord) 92 | { 93 | if (ord < 0 || ord > 255) 94 | ord = 0; 95 | 96 | /* The sprite sheet is 32 characters wide, 8 tall. */ 97 | var row = Math.floor(ord / 32); 98 | var col = ord % 32; 99 | 100 | return { 'y': row*this.characterHeight, 'x': col*this.characterWidth }; 101 | } 102 | 103 | TextConsole.prototype.init = function(callback) 104 | { 105 | this.loadFont("cp437.png", callback); 106 | } 107 | 108 | TextConsole.prototype.loadFont = function(url, callback) 109 | { 110 | var self = this; 111 | 112 | /* load the image that we use as the spritemap for the font. */ 113 | var fontImage = new Image(); 114 | fontImage.src = url; 115 | fontImage.onload = function() { 116 | /* once the image is loaded, use it as a template to generate all 117 | of the foreground colors. We only get pixel manipulation with 118 | a canvas, though, so we'll need a temporary canvas in order to 119 | get at the pixel data. And then we'll need to create a bunch 120 | of images or canvases, because an ImageData isn't a valid 121 | CanvasImageSource. ugggggh. */ 122 | /* kinda wondering if it'd be easier just to base64-encode the image 123 | and insert it inline then having to deal with this all in an 124 | onload handler... */ 125 | var canvas = document.createElement("canvas"); 126 | canvas.width = fontImage.width; 127 | canvas.height = fontImage.height; 128 | var ctx = canvas.getContext('2d'); 129 | ctx.drawImage(fontImage, 0, 0); 130 | var sourceImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); 131 | 132 | /* sprite sheets are assumed to be 32 chars wide, 8 tall */ 133 | self.characterWidth = Math.floor(canvas.width / 32); 134 | self.characterHeight = Math.floor(canvas.height / 8); 135 | 136 | for (var c = 0; c < VGA.COLOR.length; ++c) 137 | { 138 | self.fontImages[c] = document.createElement("canvas"); 139 | self.fontImages[c].width = fontImage.width; 140 | self.fontImages[c].height = fontImage.height; 141 | /* Contrary to what you would expect, this doesn't copy, 142 | it just makes a new image data with the same width/height */ 143 | var fontData = ctx.createImageData(sourceImageData); 144 | /* replace every black pixel with transparent, 145 | replace every white pixel with the color */ 146 | var fontDataPixels = fontData.data; 147 | var srcDataPixels = sourceImageData.data; 148 | for (var i = 0; i < fontDataPixels.length; i += 4) 149 | { 150 | if (srcDataPixels[i] == 0) 151 | { 152 | fontDataPixels[i] = 0; 153 | fontDataPixels[i+1] = 0; 154 | fontDataPixels[i+2] = 0; 155 | fontDataPixels[i+3] = 0; 156 | } 157 | else 158 | { 159 | fontDataPixels[i] = VGA.COLOR[c][0]; 160 | fontDataPixels[i+1] = VGA.COLOR[c][1]; 161 | fontDataPixels[i+2] = VGA.COLOR[c][2]; 162 | fontDataPixels[i+3] = 255; 163 | } 164 | } 165 | /* set it back on the convas */ 166 | self.fontImages[c].getContext('2d').putImageData(fontData, 0, 0); 167 | } 168 | 169 | callback(); 170 | } 171 | } 172 | 173 | TextConsole.prototype.resizeToScreen = function() 174 | { 175 | var gameWidth = window.innerWidth; 176 | var gameHeight = window.innerHeight; 177 | var scaleToFitX = gameWidth / (this.width*this.characterWidth); 178 | var scaleToFitY = gameHeight / (this.height*this.characterHeight); 179 | var bestRatio = Math.min(scaleToFitX, scaleToFitY); 180 | this.canvas.style.width = (this.width*this.characterWidth) * bestRatio + "px"; 181 | this.canvas.style.height = (this.height*this.characterHeight) * bestRatio + "px"; 182 | } 183 | 184 | TextConsole.prototype.set = function(x, y, ch, attr) 185 | { 186 | var index = y*this.width + x; 187 | this.screenText[index] = ch; 188 | this.screenAttr[index] = attr; 189 | } 190 | 191 | TextConsole.prototype.setString = function(x, y, str, attr) 192 | { 193 | for (var i = 0; i < str.length; ++i) 194 | this.set(x+i, y, str.charCodeAt(i), attr); 195 | } 196 | 197 | TextConsole.prototype.redrawAt = function(x, y) 198 | { 199 | var ctx = this.canvas.getContext('2d'); 200 | 201 | /* cheat and blit a full-block instead of messing with fillStyle */ 202 | var bgcell = this.getSpriteCoords(219); 203 | 204 | var index = y*this.width+x; 205 | var src = this.getSpriteCoords(this.screenText[index]); 206 | var attr = this.screenAttr[index]; 207 | var bgcolor = VGA.backgroundColorFromAttribute(attr); 208 | var fgcolor = VGA.foregroundColorFromAttribute(attr); 209 | 210 | if (this.fontImages[bgcolor] == null || this.fontImages[fgcolor] == null) 211 | { 212 | console.log("trying to draw before images loaded!"); 213 | return; 214 | } 215 | 216 | ctx.drawImage(this.fontImages[VGA.backgroundColorFromAttribute(attr)], 217 | bgcell.x, bgcell.y, 218 | this.characterWidth, this.characterHeight, 219 | x * this.characterWidth, y * this.characterHeight, 220 | this.characterWidth, this.characterHeight); 221 | ctx.drawImage(this.fontImages[VGA.foregroundColorFromAttribute(attr)], 222 | src.x, src.y, 223 | this.characterWidth, this.characterHeight, 224 | x * this.characterWidth, y * this.characterHeight, 225 | this.characterWidth, this.characterHeight); 226 | } 227 | 228 | TextConsole.prototype.redraw = function() 229 | { 230 | 231 | for (var y = 0; y < this.height; y++) 232 | { 233 | for (var x = 0; x < this.width; x++) 234 | { 235 | this.redrawAt(x, y); 236 | } 237 | } 238 | } 239 | 240 | -------------------------------------------------------------------------------- /worldloader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ZZTWorld() {} 4 | 5 | function ZZTWorldLoader() 6 | { 7 | } 8 | 9 | ZZTWorldLoader.prototype.init = function(url, callback) 10 | { 11 | var self = this; 12 | var request = new XMLHttpRequest(); 13 | request.open("GET", url, true); 14 | request.responseType = 'arraybuffer'; 15 | request.onload = function(e) 16 | { 17 | var world = null; 18 | if (this.status == 200) 19 | { 20 | var stream = new ZZTFileStream(this.response); 21 | world = self.parseWorldData(stream); 22 | } 23 | callback(world); 24 | } 25 | request.send(); 26 | } 27 | 28 | ZZTWorldLoader.prototype.parseWorldData = function(stream) 29 | { 30 | var world = new ZZTWorld(); 31 | 32 | world.worldType = stream.getInt16(); 33 | world.numBoards = stream.getInt16(); 34 | world.playerAmmo = stream.getInt16(); 35 | world.playerGems = stream.getInt16(); 36 | world.playerKeys = new Array(7); 37 | for (var i = 0; i < 7; ++i) 38 | world.playerKeys[i] = stream.getBoolean(); 39 | world.playerHealth = stream.getInt16(); 40 | world.playerBoard = stream.getInt16(); 41 | 42 | world.playerTorches = stream.getInt16(); 43 | world.torchCycles = stream.getInt16(); 44 | world.energyCycles = stream.getInt16(); 45 | stream.position += 2; /* unused */ 46 | world.playerScore = stream.getInt16(); 47 | 48 | world.worldName = stream.getFixedPascalString(20); 49 | world.flag = new Array(10); 50 | for (var i = 0; i < 10; ++i) 51 | world.flag[i] = stream.getFixedPascalString(20); 52 | 53 | world.timeLeft = stream.getInt16(); 54 | stream.position += 2; /* playerdata pointer */ 55 | world.locked = stream.getBoolean(); 56 | world.board = []; 57 | 58 | /* board information then starts at offset 512 */ 59 | stream.position = 512; 60 | 61 | for (var i = 0; i < world.numBoards; ++i) 62 | world.board.push(this.parseZZTBoard(stream)); 63 | 64 | return world; 65 | } 66 | 67 | ZZTWorldLoader.prototype.parseZZTBoard = function(stream) 68 | { 69 | var boardOffset = stream.position; 70 | var boardSize = stream.getInt16(); 71 | 72 | var board = new ZZTBoard; 73 | board.name = stream.getFixedPascalString(50); 74 | 75 | board.width = 60; 76 | board.height = 25; 77 | board.player = null; 78 | 79 | var tiles = []; 80 | /* what follows now is RLE data, encoding 1500 tiles */ 81 | while (tiles.length < (board.width * board.height)) 82 | { 83 | var count = stream.getUint8(); 84 | var typeid = stream.getUint8(); 85 | var color = stream.getUint8(); 86 | 87 | /* A count of zero actually means 256 tiles. The built-in editor 88 | never encodes like this, but some other editors do. */ 89 | if (count == 0) count = 256; 90 | 91 | for (var i = 0; i < count; ++i) 92 | { 93 | tiles.push(makeTile(typeid, color)); 94 | } 95 | } 96 | board.tiles = tiles; 97 | 98 | /* following the RLE data, we then have... */ 99 | board.maxPlayerShots = stream.getUint8(); 100 | board.isDark = stream.getUint8(); 101 | board.exitNorth = stream.getUint8(); 102 | board.exitSouth = stream.getUint8(); 103 | board.exitWest = stream.getUint8(); 104 | board.exitEast = stream.getUint8(); 105 | board.restartOnZap = stream.getUint8(); 106 | board.onScreenMessage = stream.getFixedPascalString(58); /* never used? */ 107 | board.messageTimer = 0; 108 | board.playerEnterX = stream.getUint8(); 109 | board.playerEnterY = stream.getUint8(); 110 | board.timeLimit = stream.getInt16(); 111 | stream.position += 16; /* unused */ 112 | var statusElementCount = stream.getInt16() + 1; 113 | 114 | var statusElement = []; 115 | for (var i = 0; i < statusElementCount; ++i) 116 | statusElement.push(this.parseStatusElement(stream)); 117 | 118 | /* for objects with code pointers referring to a different object, link them. */ 119 | for (var i = 0; i < statusElementCount; ++i) 120 | { 121 | if (statusElement[i].codeLength < 0) 122 | statusElement[i].code = this.statusElement[-this.statusElement[i].codeLength].code; 123 | } 124 | 125 | board.statusElement = statusElement; 126 | 127 | /* update all the line characters */ 128 | board.updateLines(); 129 | 130 | /* jump to next board */ 131 | stream.position = boardOffset + boardSize + 2; 132 | 133 | return board; 134 | } 135 | 136 | ZZTWorldLoader.prototype.parseStatusElement = function(stream) 137 | { 138 | var status = {}; 139 | 140 | /* x and y coordinates are 1-based for some reason */ 141 | status.x = stream.getUint8() - 1; 142 | status.y = stream.getUint8() - 1; 143 | 144 | status.xStep = stream.getInt16(); 145 | status.yStep = stream.getInt16(); 146 | status.cycle = stream.getInt16(); 147 | 148 | status.param1 = stream.getUint8(); 149 | status.param2 = stream.getUint8(); 150 | status.param3 = stream.getUint8(); 151 | 152 | status.follower = stream.getInt16(); 153 | status.leader = stream.getInt16(); 154 | var underType = stream.getUint8(); 155 | var underColor = stream.getUint8(); 156 | status.underTile = makeTile(underType, underColor); 157 | stream.position += 4; /* pointer is not used when loading */ 158 | status.currentInstruction = stream.getInt16(); 159 | status.codeLength = stream.getInt16(); 160 | 161 | /* for ZZT and not Super ZZT, eight bytes of padding follow */ 162 | stream.position += 8; 163 | 164 | /* if status.codeLength is positive, there is that much ZZT-OOP code following */ 165 | if (status.codeLength > 0) 166 | { 167 | status.code = stream.getFixedString(status.codeLength); 168 | } 169 | else 170 | { 171 | /* it's negative, which means that we'll need to look at a different 172 | object in order to use it's code instead; we'll do that later. */ 173 | status.code = null; 174 | } 175 | 176 | return status; 177 | } 178 | -------------------------------------------------------------------------------- /worlds/basetest.zzt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstreiff/zztjs/bf58ab7536d9dc1255676e0b6a492f0bd5e5bd42/worlds/basetest.zzt -------------------------------------------------------------------------------- /worlds/caves.zzt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstreiff/zztjs/bf58ab7536d9dc1255676e0b6a492f0bd5e5bd42/worlds/caves.zzt -------------------------------------------------------------------------------- /worlds/city.zzt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstreiff/zztjs/bf58ab7536d9dc1255676e0b6a492f0bd5e5bd42/worlds/city.zzt -------------------------------------------------------------------------------- /worlds/demo.zzt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstreiff/zztjs/bf58ab7536d9dc1255676e0b6a492f0bd5e5bd42/worlds/demo.zzt -------------------------------------------------------------------------------- /worlds/dungeons.zzt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstreiff/zztjs/bf58ab7536d9dc1255676e0b6a492f0bd5e5bd42/worlds/dungeons.zzt -------------------------------------------------------------------------------- /worlds/index.json: -------------------------------------------------------------------------------- 1 | { "worlds": [ 2 | { "file": "basetest.zzt", "shortname": "BASETEST", "name": "Behavior And Side Effect Test" }, 3 | { "file": "caves.zzt", "shortname": "CAVES", "name": "The Caves of ZZT" }, 4 | { "file": "city.zzt", "shortname": "CITY", "name": "Underground City of ZZT" }, 5 | { "file": "demo.zzt", "shortname": "DEMO", "name": "Demo of the ZZT World Editor" }, 6 | { "file": "dungeons.zzt", "shortname": "DUNGEONS", "name": "The Dungeons of ZZT" }, 7 | { "file": "tour.zzt", "shortname": "TOUR", "name": "Guided Tour ZZT's Other Worlds" }, 8 | { "file": "town.zzt", "shortname": "TOWN", "name": "The Town of ZZT" } 9 | ]} -------------------------------------------------------------------------------- /worlds/tour.zzt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstreiff/zztjs/bf58ab7536d9dc1255676e0b6a492f0bd5e5bd42/worlds/tour.zzt -------------------------------------------------------------------------------- /worlds/town.zzt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bstreiff/zztjs/bf58ab7536d9dc1255676e0b6a492f0bd5e5bd42/worlds/town.zzt -------------------------------------------------------------------------------- /zztboard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ZZTTile(typeid, color) 4 | { 5 | this.typeid = typeid; 6 | this.color = color; 7 | this.properties = BoardObjects[this.typeid]; 8 | } 9 | 10 | var _ZZTBoard_BoardEmpty = new ZZTTile(0, 0); 11 | var _ZZTBoard_BoardEdge = new ZZTTile(1, 0); 12 | 13 | /* Construct a tile, with a special case for empties: empty tiles have no color, 14 | so we can reuse the same reference for all of them. */ 15 | function makeTile(typeid, color) 16 | { 17 | if (typeid == 0) 18 | return _ZZTBoard_BoardEmpty; 19 | else 20 | return new ZZTTile(typeid, color); 21 | } 22 | 23 | function ZZTBoard() 24 | { 25 | this.actorIndex = 0; 26 | this.tick = 0; 27 | } 28 | 29 | ZZTBoard.prototype.withinBoard = function(x, y) 30 | { 31 | if (x < 0 || x >= this.width || y < 0 || y >= this.height) 32 | return false; 33 | else 34 | return true; 35 | } 36 | 37 | ZZTBoard.prototype.get = function(x, y) 38 | { 39 | if (!this.withinBoard(x, y)) 40 | return _ZZTBoard_BoardEdge; 41 | else 42 | return this.tiles[y * this.width + x]; 43 | } 44 | 45 | ZZTBoard.prototype.getActorIndexAt = function(x, y) 46 | { 47 | for (var i = 0; i < this.statusElement.length; ++i) 48 | { 49 | if (this.statusElement[i].x == x && this.statusElement[i].y == y) 50 | return i; 51 | } 52 | return -1; 53 | } 54 | 55 | ZZTBoard.prototype.getActorAt = function(x, y) 56 | { 57 | var index = this.getActorIndexAt(x, y); 58 | if (index >= 0) 59 | return this.statusElement[index]; 60 | else 61 | return null; 62 | } 63 | 64 | ZZTBoard.prototype.set = function(x, y, tile) 65 | { 66 | this.tiles[y * this.width + x] = tile; 67 | } 68 | 69 | ZZTBoard.prototype.update = function() 70 | { 71 | var self = this; 72 | 73 | if (this.actorIndex >= this.statusElement.length) 74 | { 75 | this.tick++; 76 | /* According to roton the tick counter wraps at 420. */ 77 | if (this.tick > 420) 78 | this.tick = 1; 79 | this.actorIndex = 0; 80 | } 81 | 82 | while (this.actorIndex < this.statusElement.length) 83 | { 84 | var actor = this.statusElement[this.actorIndex]; 85 | var cycle = actor.cycle; 86 | if (cycle != 0) 87 | { 88 | if (!(this.tick % cycle)) 89 | { 90 | var tile = this.get(actor.x, actor.y); 91 | if (tile.properties.update) 92 | tile.properties.update(this, this.actorIndex); 93 | } 94 | } 95 | this.actorIndex++; 96 | } 97 | } 98 | 99 | ZZTBoard.prototype.remove = function(x, y) 100 | { 101 | this.set(x, y, _ZZTBoard_BoardEmpty); 102 | } 103 | 104 | ZZTBoard.prototype.move = function(sx, sy, dx, dy) 105 | { 106 | var actorIndex = this.getActorIndexAt(sx, sy); 107 | if (actorIndex < -1) 108 | { 109 | /* not an actor, just move tile */ 110 | this.set(dx, dy, this.get(sx, sy)); 111 | this.remove(sx, sy); 112 | } 113 | else 114 | { 115 | this.moveActor(actorIndex, dx, dy); 116 | } 117 | } 118 | 119 | ZZTBoard.prototype.moveActor = function(actorIndex, x, y) 120 | { 121 | var actorData = this.statusElement[actorIndex]; 122 | var srcTile = this.get(actorData.x, actorData.y); 123 | var dstTile = this.get(x, y); 124 | 125 | this.set(actorData.x, actorData.y, actorData.underTile); 126 | this.set(x, y, srcTile); 127 | 128 | actorData.x = x; 129 | actorData.y = y; 130 | } 131 | 132 | ZZTBoard.prototype.draw = function(textconsole) 133 | { 134 | for (var y = 0; y < this.height; ++y) 135 | { 136 | for (var x = 0; x < this.width; ++x) 137 | { 138 | var tile = this.get(x, y); 139 | var renderInfo = null; 140 | 141 | if (tile.properties.draw) 142 | { 143 | renderInfo = tile.properties.draw(this, x, y); 144 | } 145 | else 146 | { 147 | renderInfo = getTileRenderInfo(tile); 148 | } 149 | textconsole.set(x, y, renderInfo.glyph, renderInfo.color); 150 | } 151 | } 152 | 153 | if (this.messageTimer > 0) 154 | { 155 | /* TODO: actually work out how to make this multiline */ 156 | textconsole.setString( 157 | Math.floor((this.width / 2) - (this.onScreenMessage.length / 2)), 158 | 24, 159 | this.onScreenMessage, 160 | (this.messageTimer % 6) + VGA.ATTR_FG_BBLUE); 161 | --this.messageTimer; 162 | } 163 | } 164 | 165 | ZZTBoard.prototype.setMessage = function(msg) 166 | { 167 | /* TODO: actually work out how to make this multiline */ 168 | if (msg.length >= (this.width - 2)) 169 | { 170 | msg = msg.substr(0, (this.width - 2)) 171 | } 172 | this.onScreenMessage = " " + msg + " "; 173 | this.messageTimer = 24; 174 | } 175 | 176 | var _ZZTBoard_LineGlyphs = 177 | [ 178 | /* NESW */ 179 | /* 0000 */ 249, 180 | /* 0001 */ 181, 181 | /* 0010 */ 210, 182 | /* 0011 */ 187, 183 | /* 0100 */ 198, 184 | /* 0101 */ 205, 185 | /* 0110 */ 201, 186 | /* 0111 */ 203, 187 | /* 1000 */ 208, 188 | /* 1001 */ 188, 189 | /* 1010 */ 186, 190 | /* 1011 */ 185, 191 | /* 1100 */ 200, 192 | /* 1101 */ 202, 193 | /* 1110 */ 204, 194 | /* 1111 */ 206 195 | ]; 196 | 197 | /* Update the glyphs of all line characters on the board. 198 | 199 | We only need to do this whenever one of them changes. */ 200 | ZZTBoard.prototype.updateLines = function() 201 | { 202 | for (var y = 0; y < this.height; ++y) 203 | { 204 | for (var x = 0; x < this.width; ++x) 205 | { 206 | var tile = this.get(x, y); 207 | if (tile.name == "line") 208 | { 209 | var glyphIndex = 0; 210 | 211 | if ((y == 0) || (this.get(x, y-1).name == "line")) 212 | glyphIndex += 8; 213 | 214 | if ((x == this.width-1) || (this.get(x+1, y).name == "line")) 215 | glyphIndex += 4; 216 | 217 | if ((y == this.height-1) || (this.get(x, y+1).name == "line")) 218 | glyphIndex += 2; 219 | 220 | if ((x == 0) || (this.get(x-1, y).name == "line")) 221 | glyphIndex += 1; 222 | 223 | tile.glyph = _ZZTBoard_LineGlyphs[glyphIndex]; 224 | } 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /zztdialog.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Dialog. 4 | 5 | The ZZT dialog is rendered like: 6 | 7 | |=========================| 8 | | Title | 9 | |=======================| 10 | | | 11 | | Some text | 12 | |> Text <| 13 | | More text | 14 | | | 15 | |=========================| 16 | 17 | (It actually uses linedrawing characters, but I try to stick to ASCII 18 | encoded text for comments.) 19 | 20 | The .x, .y, .width, and .height are specified in terms of character cells. 21 | The selected line is always in the center. 22 | 23 | Text lines can come in three forms: 24 | 'text' - Regular text. Yellow-on-blue. 25 | '!msg;text' - Label selection. Solid purple arrow, rest of text is white. 26 | Sends 'msg' to the object on selection. 27 | '$text' - Header. Centered, white-on-blue. 28 | */ 29 | 30 | function _ZZTDialog_parseLine(line) 31 | { 32 | var parsed = {}; 33 | if (line.charAt(0) == "!") 34 | { 35 | // !msg;text 36 | var delim = line.slice(1).split(";"); 37 | parsed.message = delim[0]; 38 | parsed.text = delim[1]; 39 | } 40 | else if (line.charAt(0) == "$") 41 | { 42 | // $text 43 | parsed.text = line.slice(1); 44 | parsed.centered = true; 45 | } 46 | else 47 | { 48 | parsed.text = line; 49 | } 50 | return parsed; 51 | } 52 | 53 | function ZZTDialog(title, lines) 54 | { 55 | /* x/y/w/h includes the borders. */ 56 | this.x = 5; 57 | this.y = 3; 58 | this.width = 49; 59 | this.height = 19; 60 | 61 | this.title = title; 62 | this.lines = []; 63 | for (var i = 0; i < lines.length; ++i) 64 | { 65 | this.lines.push(_ZZTDialog_parseLine(lines[i])) 66 | } 67 | 68 | this.selectedLine = 0; 69 | this.done = null; 70 | } 71 | 72 | ZZTDialog.prototype.keydown = function(event) 73 | { 74 | if (event.keyCode == 40) /* down */ 75 | { 76 | if (this.selectedLine < this.lines.length-2); 77 | this.selectedLine++; 78 | } 79 | else if (event.keyCode == 38) /* up */ 80 | { 81 | if (this.selectedLine > 0) 82 | this.selectedLine--; 83 | } 84 | else if (event.keyCode == 13) /* enter */ 85 | { 86 | this.done = { dialog: this, line: this.selectedLine }; 87 | } 88 | else if (event.keyCode == 27) /* escape */ 89 | { 90 | this.done = { dialog: this, cancelled: true }; 91 | } 92 | } 93 | 94 | ZZTDialog.prototype.draw = function(textconsole) 95 | { 96 | /* top row */ 97 | textconsole.set(this.x, this.y, 198, VGA.ATTR_FG_WHITE); 98 | textconsole.set(this.x+1, this.y, 209, VGA.ATTR_FG_WHITE); 99 | 100 | for (var x = this.x+2; x < this.x+this.width-2; ++x) 101 | textconsole.set(x, this.y, 205, VGA.ATTR_FG_WHITE); 102 | 103 | textconsole.set(this.x+this.width-2, this.y, 209, VGA.ATTR_FG_WHITE); 104 | textconsole.set(this.x+this.width-1, this.y, 181, VGA.ATTR_FG_WHITE); 105 | 106 | /* title row */ 107 | 108 | textconsole.set(this.x, this.y+1, 32, 0); 109 | textconsole.set(this.x+1, this.y+1, 179, VGA.ATTR_FG_WHITE); 110 | 111 | for (var x = this.x+2; x < this.x+this.width-2; ++x) 112 | textconsole.set(x, this.y+1, 32, VGA.ATTR_BG_BLUE); 113 | 114 | textconsole.setString( 115 | Math.floor((60 - this.title.length) / 2), 116 | this.y+1, 117 | this.title, 118 | VGA.ATTR_BG_BLUE|VGA.ATTR_FG_YELLOW); 119 | 120 | textconsole.set(this.x+this.width-2, this.y+1, 179, VGA.ATTR_FG_WHITE); 121 | textconsole.set(this.x+this.width-1, this.y+1, 32, 0); 122 | 123 | /* titlebar dividing row */ 124 | 125 | textconsole.set(this.x, this.y+2, 32, 0); 126 | textconsole.set(this.x+1, this.y+2, 198, VGA.ATTR_FG_WHITE); 127 | 128 | for (var x = this.x+2; x < this.x+this.width-2; ++x) 129 | textconsole.set(x, this.y+2, 205, VGA.ATTR_FG_WHITE); 130 | 131 | textconsole.set(this.x+this.width-2, this.y+2, 181, VGA.ATTR_FG_WHITE); 132 | textconsole.set(this.x+this.width-1, this.y+2, 32, 0); 133 | 134 | /* now, we do the text portion */ 135 | 136 | var viewportHeight = this.height-4; 137 | var centerLineInViewport = Math.floor(viewportHeight/2); 138 | 139 | for (var l = 0; l < viewportHeight; ++l) 140 | { 141 | var y = this.y+3+l; 142 | 143 | textconsole.set(this.x, y, 32, 0); 144 | textconsole.set(this.x+1, y, 179, VGA.ATTR_FG_WHITE); 145 | 146 | for (var x = this.x+2; x < this.x+this.width-2; ++x) 147 | textconsole.set(x, y, 32, VGA.ATTR_BG_BLUE); 148 | 149 | if (l == centerLineInViewport) 150 | { 151 | textconsole.set(this.x+2, y, 175, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_BRED); 152 | textconsole.set(this.x+this.width-3, y, 174, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_BRED); 153 | } 154 | 155 | var textLineIndex = this.selectedLine + (l - centerLineInViewport); 156 | if (textLineIndex < 0 || textLineIndex >= this.lines.length) 157 | { 158 | /* off the page */ 159 | for (var x = this.x+2; x < this.x+this.width-2; ++x) 160 | textconsole.set(x, y, 32, VGA.ATTR_BG_BLUE); 161 | } 162 | else 163 | { 164 | if (l == centerLineInViewport) 165 | textconsole.set(this.x+2, y, 175, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_BRED); 166 | else 167 | textconsole.set(this.x+2, y, 32, VGA.ATTR_BG_BLUE); 168 | textconsole.set(this.x+3, y, 32, VGA.ATTR_BG_BLUE); 169 | 170 | var line = this.lines[textLineIndex]; 171 | 172 | if (line.message) 173 | { 174 | textconsole.set(this.x+6, y, 16, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_BMAGENTA); 175 | textconsole.setString(this.x+9, y, line.text, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 176 | } 177 | else if (line.centered) 178 | { 179 | textconsole.setString( 180 | Math.floor((60 - this.title.length) / 2), y, 181 | line.text, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 182 | } 183 | else 184 | { 185 | textconsole.setString(this.x+4, y, this.lines[textLineIndex].text, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_YELLOW); 186 | } 187 | 188 | textconsole.set(this.x+this.width-4, y, 32, VGA.ATTR_BG_BLUE); 189 | if (l == centerLineInViewport) 190 | textconsole.set(this.x+this.width-3, y, 174, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_BRED); 191 | else 192 | textconsole.set(this.x+this.width-3, y, 32, VGA.ATTR_BG_BLUE); 193 | } 194 | 195 | textconsole.set(this.x+this.width-2, y, 179, VGA.ATTR_FG_WHITE); 196 | textconsole.set(this.x+this.width-1, y, 32, 0); 197 | } 198 | 199 | /* bottom row */ 200 | textconsole.set(this.x, this.y+this.height-1, 198, VGA.ATTR_FG_WHITE); 201 | textconsole.set(this.x+1, this.y+this.height-1, 207, VGA.ATTR_FG_WHITE); 202 | 203 | for (var x = this.x+2; x < this.x+this.width-2; ++x) 204 | textconsole.set(x, this.y+this.height-1, 205, VGA.ATTR_FG_WHITE); 205 | 206 | textconsole.set(this.x+this.width-2, this.y+this.height-1, 207, VGA.ATTR_FG_WHITE); 207 | textconsole.set(this.x+this.width-1, this.y+this.height-1, 181, VGA.ATTR_FG_WHITE); 208 | } 209 | -------------------------------------------------------------------------------- /zztfilestream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function ZZTFileStream(arrayBuffer) 4 | { 5 | this.dataView = new DataView(arrayBuffer); 6 | this.position = 0; 7 | } 8 | 9 | ZZTFileStream.prototype.getUint8 = function() 10 | { 11 | return this.dataView.getUint8(this.position++); 12 | } 13 | 14 | ZZTFileStream.prototype.getBoolean = function() 15 | { 16 | return this.dataView.getUint8(this.position++) > 0; 17 | } 18 | 19 | ZZTFileStream.prototype.getInt16 = function() 20 | { 21 | var v = this.dataView.getInt16(this.position, true); 22 | this.position += 2; 23 | return v; 24 | } 25 | 26 | /* Strings are 1 byte length, followed by maxlen bytes of data */ 27 | ZZTFileStream.prototype.getFixedPascalString = function(maxlen) 28 | { 29 | var len = this.getUint8(); 30 | if (len > maxlen) 31 | len = maxlen; 32 | 33 | var str = this.getFixedString(len); 34 | 35 | /* advance the rest */ 36 | this.position += (maxlen - len); 37 | return str; 38 | } 39 | 40 | ZZTFileStream.prototype.getFixedString = function(len) 41 | { 42 | var str = ""; 43 | for (var i = 0; i < len; ++i) 44 | { 45 | var ch = this.getUint8(); 46 | if (ch == 13) 47 | str += "\n"; 48 | else 49 | str += String.fromCharCode(ch); 50 | } 51 | return str; 52 | } 53 | -------------------------------------------------------------------------------- /zztgame.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* parse out key-value pairs from the fragment identifier. 4 | http://.../blorp#!abc&def=blah 5 | 6 | The first entry without a '=' is returned as 'world'. 7 | 8 | If the string is empty, returns {world:""}. 9 | */ 10 | function parseFragmentParams() 11 | { 12 | var kvs = {}; 13 | 14 | /* split on '&' */ 15 | var str = window.location.hash; 16 | var hashes = str.slice(str.indexOf("#!") + 2).split('&'); 17 | 18 | for (var i = 0; i < hashes.length; i++) 19 | { 20 | var hash = hashes[i].split('='); 21 | 22 | if (hash.length > 1) 23 | { 24 | /* there is a key and there is a value */ 25 | kvs[hash[0]] = hash[1]; 26 | } 27 | else 28 | { 29 | /* if we haven't declared 'world', the first entry with no '=' is it. */ 30 | if (!("world" in kvs)) 31 | { 32 | kvs["world"] = hash[0]; 33 | } 34 | else 35 | { 36 | kvs[hash[0]] = true; 37 | } 38 | } 39 | } 40 | 41 | return kvs; 42 | } 43 | 44 | /* Yay, browser quirks! */ 45 | window.requestAnimationFrame = 46 | window.requestAnimationFrame || 47 | window.mozRequestAnimationFrame || 48 | window.webkitRequestAnimationFrame || 49 | window.msRequestAnimationFrame; 50 | 51 | var game = { 52 | inputEvent: 0, 53 | quiet: false, 54 | fps: 9.1032548384, 55 | debug: true, 56 | dialog: null, 57 | tick: 0 58 | }; 59 | 60 | var ZInputEvent = Object.freeze({ 61 | USE_TORCH : 1, 62 | HELP : 3, 63 | SAVE : 4, 64 | PAUSE : 5, 65 | QUIT : 6, 66 | WALK_NORTH : 7, 67 | SHOOT_NORTH : 8, 68 | WALK_EAST : 9, 69 | SHOOT_EAST : 10, 70 | WALK_SOUTH : 11, 71 | SHOOT_SOUTH : 12, 72 | WALK_WEST : 13, 73 | SHOOT_WEST : 14 74 | }); 75 | 76 | function getWorldList(callback) 77 | { 78 | var request = new XMLHttpRequest(); 79 | request.open("GET", "worlds/index.json", true); 80 | request.onload = function(e) 81 | { 82 | var worlds = {}; 83 | if (this.status == 200) 84 | { 85 | console.log(this.response); 86 | var resp = JSON.parse(this.response); 87 | worlds = resp.worlds; 88 | } 89 | callback(worlds); 90 | } 91 | request.send(); 92 | } 93 | 94 | function mainMenuKeyDown(event) 95 | { 96 | if (event.keyCode == 87) /* "W" */ 97 | { 98 | /* select world */ 99 | getWorldList(function(worlds) { 100 | var entries = []; 101 | var filenames = []; 102 | for (var i = 0; i < worlds.length; ++i) 103 | { 104 | entries.push(worlds[i].shortname); 105 | filenames.push(worlds[i].file); 106 | } 107 | entries.push("Exit"); 108 | filenames.push(null); 109 | game.dialog = new ZZTDialog("ZZT Worlds", entries); 110 | game.dialog.filenames = filenames; 111 | 112 | game.dialog.callback = function(ev) 113 | { 114 | if (!ev.cancelled && ev.dialog.filenames[ev.line]) 115 | { 116 | var filename = ev.dialog.filenames[ev.line]; 117 | window.location.hash = "#!" + filename; 118 | gameLoad("worlds/" + filename); 119 | return true; 120 | } 121 | return false; 122 | } 123 | }); 124 | } 125 | else if (event.keyCode == 80) /* "P" */ 126 | { 127 | /* play game */ 128 | game.world.currentBoard = game.world.board[game.world.playerBoard]; 129 | game.atTitleScreen = false; 130 | } 131 | else if (event.keyCode == 82) /* "R" */ 132 | { 133 | /* restore game, does nothing right now */ 134 | } 135 | } 136 | 137 | function inGameKeyDown(event) 138 | { 139 | if (event.keyCode == 84) /* "T" */ 140 | { 141 | game.inputEvent = ZInputEvent.USE_TORCH; 142 | return true; 143 | } 144 | else if (event.keyCode == 66) /* "B" */ 145 | { 146 | /* toggling sound doesn't alter the game state at all, so 147 | lets just do it immediately */ 148 | game.quiet = !game.quiet; 149 | return true; 150 | } 151 | else if (event.keyCode == 72) /* "H" */ 152 | { 153 | game.inputEvent = ZInputEvent.HELP; 154 | return true; 155 | } 156 | else if (event.keyCode == 83) /* "S" */ 157 | { 158 | game.inputEvent = ZInputEvent.SAVE; 159 | return true; 160 | } 161 | else if (event.keyCode == 80) /* "P" */ 162 | { 163 | game.inputEvent = ZInputEvent.PAUSE; 164 | return true; 165 | } 166 | else if (event.keyCode == 81) /* "Q" */ 167 | { 168 | game.inputEvent = ZInputEvent.QUIT; 169 | return true; 170 | } 171 | else if (event.keyCode == 37) /* Left */ 172 | { 173 | if (event.shiftKey) 174 | game.inputEvent = ZInputEvent.SHOOT_WEST; 175 | else 176 | game.inputEvent = ZInputEvent.WALK_WEST; 177 | return true; 178 | } 179 | else if (event.keyCode == 38) /* Up */ 180 | { 181 | if (event.shiftKey) 182 | game.inputEvent = ZInputEvent.SHOOT_NORTH; 183 | else 184 | game.inputEvent = ZInputEvent.WALK_NORTH; 185 | return true; 186 | } 187 | else if (event.keyCode == 39) /* Right */ 188 | { 189 | if (event.shiftKey) 190 | game.inputEvent = ZInputEvent.SHOOT_EAST; 191 | else 192 | game.inputEvent = ZInputEvent.WALK_EAST; 193 | return true; 194 | } 195 | else if (event.keyCode == 40) /* Down */ 196 | { 197 | if (event.shiftKey) 198 | game.inputEvent = ZInputEvent.SHOOT_SOUTH; 199 | else 200 | game.inputEvent = ZInputEvent.WALK_SOUTH; 201 | return true; 202 | } 203 | 204 | return false; 205 | } 206 | 207 | function gameKeyDown(event) 208 | { 209 | if (game.dialog) 210 | game.dialog.keydown(event); 211 | else if (game.atTitleScreen) 212 | mainMenuKeyDown(event); 213 | else 214 | inGameKeyDown(event); 215 | } 216 | 217 | function gameInit(canvas) 218 | { 219 | // gotta start somewhere. 220 | game.console = new TextConsole(canvas, 80, 25); 221 | 222 | var opts = parseFragmentParams(); 223 | 224 | // Initialize the console. 225 | game.console.init(function() { 226 | game.console.resizeToScreen(); 227 | game.console.redraw(); 228 | 229 | // Resize the console when the window resizes. 230 | window.addEventListener("resize", function() { 231 | game.console.resizeToScreen(); 232 | }, false); 233 | 234 | window.addEventListener("keydown", gameKeyDown, false); 235 | 236 | game.console.onclick = function(event) 237 | { 238 | if (game.debug) 239 | { 240 | if (event.cellX < 60 && game.world) 241 | { 242 | var board = game.world.board[game.world.playerBoard]; 243 | // find the tile at this location 244 | var tile = board.tiles[event.cellY*60+event.cellX]; 245 | console.log({x:event.cellX,y:event.cellY}); 246 | console.log(tile); 247 | } 248 | } 249 | } 250 | }); 251 | 252 | game.audio = new ZZTAudio(); 253 | 254 | if (!opts.world) 255 | opts.world = "town.zzt"; 256 | 257 | gameLoad("worlds/" + opts.world); 258 | } 259 | 260 | function goToTitleScreen() 261 | { 262 | game.world.currentBoard = game.world.board[0]; 263 | 264 | /* remove the player from the title screen */ 265 | /* 266 | if (game.world.currentBoard.player) 267 | { 268 | var obj = new Empty; 269 | obj.x = game.world.currentBoard.player.x; 270 | obj.y = game.world.currentBoard.player.y; 271 | game.world.currentBoard.set( 272 | game.world.currentBoard.player.x, 273 | game.world.currentBoard.player.y, 274 | obj); 275 | game.world.currentBoard.player = null; 276 | } 277 | */ 278 | 279 | game.atTitleScreen = true; 280 | } 281 | 282 | function gameLoad(url) 283 | { 284 | game.worldurl = url; 285 | var worldLoader = new ZZTWorldLoader(); 286 | worldLoader.init(game.worldurl, function(world) { 287 | game.world = world; 288 | game.dialog = null; 289 | goToTitleScreen(); 290 | gameTick(); 291 | }); 292 | } 293 | 294 | function drawTitleScreenStatusBar() 295 | { 296 | game.console.setString(62, 7, " W ", VGA.ATTR_BG_CYAN); 297 | game.console.setString(66, 7, "World", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_YELLOW); 298 | game.console.setString(69, 8, game.world.worldName, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 299 | 300 | game.console.setString(62, 11, " P ", VGA.ATTR_BG_GRAY); 301 | game.console.setString(66, 11, "Play", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 302 | game.console.setString(62, 12, " R ", VGA.ATTR_BG_CYAN); 303 | game.console.setString(66, 12, "Restore game", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_YELLOW); 304 | 305 | game.console.setString(62, 16, " A ", VGA.ATTR_BG_CYAN); 306 | game.console.setString(66, 16, "About ZZT!", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 307 | game.console.setString(62, 17, " H ", VGA.ATTR_BG_GRAY); 308 | game.console.setString(66, 17, "High Scores", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_YELLOW); 309 | game.console.setString(62, 18, " E ", VGA.ATTR_BG_CYAN); 310 | game.console.setString(66, 18, "Board Editor", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_YELLOW); 311 | 312 | game.console.setString(62, 21, " S ", VGA.ATTR_BG_GRAY); 313 | game.console.setString(66, 21, "Game speed:", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_YELLOW); 314 | game.console.setString(66, 23, "F....:....S", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_YELLOW); 315 | } 316 | 317 | function drawGameStatusBar() 318 | { 319 | var yellowOnBlue = VGA.ATTR_BG_BLUE|VGA.ATTR_FG_YELLOW; 320 | 321 | game.console.set(62, 7, 2, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 322 | game.console.set(62, 8, 132, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_BCYAN); 323 | game.console.set(62, 9, 157, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_BROWN); 324 | game.console.set(62, 10, 4, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_BCYAN); 325 | game.console.set(62, 12, 12, VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 326 | 327 | game.console.setString(64, 7, " Health:" + game.world.playerHealth, yellowOnBlue); 328 | game.console.setString(64, 8, " Ammo:" + game.world.playerAmmo, yellowOnBlue); 329 | game.console.setString(64, 9, "Torches:" + game.world.playerTorches, yellowOnBlue); 330 | game.console.setString(64, 10, " Gems:" + game.world.playerGems, yellowOnBlue); 331 | game.console.setString(64, 11, " Score:" + game.world.playerScore, yellowOnBlue); 332 | game.console.setString(64, 12, " Keys:", yellowOnBlue); 333 | 334 | var keyColors = [ 335 | VGA.ATTR_FG_BBLUE, 336 | VGA.ATTR_FG_BGREEN, 337 | VGA.ATTR_FG_BCYAN, 338 | VGA.ATTR_FG_BRED, 339 | VGA.ATTR_FG_BMAGENTA, 340 | VGA.ATTR_FG_YELLOW, 341 | VGA.ATTR_FG_WHITE ]; 342 | 343 | for (var i = 0; i < 7; ++i) 344 | { 345 | game.console.set(72+i, 12, 346 | (game.world.playerKeys[i] ? 12 : 0), 347 | VGA.ATTR_BG_BLUE|keyColors[i]); 348 | } 349 | 350 | game.console.setString(62, 14, " T ", VGA.ATTR_BG_GRAY); 351 | game.console.setString(66, 14, "Torch", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 352 | game.console.setString(62, 15, " B ", VGA.ATTR_BG_CYAN); 353 | if (game.quiet) 354 | game.console.setString(66, 15, "Be noisy", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 355 | else 356 | game.console.setString(66, 15, "Be quiet", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 357 | game.console.setString(62, 16, " H ", VGA.ATTR_BG_GRAY); 358 | game.console.setString(66, 16, "Help", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 359 | 360 | // UDRL arrows are chars 24-27. 361 | game.console.set(67, 18, 0, VGA.ATTR_BG_CYAN); 362 | for (var i = 0; i < 4; ++i) 363 | game.console.set(68+i, 18, 24+i, VGA.ATTR_BG_CYAN); 364 | game.console.setString(73, 18, "Move", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 365 | game.console.setString(61, 19, " Shift ", VGA.ATTR_BG_GRAY); 366 | for (var i = 0; i < 4; ++i) 367 | game.console.set(68+i, 19, 24+i, VGA.ATTR_BG_GRAY); 368 | game.console.setString(73, 19, "Shoot", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 369 | 370 | game.console.setString(62, 21, " S ", VGA.ATTR_BG_GRAY); 371 | game.console.setString(66, 21, "Save game", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 372 | game.console.setString(62, 22, " P ", VGA.ATTR_BG_CYAN); 373 | game.console.setString(66, 22, "Pause", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 374 | game.console.setString(62, 23, " Q ", VGA.ATTR_BG_GRAY); 375 | game.console.setString(66, 23, "Quit", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 376 | 377 | } 378 | 379 | function drawStatusBar() 380 | { 381 | /* fill everything with a blue background */ 382 | for (var y = 0; y < 25; ++y) 383 | { 384 | for (var x = 60; x < 80; ++x) 385 | { 386 | game.console.set(x, y, 32, VGA.ATTR_BG_BLUE); 387 | } 388 | } 389 | 390 | game.console.setString(62, 0, " - - - - - ", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 391 | game.console.setString(62, 1, " ZZT ", VGA.ATTR_BG_GRAY); 392 | game.console.setString(62, 2, " - - - - - ", VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE); 393 | 394 | if (game.atTitleScreen) 395 | drawTitleScreenStatusBar(); 396 | else 397 | drawGameStatusBar(); 398 | } 399 | 400 | function gameTick() 401 | { 402 | setTimeout(function() { 403 | if (game.dialog && game.dialog.done) 404 | { 405 | /* If the dialog is done, we're doing to dismiss it. */ 406 | /* However, we do want to execute the callback. */ 407 | var dialog = game.dialog; 408 | game.dialog = null; 409 | if (dialog.callback) 410 | { 411 | if (dialog.callback(dialog.done)) 412 | return; 413 | } 414 | } 415 | 416 | /* queue up the next tick */ 417 | window.requestAnimationFrame(gameTick); 418 | 419 | /* if we're actually playing, handle player-related timeouts */ 420 | if (game.world.currentBoard == game.world.board[0]) 421 | { 422 | if (game.world.torchCycles > 0) 423 | { 424 | game.world.torchCycles--; 425 | // draw the torch darkness stuff 426 | if (game.world.torchCycle == 0) 427 | { 428 | //game.audio.play(game.audio.SFX_TORCH_DEAD); 429 | } 430 | } 431 | 432 | if (game.world.timeLeft > 0) 433 | { 434 | // display the timer 435 | } 436 | 437 | // handle player health 438 | // handle timer 439 | } 440 | 441 | var board = game.world.currentBoard; 442 | 443 | game.tick++; 444 | 445 | // now, iterate through all objects on the board and update them 446 | board.update(); 447 | 448 | /* update the status bar */ 449 | drawStatusBar(); 450 | /* update the console */ 451 | board.draw(game.console); 452 | 453 | if (game.dialog) 454 | game.dialog.draw(game.console); 455 | 456 | /* redraw the whole console */ 457 | game.console.redraw(); 458 | }, 1000 / game.fps); 459 | } 460 | 461 | -------------------------------------------------------------------------------- /zztobject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function getRandomInt(min, max) 4 | { 5 | return Math.floor(Math.random() * (max - min + 1) + min); 6 | } 7 | 8 | var Direction = Object.freeze({ 9 | NONE : 0, 10 | NORTH : 1, 11 | SOUTH : 2, 12 | EAST : 3, 13 | WEST : 4, 14 | 15 | _opposites : [ this.SOUTH, this.NORTH, this.WEST, this.EAST ], 16 | _clockwise : [ this.EAST, this.WEST, this.SOUTH, this.NORTH ], 17 | 18 | opposite : function(dir) 19 | { 20 | return this._opposites[dir]; 21 | }, 22 | 23 | clockwise : function(dir) 24 | { 25 | return _clockwise[dir]; 26 | }, 27 | 28 | counterClockwise : function(dir) 29 | { 30 | return this._opposites[_clockwise[dir]]; 31 | }, 32 | 33 | random : function() 34 | { 35 | return getRandomInt(1, 4); 36 | } 37 | }); 38 | 39 | var ObjectFlags = Object.freeze({ 40 | NONE : 0, 41 | TEXT : 1 42 | }); 43 | 44 | var SpinGlyph = Object.freeze([ 124, 47, 45, 92 ]); 45 | var SpinGunGlyph = Object.freeze([ 27, 24, 26, 25 ]); 46 | var KeyColors = [ "black", "blue", "green", "cyan", "red", "purple", "yellow", "white" ]; 47 | 48 | var applyDirection = function(x, y, dir) 49 | { 50 | if (dir == Direction.NONE) 51 | return {x:x, y:y}; 52 | else if (dir == Direction.NORTH) 53 | return {x:x, y:y-1}; 54 | else if (dir == Direction.SOUTH) 55 | return {x:x, y:y+1}; 56 | else if (dir == Direction.WEST) 57 | return {x:x-1, y:y}; 58 | else if (dir == Direction.EAST) 59 | return {x:x+1, y:y}; 60 | } 61 | 62 | var genericEnemyMove = function(actorIndex, board, dir) 63 | { 64 | var actorData = board.statusElement[actorIndex]; 65 | var newPosition = applyDirection(actorData.x, actorData.y, dir); 66 | var dstTile = board.get(newPosition.x, newPosition.y); 67 | if (dstTile.properties.floor) 68 | { 69 | board.moveActor(actorIndex, newPosition.x, newPosition.y); 70 | } 71 | /* else if player or if breakable, attack */ 72 | } 73 | 74 | var baseObjectMove = function (board, dir) 75 | { 76 | if (dir == Direction.NONE) 77 | return; 78 | 79 | var oldX = this.x; 80 | var oldY = this.y; 81 | var newX = this.x; 82 | var newY = this.y; 83 | 84 | if (dir == Direction.NORTH) 85 | --newY; 86 | else if (dir == Direction.SOUTH) 87 | ++newY; 88 | else if (dir == Direction.EAST) 89 | ++newX; 90 | else if (dir == Direction.WEST) 91 | --newX; 92 | 93 | // If the player is trying to move off the edge, then we might need to switch 94 | // boards... 95 | // 96 | // TODO: Does this belong here in move()? 97 | if (this.name == "player") 98 | { 99 | var boardSwitch = false; 100 | var newBoardID = 0; 101 | if (newY < 0 && board.exitNorth > 0) 102 | { 103 | newBoardID = board.exitNorth; 104 | boardSwitch = true; 105 | } 106 | else if (newY >= board.height && board.exitSouth > 0) 107 | { 108 | newBoardID = board.exitSouth; 109 | boardSwitch = true; 110 | } 111 | else if (newX < 0 && board.exitWest > 0) 112 | { 113 | newBoardID = board.exitWest; 114 | boardSwitch = true; 115 | } 116 | else if (newX >= board.width && board.exitEast > 0) 117 | { 118 | newBoardID = board.exitEast; 119 | boardSwitch = true; 120 | } 121 | 122 | if (boardSwitch) 123 | { 124 | /* Correct newX/newY for the fact that we've crossed boards */ 125 | 126 | var newBoard = game.world.board[newBoardID]; 127 | 128 | if (newX < 0) 129 | newX = newBoard.width - 1; 130 | else if (newX >= board.width) 131 | newX = 0; 132 | 133 | if (newY < 0) 134 | newY = newBoard.height - 1; 135 | else if (newY >= board.height) 136 | newY = 0; 137 | 138 | /* we need to move the player into position. 139 | clear the old player position */ 140 | var empty = new Empty; 141 | empty.x = newBoard.player.x; 142 | empty.y = newBoard.player.y; 143 | newBoard.set(newBoard.player.x, newBoard.player.y, empty); 144 | 145 | /* put the player at the new position */ 146 | newBoard.player.x = newX; 147 | newBoard.player.y = newY; 148 | newBoard.set(newBoard.player.x, newBoard.player.y, newBoard.player); 149 | 150 | /* make this the new current board */ 151 | game.world.playerBoard = newBoardID; 152 | game.world.currentBoard = newBoard; 153 | 154 | return true; 155 | } 156 | } 157 | 158 | if (newY < 0) 159 | newY = 0; 160 | else if (newY >= board.height) 161 | newY = board.height - 1; 162 | 163 | if (newX < 0) 164 | newX = 0; 165 | else if (newX >= board.width) 166 | newX = board.width - 1; 167 | 168 | var that = board.get(newX, newY); 169 | if (that.name == "empty") 170 | { 171 | /* If where we're trying to move is an Empty, then just swap. */ 172 | this.x = newX; 173 | this.y = newY; 174 | board.set(newX, newY, this); 175 | 176 | that.x = oldX; 177 | that.y = oldY; 178 | board.set(oldX, oldY, that); 179 | 180 | return true; 181 | } 182 | /* else if not empty then it's a little more complicated */ 183 | else 184 | { 185 | /* if we're the player, and we're touching an item, and that item can be taken, 186 | then we take it. */ 187 | if (this.name == "player" && that.takeItem && that.takeItem()) 188 | { 189 | this.x = newX; 190 | this.y = newY; 191 | board.set(newX, newY, this); 192 | 193 | /* Where the player used to be, put an Empty. */ 194 | that = new Empty; 195 | that.x = oldX; 196 | that.y = oldY; 197 | board.set(oldX, oldY, that); 198 | } 199 | } 200 | 201 | return false; 202 | } 203 | 204 | /* direction from (x1,y1) to (x2, y2) */ 205 | function toward(x1, y1, x2, y2) 206 | { 207 | var dx = x1 - x2; 208 | var dy = y1 - y2; 209 | var dirx = Direction.NONE; 210 | var diry = Direction.NONE; 211 | 212 | if (dx < 0) 213 | dirx = Direction.EAST; 214 | else if (dx > 0) 215 | dirx = Direction.WEST; 216 | 217 | if (dy < 0) 218 | diry = Direction.SOUTH; 219 | else if (dy > 0) 220 | diry = Direction.NORTH; 221 | 222 | /* could stand to be a little more intelligent here... */ 223 | if (Math.abs(dx) > Math.abs(dy)) 224 | { 225 | if (dirx != Direction.NONE) 226 | return dirx; 227 | else 228 | return diry; 229 | } 230 | else 231 | { 232 | if (diry != Direction.NONE) 233 | return diry; 234 | else 235 | return dirx; 236 | } 237 | } 238 | 239 | var Empty = 240 | { 241 | glyph: 32, 242 | name: "empty", 243 | floor: true 244 | }; 245 | 246 | var Edge = 247 | { 248 | glyph: 69 249 | } 250 | 251 | var Player = 252 | { 253 | glyph: 2, 254 | name: "player", 255 | color: VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE, 256 | update: function(board, actorIndex) 257 | { 258 | var walkDirection = Direction.NONE; 259 | // get player position 260 | var pos; 261 | game.world.currentBoard.tiles.forEach(function(el, ind) { 262 | if (el.typeid === 4) { pos = ind; } 263 | }); 264 | 265 | this.y = Math.floor(pos / 60); 266 | this.x = pos % 60; 267 | 268 | if (game.inputEvent != 0) 269 | { 270 | if (game.inputEvent == ZInputEvent.WALK_NORTH) 271 | walkDirection = Direction.NORTH; 272 | else if (game.inputEvent == ZInputEvent.WALK_SOUTH) 273 | walkDirection = Direction.SOUTH; 274 | else if (game.inputEvent == ZInputEvent.WALK_EAST) 275 | walkDirection = Direction.EAST; 276 | else if (game.inputEvent == ZInputEvent.WALK_WEST) 277 | walkDirection = Direction.WEST; 278 | else if (game.inputEvent == ZInputEvent.QUIT) 279 | { 280 | /* ? */ 281 | goToTitleScreen(); 282 | } 283 | 284 | game.inputEvent = 0; 285 | } 286 | 287 | if (walkDirection != Direction.NONE) 288 | { 289 | var oldX = this.x; 290 | var oldY = this.y; 291 | var newX = this.x; 292 | var newY = this.y; 293 | 294 | if (walkDirection == Direction.NORTH) 295 | --newY; 296 | else if (walkDirection == Direction.SOUTH) 297 | ++newY; 298 | else if (walkDirection == Direction.EAST) 299 | ++newX; 300 | else if (walkDirection == Direction.WEST) 301 | --newX; 302 | 303 | // If the player is trying to move off the edge, then we might need to switch 304 | // boards... 305 | // 306 | // TODO: Does this belong here in move()? 307 | var boardSwitch = false; 308 | var newBoardID = 0; 309 | if (newY < 0 && board.exitNorth > 0) 310 | { 311 | newBoardID = board.exitNorth; 312 | boardSwitch = true; 313 | } 314 | else if (newY >= board.height && board.exitSouth > 0) 315 | { 316 | newBoardID = board.exitSouth; 317 | boardSwitch = true; 318 | } 319 | else if (newX < 0 && board.exitWest > 0) 320 | { 321 | newBoardID = board.exitWest; 322 | boardSwitch = true; 323 | } 324 | else if (newX >= board.width && board.exitEast > 0) 325 | { 326 | newBoardID = board.exitEast; 327 | boardSwitch = true; 328 | } 329 | 330 | if (boardSwitch) 331 | { 332 | /* Correct newX/newY for the fact that we've crossed boards */ 333 | 334 | var newBoard = game.world.board[newBoardID]; 335 | 336 | if (newX < 0) 337 | newX = newBoard.width - 1; 338 | else if (newX >= board.width) 339 | newX = 0; 340 | 341 | if (newY < 0) 342 | newY = newBoard.height - 1; 343 | else if (newY >= board.height) 344 | newY = 0; 345 | 346 | /* make this the new current board and move the player there */ 347 | game.world.playerBoard = newBoardID; 348 | game.world.currentBoard = newBoard; 349 | game.world.currentBoard.moveActor(actorIndex, newX, newY); 350 | 351 | return true; 352 | } 353 | else { 354 | genericEnemyMove(actorIndex, board, walkDirection); 355 | } 356 | } 357 | } 358 | } 359 | 360 | var Ammo = 361 | { 362 | glyph: 132, 363 | name: "ammo", 364 | color: VGA.ATTR_FG_CYAN, 365 | takeItem: function() 366 | { 367 | if (!game.world.hasGotAmmoMsg) 368 | { 369 | game.world.currentBoard.setMessage("Ammunition - 5 shots per container."); 370 | game.world.hasGotAmmoMsg = true; 371 | } 372 | 373 | game.world.playerAmmo += 5; 374 | game.audio.play("tcc#d"); 375 | return true; 376 | } 377 | } 378 | 379 | var Torch = 380 | { 381 | glyph: 157, 382 | name: "torch", 383 | takeItem: function() 384 | { 385 | if (!game.world.hasGotTorchMsg) 386 | { 387 | game.world.currentBoard.setMessage("Torch - used for lighting in the underground."); 388 | game.world.hasGotTorchMsg = true; 389 | } 390 | 391 | game.world.playerTorches += 1; 392 | game.audio.play("tcase"); 393 | return true; 394 | } 395 | } 396 | 397 | var Gem = 398 | { 399 | glyph: 4, 400 | name: "gem", 401 | takeItem: function() 402 | { 403 | if (!game.world.hasGotGemMsg) 404 | { 405 | game.world.currentBoard.setMessage("Gems give you Health!"); 406 | game.world.hasGotGemMsg = true; 407 | } 408 | 409 | game.world.playerGems += 1; 410 | game.world.playerHealth += 1; 411 | game.world.playerScore += 10; 412 | game.audio.play("t+c-gec"); 413 | return true; 414 | } 415 | } 416 | 417 | var Key = 418 | { 419 | glyph: 12, 420 | name: "key", 421 | takeItem: function() 422 | { 423 | var keyColor = (this.color & 0x07); 424 | var couldGiveKey = false; 425 | if (keyColor == 0) 426 | { 427 | /* The 'black' key is weird. Black keys are technically 428 | invalid, and overwrite the space in the player info 429 | just before the keys, which happens to give the player 430 | 256 gems instead. */ 431 | game.world.playerGems += 256; 432 | couldGiveKey = true; 433 | } 434 | else if (keyColor > 0 && keyColor <= 7) 435 | { 436 | if (!game.world.playerKeys[keyColor-1]) 437 | { 438 | couldGiveKey = true; 439 | game.world.playerKeys[keyColor-1] = true; 440 | } 441 | } 442 | else 443 | { 444 | console.log("this key's an invalid color!"); 445 | return false; 446 | } 447 | 448 | if (!couldGiveKey) 449 | { 450 | game.world.currentBoard.setMessage("You already have a " + KeyColors[keyColor] + " key!"); 451 | game.audio.play("sc-c"); 452 | return false; 453 | } 454 | else 455 | { 456 | game.world.currentBoard.setMessage("You now have the " + KeyColors[keyColor] + " key"); 457 | game.audio.play("t+cegcegceg+sc"); 458 | return true; 459 | } 460 | } 461 | } 462 | 463 | var Door = 464 | { 465 | glyph: 10, 466 | name: "door", 467 | takeItem: function() 468 | { 469 | /* A door isn't really an 'item' per se but works similarly-- 470 | it needs to disappear when the player walks over it, if they 471 | have the key. */ 472 | var keyColor = ((this.color & 0x70) >> 4); 473 | var doorUnlocked = false; 474 | if (keyColor == 0) 475 | { 476 | /* Black doors, like black keys, are weird. */ 477 | if (game.world.playerGems >= 256) 478 | { 479 | game.world.playerGems -= 256; 480 | doorUnlocked = true; 481 | } 482 | } 483 | else if (keyColor > 0 && keyColor <= 7) 484 | { 485 | if (game.world.playerKeys[keyColor-1]) 486 | { 487 | game.world.playerKeys[keyColor-1] = false; 488 | doorUnlocked = true; 489 | } 490 | } 491 | else 492 | { 493 | console.log("this door's an invalid color!"); 494 | return false; 495 | } 496 | 497 | if (doorUnlocked) 498 | { 499 | game.world.currentBoard.setMessage("The " + KeyColors[keyColor] + " door is now open!"); 500 | game.audio.play("tcgbcgb+ic"); 501 | return true; 502 | } 503 | else 504 | { 505 | game.world.currentBoard.setMessage("The " + KeyColors[keyColor] + " door is locked."); 506 | game.audio.play("t--gc"); 507 | return false; 508 | } 509 | } 510 | } 511 | 512 | var Scroll = 513 | { 514 | glyph: 232, 515 | name: "scroll" 516 | } 517 | 518 | /* Passages use P3 for destination board */ 519 | var Passage = 520 | { 521 | glyph: 240, 522 | name: "passage" 523 | } 524 | 525 | /* xstep/ystep are relative coords for source, rate is P2 */ 526 | var Duplicator = 527 | { 528 | glyph: 250, 529 | name: "duplicator" 530 | } 531 | 532 | var Bomb = 533 | { 534 | glyph: 11, 535 | name: "bomb" 536 | } 537 | 538 | var Energizer = 539 | { 540 | glyph: 127, 541 | name: "energizer" 542 | } 543 | 544 | var Throwstar = 545 | { 546 | glyph: 47, 547 | name: "star" 548 | } 549 | 550 | /* uses SpinGlyph for iteration */ 551 | var CWConveyor = 552 | { 553 | glyph: 179, 554 | name: "clockwise" 555 | } 556 | 557 | /* uses SpinGlyph for iteration, backwards */ 558 | var CCWConveyor = 559 | { 560 | glyph: 92, 561 | name: "counter" 562 | } 563 | 564 | var Bullet = 565 | { 566 | glyph: 248, 567 | name: "bullet" 568 | } 569 | 570 | var Water = 571 | { 572 | glyph: 176, 573 | name: "water" 574 | } 575 | 576 | var Forest = 577 | { 578 | glyph: 176, 579 | name: "forest" 580 | } 581 | 582 | var SolidWall = 583 | { 584 | glyph: 219, 585 | name: "solid" 586 | } 587 | 588 | var NormalWall = 589 | { 590 | glyph: 178, 591 | name: "normal" 592 | } 593 | 594 | var BreakableWall = 595 | { 596 | glyph: 177, 597 | name: "breakable" 598 | } 599 | 600 | var Boulder = 601 | { 602 | glyph: 254, 603 | name: "boulder" 604 | } 605 | 606 | var SliderNS = 607 | { 608 | glyph: 18, 609 | name: "sliderns" 610 | } 611 | 612 | var SliderEW = 613 | { 614 | glyph: 29, 615 | name: "sliderew" 616 | } 617 | 618 | var FakeWall = 619 | { 620 | glyph: 178, 621 | name: "fake", 622 | floor: true 623 | } 624 | 625 | var InvisibleWall = 626 | { 627 | glyph: 176, 628 | name: "invisible" 629 | } 630 | 631 | var BlinkWall = 632 | { 633 | glyph: 206, 634 | name: "blinkwall" 635 | } 636 | 637 | var Transporter = 638 | { 639 | glyph: 60, 640 | name: "transporter" 641 | } 642 | 643 | var Line = 644 | { 645 | glyph: 250, 646 | name: "line" 647 | } 648 | 649 | var Ricochet = 650 | { 651 | glyph: 42, 652 | name: "ricochet" 653 | } 654 | 655 | var HorizBlinkWallRay = 656 | { 657 | glyph: 205 658 | } 659 | 660 | var Bear = 661 | { 662 | glyph: 153, 663 | name: "bear" 664 | } 665 | 666 | var Ruffian = 667 | { 668 | glyph: 5, 669 | name: "ruffian" 670 | } 671 | 672 | /* glyph to draw comes from P1 */ 673 | var ZObject = 674 | { 675 | glyph: 2, 676 | name: "object", 677 | draw: function(board, x, y) 678 | { 679 | var actor = board.getActorAt(x, y); 680 | var tile = board.get(x, y); 681 | return { glyph: actor.param1, color: tile.color } 682 | } 683 | } 684 | 685 | var Slime = 686 | { 687 | glyph: 42, 688 | name: "slime" 689 | } 690 | 691 | var Shark = 692 | { 693 | glyph: 94, 694 | name: "shark" 695 | } 696 | 697 | /* animation rotates through SpinGunGlyph */ 698 | var SpinningGun = 699 | { 700 | glyph: 24, 701 | name: "spinninggun" 702 | } 703 | 704 | var Pusher = 705 | { 706 | glyph: 31, 707 | name: "pusher" 708 | } 709 | 710 | var Lion = 711 | { 712 | glyph: 234, 713 | name: "lion", 714 | update: function(board, actorIndex) 715 | { 716 | var dir = Direction.random(); 717 | genericEnemyMove(actorIndex, board, dir); 718 | } 719 | } 720 | 721 | var Tiger = 722 | { 723 | glyph: 227, 724 | name: "tiger" 725 | } 726 | 727 | var VertBlinkWallRay = 728 | { 729 | glyph: 186 730 | } 731 | 732 | var CentipedeHead = 733 | { 734 | glyph: 233, 735 | name: "head" 736 | } 737 | 738 | var CentipedeBody = 739 | { 740 | glyph: 79, 741 | name: "segment" 742 | } 743 | 744 | var BlueText = 745 | { 746 | color: VGA.ATTR_BG_BLUE|VGA.ATTR_FG_WHITE, 747 | isText: true 748 | } 749 | 750 | var GreenText = 751 | { 752 | color: VGA.ATTR_BG_GREEN|VGA.ATTR_FG_WHITE, 753 | isText: true 754 | } 755 | 756 | var CyanText = 757 | { 758 | color: VGA.ATTR_BG_CYAN|VGA.ATTR_FG_WHITE, 759 | isText: true 760 | } 761 | 762 | var RedText = 763 | { 764 | color: VGA.ATTR_BG_RED|VGA.ATTR_FG_WHITE, 765 | isText: true 766 | } 767 | 768 | var PurpleText = 769 | { 770 | color: VGA.ATTR_BG_MAGENTA|VGA.ATTR_FG_WHITE, 771 | isText: true 772 | } 773 | 774 | var YellowText = 775 | { 776 | color: VGA.ATTR_BG_BROWN|VGA.ATTR_FG_WHITE, 777 | isText: true 778 | } 779 | 780 | var WhiteText = 781 | { 782 | color: VGA.ATTR_FG_WHITE, 783 | isText: true 784 | } 785 | 786 | var BoardObjects = [ 787 | Empty, 788 | Edge, 789 | null, // 02 is unused 790 | null, // 03 is unused 791 | Player, 792 | Ammo, 793 | Torch, 794 | Gem, 795 | Key, 796 | Door, 797 | Scroll, 798 | Passage, 799 | Duplicator, 800 | Bomb, 801 | Energizer, 802 | Throwstar, 803 | CWConveyor, 804 | CCWConveyor, 805 | Bullet, 806 | Water, 807 | Forest, 808 | SolidWall, 809 | NormalWall, 810 | BreakableWall, 811 | Boulder, 812 | SliderNS, 813 | SliderEW, 814 | FakeWall, 815 | InvisibleWall, 816 | BlinkWall, 817 | Transporter, 818 | Line, 819 | Ricochet, 820 | HorizBlinkWallRay, 821 | Bear, 822 | Ruffian, 823 | ZObject, 824 | Slime, 825 | Shark, 826 | SpinningGun, 827 | Pusher, 828 | Lion, 829 | Tiger, 830 | VertBlinkWallRay, 831 | CentipedeHead, 832 | CentipedeBody, 833 | null, /* unused */ 834 | BlueText, 835 | GreenText, 836 | CyanText, 837 | RedText, 838 | PurpleText, 839 | YellowText, 840 | WhiteText, 841 | null 842 | ]; 843 | 844 | function getTileRenderInfo(tile) 845 | { 846 | /* specific check for zero here because town.zzt has some 'empty' cells marked w/color, 847 | possible editor corruption? */ 848 | if (tile.typeid == 0 || !tile.properties) 849 | return { glyph: Empty.glyph, color: Empty.color } 850 | 851 | if (tile.properties.isText) 852 | { 853 | /* For text, the tile's 'color' is the glyph, and the element type determines the color. */ 854 | return { glyph: tile.color, color: tile.properties.color }; 855 | } 856 | else 857 | { 858 | return { glyph: tile.properties.glyph, color: tile.color } 859 | } 860 | } 861 | 862 | function getNameForType(typeid) 863 | { 864 | if (typeid > BoardObjects.length) 865 | console.log("invalid element type"); 866 | 867 | if (BoardObjects[typeid] == null) 868 | return "(unknown)"; 869 | else if (BoardObjects[typeid].name) 870 | return BoardObjects[typeid].name; 871 | else 872 | return ""; 873 | } 874 | --------------------------------------------------------------------------------