Step 4 and 5 – Gathering and Saving XP
The middle column of the advancement frame looks like this:

Don’t think too much about the labels – they’ll likely change. The HTML looks like this:
<div class="questions">
<h4 class="full-width">XP Questions</h4>
<div class="show-through">
<input type="checkbox" class="question" name="attr_question0" value="1">
<span class="display" class="question" title="Popup text for this question">Name of this Question</span>
</div>
<div class="show-through">
<input type="checkbox" class="question" name="attr_question1" value="1">
<span class="display" class="question" title="Popup text for this question">Name of this Question</span>
</div>
<!-- some questions trimmed out -->
<div class="show-through">
<input type="number" class="question no-spinner" name="attr_question5" value="0">
<input type="checkbox" name="question0_stop" class="hidden" value="1">
<span class="display" class="question" title="How many achievements this Episode? (See the list below.)">Achievements?</span>
</div>
<h4 class="achievements full-width">Achievements</h4>
<div class="show-through">
<input type="checkbox" class="achievement" name="attr_achievement0" value="1">
<input type="checkbox" name="attr_achievement0_stop" class="hidden" value="1">
<span class="display" class="achievement" title="Popup text for this achievement">Name of this Achievement</span>
</div>
<div class="show-through">
<input type="checkbox" class="achievement" name="attr_achievement1" value="1">
<input type="checkbox" name="attr_achievement1_stop" class="hidden" value="1">
<span class="display" class="achievement" title="Popup text for this achievement">Name of this Achievement</span>
</div>
<!-- some achievements trimmed out -->
Code language: HTML, XML (xml)
This is a snippet with lots of items cut out – it’s just to get the basic idea of the layout. This section also refers to the show-through
class, which is made global near the start, and looks like this:
.show-through {
display: contents;
}
Code language: CSS (css)
display: contents
is a useful setting, it says to ignore this element in the layout. So, everything below the divs
with this class are treated as if they were on the level of that div
. This can be very handy for layout.
This leads us to the sheet workers, which implement these features:
During an adventure, once you click a question, it can’t be unchecked. But when the save XP button is clicked, all the question checkboxes are set to zero, and the XP total is increased.
For achievements, these can be unchecked, but once the save XP button is clicked, any achievement has been used, and since each can only be used once, it is fixed from then on.
These is an input which shows how many achievements can contribute to this adventure, so its the total of the checked boxes, minus those which are fixed. Also, fixed achievements cannot be unchecked.
Let’s look at the sheet workers that make all of this work.
First, the simplest sheet worker (actually two sheet workers). The XP total is at the bottom of the first column:

