diff --git a/xtrn/lord2/lord2.js b/xtrn/lord2/lord2.js
index 8026facd87e1714b2793393cb08f98b165ddcd30..6f29c062f8272d0c3efead64afb447e9817d9a74 100644
--- a/xtrn/lord2/lord2.js
+++ b/xtrn/lord2/lord2.js
@@ -597,8 +597,8 @@ var vars = {
 	'`d':{type:'const', val:'\b'},
 	'`\\':{type:'const', val:'\r\n'},
 	'`*':{type:'const', val:dk.connection.node},
-	x:{type:'fn', get:function() { return player.x }, set:function(x) { erase (player.x-1, player.y-1); player.x = clamp_integer(x, 's8'); update(true); } },
-	y:{type:'fn', get:function() { return player.y }, set:function(y) { erase (player.x-1, player.y-1); player.y = clamp_integer(y, 's8'); update(true); } },
+	x:{type:'fn', get:function() { return player.x }, set:function(x) { erase(player.x-1, player.y-1); player.x = clamp_integer(x, 's8'); update(true); } },
+	y:{type:'fn', get:function() { return player.y }, set:function(y) { erase(player.x-1, player.y-1); player.y = clamp_integer(y, 's8'); update(true); } },
 	map:{type:'fn', get:function() { return player.map }, set:function(map) { player.map = clamp_integer(map, 's16') } },
 	dead:{type:'fn', get:function() { return player.dead }, set:function(dead) { player.dead = clamp_integer(dead, 's8') } },
 	sexmale:{type:'fn', get:function() { return player.sexmale }, set:function(sexmale) { player.sexmale = clamp_integer(sexmale, 's16') } },
@@ -2988,11 +2988,13 @@ function getpoffset() {
 function erase(x, y) {
 	var off = getoffset(x,y);
 	var mi = map.mapinfo[off];
+	var attr = dk.console.attr.value;
 
 	foreground(mi.forecolour);
 	background(mi.backcolour);
 	dk.console.gotoxy(x, y);
 	dk.console.print(mi.ch === '' ? ' ' : mi.ch);
+	dk.console.attr.value = attr;
 }
 
 function update_update()
@@ -3144,9 +3146,10 @@ function chat(op)
 			if (l !== null) {
 				if (l === 'EXIT') {
 					lln('  `0'+op.name+'`2 has left.');
+					sln('');
 					file_remove(ch.name);
 					more();
-					return;
+					return false;
 				}
 				lln(l+'`r');
 				continue;
@@ -3166,7 +3169,7 @@ function chat(op)
 					chop.close();
 					update_update()
 					file_remove(ch.name);
-					return;
+					return true;
 				case '':
 					break;
 				default:
@@ -3191,11 +3194,15 @@ function hailed(pl)
 	var al;
 	var pos = 0;
 	var exit = false;
+	var lin;
+	var givfile = new File(getfname(maildir + 'giv'+(player.Record + 1)+'.tmp'));
+	var givfile_pos = 0;
 
 	if (!file_exists(tx.name))
 		return;
 	update_bar('`0'+op.name+' `2hails you.  Press (`0A`2) to leave.', true);
 	lw('`r0`2');
+	hail_cleanup();
 	file_remove(tx.name);
 	do {
 		inn = getfname(maildir + 'inf'+(player.Record + 1)+'.tmp');
@@ -3210,19 +3217,42 @@ function hailed(pl)
 			inf.close();
 			al.forEach(function(l) {
 				if (l === 'CHAT') {
-					chat(op);
-					exit = true;
+					if (chat(op))
+						exit = true;
+				}
+				else {
+					update_bar(l, true);
 				}
 			});
 		}
+		if (givfile.open('rb')) {
+			givfile.position = givfile_pos;
+			lin = givfile.readln();
+			switch(lin) {
+				case null:
+					break;
+				case 'BYE':
+					exit = true;
+					// Fall-through
+				default:
+					givfile_pos = givfile.position;
+					break;
+			}
+			givfile.close();
+		}
+		if (file_exists(getfname(maildir + 'bat'+(player.Record + 1)+'.tmp'))) {
+			online_battle(op, false);
+			break;
+		}
 		if (dk.console.waitkey(game.delay)) {
-			if (getkey().toUpperCase() === 'A')
+			if (getkey().toUpperCase() === 'A') {
+				exit = true;
 				break;
+			}
 		}
 		op.reLoad();
 	} while((!exit) && op.battle === 1);
-	if (inf !== undefined)
-		file_remove(inf.name);
+	hail_cleanup();
 	draw_map();
 	redraw_bar(true);
 	update();
@@ -3306,6 +3336,7 @@ function update(skip) {
 	var op;
 	var now = (new Date().valueOf());
 	var done;
+	var orig_attr = dk.console.attr.value;
 
 	if ((!skip) || now > next_update) {
 		next_update = now + game.delay;
@@ -3360,6 +3391,7 @@ function update(skip) {
 	dk.console.print('\x02');
 	dk.console.gotoxy(player.x - 1, player.y - 1);
 	update_update();
+	dk.console.attr.value = orig_attr;
 }
 
 function draw_map() {
@@ -3816,6 +3848,26 @@ function enemy_hp(enm)
 	lw(space_pad('`r1`0`e\'s `2hitpoints: '+disp_hp(enm.hp, enm.maxhp), 44));
 }
 
+function fist_string(ename)
+{
+	switch(random(5)) {
+		case 0:
+			return '`2You punch `0'+ename+'`2 in the jimmy';
+		case 1:
+			return 'You kick `0'+ename+'`2 hard as you can';
+			break;
+		case 2:
+			return '`2You slap `0'+ename+'`2 a good one';
+			break;
+		case 3:
+			return '`2You headbutt `0'+ename;
+			break;
+		default:
+			return '`2You trip `0'+ename;
+			break;
+	}
+}
+
 function offline_battle(no_super, skip_see)
 {
 	var ch;
@@ -3902,26 +3954,10 @@ function offline_battle(no_super, skip_see)
 				if (supr)
 					astr = '`2You `%SUPER STRIKE`2';
 				else if (wep === undefined) {
-					switch(random(5)) {
-						case 0:
-							astr = '`2You punch `0`e`2 in the jimmy';//Him,it
-							break;
-						case 1:
-							astr = 'You kick `0`e`2 hard as you can';// Wild Boar
-							break;
-						case 2:
-							astr = '`2You slap `0`e`2 a good one';//Rabid dog
-							break;
-						case 3:
-							astr = '`2You headbutt `0`e';//Rabid dog
-							break;
-						case 4:
-							astr = '`2You trip `0`e';//Rabid dog
-							break;
-					}
+					astr = fist_string(enm.name);
 				}
 				else
-					astr = wep.hitaction
+					astr = wep.hitaction;
 				lw('`2' + astr + '`2 for `0'+dmg+'`2 damage!');
 				enm.hp -= dmg;
 				enemy_hp(enm);
@@ -3934,9 +3970,9 @@ function offline_battle(no_super, skip_see)
 			lw('`r0`0You killed '+him+'!')
 			dk.console.gotoxy(2, 23);
 			dk.console.cleareol();
-			lw('`2You find `$$'+enm.gold+'`2 and gain `%'+enm.experience+' `2experience in this battle.  `2<`0MORE`2>')
-			player.money += enm.gold;
-			player.p[0] += enm.experience;
+			lw('`2You find `$$'+enm.gold+'`2 and gain `%'+pretty_int(enm.experience)+' `2experience in this battle.  `2<`0MORE`2>')
+			player.money = clamp_integer(player.money + enm.gold, 's32');
+			player.p[0] = clamp_integer(player.p[0] + enm.experience, 's32');
 			if (supr)
 				update_bar(enm.killstr, true, 0);
 			getkey();
@@ -4298,6 +4334,263 @@ function items_menu(itms, cur, buying, selling, extras, starty, endy)
 	}
 }
 
+function player_to_enemy(op)
+{
+	enemy = {
+		name:op.name,
+		see:'',
+		killstr:'',
+		sex:op.sexmale,
+		defence:op.p[4],
+		gold:op.money === 0 ? 0 : Math.floor(op.money / 2),
+		experience:op.experience === 0 ? 0 : Math.floor(op.experience / 10),
+		hp:op.p[1],
+		maxhp:op.p[2],
+		attacks:[],
+		win_reffile:'gametxt.ref',
+		lose_reffile:'gametxt.ref',
+		run_reffile:'gametxt.ref',
+		win_refname:'live',
+		lose_refname:'die',
+		run_refname:'run',
+	};
+	if (op.weaponnumber == 0) {
+		enemy.attacks.push({
+			strength:(op.p[3]),
+			hitaction:'`0`e`2 hits you with '+(op.sexmale === 1 ? 'his' : 'her')+' fists',
+		});
+	}
+	else {
+		enemy.attacks.push({
+			strength:(op.p[3] + items[op.weaponnumber - 1].strength),
+			hitaction:'`0`e`2 hits you with '+(op.sexmale === 1 ? 'his' : 'her')+' `0'+items[op.weaponnumber-1].name+'`2',
+		});
+	}
+	if (op.armournumber !== 0) {
+		enemy.defence += items[op.armournumber - 1].defence;
+	}
+	setvar('`v39', op.Record + 1);
+	setvar('enemy', op.name);
+}
+
+// Deletes files that are *read* during an online interaction (not written)
+function hail_cleanup()
+{
+	file_remove(getfname(maildir + 'bat'+(player.Record + 1)+'.tmp'));
+	file_remove(getfname(maildir + 'tx'+(player.Record + 1)+'.tmp'));
+	file_remove(getfname(maildir + 'chat'+(player.Record + 1)+'.tmp'));
+	file_remove(getfname(maildir + 'giv'+(player.Record + 1)+'.tmp'));
+	file_remove(getfname(maildir + 'inf'+(player.Record + 1)+'.tmp'));
+}
+
+function online_battle(op, attack_first) {
+	var str;
+	var hstr;
+	var def;
+	var edef;
+	var pbat = new File(getfname(maildir+'bat'+(player.Record + 1)+'.tmp'));
+	var ebat = new File(getfname(maildir+'bat'+(op.Record + 1)+'.tmp'));
+	var wname;
+	var weap;
+	var astr;
+	var wend;
+	var rln;
+	var tleft;
+	var m;
+	var ret;
+	var dmg;
+	var doneBattle = false;
+	var doneMenu;
+	var doneMessages = false;
+	var cur;
+	var ln;
+	var poff = 0;
+
+	if (player.weaponnumber == 0)
+		wname = 'fists';
+	else {
+		wname = items[player.weaponnumber - 1].name;
+		weap = items[player.weaponnumber - 1];
+	}
+
+	str = (player.weaponnumber === 0 ? 0 : items[player.weaponnumber - 1].strength) + player.p[3];
+	hstr = str >> 1;
+	def = (player.armournumber === 0 ? 0 : items[player.armournumber - 1].defence) + player.p[4];
+	edef = (op.armournumber === 0 ? 0 : items[op.armournumber - 1].defence) + op.p[4];
+	player_to_enemy(op);
+
+	enemy_hp(enemy);
+	your_hp();
+	while (!doneBattle) {
+		if (attack_first) {
+			// Make an attack...
+			clearrows(21,23);
+			dmg = hstr + random(hstr) + 1 - edef;
+			if (!ebat.open('ab'))
+				throw new Error('Unable to open '+ebat.name);
+			if (dmg < 0) {
+				ebat.write('`r0`0'+player.name+' `2misses you completely!|0\r\n');
+				ebat.close();
+				dk.console.gotoxy(2, 21);
+				lw('`4You completely miss!`2');
+			}
+			else {
+				ebat.write('`r0`0'+player.name+' `2hits you with '+(player.sexmale == 1 ? 'his' : 'her')+' `0'+wname+'`2 for `b'+pretty_int(dmg)+'`2 damage!|'+dmg+'\r\n');
+				ebat.close();
+				if (weap === undefined) {
+					astr = fist_string(op.name);
+				}
+				else {
+					astr = weap.hitaction;
+				}
+				dk.console.gotoxy(2, 21);
+				lw('`2' + astr + '`2 for `0'+dmg+'`2 damage!');
+				enemy.hp -= dmg;
+			}
+			enemy_hp(enemy);
+			if (enemy.hp < 1) {
+				dk.console.gotoxy(2, 22);
+				lw('`r0`0You killed '+(enemy.sex === 1 ? 'him' : 'her')+'!');
+				dk.console.gotoxy(2, 23);
+				lw('`2You find `$$'+pretty_int(enemy.gold)+'`2 and gain `%'+pretty_int(enemy.experience)+' `2experience in this battle.  `2<`0MORE`2>');
+				player.money = clamp_integer(player.money + enemy.gold, 's32');
+				player.p[0] = clamp_integer(player.p[0] + enemy.experience, 's32');
+				player.put();
+				getkey();
+				run_ref('iwon', 'gametxt.ref');
+				ret = 'WON';
+				break;
+			}
+
+			// Wait for response from remote...
+			dk.console.gotoxy(2, 23);
+			lw('`r0`2You wait for the counter attack...');
+			// Timeout count... 
+			wend = time() + 30;
+		}
+		else
+			attack_first = true;
+
+		doneMessages = false;
+		while (!doneMessages) {
+			rln = undefined;
+			if (!pbat.is_open) {
+				if (file_exists(pbat.name))
+					pbat.open('rb');
+			}
+			if (pbat.is_open) {
+				pbat.position = poff;
+				rln = pbat.readln();
+				poff = pbat.position;
+			}
+			if (rln === null || rln === undefined) {
+				if (pbat.is_open)
+					pbat.close();
+				tleft = wend - time();
+				if (tleft < 1) {
+					ret = 'TIMEOUT';
+					if (pbat.is_open)
+						pbat.close();
+					file_remove(pbat.name);
+					poff = 0;
+					doneBattle = true;
+					break;
+				}
+				if (tleft <= 10) {
+					dk.console.gotoxy(55, 23);
+					dk.console.cleareol();
+					lw('`0(`2timeout in '+tleft+'`0)`2');
+				}
+				mswait(game.delay);
+				continue;
+			}
+			if (pbat.is_open)
+				pbat.close();
+
+			m = rln.match(/^(.*)\|(-500|-1000|[0-9]+)\r?\n?$/);
+			if (m !== null) {
+				dmg = parseInt(m[2], 10);
+				switch(dmg) {
+					case -500:	// Other side left
+						doneBattle = true;
+						doneMessages = true;
+						ret = 'LEFT';
+						break;
+					case -1000:	// Yelled something
+						update_bar(m[1], true);
+						wend = time() + 30;
+						break;
+					default:
+						clearrows(22, 22);
+						dk.console.gotoxy(2, 22);
+						lw(m[1]);
+						player.p[1] -= dmg;
+						your_hp();
+						if (player.p[1] < 1) {
+							player.p[1] = 0;
+							player.put();
+							// TODO: Dead notification, etc...
+							run_ref('die', 'gametxt.ref');
+							ret = 'LOST';
+							doneBattle = true;
+							doneMessages = true;
+							break;
+						}
+						player.put();
+						doneMessages = true;
+						break;
+				}
+			}
+		}
+
+		if (pbat.is_open)
+			pbat.close();
+		file_remove(pbat.name);
+		poff = 0;
+		if (doneBattle)
+			break;
+
+		// Ask for next battle action...
+		doneMenu = false;
+		cur = 0;
+		while (!doneMenu) {
+			clearrows(23, 23);
+			cur = hbar(0, 23, ['Attack', 'Yell Something', 'Leave'], cur);
+			switch(cur) {
+				case 0:	// Attack
+					doneMenu = true;
+					break;
+				case 1:	// Yell something
+					// Loops back to this message...
+					clearrows(23, 23);
+					dk.console.gotoxy(2, 23);
+					lw('`r0`2Message? : ');
+					ln = clean_str(dk.console.getstr({input_box:true, attr:new Attribute(31), len:66, crlf:false}));
+					if (!ebat.open('ab'))
+						throw new Error('Unable to open '+ebat.name);
+					ebat.write(ln + "|-1000\r\n");
+					ebat.close();
+					break;
+				case 2:	// Leave
+					// Exits and away we go...
+					if (!ebat.open('ab'))
+						throw new Error('Unable to open '+ebat.name);
+					ebat.write("RUN AWAY!|-500\r\n");
+					ebat.close();
+					doneMenu = true;
+					doneBattle = true;
+					ret = 'RANAWAY';
+					break;
+			}
+		}
+	}
+
+	enemy = undefined;
+	clearrows(21,23);
+	return ret;
+}
+
+
 function hail()
 {
 	var op;
@@ -4338,6 +4631,13 @@ function hail()
 			} while (ch !== 'Q');
 		}
 		else {
+			if (online) {
+				f = new File(getfname(maildir+'inf'+(op.Record + 1)+'.tmp'));
+				if (!f.open('ab'))
+					throw new Error('Unable to open '+f.name);
+				f.write(player.name+' `2searches through '+(player.sexmale == 1 ? 'his' : 'her')+' backpack. (`0A`2 to abort)\r\n');
+				f.close();
+			}
 			while (1) {
 				choice = items_menu(inv, cur, false, false, '', 7, 22);
 				cur = choice.cur;
@@ -4369,23 +4669,28 @@ function hail()
 								throw new Error('Unable to open '+f.name);
 							f.write('ADDITEM|'+inv[choice.cur]+'|'+ch+'\r\n');
 							f.close();
-							if (!online) {
-								f = new File(getfname(maildir+'mail'+(op.Record + 1)+'.dat'));
-								if (!f.open('ab'))
-									throw new Error('Unable to open '+f.name);
-								f.write('  `%`r1GOOD NEWS!`r0\r\n`0  '+repeat_chars(ascii(0xc4), 77)+'\r\n  `0'+player.name+' `2gives you ');
-								if (ch === 1)
-									f.write('a ');
-								else
-									f.write(pretty_int(ch) + ' ');
-								f.write('really nice `$' + items[inv[choice.cur] - 1].name + '`2!\r\n');
-								f.write('@DONE\r\n');
-								f.close();
-							}
+							f = new File(getfname(maildir+'mail'+(op.Record + 1)+'.dat'));
+							if (!f.open('ab'))
+								throw new Error('Unable to open '+f.name);
+							f.write('  `%`r1GOOD NEWS!`r0\r\n`0  '+repeat_chars(ascii(0xc4), 77)+'\r\n  `0'+player.name+' `2gives you ');
+							if (ch === 1)
+								f.write('a ');
+							else
+								f.write(pretty_int(ch) + ' ');
+							f.write('really nice `$' + items[inv[choice.cur] - 1].name + '`2!\r\n');
+							f.write('@DONE\r\n');
+							f.close();
 						}
 					}
 				}
 			}
