2021 Game Diary









Monday 04/01/2020{shk}{shk}


Monday 04/01/2020{shk}{shk}

# BITSY VERSION 7.2

! ROOM_FORMAT 1

PAL 0
NAME blueprint
0,82,204
128,159,255
255,255,255

PAL 1
2,0,14
255,255,255
172,68,75

ROOM 0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
0,a,a,a,a,a,a,a,a,a,a,a,a,a,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0
0,a,a,a,a,a,a,a,a,a,a,a,a,a,a,0
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
NAME example room
PAL 0

ROOM 2
0,2r,2s,2s,2r,2s,2r,2s,2r,2s,2r,2s,2r,2s,2r,0
2s,b,c,c,c,c,c,c,c,c,c,c,c,c,d,2r
2s,g,0,0,0,0,0,1n,1o,1o,1o,1o,1o,0,e,2r
2s,g,2u,2v,2v,2w,0,3k,1u,1t,1t,1v,0,2k,e,2r
2s,g,36,33,33,37,0,3k,1r,20,20,1r,0,2k,e,2r
2s,g,36,33,33,37,0,2h,1r,22,0,1r,0,1h,e,2r
2s,g,32,0,0,2x,0,3k,1r,23,24,1r,0,2k,e,2r
2s,g,32,0,0,2x,0,3k,2n,2n,2n,2n,2n,1h,e,2r
2s,g,30,2z,2z,2y,0,1l,1j,1j,1j,1j,2m,1i,e,2r
2s,g,0,0,0,0,0,0,0,0,0,0,0,0,e,2r
2s,f,f,f,f,f,f,f,f,f,f,f,f,f,f,2r
0,g,0,0,0,0,0,0,0,0,0,0,0,1d,e,0
0,g,l,m,n,o,p,10,10,t,s,v,w,a,e,0
0,g,x,y,z,10,11,12,13,14,15,16,17,a,e,0
0,g,0,1a,15,v,x,z,11,y,12,u,t,a,e,0
0,j,h,h,h,h,0,h,h,h,h,0,h,h,i,0
NAME room1
EXT 13,2 3 13,13
PAL 1

ROOM 3
0,2r,2s,2s,2r,2s,2r,2s,2r,2s,2r,2s,2r,2s,2r,0
2s,b,c,c,c,c,c,c,c,c,c,c,c,c,d,2r
2s,g,0,0,0,0,0,1n,1o,1o,1o,1o,1o,0,e,2r
2s,g,2u,2v,2v,2w,0,3h,0,0,0,0,0,2k,e,2r
2s,g,36,33,33,37,0,3h,0,3g,38,39,0,2k,e,2r
2s,g,36,33,33,37,0,3h,0,3f,1m,3a,0,1h,e,2r
2s,g,32,0,0,2x,0,3h,0,3e,3c,3b,0,2k,e,2r
2s,g,32,0,0,2x,0,3h,0,0,0,0,0,1h,e,2r
2s,g,30,2z,2z,2y,0,1l,1j,1j,1j,1j,3i,3j,e,2r
2s,g,0,0,0,0,0,0,0,0,0,0,0,3h,e,2r
2s,f,f,f,f,f,f,f,f,f,f,f,f,f,f,2r
0,g,0,0,0,0,0,0,0,0,0,0,0,1d,e,0
0,g,l,m,n,o,p,10,10,t,s,v,w,a,e,0
0,g,x,y,z,10,11,12,13,14,15,16,17,a,e,0
0,g,0,1a,15,v,x,z,11,y,12,u,t,a,e,0
0,j,h,h,h,h,0,h,h,h,h,0,h,h,i,0
NAME changeroom
END 6 13,2
PAL 1

TIL 10
00000000
00111100
00100000
00100000
00111100
00100000
00100000
00000000

TIL 11
00000000
00001100
00010000
00100000
00101100
00100100
00111100
00000000

TIL 12
00000000
00100100
00100100
00100100
00111100
00100100
00100100
00000000

TIL 13
00000000
00000100
00000100
00000100
00000100
00000100
00111000
00000000

TIL 14
00000000
00100100
00101000
00110000
00110000
00101000
00100100
00000000

TIL 15
00000000
00100000
00100000
00100000
00100000
00100000
00111100
00000000

TIL 16
00000000
00001000
00001000
00001000
00001000
00001000
00010000
00000000

TIL 17
00000000
00001010
01111110
00010100
01111110
00101000
01010000
00000000

TIL 18
00000000
00000000
00001010
01010100
00101010
01010000
00101000
01010000

TIL 19
00000000
00111100
00000100
00001000
00010000
00100000
00111100
00000000

TIL 20
00100100
01000010
00011000
00100100
01010010
00100100
00011000
00000000
>
00100100
01000010
00011000
00100100
01001010
00100100
00011000
00000000

TIL 21
00000000
00000000
00001000
00010000
00100000
00011000
00000000
00000000

TIL 22
00000001
00000010
00000100
00000011
00000000
00000010
00001000
00000100

TIL 23
10000000
01000000
00110000
00001111
00001000
00010000
00100000
00100000

TIL 24
00000000
00000001
00000110
11111000
00000000
00000000
00000000
00000000

TIL 25
11111111
10000000
10000000
10000000
10000000
10000000
10000000
10000000

TIL 26
11111111
00000001
00000001
00000001
00000001
00000001
00000001
00000001

TIL 27
00000001
00000001
00000001
00000001
00000001
00000001
00000001
11111111

TIL 28
10000000
10000000
10000000
10000000
10000000
10000000
10000000
11111111

TIL 29
11111111
00000001
00000001
00000001
00000001
00000001
00000001
00000001

TIL 30
10000000
10000000
10000000
10000000
10000000
10000000
10000000
11111111

TIL 31
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 32
10000000
10000000
10000000
10000000
10000000
10000000
10000000
10000000

TIL 33
00000000
01010101
10101010
00000000
01010100
10101010
00000000
00000000

TIL 34
00000000
01010101
10101010
00000000
01010100
10101010
00000000
00000000

TIL 35
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 36
10000000
11010101
10101010
10000000
11010101
10101010
10000000
10000000

TIL 37
00000001
10101011
01010101
00000001
10101011
01010101
00000001
00000001

TIL 38
00011000
00100100
01000010
10011001
00100100
01000010
10000001
00000000
>
00011000
00100100
01000010
10000001
00000000
00000000
00011000
00100100

TIL 39
00000000
10000000
01000000
00100000
10010000
01001000
00100100
00010010

TIL a
11110111
10000001
10000001
10011001
10011001
10000001
10000001
11111111
NAME block

TIL b
11111111
11111111
11000000
11011111
11010000
11010000
11010000
11010000

TIL c
11111111
11111111
00000000
11111111
00000000
00000000
00000000
00000000

TIL d
11111111
11111111
00000011
11111011
00001011
00001011
00001011
00001011

TIL e
00001011
00001011
00001011
00001011
00001011
00001011
00001011
00001011

TIL f
11111111
11111111
00000000
00000000
00000000
00000000
11111111
11111111

TIL g
11010000
11010000
11010000
11010000
11010000
11010000
11010000
11010000

TIL h
00000000
00000000
00000000
00000000
11111111
00000000
11111111
11111111

TIL i
00001011
00001011
00001011
00001011
00001011
00001011
11111111
11111111

TIL j
11010000
11010000
11010000
11010000
11010000
11010000
11111111
11111111

TIL k
00000000
00111100
00100100
00100100
00111100
00100100
00100100
00000000

TIL l
00000000
00111100
00100100
00100100
00100100
00111100
00000010
00000000

TIL m
00000000
01000010
01000010
01011010
01011010
01011010
00111100
00000000

TIL n
00000000
00111100
00100100
00111100
00100000
00100000
00111100
00000000

TIL o
00000000
00111100
01100100
01000100
01001000
01010000
01001100
00000000

TIL p
00000000
01111110
00011000
00011000
00011000
00011000
00011000
00000000

TIL q
00000000
00100100
00100100
00010100
00001100
00000100
00000100
00000000

TIL r
00000000
01000010
01000010
00111100
00011000
00011000
00011000
00000000

TIL s
00000000
00100100
00100100
00100100
00100100
00111100
00111100
00000000

TIL t
00000000
00011000
00011000
00011000
00011000
00011000
00011000
00000000

TIL u
00000000
00111100
01000010
01000010
01000010
01000010
00111100
00000000

TIL v
00000000
00111100
00100100
00100100
00111100
00100000
00100000
00000000

TIL w
00000000
00111100
00100000
00100000
01100000
00100000
00111100
00000000

TIL x
00000000
00011000
00100100
00100100
00111100
00100100
00100100
00000000

TIL y
00000000
00011100
00100000
00111100
00000100
00000100
00111000
00000000

TIL z
00000000
00111100
00100100
01000100
01000100
01000100
01111000
00000000

TIL 1a
00000000
01000010
00100100
00011000
00011000
00100100
01000010
00000000

TIL 1b
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 1c
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 1d
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 1e
00000000
01111111
01000000
01000000
01000000
01000000
01000000
01000000

TIL 1f
00000000
11111111
00000000
00000000
00000000
00000000
00000000
00000000

TIL 1g
00000000
11111111
00000001
00000001
00000001
00000001
00000001
00000001

TIL 1h
00000001
00000001
00000001
00000001
00000001
00000001
00000001
00000001

TIL 1i
00000001
00000001
00000001
00000001
00000001
00000001
00000001
11111111

TIL 1j
00000000
00000000
00000000
00000000
00000000
00000000
00000000
11111111

TIL 1k
10000000
10000000
10000000
10000000
10000000
10000000
10000000
11111111

TIL 1l
10000000
10000000
10000000
10000000
10000000
10000000
10000000
11111111

TIL 1m
00000000
00001000
00010100
00101010
00101010
00010100
00001000
00000000

TIL 1n
11111111
10000000
10000000
10000000
10000000
10000000
10000000
10000000

TIL 1o
11111111
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 1p
11111111
00000001
00000001
00000001
00000001
00000001
00000001
00000001

TIL 1q
00000111
00001010
00010100
00101000
01010000
10100000
01000000
10000000

TIL 1r
01010100
00101010
01010100
00101010
01010100
00101010
01010100
00101010

TIL 1s
11111111
10101010
00000000
00000000
00000000
00000000
00000000
00000000

TIL 1t
11111111
01010101
10101010
01010101
10101010
00000000
00000000
00000000

TIL 1u
00000001
00000010
00000101
00001010
00010101
00101010
01010101
10101010

TIL 1v
10000000
01000000
10100000
01010000
10101000
01010100
10101010
01010100

TIL 1w
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 1x
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 1y
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 1z
00000101
00000010
00000101
00000010
00000101
00000010
00000101
00000010

TIL 2a
11111111
10000000
10000000
10000000
10000000
10000000
10000000
10000000

TIL 2b
11111111
00000001
00000001
00000001
00000001
00000001
00000001
00000001

TIL 2c
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 2d
11111111
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 2e
11111111
10000000
10000000
11111111
10000000
10000000
10000000
10000000

TIL 2f
11111111
00000000
00000000
11111111
00000000
00000000
00000000
00000000

TIL 2g
11111111
00000001
00000001
11111111
00000001
00000001
00000001
00000001

TIL 2h
10000000
10000000
10000000
10000000
10000000
10000000
10000000
10000000

TIL 2i
00000001
00000001
00000001
00000001
00000001
00000001
00000001
11111111

TIL 2j
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 2k
00000001
00000001
00000001
00000001
00000001
00000001
00000001
00000001

TIL 2l
11111111
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 2m
00000000
00000000
00000000
00000000
00000000
00000000
00000000
11111111

TIL 2n
00000000
00000000
00000000
00010100
00101010
01010101
10101010
01010101
>
00000000
00000000
00000000
00000000
00000010
01000101
10101010
01010101

TIL 2o
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 2p
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 2q
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 2r
00100100
00010010
00001001
00000100
10000010
01000001
00100000
00010000

TIL 2s
01001000
10010000
00100000
01000000
10000001
00000010
00000100
00001000

TIL 2t
11111111
11111111
11000000
11011111
11010000
11010000
11010000
11010000

TIL 2u
11111111
10000000
10000000
11111111
10000000
10000000
10000000
10000000

TIL 2v
11111111
00000000
00000000
11111111
00000000
00000000
00000000
00000000

TIL 2w
11111111
00000001
00000001
11111111
00000001
00000001
00000001
00000001

TIL 2x
00000001
00000001
00000001
00000001
00000001
00000001
00000001
00000001

TIL 2y
00000001
00000001
00000001
00000001
00000001
00000001
00000001
11111111

TIL 2z
00000000
00000000
00000000
00000000
00000000
00000000
00000000
11111111

TIL 3a
01001000
00100100
00010010
00001001
00000100
00001001
00010010
00100100
>
01001000
00100100
10010010
01001001
00100100
01001001
10010010
00100100

TIL 3b
00001001
00010010
00100100
01001000
10010000
00100000
01000000
10000000

TIL 3c
01000010
00100100
10011001
01000010
00100100
10000001
01000010
00111100
>
01000010
00100100
10000001
01000010
00100100
10011001
01000010
00111100

TIL 3d
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

TIL 3e
00010000
10001000
01000100
00100010
00010001
00001000
00000100
00000010

TIL 3f
00010010
00100100
01001000
10010000
00100000
10010000
01001000
00100100
>
00010010
00100100
01001001
10010010
00100100
10010010
01001001
00100100

TIL 3g
00000000
00000001
00000010
00000100
00001001
00010010
00100100
01001000

TIL 3h
10000000
10000000
10000000
10000000
10000000
10000000
10000000
10000000

TIL 3i
00000000
00000000
00000000
00000000
00000000
00000000
00000000
11111111

TIL 3j
00000001
00000001
00000001
00000001
00000001
00000001
00000001
11111111

TIL 3k
10000000
10000000
10000000
10000000
10000000
10000000
10000000
10000000

TIL 3l
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000

SPR A
00000000
00000000
00011000
00111100
01111110
01111110
00011000
00011000
POS 2 9,9

SPR a
00000000
00000000
01010001
01110001
01110010
01111100
00111100
00100100
NAME cat
DLG 0
POS 3 13,9

SPR b
11000001
01000010
00100100
00011000
00011000
00100100
01000010
10000001
DLG 3
POS 2 5,4

SPR c
00000001
00000010
00000100
10001000
01010000
00100000
00000000
00000000
DLG 4
POS 3 5,4

SPR d
00000000
01101110
01001000
01100100
01000010
01101110
00000000
00000000
DLG 5
POS 3 13,12

ITM 0
00000000
00000000
00000000
00111100
01100100
00100100
00011000
00000000
NAME tea
DLG 1

ITM 1
00000000
00111100
00100100
00111100
00010000
00011000
00010000
00011000
NAME key
DLG 2

DLG 0
Woof! Shhh, I'm in a meeting.
NAME cat dialog

DLG 1
You found a nice warm cup of tea
NAME tea dialog

DLG 2
A key! {wvy}What does it open?{wvy}
NAME key dialog

DLG 3
Another day chained to the desk teaching. By midday, all of my students faces blur into one. I change things up, I plan my lessons, but the same dead eye expression.{shk}{shk} Zoom is the mind killer. 
NAME sprite b dialog

DLG 4
Tick tock, check, next. Boulot, metro, dodo.
NAME sprite c dialog

DLG 5
Escape, where, it's office hours?
NAME sprite d dialog

DLG 6
Another day done. At first working from home was a novelty. But, for me it's been almost three years. I even miss catching the train. 
NAME ending 1

VAR a
42





html {
	margin:0px;
	padding:0px;
}

body {
	margin:0px;
	padding:0px;
	overflow:hidden;
	background:#ffffff;
}

#game {
	background:black;
	width:100vw;
	max-width:100vh;
	margin:auto;
	display:block;
}




function startExportedGame() {
	attachCanvas( document.getElementById("game") );
	load_game( document.getElementById("exportedGameData").text.slice(1) );
}



//hex-to-rgb method borrowed from stack overflow
function hexToRgb(hex) {
	// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
	var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
	hex = hex.replace(shorthandRegex, function(m, r, g, b) {
		return r + r + g + g + b + b;
	});

	var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
	return result ? {
		r: parseInt(result[1], 16),
		g: parseInt(result[2], 16),
		b: parseInt(result[3], 16)
	} : null;
}
function componentToHex(c) {
    var hex = c.toString(16);
    return hex.length == 1 ? "0" + hex : hex;
}
function rgbToHex(r, g, b) {
    return "#" + componentToHex(Math.floor(r)) + componentToHex(Math.floor(g)) + componentToHex(Math.floor(b));
}

function hslToHex(h,s,l) {
    var rgbArr = hslToRgb(h,s,l);
    return rgbToHex( Math.floor(rgbArr[0]), Math.floor(rgbArr[1]), Math.floor(rgbArr[2]) );
}

function hexToHsl(hex) {
    var rgb = hexToRgb(hex);
    return rgbToHsl(rgb.r, rgb.g, rgb.b);
}

// really just a vector distance
function colorDistance(a1,b1,c1,a2,b2,c2) {
    return Math.sqrt( Math.pow(a1 - a2, 2) + Math.pow(b1 - b2, 2) + Math.pow(c1 - c2, 2) );
}

function hexColorDistance(hex1,hex2) {
    var color1 = hexToRgb(hex1);
    var color2 = hexToRgb(hex2);
    return rgbColorDistance(color1.r, color1.g, color1.b, color2.r, color2.g, color2.b);
}


// source : http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c
/* accepts parameters
 * h  Object = {h:x, s:y, v:z}
 * OR 
 * h, s, v
*/
function HSVtoRGB(h, s, v) {
    var r, g, b, i, f, p, q, t;
    if (arguments.length === 1) {
        s = h.s, v = h.v, h = h.h;
    }
    i = Math.floor(h * 6);
    f = h * 6 - i;
    p = v * (1 - s);
    q = v * (1 - f * s);
    t = v * (1 - (1 - f) * s);
    switch (i % 6) {
        case 0: r = v, g = t, b = p; break;
        case 1: r = q, g = v, b = p; break;
        case 2: r = p, g = v, b = t; break;
        case 3: r = p, g = q, b = v; break;
        case 4: r = t, g = p, b = v; break;
        case 5: r = v, g = p, b = q; break;
    }
    return {
        r: Math.round(r * 255),
        g: Math.round(g * 255),
        b: Math.round(b * 255)
    };
}

/* accepts parameters
 * r  Object = {r:x, g:y, b:z}
 * OR 
 * r, g, b
*/
function RGBtoHSV(r, g, b) {
    if (arguments.length === 1) {
        g = r.g, b = r.b, r = r.r;
    }
    var max = Math.max(r, g, b), min = Math.min(r, g, b),
        d = max - min,
        h,
        s = (max === 0 ? 0 : d / max),
        v = max / 255;

    switch (max) {
        case min: h = 0; break;
        case r: h = (g - b) + d * (g < b ? 6: 0); h /= 6 * d; break;
        case g: h = (b - r) + d * 2; h /= 6 * d; break;
        case b: h = (r - g) + d * 4; h /= 6 * d; break;
    }

    return {
        h: h,
        s: s,
        v: v
    };
}

// source : https://gist.github.com/mjackson/5311256
/**
 * Converts an HSL color value to RGB. Conversion formula
 * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
 * Assumes h, s, and l are contained in the set [0, 1] and
 * returns r, g, and b in the set [0, 255].
 *
 * @param   Number  h       The hue
 * @param   Number  s       The saturation
 * @param   Number  l       The lightness
 * @return  Array           The RGB representation
 */
function hslToRgb(h, s, l) {
  var r, g, b;

  if (s == 0) {
    r = g = b = l; // achromatic
  } else {
    function hue2rgb(p, q, t) {
      if (t  1) t -= 1;
      if (t < 1/6) return p + (q - p) * 6 * t;
      if (t < 1/2) return q;
      if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
      return p;
    }

    var q = l  0.5 ? d / (2 - max - min) : d / (max + min);
        switch(max){
            case r: h = (g - b) / d + (g < b ? 6 : 0); break;
            case g: h = (b - r) / d + 2; break;
            case b: h = (r - g) / d + 4; break;
        }
        h /= 6;
    }

    return [h, s, l];
}



var TransitionManager = function() {
	var transitionStart = null;
	var transitionEnd = null;
	var effectImage = null;

	var isTransitioning = false;
	var transitionTime = 0; // milliseconds
	var frameRate = 8; // cap the FPS
	var prevStep = -1; // used to avoid running post-process effect constantly

	this.BeginTransition = function(startRoom,startX,startY,endRoom,endX,endY,effectName) {
		// console.log("--- START ROOM TRANSITION ---");

		curEffect = effectName;

		var tmpRoom = player().room;
		var tmpX = player().x;
		var tmpY = player().y;

		if (transitionEffects[curEffect].showPlayerStart) {
			player().room = startRoom;
			player().x = startX;
			player().y = startY;
		}
		else {
			player().room = "_transition_none"; // kind of hacky!!
		}

		drawRoom(room[startRoom]);
		var startPalette = getPal( room[startRoom].pal );
		var startImage = new PostProcessImage( ctx.getImageData(0,0,canvas.width,canvas.height) ); // TODO : don't use global ctx?
		transitionStart = new TransitionInfo(startImage, startPalette, startX, startY);

		if (transitionEffects[curEffect].showPlayerEnd) {
			player().room = endRoom;
			player().x = endX;
			player().y = endY;
		}
		else {
			player().room = "_transition_none";
		}

		drawRoom(room[endRoom]);
		var endPalette = getPal( room[endRoom].pal );
		var endImage = new PostProcessImage( ctx.getImageData(0,0,canvas.width,canvas.height) );
		transitionEnd = new TransitionInfo(endImage, endPalette, endX, endY);

		effectImage = new PostProcessImage( ctx.createImageData(canvas.width,canvas.height) );

		isTransitioning = true;
		transitionTime = 0;
		prevStep = -1;

		player().room = tmpRoom;
		player().x = tmpX;
		player().y = tmpY;
	}

	this.UpdateTransition = function(dt) {
		if (!isTransitioning) {
			return;
		}

		transitionTime += dt;

		var transitionDelta = transitionTime / transitionEffects[curEffect].duration;
		var maxStep = Math.floor(frameRate * (transitionEffects[curEffect].duration / 1000));
		var step = Math.floor(transitionDelta * maxStep);

		if (step != prevStep) {
			// console.log("step! " + step + " " + transitionDelta);
			for (var y = 0; y < effectImage.Height; y++) {
				for (var x = 0; x = transitionEffects[curEffect].duration) {
			isTransitioning = false;
			transitionTime = 0;
			transitionStart = null;
			transitionEnd = null;
			effectImage = null;
			prevStep = -1;

			if (transitionCompleteCallback != null) {
				transitionCompleteCallback();
			}
			transitionCompleteCallback = null;
		}
	}

	this.IsTransitionActive = function() {
		return isTransitioning;
	}

	// todo : should this be part of the constructor?
	var transitionCompleteCallback = null;
	this.OnTransitionComplete = function(callback) {
		if (isTransitioning) { // TODO : safety check necessary?
			transitionCompleteCallback = callback;
		}
	}

	var transitionEffects = {};
	var curEffect = "none";
	this.RegisterTransitionEffect = function(name, effect) {
		transitionEffects[name] = effect;
	}

	this.RegisterTransitionEffect("none", {
		showPlayerStart : false,
		showPlayerEnd : false,
		pixelEffectFunc : function() {},
	});

	this.RegisterTransitionEffect("fade_w", { // TODO : have it linger on full white briefly?
		showPlayerStart : false,
		showPlayerEnd : true,
		duration : 750,
		pixelEffectFunc : function(start,end,pixelX,pixelY,delta) {
			var pixelColorA = delta < 0.5 ? start.Image.GetPixel(pixelX,pixelY) : {r:255,g:255,b:255,a:255};
			var pixelColorB = delta < 0.5 ? {r:255,g:255,b:255,a:255} : end.Image.GetPixel(pixelX,pixelY);

			delta = delta < 0.5 ? (delta / 0.5) : ((delta - 0.5) / 0.5); // hacky

			return PostProcessUtilities.LerpColor(pixelColorA, pixelColorB, delta);
		}
	});

	this.RegisterTransitionEffect("fade_b", {
		showPlayerStart : false,
		showPlayerEnd : true,
		duration : 750,
		pixelEffectFunc : function(start,end,pixelX,pixelY,delta) {
			var pixelColorA = delta < 0.5 ? start.Image.GetPixel(pixelX,pixelY) : {r:0,g:0,b:0,a:255};
			var pixelColorB = delta < 0.5 ? {r:0,g:0,b:0,a:255} : end.Image.GetPixel(pixelX,pixelY);

			delta = delta < 0.5 ? (delta / 0.5) : ((delta - 0.5) / 0.5); // hacky

			return PostProcessUtilities.LerpColor(pixelColorA, pixelColorB, delta);
		}
	});

	this.RegisterTransitionEffect("wave", {
		showPlayerStart : true,
		showPlayerEnd : true,
		duration : 1500,
		pixelEffectFunc : function(start,end,pixelX,pixelY,delta) {
			var waveDelta = delta < 0.5 ? delta / 0.5 : 1 - ((delta - 0.5) / 0.5);

			var offset = (pixelY + (waveDelta * waveDelta * 0.2 * start.Image.Height));
			var freq = 4;
			var size = 2 + (14 * waveDelta);
			pixelX += Math.floor(Math.sin(offset / freq) * size);

			if (pixelX = start.Image.Width) {
				pixelX -= start.Image.Width;
			}

			var curImage = delta < 0.5 ? start.Image : end.Image;
			return curImage.GetPixel(pixelX,pixelY);
		}
	});

	this.RegisterTransitionEffect("tunnel", {
		showPlayerStart : true,
		showPlayerEnd : true,
		duration : 1500,
		pixelEffectFunc : function(start,end,pixelX,pixelY,delta) {
			if (delta  start.Image.Width * tunnelDelta) {
					return {r:0,g:0,b:0,a:255};
				}
				else {
					return start.Image.GetPixel(pixelX,pixelY);
				}
			}
			else if (delta  end.Image.Width * tunnelDelta) {
					return {r:0,g:0,b:0,a:255};
				}
				else {
					return end.Image.GetPixel(pixelX,pixelY);
				}
			}
		}
	});

	this.RegisterTransitionEffect("slide_u", {
		showPlayerStart : false,
		showPlayerEnd : true,
		duration : 1000,
		pixelEffectFunc : function(start,end,pixelX,pixelY,delta) {
			var pixelOffset = -1 * Math.floor(start.Image.Height * delta);
			var slidePixelY = pixelY + pixelOffset;

			var colorDelta = clampLerp(delta, 0.4);

			if (slidePixelY >= 0) {
				var colorA = start.Image.GetPixel(pixelX,slidePixelY);
				var colorB = PostProcessUtilities.GetCorrespondingColorFromPal(colorA,start.Palette,end.Palette);
				var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta);
				return colorLerped;
			}
			else {
				slidePixelY += start.Image.Height;
				var colorB = end.Image.GetPixel(pixelX,slidePixelY);
				var colorA = PostProcessUtilities.GetCorrespondingColorFromPal(colorB,end.Palette,start.Palette);
				var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta);
				return colorLerped;
			}
		}
	});

	this.RegisterTransitionEffect("slide_d", {
		showPlayerStart : false,
		showPlayerEnd : true,
		duration : 1000,
		pixelEffectFunc : function(start,end,pixelX,pixelY,delta) {
			var pixelOffset = Math.floor(start.Image.Height * delta);
			var slidePixelY = pixelY + pixelOffset;

			var colorDelta = clampLerp(delta, 0.4);

			if (slidePixelY = 0) {
				var colorA = start.Image.GetPixel(slidePixelX,pixelY);
				var colorB = PostProcessUtilities.GetCorrespondingColorFromPal(colorA,start.Palette,end.Palette);
				var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta);
				return colorLerped;
			}
			else {
				slidePixelX += start.Image.Width;
				var colorB = end.Image.GetPixel(slidePixelX,pixelY);
				var colorA = PostProcessUtilities.GetCorrespondingColorFromPal(colorB,end.Palette,start.Palette);
				var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta);
				return colorLerped;
			}
		}
	});

	this.RegisterTransitionEffect("slide_r", {
		showPlayerStart : false,
		showPlayerEnd : true,
		duration : 1000,
		pixelEffectFunc : function(start,end,pixelX,pixelY,delta) {
			var pixelOffset = Math.floor(start.Image.Width * delta);
			var slidePixelX = pixelX + pixelOffset;

			var colorDelta = clampLerp(delta, 0.4);

			if (slidePixelX < start.Image.Width) {
				var colorA = start.Image.GetPixel(slidePixelX,pixelY);
				var colorB = PostProcessUtilities.GetCorrespondingColorFromPal(colorA,start.Palette,end.Palette);
				var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta);
				return colorLerped;
			}
			else {
				slidePixelX -= start.Image.Width;
				var colorB = end.Image.GetPixel(slidePixelX,pixelY);
				var colorA = PostProcessUtilities.GetCorrespondingColorFromPal(colorB,end.Palette,start.Palette);
				var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta);
				return colorLerped;
			}
		}
	});

	function clampLerp(deltaIn, clampDuration) {
		var clampOffset = (1.0 - clampDuration) / 2;
		var deltaOut = Math.min(clampDuration, Math.max(0.0, deltaIn - clampOffset)) / clampDuration;
		return deltaOut;
	}

	// TODO : WIP
	// this.RegisterTransitionEffect("fuzz", {
	// 	showPlayerStart : true,
	// 	showPlayerEnd : true,
	// 	duration : 1500,
	// 	pixelEffectFunc : function(start,end,pixelX,pixelY,delta) {
	// 		var curImage = delta <= 0.5 ? start : end;
	// 		var sampleSize = delta <= 0.5 ? (2 + Math.floor(14 * (delta/0.5))) : (16 - Math.floor(14 * ((delta-0.5)/0.5)));

	// 		var palIndex = 0;

	// 		var sampleX = Math.floor(pixelX / sampleSize) * sampleSize;
	// 		var sampleY = Math.floor(pixelY / sampleSize) * sampleSize;

	// 		var frameState = transitionEffects["fuzz"].frameState;

	// 		if (frameState.time != delta) {
	// 			frameState.time = delta;
	// 			frameState.preCalcSampleValues = {};
	// 		}

	// 		if (frameState.preCalcSampleValues[[sampleX,sampleY]]) {
	// 			palIndex = frameState.preCalcSampleValues[[sampleX,sampleY]];
	// 		}
	// 		else {
	// 			var paletteCount = {};
	// 			var foregroundValue = 1.0;
	// 			var backgroundValue = 0.4;
	// 			for (var y = sampleY; y < sampleY + sampleSize; y++) {
	// 				for (var x = sampleX; x  maxCount) {
	// 					palIndex = i;
	// 					maxCount = paletteCount[i];
	// 				}
	// 			}

	// 			frameState.preCalcSampleValues[[sampleX,sampleY]] = palIndex;
	// 		}

	// 		return PostProcessUtilities.GetPalColor(curImage.Palette,palIndex);
	// 	},
	// 	frameState : { // ok this is hacky but it's for performance ok
	// 		time : -1,
	// 		preCalcSampleValues : {}
	// 	}
	// });
}; // TransitionManager()


// TODO : extract the scale variable so it can be changed?
var PostProcessUtilities = {
	SamplePixelColor : function(image,x,y) {
		var pixelIndex = (y * scale * image.width * 4) + (x * scale * 4);
		var r = image.data[pixelIndex + 0];
		var g = image.data[pixelIndex + 1];
		var b = image.data[pixelIndex + 2];
		var a = image.data[pixelIndex + 3];
		return { r:r, g:g, b:b, a:a };
	},
	SetPixelColor : function(image,x,y,colorRgba) {
		for (var yDelta = 0; yDelta < scale; yDelta++) {
			for (var xDelta = 0; xDelta < scale; xDelta++) {
				var pixelIndex = (((y * scale) + yDelta) * image.width * 4) + (((x * scale) + xDelta) * 4);
				image.data[pixelIndex + 0] = colorRgba.r;
				image.data[pixelIndex + 1] = colorRgba.g;
				image.data[pixelIndex + 2] = colorRgba.b;
				image.data[pixelIndex + 3] = colorRgba.a;
			}
		}
	},
	LerpColor : function(colorA,colorB,t) {
		// TODO: move to color_util.js?
		return {
			r : colorA.r + ((colorB.r - colorA.r) * t),
			g : colorA.g + ((colorB.g - colorA.g) * t),
			b : colorA.b + ((colorB.b - colorA.b) * t),
			a : colorA.a + ((colorB.a - colorA.a) * t),
		};
	},
	GetColorPalIndex : function(colorIn,curPal) {
		var colorIndex = -1;

		for (var i = 0; i = 0 && colorIndex <= otherPal.length) {
			return PostProcessUtilities.GetPalColor(otherPal,colorIndex);
		}
		else {
			return colorIn;
		}
	},
};

var PostProcessImage = function(imageData) {
	this.Width = imageData.width / scale;
	this.Height = imageData.height / scale;

	this.GetPixel = function(x,y) {
		return PostProcessUtilities.SamplePixelColor(imageData,x,y);
	};

	this.SetPixel = function(x,y,colorRgba) {
		PostProcessUtilities.SetPixelColor(imageData,x,y,colorRgba);
	};

	this.GetData = function() {
		return imageData;
	};
};

var TransitionInfo = function(image, palette, playerX, playerY) {
	this.Image = image;
	this.Palette = palette;
	this.PlayerTilePos = { x: playerX, y: playerY };
	this.PlayerCenter = { x: Math.floor((playerX * tilesize) + (tilesize / 2)), y: Math.floor((playerY * tilesize) + (tilesize / 2)) };
};



/*
TODO:
- can I simplify this more now that I've removed the external resources stuff?
*/

function FontManager(packagedFontNames) {

var self = this;

var fontExtension = ".bitsyfont";
this.GetExtension = function() {
	return fontExtension;
}

// place to store font data
var fontResources = {};

// load fonts from the editor
if (packagedFontNames != undefined && packagedFontNames != null && packagedFontNames.length > 0
		&& Resources != undefined && Resources != null) {

	for (var i = 0; i < packagedFontNames.length; i++) {
		var filename = packagedFontNames[i];
		fontResources[filename] = Resources[filename];
	}
}

// manually add resource
this.AddResource = function(filename, fontdata) {
	fontResources[filename] = fontdata;
}

this.ContainsResource = function(filename) {
	return fontResources[filename] != null;
}

function GetData(fontName) {
	return fontResources[fontName + fontExtension];
}
this.GetData = GetData;

function Create(fontData) {
	return new Font(fontData);
}
this.Create = Create;

this.Get = function(fontName) {
	var fontData = self.GetData(fontName);
	return self.Create(fontData);
}

function Font(fontData) {
	var name = "unknown";
	var width = 6; // default size so if you have NO font or an invalid font it displays boxes
	var height = 8;
	var chardata = {};

	// create invalid char data at default size in case the font is missing
	var invalidCharData = {};
	updateInvalidCharData();

	this.getName = function() {
		return name;
	}

	this.getData = function() {
		return chardata;
	}

	this.getWidth = function() {
		return width;
	}

	this.getHeight = function() {
		return height;
	}

	this.hasChar = function(char) {
		var codepoint = char.charCodeAt(0);
		return chardata[codepoint] != null;
	}

	this.getChar = function(char) {

		var codepoint = char.charCodeAt(0);

		if (chardata[codepoint] != null) {
			return chardata[codepoint];
		}
		else {
			return invalidCharData;
		}
	}

	this.allCharCodes = function() {
		var codeList = [];
		for (var code in chardata) {
			codeList.push(code);
		}
		return codeList;
	}

	function createCharData() {
		return { 
			width: width,
			height: height,
			offset: {
				x: 0,
				y: 0
			},
			spacing: width,
			data: [],
		};
	}

	function updateInvalidCharData() {
		invalidCharData = createCharData();
		for (var y = 0; y < height; y++) {
			for (var x = 0; x < width; x++) {
				if (x < width-1 && y < height-1) {
					invalidCharData.data.push(1);
				}
				else {
					invalidCharData.data.push(0);
				}
			}
		}
	}

	function parseFont(fontData) {
		if (fontData == null) {
			return;
		}

		var lines = fontData.split("\n");

		var isReadingChar = false;
		var isReadingCharProperties = false;
		var curCharLineCount = 0;
		var curCharCode = 0;

		for (var i = 0; i < lines.length; i++) {
			var line = lines[i];

			if (line[0] === "#") {
				continue; // skip comment lines
			}

			if (!isReadingChar) {
				// READING NON CHARACTER DATA LINE
				var args = line.split(" ");
				if (args[0] == "FONT") {
					name = args[1];
				}
				else if (args[0] == "SIZE") {
					width = parseInt(args[1]);
					height = parseInt(args[2]);
				}
				else if (args[0] == "CHAR") {
					isReadingChar = true;
					isReadingCharProperties = true;

					curCharLineCount = 0;
					curCharCode = parseInt(args[1]);
					chardata[curCharCode] = createCharData();
				}
			}
			else {
				// CHAR PROPERTIES
				if (isReadingCharProperties) {
					var args = line.split(" ");
					if (args[0].indexOf("CHAR_") == 0) { // Sub-properties start with "CHAR_"
						if (args[0] == "CHAR_SIZE") {
							// Custom character size - overrides the default character size for the font
							chardata[curCharCode].width = parseInt(args[1]);
							chardata[curCharCode].height = parseInt(args[2]);
							chardata[curCharCode].spacing = parseInt(args[1]); // HACK : assumes CHAR_SIZE is always declared first
						}
						else if (args[0] == "CHAR_OFFSET") {
							// Character offset - shift the origin of the character on the X or Y axis
							chardata[curCharCode].offset.x = parseInt(args[1]);
							chardata[curCharCode].offset.y = parseInt(args[2]);
						}
						else if (args[0] == "CHAR_SPACING") {
							// Character spacing:
							// specify total horizontal space taken up by the character
							// lets chars take up more or less space on a line than its bitmap does
							chardata[curCharCode].spacing = parseInt(args[1]);
						}
					}
					else {
						isReadingCharProperties = false;
					}
				}

				// CHAR DATA
				if (!isReadingCharProperties) {
					// READING CHARACTER DATA LINE
					for (var j = 0; j = height) {
						isReadingChar = false;
					}
				}
			}
		}

		// re-init invalid character box at the actual font size once it's loaded
		updateInvalidCharData();
	}

	parseFont(fontData);
}

} // FontManager



function Script() {

this.CreateInterpreter = function() {
	return new Interpreter();
};

this.CreateUtils = function() {
	return new Utils();
};

var Interpreter = function() {
	var env = new Environment();
	var parser = new Parser( env );

	this.SetDialogBuffer = function(buffer) { env.SetDialogBuffer( buffer ); };

	// TODO -- maybe this should return a string instead othe actual script??
	this.Compile = function(scriptName, scriptStr) {
		// console.log("COMPILE");
		var script = parser.Parse(scriptStr, scriptName);
		env.SetScript(scriptName, script);
	}
	this.Run = function(scriptName, exitHandler, objectContext) { // Runs pre-compiled script
		var localEnv = new LocalEnvironment(env);

		if (objectContext) {
			localEnv.SetObject(objectContext); // PROTO : should this be folded into the constructor?
		}

		var script = env.GetScript(scriptName);

		script.Eval( localEnv, function(result) { OnScriptReturn(localEnv, exitHandler); } );
	}
	this.Interpret = function(scriptStr, exitHandler, objectContext) { // Compiles and runs code immediately
		// console.log("INTERPRET");
		var localEnv = new LocalEnvironment(env);

		if (objectContext) {
			localEnv.SetObject(objectContext); // PROTO : should this be folded into the constructor?
		}

		var script = parser.Parse(scriptStr, "anonymous");
		script.Eval( localEnv, function(result) { OnScriptReturn(localEnv, exitHandler); } );
	}
	this.HasScript = function(name) { return env.HasScript(name); };

	this.ResetEnvironment = function() {
		env = new Environment();
		parser = new Parser( env );
	}

	this.Parse = function(scriptStr, rootId) { // parses a script but doesn't save it
		return parser.Parse(scriptStr, rootId);
	}

	// TODO : add back in if needed later...
	// this.CompatibilityParse = function(scriptStr, compatibilityFlags) {
	// 	env.compatibilityFlags = compatibilityFlags;

	// 	var result = parser.Parse(scriptStr);

	// 	delete env.compatibilityFlags;

	// 	return result;
	// }

	this.Eval = function(scriptTree, exitHandler) { // runs a script stored externally
		var localEnv = new LocalEnvironment(env); // TODO : does this need an object context?
		scriptTree.Eval(
			localEnv,
			function(result) {
				OnScriptReturn(result, exitHandler);
			});
	}

	function OnScriptReturn(result, exitHandler) {
		if (exitHandler != null) {
			exitHandler(result);
		}
	}

	this.CreateExpression = function(expStr) {
		return parser.CreateExpression(expStr);
	}

	this.SetVariable = function(name,value,useHandler) {
		env.SetVariable(name,value,useHandler);
	}

	this.DeleteVariable = function(name,useHandler) {
		env.DeleteVariable(name,useHandler);
	}
	this.HasVariable = function(name) {
		return env.HasVariable(name);
	}

	this.SetOnVariableChangeHandler = function(onVariableChange) {
		env.SetOnVariableChangeHandler(onVariableChange);
	}
	this.GetVariableNames = function() {
		return env.GetVariableNames();
	}
	this.GetVariable = function(name) {
		return env.GetVariable(name);
	}

	function DebugVisualizeScriptTree(scriptTree) {
		var printVisitor = {
			Visit : function(node,depth) {
				console.log("-".repeat(depth) + "- " + node.ToString());
			},
		};

		scriptTree.VisitAll( printVisitor );
	}

	this.DebugVisualizeScriptTree = DebugVisualizeScriptTree;

	this.DebugVisualizeScript = function(scriptName) {
		DebugVisualizeScriptTree(env.GetScript(scriptName));
	}
}


var Utils = function() {
	// for editor ui
	this.CreateDialogBlock = function(children,doIndentFirstLine) {
		if (doIndentFirstLine === undefined) {
			doIndentFirstLine = true;
		}

		var block = new DialogBlockNode(doIndentFirstLine);

		for (var i = 0; i < children.length; i++) {
			block.AddChild(children[i]);
		}
		return block;
	}

	this.CreateOptionBlock = function() {
		var block = new DialogBlockNode(false);
		block.AddChild(new FuncNode("print", [new LiteralNode(" ")]));
		return block;
	}

	this.CreateItemConditionPair = function() {
		var itemFunc = this.CreateFunctionBlock("item", ["0"]);
		var condition = new ExpNode("==", itemFunc, new LiteralNode(1));
		var result = new DialogBlockNode(true);
		result.AddChild(new FuncNode("print", [new LiteralNode(" ")]));
		var conditionPair = new ConditionPairNode(condition, result);
		return conditionPair;
	}

	this.CreateVariableConditionPair = function() {
		var varNode = this.CreateVariableNode("a");
		var condition = new ExpNode("==", varNode, new LiteralNode(1));
		var result = new DialogBlockNode(true);
		result.AddChild(new FuncNode("print", [new LiteralNode(" ")]));
		var conditionPair = new ConditionPairNode(condition, result);
		return conditionPair;
	}

	this.CreateDefaultConditionPair = function() {
		var condition = this.CreateElseNode();
		var result = new DialogBlockNode(true);
		result.AddChild(new FuncNode("print", [new LiteralNode(" ")]));
		var conditionPair = new ConditionPairNode(condition, result);
		return conditionPair;
	}

	this.CreateEmptyPrintFunc = function() {
		return new FuncNode("print", [new LiteralNode("...")]);
	}

	this.CreateFunctionBlock = function(name, initParamValues) {
		var parameters = [];
		for (var i = 0; i  -1) {
			dialogStr = Sym.DialogOpen + "\n" + dialogStr + "\n" + Sym.DialogClose;
		}
		return dialogStr;
	}

	this.RemoveDialogBlockFormat = function(source) {
		var sourceLines = source.split("\n");
		var dialogStr = "";
		if(sourceLines[0] === Sym.DialogOpen) {
			// multi line
			var i = 1;
			while (i < sourceLines.length && sourceLines[i] != Sym.DialogClose) {
				dialogStr += sourceLines[i] + (sourceLines[i+1] != Sym.DialogClose ? '\n' : '');
				i++;
			}
		}
		else {
			// single line
			dialogStr = source;
		}
		return dialogStr;
	}

	this.SerializeDialogNodeList = function(nodeList) {
		var tempBlock = new DialogBlockNode(false);
		 // set children directly to avoid breaking the parenting chain for this temp operation
		tempBlock.children = nodeList;
		return tempBlock.Serialize();
	}

	this.GetOperatorList = function() {
		return [Sym.Set].concat(Sym.Operators);
	}

	this.IsInlineCode = function(node) {
		return isInlineCode(node);
	}
}


/* BUILT-IN FUNCTIONS */ // TODO: better way to encapsulate these?
function deprecatedFunc(environment,parameters,onReturn) {
	console.log("BITSY SCRIPT WARNING: Tried to use deprecated function");
	onReturn(null);
}

function printFunc(environment, parameters, onReturn) {
	if (parameters[0] != undefined && parameters[0] != null) {
		var textStr = "" + parameters[0];
		environment.GetDialogBuffer().AddText(textStr);
		environment.GetDialogBuffer().AddScriptReturn(function() { onReturn(null); });
	}
	else {
		onReturn(null);
	}
}

function linebreakFunc(environment, parameters, onReturn) {
	// console.log("LINEBREAK FUNC");
	environment.GetDialogBuffer().AddLinebreak();
	environment.GetDialogBuffer().AddScriptReturn(function() { onReturn(null); });
}

function pagebreakFunc(environment, parameters, onReturn) {
	environment.GetDialogBuffer().AddPagebreak(function() { onReturn(null); });
}

function printDrawingFunc(environment, parameters, onReturn) {
	var drawingId = parameters[0];
	environment.GetDialogBuffer().AddDrawing(drawingId);
	environment.GetDialogBuffer().AddScriptReturn(function() { onReturn(null); });
}

function printSpriteFunc(environment,parameters,onReturn) {
	var spriteId = parameters[0];
	if(names.sprite.has(spriteId)) spriteId = names.sprite.get(spriteId); // id is actually a name
	var drawingId = sprite[spriteId].drw;
	printDrawingFunc(environment, [drawingId], onReturn);
}

function printTileFunc(environment,parameters,onReturn) {
	var tileId = parameters[0];
	if(names.tile.has(tileId)) tileId = names.tile.get(tileId); // id is actually a name
	var drawingId = tile[tileId].drw;
	printDrawingFunc(environment, [drawingId], onReturn);
}

function printItemFunc(environment,parameters,onReturn) {
	var itemId = parameters[0];
	if(names.item.has(itemId)) itemId = names.item.get(itemId); // id is actually a name
	var drawingId = item[itemId].drw;
	printDrawingFunc(environment, [drawingId], onReturn);
}

function printFontFunc(environment, parameters, onReturn) {
	var allCharacters = "";
	var font = fontManager.Get( fontName );
	var codeList = font.allCharCodes();
	for (var i = 0; i  1) {
		// TODO : is it a good idea to force inventory to be >= 0?
		player().inventory[itemId] = Math.max(0, parseInt(parameters[1]));
		curItemCount = player().inventory[itemId];

		if (onInventoryChanged != null) {
			onInventoryChanged(itemId);
		}
	}

	onReturn(curItemCount);
}

function addOrRemoveTextEffect(environment,name) {
	if( environment.GetDialogBuffer().HasTextEffect(name) )
		environment.GetDialogBuffer().RemoveTextEffect(name);
	else
		environment.GetDialogBuffer().AddTextEffect(name);
}

function rainbowFunc(environment,parameters,onReturn) {
	addOrRemoveTextEffect(environment,"rbw");
	onReturn(null);
}

// TODO : should the colors use a parameter instead of special names?
function color1Func(environment,parameters,onReturn) {
	addOrRemoveTextEffect(environment,"clr1");
	onReturn(null);
}

function color2Func(environment,parameters,onReturn) {
	addOrRemoveTextEffect(environment,"clr2");
	onReturn(null);
}

function color3Func(environment,parameters,onReturn) {
	addOrRemoveTextEffect(environment,"clr3");
	onReturn(null);
}

function wavyFunc(environment,parameters,onReturn) {
	addOrRemoveTextEffect(environment,"wvy");
	onReturn(null);
}

function shakyFunc(environment,parameters,onReturn) {
	addOrRemoveTextEffect(environment,"shk");
	onReturn(null);
}

function propertyFunc(environment, parameters, onReturn) {
	var outValue = null;

	if (parameters.length > 0 && parameters[0]) {
		var propertyName = parameters[0];

		if (environment.HasProperty(propertyName)) {
			// TODO : in a future update I can handle the case of initializing a new property
			// after which we can move this block outside the HasProperty check
			if (parameters.length > 1) {
				var inValue = parameters[1];
				environment.SetProperty(propertyName, inValue);
			}

			outValue = environment.GetProperty(propertyName);
		}
	}

	console.log("PROPERTY! " + propertyName + " " + outValue);

	onReturn(outValue);
}

function endFunc(environment,parameters,onReturn) {
	isEnding = true;
	isNarrating = true;
	dialogRenderer.SetCentered(true);
	onReturn(null);
}

function exitFunc(environment,parameters,onReturn) {
	var destRoom = parameters[0];

	if (names.room.has(destRoom)) {
		// it's a name, not an id! (note: these could cause trouble if people names things weird)
		destRoom = names.room.get(destRoom);
	}

	var destX = parseInt(parameters[1]);
	var destY = parseInt(parameters[2]);

	if (parameters.length >= 4) {
		var transitionEffect = parameters[3];

		transition.BeginTransition(
			player().room,
			player().x,
			player().y,
			destRoom,
			destX,
			destY,
			transitionEffect);
		transition.UpdateTransition(0);
	}

	player().room = destRoom;
	player().x = destX;
	player().y = destY;
	curRoom = destRoom;
	initRoom(curRoom);

	// TODO : this doesn't play nice with pagebreak because it thinks the dialog is finished!
	if (transition.IsTransitionActive()) {
		transition.OnTransitionComplete(function() { onReturn(null); });
	}
	else {
		onReturn(null);
	}
}

/* BUILT-IN OPERATORS */
function setExp(environment,left,right,onReturn) {
	// console.log("SET " + left.name);

	if(left.type != "variable") {
		// not a variable! return null and hope for the best D:
		onReturn( null );
		return;
	}

	right.Eval(environment,function(rVal) {
		environment.SetVariable( left.name, rVal );
		// console.log("VAL " + environment.GetVariable( left.name ) );
		left.Eval(environment,function(lVal) {
			onReturn( lVal );
		});
	});
}
function equalExp(environment,left,right,onReturn) {
	// console.log("EVAL EQUAL");
	// console.log(left);
	// console.log(right);
	right.Eval(environment,function(rVal){
		left.Eval(environment,function(lVal){
			onReturn( lVal === rVal );
		});
	});
}
function greaterExp(environment,left,right,onReturn) {
	right.Eval(environment,function(rVal){
		left.Eval(environment,function(lVal){
			onReturn( lVal > rVal );
		});
	});
}
function lessExp(environment,left,right,onReturn) {
	right.Eval(environment,function(rVal){
		left.Eval(environment,function(lVal){
			onReturn( lVal = rVal );
		});
	});
}
function lessEqExp(environment,left,right,onReturn) {
	right.Eval(environment,function(rVal){
		left.Eval(environment,function(lVal){
			onReturn( lVal ", greaterExp);
	operatorMap.set("=", greaterEqExp);
	operatorMap.set("<=", lessEqExp);
	operatorMap.set("*", multExp);
	operatorMap.set("/", divExp);
	operatorMap.set("+", addExp);
	operatorMap.set("-", subExp);

	this.HasOperator = function(sym) { return operatorMap.get(sym); };
	this.EvalOperator = function(sym,left,right,onReturn) {
		operatorMap.get( sym )( this, left, right, onReturn );
	}

	var scriptMap = new Map();
	this.HasScript = function(name) { return scriptMap.has(name); };
	this.GetScript = function(name) { return scriptMap.get(name); };
	this.SetScript = function(name,script) { scriptMap.set(name, script); };

	var onVariableChangeHandler = null;
	this.SetOnVariableChangeHandler = function(onVariableChange) {
		onVariableChangeHandler = onVariableChange;
	}
	this.GetVariableNames = function() {
		return Array.from( variableMap.keys() );
	}
}

// Local environment for a single run of a script: knows local context
var LocalEnvironment = function(parentEnvironment) {
	// this.SetDialogBuffer // not allowed in local environment?
	this.GetDialogBuffer = function() { return parentEnvironment.GetDialogBuffer(); };

	this.HasFunction = function(name) { return parentEnvironment.HasFunction(name); };
	this.EvalFunction = function(name,parameters,onReturn,env) {
		if (env == undefined || env == null) {
			env = this;
		}

		parentEnvironment.EvalFunction(name,parameters,onReturn,env);
	}

	this.HasVariable = function(name) { return parentEnvironment.HasVariable(name); };
	this.GetVariable = function(name) { return parentEnvironment.GetVariable(name); };
	this.SetVariable = function(name,value,useHandler) { parentEnvironment.SetVariable(name,value,useHandler); };
	// this.DeleteVariable // not needed in local environment?

	this.HasOperator = function(sym) { return parentEnvironment.HasOperator(sym); };
	this.EvalOperator = function(sym,left,right,onReturn,env) {
		if (env == undefined || env == null) {
			env = this;
		}

		parentEnvironment.EvalOperator(sym,left,right,onReturn,env);
	};

	// TODO : I don't *think* any of this is required by the local environment
	// this.HasScript
	// this.GetScript
	// this.SetScript

	// TODO : pretty sure these debug methods aren't required by the local environment either
	// this.SetOnVariableChangeHandler
	// this.GetVariableNames

	/* Here's where specific local context data goes:
	 * this includes access to the object running the script
	 * and any properties it may have (so far only "locked")
	 */

	// The local environment knows what object called it -- currently only used to access properties
	var curObject = null;
	this.HasObject = function() { return curObject != undefined && curObject != null; }
	this.SetObject = function(object) { curObject = object; }
	this.GetObject = function() { return curObject; }

	// accessors for properties of the object that's running the script
	this.HasProperty = function(name) {
		if (curObject && curObject.property && curObject.property.hasOwnProperty(name)) {
			return true;
		}
		else {
			return false;
		}
	};
	this.GetProperty = function(name) {
		if (curObject && curObject.property && curObject.property.hasOwnProperty(name)) {
			return curObject.property[name]; // TODO : should these be getters and setters instead?
		}
		else {
			return null;
		}
	};
	this.SetProperty = function(name, value) {
		// NOTE : for now, we need to gaurd against creating new properties
		if (curObject && curObject.property && curObject.property.hasOwnProperty(name)) {
			curObject.property[name] = value;
		}
	};
}

function leadingWhitespace(depth) {
	var str = "";
	for(var i = 0; i < depth; i++) {
		str += "  "; // two spaces per indent
	}
	// console.log("WHITESPACE " + depth + " ::" + str + "::");
	return str;
}

/* NODES */
var TreeRelationship = function() {
	this.parent = null;
	this.children = [];

	this.AddChild = function(node) {
		this.children.push(node);
		node.parent = this;
	};

	this.AddChildren = function(nodeList) {
		for (var i = 0; i < nodeList.length; i++) {
			this.AddChild(nodeList[i]);
		}
	};

	this.SetChildren = function(nodeList) {
		this.children = [];
		this.AddChildren(nodeList);
	};

	this.VisitAll = function(visitor, depth) {
		if (depth == undefined || depth == null) {
			depth = 0;
		}

		visitor.Visit(this, depth);
		for (var i = 0; i < this.children.length; i++) {
			this.children[i].VisitAll( visitor, depth + 1 );
		}
	};

	this.rootId = null; // for debugging
	this.GetId = function() {
		// console.log(this);
		if (this.rootId != null) {
			return this.rootId;
		}
		else if (this.parent != null) {
			var parentId = this.parent.GetId();
			if (parentId != null) {
				return parentId + "_" + this.parent.children.indexOf(this);
			}
		}
		else {
			return null;
		}
	}
}

var DialogBlockNode = function(doIndentFirstLine) {
	Object.assign( this, new TreeRelationship() );
	// Object.assign( this, new Runnable() );
	this.type = "dialog_block";

	this.Eval = function(environment, onReturn) {
		// console.log("EVAL BLOCK " + this.children.length);

		if (isPlayerEmbeddedInEditor && events != undefined && events != null) {
			events.Raise("script_node_enter", { id: this.GetId() });
		}

		var lastVal = null;
		var i = 0;

		function evalChildren(children, done) {
			if (i > CHILD " + i);
				children[i].Eval(environment, function(val) {
					// console.log("<< CHILD " + i);
					lastVal = val;
					i++;
					evalChildren(children,done);
				});
			}
			else {
				done();
			}
		};

		var self = this;
		evalChildren(this.children, function() {
			if (isPlayerEmbeddedInEditor && events != undefined && events != null) {
				events.Raise("script_node_exit", { id: self.GetId() });
			}

			onReturn(lastVal);
		});
	}

	if (doIndentFirstLine === undefined) {
		doIndentFirstLine = true; // This is just for serialization
	}

	this.Serialize = function(depth) {
		if (depth === undefined) {
			depth = 0;
		}

		var str = "";
		var lastNode = null;

		for (var i = 0; i  0 && curNodeIsNonInlineCode;
			var shouldIndentAfterCodeBlock = prevNodeIsNonInlineCode;

			// need to insert a newline before the first block of non-inline code that isn't 
			// preceded by a {br}, since those will create their own newline
			if (i > 0 && curNodeIsNonInlineCode && !prevNodeIsNonInlineCode && !shouldIndentAfterLinebreak) {
				str += "\n";
			}

			if (shouldIndentFirstLine || shouldIndentAfterLinebreak || shouldIndentCodeBlock || shouldIndentAfterCodeBlock) {
				str += leadingWhitespace(depth);
			}

			str += curNode.Serialize(depth);

			if (i < this.children.length-1 && curNodeIsNonInlineCode) {
				str += "\n";
			}

			lastNode = curNode;
		}

		return str;
	}

	this.ToString = function() {
		return this.type + " " + this.GetId();
	};
}

var CodeBlockNode = function() {
	Object.assign( this, new TreeRelationship() );
	this.type = "code_block";

	this.Eval = function(environment, onReturn) {
		// console.log("EVAL BLOCK " + this.children.length);

		if (isPlayerEmbeddedInEditor && events != undefined && events != null) {
			events.Raise("script_node_enter", { id: this.GetId() });
		}

		var lastVal = null;
		var i = 0;

		function evalChildren(children, done) {
			if (i > CHILD " + i);
				children[i].Eval(environment, function(val) {
					// console.log("<< CHILD " + i);
					lastVal = val;
					i++;
					evalChildren(children,done);
				});
			}
			else {
				done();
			}
		};

		var self = this;
		evalChildren(this.children, function() {
			if (isPlayerEmbeddedInEditor && events != undefined && events != null) {
				events.Raise("script_node_exit", { id: self.GetId() });
			}

			onReturn(lastVal);
		});
	}

	this.Serialize = function(depth) {
		if(depth === undefined) {
			depth = 0;
		}

		// console.log("SERIALIZE BLOCK!!!");
		// console.log(depth);
		// console.log(doIndentFirstLine);

		var str = "{"; // todo: increase scope of Sym?

		// TODO : do code blocks ever have more than one child anymore????
		for (var i = 0; i  0 && node.children[0].type === "undefined";
}

var textEffectBlockNames = ["clr1", "clr2", "clr3", "wvy", "shk", "rbw", "printSprite", "printItem", "printTile", "print", "say", "br"];
function isTextEffectBlock(node) {
	if (node.type === "code_block") {
		if (node.children.length > 0 && node.children[0].type === "function") {
			var func = node.children[0];
			return textEffectBlockNames.indexOf(func.name) != -1;
		}
	}
	return false;
}

var listBlockTypes = ["sequence", "cycle", "shuffle", "if"];
function isMultilineListBlock(node) {
	if (node.type === "code_block") {
		if (node.children.length > 0) {
			var child = node.children[0];
			return listBlockTypes.indexOf(child.type) != -1;
		}
	}
	return false;
}

// for round-tripping undefined code through the parser (useful for hacks!)
var UndefinedNode = function(sourceStr) {
	Object.assign(this, new TreeRelationship());
	this.type = "undefined";
	this.source = sourceStr;

	this.Eval = function(environment,onReturn) {
		addOrRemoveTextEffect(environment, "_debug_highlight");
		printFunc(environment, ["{" + sourceStr + "}"], function() {
			onReturn(null);
		});
		addOrRemoveTextEffect(environment, "_debug_highlight");
	}

	this.Serialize = function(depth) {
		return this.source;
	}

	this.ToString = function() {
		return "undefined" + " " + this.GetId();
	}
}

var FuncNode = function(name,args) {
	Object.assign( this, new TreeRelationship() );
	// Object.assign( this, new Runnable() );
	this.type = "function";
	this.name = name;
	this.args = args;

	this.Eval = function(environment,onReturn) {
		if (isPlayerEmbeddedInEditor && events != undefined && events != null) {
			events.Raise("script_node_enter", { id: this.GetId() });
		}

		var self = this; // hack to deal with scope (TODO : move up higher?)

		var argumentValues = [];
		var i = 0;

		function evalArgs(args, done) {
			// TODO : really hacky way to make we get the first
			// symbol's NAME instead of its variable value
			// if we are trying to do something with a property
			if (self.name === "property" && i === 0 && i < args.length) {
				if (args[i].type === "variable") {
					argumentValues.push(args[i].name);
					i++;
				}
				else {
					// first argument for a property MUST be a variable symbol
					// -- so skip everything if it's not!
					i = args.length;
				}
			}

			if (i < args.length) {
				// Evaluate each argument
				args[i].Eval(
					environment,
					function(val) {
						argumentValues.push(val);
						i++;
						evalArgs(args, done);
					});
			}
			else {
				done();
			}
		};

		evalArgs(
			this.args,
			function() {
				if (isPlayerEmbeddedInEditor && events != undefined && events != null) {
					events.Raise("script_node_exit", { id: self.GetId() });
				}

				environment.EvalFunction(self.name, argumentValues, onReturn);
			});
	}

	this.Serialize = function(depth) {
		var isDialogBlock = this.parent.type === "dialog_block";
		if (isDialogBlock && this.name === "print") {
			// TODO this could cause problems with "real" print functions
			return this.args[0].value; // first argument should be the text of the {print} func
		}
		else if (isDialogBlock && this.name === "br") {
			return "\n";
		}
		else {
			var str = "";
			str += this.name;
			for(var i = 0; i < this.args.length; i++) {
				str += " ";
				str += this.args[i].Serialize(depth);
			}
			return str;
		}
	}

	this.ToString = function() {
		return this.type + " " + this.name + " " + this.GetId();
	};
}

var LiteralNode = function(value) {
	Object.assign( this, new TreeRelationship() );
	// Object.assign( this, new Runnable() );
	this.type = "literal";
	this.value = value;

	this.Eval = function(environment,onReturn) {
		onReturn(this.value);
	}

	this.Serialize = function(depth) {
		var str = "";

		if (this.value === null) {
			return str;
		}

		if (typeof this.value === "string") {
			str += '"';
		}

		str += this.value;

		if (typeof this.value === "string") {
			str += '"';
		}

		return str;
	}

	this.ToString = function() {
		return this.type + " " + this.value + " " + this.GetId();
	};
}

var VarNode = function(name) {
	Object.assign( this, new TreeRelationship() );
	// Object.assign( this, new Runnable() );
	this.type = "variable";
	this.name = name;

	this.Eval = function(environment,onReturn) {
		// console.log("EVAL " + this.name + " " + environment.HasVariable(this.name) + " " + environment.GetVariable(this.name));
		if( environment.HasVariable(this.name) )
			onReturn( environment.GetVariable( this.name ) );
		else
			onReturn(null); // not a valid variable -- return null and hope that's ok
	} // TODO: might want to store nodes in the variableMap instead of values???

	this.Serialize = function(depth) {
		var str = "" + this.name;
		return str;
	}

	this.ToString = function() {
		return this.type + " " + this.name + " " + this.GetId();
	};
}

var ExpNode = function(operator, left, right) {
	Object.assign( this, new TreeRelationship() );
	this.type = "operator";
	this.operator = operator;
	this.left = left;
	this.right = right;

	this.Eval = function(environment,onReturn) {
		// console.log("EVAL " + this.operator);
		var self = this; // hack to deal with scope
		environment.EvalOperator( this.operator, this.left, this.right, 
			function(val){
				// console.log("EVAL EXP " + self.operator + " " + val);
				onReturn(val);
			} );
		// NOTE : sadly this pushes a lot of complexity down onto the actual operator methods
	}

	this.Serialize = function(depth) {
		var isNegativeNumber = this.operator === "-" && this.left.type === "literal" && this.left.value === null;

		if (!isNegativeNumber) {
			var str = "";

			if (this.left != undefined && this.left != null) {
				str += this.left.Serialize(depth) + " ";
			}

			str += this.operator;

			if (this.right != undefined && this.right != null) {
				str += " " + this.right.Serialize(depth);
			}

			return str;
		}
		else {
			return this.operator + this.right.Serialize(depth); // hacky but seems to work
		}
	}

	this.VisitAll = function(visitor, depth) {
		if (depth == undefined || depth == null) {
			depth = 0;
		}

		visitor.Visit( this, depth );
		if(this.left != null)
			this.left.VisitAll( visitor, depth + 1 );
		if(this.right != null)
			this.right.VisitAll( visitor, depth + 1 );
	};

	this.ToString = function() {
		return this.type + " " + this.operator + " " + this.GetId();
	};
}

var SequenceBase = function() {
	this.Serialize = function(depth) {
		var str = "";
		str += this.type + "\n";
		for (var i = 0; i < this.children.length; i++) {
			str += leadingWhitespace(depth + 1) + Sym.List + " ";
			str += this.children[i].Serialize(depth + 2);
			str += "\n";
		}
		str += leadingWhitespace(depth);
		return str;
	}

	this.VisitAll = function(visitor, depth) {
		if (depth == undefined || depth == null) {
			depth = 0;
		}

		visitor.Visit(this, depth);
		for (var i = 0; i < this.children.length; i++) {
			this.children[i].VisitAll( visitor, depth + 1 );
		}
	};

	this.ToString = function() {
		return this.type + " " + this.GetId();
	};
}

var SequenceNode = function(options) {
	Object.assign(this, new TreeRelationship());
	Object.assign(this, new SequenceBase());
	this.type = "sequence";
	this.AddChildren(options);

	var index = 0;
	this.Eval = function(environment, onReturn) {
		// console.log("SEQUENCE " + index);
		this.children[index].Eval(environment, onReturn);

		var next = index + 1;
		if (next < this.children.length) {
			index = next;
		}
	}
}

var CycleNode = function(options) {
	Object.assign(this, new TreeRelationship());
	Object.assign(this, new SequenceBase());
	this.type = "cycle";
	this.AddChildren(options);

	var index = 0;
	this.Eval = function(environment, onReturn) {
		// console.log("CYCLE " + index);
		this.children[index].Eval(environment, onReturn);

		var next = index + 1;
		if (next  0) {
			var i = Math.floor(Math.random() * optionsUnshuffled.length);
			optionsShuffled.push(optionsUnshuffled.splice(i,1)[0]);
		}
	}
	shuffle(this.children);

	var index = 0;
	this.Eval = function(environment, onReturn) {
		optionsShuffled[index].Eval(environment, onReturn);
		
		index++;
		if (index >= this.children.length) {
			shuffle(this.children);
			index = 0;
		}
	}
}

// TODO : rename? ConditionalNode?
var IfNode = function(conditions, results, isSingleLine) {
	Object.assign(this, new TreeRelationship());
	this.type = "if";

	for (var i = 0; i < conditions.length; i++) {
		this.AddChild(new ConditionPairNode(conditions[i], results[i]));
	}

	var self = this;
	this.Eval = function(environment, onReturn) {
		// console.log("EVAL IF");
		var i = 0;
		function TestCondition() {
			self.children[i].Eval(environment, function(result) {
				if (result.conditionValue == true) {
					onReturn(result.resultValue);
				}
				else if (i+1  1 && this.children[1].children[0].type === Sym.Else) {
				str += " " + Sym.ElseExp + " " + this.children[1].children[1].Serialize();
			}
		}
		else {
			str += "\n";
			for (var i = 0; i < this.children.length; i++) {
				str += this.children[i].Serialize(depth);
			}
			str += leadingWhitespace(depth);
		}
		return str;
	}

	this.IsSingleLine = function() {
		return isSingleLine;
	}

	this.VisitAll = function(visitor, depth) {
		if (depth == undefined || depth == null) {
			depth = 0;
		}

		visitor.Visit(this, depth);

		for (var i = 0; i < this.children.length; i++) {
			this.children[i].VisitAll(visitor, depth + 1);
		}
	};

	this.ToString = function() {
		return this.type + " " + this.mode + " " + this.GetId();
	};
}

var ConditionPairNode = function(condition, result) {
	Object.assign(this, new TreeRelationship());

	this.type = "condition_pair";

	this.AddChild(condition);
	this.AddChild(result);

	var self = this;

	this.Eval = function(environment, onReturn) {
		self.children[0].Eval(environment, function(conditionSuccess) {
			if (conditionSuccess) {
				self.children[1].Eval(environment, function(resultValue) {
					onReturn({ conditionValue:true, resultValue:resultValue });
				});
			}
			else {
				onReturn({ conditionValue:false });
			}
		});
	}

	this.Serialize = function(depth) {
		var str = "";
		str += leadingWhitespace(depth + 1);
		str += Sym.List + " " + this.children[0].Serialize(depth) + " " + Sym.ConditionEnd + Sym.Linebreak;
		str += this.children[1].Serialize(depth + 2) + Sym.Linebreak;
		return str;
	}

	this.VisitAll = function(visitor, depth) {
		if (depth == undefined || depth == null) {
			depth = 0;
		}

		visitor.Visit(this, depth);

		for (var i = 0; i =", "", "= sourceStr.length; };
		this.Char = function() { return sourceStr[i]; };
		this.Step = function(n) { if(n===undefined) n=1; i += n; };
		this.MatchAhead = function(str) {
			// console.log(str);
			str = "" + str; // hack to turn single chars into strings
			// console.log(str);
			// console.log(str.length);
			for (var j = 0; j = sourceStr.length) {
					return false;
				}
				else if (str[j] != sourceStr[i+j]) {
					return false;
				}
			}
			return true;
		}
		this.Peak = function(end) {
			var str = "";
			var j = i;
			// console.log(j);
			while (j  0 && !this.Done()) {
				if (this.MatchAhead(close)) {
					matchCount--;
					this.Step( close.length );
				}
				else if (this.MatchAhead(open)) {
					matchCount++;
					this.Step(open.length);
				}
				else {
					this.Step();
				}
			}

			if (includeSymbols) {
				return sourceStr.slice(startIndex, i);
			}
			else {
				return sourceStr.slice(startIndex + open.length, i - close.length);
			}
		}

		this.Print = function() { console.log(sourceStr); };
		this.Source = function() { return sourceStr; };
	};

	/*
		ParseDialog():
		This function adds {print} nodes and linebreak {br} nodes to display text,
		interleaved with bracketed code nodes for functions and flow control,
		such as text effects {shk} {wvy} or sequences like {cycle} and {shuffle}.
		The parsing of those code blocks is handled by ParseCode.

		Note on parsing newline characters:
		- there should be an implicit linebreak {br} after each dialog line
		- a "dialog line" is defined as any line that either:
			- 1) contains dialog text (any text outside of a code block)
			- 2) is entirely empty (no text, no code)
			- *or* 3) contains a list block (sequence, cycle, shuffle, or conditional)
		- lines *only* containing {code} blocks are not dialog lines

		NOTE TO SELF: all the state I'm storing in here feels like
		evidence that the parsing system kind of broke down at this point :(
		Maybe it would feel better if I move into the "state" object
	*/
	function ParseDialog(state) {
		var curLineNodeList = [];
		var curText = "";
		var curLineIsEmpty = true;
		var curLineContainsDialogText = false;
		var prevLineIsDialogLine = false;

		var curLineIsDialogLine = function() {
			return curLineContainsDialogText || curLineIsEmpty;
		}

		var resetLineStateForNewLine = function() {
			prevLineIsDialogLine = curLineIsDialogLine();
			curLineContainsDialogText = false;
			curLineIsEmpty = true;
			curText = "";
			curLineNodeList = [];
		}

		var tryAddTextNodeToList = function() {
			if (curText.length > 0) {
				var printNode = new FuncNode("print", [new LiteralNode(curText)]);
				curLineNodeList.push(printNode);

				curText = "";
				curLineIsEmpty = false;
				curLineContainsDialogText = true;
			}
		}

		var addCodeNodeToList = function() {
			var codeSource = state.ConsumeBlock(Sym.CodeOpen, Sym.CodeClose);
			var codeState = new ParserState(new CodeBlockNode(), codeSource);
			codeState = ParseCode(codeState);
			var codeBlockNode = codeState.rootNode;
			curLineNodeList.push(codeBlockNode);

			curLineIsEmpty = false;

			// lists count as dialog text, because they can contain it
			if (isMultilineListBlock(codeBlockNode)) {
				curLineContainsDialogText = true;
			}
		}

		var tryAddLinebreakNodeToList = function() {
			if (prevLineIsDialogLine) {
				var linebreakNode = new FuncNode("br", []);
				curLineNodeList.unshift(linebreakNode);
			}
		}

		var addLineNodesToParent = function() {
			for (var i = 0; i = requiredLeadingWhitespace) {
				var trimmedText = trimLeadingWhitespace(lineResults.text, requiredLeadingWhitespace);

				if (lineResults.isNewCondition) {
					conditionStrings[curIndex] += trimmedText;
				}
				else {
					resultStrings[curIndex] += trimmedText + Sym.Linebreak;
				}
			}
		}

		// hack: cut off the trailing newlines from all the result strings
		resultStrings = resultStrings.map(function(result) { return result.slice(0,-1); });

		var conditions = [];
		for (var i = 0; i < conditionStrings.length; i++) {
			var str = conditionStrings[i].trim();
			if (str === Sym.Else) {
				conditions.push(new ElseNode());
			}
			else {
				var exp = CreateExpression(str);
				conditions.push(exp);
			}
		}

		var results = [];
		for (var i = 0; i = requiredLeadingWhitespace) {
				var trimmedText = trimLeadingWhitespace(lineResults.text, requiredLeadingWhitespace);
				itemStrings[curItemIndex] += trimmedText + Sym.Linebreak;
			}
		}

		// a bit hacky: cut off the trailing newlines from all the items
		itemStrings = itemStrings.map(function(item) { return item.slice(0,-1); });

		var options = [];
		for (var i = 0; i  0) {
				OnSymbolEnd();
			}
			else {
				curSymbol += state.Char();
			}
			state.Step();
		}

		if(curSymbol.length > 0) {
			OnSymbolEnd();
		}

		state.curNode.AddChild( new FuncNode( funcName, args ) );

		return state;
	}

	function IsValidVariableName(str) {
		var reg = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
		var isValid = reg.test(str);
		// console.log("VALID variable??? " + isValid);
		return isValid;
	}

	function StringToValue(valStr) {
		if(valStr[0] === Sym.CodeOpen) {
			// CODE BLOCK!!!
			var codeStr = (new ParserState( null, valStr )).ConsumeBlock(Sym.CodeOpen, Sym.CodeClose); //hacky
			var codeBlockState = new ParserState(new CodeBlockNode(), codeStr);
			codeBlockState = ParseCode( codeBlockState );
			return codeBlockState.rootNode;
		}
		else if(valStr[0] === Sym.String) {
			// STRING!!
			// console.log("STRING");
			var str = "";
			var i = 1;
			while (i < valStr.length && valStr[i] != Sym.String) {
				str += valStr[i];
				i++;
			}
			// console.log(str);
			return new LiteralNode( str );
		}
		else if(valStr === "true") {
			// BOOL
			return new LiteralNode( true );
		}
		else if(valStr === "false") {
			// BOOL
			return new LiteralNode( false );
		}
		else if( !isNaN(parseFloat(valStr)) ) {
			// NUMBER!!
			// console.log("NUMBER!!! " + valStr);
			return new LiteralNode( parseFloat(valStr) );
		}
		else if(IsValidVariableName(valStr)) {
			// VARIABLE!!
			// console.log("VARIABLE");
			return new VarNode(valStr); // TODO : check for valid potential variables
		}
		else {
			// uh oh
			return new LiteralNode(null);
		}
	}

	function CreateExpression(expStr) {
		expStr = expStr.trim();

		function IsInsideString(index) {
			var inString = false;
			for(var i = 0; i < expStr.length; i++) {
				if(expStr[i] === Sym.String)
					inString = !inString;

				if(index === i)
					return inString;
			}
			return false;
		}

		function IsInsideCode(index) {
			var count = 0;
			for(var i = 0; i  0;
			}
			return false;
		}

		var operator = null;

		// set is special because other operator can look like it, and it has to go first in the order of operations
		var setIndex = expStr.indexOf(Sym.Set);
		if( setIndex > -1 && !IsInsideString(setIndex) && !IsInsideCode(setIndex) ) { // it might be a set operator
			if( expStr[setIndex+1] != "=" && expStr[setIndex-1] != ">" && expStr[setIndex-1] != "=, or  -1 && !IsInsideString(ifIndex) && !IsInsideCode(ifIndex) ) {
			operator = Sym.ConditionEnd;
			var conditionStr = expStr.substring(0,ifIndex).trim();
			var conditions = [ CreateExpression(conditionStr) ];

			var resultStr = expStr.substring(ifIndex+Sym.ConditionEnd.length);
			var results = [];
			function AddResult(str) {
				var dialogBlockState = new ParserState(new DialogBlockNode(), str);
				dialogBlockState = ParseDialog( dialogBlockState );
				var dialogBlock = dialogBlockState.rootNode;
				results.push( dialogBlock );
			}

			var elseIndex = resultStr.indexOf(Sym.ElseExp); // does this need to test for strings?
			if(elseIndex > -1) {
				conditions.push( new ElseNode() );

				var elseStr = resultStr.substring(elseIndex+Sym.ElseExp.length);
				var resultStr = resultStr.substring(0,elseIndex);

				AddResult( resultStr.trim() );
				AddResult( elseStr.trim() );
			}
			else {
				AddResult( resultStr.trim() );
			}

			return new IfNode( conditions, results, true /*isSingleLine*/ );
		}

		for( var i = 0; (operator == null) && (i  -1 && !IsInsideString(opIndex) && !IsInsideCode(opIndex) ) {
				operator = opSym;
				var left = CreateExpression( expStr.substring(0,opIndex) );
				var right = CreateExpression( expStr.substring(opIndex+opSym.length) );
				var exp = new ExpNode( operator, left, right );
				return exp;
			}
		}

		if( operator == null ) {
			return StringToValue(expStr);
		}
	}
	this.CreateExpression = CreateExpression;

	function IsWhitespace(str) {
		return ( str === " " || str === "\t" || str === "\n" );
	}

	function IsExpression(str) {
		var tempState = new ParserState(null, str); // hacky
		var textOutsideCodeBlocks = "";

		while (!tempState.Done()) {
			if (tempState.MatchAhead(Sym.CodeOpen)) {
				tempState.ConsumeBlock(Sym.CodeOpen, Sym.CodeClose);
			}
			else {
				textOutsideCodeBlocks += tempState.Char();
				tempState.Step();
			}
		}

		var containsAnyExpressionOperators = (textOutsideCodeBlocks.indexOf(Sym.ConditionEnd) != -1) ||
				(textOutsideCodeBlocks.indexOf(Sym.Set) != -1) ||
				(Sym.Operators.some(function(opSym) { return textOutsideCodeBlocks.indexOf(opSym) != -1; }));

		return containsAnyExpressionOperators;
	}

	function IsLiteral(str) {
		var isBool = str === "true" || str === "false";
		var isNum = !isNaN(parseFloat(str));
		var isStr = str[0] === '"' && str[str.length-1] === '"';
		var isVar = IsValidVariableName(str);
		var isEmpty = str.length === 0;
		return isBool || isNum || isStr || isVar || isEmpty;
	}

	function ParseExpression(state) {
		var line = state.Source(); // state.Peak( [Sym.Linebreak] ); // TODO : remove the linebreak thing
		// console.log("EXPRESSION " + line);
		var exp = CreateExpression(line);
		// console.log(exp);
		state.curNode.AddChild(exp);
		state.Step(line.length);
		return state;
	}

	function IsConditionalBlock(state) {
		var peakToFirstListSymbol = state.Peak([Sym.List]);

		var foundListSymbol = peakToFirstListSymbol < state.Source().length;

		var areAllCharsBeforeListWhitespace = true;
		for (var i = 0; i < peakToFirstListSymbol.length; i++) {
			if (!IsWhitespace(peakToFirstListSymbol[i])) {
				areAllCharsBeforeListWhitespace = false;
			}
		}

		var peakToFirstConditionSymbol = state.Peak([Sym.ConditionEnd]);
		peakToFirstConditionSymbol = peakToFirstConditionSymbol.slice(peakToFirstListSymbol.length);
		var hasNoLinebreakBetweenListAndConditionEnd = peakToFirstConditionSymbol.indexOf(Sym.Linebreak) == -1;

		return foundListSymbol && 
			areAllCharsBeforeListWhitespace && 
			hasNoLinebreakBetweenListAndConditionEnd;
	}

	function ParseCode(state) {
		if (IsConditionalBlock(state)) {
			state = ParseConditional(state);
		}
		else if (environment.HasFunction(state.Peak([" "]))) { // TODO --- what about newlines???
			var funcName = state.Peak([" "]);
			state.Step(funcName.length);
			state = ParseFunction(state, funcName);
		}
		else if (IsSequence(state.Peak([" ", Sym.Linebreak]))) {
			var sequenceType = state.Peak([" ", Sym.Linebreak]);
			state.Step(sequenceType.length);
			state = ParseSequence(state, sequenceType);
		}
		else if (IsLiteral(state.Source()) || IsExpression(state.Source())) {
			state = ParseExpression(state);
		}
		else {
			var undefinedSrc = state.Peak([]);
			var undefinedNode = new UndefinedNode(undefinedSrc);
			state.curNode.AddChild(undefinedNode);
		}

		// just go to the end now
		while (!state.Done()) {
			state.Step();
		}

		return state;
	}

	function ParseCodeBlock(state) {
		var codeStr = state.ConsumeBlock( Sym.CodeOpen, Sym.CodeClose );
		var codeState = new ParserState(new CodeBlockNode(), codeStr);
		codeState = ParseCode( codeState );
		state.curNode.AddChild( codeState.rootNode );
		return state;
	}

}

} // Script()



function Dialog() {

this.CreateRenderer = function() {
	return new DialogRenderer();
};

this.CreateBuffer = function() {
	return new DialogBuffer();
};

var DialogRenderer = function() {

	// TODO : refactor this eventually? remove everything from struct.. avoid the defaults?
	var textboxInfo = {
		img : null,
		width : 104,
		height : 8+4+2+5, //8 for text, 4 for top-bottom padding, 2 for line padding, 5 for arrow
		top : 12,
		left : 12,
		bottom : 12, //for drawing it from the bottom
		font_scale : 0.5, // we draw font at half-size compared to everything else
		padding_vert : 2,
		padding_horz : 4,
		arrow_height : 5,
	};

	var font = null;
	this.SetFont = function(f) {
		font = f;
		textboxInfo.height = (textboxInfo.padding_vert * 3) + (relativeFontHeight() * 2) + textboxInfo.arrow_height;
		textboxInfo.img = context.createImageData(textboxInfo.width*scale, textboxInfo.height*scale);
	}

	function textScale() {
		return scale * textboxInfo.font_scale;
	}

	function relativeFontWidth() {
		return Math.ceil( font.getWidth() * textboxInfo.font_scale );
	}

	function relativeFontHeight() {
		return Math.ceil( font.getHeight() * textboxInfo.font_scale );
	}

	var context = null;
	this.AttachContext = function(c) {
		context = c;
	};

	this.ClearTextbox = function() {
		if(context == null) return;

		//create new image none exists
		if(textboxInfo.img == null)
			textboxInfo.img = context.createImageData(textboxInfo.width*scale, textboxInfo.height*scale);

		// fill text box with black
		for (var i=0;i<textboxInfo.img.data.length;i+=4)
		{
			textboxInfo.img.data[i+0]=0;
			textboxInfo.img.data[i+1]=0;
			textboxInfo.img.data[i+2]=0;
			textboxInfo.img.data[i+3]=255;
		}
	};

	var isCentered = false;
	this.SetCentered = function(centered) {
		isCentered = centered;
	};

	this.DrawTextbox = function() {
		if(context == null) return;
		if (isCentered) {
			context.putImageData(textboxInfo.img, textboxInfo.left*scale, ((height/2)-(textboxInfo.height/2))*scale);
		}
		else if (player().y < mapsize/2) {
			//bottom
			context.putImageData(textboxInfo.img, textboxInfo.left*scale, (height-textboxInfo.bottom-textboxInfo.height)*scale);
		}
		else {
			//top
			context.putImageData(textboxInfo.img, textboxInfo.left*scale, textboxInfo.top*scale);
		}
	};

	var arrowdata = [
		1,1,1,1,1,
		0,1,1,1,0,
		0,0,1,0,0
	];
	this.DrawNextArrow = function() {
		// console.log("draw arrow!");
		var top = (textboxInfo.height-5) * scale;
		var left = (textboxInfo.width-(5+4)) * scale;
		if (textDirection === TextDirection.RightToLeft) { // RTL hack
			left = 4 * scale;
		}

		for (var y = 0; y < 3; y++) {
			for (var x = 0; x < 5; x++) {
				var i = (y * 5) + x;
				if (arrowdata[i] == 1) {
					//scaling nonsense
					for (var sy = 0; sy < scale; sy++) {
						for (var sx = 0; sx < scale; sx++) {
							var pxl = 4 * ( ((top+(y*scale)+sy) * (textboxInfo.width*scale)) + (left+(x*scale)+sx) );
							textboxInfo.img.data[pxl+0] = 255;
							textboxInfo.img.data[pxl+1] = 255;
							textboxInfo.img.data[pxl+2] = 255;
							textboxInfo.img.data[pxl+3] = 255;
						}
					}
				}
			}
		}
	};

	var text_scale = 2; //using a different scaling factor for text feels like cheating... but it looks better
	this.DrawChar = function(char, row, col, leftPos) {
		char.offset = {
			x: char.base_offset.x,
			y: char.base_offset.y
		}; // compute render offset *every* frame

		char.SetPosition(row,col);
		char.ApplyEffects(effectTime);

		var charData = char.bitmap;

		var top = (4 * scale) + (row * 2 * scale) + (row * font.getHeight() * text_scale) + Math.floor( char.offset.y );
		var left = (4 * scale) + (leftPos * text_scale) + Math.floor( char.offset.x );

		var debug_r = Math.random() * 255;

		for (var y = 0; y < char.height; y++) {
			for (var x = 0; x < char.width; x++) {

				var i = (y * char.width) + x;
				if ( charData[i] == 1 ) {

					//scaling nonsense
					for (var sy = 0; sy < text_scale; sy++) {
						for (var sx = 0; sx < text_scale; sx++) {
							var pxl = 4 * ( ((top+(y*text_scale)+sy) * (textboxInfo.width*scale)) + (left+(x*text_scale)+sx) );
							textboxInfo.img.data[pxl+0] = char.color.r;
							textboxInfo.img.data[pxl+1] = char.color.g;
							textboxInfo.img.data[pxl+2] = char.color.b;
							textboxInfo.img.data[pxl+3] = char.color.a;
						}
					}
				}
				// else {
				// 	// DEBUG

				// 	//scaling nonsense
				// 	for (var sy = 0; sy < text_scale; sy++) {
				// 		for (var sx = 0; sx < text_scale; sx++) {
				// 			var pxl = 4 * ( ((top+(y*text_scale)+sy) * (textboxInfo.width*scale)) + (left+(x*text_scale)+sx) );
				// 			textboxInfo.img.data[pxl+0] = debug_r;
				// 			textboxInfo.img.data[pxl+1] = 0;
				// 			textboxInfo.img.data[pxl+2] = 0;
				// 			textboxInfo.img.data[pxl+3] = 255;
				// 		}
				// 	}
				// }

			}
		}
		
		// call printHandler for character
		char.OnPrint();
	};

	var effectTime = 0; // TODO this variable should live somewhere better
	this.Draw = function(buffer, dt) {
		effectTime += dt;

		this.ClearTextbox();

		buffer.ForEachActiveChar(this.DrawChar);

		if (buffer.CanContinue()) {
			this.DrawNextArrow();
		}

		this.DrawTextbox();

		if (buffer.DidPageFinishThisFrame() && onPageFinish != null) {
			onPageFinish();
		}
	};

	/* this is a hook for GIF rendering */
	var onPageFinish = null;
	this.SetPageFinishHandler = function(handler) {
		onPageFinish = handler;
	};

	this.Reset = function() {
		effectTime = 0;
		// TODO - anything else?
	}

	// this.CharsPerRow = function() {
	// 	return textboxInfo.charsPerRow;
	// }
}


var DialogBuffer = function() {
	var buffer = [[[]]]; // holds dialog in an array buffer
	var pageIndex = 0;
	var rowIndex = 0;
	var charIndex = 0;
	var nextCharTimer = 0;
	var nextCharMaxTime = 50; // in milliseconds
	var isDialogReadyToContinue = false;
	var activeTextEffects = [];
	var font = null;
	var arabicHandler = new ArabicHandler();
	var onDialogEndCallbacks = [];

	this.SetFont = function(f) {
		font = f;
	}

	this.CurPage = function() { return buffer[ pageIndex ]; };
	this.CurRow = function() { return this.CurPage()[ rowIndex ]; };
	this.CurChar = function() { return this.CurRow()[ charIndex ]; };
	this.CurPageCount = function() { return buffer.length; };
	this.CurRowCount = function() { return this.CurPage().length; };
	this.CurCharCount = function() { return this.CurRow().length; };

	this.ForEachActiveChar = function(handler) { // Iterates over visible characters on the active page
		var rowCount = rowIndex + 1;
		for (var i = 0; i < rowCount; i++) {
			var row = this.CurPage()[i];
			var charCount = (i == rowIndex) ? charIndex+1 : row.length;
			// console.log(charCount);

			var leftPos = 0;
			if (textDirection === TextDirection.RightToLeft) {
				leftPos = 24 * 8; // hack -- I think this is correct?
			}

			for(var j = 0; j < charCount; j++) {
				var char = row[j];
				if(char) {
					if (textDirection === TextDirection.RightToLeft) {
						leftPos -= char.spacing;
					}
					// console.log(j + " " + leftPos);

					// handler( char, i /*rowIndex*/, j /*colIndex*/ );
					handler(char, i /*rowIndex*/, j /*colIndex*/, leftPos)

					if (textDirection === TextDirection.LeftToRight) {
						leftPos += char.spacing;
					}
				}
			}
		}
	}

	this.Reset = function() {
		buffer = [[[]]];
		pageIndex = 0;
		rowIndex = 0;
		charIndex = 0;
		isDialogReadyToContinue = false;

		afterManualPagebreak = false;

		activeTextEffects = [];

		onDialogEndCallbacks = [];

		isActive = false;
	};

	this.DoNextChar = function() {
		nextCharTimer = 0; //reset timer

		//time to update characters
		if (charIndex + 1 < this.CurCharCount()) {
			//add char to current row
			charIndex++;
		}
		else if (rowIndex + 1  nextCharMaxTime) {
			this.DoNextChar();
		}
	};

	this.Skip = function() {
		console.log("SKIPPP");
		didPageFinishThisFrame = false;
		didFlipPageThisFrame = false;
		// add new characters until you get to the end of the current line of dialog
		while (rowIndex < this.CurRowCount()) {
			this.DoNextChar();

			if(isDialogReadyToContinue) {
				//make sure to push the rowIndex past the end to break out of the loop
				rowIndex++;
				charIndex = 0;
			}
		}
		rowIndex = this.CurRowCount()-1;
		charIndex = this.CurCharCount()-1;
	};

	this.FlipPage = function() {
		didFlipPageThisFrame = true;
		isDialogReadyToContinue = false;
		pageIndex++;
		rowIndex = 0;
		charIndex = 0;
	}

	this.EndDialog = function() {
		isActive = false; // no more text to show... this should be a sign to stop rendering dialog

		for (var i = 0; i < onDialogEndCallbacks.length; i++) {
			onDialogEndCallbacks[i]();
		}
	}

	var afterManualPagebreak = false; // is it bad to track this state like this?

	this.Continue = function() {
		console.log("CONTINUE");

		// if we used a page break character to continue we need
		// to run whatever is in the script afterwards! // TODO : make this comment better
		if (this.CurChar().isPageBreak) {
			// hacky: always treat a page break as the end of dialog
			// if there's more dialog later we re-activate the dialog buffer
			this.EndDialog();
			afterManualPagebreak = true;
			this.CurChar().OnContinue();
			return false;
		}
		if (pageIndex + 1 < this.CurPageCount()) {
			console.log("FLIP PAGE!");
			//start next page
			this.FlipPage();
			return true; /* hasMoreDialog */
		}
		else {
			console.log("END DIALOG!");
			//end dialog mode
			this.EndDialog();
			return false; /* hasMoreDialog */
		}
	};

	var isActive = false;
	this.IsActive = function() { return isActive; };

	this.OnDialogEnd = function(callback) {
		if (!isActive) {
			callback();
		}
		else {
			onDialogEndCallbacks.push(callback);
		}
	}

	this.CanContinue = function() { return isDialogReadyToContinue; };

	function DialogChar(effectList) {
		this.effectList = effectList.slice(); // clone effect list (since it can change between chars)

		this.color = { r:255, g:255, b:255, a:255 };
		this.offset = { x:0, y:0 }; // in pixels (screen pixels?)

		this.col = 0;
		this.row = 0;

		this.SetPosition = function(row,col) {
			// console.log("SET POS");
			// console.log(this);
			this.row = row;
			this.col = col;
		}

		this.ApplyEffects = function(time) {
			// console.log("APPLY EFFECTS! " + time);
			for(var i = 0; i < this.effectList.length; i++) {
				var effectName = this.effectList[i];
				// console.log("FX " + effectName);
				TextEffects[ effectName ].DoEffect( this, time );
			}
		}

		var printHandler = null; // optional function to be called once on printing character
		this.SetPrintHandler = function(handler) {
			printHandler = handler;
		}
		this.OnPrint = function() {
			if (printHandler != null) {
				// console.log("PRINT HANDLER ---- DIALOG BUFFER");
				printHandler();
				printHandler = null; // only call handler once (hacky)
			}
		}

		this.bitmap = [];
		this.width = 0;
		this.height = 0;
		this.base_offset = { // hacky name
 			x: 0,
			y: 0
		};
		this.spacing = 0;
	}

	function DialogFontChar(font, char, effectList) {
		Object.assign(this, new DialogChar(effectList));

		var charData = font.getChar(char);
		this.bitmap = charData.data;
		this.width = charData.width;
		this.height = charData.height;
		this.base_offset.x = charData.offset.x;
		this.base_offset.y = charData.offset.y;
		this.spacing = charData.spacing;
	}

	function DialogDrawingChar(drawingId, effectList) {
		Object.assign(this, new DialogChar(effectList));

		var imageData = renderer.GetImageSource(drawingId)[0];
		var imageDataFlat = [];
		for (var i = 0; i < imageData.length; i++) {
			// console.log(imageData[i]);
			imageDataFlat = imageDataFlat.concat(imageData[i]);
		}

		this.bitmap = imageDataFlat;
		this.width = 8;
		this.height = 8;
		this.spacing = 8;
	}

	function DialogScriptControlChar() {
		Object.assign(this, new DialogChar([]));

		this.width = 0;
		this.height = 0;
		this.spacing = 0;
	}

	// is a control character really the best way to handle page breaks?
	function DialogPageBreakChar() {
		Object.assign(this, new DialogChar([]));

		this.width = 0;
		this.height = 0;
		this.spacing = 0;

		this.isPageBreak = true;

		var continueHandler = null;

		this.SetContinueHandler = function(handler) {
			continueHandler = handler;
		}

		this.OnContinue = function() {
			if (continueHandler) {
				continueHandler();
			}
		}
	}

	function AddWordToCharArray(charArray,word,effectList) {
		for(var i = 0; i < word.length; i++) {
			charArray.push( new DialogFontChar( font, word[i], effectList ) );
		}
		return charArray;
	}

	function GetCharArrayWidth(charArray) {
		var width = 0;
		for(var i = 0; i < charArray.length; i++) {
			width += charArray[i].spacing;
		}
		return width;
	}

	function GetStringWidth(str) {
		var width = 0;
		for (var i = 0; i < str.length; i++) {
			var charData = font.getChar(str[i]);
			width += charData.spacing;
		}
		return width;
	}

	var pixelsPerRow = 192; // hard-coded fun times!!!

	this.AddScriptReturn = function(onReturnHandler) {
		var curPageIndex = buffer.length - 1;
		var curRowIndex = buffer[curPageIndex].length - 1;
		var curRowArr = buffer[curPageIndex][curRowIndex];

		var controlChar = new DialogScriptControlChar();
		controlChar.SetPrintHandler(onReturnHandler);

		curRowArr.push(controlChar);

		isActive = true;
	}

	this.AddDrawing = function(drawingId) {
		// console.log("DRAWING ID " + drawingId);

		var curPageIndex = buffer.length - 1;
		var curRowIndex = buffer[curPageIndex].length - 1;
		var curRowArr = buffer[curPageIndex][curRowIndex];

		var drawingChar = new DialogDrawingChar(drawingId, activeTextEffects);

		var rowLength = GetCharArrayWidth(curRowArr);

		// TODO : clean up copy-pasted code here :/
		if (afterManualPagebreak) {
			this.FlipPage(); // hacky

			buffer[curPageIndex][curRowIndex] = curRowArr;
			buffer.push([]);
			curPageIndex++;
			buffer[curPageIndex].push([]);
			curRowIndex = 0;
			curRowArr = buffer[curPageIndex][curRowIndex];
			curRowArr.push(drawingChar);

			afterManualPagebreak = false;
		}
		else if (rowLength + drawingChar.spacing  <= pixelsPerRow || rowLength <= 0) {
			//stay on same row
			curRowArr.push(drawingChar);
		}
		else if (curRowIndex == 0) {
			//start next row
			buffer[curPageIndex][curRowIndex] = curRowArr;
			buffer[curPageIndex].push([]);
			curRowIndex++;
			curRowArr = buffer[curPageIndex][curRowIndex];
			curRowArr.push(drawingChar);
		}
		else {
			//start next page
			buffer[curPageIndex][curRowIndex] = curRowArr;
			buffer.push([]);
			curPageIndex++;
			buffer[curPageIndex].push([]);
			curRowIndex = 0;
			curRowArr = buffer[curPageIndex][curRowIndex];
			curRowArr.push(drawingChar);
		}

		isActive = true; // this feels like a bad way to do this???
	}

	// TODO : convert this into something that takes DialogChar arrays
	this.AddText = function(textStr) {
		console.log("ADD TEXT " + textStr);

		//process dialog so it's easier to display
		var words = textStr.split(" ");

		// var curPageIndex = this.CurPageCount() - 1;
		// var curRowIndex = this.CurRowCount() - 1;
		// var curRowArr = this.CurRow();

		var curPageIndex = buffer.length - 1;
		var curRowIndex = buffer[curPageIndex].length - 1;
		var curRowArr = buffer[curPageIndex][curRowIndex];

		for (var i = 0; i < words.length; i++) {
			var word = words[i];
			if (arabicHandler.ContainsArabicCharacters(word)) {
				word = arabicHandler.ShapeArabicCharacters(word);
			}

			var wordWithPrecedingSpace = ((i == 0) ? "" : " ") + word;
			var wordLength = GetStringWidth(wordWithPrecedingSpace);

			var rowLength = GetCharArrayWidth(curRowArr);

			if (afterManualPagebreak) {
				this.FlipPage();

				// hacky copied bit for page breaks
				buffer[curPageIndex][curRowIndex] = curRowArr;
				buffer.push([]);
				curPageIndex++;
				buffer[curPageIndex].push([]);
				curRowIndex = 0;
				curRowArr = buffer[curPageIndex][curRowIndex];
				curRowArr = AddWordToCharArray(curRowArr, word, activeTextEffects);

				afterManualPagebreak = false;
			}
			else if (rowLength + wordLength <= pixelsPerRow || rowLength  0) {
			var lastChar = lastRow[lastRow.length-1];
		}

		// console.log(buffer);

		isActive = true;
	};

	this.AddLinebreak = function() {
		var lastPage = buffer[buffer.length-1];
		if (lastPage.length  -1;
	}
	this.AddTextEffect = function(name) {
		activeTextEffects.push( name );
	}
	this.RemoveTextEffect = function(name) {
		activeTextEffects.splice( activeTextEffects.indexOf( name ), 1 );
	}

	/* this is a hook for GIF rendering */
	var didPageFinishThisFrame = false;
	this.DidPageFinishThisFrame = function(){ return didPageFinishThisFrame; };

	var didFlipPageThisFrame = false;
	this.DidFlipPageThisFrame = function(){ return didFlipPageThisFrame; };

	// this.SetCharsPerRow = function(num){ charsPerRow = num; }; // hacky
};

/* ARABIC */
var ArabicHandler = function() {

	var arabicCharStart = 0x0621;
	var arabicCharEnd = 0x064E;

	var CharacterForm = {
		Isolated : 0,
		Final : 1,
		Initial : 2,
		Middle : 3
	};

	// map glyphs to their character forms
	var glyphForms = {
		/*		 Isolated, Final, Initial, Middle Forms	*/
		0x0621: [0xFE80,0xFE80,0xFE80,0xFE80], /*  HAMZA  */ 
		0x0622: [0xFE81,0xFE82,0xFE81,0xFE82], /*  ALEF WITH MADDA ABOVE  */ 
		0x0623: [0xFE83,0xFE84,0xFE83,0xFE84], /*  ALEF WITH HAMZA ABOVE  */ 
		0x0624: [0xFE85,0xFE86,0xFE85,0xFE86], /*  WAW WITH HAMZA ABOVE  */ 
		0x0625: [0xFE87,0xFE88,0xFE87,0xFE88], /*  ALEF WITH HAMZA BELOW  */ 
		0x0626: [0xFE89,0xFE8A,0xFE8B,0xFE8C], /*  YEH WITH HAMZA ABOVE  */ 
		0x0627: [0xFE8D,0xFE8E,0xFE8D,0xFE8E], /*  ALEF  */ 
		0x0628: [0xFE8F,0xFE90,0xFE91,0xFE92], /*  BEH  */ 
		0x0629: [0xFE93,0xFE94,0xFE93,0xFE94], /*  TEH MARBUTA  */ 
		0x062A: [0xFE95,0xFE96,0xFE97,0xFE98], /*  TEH  */ 
		0x062B: [0xFE99,0xFE9A,0xFE9B,0xFE9C], /*  THEH  */ 
		0x062C: [0xFE9D,0xFE9E,0xFE9F,0xFEA0], /*  JEEM  */ 
		0x062D: [0xFEA1,0xFEA2,0xFEA3,0xFEA4], /*  HAH  */ 
		0x062E: [0xFEA5,0xFEA6,0xFEA7,0xFEA8], /*  KHAH  */ 
		0x062F: [0xFEA9,0xFEAA,0xFEA9,0xFEAA], /*  DAL  */ 
		0x0630: [0xFEAB,0xFEAC,0xFEAB,0xFEAC], /*  THAL */ 
		0x0631: [0xFEAD,0xFEAE,0xFEAD,0xFEAE], /*  RAA  */ 
		0x0632: [0xFEAF,0xFEB0,0xFEAF,0xFEB0], /*  ZAIN  */ 
		0x0633: [0xFEB1,0xFEB2,0xFEB3,0xFEB4], /*  SEEN  */ 
		0x0634: [0xFEB5,0xFEB6,0xFEB7,0xFEB8], /*  SHEEN  */ 
		0x0635: [0xFEB9,0xFEBA,0xFEBB,0xFEBC], /*  SAD  */ 
		0x0636: [0xFEBD,0xFEBE,0xFEBF,0xFEC0], /*  DAD  */ 
		0x0637: [0xFEC1,0xFEC2,0xFEC3,0xFEC4], /*  TAH  */ 
		0x0638: [0xFEC5,0xFEC6,0xFEC7,0xFEC8], /*  ZAH  */ 
		0x0639: [0xFEC9,0xFECA,0xFECB,0xFECC], /*  AIN  */ 
		0x063A: [0xFECD,0xFECE,0xFECF,0xFED0], /*  GHAIN  */ 
		0x063B: [0x0000,0x0000,0x0000,0x0000], /*  space */
		0x063C: [0x0000,0x0000,0x0000,0x0000], /*  space */
		0x063D: [0x0000,0x0000,0x0000,0x0000], /*  space */
		0x063E: [0x0000,0x0000,0x0000,0x0000], /*  space */
		0x063F: [0x0000,0x0000,0x0000,0x0000], /*  space */
		0x0640: [0x0640,0x0640,0x0640,0x0640], /*  TATWEEL  */ 
		0x0641: [0xFED1,0xFED2,0xFED3,0xFED4], /*  FAA  */ 
		0x0642: [0xFED5,0xFED6,0xFED7,0xFED8], /*  QAF  */ 
		0x0643: [0xFED9,0xFEDA,0xFEDB,0xFEDC], /*  KAF  */ 
		0x0644: [0xFEDD,0xFEDE,0xFEDF,0xFEE0], /*  LAM  */ 
		0x0645: [0xFEE1,0xFEE2,0xFEE3,0xFEE4], /*  MEEM  */ 
		0x0646: [0xFEE5,0xFEE6,0xFEE7,0xFEE8], /*  NOON  */ 
		0x0647: [0xFEE9,0xFEEA,0xFEEB,0xFEEC], /*  HEH  */ 
		0x0648: [0xFEED,0xFEEE,0xFEED,0xFEEE], /*  WAW  */ 
		0x0649: [0xFEEF,0xFEF0,0xFBE8,0xFBE9], /*  ALEF MAKSURA  */ 
		0x064A: [0xFEF1,0xFEF2,0xFEF3,0xFEF4], /*  YEH  */ 
		0x064B: [0xFEF5,0xFEF6,0xFEF5,0xFEF6], /*  LAM ALEF MADD*/
		0x064C: [0xFEF7,0xFEF8,0xFEF7,0xFEF8], /*  LAM ALEF HAMZA ABOVE*/
		0x064D: [0xFEF9,0xFEFa,0xFEF9,0xFEFa], /*  LAM ALEF HAMZA BELOW*/
		0x064E: [0xFEFb,0xFEFc,0xFEFb,0xFEFc], /*  LAM ALEF */
	};

	var disconnectedCharacters = [0x0621,0x0622,0x0623,0x0624,0x0625,0x0627,0x062f,0x0630,0x0631,0x0632,0x0648,0x0649,0x064b,0x064c,0x064d,0x064e];

	function IsArabicCharacter(char) {
		var code = char.charCodeAt(0);
		return (code >= arabicCharStart && code <= arabicCharEnd);
	}

	function ContainsArabicCharacters(word) {
		for (var i = 0; i < word.length; i++) {
			if (IsArabicCharacter(word[i])) {
				return true;
			}
		}
		return false;
	}

	function IsDisconnectedCharacter(char) {
		var code = char.charCodeAt(0);
		return disconnectedCharacters.indexOf(code) != -1;
	}

	function ShapeArabicCharacters(word) {
		var shapedWord = "";

		for (var i = 0; i = 0 && IsArabicCharacter(word[i-1]) && !IsDisconnectedCharacter(word[i-1]);

			var connectedToNextChar = i+1 < word.length && IsArabicCharacter(word[i+1]) && !IsDisconnectedCharacter(word[i]);

			var form;
			if (!connectedToPreviousChar && !connectedToNextChar) {
				form = CharacterForm.Isolated;
			}
			else if (connectedToPreviousChar && !connectedToNextChar) {
				form = CharacterForm.Final;
			}
			else if (!connectedToPreviousChar && connectedToNextChar) {
				form = CharacterForm.Initial;
			}
			else if (connectedToPreviousChar && connectedToNextChar) {
				form = CharacterForm.Middle;
			}

			var code = word[i].charCodeAt(0);

			// handle lam alef special case
			if (code == 0x0644 && connectedToNextChar) {
				var nextCode = word[i+1].charCodeAt(0);
				var specialCode = null;
				if (nextCode == 0x0622) {
					// alef madd
					specialCode = glyphForms[0x064b][form];
				}
				else if (nextCode == 0x0623) {
					// hamza above
					specialCode = glyphForms[0x064c][form];
				}
				else if (nextCode == 0x0625) {
					// hamza below
					specialCode = glyphForms[0x064d][form];
				}
				else if (nextCode == 0x0627) {
					// alef
					specialCode = glyphForms[0x064e][form];
				}

				if (specialCode != null) {
					shapedWord += String.fromCharCode(specialCode);
					i++; // skip a step
					continue;
				}
			}

			// hacky?
			if (form === CharacterForm.Isolated) {
				shapedWord += word[i];
				continue;
			}

			var shapedCode = glyphForms[code][form];
			shapedWord += String.fromCharCode(shapedCode);
		}

		return shapedWord;
	}

	this.ContainsArabicCharacters = ContainsArabicCharacters;
	this.ShapeArabicCharacters = ShapeArabicCharacters;
}

/* NEW TEXT EFFECTS */
var TextEffects = new Map();

var RainbowEffect = function() { // TODO - should it be an object or just a method?
	this.DoEffect = function(char,time) {
		// console.log("RAINBOW!!!");
		// console.log(char);
		// console.log(char.color);
		// console.log(char.col);

		var h = Math.abs( Math.sin( (time / 600) - (char.col / 8) ) );
		var rgb = hslToRgb( h, 1, 0.5 );
		char.color.r = rgb[0];
		char.color.g = rgb[1];
		char.color.b = rgb[2];
		char.color.a = 255;
	}
};
TextEffects["rbw"] = new RainbowEffect();

var ColorEffect = function(index) {
	this.DoEffect = function(char) {
		var pal = getPal( curPal() );
		var color = pal[ parseInt( index ) ];
		// console.log(color);
		char.color.r = color[0];
		char.color.g = color[1];
		char.color.b = color[2];
		char.color.a = 255;
	}
};
TextEffects["clr1"] = new ColorEffect(0);
TextEffects["clr2"] = new ColorEffect(1); // TODO : should I use parameters instead of special names?
TextEffects["clr3"] = new ColorEffect(2);

var WavyEffect = function() {
	this.DoEffect = function(char,time) {
		char.offset.y += Math.sin( (time / 250) - (char.col / 2) ) * 4;
	}
};
TextEffects["wvy"] = new WavyEffect();

var ShakyEffect = function() {
	function disturb(func,time,offset,mult1,mult2) {
		return func( (time * mult1) - (offset * mult2) );
	}

	this.DoEffect = function(char,time) {
		char.offset.y += 3
						* disturb(Math.sin,time,char.col,0.1,0.5)
						* disturb(Math.cos,time,char.col,0.3,0.2)
						* disturb(Math.sin,time,char.row,2.0,1.0);
		char.offset.x += 3
						* disturb(Math.cos,time,char.row,0.1,1.0)
						* disturb(Math.sin,time,char.col,3.0,0.7)
						* disturb(Math.cos,time,char.col,0.2,0.3);
	}
};
TextEffects["shk"] = new ShakyEffect();

var DebugHighlightEffect = function() {
	this.DoEffect = function(char) {
		char.color.r = 255;
		char.color.g = 255;
		char.color.b = 0;
		char.color.a = 255;
	}
}
TextEffects["_debug_highlight"] = new DebugHighlightEffect();

} // Dialog()



/*
TODO
- reset renderer function
- react to changes in: drawings, palettes
- possible future plan: limit size of cache (remove old images)
- change image store path from (pal > col > draw) to (draw > pal > col)
- get rid of old getSpriteImage (etc) methods
- get editor working again [in progress]
- move debug timer class into core (seems useful)
*/

function Renderer(tilesize, scale) {

console.log("!!!!! NEW RENDERER");

var imageStore = { // TODO : rename to imageCache
	source: {},
	render: {}
};

var palettes = null; // TODO : need null checks?
var context = null;

function setPalettes(paletteObj) {
	palettes = paletteObj;

	// TODO : should this really clear out the render cache?
	imageStore.render = {};
}

function getPaletteColor(paletteId, colorIndex) {
	if (palettes[paletteId] === undefined) {
		paletteId = "default";
	}

	var palette = palettes[paletteId];

	if (colorIndex > palette.colors.length) { // do I need this failure case? (seems un-reliable)
		colorIndex = 0;
	}

	var color = palette.colors[colorIndex];

	return {
		r : color[0],
		g : color[1],
		b : color[2]
	};
}

var debugRenderCount = 0;

// TODO : change image store path from (pal > col > draw) to (draw > pal > col)
function renderImage(drawing, paletteId) {
	// debugRenderCount++;
	// console.log("RENDER COUNT " + debugRenderCount);

	var col = drawing.col;
	var colStr = "" + col;
	var pal = paletteId;
	var drwId = drawing.drw;
	var imgSrc = imageStore.source[ drawing.drw ];

	// initialize render cache entry
	if (imageStore.render[drwId] === undefined || imageStore.render[drwId] === null) {
		imageStore.render[drwId] = {};
	}

	if (imageStore.render[drwId][pal] === undefined || imageStore.render[drwId][pal] === null) {
		imageStore.render[drwId][pal] = {};
	}

	// create array of ImageData frames
	imageStore.render[drwId][pal][colStr] = [];

	for (var i = 0; i < imgSrc.length; i++) {
		var frameSrc = imgSrc[i];
		var frameData = imageDataFromImageSource( frameSrc, pal, col );
		imageStore.render[drwId][pal][colStr].push(frameData);
	}
}

function imageDataFromImageSource(imageSource, pal, col) {
	//console.log(imageSource);

	var img = context.createImageData(tilesize*scale,tilesize*scale);

	var backgroundColor = getPaletteColor(pal,0);
	var foregroundColor = getPaletteColor(pal,col);

	for (var y = 0; y < tilesize; y++) {
		for (var x = 0; x < tilesize; x++) {
			var px = imageSource[y][x];
			for (var sy = 0; sy < scale; sy++) {
				for (var sx = 0; sx < scale; sx++) {
					var pxl = (((y * scale) + sy) * tilesize * scale * 4) + (((x*scale) + sx) * 4);
					if ( px === 1 ) {
						img.data[pxl + 0] = foregroundColor.r;
						img.data[pxl + 1] = foregroundColor.g;
						img.data[pxl + 2] = foregroundColor.b;
						img.data[pxl + 3] = 255;
					}
					else { //ch === 0
						img.data[pxl + 0] = backgroundColor.r;
						img.data[pxl + 1] = backgroundColor.g;
						img.data[pxl + 2] = backgroundColor.b;
						img.data[pxl + 3] = 255;
					}
				}
			}
		}
	}

	// convert to canvas: chrome has poor performance when working directly with image data
	var imageCanvas = document.createElement("canvas");
	imageCanvas.width = img.width;
	imageCanvas.height = img.height;
	var imageContext = imageCanvas.getContext("2d");
	imageContext.putImageData(img,0,0);

	return imageCanvas;
}

// TODO : move into core
function undefinedOrNull(x) {
	return x === undefined || x === null;
}

function isImageRendered(drawing, paletteId) {
	var col = drawing.col;
	var colStr = "" + col;
	var pal = paletteId;
	var drwId = drawing.drw;

	if (undefinedOrNull(imageStore.render[drwId]) ||
		undefinedOrNull(imageStore.render[drwId][pal]) ||
		undefinedOrNull(imageStore.render[drwId][pal][colStr])) {
			return false;
	}
	else {
		return true;
	}
}

function getImageSet(drawing, paletteId) {
	return imageStore.render[drawing.drw][paletteId][drawing.col];
}

function getImageFrame(drawing, paletteId, frameOverride) {
	var frameIndex = 0;
	if (drawing.animation.isAnimated) {
		if (frameOverride != undefined && frameOverride != null) {
			frameIndex = frameOverride;
		}
		else {
			frameIndex = drawing.animation.frameIndex;
		}
	}

	return getImageSet(drawing, paletteId)[frameIndex];
}

function getOrRenderImage(drawing, paletteId, frameOverride) {
	if (!isImageRendered(drawing, paletteId)) {
		renderImage(drawing, paletteId);
	}

	return getImageFrame(drawing, paletteId, frameOverride);
}

/* PUBLIC INTERFACE */
this.GetImage = getOrRenderImage;

this.SetPalettes = setPalettes;

this.SetImageSource = function(drawingId, imageSourceData) {
	imageStore.source[drawingId] = imageSourceData;
	imageStore.render[drawingId] = {}; // reset render cache for this image
}

this.GetImageSource = function(drawingId) {
	return imageStore.source[drawingId];
}

this.GetFrameCount = function(drawingId) {
	return imageStore.source[drawingId].length;
}

this.AttachContext = function(ctx) {
	context = ctx;
}

} // Renderer()



var xhr; // TODO : remove
var canvas;
var context; // TODO : remove if safe?
var ctx;

var room = {};
var tile = {};
var sprite = {};
var item = {};
var dialog = {};
var palette = { //start off with a default palette
		"default" : {
			name : "default",
			colors : [[0,0,0],[255,255,255],[255,255,255]]
		}
	};
var variable = {}; // these are starting variable values -- they don't update (or I don't think they will)
var playerId = "A";

var titleDialogId = "title";
function getTitle() {
	return dialog[titleDialogId].src;
}
function setTitle(titleSrc) {
	dialog[titleDialogId] = { src:titleSrc, name:null };
}

var defaultFontName = "ascii_small";
var fontName = defaultFontName;
var TextDirection = {
	LeftToRight : "LTR",
	RightToLeft : "RTL"
};
var textDirection = TextDirection.LeftToRight;

var names = {
	room : new Map(),
	tile : new Map(), // Note: Not currently enabled in the UI
	sprite : new Map(),
	item : new Map(),
	dialog : new Map(),
};
function updateNamesFromCurData() {

	function createNameMap(objectStore) {
		var map = new Map();
		for (id in objectStore) {
			if (objectStore[id].name != undefined && objectStore[id].name != null) {
				map.set(objectStore[id].name, id);
			}
		}
		return map;
	}

	names.room = createNameMap(room);
	names.tile = createNameMap(tile);
	names.sprite = createNameMap(sprite);
	names.item = createNameMap(item);
	names.dialog = createNameMap(dialog);
}

var spriteStartLocations = {};

/* VERSION */
var version = {
	major: 7, // major changes
	minor: 2, // smaller changes
	devBuildPhase: "RELEASE",
};
function getEngineVersion() {
	return version.major + "." + version.minor;
}

/* FLAGS */
var flags;
function resetFlags() {
	flags = {
		ROOM_FORMAT : 0 // 0 = non-comma separated, 1 = comma separated
	};
}
resetFlags(); //init flags on load script

// SUPER hacky location... :/
var editorDevFlags = {
	// NONE right now!
};

function clearGameData() {
	room = {};
	tile = {};
	sprite = {};
	item = {};
	dialog = {};
	palette = { //start off with a default palette
		"default" : {
			name : "default",
			colors : [[0,0,0],[255,255,255],[255,255,255]]
		}
	};
	isEnding = false; //todo - correct place for this?
	variable = {};

	// TODO RENDERER : clear data?

	spriteStartLocations = {};

	// hacky to have this multiple times...
	names = {
		room : new Map(),
		tile : new Map(),
		sprite : new Map(),
		item : new Map(),
		dialog : new Map(),
	};

	fontName = defaultFontName; // TODO : reset font manager too?
	textDirection = TextDirection.LeftToRight;
}

var width = 128;
var height = 128;
var scale = 4; //this is stupid but necessary
var tilesize = 8;
var mapsize = 16;

var curRoom = "0";

var key = {
	left : 37,
	right : 39,
	up : 38,
	down : 40,
	space : 32,
	enter : 13,
	w : 87,
	a : 65,
	s : 83,
	d : 68,
	r : 82,
	shift : 16,
	ctrl : 17,
	alt : 18,
	cmd : 224
};

var prevTime = 0;
var deltaTime = 0;

//inventory update UI handles
var onInventoryChanged = null;
var onVariableChanged = null;
var onGameReset = null;

var isPlayerEmbeddedInEditor = false;

var renderer = new Renderer(tilesize, scale);

function getGameNameFromURL() {
	var game = window.location.hash.substring(1);
	// console.log("game name --- " + game);
	return game;
}

function attachCanvas(c) {
	canvas = c;
	canvas.width = width * scale;
	canvas.height = width * scale;
	ctx = canvas.getContext("2d");
	dialogRenderer.AttachContext(ctx);
	renderer.AttachContext(ctx);
}

var curGameData = null;
function load_game(game_data, startWithTitle) {
	curGameData = game_data; //remember the current game (used to reset the game)

	dialogBuffer.Reset();
	scriptInterpreter.ResetEnvironment(); // ensures variables are reset -- is this the best way?

	parseWorld(game_data);

	if (!isPlayerEmbeddedInEditor) {
		// hack to ensure default font is available
		fontManager.AddResource(defaultFontName + fontManager.GetExtension(), document.getElementById(defaultFontName).text.slice(1));
	}

	var font = fontManager.Get( fontName );
	dialogBuffer.SetFont(font);
	dialogRenderer.SetFont(font);

	setInitialVariables();

	// setInterval(updateLoadingScreen, 300); // hack test

	onready(startWithTitle);
}

function reset_cur_game() {
	if (curGameData == null) {
		return; //can't reset if we don't have the game data
	}

	stopGame();
	clearGameData();
	load_game(curGameData);

	if (isPlayerEmbeddedInEditor && onGameReset != null) {
		onGameReset();
	}
}

var update_interval = null;
function onready(startWithTitle) {
	if(startWithTitle === undefined || startWithTitle === null) startWithTitle = true;

	clearInterval(loading_interval);

	input = new InputManager();

	document.addEventListener('keydown', input.onkeydown);
	document.addEventListener('keyup', input.onkeyup);

	if (isPlayerEmbeddedInEditor) {
		canvas.addEventListener('touchstart', input.ontouchstart, {passive:false});
		canvas.addEventListener('touchmove', input.ontouchmove, {passive:false});
		canvas.addEventListener('touchend', input.ontouchend, {passive:false});
	}
	else {
		// creates a 'touchTrigger' element that covers the entire screen and can universally have touch event listeners added w/o issue.

		// we're checking for existing touchTriggers both at game start and end, so it's slightly redundant.
	  	var existingTouchTrigger = document.querySelector('#touchTrigger');
	  	if (existingTouchTrigger === null){
	  	  var touchTrigger = document.createElement("div");
	  	  touchTrigger.setAttribute("id","touchTrigger");

	  	  // afaik css in js is necessary here to force a fullscreen element
	  	  touchTrigger.setAttribute(
	  	    "style","position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; overflow: hidden;"
	  	  );
	  	  document.body.appendChild(touchTrigger);

	  	  touchTrigger.addEventListener('touchstart', input.ontouchstart);
	  	  touchTrigger.addEventListener('touchmove', input.ontouchmove);
	  	  touchTrigger.addEventListener('touchend', input.ontouchend);
	  	}
	}

	window.onblur = input.onblur;

	update_interval = setInterval(update,16);

	if(startWithTitle) { // used by editor 
		startNarrating(getTitle());
	}
}

function setInitialVariables() {
	for(id in variable) {
		var value = variable[id]; // default to string
		if(value === "true") {
			value = true;
		}
		else if(value === "false") {
			value = false;
		}
		else if(!isNaN(parseFloat(value))) {
			value = parseFloat(value);
		}
		scriptInterpreter.SetVariable(id,value);
	}
	scriptInterpreter.SetOnVariableChangeHandler( onVariableChanged );
}

function getOffset(evt) {
	var offset = { x:0, y:0 };

	var el = evt.target;
	var rect = el.getBoundingClientRect();

	offset.x += rect.left + el.scrollLeft;
	offset.y += rect.top + el.scrollTop;

	offset.x = evt.clientX - offset.x;
	offset.y = evt.clientY - offset.y;

	return offset;
}

function stopGame() {
	console.log("stop GAME!");

	document.removeEventListener('keydown', input.onkeydown);
	document.removeEventListener('keyup', input.onkeyup);

	if (isPlayerEmbeddedInEditor) {
		canvas.removeEventListener('touchstart', input.ontouchstart);
		canvas.removeEventListener('touchmove', input.ontouchmove);
		canvas.removeEventListener('touchend', input.ontouchend);
	}
	else {
		//check for touchTrigger and removes it

    		var existingTouchTrigger = document.querySelector('#touchTrigger');
    		if (existingTouchTrigger !== null){
    			existingTouchTrigger.removeEventListener('touchstart', input.ontouchstart);
    			existingTouchTrigger.removeEventListener('touchmove', input.ontouchmove);
    			existingTouchTrigger.removeEventListener('touchend', input.ontouchend);

    			existingTouchTrigger.parentElement.removeChild(existingTouchTrigger);
    		}
	}

	window.onblur = null;

	clearInterval(update_interval);
}

/* loading animation */
var loading_anim_data = [
	[
		0,1,1,1,1,1,1,0,
		0,0,1,1,1,1,0,0,
		0,0,1,1,1,1,0,0,
		0,0,0,1,1,0,0,0,
		0,0,0,1,1,0,0,0,
		0,0,1,0,0,1,0,0,
		0,0,1,0,0,1,0,0,
		0,1,1,1,1,1,1,0,
	],
	[
		0,1,1,1,1,1,1,0,
		0,0,1,0,0,1,0,0,
		0,0,1,1,1,1,0,0,
		0,0,0,1,1,0,0,0,
		0,0,0,1,1,0,0,0,
		0,0,1,0,0,1,0,0,
		0,0,1,1,1,1,0,0,
		0,1,1,1,1,1,1,0,
	],
	[
		0,1,1,1,1,1,1,0,
		0,0,1,0,0,1,0,0,
		0,0,1,0,0,1,0,0,
		0,0,0,1,1,0,0,0,
		0,0,0,1,1,0,0,0,
		0,0,1,1,1,1,0,0,
		0,0,1,1,1,1,0,0,
		0,1,1,1,1,1,1,0,
	],
	[
		0,1,1,1,1,1,1,0,
		0,0,1,0,0,1,0,0,
		0,0,1,0,0,1,0,0,
		0,0,0,1,1,0,0,0,
		0,0,0,1,1,0,0,0,
		0,0,1,1,1,1,0,0,
		0,0,1,1,1,1,0,0,
		0,1,1,1,1,1,1,0,
	],
	[
		0,0,0,0,0,0,0,0,
		1,0,0,0,0,0,0,1,
		1,1,1,0,0,1,1,1,
		1,1,1,1,1,0,0,1,
		1,1,1,1,1,0,0,1,
		1,1,1,0,0,1,1,1,
		1,0,0,0,0,0,0,1,
		0,0,0,0,0,0,0,0,
	]
];
var loading_anim_frame = 0;
var loading_anim_speed = 500;
var loading_interval = null;

function loadingAnimation() {
	//create image
	var loadingAnimImg = ctx.createImageData(8*scale, 8*scale);
	//draw image
	for (var y = 0; y < 8; y++) {
		for (var x = 0; x < 8; x++) {
			var i = (y * 8) + x;
			if (loading_anim_data[loading_anim_frame][i] == 1) {
				//scaling nonsense
				for (var sy = 0; sy < scale; sy++) {
					for (var sx = 0; sx = 5) loading_anim_frame = 0;
}

function updateLoadingScreen() {
	// TODO : in progress
	ctx.fillStyle = "rgb(0,0,0)";
	ctx.fillRect(0,0,canvas.width,canvas.height);

	loadingAnimation();
	drawSprite( getSpriteImage(sprite["a"],"0",0), 8, 8, ctx );
}

function update() {
	var curTime = Date.now();
	deltaTime = curTime - prevTime;

	if (curRoom == null) {
		// in the special case where there is no valid room, end the game
		startNarrating( "", true /*isEnding*/ );
	}

	if (!transition.IsTransitionActive()) {
		updateInput();
	}

	if (transition.IsTransitionActive()) {
		// transition animation takes over everything!
		transition.UpdateTransition(deltaTime);
	}
	else {
		if (!isNarrating && !isEnding) {
			updateAnimation();
			drawRoom( room[curRoom] ); // draw world if game has begun
		}
		else {
			//make sure to still clear screen
			ctx.fillStyle = "rgb(" + getPal(curPal())[0][0] + "," + getPal(curPal())[0][1] + "," + getPal(curPal())[0][2] + ")";
			ctx.fillRect(0,0,canvas.width,canvas.height);
		}

		// if (isDialogMode) { // dialog mode
		if(dialogBuffer.IsActive()) {
			dialogRenderer.Draw( dialogBuffer, deltaTime );
			dialogBuffer.Update( deltaTime );
		}

		// keep moving avatar if player holds down button
		if( !dialogBuffer.IsActive() && !isEnding )
		{
			if( curPlayerDirection != Direction.None ) {
				playerHoldToMoveTimer -= deltaTime;

				if( playerHoldToMoveTimer = animationTime ) {

		// animate sprites
		for (id in sprite) {
			var spr = sprite[id];
			if (spr.animation.isAnimated) {
				spr.animation.frameIndex = ( spr.animation.frameIndex + 1 ) % spr.animation.frameCount;
			}
		}

		// animate tiles
		for (id in tile) {
			var til = tile[id];
			if (til.animation.isAnimated) {
				til.animation.frameIndex = ( til.animation.frameIndex + 1 ) % til.animation.frameCount;
			}
		}

		// animate items
		for (id in item) {
			var itm = item[id];
			if (itm.animation.isAnimated) {
				itm.animation.frameIndex = ( itm.animation.frameIndex + 1 ) % itm.animation.frameCount;
			}
		}

		// reset counter
		animationCounter = 0;

	}
}

function resetAllAnimations() {
	for (id in sprite) {
		var spr = sprite[id];
		if (spr.animation.isAnimated) {
			spr.animation.frameIndex = 0;
		}
	}

	for (id in tile) {
		var til = tile[id];
		if (til.animation.isAnimated) {
			til.animation.frameIndex = 0;
		}
	}

	for (id in item) {
		var itm = item[id];
		if (itm.animation.isAnimated) {
			itm.animation.frameIndex = 0;
		}
	}
}

function getSpriteAt(x,y) {
	for (id in sprite) {
		var spr = sprite[id];
		if (spr.room === curRoom) {
			if (spr.x == x && spr.y == y) {
				return id;
			}
		}
	}
	return null;
}

var Direction = {
	None : -1,
	Up : 0,
	Down : 1,
	Left : 2,
	Right : 3
};

var curPlayerDirection = Direction.None;
var playerHoldToMoveTimer = 0;

var InputManager = function() {
	var self = this;

	var pressed;
	var ignored;
	var newKeyPress;
	var touchState;

	function resetAll() {
		pressed = {};
		ignored = {};
		newKeyPress = false;

		touchState = {
			isDown : false,
			startX : 0,
			startY : 0,
			curX : 0,
			curY : 0,
			swipeDistance : 30,
			swipeDirection : Direction.None,
			tapReleased : false
		};
	}
	resetAll();

	function stopWindowScrolling(e) {
		if(e.keyCode == key.left || e.keyCode == key.right || e.keyCode == key.up || e.keyCode == key.down || !isPlayerEmbeddedInEditor)
			e.preventDefault();
	}

	function tryRestartGame(e) {
		/* RESTART GAME */
		if ( e.keyCode === key.r && ( e.getModifierState("Control") || e.getModifierState("Meta") ) ) {
			if ( confirm("Restart the game?") ) {
				reset_cur_game();
			}
		}
	}

	function eventIsModifier(event) {
		return (event.keyCode == key.shift || event.keyCode == key.ctrl || event.keyCode == key.alt || event.keyCode == key.cmd);
	}

	function isModifierKeyDown() {
		return ( self.isKeyDown(key.shift) || self.isKeyDown(key.ctrl) || self.isKeyDown(key.alt) || self.isKeyDown(key.cmd) );
	}

	this.ignoreHeldKeys = function() {
		for (var key in pressed) {
			if (pressed[key]) { // only ignore keys that are actually held
				ignored[key] = true;
				// console.log("IGNORE -- " + key);
			}
		}
	}

	this.onkeydown = function(event) {
		// console.log("KEYDOWN -- " + event.keyCode);

		stopWindowScrolling(event);

		tryRestartGame(event);

		// Special keys being held down can interfere with keyup events and lock movement
		// so just don't collect input when they're held
		{
			if (isModifierKeyDown()) {
				return;
			}

			if (eventIsModifier(event)) {
				resetAll();
			}
		}

		if (ignored[event.keyCode]) {
			return;
		}

		if (!self.isKeyDown(event.keyCode)) {
			newKeyPress = true;
		}

		pressed[event.keyCode] = true;
		ignored[event.keyCode] = false;
	}

	this.onkeyup = function(event) {
		// console.log("KEYUP -- " + event.keyCode);
		pressed[event.keyCode] = false;
		ignored[event.keyCode] = false;
	}

	this.ontouchstart = function(event) {
		event.preventDefault();

		if( event.changedTouches.length > 0 ) {
			touchState.isDown = true;

			touchState.startX = touchState.curX = event.changedTouches[0].clientX;
			touchState.startY = touchState.curY = event.changedTouches[0].clientY;

			touchState.swipeDirection = Direction.None;
		}
	}

	this.ontouchmove = function(event) {
		event.preventDefault();

		if( touchState.isDown && event.changedTouches.length > 0 ) {
			touchState.curX = event.changedTouches[0].clientX;
			touchState.curY = event.changedTouches[0].clientY;

			var prevDirection = touchState.swipeDirection;

			if( touchState.curX - touchState.startX = touchState.swipeDistance ) {
				touchState.swipeDirection = Direction.Right;
			}
			else if( touchState.curY - touchState.startY = touchState.swipeDistance ) {
				touchState.swipeDirection = Direction.Down;
			}

			if( touchState.swipeDirection != prevDirection ) {
				// reset center so changing directions is easier
				touchState.startX = touchState.curX;
				touchState.startY = touchState.curY;
			}
		}
	}

	this.ontouchend = function(event) {
		event.preventDefault();

		touchState.isDown = false;

		if( touchState.swipeDirection == Direction.None ) {
			// tap!
			touchState.tapReleased = true;
		}

		touchState.swipeDirection = Direction.None;
	}

	this.isKeyDown = function(keyCode) {
		return pressed[keyCode] != null && pressed[keyCode] == true && (ignored[keyCode] == null || ignored[keyCode] == false);
	}

	this.anyKeyPressed = function() {
		return newKeyPress;
	}

	this.resetKeyPressed = function() {
		newKeyPress = false;
	}

	this.swipeLeft = function() {
		return touchState.swipeDirection == Direction.Left;
	}

	this.swipeRight = function() {
		return touchState.swipeDirection == Direction.Right;
	}

	this.swipeUp = function() {
		return touchState.swipeDirection == Direction.Up;
	}

	this.swipeDown = function() {
		return touchState.swipeDirection == Direction.Down;
	}

	this.isTapReleased = function() {
		return touchState.tapReleased;
	}

	this.resetTapReleased = function() {
		touchState.tapReleased = false;
	}

	this.onblur = function() {
		// console.log("~~~ BLUR ~~");
		resetAll();
	}
}
var input = null;

function movePlayer(direction) {
	if (player().room == null || !Object.keys(room).includes(player().room)) {
		return; // player room is missing or invalid.. can't move them!
	}

	var spr = null;

	if ( curPlayerDirection == Direction.Left && !(spr = getSpriteLeft()) && !isWallLeft()) {
		player().x -= 1;
	}
	else if ( curPlayerDirection == Direction.Right && !(spr = getSpriteRight()) && !isWallRight()) {
		player().x += 1;
	}
	else if ( curPlayerDirection == Direction.Up && !(spr = getSpriteUp()) && !isWallUp()) {
		player().y -= 1;
	}
	else if ( curPlayerDirection == Direction.Down && !(spr = getSpriteDown()) && !isWallDown()) {
		player().y += 1;
	}
	
	var ext = getExit( player().room, player().x, player().y );
	var end = getEnding( player().room, player().x, player().y );
	var itmIndex = getItemIndex( player().room, player().x, player().y );

	// do items first, because you can pick up an item AND go through a door
	if (itmIndex > -1) {
		var itm = room[player().room].items[itmIndex];
		var itemRoom = player().room;

		startItemDialog(itm.id, function() {
			// remove item from room
			room[itemRoom].items.splice(itmIndex, 1);

			// update player inventory
			if (player().inventory[itm.id]) {
				player().inventory[itm.id] += 1;
			}
			else {
				player().inventory[itm.id] = 1;
			}

			// show inventory change in UI
			if (onInventoryChanged != null) {
				onInventoryChanged(itm.id);
			}
		});
	}

	if (end) {
		startEndingDialog(end);
	}
	else if (ext) {
		movePlayerThroughExit(ext);
	}
	else if (spr) {
		startSpriteDialog(spr /*spriteId*/);
	}
}

var transition = new TransitionManager();

function movePlayerThroughExit(ext) {
	var GoToDest = function() {
		if (ext.transition_effect != null) {
			transition.BeginTransition(player().room, player().x, player().y, ext.dest.room, ext.dest.x, ext.dest.y, ext.transition_effect);
			transition.UpdateTransition(0);
		}

		player().room = ext.dest.room;
		player().x = ext.dest.x;
		player().y = ext.dest.y;
		curRoom = ext.dest.room;

		initRoom(curRoom);
	};

	if (ext.dlg != undefined && ext.dlg != null) {
		// TODO : I need to simplify dialog code,
		// so I don't have to get the ID and the source str
		// every time!
		startDialog(
			dialog[ext.dlg].src,
			ext.dlg,
			function(result) {
				var isLocked = ext.property && ext.property.locked === true;
				if (!isLocked) {
					GoToDest();
				}
			},
			ext);
	}
	else {
		GoToDest();
	}
}

function initRoom(roomId) {
	// init exit properties
	for (var i = 0; i < room[roomId].exits.length; i++) {
		room[roomId].exits[i].property = { locked:false };
	}

	// init ending properties
	for (var i = 0; i < room[roomId].endings.length; i++) {
		room[roomId].endings[i].property = { locked:false };
	}
}

function getItemIndex( roomId, x, y ) {
	for( var i = 0; i < room[roomId].items.length; i++ ) {
		var itm = room[roomId].items[i];
		if ( itm.x == x && itm.y == y)
			return i;
	}
	return -1;
}

function getSpriteLeft() { //repetitive?
	return getSpriteAt( player().x - 1, player().y );
}

function getSpriteRight() {
	return getSpriteAt( player().x + 1, player().y );
}

function getSpriteUp() {
	return getSpriteAt( player().x, player().y - 1 );
}

function getSpriteDown() {
	return getSpriteAt( player().x, player().y + 1 );
}

function isWallLeft() {
	return (player().x - 1 = 16) || isWall( player().x + 1, player().y );
}

function isWallUp() {
	return (player().y - 1 = 16) || isWall( player().x, player().y + 1 );
}

function isWall(x,y,roomId) {
	if(roomId == undefined || roomId == null)
		roomId = curRoom;

	var tileId = getTile( x, y );

	if( tileId === '0' )
		return false; // Blank spaces aren't walls, ya doofus

	if( tile[tileId].isWall === undefined || tile[tileId].isWall === null ) {
		// No wall-state defined: check room-specific walls
		var i = room[roomId].walls.indexOf( getTile(x,y) );
		return i > -1;
	}

	// Otherwise, use the tile's own wall-state
	return tile[tileId].isWall;
}

function getItem(roomId,x,y) {
	for (i in room[roomId].items) {
		var item = room[roomId].items[i];
		if (x == item.x && y == item.y) {
			return item;
		}
	}
	return null;
}

function getExit(roomId,x,y) {
	for (i in room[roomId].exits) {
		var e = room[roomId].exits[i];
		if (x == e.x && y == e.y) {
			return e;
		}
	}
	return null;
}

function getEnding(roomId,x,y) {
	for (i in room[roomId].endings) {
		var e = room[roomId].endings[i];
		if (x == e.x && y == e.y) {
			return e;
		}
	}
	return null;
}

function getTile(x,y) {
	// console.log(x + " " + y);
	var t = getRoom().tilemap[y][x];
	return t;
}

function player() {
	return sprite[playerId];
}

// Sort of a hack for legacy palette code (when it was just an array)
function getPal(id) {
	if (palette[id] === undefined) {
		id = "default";
	}

	return palette[ id ].colors;
}

function getRoom() {
	return room[curRoom];
}

function isSpriteOffstage(id) {
	return sprite[id].room == null;
}

function parseWorld(file) {
	spriteStartLocations = {};

	resetFlags();

	var versionNumber = 0;

	// flags to keep track of which compatibility conversions
	// need to be applied to this game data
	var compatibilityFlags = {
		convertSayToPrint : false,
		combineEndingsWithDialog : false,
		convertImplicitSpriteDialogIds : false,
	};

	var lines = file.split("\n");
	var i = 0;
	while (i < lines.length) {
		var curLine = lines[i];

		// console.log(lines[i]);

		if (i == 0) {
			i = parseTitle(lines, i);
		}
		else if (curLine.length <= 0 || curLine.charAt(0) === "#") {
			// collect version number (from a comment.. hacky I know)
			if (curLine.indexOf("# BITSY VERSION ") != -1) {
				versionNumber = parseFloat(curLine.replace("# BITSY VERSION ", ""));

				if (versionNumber < 5.0) {
					compatibilityFlags.convertSayToPrint = true;
				}

				if (versionNumber  0) {
		// player not in any room! what the heck
		curRoom = roomIds[0];
	}
	else {
		// uh oh there are no rooms I guess???
		curRoom = null;
	}

	if (curRoom != null) {
		initRoom(curRoom);
	}

	renderer.SetPalettes(palette);

	scriptCompatibility(compatibilityFlags);

	return versionNumber;
}

function scriptCompatibility(compatibilityFlags) {
	if (compatibilityFlags.convertSayToPrint) {
		console.log("CONVERT SAY TO PRINT!");

		var PrintFunctionVisitor = function() {
			var didChange = false;
			this.DidChange = function() { return didChange; };

			this.Visit = function(node) {
				if (node.type != "function") {
					return;
				}

				if (node.name === "say") {
					node.name = "print";
					didChange = true;
				}
			};
		};

		for (dlgId in dialog) {
			var dialogScript = scriptInterpreter.Parse(dialog[dlgId].src);
			var visitor = new PrintFunctionVisitor();
			dialogScript.VisitAll(visitor);
			if (visitor.DidChange()) {
				var newDialog = dialogScript.Serialize();
				if (newDialog.indexOf("\n") > -1) {
					newDialog = '"""\n' + newDialog + '\n"""';
				}
				dialog[dlgId].src = newDialog;
			}
		}
	}
}

//TODO this is in progress and doesn't support all features
function serializeWorld(skipFonts) {
	if (skipFonts === undefined || skipFonts === null)
		skipFonts = false;

	var worldStr = "";
	/* TITLE */
	worldStr += getTitle() + "\n";
	worldStr += "\n";
	/* VERSION */
	worldStr += "# BITSY VERSION " + getEngineVersion() + "\n"; // add version as a comment for debugging purposes
	if (version.devBuildPhase != "RELEASE") {
		worldStr += "# DEVELOPMENT BUILD -- " + version.devBuildPhase;
	}
	worldStr += "\n";
	/* FLAGS */
	for (f in flags) {
		worldStr += "! " + f + " " + flags[f] + "\n";
	}
	worldStr += "\n"
	/* FONT */
	if (fontName != defaultFontName) {
		worldStr += "DEFAULT_FONT " + fontName + "\n";
		worldStr += "\n"
	}
	if (textDirection != TextDirection.LeftToRight) {
		worldStr += "TEXT_DIRECTION " + textDirection + "\n";
		worldStr += "\n"
	}
	/* PALETTE */
	for (id in palette) {
		if (id != "default") {
			worldStr += "PAL " + id + "\n";
			if( palette[id].name != null )
				worldStr += "NAME " + palette[id].name + "\n";
			for (i in getPal(id)) {
				for (j in getPal(id)[i]) {
					worldStr += getPal(id)[i][j];
					if (j < 2) worldStr += ",";
				}
				worldStr += "\n";
			}
			worldStr += "\n";
		}
	}
	/* ROOM */
	for (id in room) {
		worldStr += "ROOM " + id + "\n";
		if ( flags.ROOM_FORMAT == 0 ) {
			// old non-comma separated format
			for (i in room[id].tilemap) {
				for (j in room[id].tilemap[i]) {
					worldStr += room[id].tilemap[i][j];	
				}
				worldStr += "\n";
			}
		}
		else if ( flags.ROOM_FORMAT == 1 ) {
			// new comma separated format
			for (i in room[id].tilemap) {
				for (j in room[id].tilemap[i]) {
					worldStr += room[id].tilemap[i][j];
					if (j  0) {
			/* WALLS */
			worldStr += "WAL ";
			for (j in room[id].walls) {
				worldStr += room[id].walls[j];
				if (j  0) {
			/* ITEMS */
			for (j in room[id].items) {
				var itm = room[id].items[j];
				worldStr += "ITM " + itm.id + " " + itm.x + "," + itm.y;
				worldStr += "\n";
			}
		}
		if (room[id].exits.length > 0) {
			/* EXITS */
			for (j in room[id].exits) {
				var e = room[id].exits[j];
				if ( isExitValid(e) ) {
					worldStr += "EXT " + e.x + "," + e.y + " " + e.dest.room + " " + e.dest.x + "," + e.dest.y;
					if (e.transition_effect != undefined && e.transition_effect != null) {
						worldStr += " FX " + e.transition_effect;
					}
					if (e.dlg != undefined && e.dlg != null) {
						worldStr += " DLG " + e.dlg;
					}
					worldStr += "\n";
				}
			}
		}
		if (room[id].endings.length > 0) {
			/* ENDINGS */
			for (j in room[id].endings) {
				var e = room[id].endings[j];
				// todo isEndingValid
				worldStr += "END " + e.id + " " + e.x + "," + e.y;
				worldStr += "\n";
			}
		}
		if (room[id].pal != null && room[id].pal != "default") {
			/* PALETTE */
			worldStr += "PAL " + room[id].pal + "\n";
		}
		worldStr += "\n";
	}
	/* TILES */
	for (id in tile) {
		worldStr += "TIL " + id + "\n";
		worldStr += serializeDrawing( "TIL_" + id );
		if (tile[id].name != null && tile[id].name != undefined) {
			/* NAME */
			worldStr += "NAME " + tile[id].name + "\n";
		}
		if (tile[id].isWall != null && tile[id].isWall != undefined) {
			/* WALL */
			worldStr += "WAL " + tile[id].isWall + "\n";
		}
		if (tile[id].col != null && tile[id].col != undefined && tile[id].col != 1) {
			/* COLOR OVERRIDE */
			worldStr += "COL " + tile[id].col + "\n";
		}
		worldStr += "\n";
	}
	/* SPRITES */
	for (id in sprite) {
		worldStr += "SPR " + id + "\n";
		worldStr += serializeDrawing( "SPR_" + id );
		if (sprite[id].name != null && sprite[id].name != undefined) {
			/* NAME */
			worldStr += "NAME " + sprite[id].name + "\n";
		}
		if (sprite[id].dlg != null) {
			worldStr += "DLG " + sprite[id].dlg + "\n";
		}
		if (sprite[id].room != null) {
			/* SPRITE POSITION */
			worldStr += "POS " + sprite[id].room + " " + sprite[id].x + "," + sprite[id].y + "\n";
		}
		if (sprite[id].inventory != null) {
			for(itemId in sprite[id].inventory) {
				worldStr += "ITM " + itemId + " " + sprite[id].inventory[itemId] + "\n";
			}
		}
		if (sprite[id].col != null && sprite[id].col != undefined && sprite[id].col != 2) {
			/* COLOR OVERRIDE */
			worldStr += "COL " + sprite[id].col + "\n";
		}
		worldStr += "\n";
	}
	/* ITEMS */
	for (id in item) {
		worldStr += "ITM " + id + "\n";
		worldStr += serializeDrawing( "ITM_" + id );
		if (item[id].name != null && item[id].name != undefined) {
			/* NAME */
			worldStr += "NAME " + item[id].name + "\n";
		}
		if (item[id].dlg != null) {
			worldStr += "DLG " + item[id].dlg + "\n";
		}
		if (item[id].col != null && item[id].col != undefined && item[id].col != 2) {
			/* COLOR OVERRIDE */
			worldStr += "COL " + item[id].col + "\n";
		}
		worldStr += "\n";
	}
	/* DIALOG */
	for (id in dialog) {
		if (id != titleDialogId) {
			worldStr += "DLG " + id + "\n";
			worldStr += dialog[id].src + "\n";
			if (dialog[id].name != null) {
				worldStr += "NAME " + dialog[id].name + "\n";
			}
			worldStr += "\n";
		}
	}
	/* VARIABLES */
	for (id in variable) {
		worldStr += "VAR " + id + "\n";
		worldStr += variable[id] + "\n";
		worldStr += "\n";
	}
	/* FONT */
	// TODO : support multiple fonts
	if (fontName != defaultFontName && !skipFonts) {
		worldStr += fontManager.GetData(fontName);
	}

	return worldStr;
}

function serializeDrawing(drwId) {
	var imageSource = renderer.GetImageSource(drwId);
	var drwStr = "";
	for (f in imageSource) {
		for (y in imageSource[f]) {
			var rowStr = "";
			for (x in imageSource[f][y]) {
				rowStr += imageSource[f][y][x];
			}
			drwStr += rowStr + "\n";
		}
		if (f \n";
	}
	return drwStr;
}

function isExitValid(e) {
	var hasValidStartPos = e.x >= 0 && e.x = 0 && e.y = 0 && e.dest.x = 0 && e.dest.y < 16);
	return hasValidStartPos && hasDest && hasValidRoomDest;
}

function placeSprites() {
	for (id in spriteStartLocations) {
		//console.log(id);
		//console.log( spriteStartLocations[id] );
		//console.log(sprite[id]);
		sprite[id].room = spriteStartLocations[id].room;
		sprite[id].x = spriteStartLocations[id].x;
		sprite[id].y = spriteStartLocations[id].y;
		//console.log(sprite[id]);
	}
}

/* ARGUMENT GETTERS */
function getType(line) {
	return getArg(line,0);
}

function getId(line) {
	return getArg(line,1);
}

function getArg(line,arg) {
	return line.split(" ")[arg];
}

function getCoord(line,arg) {
	return getArg(line,arg).split(",");
}

function parseTitle(lines, i) {
	var results = scriptUtils.ReadDialogScript(lines,i);
	setTitle(results.script);
	i = results.index;

	i++;

	return i;
}

function parseRoom(lines, i, compatibilityFlags) {
	var id = getId(lines[i]);
	room[id] = {
		id : id,
		tilemap : [],
		walls : [],
		exits : [],
		endings : [],
		items : [],
		pal : null,
		name : null
	};
	i++;

	// create tile map
	if ( flags.ROOM_FORMAT == 0 ) {
		// old way: no commas, single char tile ids
		var end = i + mapsize;
		var y = 0;
		for (; i<end; i++) {
			room[id].tilemap.push( [] );
			for (x = 0; x<mapsize; x++) {
				room[id].tilemap[y].push( lines[i].charAt(x) );
			}
			y++;
		}
	}
	else if ( flags.ROOM_FORMAT == 1 ) {
		// new way: comma separated, multiple char tile ids
		var end = i + mapsize;
		var y = 0;
		for (; i<end; i++) {
			room[id].tilemap.push( [] );
			var lineSep = lines[i].split(",");
			for (x = 0; x<mapsize; x++) {
				room[id].tilemap[y].push( lineSep[x] );
			}
			y++;
		}
	}

	while (i  0) { //look for empty line
		// console.log(getType(lines[i]));
		if (getType(lines[i]) === "SPR") {
			/* NOTE SPRITE START LOCATIONS */
			var sprId = getId(lines[i]);
			if (sprId.indexOf(",") == -1 && lines[i].split(" ").length >= 3) { //second conditional checks for coords
				/* PLACE A SINGLE SPRITE */
				var sprCoord = lines[i].split(" ")[2].split(",");
				spriteStartLocations[sprId] = {
					room : id,
					x : parseInt(sprCoord[0]),
					y : parseInt(sprCoord[1])
				};
			}
			else if ( flags.ROOM_FORMAT == 0 ) { // TODO: right now this shortcut only works w/ the old comma separate format
				/* PLACE MULTIPLE SPRITES*/ 
				//Does find and replace in the tilemap (may be hacky, but its convenient)
				var sprList = sprId.split(",");
				for (row in room[id].tilemap) {
					for (s in sprList) {
						var col = room[id].tilemap[row].indexOf( sprList[s] );
						//if the sprite is in this row, replace it with the "null tile" and set its starting position
						if (col != -1) {
							room[id].tilemap[row][col] = "0";
							spriteStartLocations[ sprList[s] ] = {
								room : id,
								x : parseInt(col),
								y : parseInt(row)
							};
						}
					}
				}
			}
		}
		else if (getType(lines[i]) === "ITM") {
			var itmId = getId(lines[i]);
			var itmCoord = lines[i].split(" ")[2].split(",");
			var itm = {
				id: itmId,
				x : parseInt(itmCoord[0]),
				y : parseInt(itmCoord[1])
			};
			room[id].items.push( itm );
		}
		else if (getType(lines[i]) === "WAL") {
			/* DEFINE COLLISIONS (WALLS) */
			room[id].walls = getId(lines[i]).split(",");
		}
		else if (getType(lines[i]) === "EXT") {
			/* ADD EXIT */
			var exitArgs = lines[i].split(" ");
			//arg format: EXT 10,5 M 3,2 [AVA:7 LCK:a,9] [AVA 7 LCK a 9]
			var exitCoords = exitArgs[1].split(",");
			var destName = exitArgs[2];
			var destCoords = exitArgs[3].split(",");
			var ext = {
				x : parseInt(exitCoords[0]),
				y : parseInt(exitCoords[1]),
				dest : {
					room : destName,
					x : parseInt(destCoords[0]),
					y : parseInt(destCoords[1])
				},
				transition_effect : null,
				dlg: null,
			};

			// optional arguments
			var exitArgIndex = 4;
			while (exitArgIndex < exitArgs.length) {
				if (exitArgs[exitArgIndex] == "FX") {
					ext.transition_effect = exitArgs[exitArgIndex+1];
					exitArgIndex += 2;
				}
				else if (exitArgs[exitArgIndex] == "DLG") {
					ext.dlg = exitArgs[exitArgIndex+1];
					exitArgIndex += 2;
				}
				else {
					exitArgIndex += 1;
				}
			}

			room[id].exits.push(ext);
		}
		else if (getType(lines[i]) === "END") {
			/* ADD ENDING */
			var endId = getId(lines[i]);

			// compatibility with when endings were stored separate from other dialog
			if (compatibilityFlags.combineEndingsWithDialog) {
				endId = "end_" + endId;
			}

			var endCoords = getCoord(lines[i], 2);
			var end = {
				id : endId,
				x : parseInt(endCoords[0]),
				y : parseInt(endCoords[1])
			};

			room[id].endings.push(end);
		}
		else if (getType(lines[i]) === "PAL") {
			/* CHOOSE PALETTE (that's not default) */
			room[id].pal = getId(lines[i]);
		}
		else if (getType(lines[i]) === "NAME") {
			var name = lines[i].split(/\s(.+)/)[1];
			room[id].name = name;
			names.room.set(name, id);
		}

		i++;
	}

	return i;
}

function parsePalette(lines,i) { //todo this has to go first right now :(
	var id = getId(lines[i]);
	i++;
	var colors = [];
	var name = null;
	while (i  0) { //look for empty line
		var args = lines[i].split(" ");
		if (args[0] === "NAME") {
			name = lines[i].split(/\s(.+)/)[1];
		}
		else {
			var col = [];
			lines[i].split(",").forEach(function(i) {
				col.push(parseInt(i));
			});
			colors.push(col);
		}
		i++;
	}
	palette[id] = {
		id : id,
		name : name,
		colors : colors
	};
	return i;
}

function parseTile(lines, i) {
	var id = getId(lines[i]);
	var drwId = null;
	var name = null;

	i++;

	if (getType(lines[i]) === "DRW") { //load existing drawing
		drwId = getId(lines[i]);
		i++;
	}
	else {
		// store tile source
		drwId = "TIL_" + id;
		i = parseDrawingCore( lines, i, drwId );
	}

	//other properties
	var colorIndex = 1; // default palette color index is 1
	var isWall = null; // null indicates it can vary from room to room (original version)
	while (i  0) { //look for empty line
		if (getType(lines[i]) === "COL") {
			colorIndex = parseInt( getId(lines[i]) );
		}
		else if (getType(lines[i]) === "NAME") {
			/* NAME */
			name = lines[i].split(/\s(.+)/)[1];
			names.tile.set( name, id );
		}
		else if (getType(lines[i]) === "WAL") {
			var wallArg = getArg( lines[i], 1 );
			if( wallArg === "true" ) {
				isWall = true;
			}
			else if( wallArg === "false" ) {
				isWall = false;
			}
		}
		i++;
	}

	//tile data
	tile[id] = {
		id : id,
		drw : drwId, //drawing id
		col : colorIndex,
		animation : {
			isAnimated : (renderer.GetFrameCount(drwId) > 1),
			frameIndex : 0,
			frameCount : renderer.GetFrameCount(drwId)
		},
		name : name,
		isWall : isWall
	};

	return i;
}

function parseSprite(lines, i) {
	var id = getId(lines[i]);
	var drwId = null;
	var name = null;

	i++;

	if (getType(lines[i]) === "DRW") { //load existing drawing
		drwId = getId(lines[i]);
		i++;
	}
	else {
		// store sprite source
		drwId = "SPR_" + id;
		i = parseDrawingCore( lines, i, drwId );
	}

	//other properties
	var colorIndex = 2; //default palette color index is 2
	var dialogId = null;
	var startingInventory = {};
	while (i  0) { //look for empty line
		if (getType(lines[i]) === "COL") {
			/* COLOR OFFSET INDEX */
			colorIndex = parseInt( getId(lines[i]) );
		}
		else if (getType(lines[i]) === "POS") {
			/* STARTING POSITION */
			var posArgs = lines[i].split(" ");
			var roomId = posArgs[1];
			var coordArgs = posArgs[2].split(",");
			spriteStartLocations[id] = {
				room : roomId,
				x : parseInt(coordArgs[0]),
				y : parseInt(coordArgs[1])
			};
		}
		else if(getType(lines[i]) === "DLG") {
			dialogId = getId(lines[i]);
		}
		else if (getType(lines[i]) === "NAME") {
			/* NAME */
			name = lines[i].split(/\s(.+)/)[1];
			names.sprite.set( name, id );
		}
		else if (getType(lines[i]) === "ITM") {
			/* ITEM STARTING INVENTORY */
			var itemId = getId(lines[i]);
			var itemCount = parseFloat( getArg(lines[i], 2) );
			startingInventory[itemId] = itemCount;
		}
		i++;
	}

	//sprite data
	sprite[id] = {
		id : id,
		drw : drwId, //drawing id
		col : colorIndex,
		dlg : dialogId,
		room : null, //default location is "offstage"
		x : -1,
		y : -1,
		animation : {
			isAnimated : (renderer.GetFrameCount(drwId) > 1),
			frameIndex : 0,
			frameCount : renderer.GetFrameCount(drwId)
		},
		inventory : startingInventory,
		name : name
	};
	return i;
}

function parseItem(lines, i) {
	var id = getId(lines[i]);
	var drwId = null;
	var name = null;

	i++;

	if (getType(lines[i]) === "DRW") { //load existing drawing
		drwId = getId(lines[i]);
		i++;
	}
	else {
		// store item source
		drwId = "ITM_" + id; // these prefixes are maybe a terrible way to differentiate drawing tyepes :/
		i = parseDrawingCore( lines, i, drwId );
	}

	//other properties
	var colorIndex = 2; //default palette color index is 2
	var dialogId = null;
	while (i  0) { //look for empty line
		if (getType(lines[i]) === "COL") {
			/* COLOR OFFSET INDEX */
			colorIndex = parseInt( getArg( lines[i], 1 ) );
		}
		// else if (getType(lines[i]) === "POS") {
		// 	/* STARTING POSITION */
		// 	var posArgs = lines[i].split(" ");
		// 	var roomId = posArgs[1];
		// 	var coordArgs = posArgs[2].split(",");
		// 	spriteStartLocations[id] = {
		// 		room : roomId,
		// 		x : parseInt(coordArgs[0]),
		// 		y : parseInt(coordArgs[1])
		// 	};
		// }
		else if(getType(lines[i]) === "DLG") {
			dialogId = getId(lines[i]);
		}
		else if (getType(lines[i]) === "NAME") {
			/* NAME */
			name = lines[i].split(/\s(.+)/)[1];
			names.item.set( name, id );
		}
		i++;
	}

	//item data
	item[id] = {
		id : id,
		drw : drwId, //drawing id
		col : colorIndex,
		dlg : dialogId,
		// room : null, //default location is "offstage"
		// x : -1,
		// y : -1,
		animation : {
			isAnimated : (renderer.GetFrameCount(drwId) > 1),
			frameIndex : 0,
			frameCount : renderer.GetFrameCount(drwId)
		},
		name : name
	};

	// console.log("ITM " + id);
	// console.log(item[id]);

	return i;
}

function parseDrawing(lines, i) {
	// store drawing source
	var drwId = getId( lines[i] );
	return parseDrawingCore( lines, i, drwId );
}

function parseDrawingCore(lines, i, drwId) {
	var frameList = []; //init list of frames
	frameList.push( [] ); //init first frame
	var frameIndex = 0;
	var y = 0;
	while ( y < tilesize ) {
		var l = lines[i+y];
		var row = [];
		for (x = 0; x " ) {
				// start next frame!
				frameList.push( [] );
				frameIndex++;
				//start the count over again for the next frame
				i++;
				y = 0;
			}
		}
	}

	renderer.SetImageSource(drwId, frameList);

	return i;
}

function parseScript(lines, i, backCompatPrefix, compatibilityFlags) {
	var id = getId(lines[i]);
	id = backCompatPrefix + id;
	i++;

	var results = scriptUtils.ReadDialogScript(lines,i);

	dialog[id] = { src:results.script, name:null };

	if (compatibilityFlags.convertImplicitSpriteDialogIds) {
		// explicitly hook up dialog that used to be implicitly
		// connected by sharing sprite and dialog IDs in old versions
		if (sprite[id]) {
			if (sprite[id].dlg === undefined || sprite[id].dlg === null) {
				sprite[id].dlg = id;
			}
		}
	}

	i = results.index;

	return i;
}

function parseDialog(lines, i, compatibilityFlags) {
	// hacky but I need to store this so I can set the name below
	var id = getId(lines[i]);

	i = parseScript(lines, i, "", compatibilityFlags);

	if (lines[i].length > 0 && getType(lines[i]) === "NAME") {
		dialog[id].name = lines[i].split(/\s(.+)/)[1]; // TODO : hacky to keep copying this regex around...
		names.dialog.set(dialog[id].name, id);
		i++;
	}

	return i;
}

// keeping this around to parse old files where endings were separate from dialogs
function parseEnding(lines, i, compatibilityFlags) {
	return parseScript(lines, i, "end_", compatibilityFlags);
}

function parseVariable(lines, i) {
	var id = getId(lines[i]);
	i++;
	var value = lines[i];
	i++;
	variable[id] = value;
	return i;
}

function parseFontName(lines, i) {
	fontName = getArg(lines[i], 1);
	i++;
	return i;
}

function parseTextDirection(lines, i) {
	textDirection = getArg(lines[i], 1);
	i++;
	return i;
}

function parseFontData(lines, i) {
	// NOTE : we're not doing the actual parsing here --
	// just grabbing the block of text that represents the font
	// and giving it to the font manager to use later

	var localFontName = getId(lines[i]);
	var localFontData = lines[i];
	i++;

	while (i < lines.length && lines[i] != "") {
		localFontData += "\n" + lines[i];
		i++;
	}

	var localFontFilename = localFontName + fontManager.GetExtension();
	fontManager.AddResource( localFontFilename, localFontData );

	return i;
}

function parseFlag(lines, i) {
	var id = getId(lines[i]);
	var valStr = lines[i].split(" ")[2];
	flags[id] = parseInt( valStr );
	i++;
	return i;
}

function drawTile(img,x,y,context) {
	if (!context) { //optional pass in context; otherwise, use default
		context = ctx;
	}
	// NOTE: images are now canvases, instead of raw image data (for chrome performance reasons)
	context.drawImage(img,x*tilesize*scale,y*tilesize*scale,tilesize*scale,tilesize*scale);
}

function drawSprite(img,x,y,context) { //this may differ later (or not haha)
	drawTile(img,x,y,context);
}

function drawItem(img,x,y,context) {
	drawTile(img,x,y,context); //TODO these methods are dumb and repetitive
}

// var debugLastRoomDrawn = "0";

function drawRoom(room,context,frameIndex) { // context & frameIndex are optional
	if (!context) { //optional pass in context; otherwise, use default (ok this is REAL hacky isn't it)
		context = ctx;
	}

	// if (room.id != debugLastRoomDrawn) {
	// 	debugLastRoomDrawn = room.id;
	// 	console.log("DRAW ROOM " + debugLastRoomDrawn);
	// }

	var paletteId = "default";

	if (room === undefined) {
		// protect against invalid rooms
		context.fillStyle = "rgb(" + getPal(paletteId)[0][0] + "," + getPal(paletteId)[0][1] + "," + getPal(paletteId)[0][2] + ")";
		context.fillRect(0,0,canvas.width,canvas.height);
		return;
	}

	//clear screen
	if (room.pal != null && palette[paletteId] != undefined) {
		paletteId = room.pal;
	}
	context.fillStyle = "rgb(" + getPal(paletteId)[0][0] + "," + getPal(paletteId)[0][1] + "," + getPal(paletteId)[0][2] + ")";
	context.fillRect(0,0,canvas.width,canvas.height);

	//draw tiles
	for (i in room.tilemap) {
		for (j in room.tilemap[i]) {
			var id = room.tilemap[i][j];
			if (id != "0") {
				//console.log(id);
				if (tile[id] == null) { // hack-around to avoid corrupting files (not a solution though!)
					id = "0";
					room.tilemap[i][j] = id;
				}
				else {
					// console.log(id);
					drawTile( getTileImage(tile[id],paletteId,frameIndex), j, i, context );
				}
			}
		}
	}

	//draw items
	for (var i = 0; i < room.items.length; i++) {
		var itm = room.items[i];
		drawItem( getItemImage(item[itm.id],paletteId,frameIndex), itm.x, itm.y, context );
	}

	//draw sprites
	for (id in sprite) {
		var spr = sprite[id];
		if (spr.room === room.id) {
			drawSprite( getSpriteImage(spr,paletteId,frameIndex), spr.x, spr.y, context );
		}
	}
}

// TODO : remove these get*Image methods
function getTileImage(t,palId,frameIndex) {
	return renderer.GetImage(t,palId,frameIndex);
}

function getSpriteImage(s,palId,frameIndex) {
	return renderer.GetImage(s,palId,frameIndex);
}

function getItemImage(itm,palId,frameIndex) {
	return renderer.GetImage(itm,palId,frameIndex);
}

function curPal() {
	return getRoomPal(curRoom);
}

function getRoomPal(roomId) {
	var defaultId = "default";

	if (roomId == null) {
		return defaultId;
	}
	else if (room[roomId].pal != null) {
		//a specific palette was chosen
		return room[roomId].pal;
	}
	else {
		if (roomId in palette) {
			//there is a palette matching the name of the room
			return roomId;
		}
		else {
			//use the default palette
			return defaultId;
		}
	}
	return defaultId;
}

var isDialogMode = false;
var isNarrating = false;
var isEnding = false;
var dialogModule = new Dialog();
var dialogRenderer = dialogModule.CreateRenderer();
var dialogBuffer = dialogModule.CreateBuffer();
var fontManager = new FontManager();

// TODO : is this scriptResult thing being used anywhere???
function onExitDialog(scriptResult, dialogCallback) {
	console.log("EXIT DIALOG!");

	isDialogMode = false;

	if (isNarrating) {
		isNarrating = false;
	}

	if (isDialogPreview) {
		isDialogPreview = false;

		if (onDialogPreviewEnd != null) {
			onDialogPreviewEnd();
		}
	}

	if (dialogCallback != undefined && dialogCallback != null) {
		dialogCallback(scriptResult);
	}
}

/*
TODO
- titles and endings should also take advantage of the script pre-compilation if possible??
- could there be a namespace collision?
- what about dialog NAMEs vs IDs?
- what about a special script block separate from DLG?
*/
function startNarrating(dialogStr,end) {
	console.log("NARRATE " + dialogStr);

	if(end === undefined) {
		end = false;
	}

	isNarrating = true;
	isEnding = end;

	startDialog(dialogStr);
}

function startEndingDialog(ending) {
	isNarrating = true;
	isEnding = true;

	startDialog(
		dialog[ending.id].src,
		ending.id,
		function() {
			var isLocked = ending.property && ending.property.locked === true;
			if (isLocked) {
				isEnding = false;
			}
		},
		ending);
}

function startItemDialog(itemId, dialogCallback) {
	var dialogId = item[itemId].dlg;
	// console.log("START ITEM DIALOG " + dialogId);
	if (dialog[dialogId]) {
		var dialogStr = dialog[dialogId].src;
		startDialog(dialogStr, dialogId, dialogCallback);
	}
	else {
		dialogCallback();
	}
}

function startSpriteDialog(spriteId) {
	var spr = sprite[spriteId];
	var dialogId = spr.dlg;
	// console.log("START SPRITE DIALOG " + dialogId);
	if (dialog[dialogId]){
		var dialogStr = dialog[dialogId].src;
		startDialog(dialogStr,dialogId);
	}
}

function startDialog(dialogStr, scriptId, dialogCallback, objectContext) {
	// console.log("START DIALOG ");
	if (dialogStr.length <= 0) {
		// console.log("ON EXIT DIALOG -- startDialog 1");
		onExitDialog(null, dialogCallback);
		return;
	}

	isDialogMode = true;

	dialogRenderer.Reset();
	dialogRenderer.SetCentered(isNarrating /*centered*/);
	dialogBuffer.Reset();
	scriptInterpreter.SetDialogBuffer(dialogBuffer);

	var onScriptEnd = function(scriptResult) {
		dialogBuffer.OnDialogEnd(function() {
			onExitDialog(scriptResult, dialogCallback);
		});
	};

	if (scriptId === undefined) { // TODO : what's this for again?
		scriptInterpreter.Interpret(dialogStr, onScriptEnd);
	}
	else {
		if (!scriptInterpreter.HasScript(scriptId)) {
			scriptInterpreter.Compile(scriptId, dialogStr);
		}
		// scriptInterpreter.DebugVisualizeScript(scriptId);
		scriptInterpreter.Run(scriptId, onScriptEnd, objectContext);
	}

}

var isDialogPreview = false;
function startPreviewDialog(script, dialogCallback) {
	isNarrating = true;

	isDialogMode = true;

	isDialogPreview = true;

	dialogRenderer.Reset();
	dialogRenderer.SetCentered(true);
	dialogBuffer.Reset();
	scriptInterpreter.SetDialogBuffer(dialogBuffer);

	// TODO : do I really need a seperate callback for this debug mode??
	onDialogPreviewEnd = dialogCallback;

	var onScriptEndCallback = function(scriptResult) {
		dialogBuffer.OnDialogEnd(function() {
			onExitDialog(scriptResult, null);
		});
	};

	scriptInterpreter.Eval(script, onScriptEndCallback);
}

/* NEW SCRIPT STUFF */
var scriptModule = new Script();
var scriptInterpreter = scriptModule.CreateInterpreter();
var scriptUtils = scriptModule.CreateUtils(); // TODO: move to editor.js?
// scriptInterpreter.SetDialogBuffer( dialogBuffer );





FONT ascii_small
SIZE 6 8
CHAR 0
000000
000000
000000
000000
000000
000000
000000
000000
CHAR 1
001110
010001
011011
010001
010101
010001
001110
000000
CHAR 2
001110
011111
010101
011111
010001
011111
001110
000000
CHAR 3
000000
001010
011111
011111
011111
001110
000100
000000
CHAR 4
000000
000000
001010
001110
001110
000100
000000
000000
CHAR 5
000100
001110
001110
000100
011111
011111
000100
000000
CHAR 6
000000
000100
001110
011111
011111
000100
001110
000000
CHAR 7
000000
000000
000000
001100
001100
000000
000000
000000
CHAR 8
111111
111111
111111
110011
110011
111111
111111
111111
CHAR 9
000000
000000
011110
010010
010010
011110
000000
000000
CHAR 10
111111
111111
100001
101101
101101
100001
111111
111111
CHAR 11
000000
000111
000011
001101
010010
010010
001100
000000
CHAR 12
001110
010001
010001
001110
000100
001110
000100
000000
CHAR 13
000100
000110
000101
000100
001100
011100
011000
000000
CHAR 14
000011
001101
001011
001101
001011
011011
011000
000000
CHAR 15
000000
010101
001110
011011
001110
010101
000000
000000
CHAR 16
001000
001100
001110
001111
001110
001100
001000
000000
CHAR 17
000010
000110
001110
011110
001110
000110
000010
000000
CHAR 18
000100
001110
011111
000100
011111
001110
000100
000000
CHAR 19
001010
001010
001010
001010
001010
000000
001010
000000
CHAR 20
001111
010101
010101
001101
000101
000101
000101
000000
CHAR 21
001110
010001
001100
001010
000110
010001
001110
000000
CHAR 22
000000
000000
000000
000000
000000
011110
011110
000000
CHAR 23
000100
001110
011111
000100
011111
001110
000100
001110
CHAR 24
000100
001110
011111
000100
000100
000100
000100
000000
CHAR 25
000100
000100
000100
000100
011111
001110
000100
000000
CHAR 26
000000
000100
000110
011111
000110
000100
000000
000000
CHAR 27
000000
000100
001100
011111
001100
000100
000000
000000
CHAR 28
000000
000000
000000
010000
010000
010000
011111
000000
CHAR 29
000000
001010
001010
011111
001010
001010
000000
000000
CHAR 30
000100
000100
001110
001110
011111
011111
000000
000000
CHAR 31
011111
011111
001110
001110
000100
000100
000000
000000
CHAR 32
000000
000000
000000
000000
000000
000000
000000
000000
CHAR 33
000100
001110
001110
000100
000100
000000
000100
000000
CHAR 34
011011
011011
010010
000000
000000
000000
000000
000000
CHAR 35
000000
001010
011111
001010
001010
011111
001010
000000
CHAR 36
001000
001110
010000
001100
000010
011100
000100
000000
CHAR 37
011001
011001
000010
000100
001000
010011
010011
000000
CHAR 38
001000
010100
010100
001000
010101
010010
001101
000000
CHAR 39
001100
001100
001000
000000
000000
000000
000000
000000
CHAR 40
000100
001000
001000
001000
001000
001000
000100
000000
CHAR 41
001000
000100
000100
000100
000100
000100
001000
000000
CHAR 42
000000
001010
001110
011111
001110
001010
000000
000000
CHAR 43
000000
000100
000100
011111
000100
000100
000000
000000
CHAR 44
000000
000000
000000
000000
000000
001100
001100
001000
CHAR 45
000000
000000
000000
011111
000000
000000
000000
000000
CHAR 46
000000
000000
000000
000000
000000
001100
001100
000000
CHAR 47
000000
000001
000010
000100
001000
010000
000000
000000
CHAR 48
001110
010001
010011
010101
011001
010001
001110
000000
CHAR 49
000100
001100
000100
000100
000100
000100
001110
000000
CHAR 50
001110
010001
000001
000110
001000
010000
011111
000000
CHAR 51
001110
010001
000001
001110
000001
010001
001110
000000
CHAR 52
000010
000110
001010
010010
011111
000010
000010
000000
CHAR 53
011111
010000
010000
011110
000001
010001
001110
000000
CHAR 54
000110
001000
010000
011110
010001
010001
001110
000000
CHAR 55
011111
000001
000010
000100
001000
001000
001000
000000
CHAR 56
001110
010001
010001
001110
010001
010001
001110
000000
CHAR 57
001110
010001
010001
001111
000001
000010
001100
000000
CHAR 58
000000
000000
001100
001100
000000
001100
001100
000000
CHAR 59
000000
000000
001100
001100
000000
001100
001100
001000
CHAR 60
000010
000100
001000
010000
001000
000100
000010
000000
CHAR 61
000000
000000
011111
000000
000000
011111
000000
000000
CHAR 62
001000
000100
000010
000001
000010
000100
001000
000000
CHAR 63
001110
010001
000001
000110
000100
000000
000100
000000
CHAR 64
001110
010001
010111
010101
010111
010000
001110
000000
CHAR 65
001110
010001
010001
010001
011111
010001
010001
000000
CHAR 66
011110
010001
010001
011110
010001
010001
011110
000000
CHAR 67
001110
010001
010000
010000
010000
010001
001110
000000
CHAR 68
011110
010001
010001
010001
010001
010001
011110
000000
CHAR 69
011111
010000
010000
011110
010000
010000
011111
000000
CHAR 70
011111
010000
010000
011110
010000
010000
010000
000000
CHAR 71
001110
010001
010000
010111
010001
010001
001111
000000
CHAR 72
010001
010001
010001
011111
010001
010001
010001
000000
CHAR 73
001110
000100
000100
000100
000100
000100
001110
000000
CHAR 74
000001
000001
000001
000001
010001
010001
001110
000000
CHAR 75
010001
010010
010100
011000
010100
010010
010001
000000
CHAR 76
010000
010000
010000
010000
010000
010000
011111
000000
CHAR 77
010001
011011
010101
010001
010001
010001
010001
000000
CHAR 78
010001
011001
010101
010011
010001
010001
010001
000000
CHAR 79
001110
010001
010001
010001
010001
010001
001110
000000
CHAR 80
011110
010001
010001
011110
010000
010000
010000
000000
CHAR 81
001110
010001
010001
010001
010101
010010
001101
000000
CHAR 82
011110
010001
010001
011110
010010
010001
010001
000000
CHAR 83
001110
010001
010000
001110
000001
010001
001110
000000
CHAR 84
011111
000100
000100
000100
000100
000100
000100
000000
CHAR 85
010001
010001
010001
010001
010001
010001
001110
000000
CHAR 86
010001
010001
010001
010001
010001
001010
000100
000000
CHAR 87
010001
010001
010101
010101
010101
010101
001010
000000
CHAR 88
010001
010001
001010
000100
001010
010001
010001
000000
CHAR 89
010001
010001
010001
001010
000100
000100
000100
000000
CHAR 90
011110
000010
000100
001000
010000
010000
011110
000000
CHAR 91
001110
001000
001000
001000
001000
001000
001110
000000
CHAR 92
000000
010000
001000
000100
000010
000001
000000
000000
CHAR 93
001110
000010
000010
000010
000010
000010
001110
000000
CHAR 94
000100
001010
010001
000000
000000
000000
000000
000000
CHAR 95
000000
000000
000000
000000
000000
000000
000000
111111
CHAR 96
001100
001100
000100
000000
000000
000000
000000
000000
CHAR 97
000000
000000
001110
000001
001111
010001
001111
000000
CHAR 98
010000
010000
011110
010001
010001
010001
011110
000000
CHAR 99
000000
000000
001110
010001
010000
010001
001110
000000
CHAR 100
000001
000001
001111
010001
010001
010001
001111
000000
CHAR 101
000000
000000
001110
010001
011110
010000
001110
000000
CHAR 102
000110
001000
001000
011110
001000
001000
001000
000000
CHAR 103
000000
000000
001111
010001
010001
001111
000001
001110
CHAR 104
010000
010000
011100
010010
010010
010010
010010
000000
CHAR 105
000100
000000
000100
000100
000100
000100
000110
000000
CHAR 106
000010
000000
000110
000010
000010
000010
010010
001100
CHAR 107
010000
010000
010010
010100
011000
010100
010010
000000
CHAR 108
000100
000100
000100
000100
000100
000100
000110
000000
CHAR 109
000000
000000
011010
010101
010101
010001
010001
000000
CHAR 110
000000
000000
011100
010010
010010
010010
010010
000000
CHAR 111
000000
000000
001110
010001
010001
010001
001110
000000
CHAR 112
000000
000000
011110
010001
010001
010001
011110
010000
CHAR 113
000000
000000
001111
010001
010001
010001
001111
000001
CHAR 114
000000
000000
010110
001001
001000
001000
011100
000000
CHAR 115
000000
000000
001110
010000
001110
000001
001110
000000
CHAR 116
000000
001000
011110
001000
001000
001010
000100
000000
CHAR 117
000000
000000
010010
010010
010010
010110
001010
000000
CHAR 118
000000
000000
010001
010001
010001
001010
000100
000000
CHAR 119
000000
000000
010001
010001
010101
011111
001010
000000
CHAR 120
000000
000000
010010
010010
001100
010010
010010
000000
CHAR 121
000000
000000
010010
010010
010010
001110
000100
011000
CHAR 122
000000
000000
011110
000010
001100
010000
011110
000000
CHAR 123
000110
001000
001000
011000
001000
001000
000110
000000
CHAR 124
000100
000100
000100
000100
000100
000100
000100
000100
CHAR 125
001100
000010
000010
000011
000010
000010
001100
000000
CHAR 126
001010
010100
000000
000000
000000
000000
000000
000000
CHAR 127
000100
001110
011011
010001
010001
011111
000000
000000
CHAR 128
001110
010001
010000
010000
010001
001110
000100
001100
CHAR 129
010010
000000
010010
010010
010010
010110
001010
000000
CHAR 130
000011
000000
001110
010001
011110
010000
001110
000000
CHAR 131
001110
000000
001110
000001
001111
010001
001111
000000
CHAR 132
001010
000000
001110
000001
001111
010001
001111
000000
CHAR 133
001100
000000
001110
000001
001111
010001
001111
000000
CHAR 134
001110
001010
001110
000001
001111
010001
001111
000000
CHAR 135
000000
001110
010001
010000
010001
001110
000100
001100
CHAR 136
001110
000000
001110
010001
011110
010000
001110
000000
CHAR 137
001010
000000
001110
010001
011110
010000
001110
000000
CHAR 138
001100
000000
001110
010001
011110
010000
001110
000000
CHAR 139
001010
000000
000100
000100
000100
000100
000110
000000
CHAR 140
000100
001010
000000
000100
000100
000100
000110
000000
CHAR 141
001000
000000
000100
000100
000100
000100
000110
000000
CHAR 142
001010
000000
000100
001010
010001
011111
010001
000000
CHAR 143
001110
001010
001110
011011
010001
011111
010001
000000
CHAR 144
000011
000000
011111
010000
011110
010000
011111
000000
CHAR 145
000000
000000
011110
000101
011111
010100
001111
000000
CHAR 146
001111
010100
010100
011111
010100
010100
010111
000000
CHAR 147
001110
000000
001100
010010
010010
010010
001100
000000
CHAR 148
001010
000000
001100
010010
010010
010010
001100
000000
CHAR 149
011000
000000
001100
010010
010010
010010
001100
000000
CHAR 150
001110
000000
010010
010010
010010
010110
001010
000000
CHAR 151
011000
000000
010010
010010
010010
010110
001010
000000
CHAR 152
001010
000000
010010
010010
010010
001110
000100
011000
CHAR 153
010010
001100
010010
010010
010010
010010
001100
000000
CHAR 154
001010
000000
010010
010010
010010
010010
001100
000000
CHAR 155
000000
000100
001110
010000
010000
001110
000100
000000
CHAR 156
000110
001001
001000
011110
001000
001001
010111
000000
CHAR 157
010001
001010
000100
011111
000100
011111
000100
000000
CHAR 158
011000
010100
010100
011010
010111
010010
010010
000000
CHAR 159
000010
000101
000100
001110
000100
000100
010100
001000
CHAR 160
000110
000000
001110
000001
001111
010001
001111
000000
CHAR 161
000110
000000
000100
000100
000100
000100
000110
000000
CHAR 162
000110
000000
001100
010010
010010
010010
001100
000000
CHAR 163
000110
000000
010010
010010
010010
010110
001010
000000
CHAR 164
001010
010100
000000
011100
010010
010010
010010
000000
CHAR 165
001010
010100
000000
010010
011010
010110
010010
000000
CHAR 166
001110
000001
001111
010001
001111
000000
001111
000000
CHAR 167
001100
010010
010010
010010
001100
000000
011110
000000
CHAR 168
000100
000000
000100
001100
010000
010001
001110
000000
CHAR 169
000000
000000
011111
010000
010000
010000
000000
000000
CHAR 170
000000
000000
111111
000001
000001
000000
000000
000000
CHAR 171
010000
010010
010100
001110
010001
000010
000111
000000
CHAR 172
010000
010010
010100
001011
010101
000111
000001
000000
CHAR 173
000100
000000
000100
000100
001110
001110
000100
000000
CHAR 174
000000
000000
001001
010010
001001
000000
000000
000000
CHAR 175
000000
000000
010010
001001
010010
000000
000000
000000
CHAR 176
010101
000000
101010
000000
010101
000000
101010
000000
CHAR 177
010101
101010
010101
101010
010101
101010
010101
101010
CHAR 178
101010
111111
010101
111111
101010
111111
010101
111111
CHAR 179
000100
000100
000100
000100
000100
000100
000100
000100
CHAR 180
000100
000100
000100
111100
000100
000100
000100
000100
CHAR 181
000000
000000
010010
010010
010010
011100
010000
010000
CHAR 182
010100
010100
010100
110100
010100
010100
010100
010100
CHAR 183
000000
000000
000000
111100
010100
010100
010100
010100
CHAR 184
000000
111100
000100
111100
000100
000100
000100
000100
CHAR 185
010100
110100
000100
110100
010100
010100
010100
010100
CHAR 186
010100
010100
010100
010100
010100
010100
010100
010100
CHAR 187
000000
111100
000100
110100
010100
010100
010100
010100
CHAR 188
010100
110100
000100
111100
000000
000000
000000
000000
CHAR 189
010100
010100
010100
111100
000000
000000
000000
000000
CHAR 190
000100
111100
000100
111100
000000
000000
000000
000000
CHAR 191
000000
000000
000000
111100
000100
000100
000100
000100
CHAR 192
000100
000100
000100
000111
000000
000000
000000
000000
CHAR 193
000100
000100
000100
111111
000000
000000
000000
000000
CHAR 194
000000
000000
000000
111111
000100
000100
000100
000100
CHAR 195
000100
000100
000100
000111
000100
000100
000100
000100
CHAR 196
000000
000000
000000
111111
000000
000000
000000
000000
CHAR 197
000100
000100
000100
111111
000100
000100
000100
000100
CHAR 198
000100
000111
000100
000111
000100
000100
000100
000100
CHAR 199
010100
010100
010100
010111
010100
010100
010100
010100
CHAR 200
010100
010111
010000
011111
000000
000000
000000
000000
CHAR 201
000000
011111
010000
010111
010100
010100
010100
010100
CHAR 202
010100
110111
000000
111111
000000
000000
000000
000000
CHAR 203
000000
111111
000000
110111
010100
010100
010100
010100
CHAR 204
010100
010111
010000
010111
010100
010100
010100
010100
CHAR 205
000000
111111
000000
111111
000000
000000
000000
000000
CHAR 206
010100
110111
000000
110111
010100
010100
010100
010100
CHAR 207
000100
111111
000000
111111
000000
000000
000000
000000
CHAR 208
010100
010100
010100
111111
000000
000000
000000
000000
CHAR 209
000000
111111
000000
111111
000100
000100
000100
000100
CHAR 210
000000
000000
000000
111111
010100
010100
010100
010100
CHAR 211
010100
010100
010100
011111
000000
000000
000000
000000
CHAR 212
000000
000000
000000
000000
000000
000000
000000
111111
CHAR 213
000000
000000
000000
000000
000000
000000
111111
111111
CHAR 214
000000
000000
000000
000000
000000
111111
111111
111111
CHAR 215
000000
000000
000000
000000
111111
111111
111111
111111
CHAR 216
000000
000000
000000
111111
111111
111111
111111
111111
CHAR 217
000000
000000
111111
111111
111111
111111
111111
111111
CHAR 218
000000
111111
111111
111111
111111
111111
111111
111111
CHAR 219
111111
111111
111111
111111
111111
111111
111111
111111
CHAR 220
100000
100000
100000
100000
100000
100000
100000
100000
CHAR 221
110000
110000
110000
110000
110000
110000
110000
110000
CHAR 222
111000
111000
111000
111000
111000
111000
111000
111000
CHAR 223
111100
111100
111100
111100
111100
111100
111100
111100
CHAR 224
111110
111110
111110
111110
111110
111110
111110
111110
CHAR 225
000000
011100
010010
011100
010010
010010
011100
010000
CHAR 226
011110
010010
010000
010000
010000
010000
010000
000000
CHAR 227
000000
011111
001010
001010
001010
001010
001010
000000
CHAR 228
001010
000000
001110
000001
001111
010001
001111
000000
CHAR 229
000000
000000
001111
010010
010010
001100
000000
000000
CHAR 230
000000
000000
010010
010010
010010
011100
010000
010000
CHAR 231
000000
000000
001010
010100
000100
000100
000100
000000
CHAR 232
001110
000100
001110
010001
001110
000100
001110
000000
CHAR 233
001100
010010
010010
011110
010010
010010
001100
000000
CHAR 234
000000
001110
010001
010001
001010
001010
011011
000000
CHAR 235
001100
010000
001000
000100
001110
010010
001100
000000
CHAR 236
000000
000000
001010
010101
010101
001010
000000
000000
CHAR 237
000000
000100
001110
010101
010101
001110
000100
000000
CHAR 238
000000
001110
010000
011110
010000
001110
000000
000000
CHAR 239
000000
001100
010010
010010
010010
010010
000000
000000
CHAR 240
000000
011110
000000
011110
000000
011110
000000
000000
CHAR 241
000000
000100
001110
000100
000000
001110
000000
000000
CHAR 242
010000
001100
000010
001100
010000
000000
011110
000000
CHAR 243
000000
000000
111111
111000
100110
100001
100000
111111
CHAR 244
000000
000000
111111
000111
011001
100001
000001
111111
CHAR 245
000100
000100
000100
000100
000100
010100
001000
000000
CHAR 246
001010
000000
001110
010001
010001
010001
001110
000000
CHAR 247
111110
111110
111110
111110
111110
111110
111110
111110
CHAR 248
111100
111100
111100
111100
111100
111100
111100
111100
CHAR 249
111000
111000
111000
111000
111000
111000
111000
111000
CHAR 250
110000
110000
110000
110000
110000
110000
110000
110000
CHAR 251
100000
100000
100000
100000
100000
100000
100000
100000
CHAR 252
001010
000000
010010
010010
010010
010110
001010
000000
CHAR 253
011000
000100
001000
011100
000000
000000
000000
000000
CHAR 254
000000
000000
000000
011110
110010
110011
111110
001111
CHAR 255
010010
111111
010010
010010
111111
010010
000000
000000







	
	



Monday 04/01/2020{shk}{shk} Monday 04/01/2020{shk}{shk} # BITSY VERSION 7.2 ! ROOM_FORMAT 1 PAL 0 NAME blueprint 0,82,204 128,159,255 255,255,255 PAL 1 2,0,14 255,255,255 172,68,75 ROOM 0 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 0,a,a,a,a,a,a,a,a,a,a,a,a,a,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,0,0,0,0,0,0,0,0,0,0,0,0,a,0 0,a,a,a,a,a,a,a,a,a,a,a,a,a,a,0 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 NAME example room PAL 0 ROOM 2 0,2r,2s,2s,2r,2s,2r,2s,2r,2s,2r,2s,2r,2s,2r,0 2s,b,c,c,c,c,c,c,c,c,c,c,c,c,d,2r 2s,g,0,0,0,0,0,1n,1o,1o,1o,1o,1o,0,e,2r 2s,g,2u,2v,2v,2w,0,3k,1u,1t,1t,1v,0,2k,e,2r 2s,g,36,33,33,37,0,3k,1r,20,20,1r,0,2k,e,2r 2s,g,36,33,33,37,0,2h,1r,22,0,1r,0,1h,e,2r 2s,g,32,0,0,2x,0,3k,1r,23,24,1r,0,2k,e,2r 2s,g,32,0,0,2x,0,3k,2n,2n,2n,2n,2n,1h,e,2r 2s,g,30,2z,2z,2y,0,1l,1j,1j,1j,1j,2m,1i,e,2r 2s,g,0,0,0,0,0,0,0,0,0,0,0,0,e,2r 2s,f,f,f,f,f,f,f,f,f,f,f,f,f,f,2r 0,g,0,0,0,0,0,0,0,0,0,0,0,1d,e,0 0,g,l,m,n,o,p,10,10,t,s,v,w,a,e,0 0,g,x,y,z,10,11,12,13,14,15,16,17,a,e,0 0,g,0,1a,15,v,x,z,11,y,12,u,t,a,e,0 0,j,h,h,h,h,0,h,h,h,h,0,h,h,i,0 NAME room1 EXT 13,2 3 13,13 PAL 1 ROOM 3 0,2r,2s,2s,2r,2s,2r,2s,2r,2s,2r,2s,2r,2s,2r,0 2s,b,c,c,c,c,c,c,c,c,c,c,c,c,d,2r 2s,g,0,0,0,0,0,1n,1o,1o,1o,1o,1o,0,e,2r 2s,g,2u,2v,2v,2w,0,3h,0,0,0,0,0,2k,e,2r 2s,g,36,33,33,37,0,3h,0,3g,38,39,0,2k,e,2r 2s,g,36,33,33,37,0,3h,0,3f,1m,3a,0,1h,e,2r 2s,g,32,0,0,2x,0,3h,0,3e,3c,3b,0,2k,e,2r 2s,g,32,0,0,2x,0,3h,0,0,0,0,0,1h,e,2r 2s,g,30,2z,2z,2y,0,1l,1j,1j,1j,1j,3i,3j,e,2r 2s,g,0,0,0,0,0,0,0,0,0,0,0,3h,e,2r 2s,f,f,f,f,f,f,f,f,f,f,f,f,f,f,2r 0,g,0,0,0,0,0,0,0,0,0,0,0,1d,e,0 0,g,l,m,n,o,p,10,10,t,s,v,w,a,e,0 0,g,x,y,z,10,11,12,13,14,15,16,17,a,e,0 0,g,0,1a,15,v,x,z,11,y,12,u,t,a,e,0 0,j,h,h,h,h,0,h,h,h,h,0,h,h,i,0 NAME changeroom END 6 13,2 PAL 1 TIL 10 00000000 00111100 00100000 00100000 00111100 00100000 00100000 00000000 TIL 11 00000000 00001100 00010000 00100000 00101100 00100100 00111100 00000000 TIL 12 00000000 00100100 00100100 00100100 00111100 00100100 00100100 00000000 TIL 13 00000000 00000100 00000100 00000100 00000100 00000100 00111000 00000000 TIL 14 00000000 00100100 00101000 00110000 00110000 00101000 00100100 00000000 TIL 15 00000000 00100000 00100000 00100000 00100000 00100000 00111100 00000000 TIL 16 00000000 00001000 00001000 00001000 00001000 00001000 00010000 00000000 TIL 17 00000000 00001010 01111110 00010100 01111110 00101000 01010000 00000000 TIL 18 00000000 00000000 00001010 01010100 00101010 01010000 00101000 01010000 TIL 19 00000000 00111100 00000100 00001000 00010000 00100000 00111100 00000000 TIL 20 00100100 01000010 00011000 00100100 01010010 00100100 00011000 00000000 > 00100100 01000010 00011000 00100100 01001010 00100100 00011000 00000000 TIL 21 00000000 00000000 00001000 00010000 00100000 00011000 00000000 00000000 TIL 22 00000001 00000010 00000100 00000011 00000000 00000010 00001000 00000100 TIL 23 10000000 01000000 00110000 00001111 00001000 00010000 00100000 00100000 TIL 24 00000000 00000001 00000110 11111000 00000000 00000000 00000000 00000000 TIL 25 11111111 10000000 10000000 10000000 10000000 10000000 10000000 10000000 TIL 26 11111111 00000001 00000001 00000001 00000001 00000001 00000001 00000001 TIL 27 00000001 00000001 00000001 00000001 00000001 00000001 00000001 11111111 TIL 28 10000000 10000000 10000000 10000000 10000000 10000000 10000000 11111111 TIL 29 11111111 00000001 00000001 00000001 00000001 00000001 00000001 00000001 TIL 30 10000000 10000000 10000000 10000000 10000000 10000000 10000000 11111111 TIL 31 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 32 10000000 10000000 10000000 10000000 10000000 10000000 10000000 10000000 TIL 33 00000000 01010101 10101010 00000000 01010100 10101010 00000000 00000000 TIL 34 00000000 01010101 10101010 00000000 01010100 10101010 00000000 00000000 TIL 35 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 36 10000000 11010101 10101010 10000000 11010101 10101010 10000000 10000000 TIL 37 00000001 10101011 01010101 00000001 10101011 01010101 00000001 00000001 TIL 38 00011000 00100100 01000010 10011001 00100100 01000010 10000001 00000000 > 00011000 00100100 01000010 10000001 00000000 00000000 00011000 00100100 TIL 39 00000000 10000000 01000000 00100000 10010000 01001000 00100100 00010010 TIL a 11110111 10000001 10000001 10011001 10011001 10000001 10000001 11111111 NAME block TIL b 11111111 11111111 11000000 11011111 11010000 11010000 11010000 11010000 TIL c 11111111 11111111 00000000 11111111 00000000 00000000 00000000 00000000 TIL d 11111111 11111111 00000011 11111011 00001011 00001011 00001011 00001011 TIL e 00001011 00001011 00001011 00001011 00001011 00001011 00001011 00001011 TIL f 11111111 11111111 00000000 00000000 00000000 00000000 11111111 11111111 TIL g 11010000 11010000 11010000 11010000 11010000 11010000 11010000 11010000 TIL h 00000000 00000000 00000000 00000000 11111111 00000000 11111111 11111111 TIL i 00001011 00001011 00001011 00001011 00001011 00001011 11111111 11111111 TIL j 11010000 11010000 11010000 11010000 11010000 11010000 11111111 11111111 TIL k 00000000 00111100 00100100 00100100 00111100 00100100 00100100 00000000 TIL l 00000000 00111100 00100100 00100100 00100100 00111100 00000010 00000000 TIL m 00000000 01000010 01000010 01011010 01011010 01011010 00111100 00000000 TIL n 00000000 00111100 00100100 00111100 00100000 00100000 00111100 00000000 TIL o 00000000 00111100 01100100 01000100 01001000 01010000 01001100 00000000 TIL p 00000000 01111110 00011000 00011000 00011000 00011000 00011000 00000000 TIL q 00000000 00100100 00100100 00010100 00001100 00000100 00000100 00000000 TIL r 00000000 01000010 01000010 00111100 00011000 00011000 00011000 00000000 TIL s 00000000 00100100 00100100 00100100 00100100 00111100 00111100 00000000 TIL t 00000000 00011000 00011000 00011000 00011000 00011000 00011000 00000000 TIL u 00000000 00111100 01000010 01000010 01000010 01000010 00111100 00000000 TIL v 00000000 00111100 00100100 00100100 00111100 00100000 00100000 00000000 TIL w 00000000 00111100 00100000 00100000 01100000 00100000 00111100 00000000 TIL x 00000000 00011000 00100100 00100100 00111100 00100100 00100100 00000000 TIL y 00000000 00011100 00100000 00111100 00000100 00000100 00111000 00000000 TIL z 00000000 00111100 00100100 01000100 01000100 01000100 01111000 00000000 TIL 1a 00000000 01000010 00100100 00011000 00011000 00100100 01000010 00000000 TIL 1b 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 1c 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 1d 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 1e 00000000 01111111 01000000 01000000 01000000 01000000 01000000 01000000 TIL 1f 00000000 11111111 00000000 00000000 00000000 00000000 00000000 00000000 TIL 1g 00000000 11111111 00000001 00000001 00000001 00000001 00000001 00000001 TIL 1h 00000001 00000001 00000001 00000001 00000001 00000001 00000001 00000001 TIL 1i 00000001 00000001 00000001 00000001 00000001 00000001 00000001 11111111 TIL 1j 00000000 00000000 00000000 00000000 00000000 00000000 00000000 11111111 TIL 1k 10000000 10000000 10000000 10000000 10000000 10000000 10000000 11111111 TIL 1l 10000000 10000000 10000000 10000000 10000000 10000000 10000000 11111111 TIL 1m 00000000 00001000 00010100 00101010 00101010 00010100 00001000 00000000 TIL 1n 11111111 10000000 10000000 10000000 10000000 10000000 10000000 10000000 TIL 1o 11111111 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 1p 11111111 00000001 00000001 00000001 00000001 00000001 00000001 00000001 TIL 1q 00000111 00001010 00010100 00101000 01010000 10100000 01000000 10000000 TIL 1r 01010100 00101010 01010100 00101010 01010100 00101010 01010100 00101010 TIL 1s 11111111 10101010 00000000 00000000 00000000 00000000 00000000 00000000 TIL 1t 11111111 01010101 10101010 01010101 10101010 00000000 00000000 00000000 TIL 1u 00000001 00000010 00000101 00001010 00010101 00101010 01010101 10101010 TIL 1v 10000000 01000000 10100000 01010000 10101000 01010100 10101010 01010100 TIL 1w 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 1x 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 1y 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 1z 00000101 00000010 00000101 00000010 00000101 00000010 00000101 00000010 TIL 2a 11111111 10000000 10000000 10000000 10000000 10000000 10000000 10000000 TIL 2b 11111111 00000001 00000001 00000001 00000001 00000001 00000001 00000001 TIL 2c 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 2d 11111111 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 2e 11111111 10000000 10000000 11111111 10000000 10000000 10000000 10000000 TIL 2f 11111111 00000000 00000000 11111111 00000000 00000000 00000000 00000000 TIL 2g 11111111 00000001 00000001 11111111 00000001 00000001 00000001 00000001 TIL 2h 10000000 10000000 10000000 10000000 10000000 10000000 10000000 10000000 TIL 2i 00000001 00000001 00000001 00000001 00000001 00000001 00000001 11111111 TIL 2j 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 2k 00000001 00000001 00000001 00000001 00000001 00000001 00000001 00000001 TIL 2l 11111111 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 2m 00000000 00000000 00000000 00000000 00000000 00000000 00000000 11111111 TIL 2n 00000000 00000000 00000000 00010100 00101010 01010101 10101010 01010101 > 00000000 00000000 00000000 00000000 00000010 01000101 10101010 01010101 TIL 2o 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 2p 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 2q 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 2r 00100100 00010010 00001001 00000100 10000010 01000001 00100000 00010000 TIL 2s 01001000 10010000 00100000 01000000 10000001 00000010 00000100 00001000 TIL 2t 11111111 11111111 11000000 11011111 11010000 11010000 11010000 11010000 TIL 2u 11111111 10000000 10000000 11111111 10000000 10000000 10000000 10000000 TIL 2v 11111111 00000000 00000000 11111111 00000000 00000000 00000000 00000000 TIL 2w 11111111 00000001 00000001 11111111 00000001 00000001 00000001 00000001 TIL 2x 00000001 00000001 00000001 00000001 00000001 00000001 00000001 00000001 TIL 2y 00000001 00000001 00000001 00000001 00000001 00000001 00000001 11111111 TIL 2z 00000000 00000000 00000000 00000000 00000000 00000000 00000000 11111111 TIL 3a 01001000 00100100 00010010 00001001 00000100 00001001 00010010 00100100 > 01001000 00100100 10010010 01001001 00100100 01001001 10010010 00100100 TIL 3b 00001001 00010010 00100100 01001000 10010000 00100000 01000000 10000000 TIL 3c 01000010 00100100 10011001 01000010 00100100 10000001 01000010 00111100 > 01000010 00100100 10000001 01000010 00100100 10011001 01000010 00111100 TIL 3d 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 TIL 3e 00010000 10001000 01000100 00100010 00010001 00001000 00000100 00000010 TIL 3f 00010010 00100100 01001000 10010000 00100000 10010000 01001000 00100100 > 00010010 00100100 01001001 10010010 00100100 10010010 01001001 00100100 TIL 3g 00000000 00000001 00000010 00000100 00001001 00010010 00100100 01001000 TIL 3h 10000000 10000000 10000000 10000000 10000000 10000000 10000000 10000000 TIL 3i 00000000 00000000 00000000 00000000 00000000 00000000 00000000 11111111 TIL 3j 00000001 00000001 00000001 00000001 00000001 00000001 00000001 11111111 TIL 3k 10000000 10000000 10000000 10000000 10000000 10000000 10000000 10000000 TIL 3l 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 SPR A 00000000 00000000 00011000 00111100 01111110 01111110 00011000 00011000 POS 2 9,9 SPR a 00000000 00000000 01010001 01110001 01110010 01111100 00111100 00100100 NAME cat DLG 0 POS 3 13,9 SPR b 11000001 01000010 00100100 00011000 00011000 00100100 01000010 10000001 DLG 3 POS 2 5,4 SPR c 00000001 00000010 00000100 10001000 01010000 00100000 00000000 00000000 DLG 4 POS 3 5,4 SPR d 00000000 01101110 01001000 01100100 01000010 01101110 00000000 00000000 DLG 5 POS 3 13,12 ITM 0 00000000 00000000 00000000 00111100 01100100 00100100 00011000 00000000 NAME tea DLG 1 ITM 1 00000000 00111100 00100100 00111100 00010000 00011000 00010000 00011000 NAME key DLG 2 DLG 0 Woof! Shhh, I’m in a meeting. NAME cat dialog DLG 1 You found a nice warm cup of tea NAME tea dialog DLG 2 A key! {wvy}What does it open?{wvy} NAME key dialog DLG 3 Another day chained to the desk teaching. By midday, all of my students faces blur into one. I change things up, I plan my lessons, but the same dead eye expression.{shk}{shk} Zoom is the mind killer. NAME sprite b dialog DLG 4 Tick tock, check, next. Boulot, metro, dodo. NAME sprite c dialog DLG 5 Escape, where, it’s office hours? NAME sprite d dialog DLG 6 Another day done. At first working from home was a novelty. But, for me it’s been almost three years. I even miss catching the train. NAME ending 1 VAR a 42 html { margin:0px; padding:0px; } body { margin:0px; padding:0px; overflow:hidden; background:#ffffff; } #game { background:black; width:100vw; max-width:100vh; margin:auto; display:block; } function startExportedGame() { attachCanvas( document.getElementById(“game”) ); load_game( document.getElementById(“exportedGameData”).text.slice(1) ); } //hex-to-rgb method borrowed from stack overflow function hexToRgb(hex) { // Expand shorthand form (e.g. “03F”) to full form (e.g. “0033FF”) var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; hex = hex.replace(shorthandRegex, function(m, r, g, b) { return r + r + g + g + b + b; }); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; } function componentToHex(c) { var hex = c.toString(16); return hex.length == 1 ? “0” + hex : hex; } function rgbToHex(r, g, b) { return “#” + componentToHex(Math.floor(r)) + componentToHex(Math.floor(g)) + componentToHex(Math.floor(b)); } function hslToHex(h,s,l) { var rgbArr = hslToRgb(h,s,l); return rgbToHex( Math.floor(rgbArr[0]), Math.floor(rgbArr[1]), Math.floor(rgbArr[2]) ); } function hexToHsl(hex) { var rgb = hexToRgb(hex); return rgbToHsl(rgb.r, rgb.g, rgb.b); } // really just a vector distance function colorDistance(a1,b1,c1,a2,b2,c2) { return Math.sqrt( Math.pow(a1 – a2, 2) + Math.pow(b1 – b2, 2) + Math.pow(c1 – c2, 2) ); } function hexColorDistance(hex1,hex2) { var color1 = hexToRgb(hex1); var color2 = hexToRgb(hex2); return rgbColorDistance(color1.r, color1.g, color1.b, color2.r, color2.g, color2.b); } // source : http://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c /* accepts parameters * h Object = {h:x, s:y, v:z} * OR * h, s, v */ function HSVtoRGB(h, s, v) { var r, g, b, i, f, p, q, t; if (arguments.length === 1) { s = h.s, v = h.v, h = h.h; } i = Math.floor(h * 6); f = h * 6 – i; p = v * (1 – s); q = v * (1 – f * s); t = v * (1 – (1 – f) * s); switch (i % 6) { case 0: r = v, g = t, b = p; break; case 1: r = q, g = v, b = p; break; case 2: r = p, g = v, b = t; break; case 3: r = p, g = q, b = v; break; case 4: r = t, g = p, b = v; break; case 5: r = v, g = p, b = q; break; } return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }; } /* accepts parameters * r Object = {r:x, g:y, b:z} * OR * r, g, b */ function RGBtoHSV(r, g, b) { if (arguments.length === 1) { g = r.g, b = r.b, r = r.r; } var max = Math.max(r, g, b), min = Math.min(r, g, b), d = max – min, h, s = (max === 0 ? 0 : d / max), v = max / 255; switch (max) { case min: h = 0; break; case r: h = (g – b) + d * (g < b ? 6: 0); h /= 6 * d; break; case g: h = (b – r) + d * 2; h /= 6 * d; break; case b: h = (r – g) + d * 4; h /= 6 * d; break; } return { h: h, s: s, v: v }; } // source : https://gist.github.com/mjackson/5311256 /** * Converts an HSL color value to RGB. Conversion formula * adapted from http://en.wikipedia.org/wiki/HSL_color_space. * Assumes h, s, and l are contained in the set [0, 1] and * returns r, g, and b in the set [0, 255]. * * @param Number h The hue * @param Number s The saturation * @param Number l The lightness * @return Array The RGB representation */ function hslToRgb(h, s, l) { var r, g, b; if (s == 0) { r = g = b = l; // achromatic } else { function hue2rgb(p, q, t) { if (t 1) t -= 1; if (t < 1/6) return p + (q – p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q – p) * (2/3 – t) * 6; return p; } var q = l 0.5 ? d / (2 – max – min) : d / (max + min); switch(max){ case r: h = (g – b) / d + (g < b ? 6 : 0); break; case g: h = (b – r) / d + 2; break; case b: h = (r – g) / d + 4; break; } h /= 6; } return [h, s, l]; } var TransitionManager = function() { var transitionStart = null; var transitionEnd = null; var effectImage = null; var isTransitioning = false; var transitionTime = 0; // milliseconds var frameRate = 8; // cap the FPS var prevStep = -1; // used to avoid running post-process effect constantly this.BeginTransition = function(startRoom,startX,startY,endRoom,endX,endY,effectName) { // console.log(“— START ROOM TRANSITION —“); curEffect = effectName; var tmpRoom = player().room; var tmpX = player().x; var tmpY = player().y; if (transitionEffects[curEffect].showPlayerStart) { player().room = startRoom; player().x = startX; player().y = startY; } else { player().room = “_transition_none”; // kind of hacky!! } drawRoom(room[startRoom]); var startPalette = getPal( room[startRoom].pal ); var startImage = new PostProcessImage( ctx.getImageData(0,0,canvas.width,canvas.height) ); // TODO : don’t use global ctx? transitionStart = new TransitionInfo(startImage, startPalette, startX, startY); if (transitionEffects[curEffect].showPlayerEnd) { player().room = endRoom; player().x = endX; player().y = endY; } else { player().room = “_transition_none”; } drawRoom(room[endRoom]); var endPalette = getPal( room[endRoom].pal ); var endImage = new PostProcessImage( ctx.getImageData(0,0,canvas.width,canvas.height) ); transitionEnd = new TransitionInfo(endImage, endPalette, endX, endY); effectImage = new PostProcessImage( ctx.createImageData(canvas.width,canvas.height) ); isTransitioning = true; transitionTime = 0; prevStep = -1; player().room = tmpRoom; player().x = tmpX; player().y = tmpY; } this.UpdateTransition = function(dt) { if (!isTransitioning) { return; } transitionTime += dt; var transitionDelta = transitionTime / transitionEffects[curEffect].duration; var maxStep = Math.floor(frameRate * (transitionEffects[curEffect].duration / 1000)); var step = Math.floor(transitionDelta * maxStep); if (step != prevStep) { // console.log(“step! ” + step + ” ” + transitionDelta); for (var y = 0; y < effectImage.Height; y++) { for (var x = 0; x = transitionEffects[curEffect].duration) { isTransitioning = false; transitionTime = 0; transitionStart = null; transitionEnd = null; effectImage = null; prevStep = -1; if (transitionCompleteCallback != null) { transitionCompleteCallback(); } transitionCompleteCallback = null; } } this.IsTransitionActive = function() { return isTransitioning; } // todo : should this be part of the constructor? var transitionCompleteCallback = null; this.OnTransitionComplete = function(callback) { if (isTransitioning) { // TODO : safety check necessary? transitionCompleteCallback = callback; } } var transitionEffects = {}; var curEffect = “none”; this.RegisterTransitionEffect = function(name, effect) { transitionEffects[name] = effect; } this.RegisterTransitionEffect(“none”, { showPlayerStart : false, showPlayerEnd : false, pixelEffectFunc : function() {}, }); this.RegisterTransitionEffect(“fade_w”, { // TODO : have it linger on full white briefly? showPlayerStart : false, showPlayerEnd : true, duration : 750, pixelEffectFunc : function(start,end,pixelX,pixelY,delta) { var pixelColorA = delta < 0.5 ? start.Image.GetPixel(pixelX,pixelY) : {r:255,g:255,b:255,a:255}; var pixelColorB = delta < 0.5 ? {r:255,g:255,b:255,a:255} : end.Image.GetPixel(pixelX,pixelY); delta = delta < 0.5 ? (delta / 0.5) : ((delta – 0.5) / 0.5); // hacky return PostProcessUtilities.LerpColor(pixelColorA, pixelColorB, delta); } }); this.RegisterTransitionEffect("fade_b", { showPlayerStart : false, showPlayerEnd : true, duration : 750, pixelEffectFunc : function(start,end,pixelX,pixelY,delta) { var pixelColorA = delta < 0.5 ? start.Image.GetPixel(pixelX,pixelY) : {r:0,g:0,b:0,a:255}; var pixelColorB = delta < 0.5 ? {r:0,g:0,b:0,a:255} : end.Image.GetPixel(pixelX,pixelY); delta = delta < 0.5 ? (delta / 0.5) : ((delta – 0.5) / 0.5); // hacky return PostProcessUtilities.LerpColor(pixelColorA, pixelColorB, delta); } }); this.RegisterTransitionEffect("wave", { showPlayerStart : true, showPlayerEnd : true, duration : 1500, pixelEffectFunc : function(start,end,pixelX,pixelY,delta) { var waveDelta = delta < 0.5 ? delta / 0.5 : 1 – ((delta – 0.5) / 0.5); var offset = (pixelY + (waveDelta * waveDelta * 0.2 * start.Image.Height)); var freq = 4; var size = 2 + (14 * waveDelta); pixelX += Math.floor(Math.sin(offset / freq) * size); if (pixelX = start.Image.Width) { pixelX -= start.Image.Width; } var curImage = delta < 0.5 ? start.Image : end.Image; return curImage.GetPixel(pixelX,pixelY); } }); this.RegisterTransitionEffect("tunnel", { showPlayerStart : true, showPlayerEnd : true, duration : 1500, pixelEffectFunc : function(start,end,pixelX,pixelY,delta) { if (delta start.Image.Width * tunnelDelta) { return {r:0,g:0,b:0,a:255}; } else { return start.Image.GetPixel(pixelX,pixelY); } } else if (delta end.Image.Width * tunnelDelta) { return {r:0,g:0,b:0,a:255}; } else { return end.Image.GetPixel(pixelX,pixelY); } } } }); this.RegisterTransitionEffect(“slide_u”, { showPlayerStart : false, showPlayerEnd : true, duration : 1000, pixelEffectFunc : function(start,end,pixelX,pixelY,delta) { var pixelOffset = -1 * Math.floor(start.Image.Height * delta); var slidePixelY = pixelY + pixelOffset; var colorDelta = clampLerp(delta, 0.4); if (slidePixelY >= 0) { var colorA = start.Image.GetPixel(pixelX,slidePixelY); var colorB = PostProcessUtilities.GetCorrespondingColorFromPal(colorA,start.Palette,end.Palette); var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta); return colorLerped; } else { slidePixelY += start.Image.Height; var colorB = end.Image.GetPixel(pixelX,slidePixelY); var colorA = PostProcessUtilities.GetCorrespondingColorFromPal(colorB,end.Palette,start.Palette); var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta); return colorLerped; } } }); this.RegisterTransitionEffect(“slide_d”, { showPlayerStart : false, showPlayerEnd : true, duration : 1000, pixelEffectFunc : function(start,end,pixelX,pixelY,delta) { var pixelOffset = Math.floor(start.Image.Height * delta); var slidePixelY = pixelY + pixelOffset; var colorDelta = clampLerp(delta, 0.4); if (slidePixelY = 0) { var colorA = start.Image.GetPixel(slidePixelX,pixelY); var colorB = PostProcessUtilities.GetCorrespondingColorFromPal(colorA,start.Palette,end.Palette); var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta); return colorLerped; } else { slidePixelX += start.Image.Width; var colorB = end.Image.GetPixel(slidePixelX,pixelY); var colorA = PostProcessUtilities.GetCorrespondingColorFromPal(colorB,end.Palette,start.Palette); var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta); return colorLerped; } } }); this.RegisterTransitionEffect(“slide_r”, { showPlayerStart : false, showPlayerEnd : true, duration : 1000, pixelEffectFunc : function(start,end,pixelX,pixelY,delta) { var pixelOffset = Math.floor(start.Image.Width * delta); var slidePixelX = pixelX + pixelOffset; var colorDelta = clampLerp(delta, 0.4); if (slidePixelX < start.Image.Width) { var colorA = start.Image.GetPixel(slidePixelX,pixelY); var colorB = PostProcessUtilities.GetCorrespondingColorFromPal(colorA,start.Palette,end.Palette); var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta); return colorLerped; } else { slidePixelX -= start.Image.Width; var colorB = end.Image.GetPixel(slidePixelX,pixelY); var colorA = PostProcessUtilities.GetCorrespondingColorFromPal(colorB,end.Palette,start.Palette); var colorLerped = PostProcessUtilities.LerpColor(colorA, colorB, colorDelta); return colorLerped; } } }); function clampLerp(deltaIn, clampDuration) { var clampOffset = (1.0 – clampDuration) / 2; var deltaOut = Math.min(clampDuration, Math.max(0.0, deltaIn – clampOffset)) / clampDuration; return deltaOut; } // TODO : WIP // this.RegisterTransitionEffect("fuzz", { // showPlayerStart : true, // showPlayerEnd : true, // duration : 1500, // pixelEffectFunc : function(start,end,pixelX,pixelY,delta) { // var curImage = delta <= 0.5 ? start : end; // var sampleSize = delta <= 0.5 ? (2 + Math.floor(14 * (delta/0.5))) : (16 – Math.floor(14 * ((delta-0.5)/0.5))); // var palIndex = 0; // var sampleX = Math.floor(pixelX / sampleSize) * sampleSize; // var sampleY = Math.floor(pixelY / sampleSize) * sampleSize; // var frameState = transitionEffects["fuzz"].frameState; // if (frameState.time != delta) { // frameState.time = delta; // frameState.preCalcSampleValues = {}; // } // if (frameState.preCalcSampleValues[[sampleX,sampleY]]) { // palIndex = frameState.preCalcSampleValues[[sampleX,sampleY]]; // } // else { // var paletteCount = {}; // var foregroundValue = 1.0; // var backgroundValue = 0.4; // for (var y = sampleY; y < sampleY + sampleSize; y++) { // for (var x = sampleX; x maxCount) { // palIndex = i; // maxCount = paletteCount[i]; // } // } // frameState.preCalcSampleValues[[sampleX,sampleY]] = palIndex; // } // return PostProcessUtilities.GetPalColor(curImage.Palette,palIndex); // }, // frameState : { // ok this is hacky but it’s for performance ok // time : -1, // preCalcSampleValues : {} // } // }); }; // TransitionManager() // TODO : extract the scale variable so it can be changed? var PostProcessUtilities = { SamplePixelColor : function(image,x,y) { var pixelIndex = (y * scale * image.width * 4) + (x * scale * 4); var r = image.data[pixelIndex + 0]; var g = image.data[pixelIndex + 1]; var b = image.data[pixelIndex + 2]; var a = image.data[pixelIndex + 3]; return { r:r, g:g, b:b, a:a }; }, SetPixelColor : function(image,x,y,colorRgba) { for (var yDelta = 0; yDelta < scale; yDelta++) { for (var xDelta = 0; xDelta < scale; xDelta++) { var pixelIndex = (((y * scale) + yDelta) * image.width * 4) + (((x * scale) + xDelta) * 4); image.data[pixelIndex + 0] = colorRgba.r; image.data[pixelIndex + 1] = colorRgba.g; image.data[pixelIndex + 2] = colorRgba.b; image.data[pixelIndex + 3] = colorRgba.a; } } }, LerpColor : function(colorA,colorB,t) { // TODO: move to color_util.js? return { r : colorA.r + ((colorB.r – colorA.r) * t), g : colorA.g + ((colorB.g – colorA.g) * t), b : colorA.b + ((colorB.b – colorA.b) * t), a : colorA.a + ((colorB.a – colorA.a) * t), }; }, GetColorPalIndex : function(colorIn,curPal) { var colorIndex = -1; for (var i = 0; i = 0 && colorIndex <= otherPal.length) { return PostProcessUtilities.GetPalColor(otherPal,colorIndex); } else { return colorIn; } }, }; var PostProcessImage = function(imageData) { this.Width = imageData.width / scale; this.Height = imageData.height / scale; this.GetPixel = function(x,y) { return PostProcessUtilities.SamplePixelColor(imageData,x,y); }; this.SetPixel = function(x,y,colorRgba) { PostProcessUtilities.SetPixelColor(imageData,x,y,colorRgba); }; this.GetData = function() { return imageData; }; }; var TransitionInfo = function(image, palette, playerX, playerY) { this.Image = image; this.Palette = palette; this.PlayerTilePos = { x: playerX, y: playerY }; this.PlayerCenter = { x: Math.floor((playerX * tilesize) + (tilesize / 2)), y: Math.floor((playerY * tilesize) + (tilesize / 2)) }; }; /* TODO: – can I simplify this more now that I’ve removed the external resources stuff? */ function FontManager(packagedFontNames) { var self = this; var fontExtension = “.bitsyfont”; this.GetExtension = function() { return fontExtension; } // place to store font data var fontResources = {}; // load fonts from the editor if (packagedFontNames != undefined && packagedFontNames != null && packagedFontNames.length > 0 && Resources != undefined && Resources != null) { for (var i = 0; i < packagedFontNames.length; i++) { var filename = packagedFontNames[i]; fontResources[filename] = Resources[filename]; } } // manually add resource this.AddResource = function(filename, fontdata) { fontResources[filename] = fontdata; } this.ContainsResource = function(filename) { return fontResources[filename] != null; } function GetData(fontName) { return fontResources[fontName + fontExtension]; } this.GetData = GetData; function Create(fontData) { return new Font(fontData); } this.Create = Create; this.Get = function(fontName) { var fontData = self.GetData(fontName); return self.Create(fontData); } function Font(fontData) { var name = "unknown"; var width = 6; // default size so if you have NO font or an invalid font it displays boxes var height = 8; var chardata = {}; // create invalid char data at default size in case the font is missing var invalidCharData = {}; updateInvalidCharData(); this.getName = function() { return name; } this.getData = function() { return chardata; } this.getWidth = function() { return width; } this.getHeight = function() { return height; } this.hasChar = function(char) { var codepoint = char.charCodeAt(0); return chardata[codepoint] != null; } this.getChar = function(char) { var codepoint = char.charCodeAt(0); if (chardata[codepoint] != null) { return chardata[codepoint]; } else { return invalidCharData; } } this.allCharCodes = function() { var codeList = []; for (var code in chardata) { codeList.push(code); } return codeList; } function createCharData() { return { width: width, height: height, offset: { x: 0, y: 0 }, spacing: width, data: [], }; } function updateInvalidCharData() { invalidCharData = createCharData(); for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { if (x < width-1 && y < height-1) { invalidCharData.data.push(1); } else { invalidCharData.data.push(0); } } } } function parseFont(fontData) { if (fontData == null) { return; } var lines = fontData.split("\n"); var isReadingChar = false; var isReadingCharProperties = false; var curCharLineCount = 0; var curCharCode = 0; for (var i = 0; i < lines.length; i++) { var line = lines[i]; if (line[0] === "#") { continue; // skip comment lines } if (!isReadingChar) { // READING NON CHARACTER DATA LINE var args = line.split(" "); if (args[0] == "FONT") { name = args[1]; } else if (args[0] == "SIZE") { width = parseInt(args[1]); height = parseInt(args[2]); } else if (args[0] == "CHAR") { isReadingChar = true; isReadingCharProperties = true; curCharLineCount = 0; curCharCode = parseInt(args[1]); chardata[curCharCode] = createCharData(); } } else { // CHAR PROPERTIES if (isReadingCharProperties) { var args = line.split(" "); if (args[0].indexOf("CHAR_") == 0) { // Sub-properties start with "CHAR_" if (args[0] == "CHAR_SIZE") { // Custom character size – overrides the default character size for the font chardata[curCharCode].width = parseInt(args[1]); chardata[curCharCode].height = parseInt(args[2]); chardata[curCharCode].spacing = parseInt(args[1]); // HACK : assumes CHAR_SIZE is always declared first } else if (args[0] == "CHAR_OFFSET") { // Character offset – shift the origin of the character on the X or Y axis chardata[curCharCode].offset.x = parseInt(args[1]); chardata[curCharCode].offset.y = parseInt(args[2]); } else if (args[0] == "CHAR_SPACING") { // Character spacing: // specify total horizontal space taken up by the character // lets chars take up more or less space on a line than its bitmap does chardata[curCharCode].spacing = parseInt(args[1]); } } else { isReadingCharProperties = false; } } // CHAR DATA if (!isReadingCharProperties) { // READING CHARACTER DATA LINE for (var j = 0; j = height) { isReadingChar = false; } } } } // re-init invalid character box at the actual font size once it’s loaded updateInvalidCharData(); } parseFont(fontData); } } // FontManager function Script() { this.CreateInterpreter = function() { return new Interpreter(); }; this.CreateUtils = function() { return new Utils(); }; var Interpreter = function() { var env = new Environment(); var parser = new Parser( env ); this.SetDialogBuffer = function(buffer) { env.SetDialogBuffer( buffer ); }; // TODO — maybe this should return a string instead othe actual script?? this.Compile = function(scriptName, scriptStr) { // console.log(“COMPILE”); var script = parser.Parse(scriptStr, scriptName); env.SetScript(scriptName, script); } this.Run = function(scriptName, exitHandler, objectContext) { // Runs pre-compiled script var localEnv = new LocalEnvironment(env); if (objectContext) { localEnv.SetObject(objectContext); // PROTO : should this be folded into the constructor? } var script = env.GetScript(scriptName); script.Eval( localEnv, function(result) { OnScriptReturn(localEnv, exitHandler); } ); } this.Interpret = function(scriptStr, exitHandler, objectContext) { // Compiles and runs code immediately // console.log(“INTERPRET”); var localEnv = new LocalEnvironment(env); if (objectContext) { localEnv.SetObject(objectContext); // PROTO : should this be folded into the constructor? } var script = parser.Parse(scriptStr, “anonymous”); script.Eval( localEnv, function(result) { OnScriptReturn(localEnv, exitHandler); } ); } this.HasScript = function(name) { return env.HasScript(name); }; this.ResetEnvironment = function() { env = new Environment(); parser = new Parser( env ); } this.Parse = function(scriptStr, rootId) { // parses a script but doesn’t save it return parser.Parse(scriptStr, rootId); } // TODO : add back in if needed later… // this.CompatibilityParse = function(scriptStr, compatibilityFlags) { // env.compatibilityFlags = compatibilityFlags; // var result = parser.Parse(scriptStr); // delete env.compatibilityFlags; // return result; // } this.Eval = function(scriptTree, exitHandler) { // runs a script stored externally var localEnv = new LocalEnvironment(env); // TODO : does this need an object context? scriptTree.Eval( localEnv, function(result) { OnScriptReturn(result, exitHandler); }); } function OnScriptReturn(result, exitHandler) { if (exitHandler != null) { exitHandler(result); } } this.CreateExpression = function(expStr) { return parser.CreateExpression(expStr); } this.SetVariable = function(name,value,useHandler) { env.SetVariable(name,value,useHandler); } this.DeleteVariable = function(name,useHandler) { env.DeleteVariable(name,useHandler); } this.HasVariable = function(name) { return env.HasVariable(name); } this.SetOnVariableChangeHandler = function(onVariableChange) { env.SetOnVariableChangeHandler(onVariableChange); } this.GetVariableNames = function() { return env.GetVariableNames(); } this.GetVariable = function(name) { return env.GetVariable(name); } function DebugVisualizeScriptTree(scriptTree) { var printVisitor = { Visit : function(node,depth) { console.log(“-“.repeat(depth) + “- ” + node.ToString()); }, }; scriptTree.VisitAll( printVisitor ); } this.DebugVisualizeScriptTree = DebugVisualizeScriptTree; this.DebugVisualizeScript = function(scriptName) { DebugVisualizeScriptTree(env.GetScript(scriptName)); } } var Utils = function() { // for editor ui this.CreateDialogBlock = function(children,doIndentFirstLine) { if (doIndentFirstLine === undefined) { doIndentFirstLine = true; } var block = new DialogBlockNode(doIndentFirstLine); for (var i = 0; i < children.length; i++) { block.AddChild(children[i]); } return block; } this.CreateOptionBlock = function() { var block = new DialogBlockNode(false); block.AddChild(new FuncNode("print", [new LiteralNode(" ")])); return block; } this.CreateItemConditionPair = function() { var itemFunc = this.CreateFunctionBlock("item", ["0"]); var condition = new ExpNode("==", itemFunc, new LiteralNode(1)); var result = new DialogBlockNode(true); result.AddChild(new FuncNode("print", [new LiteralNode(" ")])); var conditionPair = new ConditionPairNode(condition, result); return conditionPair; } this.CreateVariableConditionPair = function() { var varNode = this.CreateVariableNode("a"); var condition = new ExpNode("==", varNode, new LiteralNode(1)); var result = new DialogBlockNode(true); result.AddChild(new FuncNode("print", [new LiteralNode(" ")])); var conditionPair = new ConditionPairNode(condition, result); return conditionPair; } this.CreateDefaultConditionPair = function() { var condition = this.CreateElseNode(); var result = new DialogBlockNode(true); result.AddChild(new FuncNode("print", [new LiteralNode(" ")])); var conditionPair = new ConditionPairNode(condition, result); return conditionPair; } this.CreateEmptyPrintFunc = function() { return new FuncNode("print", [new LiteralNode("…")]); } this.CreateFunctionBlock = function(name, initParamValues) { var parameters = []; for (var i = 0; i -1) { dialogStr = Sym.DialogOpen + “\n” + dialogStr + “\n” + Sym.DialogClose; } return dialogStr; } this.RemoveDialogBlockFormat = function(source) { var sourceLines = source.split(“\n”); var dialogStr = “”; if(sourceLines[0] === Sym.DialogOpen) { // multi line var i = 1; while (i < sourceLines.length && sourceLines[i] != Sym.DialogClose) { dialogStr += sourceLines[i] + (sourceLines[i+1] != Sym.DialogClose ? '\n' : ''); i++; } } else { // single line dialogStr = source; } return dialogStr; } this.SerializeDialogNodeList = function(nodeList) { var tempBlock = new DialogBlockNode(false); // set children directly to avoid breaking the parenting chain for this temp operation tempBlock.children = nodeList; return tempBlock.Serialize(); } this.GetOperatorList = function() { return [Sym.Set].concat(Sym.Operators); } this.IsInlineCode = function(node) { return isInlineCode(node); } } /* BUILT-IN FUNCTIONS */ // TODO: better way to encapsulate these? function deprecatedFunc(environment,parameters,onReturn) { console.log("BITSY SCRIPT WARNING: Tried to use deprecated function"); onReturn(null); } function printFunc(environment, parameters, onReturn) { if (parameters[0] != undefined && parameters[0] != null) { var textStr = "" + parameters[0]; environment.GetDialogBuffer().AddText(textStr); environment.GetDialogBuffer().AddScriptReturn(function() { onReturn(null); }); } else { onReturn(null); } } function linebreakFunc(environment, parameters, onReturn) { // console.log("LINEBREAK FUNC"); environment.GetDialogBuffer().AddLinebreak(); environment.GetDialogBuffer().AddScriptReturn(function() { onReturn(null); }); } function pagebreakFunc(environment, parameters, onReturn) { environment.GetDialogBuffer().AddPagebreak(function() { onReturn(null); }); } function printDrawingFunc(environment, parameters, onReturn) { var drawingId = parameters[0]; environment.GetDialogBuffer().AddDrawing(drawingId); environment.GetDialogBuffer().AddScriptReturn(function() { onReturn(null); }); } function printSpriteFunc(environment,parameters,onReturn) { var spriteId = parameters[0]; if(names.sprite.has(spriteId)) spriteId = names.sprite.get(spriteId); // id is actually a name var drawingId = sprite[spriteId].drw; printDrawingFunc(environment, [drawingId], onReturn); } function printTileFunc(environment,parameters,onReturn) { var tileId = parameters[0]; if(names.tile.has(tileId)) tileId = names.tile.get(tileId); // id is actually a name var drawingId = tile[tileId].drw; printDrawingFunc(environment, [drawingId], onReturn); } function printItemFunc(environment,parameters,onReturn) { var itemId = parameters[0]; if(names.item.has(itemId)) itemId = names.item.get(itemId); // id is actually a name var drawingId = item[itemId].drw; printDrawingFunc(environment, [drawingId], onReturn); } function printFontFunc(environment, parameters, onReturn) { var allCharacters = ""; var font = fontManager.Get( fontName ); var codeList = font.allCharCodes(); for (var i = 0; i 1) { // TODO : is it a good idea to force inventory to be >= 0? player().inventory[itemId] = Math.max(0, parseInt(parameters[1])); curItemCount = player().inventory[itemId]; if (onInventoryChanged != null) { onInventoryChanged(itemId); } } onReturn(curItemCount); } function addOrRemoveTextEffect(environment,name) { if( environment.GetDialogBuffer().HasTextEffect(name) ) environment.GetDialogBuffer().RemoveTextEffect(name); else environment.GetDialogBuffer().AddTextEffect(name); } function rainbowFunc(environment,parameters,onReturn) { addOrRemoveTextEffect(environment,”rbw”); onReturn(null); } // TODO : should the colors use a parameter instead of special names? function color1Func(environment,parameters,onReturn) { addOrRemoveTextEffect(environment,”clr1″); onReturn(null); } function color2Func(environment,parameters,onReturn) { addOrRemoveTextEffect(environment,”clr2″); onReturn(null); } function color3Func(environment,parameters,onReturn) { addOrRemoveTextEffect(environment,”clr3″); onReturn(null); } function wavyFunc(environment,parameters,onReturn) { addOrRemoveTextEffect(environment,”wvy”); onReturn(null); } function shakyFunc(environment,parameters,onReturn) { addOrRemoveTextEffect(environment,”shk”); onReturn(null); } function propertyFunc(environment, parameters, onReturn) { var outValue = null; if (parameters.length > 0 && parameters[0]) { var propertyName = parameters[0]; if (environment.HasProperty(propertyName)) { // TODO : in a future update I can handle the case of initializing a new property // after which we can move this block outside the HasProperty check if (parameters.length > 1) { var inValue = parameters[1]; environment.SetProperty(propertyName, inValue); } outValue = environment.GetProperty(propertyName); } } console.log(“PROPERTY! ” + propertyName + ” ” + outValue); onReturn(outValue); } function endFunc(environment,parameters,onReturn) { isEnding = true; isNarrating = true; dialogRenderer.SetCentered(true); onReturn(null); } function exitFunc(environment,parameters,onReturn) { var destRoom = parameters[0]; if (names.room.has(destRoom)) { // it’s a name, not an id! (note: these could cause trouble if people names things weird) destRoom = names.room.get(destRoom); } var destX = parseInt(parameters[1]); var destY = parseInt(parameters[2]); if (parameters.length >= 4) { var transitionEffect = parameters[3]; transition.BeginTransition( player().room, player().x, player().y, destRoom, destX, destY, transitionEffect); transition.UpdateTransition(0); } player().room = destRoom; player().x = destX; player().y = destY; curRoom = destRoom; initRoom(curRoom); // TODO : this doesn’t play nice with pagebreak because it thinks the dialog is finished! if (transition.IsTransitionActive()) { transition.OnTransitionComplete(function() { onReturn(null); }); } else { onReturn(null); } } /* BUILT-IN OPERATORS */ function setExp(environment,left,right,onReturn) { // console.log(“SET ” + left.name); if(left.type != “variable”) { // not a variable! return null and hope for the best D: onReturn( null ); return; } right.Eval(environment,function(rVal) { environment.SetVariable( left.name, rVal ); // console.log(“VAL ” + environment.GetVariable( left.name ) ); left.Eval(environment,function(lVal) { onReturn( lVal ); }); }); } function equalExp(environment,left,right,onReturn) { // console.log(“EVAL EQUAL”); // console.log(left); // console.log(right); right.Eval(environment,function(rVal){ left.Eval(environment,function(lVal){ onReturn( lVal === rVal ); }); }); } function greaterExp(environment,left,right,onReturn) { right.Eval(environment,function(rVal){ left.Eval(environment,function(lVal){ onReturn( lVal > rVal ); }); }); } function lessExp(environment,left,right,onReturn) { right.Eval(environment,function(rVal){ left.Eval(environment,function(lVal){ onReturn( lVal = rVal ); }); }); } function lessEqExp(environment,left,right,onReturn) { right.Eval(environment,function(rVal){ left.Eval(environment,function(lVal){ onReturn( lVal “, greaterExp); operatorMap.set(“=”, greaterEqExp); operatorMap.set(“<=", lessEqExp); operatorMap.set("*", multExp); operatorMap.set("/", divExp); operatorMap.set("+", addExp); operatorMap.set("-", subExp); this.HasOperator = function(sym) { return operatorMap.get(sym); }; this.EvalOperator = function(sym,left,right,onReturn) { operatorMap.get( sym )( this, left, right, onReturn ); } var scriptMap = new Map(); this.HasScript = function(name) { return scriptMap.has(name); }; this.GetScript = function(name) { return scriptMap.get(name); }; this.SetScript = function(name,script) { scriptMap.set(name, script); }; var onVariableChangeHandler = null; this.SetOnVariableChangeHandler = function(onVariableChange) { onVariableChangeHandler = onVariableChange; } this.GetVariableNames = function() { return Array.from( variableMap.keys() ); } } // Local environment for a single run of a script: knows local context var LocalEnvironment = function(parentEnvironment) { // this.SetDialogBuffer // not allowed in local environment? this.GetDialogBuffer = function() { return parentEnvironment.GetDialogBuffer(); }; this.HasFunction = function(name) { return parentEnvironment.HasFunction(name); }; this.EvalFunction = function(name,parameters,onReturn,env) { if (env == undefined || env == null) { env = this; } parentEnvironment.EvalFunction(name,parameters,onReturn,env); } this.HasVariable = function(name) { return parentEnvironment.HasVariable(name); }; this.GetVariable = function(name) { return parentEnvironment.GetVariable(name); }; this.SetVariable = function(name,value,useHandler) { parentEnvironment.SetVariable(name,value,useHandler); }; // this.DeleteVariable // not needed in local environment? this.HasOperator = function(sym) { return parentEnvironment.HasOperator(sym); }; this.EvalOperator = function(sym,left,right,onReturn,env) { if (env == undefined || env == null) { env = this; } parentEnvironment.EvalOperator(sym,left,right,onReturn,env); }; // TODO : I don't *think* any of this is required by the local environment // this.HasScript // this.GetScript // this.SetScript // TODO : pretty sure these debug methods aren't required by the local environment either // this.SetOnVariableChangeHandler // this.GetVariableNames /* Here's where specific local context data goes: * this includes access to the object running the script * and any properties it may have (so far only "locked") */ // The local environment knows what object called it — currently only used to access properties var curObject = null; this.HasObject = function() { return curObject != undefined && curObject != null; } this.SetObject = function(object) { curObject = object; } this.GetObject = function() { return curObject; } // accessors for properties of the object that's running the script this.HasProperty = function(name) { if (curObject && curObject.property && curObject.property.hasOwnProperty(name)) { return true; } else { return false; } }; this.GetProperty = function(name) { if (curObject && curObject.property && curObject.property.hasOwnProperty(name)) { return curObject.property[name]; // TODO : should these be getters and setters instead? } else { return null; } }; this.SetProperty = function(name, value) { // NOTE : for now, we need to gaurd against creating new properties if (curObject && curObject.property && curObject.property.hasOwnProperty(name)) { curObject.property[name] = value; } }; } function leadingWhitespace(depth) { var str = ""; for(var i = 0; i < depth; i++) { str += " "; // two spaces per indent } // console.log("WHITESPACE " + depth + " ::" + str + "::"); return str; } /* NODES */ var TreeRelationship = function() { this.parent = null; this.children = []; this.AddChild = function(node) { this.children.push(node); node.parent = this; }; this.AddChildren = function(nodeList) { for (var i = 0; i < nodeList.length; i++) { this.AddChild(nodeList[i]); } }; this.SetChildren = function(nodeList) { this.children = []; this.AddChildren(nodeList); }; this.VisitAll = function(visitor, depth) { if (depth == undefined || depth == null) { depth = 0; } visitor.Visit(this, depth); for (var i = 0; i < this.children.length; i++) { this.children[i].VisitAll( visitor, depth + 1 ); } }; this.rootId = null; // for debugging this.GetId = function() { // console.log(this); if (this.rootId != null) { return this.rootId; } else if (this.parent != null) { var parentId = this.parent.GetId(); if (parentId != null) { return parentId + "_" + this.parent.children.indexOf(this); } } else { return null; } } } var DialogBlockNode = function(doIndentFirstLine) { Object.assign( this, new TreeRelationship() ); // Object.assign( this, new Runnable() ); this.type = "dialog_block"; this.Eval = function(environment, onReturn) { // console.log("EVAL BLOCK " + this.children.length); if (isPlayerEmbeddedInEditor && events != undefined && events != null) { events.Raise("script_node_enter", { id: this.GetId() }); } var lastVal = null; var i = 0; function evalChildren(children, done) { if (i > CHILD ” + i); children[i].Eval(environment, function(val) { // console.log(“<< CHILD " + i); lastVal = val; i++; evalChildren(children,done); }); } else { done(); } }; var self = this; evalChildren(this.children, function() { if (isPlayerEmbeddedInEditor && events != undefined && events != null) { events.Raise("script_node_exit", { id: self.GetId() }); } onReturn(lastVal); }); } if (doIndentFirstLine === undefined) { doIndentFirstLine = true; // This is just for serialization } this.Serialize = function(depth) { if (depth === undefined) { depth = 0; } var str = ""; var lastNode = null; for (var i = 0; i 0 && curNodeIsNonInlineCode; var shouldIndentAfterCodeBlock = prevNodeIsNonInlineCode; // need to insert a newline before the first block of non-inline code that isn’t // preceded by a {br}, since those will create their own newline if (i > 0 && curNodeIsNonInlineCode && !prevNodeIsNonInlineCode && !shouldIndentAfterLinebreak) { str += “\n”; } if (shouldIndentFirstLine || shouldIndentAfterLinebreak || shouldIndentCodeBlock || shouldIndentAfterCodeBlock) { str += leadingWhitespace(depth); } str += curNode.Serialize(depth); if (i < this.children.length-1 && curNodeIsNonInlineCode) { str += "\n"; } lastNode = curNode; } return str; } this.ToString = function() { return this.type + " " + this.GetId(); }; } var CodeBlockNode = function() { Object.assign( this, new TreeRelationship() ); this.type = "code_block"; this.Eval = function(environment, onReturn) { // console.log("EVAL BLOCK " + this.children.length); if (isPlayerEmbeddedInEditor && events != undefined && events != null) { events.Raise("script_node_enter", { id: this.GetId() }); } var lastVal = null; var i = 0; function evalChildren(children, done) { if (i > CHILD ” + i); children[i].Eval(environment, function(val) { // console.log(“<< CHILD " + i); lastVal = val; i++; evalChildren(children,done); }); } else { done(); } }; var self = this; evalChildren(this.children, function() { if (isPlayerEmbeddedInEditor && events != undefined && events != null) { events.Raise("script_node_exit", { id: self.GetId() }); } onReturn(lastVal); }); } this.Serialize = function(depth) { if(depth === undefined) { depth = 0; } // console.log("SERIALIZE BLOCK!!!"); // console.log(depth); // console.log(doIndentFirstLine); var str = "{"; // todo: increase scope of Sym? // TODO : do code blocks ever have more than one child anymore???? for (var i = 0; i 0 && node.children[0].type === “undefined”; } var textEffectBlockNames = [“clr1”, “clr2”, “clr3”, “wvy”, “shk”, “rbw”, “printSprite”, “printItem”, “printTile”, “print”, “say”, “br”]; function isTextEffectBlock(node) { if (node.type === “code_block”) { if (node.children.length > 0 && node.children[0].type === “function”) { var func = node.children[0]; return textEffectBlockNames.indexOf(func.name) != -1; } } return false; } var listBlockTypes = [“sequence”, “cycle”, “shuffle”, “if”]; function isMultilineListBlock(node) { if (node.type === “code_block”) { if (node.children.length > 0) { var child = node.children[0]; return listBlockTypes.indexOf(child.type) != -1; } } return false; } // for round-tripping undefined code through the parser (useful for hacks!) var UndefinedNode = function(sourceStr) { Object.assign(this, new TreeRelationship()); this.type = “undefined”; this.source = sourceStr; this.Eval = function(environment,onReturn) { addOrRemoveTextEffect(environment, “_debug_highlight”); printFunc(environment, [“{” + sourceStr + “}”], function() { onReturn(null); }); addOrRemoveTextEffect(environment, “_debug_highlight”); } this.Serialize = function(depth) { return this.source; } this.ToString = function() { return “undefined” + ” ” + this.GetId(); } } var FuncNode = function(name,args) { Object.assign( this, new TreeRelationship() ); // Object.assign( this, new Runnable() ); this.type = “function”; this.name = name; this.args = args; this.Eval = function(environment,onReturn) { if (isPlayerEmbeddedInEditor && events != undefined && events != null) { events.Raise(“script_node_enter”, { id: this.GetId() }); } var self = this; // hack to deal with scope (TODO : move up higher?) var argumentValues = []; var i = 0; function evalArgs(args, done) { // TODO : really hacky way to make we get the first // symbol’s NAME instead of its variable value // if we are trying to do something with a property if (self.name === “property” && i === 0 && i < args.length) { if (args[i].type === "variable") { argumentValues.push(args[i].name); i++; } else { // first argument for a property MUST be a variable symbol // — so skip everything if it's not! i = args.length; } } if (i < args.length) { // Evaluate each argument args[i].Eval( environment, function(val) { argumentValues.push(val); i++; evalArgs(args, done); }); } else { done(); } }; evalArgs( this.args, function() { if (isPlayerEmbeddedInEditor && events != undefined && events != null) { events.Raise("script_node_exit", { id: self.GetId() }); } environment.EvalFunction(self.name, argumentValues, onReturn); }); } this.Serialize = function(depth) { var isDialogBlock = this.parent.type === "dialog_block"; if (isDialogBlock && this.name === "print") { // TODO this could cause problems with "real" print functions return this.args[0].value; // first argument should be the text of the {print} func } else if (isDialogBlock && this.name === "br") { return "\n"; } else { var str = ""; str += this.name; for(var i = 0; i < this.args.length; i++) { str += " "; str += this.args[i].Serialize(depth); } return str; } } this.ToString = function() { return this.type + " " + this.name + " " + this.GetId(); }; } var LiteralNode = function(value) { Object.assign( this, new TreeRelationship() ); // Object.assign( this, new Runnable() ); this.type = "literal"; this.value = value; this.Eval = function(environment,onReturn) { onReturn(this.value); } this.Serialize = function(depth) { var str = ""; if (this.value === null) { return str; } if (typeof this.value === "string") { str += '"'; } str += this.value; if (typeof this.value === "string") { str += '"'; } return str; } this.ToString = function() { return this.type + " " + this.value + " " + this.GetId(); }; } var VarNode = function(name) { Object.assign( this, new TreeRelationship() ); // Object.assign( this, new Runnable() ); this.type = "variable"; this.name = name; this.Eval = function(environment,onReturn) { // console.log("EVAL " + this.name + " " + environment.HasVariable(this.name) + " " + environment.GetVariable(this.name)); if( environment.HasVariable(this.name) ) onReturn( environment.GetVariable( this.name ) ); else onReturn(null); // not a valid variable — return null and hope that's ok } // TODO: might want to store nodes in the variableMap instead of values??? this.Serialize = function(depth) { var str = "" + this.name; return str; } this.ToString = function() { return this.type + " " + this.name + " " + this.GetId(); }; } var ExpNode = function(operator, left, right) { Object.assign( this, new TreeRelationship() ); this.type = "operator"; this.operator = operator; this.left = left; this.right = right; this.Eval = function(environment,onReturn) { // console.log("EVAL " + this.operator); var self = this; // hack to deal with scope environment.EvalOperator( this.operator, this.left, this.right, function(val){ // console.log("EVAL EXP " + self.operator + " " + val); onReturn(val); } ); // NOTE : sadly this pushes a lot of complexity down onto the actual operator methods } this.Serialize = function(depth) { var isNegativeNumber = this.operator === "-" && this.left.type === "literal" && this.left.value === null; if (!isNegativeNumber) { var str = ""; if (this.left != undefined && this.left != null) { str += this.left.Serialize(depth) + " "; } str += this.operator; if (this.right != undefined && this.right != null) { str += " " + this.right.Serialize(depth); } return str; } else { return this.operator + this.right.Serialize(depth); // hacky but seems to work } } this.VisitAll = function(visitor, depth) { if (depth == undefined || depth == null) { depth = 0; } visitor.Visit( this, depth ); if(this.left != null) this.left.VisitAll( visitor, depth + 1 ); if(this.right != null) this.right.VisitAll( visitor, depth + 1 ); }; this.ToString = function() { return this.type + " " + this.operator + " " + this.GetId(); }; } var SequenceBase = function() { this.Serialize = function(depth) { var str = ""; str += this.type + "\n"; for (var i = 0; i < this.children.length; i++) { str += leadingWhitespace(depth + 1) + Sym.List + " "; str += this.children[i].Serialize(depth + 2); str += "\n"; } str += leadingWhitespace(depth); return str; } this.VisitAll = function(visitor, depth) { if (depth == undefined || depth == null) { depth = 0; } visitor.Visit(this, depth); for (var i = 0; i < this.children.length; i++) { this.children[i].VisitAll( visitor, depth + 1 ); } }; this.ToString = function() { return this.type + " " + this.GetId(); }; } var SequenceNode = function(options) { Object.assign(this, new TreeRelationship()); Object.assign(this, new SequenceBase()); this.type = "sequence"; this.AddChildren(options); var index = 0; this.Eval = function(environment, onReturn) { // console.log("SEQUENCE " + index); this.children[index].Eval(environment, onReturn); var next = index + 1; if (next < this.children.length) { index = next; } } } var CycleNode = function(options) { Object.assign(this, new TreeRelationship()); Object.assign(this, new SequenceBase()); this.type = "cycle"; this.AddChildren(options); var index = 0; this.Eval = function(environment, onReturn) { // console.log("CYCLE " + index); this.children[index].Eval(environment, onReturn); var next = index + 1; if (next 0) { var i = Math.floor(Math.random() * optionsUnshuffled.length); optionsShuffled.push(optionsUnshuffled.splice(i,1)[0]); } } shuffle(this.children); var index = 0; this.Eval = function(environment, onReturn) { optionsShuffled[index].Eval(environment, onReturn); index++; if (index >= this.children.length) { shuffle(this.children); index = 0; } } } // TODO : rename? ConditionalNode? var IfNode = function(conditions, results, isSingleLine) { Object.assign(this, new TreeRelationship()); this.type = “if”; for (var i = 0; i < conditions.length; i++) { this.AddChild(new ConditionPairNode(conditions[i], results[i])); } var self = this; this.Eval = function(environment, onReturn) { // console.log("EVAL IF"); var i = 0; function TestCondition() { self.children[i].Eval(environment, function(result) { if (result.conditionValue == true) { onReturn(result.resultValue); } else if (i+1 1 && this.children[1].children[0].type === Sym.Else) { str += ” ” + Sym.ElseExp + ” ” + this.children[1].children[1].Serialize(); } } else { str += “\n”; for (var i = 0; i < this.children.length; i++) { str += this.children[i].Serialize(depth); } str += leadingWhitespace(depth); } return str; } this.IsSingleLine = function() { return isSingleLine; } this.VisitAll = function(visitor, depth) { if (depth == undefined || depth == null) { depth = 0; } visitor.Visit(this, depth); for (var i = 0; i < this.children.length; i++) { this.children[i].VisitAll(visitor, depth + 1); } }; this.ToString = function() { return this.type + " " + this.mode + " " + this.GetId(); }; } var ConditionPairNode = function(condition, result) { Object.assign(this, new TreeRelationship()); this.type = "condition_pair"; this.AddChild(condition); this.AddChild(result); var self = this; this.Eval = function(environment, onReturn) { self.children[0].Eval(environment, function(conditionSuccess) { if (conditionSuccess) { self.children[1].Eval(environment, function(resultValue) { onReturn({ conditionValue:true, resultValue:resultValue }); }); } else { onReturn({ conditionValue:false }); } }); } this.Serialize = function(depth) { var str = ""; str += leadingWhitespace(depth + 1); str += Sym.List + " " + this.children[0].Serialize(depth) + " " + Sym.ConditionEnd + Sym.Linebreak; str += this.children[1].Serialize(depth + 2) + Sym.Linebreak; return str; } this.VisitAll = function(visitor, depth) { if (depth == undefined || depth == null) { depth = 0; } visitor.Visit(this, depth); for (var i = 0; i =”, “”, “= sourceStr.length; }; this.Char = function() { return sourceStr[i]; }; this.Step = function(n) { if(n===undefined) n=1; i += n; }; this.MatchAhead = function(str) { // console.log(str); str = “” + str; // hack to turn single chars into strings // console.log(str); // console.log(str.length); for (var j = 0; j = sourceStr.length) { return false; } else if (str[j] != sourceStr[i+j]) { return false; } } return true; } this.Peak = function(end) { var str = “”; var j = i; // console.log(j); while (j 0 && !this.Done()) { if (this.MatchAhead(close)) { matchCount–; this.Step( close.length ); } else if (this.MatchAhead(open)) { matchCount++; this.Step(open.length); } else { this.Step(); } } if (includeSymbols) { return sourceStr.slice(startIndex, i); } else { return sourceStr.slice(startIndex + open.length, i – close.length); } } this.Print = function() { console.log(sourceStr); }; this.Source = function() { return sourceStr; }; }; /* ParseDialog(): This function adds {print} nodes and linebreak {br} nodes to display text, interleaved with bracketed code nodes for functions and flow control, such as text effects {shk} {wvy} or sequences like {cycle} and {shuffle}. The parsing of those code blocks is handled by ParseCode. Note on parsing newline characters: – there should be an implicit linebreak {br} after each dialog line – a “dialog line” is defined as any line that either: – 1) contains dialog text (any text outside of a code block) – 2) is entirely empty (no text, no code) – *or* 3) contains a list block (sequence, cycle, shuffle, or conditional) – lines *only* containing {code} blocks are not dialog lines NOTE TO SELF: all the state I’m storing in here feels like evidence that the parsing system kind of broke down at this point 😦 Maybe it would feel better if I move into the “state” object */ function ParseDialog(state) { var curLineNodeList = []; var curText = “”; var curLineIsEmpty = true; var curLineContainsDialogText = false; var prevLineIsDialogLine = false; var curLineIsDialogLine = function() { return curLineContainsDialogText || curLineIsEmpty; } var resetLineStateForNewLine = function() { prevLineIsDialogLine = curLineIsDialogLine(); curLineContainsDialogText = false; curLineIsEmpty = true; curText = “”; curLineNodeList = []; } var tryAddTextNodeToList = function() { if (curText.length > 0) { var printNode = new FuncNode(“print”, [new LiteralNode(curText)]); curLineNodeList.push(printNode); curText = “”; curLineIsEmpty = false; curLineContainsDialogText = true; } } var addCodeNodeToList = function() { var codeSource = state.ConsumeBlock(Sym.CodeOpen, Sym.CodeClose); var codeState = new ParserState(new CodeBlockNode(), codeSource); codeState = ParseCode(codeState); var codeBlockNode = codeState.rootNode; curLineNodeList.push(codeBlockNode); curLineIsEmpty = false; // lists count as dialog text, because they can contain it if (isMultilineListBlock(codeBlockNode)) { curLineContainsDialogText = true; } } var tryAddLinebreakNodeToList = function() { if (prevLineIsDialogLine) { var linebreakNode = new FuncNode(“br”, []); curLineNodeList.unshift(linebreakNode); } } var addLineNodesToParent = function() { for (var i = 0; i = requiredLeadingWhitespace) { var trimmedText = trimLeadingWhitespace(lineResults.text, requiredLeadingWhitespace); if (lineResults.isNewCondition) { conditionStrings[curIndex] += trimmedText; } else { resultStrings[curIndex] += trimmedText + Sym.Linebreak; } } } // hack: cut off the trailing newlines from all the result strings resultStrings = resultStrings.map(function(result) { return result.slice(0,-1); }); var conditions = []; for (var i = 0; i < conditionStrings.length; i++) { var str = conditionStrings[i].trim(); if (str === Sym.Else) { conditions.push(new ElseNode()); } else { var exp = CreateExpression(str); conditions.push(exp); } } var results = []; for (var i = 0; i = requiredLeadingWhitespace) { var trimmedText = trimLeadingWhitespace(lineResults.text, requiredLeadingWhitespace); itemStrings[curItemIndex] += trimmedText + Sym.Linebreak; } } // a bit hacky: cut off the trailing newlines from all the items itemStrings = itemStrings.map(function(item) { return item.slice(0,-1); }); var options = []; for (var i = 0; i 0) { OnSymbolEnd(); } else { curSymbol += state.Char(); } state.Step(); } if(curSymbol.length > 0) { OnSymbolEnd(); } state.curNode.AddChild( new FuncNode( funcName, args ) ); return state; } function IsValidVariableName(str) { var reg = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/; var isValid = reg.test(str); // console.log(“VALID variable??? ” + isValid); return isValid; } function StringToValue(valStr) { if(valStr[0] === Sym.CodeOpen) { // CODE BLOCK!!! var codeStr = (new ParserState( null, valStr )).ConsumeBlock(Sym.CodeOpen, Sym.CodeClose); //hacky var codeBlockState = new ParserState(new CodeBlockNode(), codeStr); codeBlockState = ParseCode( codeBlockState ); return codeBlockState.rootNode; } else if(valStr[0] === Sym.String) { // STRING!! // console.log(“STRING”); var str = “”; var i = 1; while (i < valStr.length && valStr[i] != Sym.String) { str += valStr[i]; i++; } // console.log(str); return new LiteralNode( str ); } else if(valStr === "true") { // BOOL return new LiteralNode( true ); } else if(valStr === "false") { // BOOL return new LiteralNode( false ); } else if( !isNaN(parseFloat(valStr)) ) { // NUMBER!! // console.log("NUMBER!!! " + valStr); return new LiteralNode( parseFloat(valStr) ); } else if(IsValidVariableName(valStr)) { // VARIABLE!! // console.log("VARIABLE"); return new VarNode(valStr); // TODO : check for valid potential variables } else { // uh oh return new LiteralNode(null); } } function CreateExpression(expStr) { expStr = expStr.trim(); function IsInsideString(index) { var inString = false; for(var i = 0; i < expStr.length; i++) { if(expStr[i] === Sym.String) inString = !inString; if(index === i) return inString; } return false; } function IsInsideCode(index) { var count = 0; for(var i = 0; i 0; } return false; } var operator = null; // set is special because other operator can look like it, and it has to go first in the order of operations var setIndex = expStr.indexOf(Sym.Set); if( setIndex > -1 && !IsInsideString(setIndex) && !IsInsideCode(setIndex) ) { // it might be a set operator if( expStr[setIndex+1] != “=” && expStr[setIndex-1] != “>” && expStr[setIndex-1] != “=, or -1 && !IsInsideString(ifIndex) && !IsInsideCode(ifIndex) ) { operator = Sym.ConditionEnd; var conditionStr = expStr.substring(0,ifIndex).trim(); var conditions = [ CreateExpression(conditionStr) ]; var resultStr = expStr.substring(ifIndex+Sym.ConditionEnd.length); var results = []; function AddResult(str) { var dialogBlockState = new ParserState(new DialogBlockNode(), str); dialogBlockState = ParseDialog( dialogBlockState ); var dialogBlock = dialogBlockState.rootNode; results.push( dialogBlock ); } var elseIndex = resultStr.indexOf(Sym.ElseExp); // does this need to test for strings? if(elseIndex > -1) { conditions.push( new ElseNode() ); var elseStr = resultStr.substring(elseIndex+Sym.ElseExp.length); var resultStr = resultStr.substring(0,elseIndex); AddResult( resultStr.trim() ); AddResult( elseStr.trim() ); } else { AddResult( resultStr.trim() ); } return new IfNode( conditions, results, true /*isSingleLine*/ ); } for( var i = 0; (operator == null) && (i -1 && !IsInsideString(opIndex) && !IsInsideCode(opIndex) ) { operator = opSym; var left = CreateExpression( expStr.substring(0,opIndex) ); var right = CreateExpression( expStr.substring(opIndex+opSym.length) ); var exp = new ExpNode( operator, left, right ); return exp; } } if( operator == null ) { return StringToValue(expStr); } } this.CreateExpression = CreateExpression; function IsWhitespace(str) { return ( str === ” ” || str === “\t” || str === “\n” ); } function IsExpression(str) { var tempState = new ParserState(null, str); // hacky var textOutsideCodeBlocks = “”; while (!tempState.Done()) { if (tempState.MatchAhead(Sym.CodeOpen)) { tempState.ConsumeBlock(Sym.CodeOpen, Sym.CodeClose); } else { textOutsideCodeBlocks += tempState.Char(); tempState.Step(); } } var containsAnyExpressionOperators = (textOutsideCodeBlocks.indexOf(Sym.ConditionEnd) != -1) || (textOutsideCodeBlocks.indexOf(Sym.Set) != -1) || (Sym.Operators.some(function(opSym) { return textOutsideCodeBlocks.indexOf(opSym) != -1; })); return containsAnyExpressionOperators; } function IsLiteral(str) { var isBool = str === “true” || str === “false”; var isNum = !isNaN(parseFloat(str)); var isStr = str[0] === ‘”‘ && str[str.length-1] === ‘”‘; var isVar = IsValidVariableName(str); var isEmpty = str.length === 0; return isBool || isNum || isStr || isVar || isEmpty; } function ParseExpression(state) { var line = state.Source(); // state.Peak( [Sym.Linebreak] ); // TODO : remove the linebreak thing // console.log(“EXPRESSION ” + line); var exp = CreateExpression(line); // console.log(exp); state.curNode.AddChild(exp); state.Step(line.length); return state; } function IsConditionalBlock(state) { var peakToFirstListSymbol = state.Peak([Sym.List]); var foundListSymbol = peakToFirstListSymbol < state.Source().length; var areAllCharsBeforeListWhitespace = true; for (var i = 0; i < peakToFirstListSymbol.length; i++) { if (!IsWhitespace(peakToFirstListSymbol[i])) { areAllCharsBeforeListWhitespace = false; } } var peakToFirstConditionSymbol = state.Peak([Sym.ConditionEnd]); peakToFirstConditionSymbol = peakToFirstConditionSymbol.slice(peakToFirstListSymbol.length); var hasNoLinebreakBetweenListAndConditionEnd = peakToFirstConditionSymbol.indexOf(Sym.Linebreak) == -1; return foundListSymbol && areAllCharsBeforeListWhitespace && hasNoLinebreakBetweenListAndConditionEnd; } function ParseCode(state) { if (IsConditionalBlock(state)) { state = ParseConditional(state); } else if (environment.HasFunction(state.Peak([" "]))) { // TODO — what about newlines??? var funcName = state.Peak([" "]); state.Step(funcName.length); state = ParseFunction(state, funcName); } else if (IsSequence(state.Peak([" ", Sym.Linebreak]))) { var sequenceType = state.Peak([" ", Sym.Linebreak]); state.Step(sequenceType.length); state = ParseSequence(state, sequenceType); } else if (IsLiteral(state.Source()) || IsExpression(state.Source())) { state = ParseExpression(state); } else { var undefinedSrc = state.Peak([]); var undefinedNode = new UndefinedNode(undefinedSrc); state.curNode.AddChild(undefinedNode); } // just go to the end now while (!state.Done()) { state.Step(); } return state; } function ParseCodeBlock(state) { var codeStr = state.ConsumeBlock( Sym.CodeOpen, Sym.CodeClose ); var codeState = new ParserState(new CodeBlockNode(), codeStr); codeState = ParseCode( codeState ); state.curNode.AddChild( codeState.rootNode ); return state; } } } // Script() function Dialog() { this.CreateRenderer = function() { return new DialogRenderer(); }; this.CreateBuffer = function() { return new DialogBuffer(); }; var DialogRenderer = function() { // TODO : refactor this eventually? remove everything from struct.. avoid the defaults? var textboxInfo = { img : null, width : 104, height : 8+4+2+5, //8 for text, 4 for top-bottom padding, 2 for line padding, 5 for arrow top : 12, left : 12, bottom : 12, //for drawing it from the bottom font_scale : 0.5, // we draw font at half-size compared to everything else padding_vert : 2, padding_horz : 4, arrow_height : 5, }; var font = null; this.SetFont = function(f) { font = f; textboxInfo.height = (textboxInfo.padding_vert * 3) + (relativeFontHeight() * 2) + textboxInfo.arrow_height; textboxInfo.img = context.createImageData(textboxInfo.width*scale, textboxInfo.height*scale); } function textScale() { return scale * textboxInfo.font_scale; } function relativeFontWidth() { return Math.ceil( font.getWidth() * textboxInfo.font_scale ); } function relativeFontHeight() { return Math.ceil( font.getHeight() * textboxInfo.font_scale ); } var context = null; this.AttachContext = function(c) { context = c; }; this.ClearTextbox = function() { if(context == null) return; //create new image none exists if(textboxInfo.img == null) textboxInfo.img = context.createImageData(textboxInfo.width*scale, textboxInfo.height*scale); // fill text box with black for (var i=0;i<textboxInfo.img.data.length;i+=4) { textboxInfo.img.data[i+0]=0; textboxInfo.img.data[i+1]=0; textboxInfo.img.data[i+2]=0; textboxInfo.img.data[i+3]=255; } }; var isCentered = false; this.SetCentered = function(centered) { isCentered = centered; }; this.DrawTextbox = function() { if(context == null) return; if (isCentered) { context.putImageData(textboxInfo.img, textboxInfo.left*scale, ((height/2)-(textboxInfo.height/2))*scale); } else if (player().y < mapsize/2) { //bottom context.putImageData(textboxInfo.img, textboxInfo.left*scale, (height-textboxInfo.bottom-textboxInfo.height)*scale); } else { //top context.putImageData(textboxInfo.img, textboxInfo.left*scale, textboxInfo.top*scale); } }; var arrowdata = [ 1,1,1,1,1, 0,1,1,1,0, 0,0,1,0,0 ]; this.DrawNextArrow = function() { // console.log("draw arrow!"); var top = (textboxInfo.height-5) * scale; var left = (textboxInfo.width-(5+4)) * scale; if (textDirection === TextDirection.RightToLeft) { // RTL hack left = 4 * scale; } for (var y = 0; y < 3; y++) { for (var x = 0; x < 5; x++) { var i = (y * 5) + x; if (arrowdata[i] == 1) { //scaling nonsense for (var sy = 0; sy < scale; sy++) { for (var sx = 0; sx < scale; sx++) { var pxl = 4 * ( ((top+(y*scale)+sy) * (textboxInfo.width*scale)) + (left+(x*scale)+sx) ); textboxInfo.img.data[pxl+0] = 255; textboxInfo.img.data[pxl+1] = 255; textboxInfo.img.data[pxl+2] = 255; textboxInfo.img.data[pxl+3] = 255; } } } } } }; var text_scale = 2; //using a different scaling factor for text feels like cheating… but it looks better this.DrawChar = function(char, row, col, leftPos) { char.offset = { x: char.base_offset.x, y: char.base_offset.y }; // compute render offset *every* frame char.SetPosition(row,col); char.ApplyEffects(effectTime); var charData = char.bitmap; var top = (4 * scale) + (row * 2 * scale) + (row * font.getHeight() * text_scale) + Math.floor( char.offset.y ); var left = (4 * scale) + (leftPos * text_scale) + Math.floor( char.offset.x ); var debug_r = Math.random() * 255; for (var y = 0; y < char.height; y++) { for (var x = 0; x < char.width; x++) { var i = (y * char.width) + x; if ( charData[i] == 1 ) { //scaling nonsense for (var sy = 0; sy < text_scale; sy++) { for (var sx = 0; sx < text_scale; sx++) { var pxl = 4 * ( ((top+(y*text_scale)+sy) * (textboxInfo.width*scale)) + (left+(x*text_scale)+sx) ); textboxInfo.img.data[pxl+0] = char.color.r; textboxInfo.img.data[pxl+1] = char.color.g; textboxInfo.img.data[pxl+2] = char.color.b; textboxInfo.img.data[pxl+3] = char.color.a; } } } // else { // // DEBUG // //scaling nonsense // for (var sy = 0; sy < text_scale; sy++) { // for (var sx = 0; sx < text_scale; sx++) { // var pxl = 4 * ( ((top+(y*text_scale)+sy) * (textboxInfo.width*scale)) + (left+(x*text_scale)+sx) ); // textboxInfo.img.data[pxl+0] = debug_r; // textboxInfo.img.data[pxl+1] = 0; // textboxInfo.img.data[pxl+2] = 0; // textboxInfo.img.data[pxl+3] = 255; // } // } // } } } // call printHandler for character char.OnPrint(); }; var effectTime = 0; // TODO this variable should live somewhere better this.Draw = function(buffer, dt) { effectTime += dt; this.ClearTextbox(); buffer.ForEachActiveChar(this.DrawChar); if (buffer.CanContinue()) { this.DrawNextArrow(); } this.DrawTextbox(); if (buffer.DidPageFinishThisFrame() && onPageFinish != null) { onPageFinish(); } }; /* this is a hook for GIF rendering */ var onPageFinish = null; this.SetPageFinishHandler = function(handler) { onPageFinish = handler; }; this.Reset = function() { effectTime = 0; // TODO – anything else? } // this.CharsPerRow = function() { // return textboxInfo.charsPerRow; // } } var DialogBuffer = function() { var buffer = [[[]]]; // holds dialog in an array buffer var pageIndex = 0; var rowIndex = 0; var charIndex = 0; var nextCharTimer = 0; var nextCharMaxTime = 50; // in milliseconds var isDialogReadyToContinue = false; var activeTextEffects = []; var font = null; var arabicHandler = new ArabicHandler(); var onDialogEndCallbacks = []; this.SetFont = function(f) { font = f; } this.CurPage = function() { return buffer[ pageIndex ]; }; this.CurRow = function() { return this.CurPage()[ rowIndex ]; }; this.CurChar = function() { return this.CurRow()[ charIndex ]; }; this.CurPageCount = function() { return buffer.length; }; this.CurRowCount = function() { return this.CurPage().length; }; this.CurCharCount = function() { return this.CurRow().length; }; this.ForEachActiveChar = function(handler) { // Iterates over visible characters on the active page var rowCount = rowIndex + 1; for (var i = 0; i < rowCount; i++) { var row = this.CurPage()[i]; var charCount = (i == rowIndex) ? charIndex+1 : row.length; // console.log(charCount); var leftPos = 0; if (textDirection === TextDirection.RightToLeft) { leftPos = 24 * 8; // hack — I think this is correct? } for(var j = 0; j < charCount; j++) { var char = row[j]; if(char) { if (textDirection === TextDirection.RightToLeft) { leftPos -= char.spacing; } // console.log(j + " " + leftPos); // handler( char, i /*rowIndex*/, j /*colIndex*/ ); handler(char, i /*rowIndex*/, j /*colIndex*/, leftPos) if (textDirection === TextDirection.LeftToRight) { leftPos += char.spacing; } } } } } this.Reset = function() { buffer = [[[]]]; pageIndex = 0; rowIndex = 0; charIndex = 0; isDialogReadyToContinue = false; afterManualPagebreak = false; activeTextEffects = []; onDialogEndCallbacks = []; isActive = false; }; this.DoNextChar = function() { nextCharTimer = 0; //reset timer //time to update characters if (charIndex + 1 < this.CurCharCount()) { //add char to current row charIndex++; } else if (rowIndex + 1 nextCharMaxTime) { this.DoNextChar(); } }; this.Skip = function() { console.log(“SKIPPP”); didPageFinishThisFrame = false; didFlipPageThisFrame = false; // add new characters until you get to the end of the current line of dialog while (rowIndex < this.CurRowCount()) { this.DoNextChar(); if(isDialogReadyToContinue) { //make sure to push the rowIndex past the end to break out of the loop rowIndex++; charIndex = 0; } } rowIndex = this.CurRowCount()-1; charIndex = this.CurCharCount()-1; }; this.FlipPage = function() { didFlipPageThisFrame = true; isDialogReadyToContinue = false; pageIndex++; rowIndex = 0; charIndex = 0; } this.EndDialog = function() { isActive = false; // no more text to show… this should be a sign to stop rendering dialog for (var i = 0; i < onDialogEndCallbacks.length; i++) { onDialogEndCallbacks[i](); } } var afterManualPagebreak = false; // is it bad to track this state like this? this.Continue = function() { console.log("CONTINUE"); // if we used a page break character to continue we need // to run whatever is in the script afterwards! // TODO : make this comment better if (this.CurChar().isPageBreak) { // hacky: always treat a page break as the end of dialog // if there's more dialog later we re-activate the dialog buffer this.EndDialog(); afterManualPagebreak = true; this.CurChar().OnContinue(); return false; } if (pageIndex + 1 < this.CurPageCount()) { console.log("FLIP PAGE!"); //start next page this.FlipPage(); return true; /* hasMoreDialog */ } else { console.log("END DIALOG!"); //end dialog mode this.EndDialog(); return false; /* hasMoreDialog */ } }; var isActive = false; this.IsActive = function() { return isActive; }; this.OnDialogEnd = function(callback) { if (!isActive) { callback(); } else { onDialogEndCallbacks.push(callback); } } this.CanContinue = function() { return isDialogReadyToContinue; }; function DialogChar(effectList) { this.effectList = effectList.slice(); // clone effect list (since it can change between chars) this.color = { r:255, g:255, b:255, a:255 }; this.offset = { x:0, y:0 }; // in pixels (screen pixels?) this.col = 0; this.row = 0; this.SetPosition = function(row,col) { // console.log("SET POS"); // console.log(this); this.row = row; this.col = col; } this.ApplyEffects = function(time) { // console.log("APPLY EFFECTS! " + time); for(var i = 0; i < this.effectList.length; i++) { var effectName = this.effectList[i]; // console.log("FX " + effectName); TextEffects[ effectName ].DoEffect( this, time ); } } var printHandler = null; // optional function to be called once on printing character this.SetPrintHandler = function(handler) { printHandler = handler; } this.OnPrint = function() { if (printHandler != null) { // console.log("PRINT HANDLER —- DIALOG BUFFER"); printHandler(); printHandler = null; // only call handler once (hacky) } } this.bitmap = []; this.width = 0; this.height = 0; this.base_offset = { // hacky name x: 0, y: 0 }; this.spacing = 0; } function DialogFontChar(font, char, effectList) { Object.assign(this, new DialogChar(effectList)); var charData = font.getChar(char); this.bitmap = charData.data; this.width = charData.width; this.height = charData.height; this.base_offset.x = charData.offset.x; this.base_offset.y = charData.offset.y; this.spacing = charData.spacing; } function DialogDrawingChar(drawingId, effectList) { Object.assign(this, new DialogChar(effectList)); var imageData = renderer.GetImageSource(drawingId)[0]; var imageDataFlat = []; for (var i = 0; i < imageData.length; i++) { // console.log(imageData[i]); imageDataFlat = imageDataFlat.concat(imageData[i]); } this.bitmap = imageDataFlat; this.width = 8; this.height = 8; this.spacing = 8; } function DialogScriptControlChar() { Object.assign(this, new DialogChar([])); this.width = 0; this.height = 0; this.spacing = 0; } // is a control character really the best way to handle page breaks? function DialogPageBreakChar() { Object.assign(this, new DialogChar([])); this.width = 0; this.height = 0; this.spacing = 0; this.isPageBreak = true; var continueHandler = null; this.SetContinueHandler = function(handler) { continueHandler = handler; } this.OnContinue = function() { if (continueHandler) { continueHandler(); } } } function AddWordToCharArray(charArray,word,effectList) { for(var i = 0; i < word.length; i++) { charArray.push( new DialogFontChar( font, word[i], effectList ) ); } return charArray; } function GetCharArrayWidth(charArray) { var width = 0; for(var i = 0; i < charArray.length; i++) { width += charArray[i].spacing; } return width; } function GetStringWidth(str) { var width = 0; for (var i = 0; i < str.length; i++) { var charData = font.getChar(str[i]); width += charData.spacing; } return width; } var pixelsPerRow = 192; // hard-coded fun times!!! this.AddScriptReturn = function(onReturnHandler) { var curPageIndex = buffer.length – 1; var curRowIndex = buffer[curPageIndex].length – 1; var curRowArr = buffer[curPageIndex][curRowIndex]; var controlChar = new DialogScriptControlChar(); controlChar.SetPrintHandler(onReturnHandler); curRowArr.push(controlChar); isActive = true; } this.AddDrawing = function(drawingId) { // console.log("DRAWING ID " + drawingId); var curPageIndex = buffer.length – 1; var curRowIndex = buffer[curPageIndex].length – 1; var curRowArr = buffer[curPageIndex][curRowIndex]; var drawingChar = new DialogDrawingChar(drawingId, activeTextEffects); var rowLength = GetCharArrayWidth(curRowArr); // TODO : clean up copy-pasted code here :/ if (afterManualPagebreak) { this.FlipPage(); // hacky buffer[curPageIndex][curRowIndex] = curRowArr; buffer.push([]); curPageIndex++; buffer[curPageIndex].push([]); curRowIndex = 0; curRowArr = buffer[curPageIndex][curRowIndex]; curRowArr.push(drawingChar); afterManualPagebreak = false; } else if (rowLength + drawingChar.spacing <= pixelsPerRow || rowLength <= 0) { //stay on same row curRowArr.push(drawingChar); } else if (curRowIndex == 0) { //start next row buffer[curPageIndex][curRowIndex] = curRowArr; buffer[curPageIndex].push([]); curRowIndex++; curRowArr = buffer[curPageIndex][curRowIndex]; curRowArr.push(drawingChar); } else { //start next page buffer[curPageIndex][curRowIndex] = curRowArr; buffer.push([]); curPageIndex++; buffer[curPageIndex].push([]); curRowIndex = 0; curRowArr = buffer[curPageIndex][curRowIndex]; curRowArr.push(drawingChar); } isActive = true; // this feels like a bad way to do this??? } // TODO : convert this into something that takes DialogChar arrays this.AddText = function(textStr) { console.log("ADD TEXT " + textStr); //process dialog so it's easier to display var words = textStr.split(" "); // var curPageIndex = this.CurPageCount() – 1; // var curRowIndex = this.CurRowCount() – 1; // var curRowArr = this.CurRow(); var curPageIndex = buffer.length – 1; var curRowIndex = buffer[curPageIndex].length – 1; var curRowArr = buffer[curPageIndex][curRowIndex]; for (var i = 0; i < words.length; i++) { var word = words[i]; if (arabicHandler.ContainsArabicCharacters(word)) { word = arabicHandler.ShapeArabicCharacters(word); } var wordWithPrecedingSpace = ((i == 0) ? "" : " ") + word; var wordLength = GetStringWidth(wordWithPrecedingSpace); var rowLength = GetCharArrayWidth(curRowArr); if (afterManualPagebreak) { this.FlipPage(); // hacky copied bit for page breaks buffer[curPageIndex][curRowIndex] = curRowArr; buffer.push([]); curPageIndex++; buffer[curPageIndex].push([]); curRowIndex = 0; curRowArr = buffer[curPageIndex][curRowIndex]; curRowArr = AddWordToCharArray(curRowArr, word, activeTextEffects); afterManualPagebreak = false; } else if (rowLength + wordLength <= pixelsPerRow || rowLength 0) { var lastChar = lastRow[lastRow.length-1]; } // console.log(buffer); isActive = true; }; this.AddLinebreak = function() { var lastPage = buffer[buffer.length-1]; if (lastPage.length -1; } this.AddTextEffect = function(name) { activeTextEffects.push( name ); } this.RemoveTextEffect = function(name) { activeTextEffects.splice( activeTextEffects.indexOf( name ), 1 ); } /* this is a hook for GIF rendering */ var didPageFinishThisFrame = false; this.DidPageFinishThisFrame = function(){ return didPageFinishThisFrame; }; var didFlipPageThisFrame = false; this.DidFlipPageThisFrame = function(){ return didFlipPageThisFrame; }; // this.SetCharsPerRow = function(num){ charsPerRow = num; }; // hacky }; /* ARABIC */ var ArabicHandler = function() { var arabicCharStart = 0x0621; var arabicCharEnd = 0x064E; var CharacterForm = { Isolated : 0, Final : 1, Initial : 2, Middle : 3 }; // map glyphs to their character forms var glyphForms = { /* Isolated, Final, Initial, Middle Forms */ 0x0621: [0xFE80,0xFE80,0xFE80,0xFE80], /* HAMZA */ 0x0622: [0xFE81,0xFE82,0xFE81,0xFE82], /* ALEF WITH MADDA ABOVE */ 0x0623: [0xFE83,0xFE84,0xFE83,0xFE84], /* ALEF WITH HAMZA ABOVE */ 0x0624: [0xFE85,0xFE86,0xFE85,0xFE86], /* WAW WITH HAMZA ABOVE */ 0x0625: [0xFE87,0xFE88,0xFE87,0xFE88], /* ALEF WITH HAMZA BELOW */ 0x0626: [0xFE89,0xFE8A,0xFE8B,0xFE8C], /* YEH WITH HAMZA ABOVE */ 0x0627: [0xFE8D,0xFE8E,0xFE8D,0xFE8E], /* ALEF */ 0x0628: [0xFE8F,0xFE90,0xFE91,0xFE92], /* BEH */ 0x0629: [0xFE93,0xFE94,0xFE93,0xFE94], /* TEH MARBUTA */ 0x062A: [0xFE95,0xFE96,0xFE97,0xFE98], /* TEH */ 0x062B: [0xFE99,0xFE9A,0xFE9B,0xFE9C], /* THEH */ 0x062C: [0xFE9D,0xFE9E,0xFE9F,0xFEA0], /* JEEM */ 0x062D: [0xFEA1,0xFEA2,0xFEA3,0xFEA4], /* HAH */ 0x062E: [0xFEA5,0xFEA6,0xFEA7,0xFEA8], /* KHAH */ 0x062F: [0xFEA9,0xFEAA,0xFEA9,0xFEAA], /* DAL */ 0x0630: [0xFEAB,0xFEAC,0xFEAB,0xFEAC], /* THAL */ 0x0631: [0xFEAD,0xFEAE,0xFEAD,0xFEAE], /* RAA */ 0x0632: [0xFEAF,0xFEB0,0xFEAF,0xFEB0], /* ZAIN */ 0x0633: [0xFEB1,0xFEB2,0xFEB3,0xFEB4], /* SEEN */ 0x0634: [0xFEB5,0xFEB6,0xFEB7,0xFEB8], /* SHEEN */ 0x0635: [0xFEB9,0xFEBA,0xFEBB,0xFEBC], /* SAD */ 0x0636: [0xFEBD,0xFEBE,0xFEBF,0xFEC0], /* DAD */ 0x0637: [0xFEC1,0xFEC2,0xFEC3,0xFEC4], /* TAH */ 0x0638: [0xFEC5,0xFEC6,0xFEC7,0xFEC8], /* ZAH */ 0x0639: [0xFEC9,0xFECA,0xFECB,0xFECC], /* AIN */ 0x063A: [0xFECD,0xFECE,0xFECF,0xFED0], /* GHAIN */ 0x063B: [0x0000,0x0000,0x0000,0x0000], /* space */ 0x063C: [0x0000,0x0000,0x0000,0x0000], /* space */ 0x063D: [0x0000,0x0000,0x0000,0x0000], /* space */ 0x063E: [0x0000,0x0000,0x0000,0x0000], /* space */ 0x063F: [0x0000,0x0000,0x0000,0x0000], /* space */ 0x0640: [0x0640,0x0640,0x0640,0x0640], /* TATWEEL */ 0x0641: [0xFED1,0xFED2,0xFED3,0xFED4], /* FAA */ 0x0642: [0xFED5,0xFED6,0xFED7,0xFED8], /* QAF */ 0x0643: [0xFED9,0xFEDA,0xFEDB,0xFEDC], /* KAF */ 0x0644: [0xFEDD,0xFEDE,0xFEDF,0xFEE0], /* LAM */ 0x0645: [0xFEE1,0xFEE2,0xFEE3,0xFEE4], /* MEEM */ 0x0646: [0xFEE5,0xFEE6,0xFEE7,0xFEE8], /* NOON */ 0x0647: [0xFEE9,0xFEEA,0xFEEB,0xFEEC], /* HEH */ 0x0648: [0xFEED,0xFEEE,0xFEED,0xFEEE], /* WAW */ 0x0649: [0xFEEF,0xFEF0,0xFBE8,0xFBE9], /* ALEF MAKSURA */ 0x064A: [0xFEF1,0xFEF2,0xFEF3,0xFEF4], /* YEH */ 0x064B: [0xFEF5,0xFEF6,0xFEF5,0xFEF6], /* LAM ALEF MADD*/ 0x064C: [0xFEF7,0xFEF8,0xFEF7,0xFEF8], /* LAM ALEF HAMZA ABOVE*/ 0x064D: [0xFEF9,0xFEFa,0xFEF9,0xFEFa], /* LAM ALEF HAMZA BELOW*/ 0x064E: [0xFEFb,0xFEFc,0xFEFb,0xFEFc], /* LAM ALEF */ }; var disconnectedCharacters = [0x0621,0x0622,0x0623,0x0624,0x0625,0x0627,0x062f,0x0630,0x0631,0x0632,0x0648,0x0649,0x064b,0x064c,0x064d,0x064e]; function IsArabicCharacter(char) { var code = char.charCodeAt(0); return (code >= arabicCharStart && code <= arabicCharEnd); } function ContainsArabicCharacters(word) { for (var i = 0; i < word.length; i++) { if (IsArabicCharacter(word[i])) { return true; } } return false; } function IsDisconnectedCharacter(char) { var code = char.charCodeAt(0); return disconnectedCharacters.indexOf(code) != -1; } function ShapeArabicCharacters(word) { var shapedWord = ""; for (var i = 0; i = 0 && IsArabicCharacter(word[i-1]) && !IsDisconnectedCharacter(word[i-1]); var connectedToNextChar = i+1 < word.length && IsArabicCharacter(word[i+1]) && !IsDisconnectedCharacter(word[i]); var form; if (!connectedToPreviousChar && !connectedToNextChar) { form = CharacterForm.Isolated; } else if (connectedToPreviousChar && !connectedToNextChar) { form = CharacterForm.Final; } else if (!connectedToPreviousChar && connectedToNextChar) { form = CharacterForm.Initial; } else if (connectedToPreviousChar && connectedToNextChar) { form = CharacterForm.Middle; } var code = word[i].charCodeAt(0); // handle lam alef special case if (code == 0x0644 && connectedToNextChar) { var nextCode = word[i+1].charCodeAt(0); var specialCode = null; if (nextCode == 0x0622) { // alef madd specialCode = glyphForms[0x064b][form]; } else if (nextCode == 0x0623) { // hamza above specialCode = glyphForms[0x064c][form]; } else if (nextCode == 0x0625) { // hamza below specialCode = glyphForms[0x064d][form]; } else if (nextCode == 0x0627) { // alef specialCode = glyphForms[0x064e][form]; } if (specialCode != null) { shapedWord += String.fromCharCode(specialCode); i++; // skip a step continue; } } // hacky? if (form === CharacterForm.Isolated) { shapedWord += word[i]; continue; } var shapedCode = glyphForms[form]; shapedWord += String.fromCharCode(shapedCode); } return shapedWord; } this.ContainsArabicCharacters = ContainsArabicCharacters; this.ShapeArabicCharacters = ShapeArabicCharacters; } /* NEW TEXT EFFECTS */ var TextEffects = new Map(); var RainbowEffect = function() { // TODO - should it be an object or just a method? this.DoEffect = function(char,time) { // console.log("RAINBOW!!!"); // console.log(char); // console.log(char.color); // console.log(char.col); var h = Math.abs( Math.sin( (time / 600) - (char.col / 8) ) ); var rgb = hslToRgb( h, 1, 0.5 ); char.color.r = rgb[0]; char.color.g = rgb[1]; char.color.b = rgb[2]; char.color.a = 255; } }; TextEffects["rbw"] = new RainbowEffect(); var ColorEffect = function(index) { this.DoEffect = function(char) { var pal = getPal( curPal() ); var color = pal[ parseInt( index ) ]; // console.log(color); char.color.r = color[0]; char.color.g = color[1]; char.color.b = color[2]; char.color.a = 255; } }; TextEffects["clr1"] = new ColorEffect(0); TextEffects["clr2"] = new ColorEffect(1); // TODO : should I use parameters instead of special names? TextEffects["clr3"] = new ColorEffect(2); var WavyEffect = function() { this.DoEffect = function(char,time) { char.offset.y += Math.sin( (time / 250) - (char.col / 2) ) * 4; } }; TextEffects["wvy"] = new WavyEffect(); var ShakyEffect = function() { function disturb(func,time,offset,mult1,mult2) { return func( (time * mult1) - (offset * mult2) ); } this.DoEffect = function(char,time) { char.offset.y += 3 * disturb(Math.sin,time,char.col,0.1,0.5) * disturb(Math.cos,time,char.col,0.3,0.2) * disturb(Math.sin,time,char.row,2.0,1.0); char.offset.x += 3 * disturb(Math.cos,time,char.row,0.1,1.0) * disturb(Math.sin,time,char.col,3.0,0.7) * disturb(Math.cos,time,char.col,0.2,0.3); } }; TextEffects["shk"] = new ShakyEffect(); var DebugHighlightEffect = function() { this.DoEffect = function(char) { char.color.r = 255; char.color.g = 255; char.color.b = 0; char.color.a = 255; } } TextEffects["_debug_highlight"] = new DebugHighlightEffect(); } // Dialog() /* TODO - reset renderer function - react to changes in: drawings, palettes - possible future plan: limit size of cache (remove old images) - change image store path from (pal > col > draw) to (draw > pal > col) - get rid of old getSpriteImage (etc) methods - get editor working again [in progress] - move debug timer class into core (seems useful) */ function Renderer(tilesize, scale) { console.log("!!!!! NEW RENDERER"); var imageStore = { // TODO : rename to imageCache source: {}, render: {} }; var palettes = null; // TODO : need null checks? var context = null; function setPalettes(paletteObj) { palettes = paletteObj; // TODO : should this really clear out the render cache? imageStore.render = {}; } function getPaletteColor(paletteId, colorIndex) { if (palettes[paletteId] === undefined) { paletteId = "default"; } var palette = palettes[paletteId]; if (colorIndex > palette.colors.length) { // do I need this failure case? (seems un-reliable) colorIndex = 0; } var color = palette.colors[colorIndex]; return { r : color[0], g : color[1], b : color[2] }; } var debugRenderCount = 0; // TODO : change image store path from (pal > col > draw) to (draw > pal > col) function renderImage(drawing, paletteId) { // debugRenderCount++; // console.log("RENDER COUNT " + debugRenderCount); var col = drawing.col; var colStr = "" + col; var pal = paletteId; var drwId = drawing.drw; var imgSrc = imageStore.source[ drawing.drw ]; // initialize render cache entry if (imageStore.render[drwId] === undefined || imageStore.render[drwId] === null) { imageStore.render[drwId] = {}; } if (imageStore.render[drwId][pal] === undefined || imageStore.render[drwId][pal] === null) { imageStore.render[drwId][pal] = {}; } // create array of ImageData frames imageStore.render[drwId][pal][colStr] = []; for (var i = 0; i < imgSrc.length; i++) { var frameSrc = imgSrc[i]; var frameData = imageDataFromImageSource( frameSrc, pal, col ); imageStore.render[drwId][pal][colStr].push(frameData); } } function imageDataFromImageSource(imageSource, pal, col) { //console.log(imageSource); var img = context.createImageData(tilesize*scale,tilesize*scale); var backgroundColor = getPaletteColor(pal,0); var foregroundColor = getPaletteColor(pal,col); for (var y = 0; y < tilesize; y++) { for (var x = 0; x < tilesize; x++) { var px = imageSource[y][x]; for (var sy = 0; sy < scale; sy++) { for (var sx = 0; sx < scale; sx++) { var pxl = (((y * scale) + sy) * tilesize * scale * 4) + (((x*scale) + sx) * 4); if ( px === 1 ) { img.data[pxl + 0] = foregroundColor.r; img.data[pxl + 1] = foregroundColor.g; img.data[pxl + 2] = foregroundColor.b; img.data[pxl + 3] = 255; } else { //ch === 0 img.data[pxl + 0] = backgroundColor.r; img.data[pxl + 1] = backgroundColor.g; img.data[pxl + 2] = backgroundColor.b; img.data[pxl + 3] = 255; } } } } } // convert to canvas: chrome has poor performance when working directly with image data var imageCanvas = document.createElement("canvas"); imageCanvas.width = img.width; imageCanvas.height = img.height; var imageContext = imageCanvas.getContext("2d"); imageContext.putImageData(img,0,0); return imageCanvas; } // TODO : move into core function undefinedOrNull(x) { return x === undefined || x === null; } function isImageRendered(drawing, paletteId) { var col = drawing.col; var colStr = "" + col; var pal = paletteId; var drwId = drawing.drw; if (undefinedOrNull(imageStore.render[drwId]) || undefinedOrNull(imageStore.render[drwId][pal]) || undefinedOrNull(imageStore.render[drwId][pal][colStr])) { return false; } else { return true; } } function getImageSet(drawing, paletteId) { return imageStore.render[drawing.drw][paletteId][drawing.col]; } function getImageFrame(drawing, paletteId, frameOverride) { var frameIndex = 0; if (drawing.animation.isAnimated) { if (frameOverride != undefined && frameOverride != null) { frameIndex = frameOverride; } else { frameIndex = drawing.animation.frameIndex; } } return getImageSet(drawing, paletteId)[frameIndex]; } function getOrRenderImage(drawing, paletteId, frameOverride) { if (!isImageRendered(drawing, paletteId)) { renderImage(drawing, paletteId); } return getImageFrame(drawing, paletteId, frameOverride); } /* PUBLIC INTERFACE */ this.GetImage = getOrRenderImage; this.SetPalettes = setPalettes; this.SetImageSource = function(drawingId, imageSourceData) { imageStore.source[drawingId] = imageSourceData; imageStore.render[drawingId] = {}; // reset render cache for this image } this.GetImageSource = function(drawingId) { return imageStore.source[drawingId]; } this.GetFrameCount = function(drawingId) { return imageStore.source[drawingId].length; } this.AttachContext = function(ctx) { context = ctx; } } // Renderer() var xhr; // TODO : remove var canvas; var context; // TODO : remove if safe? var ctx; var room = {}; var tile = {}; var sprite = {}; var item = {}; var dialog = {}; var palette = { //start off with a default palette "default" : { name : "default", colors : [[0,0,0],[255,255,255],[255,255,255]] } }; var variable = {}; // these are starting variable values -- they don't update (or I don't think they will) var playerId = "A"; var titleDialogId = "title"; function getTitle() { return dialog[titleDialogId].src; } function setTitle(titleSrc) { dialog[titleDialogId] = { src:titleSrc, name:null }; } var defaultFontName = "ascii_small"; var fontName = defaultFontName; var TextDirection = { LeftToRight : "LTR", RightToLeft : "RTL" }; var textDirection = TextDirection.LeftToRight; var names = { room : new Map(), tile : new Map(), // Note: Not currently enabled in the UI sprite : new Map(), item : new Map(), dialog : new Map(), }; function updateNamesFromCurData() { function createNameMap(objectStore) { var map = new Map(); for (id in objectStore) { if (objectStore[id].name != undefined && objectStore[id].name != null) { map.set(objectStore[id].name, id); } } return map; } names.room = createNameMap(room); names.tile = createNameMap(tile); names.sprite = createNameMap(sprite); names.item = createNameMap(item); names.dialog = createNameMap(dialog); } var spriteStartLocations = {}; /* VERSION */ var version = { major: 7, // major changes minor: 2, // smaller changes devBuildPhase: "RELEASE", }; function getEngineVersion() { return version.major + "." + version.minor; } /* FLAGS */ var flags; function resetFlags() { flags = { ROOM_FORMAT : 0 // 0 = non-comma separated, 1 = comma separated }; } resetFlags(); //init flags on load script // SUPER hacky location... :/ var editorDevFlags = { // NONE right now! }; function clearGameData() { room = {}; tile = {}; sprite = {}; item = {}; dialog = {}; palette = { //start off with a default palette "default" : { name : "default", colors : [[0,0,0],[255,255,255],[255,255,255]] } }; isEnding = false; //todo - correct place for this? variable = {}; // TODO RENDERER : clear data? spriteStartLocations = {}; // hacky to have this multiple times... names = { room : new Map(), tile : new Map(), sprite : new Map(), item : new Map(), dialog : new Map(), }; fontName = defaultFontName; // TODO : reset font manager too? textDirection = TextDirection.LeftToRight; } var width = 128; var height = 128; var scale = 4; //this is stupid but necessary var tilesize = 8; var mapsize = 16; var curRoom = "0"; var key = { left : 37, right : 39, up : 38, down : 40, space : 32, enter : 13, w : 87, a : 65, s : 83, d : 68, r : 82, shift : 16, ctrl : 17, alt : 18, cmd : 224 }; var prevTime = 0; var deltaTime = 0; //inventory update UI handles var onInventoryChanged = null; var onVariableChanged = null; var onGameReset = null; var isPlayerEmbeddedInEditor = false; var renderer = new Renderer(tilesize, scale); function getGameNameFromURL() { var game = window.location.hash.substring(1); // console.log("game name --- " + game); return game; } function attachCanvas(c) { canvas = c; canvas.width = width * scale; canvas.height = width * scale; ctx = canvas.getContext("2d"); dialogRenderer.AttachContext(ctx); renderer.AttachContext(ctx); } var curGameData = null; function load_game(game_data, startWithTitle) { curGameData = game_data; //remember the current game (used to reset the game) dialogBuffer.Reset(); scriptInterpreter.ResetEnvironment(); // ensures variables are reset -- is this the best way? parseWorld(game_data); if (!isPlayerEmbeddedInEditor) { // hack to ensure default font is available fontManager.AddResource(defaultFontName + fontManager.GetExtension(), document.getElementById(defaultFontName).text.slice(1)); } var font = fontManager.Get( fontName ); dialogBuffer.SetFont(font); dialogRenderer.SetFont(font); setInitialVariables(); // setInterval(updateLoadingScreen, 300); // hack test onready(startWithTitle); } function reset_cur_game() { if (curGameData == null) { return; //can't reset if we don't have the game data } stopGame(); clearGameData(); load_game(curGameData); if (isPlayerEmbeddedInEditor && onGameReset != null) { onGameReset(); } } var update_interval = null; function onready(startWithTitle) { if(startWithTitle === undefined || startWithTitle === null) startWithTitle = true; clearInterval(loading_interval); input = new InputManager(); document.addEventListener('keydown', input.onkeydown); document.addEventListener('keyup', input.onkeyup); if (isPlayerEmbeddedInEditor) { canvas.addEventListener('touchstart', input.ontouchstart, {passive:false}); canvas.addEventListener('touchmove', input.ontouchmove, {passive:false}); canvas.addEventListener('touchend', input.ontouchend, {passive:false}); } else { // creates a 'touchTrigger' element that covers the entire screen and can universally have touch event listeners added w/o issue. // we're checking for existing touchTriggers both at game start and end, so it's slightly redundant. var existingTouchTrigger = document.querySelector('#touchTrigger'); if (existingTouchTrigger === null){ var touchTrigger = document.createElement("div"); touchTrigger.setAttribute("id","touchTrigger"); // afaik css in js is necessary here to force a fullscreen element touchTrigger.setAttribute( "style","position: absolute; top: 0; left: 0; width: 100vw; height: 100vh; overflow: hidden;" ); document.body.appendChild(touchTrigger); touchTrigger.addEventListener('touchstart', input.ontouchstart); touchTrigger.addEventListener('touchmove', input.ontouchmove); touchTrigger.addEventListener('touchend', input.ontouchend); } } window.onblur = input.onblur; update_interval = setInterval(update,16); if(startWithTitle) { // used by editor startNarrating(getTitle()); } } function setInitialVariables() { for(id in variable) { var value = variable[id]; // default to string if(value === "true") { value = true; } else if(value === "false") { value = false; } else if(!isNaN(parseFloat(value))) { value = parseFloat(value); } scriptInterpreter.SetVariable(id,value); } scriptInterpreter.SetOnVariableChangeHandler( onVariableChanged ); } function getOffset(evt) { var offset = { x:0, y:0 }; var el = evt.target; var rect = el.getBoundingClientRect(); offset.x += rect.left + el.scrollLeft; offset.y += rect.top + el.scrollTop; offset.x = evt.clientX - offset.x; offset.y = evt.clientY - offset.y; return offset; } function stopGame() { console.log("stop GAME!"); document.removeEventListener('keydown', input.onkeydown); document.removeEventListener('keyup', input.onkeyup); if (isPlayerEmbeddedInEditor) { canvas.removeEventListener('touchstart', input.ontouchstart); canvas.removeEventListener('touchmove', input.ontouchmove); canvas.removeEventListener('touchend', input.ontouchend); } else { //check for touchTrigger and removes it var existingTouchTrigger = document.querySelector('#touchTrigger'); if (existingTouchTrigger !== null){ existingTouchTrigger.removeEventListener('touchstart', input.ontouchstart); existingTouchTrigger.removeEventListener('touchmove', input.ontouchmove); existingTouchTrigger.removeEventListener('touchend', input.ontouchend); existingTouchTrigger.parentElement.removeChild(existingTouchTrigger); } } window.onblur = null; clearInterval(update_interval); } /* loading animation */ var loading_anim_data = [ [ 0,1,1,1,1,1,1,0, 0,0,1,1,1,1,0,0, 0,0,1,1,1,1,0,0, 0,0,0,1,1,0,0,0, 0,0,0,1,1,0,0,0, 0,0,1,0,0,1,0,0, 0,0,1,0,0,1,0,0, 0,1,1,1,1,1,1,0, ], [ 0,1,1,1,1,1,1,0, 0,0,1,0,0,1,0,0, 0,0,1,1,1,1,0,0, 0,0,0,1,1,0,0,0, 0,0,0,1,1,0,0,0, 0,0,1,0,0,1,0,0, 0,0,1,1,1,1,0,0, 0,1,1,1,1,1,1,0, ], [ 0,1,1,1,1,1,1,0, 0,0,1,0,0,1,0,0, 0,0,1,0,0,1,0,0, 0,0,0,1,1,0,0,0, 0,0,0,1,1,0,0,0, 0,0,1,1,1,1,0,0, 0,0,1,1,1,1,0,0, 0,1,1,1,1,1,1,0, ], [ 0,1,1,1,1,1,1,0, 0,0,1,0,0,1,0,0, 0,0,1,0,0,1,0,0, 0,0,0,1,1,0,0,0, 0,0,0,1,1,0,0,0, 0,0,1,1,1,1,0,0, 0,0,1,1,1,1,0,0, 0,1,1,1,1,1,1,0, ], [ 0,0,0,0,0,0,0,0, 1,0,0,0,0,0,0,1, 1,1,1,0,0,1,1,1, 1,1,1,1,1,0,0,1, 1,1,1,1,1,0,0,1, 1,1,1,0,0,1,1,1, 1,0,0,0,0,0,0,1, 0,0,0,0,0,0,0,0, ] ]; var loading_anim_frame = 0; var loading_anim_speed = 500; var loading_interval = null; function loadingAnimation() { //create image var loadingAnimImg = ctx.createImageData(8*scale, 8*scale); //draw image for (var y = 0; y < 8; y++) { for (var x = 0; x < 8; x++) { var i = (y * 8) + x; if (loading_anim_data[loading_anim_frame][i] == 1) { //scaling nonsense for (var sy = 0; sy < scale; sy++) { for (var sx = 0; sx = 5) loading_anim_frame = 0; } function updateLoadingScreen() { // TODO : in progress ctx.fillStyle = "rgb(0,0,0)"; ctx.fillRect(0,0,canvas.width,canvas.height); loadingAnimation(); drawSprite( getSpriteImage(sprite["a"],"0",0), 8, 8, ctx ); } function update() { var curTime = Date.now(); deltaTime = curTime - prevTime; if (curRoom == null) { // in the special case where there is no valid room, end the game startNarrating( "", true /*isEnding*/ ); } if (!transition.IsTransitionActive()) { updateInput(); } if (transition.IsTransitionActive()) { // transition animation takes over everything! transition.UpdateTransition(deltaTime); } else { if (!isNarrating && !isEnding) { updateAnimation(); drawRoom( room[curRoom] ); // draw world if game has begun } else { //make sure to still clear screen ctx.fillStyle = "rgb(" + getPal(curPal())[0][0] + "," + getPal(curPal())[0][1] + "," + getPal(curPal())[0][2] + ")"; ctx.fillRect(0,0,canvas.width,canvas.height); } // if (isDialogMode) { // dialog mode if(dialogBuffer.IsActive()) { dialogRenderer.Draw( dialogBuffer, deltaTime ); dialogBuffer.Update( deltaTime ); } // keep moving avatar if player holds down button if( !dialogBuffer.IsActive() && !isEnding ) { if( curPlayerDirection != Direction.None ) { playerHoldToMoveTimer -= deltaTime; if( playerHoldToMoveTimer = animationTime ) { // animate sprites for (id in sprite) { var spr = sprite[id]; if (spr.animation.isAnimated) { spr.animation.frameIndex = ( spr.animation.frameIndex + 1 ) % spr.animation.frameCount; } } // animate tiles for (id in tile) { var til = tile[id]; if (til.animation.isAnimated) { til.animation.frameIndex = ( til.animation.frameIndex + 1 ) % til.animation.frameCount; } } // animate items for (id in item) { var itm = item[id]; if (itm.animation.isAnimated) { itm.animation.frameIndex = ( itm.animation.frameIndex + 1 ) % itm.animation.frameCount; } } // reset counter animationCounter = 0; } } function resetAllAnimations() { for (id in sprite) { var spr = sprite[id]; if (spr.animation.isAnimated) { spr.animation.frameIndex = 0; } } for (id in tile) { var til = tile[id]; if (til.animation.isAnimated) { til.animation.frameIndex = 0; } } for (id in item) { var itm = item[id]; if (itm.animation.isAnimated) { itm.animation.frameIndex = 0; } } } function getSpriteAt(x,y) { for (id in sprite) { var spr = sprite[id]; if (spr.room === curRoom) { if (spr.x == x && spr.y == y) { return id; } } } return null; } var Direction = { None : -1, Up : 0, Down : 1, Left : 2, Right : 3 }; var curPlayerDirection = Direction.None; var playerHoldToMoveTimer = 0; var InputManager = function() { var self = this; var pressed; var ignored; var newKeyPress; var touchState; function resetAll() { pressed = {}; ignored = {}; newKeyPress = false; touchState = { isDown : false, startX : 0, startY : 0, curX : 0, curY : 0, swipeDistance : 30, swipeDirection : Direction.None, tapReleased : false }; } resetAll(); function stopWindowScrolling(e) { if(e.keyCode == key.left || e.keyCode == key.right || e.keyCode == key.up || e.keyCode == key.down || !isPlayerEmbeddedInEditor) e.preventDefault(); } function tryRestartGame(e) { /* RESTART GAME */ if ( e.keyCode === key.r && ( e.getModifierState("Control") || e.getModifierState("Meta") ) ) { if ( confirm("Restart the game?") ) { reset_cur_game(); } } } function eventIsModifier(event) { return (event.keyCode == key.shift || event.keyCode == key.ctrl || event.keyCode == key.alt || event.keyCode == key.cmd); } function isModifierKeyDown() { return ( self.isKeyDown(key.shift) || self.isKeyDown(key.ctrl) || self.isKeyDown(key.alt) || self.isKeyDown(key.cmd) ); } this.ignoreHeldKeys = function() { for (var key in pressed) { if (pressed[key]) { // only ignore keys that are actually held ignored[key] = true; // console.log("IGNORE -- " + key); } } } this.onkeydown = function(event) { // console.log("KEYDOWN -- " + event.keyCode); stopWindowScrolling(event); tryRestartGame(event); // Special keys being held down can interfere with keyup events and lock movement // so just don't collect input when they're held { if (isModifierKeyDown()) { return; } if (eventIsModifier(event)) { resetAll(); } } if (ignored[event.keyCode]) { return; } if (!self.isKeyDown(event.keyCode)) { newKeyPress = true; } pressed[event.keyCode] = true; ignored[event.keyCode] = false; } this.onkeyup = function(event) { // console.log("KEYUP -- " + event.keyCode); pressed[event.keyCode] = false; ignored[event.keyCode] = false; } this.ontouchstart = function(event) { event.preventDefault(); if( event.changedTouches.length > 0 ) { touchState.isDown = true; touchState.startX = touchState.curX = event.changedTouches[0].clientX; touchState.startY = touchState.curY = event.changedTouches[0].clientY; touchState.swipeDirection = Direction.None; } } this.ontouchmove = function(event) { event.preventDefault(); if( touchState.isDown && event.changedTouches.length > 0 ) { touchState.curX = event.changedTouches[0].clientX; touchState.curY = event.changedTouches[0].clientY; var prevDirection = touchState.swipeDirection; if( touchState.curX - touchState.startX = touchState.swipeDistance ) { touchState.swipeDirection = Direction.Right; } else if( touchState.curY - touchState.startY = touchState.swipeDistance ) { touchState.swipeDirection = Direction.Down; } if( touchState.swipeDirection != prevDirection ) { // reset center so changing directions is easier touchState.startX = touchState.curX; touchState.startY = touchState.curY; } } } this.ontouchend = function(event) { event.preventDefault(); touchState.isDown = false; if( touchState.swipeDirection == Direction.None ) { // tap! touchState.tapReleased = true; } touchState.swipeDirection = Direction.None; } this.isKeyDown = function(keyCode) { return pressed[keyCode] != null && pressed[keyCode] == true && (ignored[keyCode] == null || ignored[keyCode] == false); } this.anyKeyPressed = function() { return newKeyPress; } this.resetKeyPressed = function() { newKeyPress = false; } this.swipeLeft = function() { return touchState.swipeDirection == Direction.Left; } this.swipeRight = function() { return touchState.swipeDirection == Direction.Right; } this.swipeUp = function() { return touchState.swipeDirection == Direction.Up; } this.swipeDown = function() { return touchState.swipeDirection == Direction.Down; } this.isTapReleased = function() { return touchState.tapReleased; } this.resetTapReleased = function() { touchState.tapReleased = false; } this.onblur = function() { // console.log("~~~ BLUR ~~"); resetAll(); } } var input = null; function movePlayer(direction) { if (player().room == null || !Object.keys(room).includes(player().room)) { return; // player room is missing or invalid.. can't move them! } var spr = null; if ( curPlayerDirection == Direction.Left && !(spr = getSpriteLeft()) && !isWallLeft()) { player().x -= 1; } else if ( curPlayerDirection == Direction.Right && !(spr = getSpriteRight()) && !isWallRight()) { player().x += 1; } else if ( curPlayerDirection == Direction.Up && !(spr = getSpriteUp()) && !isWallUp()) { player().y -= 1; } else if ( curPlayerDirection == Direction.Down && !(spr = getSpriteDown()) && !isWallDown()) { player().y += 1; } var ext = getExit( player().room, player().x, player().y ); var end = getEnding( player().room, player().x, player().y ); var itmIndex = getItemIndex( player().room, player().x, player().y ); // do items first, because you can pick up an item AND go through a door if (itmIndex > -1) { var itm = room[player().room].items[itmIndex]; var itemRoom = player().room; startItemDialog(itm.id, function() { // remove item from room room[itemRoom].items.splice(itmIndex, 1); // update player inventory if (player().inventory[itm.id]) { player().inventory[itm.id] += 1; } else { player().inventory[itm.id] = 1; } // show inventory change in UI if (onInventoryChanged != null) { onInventoryChanged(itm.id); } }); } if (end) { startEndingDialog(end); } else if (ext) { movePlayerThroughExit(ext); } else if (spr) { startSpriteDialog(spr /*spriteId*/); } } var transition = new TransitionManager(); function movePlayerThroughExit(ext) { var GoToDest = function() { if (ext.transition_effect != null) { transition.BeginTransition(player().room, player().x, player().y, ext.dest.room, ext.dest.x, ext.dest.y, ext.transition_effect); transition.UpdateTransition(0); } player().room = ext.dest.room; player().x = ext.dest.x; player().y = ext.dest.y; curRoom = ext.dest.room; initRoom(curRoom); }; if (ext.dlg != undefined && ext.dlg != null) { // TODO : I need to simplify dialog code, // so I don't have to get the ID and the source str // every time! startDialog( dialog[ext.dlg].src, ext.dlg, function(result) { var isLocked = ext.property && ext.property.locked === true; if (!isLocked) { GoToDest(); } }, ext); } else { GoToDest(); } } function initRoom(roomId) { // init exit properties for (var i = 0; i < room[roomId].exits.length; i++) { room[roomId].exits[i].property = { locked:false }; } // init ending properties for (var i = 0; i < room[roomId].endings.length; i++) { room[roomId].endings[i].property = { locked:false }; } } function getItemIndex( roomId, x, y ) { for( var i = 0; i < room[roomId].items.length; i++ ) { var itm = room[roomId].items[i]; if ( itm.x == x && itm.y == y) return i; } return -1; } function getSpriteLeft() { //repetitive? return getSpriteAt( player().x - 1, player().y ); } function getSpriteRight() { return getSpriteAt( player().x + 1, player().y ); } function getSpriteUp() { return getSpriteAt( player().x, player().y - 1 ); } function getSpriteDown() { return getSpriteAt( player().x, player().y + 1 ); } function isWallLeft() { return (player().x - 1 = 16) || isWall( player().x + 1, player().y ); } function isWallUp() { return (player().y - 1 = 16) || isWall( player().x, player().y + 1 ); } function isWall(x,y,roomId) { if(roomId == undefined || roomId == null) roomId = curRoom; var tileId = getTile( x, y ); if( tileId === '0' ) return false; // Blank spaces aren't walls, ya doofus if( tile[tileId].isWall === undefined || tile[tileId].isWall === null ) { // No wall-state defined: check room-specific walls var i = room[roomId].walls.indexOf( getTile(x,y) ); return i > -1; } // Otherwise, use the tile's own wall-state return tile[tileId].isWall; } function getItem(roomId,x,y) { for (i in room[roomId].items) { var item = room[roomId].items[i]; if (x == item.x && y == item.y) { return item; } } return null; } function getExit(roomId,x,y) { for (i in room[roomId].exits) { var e = room[roomId].exits[i]; if (x == e.x && y == e.y) { return e; } } return null; } function getEnding(roomId,x,y) { for (i in room[roomId].endings) { var e = room[roomId].endings[i]; if (x == e.x && y == e.y) { return e; } } return null; } function getTile(x,y) { // console.log(x + " " + y); var t = getRoom().tilemap[y][x]; return t; } function player() { return sprite[playerId]; } // Sort of a hack for legacy palette code (when it was just an array) function getPal(id) { if (palette[id] === undefined) { id = "default"; } return palette[ id ].colors; } function getRoom() { return room[curRoom]; } function isSpriteOffstage(id) { return sprite[id].room == null; } function parseWorld(file) { spriteStartLocations = {}; resetFlags(); var versionNumber = 0; // flags to keep track of which compatibility conversions // need to be applied to this game data var compatibilityFlags = { convertSayToPrint : false, combineEndingsWithDialog : false, convertImplicitSpriteDialogIds : false, }; var lines = file.split("\n"); var i = 0; while (i < lines.length) { var curLine = lines[i]; // console.log(lines[i]); if (i == 0) { i = parseTitle(lines, i); } else if (curLine.length <= 0 || curLine.charAt(0) === "#") { // collect version number (from a comment.. hacky I know) if (curLine.indexOf("# BITSY VERSION ") != -1) { versionNumber = parseFloat(curLine.replace("# BITSY VERSION ", "")); if (versionNumber < 5.0) { compatibilityFlags.convertSayToPrint = true; } if (versionNumber 0) { // player not in any room! what the heck curRoom = roomIds[0]; } else { // uh oh there are no rooms I guess??? curRoom = null; } if (curRoom != null) { initRoom(curRoom); } renderer.SetPalettes(palette); scriptCompatibility(compatibilityFlags); return versionNumber; } function scriptCompatibility(compatibilityFlags) { if (compatibilityFlags.convertSayToPrint) { console.log("CONVERT SAY TO PRINT!"); var PrintFunctionVisitor = function() { var didChange = false; this.DidChange = function() { return didChange; }; this.Visit = function(node) { if (node.type != "function") { return; } if (node.name === "say") { node.name = "print"; didChange = true; } }; }; for (dlgId in dialog) { var dialogScript = scriptInterpreter.Parse(dialog[dlgId].src); var visitor = new PrintFunctionVisitor(); dialogScript.VisitAll(visitor); if (visitor.DidChange()) { var newDialog = dialogScript.Serialize(); if (newDialog.indexOf("\n") > -1) { newDialog = '"""\n' + newDialog + '\n"""'; } dialog[dlgId].src = newDialog; } } } } //TODO this is in progress and doesn't support all features function serializeWorld(skipFonts) { if (skipFonts === undefined || skipFonts === null) skipFonts = false; var worldStr = ""; /* TITLE */ worldStr += getTitle() + "\n"; worldStr += "\n"; /* VERSION */ worldStr += "# BITSY VERSION " + getEngineVersion() + "\n"; // add version as a comment for debugging purposes if (version.devBuildPhase != "RELEASE") { worldStr += "# DEVELOPMENT BUILD -- " + version.devBuildPhase; } worldStr += "\n"; /* FLAGS */ for (f in flags) { worldStr += "! " + f + " " + flags[f] + "\n"; } worldStr += "\n" /* FONT */ if (fontName != defaultFontName) { worldStr += "DEFAULT_FONT " + fontName + "\n"; worldStr += "\n" } if (textDirection != TextDirection.LeftToRight) { worldStr += "TEXT_DIRECTION " + textDirection + "\n"; worldStr += "\n" } /* PALETTE */ for (id in palette) { if (id != "default") { worldStr += "PAL " + id + "\n"; if( palette[id].name != null ) worldStr += "NAME " + palette[id].name + "\n"; for (i in getPal(id)) { for (j in getPal(id)[i]) { worldStr += getPal(id)[i][j]; if (j < 2) worldStr += ","; } worldStr += "\n"; } worldStr += "\n"; } } /* ROOM */ for (id in room) { worldStr += "ROOM " + id + "\n"; if ( flags.ROOM_FORMAT == 0 ) { // old non-comma separated format for (i in room[id].tilemap) { for (j in room[id].tilemap[i]) { worldStr += room[id].tilemap[i][j]; } worldStr += "\n"; } } else if ( flags.ROOM_FORMAT == 1 ) { // new comma separated format for (i in room[id].tilemap) { for (j in room[id].tilemap[i]) { worldStr += room[id].tilemap[i][j]; if (j 0) { /* WALLS */ worldStr += "WAL "; for (j in room[id].walls) { worldStr += room[id].walls[j]; if (j 0) { /* ITEMS */ for (j in room[id].items) { var itm = room[id].items[j]; worldStr += "ITM " + itm.id + " " + itm.x + "," + itm.y; worldStr += "\n"; } } if (room[id].exits.length > 0) { /* EXITS */ for (j in room[id].exits) { var e = room[id].exits[j]; if ( isExitValid(e) ) { worldStr += "EXT " + e.x + "," + e.y + " " + e.dest.room + " " + e.dest.x + "," + e.dest.y; if (e.transition_effect != undefined && e.transition_effect != null) { worldStr += " FX " + e.transition_effect; } if (e.dlg != undefined && e.dlg != null) { worldStr += " DLG " + e.dlg; } worldStr += "\n"; } } } if (room[id].endings.length > 0) { /* ENDINGS */ for (j in room[id].endings) { var e = room[id].endings[j]; // todo isEndingValid worldStr += "END " + e.id + " " + e.x + "," + e.y; worldStr += "\n"; } } if (room[id].pal != null && room[id].pal != "default") { /* PALETTE */ worldStr += "PAL " + room[id].pal + "\n"; } worldStr += "\n"; } /* TILES */ for (id in tile) { worldStr += "TIL " + id + "\n"; worldStr += serializeDrawing( "TIL_" + id ); if (tile[id].name != null && tile[id].name != undefined) { /* NAME */ worldStr += "NAME " + tile[id].name + "\n"; } if (tile[id].isWall != null && tile[id].isWall != undefined) { /* WALL */ worldStr += "WAL " + tile[id].isWall + "\n"; } if (tile[id].col != null && tile[id].col != undefined && tile[id].col != 1) { /* COLOR OVERRIDE */ worldStr += "COL " + tile[id].col + "\n"; } worldStr += "\n"; } /* SPRITES */ for (id in sprite) { worldStr += "SPR " + id + "\n"; worldStr += serializeDrawing( "SPR_" + id ); if (sprite[id].name != null && sprite[id].name != undefined) { /* NAME */ worldStr += "NAME " + sprite[id].name + "\n"; } if (sprite[id].dlg != null) { worldStr += "DLG " + sprite[id].dlg + "\n"; } if (sprite[id].room != null) { /* SPRITE POSITION */ worldStr += "POS " + sprite[id].room + " " + sprite[id].x + "," + sprite[id].y + "\n"; } if (sprite[id].inventory != null) { for(itemId in sprite[id].inventory) { worldStr += "ITM " + itemId + " " + sprite[id].inventory[itemId] + "\n"; } } if (sprite[id].col != null && sprite[id].col != undefined && sprite[id].col != 2) { /* COLOR OVERRIDE */ worldStr += "COL " + sprite[id].col + "\n"; } worldStr += "\n"; } /* ITEMS */ for (id in item) { worldStr += "ITM " + id + "\n"; worldStr += serializeDrawing( "ITM_" + id ); if (item[id].name != null && item[id].name != undefined) { /* NAME */ worldStr += "NAME " + item[id].name + "\n"; } if (item[id].dlg != null) { worldStr += "DLG " + item[id].dlg + "\n"; } if (item[id].col != null && item[id].col != undefined && item[id].col != 2) { /* COLOR OVERRIDE */ worldStr += "COL " + item[id].col + "\n"; } worldStr += "\n"; } /* DIALOG */ for (id in dialog) { if (id != titleDialogId) { worldStr += "DLG " + id + "\n"; worldStr += dialog[id].src + "\n"; if (dialog[id].name != null) { worldStr += "NAME " + dialog[id].name + "\n"; } worldStr += "\n"; } } /* VARIABLES */ for (id in variable) { worldStr += "VAR " + id + "\n"; worldStr += variable[id] + "\n"; worldStr += "\n"; } /* FONT */ // TODO : support multiple fonts if (fontName != defaultFontName && !skipFonts) { worldStr += fontManager.GetData(fontName); } return worldStr; } function serializeDrawing(drwId) { var imageSource = renderer.GetImageSource(drwId); var drwStr = ""; for (f in imageSource) { for (y in imageSource[f]) { var rowStr = ""; for (x in imageSource[f][y]) { rowStr += imageSource[f][y][x]; } drwStr += rowStr + "\n"; } if (f \n"; } return drwStr; } function isExitValid(e) { var hasValidStartPos = e.x >= 0 && e.x = 0 && e.y = 0 && e.dest.x = 0 && e.dest.y < 16); return hasValidStartPos && hasDest && hasValidRoomDest; } function placeSprites() { for (id in spriteStartLocations) { //console.log(id); //console.log( spriteStartLocations[id] ); //console.log(sprite[id]); sprite[id].room = spriteStartLocations[id].room; sprite[id].x = spriteStartLocations[id].x; sprite[id].y = spriteStartLocations[id].y; //console.log(sprite[id]); } } /* ARGUMENT GETTERS */ function getType(line) { return getArg(line,0); } function getId(line) { return getArg(line,1); } function getArg(line,arg) { return line.split(" ")[arg]; } function getCoord(line,arg) { return getArg(line,arg).split(","); } function parseTitle(lines, i) { var results = scriptUtils.ReadDialogScript(lines,i); setTitle(results.script); i = results.index; i++; return i; } function parseRoom(lines, i, compatibilityFlags) { var id = getId(lines[i]); room[id] = { id : id, tilemap : [], walls : [], exits : [], endings : [], items : [], pal : null, name : null }; i++; // create tile map if ( flags.ROOM_FORMAT == 0 ) { // old way: no commas, single char tile ids var end = i + mapsize; var y = 0; for (; i<end; i++) { room[id].tilemap.push( [] ); for (x = 0; x<mapsize; x++) { room[id].tilemap[y].push( lines[i].charAt(x) ); } y++; } } else if ( flags.ROOM_FORMAT == 1 ) { // new way: comma separated, multiple char tile ids var end = i + mapsize; var y = 0; for (; i<end; i++) { room[id].tilemap.push( [] ); var lineSep = lines[i].split(","); for (x = 0; x<mapsize; x++) { room[id].tilemap[y].push( lineSep[x] ); } y++; } } while (i 0) { //look for empty line // console.log(getType(lines[i])); if (getType(lines[i]) === "SPR") { /* NOTE SPRITE START LOCATIONS */ var sprId = getId(lines[i]); if (sprId.indexOf(",") == -1 && lines[i].split(" ").length >= 3) { //second conditional checks for coords /* PLACE A SINGLE SPRITE */ var sprCoord = lines[i].split(" ")[2].split(","); spriteStartLocations[sprId] = { room : id, x : parseInt(sprCoord[0]), y : parseInt(sprCoord[1]) }; } else if ( flags.ROOM_FORMAT == 0 ) { // TODO: right now this shortcut only works w/ the old comma separate format /* PLACE MULTIPLE SPRITES*/ //Does find and replace in the tilemap (may be hacky, but its convenient) var sprList = sprId.split(","); for (row in room[id].tilemap) { for (s in sprList) { var col = room[id].tilemap[row].indexOf( sprList[s] ); //if the sprite is in this row, replace it with the "null tile" and set its starting position if (col != -1) { room[id].tilemap[row][col] = "0"; spriteStartLocations[ sprList[s] ] = { room : id, x : parseInt(col), y : parseInt(row) }; } } } } } else if (getType(lines[i]) === "ITM") { var itmId = getId(lines[i]); var itmCoord = lines[i].split(" ")[2].split(","); var itm = { id: itmId, x : parseInt(itmCoord[0]), y : parseInt(itmCoord[1]) }; room[id].items.push( itm ); } else if (getType(lines[i]) === "WAL") { /* DEFINE COLLISIONS (WALLS) */ room[id].walls = getId(lines[i]).split(","); } else if (getType(lines[i]) === "EXT") { /* ADD EXIT */ var exitArgs = lines[i].split(" "); //arg format: EXT 10,5 M 3,2 [AVA:7 LCK:a,9] [AVA 7 LCK a 9] var exitCoords = exitArgs[1].split(","); var destName = exitArgs[2]; var destCoords = exitArgs[3].split(","); var ext = { x : parseInt(exitCoords[0]), y : parseInt(exitCoords[1]), dest : { room : destName, x : parseInt(destCoords[0]), y : parseInt(destCoords[1]) }, transition_effect : null, dlg: null, }; // optional arguments var exitArgIndex = 4; while (exitArgIndex < exitArgs.length) { if (exitArgs[exitArgIndex] == "FX") { ext.transition_effect = exitArgs[exitArgIndex+1]; exitArgIndex += 2; } else if (exitArgs[exitArgIndex] == "DLG") { ext.dlg = exitArgs[exitArgIndex+1]; exitArgIndex += 2; } else { exitArgIndex += 1; } } room[id].exits.push(ext); } else if (getType(lines[i]) === "END") { /* ADD ENDING */ var endId = getId(lines[i]); // compatibility with when endings were stored separate from other dialog if (compatibilityFlags.combineEndingsWithDialog) { endId = "end_" + endId; } var endCoords = getCoord(lines[i], 2); var end = { id : endId, x : parseInt(endCoords[0]), y : parseInt(endCoords[1]) }; room[id].endings.push(end); } else if (getType(lines[i]) === "PAL") { /* CHOOSE PALETTE (that's not default) */ room[id].pal = getId(lines[i]); } else if (getType(lines[i]) === "NAME") { var name = lines[i].split(/\s(.+)/)[1]; room[id].name = name; names.room.set(name, id); } i++; } return i; } function parsePalette(lines,i) { //todo this has to go first right now 😦 var id = getId(lines[i]); i++; var colors = []; var name = null; while (i 0) { //look for empty line var args = lines[i].split(" "); if (args[0] === "NAME") { name = lines[i].split(/\s(.+)/)[1]; } else { var col = []; lines[i].split(",").forEach(function(i) { col.push(parseInt(i)); }); colors.push(col); } i++; } palette[id] = { id : id, name : name, colors : colors }; return i; } function parseTile(lines, i) { var id = getId(lines[i]); var drwId = null; var name = null; i++; if (getType(lines[i]) === "DRW") { //load existing drawing drwId = getId(lines[i]); i++; } else { // store tile source drwId = "TIL_" + id; i = parseDrawingCore( lines, i, drwId ); } //other properties var colorIndex = 1; // default palette color index is 1 var isWall = null; // null indicates it can vary from room to room (original version) while (i 0) { //look for empty line if (getType(lines[i]) === "COL") { colorIndex = parseInt( getId(lines[i]) ); } else if (getType(lines[i]) === "NAME") { /* NAME */ name = lines[i].split(/\s(.+)/)[1]; names.tile.set( name, id ); } else if (getType(lines[i]) === "WAL") { var wallArg = getArg( lines[i], 1 ); if( wallArg === "true" ) { isWall = true; } else if( wallArg === "false" ) { isWall = false; } } i++; } //tile data tile[id] = { id : id, drw : drwId, //drawing id col : colorIndex, animation : { isAnimated : (renderer.GetFrameCount(drwId) > 1), frameIndex : 0, frameCount : renderer.GetFrameCount(drwId) }, name : name, isWall : isWall }; return i; } function parseSprite(lines, i) { var id = getId(lines[i]); var drwId = null; var name = null; i++; if (getType(lines[i]) === "DRW") { //load existing drawing drwId = getId(lines[i]); i++; } else { // store sprite source drwId = "SPR_" + id; i = parseDrawingCore( lines, i, drwId ); } //other properties var colorIndex = 2; //default palette color index is 2 var dialogId = null; var startingInventory = {}; while (i 0) { //look for empty line if (getType(lines[i]) === "COL") { /* COLOR OFFSET INDEX */ colorIndex = parseInt( getId(lines[i]) ); } else if (getType(lines[i]) === "POS") { /* STARTING POSITION */ var posArgs = lines[i].split(" "); var roomId = posArgs[1]; var coordArgs = posArgs[2].split(","); spriteStartLocations[id] = { room : roomId, x : parseInt(coordArgs[0]), y : parseInt(coordArgs[1]) }; } else if(getType(lines[i]) === "DLG") { dialogId = getId(lines[i]); } else if (getType(lines[i]) === "NAME") { /* NAME */ name = lines[i].split(/\s(.+)/)[1]; names.sprite.set( name, id ); } else if (getType(lines[i]) === "ITM") { /* ITEM STARTING INVENTORY */ var itemId = getId(lines[i]); var itemCount = parseFloat( getArg(lines[i], 2) ); startingInventory[itemId] = itemCount; } i++; } //sprite data sprite[id] = { id : id, drw : drwId, //drawing id col : colorIndex, dlg : dialogId, room : null, //default location is "offstage" x : -1, y : -1, animation : { isAnimated : (renderer.GetFrameCount(drwId) > 1), frameIndex : 0, frameCount : renderer.GetFrameCount(drwId) }, inventory : startingInventory, name : name }; return i; } function parseItem(lines, i) { var id = getId(lines[i]); var drwId = null; var name = null; i++; if (getType(lines[i]) === "DRW") { //load existing drawing drwId = getId(lines[i]); i++; } else { // store item source drwId = "ITM_" + id; // these prefixes are maybe a terrible way to differentiate drawing tyepes :/ i = parseDrawingCore( lines, i, drwId ); } //other properties var colorIndex = 2; //default palette color index is 2 var dialogId = null; while (i 0) { //look for empty line if (getType(lines[i]) === "COL") { /* COLOR OFFSET INDEX */ colorIndex = parseInt( getArg( lines[i], 1 ) ); } // else if (getType(lines[i]) === "POS") { // /* STARTING POSITION */ // var posArgs = lines[i].split(" "); // var roomId = posArgs[1]; // var coordArgs = posArgs[2].split(","); // spriteStartLocations[id] = { // room : roomId, // x : parseInt(coordArgs[0]), // y : parseInt(coordArgs[1]) // }; // } else if(getType(lines[i]) === "DLG") { dialogId = getId(lines[i]); } else if (getType(lines[i]) === "NAME") { /* NAME */ name = lines[i].split(/\s(.+)/)[1]; names.item.set( name, id ); } i++; } //item data item[id] = { id : id, drw : drwId, //drawing id col : colorIndex, dlg : dialogId, // room : null, //default location is "offstage" // x : -1, // y : -1, animation : { isAnimated : (renderer.GetFrameCount(drwId) > 1), frameIndex : 0, frameCount : renderer.GetFrameCount(drwId) }, name : name }; // console.log("ITM " + id); // console.log(item[id]); return i; } function parseDrawing(lines, i) { // store drawing source var drwId = getId( lines[i] ); return parseDrawingCore( lines, i, drwId ); } function parseDrawingCore(lines, i, drwId) { var frameList = []; //init list of frames frameList.push( [] ); //init first frame var frameIndex = 0; var y = 0; while ( y < tilesize ) { var l = lines[i+y]; var row = []; for (x = 0; x " ) { // start next frame! frameList.push( [] ); frameIndex++; //start the count over again for the next frame i++; y = 0; } } } renderer.SetImageSource(drwId, frameList); return i; } function parseScript(lines, i, backCompatPrefix, compatibilityFlags) { var id = getId(lines[i]); id = backCompatPrefix + id; i++; var results = scriptUtils.ReadDialogScript(lines,i); dialog[id] = { src:results.script, name:null }; if (compatibilityFlags.convertImplicitSpriteDialogIds) { // explicitly hook up dialog that used to be implicitly // connected by sharing sprite and dialog IDs in old versions if (sprite[id]) { if (sprite[id].dlg === undefined || sprite[id].dlg === null) { sprite[id].dlg = id; } } } i = results.index; return i; } function parseDialog(lines, i, compatibilityFlags) { // hacky but I need to store this so I can set the name below var id = getId(lines[i]); i = parseScript(lines, i, "", compatibilityFlags); if (lines[i].length > 0 && getType(lines[i]) === "NAME") { dialog[id].name = lines[i].split(/\s(.+)/)[1]; // TODO : hacky to keep copying this regex around... names.dialog.set(dialog[id].name, id); i++; } return i; } // keeping this around to parse old files where endings were separate from dialogs function parseEnding(lines, i, compatibilityFlags) { return parseScript(lines, i, "end_", compatibilityFlags); } function parseVariable(lines, i) { var id = getId(lines[i]); i++; var value = lines[i]; i++; variable[id] = value; return i; } function parseFontName(lines, i) { fontName = getArg(lines[i], 1); i++; return i; } function parseTextDirection(lines, i) { textDirection = getArg(lines[i], 1); i++; return i; } function parseFontData(lines, i) { // NOTE : we're not doing the actual parsing here -- // just grabbing the block of text that represents the font // and giving it to the font manager to use later var localFontName = getId(lines[i]); var localFontData = lines[i]; i++; while (i < lines.length && lines[i] != "") { localFontData += "\n" + lines[i]; i++; } var localFontFilename = localFontName + fontManager.GetExtension(); fontManager.AddResource( localFontFilename, localFontData ); return i; } function parseFlag(lines, i) { var id = getId(lines[i]); var valStr = lines[i].split(" ")[2]; flags[id] = parseInt( valStr ); i++; return i; } function drawTile(img,x,y,context) { if (!context) { //optional pass in context; otherwise, use default context = ctx; } // NOTE: images are now canvases, instead of raw image data (for chrome performance reasons) context.drawImage(img,x*tilesize*scale,y*tilesize*scale,tilesize*scale,tilesize*scale); } function drawSprite(img,x,y,context) { //this may differ later (or not haha) drawTile(img,x,y,context); } function drawItem(img,x,y,context) { drawTile(img,x,y,context); //TODO these methods are dumb and repetitive } // var debugLastRoomDrawn = "0"; function drawRoom(room,context,frameIndex) { // context & frameIndex are optional if (!context) { //optional pass in context; otherwise, use default (ok this is REAL hacky isn't it) context = ctx; } // if (room.id != debugLastRoomDrawn) { // debugLastRoomDrawn = room.id; // console.log("DRAW ROOM " + debugLastRoomDrawn); // } var paletteId = "default"; if (room === undefined) { // protect against invalid rooms context.fillStyle = "rgb(" + getPal(paletteId)[0][0] + "," + getPal(paletteId)[0][1] + "," + getPal(paletteId)[0][2] + ")"; context.fillRect(0,0,canvas.width,canvas.height); return; } //clear screen if (room.pal != null && palette[paletteId] != undefined) { paletteId = room.pal; } context.fillStyle = "rgb(" + getPal(paletteId)[0][0] + "," + getPal(paletteId)[0][1] + "," + getPal(paletteId)[0][2] + ")"; context.fillRect(0,0,canvas.width,canvas.height); //draw tiles for (i in room.tilemap) { for (j in room.tilemap[i]) { var id = room.tilemap[i][j]; if (id != "0") { //console.log(id); if (tile[id] == null) { // hack-around to avoid corrupting files (not a solution though!) id = "0"; room.tilemap[i][j] = id; } else { // console.log(id); drawTile( getTileImage(tile[id],paletteId,frameIndex), j, i, context ); } } } } //draw items for (var i = 0; i < room.items.length; i++) { var itm = room.items[i]; drawItem( getItemImage(item[itm.id],paletteId,frameIndex), itm.x, itm.y, context ); } //draw sprites for (id in sprite) { var spr = sprite[id]; if (spr.room === room.id) { drawSprite( getSpriteImage(spr,paletteId,frameIndex), spr.x, spr.y, context ); } } } // TODO : remove these get*Image methods function getTileImage(t,palId,frameIndex) { return renderer.GetImage(t,palId,frameIndex); } function getSpriteImage(s,palId,frameIndex) { return renderer.GetImage(s,palId,frameIndex); } function getItemImage(itm,palId,frameIndex) { return renderer.GetImage(itm,palId,frameIndex); } function curPal() { return getRoomPal(curRoom); } function getRoomPal(roomId) { var defaultId = "default"; if (roomId == null) { return defaultId; } else if (room[roomId].pal != null) { //a specific palette was chosen return room[roomId].pal; } else { if (roomId in palette) { //there is a palette matching the name of the room return roomId; } else { //use the default palette return defaultId; } } return defaultId; } var isDialogMode = false; var isNarrating = false; var isEnding = false; var dialogModule = new Dialog(); var dialogRenderer = dialogModule.CreateRenderer(); var dialogBuffer = dialogModule.CreateBuffer(); var fontManager = new FontManager(); // TODO : is this scriptResult thing being used anywhere??? function onExitDialog(scriptResult, dialogCallback) { console.log("EXIT DIALOG!"); isDialogMode = false; if (isNarrating) { isNarrating = false; } if (isDialogPreview) { isDialogPreview = false; if (onDialogPreviewEnd != null) { onDialogPreviewEnd(); } } if (dialogCallback != undefined && dialogCallback != null) { dialogCallback(scriptResult); } } /* TODO - titles and endings should also take advantage of the script pre-compilation if possible?? - could there be a namespace collision? - what about dialog NAMEs vs IDs? - what about a special script block separate from DLG? */ function startNarrating(dialogStr,end) { console.log("NARRATE " + dialogStr); if(end === undefined) { end = false; } isNarrating = true; isEnding = end; startDialog(dialogStr); } function startEndingDialog(ending) { isNarrating = true; isEnding = true; startDialog( dialog[ending.id].src, ending.id, function() { var isLocked = ending.property && ending.property.locked === true; if (isLocked) { isEnding = false; } }, ending); } function startItemDialog(itemId, dialogCallback) { var dialogId = item[itemId].dlg; // console.log("START ITEM DIALOG " + dialogId); if (dialog[dialogId]) { var dialogStr = dialog[dialogId].src; startDialog(dialogStr, dialogId, dialogCallback); } else { dialogCallback(); } } function startSpriteDialog(spriteId) { var spr = sprite[spriteId]; var dialogId = spr.dlg; // console.log("START SPRITE DIALOG " + dialogId); if (dialog[dialogId]){ var dialogStr = dialog[dialogId].src; startDialog(dialogStr,dialogId); } } function startDialog(dialogStr, scriptId, dialogCallback, objectContext) { // console.log("START DIALOG "); if (dialogStr.length <= 0) { // console.log("ON EXIT DIALOG -- startDialog 1"); onExitDialog(null, dialogCallback); return; } isDialogMode = true; dialogRenderer.Reset(); dialogRenderer.SetCentered(isNarrating /*centered*/); dialogBuffer.Reset(); scriptInterpreter.SetDialogBuffer(dialogBuffer); var onScriptEnd = function(scriptResult) { dialogBuffer.OnDialogEnd(function() { onExitDialog(scriptResult, dialogCallback); }); }; if (scriptId === undefined) { // TODO : what's this for again? scriptInterpreter.Interpret(dialogStr, onScriptEnd); } else { if (!scriptInterpreter.HasScript(scriptId)) { scriptInterpreter.Compile(scriptId, dialogStr); } // scriptInterpreter.DebugVisualizeScript(scriptId); scriptInterpreter.Run(scriptId, onScriptEnd, objectContext); } } var isDialogPreview = false; function startPreviewDialog(script, dialogCallback) { isNarrating = true; isDialogMode = true; isDialogPreview = true; dialogRenderer.Reset(); dialogRenderer.SetCentered(true); dialogBuffer.Reset(); scriptInterpreter.SetDialogBuffer(dialogBuffer); // TODO : do I really need a seperate callback for this debug mode?? onDialogPreviewEnd = dialogCallback; var onScriptEndCallback = function(scriptResult) { dialogBuffer.OnDialogEnd(function() { onExitDialog(scriptResult, null); }); }; scriptInterpreter.Eval(script, onScriptEndCallback); } /* NEW SCRIPT STUFF */ var scriptModule = new Script(); var scriptInterpreter = scriptModule.CreateInterpreter(); var scriptUtils = scriptModule.CreateUtils(); // TODO: move to editor.js? // scriptInterpreter.SetDialogBuffer( dialogBuffer ); FONT ascii_small SIZE 6 8 CHAR 0 000000 000000 000000 000000 000000 000000 000000 000000 CHAR 1 001110 010001 011011 010001 010101 010001 001110 000000 CHAR 2 001110 011111 010101 011111 010001 011111 001110 000000 CHAR 3 000000 001010 011111 011111 011111 001110 000100 000000 CHAR 4 000000 000000 001010 001110 001110 000100 000000 000000 CHAR 5 000100 001110 001110 000100 011111 011111 000100 000000 CHAR 6 000000 000100 001110 011111 011111 000100 001110 000000 CHAR 7 000000 000000 000000 001100 001100 000000 000000 000000 CHAR 8 111111 111111 111111 110011 110011 111111 111111 111111 CHAR 9 000000 000000 011110 010010 010010 011110 000000 000000 CHAR 10 111111 111111 100001 101101 101101 100001 111111 111111 CHAR 11 000000 000111 000011 001101 010010 010010 001100 000000 CHAR 12 001110 010001 010001 001110 000100 001110 000100 000000 CHAR 13 000100 000110 000101 000100 001100 011100 011000 000000 CHAR 14 000011 001101 001011 001101 001011 011011 011000 000000 CHAR 15 000000 010101 001110 011011 001110 010101 000000 000000 CHAR 16 001000 001100 001110 001111 001110 001100 001000 000000 CHAR 17 000010 000110 001110 011110 001110 000110 000010 000000 CHAR 18 000100 001110 011111 000100 011111 001110 000100 000000 CHAR 19 001010 001010 001010 001010 001010 000000 001010 000000 CHAR 20 001111 010101 010101 001101 000101 000101 000101 000000 CHAR 21 001110 010001 001100 001010 000110 010001 001110 000000 CHAR 22 000000 000000 000000 000000 000000 011110 011110 000000 CHAR 23 000100 001110 011111 000100 011111 001110 000100 001110 CHAR 24 000100 001110 011111 000100 000100 000100 000100 000000 CHAR 25 000100 000100 000100 000100 011111 001110 000100 000000 CHAR 26 000000 000100 000110 011111 000110 000100 000000 000000 CHAR 27 000000 000100 001100 011111 001100 000100 000000 000000 CHAR 28 000000 000000 000000 010000 010000 010000 011111 000000 CHAR 29 000000 001010 001010 011111 001010 001010 000000 000000 CHAR 30 000100 000100 001110 001110 011111 011111 000000 000000 CHAR 31 011111 011111 001110 001110 000100 000100 000000 000000 CHAR 32 000000 000000 000000 000000 000000 000000 000000 000000 CHAR 33 000100 001110 001110 000100 000100 000000 000100 000000 CHAR 34 011011 011011 010010 000000 000000 000000 000000 000000 CHAR 35 000000 001010 011111 001010 001010 011111 001010 000000 CHAR 36 001000 001110 010000 001100 000010 011100 000100 000000 CHAR 37 011001 011001 000010 000100 001000 010011 010011 000000 CHAR 38 001000 010100 010100 001000 010101 010010 001101 000000 CHAR 39 001100 001100 001000 000000 000000 000000 000000 000000 CHAR 40 000100 001000 001000 001000 001000 001000 000100 000000 CHAR 41 001000 000100 000100 000100 000100 000100 001000 000000 CHAR 42 000000 001010 001110 011111 001110 001010 000000 000000 CHAR 43 000000 000100 000100 011111 000100 000100 000000 000000 CHAR 44 000000 000000 000000 000000 000000 001100 001100 001000 CHAR 45 000000 000000 000000 011111 000000 000000 000000 000000 CHAR 46 000000 000000 000000 000000 000000 001100 001100 000000 CHAR 47 000000 000001 000010 000100 001000 010000 000000 000000 CHAR 48 001110 010001 010011 010101 011001 010001 001110 000000 CHAR 49 000100 001100 000100 000100 000100 000100 001110 000000 CHAR 50 001110 010001 000001 000110 001000 010000 011111 000000 CHAR 51 001110 010001 000001 001110 000001 010001 001110 000000 CHAR 52 000010 000110 001010 010010 011111 000010 000010 000000 CHAR 53 011111 010000 010000 011110 000001 010001 001110 000000 CHAR 54 000110 001000 010000 011110 010001 010001 001110 000000 CHAR 55 011111 000001 000010 000100 001000 001000 001000 000000 CHAR 56 001110 010001 010001 001110 010001 010001 001110 000000 CHAR 57 001110 010001 010001 001111 000001 000010 001100 000000 CHAR 58 000000 000000 001100 001100 000000 001100 001100 000000 CHAR 59 000000 000000 001100 001100 000000 001100 001100 001000 CHAR 60 000010 000100 001000 010000 001000 000100 000010 000000 CHAR 61 000000 000000 011111 000000 000000 011111 000000 000000 CHAR 62 001000 000100 000010 000001 000010 000100 001000 000000 CHAR 63 001110 010001 000001 000110 000100 000000 000100 000000 CHAR 64 001110 010001 010111 010101 010111 010000 001110 000000 CHAR 65 001110 010001 010001 010001 011111 010001 010001 000000 CHAR 66 011110 010001 010001 011110 010001 010001 011110 000000 CHAR 67 001110 010001 010000 010000 010000 010001 001110 000000 CHAR 68 011110 010001 010001 010001 010001 010001 011110 000000 CHAR 69 011111 010000 010000 011110 010000 010000 011111 000000 CHAR 70 011111 010000 010000 011110 010000 010000 010000 000000 CHAR 71 001110 010001 010000 010111 010001 010001 001111 000000 CHAR 72 010001 010001 010001 011111 010001 010001 010001 000000 CHAR 73 001110 000100 000100 000100 000100 000100 001110 000000 CHAR 74 000001 000001 000001 000001 010001 010001 001110 000000 CHAR 75 010001 010010 010100 011000 010100 010010 010001 000000 CHAR 76 010000 010000 010000 010000 010000 010000 011111 000000 CHAR 77 010001 011011 010101 010001 010001 010001 010001 000000 CHAR 78 010001 011001 010101 010011 010001 010001 010001 000000 CHAR 79 001110 010001 010001 010001 010001 010001 001110 000000 CHAR 80 011110 010001 010001 011110 010000 010000 010000 000000 CHAR 81 001110 010001 010001 010001 010101 010010 001101 000000 CHAR 82 011110 010001 010001 011110 010010 010001 010001 000000 CHAR 83 001110 010001 010000 001110 000001 010001 001110 000000 CHAR 84 011111 000100 000100 000100 000100 000100 000100 000000 CHAR 85 010001 010001 010001 010001 010001 010001 001110 000000 CHAR 86 010001 010001 010001 010001 010001 001010 000100 000000 CHAR 87 010001 010001 010101 010101 010101 010101 001010 000000 CHAR 88 010001 010001 001010 000100 001010 010001 010001 000000 CHAR 89 010001 010001 010001 001010 000100 000100 000100 000000 CHAR 90 011110 000010 000100 001000 010000 010000 011110 000000 CHAR 91 001110 001000 001000 001000 001000 001000 001110 000000 CHAR 92 000000 010000 001000 000100 000010 000001 000000 000000 CHAR 93 001110 000010 000010 000010 000010 000010 001110 000000 CHAR 94 000100 001010 010001 000000 000000 000000 000000 000000 CHAR 95 000000 000000 000000 000000 000000 000000 000000 111111 CHAR 96 001100 001100 000100 000000 000000 000000 000000 000000 CHAR 97 000000 000000 001110 000001 001111 010001 001111 000000 CHAR 98 010000 010000 011110 010001 010001 010001 011110 000000 CHAR 99 000000 000000 001110 010001 010000 010001 001110 000000 CHAR 100 000001 000001 001111 010001 010001 010001 001111 000000 CHAR 101 000000 000000 001110 010001 011110 010000 001110 000000 CHAR 102 000110 001000 001000 011110 001000 001000 001000 000000 CHAR 103 000000 000000 001111 010001 010001 001111 000001 001110 CHAR 104 010000 010000 011100 010010 010010 010010 010010 000000 CHAR 105 000100 000000 000100 000100 000100 000100 000110 000000 CHAR 106 000010 000000 000110 000010 000010 000010 010010 001100 CHAR 107 010000 010000 010010 010100 011000 010100 010010 000000 CHAR 108 000100 000100 000100 000100 000100 000100 000110 000000 CHAR 109 000000 000000 011010 010101 010101 010001 010001 000000 CHAR 110 000000 000000 011100 010010 010010 010010 010010 000000 CHAR 111 000000 000000 001110 010001 010001 010001 001110 000000 CHAR 112 000000 000000 011110 010001 010001 010001 011110 010000 CHAR 113 000000 000000 001111 010001 010001 010001 001111 000001 CHAR 114 000000 000000 010110 001001 001000 001000 011100 000000 CHAR 115 000000 000000 001110 010000 001110 000001 001110 000000 CHAR 116 000000 001000 011110 001000 001000 001010 000100 000000 CHAR 117 000000 000000 010010 010010 010010 010110 001010 000000 CHAR 118 000000 000000 010001 010001 010001 001010 000100 000000 CHAR 119 000000 000000 010001 010001 010101 011111 001010 000000 CHAR 120 000000 000000 010010 010010 001100 010010 010010 000000 CHAR 121 000000 000000 010010 010010 010010 001110 000100 011000 CHAR 122 000000 000000 011110 000010 001100 010000 011110 000000 CHAR 123 000110 001000 001000 011000 001000 001000 000110 000000 CHAR 124 000100 000100 000100 000100 000100 000100 000100 000100 CHAR 125 001100 000010 000010 000011 000010 000010 001100 000000 CHAR 126 001010 010100 000000 000000 000000 000000 000000 000000 CHAR 127 000100 001110 011011 010001 010001 011111 000000 000000 CHAR 128 001110 010001 010000 010000 010001 001110 000100 001100 CHAR 129 010010 000000 010010 010010 010010 010110 001010 000000 CHAR 130 000011 000000 001110 010001 011110 010000 001110 000000 CHAR 131 001110 000000 001110 000001 001111 010001 001111 000000 CHAR 132 001010 000000 001110 000001 001111 010001 001111 000000 CHAR 133 001100 000000 001110 000001 001111 010001 001111 000000 CHAR 134 001110 001010 001110 000001 001111 010001 001111 000000 CHAR 135 000000 001110 010001 010000 010001 001110 000100 001100 CHAR 136 001110 000000 001110 010001 011110 010000 001110 000000 CHAR 137 001010 000000 001110 010001 011110 010000 001110 000000 CHAR 138 001100 000000 001110 010001 011110 010000 001110 000000 CHAR 139 001010 000000 000100 000100 000100 000100 000110 000000 CHAR 140 000100 001010 000000 000100 000100 000100 000110 000000 CHAR 141 001000 000000 000100 000100 000100 000100 000110 000000 CHAR 142 001010 000000 000100 001010 010001 011111 010001 000000 CHAR 143 001110 001010 001110 011011 010001 011111 010001 000000 CHAR 144 000011 000000 011111 010000 011110 010000 011111 000000 CHAR 145 000000 000000 011110 000101 011111 010100 001111 000000 CHAR 146 001111 010100 010100 011111 010100 010100 010111 000000 CHAR 147 001110 000000 001100 010010 010010 010010 001100 000000 CHAR 148 001010 000000 001100 010010 010010 010010 001100 000000 CHAR 149 011000 000000 001100 010010 010010 010010 001100 000000 CHAR 150 001110 000000 010010 010010 010010 010110 001010 000000 CHAR 151 011000 000000 010010 010010 010010 010110 001010 000000 CHAR 152 001010 000000 010010 010010 010010 001110 000100 011000 CHAR 153 010010 001100 010010 010010 010010 010010 001100 000000 CHAR 154 001010 000000 010010 010010 010010 010010 001100 000000 CHAR 155 000000 000100 001110 010000 010000 001110 000100 000000 CHAR 156 000110 001001 001000 011110 001000 001001 010111 000000 CHAR 157 010001 001010 000100 011111 000100 011111 000100 000000 CHAR 158 011000 010100 010100 011010 010111 010010 010010 000000 CHAR 159 000010 000101 000100 001110 000100 000100 010100 001000 CHAR 160 000110 000000 001110 000001 001111 010001 001111 000000 CHAR 161 000110 000000 000100 000100 000100 000100 000110 000000 CHAR 162 000110 000000 001100 010010 010010 010010 001100 000000 CHAR 163 000110 000000 010010 010010 010010 010110 001010 000000 CHAR 164 001010 010100 000000 011100 010010 010010 010010 000000 CHAR 165 001010 010100 000000 010010 011010 010110 010010 000000 CHAR 166 001110 000001 001111 010001 001111 000000 001111 000000 CHAR 167 001100 010010 010010 010010 001100 000000 011110 000000 CHAR 168 000100 000000 000100 001100 010000 010001 001110 000000 CHAR 169 000000 000000 011111 010000 010000 010000 000000 000000 CHAR 170 000000 000000 111111 000001 000001 000000 000000 000000 CHAR 171 010000 010010 010100 001110 010001 000010 000111 000000 CHAR 172 010000 010010 010100 001011 010101 000111 000001 000000 CHAR 173 000100 000000 000100 000100 001110 001110 000100 000000 CHAR 174 000000 000000 001001 010010 001001 000000 000000 000000 CHAR 175 000000 000000 010010 001001 010010 000000 000000 000000 CHAR 176 010101 000000 101010 000000 010101 000000 101010 000000 CHAR 177 010101 101010 010101 101010 010101 101010 010101 101010 CHAR 178 101010 111111 010101 111111 101010 111111 010101 111111 CHAR 179 000100 000100 000100 000100 000100 000100 000100 000100 CHAR 180 000100 000100 000100 111100 000100 000100 000100 000100 CHAR 181 000000 000000 010010 010010 010010 011100 010000 010000 CHAR 182 010100 010100 010100 110100 010100 010100 010100 010100 CHAR 183 000000 000000 000000 111100 010100 010100 010100 010100 CHAR 184 000000 111100 000100 111100 000100 000100 000100 000100 CHAR 185 010100 110100 000100 110100 010100 010100 010100 010100 CHAR 186 010100 010100 010100 010100 010100 010100 010100 010100 CHAR 187 000000 111100 000100 110100 010100 010100 010100 010100 CHAR 188 010100 110100 000100 111100 000000 000000 000000 000000 CHAR 189 010100 010100 010100 111100 000000 000000 000000 000000 CHAR 190 000100 111100 000100 111100 000000 000000 000000 000000 CHAR 191 000000 000000 000000 111100 000100 000100 000100 000100 CHAR 192 000100 000100 000100 000111 000000 000000 000000 000000 CHAR 193 000100 000100 000100 111111 000000 000000 000000 000000 CHAR 194 000000 000000 000000 111111 000100 000100 000100 000100 CHAR 195 000100 000100 000100 000111 000100 000100 000100 000100 CHAR 196 000000 000000 000000 111111 000000 000000 000000 000000 CHAR 197 000100 000100 000100 111111 000100 000100 000100 000100 CHAR 198 000100 000111 000100 000111 000100 000100 000100 000100 CHAR 199 010100 010100 010100 010111 010100 010100 010100 010100 CHAR 200 010100 010111 010000 011111 000000 000000 000000 000000 CHAR 201 000000 011111 010000 010111 010100 010100 010100 010100 CHAR 202 010100 110111 000000 111111 000000 000000 000000 000000 CHAR 203 000000 111111 000000 110111 010100 010100 010100 010100 CHAR 204 010100 010111 010000 010111 010100 010100 010100 010100 CHAR 205 000000 111111 000000 111111 000000 000000 000000 000000 CHAR 206 010100 110111 000000 110111 010100 010100 010100 010100 CHAR 207 000100 111111 000000 111111 000000 000000 000000 000000 CHAR 208 010100 010100 010100 111111 000000 000000 000000 000000 CHAR 209 000000 111111 000000 111111 000100 000100 000100 000100 CHAR 210 000000 000000 000000 111111 010100 010100 010100 010100 CHAR 211 010100 010100 010100 011111 000000 000000 000000 000000 CHAR 212 000000 000000 000000 000000 000000 000000 000000 111111 CHAR 213 000000 000000 000000 000000 000000 000000 111111 111111 CHAR 214 000000 000000 000000 000000 000000 111111 111111 111111 CHAR 215 000000 000000 000000 000000 111111 111111 111111 111111 CHAR 216 000000 000000 000000 111111 111111 111111 111111 111111 CHAR 217 000000 000000 111111 111111 111111 111111 111111 111111 CHAR 218 000000 111111 111111 111111 111111 111111 111111 111111 CHAR 219 111111 111111 111111 111111 111111 111111 111111 111111 CHAR 220 100000 100000 100000 100000 100000 100000 100000 100000 CHAR 221 110000 110000 110000 110000 110000 110000 110000 110000 CHAR 222 111000 111000 111000 111000 111000 111000 111000 111000 CHAR 223 111100 111100 111100 111100 111100 111100 111100 111100 CHAR 224 111110 111110 111110 111110 111110 111110 111110 111110 CHAR 225 000000 011100 010010 011100 010010 010010 011100 010000 CHAR 226 011110 010010 010000 010000 010000 010000 010000 000000 CHAR 227 000000 011111 001010 001010 001010 001010 001010 000000 CHAR 228 001010 000000 001110 000001 001111 010001 001111 000000 CHAR 229 000000 000000 001111 010010 010010 001100 000000 000000 CHAR 230 000000 000000 010010 010010 010010 011100 010000 010000 CHAR 231 000000 000000 001010 010100 000100 000100 000100 000000 CHAR 232 001110 000100 001110 010001 001110 000100 001110 000000 CHAR 233 001100 010010 010010 011110 010010 010010 001100 000000 CHAR 234 000000 001110 010001 010001 001010 001010 011011 000000 CHAR 235 001100 010000 001000 000100 001110 010010 001100 000000 CHAR 236 000000 000000 001010 010101 010101 001010 000000 000000 CHAR 237 000000 000100 001110 010101 010101 001110 000100 000000 CHAR 238 000000 001110 010000 011110 010000 001110 000000 000000 CHAR 239 000000 001100 010010 010010 010010 010010 000000 000000 CHAR 240 000000 011110 000000 011110 000000 011110 000000 000000 CHAR 241 000000 000100 001110 000100 000000 001110 000000 000000 CHAR 242 010000 001100 000010 001100 010000 000000 011110 000000 CHAR 243 000000 000000 111111 111000 100110 100001 100000 111111 CHAR 244 000000 000000 111111 000111 011001 100001 000001 111111 CHAR 245 000100 000100 000100 000100 000100 010100 001000 000000 CHAR 246 001010 000000 001110 010001 010001 010001 001110 000000 CHAR 247 111110 111110 111110 111110 111110 111110 111110 111110 CHAR 248 111100 111100 111100 111100 111100 111100 111100 111100 CHAR 249 111000 111000 111000 111000 111000 111000 111000 111000 CHAR 250 110000 110000 110000 110000 110000 110000 110000 110000 CHAR 251 100000 100000 100000 100000 100000 100000 100000 100000 CHAR 252 001010 000000 010010 010010 010010 010110 001010 000000 CHAR 253 011000 000100 001000 011100 000000 000000 000000 000000 CHAR 254 000000 000000 000000 011110 110010 110011 111110 001111 CHAR 255 010010 111111 010010 010010 111111 010010 000000 000000