In this post, we look over the sheet and check everything is working (done) and add some generally useful (and easily transferable) features. In fact, this is a post full of things which you can transfer to your own projects.
Scene and Chapter Buttons
Certain things happen at the end of each scene or chapter (adventure) It would be fine to let players go through and uncheck everything they need to do, but that is irritating and time consuming. These buttons do that for the players – they reset a bunch of attributes with just the click of a button.
Scene Button
At the end of a scene, you reset all used Stress and Traits. Other usable values are left alone.
on('clicked:scene', () => {
getSectionIDs('repeating_trait', ids => {
const checks = ids.reduce((all, one) => [...all, `repeating_trait_${one}_check`], [])
getAttrs(['traits_running_total', 'traits_running_total_max', ...checks], v => {
const output = {};
const scene = int(v.traits_running_total);
const max = int(v.traits_running_total_max);
const sum = Math.min(max, checks.reduce((all, one) => all + int(v[one]), 0));
output.traits_running_total = Math.min(max, scene + sum);
ids.forEach(i => {
output[`repeating_trait_${i}_check`] = scene >= (max -1) ? 1 : 0;
});
seq(10).forEach(i => output[`stress${i}`] = 0);
output.stress = 0;
output.doom = 0;
setAttrs(output);
});
});
});
Code language: JavaScript (javascript)
During a scene, when you check a Trait, the total for traits used is updated. When the scene ends, the total of checked traits is found, and added to a running total. That is used to dhow the ongoing used total.
If it equals or exceeds the max traits, all checkboxes are marked used. (Now, if a character adds a new Trait before running the end scene button, these attributes may not be accurate, but that should not happen.)
Then used Stress is reset to 0, Along with all stress boxes. And sinced Doom is unchecked at the end of each scene, that’s there too.
Chapter Button
At the end of a Chapter you reset everything usable in that adventure, and also gain one free Advance.
on('clicked:chapter', () => {
const output = {};
output.traits_running_total = 0;
seq(10).forEach(i => {
output[`trait${i}`] = 0;
});
output.advance_enabled = 1;
getSectionIDs('repeating_trait', traits => {
traits.forEach(i => output[`repeating_trait_${i}_check`] = 0);
getSectionIDs('repeating_things', things => {
things.forEach(i => output[`repeating_things_${i}_check`] = 0);
getSectionIDs('repeating_extras', extras => {
extras.forEach(i => output[`repeating_extras_${i}_check`] = 0);
seq(10).forEach(i => output[`stress${i}`] = 0);
output.stress = 0;
conditions.forEach(c => output[c] = 0)
output.doomed = 0;
setAttrs(output);
});
});
});
});
Code language: JavaScript (javascript)
This affects more things than the Scene button, but is ultimately easy because it just resets a lot of things to 0. Here is the only exception: output.advance_enabled = 1;
this gives a free advance at the end of an adventure.
Send A Notification to Chat
When someone is working on a character sheet, any changes made are visible only to them. It would be handy if notifications were sent to the other players or just the GM. At present the only way to do that is send a message to chat. Here, we’ll define how to create a message and send it to chat.
This can be included in other sheet workers quickly and easily. Here is a fairly simple function that works with the existing roll template.
/*
msg: the output you want to show
title: should be full text you wwant displayed, without any {{ttle= }}.
Can include @{character_name} in either of these.
send_message('this is a message');
send_message('this is a message', 'this is a title')
*/
const send_message = (msg, title='') => {
startRoll(`&{template:ladder} ${title ?
`{{header=1}} {{title=${title} }}` : ''} {{message=${msg}}}`, no_roll => {
finishRoll(no_roll.rollId)
});
};
Code language: JavaScript (javascript)
Som,, you can just add something like the following to existing sheet workers:
send_message('@{character_name} used a Trait.');
Code language: JavaScript (javascript)
and that notification will be sent to chat whenever that sheet worker is run. It could be placed in a conditional branch, so it is only run sometimes.
Block Changes
There are several things on the sheet that should be hard to change, but when you make something harder to change, you have to account for accidents – what if someone makes a change accidentally and then wants to undo it.
So, for the most part, we’ll just send messages to chat when many things are changed, so someone can say, “Hey, that’s not supposed to happen, you should change it back.”
XP Questions and Achievements
But there are a couple of things that are still hard to change, because of the way they work. The main things are Achievements, which once XP is committed, can’t be changed, and XP Questions, which once selected can’t be undone.
The way these work is that when you unselect them, they are immediately selected again. Now, we’ll modify that behaviour, so that before being reselected, you”ll be asked if you want to do that.
The worker for this looks pretty complicated, but I’ll explain it in detail below.
const questions_list = ["Relationship", "Heroism", "Setback", "Forbidden", "Conclusion"];
const achievements_list = ["Conceder", "Contacter", "Demonstrator", "Evoker", "Imaginator", "Manifestor", "Refresher", "Retrofitter", "Sacrificer", "Teamworker", "Troublemaker"];
on(`${changes(seq(5).map(s => `question${s}`))} ${changes(seq(10).map(s => `achievement${s}`))}`, event => {
const output = {};
const msg_string = `${event.sourceAttribute.startsWith('q') ?
`the XP Question: ${questions_list[int(event.sourceAttribute.slice(-1))]}` :
`the Achievement: ${achievements_list[int(event.sourceAttribute.slice(-1))]}` }`;
if(event.sourceType == 'player' && event.newValue == 1) {
send_message(`@{character_name} selected ${msg_string}`);
} else if(event.sourceType == 'player' && event.previousValue == 1) {
const report_string = `!{{ask=[[?{Are you sure you want to do this?|Cancel,0|Do It,1}]]}}`;
startRoll(report_string, function(question) {
const query = question.results.ask.result;
if(query) {
if(event.sourceAttribute.startsWith('a')) {
const stop = event.sourceAttribute + '_stop';
output[stop] = 0;
setAttrs(output);
}
send_message(`@{character_name} deselected ${msg_string}`);
} else {
output[event.sourceAttribute] = 1;
send_message(`@{character_name} considered deselecting ${msg_string}`);
setAttrs(output)
}
});
}
});
Code language: JavaScript (javascript)
There’s a lot going on here, so I’ll break it down into sections.
const questions_list = ["Relationship", "Heroism", "Setback", "Forbidden", "Conclusion"];
const achievements_list = ["Conceder", "Contacter", "Demonstrator", "Evoker", "Imaginator", "Manifestor", "Refresher", "Retrofitter", "Sacrificer", "Teamworker", "Troublemaker"];
Code language: JavaScript (javascript)
We start by creating a couple of array variables. This will make some of the later code easier. The questions are listed from 0 to 4, and achievements 0 to 9, and are listed here in the same oerrder they are on the character sheet.
on(`${changes(seq(5).map(s => `question${s}`))} ${changes(seq(10).map(s => `achievement${s}`))}`, event => {
Code language: JavaScript (javascript)
The custom functions, seq
and changes
, along with the standard JavaScript function map
, are extremely useful, and are here used to build the on(‘change’ text without having to type out 15 items.
const output = {};
const msg_string = `${event.sourceAttribute.startsWith('q') ?
`the XP Question: ${questions_list[int(event.sourceAttribute.slice(-1))]}` :
`the Achievement: ${achievements_list[int(event.sourceAttribute.slice(-1))]}` }`;
Code language: JavaScript (javascript)
msg_string
builds a string that will be used three times in the following code, so having it built here is handy and saves in typing.
This variable finds the identity of the triggered attribute – which XP Question or Achievement is it? It first uses event.sourceAttribute
, and its first character tells us whether its an XP Question or an Achievement, and the last character (found with the slice
function) tells us the number top grab in the relevant array.
The rest of the code is split into three sections: first, to identify when a checkbox is checked, then when it is already checked and the player unclicks that checkbox, and finally when the player clicks a checked box and chooses not to uncheck it.
if(event.sourceType == 'player' && event.newValue == 1) {
send_message(`@{character_name} selected ${msg_string}`);
Code language: JavaScript (javascript)
event.newValue
tells us the player just clicked the checkbox setting it from unchecked to checked. So the send_message()
function sends a message to chat.
} else if(event.sourceType == 'player' && event.previousValue == 1) {
const report_string = `!{{ask=[[?{Are you sure you want to do this?|Cancel,0|Do It,1}]]}}`;
startRoll(report_string, function(question) {
const query = question.results.ask.result;
if(query) {
Code language: JavaScript (javascript)
Now e know the player is clicking on an already checked checkbox, so we ask thenm if they sure they want to uncheck that question or achievement, and grab their answer which is yes or no. That answer is numeric, a 1 or 0. Then we do a query on that number.
if(event.sourceAttribute.startsWith('a')) {
const stop = event.sourceAttribute + '_stop';
output[stop] = 0;
setAttrs(output);
}
send_message(`@{character_name} deselected ${msg_string}`);
Code language: JavaScript (javascript)
If the player selected “yes”, we check if its an achievement and if so, change the matching _stop
attribute to 0, then send a message saying this attribute was unselected.
} else {
output[event.sourceAttribute] = 1;
send_message(`@{character_name} considered deselecting ${msg_string}`);
setAttrs(output)
}
});
}
});
Code language: JavaScript (javascript)
Finally, when the query is 0 (the player chose to cancel), we reset the checkbox to 1, and send a message saying the player considered deselecting this attribute.,. Actually, we might remove that message.
And there we have it, the character sheet is fully complete – except for that Special box which is for individual game setting variations. We’ll look at that soon.
The “Do You really Want To?” Function
The last part of this post suggests we should really have a custom function. When the player clicks it, they get asked, “Do You really Want? This Will Make Major Changes.” Then they can choose whether to proceed or not. If they do, a message gets sent to chat about with the changes they’ve committed. And so, here is code for that!
const ask = (msg, prompt = 0, announce = '', callback) => {
const report_string = `!{{ask=[[?{${msg}|Cancel,0|Do It,1}]]}}`;
if(prompt) {
startRoll(report_string, function(question) {
const query = question.results.ask.result;
if(query) {
callback();
if(msg) {
send_message(announce);
}
}
});
} else {
callback();
if(msg) {
send_message(announce);
}
}
};
Code language: JavaScript (javascript)
This is a function that needs to called from another worker. We have the Scene function above, here’s what it looks like with that function added:
on('clicked:scene', () => {
getSectionIDs('repeating_trait', ids => {
const checks = ids.reduce((all, one) => [...all, `repeating_trait_${one}_check`], [])
getAttrs(['traits_running_total', 'traits_running_total_max', ...checks, 'ask_first'], v => {
const output = {};
const scene = int(v.traits_running_total);
const max = int(v.traits_running_total_max);
const sum = Math.min(max, checks.reduce((all, one) => all + int(v[one]), 0));
output.traits_running_total = Math.min(max, scene + sum);
ids.forEach(i => {
output[`repeating_trait_${i}_check`] = scene >= (max -1) ? 1 : 0;
});
seq(10).forEach(i => output[`stress${i}`] = 0);
output.stress = 0;
output.doom = 0;
const ask_first = int(v.ask_first);
ask("Are you sure you want to do this?", ask_first, `@{character_name} advanced a Scene.`, callback => {
setAttrs(output);
});
});
});
});
Code language: JavaScript (javascript)
Very little has changed here. We added 'ask_first'
to getAttrs
, and changed the setAttrs
line to this:
const ask_first = int(v.ask_first);
ask("Are you sure you want to do this?", ask_first, `@{character_name} advanced a Scene.`, callback => {
setAttrs(output);
});
Code language: JavaScript (javascript)
So, elsewhere on the sheet we have a query, “should the sheet ask permission?” and that is the ask_first query, which has a vaue of 0 or 1. If 0, no question is asked, but on 1 the player is asked if they want to proceed.
So, lets break down the ask
function.
const ask = (msg, prompt = 0, announce = '', callback) => {
Code language: JavaScript (javascript)
The first line creates the initial parameters which can be set when the function is initiated. Recall that our scene function used this:
const ask_first = int(v.ask_first);
ask("Are you sure you want to do this?", ask_first, `@{character_name} advanced a Scene.`, callback => {
setAttrs(output);
});
Code language: JavaScript (javascript)
Look at the ask
line. It is creating a msg
– which is shown to the player, has a place for the prompt
(1 or 0), and then shows the message that will be shown to chat.
It ends with calllback
, and callbacks are a much-maligned feature of JavaScript (well-deserved really). Roll20 already uses a lot of callbacks, like getAttrs([array of stats]., callback => {
A callback is a function that is passed to this function, and run on command. It;’s basically whatever is nested here – in the Scene function, that’s the setAttrs
command. Don’t worry if you don’t grasp them yet, you don’t need to.
Essentially, this is the ask function:
const ask = (query msg, should we do this, chat message if we do, callback) => {
if(we should do this) {
use custom roll parsing to get a 1 or 0 {
if(we get a 1) {
callback() - do the thing(s) in the ask function;
send a message to chat that we did it
}
};
} else if we shouldnt ask {
callback() - just do the thing without asking;
send a message to chat that we did it
}
};
Code language: JavaScript (javascript)
And that’s it basically – you can study the function to see how it works, of just use it.
Colouring Based on a Choice
A lot of the buttons are quite bland. It would be nice to have skill rank boxes coloured according to their rank. This can be done with Roll20’s version of JQuery, but since that is something I haven’t described anywhere else on the site yet, I’ll properly explore this in its own post next week.