diff --git a/classes/classes/DebugMenu.as b/classes/classes/DebugMenu.as
index c4e8a3925c018a79f0229da7a50dc5fc1c92f456..faeb239284f810c50305e2d8c489ecf28a6d3cf4 100644
--- a/classes/classes/DebugMenu.as
+++ b/classes/classes/DebugMenu.as
@@ -15,6 +15,7 @@ import coc.view.selfDebug.DebugComponentFactory;
 import com.bit101.components.Window;
 
 import flash.display.Sprite;
+import flash.display.StageAlign;
 import flash.events.Event;
 import flash.events.KeyboardEvent;
 import flash.events.MouseEvent;
@@ -71,6 +72,12 @@ public class DebugMenu extends BaseContent {
 			addButton(9, "Age Change", ageChangeMenu).hint("What's my age again?");
 			addNextButton("Scene Build", echo).hint("Test a thing.");
 			addNextButton("Save Edit", selfDebug).hint("Edit characters, places, and things that use the new save system instead of flags.");
+
+			CONFIG::STANDALONE {
+				// As the console does not yet have any way to account use the virtual keyboard, prevent mobile users from becoming stuck
+				addNextButton("Console", debugConsole).hint(_console? "Switch back to the default UI":"Switch to using the console UI");
+			}
+
 			addButton(14, "Exit", playerMenu);
 		}
 		if (game.inCombat) {
@@ -80,6 +87,38 @@ public class DebugMenu extends BaseContent {
 		}
 	}
 
+	private var _console:Console;
+	public function debugConsole():void {
+		if (!_console) {
+			warningScreen();
+		} else {
+			_console.dispose();
+			mainView.stage.removeChild(_console);
+			_console = null;
+		}
+
+
+		function warningScreen():void {
+			clearOutput();
+			outputText(
+					"Warning: This feature is experimental and drastically changes the user interface and input." +
+					"\nOnly enable this if you are comfortable using console-style inputs"
+			);
+			menu();
+			addNextButton("Enable", enableConsole);
+			addNextButton("Cancel", accessDebugMenu);
+		}
+
+		function enableConsole():void {
+			_console= new Console(mainView.height, mainView.width);
+			mainView.stage.addChild(_console);
+			clearOutput();
+			menu();
+			doNext(accessDebugMenu);
+			_console.startupHelp();
+		}
+	}
+
 	private function setList(lib:Object):void {
 		var btnData:ButtonDataList = new ButtonDataList();
 		var libClass:Class = Class(getDefinitionByName(getQualifiedClassName(lib)));
diff --git a/classes/classes/GameSettings.as b/classes/classes/GameSettings.as
index 0a782f415f96435a4d475547524ff6d34508d21f..613036e7f35872a2eb9a11136d9cf6ad2b871664 100644
--- a/classes/classes/GameSettings.as
+++ b/classes/classes/GameSettings.as
@@ -1,6 +1,8 @@
 package classes {
 import classes.GlobalFlags.*;
+import classes.display.GameViewData;
 import classes.display.SettingPane;
+import classes.display.SettingPaneData;
 import classes.saves.*;
 import coc.view.*;
 import classes.lists.*;
@@ -138,6 +140,7 @@ public class GameSettings extends BaseContent implements SelfSaving, ThemeObserv
 				break;
 		}
 		pane.update();
+		GameViewData.settingPaneData = new SettingPaneData(PANES_CONFIG[paneIndex(pane)], pane.settingData);
 	}
 
 	private function setupGameplayPane():void {
@@ -372,6 +375,8 @@ public class GameSettings extends BaseContent implements SelfSaving, ThemeObserv
 	}
 
 	public function exitSettings():void {
+		GameViewData.settingPaneData = null;
+		GameViewData.screenType = GameViewData.DEFAULT;
 		game.saves.savePermObject(false);
 		hideSettingPane();
 		if (quickReturn) {
@@ -409,6 +414,8 @@ public class GameSettings extends BaseContent implements SelfSaving, ThemeObserv
 		}
 		mainView.setMainFocus(pane, false, true);
 		setOrUpdateSettings(pane);
+		GameViewData.settingPaneData = new SettingPaneData(PANES_CONFIG[paneIndex(pane)], pane.settingData);
+		GameViewData.screenType = GameViewData.OPTIONS_MENU;
 		setButtons();
 	}
 
