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

A way to output text line by line in chat?

I often prepare some script for narration ahead of time before a campaign and like having them display in chat so people have an easier time understanding it even if my reading isn't super clear sometimes. But it's really hard for me to copy and paste each line (and makes a lot of noise) as I try to read. Would there be a way for me to write a script or a macro to put in a chunk of text, and then have it be put into chat line by line?
1674320250

Edited 1690909697
GiGs
Pro
Sheet Author
API Scripter
A macro will respect line breaks. It prints out everything exactly as it finds it. So if you paste the whole thing into a macro, and run the macro, it'll print into chat. But also, if you paste your whole text into chat it should be fine - you dont have to paste it line by line.
1674327744
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
I think that the intent is to have something like Closed Captioning? So that each chunk of text is revealed as you read it and not to far ahead or behind? Interesting. You could prepare each chunk ahead of time as a separate macro and use a whispered  Chat Menu  to control when each chunk is clicked. I think it would require a script to do what you are doing. It would load the contents of a handout and when you give a command, display line 1 and increment a counter. Then use that counter to display the next, increment again, and so on. A clever script writer could probably even engineer it so that the script itself can accept the initial text as an argument, rather than going with a handout.
1674329550
The Aaron
Roll20 Production Team
API Scripter
Interesting. Let me think about that, I bet I can come up with a good solution...
1674330640
GiGs
Pro
Sheet Author
API Scripter
keithcurtis said: I think that the intent is to have something like Closed Captioning? So that each chunk of text is revealed as you read it and not to far ahead or behind? I see, yes that would require a script or your staggered chat menu idea. I bet Aaron can come up with a good solution, too :)
You could take a simpler approach: put your text into an Excel/Sheets with each chunk of text on a different row in the ‘C’ column. In the ‘B’ column put whatever chat command you want with a query and incrementing #, such as: /desc ?{Line 1|Text to show to players}  Then concatenate them together for pasting into chat. You’ll get an output of a bunch of queries that you simply need to press ‘enter’ to send them to the chat in a /desc format.  I can rough that together when I’m home if it doesn’t make sense. 
1674336686
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
That's pretty brilliant, and worth of a Trick in the  Tips n Tricks  thread.
1674337400
GiGs
Pro
Sheet Author
API Scripter
Jarren said: I can rough that together when I’m home if it doesn’t make sense.  Please do, because I'm not sure I follow it.
1674342167
The Aaron
Roll20 Production Team
API Scripter
Hmm. I just tried that and I don't think it's going to work.  The intent is good, but it batches all the sends until the last Roll Query is evaluated.
The Aaron said: Hmm. I just tried that and I don't think it's going to work.  The intent is good, but it batches all the sends until the last Roll Query is evaluated. Ah dangit you're right. They would each need to be sent with a different chat message, and to make it easy to 'click through', they'd have to be done in reverse order. I'll keep thinking to see if there's any easy way to do something similar. GiGs  said: Jarren  said: I can rough that together when I’m home if it doesn’t make sense.  Please do, because I'm not sure I follow it. Here was the idea: The code in column A2 is: =CONCAT(B2,C2,D2,E2,F2,G2) That would just combine all of the columns to the right. Column F would have each 'line' of text pre-written for whatever script was prepared. Copying down column A would give an output of: /desc ?{Line 1|Two roads diverged in a wood, and I—} /desc ?{Line 2|I took the one less traveled by,} /desc ?{Line 3|And that has made all the difference.} I was hoping that each query could be clicked and input into chat, but because it's all entered as a single command, none of the queries resolve until all are completed.
1674343791
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Basically it is a series of queries that need to be acknowledged in order to complete posting. Each post a separate query. It might need to be done in the opposite direction, though, since the last query would be the topmost dialog box. It would require testing.
1674344549
GiGs
Pro
Sheet Author
API Scripter
You could do it as a sereies of API buttons, essentially a chat menu, I think.
1674345437

