├── 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 |
--------------------------------------------------------------------------------