From 73092c68a170065ec37aceb0fe0c1024cfefae26 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 25 Mar 2018 19:43:16 -0700 Subject: [PATCH] Added detailed docs on schemas, dialogs, lists, and text (#53) * Added detailed docs on schemas, dialogs, lists, and text * TSLint fixups * Finished README.md * Trailing whitespace... --- README.md | 150 ++++++++++++++++++++ docs/dialogs.md | 98 +++++++++++++ docs/lists.md | 216 +++++++++++++++++++++++++++++ docs/schemas.md | 337 +++++++++++++++++++++++++++++++++++++++++++++ docs/text.md | 74 ++++++++++ src/IMenuGraphr.ts | 295 +++++++++++++++++++++------------------ src/MenuGraphr.ts | 111 +++++++-------- 7 files changed, 1085 insertions(+), 196 deletions(-) create mode 100644 docs/dialogs.md create mode 100644 docs/lists.md create mode 100644 docs/schemas.md create mode 100644 docs/text.md diff --git a/README.md b/README.md index caf27fb..a8d2d9e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,156 @@ In-game menu and dialog creation and management for GameStartr. +MenuGraphr automates creating in-game menus containing paragraphs or scrolling lists of text. +Each menu has a unique name by which its globally identified as well as a rectangular position relative to its parent. +Menus can be positioned as children of the root game's MapScreenr viewport or of each other. + +## Usage + +MenuGraphr instances take in, at the very least, a GameStartr game to create Things within. +The game should have have a `"Menu"` Thing defined. + +### Constructor + +```typescript +const gameStarter = new GameStartr({ ... }); +const menuGrapher = new MenuGraphr({ gameStarter }); +``` + +#### `gameStarter` + +The parent GameStartr managing Things. +This is the only mandatory settings field. + +#### `aliases` + +Alternate Thing titles for characters, such as `" "` for `"Space"`. +Normally, Things used as menu text have titles equal to `"Text"` plus the name of the character. +These will replace the name of the character in that computation. + +```typescript +// Uses "TextSpace" instead of "Text " +new MenuGraphr({ + aliases: { + " ": "Space", + }, + gameStarter, +}); +``` + +#### `sounds` + +Sounds that should be played for certain menu actions. +So far, this is only `onInteraction`, which is whenever a menu is interacted with +(usually off the A or B buttons being pressed). +These are played with the GameStartr's AudioPlayr. + +```typescript +new MenuGraphr({ + gameStarter, + sounds: { + onInteraction: "Bloop", + } +}); +``` + +#### `replacements` + +Programmatic replacements for deliniated words. +Allows texts in menus to contain dynamic values using predetermined strings. + +These can be hardcoded strings or functions to generate them. + +```typescript +new MenuGraphr({ + gameStarter, + replacements: { + "DYNAMIC": () => gameStarter.itemsHolder.get("dynamic-value"), + "STATIC": "My name here!", + }, +}); +``` + +Menu dialogs and lists will directly replace the values of replacements between the menu's `replacerKey` (see below): + +```typescript +menuGrapher.addMenuDialog("GeneralText", [ + // Inserts the value of gameStarter.itemsHolder.get("dynamic-value") + "Dynamic value: %%%%%%%DYNAMIC%%%%%%%", + + // Inserts "My name here!" + "Static value: %%%%%%%STATIC%%%%%%%", +]); +``` + +#### `replacerKey` + +Separator for words to replace using `replacements`. +Defaults to `"%%%%%%%"`. + +```typescript +new MenuGraphr({ + gameStarter, + replacements: { + "STATIC": "My name here!", + }, + replacerKey: "|", +}); +``` + +```typescript +menuGrapher.addMenuDialog("GeneralText", [ + // Inserts "My name here!" + "Static value: |STATIC|", +]); +``` + +#### `schemas` + +Known menu schemas, keyed by name. +Those properties are defined on `IMenuSchema`. +See [`docs/schemas.md`](./docs/schemas.md). + +```typescript +new MenuGraphr({ + gameStarter, + schemas: { + GeneralText: { + size: { + height: 96, + width: 320, + }, + }, + }, +}); +``` + +### `createMenu` + +Menus are created with `createMenu`, which takes in the string name of the menu and any additional properties. + +```typescript +menuGrapher.createMenu("GeneralText", { /* ... */ }); +``` + +Each menu is identified by a unique string name. +When `createMenu` creates a menu, any existing menu under that name is disposed of. + +### `setActiveMenu` + +Sets a menu to appear to have user focus. +For dialogs, this allows the user to "A" through them. +For lists, this visualizes the selected index with an "Arrow" Thing. + +Only one menu may be active at any time. +There does not need to be an active menu, and menus are not active by default. + +```typescript +menuGrapher.createMenu("GeneralText"); +menuGrapher.addMenuDialog("GeneralText", "Hello world!"); +menuGrapher.setActiveMenu("GeneralText"); +``` + ## Development diff --git a/docs/dialogs.md b/docs/dialogs.md new file mode 100644 index 0000000..31eca4a --- /dev/null +++ b/docs/dialogs.md @@ -0,0 +1,98 @@ +# Dialogs + +Menu dialogs refer to any amount of text on a menu. +Some dialogs are static, and just used to display text Things on top of a menu. +Others are dynamic and can advance through formatted lines of interactive text. + +## `addMenuDialog` + +Adds dialog-style text to a menu. +If the text would overflow the menu's size, excess horizontal lines are delayed. +The user can advance through the menu with "A" button presses. + +Parameters: + +* `menuName`: Name of the menu. +* `dialog`: Raw dialog to add to the menu, as strings, arrays of strings, or complex placement commands. +* `onCompletion`: Optional callback for when the text is done. + +### Dialogs + +The actual type for menu dialogs is weirdly flexible: + +```typescript +type IMenuDialogRaw = string | (string | string[] | (string | string[])[] | IMenuWordCommandBase)[]; +``` + +#### String Dialogs + +The simplest dialogs will typically just contain strings: +When provided as a raw string, the dialog is split across whitespace to generate words. +This forces the dialog to not wrap words across lines. + +```typescript +// Creates a new GeneralText menu, deleting any existing one +menuGrapher.createMenu("GeneralText"); + +// Adds the dialog to GeneralText +menuGrapher.addMenuDialog("GeneralText", "Hello world!"); + +// Sets GeneralText as the active, input-receiving menu +menuGrapher.setActiveMenu("GeneralText"); +``` + +#### Array Dialogs + +Dialogs will show as many consecutive lines on "A" press as possible by default. +You can force a "break" in the sections by providing an array of dialog strings. +The dialog will clear any lines on the screen when moving across a break. + +```typescript +menuGrapher.addMenuDialog("GeneralText", ["Hello world!", "Me again!"]); +``` + +### Advanced Commands + +It's allowed to provide advanced "commands" to dialogs along with words in the dialogs. +These commands can insert "floating" text or change position offsets or alignments of the dialog text. + +Alas, these command types aren't well fleshed out in `IMenuGraphr.ts` and not recommended until documentation is solidified. +See [#52](https://github.com/FullScreenShenanigans/MenuGraphr/issues/52). + +## `IMenuSchema` + +These options are available on `IMenuSchema` generally, but only useful once the menu is given a dialog. + +### `deleteOnFinish` + +Whether the menu should be deleted when its dialog finishes. + +```typescript +{ + deleteOnFinish: true, +} +``` + +### `finishAutomatically` + +Whether the dialog should finish when the last word is displayed, +instead of waiting for user input. + +```typescript +{ + finishAutomatically: true, +}, +``` + +### `finishAutomaticSpeed` + +How many game ticks to delay completion by when `finishAutomatically` is true. +Defaults to `0`. + +```typescript +{ + finishAutomaticSpeed: 100, +} +``` + +See [`lists.md`](./lists.md) for examples of lists intermixed with dialogs. diff --git a/docs/lists.md b/docs/lists.md new file mode 100644 index 0000000..a5bd77d --- /dev/null +++ b/docs/lists.md @@ -0,0 +1,216 @@ +# Lists + +Menus can have scrollable lists of selectable text options in them. +Users can use direction inputs to scroll through the items. + +Lists can be one-dimensional or two-dimensional. +This is determined by the computed height of list options within the menu's height. +If enough options are added to a list to pass the bottom, unless the menu specifies `singleColumnList`, they will overflow to a column to the right. + +## `IListMenuSchema` + +When creating or declaring schemas for list menus, there are some additional properties you can apply to them. +These are all optional. + +### `saveIndex` + +Whether the last selected index should be saved. +When `true`, the parent GameStartr's ItemsHoldr will save the selected index of the menu under the menu's name. +Recreating the menu will read from that stored index if available. + +```typescript +{ + saveIndex: true, +}, +``` + +### `clearedIndicesOnDeletion` + +Names of menus whose whose selected indices that should be cleared when this menu is deleted. +Use this when there are multiple related list menus open at once, and finishing one clears another. + +```typescript +{ + clearedIndicesOnDeletion: [ + "KeyboardKeys", + "NameCollection", + ], +}, +``` + +### `scrollingItems` + +How many scrolling items should be visible within the menu vertically. +List menus will by default show all the items at once, which is bad if there are many options and not enough menu height to display them all. +Specify a `scrollingItems` number to hardcode a maximum to display at once. + +If the user shifts their selected index to below the lowest displayed item or above the highest displayed item, +the menu will "scroll" items vertically. +It does this by shifting their Things vertically and setting `hidden` on items not allowed to be seen. + +```typescript +{ + scrollingItems: 10, +}, +``` + +> There is no equivalent for horizontal items. + +### `scrollingItemsComputed` + +As an alternative to `scrollingItems`, you can have the maximum displayed number of items computed as a function of menu height and expected height per list option. +This will set the `scrollingItems` member of the menu on list creation. + +```typescript +{ + scrollingItemsComputed: true, +}, +``` + +### `singleColumnList` + +Whether the list should always be a single column, rather than auto-flow. + +```typescript +{ + singleColumnList: true, +}, +``` + +## `addMenuList` + +Adds a list of text options to a menu. + +Parameters: + +* `menuName`: Name of the menu. +* `settings`: Settings for the list, particularly its options. + +### `IListMenuOption` + +Individual option within a list. +Only `text` is required. + +#### `callback` + +Callback for when the option is triggered. +Receives just the menu name. + +```typescript +{ + options: [ + { + callback: () => console.log("First!"), + text: "First", + }, + ], +}, +``` + +#### `position` + +Position offsets to shift the option by, allowing top, right, bottom, left. + +```typescript +{ + options: [ + { + position: { + top: 30, + right: -20, + }, + text: "First", + }, + ], +}, +``` + +#### `text` + +Text displayed as the option. +This text will be rendered all in one row, similar to dialogs. + + +### `IListMenuOptions` + +Settings to create a new list menu. +Only + +#### `bottom` + +A bottom option to display underneath displayed options. +If the list is two-dimensional, this will span across all rows. + +```typescript +{ + bottom: { + callback: () => console.log("Cancelled."), + text: "Cancel", + }, +}, +``` + +#### `options` + +Options within the menu, or a function to generate them. +This is a flat list of options regarldess of whether the menu is one- or two-dimensional. + +```typescript +{ + options: [ + { + callback: () => console.log("First!"), + text: "First", + }, + ], +}, +``` + +See `IListMenuOption` above. + +#### `selectedIndex` + +Each list contains a `selectedIndex: [number, number]` of the position of the currently selected index. +This option overrides the starting selected index. +It defaults to `[0, 0]`. + +## Examples + +Showing a simple "Yes/No" menu after a general text dialog that stays alive: + +```typescript +const finalize = (choice) => { + console.log("Choice:", choice); + menuGrapher.deleteMenu("Yes/No"); +}; + +const createOptions = () => { + menuGrapher.createMenu("Yes/No", { + killOnB: ["GeneralText"], + }); + + menuGrapher.addMenuList("Yes/No", { + options: [ + { + text: "YES", + callback: () => finalize(true), + }, + { + text: "NO", + callback: () => finalize(false), + } + ], + }); + + menuGrapher.setActiveMenu("Yes/No"); +}; + +menuGrapher.createMenu("GeneralText", { + finishAutomatically: true, + keepOnBack: true, +}); +menuGrapher.addMenuDialog("GeneralText", "Are you sure?", createOptions); +menuGrapher.setActiveMenu("GeneralText"); +``` + +Creating a phone-style keyboard with 0-9, *, and #: diff --git a/docs/schemas.md b/docs/schemas.md new file mode 100644 index 0000000..9c0a39e --- /dev/null +++ b/docs/schemas.md @@ -0,0 +1,337 @@ +# Menu Schemas + +## `IMenuSchema` + +Attributes describing menu appearance and behavior. +These may be specified in the default schemas on a MenuGraphr instance or overriden with `createMenu`. + +All properties are optional. + +> See [`dialogs.md`](./dialogs.md) for properties specific to menu dialogs. + +> See [`lists.md`](./lists.md) for properties specific to menu lists. + +> See [`text.md`](./text.md) for how properties around displaying text in dialog and list menus. + +### `backMenu` + +Name of a menu to set as active when this one is deleted. + +```typescript +{ + backMenu: "GeneralText", +}, +``` + +### `callback` + +Callback for when this menu is set as active. +Called with the menu name. + +```typescript +{ + callback: (menuName) => { + console.log("Set", menuName, "as active."); + }, +}, +``` + +### `childrenSchemas` + +Schemas of children to add on creation. +These will be directly passed to `createMenuChild`, which will call `createMenu`, `createMenuWord`, or `createMenuThing` as per the child type. +As with regular menu schemas, these allow all properties as overrides. + +```typescript +{ + childrenSchemas: [ + { + type: "text", + words: ["Hello", "world!"], + }, + { + type: "thing", + thing: "PlayerPortrait", + position: { + horizontal: "right", + }, + }, + { + type: "menu", + name: "PlayerStats", + }, + ], +}, +``` + +> See `IMenuChildSchema`. + +### `container` + +Name of a containing menu to position within. +If not provided, this defaults to the entire game canvas. + +```typescript +{ + container: "GeneralText", +}, +``` + +### `height` + +How tall the menu should be, as Thing height. +This will also set the general Thing height of the menu. + +```typescript +{ + height: 80, +}, +``` + +### `ignoreA` + +Whether user selection events should be ignored. +These "A" events normally advance menu dialogs forward or trigger selected items in lists. + +```typescript +{ + ignoreA: true, +}, +``` + +### `ignoreB` + +Whether user deselection events should be ignored. +These "B" events normally exit out of menus. + +```typescript +{ + ignoreB: true, +}, +``` + +### `ignoreProgressB` + +Whether deselection events should count as selection during dialogs. +Menus with "progress" are in the middle of dialog or list creation. +Pressing B during progress would normally advance the menu forward. + +```typescript +{ + ignoreProgressB: true, +}, +``` + +### `keepOnBack` + +Whether this should be kept alive when deselected. +Useful for switching active state between multiple menus on B. + +```typescript +{ + keepOnBack: true, +}, +``` + +### `killOnB` + +Other menus to kill when this is deselected. +Commonly used with "Yes/No"-style dialogs that appear along with text descriptions in other menus. + +```typescript +{ + killOnB: ["GeneralText", "OtherDecorations"], +}, +``` + +### `onActive` + +Callback for when the menu becomes active. +Receives just the menu name. + +```typescript +{ + onActive: (menuName) => { + console.log("Menu", menuName, "is now active."); + }, +}, +``` + +### `onBPress` + +Callback for when the "B" button is pressed while the menu is active. +Receives just the menu name. + +Does not fire if `ignoreB` is true. +Also does not fire if the menu is mid-progress and `ignoreProgressB` is not true, as that simulates an "A" press. + +```typescript +{ + onBPress: (menuName) => { + console.log("Menu", menuName, "received a B press."); + }, +}, +``` + +### `onDown` + +Callback for when the "down" button is pressed. +Receives just the menu name. + +```typescript +{ + onDown: (menuName) => { + console.log("Menu", menuName, "received a down event."); + }, +}, +``` + +### `onInactive` + +Callback for when the menu becomes inactive. +Receives just the menu name. + +```typescript +{ + onActive: (menuName) => { + console.log("Menu", menuName, "is now active."); + }, +}, +``` + +### `onLeft` + +Callback for when the "left" button is pressed. +Receives just the menu name. + +```typescript +{ + onLeft: (menuName) => { + console.log("Menu", menuName, "received a left event."); + }, +}, +``` + +### `onMenuDelete` + +Callback for when the menu is deleted. +Receives just the menu name. + +This is called _after_ the menu is deleted, but _before_ menu children are deleted. + +```typescript +{ + onMenuDelete: (menuName) => { + console.log("Menu", menuName, "was deleted."); + }, +}, +``` + +### `onRight` + +Callback for when the "right" button is pressed. +Receives just the menu name. + +```typescript +{ + onRight: (menuName) => { + console.log("Menu", menuName, "received a right event."); + }, +}, +``` + +### `onUp` + +Callback for when the "up" button is pressed. +Receives just the menu name. + +```typescript +{ + onUp: (menuName) => { + console.log("Menu", menuName, "received an up event."); + }, +}, +``` + +### `size` + +Sizing description, including `height` and `width`. +This will override the native Thing `height` and `width` on the Menu. + +```typescript +{ + size: { + height: 80, + width: 40, + }, +}, +``` + +```typescript +{ + +}, +``` + +> This allows menus to act as containers at a different size from their visual Things. + +### `width` + +How wide the menu should be, as Thing width. +This will also set the general Thing width of the menu. + +```typescript +{ + width: 40, +}, +``` + +### `position` + +How the menu should be positioned within its container. +Defaults to the menu aligning itself to the top-left corner of its container and no width or height. + +#### `horizontal` + +Modifies how the schema lays itself out horizontally. + +* If `"center"`, aligns to the horizontal midpoint of its container. +* If `"right"`, its right aligns with its container's right. +* If `"stretch"`, stretches to fit its container horizontally. + +```typescript +position: { + horizontal: "right", +}, +``` + +#### `offset` + +Horizontal and vertical offsets to shift the menu by. +These are allowed to be negative numbers, are calculated relative to the menu's container, and each reduce the menu's size horizontally or vertically. + +```typescript +position: { + offset: { + top: -1, + right: 2, + bottom: 3, + left: -4, + }, +}, +``` + +#### `vertical` + +Modifies how the schema lays itself out vertically. + +* If `"center"`, aligns to the vertical midpoint of its container. +* If `"bottom"`, its bottom aligns with its container's bottom. +* If `"stretch"`, stretches to fit its container vertically. + +```typescript +position: { + vertical: "bottom", +}, +``` + diff --git a/docs/text.md b/docs/text.md new file mode 100644 index 0000000..a17ab7b --- /dev/null +++ b/docs/text.md @@ -0,0 +1,74 @@ +# Text + +Text within menu dialogs and lists can be displayed with granular controls over paddings and spacing. + +## `IMenuSchema` + +These options will be used when the menu has a dialog or list added. + +### `textPaddingRight` + +How much padding there is between the right of the text and the right side of the box. +Text in dialogs will return to the next line instead of crossing the menu's `right` minus `textPaddingRight`. +Defaults to `0` (none) if not provided. +Allowed to be negative. + +```typescript +{ + textPaddingRight: 8, +}, +``` + +### `textPaddingX` + +How much horizontal padding should be between characters. +Defaults to the `"Text"` Thing prototype's `paddingX` if it exists, or `0` otherwise. +Characters placed next to each other in dialogs and lists will be placed this number of game pixels to the right each subsequent character. + +```typescript +{ + textPaddingX: 4, +}, +``` + +### `textPaddingY` + +How much vertical padding should be between lines of text. +Defaults to the `"Text"` Thing prototype's `paddingY` if it exists, or `0` otherwise. +Lines of text in dialogs and lists will start this number of game pixels lower each line. + +```typescript +{ + textPaddingY: 12, +}, +``` + +### `textSpeed` + +How long to delay between placing characters and words. +If `0` or not provided, characters and words will be placed immediately. + +Otherwise, each character will wait a `textSpeed` delay before displaying. +Words will wait a `textSpeed` delay before appearing after each other as well, which gives the illusion of spaces between words also adhering to the delay. + +### `textXOffset` + +Horizontal offset for the text placement area. +All text will be offset horizontally by this amount. + +```typescript +{ + textXOffset: 2, +}, +``` + +### `textYOffset` + +Vertical offset for the text placement area. +All text will be offset vertically by this amount. + +```typescript +{ + textYOffset: -2, +}, +``` diff --git a/src/IMenuGraphr.ts b/src/IMenuGraphr.ts index 8d5779c..60352df 100644 --- a/src/IMenuGraphr.ts +++ b/src/IMenuGraphr.ts @@ -5,14 +5,14 @@ import { GameStartr, IThing } from "gamestartr"; */ export interface IMenuBase { /** - * A menu to set as active when this one is deleted. + * Name of a menu to set as active when this one is deleted. */ backMenu?: string; /** - * A callback for when this menu is set as active. + * Callback for when this menu is set as active. */ - callback?(...args: any[]): void; + callback?(menuName: string): void; /** * Schemas of children to add on creation. @@ -20,7 +20,7 @@ export interface IMenuBase { childrenSchemas?: IMenuChildSchema[]; /** - * A containing menu to position within. + * Name of a containing menu to position within. */ container?: string; @@ -36,7 +36,7 @@ export interface IMenuBase { finishAutomatically?: boolean; /** - * How long to delay completion when finishAutomatically is true. + * How many game ticks to delay completion by when finishAutomatically is true. */ finishAutomaticSpeed?: number; @@ -71,65 +71,50 @@ export interface IMenuBase { killOnB?: string[]; /** - * A callback for when this becomes active. + * Callback for when this becomes active. */ - onActive?(name: string): void; + onActive?(menuName: string): void; /** - * A callback for when this is deselected. + * Callback for when the "B" button is pressed. */ - onBPress?(name: string): void; + onBPress?(menuName: string): void; /** - * A callback for a user event directing down. + * Callback for when the "down" button is pressed. */ - onDown?(gameStarter: GameStartr): void; + onDown?(menuName: string): void; /** - * A callback for when this becomes inactive. + * Callback for when this becomes inactive. */ - onInactive?(name: string): void; + onInactive?(menuName: string): void; /** - * A callback for a user event directing to the left. + * Callback for when the "left" button is pressed. */ - onLeft?(gameStarter: GameStartr): void; + onLeft?(menuName: string): void; /** - * A callback for when this is deleted. + * Callback for when this is deleted. */ onMenuDelete?(gameStarter: GameStartr): void; /** - * A callback for a user event directing to the right. + * Callback for when the "right" button is pressed. */ onRight?(gameStarter: GameStartr): void; /** - * A callback for a user event directing up. + * Callback for when the "up" button is pressed. */ onUp?(gameStarter: GameStartr): void; /** - * A sizing description for this, including width and height. + * Sizing description for this, including width and height. */ size?: IMenuSchemaSize; - /** - * A menu to set as active if the start button is pressed while this menu is active. - */ - startMenu?: string; - - /** - * A manual width for the area text may be placed in. - */ - textAreaWidth?: number; - - /** - * How tall text characters should be treated as. - */ - textHeight?: number; - /** * How much padding there is between the right of the text and the right side of the box. */ @@ -146,32 +131,17 @@ export interface IMenuBase { textPaddingY?: number; /** - * How long to delay between placing words. + * How long to delay between placing characters and words. */ textSpeed?: number; /** - * A manual starting x-location for dialog text. - */ - textStartingX?: string; - - /** - * How wide text characters should be treated as. - */ - textWidth?: number; - - /** - * A multiplier for textWidth. Commonly -1 for right-to-left text. - */ - textWidthMultiplier?: number; - - /** - * A horizontal offset for the text placement area. + * Horizontal offset for the text placement area. */ textXOffset?: number; /** - * A vertical offset for text placement area. + * Vertical offset for text placement area. */ textYOffset?: number; @@ -208,7 +178,7 @@ export interface IMenuProgress { complete?: boolean; /** - * A callback for when the dialog completes. + * Callback for when the dialog completes. */ onCompletion?(...args: any[]): void; @@ -240,13 +210,12 @@ export interface IMenuSchema extends IMenuBase { */ export interface IListMenuSchema extends IMenuSchema { /** - * Whether or not the last selected index should be saved. + * Whether the last selected index should be saved. */ saveIndex?: boolean; /** - * The names of all menu indices that should be forgotton upon deletion of - * this menu. + * Names of menus whose whose selected indices that should be cleared when this menu is deleted. */ clearedIndicesOnDeletion?: string[]; @@ -259,6 +228,11 @@ export interface IListMenuSchema extends IMenuSchema { * Whether scrolling items should be computed on creation. */ scrollingItemsComputed?: boolean; + + /** + * Whether the list should always be a single column, rather than auto-flow. + */ + singleColumnList?: boolean; } /** @@ -276,30 +250,34 @@ export interface IMenuSchemaSize { height?: number; } +/** + * Modifies how a schema lays itself out horizontally. + */ +export type ISchemaPositionHorizontalModifier = "center" | "right" | "stretch"; + +/** + * Modifies how a schema lays itself out vertically. + */ +export type ISchemaPositionVerticalModifier = "center" | "bottom" | "stretch"; + /** * A description of how a meny should be positioned within its container. */ export interface IMenuSchemaPosition { /** - * An optional horizontal position modifier, as "center", "right", or "stretch". + * Modifies how the schema lays itself out horizontally. */ - horizontal?: string; + horizontal?: ISchemaPositionHorizontalModifier; /** - * Horizontal and vertical offsets to shfit the menu by. + * Horizontal and vertical offsets to shift the menu by. */ offset?: IMenuSchemaPositionOffset; /** - * Whether this should have children not shifted vertically relative to the - * menu top (used exclusively by list menus). + * Modifies how the schema lays itself out vertically. */ - relative?: boolean; - - /** - * An optional vertical position modifier, as "center", "bottom", or "stretch". - */ - vertical?: string; + vertical?: ISchemaPositionVerticalModifier; } /** @@ -330,32 +308,36 @@ export interface IMenuSchemaPositionOffset { /** * A description of a menu child to create, including name and child type. */ -export interface IMenuChildSchema extends IMenuSchema { - /** - * What type of child this is. - */ - type: "menu" | "text" | "thing"; -} +export type IMenuChildSchema = + | IMenuChildMenuSchema + | IMenuWordSchema + | IMenuThingSchema +; /** * A description of a menu to create as a menu child. */ -export interface IMenuChildMenuSchema extends IMenuChildSchema { +export interface IMenuChildMenuSchema { /** * Menu attributes to pass to the menu. */ - attributes: IMenuSchema; + attributes?: IMenuSchema; /** * The name of the menu. */ name: string; + + /** + * What type of child this is. + */ + type: "menu"; } /** * A descripion of a word to create as a menu child. */ -export interface IMenuWordSchema extends IMenuChildSchema { +export interface IMenuWordSchema { /** * How to position the word within the menu. */ @@ -366,16 +348,21 @@ export interface IMenuWordSchema extends IMenuChildSchema { */ size?: IMenuSchemaSize; + /** + * What type of child this is. + */ + type: "text"; + /** * Raw words to set as the text contents. */ - words: (string | IMenuWordCommand)[]; + words: (string | string[] | IMenuWordCommand)[]; } /** * A description of a Thing to create as a menu child. */ -export interface IMenuThingSchema extends IMenuChildSchema { +export interface IMenuThingSchema { /** * Arguments to proliferate onto the Thing. */ @@ -395,6 +382,11 @@ export interface IMenuThingSchema extends IMenuChildSchema { * What Thing title to create. */ thing: string; + + /** + * What type of child this is. + */ + type: "thing"; } /** @@ -419,6 +411,11 @@ export interface IMenuWordCommandBase { word?: string; } +/** + * Command names to modify dialogs within text. + */ +export type MenuWordCommandName = "attribute" | "attributeReset" | "padLeft" | "position"; + /** * A word command to modify dialog within its text. */ @@ -429,9 +426,9 @@ export interface IMenuWordCommand extends IMenuWordCommandBase { attribute: string; /** - * The command, as "attribute", "attributeReset", "padLeft", or "position". + * Command name identifier. */ - command: string; + command: MenuWordCommandName; /** * A value for the attribute to change, if this is an attribute change command. @@ -498,11 +495,31 @@ export interface IMenu extends IThing, IMenuSchema { */ progress?: IMenuProgress; + /** + * A manual width for the area text may be placed in. + */ + textAreaWidth?: number; + + /** + * How tall text characters should be treated as. + */ + textHeight?: number; + + /** + * A manual starting x-location for dialog text. + */ + textStartingX?: string; + /** * Where text should start displaying, horizontally. */ textX?: number; + /** + * How wide text characters should be treated as. + */ + textWidth?: number; + /** * How wide this is. */ @@ -544,17 +561,17 @@ export interface IListMenuBase { */ export interface IListMenu extends IListMenuBase, IListMenuSchema, IMenu { /** - * The arrow Thing indicating the current selection. + * Arrow Thing indicating the current selection. */ arrow: IThing; /** - * A horizontal offset for the arrow Thing. + * Horizontal offset for the arrow Thing. */ arrowXOffset?: number; /** - * A vertical offset for the arrow Thing. + * Vertical offset for the arrow Thing. */ arrowYOffset?: number; @@ -578,11 +595,6 @@ export interface IListMenu extends IListMenuBase, IListMenuSchema, IMenu { */ scrollingVisualOffset?: number; - /** - * Whether the list should be a single column, rather than auto-flow. - */ - singleColumnList: boolean; - /** * How wide each column of text should be in the grid. */ @@ -659,6 +671,33 @@ export interface IGridCell { y: number; } +/** + * Callback for when a list menu option is triggered. + * + * @param menuName Name of the containing menu. + */ +export type IListMenuOptionCallback = (menuName: string) => void; + +/** + * Single text option in a list. + */ +export interface IListMenuOption { + /** + * Callback for when the option is triggered. + */ + callback?: IListMenuOptionCallback; + + /** + * Horizontal and vertical offsets to shift the option by. + */ + position?: IMenuSchemaPositionOffset; + + /** + * Text displayed as the option. + */ + text: string; +} + /** * Settings to create a new list menu. */ @@ -666,12 +705,12 @@ export interface IListMenuOptions { /** * A bottom option to place below all grid options. */ - bottom?: any; + bottom?: IListMenuOption; /** - * Options within the menu, or a Function to generate them. + * Options within the menu, or a function to generate them. */ - options: any[] | (() => any[]); + options: IListMenuOption[] | (() => IListMenuOption[]); /** * A default starting selected index. @@ -705,7 +744,7 @@ export interface IListMenuProgress extends IMenuProgress { } /** - * A list of sounds that should be played for certain menu actions. + * Sounds that should be played for certain menu actions. */ export interface ISoundNames { /** @@ -743,18 +782,18 @@ export interface IMenuGraphrSettings { */ gameStarter: GameStartr; + /** + * Alternate Thing titles for characters, such as " " for "Space". + */ + aliases?: IAliases; + /** * Known menu schemas, keyed by name. */ schemas?: IMenuSchemas; /** - * Alternate Thing titles for charactes, such as " " for "space". - */ - aliases?: IAliases; - - /** - * A list of sounds that should be played for certain menu actions. + * Sounds that should be played for certain menu actions. */ sounds?: ISoundNames; @@ -764,7 +803,7 @@ export interface IMenuGraphrSettings { replacements?: IReplacements; /** - * The separator for words to replace using replacements. + * Separator for words to replace using replacements. */ replacerKey?: string; } @@ -783,7 +822,7 @@ export interface IMenuGraphr { * @param name A name of a menu. * @returns The menu under the given name. */ - getMenu(name: string): IMenu; + getMenu(menuName: string): IMenu; /** * Returns a menu, throwing an error if it doesn't exist. @@ -791,7 +830,7 @@ export interface IMenuGraphr { * @param name A name of a menu. * @returns The menu under the given name. */ - getExistingMenu(name: string): IMenu; + getExistingMenu(menuName: string): IMenu; /** * @returns The currently active menu. @@ -818,11 +857,11 @@ export interface IMenuGraphr { * Default information is used from the schema of that name, such as position and * children, but may be override by attributes. * - * @param name The name of the menu. + * @param menuName Name of the menu. * @param attributes Custom attributes to apply to the menu. * @returns The newly created menu. */ - createMenu(name: string, attributes?: IMenuSchema): IMenu; + createMenu(menuName: string, attributes?: IMenuSchema): IMenu; /** * Adds a child object to an existing menu. @@ -833,39 +872,39 @@ export interface IMenuGraphr { * @remarks Creating a menu is done using this.createMenu, so the created menu might * not mark itself as a child of the parent. */ - createMenuChild(name: string, schema: IMenuChildSchema): IThing | IThing[]; + createMenuChild(menuName: string, schema: IMenuChildSchema): IThing | IThing[]; /** * Creates a series of words as a child of a menu. * - * @param name The name of the menu. + * @param menuName Name of the menu. * @param schema Settings for the words. * @returns The words' character Things. */ - createMenuWord(name: string, schema: IMenuWordSchema): IThing[]; + createMenuWord(menuName: string, schema: IMenuWordSchema): IThing[]; /** * Creates a Thing as a child of a menu. * - * @param name The name of the menu. + * @param menuName Name of the menu. * @param schema Settings for the Thing. * @returns The newly created Thing. */ - createMenuThing(name: string, schema: IMenuThingSchema): IThing; + createMenuThing(menuName: string, schema: IMenuThingSchema): IThing; /** * Hides a menu of the given name and deletes its children, if it exists. * - * @param name The name of the menu to hide. + * @param menuName Name of the menu to hide. */ - hideMenu(name: string): void; + hideMenu(menuName: string): void; /** * Deletes a menu of the given name, if it exists. * - * @param name The name of the menu to delete. + * @param menuName Name of the menu to delete. */ - deleteMenu(name: string): void; + deleteMenu(menuName: string): void; /** * Deletes the active menu, if it exists. @@ -880,58 +919,57 @@ export interface IMenuGraphr { /** * Adds dialog-style text to a menu. If the text overflows, * - * @param name The name of the menu. + * @param menuName Name of the menu. * @param dialog Raw dialog to add to the menu. * @param onCompletion An optional callback for when the text is done. */ - addMenuDialog(name: string, dialog: IMenuDialogRaw, onCompletion?: () => any): void; + addMenuDialog(menuName: string, dialog: IMenuDialogRaw, onCompletion?: () => any): void; /** * Continues a menu from its current display words to the next line. * * @param name The name of the menu. */ - continueMenu(name: string): void; + continueMenu(menuName: string): void; /** * Adds a list of text options to a menu. * - * @param name The name of the menu. - * @param settings Settings for the list, particularly its options, starting - * index, and optional floating bottom. + * @param menuName Name of the menu. + * @param settings Settings for the list, particularly its options. */ - addMenuList(name: string, settings: IListMenuOptions): void; + addMenuList(menuName: string, settings: IListMenuOptions): void; /** * Retrives the currently selected grid cell of a menu. * - * @param name The name of the menu. + * @param menuName Name of the menu. * @returns The currently selected grid cell of the menu. */ - getMenuSelectedOption(name: string): IGridCell; + getMenuSelectedOption(menuName: string): IGridCell; /** * Shifts the selected index of a list menu, adjusting for scrolling if necessary. * - * @param name The name of the menu. + * @param menuName Name of the menu. * @param dx How far along the menu's grid to shift horizontally. * @param dy How far along the menu's grid to shift vertically. */ - shiftSelectedIndex(name: string, dx: number, dy: number): void; + shiftSelectedIndex(menuName: string, dx: number, dy: number): void; /** * Sets the current selected index of a menu. * - * @param name The name of the menu. + * @param menuName Name of the menu. * @param x The new horizontal value for the index. * @param y The new vertical value for the index. */ - setSelectedIndex(name: string, x: number, y: number): void; + setSelectedIndex(menuName: string, x: number, y: number): void; /** * Sets the currently active menu. * - * @param name The name of the menu to set as active. If not given, no menu + * @param menuName Name of the menu to set as active. If not given, no menu * is set as active. */ setActiveMenu(name?: string): void; @@ -972,9 +1010,4 @@ export interface IMenuGraphr { * Reacts to a user event from pressing a deselection key. */ registerB(): void; - - /** - * Reacts to a user event from pressing a start key. - */ - registerStart(): void; } diff --git a/src/MenuGraphr.ts b/src/MenuGraphr.ts index 79a075a..fdd9b2f 100644 --- a/src/MenuGraphr.ts +++ b/src/MenuGraphr.ts @@ -1,8 +1,8 @@ import { GameStartr, IThing } from "gamestartr"; import { - IAliases, IGridCell, IListMenu, IListMenuOptions, IListMenuProgress, - IMenu, IMenuBase, IMenuChildMenuSchema, IMenuChildSchema, + IAliases, IGridCell, IListMenu, IListMenuOption, IListMenuOptions, + IListMenuProgress, IMenu, IMenuBase, IMenuChildSchema, IMenuDialogRaw, IMenuGraphr, IMenuGraphrSettings, IMenuSchema, IMenuSchemaPosition, IMenuSchemaPositionOffset, IMenuSchemas, IMenuSchemaSize, IMenusContainer, IMenuThingSchema, IMenuWordCommand, @@ -41,7 +41,7 @@ export class MenuGraphr implements IMenuGraphr { private readonly schemas: IMenuSchemas; /** - * A list of sounds that should be played for certain menu actions + * Sounds that should be played for certain menu actions. */ private readonly sounds: ISoundNames; @@ -55,6 +55,11 @@ export class MenuGraphr implements IMenuGraphr { */ private readonly replacements: IReplacements; + /** + * Separator for words to replace using replacements. + */ + private readonly replacerKey: string; + /** * The currently "active" (user-selected) menu. */ @@ -66,18 +71,12 @@ export class MenuGraphr implements IMenuGraphr { * @param settings Settings to be used for initialization. */ public constructor(settings: IMenuGraphrSettings) { - if (!settings) { - throw new Error("No settings object given to MenuGraphr."); - } - if (!settings.gameStarter) { - throw new Error("No GameStartr given to MenuGraphr."); - } - this.gameStarter = settings.gameStarter; this.schemas = settings.schemas || {}; this.aliases = settings.aliases || {}; this.replacements = settings.replacements || {}; + this.replacerKey = settings.replacerKey || "%%%%%%%"; this.sounds = settings.sounds || {}; this.menus = {}; @@ -205,16 +204,16 @@ export class MenuGraphr implements IMenuGraphr { public createMenuChild(name: string, schema: IMenuChildSchema): IThing | IThing[] { switch (schema.type) { case "menu": - return this.createMenu((schema as IMenuChildMenuSchema).name, (schema as IMenuChildMenuSchema).attributes); + return this.createMenu(schema.name, schema.attributes); case "text": - return this.createMenuWord(name, schema as IMenuWordSchema); + return this.createMenuWord(name, schema); case "thing": - return this.createMenuThing(name, schema as IMenuThingSchema); + return this.createMenuThing(name, schema); default: - throw new Error(`Unknown schema type: '${schema.type}'.`); + throw new Error(`Unknown schema type: '${(schema as IMenuChildSchema).type}'.`); } } @@ -312,21 +311,21 @@ export class MenuGraphr implements IMenuGraphr { } /** - * Adds dialog-style text to a menu. If the text overflows, + * Adds dialog-style text to a menu. * - * @param name The name of the menu. + * @param menuName Name of the menu. * @param dialog Raw dialog to add to the menu. * @param onCompletion An optional callback for when the text is done. */ - public addMenuDialog(name: string, dialog: IMenuDialogRaw, onCompletion?: () => any): void { + public addMenuDialog(menuName: string, dialog: IMenuDialogRaw, onCompletion?: () => any): void { const dialogParsed: (string[] | IMenuWordCommand)[][] = this.parseRawDialog(dialog); let currentLine = 1; const callback: () => void = (): void => { // If all dialog has been exhausted, delete the menu and finish if (currentLine >= dialogParsed.length) { - if (this.menus[name].deleteOnFinish) { - this.deleteMenu(name); + if (this.menus[menuName].deleteOnFinish) { + this.deleteMenu(menuName); } if (onCompletion) { onCompletion(); @@ -338,16 +337,15 @@ export class MenuGraphr implements IMenuGraphr { // Delete any previous texts. This is only done if continuing // So that when the dialog is finished, the last text remains - this.deleteMenuChildren(name); + this.deleteMenuChildren(menuName); // This continues the dialog with the next iteration (word) - this.addMenuText(name, dialogParsed[currentLine - 1], callback); + this.addMenuText(menuName, dialogParsed[currentLine - 1], callback); }; - // This first call to addMenuText shouldn't be the callback, because if - // Being called from a childrenSchema of type "text", it shouldn't delete - // Any other menu children from childrenSchemas. - this.addMenuText(name, dialogParsed[0], callback); + // This first call to addMenuText shouldn't be the callback. + // If called from a childrenSchema of type "text", it shouldn't delete any other menu children from childrenSchemas. + this.addMenuText(menuName, dialogParsed[0], callback); } /** @@ -395,13 +393,12 @@ export class MenuGraphr implements IMenuGraphr { * Adds a list of text options to a menu. * * @param name The name of the menu. - * @param settings Settings for the list, particularly its options, starting - * index, and optional floating bottom. + * @param settings Settings for the list, particularly its options. */ public addMenuList(name: string, settings: IListMenuOptions): void { const menu: IListMenu = this.getExistingMenu(name) as IListMenu; - const options: any[] = settings.options.constructor === Function - ? (settings.options as any)() + const options: IListMenuOption[] = typeof settings.options === "function" + ? settings.options() : settings.options; let left: number = menu.left + (menu.textXOffset || 0); const top: number = menu.top + (menu.textYOffset || 0); @@ -424,7 +421,7 @@ export class MenuGraphr implements IMenuGraphr { let j: number; let k: number; - menu.options = options; + menu.options = options as any[]; menu.optionChildren = optionChildren; menu.callback = this.triggerMenuListOption.bind(this); @@ -467,7 +464,7 @@ export class MenuGraphr implements IMenuGraphr { menu.children.push(character); optionChild.things.push(character); - if (!schema.position || !schema.position.relative) { + if (!schema.position) { this.gameStarter.physics.shiftVert(character, y - menu.top); } } @@ -778,17 +775,17 @@ export class MenuGraphr implements IMenuGraphr { * Reacts to a user event directing down. */ public registerDown(): void { - const menu: IListMenu = this.activeMenu as IListMenu; + const menu = this.activeMenu; if (!menu) { return; } - if (menu.selectedIndex) { + if ((menu as IListMenu).selectedIndex) { this.shiftSelectedIndex(menu.name, 0, 1); } if (menu.onDown) { - menu.onDown(this.gameStarter); + menu.onDown(menu.name); } } @@ -806,7 +803,7 @@ export class MenuGraphr implements IMenuGraphr { } if (menu.onLeft) { - menu.onLeft(this.gameStarter); + menu.onLeft(menu.name); } } @@ -861,20 +858,6 @@ export class MenuGraphr implements IMenuGraphr { } } - /** - * Reacts to a user event from pressing a start key. - */ - public registerStart(): void { - const menu: IListMenu = this.activeMenu as IListMenu; - if (!menu) { - return; - } - - if (menu.startMenu) { - this.setActiveMenu(menu.startMenu); - } - } - /** * Adds a series of words to a menu. * @@ -939,7 +922,6 @@ export class MenuGraphr implements IMenuGraphr { let textPaddingX: number; let textPaddingY: number; let textSpeed: number; - let textWidthMultiplier: number; let character: IText; let j: number; @@ -960,16 +942,15 @@ export class MenuGraphr implements IMenuGraphr { textSpeed = typeof menu.textSpeed === undefined ? 1 : menu.textSpeed || 0; textWidth = menu.textWidth || textProperties.width; textPaddingRight = menu.textPaddingRight || 0; - textPaddingX = menu.textPaddingX || textProperties.paddingX; - textPaddingY = menu.textPaddingY || textProperties.paddingY; - textWidthMultiplier = menu.textWidthMultiplier || 1; + textPaddingX = menu.textPaddingX || textProperties.paddingX || 0; + textPaddingY = menu.textPaddingY || textProperties.paddingY || 0; // For each character in the word, schedule it appearing in the menu for (j = 0; j < word.length; j += 1) { // For non-whitespace characters, add them and move to the right if (/\S/.test(word[j])) { character = this.addMenuCharacter(name, word[j], x, y, j * textSpeed); - x += textWidthMultiplier * (character.width + textPaddingX); + x += character.width + textPaddingX; continue; } @@ -979,7 +960,7 @@ export class MenuGraphr implements IMenuGraphr { x = menu.textX!; y += textPaddingY; } else if (word[j] !== " " || x !== menu.textX) { - x += textWidth * textWidthMultiplier; + x += textWidth; } } @@ -1261,13 +1242,13 @@ export class MenuGraphr implements IMenuGraphr { /** * Runs the callback for a menu's selected list option. * - * @param name The name of the menu. + * @param menuName Name of the containing menu. */ - private triggerMenuListOption(name: string): void { - const selected: IGridCell = this.getMenuSelectedOption(name); + private triggerMenuListOption(menuName: string): void { + const selected: IGridCell = this.getMenuSelectedOption(menuName); if (selected.callback) { - selected.callback.call(this, name); + selected.callback.call(this, menuName); } } @@ -1337,7 +1318,7 @@ export class MenuGraphr implements IMenuGraphr { * character itself otherwise. */ private getCharacterEquivalent(character: string): string { - if (this.aliases.hasOwnProperty(character)) { + if ({}.hasOwnProperty.call(this.aliases, character)) { return this.aliases[character]; } @@ -1435,11 +1416,11 @@ export class MenuGraphr implements IMenuGraphr { let end: number; let inside: string | string[]; - start = word.indexOf("%%%%%%%", 0); - end = word.indexOf("%%%%%%%", start + 1); + start = word.indexOf(this.replacerKey, 0); + end = word.indexOf(this.replacerKey, start + 1); if (start !== -1 && end !== -1) { - inside = this.getReplacement(word.substring(start + "%%%%%%%".length, end)); + inside = this.getReplacement(word.substring(start + this.replacerKey.length, end)); if (inside.constructor === Number) { inside = inside.toString().split(""); } else if (inside.constructor === String) { @@ -1448,7 +1429,7 @@ export class MenuGraphr implements IMenuGraphr { output.push(...word.substring(0, start).split("")); output.push(...(inside as string[])); - output.push(...this.filterWord(word.substring(end + "%%%%%%%".length))); + output.push(...this.filterWord(word.substring(end + this.replacerKey.length))); return output; } @@ -1462,7 +1443,7 @@ export class MenuGraphr implements IMenuGraphr { * @param words The words to filter, as Strings or command Objects. * @returns The words, with all Strings filtered. */ - private filterMenuWords(words: (string | IMenuWordCommand)[]): (string[] | IMenuWordCommand)[] { + private filterMenuWords(words: (string | string[] | IMenuWordCommand)[]): (string[] | IMenuWordCommand)[] { const output: (string[] | IMenuWordCommand)[] = []; for (const word of words) {