/// /// /// /// /// interface HTMLElement { requestFullScreen: () => void; webkitRequestFullScreen: () => void; mozRequestFullScreen: () => void; webkitFullscreenElement: () => void; cancelFullScreen: () => void; webkitCancelFullScreen: () => void; mozCancelFullScreen: () => void; msCancelFullScreen: () => void; } declare module UserWrappr { export interface IGameStartr { DeviceLayer: DeviceLayr.IDeviceLayr; GamesRunner: GamesRunnr.IGamesRunnr; ItemsHolder: ItemsHoldr.IItemsHoldr; InputWriter: InputWritr.IInputWritr; LevelEditor: LevelEditr.ILevelEditr; UserWrapper: IUserWrappr; container: HTMLElement; addPageStyles(styles: StyleSheet): void; gameStart(): void; createElement(tag: string, ...args: any[]): HTMLElement; proliferate(recipient: any, donor: any, noOverride?: boolean): any; } type IGameStartrCustoms = any; export interface IGameStartrConstructor { new (GameStartrSettings?): IGameStartr; } export interface IGameStartrUIHelpSettings { globalNameAlias: string; openings: string[]; options: { [i: string]: IGameStartrUIHelpOption[] }; } export interface IGameStartrUIHelpOption { title: string; description: string; usage?: string; examples?: IGameStartrUIHelpExample[]; } export interface IGameStartrUIHelpExample { code: string; comment: string; } export module UISchemas { export interface ISchema { generator: string; title: string; } export interface IOption { title: string; source: IOptionSource; } export interface IOptionSource { (GameStarter: IGameStartr, ...args: any[]): any; } export interface IChoiceElement extends HTMLElement { setValue(value: any): void; } export interface IInputElement extends HTMLInputElement, IChoiceElement { } export interface ISelectElement extends HTMLSelectElement { valueOld?: string; setValue(value: any): void; } export interface IOptionsButtonsSchema extends ISchema { options: IOptionSource | IOptionsButtonSchema[]; callback: (GameStarter: IGameStartr, ...args: any[]) => void; keyActive?: string; assumeInactive?: boolean; } export interface IOptionsButtonSchema extends IOption { callback: (GameStarter: IGameStartr, ...args: any[]) => void; source: IOptionSource; storeLocally?: boolean; type: string; } export interface IOptionsTableSchema extends ISchema { actions?: IOptionsTableAction[]; options: IOptionsTableOption[]; } export interface IOptionsTableTypes { [i: string]: (input: any, details: IOptionsTableOption, schema: ISchema) => any; } export interface IOptionsTableAction { title: string; action: (GameStarter: IGameStartr, ...args: any[]) => void; } export interface IOptionsTableOption extends IOption { type: string; storeLocally?: boolean; } export interface IOptionsTableBooleanOption extends IOptionsTableOption { disable: (GameStarter: IGameStartr, ...args: any[]) => void; enable: (GameStarter: IGameStartr, ...args: any[]) => void; options?: (GameStarter: IGameStartr, ...args: any[]) => string[]; keyActive?: string; assumeInactive?: boolean; } export interface IOptionsTableKeysOption extends IOptionsTableOption { callback: (GameStarter: IGameStartr, ...args: any[]) => void; source: (GameStarter: IGameStartr, ...args: any[]) => string[]; } export interface IOptionsTableNumberOption extends IOptionsTableOption { minimum: number; maximum: number; update: (GameStarter: IGameStartr, ...args: any[]) => void; } export interface IOptionsTableSelectOption extends IOptionsTableOption { options: (GameStarter: IGameStartr, ...args: any[]) => string[]; source: (GameStarter: IGameStartr, ...args: any[]) => void; update: (GameStarter: IGameStartr, ...args: any[]) => void; } export interface IOptionsTableScreenSizeOption extends IOptionsTableOption { options: () => string[]; source: () => string; update: (GameStarter: IGameStartr, value: IUserWrapprSizeSummary) => ISelectElement; } export interface IOptionsEditorSchema extends ISchema { maps: IOptionsMapGridSchema; callback: (GameStarter: IGameStartr, ...args: any[]) => void; } export interface IOptionsMapGridSchema extends ISchema { rangeX: number[]; rangeY: number[]; callback: (GameStarter: IGameStartr, ...args: any[]) => void; extras?: { [i: string]: IOptionsMapGridExtra; }; } export interface IOptionsMapGridExtra { title: string; callback: (GameStarter: IGameStartr, ...args: any[]) => void; extraElements: IOptionsMapGridExtraElement[]; } export interface IOptionsMapGridExtraElement { tag: string; options: any; } } export interface IOptionsGenerator { generate: (schema: UISchemas.ISchema) => HTMLDivElement; } export interface IUserWrapprSizeSummary { width: number; height: number; name?: string; full?: boolean; } export interface IUISettings { helpSettings: IGameStartrUIHelpSettings; globalName: string; sizes: { [i: string]: IUserWrapprSizeSummary; }; sizeDefault: string; schemas: UISchemas.ISchema[]; allPossibleKeys?: string[]; gameElementSelector?: string; gameControlsSelector?: string; log?: (...args: any[]) => void; customs?: IGameStartrCustoms; styleSheet?: StyleSheet; } export interface IUserWrapprSettings extends IUISettings { GameStartrConstructor: IGameStartrConstructor; } export interface IUserWrappr { resetGameStarter(settings: IUserWrapprSettings, customs?: IGameStartrCustoms): void; getGameStartrConstructor(): IGameStartrConstructor; getGameStarter(): IGameStartr; getItemsHolder(): ItemsHoldr.ItemsHoldr; getSettings(): IUserWrapprSettings; getCustoms(): IGameStartrCustoms; getHelpSettings(): IGameStartrUIHelpSettings; getGlobalName(): string; getGameNameAlias(): string; getAllPossibleKeys(): string[]; getSizes(): { [i: string]: IUserWrapprSizeSummary }; getCurrentSize(): IUserWrapprSizeSummary; getIsFullScreen(): boolean; getIsPageHidden(): boolean; getLog(): (...args: any[]) => string; getGenerators(): { [i: string]: IOptionsGenerator }; getDocumentElement(): HTMLHtmlElement; getRequestFullScreen(): () => void; getCancelFullScreen(): () => void; setCurrentSize(size: string | IUserWrapprSizeSummary): void; displayHelpMenu(): void; displayHelpOptions(): void; displayHelpGroupSummary(optionName: string): void; displayHelpOption(optionName: string): void; logHelpText(text: string): void; filterHelpText(text: string): string; padTextRight(text: string, length: number): string; } } module UserWrappr { "use strict"; /** * A user interface manager made to work on top of GameStartr implementations * and provide a configurable HTML display of options. */ export class UserWrappr { /** * The GameStartr implementation this is wrapping around, such as * FullScreenMario or FullScreenPokemon. */ private GameStartrConstructor: IGameStartrConstructor; /** * The GameStartr instance created by GameStartrConstructor and stored * under window. */ private GameStarter: IGameStartr; /** * A ItemsHoldr used to store UI settings. */ private ItemsHolder: ItemsHoldr.ItemsHoldr; /** * The settings used to construct the UserWrappr. */ private settings: IUserWrapprSettings; /** * Custom arguments to be passed to the GameStartr's modules. */ private customs: any; /** * Help settings specifically for the user interface, obtained from * settings.helpSettings. */ private helpSettings: IGameStartrUIHelpSettings; /** * What the global object is called (typically "window" for browser * environments and "global" for node-style environments). */ private globalName: string; /** * What to replace with the name of the game in help text settings. */ private gameNameAlias: string; /** * All the keys the user is allowed to pick from as key bindings. */ private allPossibleKeys: string[]; /** * The allowed sizes for the game. */ private sizes: { [i: string]: IUserWrapprSizeSummary }; /** * The currently selected size for the game. */ private currentSize: IUserWrapprSizeSummary; /** * The CSS selector for the HTML element containing GameStarter's container. */ private gameElementSelector: string; /** * The CSS selector for the HTMl element containing the UI buttons. */ private gameControlsSelector: string; /** * Whether the game is currently in full screen mode. */ private isFullScreen: boolean; /** * Whether the page is currently known to be hidden. */ private isPageHidden: boolean; /** * A utility Function to log messages, commonly console.log. */ private log: (...args: any[]) => string; /** * Generators used to generate HTML controls for the user. */ private generators: { [i: string]: IOptionsGenerator }; /** * The document element that will contain the game. */ private documentElement: HTMLHtmlElement = document.documentElement; /** * Identifier for the interval Function checking for device input. */ private deviceChecker: number; /** * A browser-dependent method for request to enter full screen mode. */ private requestFullScreen: () => void = ( this.documentElement.requestFullScreen || this.documentElement.webkitRequestFullScreen || this.documentElement.mozRequestFullScreen || (this.documentElement).msRequestFullscreen || function (): void { console.warn("Not able to request full screen..."); } ).bind(this.documentElement); /** * A browser-dependent method for request to exit full screen mode. */ private cancelFullScreen: () => void = ( this.documentElement.cancelFullScreen || this.documentElement.webkitCancelFullScreen || this.documentElement.mozCancelFullScreen || (this.documentElement).msCancelFullScreen || function (): void { console.warn("Not able to cancel full screen..."); } ).bind(document); /** * @param {IUserWrapprSettings} settings */ constructor(settings: IUserWrapprSettings) { if (typeof settings === "undefined") { throw new Error("No settings object given to UserWrappr."); } if (typeof settings.GameStartrConstructor === "undefined") { throw new Error("No GameStartrConstructor given to UserWrappr."); } if (typeof settings.helpSettings === "undefined") { throw new Error("No helpSettings given to UserWrappr."); } if (typeof settings.globalName === "undefined") { throw new Error("No globalName given to UserWrappr."); } if (typeof settings.sizes === "undefined") { throw new Error("No sizes given to UserWrappr."); } if (typeof settings.sizeDefault === "undefined") { throw new Error("No sizeDefault given to UserWrappr."); } if (typeof settings.schemas === "undefined") { throw new Error("No schemas given to UserWrappr."); } this.settings = settings; this.GameStartrConstructor = settings.GameStartrConstructor; this.globalName = settings.globalName; this.helpSettings = this.settings.helpSettings; this.customs = settings.customs || {}; this.importSizes(settings.sizes); this.gameNameAlias = this.helpSettings.globalNameAlias || "{%%%%GAME%%%%}"; this.gameElementSelector = settings.gameElementSelector || "#game"; this.gameControlsSelector = settings.gameControlsSelector || "#controls"; this.log = settings.log || console.log.bind(console); this.isFullScreen = false; this.setCurrentSize(this.sizes[settings.sizeDefault]); this.allPossibleKeys = settings.allPossibleKeys || [ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "up", "right", "down", "left", "space", "shift", "ctrl" ]; // Size information is also passed to modules via this.customs this.GameStartrConstructor.prototype.proliferate(this.customs, this.currentSize, true); this.resetGameStarter(settings, this.customs); } /** * Resets the internal GameStarter by storing it under window, adding * InputWritr pipes for input to the page, creating the HTML buttons, * and setting additional CSS styles and page visiblity. * * @param {IUserWrapprSettings} settings * @param {IGameStartrCustoms} customs */ resetGameStarter(settings: IUserWrapprSettings, customs: any = {}): void { this.loadGameStarter(this.fixCustoms(customs || {})); window[settings.globalName || "GameStarter"] = this.GameStarter; this.GameStarter.UserWrapper = this; this.loadGenerators(); this.loadControls(settings.schemas); if (settings.styleSheet) { this.GameStarter.addPageStyles(settings.styleSheet); } this.resetPageVisibilityHandlers(); this.GameStarter.gameStart(); this.startCheckingDevices(); } /* Simple gets */ /** * @return {IGameStartrConstructor} The GameStartr implementation this * is wrapping around. */ getGameStartrConstructor(): IGameStartrConstructor { return this.GameStartrConstructor; } /** * @return {GameStartr} The GameStartr instance created by GameStartrConstructor * and stored under window. */ getGameStarter(): IGameStartr { return this.GameStarter; } /** * @return {ItemsHoldr} The ItemsHoldr used to store UI settings. */ getItemsHolder(): ItemsHoldr.ItemsHoldr { return this.ItemsHolder; } /** * @return {Object} The settings used to construct this UserWrappr. */ getSettings(): IUserWrapprSettings { return this.settings; } /** * @return {Object} The customs used to construct the GameStartr. */ getCustoms(): IGameStartrCustoms { return this.customs; } /** * @return {Object} The help settings from settings.helpSettings. */ getHelpSettings(): IGameStartrUIHelpSettings { return this.helpSettings; } /** * @return {String} What the global object is called, such as "window". */ getGlobalName(): string { return this.globalName; } /** * @return {String} What to replace with the name of the game in help * text settings. */ getGameNameAlias(): string { return this.gameNameAlias; } /** * @return {String} All the keys the user is allowed to pick from. */ getAllPossibleKeys(): string[] { return this.allPossibleKeys; } /** * @return {Object} The allowed sizes for the game. */ getSizes(): { [i: string]: IUserWrapprSizeSummary } { return this.sizes; } /** * @return {Object} The currently selected size for the game. */ getCurrentSize(): IUserWrapprSizeSummary { return this.currentSize; } /** * @return {Boolean} Whether the game is currently in full screen mode. */ getIsFullScreen(): boolean { return this.isFullScreen; } /** * @return {Boolean} Whether the page is currently known to be hidden. */ getIsPageHidden(): boolean { return this.isPageHidden; } /** * @return {Function} A utility Function to log messages, commonly console.log. */ getLog(): (...args: any[]) => string { return this.log; } /** * @return {Object} Generators used to generate HTML controls for the user. */ getGenerators(): { [i: string]: IOptionsGenerator } { return this.generators; } /** * @return {HTMLHtmlElement} The document element that contains the game. */ getDocumentElement(): HTMLHtmlElement { return this.documentElement; } /** * @return {Function} The method to request to enter full screen mode. */ getRequestFullScreen(): () => void { return this.requestFullScreen; } /** * @return {Function} The method to request to exit full screen mode. */ getCancelFullScreen(): () => void { return this.cancelFullScreen; } /** * @return {Number} The identifier for the device input checking interval. */ getDeviceChecker(): number { return this.deviceChecker; } /* Externally allowed sets */ /** * Sets the size of the GameStartr by resetting the game with the size * information as part of its customs object. Full screen status is * changed accordingly. * * @param {Mixed} The size to set, as a String to retrieve the size from * known info, or a container of settings. */ setCurrentSize(size: string | IUserWrapprSizeSummary): void { if (typeof size === "string" || size.constructor === String) { if (!this.sizes.hasOwnProperty(size)) { throw new Error("Size " + size + " does not exist on the UserWrappr."); } size = this.sizes[size]; } this.customs = this.fixCustoms(this.customs); if ((size).full) { this.requestFullScreen(); this.isFullScreen = true; } else if (this.isFullScreen) { this.cancelFullScreen(); this.isFullScreen = false; } this.currentSize = size; if (this.GameStarter) { this.GameStarter.container.parentNode.removeChild(this.GameStarter.container); this.resetGameStarter(this.settings, this.customs); } } /* Help dialog */ /** * Displays the root help menu dialog, which contains all the openings * for each help settings opening. */ displayHelpMenu(): void { this.helpSettings.openings.forEach(this.logHelpText.bind(this)); } /** * Displays the texts of each help settings options, all surrounded by * instructions on how to focus on a group. */ displayHelpOptions(): void { this.logHelpText( "To focus on a group, enter `" + this.globalName + ".UserWrapper.displayHelpOption(\"\");`" ); Object.keys(this.helpSettings.options).forEach(this.displayHelpGroupSummary.bind(this)); this.logHelpText( "\nTo focus on a group, enter `" + this.globalName + ".UserWrapper.displayHelpOption(\"\");`" ); } /** * Displays the summary for a help group of the given optionName. * * @param {String} optionName The help group to display the summary of. */ displayHelpGroupSummary(optionName: string): void { var actions: IGameStartrUIHelpOption[] = this.helpSettings.options[optionName], action: IGameStartrUIHelpOption, maxTitleLength: number = 0, i: number; this.log("\n" + optionName); for (i = 0; i < actions.length; i += 1) { maxTitleLength = Math.max(maxTitleLength, this.filterHelpText(actions[i].title).length); } for (i = 0; i < actions.length; i += 1) { action = actions[i]; this.log(this.padTextRight(this.filterHelpText(action.title), maxTitleLength) + " ... " + action.description); } } /** * Displays the full information on a help group of the given optionName. * * @param {String} optionName The help group to display the information of. */ displayHelpOption(optionName: string): void { var actions: IGameStartrUIHelpOption[] = this.helpSettings.options[optionName], action: IGameStartrUIHelpOption, example: IGameStartrUIHelpExample, maxExampleLength: number, i: number, j: number; for (i = 0; i < actions.length; i += 1) { action = actions[i]; maxExampleLength = 0; this.logHelpText(action.title + " -- " + action.description); if (action.usage) { this.logHelpText(action.usage); } if (action.examples) { for (j = 0; j < action.examples.length; j += 1) { example = action.examples[j]; maxExampleLength = Math.max( maxExampleLength, this.filterHelpText(" " + example.code).length ); } for (j = 0; j < action.examples.length; j += 1) { example = action.examples[j]; this.logHelpText( this.padTextRight( this.filterHelpText(" " + example.code), maxExampleLength ) + " // " + example.comment ); } } this.log("\n"); } } /** * Logs a bit of help text, filtered by this.filterHelpText. * * @param {String} text The text to be filtered and logged. */ logHelpText(text: string): void { this.log(this.filterHelpText(text)); } /** * @param {String} text * @return {String} The text, with gamenameAlias replaced by globalName. */ filterHelpText(text: string): string { return text.replace(new RegExp(this.gameNameAlias, "g"), this.globalName); } /** * Ensures a bit of text is of least a certain length. * * @param {String} text The text to pad. * @param {Number} length How wide the text must be, at minimum. * @return {String} The text with spaces padded to the right. */ padTextRight(text: string, length: number): string { var diff: number = 1 + length - text.length; if (diff <= 0) { return text; } return text + Array.call(Array, diff).join(" "); } /* Devices */ /** * Starts the checkDevices loop to scan for gamepad status changes. */ private startCheckingDevices(): void { this.checkDevices(); } /** * Calls the DeviceLayer to check for gamepad triggers, after scheduling * another checkDevices call via setTimeout. */ private checkDevices(): void { this.deviceChecker = setTimeout( this.checkDevices.bind(this), this.GameStarter.GamesRunner.getPaused() ? 117 : this.GameStarter.GamesRunner.getInterval() / this.GameStarter.GamesRunner.getSpeed()); this.GameStarter.DeviceLayer.checkNavigatorGamepads(); this.GameStarter.DeviceLayer.activateAllGamepadTriggers(); } /* Settings parsing */ /** * Sets the internal this.sizes as a copy of the given sizes, but with * names as members of every size summary. * * @param {Object} sizes The listing of preset sizes to go by. */ private importSizes(sizes: { [i: string]: IUserWrapprSizeSummary }): void { var i: string; this.sizes = this.GameStartrConstructor.prototype.proliferate({}, sizes); for (i in this.sizes) { if (this.sizes.hasOwnProperty(i)) { this.sizes[i].name = this.sizes[i].name || i; } } } /** * */ private fixCustoms(customsRaw: IGameStartrCustoms): any { var customs: IGameStartrCustoms = this.GameStartrConstructor.prototype.proliferate({}, customsRaw); this.GameStartrConstructor.prototype.proliferate(customs, this.currentSize); if (!isFinite(customs.width)) { customs.width = document.body.clientWidth; } if (!isFinite(customs.height)) { if (customs.full) { customs.height = screen.height; } else if (this.isFullScreen) { // Guess for browser window... // @todo Actually compute this! customs.height = window.innerHeight - 140; } else { customs.height = window.innerHeight; } // 49px from header, 77px from menus customs.height -= 126; } return customs; } /* Page visibility */ /** * Adds a "visibilitychange" handler to the document bound to * this.handleVisibilityChange. */ private resetPageVisibilityHandlers(): void { document.addEventListener("visibilitychange", this.handleVisibilityChange.bind(this)); } /** * Handles a visibility change event by calling either this.onPageHidden * or this.onPageVisible. * * @param {Event} event */ private handleVisibilityChange(event: Event): void { switch (document.visibilityState) { case "hidden": this.onPageHidden(); return; case "visible": this.onPageVisible(); return; default: return; } } /** * Reacts to the page becoming hidden by pausing the GameStartr. */ private onPageHidden(): void { if (!this.GameStarter.GamesRunner.getPaused()) { this.isPageHidden = true; this.GameStarter.GamesRunner.pause(); } } /** * Reacts to the page becoming visible by unpausing the GameStartr. */ private onPageVisible(): void { if (this.isPageHidden) { this.isPageHidden = false; this.GameStarter.GamesRunner.play(); } } /* Control section loaders */ /** * Loads the internal GameStarter, resetting it with the given customs * and attaching handlers to document.body and the holder elements. * * @param {Object} customs Custom arguments to pass to this.GameStarter. */ private loadGameStarter(customs: IGameStartrCustoms): void { var section: HTMLElement = document.querySelector(this.gameElementSelector); if (this.GameStarter) { this.GameStarter.GamesRunner.pause(); } this.GameStarter = new this.GameStartrConstructor(customs); section.textContent = ""; section.appendChild(this.GameStarter.container); this.GameStarter.proliferate(document.body, { "onkeydown": this.GameStarter.InputWriter.makePipe("onkeydown", "keyCode"), "onkeyup": this.GameStarter.InputWriter.makePipe("onkeyup", "keyCode") }); this.GameStarter.proliferate(section, { "onmousedown": this.GameStarter.InputWriter.makePipe("onmousedown", "which"), "oncontextmenu": this.GameStarter.InputWriter.makePipe("oncontextmenu", null, true) }); } /** * Loads the internal OptionsGenerator instances under this.generators. */ private loadGenerators(): void { this.generators = { OptionsButtons: new UISchemas.OptionsButtonsGenerator(this), OptionsTable: new UISchemas.OptionsTableGenerator(this), LevelEditor: new UISchemas.LevelEditorGenerator(this), MapsGrid: new UISchemas.MapsGridGenerator(this) }; } /** * Loads the externally facing UI controls and the internal ItemsHolder, * appending the controls to the controls HTML element. * * @param {Object[]} schemas The schemas each a UI control to be made. */ private loadControls(schemas: UISchemas.ISchema[]): void { var section: HTMLElement = document.querySelector(this.gameControlsSelector), length: number = schemas.length, i: number; this.ItemsHolder = new ItemsHoldr.ItemsHoldr({ "prefix": this.globalName + "::UserWrapper::ItemsHolder" }); section.textContent = ""; section.className = "length-" + length; for (i = 0; i < length; i += 1) { section.appendChild(this.loadControlDiv(schemas[i])); } } /** * Creates an individual UI control element based on a UI schema. * * @param {Object} schema * @return {HTMLDivElement} */ private loadControlDiv(schema: UISchemas.ISchema): HTMLDivElement { var control: HTMLDivElement = document.createElement("div"), heading: HTMLHeadingElement = document.createElement("h4"), inner: HTMLDivElement = document.createElement("div"); control.className = "control"; control.id = "control-" + schema.title; heading.textContent = schema.title; inner.className = "control-inner"; inner.appendChild(this.generators[schema.generator].generate(schema)); control.appendChild(heading); control.appendChild(inner); // Touch events often propogate to children before the control div has // been fully extended. Setting the "active" attribute fixes that. control.onmouseover = setTimeout.bind( undefined, function (): void { control.setAttribute("active", "on"); }, 35); control.onmouseout = function (): void { control.setAttribute("active", "off"); }; return control; } } export module UISchemas { /** * Base class for options generators. These all store a UserWrapper and * its GameStartr, along with a generate Function */ export class AbstractOptionsGenerator implements IOptionsGenerator { /** * */ protected UserWrapper: UserWrappr.UserWrappr; /** * */ protected GameStarter: IGameStartr; /** * @param {UserWrappr} UserWrappr */ constructor(UserWrapper: UserWrappr.UserWrappr) { this.UserWrapper = UserWrapper; this.GameStarter = this.UserWrapper.getGameStarter(); } /** * Generates a control element based on the provided schema. */ generate(schema: ISchema): HTMLDivElement { throw new Error("AbstractOptionsGenerator is abstract. Subclass it."); } /** * Recursively searches for an element with the "control" class * that's a parent of the given element. * * @param {HTMLElement} element * @return {HTMLElement} */ protected getParentControlDiv(element: HTMLElement): HTMLElement { if (element.className === "control") { return element; } else if (!element.parentNode) { return element; } return this.getParentControlDiv(element.parentElement); } /** * */ protected ensureLocalStorageButtonValue( child: HTMLDivElement, details: IOptionsButtonSchema, schema: IOptionsButtonsSchema): void { var key: string = schema.title + "::" + details.title, valueDefault: string = details.source.call(this, this.GameStarter).toString(), value: string; child.setAttribute("localStorageKey", key); this.GameStarter.ItemsHolder.addItem(key, { "storeLocally": true, "valueDefault": valueDefault }); value = this.GameStarter.ItemsHolder.getItem(key); if (value.toString().toLowerCase() === "true") { details[schema.keyActive || "active"] = true; schema.callback.call(this, this.GameStarter, schema, child); } } /** * Ensures an input's required local storage value is being stored, * and adds it to the internal GameStarter.ItemsHolder if not. If it * is, and the child's value isn't equal to it, the value is set. * * @param {Mixed} childRaw An input or select element, or an Array * thereof. * @param {Object} details Details containing the title of the item * and the source Function to get its value. * @param {Object} schema The container schema this child is within. */ protected ensureLocalStorageInputValue(childRaw: IChoiceElement | IChoiceElement[], details: IOption, schema: ISchema): void { if (childRaw.constructor === Array) { this.ensureLocalStorageValues(childRaw, details, schema); return; } var child: IInputElement | ISelectElement = childRaw, key: string = schema.title + "::" + details.title, valueDefault: string = details.source.call(this, this.GameStarter).toString(), value: string; child.setAttribute("localStorageKey", key); this.GameStarter.ItemsHolder.addItem(key, { "storeLocally": true, "valueDefault": valueDefault }); value = this.GameStarter.ItemsHolder.getItem(key); if (value !== "" && value !== child.value) { child.value = value; if (child.setValue) { child.setValue(value); } else if (child.onchange) { child.onchange(undefined); } else if (child.onclick) { child.onclick(undefined); } } } /** * The equivalent of ensureLocalStorageValue for an entire set of * elements, running the equivalent logic on all of them. * * @param {Mixed} childRaw An Array of input or select elements. * @param {Object} details Details containing the title of the item * and the source Function to get its value. * @param {Object} schema The container schema this child is within. */ protected ensureLocalStorageValues(children: (IInputElement | ISelectElement)[], details: IOption, schema: ISchema): void { var keyGeneral: string = schema.title + "::" + details.title, values: any[] = details.source.call(this, this.GameStarter), key: string, value: any, child: IInputElement | ISelectElement, i: number; for (i = 0; i < children.length; i += 1) { key = keyGeneral + "::" + i; child = children[i]; child.setAttribute("localStorageKey", key); this.GameStarter.ItemsHolder.addItem(key, { "storeLocally": true, "valueDefault": values[i] }); value = this.GameStarter.ItemsHolder.getItem(key); if (value !== "" && value !== child.value) { child.value = value; if (child.onchange) { child.onchange(undefined); } else if (child.onclick) { child.onclick(undefined); } } } } /** * Stores an element's value in the internal GameStarter.ItemsHolder, * if it has the "localStorageKey" attribute. * * @param {HTMLElement} child An element with a value to store. * @param {Mixed} value What value is to be stored under the key. */ protected storeLocalStorageValue(child: IInputElement | ISelectElement, value: any): void { var key: string = child.getAttribute("localStorageKey"); if (key) { this.GameStarter.ItemsHolder.setItem(key, value); this.GameStarter.ItemsHolder.saveItem(key); } } } /** * A buttons generator for an options section that contains any number * of general buttons. */ export class OptionsButtonsGenerator extends AbstractOptionsGenerator implements IOptionsGenerator { generate(schema: IOptionsButtonsSchema): HTMLDivElement { var output: HTMLDivElement = document.createElement("div"), options: IOptionsButtonSchema[] = schema.options instanceof Function ? (schema.options).call(self, this.GameStarter) : schema.options, optionKeys: string[] = Object.keys(options), keyActive: string = schema.keyActive || "active", classNameStart: string = "select-option options-button-option", scope: OptionsButtonsGenerator = this, option: IOptionsButtonSchema, element: HTMLDivElement, i: number; output.className = "select-options select-options-buttons"; for (i = 0; i < optionKeys.length; i += 1) { option = options[optionKeys[i]]; element = document.createElement("div"); element.className = classNameStart; element.textContent = optionKeys[i]; element.onclick = function (schema: IOptionsButtonSchema, element: HTMLDivElement): void { if (scope.getParentControlDiv(element).getAttribute("active") !== "on") { return; } schema.callback.call(scope, scope.GameStarter, schema, element); if (element.getAttribute("option-enabled") === "true") { element.setAttribute("option-enabled", "false"); element.className = classNameStart + " option-disabled"; } else { element.setAttribute("option-enabled", "true"); element.className = classNameStart + " option-enabled"; } }.bind(this, schema, element); this.ensureLocalStorageButtonValue(element, option, schema); if (option[keyActive]) { element.className += " option-enabled"; element.setAttribute("option-enabled", "true"); } else if (schema.assumeInactive) { element.className += " option-disabled"; element.setAttribute("option-enabled", "false"); } else { element.setAttribute("option-enabled", "true"); } output.appendChild(element); } return output; } } /** * An options generator for a table of options,. */ export class OptionsTableGenerator extends AbstractOptionsGenerator implements IOptionsGenerator { protected optionTypes: IOptionsTableTypes = { "Boolean": this.setBooleanInput, "Keys": this.setKeyInput, "Number": this.setNumberInput, "Select": this.setSelectInput, "ScreenSize": this.setScreenSizeInput }; generate(schema: IOptionsTableSchema): HTMLDivElement { var output: HTMLDivElement = document.createElement("div"), table: HTMLTableElement = document.createElement("table"), option: IOptionsTableOption, action: IOptionsTableAction, row: HTMLTableRowElement | HTMLDivElement, label: HTMLTableDataCellElement, input: HTMLTableDataCellElement, child: IInputElement | ISelectElement, i: number; output.className = "select-options select-options-table"; if (schema.options) { for (i = 0; i < schema.options.length; i += 1) { row = document.createElement("tr"); label = document.createElement("td"); input = document.createElement("td"); option = schema.options[i]; label.className = "options-label-" + option.type; label.textContent = option.title; input.className = "options-cell-" + option.type; row.appendChild(label); row.appendChild(input); child = this.optionTypes[schema.options[i].type].call(this, input, option, schema); if (option.storeLocally) { this.ensureLocalStorageInputValue(child, option, schema); } table.appendChild(row); } } output.appendChild(table); if (schema.actions) { for (i = 0; i < schema.actions.length; i += 1) { row = document.createElement("div"); action = schema.actions[i]; row.className = "select-option options-button-option"; row.textContent = action.title; row.onclick = action.action.bind(this, this.GameStarter); output.appendChild(row); } } return output; } protected setBooleanInput(input: IInputElement, details: IOptionsTableBooleanOption, schema: ISchema): IInputElement { var status: boolean = details.source.call(this, this.GameStarter), statusClass: string = status ? "enabled" : "disabled", scope: OptionsTableGenerator = this; input.className = "select-option options-button-option option-" + statusClass; input.textContent = status ? "on" : "off"; input.onclick = function (): void { input.setValue(input.textContent === "off"); }; input.setValue = function (newStatus: string | boolean): void { if (newStatus.constructor === String) { if (newStatus === "false" || newStatus === "off") { newStatus = false; } else if (newStatus === "true" || newStatus === "on") { newStatus = true; } } if (newStatus) { details.enable.call(scope, scope.GameStarter); input.textContent = "on"; input.className = input.className.replace("disabled", "enabled"); } else { details.disable.call(scope, scope.GameStarter); input.textContent = "off"; input.className = input.className.replace("enabled", "disabled"); } if (details.storeLocally) { scope.storeLocalStorageValue(input, newStatus.toString()); } }; return input; } protected setKeyInput(input: IInputElement, details: IOptionsTableKeysOption, schema: ISchema): ISelectElement[] { var values: string = details.source.call(this, this.GameStarter), possibleKeys: string[] = this.UserWrapper.getAllPossibleKeys(), children: ISelectElement[] = [], child: ISelectElement, scope: OptionsTableGenerator = this, valueLower: string, i: number, j: number; for (i = 0; i < values.length; i += 1) { valueLower = values[i].toLowerCase(); child = document.createElement("select"); child.className = "options-key-option"; child.value = child.valueOld = valueLower; for (j = 0; j < possibleKeys.length; j += 1) { child.appendChild(new Option(possibleKeys[j])); // Setting child.value won't work in IE or Edge... if (possibleKeys[j] === valueLower) { child.selectedIndex = j; } } child.onchange = (function (child: ISelectElement): void { details.callback.call(scope, scope.GameStarter, child.valueOld, child.value); if (details.storeLocally) { scope.storeLocalStorageValue(child, child.value); } }).bind(undefined, child); children.push(child); input.appendChild(child); } return children; } protected setNumberInput(input: IInputElement, details: IOptionsTableNumberOption, schema: ISchema): IInputElement { var child: IInputElement = document.createElement("input"), scope: OptionsTableGenerator = this; child.type = "number"; child.value = Number(details.source.call(scope, scope.GameStarter)).toString(); child.min = (details.minimum || 0).toString(); child.max = (details.maximum || Math.max(details.minimum + 10, 10)).toString(); child.onchange = child.oninput = function (): void { if (child.checkValidity()) { details.update.call(scope, scope.GameStarter, child.value); } if (details.storeLocally) { scope.storeLocalStorageValue(child, child.value); } }; input.appendChild(child); return child; } protected setSelectInput(input: ISelectElement, details: IOptionsTableSelectOption, schema: ISchema): ISelectElement { var child: ISelectElement = document.createElement("select"), options: string[] = details.options(this.GameStarter), scope: OptionsTableGenerator = this, i: number; for (i = 0; i < options.length; i += 1) { child.appendChild(new Option(options[i])); } child.value = details.source.call(scope, scope.GameStarter); child.onchange = function (): void { details.update.call(scope, scope.GameStarter, child.value); child.blur(); if (details.storeLocally) { scope.storeLocalStorageValue(child, child.value); } }; input.appendChild(child); return child; } protected setScreenSizeInput(input: ISelectElement, details: IOptionsTableScreenSizeOption, schema: ISchema): ISelectElement { var scope: OptionsTableGenerator = this, child: ISelectElement; details.options = function (): string[] { return Object.keys(scope.UserWrapper.getSizes()); }; details.source = function (): string { return scope.UserWrapper.getCurrentSize().name; }; details.update = function (GameStarter: IGameStartr, value: IUserWrapprSizeSummary | string): ISelectElement { if (value === scope.UserWrapper.getCurrentSize()) { return undefined; } scope.UserWrapper.setCurrentSize(value); }; child = scope.setSelectInput(input, details, schema); return child; } } /** * Options generator for a LevelEditr dialog. */ export class LevelEditorGenerator extends AbstractOptionsGenerator implements IOptionsGenerator { generate(schema: IOptionsEditorSchema): HTMLDivElement { var output: HTMLDivElement = document.createElement("div"), starter: HTMLDivElement = document.createElement("div"), betweenOne: HTMLDivElement = document.createElement("div"), betweenTwo: HTMLDivElement = document.createElement("div"), uploader: HTMLDivElement = this.createUploaderDiv(), mapper: HTMLDivElement = this.createMapSelectorDiv(schema), scope: LevelEditorGenerator = this; output.className = "select-options select-options-level-editor"; starter.className = "select-option select-option-large options-button-option"; starter.innerHTML = "Start the
Level Editor!"; starter.onclick = function (): void { scope.GameStarter.LevelEditor.enable(); }; betweenOne.className = betweenTwo.className = "select-option-title"; betweenOne.innerHTML = betweenTwo.innerHTML = "- or -
"; output.appendChild(starter); output.appendChild(betweenOne); output.appendChild(uploader); output.appendChild(betweenTwo); output.appendChild(mapper); return output; } protected createUploaderDiv(): HTMLDivElement { var uploader: HTMLDivElement = document.createElement("div"), input: HTMLInputElement = document.createElement("input"); uploader.className = "select-option select-option-large options-button-option"; uploader.innerHTML = "Continue an
editor file!"; uploader.setAttribute("textOld", uploader.textContent); input.type = "file"; input.className = "select-upload-input"; input.onchange = this.handleFileDrop.bind(this, input, uploader); uploader.ondragenter = this.handleFileDragEnter.bind(this, uploader); uploader.ondragover = this.handleFileDragOver.bind(this, uploader); uploader.ondragleave = input.ondragend = this.handleFileDragLeave.bind(this, uploader); uploader.ondrop = this.handleFileDrop.bind(this, input, uploader); uploader.onclick = input.click.bind(input); uploader.appendChild(input); return uploader; } protected createMapSelectorDiv(schema: IOptionsEditorSchema): HTMLDivElement { var expanded: boolean = true, generatorName: string = "MapsGrid", container: HTMLDivElement = this.GameStarter.createElement( "div", { "className": "select-options-group select-options-editor-maps-selector" }), toggler: HTMLDivElement = this.GameStarter.createElement( "div", { "className": "select-option select-option-large options-button-option" }), mapsOut: HTMLDivElement = this.GameStarter.createElement( "div", { "className": "select-options-holder select-options-editor-maps-holder" }), mapsIn: HTMLDivElement = this.UserWrapper.getGenerators()[generatorName].generate( this.GameStarter.proliferate( { "callback": schema.callback }, schema.maps)); toggler.onclick = function (event?: Event): void { expanded = !expanded; if (expanded) { toggler.textContent = "(cancel)"; mapsOut.style.position = ""; mapsIn.style.height = ""; } else { toggler.innerHTML = "Edit a
built-in map!"; mapsOut.style.position = "absolute"; mapsIn.style.height = "0"; } if (!container.parentElement) { return; } [].slice.call(container.parentElement.children) .forEach(function (element: HTMLElement): void { if (element !== container) { element.style.display = (expanded ? "none" : "block"); } }); }; toggler.onclick(null); mapsOut.appendChild(mapsIn); container.appendChild(toggler); container.appendChild(mapsOut); return container; } protected handleFileDragEnter(uploader: HTMLDivElement, event: LevelEditr.IDataMouseEvent): void { if (event.dataTransfer) { event.dataTransfer.dropEffect = "copy"; } uploader.className += " hovering"; } protected handleFileDragOver(uploader: HTMLElement, event: MouseEvent): boolean { event.preventDefault(); return false; } protected handleFileDragLeave(element: HTMLElement, event: LevelEditr.IDataMouseEvent): void { if (event.dataTransfer) { event.dataTransfer.dropEffect = "none"; } element.className = element.className.replace(" hovering", ""); } protected handleFileDrop(input: HTMLInputElement, uploader: HTMLDivElement, event: LevelEditr.IDataMouseEvent): void { var files: FileList = input.files || event.dataTransfer.files, file: File = files[0], reader: FileReader = new FileReader(); this.handleFileDragLeave(input, event); event.preventDefault(); event.stopPropagation(); reader.onprogress = this.handleFileUploadProgress.bind(this, file, uploader); reader.onloadend = this.handleFileUploadCompletion.bind(this, file, uploader); reader.readAsText(file); } protected handleFileUploadProgress(file: File, uploader: HTMLDivElement, event: LevelEditr.IDataProgressEvent): void { var percent: number; if (!event.lengthComputable) { return; } percent = Math.round((event.loaded / event.total) * 100); if (percent > 100) { percent = 100; } uploader.innerText = "Uploading '" + file.name + "' (" + percent + "%)..."; } protected handleFileUploadCompletion(file: File, uploader: HTMLDivElement, event: LevelEditr.IDataProgressEvent): void { this.GameStarter.LevelEditor.handleUploadCompletion(event); uploader.innerText = uploader.getAttribute("textOld"); } } /** * Options generator for a grid of maps, along with other options. */ export class MapsGridGenerator extends AbstractOptionsGenerator implements IOptionsGenerator { generate(schema: IOptionsMapGridSchema): HTMLDivElement { var output: HTMLDivElement = document.createElement("div"); output.className = "select-options select-options-maps-grid"; if (schema.rangeX && schema.rangeY) { output.appendChild(this.generateRangedTable(schema)); } if (schema.extras) { this.appendExtras(output, schema); } return output; } generateRangedTable(schema: IOptionsMapGridSchema): HTMLTableElement { var scope: MapsGridGenerator = this, table: HTMLTableElement = document.createElement("table"), rangeX: number[] = schema.rangeX, rangeY: number[] = schema.rangeY, row: HTMLTableRowElement, cell: HTMLTableCellElement, i: number, j: number; for (i = rangeY[0]; i <= rangeY[1]; i += 1) { row = document.createElement("tr"); row.className = "maps-grid-row"; for (j = rangeX[0]; j <= rangeX[1]; j += 1) { cell = document.createElement("td"); cell.className = "select-option maps-grid-option maps-grid-option-range"; cell.textContent = i + "-" + j; cell.onclick = (function (callback: () => any): void { if (scope.getParentControlDiv(cell).getAttribute("active") === "on") { callback(); } }).bind(scope, schema.callback.bind(scope, scope.GameStarter, schema, cell)); row.appendChild(cell); } table.appendChild(row); } return table; } appendExtras(output: HTMLDivElement, schema: IOptionsMapGridSchema): void { var element: HTMLDivElement, extra: IOptionsMapGridExtra, i: string, j: number; for (i in schema.extras) { if (!schema.extras.hasOwnProperty(i)) { continue; } extra = schema.extras[i]; element = document.createElement("div"); element.className = "select-option maps-grid-option maps-grid-option-extra"; element.textContent = extra.title; element.setAttribute("value", extra.title); element.onclick = extra.callback.bind(this, this.GameStarter, schema, element); output.appendChild(element); if (extra.extraElements) { for (j = 0; j < extra.extraElements.length; j += 1) { output.appendChild(this.GameStarter.createElement.apply(this.GameStarter, extra.extraElements[j])); } } } } } } }