Edited 1674354110
Ok I mocked up an alternative in Google Sheets (no Pro subscription required for a Mod script):&nbsp; <a href="https://docs.google.com/spreadsheets/d/1kLf8-3V-fKuBPoytxOLRj1LpjY8F_GVFMBdHzJjkXw4/edit?usp=sharing" rel="nofollow">https://docs.google.com/spreadsheets/d/1kLf8-3V-fKuBPoytxOLRj1LpjY8F_GVFMBdHzJjkXw4/edit?usp=sharing</a> You would cut-and-paste your pre-written script into cells A5 on down, and then you could cut-and-paste the code from column A2 into a Roll20 Collection macro, and put the macro on your Macrobar. You'd need to click the macro, and select which line you want to display next.&nbsp; (Basically this just puts all the lines into a single query that you would run over and over again. The Google sheet just helps create the macro instead of doing it by hand.) The output looks like this: /desc ?{Which line?|1,Two roads diverged in a wood, and I—|2,I took the one less traveled by,|3,And that has made all the difference.}
1674353697
The Aaron
Roll20 Production Team
API Scripter
Ok, here's&nbsp; rough cut on a first pass script, it works off of Handouts.&nbsp; Name the handout "Dialog: SOMETHING", then run: !dialog --list It will list all dialog handouts with a Next and Reset button.&nbsp; If you click Next, it outputs the next line from the handout and moves it to the end of the GM Notes section.&nbsp; If you click the Reset button, it moves all the GM Notes section to the start of the handout.&nbsp; Each time it prints out a line from the handout, it whispers a footer to the GM with &gt;&gt;&gt; for next and Reset buttons.&nbsp; You can also just call: !dialog --next&nbsp; To print the next line from the most recent Dialog that was being output.&nbsp; Here's some screenshots to show what I mean: Anyway, give that a try and let me know how it goes.&nbsp; I'll clean up the output, and add a help function. Script: on('ready',()=&gt;{ let lastID; class DoubleDashArgs { #cmd; #args; constructor(line){ let p=line.split(/\s+--/); this.#cmd = p.shift(); this.#args = [...p.map(a=&gt;a.split(/\s+/))]; } get cmd() { return this.#cmd; }; has(arg) { return !!this.#args.find(a=&gt;arg===a[0]); } keys() { return this.#args.map(a=&gt;a[0]); } params(arg,slice=1) { return [...(this.#args.find(a=&gt;arg===a[0])||[])].slice(slice); } toObject() { return {cmd:this.#cmd,args:this.#args}; } } const showHelp = (who) =&gt; { sendChat('',`/w "${who}" Dialog Help...`); }; const listDialogs = (who) =&gt; { let d = findObjs({type:'handout'}) .filter(h=&gt;/^dialog:/i.test(h.get('name'))) .map(d=&gt;`&lt;li&gt;&lt;b&gt;${d.get('name')} &lt;/b&gt;&lt;a href="!dialog --next ${d.id}"&gt;Next&lt;/a&gt;&lt;a href="!dialog --reset ${d.id}"&gt;Reset&lt;/a&gt;&lt;/li&gt;`) ; sendChat('',`/w "${who}" &lt;div&gt;&lt;h3&gt;Dialogs&lt;/h3&gt;&lt;ul&gt;${d}&lt;/ul&gt;&lt;/div&gt;`); }; const nextDialog = (who, id) =&gt; { prevLastID = lastID; lastID=id; let d = getObj('handout',lastID); if(d){ d.get('notes',(n)=&gt;{ d.get('gmnotes',(g)=&gt;{ if(/^null$/.test(g)){ g=''; } let p = n.indexOf('&lt;/p&gt;'); let line; if(-1 === p){ line = n; n=''; } else { line = n.substr(0,p+4); n=n.slice(p+4); } g+=line; d.set('notes',n); d.set('gmnotes',g); sendChat('',line); sendChat('',`/w "${who}" &lt;div style="font-size: .7em;; line-height:.7em;text-align:center;font-weight:bold;float:right;width:90%;"&gt;${n.length ? `&lt;a href="!dialog --next ${id}"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/a&gt;` : ''} &lt;a style="float:right;" href="!dialog --reset ${id}"&gt;Reset&lt;/a&gt;&lt;div style="clear:both;"&gt;&lt;/div&gt;&lt;/div&gt;`); }); }); } else { sendChat('',`/w "${who}" &lt;div&gt;&lt;b&gt;Error:&lt;/b&gt; Couldn't find dialog for id: &lt;code&gt;${lastID}&lt;/code&gt;`); lastID=prevLastID; } }; const resetDialog = (who, id) =&gt; { prevLastID = lastID; lastID=id; let d = getObj('handout',lastID); if(d){ d.get('notes',(n)=&gt;{ d.get('gmnotes',(g)=&gt;{ if(/^null$/.test(g)){ g=''; } n = g+n; g=''; d.set('notes',n); d.set('gmnotes',g); sendChat('',`/w "${who}" &lt;div style="font-size: .7em;; line-height:.7em;align:center;font-weight:bold;float:right;width:90%;"&gt;${n.length ? `&lt;a href="!dialog --next ${id}"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/a&gt;` : ''} &lt;/div&gt;`); }); }); } else { sendChat('',`/w "${who}" &lt;div&gt;&lt;b&gt;Error:&lt;/b&gt; Couldn't find dialog for id: &lt;code&gt;${lastID}&lt;/code&gt;`); lastID=prevLastID; } }; // !dialog // !dialog --help // show help // !dialog --list // list dialog handouts // !dialog --next // !dialog --next &lt;id&gt; // show next line of dialog // !dialog --reset // !dialog --reset &lt;id&gt; // reset dialog script on('chat:message',msg=&gt;{ if('api'===msg.type &amp;&amp; /^!dialog(\b\s|$)/i.test(msg.content) &amp;&amp; playerIsGM(msg.playerid)){ let who = (getObj('player',msg.playerid)||{get:()=&gt;'API'}).get('_displayname'); let args = new DoubleDashArgs(msg.content); if(0 === args.keys().length || args.has('help')){ showHelp(who); return; } args.keys().forEach(k=&gt;{ switch(k.toLowerCase()){ case 'list': listDialogs(who); break; case 'next': nextDialog(who, args.params(k)); break; case 'reset': resetDialog(who, args.params(k)); break; } }); } }); });
I never thought I needed this functionality - until now. ;)
1674414632
The Aaron
Roll20 Production Team
API Scripter
Next up is adding a way to have it speak as a given character, and possibly target specific players/characters.&nbsp; And finishing the help. =D&nbsp; I also think it needs some better parsing on the dialog lines, or a way to let it do multiple blocks at once in a clearer manner, possibly by breaking on horizontal rules instead of paragraphs.
1674493396

