Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Help with script for dynamic chat menus based on Journal folder structure

Howdy folks, Currently I build my chat menus by hand/spreadsheet to aid in quickly navigating from scene to scene. What I would really like, however, is a chat menu that is generated from my Journal structure. So let's say I currently have: Scene Manager/ Scene Manager/Forest/ Scene Manager/Forest/Faerie Woods/ Scene Manager/Forest/Faerie Woods/Faerie Ring Glade Scene Manager/Ocean/ Scene Manager/Ocean/Windhome Scene Manager/Home Base I can parse Campaign._journalFolder and expand the contents to get an array of objects. [{"n":"PCs","i":[{"name":"A Player Character","bio":"","gmnotes":"","_defaulttoken":"","archived":false,"inplayerjournals":"","controlledby":"","_id":"-NMZoQR1Bz1YLiuXhp9Q","_type":"character","avatar":""}],"id":"-NMZoPWAiPBNcx10aJXB"},{"n":"Scene Manager","i":[{"n":"Forest","i":[{"n":"Faerie Woods","i":[{"name":"Faerie Ring Glade","bio":"","gmnotes":"","_defaulttoken":"","archived":false,"inplayerjournals":"","controlledby":"","_id":"-NMVuobOxHD8qaGdXy0D","_type":"character","avatar":""}],"id":"-NMVunKEDbmeUNtTI1eN"}],"id":"-NMVukiUC7fZ-RYz3oc9"},{"n":"Ocean","i":[{"name":"Windhome","bio":"","gmnotes":"","_defaulttoken":"","archived":false,"inplayerjournals":"","controlledby":"","_id":"-NMVv6EixllSd2uWjYJB","_type":"character","avatar":""}],"id":"-NMVv34MwHbpuU0wGSZe"},{"name":"Home Base","bio":"","gmnotes":"","_defaulttoken":"","archived":false,"inplayerjournals":"","controlledby":"","_id":"-NMVuybA_n0d53UfOU3L","_type":"character","avatar":""}],"id":"-NMVuhHo71u6Dsg0vTSU"}] I want to recursively loop through that&nbsp;in order to generate a hierarchical series of chat menus that look something like this (mock-ups): The Ask What I don't want is someone to write this for me. What I do &nbsp;want is help thinking through the approach, because I think this will be a fun JavaScript tutorial project for myself. I would love some assistance with the pseudocode that'll get me to where I want from here. // Thanks to Lithl for sharing this function on the Roll20 forums // <a href="https://app.roll20.net/forum/post/4638074/read-journal-folder-contents" rel="nofollow">https://app.roll20.net/forum/post/4638074/read-journal-folder-contents</a> function getFolderObjects ( objs ) { // _.map takes a collection and uses some function to transform each element into something else return _ . map ( objs , function ( o ) { // if the element isn't a string, it should be an object; one of its properties should be i, which should be an array if ( _ . isArray ( o . i )) { // the i property is the contents of the folder, so recursively call this function to get the objects in the folder o . i = getFolderObjects ( o . i ); // return the folder return o ; } // check if the current element of the collection is a string (an id for a character or handout) if ( _ . isString ( o )) { // return the handout, or the character if it's not a handout's id return getObj ( 'handout' , o ) || getObj ( 'character' , o ); } }); } on ( "ready" , function () { // Gets all the objects in the campaign and parses the JSON const journalEntries = getFolderObjects ( JSON . parse ( Campaign (). get ( "_journalfolder" ))); /* In the dev environment, this returns: [{"n":"PCs","i":[{"name":"A Player Character","bio":"","gmnotes":"","_defaulttoken":"","archived":false,"inplayerjournals":"","controlledby":"","_id":"-NMZoQR1Bz1YLiuXhp9Q","_type":"character","avatar":""}],"id":"-NMZoPWAiPBNcx10aJXB"},{"n":"Scene Manager","i":[{"n":"Forest","i":[{"n":"Faerie Woods","i":[{"name":"Faerie Ring Glade","bio":"","gmnotes":"","_defaulttoken":"","archived":false,"inplayerjournals":"","controlledby":"","_id":"-NMVuobOxHD8qaGdXy0D","_type":"character","avatar":""}],"id":"-NMVunKEDbmeUNtTI1eN"}],"id":"-NMVukiUC7fZ-RYz3oc9"},{"n":"Ocean","i":[{"name":"Windhome","bio":"","gmnotes":"","_defaulttoken":"","archived":false,"inplayerjournals":"","controlledby":"","_id":"-NMVv6EixllSd2uWjYJB","_type":"character","avatar":""}],"id":"-NMVv34MwHbpuU0wGSZe"},{"name":"Home Base","bio":"","gmnotes":"","_defaulttoken":"","archived":false,"inplayerjournals":"","controlledby":"","_id":"-NMVuybA_n0d53UfOU3L","_type":"character","avatar":""}],"id":"-NMVuhHo71u6Dsg0vTSU"}] */ // Checks that the Scene Manager folder exists already somewhere in the root directory let readySceneManager = false ; for ( let x in journalEntries ) { &nbsp;&nbsp;&nbsp;&nbsp; folderState = Object . values ( journalEntries [ x ]). includes ( "Scene Manager" ); &nbsp;&nbsp;&nbsp;&nbsp; if ( folderState === true ) { &nbsp;&nbsp;&nbsp;&nbsp; // Flag the Scene Manager Macro as ready to be built &nbsp;&nbsp;&nbsp;&nbsp; readySceneManager = x ; &nbsp;&nbsp;&nbsp;&nbsp; // debugging log &nbsp;&nbsp;&nbsp;&nbsp; log ( "Scene Manager folder exists" ); &nbsp;&nbsp;&nbsp;&nbsp;} } if ( readySceneManager === false ) { // Roll20 does not allow the creation of folders via API // Next best thing: Send a message to chat that the folder is missing sendChat ( "Scene Manager" , "'Scene Manager' folder is missing at the root level. Please create this folder and type '!scmgr --rebuild' in chat to proceed." ) // debugging log log ( "Scene Manager folder does not exist" ); } &nbsp;&nbsp;&nbsp;&nbsp; // debugging log log ( journalEntries ); });
What I think &nbsp;I need to do next is r ecursively look through the Scene Manager directory. For each object that is an array (meaning: it's a folder), append that name in a string called "scenepath" (pre-seeded with "Scene Manager/" then recursively loop. For each object that is a string (meaning: it's a character sheet/scene), take the scenepath string and append the scene name, and store it in an array of scenes. When a recursion ends, remove the last folder from the string "scenepath". - Let's do a mental walk through: The program finds the Forest array. scenepath = "Scene Manager/Forest/" The program now recursively crawls that folder. It finds the Faerie Woods array next. scenepath = "Scene Manager/Forest/Faerie Woods/" The program now recursively crawls that folder. It finds the Faerie Ring Glade string next. scenes = {"Scene Manager/Forest/Faerie Woods/Faerie Ring Glade"} Finding nothing else in the Faerie Woods array, the program goes back up a level. scenepath = "Scene Manager/Forest/" Finding nothing else in the Forest array, the program goes back up a level. scenepath = "" Next, the program finds the Ocean array. scenepath = "Scene Manager/Ocean/" And in that folder there is a scene string for the Windhome. scenes = {"Scene Manager/Forest/Faerie Woods/Faerie Ring Glade", "Scene Manager/Ocean/Windhome"} Nothing else in that array, so the program goes back up a level. scenepath = "" Lastly, there is a scene in the root folder. scenes = {"Scene Manager/Forest/Faerie Woods/Faerie Ring Glade", "Scene Manager/Ocean/Windhome", "Scene Manager/Home Base"} From there I think I can break that down into pairs and generate macros accordingly.
1675115598

Edited 1675115803
timmaugh
Pro
API Scripter
I think I would approach it as a tokenizing operation followed by a reconstruction operation. Your function would build a data structure by returning a token like this: { &nbsp; isFolder: true, &nbsp; displayText: 'Scene Manager', &nbsp; items: [] } In building the items array, the function would call itself. Every time it found a folder (array) item, it would call itself to create a new token (with the isFolder property set to true), and then it would build the items array by calling itself. After you'd tokenized your folder structure, you'd have an object like this: { &nbsp; isFolder: true, &nbsp; displayText: 'Scene Manager', &nbsp; items: [ &nbsp; &nbsp; { &nbsp; &nbsp; &nbsp; isFolder: true, &nbsp; &nbsp; &nbsp; displayText: 'Forest', &nbsp; &nbsp; &nbsp; items: [ &nbsp; &nbsp; &nbsp; &nbsp; { &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; isFolder: true, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; displayText: 'Faerie Woods', &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; items: [ ... ] &nbsp; &nbsp; &nbsp; &nbsp; }, &nbsp; &nbsp; &nbsp; &nbsp; { ... } &nbsp; &nbsp; &nbsp; ] &nbsp; &nbsp; }, &nbsp; &nbsp; { &nbsp; &nbsp; &nbsp; isFolder: true, &nbsp; &nbsp; &nbsp; displayText: 'Ocean', &nbsp; &nbsp; &nbsp; items: [ &nbsp; &nbsp; &nbsp; &nbsp; { ... }, &nbsp; &nbsp; &nbsp; &nbsp; { ... }, &nbsp; &nbsp; &nbsp; &nbsp; { ... } &nbsp; &nbsp; &nbsp; ] &nbsp; &nbsp; } &nbsp;] } Then your reconstruction would do another trip of the recursion to build your macros. EDIT: Obviously the scenes themselves, being not-folders, would have the isFolder property set to false... and that's how you would know to trip the button creation. Folders become headers, not-folders become buttons.