+			if (online) {
+				f = new File(getfname(maildir+'inf'+(op.Record + 1)+'.tmp'));
+				if (!f.open('ab'))
+					throw new Error('Unable to open '+f.name);
+				f.write(player.name+' `2closes '+(player.sexmale == 1 ? 'his' : 'her')+' backpack. (`0A`2 to abort)\r\n');
+				f.close();
+			}
 			draw_map();
 			update();
 			dk.console.gotoxy(0, 21);
@@ -4538,40 +4843,7 @@ function hail()
 							break;
 						}
 					}
-					enemy = {
-						name:op.name,
-						see:'',
-						killstr:'',
-						sex:op.sex,
-						defence:op.p[4],
-						gold:op.money === 0 ? 0 : Math.floor(op.money / 2),
-						experience:op.experience === 0 ? 0 : Math.floor(op.experience / 10),
-						hp:op.p[1],
-						maxhp:op.p[2],
-						attacks:[],
-						win_reffile:'gametxt.ref',
-						lose_reffile:'gametxt.ref',
-						run_reffile:'gametxt.ref',
-						win_refname:'live',
-						lose_refname:'die',
-						run_refname:'run',
-					};
-					if (op.weaponnumber == 0) {
-						enemy.attacks.push({
-							strength:(op.p[3]),
-							hitaction:'`0`e`2 hits you with '+(op.sexmale === 1 ? 'his' : 'her')+' fists',
-						});
-					}
-					else {
-						enemy.attacks.push({
-							strength:(op.p[3] + items[op.weaponnumber - 1].strength),
-							hitaction:'`0`e`2 hits you with '+(op.sexmale === 1 ? 'his' : 'her')+' `0'+items[op.weaponnumber-1].name+'`2',
-						});
-					}
-					if (op.armournumber !== 0) {
-						enemy.defence += otems[op.armournumber - 1].defence;
-					}
-					setvar('`v39', op.Record + 1);
+					player_to_enemy(op);
 					switch (offline_battle(true, true)) {
 						case 'RAN':
 							// TODO: Do the opponents HP drop in an offline battle they win?
@@ -4654,6 +4926,7 @@ function hail()
 	else {
 		op.unLock();
 		update_bar('`2Hailing `0'+op.name+'`2... (hit a key to abort)', true);
+		hail_cleanup();
 
 		f = new File(getfname(maildir+'tx'+(player.Record + 1)+'.tmp'))
 		if (!f.open('ab'))
@@ -4673,6 +4946,7 @@ function hail()
 				player.battle = 0;
 				update_update();
 				player.put();
+				hail_cleanup();
 				return;
 			}
 		}
@@ -4684,6 +4958,11 @@ function hail()
 			switch(cur) {
 				case 0:
 					done = true;
+					f = new File(getfname(maildir+'giv'+(op.Record + 1)+'.tmp'));
+					if (!f.open('ab'))
+						throw new Error('Unable to open '+f.name);
+					f.write('BYE\r\n');
+					f.close();
 					break;
 				case 1:	// TODO: Attack...
 					if (map.nofighting) {
@@ -4692,6 +4971,8 @@ function hail()
 							break;
 						}
 					}
+					online_battle(op, true);
+					done = true;
 					break;
 				case 2:
 					give_item(true);
@@ -4711,6 +4992,7 @@ function hail()
 					f.close();
 					chat(op);
 					draw_map();
+					done = true;
 					break;
 			}
 		}
@@ -4718,6 +5000,7 @@ function hail()
 	player.battle = 0;
 	update_update();
 	player.put();
+	hail_cleanup();
 	erase_menu();
 	update();
 
@@ -5108,6 +5391,7 @@ load_time();
 load_game();
 
 run_ref('rules', 'rules.ref');
+hail_cleanup();
 
 setup_time_warnings();