import { EightBittr , Actor } from "eightbittr" ; import { Aliases , GridCell , ListMenu , ListMenuOption , ListMenuOptions , ListMenuProgress , Menu , MenuBase , MenuChildSchema , MenuDialogRaw , MenuGraphrSettings , MenuSchema , MenuSchemaPosition , MenuSchemaPositionOffset , MenuSchemas , MenuSchemaSize , MenusContainer , MenuActorSchema , MenuWordCommand , MenuWordCommandBase , MenuWordPadLeftCommand , MenuWordPosition , MenuWordSchema , Replacements , ReplacerFunction , SoundEvents , Text , } from "./types" ; /**
 * Cardinal directions as Numbers.
 */ export enum Direction { Top = 0 , Right = 1 , Bottom = 2 , Left = 3 } /**
 * Text-based menu and dialog management system.
 */ export class MenuGraphr { /**
     * The parent EightBittr managing Actors.
     */ private readonly game : EightBittr ; /**
     * All available menus, keyed by name.
     */ private readonly menus : MenusContainer ; /**
     * Known menu schemas, keyed by name.
     */ private readonly schemas : MenuSchemas ; /**
     * Sounds that should be played for certain menu actions.
     */ private readonly sounds : SoundEvents ; /**
     * Alternate Actor titles for characters, such as " " for "space".
     */ private readonly aliases : Aliases ; /**
     * Programmatic replacements for deliniated words.
     */ private readonly replacements : Replacements ; /**
     * Separator for words to replace using replacements.
     */ private readonly replacerKey : string ; /**
     * The currently "active" (user-selected) menu.
     */ private activeMenu ? : Menu ; param settings public constructor ( settings ) { this . game = settings . game ; this . schemas = settings . schemas || { } ; this . aliases = settings . aliases || { } ; this . replacements = settings . replacements || { } ; this . replacerKey = settings . replacerKey || "%%%%%%%" ; this . sounds = settings . sounds || { } ; this . menus = { } ; } returns public getMenus ( ) { return this . menus ; } param name returns public getMenu ( name ) { return this . menus [ name ] ; } param name returns public getExistingMenu ( name ) { if ( ! this . menus [ name ] ) { throw new Error ( `The '${ name }' menu does not exist.` ) ; } return this . menus [ name ] ; } returns public getActiveMenu ( ) { return this . activeMenu ; } returns public getActiveMenuName ( ) { if ( ! this . activeMenu ) { throw new Error ( "There is no active menu." ) ; } return this . activeMenu . name ; } returns public getAliases ( ) { return this . aliases ; } returns public getReplacements ( ) { return this . replacements ; } param name param attributes returns public createMenu ( name , attributes ? ) { const schemaRaw = this . game . utilities . proliferate ( { } , this . schemas [ name ] ) ; const schema = this . game . utilities . proliferate ( schemaRaw , attributes ) ; const menu = this . game . objectMaker . make < Menu > ( "Menu" , schema ) ; const container = schema . container ? this . getExistingMenu ( schema . container ) : ( { bottom : this . game . mapScreener . height , children : [ ] , height : this . game . mapScreener . height , left : 0 , right : this . game . mapScreener . width , top : 0 , width : this . game . mapScreener . width , } as any ) ; this . deleteMenu ( name ) ; this . menus [ name ] = menu ; menu . name = name ; this . placeMenuActor ( container , menu , schema . size , schema . position ) ; menu . children = [ ] ; menu . textAreaWidth = menu . width - ( menu . textXOffset || 0 ) * 2 ; if ( menu . childrenSchemas ) { menu . childrenSchemas . forEach ( this . createMenuChild . bind ( this , name ) ) ; } if ( container . children ) { container . children . push ( menu ) ; } this . game . utilities . proliferate ( menu , attributes ) ; return menu ; } param name param schema returns remarks public createMenuChild ( name , schema ) { switch ( schema . type ) { case "menu" : return this . createMenu ( schema . name , schema . attributes ) ; case "text" : return this . createMenuWord ( name , schema ) ; case "actor" : return this . createMenuActor ( name , schema ) ; default : throw new Error ( `Unknown schema type: '${ ( schema as MenuChildSchema ) . type }'.` ) ; } } param name param schema returns public createMenuWord ( name , schema ) { const menu = this . getExistingMenu ( name ) ; const container = this . game . objectMaker . make < Menu > ( "Menu" ) ; const words = this . filterMenuWords ( schema . words ) ; this . placeMenuActor ( menu , container , schema . size , schema . position , true ) ; menu . textX = container . left ; return this . addMenuWords ( name , words , 0 , container . left , container . top ) ; } param name param schema returns public createMenuActor ( name , schema ) { const menu = this . getExistingMenu ( name ) ; const actor = this . game . objectMaker . make < Actor > ( schema . actor , schema . args ) ; this . placeMenuActor ( menu , actor , schema . size , schema . position ) ; this . game . groupHolder . switchGroup ( actor , actor . groupType , "Text" ) ; menu . children . push ( actor ) ; return actor ; } param name public hideMenu ( name ) { const menu = this . menus [ name ] ; if ( menu ) { menu . hidden = true ; this . deleteMenuChildren ( name ) ; } } param name public deleteMenu ( name ) { const menu = this . menus [ name ] as ListMenu ; if ( ! menu ) { return ; } if ( menu . clearedIndicesOnDeletion ) { this . clearMenuIndices ( name ) ; } this . deleteMenuChild ( menu ) ; } /**
     * Deletes the active menu, if it exists.
     */ public deleteActiveMenu ( ) { if ( this . activeMenu ) { this . deleteMenu ( this . activeMenu . name ) ; } } /**
     * Deletes all menus.
     */ public deleteAllMenus ( ) { for ( const i in this . menus ) { if ( { } . hasOwnProperty . call ( this . menus , i ) ) { this . deleteMenu ( i ) ; } } } param menuName param dialog param onCompletion public addMenuDialog ( menuName , dialog , onCompletion ? ) { const dialogParsed = this . parseRawDialog ( dialog ) ; let currentLine = 1 ; const callback = ( ) : void => { if ( currentLine >= dialogParsed . length ) { if ( this . menus [ menuName ] . deleteOnFinish ) { this . deleteMenu ( menuName ) ; } if ( onCompletion ) { onCompletion ( ) ; } return ; } currentLine += 1 ; this . deleteMenuChildren ( menuName ) ; this . addMenuText ( menuName , dialogParsed [ currentLine - 1 ] , callback ) ; } ; this . addMenuText ( menuName , dialogParsed [ 0 ] , callback ) ; } param name public continueMenu ( name ) { const menu = this . getExistingMenu ( name ) as ListMenu ; const children = menu . children ; const progress = menu . progress ; if ( ! progress || progress . working ) { return ; } progress . working = true ; if ( progress . complete ) { if ( ! progress . onCompletion || progress . onCompletion ( this . game , menu ) ) { this . deleteMenu ( name ) ; } return ; } if ( menu . finishLinesAutomatically ) { for ( const character of children ) { this . scrollCharacterUp ( character , menu , 2 ) ; } this . addMenuWords ( name , progress . words , progress . i , progress . x , progress . y , progress . onCompletion ) ; } else { for ( const character of children ) { this . game . timeHandler . addEventInterval ( ( ) => this . scrollCharacterUp ( character , menu , 2 ) , 1 , ( character as Text ) . paddingY / 2 ) ; } this . game . timeHandler . addEvent ( ( ) : void => { this . addMenuWords ( name , progress . words , progress . i , progress . x , progress . y , progress . onCompletion ) ; } , ( ( children [ children . length - 1 ] as Text ) . paddingY / 2 ) | 0 ) ; } this . sounds . onInteraction ?. ( ) ; } param name param settings public addMenuList ( name , settings ) { const menu = this . getExistingMenu ( name ) as ListMenu ; const options = typeof settings . options === "function" ? settings . options ( ) : settings . options ; let left = menu . left + ( menu . textXOffset || 0 ) ; const top = menu . top + ( menu . textYOffset || 0 ) ; const textProperties = this . game . objectMaker . getPrototypeOf ( "Text" ) ; const textWidth = menu . textWidth || textProperties . width ; const textHeight = menu . textHeight || textProperties . height ; const textPaddingY = menu . textPaddingY || textProperties . paddingY || textHeight ; const selectedIndex = settings . selectedIndex || [ 0 , 0 ] ; const optionChildren = [ ] ; let index = 0 ; let y = top ; let option ; let optionChild ; let schema ; let title ; let character ; let column ; let x ; let i ; let j ; let k ; menu . options = options as any [ ] ; menu . optionChildren = optionChildren ; menu . callback = this . triggerMenuListOption . bind ( this ) ; menu . onActive = this . activateMenuList . bind ( this ) ; menu . onInactive = this . deactivateMenuList . bind ( this ) ; menu . grid = [ ] ; menu . grid [ 0 ] = column = [ ] ; menu . gridRows = 0 ; if ( ! options . length ) { return ; } for ( i = 0 ; i < options . length ; i += 1 ) { x = left ; option = options [ i ] ; optionChild = { option , actors : [ ] , } ; optionChildren . push ( optionChild ) ; option . x = x ; option . y = y ; column . push ( option ) ; option . column = column ; option . index = index ; option . columnNumber = menu . grid . length - 1 ; option . rowNumber = column . length - 1 ; menu . gridRows = Math . max ( menu . gridRows , column . length ) ; index += 1 ; if ( option . actors ) { for ( j = 0 ; j < option . actors . length ; j += 1 ) { schema = option . actors [ j ] ; character = this . createMenuActor ( name , schema ) ; menu . children . push ( character ) ; optionChild . actors . push ( character ) ; if ( ! schema . position ) { this . game . physics . shiftVert ( character , y - menu . top ) ; } } } if ( option . textsFloating ) { for ( j = 0 ; j < option . textsFloating . length ; j += 1 ) { schema = option . textsFloating [ j ] ; optionChild . actors = optionChild . actors . concat ( this . addMenuWords ( name , [ schema . text ] , 0 , x + schema . x , y + schema . y ) ) ; } } option . schema = schema = this . filterText ( option . text ) ; if ( schema !== "\n" ) { for ( j = 0 ; j < schema . length ; j += 1 ) { for ( k = 0 ; k < schema [ j ] . length ; k += 1 ) { if ( schema [ j ] [ k ] . command ) { if ( schema [ j ] [ k ] . x ) { x += schema [ j ] [ k ] . x ; } if ( schema [ j ] [ k ] . y ) { y += schema [ j ] [ k ] . y ; } } else { option . title = title = `Char${ this . getCharacterEquivalent ( schema [ j ] [ k ] ) }` ; character = this . game . objectMaker . make < Text > ( title ) ; menu . children . push ( character ) ; optionChild . actors . push ( character ) ; this . game . actors . add ( character , x , y ) ; x += character . width ; } } } } y += textPaddingY ; if ( ! menu . singleColumnList && y > menu . bottom - textHeight + 1 ) { y = top ; left += menu . textColumnWidth instanceof Array ? menu . textColumnWidth [ option . columnNumber ] : menu . textColumnWidth || textWidth ; column = [ ] ; menu . grid . push ( column ) ; } } while ( menu . grid [ menu . grid . length - 1 ] . length === 0 ) { menu . grid . pop ( ) ; } menu . gridColumns = menu . grid . length ; if ( settings . bottom ) { option = settings . bottom ; option . schema = schema = this . filterText ( option . text ) ; optionChild = { option , actors : [ ] , } ; optionChildren . push ( optionChild ) ; x = menu . left + ( menu . textXOffset || 0 ) + option . position . left ; y = menu . top + ( menu . textYOffset || 0 ) + option . position . top ; option . x = x ; option . y = y ; for ( j = 0 ; j < schema . length ; j += 1 ) { for ( k = 0 ; k < schema [ j ] . length ; k += 1 ) { if ( schema [ j ] [ k ] . command ) { if ( schema [ j ] [ k ] . x ) { x += schema [ j ] [ k ] . x ; } if ( schema [ j ] [ k ] . y ) { y += schema [ j ] [ k ] . y ; } } else if ( schema [ j ] [ k ] !== " " ) { option . title = title = "Char" + this . getCharacterEquivalent ( schema [ j ] [ k ] ) ; character = this . game . objectMaker . make < Text > ( title ) ; menu . children . push ( character ) ; optionChild . actors . push ( character ) ; this . game . actors . add ( character , x , y ) ; x += character . width ; } else { x += textWidth ; } } } menu . gridRows += 1 ; for ( j = 0 ; j < menu . grid . length ; j += 1 ) { menu . grid [ j ] . push ( option ) ; } } if ( menu . scrollingItemsComputed ) { menu . scrollingItems = this . computeMenuScrollingItems ( menu ) ; } if ( menu . scrollingItems ) { menu . scrollingVisualOffset = 0 ; for ( i = menu . scrollingItems ; i < menu . gridRows ; i += 1 ) { optionChild = optionChildren [ i ] ; for ( j = 0 ; j < optionChild . actors . length ; j += 1 ) { optionChild . actors [ j ] . hidden = true ; } } } if ( menu . saveIndex ) { if ( this . game . itemsHolder . hasKey ( name ) ) { menu . selectedIndex = this . game . itemsHolder . getItem ( name ) ; } else { menu . selectedIndex = selectedIndex ; this . game . itemsHolder . addItem ( name , { valueDefault : selectedIndex , } ) ; } } else { menu . selectedIndex = selectedIndex ; } menu . arrow = character = this . game . objectMaker . make < Actor > ( "CharArrowRight" ) ; menu . children . push ( character ) ; if ( menu . preserveArrow ) { this . game . graphics . classes . addClass ( menu . arrow , this . activeMenu === menu ? menu . preserveArrow . classActive : menu . preserveArrow . classInactive ) ; } else { character . hidden = this . activeMenu !== menu ; } option = menu . grid [ menu . selectedIndex [ 0 ] ] [ menu . selectedIndex [ 1 ] ] ; this . game . actors . add ( character ) ; this . game . physics . setRight ( character , option . x - ( menu . arrowXOffset || 0 ) ) ; this . game . physics . setTop ( character , option . y + ( menu . arrowYOffset || 0 ) ) ; } param name returns public getMenuSelectedOption ( name ) { const menu = this . getExistingMenu ( name ) as ListMenu ; if ( ! menu . grid || ! menu . selectedIndex ) { throw new Error ( "The " + name + " menu does not behave like a list menu." ) ; } return menu . grid [ menu . selectedIndex [ 0 ] ] [ menu . selectedIndex [ 1 ] ] ; } param name param dx param dy public shiftSelectedIndex ( name , dx , dy ) { const menu = this . getExistingMenu ( name ) as ListMenu ; const textProperties = this . game . objectMaker . getPrototypeOf < Text > ( "Text" ) ; const textPaddingY = menu . textPaddingY || textProperties . paddingY || 0 ; let x ; let y ; if ( menu . scrollingItems ) { x = menu . selectedIndex [ 0 ] + dx ; y = menu . selectedIndex [ 1 ] + dy ; x = Math . max ( Math . min ( menu . gridColumns - 1 , x ) , 0 ) ; y = Math . max ( Math . min ( menu . gridRows - 1 , y ) , 0 ) ; } else { x = ( menu . selectedIndex [ 0 ] + dx ) % menu . gridColumns ; y = ( menu . selectedIndex [ 1 ] + dy ) % menu . gridRows ; while ( x < 0 ) { x += menu . gridColumns ; } while ( y < 0 ) { y += menu . gridRows ; } } if ( x === menu . selectedIndex [ 0 ] && y === menu . selectedIndex [ 1 ] ) { return ; } menu . selectedIndex [ 0 ] = x ; menu . selectedIndex [ 1 ] = y ; const option = this . getMenuSelectedOption ( name ) ; if ( menu . scrollingItems ) { this . scrollListActors ( name , dy , textPaddingY ) ; } this . game . physics . setRight ( menu . arrow , option . x - ( menu . arrowXOffset || 0 ) ) ; this . game . physics . setTop ( menu . arrow , option . y + ( menu . arrowYOffset || 0 ) ) ; if ( menu . saveIndex ) { this . game . itemsHolder . setItem ( name , menu . selectedIndex ) ; } if ( menu . selectIndex ) { this . registerA ( ) ; } } param name param x param y public setSelectedIndex ( name , x , y ) { const menu = this . getExistingMenu ( name ) as ListMenu ; const selectedIndex = menu . selectedIndex ; this . shiftSelectedIndex ( name , x - selectedIndex [ 0 ] , y - selectedIndex [ 1 ] ) ; } param name public setActiveMenu ( name ) { if ( ! name ) { throw new Error ( "Tried setting an undefined active menu." ) ; } if ( this . activeMenu && this . activeMenu . onInactive ) { this . activeMenu . onInactive ( this . activeMenu . name ) ; } this . activeMenu = this . menus [ name ] ; if ( this . activeMenu && this . activeMenu . onActive ) { this . activeMenu . onActive ( name ) ; } } param direction public registerDirection ( direction ) { switch ( direction ) { case Direction . Top : this . registerUp ( ) ; break ; case Direction . Right : this . registerRight ( ) ; break ; case Direction . Bottom : this . registerDown ( ) ; break ; case Direction . Left : this . registerLeft ( ) ; break ; default : throw new Error ( "Unknown direction: " + direction ) ; } } /**
     * Reacts to a user event directing up.
     */ public registerUp ( ) { const menu = this . activeMenu as ListMenu ; if ( ! menu ) { return ; } if ( menu . selectedIndex ) { this . shiftSelectedIndex ( menu . name , 0 , - 1 ) ; } if ( menu . onUp ) { menu . onUp ( this . game ) ; } } /**
     * Reacts to a user event directing to the right.
     */ public registerRight ( ) { const menu = this . activeMenu as ListMenu ; if ( ! menu ) { return ; } if ( menu . selectedIndex ) { this . shiftSelectedIndex ( menu . name , 1 , 0 ) ; } if ( menu . onRight ) { menu . onRight ( this . game ) ; } } /**
     * Reacts to a user event directing down.
     */ public registerDown ( ) { const menu = this . activeMenu ; if ( ! menu ) { return ; } if ( ( menu as ListMenu ) . selectedIndex ) { this . shiftSelectedIndex ( menu . name , 0 , 1 ) ; } if ( menu . onDown ) { menu . onDown ( menu . name ) ; } } /**
     * Reacts to a user event directing to the left.
     */ public registerLeft ( ) { const menu = this . activeMenu as ListMenu ; if ( ! menu ) { return ; } if ( menu . selectedIndex ) { this . shiftSelectedIndex ( menu . name , - 1 , 0 ) ; } if ( menu . onLeft ) { menu . onLeft ( menu . name ) ; } } /**
     * Reacts to a user event from pressing a selection key.
     */ public registerA ( ) { const menu = this . activeMenu ; if ( ! menu || menu . ignoreA ) { return ; } if ( menu . callback ) { menu . callback ( menu . name ) ; } if ( this . sounds . onInteraction && ( ! ( menu as ListMenu ) . progress || ! ( menu as ListMenu ) . progress . working ) ) { this . sounds . onInteraction ( ) ; } } /**
     * Reacts to a user event from pressing a deselection key.
     */ public registerB ( ) { const menu = this . activeMenu ; if ( ! menu ) { return ; } if ( ( menu as ListMenu ) . progress && ! menu . ignoreProgressB ) { return this . registerA ( ) ; } if ( menu . ignoreB ) { return ; } if ( menu . onBPress ) { menu . onBPress ( menu . name ) ; return ; } if ( menu . keepOnBack ) { this . setActiveMenu ( menu . backMenu ) ; } else { this . deleteMenu ( menu . name ) ; } if ( this . sounds . onInteraction && ( ! ( menu as ListMenu ) . progress || ! ( menu as ListMenu ) . progress . working ) ) { this . sounds . onInteraction ( ) ; } } param name param words param onCompletion private addMenuText ( name , words , onCompletion ? ) { const menu = this . getExistingMenu ( name ) ; let x = this . game . physics . getMidX ( menu ) ; const y = menu . top + ( menu . textYOffset || 0 ) ; switch ( menu . textStartingX ) { case "right" : x += ( menu . textAreaWidth || 0 ) / 2 ; break ; case "center" : break ; default : x -= ( menu . textAreaWidth || 0 ) / 2 ; break ; } menu . callback = this . continueMenu . bind ( this ) ; menu . textX = x ; if ( words . length ) { this . addMenuWords ( name , words , 0 , x , y , onCompletion ) ; } else if ( onCompletion ) { onCompletion ( ) ; } } param name param words param i param x param y param onCompletion returns private addMenuWords ( name , words , i , x , y , onCompletion ? ) { const menu = this . getExistingMenu ( name ) as ListMenu ; const textProperties = this . game . objectMaker . getPrototypeOf ( "Text" ) ; const actors = [ ] ; const textPaddingRight = menu . textPaddingRight || 0 ; const textPaddingX = menu . textPaddingX || textProperties . paddingX || 0 ; const textPaddingY = menu . textPaddingY || textProperties . paddingY || 0 ; const textSpeed = typeof menu . textSpeed === undefined ? 1 : menu . textSpeed || 0 ; const textWidth = menu . textWidth || textProperties . width ; const progress = ( menu . progress = { i , onCompletion , words , x , y , } ) ; let character ; let j ; let command ; let word ; if ( ( words [ i ] as MenuWordCommand ) . command ) { command = words [ i ] as MenuWordCommand ; word = this . parseWordCommand ( command as MenuWordCommand , menu ) ; if ( ( command as MenuWordCommand ) . command === "position" ) { x += ( command as MenuWordPosition ) . x || 0 ; y += ( command as MenuWordPosition ) . y || 0 ; } } else { word = words [ i ] as string [ ] ; } for ( j = 0 ; j < word . length ; j += 1 ) { if ( /\S/ . test ( word [ j ] ) ) { character = this . addMenuCharacter ( name , word [ j ] , x , y , j * textSpeed ) ; x += character . width + textPaddingX ; continue ; } if ( word [ j ] === "\n" ) { x = menu . textX ! ; y += textPaddingY ; } else if ( word [ j ] !== " " || x !== menu . textX ) { x += textWidth ; } } const finalizeLine = ( ) => { progress . working = false ; const { callback } = menu ; if ( menu . finishLinesAutomatically && callback !== undefined ) { if ( ! menu . finishLinesAutomaticSpeed ) { callback ( menu . name ) ; } else { this . game . timeHandler . addEvent ( ( ) : void => { callback ( menu . name ) ; } , menu . finishLinesAutomaticSpeed ) ; } } } ; if ( i === words . length - 1 ) { progress . complete = true ; if ( menu . finishAutomatically && onCompletion ) { if ( ! menu . finishAutomaticSpeed ) { onCompletion ( ) ; } else { this . game . timeHandler . addEvent ( onCompletion , ( word . length + menu . finishAutomaticSpeed ) * textSpeed ) ; } } return actors ; } if ( x + this . computeFutureWordLength ( words [ i + 1 ] , textWidth , textPaddingX ) >= menu . right - ( menu . textXOffset || 0 ) - textPaddingRight ) { x = menu . textX ! ; y += textPaddingY ; } progress . working = true ; progress . complete = false ; progress . words = words ; progress . i = i + 1 ; progress . x = x ; progress . y = y - textPaddingY ; if ( y >= menu . bottom - ( menu . textYOffset || 0 ) - 1 ) { if ( textSpeed === 0 ) { finalizeLine ( ) ; } else { this . game . timeHandler . addEvent ( finalizeLine , ( j + 1 ) * textSpeed ) ; } return actors ; } if ( textSpeed ) { this . game . timeHandler . addEvent ( ( ) : void => { this . addMenuWords ( name , words , i + 1 , x , y , onCompletion ) ; } , ( j + 1 ) * textSpeed ) ; } else { this . addMenuWords ( name , words , i + 1 , x , y , onCompletion ) ; } return actors ; } param actor param size param position param skipAdd private placeMenuActor ( menu , actor , size = { } , position = { } , skipAdd ? ) { const offset = position . offset || { } ; if ( size . width ) { this . game . physics . setWidth ( actor , size . width ) ; } else if ( position . horizontal === "stretch" ) { this . game . physics . setLeft ( actor , 0 ) ; this . game . physics . setWidth ( actor , menu . width - ( offset . left || 0 ) - ( offset . right || 0 ) ) ; } if ( size . height ) { this . game . physics . setHeight ( actor , size . height ) ; } else if ( position . vertical === "stretch" ) { this . game . physics . setTop ( actor , 0 ) ; this . game . physics . setHeight ( actor , menu . height - ( offset . top || 0 ) - ( offset . bottom || 0 ) ) ; } switch ( position . horizontal ) { case "center" : this . game . physics . setMidXObj ( actor , menu ) ; break ; case "right" : this . game . physics . setRight ( actor , menu . right ) ; break ; default : this . game . physics . setLeft ( actor , menu . left ) ; break ; } switch ( position . vertical ) { case "center" : this . game . physics . setMidYObj ( actor , menu ) ; break ; case "bottom" : this . game . physics . setBottom ( actor , menu . bottom ) ; break ; default : this . game . physics . setTop ( actor , menu . top ) ; break ; } if ( offset . top ) { this . game . physics . shiftVert ( actor , offset . top ) ; } if ( offset . left ) { this . game . physics . shiftHoriz ( actor , offset . left ) ; } if ( ! skipAdd ) { this . game . actors . add ( actor , actor . left , actor . top ) ; } } param name param character param x param y param delay returns private addMenuCharacter ( name , character , x , y , delay ? ) { const menu = this . getExistingMenu ( name ) ; const textProperties = this . game . objectMaker . getPrototypeOf ( "Text" ) ; const textPaddingY = menu . textPaddingY || textProperties . paddingY ; const title = "Char" + this . getCharacterEquivalent ( character ) ; const actor = this . game . objectMaker . make < Text & MenuBase > ( title , { textPaddingY , } ) ; menu . children . push ( actor ) ; if ( delay ) { this . game . timeHandler . addEvent ( ( ) : void => { this . game . actors . add ( actor , x , y ) ; } , delay ) ; } else { this . game . actors . add ( actor , x , y ) ; } return actor ; } param character param menu param divisor returns private scrollCharacterUp ( character , menu , speed ) { this . game . physics . shiftVert ( character , - speed ) ; if ( character . top < menu . top + ( ( menu . textYOffset || 0 ) - speed ) ) { this . game . death . kill ( character ) ; return true ; } return false ; } param name private clearMenuIndices ( name ) { const menu = this . menus [ name ] as ListMenu ; if ( ! menu . clearedIndicesOnDeletion ) { return ; } for ( const menuName of menu . clearedIndicesOnDeletion ) { this . game . itemsHolder . setItem ( menuName , [ 0 , 0 ] ) ; } } param name private deleteMenuChildren ( name ) { const menu = this . menus [ name ] ; if ( menu && menu . children ) { menu . children . forEach ( ( child ) => this . deleteMenuChild ( child ) ) ; } } param child private deleteMenuChild ( child ) { if ( this . activeMenu === child ) { if ( child . backMenu ) { this . setActiveMenu ( child . backMenu ) ; } else { this . activeMenu = undefined ; } } if ( child . killOnB ) { child . killOnB . forEach ( this . deleteMenu . bind ( this ) ) ; } if ( child . name ) { delete this . menus [ child . name ] ; } this . game . death . kill ( child ) ; this . deleteMenuChildren ( child . name ) ; if ( child . onMenuDelete ) { child . onMenuDelete . call ( this . game , this . game ) ; } if ( child . children ) { child . children . forEach ( this . deleteMenuChild . bind ( this ) ) ; } } param name private activateMenuList ( name ) { const menu = this . menus [ name ] as ListMenu ; if ( ! menu || ! menu . arrow ) { return ; } if ( menu . preserveArrow ) { this . game . graphics . classes . switchClass ( menu . arrow , menu . preserveArrow . classInactive , menu . preserveArrow . classActive ) ; } else { menu . arrow . hidden = false ; } } param name private deactivateMenuList ( name ) { const menu = this . menus [ name ] as ListMenu ; if ( ! menu || ! menu . arrow ) { return ; } if ( menu . preserveArrow ) { this . game . graphics . classes . switchClass ( menu . arrow , menu . preserveArrow . classActive , menu . preserveArrow . classInactive ) ; } else { menu . arrow . hidden = true ; } } param menuName private triggerMenuListOption ( menuName ) { const selected = this . getMenuSelectedOption ( menuName ) ; if ( selected . callback ) { selected . callback . call ( this , menuName ) ; } } param menu returns private computeMenuScrollingItems ( menu ) { const bottom = menu . bottom - ( menu . textPaddingY || 0 ) - ( menu . textYOffset || 0 ) ; for ( let i = 0 ; i < menu . gridRows ; i += 1 ) { if ( menu . grid [ 0 ] [ i ] . y >= bottom ) { return i ; } } return Infinity ; } param name param dy param textPaddingY private scrollListActors ( name , dy , textPaddingY ) { const menu = this . getExistingMenu ( name ) as ListMenu ; const scrollingOld = menu . selectedIndex [ 1 ] - dy ; const offset = - dy * textPaddingY ; let option ; let optionChild ; let i ; let j ; if ( dy > 0 ) { if ( scrollingOld - ( menu . scrollingVisualOffset || 0 ) < ( menu . scrollingItems || 1 ) - 1 ) { return ; } } else if ( scrollingOld - ( menu . scrollingVisualOffset || 0 ) > 0 ) { return ; } menu . scrollingVisualOffset = ( menu . scrollingVisualOffset || 0 ) + dy ; for ( i = 0 ; i < menu . optionChildren . length ; i += 1 ) { option = menu . options [ i ] ; optionChild = menu . optionChildren [ i ] ; option . y += offset ; for ( j = 0 ; j < optionChild . actors . length ; j += 1 ) { this . game . physics . shiftVert ( optionChild . actors [ j ] , offset ) ; optionChild . actors [ j ] . hidden = ! ! ( i < menu . scrollingVisualOffset || i >= ( menu . scrollingItems || 1 ) + menu . scrollingVisualOffset ) ; } } } param character returns private getCharacterEquivalent ( character ) { if ( { } . hasOwnProperty . call ( this . aliases , character ) ) { return this . aliases [ character ] ; } return character ; } param dialogRaw returns private parseRawDialog ( dialogRaw ) { if ( dialogRaw . constructor === String ) { return [ this . parseRawDialogString ( dialogRaw ) ] ; } const output = [ ] ; for ( const component of dialogRaw as string [ ] | string [ ] [ ] ) { if ( component . constructor === String ) { output . push ( this . parseRawDialogString ( component ) ) ; } else { output . push ( this . parseRawDialogStrings ( component as string [ ] ) ) ; } } return output ; } param dialogRaw returns private parseRawDialogString ( dialogRaw ) { const characters = this . filterWord ( dialogRaw ) ; const words = [ ] ; let word = [ ] ; let currentlyWhitespace = false ; for ( const character of characters ) { if ( currentlyWhitespace ) { if ( /\s/ . test ( character ) ) { word . push ( character ) ; continue ; } } else { if ( /\S/ . test ( character ) ) { word . push ( character ) ; continue ; } } currentlyWhitespace = /\s/ . test ( character ) ; words . push ( word ) ; word = [ character ] ; } if ( word . length > 0 ) { words . push ( word ) ; } return words ; } param words returns private parseRawDialogStrings ( words ) { const output = [ ] ; for ( const word of words ) { output . push ( ... this . parseRawDialogString ( word ) ) ; } return output ; } param wordRaw returns private filterWord ( wordRaw ) { if ( wordRaw . constructor === Array ) { return wordRaw ; } const word = wordRaw as string ; const output = [ ] ; let inside ; const start = word . indexOf ( this . replacerKey , 0 ) ; const end = word . indexOf ( this . replacerKey , start + 1 ) ; if ( start !== - 1 && end !== - 1 ) { inside = this . getReplacement ( word . substring ( start + this . replacerKey . length , end ) ) ; if ( inside . constructor === Number ) { inside = ( inside as any ) . toString ( ) . split ( "" ) ; } else if ( inside . constructor === String ) { inside = ( inside as any ) . split ( "" ) ; } output . push ( ... word . substring ( 0 , start ) . split ( "" ) ) ; output . push ( ... ( inside as string [ ] ) ) ; output . push ( ... this . filterWord ( word . substring ( end + this . replacerKey . length ) ) ) ; return output ; } return word . split ( "" ) ; } param words returns private filterMenuWords ( words ) { const output = [ ] ; for ( const word of words ) { if ( word . constructor === String ) { output . push ( this . filterWord ( word ) ) ; } else { output . push ( word as MenuWordCommand ) ; } } return output ; } param textRaw returns private filterText ( textRaw ) { if ( textRaw . constructor === Array ) { if ( textRaw . length === 0 ) { return [ ] ; } if ( textRaw [ 0 ] . constructor === String ) { return [ textRaw as string [ ] ] ; } return textRaw as string [ ] [ ] ; } const characters = [ ] ; const total = textRaw as string ; let component = "" ; let i ; for ( i = 0 ; i < total . length ; i += 1 ) { if ( /\s/ . test ( total [ i ] ) ) { if ( component . length > 0 ) { characters . push ( ... this . filterWord ( component ) ) ; component = "" ; } characters . push ( total [ i ] ) ; continue ; } component += total [ i ] ; } if ( component . length > 0 ) { characters . push ( ... this . filterWord ( component ) ) ; } return [ characters ] ; } param wordCommand param menu returns private parseWordCommand ( wordCommand , menu ? ) { if ( ! menu ) { menu = { } ; } switch ( wordCommand . command ) { case "attribute" : menu [ wordCommand . attribute + "Old" ] = menu [ wordCommand . attribute ] ; menu [ wordCommand . attribute ] = wordCommand . value ; break ; case "attributeReset" : menu [ wordCommand . attribute ] = menu [ wordCommand . attribute + "Old" ] ; break ; case "padLeft" : return this . parseWordCommandPadLeft ( wordCommand as MenuWordPadLeftCommand ) ; case "position" : break ; default : throw new Error ( "Unknown word command: " + ( wordCommand as any ) . command ) ; } return wordCommand . word ! . split ( "" ) ; } param wordCommand returns private parseWordCommandPadLeft ( wordCommand ) { const filtered = this . filterWord ( wordCommand . word ! ) ; let length ; switch ( wordCommand . length ! . constructor ) { case String : length = this . filterText ( wordCommand . length as string ) [ 0 ] . length ; break ; case Number : length = wordCommand . length as number ; break ; default : throw new Error ( "Unknown padLeft command: " + wordCommand ) ; } if ( wordCommand . alignRight ) { length = Math . max ( 0 , length - filtered . length ) ; } filtered . unshift . apply ( filtered , this . stringOf ( "\t" , length ) . split ( "" ) ) ; return filtered ; } param key returns private getReplacement ( key ) { const replacement = this . replacements [ key ] ; if ( typeof replacement === "undefined" ) { return [ "" ] ; } if ( typeof replacement === "function" ) { return replacement ( this . game ) ; } return replacement ; } param text param times returns private stringOf ( text , times = 1 ) { return times === 0 ? "" : new Array ( times + 1 ) . join ( text ) ; } param wordRaw param textWidth param textPaddingX returns remarks private computeFutureWordLength ( wordRaw , textWidth , textPaddingX ) { let total = 0 ; const word = wordRaw . constructor === Array ? wordRaw : this . parseWordCommand ( wordRaw as MenuWordCommand ) ; for ( const character of word ) { if ( /\s/ . test ( character ) ) { total += textWidth + textPaddingX ; } else { total += this . computeFutureLetterLength ( character ) + textPaddingX ; } } return total ; } param letter returns private computeFutureLetterLength ( letter ) { const title = "Char" + this . getCharacterEquivalent ( letter ) ; const properties = this . game . objectMaker . getPrototypeOf < MenuBase > ( title ) ; return properties . width ! ; } }