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