Due to an upcoming cutscene in my campaign (and my inability to do live multi-personality acting with six characters talking to each other), this evening I whipped this up. Perhaps in the future, I'll update it to make it more user-friendly or make the code prettier, but for now, this is it. Simply put, this is a script that automates the printing of chat messages from NPCs, speaking as those NPCs. The key feature is that the messages appear automatically on a delay based on length. FAQ Why not just use !delay? !delay requires the DM to specify the time durations, whereas Dialogue does not. Dialogue allows for a DM-specified reading speed (default=60ms per character), giving players ample time to read each message without requiring the DM to precompute everything. You just write the message and Dialogue does the rest! You can also alter playback speed while it's executing. Why not just print out the dialogue in one go? Dialogue allows messaging to occur with dynamic delays between messages, both making it more immersive as it appears to be happening in real-time and also keeping speed-readers (or flip-to-the-end readers) from spoiling it for everyone else. Commands !dialogue -speed <speed> Sets the playback speed of the message. Bigger number = slower playback. !dialogue -play Allows playback. !dialogue -stop Disallows playback. If playback is currently executing, it is immediately halted. !dialogue UserA|UserA's message||UserB|UserB's message||UserC|UserC's Message Each message is separated by two pipes "||" and you separate the speaker from the message with a single pipe "|". Future Ideas Allow DM to play/pause playback at will without entirely aborting. Allow DM to pre-create dialogues that can be executed from a menu. At present, if you want to have multiple dialogues, you'll need to have different macros pre-made or manually copy-paste them in. Allow conversation paths. Allow formatting of messages, possibly including avatar images if the speaker is a named NPC. Alternatively, this could end up getting bundled into the Shopmaster update to Cashmaster, enabling scripted interactions with shopkeepers. I may never get around to those since this is pretty much a one-off, but if anyone ever wants to do it themselves, they're welcome to do so. Code /*
* ============
* Dialogue
* ============
*
* Michael Greene
*
* A simple conversation script.
* !dialogue -play
* !dialogue -stop
* !dialogue -speed <speed>
* !dialogue Joe|short||Jill|medium ish message right here, thanks||Bill|i'ma gonna give a long message right here, oh yes sir I am. big bad joe jumped on the happy hill holiday.||Bill|I know this doesn't make any sense, but that's not the point. It's a demo.||Joe|You are correct. It doesn't make sense.
*/
on('ready', () => {
const initDialogue = () => {
// Initialize State object
if (!state.Dialogue) {
log('Initializing Dialogue');
state.Dialogue = {
Play: true,
Speed: 60
};
}
};
const printLine = (lines, lineNumber) => {
if (!state.Dialogue.Play || lineNumber === lines.length) {
return;
}
const line = lines[lineNumber];
const segments = line.split('|');
const speaker = segments[0];
const message = segments[1];
log(`Sending ${speaker} as ${message}`);
sendChat(speaker, message);
// Order next message
readNextLine(lines, lineNumber + 1, message.length);
};
const readNextLine = (lines, lineNumber, lengthOfLastLine) => {
const timeToWait = 600 + lengthOfLastLine * state.Dialogue.Speed;
log(`Waiting for ${timeToWait}, then will initiate line[${lineNumber}].`);
setTimeout(printLine, timeToWait, lines, lineNumber);
};
on('chat:message', (msg) => {
if (msg.type !== 'api') return;
if (msg.content.startsWith('!dialogue') !== true) return;
if (!playerIsGM(msg.playerid)) return;
const parsedContent = msg.content.substr(msg.content.indexOf('!dialogue') + '!dialogue'.length + 1);
const subcommands = parsedContent.split(';;');
subcommands.forEach((subcommand) => {
if (!subcommand.includes('|')) {
if (subcommand.includes('-play')) {
state.Dialogue.Play = true;
log('Allowing Playback');
sendChat('Dialogue', '/w gm Allowing Playback.');
}
else if (subcommand.includes('-stop')) {
state.Dialogue.Play = false;
log('Stopping Playback');
sendChat('Dialogue', '/w gm Stopping Message.');
}
else if (subcommand.includes('-speed')) {
const speedString = subcommand.substr(subcommand.indexOf('-speed') + '-speed'.length + 1);
const speed = parseInt(speedString);
if(!isNaN(speed)) {
state.Dialogue.Speed = speed;
log(`Setting speed to ${speed}`);
sendChat('Dialogue', `/w gm Setting speed to ${speed}.`);
}
}
return;
}
if (!state.Dialogue.Play) {
sendChat('Dialogue', '/w gm Playback currently paused, so Dialogue not initiated.');
}
const lines = parsedContent.split('||');
readNextLine(lines, 0, 0);
});
});
initDialogue();
log("Dialogue Started Up");
});