Those two buttons at the bottom each add or subtract 1 XP from the total each time they are clicked. This is handy for correcting errors or making small changes. The sheet worker reads the XP total, then depending on which button was clicked, adds or subtracts 1 from the XP total.
['add', 'subtract'].forEach(x => {
on(`clicked:${x}_xp`, () => {
getAttrs(['xp'], v => {
xp = int(v.xp);
xp = xp + (x === 'add' ? 1 : -1);
setAttrs({
xp: xp
});
});
})
});
Code language: JavaScript (javascript)
Now on to the more complicated stuff. When that save XP button is clicked (called the confirm button), all the XP has to be summed up, deleted from the questions section, and added to the XP total. This sheet worker does that.
// when confirm button is clicked:
// all XP from question is added to XP
// all achievements are changed to _stop
on('clicked:confirm', () => {
getAttrs(['XP', ...seq(5).map(q => `question${q}`), 'question5',
...seq(10).map(q => `achievement${q}`)], v => {
let xp = int(v.XP);
const output = {};
seq(6).forEach(q => {
xp = xp + int(v[`question${q}`])
output[`question${q}`] = 0;
});
seq(10).forEach(i => {
const check = int(v[`achievement${i}`]);
output[`achievement${i}_stop`] = check;
});
output.xp = xp;
setAttrs(output);
});
});
Code language: JavaScript (javascript)
Most of the question attributes are checkboxes, and can only have values of 0 or 1, but question5 is the sum of all the achievements and can have any number up to 10. The section below goes through all the achievements, and saves their values to that attribute’s the _stop
counterpart.
seq(10).forEach(i => {
const check = int(v[`achievement${i}`]);
output[`achievement${i}_stop`] = check;
});
Code language: JavaScript (javascript)
This sheet worker makes use of the custom seq
function, which creates a list of numbers starting at 0. It’s probably one of my useful custom functions – I use it a lot.
Step 6 – The Special Case of Achievements
Each of the questions is a checkbox with a value of 0 or 1, except the last which is a number and can be higher than 1. It is the sum of all achievements gained this adventure. To find this total, we add all the achievement chekboxes, and subtract their _stop
counterpart.
The _stop
attribute is stored when saving XP to fix that achievement’s value. Here’s where that total is calculated. Notice how we see if the achievement is checked, then also see if it has a stop value checked. If check, but no stop, xp i gained – see the sum attribute.
// calculating final xp question: sum of all achievements that do not have a stop box checked.
on(`${changes(seq(10).map(s => `achievement${s}`))} ${changes(seq(10).map(s => `achievement${s}_stop`))}`, (q) => {
getAttrs([...seq(10).map(q => `achievement${q}`), ...seq(10).map(q => `achievement${q}_stop`)], v => {
let sum = 0;
seq(10).forEach(i => {
const check = int(v[`achievement${i}`]);
const stop = int(v[`achievement${i}_stop`])
sum = sum + check - stop;
});
setAttrs({
question5: sum
});
});
});
Code language: JavaScript (javascript)
Then the total is saved in the question5
attribute, which exists just to hold this total.
The Reset XP Attribute
So, that’s the XP mostly sorted, so now we turn to questions of usability. When you try to uncheck an xp question or achievement it is blocked (in reality, it is unchecked then rechecked). This is to make it easier to keep track of these totals. But it’s a little tricky to figure out how to do it.
There is a configuration attribute, reset_xp
, which when checked, allows you to uncheck XP boxes. If that’s not set to a value above zero, you can’t. So we need to grab its value, then perform some logical tests (if statements) see below.
// when try to uncheck
// question: only allowed if reset XP checked
// achievements: ditto, but also blocked if stop is checked.
on(`${changes(seq(5).map(s => `question${s}`))} ${changes(seq(10).map(s => `achievement${s}`))}`, (q) => {
getAttrs(['reset_xp', ...seq(10).map(q => `achievement${q}_stop`)], v => {
const reset = int(v.reset_xp);
const output = {};
if(!reset && q.sourceType == 'player' && q.previousValue == 1) {
output[q.sourceAttribute] = 1
}
seq(10).forEach(i => {
const stop = int(v[`achievement${i}_stop`]);
if(!reset && stop) output[`achievement${i}`] = 1;
});
// need to reset stop to 0 if reset_xp
if(reset && seq(10).map(q => `achievement${q}`).includes(q.sourceAttribute)) {
const checked = int(v[`achievement${q}`]);
if(!checked) {
output[`${q.sourceAttribute}_stop`] = 0;
}
log({q})
}
setAttrs(output)
});
});
Code language: JavaScript (javascript)
The first logical test here allows you to reset question boxes. The second is for achievements.
The 3rd logical test first checks is the attribute just clicked is an achievement, and if it is, it makes sure that achievement’s _stop
counterpart is set to a value of zero if the achievement is also zero. If the main achievement has a value of 0 and it’s _stop
counterpart has a value of 1, the xp totals will be negative and incorrect.
Finishing Up Advancement
At the start of the experience tab, we have threse 3 buttons, allowing you to buy certain advances.

At the moment, they cost nothing – but we can fix that. First step, we create another hidden configuration attribute, advance_enabled, which has a default value of zero. Whenever a chapter ends, this is set to a value of 1. At this point you can buy a single advance, and 1 extra for each unit of 5 XP that you spend. To make this happen, we alter some already written code:
['skill', 'trait', 'extras'].forEach(type => {
on(`clicked:advance_${type}`, () => {
getAttrs([`${type}_advances`, 'advance_enabled', 'xp'], v => {
const output = {}
const enabled = int(v.advance_enabled);
const xp = int(v.xp);
output[`${type}_advances`] = int(v[`${type}_advances`]) +1;
if(!enabled) output.xp = xp-5;
output.advance_enabled = 0;
setAttrs(output);
});
});
});
Code language: JavaScript (javascript)
Here we add 1 to the number of advances, and other code already written will redraw the advances table and handle extra skills or whatever.
Then we check if the character has the advance _eenabled checked, and if not, we spend 5 xp. Then we make sure advance_enabled is set to 0 – it’s good for one free purchase.
Finally, we make sure we can’t try to buy an advance unless we have advance_enabled or ave 5 or more XP. If we cant buy an advance, those buttons are greyed out.

We do this with a CSS rule:
.charsheet .check:not([value="1"]) ~ .advance-button {
pointer-events: none;
background-color: grey;
}
Code language: CSS (css)
This greys out the buttons and through pointer-events
, makes sure you can’t click the buttons.
That leaves one piece of code to write. We want to make sure that a hidden input before those buttons is sett o a value of 0 or 1, so that those buttons are made usable when needed.
That hidden input is named advance_buttons
and given a class of check
, and its value is calculated as follow.
on('change:advance_enabled change:xp', () => {
getAttrs(['advance_enabled', 'xp'], v => {
const enabled = int(v.advance_enabled);
const xp = int(v.xp);
const buttons = +(enabled || xp >= 5);
setAttrs({
advance_buttons: buttons
});
});
});
Code language: JavaScript (javascript)
With that advancement tab sorted finally, after a month of posts, we can get back to the rest of the sheet.