diff --git a/xtrn/lemons/commands.js b/xtrn/lemons/commands.js
new file mode 100644
index 0000000000000000000000000000000000000000..d66382e8bd881b010e3f2c36192f65aa4039f3bf
--- /dev/null
+++ b/xtrn/lemons/commands.js
@@ -0,0 +1,37 @@
+/*	Lemons JSON DB module commands filter
+	Any commands sent by clients to the Lemons JSON DB module are passed
+	through the methods defined here before any changes are made to the DB. */
+
+// Restrict what parts of the DB clients can write to
+this.QUERY = function(client, packet) {
+
+	// Read-only commands are okay
+	var openOpers = [
+		"SUBSCRIBE",
+		"UNSUBSCRIBE",
+		"READ",
+		"KEYS",
+		"SLICE"
+	];
+
+	var location = packet.location.split(".");
+
+	// It's okay for clients to write to *.LATEST
+	if(packet.oper == "WRITE" && location.length == 2 && location[1] == "LATEST")
+		return false; // Command not handled (the JSON service can continue processing this command)
+	
+	/*	Only sysops can use unapproved commands everywhere else.
+		service.js identifies as the host BBS' sysop. */
+	if(openOpers.indexOf(packet.oper) >= 0 || admin.verify(client, packet, 90))
+		return false; // Command not handled (the JSON service can continue processing this command)
+	
+	// If the command didn't pass our tests, mark it as handled and log
+	log(LOG_ERR,
+		format(
+			"Invalid command %s from client %s",
+			packet.oper, client.remote_ip_address
+		)
+	);	
+	return true; // Command has been handled
+
+}
\ No newline at end of file
diff --git a/xtrn/lemons/defs.js b/xtrn/lemons/defs.js
new file mode 100644
index 0000000000000000000000000000000000000000..ce423b43416af6e77ca585f75efac9c54dd0982b
--- /dev/null
+++ b/xtrn/lemons/defs.js
@@ -0,0 +1,38 @@
+// Magic 'state' numbers used by most Lemons modules and the main loop.
+const	STATE_MENU = 0,
+		STATE_PLAY = 1,
+		STATE_PAUSE = 2,
+		STATE_HELP = 3,
+		STATE_SCORES = 4,
+		STATE_EXIT = 5;
+
+/*	Magic numbers returned by Level.cycle() and Level.getcmd(), to be
+	evaluated by the Game object. */
+const	LEVEL_DEAD = 0,
+		LEVEL_TIME = 1,
+		LEVEL_NEXT = 2,
+		LEVEL_CONTINUE = 3;
+
+//	Magic numbers for state-tracking within the Game object
+const	GAME_STATE_CHOOSELEVEL = 0,
+		GAME_STATE_NORMAL = 1,
+		GAME_STATE_POPUP = 2;
+
+/*	These are used by the Level object during colorization and lemonization of
+	skilled lemons, and also to colorize the skill list in the status bar. You
+	can change these values if you want. */
+const	COLOUR_LEMON = YELLOW,
+		COLOUR_BASHER = LIGHTMAGENTA,
+		COLOUR_BLOCKER = GREEN,
+		COLOUR_BOMBER = LIGHTRED,
+		COLOUR_BUILDER = LIGHTGREEN,
+		COLOUR_CLIMBER = LIGHTCYAN,
+		COLOUR_DIGGER = CYAN,
+		COLOUR_NUKED = LIGHTRED,
+		COLOUR_DYING = LIGHTGRAY,
+		// The default status bar colours
+		COLOUR_STATUSBAR_BG = BG_BLUE,
+		COLOUR_STATUSBAR_FG = WHITE;
+
+// The name of the JSON database.  You probably don't need to change this.
+const	DBNAME = "LEMONS";
\ No newline at end of file
diff --git a/xtrn/lemons/game.js b/xtrn/lemons/game.js
new file mode 100644
index 0000000000000000000000000000000000000000..0fb27b10a9b40ee083c29c89f8c6aefbdaa94b9f
--- /dev/null
+++ b/xtrn/lemons/game.js
@@ -0,0 +1,329 @@
+// The Game object tracks a user's progress and score across multiple levels
+var Game = function() {
+
+	// We want the user to start by choosing an initial level, if applicable.
+	var gameState = GAME_STATE_CHOOSELEVEL;
+
+	// Some values to be tracked throughout this gaming session
+	var stats = {
+		'level' : 0, // Dummy value
+		'score' : 0, // Index into the 'levels' array (see below)
+		'turns' : 5  // How many turns to start the player off with
+	};
+
+	// Just an initial value
+	var levelState = LEVEL_CONTINUE;
+
+	// This will always either be false or an instance of PopUp (lemons.js)
+	var popUp = false;
+
+	/*	This will always either be false (if not paused) or an array of stored
+		Timer events (if paused). */
+	var events = false;
+
+	// Load the JSON-DB server details if available
+	if(file_exists(js.exec_dir + "server.ini")) {
+		var f = new File(js.exec_dir + "server.ini");
+		f.open("r");
+		var ini = f.iniGetObject();
+		f.close();
+		ini.port = parseInt(ini.port);
+	// Or just set some default values
+	} else {
+		var ini = { 'host' : '127.0.0.1', 'port' : 10088 };
+	}
+
+	// Create a ScoreBoard object, which we'll use for a few things later
+	var scoreBoard = new ScoreBoard(ini.host, ini.port);
+	var l = scoreBoard.getHighestLevel();
+
+	// This will be a LevelChooser if we're picking a level
+	var levelChooser = false;
+
+	// Make the paused/unpaused status of this Game publically accessible
+	this.paused = false;
+
+	// Load the levels JSON file and parse it into the 'levels' array
+	var f = new File(js.exec_dir + "levels.json");
+	f.open("r");
+	var levels = JSON.parse(f.read());
+	f.close();
+
+	// A custom prompt that works with this game's input & state model
+	var LevelChooser = function() {
+
+		var lcFrame = new Frame(
+			frame.x + Math.ceil((frame.width - 32) / 2),
+			frame.y + Math.ceil((frame.height - 3) / 2),
+			32,
+			3,
+			BG_BLACK|WHITE,
+			frame
+		);
+
+		var lcSubFrame = new Frame(
+			lcFrame.x + 1,
+			lcFrame.y + 1,
+			lcFrame.width - 2,
+			1,
+			BG_BLACK|WHITE,
+			lcFrame
+		);
+
+		var inputFrame = new Frame(
+			lcSubFrame.x + lcSubFrame.width - 5,
+			lcSubFrame.y,
+			3,
+			1,
+			BG_BLUE|WHITE,
+			lcSubFrame
+		);
+
+		lcFrame.drawBorder([CYAN, LIGHTCYAN, WHITE]);
+		lcSubFrame.putmsg("Start at level: (1 - " + (l + 1) + ") ");
+
+		var input = (l + 1) + "";
+		inputFrame.putmsg(input);
+
+		this.getcmd = function(userInput) {
+			
+			// We're only looking for numbers, backspace, or enter
+			if(	userInput == ""
+				||
+				(	userInput != "\x08"
+					&&
+					userInput != "\r"
+					&&
+					userInput != "\n"
+					&&
+					isNaN(parseInt(userInput))
+				)
+			) {
+				return;
+			}
+
+			// Handle backspace
+			if(userInput == "\x08" && input.length > 0) {
+			
+				input = input.substr(0, input.length - 1);
+			
+			// Handle enter
+			} else if(userInput == "\r" || userInput == "\n") {
+			
+				var ret = parseInt(input);
+				
+				input = "";
+				inputFrame.clear();
+				inputFrame.putmsg(input);
+
+				// Don't return an invalid number
+				if(input.length > 3 || isNaN(ret) || ret > (l + 1) || ret < 1)
+					return;
+				else
+					return (ret - 1);
+			
+			// Anything else that's gotten this far can be appended
+			} else {
+			
+				input += userInput;
+			
+			}
+
+			// If we've gotten this far, the input box needs a refresh
+			inputFrame.clear();
+			inputFrame.putmsg(input);
+
+			return;
+
+		}
+
+		this.close = function() {
+			lcFrame.delete();
+		}
+
+		lcFrame.open();
+
+	}
+
+	var level; // We'll need this variable within some of the following methods
+
+	// Handle user input passed to us by the parent script	
+	this.getcmd = function(userInput) {
+
+		// If the user should be choosing a level ...
+		if(gameState == GAME_STATE_CHOOSELEVEL) {
+		
+			// If this is a new user or they haven't beat level 0, start there
+			if(l == 0) {
+	
+				level = new Level(levels[stats.level], stats.level);
+				gameState = GAME_STATE_NORMAL;
+	
+			// Otherwise let them start at up to their highest level
+			} else if(!levelChooser) {
+
+				levelChooser = new LevelChooser();
+
+			// And if a chooser is already open, use it
+			} else {
+
+				var ll = levelChooser.getcmd(userInput);
+				// The chooser should return a valid level number, or nothing
+				if(typeof ll != "undefined") {
+
+					levelChooser.close();
+					levelChooser = false;
+					stats.level = ll;
+					level = new Level(levels[stats.level], stats.level);
+					gameState = GAME_STATE_NORMAL;
+					return true;
+
+				}
+
+			}
+		
+		/*	If gameplay is in progress, pass user input to the Level object.
+			If Level.getcmd returns false, the user hit 'Q' to quit. */
+		} else if(gameState == GAME_STATE_NORMAL && !level.getcmd(userInput)) {
+		
+			level.close();
+			return false;
+		
+		//	If a pop-up is on the screen, remove it if the user hits a key
+		} else if(gameState == GAME_STATE_POPUP && userInput != "") {
+		
+			gameState = GAME_STATE_NORMAL;
+			popUp.close();
+		
+		}
+		
+		// Game.getcmd will return true unless the user hit 'Q' during gameplay
+		return true;
+	
+	}
+
+	/*	This is called during the main loop when in gameplay state, and returns
+		true unless the user runs out of turns or beats the game. */
+	this.cycle = function() {
+
+		// If we're waiting for the player to choose a level
+		if(gameState == GAME_STATE_CHOOSELEVEL) {
+
+			return true;
+
+		// If we're waiting for them to dismiss a pop-up message
+		} else if(gameState == GAME_STATE_POPUP) {
+
+			return true;
+
+		// If they failed to rescue enough lemons, or ran out of time
+		} else if(levelState == LEVEL_DEAD || levelState == LEVEL_TIME) {
+
+			// Close the level, perchance to reopen it and start again
+			level.close();
+			// If the user has run out of turns ...
+			if(stats.turns < 1) {
+				stats.score = stats.score + level.score;
+				return false;
+			// If the user has turns left, let them try again
+			} else {
+				level = new Level(levels[stats.level], stats.level);
+			}
+
+		// If they beat the level, move on to the next one if it exists
+		} else if(levelState == LEVEL_NEXT) {
+
+			level.close();
+			if(stats.level < levels.length - 1) {
+				stats.level++;
+				level = new Level(levels[stats.level], stats.level);
+			} else {
+				return false;
+			}
+
+		}
+
+		// Call the Level.cycle housekeeping method, respond as necessary
+		levelState = level.cycle();
+		switch(levelState) {
+
+			// The user failed to rescue enough lemons.  Raise a pop-up.
+			case LEVEL_DEAD:
+				stats.turns--;
+				popUp = new PopUp(
+					[	"What a sour outcome - less than 50% of your lemons survived!",
+						"Score: " + stats.score,
+						stats.turns + " turns remaining",
+						"[ Press any key to continue ]"
+					]
+				);
+				gameState = GAME_STATE_POPUP;
+				break;
+
+			// The user ran out of time.  Raise a pop-up.
+			case LEVEL_TIME:
+				stats.turns--;
+				popUp = new PopUp(
+					[	"You ran out of time!  Now the lemons are going to miss their party.",
+						"Score: " + stats.score,
+						stats.turns + " turns remaining",
+						"[ Press any key to continue ]"
+					]
+				);
+				gameState = GAME_STATE_POPUP;
+				break;
+
+			// The user beat the level.  Raise a pop-up.
+			case LEVEL_NEXT:
+				stats.score += level.score;
+				popUp = new PopUp(
+					[	"You saved some lemons.  Savour this pointless victory!",
+						"Score: " + stats.score,
+						"[ Press any key to continue ]"
+					]
+				);
+				gameState = GAME_STATE_POPUP;
+				break;
+
+			// The level can continue cycling.  Don't do shit.
+			case LEVEL_CONTINUE:
+				break;
+
+			default:
+				break;
+
+		}
+
+		return true; // Everything's satisfactory in the neighbourhood
+
+	}
+
+	// Pause or resume the level
+	this.pause = function() {
+
+		/*	If the level isn't paused, pause it and store the timed events
+			that it returns. */
+		if(!events) {
+
+			events = level.pause();
+			this.paused = true;
+
+		/*	If the level is paused, unpause it by passing in the array of
+			stored events. */
+		} else {
+
+			level.pause(events);
+			events = false;
+			this.paused = false;
+
+		}
+
+	}
+
+	// If the user is done with this gaming session, save their score
+	this.close = function() {
+		scoreBoard.addScore(stats.score, stats.level);
+		scoreBoard.close();
+	}
+
+}
\ No newline at end of file
diff --git a/xtrn/lemons/help.bin b/xtrn/lemons/help.bin
new file mode 100644
index 0000000000000000000000000000000000000000..b79ac845e9039f5b2b4659f8730b9931ff33cbaa
--- /dev/null
+++ b/xtrn/lemons/help.bin
@@ -0,0 +1,14 @@
+���    ��.�    ��+� In each level, your goal is to guide a herd of lemons from��� -> ��� -> ���. the e	n	t	r	a	n	c	e	 door to the e
+x
+i
+t
+ door.  The average lemon is ���     �n�    ��� not very clever, and will need assistance along the way.                                                                              - A lemon will turn and walk the other way if it encounters an obstacle.    - Beacuse they are stupid pieces of fruit who don't know any better, lemons   walk off of the edges of platforms, even when there's nothing to land on. - Lemons will die if they fall into water, lava, or slime.                                                                                              Lemons aren't entirely hopeless.  Though their long-term memory is poor     they are able to acquire new skills and remember them for brief periods.    You can teach a lemon to b	a	s	h	 or dig through �F�F� blocks, climb over certain obstacles, b
+u
+i
+l
+d
+ staircases, block other lemons from following a path, or   even to explode and cause damage to their surroundings.                                                                                                 To teach a lemon new tricks, use your arrow keys to hover the cursor over   the lemon, then press the number-key that corresponds with the skill you    wish to assign (as shown in the status-bar during gameplay.)                                                                                            To pass each level, you must guide at least 50% of the lemons to the e
+x
+i
+t
+   before time runs out.  If all hope is lost, you can always hit N to nuke    any lemons remaining on the screen.                                         
\ No newline at end of file
diff --git a/xtrn/lemons/help.js b/xtrn/lemons/help.js
new file mode 100644
index 0000000000000000000000000000000000000000..3f40133455e13b24d5aedcabee0bce1da83491a2
--- /dev/null
+++ b/xtrn/lemons/help.js
@@ -0,0 +1,34 @@
+// The Help object loads the help screen into the terminal.
+var Help = function() {
+
+	var helpFrame = new Frame(
+		frame.x,
+		frame.y,
+		frame.width,
+		frame.height,
+		BG_BLACK|WHITE,
+		frame
+	);
+
+	var helpSubFrame = new Frame(
+		helpFrame.x + 1,
+		helpFrame.y + 1,
+		helpFrame.width - 2,
+		helpFrame.height - 2,
+		BG_BLACK|WHITE,
+		helpFrame
+	);
+
+	helpFrame.open();
+	helpFrame.drawBorder([CYAN, LIGHTCYAN, WHITE]);
+	helpFrame.home();
+	helpFrame.center(ascii(180) + "Lemons - Help" + ascii(195));
+	helpFrame.end();
+	helpFrame.center(ascii(180) + "Press any key to continue" + ascii(195));
+	helpSubFrame.load(js.exec_dir + "help.bin", 76, 22);
+
+	this.close = function() {
+		helpFrame.delete();
+	}
+
+}
\ No newline at end of file
diff --git a/xtrn/lemons/lemons.bin b/xtrn/lemons/lemons.bin
new file mode 100644
index 0000000000000000000000000000000000000000..ff30b062724d009186366684d45693fd2b740437
--- /dev/null
+++ b/xtrn/lemons/lemons.bin
@@ -0,0 +1 @@
+�����������������s�s�s�s�������������������������������������������������������������s�s�s�>���>�����>���>�����>���>�����>���>�����>���>�����>���>���s�������������������s�s�s������������ ������� �����s�� �� ���� �� ����  � ���� �����s���������������������������������>� �����>�>� �����>�>� �� ��>�>� �� ��.�.� �  ��>�>���� ��>���������������s�s�s�������s�s�s��������>�����>���>�����>���>�����>���>�����.���.��������>�����>���������s�s�������s���������������� ?b?y? ?e?c?h?i?c?k?e?n?�s���������s�s�������� ������� �����������������s����������������������������������s�������������s��������   ���b� �����������s������������������������s�������������������������������� ����������n� ���������s�s�������������������s�s�s�s���������������������� ���� ��� ���������������� ���������s���������������������s�s�s��������������������� �/�/�� � ���N�O��O�O�N����������� ��������������������������������������������������� ���   ������O  ��O���������������������������������������������������������������� ����.������N�O���O�N������������ ���s�s�s�������s�s�s���������������������������s�s����������  ������������������N�O��O�O�N�� ����s���������s�����������������������������s�s�����������������������������O�  ��O��������������������������s�s�s��������������������������� �������������������N�O�O��O�N� ��������������������������s��������s������������������������      ��N������������������� ��������s������������������������������������� �  ����� ���     ��������� � �/�/�� �������s�s�s���������������������������������������  �.������N�N�N���� �������.������ �������������������������s�s��������������������� �����������������������  ��� �������������������������������������������s�s��s����� �����  ������������� ����������������������������������������������������s�s������ ��� ��� ������� �������������������������s�s�s������������s�s������������������������������ ����  �� ������s�s��������s�s�s����������������������������������������������������� �������� �������������s�s�s�s�s��������������s�s�s��������������������������������s�s������� ���� �����������������s�s�s�������������������������s�s���
\ No newline at end of file
diff --git a/xtrn/lemons/lemons.js b/xtrn/lemons/lemons.js
new file mode 100644
index 0000000000000000000000000000000000000000..019efc1a43ce74f459a331d0e5aa16b17bffaa54
--- /dev/null
+++ b/xtrn/lemons/lemons.js
@@ -0,0 +1,261 @@
+// Dependencies
+load('sbbsdefs.js');
+load('frame.js');
+load('tree.js');
+load('event-timer.js');
+load('json-client.js');
+load('sprite.js');
+
+// Lemons modules
+load(js.exec_dir + "defs.js");
+load(js.exec_dir + "game.js");
+load(js.exec_dir + "level.js");
+load(js.exec_dir + "menu.js");
+load(js.exec_dir + "help.js");
+load(js.exec_dir + "scoreboard.js");
+
+var	status,		// A place to store bbs.sys_status until exit
+	attributes;	// A place to store console attributes until exit
+
+// The Lemons modules expect these globals:
+var state, // What we're currently doing
+	frame; // The parent frame
+
+/*	Draw a border of colour 'color' inside of a frame.
+	'color' can be a number or an array of colours. */
+Frame.prototype.drawBorder = function(color) {
+	var theColor = color;
+	if(Array.isArray(color));
+		var sectionLength = Math.round(this.width / color.length);
+	this.pushxy();
+	for(var y = 1; y <= this.height; y++) {
+		for(var x = 1; x <= this.width; x++) {
+			if(x > 1 && x < this.width && y > 1 && y < this.height)
+				continue;
+			var msg;
+			this.gotoxy(x, y);
+			if(y == 1 && x == 1)
+				msg = ascii(218);
+			else if(y == 1 && x == this.width)
+				msg = ascii(191);
+			else if(y == this.height && x == 1)
+				msg = ascii(192);
+			else if(y == this.height && x == this.width)
+				msg = ascii(217);
+			else if(x == 1 || x == this.width)
+				msg = ascii(179);
+			else
+				msg = ascii(196);
+			if(Array.isArray(color)) {
+				if(x == 1)
+					theColor = color[0];
+				else if(x % sectionLength == 0 && x < this.width)
+					theColor = color[x / sectionLength];
+				else if(x == this.width)
+					theColor = color[color.length - 1];
+			}
+			this.putmsg(msg, theColor);
+		}
+	}
+	this.popxy();
+}
+
+/*	Pop a message up near the centre of frame 'frame':
+	var popUp = new PopUp(["Line 1", "Line 2" ...]);
+	console.getkey();
+	popUp.close(); */
+var PopUp = function(message) {
+
+	var longestLine = 0;
+	for(var m = 0; m < message.length; m++) {
+		if(message[m].length > longestLine)
+			longestLine = message[m].length;
+	}
+
+	this.frame = new Frame(
+		frame.x + Math.ceil((frame.width - longestLine - 4) / 2),
+		frame.y + Math.ceil((frame.height - message.length - 2) / 2),
+		longestLine + 4,
+		message.length + 2,
+		BG_BLACK|WHITE,
+		frame
+	);
+
+	this.frame.open();
+	this.frame.drawBorder([CYAN, LIGHTCYAN, WHITE]);
+	this.frame.gotoxy(1, 2);
+	for(var m = 0; m < message.length; m++)
+		this.frame.center(message[m] + "\r\n");
+
+	this.close = function() {
+		this.frame.delete();
+	}
+
+}
+
+// Prepare the display, set the initial state
+var init = function() {
+	
+	status = bbs.sys_status; // We'll restore this on cleanup
+	bbs.sys_status|=SS_MOFF; // Turn off node message delivery
+
+	attributes = console.attributes; // We'll restore this on cleanup
+	console.clear(BG_BLACK|WHITE); // Wipe away any poops
+	
+	// Set up the root frame
+	frame = new Frame(
+		Math.ceil((console.screen_columns - 79) / 2),
+		Math.ceil((console.screen_rows - 23) / 2),
+		80,
+		24,
+		BG_BLACK|LIGHTGRAY
+	);
+	frame.open();
+
+	// We start off at the menu screen
+	state = STATE_MENU;
+
+}
+
+// Return the display & system status to normal
+var cleanUp = function() {
+	frame.delete();
+	bbs.sys_status = status;
+	console.clear(attributes);
+}
+
+// Get input from the user, make things happen
+var main = function() {
+
+	// These will always be either 'false' or an instance of a Lemons module
+	var game = false,
+		menu = false,
+		help = false,
+		scoreboard = false;
+
+	while(!js.terminated) {
+
+		// Refresh the user's terminal and bury the (real) cursor if necessary
+		if(frame.cycle())
+			console.gotoxy(console.screen_columns, console.screen_rows);
+
+		/*	This is the only place where we take input from the user.
+			Input is passed to the current (as per state) module from here. */
+		var userInput = console.inkey(K_UPPER, 5);
+
+		switch(state) {
+
+			case STATE_MENU:
+				// Load the menu if it isn't already showing
+				if(!menu)
+					menu = new Menu();
+				// Pass user input to the menu
+				menu.getcmd(userInput);
+				// If we've left the menu, close and falsify it
+				if(state != STATE_MENU) {
+					menu.close();
+					menu = false;
+				}
+				break;
+
+			case STATE_PLAY:
+				// Create a new Game if we're not already in one
+				if(!game)
+					game = new Game();
+				// Pass user input to the game module
+				if(!game.getcmd(userInput)) {
+					// If Game.getcmd returns false, the user has hit 'Q'
+					game.close();
+					game = false;
+					// Return to the menu on the next loop
+					state = STATE_MENU;
+				} else if(!game.cycle()) {
+					/*	If Game.cycle returns false, the user is out of turns
+						or has beat the last level. */
+					game.close();
+					game = false;
+					// Return to the menu on the next loop
+					state = STATE_MENU;
+				}
+				break;
+
+			case STATE_PAUSE:
+				// If the game isn't paused, pause it
+				if(!game.paused)
+					game.pause();
+				// If the user hit a key and the game is paused, unpause it
+				if(userInput != "" && game.paused) {
+					game.pause();
+					// Return to gameplay on the next loop
+					state = STATE_PLAY;
+				}
+				break;
+
+			case STATE_HELP:
+				// If a game is in progress, pause it
+				if(game instanceof Game && !game.paused)
+					game.pause();
+				// If the help screen isn't showing, load it
+				if(!help)
+					help = new Help();
+				/*	If a game is in progress and the user hit a key, unpause
+					the game, remove the help screen, and return to gameplay. */
+				if(	userInput != ""
+					&&
+					game instanceof Game && game.paused
+				) {
+					help.close();
+					help = false;
+					game.pause();
+					state = STATE_PLAY;
+				/*	If there's no game in progress, close the help screen and
+					return to the menu.	*/
+				} else if(userInput != "") {
+					help.close();
+					help = false;
+					state = STATE_MENU;
+				}
+				break;
+
+			case STATE_SCORES:
+				// Bring up the scoreboard if it isn't showing already
+				if(!scoreboard) {
+					if(file_exists(js.exec_dir + "server.ini")) {
+						var f = new File(js.exec_dir + "server.ini");
+						f.open("r");
+						var ini = f.iniGetObject();
+						f.close();
+					} else {
+						var ini = { 'host' : "127.0.0.1", 'port' : 10088 };
+					}
+					scoreboard = new ScoreBoard(ini.host, ini.port);
+					scoreboard.open();
+				// Remove the scoreboard when the user hits a key
+				} else if(userInput != "") {
+					scoreboard.close();
+					state = STATE_MENU;
+					scoreboard = false;
+				}
+				break;
+
+			case STATE_EXIT:
+				// Break the main loop
+				return;
+				break; // I just can't not.  :|
+
+			default:
+				break;
+
+		}
+
+	}
+
+}
+
+// Prepare the things
+init();
+// Do the things
+main();
+// Remove the things
+cleanUp();
+// We're done
\ No newline at end of file
diff --git a/xtrn/lemons/level.js b/xtrn/lemons/level.js
new file mode 100644
index 0000000000000000000000000000000000000000..384fcb696cc43fe95e4baf78c57a477f17db78d4
--- /dev/null
+++ b/xtrn/lemons/level.js
@@ -0,0 +1,1218 @@
+// This is the actual game.  It's a disaster, but it gets the job done.
+var Level = function(l, n) {
+
+	// Scope some variables for use by functions and methods in this object
+	var timer = new Timer(), // A Timer
+		// A place to organize this level's frames
+		frames = {
+			'counters' : {}
+		},
+		cursor,		// The 1x1 cursor frame
+		countDown,	// Level timeout
+		lost = 0,	// Lemons lost
+		saved = 0,	// Lemons saved
+		total = 0,	// Total lemon count for this level
+		// Placeholder values for how many of each skill may be assigned
+		quotas = {
+			'basher' : 0,
+			'blocker' : 0,
+			'bomber' : 0,
+			'builder' : 0,
+			'climber' : 0,
+			'digger' : 0
+		};
+
+	// The parent Game object will want to read this
+	this.score = 0;
+
+	/*	Change the colour of a lemon's peel without altering the colour of its
+		shoes or 'nuked' position. */
+	var colorize = function(sprite, colour) {
+		var fgmask = (1<<0)|(1<<1)|(1<<2)|(1<<3);
+		for(var y = 0; y < sprite.frame.data.length; y++) {
+			for(var x = 0; x < 9; x++) {
+				sprite.frame.data[y][x].attr&=~fgmask;
+				if((y == 2 || y == 5) && (x == 0 || x == 2 || x == 3 || x == 5 || x == 6 || x == 8))
+					sprite.frame.data[y][x].attr=BG_BLACK|BROWN;
+				else
+					sprite.frame.data[y][x].attr|=colour;
+			}
+		}
+		sprite.frame.invalidate();
+	}
+
+	//	Turn skilled lemon back into an ordinary zombie-lemon.
+	var lemonize = function(sprite) {
+		colorize(sprite, COLOUR_LEMON);
+		sprite.ini.constantmotion = 1;
+		sprite.ini.gravity = 1;
+		sprite.ini.speed = .25;
+		sprite.ini.skill = "lemon";
+	}
+
+	// Remove the lemon from the screen
+	var remove = function(sprite) {
+		if(!sprite.open)
+			return;
+		sprite.remove();
+	}
+
+	/*	If a lemon encounters blocks stacked >= 2 characters high it
+		will turn and start walking the other way.
+		If a lemon encounters a block 1 character high, it will climb
+		on top of it (stairs, etc.)
+		Normal, bomber, and builder lemons should be passed to this function.
+	*/
+	var turnOrClimbIfObstacle = function(sprite) {
+
+		var beside = (sprite.bearing == "w") ? Sprite.checkLeft(sprite) : Sprite.checkRight(sprite);
+		if(!beside)
+			beside = Sprite.checkOverlap(sprite);
+		if(!beside)
+			return;
+
+		for(var b = 0; b < beside.length; b++) {
+
+			if(	beside[b].ini.type != "block"
+				&&
+				beside[b].ini.type != "lemon"
+				&&
+				beside[b].ini.type != "hazard"
+			) {
+				continue;
+			}
+
+			if(	beside[b].ini.type == "block"
+				&&
+				beside[b].y == sprite.y + sprite.ini.height - 1
+			) {
+				sprite.moveTo(
+					(sprite.bearing == "w") ? (sprite.x - 1) : (sprite.x + 1),
+					sprite.y - 1
+				);
+				if(Sprite.checkOverlap(sprite)) {
+					sprite.moveTo(
+						(sprite.bearing == "w") ? (sprite.x + 1) : (sprite.x - 1),
+						sprite.y + 1
+					);
+					sprite.turnTo((sprite.bearing == "w") ? "e" : "w");
+					if(sprite.ini.skill == "builder")
+						lemonize(sprite);
+				}
+				break;
+
+			} else {
+				sprite.turnTo((sprite.bearing == "w") ? "e" : "w");
+				var overlaps = Sprite.checkOverlap(sprite);
+				if(overlaps) {
+					for(var o = 0; o < overlaps.length; o++) {
+						if(	overlaps[o].ini.type != "block"
+							&&
+							overlaps[o].ini.type != "lemon"
+							&&
+							overlaps[o].ini.type != "hazard"
+						) {
+							continue;
+						}
+						if(sprite.bearing == "w") {
+							while(sprite.x < overlaps[o].x + overlaps[o].frame.width) {
+								sprite.move("reverse");
+							}
+						} else if(sprite.bearing == "e") {
+							while(sprite.x + sprite.ini.width > overlaps[o].x) {
+								sprite.move("reverse");
+							}
+						}
+					}
+				}
+				if(sprite.ini.skill == "builder")
+					lemonize(sprite);
+				break;
+			}
+
+		}
+
+	}
+
+	/*	This function is strictly for climber lemons.  The lemon keeps
+		scaling the wall until it is able to continue moving along its
+		original bearing.  If the climb is only one cell in height, this
+		was just a single block (stairs, perhaps) and the 'climber' skill
+		is retained for later use.  Otherwise convert back to normal lemon
+		status. */
+	var climbIfObstacle = function(sprite) {
+
+		if(system.timer - sprite.lastYMove < sprite.ini.speed)
+			return;
+
+		var beside = (sprite.bearing == "w") ? Sprite.checkLeft(sprite) : Sprite.checkRight(sprite);
+		if(!beside)
+			beside = Sprite.checkOverlap(sprite);
+		if(!beside)
+			return;
+
+		if(typeof sprite.ini.climbStart == "undefined")
+			sprite.ini.climbStart = sprite.y;
+
+		sprite.ini.gravity = 0;
+		sprite.ini.constantmotion = 0;
+
+		for(var b = 0; b < beside.length; b++) {
+
+			if(	beside[b].ini.type == "entrance"
+				||
+				beside[b].ini.type == "exit"
+				||
+				beside[b].ini.type == "projectile"
+			) {
+				lemonize(sprite);
+				delete sprite.ini.climbStart;
+				return;
+			} else if(beside[b].ini.type == "lemon") {
+				sprite.turnTo((sprite.bearing == "w") ? "e" : "w");
+				sprite.ini.gravity = 1;
+				sprite.ini.constantmotion = 1;
+				return;
+			}
+
+		}
+
+		sprite.moveTo(
+			(sprite.bearing == "w") ? (sprite.x - 1) : (sprite.x + 1),
+			sprite.y - 1
+		);
+		sprite.lastYMove = system.timer;
+		var overlaps = Sprite.checkOverlap(sprite);
+		if(overlaps) {
+			for(var o = 0; o < overlaps.length; o++) {
+				if(overlaps[o].y != sprite.y + sprite.ini.height - 1)
+					continue;
+				if(sprite.bearing == "w") {
+					while(sprite.x < overlaps[o].x + overlaps[o].frame.width) {
+						sprite.move("reverse");
+					}
+				} else if(sprite.bearing == "e") {
+					while(sprite.x + sprite.ini.width > overlaps[o].x) {
+						sprite.move("reverse");
+					}
+				}
+				break;
+			}
+		} else if(
+			sprite.ini.climbStart > sprite.y
+			&&
+			sprite.ini.climbStart - sprite.y > 1
+		) {
+			lemonize(sprite);
+			delete sprite.ini.climbStart;
+		}
+
+	}
+
+	/*	The Sprite class actually takes care of most aspects of falling
+		for us.  We just need to enforce straight vertical drops, and make
+		sure certain types don't start moving again once they land. 
+		All lemons should be passed to this function.  */
+	var fallIfNoFloor = function(sprite) {
+
+		if(sprite.inFall && sprite.ini.constantmotion == 1) {
+			// Stop lemons from moving horizontally when falling
+			sprite.ini.constantmotion = 0;
+		} else if(
+			!sprite.inFall
+			&&
+			sprite.ini.constantmotion == 0
+			&&
+			sprite.ini.gravity == 1
+			&&
+			sprite.ini.skill != "blocker"
+			&&
+			sprite.ini.skill != "digger"
+			&&
+			sprite.ini.skill != "basher"
+			&&
+			sprite.ini.skill != "builder"
+			&&
+			sprite.ini.skill != "dying"
+		) {
+
+			if(typeof sprite.ini.forceDrop == "boolean")
+				delete sprite.ini.forceDrop;
+
+			if(	typeof sprite.ini.ticker == "undefined"
+				&&
+				sprite.ini.skill != "climber"
+			) {
+				lemonize(sprite);
+			} else {
+				sprite.ini.constantmotion = 1;
+			}
+		}
+	
+	}
+
+	/*	If a lemon has reached the exit, remove it and update counters
+		accordingly.
+		All but blocker lemons should be passed to this function. */
+	var removeIfAtExit = function(sprite) {
+		var overlaps = Sprite.checkOverlap(sprite);
+		if(!overlaps)
+			return;
+		for(var o = 0; o < overlaps.length; o++) {
+			if(overlaps[o].ini.type != "exit")
+				continue;
+			sprite.remove();
+			saved++;
+			frames.counters.lostSaved.clear();
+			frames.counters.lostSaved.putmsg(lost + "/" + saved);
+			break;
+		}
+	}
+
+	/*	If any blocks get in the way of a 'basher' lemon, it will bash
+		through them until it reaches free space again, at which point
+		it loses its 'basher' skill.
+		It's too easy to just remove an entire block from the screen,
+		so we will produce a nice effect by dismantling the block by
+		one half-cell every ~ .25 seconds. */
+	var bashersGonnaBash = function(sprite) {
+
+		if(typeof sprite.ini.lastDig == "undefined")
+			sprite.ini.lastDig = system.timer;
+		if(system.timer - sprite.ini.lastDig < .5)
+			return;
+
+		var beside = (sprite.bearing == "e") ? Sprite.checkRight(sprite) : Sprite.checkLeft(sprite);
+		if(!beside)
+			beside = Sprite.checkOverlap(sprite);
+		if(!beside) {
+			sprite.ini.constantmotion = 1;
+			return;
+		}
+
+		sprite.ini.constantmotion = 0;
+
+		var blockFound = false;
+		var columnClear = false;
+		for(var b = 0; b < beside.length; b++) {
+
+			if(beside[b].ini.type == "lemon") {
+				sprite.turnTo((sprite.bearing == "e") ? "w" : "e");
+				return;
+			}
+
+			if(	beside[b].ini.type != "block"
+				||
+				!beside[b].open
+			) {
+				continue;
+			}
+
+			if(beside[b].y == sprite.y + sprite.ini.height - 1) {
+				sprite.moveTo(
+					(sprite.bearing == "w") ? (sprite.x - 1) : (sprite.x + 1),
+					sprite.y - 1
+				);
+				if(!Sprite.checkOverlap(sprite)) {
+					sprite.ini.constantmotion = 1;
+					return;
+				}
+				sprite.moveTo(
+					(sprite.bearing == "w") ? (sprite.x + 1) : (sprite.x - 1),
+					sprite.y + 1
+				);
+			}
+
+			blockFound = true;
+
+			if(beside[b].ini.material == "metal") {
+				lemonize(sprite);
+				sprite.turnTo((sprite.bearing == "e") ? "w" : "e");
+				return;
+			}
+
+			if(sprite.bearing == "e")
+				var x = (beside[b].x - sprite.x == 3) ? 0 : ((beside[b].x - sprite.x == 2) ? 1 : 2);
+			else
+				var x = (sprite.x - beside[b].x == 3) ? 2 : ((sprite.x - beside[b].x == 2) ? 1 : 0);
+
+			beside[b].frame.invalidate();
+			sprite.ini.lastDig = system.timer;
+			sprite.lastMove = system.timer;
+
+			if(beside[b].frame.data[0][x].ch == ascii(220)) {
+				beside[b].frame.data[0][x].ch = " ";
+				beside[b].frame.data[0][x].attr = BG_BLACK;
+				if(	(sprite.bearing == "e" && x == 2)
+					||
+					(sprite.bearing == "w" && x == 0)
+				) {
+					beside[b].remove();
+				}
+				columnClear = (b == beside.length - 1);
+				break;
+			}
+
+			if(beside[b].frame.data[0][x].ch == ascii(223)) {
+				beside[b].frame.data[0][x].ch = ascii(220);
+				beside[b].frame.data[0][x].attr = BG_BLACK|BROWN;
+				break;
+			}
+
+			if(beside[b].frame.data[0][x].ch == ascii(219)) {
+				beside[b].frame.data[0][x].ch = ascii(220);
+				beside[b].frame.data[0][x].attr = BG_BLACK|RED;
+				break;
+			}
+
+		}
+
+		if(columnClear) {
+			sprite.move("forward");
+			if(	((sprite.bearing == "e" && !Sprite.checkRight(sprite))
+				||
+				(sprite.bearing == "w" && !Sprite.checkRight(sprite)))
+				&&
+				!Sprite.checkOverlap(sprite)
+			) {
+				lemonize(sprite);
+				return;
+			}
+		} else if(!blockFound) {
+			lemonize(sprite);
+		}
+
+	}
+
+	//	Like bashersGonnaBash, but for digging downward.
+	var diggersGonnaDig = function(sprite) {
+
+		if(typeof sprite.ini.lastDig == "undefined")
+			sprite.ini.lastDig = system.timer;
+		if(system.timer - sprite.ini.lastDig < .5)
+			return;
+
+		var below = Sprite.checkBelow(sprite);
+		if(!below)
+			below = Sprite.checkOverlap(sprite);
+		if(!below)
+			return;
+
+		sprite.ini.constantmotion = 0;
+
+		var clear = below.length;
+		for(var b = 0; b < below.length; b++) {
+
+			if(	below[b].ini.type != "block"
+				||
+				!below[b].open
+			) {
+				clear--;
+				continue;
+			}
+
+			if(below[b].ini.material == "metal") {
+				lemonize(sprite);
+				return;
+			}
+
+			var cleared = true;
+			for(var c = 0; c < 3; c++) {
+				if(below[b].frame.data[0][c].ch != " ")
+					cleared = false;
+			}				
+			if(cleared) {
+				below[b].remove();
+				clear--;
+				continue;
+			}
+
+			below[b].frame.invalidate();
+			sprite.ini.lastDig = system.timer;
+
+			for(var c = 0; c < 3; c++) {
+
+				if(below[b].frame.data[0][c].ch == ascii(220)) {
+					below[b].frame.data[0][c].ch = " ";
+					below[b].frame.data[0][c].attr = 0;
+					break;
+				}
+
+				if(below[b].frame.data[0][c].ch == ascii(223)) {
+					below[b].frame.data[0][c].ch = ascii(220);
+					below[b].frame.data[0][c].attr = BG_BLACK|BROWN;
+					break;
+				}
+
+				if(below[b].frame.data[0][c].ch == ascii(219)) {
+					below[b].frame.data[0][c].ch = ascii(220);
+					below[b].frame.data[0][c].attr = BG_BLACK|RED;
+					break;
+				}
+
+			}
+
+		}
+
+		if(clear == 0) {
+			sprite.moveTo(sprite.x, sprite.y + 1);
+			if(!Sprite.checkBelow(sprite)) {
+				lemonize(sprite);
+				sprite.lastMove = system.timer;
+			}
+		}
+
+	}
+
+	// Build a staircase four blocks high, or until an obstacle is encountered
+	var buildersGonnaBuild = function(sprite) {
+
+		if(typeof sprite.ini.lastBuild == "undefined") {
+			sprite.ini.lastBuild = system.timer;
+			sprite.ini.buildCount = 0;
+			sprite.ini.constantmotion = 0;
+		}
+		if(system.timer - sprite.ini.lastBuild < .5)
+			return;
+
+		if(sprite.ini.buildCount == 4) {
+			lemonize(sprite);
+			delete sprite.ini.buildCount;
+			delete sprite.ini.lastBuild;
+			return;
+		}
+
+		sprite.ini.buildCount++;
+		sprite.ini.lastBuild = system.timer;
+
+		var block = new Sprite.Profile(
+			"brick",
+			frames.field,
+			(sprite.bearing == "e") ? (sprite.x + sprite.ini.width) : (sprite.x - 4),
+			sprite.y + sprite.ini.height - 1,
+			"e",
+			"normal"
+		);
+		var overlap = Sprite.checkOverlap(block);
+		if(overlap) {
+			block.remove();
+			Sprite.profiles.splice(block.index, 1);
+			lemonize(sprite);
+			delete sprite.ini.lastBuild;
+			delete sprite.ini.buildCount;
+			return;
+		}
+		block.frame.open();
+		sprite.moveTo(
+			(sprite.bearing == "e") ? (sprite.x + 3) : (sprite.x - 3),
+			sprite.y - 1
+		);
+
+	}
+
+	// Draw the time left until explosion at the centre of each of a lemon's positions
+	var ticker = function(sprite) {
+		sprite.ini.ticker--;
+		sprite.frame.data[1][1].ch = sprite.ini.ticker;
+		sprite.frame.data[1][4].ch = sprite.ini.ticker;
+		sprite.frame.data[1][7].ch = sprite.ini.ticker;
+		sprite.frame.data[4][1].ch = sprite.ini.ticker;
+		sprite.frame.data[4][4].ch = sprite.ini.ticker;
+		sprite.frame.data[4][7].ch = sprite.ini.ticker;
+	}
+
+	// Make the lemon look explodey, cause some damage
+	var explode = function(sprite) {
+		if(!sprite.open)
+			return;
+		sprite.changePosition("nuked");
+		sprite.ini.speed = 2000;
+		var overlap = Sprite.checkOverlap(sprite, 1);
+		for(var o = 0; overlap && o < overlap.length; o++) {
+			if(overlap[o].ini.type == "lemon")
+				continue;
+			overlap[o].remove();
+		}
+		lost++;
+		frames.counters.lostSaved.clear();
+		frames.counters.lostSaved.putmsg(lost + "/" + saved);
+	}
+
+	// Redden a lemon, and prepare it for obliteration
+	var nuke = function(sprite) {
+		sprite.ini.ticker = 5;
+		timer.addEvent(5000, false, explode, [sprite]);
+		timer.addEvent(6000, false, remove, [sprite]);
+		timer.addEvent(1000, 5, ticker, [sprite]);
+		colorize(sprite, COLOUR_NUKED);
+		ticker(sprite);
+	}
+
+	// Make a lemon tap its foot
+	var tapFoot = function(sprite) {
+		sprite.changePosition(
+			(sprite.position == "normal") ? "normal2" : "normal"
+		);
+	}
+
+	/*	Populate the terminal with a status bar, the level's static sprites,
+		and set up timed events for releasing lemons and level timeout.	*/
+	var loadLevel = function(level, frame) {
+
+		// The parent frame for all sprites & frames created by Level
+		frames.game = new Frame(
+			frame.x,
+			frame.y,
+			frame.width,
+			frame.height,
+			BG_BLACK|LIGHTGRAY,
+			frame
+		);
+
+		// The gameplay area
+		frames.field = new Frame(
+			frames.game.x,
+			frames.game.y,
+			frames.game.width,
+			frames.game.height - 2,
+			BG_BLACK|LIGHTGRAY,
+			frames.game
+		);
+
+		// The status bar and its various subframes
+		frames.statusBar = new Frame(
+			frames.game.x,
+			frames.game.y + frames.game.height - 2,
+			frames.game.width,
+			2,
+			COLOUR_STATUSBAR_BG|COLOUR_STATUSBAR_FG,
+			frames.game
+		);
+
+		frames.counters.basher = new Frame(
+			frames.statusBar.x + 10,
+			frames.statusBar.y,
+			3,
+			1,
+			COLOUR_STATUSBAR_BG|COLOUR_STATUSBAR_FG,
+			frames.statusBar
+		);
+		
+		frames.counters.bomber = new Frame(
+			frames.statusBar.x + 24,
+			frames.statusBar.y,
+			3,
+			1,
+			COLOUR_STATUSBAR_BG|COLOUR_STATUSBAR_FG,
+			frames.statusBar
+		);
+		
+		frames.counters.climber = new Frame(
+			frames.statusBar.x + 38,
+			frames.statusBar.y,
+			3,
+			1,
+			COLOUR_STATUSBAR_BG|COLOUR_STATUSBAR_FG,
+			frames.statusBar
+		);
+		
+		frames.counters.blocker = new Frame(
+			frames.statusBar.x + 10,
+			frames.statusBar.y + 1,
+			3,
+			1,
+			COLOUR_STATUSBAR_BG|COLOUR_STATUSBAR_FG,
+			frames.statusBar
+		);
+		
+		frames.counters.builder = new Frame(
+			frames.statusBar.x + 24,
+			frames.statusBar.y + 1,
+			3,
+			1,
+			COLOUR_STATUSBAR_BG|COLOUR_STATUSBAR_FG,
+			frames.statusBar
+		);
+		
+		frames.counters.digger = new Frame(
+			frames.statusBar.x + 38,
+			frames.statusBar.y + 1,
+			3,
+			1,
+			COLOUR_STATUSBAR_BG|COLOUR_STATUSBAR_FG,
+			frames.statusBar
+		);
+
+		frames.counters.remaining = new Frame(
+			frames.statusBar.x + 69,
+			frames.statusBar.y,
+			3,
+			1,
+			COLOUR_STATUSBAR_BG|COLOUR_STATUSBAR_FG,
+			frames.statusBar
+		);
+
+		frames.counters.time = new Frame(
+			frames.statusBar.x + 76,
+			frames.statusBar.y + 1,
+			3,
+			1,
+			COLOUR_STATUSBAR_BG|COLOUR_STATUSBAR_FG,
+			frames.statusBar
+		);
+
+		frames.counters.lostSaved = new Frame(
+			frames.statusBar.x + 69,
+			frames.statusBar.y + 1,
+			4,
+			1,
+			COLOUR_STATUSBAR_BG|COLOUR_STATUSBAR_FG,
+			frames.statusBar
+		);		
+		
+		// Populate the status bar's static text fields
+		frames.statusBar.putmsg("1) Bash :     ", COLOUR_BASHER|COLOUR_STATUSBAR_BG);
+		frames.statusBar.putmsg("3) Bomb :     ", COLOUR_BOMBER|COLOUR_STATUSBAR_BG);
+		frames.statusBar.putmsg("5) Climb:     ", COLOUR_CLIMBER|COLOUR_STATUSBAR_BG);
+		frames.statusBar.putmsg("N)uke   ", COLOUR_NUKED|COLOUR_STATUSBAR_BG);
+		frames.statusBar.putmsg("H)elp  ", WHITE|COLOUR_STATUSBAR_BG);
+		frames.statusBar.putmsg(" Remaining:       Time:\r\n", WHITE|COLOUR_STATUSBAR_BG);
+		frames.statusBar.putmsg("2) Block:     ", COLOUR_BLOCKER|COLOUR_STATUSBAR_BG);
+		frames.statusBar.putmsg("4) Build:     ", COLOUR_BUILDER|COLOUR_STATUSBAR_BG);
+		frames.statusBar.putmsg("6) Dig  :     ", COLOUR_DIGGER|COLOUR_STATUSBAR_BG);
+		frames.statusBar.putmsg("P)ause  ", WHITE|COLOUR_STATUSBAR_BG);
+		frames.statusBar.putmsg("Q)uit  ", WHITE|COLOUR_STATUSBAR_BG);
+		frames.statusBar.putmsg("Lost/Saved: ", WHITE|COLOUR_STATUSBAR_BG);
+
+		// Display the initial lost/saved value
+		frames.counters.lostSaved.putmsg(lost + "/" + saved);
+
+		// Set the quota values
+		if(typeof level.quotas == "undefined")
+			level.quotas = {};
+		for(var q in level.quotas) {
+			quotas[q] = level.quotas[q];
+			frames.counters[q].putmsg(quotas[q]);
+		}
+
+		// Add bricks, etc. to the screen
+		for(var b = 0; b < level.blocks.length; b++) {
+			new Sprite.Profile(
+				level.blocks[b].type,
+				frames.field,
+				frames.field.x + level.blocks[b].x - 1,
+				frames.field.y + level.blocks[b].y - 1,
+				"e",
+				"normal"
+			);
+		}
+
+		// Add hazards (water, slime, lava) to the screen
+		for(var h = 0; h < level.hazards.length; h++) {
+			new Sprite.Profile(
+				level.hazards[h].type,
+				frames.field,
+				frames.field.x + level.hazards[h].x - 1,
+				frames.field.y + level.hazards[h].y - 1,
+				"e",
+				"normal"
+			);
+		}
+
+		// Add any shooters
+		for(var s = 0; s < level.shooters.length; s++) {
+			new Sprite.Profile(
+				level.shooters[s].type,
+				frames.field,
+				frames.field.x + level.shooters[s].x - 1,
+				frames.field.y + level.shooters[s].y - 1,
+				(level.shooters[s].type == "shooter-e") ? "e" : "w",
+				"normal"
+			);
+		}
+
+		// Create the cursor "sprite"
+		cursor = new Sprite.Platform(
+			frames.field,
+			frames.field.x + 39,
+			frames.field.y + 10,
+			1,
+			1,
+			ascii(219),
+			WHITE,
+			false,
+			false,
+			0
+		);
+		cursor.ini = {};
+		cursor.open = false; // Exempt it from collision-detection
+
+		// Add the in & out doors to the screen
+		new Sprite.Profile(
+			"entrance",
+			frames.field,
+			frames.field.x + level.entrance.x - 1,
+			frames.field.y + level.entrance.y - 1,
+			"e",
+			"normal"
+		);
+
+		new Sprite.Profile(
+			"exit",
+			frames.field,
+			frames.field.x + level.exit.x - 1,
+			frames.field.y + level.exit.y - 1,
+			"e",
+			"normal"
+		);
+
+		// Set up a timed lemon-release event and "remaining lemons" counter
+		var remaining = level.lemons;
+		var releaseLemon = function(x, y) {
+			new Sprite.Profile(
+				"lemon",
+				frames.field,
+				x,
+				y,
+				"e",
+				"normal"
+			);
+			Sprite.profiles[Sprite.profiles.length - 1].frame.open();
+			Sprite.profiles[Sprite.profiles.length - 1].ini.skill = "lemon";
+			remaining--;
+			frames.counters.remaining.clear();
+			frames.counters.remaining.putmsg(remaining);
+		}
+		frames.counters.remaining.putmsg(remaining);
+
+		timer.addEvent(
+			3000,
+			level.lemons,
+			releaseLemon,
+			[	frames.field.x + level.entrance.x - 1,
+				frames.field.y + level.entrance.y - 1
+			]
+		);
+		total = level.lemons; // When lost + saved == total, the level is done
+
+		// Set up a timed event to update the clock
+		countDown = level.time + 3; // Don't penalize users for delayed release
+		var tickTock = function() {
+			countDown--;
+			frames.counters.time.clear();
+			frames.counters.time.putmsg(countDown);
+		}
+		timer.addEvent(1000, level.time, tickTock);
+
+		// Open the top frame and all its chilluns
+		frames.game.open();
+
+		// Display the level number and name for a few sex
+		var popUp = new PopUp(["Level " + (n + 1) + ": " + level.name]);
+		timer.addEvent(3000, false, popUp.close, [], popUp);
+
+	}
+
+	/*	Make the lemons behave according to skillset, or die if necessary.
+		Return a LEVEL_ value to the parent script indicating whether the level
+		is complete (and why) or if it is in progress. */
+	this.cycle = function() {
+
+		/*	Record the index of the first lemon that overlaps with the cursor,
+			if any, for use when assigning skills during this.getcmd.
+			Make the cursor red if there's an overlap, white if not. */
+		cursor.frame.top();
+		var overlaps = Sprite.checkOverlap(cursor);
+		if(overlaps) {
+			for(var o = 0; o < overlaps.length; o++) {
+				if(overlaps[o].ini.type != "lemon")
+					continue;
+				cursor.frame.data[0][0].attr = LIGHTRED;
+				cursor.ini.hoveringOver = overlaps[o].index;
+				break;
+			}
+		} else if(typeof cursor.ini.hoveringOver != "undefined") {
+			cursor.frame.data[0][0].attr = WHITE;
+			delete cursor.ini.hoveringOver;
+		}
+
+		for(var s = 0; s < Sprite.profiles.length; s++) {
+
+			if(Sprite.profiles[s].ini.type == "shooter") {
+				Sprite.profiles[s].putWeapon();
+				continue;
+			}
+
+			if(	Sprite.profiles[s].ini.type == "projectile"
+				&&
+				Sprite.profiles[s].open
+			) {
+				var overlaps = Sprite.checkOverlap(Sprite.profiles[s]);
+				for(var o = 0; o < overlaps.length; o++) {
+					if(overlaps[o].ini.type != "lemon")
+						continue;
+					overlaps[o].remove();
+					lost++;
+					frames.counters.lostSaved.clear();
+					frames.counters.lostSaved.putmsg(lost + "/" + saved);
+				}
+			}
+
+			if(Sprite.profiles[s].ini.type != "lemon" || !Sprite.profiles[s].open)
+				continue;
+
+			// Remove any lemons that have gone off the screen
+			if(	!Sprite.profiles[s].open
+				||
+				Sprite.profiles[s].y + Sprite.profiles[s].ini.height > frames.field.y + frames.field.height
+				||
+				Sprite.profiles[s].y + Sprite.profiles[s].ini.height <= frames.field.y
+				||
+				Sprite.profiles[s].x + Sprite.profiles[s].ini.width <= frames.field.x
+				||
+				Sprite.profiles[s].x >= frames.field.x + frames.field.width
+			) {
+				Sprite.profiles[s].remove();
+				lost++;
+				frames.counters.lostSaved.clear();
+				frames.counters.lostSaved.putmsg(lost + "/" + saved);
+			}
+
+			// Don't stand on top of a door, start a death march if on a hazard
+			var below = Sprite.checkBelow(Sprite.profiles[s]);
+			if(below) {
+				for(var b = 0; b < below.length; b++) {
+					if(	below[b].ini.type == "exit"
+						||
+						below[b].ini.type == "entrance"
+					) {
+						Sprite.profiles[s].moveTo(
+							Sprite.profiles[s].x,
+							Sprite.profiles[s].y + 1
+						);
+						break;
+					} else if(
+						below[b].ini.type == "hazard"
+						&&
+						Sprite.profiles[s].ini.skill != "dying"
+					) {
+						Sprite.profiles[s].ini.constantmotion = 0;
+						lost++;
+						frames.counters.lostSaved.clear();
+						frames.counters.lostSaved.putmsg(lost + "/" + saved);
+						Sprite.profiles[s].ini.skill = "dying";
+						colorize(Sprite.profiles[s], COLOUR_DYING);
+						timer.addEvent(
+							1000,
+							false,
+							remove,
+							[Sprite.profiles[s]]
+						);
+						break;
+					}
+				}
+			} else if(
+				Sprite.profiles[s].ini.skill != "digger"
+				&&
+				typeof Sprite.profiles[s].ini.forceDrop == "undefined"
+				&&
+				Sprite.profiles[s].ini.skill != "basher"
+			) {
+				/*	Sprite() moves things horizontally first, then vertically.
+					In certain circumstances (a hole of only the same width as
+					the sprite has just appeared beneath it) we need to force
+					the sprite to drop one cell to initiate a fall.	*/
+				Sprite.profiles[s].moveTo(
+					Sprite.profiles[s].x,
+					Sprite.profiles[s].y + 1
+				);
+				// But we only want to do it once, not every .cycle()
+				Sprite.profiles[s].ini.forceDrop = true;
+				/*	This will be cleared in fallIfNoFloor once the lemon
+					hits bottom. */
+			}
+
+			// Animate the lemon's walk, if applicable
+			if(	((	!Sprite.profiles[s].inFall
+					&&
+					system.timer - Sprite.profiles[s].lastMove > Sprite.profiles[s].ini.speed
+				)
+				||
+				(	Sprite.profiles[s].inFall
+					&&
+					system.timer - Sprite.profiles[s].lastYMove > Sprite.profiles[s].ini.speed	
+				))
+				&&
+				Sprite.profiles[s].ini.gravity == 1
+				&&
+				Sprite.profiles[s].ini.skill != "blocker"
+				&&
+				Sprite.profiles[s].position != "nuked"
+				&&
+				Sprite.profiles[s].ini.skill != "builder"
+				&&
+				Sprite.profiles[s].ini.skill != "digger"
+			) {
+				Sprite.profiles[s].changePosition(
+					(Sprite.profiles[s].position == "normal") ? "normal2" : "normal"
+				);
+			}
+
+			// Make the different types of lemons behave as they should
+			switch(Sprite.profiles[s].ini.skill) {
+
+				case "lemon":
+					turnOrClimbIfObstacle(Sprite.profiles[s]);
+					fallIfNoFloor(Sprite.profiles[s]);
+					removeIfAtExit(Sprite.profiles[s]);
+					break;
+
+				case "basher":
+					bashersGonnaBash(Sprite.profiles[s]);
+					fallIfNoFloor(Sprite.profiles[s]);
+					removeIfAtExit(Sprite.profiles[s]);
+					break;
+
+				case "blocker":
+					fallIfNoFloor(Sprite.profiles[s]);
+					break;
+
+				case "bomber":
+					turnOrClimbIfObstacle(Sprite.profiles[s]);
+					fallIfNoFloor(Sprite.profiles[s]);
+					removeIfAtExit(Sprite.profiles[s]);
+					break;
+
+				case "builder":
+					turnOrClimbIfObstacle(Sprite.profiles[s]);
+					buildersGonnaBuild(Sprite.profiles[s]);
+					fallIfNoFloor(Sprite.profiles[s]);
+					removeIfAtExit(Sprite.profiles[s]);
+					break;
+
+				case "climber":
+					climbIfObstacle(Sprite.profiles[s]);
+					fallIfNoFloor(Sprite.profiles[s]);
+					removeIfAtExit(Sprite.profiles[s]);
+					break;
+
+				case "digger":
+					diggersGonnaDig(Sprite.profiles[s]);
+					fallIfNoFloor(Sprite.profiles[s]);
+					break;
+
+				default:
+					break;
+
+			}
+
+		}
+
+		// Cycle the other cyclable things
+		timer.cycle();
+		Sprite.cycle();
+
+		// Tell the parent script how to proceed
+		if(countDown <= 0) {
+			if(saved < (total / 2))
+				return LEVEL_TIME;
+			this.score = (saved * 100);
+			return LEVEL_NEXT;
+		} else if(lost + saved == total && saved >= (total / 2)) {
+			this.score = (saved * 100) + (countDown * 10);
+			return LEVEL_NEXT;
+		} else if(lost + saved == total && saved < (total / 2)) {
+			return LEVEL_DEAD;
+		} else {
+			return LEVEL_CONTINUE;
+		}
+
+	}
+
+	// Do what the user asked us to do, if it's valid
+	this.getcmd = function(userInput) {
+
+		var ret = true;
+
+		switch(userInput.toUpperCase()) {
+
+			case "":
+				break;
+
+			// Quit
+			case "Q":
+				ret = false;
+				break;
+
+			// Pause
+			case "P":
+				state = STATE_PAUSE;
+				break;
+
+			// Help
+			case "H":
+				state = STATE_HELP;
+				break;
+
+			// Cursor movement
+			case KEY_UP:
+				if(cursor.y > frames.field.y)
+					cursor.moveTo(cursor.x, cursor.y - 1);
+				break;
+
+			case KEY_DOWN:
+				if(cursor.y < frames.field.y + frames.field.height - 1)
+					cursor.moveTo(cursor.x, cursor.y + 1);
+				break;
+
+			case KEY_LEFT:
+				if(cursor.x > frames.field.x)
+					cursor.moveTo(cursor.x - 1, cursor.y);
+				break;
+
+			case KEY_RIGHT:
+				if(cursor.x < frames.field.x + frames.field.width - 1)
+					cursor.moveTo(cursor.x + 1, cursor. y);
+				break;
+
+			// Nuke the h'wales
+			case "N":
+				for(var s = 0; s < Sprite.profiles.length; s++) {
+					if(Sprite.profiles[s].ini.type != "lemon" || !Sprite.profiles[s].open)
+						continue;
+					nuke(Sprite.profiles[s]);
+				}
+				break;
+
+			// Basher
+			case "1":
+				if(typeof cursor.ini.hoveringOver == "undefined")
+					break;
+				if(quotas.basher < 1)
+					break;
+				quotas.basher--;
+				frames.counters.basher.clear();
+				frames.counters.basher.putmsg(quotas.basher);
+				Sprite.profiles[cursor.ini.hoveringOver].ini.skill = "basher";
+				colorize(Sprite.profiles[cursor.ini.hoveringOver], COLOUR_BASHER);
+				break;
+
+			// Blocker
+			case "2":
+				if(typeof cursor.ini.hoveringOver == "undefined")
+					break;
+				if(quotas.blocker < 1)
+					break;
+				quotas.blocker--;
+				frames.counters.blocker.clear();
+				frames.counters.blocker.putmsg(quotas.blocker);
+				Sprite.profiles[cursor.ini.hoveringOver].ini.skill = "blocker";
+				Sprite.profiles[cursor.ini.hoveringOver].ini.constantmotion = 0;
+				Sprite.profiles[cursor.ini.hoveringOver].changePosition("fall");
+				colorize(Sprite.profiles[cursor.ini.hoveringOver], COLOUR_BLOCKER);
+				timer.addEvent(1000, true, tapFoot, [Sprite.profiles[cursor.ini.hoveringOver]]);
+				break;
+
+			// Bomber
+			case "3":
+				if(typeof cursor.ini.hoveringOver == "undefined")
+					break;
+				if(quotas.bomber < 1)
+					break;
+				quotas.bomber--;
+				frames.counters.bomber.clear();
+				frames.counters.bomber.putmsg(quotas.bomber);
+				Sprite.profiles[cursor.ini.hoveringOver].ini.skill = "bomber";
+				nuke(Sprite.profiles[cursor.ini.hoveringOver]);
+				break;
+
+			// Builder
+			case "4":
+				if(typeof cursor.ini.hoveringOver == "undefined")
+					break;
+				if(quotas.builder < 1)
+					break;
+				quotas.builder--;
+				frames.counters.builder.clear();
+				frames.counters.builder.putmsg(quotas.builder);
+				Sprite.profiles[cursor.ini.hoveringOver].ini.skill = "builder";
+				colorize(Sprite.profiles[cursor.ini.hoveringOver], COLOUR_BUILDER);
+				break;
+
+			// Climber
+			case "5":
+				if(typeof cursor.ini.hoveringOver == "undefined")
+					break;
+				if(quotas.climber < 1)
+					break;
+				quotas.climber--;
+				frames.counters.climber.clear();
+				frames.counters.climber.putmsg(quotas.climber);
+				Sprite.profiles[cursor.ini.hoveringOver].ini.skill = "climber";
+				colorize(Sprite.profiles[cursor.ini.hoveringOver], COLOUR_CLIMBER);
+				break;
+
+			// Digger
+			case "6":
+				if(typeof cursor.ini.hoveringOver == "undefined")
+					break;
+				if(quotas.digger < 1)
+					break;
+				quotas.digger--;
+				frames.counters.digger.clear();
+				frames.counters.digger.putmsg(quotas.digger);
+				Sprite.profiles[cursor.ini.hoveringOver].ini.skill = "digger";
+				Sprite.profiles[cursor.ini.hoveringOver].ini.constantmotion = 0;
+				Sprite.profiles[cursor.ini.hoveringOver].changePosition("fall");
+				colorize(Sprite.profiles[cursor.ini.hoveringOver], COLOUR_DIGGER);
+				break;
+
+			default:
+				break;
+		}
+
+		return ret;
+
+	}
+
+	/*	Halt or resume all timed events.
+		This doesn't actually block - that should be done in the main loop.
+		If no argument is specified, events are removed from the timer and an
+		array of these events is returned.
+		If an argument is specified, it is assumed to be an array of events,
+		and they are added back into the timer. */
+	this.pause = function(events) {
+		if(typeof events == "undefined") {
+			var events = timer.events;
+			timer.events = [];
+			return events;
+		} else {
+			for(var e = 0; e < events.length; e++) {
+				timer.addEvent(
+					events[e].interval,
+					events[e].repeat,
+					events[e].action,
+					events[e].arguments
+				);
+			}
+		}
+	}
+
+	// Reset the Sprite globals, clean up the display
+	this.close = function() {
+		
+		cursor.remove();
+		Sprite.platforms = [];
+
+		for(var s = 0; s < Sprite.profiles.length; s++) {
+			Sprite.profiles[s].remove();
+		}
+		Sprite.profiles = [];
+
+		frames.game.delete();
+
+	}
+
+	loadLevel(l, frame);
+
+}
\ No newline at end of file
diff --git a/xtrn/lemons/leveledit.js b/xtrn/lemons/leveledit.js
new file mode 100644
index 0000000000000000000000000000000000000000..2685778dc4049f3e07a09493aff8ce30a163b720
--- /dev/null
+++ b/xtrn/lemons/leveledit.js
@@ -0,0 +1,112 @@
+/*	Uses the LevelEditor object from leveleditor.js to make changes to the
+	levels.json file.  This whole setup is quick and dirty, and was only
+	made so that I could easily add levels to the game.  It isn't user-
+	friendly and I don't expect (or want to support) other people using it. */
+
+load("sbbsdefs.js");
+load("frame.js");
+load("tree.js");
+load(js.exec_dir + "leveleditor.js");
+
+var frame, headFrame, footFrame, treeFrame, tree, treeItems = [];
+
+var loadLevels = function() {
+	if(!file_exists(js.exec_dir + "levels.json"))
+		return [];
+	var f = new File(js.exec_dir + "levels.json");
+	f.open("r");
+	var levels = JSON.parse(f.read());
+	f.close();
+	return levels;
+}
+
+var saveLevels = function(levels) {
+	file_backup(js.exec_dir + "levels.json");
+	var f = new File(js.exec_dir + "levels.json");
+	f.open("w");
+	f.write(JSON.stringify(levels));
+	f.close();
+}
+
+var initFrames = function() {
+	console.clear();
+	frame = new Frame(1, 1, 80, 24, WHITE);
+	headFrame = new Frame(1, 1, 80, 1, BG_BLUE|WHITE, frame);
+	treeFrame = new Frame(1, 2, 80, 22, BG_BLACK|WHITE, frame);
+	footFrame = new Frame(1, 24, 80, 1, BG_BLUE|WHITE, frame);
+	headFrame.putmsg("Lemons Level Editor");
+	footFrame.putmsg("[Enter], A)dd, D)elete, [ESC] quit");
+	tree = new Tree(treeFrame);
+	frame.open();
+	tree.open();
+}
+
+var clearTree = function() {
+	for(var item in treeItems)
+		tree.deleteItem(tree.trace(treeItems[item].hash));
+	treeItems = [];
+}
+
+var initMenu = function() {
+	clearTree();
+	var levels = loadLevels();
+	for(var l = 0; l < levels.length; l++)
+		treeItems.push(tree.addItem(levels[l].name, editLevel, l));
+	tree.index = 0;
+}
+
+var editLevel = function(level) {
+	var levels = loadLevels();
+	var editor = new LevelEditor(frame);
+	editor.open();
+	if(typeof level != "undefined")
+		editor.loadLevel(levels[level]);
+	while(!js.terminated) {
+		var userInput = console.inkey(K_UPPER, 5);
+		if(ascii(userInput) == 27)
+			break;
+		editor.getcmd(userInput);
+		editor.cycle();
+		if(frame.cycle())
+			console.gotoxy(80, 24);
+	}
+	var result = editor.close();
+	if(typeof level == "undefined")
+		levels.push(result);
+	else
+		levels[level] = result;
+	saveLevels(levels);
+	frame.invalidate();
+}
+
+var main = function() {
+	initFrames();
+	initMenu();
+	while(!js.terminated) {
+		var userInput = console.inkey(K_UPPER, 5);
+		if(ascii(userInput) == 27)
+			break;
+		if(userInput == "A") {
+			editLevel();
+			initMenu();
+		} else if(userInput == "D") {
+			log(tree.index);
+			var levels = loadLevels();
+			levels.splice(tree.index, 1);
+			saveLevels(levels);
+			initMenu();
+		} else {
+			tree.getcmd(userInput);
+		}
+		if(frame.cycle())
+			console.gotoxy(80, 24);
+	}
+}
+
+var cleanUp = function() {
+	tree.close();
+	frame.close();
+}
+
+main();
+cleanUp();
diff --git a/xtrn/lemons/leveleditor.js b/xtrn/lemons/leveleditor.js
new file mode 100644
index 0000000000000000000000000000000000000000..b98f7e97332fd65bd6b4c4c3bda653430375ddcf
--- /dev/null
+++ b/xtrn/lemons/leveleditor.js
@@ -0,0 +1,524 @@
+/*	This is a terrible modification of the terrible Chicken Delivery
+	Level Editor.  This script provides the LevelEditor object, but
+	doesn't actually do anything on its own.  See leveledit.js. */
+
+load("sbbsdefs.js");
+load("frame.js");
+load("tree.js");
+load("funclib.js");
+
+var LevelEditor = function(parentFrame) {
+
+	var frame,
+		fieldFrame,
+		cursorFrame,
+		headFrame,
+		footFrame,
+		treeFrame,
+		treeSubFrame;
+
+	var stuff = {
+		name : "",
+		time : 120,
+		blocks : [],
+		hazards : [],
+		shooters : [],
+		entrance : false,
+		exit : false
+	};
+
+	var state = {
+		'block' : false,
+		'entrance' : false,
+		'exit' : false,
+		'hazard' : false,
+		'shooter' : false
+	};
+
+	var entrance,
+		exit,
+		blocks = [],
+		hazards = []
+		shooters = [];
+
+	var loadSprites = function() {
+		var files = directory(js.exec_dir + "sprites/*.ini");
+		for(var f = 0; f < files.length; f++) {
+			var file = new File(files[f]);
+			file.open("r");
+			var ini = file.iniGetObject();
+			file.close();
+			var shortName = file_getname(files[f]).replace(/\.ini$/, "");
+			if(ini.type == "entrance")
+				entrance = shortName;
+			else if(ini.type == "exit")
+				exit = shortName;
+			else if(ini.type == "block")
+				blocks.push(shortName);
+			else if(ini.type == "hazard")
+				hazards.push(shortName);
+			else if(ini.type == "shooter")
+				shooters.push(shortName);
+		}
+	}
+
+	var noYes = function(question) {
+		var nyFrame = new Frame(
+			Math.floor((fieldFrame.width - question.length - 9) / 2),
+			Math.floor((fieldFrame.height - 1) / 2),
+			question.length + 9,
+			1,
+			BG_BLUE|WHITE,
+			fieldFrame
+		);
+		nyFrame.open();
+		nyFrame.putmsg(question + "? Y/N: ");
+		frame.cycle();
+		var ret = console.getkeys("YN");
+		nyFrame.delete();
+		return (ret == "Y");
+	}
+
+	var moveUp = function(subFrame, frame) {
+		if(subFrame.y > frame.y)
+			subFrame.move(0, -1);
+	}
+
+	var moveDown = function(subFrame, frame) {
+		if(subFrame.y + subFrame.height - 1 < frame.y + frame.height - 1)
+			subFrame.move(0, 1);
+	}
+
+	var moveLeft = function(subFrame, frame) {
+		if(subFrame.x > frame.x)
+			subFrame.move(-1, 0);
+	}
+
+	var moveRight = function(subFrame, frame) {
+		if(subFrame.x + subFrame.width - 1 < frame.x + frame.width - 1)
+			subFrame.move(1, 0);
+	}
+
+	var checkDelete = function(frame) {
+		if(cursorFrame.x < frame.x)
+			return false;
+		if(cursorFrame.x >= frame.x + frame.width)
+			return false;
+		if(cursorFrame.y < frame.y)
+			return false;
+		if(cursorFrame.y >= frame.y + frame.height)
+			return false;
+		return true;
+	}
+
+	var blockChooser = function() {
+		var tree = new Tree(treeSubFrame);
+		for(var b in blocks)
+			tree.addItem(blocks[b], blocks[b]);
+		treeFrame.top();
+		tree.open();
+		var choice = "";
+		while(blocks.indexOf(choice) < 0) {
+			choice = console.inkey(K_NONE, 5);
+			if(ascii(choice) == 27)
+				break;
+			choice = tree.getcmd(choice);
+			if(frame.cycle())
+				console.gotoxy(console.screen_columns, console.screen_rows);
+		}
+		tree.close();
+		treeFrame.bottom();
+		if(typeof choice != "string")
+			return false;
+		var f = loadItem(choice);
+		f.type = choice;
+		return f;
+	}
+
+	var hazardChooser = function() {
+		var tree = new Tree(treeSubFrame);
+		for(var h in hazards)
+			tree.addItem(hazards[h], hazards[h]);
+		treeFrame.top();
+		tree.open();
+		var choice = "";
+		while(hazards.indexOf(choice) < 0) {
+			choice = console.inkey(K_NONE, 5);
+			if(ascii(choice) == 27)
+				break;
+			choice = tree.getcmd(choice);
+			if(frame.cycle())
+				console.gotoxy(console.screen_columns, console.screen_rows);
+		}
+		tree.close();
+		treeFrame.bottom();
+		if(typeof choice != "string")
+			return false;
+		var f = loadItem(choice);
+		f.type = choice;
+		return f;
+	}
+
+	var shooterChooser = function() {
+		var tree = new Tree(treeSubFrame);
+		for(var s in shooters)
+			tree.addItem(shooters[s], shooters[s]);
+		treeFrame.top();
+		tree.open();
+		var choice = "";
+		while(shooters.indexOf(choice) < 0) {
+			choice = console.inkey(K_NONE, 5);
+			if(ascii(choice) == 27)
+				break;
+			choice = tree.getcmd(choice);
+			if(frame.cycle())
+				console.gotoxy(console.screen_columns, console.screen_rows);
+		}
+		tree.close();
+		treeFrame.bottom();
+		if(typeof choice != "string")
+			return false;
+		var f = loadItem(choice);
+		f.type = choice;
+		return f;
+	}
+
+	var initFrames = function() {
+
+		console.clear(LIGHTGRAY);
+
+		frame = new Frame(1, 1,	80, 24, LIGHTGRAY);
+
+		headFrame = new Frame(
+			frame.x,
+			frame.y,
+			frame.width,
+			1,
+			BG_BLUE|WHITE,
+			frame
+		);
+
+		fieldFrame = new Frame(
+			frame.x,
+			frame.y,
+			frame.width,
+			frame.height - 2,
+			LIGHTGRAY,
+			frame
+		);
+
+		footFrame = new Frame(
+			frame.x,
+			frame.y + frame.height - 1,
+			frame.width,
+			1,
+			BG_BLUE|WHITE,
+			frame
+		);
+
+		cursorFrame = new Frame(
+			fieldFrame.x,
+			fieldFrame.y,
+			1,
+			1,
+			WHITE,
+			fieldFrame
+		);
+
+		treeFrame = new Frame(
+			Math.floor(fieldFrame.x + (fieldFrame.width / 4)),
+			fieldFrame.y + 2,
+			Math.floor(fieldFrame.width / 2),
+			fieldFrame.height - 4,
+			BG_BLUE|WHITE,
+			frame
+		);
+
+		treeSubFrame = new Frame(
+			treeFrame.x + 1,
+			treeFrame.y + 1,
+			treeFrame.width - 2,
+			treeFrame.height - 2,
+			WHITE,
+			treeFrame
+		);
+
+		headFrame.putmsg("Lemons Level Editor");
+		footFrame.putmsg("B)lock, H)azard, S)hooter, E)ntrance, e(X)it, [DEL], [ENTER], [ESC]");
+		treeFrame.center("Choose an item");
+		cursorFrame.putmsg(ascii(219));
+
+		frame.open();
+		treeFrame.bottom();
+
+	}
+
+	var loadItem = function(item) {
+		var f = new File(js.exec_dir + "sprites/" + item + ".ini");
+		f.open("r");
+		var ini = f.iniGetObject();
+		f.close();
+		ini.width = parseInt(ini.width);
+		ini.height = parseInt(ini.height);
+		if(cursorFrame.x + ini.width > fieldFrame.x + fieldFrame.width)
+			return false;
+		if(cursorFrame.y + ini.height > fieldFrame.y + fieldFrame.height)
+			return false;
+		var f = new Frame(cursorFrame.x, cursorFrame.y, ini.width, ini.height, 0, fieldFrame);
+		f.open();
+		f.load(js.exec_dir + "sprites/" + item + ".bin", ini.width, ini.height);
+		f.type = item;
+		return f;
+	}
+
+	this.open = function() {
+		initFrames();
+		loadSprites();
+	}
+
+	this.loadLevel = function(level) {
+		stuff.name = level.name;
+		stuff.time = (typeof level.time == "undefined") ? 60 : level.time;
+		stuff.blocks = [];
+		stuff.hazards = [];
+		stuff.shooters = [];
+		stuff.entrance = { 'x' : fieldFrame.x, 'y' : fieldFrame.y };
+		stuff.exit = { 'x' : fieldFrame.x, 'y' : fieldFrame.y };
+		for(var property in state)
+			state[property] = false;
+		fieldFrame.clear();
+		cursorFrame.moveTo(level.entrance.x, level.entrance.y);
+		stuff.entrance = loadItem(entrance);
+		cursorFrame.moveTo(level.exit.x, level.exit.y);
+		stuff.exit = loadItem(exit);
+		for(var b = 0; b < level.blocks.length; b++) {
+			cursorFrame.moveTo(level.blocks[b].x, level.blocks[b].y);
+			stuff.blocks.push(loadItem(level.blocks[b].type));
+		}
+		for(var h = 0; h < level.hazards.length; h++) {
+			cursorFrame.moveTo(level.hazards[h].x, level.hazards[h].y);
+			stuff.hazards.push(loadItem(level.hazards[h].type));
+		}
+		for(var s = 0; s < level.shooters.length; s++) {
+			cursorFrame.moveTo(level.shooters[s].x, level.shooters[s].y);
+			stuff.shooters.push(loadItem(level.shooters[s].type));
+		}
+	}
+
+	this.getcmd = function(userInput) {
+
+		switch(userInput) {
+			case KEY_UP:
+				moveUp(cursorFrame, fieldFrame);
+				if(state.block)
+					moveUp(state.block, fieldFrame);
+				else if(state.hazard)
+					moveUp(state.hazard, fieldFrame);
+				else if(state.entrance)
+					moveUp(state.entrance, fieldFrame);
+				else if(state.exit)
+					moveUp(state.exit, fieldFrame);
+				else if(state.shooter)
+					moveUp(state.shooter, fieldFrame);
+				break;
+			case KEY_DOWN:
+				moveDown(cursorFrame, fieldFrame);
+				if(state.block)
+					moveDown(state.block, fieldFrame);
+				else if(state.hazard)
+					moveDown(state.hazard, fieldFrame);
+				else if(state.entrance)
+					moveDown(state.entrance, fieldFrame);
+				else if(state.exit)
+					moveDown(state.exit, fieldFrame);
+				else if(state.shooter)
+					moveDown(state.shooter, fieldFrame);
+				break;
+			case KEY_LEFT:
+				moveLeft(cursorFrame, fieldFrame);
+				if(state.block)
+					moveLeft(state.block, fieldFrame);
+				else if(state.hazard)
+					moveLeft(state.hazard, fieldFrame);
+				else if(state.entrance)
+					moveLeft(state.entrance, fieldFrame);
+				else if(state.exit)
+					moveLeft(state.exit, fieldFrame);
+				else if(state.shooter)
+					moveLeft(state.shooter, fieldFrame);
+				break;
+			case KEY_RIGHT:
+				moveRight(cursorFrame, fieldFrame);
+				if(state.block)
+					moveRight(state.block, fieldFrame);
+				else if(state.hazard)
+					moveRight(state.hazard, fieldFrame);
+				else if(state.entrance)
+					moveRight(state.entrance, fieldFrame);
+				else if(state.exit)
+					moveRight(state.exit, fieldFrame);
+				else if(state.shooter)
+					moveRight(state.shooter, fieldFrame);
+				break;
+			case KEY_DEL:
+				if(stuff.entrance && checkDelete(stuff.entrance)) {
+					stuff.entrance.delete();
+					stuff.entrance = false;
+				}
+				if(stuff.exit && checkDelete(stuff.exit)) {
+					stuff.exit.delete();
+					stuff.exit = false;
+				}
+				for(var b = 0; b < stuff.blocks.length; b++) {
+					if(!checkDelete(stuff.blocks[b]))
+						continue;
+					var block = stuff.blocks.splice(b, 1)[0];
+					block.delete();
+				}
+				for(var h = 0; h < stuff.hazards.length; h++) {
+					if(!checkDelete(stuff.hazards[h]))
+						continue;
+					var hazard = stuff.hazards.splice(h, 1)[0];
+					hazard.delete();
+				}
+				for(var s = 0; s < stuff.shooters.length; s++) {
+					if(!checkDelete(stuff.shooters[s]))
+						continue;
+					var shooter = stuff.shooters.splice(s, 1)[0];
+					shooter.delete();
+				}
+				break;
+			case "B":
+				if(state.shooter || state.block || state.entrance || state.exit || state.hazard)
+					break;
+				state.block = blockChooser();
+				if(!state.block)
+					break;
+				state.block.moveTo(cursorFrame.x, cursorFrame.y);
+				break;
+			case "H":
+				if(state.shooter || state.hazard || state.entrance || state.exit || state.block)
+					break;
+				state.hazard = hazardChooser();
+				if(!state.hazard)
+					break;
+				state.hazard.moveTo(cursorFrame.x, cursorFrame.y);
+				break;
+			case "E":
+				if(state.shooter || state.entrance || stuff.entrance || state.exit || state.block || state.hazard)
+					break;
+				state.entrance = loadItem(entrance);
+				break;
+			case "X":
+				if(state.shooter || state.entrance || stuff.exit || state.exit || state.block || state.hazard)
+					break;
+				state.exit = loadItem(exit);
+				break;
+			case "I":
+				// "I" don't know what this is for
+				break;
+			case "S":
+				if(state.shooter || state.entrance || state.exit || state.hazard || state.block)
+					break;
+				state.shooter = shooterChooser();
+				if(!state.shooter)
+					break;
+				state.shooter.moveTo(cursorFrame.x, cursorFrame.y);
+				break;
+			case "\r":
+				if(state.block) {
+					stuff.blocks.push(state.block);
+					state.block = false;
+				} else if(state.hazard) {
+					stuff.hazards.push(state.hazard);
+					state.hazard = false;
+				} else if(state.entrance) {
+					stuff.entrance = state.entrance;
+					state.entrance = false;
+				} else if(state.exit) {
+					stuff.exit = state.exit;
+					state.exit = false;
+				} else if(state.shooter) {
+					stuff.shooters.push(state.shooter);
+					state.shooter = false;
+				}
+				break;
+			default:
+				break;
+
+		}
+	}
+
+	this.cycle = function() {
+		if(frame.cycle())
+			console.gotoxy(console.screen_columns, console.screen_rows);
+		cursorFrame.top();
+	}
+
+	this.close = function() {
+		var ret = {
+			'name' : stuff.name,
+			'time' : stuff.time,
+			'author' : user.alias,
+			'system' : system.name,
+			'date' : time(),
+			'blocks' : [],
+			'hazards' : [],
+			'shooters' : [],
+			'lemons' : 1,
+			'quotas' : {
+				'basher' : 5,
+				'blocker' : 5,
+				'bomber' : 5,
+				'builder' : 5,
+				'climber' : 5,
+				'digger' : 5
+			},
+			'entrance' : (!stuff.entrance) ? {'x' : fieldFrame.x , 'y' : fieldFrame.y } : { 'x' : stuff.entrance.x, 'y' : stuff.entrance.y },
+			'exit' : (!stuff.exit) ? {'x' : fieldFrame.x , 'y' : fieldFrame.y } : { 'x' : stuff.exit.x, 'y' : stuff.exit.y }
+		};
+		for(var b in stuff.blocks) {
+			ret.blocks.push(
+				{	'x' : stuff.blocks[b].x,
+					'y' : stuff.blocks[b].y,
+					'type' : stuff.blocks[b].type
+				}
+			);
+		}
+		for(var h in stuff.hazards) {
+			ret.hazards.push(
+				{	'x' : stuff.hazards[h].x,
+					'y' : stuff.hazards[h].y,
+					'type' : stuff.hazards[h].type
+				}
+			);
+		}
+		for(var s in stuff.shooters) {
+			ret.shooters.push(
+				{	'x' : stuff.shooters[s].x,
+					'y' : stuff.shooters[s].y,
+					'type' : stuff.shooters[s].type
+				}
+			);
+		}
+		if(typeof frame.parent != "undefined")
+			frame.parent.invalidate();
+		frame.close();
+		console.clear(LIGHTGRAY);
+		console.putmsg("How many lemons: (1 - 99) ");
+		ret.lemons = console.getnum(99);
+		console.putmsg("Time limit, in seconds: (1 - 600) ");
+		ret.time = console.getnum(600);
+		console.putmsg("Quotas (1 - 99 of each skill type) \r\n");
+		for(var q in ret.quotas) {
+			console.putmsg("\t" + q + ": ");
+			ret.quotas[q] = console.getnum(99);
+		}
+		console.putmsg("Name this level: ");
+		ret.name = console.getstr(ret.name, 60, K_LINE|K_EDIT);
+		if(console.strlen(ret.name.replace(/\s/g, "")) < 1)
+			ret.name = "Untitled";
+		return ret;
+	}
+
+}
diff --git a/xtrn/lemons/levels.json b/xtrn/lemons/levels.json
new file mode 100644
index 0000000000000000000000000000000000000000..8d1973acacfd5ac7c35ce94bda9370009ad4b49c
--- /dev/null
+++ b/xtrn/lemons/levels.json
@@ -0,0 +1 @@
+[{"name":"Lemon Party","time":180,"author":"echicken","system":"electronic chicken bbs","date":1426565307,"blocks":[{"x":4,"y":4,"type":"brick"},{"x":7,"y":4,"type":"brick"},{"x":10,"y":4,"type":"brick"},{"x":13,"y":4,"type":"brick"},{"x":16,"y":4,"type":"brick"},{"x":19,"y":4,"type":"brick"},{"x":22,"y":4,"type":"brick"},{"x":25,"y":4,"type":"brick"},{"x":28,"y":4,"type":"brick"},{"x":31,"y":4,"type":"brick"},{"x":34,"y":4,"type":"brick"},{"x":37,"y":4,"type":"brick"},{"x":40,"y":4,"type":"brick"},{"x":43,"y":4,"type":"brick"},{"x":46,"y":4,"type":"brick"},{"x":49,"y":4,"type":"brick"},{"x":52,"y":4,"type":"brick"},{"x":55,"y":4,"type":"brick"},{"x":58,"y":4,"type":"brick"},{"x":61,"y":4,"type":"brick"},{"x":64,"y":4,"type":"brick"},{"x":67,"y":4,"type":"brick"},{"x":70,"y":4,"type":"brick"},{"x":73,"y":4,"type":"brick"},{"x":76,"y":1,"type":"metal"},{"x":76,"y":2,"type":"metal"},{"x":76,"y":3,"type":"metal"},{"x":76,"y":4,"type":"metal"},{"x":1,"y":1,"type":"metal"},{"x":1,"y":2,"type":"metal"},{"x":1,"y":3,"type":"metal"},{"x":1,"y":4,"type":"metal"},{"x":75,"y":22,"type":"brick"},{"x":72,"y":22,"type":"brick"},{"x":69,"y":22,"type":"brick"},{"x":66,"y":22,"type":"brick"},{"x":63,"y":22,"type":"brick"},{"x":60,"y":22,"type":"brick"},{"x":57,"y":22,"type":"brick"},{"x":54,"y":22,"type":"brick"},{"x":51,"y":22,"type":"brick"},{"x":36,"y":22,"type":"brick"},{"x":33,"y":22,"type":"brick"},{"x":30,"y":22,"type":"brick"},{"x":27,"y":22,"type":"brick"},{"x":24,"y":22,"type":"brick"},{"x":21,"y":22,"type":"brick"},{"x":18,"y":22,"type":"brick"},{"x":15,"y":22,"type":"brick"},{"x":12,"y":22,"type":"brick"},{"x":9,"y":22,"type":"brick"},{"x":6,"y":22,"type":"brick"},{"x":3,"y":22,"type":"brick"},{"x":5,"y":11,"type":"brick"},{"x":8,"y":11,"type":"brick"},{"x":11,"y":11,"type":"brick"},{"x":14,"y":11,"type":"brick"},{"x":17,"y":11,"type":"brick"},{"x":23,"y":11,"type":"brick"},{"x":26,"y":11,"type":"brick"},{"x":29,"y":11,"type":"brick"},{"x":32,"y":11,"type":"brick"},{"x":35,"y":11,"type":"brick"},{"x":41,"y":11,"type":"brick"},{"x":44,"y":11,"type":"brick"},{"x":47,"y":11,"type":"brick"},{"x":50,"y":11,"type":"brick"},{"x":53,"y":11,"type":"brick"},{"x":56,"y":11,"type":"brick"},{"x":59,"y":11,"type":"brick"},{"x":62,"y":11,"type":"brick"},{"x":65,"y":11,"type":"brick"},{"x":68,"y":11,"type":"brick"},{"x":71,"y":11,"type":"brick"},{"x":74,"y":11,"type":"brick"},{"x":77,"y":10,"type":"metal"},{"x":77,"y":11,"type":"metal"},{"x":77,"y":9,"type":"metal"},{"x":77,"y":8,"type":"metal"},{"x":2,"y":11,"type":"metal"},{"x":2,"y":10,"type":"metal"},{"x":2,"y":9,"type":"metal"},{"x":2,"y":8,"type":"metal"},{"x":29,"y":10,"type":"brick"},{"x":32,"y":10,"type":"brick"},{"x":35,"y":10,"type":"brick"},{"x":38,"y":10,"type":"brick"},{"x":41,"y":10,"type":"brick"},{"x":44,"y":10,"type":"brick"},{"x":47,"y":10,"type":"brick"},{"x":5,"y":12,"type":"brick"},{"x":8,"y":12,"type":"brick"},{"x":11,"y":12,"type":"brick"},{"x":14,"y":12,"type":"brick"},{"x":23,"y":12,"type":"brick"},{"x":26,"y":12,"type":"brick"},{"x":65,"y":12,"type":"metal"},{"x":68,"y":12,"type":"metal"},{"x":71,"y":12,"type":"metal"},{"x":74,"y":12,"type":"metal"},{"x":77,"y":12,"type":"metal"},{"x":2,"y":12,"type":"metal"},{"x":38,"y":11,"type":"metal"},{"x":20,"y":11,"type":"metal"},{"x":20,"y":12,"type":"metal"},{"x":17,"y":12,"type":"metal"},{"x":39,"y":22,"type":"brick"},{"x":78,"y":18,"type":"metal"},{"x":78,"y":19,"type":"metal"},{"x":78,"y":20,"type":"metal"},{"x":78,"y":21,"type":"metal"},{"x":78,"y":22,"type":"metal"},{"x":38,"y":17,"type":"brick"},{"x":41,"y":17,"type":"brick"},{"x":44,"y":17,"type":"brick"},{"x":62,"y":12,"type":"brick"},{"x":59,"y":12,"type":"brick"},{"x":56,"y":12,"type":"brick"},{"x":53,"y":12,"type":"brick"},{"x":50,"y":12,"type":"brick"},{"x":47,"y":12,"type":"brick"},{"x":44,"y":12,"type":"brick"},{"x":29,"y":12,"type":"brick"}],"hazards":[],"shooters":[],"lemons":4,"quotas":{"basher":10,"blocker":10,"bomber":10,"builder":10,"climber":10,"digger":10},"entrance":{"x":4,"y":1},"exit":{"x":3,"y":19}},{"name":"Blocker Party","time":180,"author":"echicken","system":"electronic chicken bbs","date":1426654242,"blocks":[{"x":14,"y":4,"type":"metal"},{"x":17,"y":4,"type":"metal"},{"x":20,"y":4,"type":"metal"},{"x":23,"y":4,"type":"metal"},{"x":23,"y":10,"type":"brick"},{"x":26,"y":10,"type":"brick"},{"x":29,"y":10,"type":"brick"},{"x":32,"y":10,"type":"brick"},{"x":35,"y":10,"type":"brick"},{"x":38,"y":10,"type":"brick"},{"x":41,"y":10,"type":"brick"},{"x":44,"y":10,"type":"brick"},{"x":47,"y":10,"type":"brick"},{"x":50,"y":10,"type":"brick"},{"x":14,"y":22,"type":"brick"},{"x":17,"y":22,"type":"brick"},{"x":20,"y":22,"type":"brick"},{"x":23,"y":22,"type":"brick"},{"x":26,"y":22,"type":"brick"},{"x":29,"y":22,"type":"brick"},{"x":32,"y":22,"type":"brick"},{"x":35,"y":22,"type":"brick"},{"x":38,"y":22,"type":"brick"},{"x":41,"y":22,"type":"brick"},{"x":44,"y":22,"type":"brick"},{"x":47,"y":22,"type":"brick"},{"x":50,"y":22,"type":"brick"},{"x":53,"y":22,"type":"brick"},{"x":56,"y":22,"type":"brick"},{"x":59,"y":22,"type":"brick"},{"x":14,"y":10,"type":"metal"},{"x":17,"y":10,"type":"metal"},{"x":20,"y":10,"type":"metal"},{"x":53,"y":10,"type":"metal"},{"x":56,"y":10,"type":"metal"},{"x":59,"y":10,"type":"metal"},{"x":47,"y":4,"type":"metal"},{"x":50,"y":4,"type":"metal"},{"x":53,"y":4,"type":"metal"},{"x":56,"y":4,"type":"metal"},{"x":59,"y":4,"type":"metal"},{"x":26,"y":4,"type":"metal"},{"x":29,"y":4,"type":"metal"},{"x":32,"y":4,"type":"brick"},{"x":35,"y":4,"type":"brick"},{"x":38,"y":4,"type":"brick"},{"x":41,"y":4,"type":"brick"},{"x":44,"y":4,"type":"brick"},{"x":11,"y":4,"type":"metal"},{"x":8,"y":4,"type":"metal"},{"x":62,"y":4,"type":"metal"},{"x":65,"y":4,"type":"metal"},{"x":68,"y":4,"type":"metal"},{"x":71,"y":4,"type":"metal"},{"x":5,"y":4,"type":"metal"}],"hazards":[],"shooters":[],"lemons":7,"quotas":{"basher":7,"blocker":7,"bomber":7,"builder":7,"climber":7,"digger":7},"entrance":{"x":23,"y":1},"exit":{"x":59,"y":19}},{"name":"Basher Bash","time":180,"author":"echicken","system":"electronic chicken bbs","date":1426572644,"blocks":[{"x":1,"y":1,"type":"metal"},{"x":1,"y":2,"type":"metal"},{"x":1,"y":3,"type":"metal"},{"x":1,"y":4,"type":"metal"},{"x":22,"y":22,"type":"metal"},{"x":34,"y":21,"type":"metal"},{"x":34,"y":22,"type":"metal"},{"x":37,"y":21,"type":"metal"},{"x":37,"y":20,"type":"metal"},{"x":40,"y":20,"type":"metal"},{"x":40,"y":19,"type":"metal"},{"x":43,"y":19,"type":"metal"},{"x":43,"y":18,"type":"metal"},{"x":46,"y":18,"type":"metal"},{"x":46,"y":17,"type":"metal"},{"x":49,"y":17,"type":"metal"},{"x":52,"y":17,"type":"metal"},{"x":55,"y":17,"type":"metal"},{"x":58,"y":17,"type":"metal"},{"x":61,"y":17,"type":"metal"},{"x":64,"y":17,"type":"metal"},{"x":67,"y":17,"type":"metal"},{"x":70,"y":17,"type":"metal"},{"x":73,"y":17,"type":"metal"},{"x":76,"y":17,"type":"metal"},{"x":4,"y":4,"type":"metal"},{"x":7,"y":4,"type":"metal"},{"x":7,"y":5,"type":"metal"},{"x":10,"y":5,"type":"metal"},{"x":10,"y":6,"type":"metal"},{"x":13,"y":6,"type":"metal"},{"x":19,"y":6,"type":"metal"},{"x":19,"y":7,"type":"metal"},{"x":19,"y":8,"type":"metal"},{"x":19,"y":9,"type":"metal"},{"x":19,"y":10,"type":"metal"},{"x":16,"y":10,"type":"metal"},{"x":16,"y":11,"type":"metal"},{"x":13,"y":11,"type":"metal"},{"x":13,"y":12,"type":"metal"},{"x":10,"y":12,"type":"metal"},{"x":4,"y":12,"type":"metal"},{"x":4,"y":13,"type":"metal"},{"x":4,"y":14,"type":"metal"},{"x":4,"y":15,"type":"metal"},{"x":4,"y":16,"type":"metal"},{"x":7,"y":16,"type":"metal"},{"x":7,"y":17,"type":"metal"},{"x":10,"y":17,"type":"metal"},{"x":10,"y":18,"type":"metal"},{"x":13,"y":18,"type":"metal"},{"x":13,"y":19,"type":"metal"},{"x":16,"y":19,"type":"metal"},{"x":16,"y":20,"type":"metal"},{"x":19,"y":20,"type":"metal"},{"x":19,"y":21,"type":"metal"},{"x":22,"y":21,"type":"metal"},{"x":19,"y":5,"type":"metal"},{"x":4,"y":11,"type":"metal"},{"x":4,"y":10,"type":"metal"},{"x":4,"y":9,"type":"metal"},{"x":4,"y":8,"type":"metal"},{"x":4,"y":7,"type":"metal"},{"x":4,"y":6,"type":"metal"},{"x":4,"y":5,"type":"metal"},{"x":19,"y":4,"type":"metal"},{"x":61,"y":16,"type":"brick"},{"x":61,"y":15,"type":"brick"},{"x":61,"y":14,"type":"brick"},{"x":61,"y":13,"type":"brick"},{"x":61,"y":12,"type":"brick"},{"x":61,"y":11,"type":"brick"},{"x":61,"y":10,"type":"brick"},{"x":61,"y":9,"type":"brick"},{"x":61,"y":8,"type":"brick"},{"x":61,"y":7,"type":"brick"},{"x":61,"y":6,"type":"brick"},{"x":61,"y":5,"type":"brick"},{"x":61,"y":4,"type":"brick"},{"x":64,"y":16,"type":"brick"},{"x":64,"y":15,"type":"brick"},{"x":64,"y":14,"type":"brick"},{"x":64,"y":13,"type":"brick"},{"x":64,"y":12,"type":"brick"},{"x":64,"y":11,"type":"brick"},{"x":64,"y":10,"type":"brick"},{"x":64,"y":9,"type":"brick"},{"x":64,"y":8,"type":"brick"},{"x":64,"y":7,"type":"brick"},{"x":64,"y":6,"type":"brick"},{"x":64,"y":5,"type":"brick"},{"x":64,"y":4,"type":"brick"},{"x":25,"y":22,"type":"brick"},{"x":28,"y":22,"type":"brick"},{"x":31,"y":22,"type":"brick"}],"hazards":[],"shooters":[],"lemons":5,"quotas":{"basher":5,"blocker":2,"bomber":5,"builder":2,"climber":10,"digger":5},"entrance":{"x":4,"y":1},"exit":{"x":76,"y":14}},{"name":"Digger Diversion","time":180,"author":"echicken","system":"electronic chicken bbs","date":1426653696,"blocks":[{"x":39,"y":4,"type":"brick"},{"x":42,"y":4,"type":"brick"},{"x":45,"y":4,"type":"brick"},{"x":48,"y":4,"type":"brick"},{"x":51,"y":4,"type":"brick"},{"x":54,"y":4,"type":"brick"},{"x":57,"y":4,"type":"brick"},{"x":60,"y":4,"type":"brick"},{"x":63,"y":4,"type":"brick"},{"x":66,"y":4,"type":"brick"},{"x":78,"y":4,"type":"metal"},{"x":78,"y":5,"type":"metal"},{"x":69,"y":4,"type":"metal"},{"x":69,"y":5,"type":"metal"},{"x":30,"y":4,"type":"brick"},{"x":27,"y":4,"type":"brick"},{"x":24,"y":4,"type":"brick"},{"x":21,"y":4,"type":"brick"},{"x":18,"y":4,"type":"brick"},{"x":18,"y":1,"type":"brick"},{"x":18,"y":2,"type":"brick"},{"x":18,"y":3,"type":"brick"},{"x":1,"y":10,"type":"brick"},{"x":4,"y":11,"type":"brick"},{"x":7,"y":12,"type":"brick"},{"x":10,"y":13,"type":"brick"},{"x":1,"y":9,"type":"brick"},{"x":1,"y":8,"type":"brick"},{"x":13,"y":14,"type":"brick"},{"x":16,"y":15,"type":"brick"},{"x":22,"y":16,"type":"brick"},{"x":25,"y":16,"type":"brick"},{"x":28,"y":16,"type":"brick"},{"x":31,"y":16,"type":"brick"},{"x":67,"y":12,"type":"brick"},{"x":70,"y":12,"type":"brick"},{"x":75,"y":17,"type":"brick"},{"x":72,"y":18,"type":"brick"},{"x":69,"y":19,"type":"brick"},{"x":66,"y":20,"type":"brick"},{"x":63,"y":21,"type":"brick"},{"x":60,"y":22,"type":"brick"},{"x":57,"y":22,"type":"brick"},{"x":54,"y":22,"type":"brick"},{"x":51,"y":22,"type":"brick"},{"x":48,"y":22,"type":"brick"},{"x":45,"y":22,"type":"brick"},{"x":42,"y":22,"type":"brick"},{"x":39,"y":22,"type":"brick"},{"x":36,"y":22,"type":"brick"},{"x":33,"y":22,"type":"brick"},{"x":30,"y":22,"type":"brick"},{"x":27,"y":22,"type":"brick"},{"x":24,"y":22,"type":"brick"},{"x":21,"y":22,"type":"brick"},{"x":18,"y":22,"type":"brick"},{"x":15,"y":22,"type":"brick"},{"x":12,"y":22,"type":"brick"},{"x":9,"y":22,"type":"brick"},{"x":6,"y":22,"type":"brick"},{"x":3,"y":22,"type":"brick"},{"x":3,"y":18,"type":"brick"},{"x":6,"y":18,"type":"brick"},{"x":6,"y":19,"type":"brick"},{"x":6,"y":20,"type":"brick"},{"x":6,"y":21,"type":"brick"},{"x":34,"y":16,"type":"metal"},{"x":37,"y":16,"type":"metal"},{"x":33,"y":4,"type":"metal"},{"x":36,"y":4,"type":"metal"},{"x":40,"y":16,"type":"metal"},{"x":43,"y":16,"type":"metal"},{"x":46,"y":16,"type":"metal"},{"x":49,"y":16,"type":"metal"},{"x":52,"y":16,"type":"metal"},{"x":55,"y":15,"type":"metal"},{"x":58,"y":14,"type":"metal"},{"x":61,"y":13,"type":"metal"},{"x":64,"y":12,"type":"metal"},{"x":73,"y":12,"type":"brick"},{"x":76,"y":12,"type":"brick"},{"x":76,"y":11,"type":"brick"},{"x":76,"y":10,"type":"brick"},{"x":76,"y":9,"type":"brick"},{"x":76,"y":8,"type":"brick"},{"x":76,"y":7,"type":"brick"},{"x":76,"y":6,"type":"brick"},{"x":75,"y":13,"type":"brick"},{"x":75,"y":14,"type":"brick"},{"x":75,"y":15,"type":"brick"},{"x":75,"y":16,"type":"brick"},{"x":1,"y":11,"type":"brick"},{"x":4,"y":12,"type":"brick"},{"x":7,"y":13,"type":"brick"},{"x":10,"y":14,"type":"brick"},{"x":13,"y":15,"type":"brick"},{"x":22,"y":15,"type":"brick"},{"x":25,"y":15,"type":"brick"},{"x":28,"y":15,"type":"brick"},{"x":31,"y":15,"type":"brick"},{"x":34,"y":5,"type":"metal"},{"x":34,"y":6,"type":"metal"},{"x":34,"y":7,"type":"metal"},{"x":34,"y":8,"type":"metal"},{"x":34,"y":9,"type":"metal"},{"x":34,"y":10,"type":"metal"},{"x":34,"y":11,"type":"metal"},{"x":34,"y":12,"type":"metal"},{"x":34,"y":13,"type":"metal"},{"x":34,"y":14,"type":"metal"},{"x":34,"y":15,"type":"metal"},{"x":31,"y":15,"type":"metal"},{"x":16,"y":16,"type":"metal"},{"x":19,"y":15,"type":"metal"},{"x":19,"y":16,"type":"metal"},{"x":15,"y":5,"type":"brick"},{"x":12,"y":6,"type":"brick"},{"x":9,"y":7,"type":"brick"}],"hazards":[{"x":72,"y":4,"type":"water"}],"shooters":[],"lemons":5,"quotas":{"basher":5,"blocker":5,"bomber":5,"builder":2,"climber":2,"digger":10},"entrance":{"x":39,"y":1},"exit":{"x":3,"y":19}},{"name":"Climber Carousal","time":180,"author":"echicken","system":"electronic chicken bbs","date":1426655291,"blocks":[{"x":1,"y":22,"type":"brick"},{"x":1,"y":21,"type":"brick"},{"x":1,"y":20,"type":"brick"},{"x":1,"y":19,"type":"brick"},{"x":1,"y":18,"type":"brick"},{"x":1,"y":17,"type":"brick"},{"x":1,"y":16,"type":"brick"},{"x":1,"y":15,"type":"brick"},{"x":1,"y":14,"type":"brick"},{"x":1,"y":13,"type":"brick"},{"x":4,"y":22,"type":"brick"},{"x":7,"y":22,"type":"brick"},{"x":10,"y":22,"type":"brick"},{"x":13,"y":22,"type":"brick"},{"x":16,"y":22,"type":"brick"},{"x":19,"y":22,"type":"brick"},{"x":22,"y":22,"type":"brick"},{"x":25,"y":22,"type":"brick"},{"x":28,"y":22,"type":"brick"},{"x":31,"y":22,"type":"brick"},{"x":34,"y":22,"type":"brick"},{"x":37,"y":22,"type":"brick"},{"x":40,"y":22,"type":"brick"},{"x":43,"y":22,"type":"brick"},{"x":46,"y":22,"type":"brick"},{"x":46,"y":21,"type":"brick"},{"x":46,"y":20,"type":"brick"},{"x":46,"y":19,"type":"brick"},{"x":46,"y":18,"type":"brick"},{"x":46,"y":17,"type":"brick"},{"x":46,"y":16,"type":"brick"},{"x":46,"y":15,"type":"brick"},{"x":46,"y":14,"type":"brick"},{"x":46,"y":13,"type":"brick"},{"x":46,"y":12,"type":"brick"},{"x":46,"y":11,"type":"brick"},{"x":49,"y":15,"type":"brick"},{"x":52,"y":15,"type":"brick"},{"x":55,"y":15,"type":"brick"},{"x":58,"y":15,"type":"brick"},{"x":61,"y":15,"type":"brick"},{"x":64,"y":15,"type":"brick"},{"x":67,"y":15,"type":"brick"},{"x":70,"y":15,"type":"brick"},{"x":73,"y":15,"type":"brick"},{"x":73,"y":14,"type":"brick"},{"x":73,"y":13,"type":"brick"},{"x":73,"y":12,"type":"brick"},{"x":73,"y":11,"type":"brick"},{"x":73,"y":10,"type":"brick"},{"x":73,"y":9,"type":"brick"},{"x":73,"y":8,"type":"brick"},{"x":73,"y":7,"type":"brick"},{"x":73,"y":6,"type":"brick"},{"x":73,"y":5,"type":"brick"},{"x":73,"y":4,"type":"brick"},{"x":76,"y":4,"type":"brick"},{"x":46,"y":10,"type":"brick"},{"x":46,"y":9,"type":"brick"},{"x":46,"y":8,"type":"brick"},{"x":10,"y":21,"type":"brick"},{"x":13,"y":21,"type":"brick"},{"x":16,"y":21,"type":"brick"},{"x":19,"y":21,"type":"brick"},{"x":13,"y":20,"type":"brick"},{"x":16,"y":20,"type":"brick"},{"x":19,"y":20,"type":"brick"},{"x":22,"y":21,"type":"brick"},{"x":16,"y":19,"type":"brick"}],"hazards":[],"shooters":[],"lemons":5,"quotas":{"basher":10,"blocker":5,"bomber":10,"builder":5,"climber":20,"digger":5},"entrance":{"x":1,"y":10},"exit":{"x":76,"y":1}},{"name":"Bomber Blast","time":180,"author":"echicken","system":"electronic chicken bbs","date":1426656508,"blocks":[{"x":1,"y":22,"type":"metal"},{"x":4,"y":22,"type":"metal"},{"x":7,"y":22,"type":"metal"},{"x":10,"y":22,"type":"metal"},{"x":13,"y":22,"type":"metal"},{"x":16,"y":22,"type":"metal"},{"x":19,"y":22,"type":"metal"},{"x":22,"y":22,"type":"metal"},{"x":25,"y":22,"type":"metal"},{"x":28,"y":22,"type":"metal"},{"x":31,"y":22,"type":"metal"},{"x":34,"y":22,"type":"metal"},{"x":37,"y":22,"type":"metal"},{"x":40,"y":22,"type":"metal"},{"x":43,"y":22,"type":"metal"},{"x":46,"y":22,"type":"metal"},{"x":49,"y":22,"type":"metal"},{"x":52,"y":22,"type":"metal"},{"x":55,"y":22,"type":"metal"},{"x":58,"y":22,"type":"metal"},{"x":61,"y":22,"type":"metal"},{"x":64,"y":22,"type":"metal"},{"x":67,"y":22,"type":"metal"},{"x":70,"y":22,"type":"metal"},{"x":73,"y":22,"type":"metal"},{"x":76,"y":22,"type":"metal"},{"x":76,"y":21,"type":"metal"},{"x":76,"y":20,"type":"metal"},{"x":76,"y":19,"type":"metal"},{"x":76,"y":18,"type":"metal"},{"x":76,"y":17,"type":"metal"},{"x":1,"y":21,"type":"metal"},{"x":1,"y":20,"type":"metal"},{"x":1,"y":19,"type":"metal"},{"x":1,"y":18,"type":"metal"},{"x":1,"y":17,"type":"metal"},{"x":4,"y":21,"type":"metal"},{"x":7,"y":21,"type":"metal"},{"x":10,"y":21,"type":"metal"},{"x":13,"y":21,"type":"metal"},{"x":16,"y":21,"type":"metal"},{"x":19,"y":21,"type":"metal"},{"x":22,"y":21,"type":"metal"},{"x":25,"y":21,"type":"metal"},{"x":28,"y":21,"type":"metal"},{"x":31,"y":21,"type":"metal"},{"x":34,"y":21,"type":"metal"},{"x":37,"y":21,"type":"metal"},{"x":40,"y":21,"type":"metal"},{"x":43,"y":21,"type":"metal"},{"x":46,"y":21,"type":"metal"},{"x":49,"y":21,"type":"metal"},{"x":52,"y":21,"type":"metal"},{"x":55,"y":21,"type":"metal"},{"x":58,"y":21,"type":"metal"},{"x":61,"y":21,"type":"metal"},{"x":64,"y":21,"type":"metal"},{"x":67,"y":21,"type":"metal"},{"x":70,"y":21,"type":"metal"},{"x":73,"y":21,"type":"metal"},{"x":4,"y":20,"type":"metal"},{"x":7,"y":20,"type":"metal"},{"x":49,"y":20,"type":"metal"},{"x":52,"y":20,"type":"metal"},{"x":55,"y":20,"type":"metal"},{"x":70,"y":20,"type":"metal"},{"x":73,"y":20,"type":"metal"},{"x":55,"y":19,"type":"metal"},{"x":55,"y":18,"type":"metal"},{"x":55,"y":17,"type":"metal"},{"x":55,"y":16,"type":"metal"},{"x":55,"y":15,"type":"metal"},{"x":55,"y":14,"type":"metal"},{"x":46,"y":20,"type":"metal"},{"x":43,"y":20,"type":"metal"},{"x":40,"y":20,"type":"metal"},{"x":43,"y":19,"type":"metal"},{"x":46,"y":19,"type":"metal"},{"x":49,"y":19,"type":"metal"},{"x":52,"y":19,"type":"metal"},{"x":37,"y":20,"type":"metal"},{"x":55,"y":13,"type":"metal"},{"x":55,"y":12,"type":"metal"},{"x":52,"y":12,"type":"metal"},{"x":49,"y":12,"type":"metal"},{"x":34,"y":20,"type":"metal"},{"x":31,"y":20,"type":"metal"},{"x":37,"y":19,"type":"metal"},{"x":40,"y":19,"type":"metal"},{"x":43,"y":18,"type":"metal"},{"x":46,"y":18,"type":"metal"},{"x":49,"y":18,"type":"metal"},{"x":52,"y":18,"type":"metal"},{"x":46,"y":17,"type":"metal"},{"x":49,"y":17,"type":"metal"},{"x":52,"y":17,"type":"metal"},{"x":40,"y":18,"type":"metal"},{"x":34,"y":19,"type":"metal"},{"x":28,"y":20,"type":"metal"}],"hazards":[],"shooters":[],"lemons":7,"quotas":{"basher":7,"blocker":7,"bomber":7,"builder":7,"climber":7,"digger":7},"entrance":{"x":4,"y":17},"exit":{"x":73,"y":17}},{"name":"Builder Blowout","time":180,"author":"echicken","system":"electronic chicken bbs","date":1426657279,"blocks":[{"x":1,"y":4,"type":"brick"},{"x":4,"y":5,"type":"brick"},{"x":7,"y":6,"type":"brick"},{"x":10,"y":7,"type":"brick"},{"x":13,"y":8,"type":"brick"},{"x":13,"y":9,"type":"brick"},{"x":13,"y":10,"type":"brick"},{"x":13,"y":11,"type":"brick"},{"x":13,"y":12,"type":"brick"},{"x":13,"y":13,"type":"brick"},{"x":13,"y":14,"type":"brick"},{"x":13,"y":15,"type":"brick"},{"x":13,"y":16,"type":"brick"},{"x":13,"y":17,"type":"brick"},{"x":13,"y":18,"type":"brick"},{"x":13,"y":19,"type":"brick"},{"x":13,"y":20,"type":"brick"},{"x":13,"y":21,"type":"brick"},{"x":13,"y":22,"type":"brick"},{"x":16,"y":22,"type":"brick"},{"x":19,"y":22,"type":"brick"},{"x":22,"y":22,"type":"brick"},{"x":25,"y":22,"type":"brick"},{"x":28,"y":22,"type":"brick"},{"x":31,"y":22,"type":"brick"},{"x":34,"y":22,"type":"brick"},{"x":37,"y":22,"type":"brick"},{"x":40,"y":22,"type":"brick"},{"x":43,"y":22,"type":"brick"},{"x":46,"y":22,"type":"brick"},{"x":49,"y":22,"type":"brick"},{"x":52,"y":22,"type":"brick"},{"x":55,"y":22,"type":"brick"},{"x":58,"y":22,"type":"brick"},{"x":61,"y":22,"type":"brick"},{"x":64,"y":22,"type":"brick"},{"x":64,"y":21,"type":"brick"},{"x":64,"y":20,"type":"brick"},{"x":67,"y":20,"type":"brick"},{"x":70,"y":20,"type":"brick"},{"x":73,"y":20,"type":"brick"},{"x":76,"y":20,"type":"brick"},{"x":43,"y":21,"type":"brick"},{"x":43,"y":20,"type":"brick"}],"hazards":[],"shooters":[],"lemons":3,"quotas":{"basher":1,"blocker":6,"bomber":1,"builder":9,"climber":1,"digger":1},"entrance":{"x":1,"y":1},"exit":{"x":76,"y":17}}]
\ No newline at end of file
diff --git a/xtrn/lemons/menu.js b/xtrn/lemons/menu.js
new file mode 100644
index 0000000000000000000000000000000000000000..54b16087aabe563c3adf8f142f9a807156045e0c
--- /dev/null
+++ b/xtrn/lemons/menu.js
@@ -0,0 +1,65 @@
+// The Menu object displays the Lemons splash screen and a small Tree menu.
+var Menu = function() {
+
+	var frames = {};
+
+	frames.splash = new Frame(
+		frame.x,
+		frame.y,
+		frame.width,
+		frame.height,
+		BG_BLACK|WHITE,
+		frame
+	);
+
+	frames.treeFrame = new Frame(
+		frames.splash.x + 50,
+		frames.splash.y + 10,
+		20,
+		6,
+		BG_BLACK|WHITE,
+		frames.splash
+	);
+
+	frames.treeSubFrame = new Frame(
+		frames.treeFrame.x + 1,
+		frames.treeFrame.y + 1,
+		frames.treeFrame.width - 2,
+		frames.treeFrame.height - 2,
+		BG_BLACK|WHITE,
+		frames.treeFrame
+	);
+
+	frames.splash.open();
+	frames.splash.load(js.exec_dir + "lemons.bin", 80, 24)
+	frames.splash.top();
+	frames.treeFrame.drawBorder([CYAN, LIGHTCYAN, WHITE]);
+	frames.treeFrame.open();
+
+	var changeState = function(s) {
+		state = s;
+	}
+
+	var tree = new Tree(frames.treeSubFrame);
+	tree.colors.fg = LIGHTGRAY;
+	tree.colors.bg = BG_BLACK;
+	tree.colors.lfg = WHITE;
+	tree.colors.lbg = BG_BLUE;
+	tree.colors.kfg = LIGHTCYAN;
+	tree.addItem("|Play", changeState, STATE_PLAY);
+	tree.addItem("|Help", changeState, STATE_HELP);
+	tree.addItem("|Scores", changeState, STATE_SCORES);
+	tree.addItem("|Quit", changeState, STATE_EXIT);
+	tree.open();
+
+	this.getcmd = function(userInput) {
+		tree.getcmd(userInput);
+	}
+
+	this.close = function() {
+		tree.close();
+		frames.treeFrame.delete();
+		frames.splash.delete();
+	}
+
+}
\ No newline at end of file
diff --git a/xtrn/lemons/readme.txt b/xtrn/lemons/readme.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3833b69d0d64227f71cc5723dcf7ab66fd55ccda
--- /dev/null
+++ b/xtrn/lemons/readme.txt
@@ -0,0 +1,124 @@
+Lemons
+------
+A game for Synchronet BBS 3.16+
+by echicken -at- bbs.electronicchicken.com, March 2015
+
+
+Contents
+--------
+
+1) About
+2) Requirements
+3) Installation
+	3.1) Connect to the networked scoreboard
+	3.2) Host your own scoreboard
+4) Support
+
+
+1) About
+--------
+
+Sometimes this is how games are created:
+
+<MegaloYeti> someone needs to make a new lemmings game
+<mcmlxxix> bbs lemmings?
+<mcmlxxix> hmmm
+<echicken> mhm
+<echicken> little @ signs walking off of cliffs
+<echicken> or wee lemming sprites
+<echicken> actually that would be fairly easy
+<echicken> claimed
+<mcmlxxix> :|
+
+Okay, so it's "Lemons" and it's pared down somewhat, but the basic premise is
+the same.  Just try it, read the help screen, and you'll get the idea.
+
+
+2) Requirements
+---------------
+
+This game may run on Synchronet 3.15, but has not been tested with anything
+less than 3.16.
+
+Ensure that you have the latest copies of the following files in your exec/load/
+directory.  You can grab them one by one, or do a CVS update.  (If you choose
+to update everything, backing up your BBS is a good idea.)
+
+- frame.js:
+	http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/exec/load/frame.js
+- tree.js:
+	http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/exec/load/tree.js
+- event-timer.js:
+	http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/exec/load/event-timer.js
+- json-client.js:
+	http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/exec/load/json-client.js
+- sprite.js:
+	http://cvs.synchro.net/cgi-bin/viewcvs.cgi/*checkout*/exec/load/sprite.js
+
+
+3) Installation
+---------------
+
+In 'scfg' (that's BBS->Configure from the Synchronet Control Panel in Windows),
+go to "External Programs -> Online Programs (Doors)" and select the area you
+wish to add this game to.  Create a new entry, and set it up as follows:
+
+Name: Lemons
+Internal Code: LEMONS
+Start-up Directory: ../xtrn/lemons/
+Command Line: ?lemons.js
+Multiple Concurrent Users: Yes
+
+(All other options can be left at their default values.)
+
+	3.1) Connect to the networked scoreboard
+	----------------------------------------
+
+	You're welcome to connect your local game to the networked scoreboard that
+	I host.  To do so, Create a file called 'server.ini' in the Lemons game
+	directory, and put the following two lines in it:
+
+		host = bbs.electronicchicken.com
+		port = 10088
+
+
+	3.2) Host your own scoreboard
+	-----------------------------
+
+	If you prefer not to connect to my scoreboard, you will need to set up your
+	own or the game will not work.  To do so, ensure that the JSON service is
+	enabled via the following entry in your 'ctrl/services.ini'	file:
+
+		[JSON]
+		Port=10088
+		Options=STATIC | LOOP
+		Command=json-service.js
+
+	If you've just added the above to your services.ini file, you may need to
+	restart your services (just restart your BBS if you don't know how) in
+	order for the change to take effect.
+
+	You will also need to add the following to your 'ctrl/json-service.ini'
+	file (create one if it doesn't already exist):
+
+	[lemons]
+	dir=../xtrn/lemons/
+
+	In the absence of a 'server.ini' file in its directory, Lemons will attempt
+	to connect to a JSON-DB server at 127.0.0.1 on port 10088. If your JSON
+	server binds to another port or address, create a 'server.ini' file with
+	the appropriate values.
+
+	You can allow other systems to connect to your scoreboard by opening your
+	JSON-DB server's port to them.  They will need to create or modify their
+	'server.ini' files accordingly.
+
+
+4) Support
+----------
+
+There are three easy ways to reach me:
+
+- Post a message to echicken in the Synchronet Sysops sub on DOVE-Net
+- Find me in #synchronet on irc.synchro.net
+- Contact me on my BBS at bbs.electronicchicken.com
\ No newline at end of file
diff --git a/xtrn/lemons/scoreboard.js b/xtrn/lemons/scoreboard.js
new file mode 100644
index 0000000000000000000000000000000000000000..b025f4f3692f5d525364082b059a61975215a522
--- /dev/null
+++ b/xtrn/lemons/scoreboard.js
@@ -0,0 +1,127 @@
+// An interface to the Lemons JSON DB.  Slightly more than a scoreboard.
+var ScoreBoard = function(host, port) {
+
+	var frames = {};
+	var jsonClient = new JSONClient(host, port);
+
+	/*	Add a score to the database.  Impress nobody by faking an awesome
+		score in Lemons! */
+	this.addScore = function(score, level) {
+
+		if(!jsonClient.connected)
+			jsonClient.connect(host, port);
+
+		var entry = {
+			'player' : user.alias,
+			'system' : system.name,
+			'level' : level,
+			'score' : score
+		};
+
+		jsonClient.write(DBNAME, "SCORES.LATEST", entry, 2);
+
+	}
+
+	// Returns the high scores array (20 most recent scores.)
+	this.getScores = function() {
+
+		if(!jsonClient.connected)
+			jsonClient.connect(host, port);
+
+		var scores = jsonClient.read(DBNAME, "SCORES.HIGH", 1);
+
+		if(!scores)
+			scores = [];
+
+		return scores;
+
+	}
+
+	/*	Returns the number of the highest level the current user has reached.
+		(Numbering starts at zero; returns zero if this is a new player or a
+		an empty database.)	*/
+	this.getHighestLevel = function() {
+
+		if(!jsonClient.connected)
+			jsonClient.connect(host, port);
+
+		var player = jsonClient.read(
+			DBNAME,
+			"PLAYERS." + base64_encode(user.alias + "@" + system.name),
+			1
+		);
+
+		if(!player)
+			return 0;
+
+		return player.LEVEL;
+
+	}
+
+	// Display the scoreboard.
+	this.open = function() {
+
+		if(!jsonClient.connected)
+			jsonClient.connect(host, port);
+
+		frames.scoreFrame = new Frame(
+			frame.x,
+			frame.y,
+			frame.width,
+			frame.height,
+			BG_BLACK|WHITE,
+			frame
+		);
+
+		frames.scoreSubFrame = new Frame(
+			frames.scoreFrame.x + 1,
+			frames.scoreFrame.y + 1,
+			frames.scoreFrame.width - 2,
+			frames.scoreFrame.height -2,
+			BG_BLACK|WHITE,
+			frames.scoreFrame
+		);
+
+		var putLine = function(player, system, level, score) {
+			frames.scoreSubFrame.putmsg(
+				format(
+					"%-25s %-30s %5s %14s\r\n",
+					player, system, level, score
+				)
+			);
+		}
+
+		frames.scoreFrame.drawBorder([CYAN, LIGHTCYAN, WHITE]);
+		frames.scoreFrame.home();
+		frames.scoreFrame.center(ascii(180) + "Lemons - High Scores" + ascii(195));
+
+		frames.scoreSubFrame.gotoxy(1, 2);
+		frames.scoreSubFrame.attr = BG_BLACK|LIGHTCYAN;
+		putLine("Player", "System", "Level", "Score");
+		frames.scoreSubFrame.attr = BG_BLACK|WHITE;
+
+		var scores = this.getScores();
+		for(var s = 0; s < scores.length; s++) {
+			putLine(
+				scores[s].player,
+				scores[s].system,
+				(scores[s].level + 1),
+				scores[s].score
+			);
+		}
+
+		frames.scoreFrame.end();
+		frames.scoreFrame.center(ascii(180) + "Press any key to continue" + ascii(195));
+
+		frames.scoreFrame.open();
+
+	}
+
+	// Close the scoreboard if it's open, disconnect the JSON-DB client.
+	this.close = function() {
+		if(typeof frames.scoreFrame != "undefined")
+			frames.scoreFrame.delete();
+		jsonClient.disconnect();
+	}
+
+}
\ No newline at end of file
diff --git a/xtrn/lemons/service.js b/xtrn/lemons/service.js
new file mode 100644
index 0000000000000000000000000000000000000000..7fdf047c82f8d9daa4dc95c419ce5ba47babdef9
--- /dev/null
+++ b/xtrn/lemons/service.js
@@ -0,0 +1,213 @@
+/*	Lemons JSON DB module service script
+	This is loaded automatically by the JSON service, and runs in the
+	background. */
+
+// Loop forever
+js.branch_limit = 0;
+js.time_limit = 0;
+
+// The JSON service passes the module's working directory as argv[0]
+var root = argv[0];
+
+load("sbbsdefs.js");
+load("json-client.js");
+load(root + "defs.js");
+
+// We'll need a JSON client handle in various functions within this script
+var jsonClient;
+
+// This will be called when we receive an update to a valid location
+var updateScores = function(data) {
+	
+	// Make sure that the data provided is sane
+	if(	typeof data.player != "string"
+		||
+		typeof data.system != "string"
+		||
+		typeof data.level != "number"
+		||
+		typeof data.score != "number"
+	) {
+		return;
+	}
+	
+	// Make a unique ID / valid property name for the player
+	data.uid = base64_encode(data.player + "@" + data.system, true);
+	data.date = time();
+	
+	// Grab the current high scores list
+	var hs = jsonClient.read(DBNAME, "SCORES.HIGH", 1);
+	var changed = false; // We'll toggle this if we need to overwrite the list
+
+	/*	If this player's score is higher than any score in the list, shove
+		it into the list before that score. */
+	for(var s = 0; s < hs.length; s++) {
+		if(data.score < hs[s].score)
+			continue;
+		hs.splice(s, 0, data);
+		changed = true;
+		break;
+	}
+
+	/*	If this player's score is the lowest in the list, but the list isn't
+		fully populated, tack this record onto the end of the list. */
+	if(!changed && hs.length < 20) {
+		hs.push(data);
+		changed = true;
+	}
+
+	//	If we flagged a DB update, then update the DB.
+	if(changed) {
+		while(hs.length > 20)
+			hs.pop();
+		jsonClient.write(DBNAME, "SCORES.HIGH", hs, 2);
+	}
+
+	// In any event, we want to update this player's record
+	updatePlayer(data);
+
+}
+
+// This will be called via updateScores
+var updatePlayer = function(data) {
+
+	// Read the player's record from the DB
+	var player = jsonClient.read(DBNAME, "PLAYERS." + data.uid, 1);
+
+	// Or populate said record if it doesn't exist
+	if(!player) {
+		jsonClient.write(
+			DBNAME,
+			"PLAYERS." + data.uid,
+			{ 'LEVEL' : data.level, 'SCORES' : [] },
+			2
+		);
+	/*	If the player has reached a new level, update their level number so
+		they can start there next time. */
+	} else if(player.LEVEL < data.level) {
+		jsonClient.write(
+			DBNAME,
+			"PLAYERS." + data.uid + ".LEVEL",
+			data.level,
+			2
+		);
+	}
+
+	/*	Tack this score onto the player's 'scores' array.  We're not doing
+		anything with this data at the moment, but we could. */
+	jsonClient.push(
+		DBNAME,
+		"PLAYERS." + data.uid + ".SCORES",
+		{ 'level' : data.level, 'score' : data.score, 'date' : data.date },
+		2
+	);
+
+}
+
+/*	We'll set this as the JSON client's callback function, and it will be
+	called when an update to a subscribed location is received. */
+var processUpdate = function(update) {
+
+	// We're not interested in any updates that aren't writes
+	if(update.oper != "WRITE")
+		return;
+
+	// Additionally, the update must be to a *.LATEST location
+	var location = update.location.split(".");
+	if(location.length != "2" || location[1] != "LATEST")
+		return;
+
+	switch(location[0].toUpperCase()) {
+
+		// We're only subscribing to SCORES.LATEST right now
+		case "SCORES":
+			updateScores(update.data);
+			break;
+		
+		// But maybe we'll want to watch this in the future
+		case "PLAYERS":
+			updatePlayer(update.data);
+			break;
+		
+		default:
+			break;
+
+	}
+
+}
+
+// Set things up
+var init = function() {
+
+	// Load the server config if it exists, or fake it if not
+	if(file_exists(root + "server.ini")) {
+		var f = new File(root + "server.ini");
+		f.open("r");
+		var ini = f.iniGetObject();
+		f.close();
+		ini.port = parseInt(ini.port);
+	} else {
+		var ini = { 'host' : "127.0.0.1", 'port' : 10088 };
+	}
+
+	// Create our JSON client handle (declared outside this function)
+	jsonClient = new JSONClient(ini.host, ini.port);
+
+	// Authenticate to the service as this board's sysop
+	var usr = new User(1);
+	jsonClient.ident("ADMIN", usr.alias, usr.security.password);
+
+	// Subscribe to updates to SCORES.LATEST
+	jsonClient.subscribe(DBNAME, "SCORES.LATEST");
+
+	// If SCORES doesn't exist, create it with dummy data
+	if(!jsonClient.read(DBNAME, "SCORES", 1)) {
+		jsonClient.write(
+			DBNAME,
+			"SCORES",
+			{ 'LATEST' : {}, 'HIGH' : [] },
+			2
+		);
+	}
+
+	// If PLAYERS doesn't exist, create it with dummy data
+	if(!jsonClient.read(DBNAME, "PLAYERS", 1)) {
+		jsonClient.write(
+			DBNAME,
+			"PLAYERS",
+			{ 'LATEST' : {} },
+			2
+		);
+	}
+
+	// Call processUpdate when an update is received
+	jsonClient.callback = processUpdate;
+
+}
+
+// Keep things rolling until we're told to stop
+var main = function() {
+
+	while(!js.terminated) {
+		mswait(5);
+		jsonClient.cycle();
+	}
+
+}
+
+// Clean things up
+var cleanUp = function() {
+	jsonClient.disconnect();
+}
+
+// Try to do things, log an error if necessary
+try {
+	init();
+	main();
+	cleanUp();
+} catch(err) {
+	log(LOG_ERR, err);
+}
+
+// This is implied, but hey, why not?
+exit();
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/brick.bin b/xtrn/lemons/sprites/brick.bin
new file mode 100644
index 0000000000000000000000000000000000000000..1b8ddc7e9997ac014ddbc63f3c859f9258fb6875
--- /dev/null
+++ b/xtrn/lemons/sprites/brick.bin
@@ -0,0 +1 @@
+�d�d�
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/brick.ini b/xtrn/lemons/sprites/brick.ini
new file mode 100644
index 0000000000000000000000000000000000000000..d1cc7957ec46e7c5cd81fc9aa9e23e31abdc7ff9
--- /dev/null
+++ b/xtrn/lemons/sprites/brick.ini
@@ -0,0 +1,9 @@
+width = 3
+height = 1
+bearings = e
+positions = normal
+constantmotion = 0
+speed = 0
+gravity = 0
+type = block
+material = brick
diff --git a/xtrn/lemons/sprites/entrance.bin b/xtrn/lemons/sprites/entrance.bin
new file mode 100644
index 0000000000000000000000000000000000000000..3a04f95cce67a7572c6801bbcb78c0f9e9b24523
--- /dev/null
+++ b/xtrn/lemons/sprites/entrance.bin
@@ -0,0 +1 @@
+���������
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/entrance.ini b/xtrn/lemons/sprites/entrance.ini
new file mode 100644
index 0000000000000000000000000000000000000000..2938a998e5e23b82e2d20208883e72de4dd1d89e
--- /dev/null
+++ b/xtrn/lemons/sprites/entrance.ini
@@ -0,0 +1,8 @@
+width = 3
+height = 3
+bearings = e
+positions = normal
+constantmotion = 0
+speed = 0
+gravity = 0
+type = entrance
diff --git a/xtrn/lemons/sprites/exit.bin b/xtrn/lemons/sprites/exit.bin
new file mode 100644
index 0000000000000000000000000000000000000000..4552d21f369806faef0b9ba8d89e6cfef19f9e2c
--- /dev/null
+++ b/xtrn/lemons/sprites/exit.bin
@@ -0,0 +1 @@
+��+����.�����
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/exit.ini b/xtrn/lemons/sprites/exit.ini
new file mode 100644
index 0000000000000000000000000000000000000000..44c1e3e6af949e65df779a595aa8033c6239b533
--- /dev/null
+++ b/xtrn/lemons/sprites/exit.ini
@@ -0,0 +1,8 @@
+width = 3
+height = 3
+bearings = e
+positions = normal
+constantmotion = 0
+speed = 0
+gravity = 0
+type = exit
diff --git a/xtrn/lemons/sprites/lava.bin b/xtrn/lemons/sprites/lava.bin
new file mode 100644
index 0000000000000000000000000000000000000000..5d2b391c874986bd6901bfaca0516af8e2d4d0e4
--- /dev/null
+++ b/xtrn/lemons/sprites/lava.bin
@@ -0,0 +1 @@
+�L�L�L��L�L�x�x�x�x�x�x
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/lava.ini b/xtrn/lemons/sprites/lava.ini
new file mode 100644
index 0000000000000000000000000000000000000000..7511ab737b6fcb00cf8e3f1f98d947f20d9c4187
--- /dev/null
+++ b/xtrn/lemons/sprites/lava.ini
@@ -0,0 +1,8 @@
+width = 6
+height = 2
+bearings = e
+positions = normal
+constantmotion = 0
+speed = 0
+gravity = 0
+type = hazard
diff --git a/xtrn/lemons/sprites/lemon.bin b/xtrn/lemons/sprites/lemon.bin
new file mode 100644
index 0000000000000000000000000000000000000000..b5000d011c9d8095297a437311c4688d3646f2ad
--- /dev/null
+++ b/xtrn/lemons/sprites/lemon.bin
@@ -0,0 +1 @@
+��.���.���.���������������O� �n� �n���������.���.���.���������������O���n ��n ������
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/lemon.ini b/xtrn/lemons/sprites/lemon.ini
new file mode 100644
index 0000000000000000000000000000000000000000..52925ec5244fbff0378fe15d8d6e74b64422f6a5
--- /dev/null
+++ b/xtrn/lemons/sprites/lemon.ini
@@ -0,0 +1,8 @@
+width = 3
+height = 3
+bearings = e,w
+positions = normal,normal2,fall,nuked
+constantmotion = 1
+speed = .25
+gravity = 1
+type = lemon
diff --git a/xtrn/lemons/sprites/metal.bin b/xtrn/lemons/sprites/metal.bin
new file mode 100644
index 0000000000000000000000000000000000000000..141d914b7d2c373aff478250e251b46dbc519cf8
--- /dev/null
+++ b/xtrn/lemons/sprites/metal.bin
@@ -0,0 +1 @@
+�x�x�x�x
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/metal.ini b/xtrn/lemons/sprites/metal.ini
new file mode 100644
index 0000000000000000000000000000000000000000..b8248e00b8b9f49b50b35ab0d1c7a1f657977742
--- /dev/null
+++ b/xtrn/lemons/sprites/metal.ini
@@ -0,0 +1,9 @@
+width = 3
+height = 1
+bearings = e
+positions = normal
+constantmotion = 0
+speed = 0
+gravity = 0
+type = block
+material = metal
diff --git a/xtrn/lemons/sprites/shooter-e.bin b/xtrn/lemons/sprites/shooter-e.bin
new file mode 100644
index 0000000000000000000000000000000000000000..c16e39f2eef57bbbec0fd230fd2af6de36283640
--- /dev/null
+++ b/xtrn/lemons/sprites/shooter-e.bin
@@ -0,0 +1 @@
+���������
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/shooter-e.ini b/xtrn/lemons/sprites/shooter-e.ini
new file mode 100644
index 0000000000000000000000000000000000000000..1ba2042d905b0469dd482cc1a2fcd1d0694e8e80
--- /dev/null
+++ b/xtrn/lemons/sprites/shooter-e.ini
@@ -0,0 +1,10 @@
+width = 3
+height = 3
+bearings = e
+positions = normal
+constantmotion = 0
+speed = 0
+gravity = 0
+type = shooter
+weapon = shot
+attackspeed = 5
diff --git a/xtrn/lemons/sprites/shooter-w.bin b/xtrn/lemons/sprites/shooter-w.bin
new file mode 100644
index 0000000000000000000000000000000000000000..8dcc0adf734d1bfd1f8b9227b4eab8877cd7d014
--- /dev/null
+++ b/xtrn/lemons/sprites/shooter-w.bin
@@ -0,0 +1 @@
+���������
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/shooter-w.ini b/xtrn/lemons/sprites/shooter-w.ini
new file mode 100644
index 0000000000000000000000000000000000000000..96cc87479b29d280f86c6ae8450b67bf400e1e20
--- /dev/null
+++ b/xtrn/lemons/sprites/shooter-w.ini
@@ -0,0 +1,10 @@
+width = 3
+height = 3
+bearings = w
+positions = normal
+constantmotion = 0
+speed = 0
+gravity = 0
+type = shooter
+weapon = shot
+attackspeed = 5
diff --git a/xtrn/lemons/sprites/shot.bin b/xtrn/lemons/sprites/shot.bin
new file mode 100644
index 0000000000000000000000000000000000000000..3e3ec673c02ac9892170b3eb5285aa1f85be789d
--- /dev/null
+++ b/xtrn/lemons/sprites/shot.bin
@@ -0,0 +1 @@
+��L���L���L���L�
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/shot.ini b/xtrn/lemons/sprites/shot.ini
new file mode 100644
index 0000000000000000000000000000000000000000..23002b990d1fa299f01458c8aa5f1288af3aadec
--- /dev/null
+++ b/xtrn/lemons/sprites/shot.ini
@@ -0,0 +1,9 @@
+width = 3
+height = 2
+bearings = e,w
+positions = normal
+constantmotion = 1
+speed = .15
+gravity = 0
+type = projectile
+range = 15
diff --git a/xtrn/lemons/sprites/slime.bin b/xtrn/lemons/sprites/slime.bin
new file mode 100644
index 0000000000000000000000000000000000000000..329fac618319d112368157341a67f8971168e866
--- /dev/null
+++ b/xtrn/lemons/sprites/slime.bin
@@ -0,0 +1 @@
+�*�*�*�*�*�*�x�x�x�x�x�x
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/slime.ini b/xtrn/lemons/sprites/slime.ini
new file mode 100644
index 0000000000000000000000000000000000000000..7511ab737b6fcb00cf8e3f1f98d947f20d9c4187
--- /dev/null
+++ b/xtrn/lemons/sprites/slime.ini
@@ -0,0 +1,8 @@
+width = 6
+height = 2
+bearings = e
+positions = normal
+constantmotion = 0
+speed = 0
+gravity = 0
+type = hazard
diff --git a/xtrn/lemons/sprites/water.bin b/xtrn/lemons/sprites/water.bin
new file mode 100644
index 0000000000000000000000000000000000000000..54bbeffc853d4498740903c74634b31a4733316a
--- /dev/null
+++ b/xtrn/lemons/sprites/water.bin
@@ -0,0 +1 @@
+�	�	��	��	�x�x�x�x�x�x
\ No newline at end of file
diff --git a/xtrn/lemons/sprites/water.ini b/xtrn/lemons/sprites/water.ini
new file mode 100644
index 0000000000000000000000000000000000000000..7511ab737b6fcb00cf8e3f1f98d947f20d9c4187
--- /dev/null
+++ b/xtrn/lemons/sprites/water.ini
@@ -0,0 +1,8 @@
+width = 6
+height = 2
+bearings = e
+positions = normal
+constantmotion = 0
+speed = 0
+gravity = 0
+type = hazard