Edited 1674493529
timmaugh
Pro
API Scripter
*knock knock knock* ... Hello there! Just your favorite neighborhood metascript vacuum salesman, here! Could I possibly interest you in a metascript? Yes? No? No? Yes? Yes? We'll go with yes. Especially as I throw dirt all over your Roll20 carpet. Create your narrative in a Mule. I'll call mine PuzzleBook on a character called NarrativeMule : 1=First text 2=Second text 3=Third text index=1 Then create an ability to play the narration; I'll call it PlayPuzzleBook : !&amp;{template:default}{{name=Puzzle Book}}{{=get\.NarrativeMule.PuzzleBook.get.NarrativeMule.PuzzleBook.index/get/get}}{&amp;simple}set.NarrativeMule.PuzzleBook.index = {&amp;math get.NarrativeMule.PuzzleBook.index + 1}/set Run PlayPuzzleBook to issue the next message in the sequence. You could create a button for easy access. Here I've run it three times: It increments the index so it's ready for the next time you run the command. Need to reset it? Just create an ability to set the index back to 1: !/w gm &amp;{template:default}{{name=Resetting}}{{PuzzleBook=Resetting PuzzleBook narrative index.}}{&amp;simple}set.NarrativeMule.PuzzleBook.index = 1/set Want to issue a narrative index that isn't the current one? Create an ability with a query for the index to play: !&amp;{template:default}{{name=Puzzle Book}}{{=get.NarrativeMule.PuzzleBook.?{Index to play?|1}/get}}{&amp;simple} Want to issue a narrative index that isn't the current one *and* set the index at the same time (to restart the story from that point)? Do the above but include the set clause from the original ability: !&amp;{template:default}{{name=Puzzle Book}}{{=get.NarrativeMule.PuzzleBook.?{Index to play?|1}/get}}{&amp;simple} set.NarrativeMule.PuzzleBook.index = {&amp;math ?{Index to play?} + 1}/set Want to have a single button able to issue the next message from different narrations? Just turn the direct reference to PuzzleBook into a query with the names of your available narrative Mules (works in any of the quoted command lines, above): ?{Narrative?|PuzzleBook|TomeOfUnduul|TornLetter} But WAIT! There's MORE! This package comes for the low, low price of Muler, ZeroFrame, and MathOps. If you wanted to upgrade to the Platinum Package and include APILogic, you could introduce a conditional to your original command. This way, if your index went above the maximum index of your narration, the command line wouldn't try to output the next, non-existent portion of the narration. It would, instead, prompt the GM to reset the index. To do this, add a limit variable to the PuzzleBook Mule, and set it to the maximum index (if you add/remove lines of text later, just increase/decrease the limit appropriately): Then add the conditional to your PlayPuzzleBook: !&amp;{template:default}{{name=Puzzle Book}}{&amp;if get.NarrativeMule.PuzzleBook.index/get &gt; get.NarrativeMule.PuzzleBook.limit/get }{{Reset Required=PuzzleBook index requires reset. Proceed?}}{{=[Reset](~NarrativeMule|ResetPuzzleBook)}}{&amp;else}{{=get\.NarrativeMule.PuzzleBook.get.NarrativeMule.PuzzleBook.index/get/get}}set.NarrativeMule.PuzzleBook.index = {&amp;math get.NarrativeMule.PuzzleBook.index + 1}/set{&amp;end}{&amp;simple} Ta da. I'll just leave a brochure. No? No, I should keep my broch-- OK.