import { Direction , ArgumentPossibility , Choice , Command , DirectionsMap , OnPlacement , PercentageOption , Position , Possibility , PossibilityChild , PossibilityContainer , PossibilityContents , PossibilitySpacingOption , RandomNumberGenerator , WorldSeedrSettings , Spacing , } from "./types" ; import { SpacingCalculator } from "./SpacingCalculator" ; /**
 * A constant listing of direction opposites, like top-bottom.
 */ const directionOpposites = { bottom : "top" , left : "right" , right : "left" , top : "bottom" , } ; /**
 * A constant listing of what direction the sides of areas correspond to.
 */ const directionSizing = { bottom : "height" , left : "width" , right : "width" , top : "height" , } ; /**
 * A constant Array of direction names.
 */ const directionNames = [ "top" , "right" , "bottom" , "left" ] ; /**
 * A constant Array of the dimension descriptors.
 */ const sizingNames = [ "width" , "height" ] ; /**
 * Automates random, recursive generation of possibilities from JSON schemas.
 */ export class WorldSeedr { /**
     * A listing of possibility schemas, keyed by title.
     */ private possibilities : PossibilityContainer ; /**
     * Function used to generate a random number
     */ private readonly random : RandomNumberGenerator ; /**
     * Function called in generateFull to place a command.
     */ private onPlacement : OnPlacement ; /**
     * Scratch Array of PreActors to be added to during generation.
     */ private generatedCommands : Command [ ] ; /**
     * Utility to generate spacing distances based on possibility schemas.
     */ private readonly spacingCalculator : SpacingCalculator ; param settings public constructor ( settings ) { this . possibilities = settings . possibilities || { } ; this . random = settings . random || ( ( ) : number => Math . random ( ) ) ; this . onPlacement = settings . onPlacement ; this . spacingCalculator = new SpacingCalculator ( ( min , max ) : number => this . randomBetween ( min , max ) , ( choices ) : PossibilitySpacingOption => this . chooseAmong ( choices ) ! ) ; this . clearGeneratedCommands ( ) ; } returns public getPossibilities ( ) { return this . possibilities ; } param possibilitiesNew public setPossibilities ( possibilities ) { this . possibilities = possibilities ; } returns public getOnPlacement ( ) { return this . onPlacement ; } param onPlacementNew public setOnPlacement ( onPlacement ) { this . onPlacement = onPlacement ; } /**
     * Resets the generatedCommands Array so runGeneratedCommands can start.
     */ public clearGeneratedCommands ( ) { this . generatedCommands = [ ] ; } /**
     * Runs the onPlacement callback on the generatedCommands Array.
     */ public runGeneratedCommands ( ) { this . onPlacement ( this . generatedCommands ) ; } param name param position returns public generate ( name , command ) { const schema = this . possibilities [ name ] ; if ( ! schema ) { throw new Error ( "No possibility exists under '" + name + "'" ) ; } if ( ! schema . contents ) { throw new Error ( "Possibility '" + name + "' has no possibile outcomes." ) ; } return this . generateChildren ( schema , this . objectCopy ( command ) ) ; } param schema returns public generateFull ( schema ) { const generated = this . generate ( schema . title , schema ) ; if ( ! generated || ! generated . children ) { return ; } for ( const child of generated . children ) { switch ( child . type ) { case "Known" : this . generatedCommands . push ( child ) ; break ; case "Random" : this . generateFull ( child ) ; break ; default : throw new Error ( "Unknown child type: " + child . type ) ; } } } param schema param position param direction returns private generateChildren ( schema , position , direction ? ) { const contents = schema . contents ; const spacing = contents . spacing || 0 ; const objectMerged = this . objectMerge ( schema , position ) ; let children ; direction = contents . direction || direction ; switch ( contents . mode ) { case "Random" : children = this . generateRandom ( contents , objectMerged , direction , spacing ) ; break ; case "Certain" : children = this . generateCertain ( contents , objectMerged , direction , spacing ) ; break ; case "Repeat" : children = this . generateRepeat ( contents , objectMerged , direction , spacing ) ; break ; case "Multiple" : children = this . generateMultiple ( contents , objectMerged , direction , spacing ) ; break ; default : throw new Error ( "Unknown contents mode: " + contents . mode ) ; } return this . wrapChoicePositionExtremes ( children ) ; } param contents param position param direction param spacing returns private generateCertain ( contents , position , direction , spacing ) { return contents . children . map ( ( choice ) : Choice => { if ( choice . type === "Final" ) { return this . parseChoiceFinal ( choice , position ) ; } const output = this . parseChoice ( choice , position , direction ) ; if ( output ) { if ( output . type !== "Known" ) { output . contents = this . generate ( output . title , position ) ; } this . shrinkPositionByChild ( position , output , direction , spacing ) ; } return output ; } ) . filter ( ( child ) : boolean => child !== undefined ) ; } param contents param position param direction param spacing returns private generateRepeat ( contents , position , direction , spacing ) { const choices = contents . children ; const children = [ ] ; let i = 0 ; while ( this . positionIsNotEmpty ( position , direction ) ) { const choice = choices [ i ] ; let child ; if ( choice . type === "Final" ) { child = this . parseChoiceFinal ( choice , position ) ; } else { child = this . parseChoice ( choice , position , direction ) ; if ( child && child . type !== "Known" ) { child . contents = this . generate ( child . title , position ) ; } } if ( child && this . choiceFitsPosition ( child , position ) ) { this . shrinkPositionByChild ( position , child , direction , spacing ) ; children . push ( child ) ; } else { break ; } i += 1 ; if ( i >= choices . length ) { i = 0 ; } } return children ; } param contents param position param direction param spacing returns private generateRandom ( contents , position , direction , spacing ) { const children = [ ] ; while ( this . positionIsNotEmpty ( position , direction ) ) { const child = this . generateChild ( contents , position , direction ) ; if ( ! child ) { break ; } this . shrinkPositionByChild ( position , child , direction , spacing ) ; children . push ( child ) ; if ( contents . limit && children . length > contents . limit ) { return undefined ; } } return children ; } param contents param position param direction param spacing returns private generateMultiple ( contents , position , direction , spacing ) { return contents . children . map ( ( choice ) : Choice => { const output = this . parseChoice ( choice , this . objectCopy ( position ) , direction ) ; if ( direction ) { this . movePositionBySpacing ( position , direction , spacing ) ; } return output ; } ) ; } param contents param position param direction returns private generateChild ( contents , position , direction ) { const choice = this . chooseAmongPosition ( contents . children , position ) ; return choice ? this . parseChoice ( choice , position , direction ) : undefined ; } param choice param position param direction returns private parseChoice ( choice , position , direction ) { const title = choice . title ; const schema = this . possibilities [ title ] ; const output = { arguments : choice . arguments instanceof Array ? ( this . chooseAmong ( choice . arguments ) as ArgumentPossibility ) . values : choice . arguments , bottom : 0 , height : 0 , left : 0 , right : 0 , title , top : 0 , type : choice . type , width : 0 , } ; this . ensureSizingOnChoice ( output , choice , schema ) ; this . ensureDirectionBoundsOnChoice ( output , position ) ; ( output as any ) [ direction ] = ( output as any ) [ ( directionOpposites as any ) [ direction ] ] + ( output as any ) [ ( directionSizing as any ) [ direction ] ] ; switch ( schema . contents . snap ) { case "top" : output . bottom = output . top - output . height ; break ; case "right" : output . left = output . right - output . width ; break ; case "bottom" : output . top = output . bottom + output . height ; break ; case "left" : output . right = output . left + output . width ; break ; } if ( choice . stretch ) { if ( ! output . arguments ) { output . arguments = { } ; } if ( choice . stretch . width ) { output . left = position . left ; output . right = position . right ; output . width = output . right - output . left ; output . arguments . width = output . width ; } if ( choice . stretch . height ) { output . top = position . top ; output . bottom = position . bottom ; output . height = output . top - output . bottom ; output . arguments . height = output . height ; } } return output ; } param choice param position param direction returns todo private parseChoiceFinal ( choice , position ) { if ( ! choice . source ) { throw new Error ( "Unknown final source choice." ) ; } const schema = this . possibilities [ choice . source ] ; return { arguments : choice . arguments , bottom : position . bottom , height : schema . height , left : position . left , right : position . right , title : choice . title , top : position . top , type : "Known" , width : schema . width , } ; } param choice returns private chooseAmong < T extends PercentageOption > ( choices ) { if ( ! choices . length ) { return undefined ; } if ( choices . length === 1 ) { return choices [ 0 ] ; } const goal = this . randomPercentage ( ) ; let sum = 0 ; for ( const possibility of choices ) { sum += possibility . percent ; if ( sum >= goal ) { return possibility ; } } return undefined ; } param choice param position returns remarks private chooseAmongPosition ( choices , position ) { const width = position . right - position . left ; const height = position . top - position . bottom ; return this . chooseAmong ( choices . filter ( ( choice ) : boolean => this . choiceFitsSize ( this . possibilities [ choice . title ] , width , height ) ) ) ; } param choice param width param height returns private choiceFitsSize ( choice , width , height ) { return choice . width <= width && choice . height <= height ; } param choice param position returns remarks private choiceFitsPosition ( choice , position ) { return this . choiceFitsSize ( choice , position . right - position . left , position . top - position . bottom ) ; } param position param direction private positionIsNotEmpty ( position , direction ) { if ( direction === "right" || direction === "left" ) { return position . left < position . right ; } return position . top > position . bottom ; } param position param child param direction param spacing private shrinkPositionByChild ( position , child , direction , spacing = 0 ) { switch ( direction ) { case "top" : position . bottom = child . top + this . spacingCalculator . calculateFromSpacing ( spacing ) ; break ; case "right" : position . left = child . right + this . spacingCalculator . calculateFromSpacing ( spacing ) ; break ; case "bottom" : position . top = child . bottom - this . spacingCalculator . calculateFromSpacing ( spacing ) ; break ; case "left" : position . right = child . left - this . spacingCalculator . calculateFromSpacing ( spacing ) ; break ; } } param position param direction param spacing private movePositionBySpacing ( position , direction , spacing = 0 ) { const space = this . spacingCalculator . calculateFromSpacing ( spacing ) ; switch ( direction ) { case "top" : position . top += space ; position . bottom += space ; break ; case "right" : position . left += space ; position . right += space ; break ; case "bottom" : position . top -= space ; position . bottom -= space ; break ; case "left" : position . left -= space ; position . right -= space ; break ; default : throw new Error ( "Unknown direction: " + direction ) ; } } param children returns private wrapChoicePositionExtremes ( children ? ) { if ( ! children || ! children . length ) { return undefined ; } const position = { bottom : children [ 0 ] . bottom , children , height : 0 , left : children [ 0 ] . left , right : children [ 0 ] . right , title : "" , top : children [ 0 ] . top , width : 0 , } ; if ( children . length === 1 ) { return position ; } for ( let i = 1 ; i < children . length ; i += 1 ) { const child = children [ i ] ; if ( ! Object . keys ( child ) . length ) { return position ; } position . top = Math . max ( position . top , child . top ) ; position . right = Math . max ( position . right , child . right ) ; position . bottom = Math . min ( position . bottom , child . bottom ) ; position . left = Math . min ( position . left , child . left ) ; } position . width = position . right - position . left ; position . height = position . top - position . bottom ; return position ; } param output param choice param schema private ensureSizingOnChoice ( output , choice , schema ) { for ( const name of sizingNames ) { ( output as any ) [ name ] = choice . sizing && typeof ( choice . sizing as any ) [ name ] !== "undefined" ? ( choice . sizing as any ) [ name ] : ( schema as any ) [ name ] ; } } param output param position private ensureDirectionBoundsOnChoice ( output , position ) { for ( const name of directionNames ) { ( output as any ) [ name ] = ( position as any ) [ name ] ; } } returns private randomPercentage ( ) { return Math . floor ( this . random ( ) * 100 ) + 1 ; } returns private randomBetween ( min , max ) { return Math . floor ( this . random ( ) * ( max - min + 1 ) ) + min ; } param original returns private objectCopy ( original ) { const output = { } ; for ( const i in original ) { if ( { } . hasOwnProperty . call ( original , i ) ) { output [ i ] = original [ i ] ; } } return output ; } param primary param secondary returns private objectMerge ( primary , secondary ) { const output = this . objectCopy ( primary ) ; for ( const i in secondary ) { if ( { } . hasOwnProperty . call ( secondary , i ) && ! { } . hasOwnProperty . call ( output , i ) ) { output [ i ] = secondary [ i ] ; } } return output ; } }