diff --git a/classes/classes/MainMenu.as b/classes/classes/MainMenu.as
index 576d7e10086287cbf77aa06c81afca269987e244..57f7ed787045c3a85239675f697ad934bd9208dd 100644
--- a/classes/classes/MainMenu.as
+++ b/classes/classes/MainMenu.as
@@ -1,4 +1,6 @@
 package classes {
+import classes.display.GameViewData;
+
 import coc.view.*;
 
 import com.bit101.components.SearchBar;
@@ -21,6 +23,7 @@ public class MainMenu extends BaseContent implements ThemeObserver {
 	}
 
 	private var _mainMenu:Block;
+	public var buttonData:Array = [];
 
 	//------------
 	// MAIN MENU
@@ -44,10 +47,17 @@ public class MainMenu extends BaseContent implements ThemeObserver {
 		if (_mainMenu == null) {
 			configureMainMenu();
 		} else {
-			setContinue(_mainMenu.getElementByName("mainmenu_button_0") as CoCButton);
+			var cont:CoCButton = _mainMenu.getElementByName("mainmenu_button_0") as CoCButton;
+			setContinue(cont);
+			buttonData[0] = cont.buttonData();
 			updateMainMenuTextColors();
-			_mainMenu.visible = true;
+			mainView.addElementAt(_mainMenu, 2);
 		}
+		GameViewData.screenType = GameViewData.MAIN_MENU;
+		GameViewData.menuData = buttonData;
+		// Fixme: This should not be handled here, instead set to main menu screen type and let UI skip showing stats
+		GameViewData.playerStatData = null;
+		GameViewData.flush();
 	}
 
 	private var _cocLogo:BitmapDataSprite;
@@ -137,6 +147,7 @@ public class MainMenu extends BaseContent implements ThemeObserver {
 			mainView.hookButton(button as Sprite);
 			mainMenuContent.addElement(button);
 			if (i == 0) setContinue(button);
+			buttonData.push(button.buttonData());
 		}
 		mainMenuContent.addElement(_cocLogo);
 		mainMenuContent.addElement(_disclaimerBackground);
@@ -162,10 +173,8 @@ public class MainMenu extends BaseContent implements ThemeObserver {
 
 	public function continueButton():void {
 		if (!player.loaded) {
-			if (!game.saves.loadLatest(mainMenu)) {
-				// Load was not successful, Saves will handle displaying error.
-				return;
-			}
+			game.saves.loadLatest(mainMenu);
+			return;
 		}
 		playerMenu();
 	}
@@ -191,6 +200,7 @@ public class MainMenu extends BaseContent implements ThemeObserver {
 
 	public function hideMainMenu():void {
 		if (_mainMenu !== null) _mainMenu.visible = false;
+		GameViewData.screenType = GameViewData.DEFAULT;
 		mainView.showMainText();
 	}
 
diff --git a/classes/classes/Output.as b/classes/classes/Output.as
index 8595643d44e646deb6bbe0e303abb3fcd68fc979..bf28ef9e922500e9eeeaf4028d912181d39626ea 100644
--- a/classes/classes/Output.as
+++ b/classes/classes/Output.as
@@ -1,10 +1,13 @@
 package classes {
 import classes.GlobalFlags.kFLAGS;
 import classes.GlobalFlags.kGAMECLASS;
+import classes.display.GameViewData;
 import classes.internals.*;
 
 import coc.view.*;
 
+import flash.utils.getQualifiedClassName;
+
 import flash.utils.setTimeout;
 
 /**
@@ -73,6 +76,25 @@ public class Output extends Utils implements GuiOutput {
 	 */
 	public function flush():void {
 		kGAMECLASS.mainViewManager.setText(_currentText, _imageText);
+
+		GameViewData.htmlText  = _currentText;
+		GameViewData.imageText = _imageText;
+
+		// TODO: Handle these elsewhere / set values directly
+		GameViewData.bottomButtons = kGAMECLASS.mainView.bottomButtons.map(function (btn:CoCButton, i:int, a:Array):ButtonData {
+			return btn.buttonData();
+		});
+		GameViewData.menuButtons = [
+				kGAMECLASS.mainView.newGameButton.buttonData(),
+				kGAMECLASS.mainView.dataButton.buttonData(),
+				kGAMECLASS.mainView.statsButton.buttonData(),
+				kGAMECLASS.mainView.levelButton.buttonData(),
+				kGAMECLASS.mainView.perksButton.buttonData(),
+				kGAMECLASS.mainView.appearanceButton.buttonData()
+		];
+		GameViewData.inputNeeded = kGAMECLASS.mainView.nameBox.visible;
+		GameViewData.inputText   = kGAMECLASS.mainView.nameBox.text;
+		GameViewData.flush();
 	}
 
 	/**
@@ -102,6 +124,7 @@ public class Output extends Utils implements GuiOutput {
 	 * @return  The instance of the class to support the 'Fluent interface' aka method-chaining
 	 */
 	public function clear(hideMenuButtons:Boolean = false):GuiOutput {
+		updateLoc();
 		if (hideMenuButtons) {
 			if (kGAMECLASS.gameState != 3) kGAMECLASS.mainView.hideMenuButton(MainView.MENU_DATA);
 			kGAMECLASS.mainView.hideMenuButton(MainView.MENU_APPEARANCE);
@@ -117,18 +140,53 @@ public class Output extends Utils implements GuiOutput {
 		kGAMECLASS.mainView.resetComboBox();
 		kGAMECLASS.mainView.resetMainFocus();
 		kGAMECLASS.parser.resetParser();
+		GameViewData.clear();
 		return this;
 	}
 
 	public function clearText():GuiOutput {
+		updateLoc();
 		nextEntry();
 		_currentText = "";
 		_imageText = "";
 		kGAMECLASS.mainView.clearOutputText();
 		kGAMECLASS.parser.resetParser();
+		GameViewData.clear();
 		return this;
 	}
 
+	private static var _currentScene:String = "";
+	public static function get currentScene():String {
+		return _currentScene;
+	}
+
+	private static function updateLoc():void {
+		// If there is a scene on the stack, it is likely the current scene
+		var _sceneRegex:RegExp = /classes.Scenes[^(]*/;
+		var st:String = new Error().getStackTrace();
+		var tr:Array = _sceneRegex.exec(st);
+		if (tr != null && tr.length == 1 && tr[0].length > 0) {
+			_currentScene = tr[0];
+			return;
+		}
+
+		// No scene on the stack, return whatever called BaseContent or Output
+		const excludes:Array = [
+			new RegExp("^" + getQualifiedClassName(Output)),
+			new RegExp("^" + getQualifiedClassName(BaseContent))
+		]
+		_sceneRegex = /classes::[^(]*/g;
+		do {
+			tr = _sceneRegex.exec(st)
+		} while (tr != null && excludes.some(function (exclude:RegExp, i:int, arr:Array):Boolean {
+			return exclude.test(tr[0]);
+		}))
+
+		if (tr != null) {
+			_currentScene = tr[0];
+		}
+	}
+
 	/**
 	 * Adds raw text to the output without passing it through the parser
 	 *
diff --git a/classes/classes/Player.as b/classes/classes/Player.as
index 8c766631b87fb3785c31e776a88d7a3662a9e525..07dd81a69561f7d50a024e57099b49ecfe918d62 100644
--- a/classes/classes/Player.as
+++ b/classes/classes/Player.as
@@ -2006,13 +2006,14 @@ public class Player extends PlayerHelper {
 	//	2 - physical
 	//	3 - non-bloodmage magic
 	override public function changeFatigue(mod:Number, type:Number = 0):Number {
+		var oldFatigue:Number = fatigue;
 		mod = super.changeFatigue(mod, type);
 		if (mod > 0) {
 			game.mainView.statsView.showStatUp('fatigue');
 			// fatigueUp.visible = true;
 			// fatigueDown.visible = false;
 		}
-		if (mod < 0) {
+		if (mod < 0 && fatigue != oldFatigue) {
 			game.mainView.statsView.showStatDown('fatigue');
 			// fatigueDown.visible = true;
 			// fatigueUp.visible = false;
diff --git a/classes/classes/Saves.as b/classes/classes/Saves.as
index 581f4ac322428c7069b9c454c5972e590690e23b..04e17240369fae7aeec874d9489803807a6f0252 100644
--- a/classes/classes/Saves.as
+++ b/classes/classes/Saves.as
@@ -331,7 +331,7 @@
 			}
 			else if (saveSlot >= 0) {
 				slotName = saveFileNames[saveSlot];
-				return loadGame(slotName);
+				return loadGame(slotName, true);
 			}
 			else return false;
 		}
diff --git a/classes/classes/Scenes/Dungeons/DungeonMap.as b/classes/classes/Scenes/Dungeons/DungeonMap.as
index 52f8a045f1fde6fa8ffafcb9431f40d97b958033..0e41e3a368c8f9e45e114e6a1a7f92191aa34551 100644
--- a/classes/classes/Scenes/Dungeons/DungeonMap.as
+++ b/classes/classes/Scenes/Dungeons/DungeonMap.as
@@ -2,6 +2,7 @@ package classes.Scenes.Dungeons {
 	import classes.*;
 	import classes.GlobalFlags.*;
 	import classes.Scenes.Dungeons.*;
+import classes.display.GameViewData;
 import classes.display.SpriteDb;
 
 import coc.view.BitmapDataSprite;
@@ -395,18 +396,37 @@ public class DungeonMap extends BaseContent {
 			if (game.dungeons.usingAlternative) {
 				outputText("<b><u>"+game.dungeons.dungeonName+"</u></b>");
 				mainView.dungeonMap.visible = true;
+
+				GameViewData.mapData = {
+					alternative: true,
+					modulus: mapModulus,
+					layout: mapLayout,
+					connectivity: connectivity,
+					playerLoc: game.dungeons.playerLoc
+				}
 			}
 			else {
-				rawOutputText(chooseRoomToDisplay());
-				outputText("[pg]<b><u>Legend</u></b>");
-				outputText("\n<font face=\"Consolas, _typewriter\">@</font> — Player Location");
-				outputText("\n<font face=\"Consolas, _typewriter\">L</font> — Locked Door");
-				outputText("\n<font face=\"Consolas, _typewriter\">^v↕</font> — Stairs");
+				var rawDisplay:String = chooseRoomToDisplay();
+				var legend:String = "[pg]<b><u>Legend</u></b>" +
+						"\n<font face=\"Consolas, _typewriter\">@</font> — Player Location" +
+						"\n<font face=\"Consolas, _typewriter\">L</font> — Locked Door" +
+						"\n<font face=\"Consolas, _typewriter\">^v↕</font> — Stairs";
+				rawOutputText(rawDisplay);
+				outputText(legend);
+
+				GameViewData.mapData = {
+					alternative: false,
+					rawText: rawDisplay,
+					legend: legend
+				}
 			}
+			GameViewData.screenType = GameViewData.DUNGEON_MAP;
 			menu();
 			addButton(0, "Close Map", closeMap);
 		}
 		public function closeMap():void {
+			GameViewData.screenType = GameViewData.DEFAULT;
+			GameViewData.mapData = null;
 			mainView.dungeonMap.visible = false;
 			playerMenu();
 		}
diff --git a/classes/classes/Scenes/Inventory.as b/classes/classes/Scenes/Inventory.as
index 840019f0e0f5e8ae2730c0c20cdd2b7e653e7737..21daefc0c28a7570f271e213979c9576470d07e7 100644
--- a/classes/classes/Scenes/Inventory.as
+++ b/classes/classes/Scenes/Inventory.as
@@ -5,6 +5,7 @@ package classes.Scenes {
 import classes.*;
 import classes.GlobalFlags.*;
 import classes.Items.*;
+import classes.display.GameViewData;
 import classes.internals.*;
 
 import coc.view.*;
@@ -834,7 +835,9 @@ public class Inventory extends BaseContent {
 		stash();
 	}
 
+	public var stashTexts:Array = [];
 	public function stash():void {
+		stashTexts = [];
 		callNext = stash;
 		setup();
 		var arr:Array = [[weaponRack, describe(weaponRack), player.hasKeyItem("Equipment Rack - Weapons")], [armorRack, describe(armorRack), player.hasKeyItem("Equipment Rack - Armor")], [shieldRack, describe(shieldRack), player.hasKeyItem("Equipment Rack - Shields")], [dresserBox, describe(dresserBox), cabin.hasDresser], [jewelryBox, describe(jewelryBox), player.hasKeyItem("Equipment Storage - Jewelry Box")],];
@@ -873,6 +876,10 @@ public class Inventory extends BaseContent {
 		mainView.setMainFocus(scrollPane, false, true);
 		scrollPane.draw();
 		scrollPane.update();
+
+		GameViewData.screenType = GameViewData.STASH_VIEW;
+		GameViewData.stashData = stashTexts;
+		output.flush();
 		//Achievement time!
 		/*
 		var isAchievementEligible:Boolean = true;
@@ -899,6 +906,8 @@ public class Inventory extends BaseContent {
 	private var scrollPane:CoCScrollPane;
 
 	internal function close(next:Function):void {
+		GameViewData.stashData = null;
+		GameViewData.screenType = GameViewData.DEFAULT;
 		DragButton.cleanUp();
 		mainView.resetMainFocus();
 		clearOutput();
@@ -963,6 +972,7 @@ public class Inventory extends BaseContent {
 	}
 
 	private function showStorage(back:Function, storage:Array, range:Object, text:String):Block {
+		var buttonData:Array = [];
 		var base:Block = new Block({
 			layoutConfig: {
 				type: Block.LAYOUT_FLOW
@@ -1004,6 +1014,7 @@ public class Inventory extends BaseContent {
 			mainView.hookButton(button);
 			block.addElement(button);
 			new DragButton(storage, x, button, range.acceptable);
+			buttonData.push(button.buttonData());
 		}
 		block.doLayout();
 		tf.height = block.height;
@@ -1012,6 +1023,7 @@ public class Inventory extends BaseContent {
 		base.addElement(block);
 		base.doLayout();
 		invenPane.addElement(base);
+		stashTexts.push([text, buttonData]);
 		return base;
 	}
 
@@ -1249,6 +1261,7 @@ class DragButton {
 	private var _origin:Point;
 	private var _parent:DisplayObjectContainer;
 	private var _stage:Stage;
+	private var _selected:Boolean = false;
 	private var _dragging:Boolean = false;
 	private var _tweening:Boolean = false;
 	private var _xTween:TweenListener;
@@ -1304,6 +1317,7 @@ class DragButton {
 	private function resetPosition():void {
 		_tweening = false;
 		_dragging = false;
+		_selected = false;
 		_parent.addChild(_button);
 		_origin = _parent.globalToLocal(_origin);
 		_button.stopDrag();
@@ -1358,10 +1372,11 @@ class DragButton {
 	}
 
 	private function dragHandler(e:MouseEvent):void {
-		if (!_button.enabled || _dragging) {return;}
+		if (!_button.enabled || _dragging || _selected) {return;}
 		if (_tweening) {
 			resetPosition();
 		}
+		_selected = true;
 		_parent = this._button.parent;
 		_origin = _parent.localToGlobal(new Point(_button.x, _button.y));
 		_stage  = _parent.stage;
diff --git a/classes/classes/display/GameView.as b/classes/classes/display/GameView.as
new file mode 100644
index 0000000000000000000000000000000000000000..848107bfbffd91c78adbd0110441eb82dd6e43cf
--- /dev/null
+++ b/classes/classes/display/GameView.as
@@ -0,0 +1,6 @@
+package classes.display {
+public interface GameView {
+    function clear():void
+    function flush():void
+}
+}
diff --git a/classes/classes/display/GameViewData.as b/classes/classes/display/GameViewData.as
new file mode 100644
index 0000000000000000000000000000000000000000..5dec1ea5c3ce270feb17a6ac74915a1c2c65bdd3
--- /dev/null
+++ b/classes/classes/display/GameViewData.as
@@ -0,0 +1,120 @@
+package classes.display {
+import mx.binding.utils.BindingUtils;
+
+public class GameViewData {
+    // TODO: Create a screen data class / interface instead of these fields? Would allow different types to describe themselves
+    // TODO: Consider making fields bindable rather than using an observer setup?
+    // TODO: Create data classes instead of using dynamic objects
+    // TODO: Readme.md
+
+    // FIXME? Multiple clears and flushes can occur from a single scene call
+    // Potentially have UI pass callbacks back to GameViewData to call, determine if clear is called, and call flush after?
+    // This would reduce the number of calls down to a single clear and flush on the UI
+    // Would also need some sort of error handling on the callback
+
+    /**
+     * Indicates how the screen data should be displayed
+     *
+     * Required types:
+     *  1. Main Menu
+     *  2. Default text display
+     *  3. Options menu
+     *  4. Stash view
+     *  5. Dungeon map
+     *
+     * Optional types:
+     *  1. Binds menu TODO: Make this part of a screen dependent options menu? Not all views handle binds (Mobile, Console)
+     *  2. Debug - Scene Builder
+     *  3. Debug - Save Edit
+     *
+     * TODO: Consider having these as methods on the GameView interface instead?
+     */
+    public static var screenType:*;
+
+    public static const DEFAULT:int      = 0;
+    public static const MAIN_MENU:int    = 1;
+    public static const OPTIONS_MENU:int = 2;
+    public static const STASH_VIEW:int   = 3;
+    public static const DUNGEON_MAP:int  = 4;
+
+    public static var menuButtons:/*ButtonData*/Array = [];
+    public static var bottomButtons:/*ButtonData*/Array = [];
+
+    /**
+     * The preparsed text that should be displayed in the default text view
+     *
+     * Note that this may contain font, colour, and formatting tags which many need to be accounted for
+     */
+    public static var htmlText:String;
+
+    /**
+     * The image pack text. For UI that support in-text images
+     */
+    public static var imageText:String;
+
+    // TODO: Create an input class?
+    /**
+     * Whether a text input is needed from the player, ie the name box
+     */
+    public static var inputNeeded:Boolean;
+    /**
+     * The text contents of the text input
+     * May contain a default value
+     */
+    public static var inputText:String;
+
+    /**
+     * @see StatsView for layout
+     */
+    public static var playerStatData:*;
+    public static var monsterStatData:*;
+
+    /**
+     * Inventory / Stash display data
+     *
+     * ```
+     * [
+     *  ["Container Description", [buttonData]]
+     * ]
+     * ```
+     */
+    public static var stashData:*;
+
+    public static var settingPaneData:SettingPaneData;
+
+    // TODO: Minimap
+    // See DungeonMap.as for layout
+    public static var mapData:*;
+
+    // Main Menu data (currently only buttons)
+    // TODO: Version, warning, credits, etc
+    public static var menuData:* = [];
+
+
+    private static var views:Vector.<GameView> = new Vector.<GameView>();
+    public static function clear():void {
+        for each (var view:GameView in views) {
+            view.clear();
+        }
+    }
+
+    public static function flush():void {
+        for each (var view:GameView in views) {
+            view.flush();
+        }
+    }
+
+    public static function subscribe(view:GameView):void {
+        views.push(view);
+    }
+
+    public static function unsubscribe(view:GameView):void {
+        var index:int = views.indexOf(view);
+        if (index > 0) {
+            views.removeAt(index);
+        }
+    }
+
+    // TODO: Add event notification? Need to be able to pass link events back to their listeners
+}
+}
diff --git a/classes/classes/display/SettingData.as b/classes/classes/display/SettingData.as
new file mode 100644
index 0000000000000000000000000000000000000000..9c3d3cddcec4f117224915b73b166ad7df069568
--- /dev/null
+++ b/classes/classes/display/SettingData.as
@@ -0,0 +1,64 @@
+package classes.display {
+import coc.view.ButtonData;
+
+public class SettingData {
+    public function SettingData(name:String, opts:Array) {
+        this.name = name;
+        this._options = new <OptionData>[];
+        for (var i:int = 0; i < opts.length; i++) {
+            var opt:* = opts[i];
+            if (opt is String) {
+                if (opt == "overridesLabel") {
+                    this._defaultLabel = _options[i - 1].description;
+                }
+                continue;
+            }
+            var option:OptionData = new OptionData(opt);
+            _options.push(option);
+            if (option.isSet) {
+                this._label = option.description;
+                this.currentValue = option.name;
+            }
+        }
+    }
+
+    public var name:String;
+    public var currentValue:String;
+    private var _options:Vector.<OptionData>;
+    private var _defaultLabel:String = "";
+    private var _label:String;
+
+    // FIXME: This is meant to be the description only, but the "overridesLabel" option overrides the entire label
+    public function get label():String {
+        if (this._label == null) {
+            return _defaultLabel;
+        }
+        return _label;
+    }
+
+    public function get buttons():Vector.<ButtonData> {
+        var buttons:Vector.<ButtonData> = new <ButtonData>[];
+        for each (var opt:OptionData in _options) {
+            buttons.push(opt.buttonData);
+        }
+        return buttons;
+    }
+}
+}
+
+import coc.view.ButtonData;
+
+class OptionData {
+    public function OptionData(data:Array) {
+        name        = data[0];
+        onSelect    = data[1];
+        description = data[2];
+        isSet       = data[3];
+        buttonData = new ButtonData(name, onSelect).disableIf(isSet);
+    }
+    internal var name:String;
+    internal var onSelect:Function;
+    internal var description:String;
+    internal var isSet:Boolean;
+    internal var buttonData:ButtonData;
+}
diff --git a/classes/classes/display/SettingPane.as b/classes/classes/display/SettingPane.as
index 30b3ad01ef3932ad673a953562f9df826b20cea3..3741773ab411b39edf2bd5446fd3aa00ec1ccfbc 100644
--- a/classes/classes/display/SettingPane.as
+++ b/classes/classes/display/SettingPane.as
@@ -111,7 +111,17 @@ public class SettingPane extends ScrollPane {
 		return helpLabel;
 	}
 
+	private var labels:Array = [];
+	public var settingData:Array = [];
 	public function addOrUpdateToggleSettings(label:String, args:Array):BindDisplay {
+		var pos:int = labels.indexOf(label);
+		if (pos >= 0) {
+			settingData[pos] = [label, args];
+		} else {
+			settingData.push([label, args]);
+			labels.push(label);
+		}
+
 		var i:int;
 		if (_content.getElementByName(label) != null) {
 			var existingSetting:BindDisplay = _content.getElementByName(label) as BindDisplay;
diff --git a/classes/classes/display/SettingPaneData.as b/classes/classes/display/SettingPaneData.as
new file mode 100644
index 0000000000000000000000000000000000000000..037549035c8ee5a1d7fb0c24b9f89f0a11b0b8f6
--- /dev/null
+++ b/classes/classes/display/SettingPaneData.as
@@ -0,0 +1,23 @@
+package classes.display {
+public class SettingPaneData {
+    public function SettingPaneData(paneConfig:Array, settingConfig:*) {
+        this.name        = paneConfig[0];
+        this.buttonName  = paneConfig[1];
+        this.title       = paneConfig[2];
+        this.description = paneConfig[3];
+        this.global      = paneConfig[4];
+
+        this.settings = new <SettingData>[];
+        for each (var data:Array in settingConfig) {
+            this.settings.push(new SettingData(data[0], data[1]));
+        }
+    }
+
+    public var name:String;       // Used by GameSettings, can ignore
+    public var buttonName:String; // Button used to get to this panel, can be ignored
+    public var title:String;      // Title to display at top of pane
+    public var description:String;// Help text that goes under the title
+    public var global:Boolean;    // Used by GameSettings, can ignore
+    public var settings:Vector.<SettingData>; // The settings to show on the pane
+}
+}
diff --git a/classes/coc/view/ButtonData.as b/classes/coc/view/ButtonData.as
index 963059984003813bbe9f35456c227fd2d623bd2b..50cba8c3fbc4a4a72372ccdb642e7151a2c02120 100644
--- a/classes/coc/view/ButtonData.as
+++ b/classes/coc/view/ButtonData.as
@@ -13,12 +13,13 @@ public class ButtonData {
 	public var toolTipHeader:String = "";
 	public var toolTipText:String = "";
 
-	public function ButtonData(text:String, callback:Function = null, toolTipText:String = "", toolTipHeader:String = "", enabled:Boolean = true) {
+	public function ButtonData(text:String, callback:Function = null, toolTipText:String = "", toolTipHeader:String = "", enabled:Boolean = true, visible:Boolean = true) {
 		this.text = text;
 		this.callback = callback;
 		this.enabled = (callback != null && enabled);
 		this.toolTipText = toolTipText;
 		this.toolTipHeader = toolTipHeader;
+		this.visible = visible;
 	}
 
 	public function hint(toolTipText:String = "", toolTipHeader:String = ""):ButtonData {
diff --git a/classes/coc/view/ButtonDataList.as b/classes/coc/view/ButtonDataList.as
index 9facdd71733aea479e34251fdf69c644fa6ac68e..17c388095f3fc4eb0aad1e3a13c3b45613b3ab2b 100644
--- a/classes/coc/view/ButtonDataList.as
+++ b/classes/coc/view/ButtonDataList.as
@@ -125,6 +125,7 @@ public class ButtonDataList {
 		if (back != null) {
 			kGAMECLASS.output.button(exitPosition).show(exitName, back, "", "", true);
 		}
+		kGAMECLASS.output.flush();
 	}
 
 	public function submenuReturn(to:int = -1):void {
diff --git a/classes/coc/view/CoCButton.as b/classes/coc/view/CoCButton.as
index 8fe4466da006acabd45564da9d27989561e300a7..bef4b3d71225e41f7c008b308fc0db8256f030f9 100644
--- a/classes/coc/view/CoCButton.as
+++ b/classes/coc/view/CoCButton.as
@@ -66,7 +66,7 @@ public class CoCButton extends Block implements ThemeObserver {
 	public function CoCButton(options:Object = null) {
 		super();
 		initButton(options);
-		if (!dummy) Theme.subscribe(this);
+//		if (!dummy) Theme.subscribe(this);
 	}
 
 	/**
@@ -500,8 +500,7 @@ public class CoCButton extends Block implements ThemeObserver {
 	}
 
 	public function buttonData():ButtonData {
-		var bd:ButtonData = new ButtonData(labelText, callback, toolTipText, toolTipHeader, enabled);
-		return bd;
+		return new ButtonData(labelText, callback, toolTipText, toolTipHeader, enabled, visible);
 	}
 
 	public function pushData():void {
diff --git a/classes/coc/view/Console.as b/classes/coc/view/Console.as
new file mode 100644
index 0000000000000000000000000000000000000000..4df64bfa8d727a14f011e058fce566f098efc909
--- /dev/null
+++ b/classes/coc/view/Console.as
@@ -0,0 +1,889 @@
+package coc.view {
+
+import classes.EventParser;
+import classes.GlobalFlags.kGAMECLASS;
+import classes.InputManager;
+import classes.Output;
+import classes.Scenes.Dungeons.DungeonRoomConst;
+import classes.display.GameView;
+import classes.display.GameViewData;
+import classes.display.SettingData;
+import classes.internals.Utils;
+
+import flash.display.BitmapData;
+import flash.display.Sprite;
+import flash.events.FocusEvent;
+import flash.events.KeyboardEvent;
+import flash.events.TextEvent;
+import flash.geom.ColorTransform;
+import flash.geom.Matrix;
+import flash.text.TextField;
+import flash.text.TextFieldType;
+import flash.text.TextFormat;
+import flash.ui.Keyboard;
+import flash.utils.getQualifiedClassName;
+
+import mx.utils.StringUtil;
+
+// TODO: Remove references to kGAMECLASS
+// TODO: Handle choosing multi combat targets (handled by click in default interface)
+// TODO: Better link handling -> GameViewData should perhaps have a handler for this
+// TODO: -> Handle GameView clears and flushes better, which may occur partway through a scene instead of only at the start and end
+// TODO: Save any console specific settings
+// TODO: Make console themeable
+// TODO: Max buffer length?
+// TODO: Virtual Keyboard for mobile build
+// TODO: Handle paste?
+// TODO: Handle buttons with duplicate labels? (Current selects the first matching one)
+// TODO: Text formatting? (Consider splitting the parser into a content parser, and a formatting parser?)
+// TODO: Better defined screen separation?
+// TODO: Additional meta Console commands? (quick save and load, as their hotkeys are disabled)
+// TODO: Console options (Autocomplete on enter?)
+// TODO: Ability to bring input text prompt back up and edit?
+// TODO: Inline images from image pack
+// TODO: Chronicler "You could have an ASCII version of the main menu logo scroll/drop/print into view line by line on bootup."
+// TODO: Fix text highlighting, if possible without ruining color (This is unlikely without going overboard - Tried TLF, introduced very noticeable lag)
+public class Console extends Sprite implements GameView {
+    // Fixed width fonts have display issues on Windows unless embedded
+    // Hopefully will be able to find some sort of workaround, but for now only allow embedded fonts
+    [Embed(source='../../../res/ui/SourceCodePro-Semibold.otf', advancedAntiAliasing='true', fontName='Source Code Pro', embedAsCFF='false')]
+    private static const FONT:Class;
+
+    // TODO: Move these into theme
+    private static const FGC:uint = Color.convertColor("#17A88B");
+    private static const BGC:uint = Color.convertColor("#1E2229");
+    private static const PRC:uint = Color.convertColor("#44853A");
+    private static const CMC:uint = Color.convertColor("#1D99f3");
+    private static const RED:uint = Color.convertColor("#ed1515");
+    private static const ORG:uint = Color.convertColor("#f67400");
+    private static const PRP:uint = Color.convertColor("#9b59b6");
+
+    private var _mainText:TextField;
+
+    // Used to prevent backspacing into content
+    private var _minLen:int = 0;
+    // Meant to keep the last screen from clearing on multiple clear events
+    private var _lastLen:int = 0;
+    // Prevents further key presses while processing a command
+    private var _locked:Boolean;
+
+    // Character data used for some line formatting
+    private var _charWidth:Number = 0;
+    private var _charHeight:Number = 0;
+    private var _charsPerLine:Number = 0;
+
+    // Auto-complete
+    private var _lastSuggestion:int = -1;
+    private var _suggestions:/*String*/Array = [];
+
+    // When true, clear entire screen instead of preserving history
+    private var _doAutoClear:Boolean = false;
+
+    // References to screens that have special display handling
+    // TODO: move or remove these if possible
+    private const doCamp:String = getQualifiedClassName(kGAMECLASS.camp) + "/doCamp";
+    private const combatMenu:String = getQualifiedClassName(kGAMECLASS.combat) + "/combatMenu";
+
+    // List of buttons with special prefixes
+    // Used for clarity on screens like the Stash and Options where buttons have additional external labels such as in
+    // the options screens or stash menu
+    private var specialPrefixed:Array = [];
+
+    // Meta console commands
+    // Note that there is an error message for providing too many arguments. Commands should handle their own error
+    // message when there are too few arguments, however.
+    // Func can return true which will cause flush to be called after applying the function
+    private var _metaCommands:* = {
+        "_hidegame" :{func:Utils.curry(setBGOpacity, 1.00), help:"Hides the default UI if rendered behind the console"},
+        "_showgame" :{func:Utils.curry(setBGOpacity, 0.75), help:"Will render the default UI behind the console, useful for debugging"},
+        "_stats"    :{func:showStats,       help:"Displays player and monster stats"},
+        "_debug"    :{func:callDebug,       help:"Opens the debug menu, if possible"},
+        "_clear"    :{func:clearScreen,     help:"Clears and re-displays the screen"},
+        "_autoclear":{func:toggleAutoClear, help:"Toggles calling _clear on every scene"},
+        "_tt"       :{func:showToolTip,     help:"Shows the tooltip text for a button. Has tab completion."},
+        "_help"     :{func:showHelp,        help:"Display this menu"}
+    }
+
+    // Whether or not a prompt has been shown
+    // Used to avoid a double prompt from the flush event and key handler
+    private var _prompted:Boolean = false;
+
+    public function Console(height:int, width:int) {
+        GameViewData.subscribe(this);
+        _mainText = new TextField();
+        _mainText.type = TextFieldType.DYNAMIC;
+        _mainText.width = width;
+        _mainText.height = height;
+        _mainText.wordWrap = true;
+        _mainText.multiline = true;
+        _mainText.selectable = true;
+        _mainText.embedFonts = true;
+
+        var tf:TextFormat = _mainText.defaultTextFormat;
+        tf.font = new FONT().fontName;
+        tf.color = FGC;
+        tf.size = 16;
+        tf.leading = -4;
+        _mainText.defaultTextFormat = tf;
+        _mainText.textColor = FGC;
+
+        addChild(_mainText);
+        setBGOpacity(1.0);
+
+        // Overrides key listeners on stage, may need to be adjusted to only do this on the console if it is ever set to
+        // display alongside other UI
+        kGAMECLASS.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown, false, 99);
+        kGAMECLASS.stage.addEventListener(FocusEvent.KEY_FOCUS_CHANGE, onFocusChange);
+
+        updateCharInfo();
+    }
+
+    public function dispose():void {
+        kGAMECLASS.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
+        kGAMECLASS.stage.removeEventListener(FocusEvent.KEY_FOCUS_CHANGE, onFocusChange);
+        GameViewData.unsubscribe(this);
+    }
+
+    // Prevents the tab character from highlighting the buttons hidden behind the console
+    private static function onFocusChange(e:FocusEvent):void {
+        e.preventDefault();
+    }
+
+    private function setBGOpacity(percent:Number):void {
+        this.graphics.clear();
+        this.graphics.beginFill(BGC, percent);
+        this.graphics.drawRect(0, 0, width, height);
+        this.graphics.endFill();
+    }
+
+    // Calculates the char width and height, and the number of characters that can fit in a line
+    private function updateCharInfo():void {
+        var tf:TextField = new TextField();
+        tf.defaultTextFormat = _mainText.defaultTextFormat;
+        tf.embedFonts = _mainText.embedFonts;
+        tf.appendText("W");
+        _charWidth = tf.textWidth;
+        _charHeight = tf.textHeight;
+        _charsPerLine = Math.floor((width - 4) / _charWidth);
+    }
+
+    private function onKeyDown(event:KeyboardEvent = null):void {
+        if (_locked) {
+            event.stopImmediatePropagation();
+            return;
+        }
+
+        const len:int = _mainText.text.length;
+        const str:String = String.fromCharCode(event.charCode);
+
+        switch (event.charCode) {
+            case Keyboard.BACKSPACE: {
+                if (len > _minLen) {
+                    _mainText.replaceText(len - 1,len,"");
+                }
+                break;
+            }
+            case Keyboard.ENTER: {
+                _locked = true;
+                _lastLen = _mainText.length;
+                var needFlush:Boolean = handleCommand(readCommand());
+                if (!_prompted) {
+                    showPrompt();
+                }
+                _locked = false;
+                if (needFlush) {
+                    flush();
+                }
+                break;
+            }
+            case Keyboard.TAB: {
+                autoComplete();
+                break;
+            }
+            default: {
+                var restricted:String = StringUtil.restrict(str, "\u0020-\u007E\n");
+                if (restricted.length > 0) {
+                    appendColoredText(FGC, restricted);
+                }
+            }
+        }
+
+        if (event.keyCode != Keyboard.TAB) {
+            _lastSuggestion = -1;
+        }
+
+        // Limits the number of lines
+//        if (_mainText.numLines > 10) {
+//            _mainText.replaceText(0, _mainText.getLineOffset(_mainText.numLines - 10), "");
+//        }
+
+        event.stopImmediatePropagation();
+    }
+
+    private function autoComplete():void {
+        if (needName()) {
+            return;
+        }
+        if (_lastSuggestion < 0) {
+            var command:String = readCommand();
+            var prefix:String = "";
+            if (command.match(/^_tt/)) {
+                prefix = "_tt ";
+                command = StringUtil.trim(command.substr(4));
+            }
+            var startsWith:RegExp = new RegExp("^" + command, "i");
+            _suggestions = availableLabels()
+            if (prefix == "_tt ") {
+                _suggestions = _suggestions.concat(disabledLabels());
+            }
+            _suggestions = _suggestions.filter(function (item:String, index:int, array:Array):Boolean {
+                return startsWith.test(item);
+            }).map(function (item:String, index:int, array:Array):String {
+                return prefix + item;
+            });
+        }
+
+        if (_suggestions.length > 0) {
+            _lastSuggestion = (_lastSuggestion + 1) % _suggestions.length;
+            _mainText.replaceText(_minLen, _mainText.length, _suggestions[_lastSuggestion]);
+        }
+    }
+
+    private function readCommand():String {
+        return StringUtil.trim(_mainText.text.slice(_minLen, _mainText.length));
+    }
+
+    private static function callDebug():Boolean {
+        // FIXME: Expose the function directly instead of sending key events
+        var im:InputManager = kGAMECLASS.inputManager;
+        im.KeyHandler(new KeyboardEvent("", true, false, 0, Keyboard.D));
+        im.KeyHandler(new KeyboardEvent("", true, false, 0, Keyboard.E));
+        im.KeyHandler(new KeyboardEvent("", true, false, 0, Keyboard.B));
+        im.KeyHandler(new KeyboardEvent("", true, false, 0, Keyboard.U));
+        im.KeyHandler(new KeyboardEvent("", true, false, 0, Keyboard.G));
+        return true;
+    }
+
+    private function clearScreen():void {
+        _mainText.htmlText = "";
+        displayMain();
+    }
+
+    private function toggleAutoClear():void {
+        _doAutoClear = !_doAutoClear;
+        appendColoredText(ORG, "\nAutoclear turned " + (_doAutoClear? "on" : "off"));
+    }
+
+    private function handleCommand(command:String):Boolean {
+        _prompted = false;
+        if (needName()) {
+            // FIXME: this should be handled elsewhere / removed
+            kGAMECLASS.mainView.nameBox.text = command;
+            GameViewData.inputText = command;
+            if (!needName()) {
+                showAvailableCommands();
+            }
+            return false;
+        }
+
+        var cmdLower:String = command.toLowerCase();
+        var metaSplit:Array = cmdLower.split(/\s+/g);
+        if (cmdLower in _metaCommands || (metaSplit.length > 1 && metaSplit[0] in _metaCommands)) {
+            var cmd:String = metaSplit.shift();
+            var func:Function = _metaCommands[cmd].func;
+            try {
+                return func.apply(this, metaSplit);
+            } catch (e:ArgumentError) {
+                appendColoredText(ORG, "There were too many parameters supplied for the command!");
+            }
+            return false;
+        }
+
+        var buttons:/*ButtonData*/Array = getCommandButtons(cmdLower);
+
+        // In cases of multiple labels, we always select the first one
+        if (buttons.length >= 1) {
+            CONFIG::debug {
+                // Don't attempt to handle exceptions in debug build, let debugger catch them instead
+                buttons[0].callback.call();
+            }
+            CONFIG::release {
+                // If something goes wrong show error output. If there are no options attempt to drop to player menu
+                // TODO: Instructions on how to report an error?
+                // TODO: Break out to Main Menu instead? In case playerMenu also errors
+                try {
+                    buttons[0].callback.call();
+                } catch (e:Error) {
+                    appendColoredText(RED, "\n" + e.getStackTrace());
+                    if (availableLabels().length == 0) {
+                        appendColoredText(RED, "\n\nThe scene is no longer functioning due to this error!! " +
+                                "Entering next will attempt to return to the player menu" +
+                                "\nThis can cause issues in the game and should likely not be saved!");
+                        kGAMECLASS.output.doNext(EventParser.playerMenu);
+                    }
+                }
+            }
+            return true;
+        }
+
+        var links:/*String*/Array = availableLinks().filter(function (item:String, index:int, array:Array):Boolean {
+            return item.toLowerCase() == cmdLower;
+        });
+        // FIXME: create an event notifier for links instead of directly dispatching on mainView
+        if (links.length > 0) {
+            kGAMECLASS.mainView.mainText.dispatchEvent(new TextEvent(TextEvent.LINK,false,false, links[0]));
+            return true;
+        }
+
+        if (command.length > 0) {
+            appendColoredText(FGC, "\nCommand \"" + command + "\" was not understood.");
+            showAvailableCommands();
+        }
+        return false;
+    }
+
+    private function getCommandButtons(command:String, includeDisabled:Boolean = false):/*ButtonData*/Array {
+        var buttons:Array = normalButtons();
+        if (includeDisabled) {
+            buttons = buttons.concat(normalButtons(false));
+        }
+
+        buttons = buttons.concat(specialPrefixed.filter(function (button:ButtonData, i:int, a:Array):Boolean {
+            return button.visible && (button.enabled || includeDisabled);
+        }));
+
+        return buttons.filter(function (item:ButtonData, index:int, array:Array):Boolean {
+            return StringUtil.trim(item.text.replace(/\s/g, "-").toLowerCase()) == command.toLowerCase()
+        });
+    }
+
+    // TODO: Implement Monster stat tooltips
+    private function showToolTip(command:String = null):void {
+        const error:String = "\nUnknown command. Usage: _tt [command]";
+        if (!command || command.length <= 0) {
+            appendColoredText(ORG, error);
+            return;
+        }
+        command = StringUtil.trim(command);
+        var buttons:Array = getCommandButtons(command, true);
+        if (buttons.length == 0) {
+            appendColoredText(ORG, error);
+            return;
+        }
+        var tf:TextField = new TextField();
+
+        // Only show the first match. Hopefully any time that there would be duplicates they would have the same text
+        var button:ButtonData = buttons[0];
+        // Strip off any parser tags and html formatting in the buttons
+        tf.htmlText = kGAMECLASS.parser.parse(button.toolTipHeader);
+        appendColoredText(CMC, "\n" + tf.getRawText() + "\n" + StringUtil.repeat("-", tf.getRawText().length));
+        tf.htmlText = kGAMECLASS.parser.parse(button.toolTipText);
+        appendColoredText(FGC, "\n" + tf.getRawText());
+    }
+
+    private function showStats():void {
+        var text:String = "";
+        var maxName:int = 0;
+        var maxValue:int = 0;
+        for each (var statData:* in GameViewData.playerStatData.stats) {
+            maxName = Math.max(maxName, statData.name.length);
+            maxValue = Math.max(maxValue, statData.max.toString().length);
+        }
+        maxName += 4;
+        for each (statData in GameViewData.playerStatData.stats) {
+            text += statDataText(statData, maxName, maxValue);
+        }
+        appendColoredText(CMC, text);
+        showMonsterStats(maxName, maxValue);
+    }
+
+    private function showMonsterStats(maxName:int = 0, maxValue:int = 0):void {
+        var text:String = "";
+        for each (var monster:* in GameViewData.monsterStatData) {
+            for each (var stat:* in monster.stats) {
+                maxName = Math.max(maxName, stat.name.length);
+                maxValue = Math.max(maxValue, stat.max.toString().length);
+            }
+            text += "\n" + StringUtil.repeat("-", maxName + maxValue + maxValue + 1) + "\n" + monster.name;
+            for each (stat in monster.stats) {
+                text += statDataText(stat, maxName, maxValue);
+            }
+        }
+        appendColoredText(ORG, text);
+    }
+
+    private static function statDataText(data:*, nameLen:int, valueLen:int):String {
+        var text:String = "\n" + padRight(data.name, nameLen) + padLeft(Math.floor(data.value).toString(), valueLen);
+        if (data.showMax) {
+            text += "/" + padLeft(data.max.toString(), valueLen);
+        }
+        return text;
+    }
+
+    private static function padLeft(text:String, length:int, character:String = " "):String {
+        return StringUtil.repeat(character, Math.max(0, length - text.length)) + text;
+    }
+
+    private static function padRight(text:String, length:int, character:String = " "):String {
+        return text + StringUtil.repeat(character, Math.max(0, length - text.length));
+    }
+
+    private function displayMain():void {
+        if (_doAutoClear) {
+            _mainText.htmlText = "";
+        }
+
+        // Reset, allow any screen handler to fill
+        specialPrefixed = [];
+
+        var specialHandled:*;
+        switch (GameViewData.screenType) {
+            case GameViewData.MAIN_MENU    : specialHandled = handleMainMenu();      break;
+            case GameViewData.OPTIONS_MENU : specialHandled = handleEnterSettings(); break;
+            case GameViewData.STASH_VIEW   : specialHandled = handleStorage();       break;
+            case GameViewData.DUNGEON_MAP  : specialHandled = handleDungeonMap();    break;
+            default: specialHandled = false;
+        }
+
+        // If the handler returns true, that means it's already displayed the screen text
+        if (!specialHandled) {
+            var tf:TextField = new TextField();
+
+            // Some older screens use \r instead of \n (notably the save menu). These get cleared when set using htmlText so replace them now
+            tf.htmlText = GameViewData.htmlText.replace(/\r/g, "\n");
+
+            // Get the raw text to remove any unwanted formatting from the HTML.
+            var text:String = tf.getRawText();
+
+            // Remove the fancy formatting
+            text = text.replace(/\u2019/g, "'");
+            text = text.replace(/\u201d/g, "\"");
+            text = text.replace(/\u201c/g, "\"");
+            text = text.replace(/\u2014/g, "--");
+            text = text.replace(/\u2500/g, "-"); // Box drawing horizontal line "─". Used in some combat screens.
+
+            // Interestingly, TextField.text appears to return \r for newlines (at least on Linux), while getRawText returns \n
+            // When output via TextField.appendText(), \r does not count towards the length, causing issues with subsequent
+            // appends and other operations, so they need to be replaced with \n.
+            // Even though getRawText should return \n instead of \r, replace \r anyway just to be sure.
+            text = text.replace(/\r/g, "\n");
+
+            // Restrict to ASCII 38 (Space) through 126 (Tilde).
+            // TODO: Find any extra special characters that are used in text that should be allowed or replaced
+            text = StringUtil.restrict(text, "\u0020-\u007E\n");
+            appendColoredText(FGC, "\n" + text);
+        }
+
+        // TODO: avoid showing in special screens?
+        // Write out any player stat changes as there is no statBar on screen to notify them
+        text = "";
+        if (GameViewData.playerStatData && GameViewData.playerStatData.stats != undefined) {
+            for each (var stat:* in GameViewData.playerStatData.stats) {
+                if (stat.name == "Level:" || !(stat.isUp || stat.isDown)) {
+                    continue;
+                }
+                text += StringUtil.substitute("\nYour {0} has {1} to {2}{3}",
+                        stat.name.replace(":", ""),
+                        stat.isUp? "increased" : "decreased",
+                        Math.floor(stat.value),
+                        stat.showMax ? "/" + stat.max : ""
+                );
+            }
+        }
+
+        // Since the level upArrow does not clear until the level-up menu is visited, only show this message in camp
+        // as the player is unable to level up elsewhere, and that would cause level up messages to show on every
+        // screen until they could get back to camp.
+        // TODO: Consider using the level up button as an indicator instead of checking against doCamp?
+        if (Output.currentScene == doCamp) {
+            // FIXME: Safety
+            stat = GameViewData.playerStatData.stats.filter(function (s:*, i:int, a:Array):* {
+                return s.name == "Level:";
+            })[0];
+            if (stat.isUp) {
+                text += "\nYou have enough experience to level up."
+            }
+        }
+        if (text.length > 0) {
+            appendColoredText(CMC, "\n" + text);
+        }
+        if (Output.currentScene == combatMenu) {
+            showMonsterStats();
+        }
+        showAvailableCommands();
+    }
+
+    // NOTE: For some reason the color can carry over into the default text format, even if the default text format is reset
+    // This happens even when using html text and ending the font tag, so use appendText to avoid needing to replace characters
+    private function appendColoredText(color:uint, text:String):void {
+        var startLen:int = _mainText.text.length
+        _mainText.appendText(text);
+        if (startLen == _mainText.length) {
+            return;
+        }
+        var tf:TextFormat = _mainText.getTextFormat(startLen, _mainText.length);
+        tf.color = color;
+        _mainText.setTextFormat(tf, startLen, _mainText.text.length);
+
+        // The Windows version of Flash cares a lot about where the cursor is in the window, and will scroll the text
+        // back up to show that position, even if the cursor is disabled. So we'll just set it to the end every time
+        // any text is appended
+        _mainText.setSelection(_mainText.length, _mainText.length);
+    }
+
+    private function showAvailableCommands():void {
+        if (!needName()) {
+            appendColoredText(FGC, "\n\nAvailable Commands are:");
+            listCommands(availableLabels());
+            var disabled:Array = disabledLabels();
+            if (disabled.length > 0) {
+                appendColoredText(FGC, "\n\nDisabled Commands are:");
+                listCommands(disabledLabels());
+            }
+        }
+    }
+
+    private function listCommands(labels:/*String*/Array):void {
+        const commandsPerLine:int = 5;
+        const charsPerCommand:int = Math.floor(_charsPerLine / commandsPerLine);
+        const lineLength:int = commandsPerLine * charsPerCommand;
+        var commands:String = "";
+        var line:String = "";
+        for (var i:int = 0; i < labels.length; i++) {
+            var command:String = labels[i];
+            command = padRight(command, Math.ceil(command.length/charsPerCommand) * charsPerCommand);
+            if (line.length + command.length > lineLength) {
+                commands += "\n" + line;
+                line = "";
+            }
+            line += command;
+        }
+        commands += "\n" + line + "\n";
+        appendColoredText(CMC, commands);
+    }
+
+    private static function get timeText():String {
+        if (!GameViewData.playerStatData) {
+            return "";
+        }
+        var time:* = GameViewData.playerStatData.time;
+
+        return "D" + time.day + "@" + padLeft(time.hour, 2, "0") + ":" + time.minutes + time.ampm;
+    }
+
+    private function showPrompt():void {
+        _prompted = true;
+        if (needName()) {
+            appendColoredText(PRC, "\nName:")
+        } else {
+            appendColoredText(PRC, "\n[" + Output.currentScene + "]" + timeText + ">");
+        }
+        // For some reason, outputting this last space in PRC can cause the default text color to stay PRC, even when
+        // the default text format is reset at the end of appendColoredText
+        appendColoredText(FGC, " ");
+        _minLen = _mainText.text.length;
+        var labels:Array = availableLabels();
+        if (labels.length == 1) {
+            appendColoredText(FGC, labels[0]);
+        }
+    }
+
+    // This also picks up the note box in the save screen, meaning all saves would require a note be entered
+    // Fixme: Move this into local variable set on flush to prevent default values from skipping input options?
+    private static function needName():Boolean {
+        return GameViewData.inputNeeded && GameViewData.inputText == "";
+    }
+
+    private function availableLabels():/*String*/Array {
+        return normalButtons().map(labelMap)
+                .concat(specialPrefixed.filter(function (button:ButtonData, i:int, array:Array):Boolean {
+                    return button.enabled && button.visible;
+                }).map(labelMap))
+                .concat(availableLinks());
+    }
+
+    private function disabledLabels():/*String*/Array {
+        return normalButtons(false).map(labelMap);
+    }
+
+    private static const labelMap:Function = function (item:ButtonData, index:int = 0, array:Array = null):String {
+        return cleanLabel(item.text);
+    }
+//    private static function labelMap (item:ButtonData, index:int = 0, array:Array = null):String {
+//        return cleanLabel(item.text);
+//    }
+
+    private function normalButtons(enabled:Boolean = true):/*ButtonData*/Array {
+        return GameViewData.bottomButtons
+                .concat(GameViewData.menuButtons)
+                .filter(function (item:ButtonData, index:int, array:Array):Boolean {
+                    return item.enabled == enabled && item.visible;
+                });
+    }
+
+    private static function availableLinks():/*String*/Array {
+        const htmlText:String = GameViewData.htmlText;
+        const linkRegex:RegExp = /<a href="event:([^"]+)">/gi;
+        const links:Array = [];
+        var result:Object = linkRegex.exec(htmlText);
+
+        while (result != null) {
+            links.push(result[1]);
+            result = linkRegex.exec(htmlText);
+        }
+        return links;
+    }
+
+    /**
+     * Converts an image to ASCII and draws to screen
+     *
+     * /!\ This will clear the screen /!\
+     */
+    private function imageToAscii():void {
+        updateCharInfo();
+        // Using a string may be slower, but it is safer since out of range will return an empty string instead of an error
+        const pixelChars:String = " .,:;i1tfLCG08@";
+//        const pixelChars:String = " â–‘â–’â–“â–ˆ";
+
+        var tWidth:int = width - 4; // 4 = default TextField padding, 2 pixels on each side
+        var scale:Number = _charsPerLine / tWidth;
+        var matrix:Matrix = new Matrix();
+        matrix.scale(scale, scale * _charWidth/_charHeight);
+
+        // TODO: Get the actual bitmap data for the image that the image manager wants to display
+        var normalBMD:BitmapData = new MainView.GameLogo().bitmapData;
+
+        // Fits the image to the window with
+        var upScale:Number = tWidth / normalBMD.width;
+        matrix.scale(upScale, upScale);
+
+        //Rescale the bitmap to fit within the text box as characters
+        var bmd:BitmapData = new BitmapData(normalBMD.width * matrix.a, normalBMD.height * matrix.d, true, 0x000000);
+        var ctf:ColorTransform = new ColorTransform(1,1,1)
+        bmd.draw(normalBMD, matrix, ctf, null, null, true);
+
+        var prevColor:uint = bmd.getPixel(0,0);
+        var htmlText:String = "<FONT COLOR='" + colorUintToString(prevColor) + "'>";
+        for (var y:int = 0; y < bmd.height; y++) {
+            for (var x:int = 0; x < bmd.width; x++) {
+                var pixelValue:uint = bmd.getPixel32(x, y);
+                var alpha:uint = pixelValue >> 24 & 0xFF;
+                var color:uint = pixelValue & 0x00FFFFFF;
+                var converted:String = pixelChars.charAt(Math.round((alpha / 255) * (pixelChars.length - 1)));
+                if (color != prevColor) {
+                    htmlText += "</FONT><FONT COLOR='" + colorUintToString(color) + "'>"
+                    prevColor = color;
+                }
+                htmlText += converted;
+            }
+            htmlText += "\n";
+        }
+        htmlText += "</FONT>";
+
+        // This looks weird, but setting it to empty string first solves some text layout issues. I do not know why.
+        // TODO: Check for workaround to allow appending to the existing text without breaking the formatting
+        _mainText.htmlText = "";
+        _mainText.htmlText = htmlText;
+    }
+
+    private static function colorUintToString(color:uint):String {
+        // Need to ensure the string has the correct leading zeroes, as the formatting can break entirely if it receives a
+        // Colour in the wrong format and start doing things like putting one character per line.
+        const colorFormat:String = "#000000"
+        var colorString:String = color.toString(16);
+        return colorFormat.substr(0, colorFormat.length - colorString.length) + colorString;
+    }
+
+    private static function relabelled(button:ButtonData, label:String):ButtonData {
+        return new ButtonData(cleanLabel(label), button.callback, button.toolTipText, button.toolTipHeader, button.enabled);
+    }
+
+    private static function cleanLabel(label:String):String {
+        return label.replace(/\s/g, "-");
+    }
+
+    private function handleMainMenu():void {
+        imageToAscii();
+        specialPrefixed = GameViewData.menuData.map(function(button:ButtonData, i:int, a:Array):ButtonData {
+            return relabelled(button, button.text);
+        })
+    }
+
+    private function handleEnterSettings():void {
+        specialPrefixed = [];
+        var screenText:String = "";
+
+        // FIXME: Setting labels can have formatting embedded in some situations.
+        // FIXME: Need to determine if that should be handled here or changed in the settings menu itself
+        for each (var setting:SettingData in GameViewData.settingPaneData.settings) {
+            if (setting.label != "") {
+                screenText += "\n\n" + setting.name + ": " + setting.currentValue + "\n" + setting.label;
+            }
+            for each (var button:ButtonData in setting.buttons) {
+                specialPrefixed.push(relabelled(button, StringUtil.trim(setting.name + ":" + button.text)));
+            }
+        }
+        appendColoredText(FGC, screenText);
+    }
+
+    private function handleStorage():void {
+        specialPrefixed = [];
+        for each (var arr:Array in GameViewData.stashData) {
+            appendColoredText(FGC, "\n" + arr[0].replace(/<\/?b>/g, ""));
+            listCommands(arr[1].map(function (b:ButtonData, i:int, arr:Array):String {
+                return b.text;
+            }));
+            for each (var button:ButtonData in arr[1]) {
+                specialPrefixed.push(relabelled(button, "take:" + button.text));
+            }
+        }
+    }
+
+    private function handleDungeonMap():Boolean {
+        const data:* = GameViewData.mapData;
+        if (!data.alternative) {
+            var tf:TextField = new TextField();
+            tf.htmlText = data.rawText + data.legend;
+            appendColoredText(FGC, "\n" + tf.getRawText());
+            return true;
+        }
+        // TODO: New format polishing
+        const modulo:int         = data.modulus;
+        const dungeonMap:Array   = data.layout;
+        const connectivity:Array = data.connectivity;
+        const playerLoc:int      = data.playerLoc;
+
+        const blank:String = "   ";
+        var text:String = "";
+
+        const shownCentres:* = {};
+        // FIXME: This does not handle locked rooms, and they are not always explained in text
+        for (var i:int = 0; i < dungeonMap.length; i += modulo) {
+            var upper:String = "";
+            var mid:String   = "";
+            var lower:String = "";
+
+            for (var j:int = 0; j < modulo; j++) {
+                var loc:int = i + j;
+                if (dungeonMap[loc] == DungeonRoomConst.EMPTY || dungeonMap[loc] == DungeonRoomConst.VOID) {
+                    upper += blank;
+                    mid   += blank;
+                    lower += blank;
+                    continue;
+                }
+                var c:String;
+                if (playerLoc == loc) {
+                    c = "@"
+                } else {
+                    switch (dungeonMap[loc]) {
+                        case DungeonRoomConst.OPEN_ROOM:    c = " "; break;
+                        case DungeonRoomConst.LOCKED_ROOM:  c = "L"; break;
+                        case DungeonRoomConst.STAIRSUP:     c = "^"; break;
+                        case DungeonRoomConst.STAIRSDOWN:   c = "v"; break;
+                        case DungeonRoomConst.STAIRSUPDOWN: c = "Z"; break;
+                        case DungeonRoomConst.NPC:          c = "N"; break;
+                        case DungeonRoomConst.TRADER:       c = "T"; break;
+                        default: c = "?";
+                    }
+                }
+                shownCentres[c] = true;
+
+                var conn:uint = connectivity[loc];
+                var n:String = (conn & DungeonRoomConst.N) - (conn & DungeonRoomConst.LN) > 0? "┴" : "─";
+                var s:String = (conn & DungeonRoomConst.S) - (conn & DungeonRoomConst.LS) > 0? "┬" : "─";
+                var e:String = (conn & DungeonRoomConst.E) - (conn & DungeonRoomConst.LE) > 0? "├" : "│";
+                var w:String = (conn & DungeonRoomConst.W) - (conn & DungeonRoomConst.LW) > 0? "┤" : "│";
+
+                upper += StringUtil.substitute("┌{0}┐", n);
+                mid   += StringUtil.substitute("{0}{1}{2}", w, c, e);
+                lower += StringUtil.substitute("└{0}┘", s);
+            }
+
+            // TODO: Determine if this would break any dungeons maps. Since maps are apparently square, there are sometimes blank lines
+            if (StringUtil.trim(upper).length > 0) {
+                text += StringUtil.substitute("\n{0}\n{1}\n{2}", upper, mid, lower);
+            }
+        }
+
+        const descriptions:* = {
+            "@" : "Player",
+            "L" : "Locked Room",
+            "^" : "Stairs Up",
+            "v" : "Stairs Down",
+            "Z" : "Stairs Up and Down",
+            "N" : "NPC",
+            "T" : "Trader",
+            "?" : "Unknown"
+        }
+        var legendText:String = "";
+        for (c in descriptions) {
+            if (!descriptions.hasOwnProperty(c)) {continue;} // To make the inspection happy
+            if (shownCentres.hasOwnProperty(c)) {
+                legendText += StringUtil.substitute("\n{0} - {1}", c, descriptions[c]);
+            }
+        }
+        if (legendText.length > 0) {
+            text += "\nLegend:" + legendText;
+        }
+        appendColoredText(FGC, text);
+        return true;
+    }
+
+    public function clear():void {
+        _prompted = false;
+        if (_doAutoClear) {
+            _mainText.htmlText = "";
+            _lastLen = _mainText.length;
+        }
+        // FIXME: Skip clearing when locked to prevent unintended single button displays
+        if (_locked) {
+            return;
+        }
+        _lastLen = _mainText.length;
+    }
+
+    public function flush():void {
+        // FIXME: Multiple flushes causes display oddities
+        if (_locked) {
+            return;
+        }
+        _mainText.replaceText(_lastLen, _mainText.length, "");
+        displayMain();
+        showPrompt();
+    }
+
+    /**
+     * Displays general help text and meta commands
+     */
+    private function showHelp():void {
+        appendColoredText(FGC, "\nEnter the commands listed under \"Available Commands\" at the prompt and press [enter] to execute." +
+                "\nPressing [tab] will trigger autocomplete, and pressing [tab] again will cycle through suggestions." +
+                "\n\nMeta Commands:");
+
+        var maxLength:int = 0;
+        for (var cmd:String in _metaCommands) {
+            if (!_metaCommands.hasOwnProperty(cmd)) {continue;} // Placate the inspections
+            maxLength = Math.max(maxLength, cmd.length);
+        }
+        maxLength += 4;
+        for (cmd in _metaCommands) {
+            if (!_metaCommands.hasOwnProperty(cmd)) {continue;} // Placate the inspections
+            appendColoredText(PRP, "\n" + padRight(cmd, maxLength));
+            appendColoredText(FGC, _metaCommands[cmd].help);
+        }
+
+        appendColoredText(ORG, "\n\nTo disable the console interface select the \"Console\" option in the debug menu.");
+    }
+
+    /**
+     * Display the help menu on startup.
+     * Allows the debug menu to provide the buttons without providing the screen text
+     */
+    public function startupHelp():void {
+        _mainText.htmlText = "";
+        showHelp();
+        displayMain();
+        showPrompt();
+    }
+}
+}
\ No newline at end of file
diff --git a/classes/coc/view/MonsterStatsView.as b/classes/coc/view/MonsterStatsView.as
index afb49e447dce7c9f45dd2bb1bbe17554978e2c60..d6ea8fbb49712706759d683e8180eb0d0fdff7ed 100644
--- a/classes/coc/view/MonsterStatsView.as
+++ b/classes/coc/view/MonsterStatsView.as
@@ -5,6 +5,7 @@ package coc.view {
 import classes.CoC;
 import classes.GlobalFlags.kFLAGS;
 import classes.GlobalFlags.kGAMECLASS;
+import classes.display.GameViewData;
 
 import flash.display.Bitmap;
 import flash.events.TimerEvent;
@@ -63,10 +64,12 @@ public class MonsterStatsView extends Block {
 
 	public function refreshStats(game:CoC):void {
 		var i:int = 0;
+		GameViewData.monsterStatData = [];
 		for (i; i < game.monsterArray.length; i++) {
 			if (game.monsterArray[i] != null) {
-				monsterViews[i].refreshStats(game, i);
+				var data:* = monsterViews[i].refreshStats(game, i);
 				monsterViews[i].show(game.monsterArray[i].generateTooltip(), "Details");
+				GameViewData.monsterStatData.push(data);
 			}
 			else {
 				monsterViews[i].hide();
@@ -97,26 +100,5 @@ public class MonsterStatsView extends Block {
 			monsterViews[i].setTheme(font, textColor, barAlpha);
 		}
 	}
-
-	public function animateBarChange(bar:StatBar, newValue:Number):void {
-		if (!kGAMECLASS.animateStatBars) {
-			bar.value = newValue;
-			return;
-		}
-		var oldValue:Number = bar.value;
-		//Now animate the bar.
-		var tmr:Timer = new Timer(32, 30);
-		tmr.addEventListener(TimerEvent.TIMER, kGAMECLASS.createCallBackFunction(stepBarChange, bar, [oldValue, newValue, tmr]));
-		tmr.start();
-	}
-
-	private function stepBarChange(bar:StatBar, args:Array):void {
-		var originalValue:Number = args[0];
-		var targetValue:Number = args[1];
-		var timer:Timer = args[2];
-		bar.value = originalValue + (((targetValue - originalValue) / timer.repeatCount) * timer.currentCount);
-		if (timer.currentCount >= timer.repeatCount) bar.value = targetValue;
-		//if (bar == hpBar) bar.bar.fillColor = Color.fromRgbFloat((1 - (bar.value / bar.maxValue)) * 0.8, (bar.value / bar.maxValue) * 0.8, 0);
-	}
 }
 }
diff --git a/classes/coc/view/OneMonsterView.as b/classes/coc/view/OneMonsterView.as
index 8bb16e2bb820c8a3545f411f2bb79f961bd08652..387c4fd921a89f3bcc601f9f9a7cab903c0fe6c9 100644
--- a/classes/coc/view/OneMonsterView.as
+++ b/classes/coc/view/OneMonsterView.as
@@ -106,7 +106,7 @@ public class OneMonsterView extends Block {
 		fatigueBar.value = 0;
 	}
 
-	public function refreshStats(game:CoC, index:Number = -1):void {
+	public function refreshStats(game:CoC, index:Number = -1):* {
 		this.index = index;
 		if (index != -1 && game.monsterArray[index] == null) return;
 		var monster:Monster = index != -1 ? game.monsterArray[index] : game.monster;
@@ -115,18 +115,30 @@ public class OneMonsterView extends Block {
 		levelBar.value = monster.level;
 		hpBar.maxValue = monster.maxHP();
 		hpBar.minValue = monster.HP;
-		animateBarChange(hpBar, monster.HP);
+		hpBar.animateChange(monster.HP);
 		lustBar.maxValue = monster.maxLust();
 		lustBar.minValue = monster.minLust();
 		//lustBar.value         = monster.lust;
-		animateBarChange(lustBar, monster.lust);
+		lustBar.animateChange(monster.lust);
 		fatigueBar.minValue = 0;
 		fatigueBar.maxValue = monster.maxFatigue();
-		animateBarChange(fatigueBar, monster.fatigue);
+		fatigueBar.animateChange(monster.fatigue);
 		toolTipHeader = "Details";
 		toolTipText = monster.generateTooltip();
 
 		invalidateLayout();
+
+		return {
+			name: nameText.getRawText(),
+			toolTipText: toolTipText,
+			toolTipHeader: toolTipHeader,
+			stats: [
+				{name:"Level:",   value:monster.level,   min:0,                 max:0,                    showMax:false},
+				{name:"HP:",      value:monster.HP,      min:monster.HP,        max:monster.maxHP(),      showMax:true},
+				{name:"Lust:",    value:monster.lust,    min:monster.minLust(), max:monster.maxLust(),    showMax:true},
+				{name:"Fatigue:", value:monster.fatigue, min:0,                 max:monster.maxFatigue(), showMax:true}
+			]
+		}
 	}
 
 	public function setBackground(bitmapClass:Class):void {
@@ -159,31 +171,6 @@ public class OneMonsterView extends Block {
 		}
 	}
 
-	public function animateBarChange(bar:StatBar, newValue:Number):void {
-		if (!kGAMECLASS.animateStatBars) {
-			bar.value = newValue;
-			return;
-		}
-		var oldValue:Number = bar.value;
-		//Now animate the bar.
-		var tmr:Timer = new Timer(32, 30);
-		tmr.addEventListener(TimerEvent.TIMER, kGAMECLASS.createCallBackFunction(stepBarChange, bar, [oldValue, newValue, tmr, oldValue > newValue]));
-		tmr.start();
-	}
-
-	private function stepBarChange(bar:StatBar, args:Array):void {
-		var originalValue:Number = args[0];
-		var targetValue:Number = args[1];
-		var timer:Timer = args[2];
-		var decreasing:Boolean = args[3];
-		if ((decreasing && bar.value < targetValue) || (!decreasing && bar.value > targetValue)) {
-			timer.stop();
-		}
-		bar.value = originalValue + (((targetValue - originalValue) / timer.repeatCount) * timer.currentCount);
-		if (timer.currentCount >= timer.repeatCount) bar.value = targetValue;
-		//if (bar == hpBar) bar.bar.fillColor = Color.fromRgbFloat((1 - (bar.value / bar.maxValue)) * 0.8, (bar.value / bar.maxValue) * 0.8, 0);
-	}
-
 	public function hint(toolTipText:String = "", toolTipHeader:String = ""):void {
 		this.toolTipText = toolTipText;
 		this.toolTipHeader = toolTipHeader;
diff --git a/classes/coc/view/StatBar.as b/classes/coc/view/StatBar.as
index d9f0c5a3a4c676cb79ba1780f2dd1df8791a3d0b..be4cd2a5dca4f2d582249ea241f531526b03e4ef 100644
--- a/classes/coc/view/StatBar.as
+++ b/classes/coc/view/StatBar.as
@@ -2,10 +2,14 @@
  * Coded by aimozg on 06.06.2017.
  */
 package coc.view {
+import classes.GlobalFlags.kGAMECLASS;
 import classes.internals.Utils;
 
+import flash.events.TimerEvent;
+
 import flash.text.TextField;
 import flash.text.TextFormat
+import flash.utils.Timer;
 
 public class StatBar extends Block implements ThemeObserver {
 	[Embed(source="../../../res/ui/statsBarBottom.png")]
@@ -276,5 +280,30 @@ public class StatBar extends Block implements ThemeObserver {
 			}
 		}
 	}
+
+	public function animateChange(newValue:Number):void {
+		if (!kGAMECLASS.animateStatBars) {
+			value = newValue;
+			return;
+		}
+		var timer:Timer = new Timer(32, 30);
+		timer.addEventListener(TimerEvent.TIMER, Utils.curry(stepBarChange, value, newValue, timer));
+		timer.start();
+	}
+
+
+	private function stepBarChange(oldValue:Number, newValue:Number, timer:Timer, event:TimerEvent):void {
+		value = oldValue + (((newValue - oldValue) / timer.repeatCount) * timer.currentCount);
+		var decreasing:Boolean = newValue < oldValue;
+
+		// Likely not required, but failsafe?
+		if ((decreasing && value < newValue) || (!decreasing && value > newValue)) {
+			timer.stop();
+		}
+
+		if (timer.currentCount >= timer.repeatCount) {
+			value = newValue;
+		}
+	}
 }
 }
diff --git a/classes/coc/view/StatsView.as b/classes/coc/view/StatsView.as
index 025c9d304d0cfdb3f007201c4dec9dc49866c686..3e807981087d4a73095080ea6dd8c4e13a5d5401 100644
--- a/classes/coc/view/StatsView.as
+++ b/classes/coc/view/StatsView.as
@@ -2,6 +2,7 @@ package coc.view {
 import classes.CoC;
 import classes.GlobalFlags.kGAMECLASS;
 import classes.Player;
+import classes.display.GameViewData;
 import classes.internals.LoggerFactory;
 import classes.internals.Utils;
 
@@ -306,7 +307,6 @@ public class StatsView extends Block implements ThemeObserver {
 		corBar.value = player.cor;
 		hpBar.maxValue = player.maxHP();
 		hpBar.minValue = player.HP;
-		animateBarChange(hpBar, player.HP);
 		//hpBar.value           = player.HP;
 		/* [INTERMOD: xianxia]
 		wrathBar.maxValue 	  = player.maxWrath();
@@ -314,9 +314,9 @@ public class StatsView extends Block implements ThemeObserver {
 		*/
 		lustBar.maxValue = player.maxLust();
 		lustBar.minValue = player.minLust();
-		animateBarChange(lustBar, player.lust);
+
 		fatigueBar.maxValue = player.maxFatigue();
-		animateBarChange(fatigueBar, player.fatigue);
+
 		/* [INTERMOD: xianxia]
 		manaBar.maxValue 	  = player.maxMana();
 		manaBar.value    	  = player.mana;
@@ -336,12 +336,22 @@ public class StatsView extends Block implements ThemeObserver {
 		advancementText.htmlText = "<b>Advancement</b>";
 		levelBar.value = player.level;
 
+		// Save old values for animations
+		var oldLustVal:Number = lustBar.value
+		var oldFatiqueVal:Number = fatigueBar.value;
+		var oldHPVal:Number = hpBar.value;
+		var oldXPVal:Number = xpBar.value;
+
+		// Set accurate values for GameViewData
+		lustBar.value    = player.lust;
+		xpBar.value      = player.XP;
+		hpBar.value      = player.HP;
+		fatigueBar.value = player.fatigue;
+
 		if (player.level < kGAMECLASS.levelCap) {
 			xpBar.maxValue = player.requiredXP();
-			animateBarChange(xpBar, player.XP);
 		} else {
 			xpBar.maxValue  = player.XP;
-			xpBar.value     = player.XP;
 			xpBar.valueText = 'MAX';
 		}
 
@@ -366,6 +376,43 @@ public class StatsView extends Block implements ThemeObserver {
 		timeText.htmlText = "<u>Day#: " + game.time.days + "</u>\nTime: " + hrs + ":" + minutesDisplay + ampm;
 
 		invalidateLayout();
+
+		GameViewData.playerStatData = {
+			stats: allStats.map(function (bar:StatBar, i:int, a:Array):* {
+				return {
+					name: bar.statName,
+					min: bar.minValue,
+					max: bar.maxValue,
+					value: bar.value,
+					showMax: bar.showMax,
+					hasBar: bar.bar != null,
+					isUp: bar.isUp,
+					isDown: bar.isDown
+				}
+			}),
+			name: player.short,
+			time: {
+				day: game.time.days,
+				hour: hrs,
+				minutes: minutesDisplay,
+				ampm: ampm
+			}
+		};
+
+		// Restore old values and animate to the new values
+		hpBar.value      = oldHPVal;
+		fatigueBar.value = oldFatiqueVal;
+		lustBar.value    = oldLustVal;
+
+		hpBar.animateChange(player.HP);
+		fatigueBar.animateChange(player.fatigue);
+		lustBar.animateChange(player.lust);
+
+		// No animation required if over level cap
+		if (player.level < kGAMECLASS.levelCap) {
+			xpBar.value = oldXPVal;
+			xpBar.animateChange(player.XP);
+		}
 	}
 
 	public function setBackground(bitmapClass:Class):void {
@@ -410,26 +457,5 @@ public class StatsView extends Block implements ThemeObserver {
 	public function update(message:String):void {
 		sideBarBG.bitmap = Theme.current.sidebarBg;
 	}
-
-	public function animateBarChange(bar:StatBar, newValue:Number):void {
-		if (!kGAMECLASS.animateStatBars) {
-			bar.value = newValue;
-			return;
-		}
-		var oldValue:Number = bar.value;
-		//Now animate the bar.
-		var tmr:Timer = new Timer(32, 30);
-		tmr.addEventListener(TimerEvent.TIMER, kGAMECLASS.createCallBackFunction(stepBarChange, bar, [oldValue, newValue, tmr]));
-		tmr.start();
-	}
-
-	private function stepBarChange(bar:StatBar, args:Array):void {
-		var originalValue:Number = args[0];
-		var targetValue:Number = args[1];
-		var timer:Timer = args[2];
-		bar.value = originalValue + (((targetValue - originalValue) / timer.repeatCount) * timer.currentCount);
-		if (timer.currentCount >= timer.repeatCount) bar.value = targetValue;
-		//if (bar == hpBar) bar.bar.fillColor = Color.fromRgbFloat((1 - (bar.value / bar.maxValue)) * 0.8, (bar.value / bar.maxValue) * 0.8, 0);
-	}
 }
 }
diff --git a/res/ui/SourceCodePro-Semibold.otf b/res/ui/SourceCodePro-Semibold.otf
new file mode 100644
index 0000000000000000000000000000000000000000..a61686ccafedbf9ec9328b38c1ba5770cd2dd98e
Binary files /dev/null and b/res/ui/SourceCodePro-Semibold.otf differ