Skip to content

CharSheetUtils | 1.1 | Beacon update #2069

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions CharSheetUtils/1.1/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Create the CharSheetUtils library. All the functionality of this library
* is exposed through its static methods.
*/
var CharSheetUtils = (() => {
'use strict';

return class {
/**
* Asynchronously gets the value of a character sheet attribute.
* @param {Character} character
* @param {string} attr
* @return {Promise<number>}
* Contains the value of the attribute.
*/
static async getSheetAttr(character, attr) {
if(attr.includes('/'))
return CharSheetUtils.getSheetRepeatingAttr(character, attr);
else {
return getSheetItem(character.id, attr).then((value) => {
return value;
});
}
}

/**
* Asynchronously gets the value of a character sheet attribute from a
* repeating row.
* @param {Character} character
* @param {string} attr
* Here, attr has the format "sectionName/nameFieldName/nameFieldValue/valueFieldName".
* For example: "skills/name/perception/total"
* @return {Promise<number>}
* Contains the value of the attribute.
*/
static getSheetRepeatingAttr(character, attr) {
let parts = attr.split('/');
if(parts.length < 4) return;

let sectionName = parts[0];
let nameFieldName = parts[1];
let nameFieldValue = parts[2].toLowerCase();
let valueFieldName = parts[3];

// Find the row with the given name.
return CharSheetUtils.getSheetRepeatingRow(character, sectionName, rowAttrs => {
let nameField = rowAttrs[nameFieldName];
if(!nameField)
return false;
return nameField.get('current').toLowerCase().trim() === nameFieldValue;
})

// Get the current value of that row.
.then(rowAttrs => {
if(!rowAttrs)
return NaN;

let valueField = rowAttrs[valueFieldName];
if(!valueField)
return NaN;
return valueField.get('current');
});
}

/**
* Gets the map of attributes inside of a repeating section row.
* @param {Character} character
* @param {string} section
* The name of the repeating section.
* @param {func} rowFilter
* A filter function to find the correct row. The argument passed to it is a
* map of attribute names (without the repeating section ID part - e.g. "name"
* instead of "repeating_skills_-123abc_name") to their actual attributes in
* the current row being filtered. The function should return true iff it is
* the correct row we're looking for.
* @return {Promise<any>}
* Contains the map of attributes.
*/
static getSheetRepeatingRow(character, section, rowFilter) {
// Get all attributes in this section and group them by row.
let attrs = findObjs({
_type: 'attribute',
_characterid: character.get('_id')
});

// Group the attributes by row.
let rows = {};
_.each(attrs, attr => {
let regex = new RegExp(`repeating_${section}_(-([0-9a-zA-Z\-_](?!_storage))+?|\$\d+?)_([0-9a-zA-Z\-_]+)`);
let match = attr.get('name').match(regex);
if(match) {
let rowId = match[1];
let attrName = match[3];
if(!rows[rowId])
rows[rowId] = {};

rows[rowId][attrName] = attr;
}
});

// Find the row that matches our filter.
return Promise.resolve(_.find(rows, rowAttrs => {
return rowFilter(rowAttrs);
}));
}

/**
* Asynchronously rolls a dice roll expression and returns the roll result
* in a Promise.
* @param {string} expr
* @return {Promise<RollResult>}
*/
static rollAsync(expr) {
return new Promise((resolve, reject) => {
sendChat('CharSheetUtils', '/w gm [[' + expr + ']]', (msg) => {
try {
let results = msg[0].inlinerolls[0].results;
resolve(results);
}
catch(err) {
log(expr);
reject(err);
}
});
});
}

static send(text) {
sendChat('API', '' + `<div>${text}</div>`, null, {noarchive:true});
}

static async handleInput(msg) {
let args = msg.content.split(' ');
let command = args.shift().substring(1);
let extracommand = args.shift();

if (command === 'roll') {
let t = await CharSheetUtils.rollAsync(extracommand);
CharSheetUtils.send(t);
}

else if(command === 'getattr') {
if(!msg.selected) return;
for (const s of msg.selected) {
if (s._type !== 'graphic') continue;

let token = getObj('graphic', s._id);
if (!token) continue;
let character = getObj('character', token.get('represents'));

let t = await CharSheetUtils.getSheetAttr(character, extracommand);
CharSheetUtils.send(t);
}
}
}
};
})();

on('ready',function() {
'use strict';

on('chat:message', CharSheetUtils.handleInput);
});
8 changes: 5 additions & 3 deletions CharSheetUtils/script.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{
"name": "Character Sheet Utils",
"script": "index.js",
"version": "1.0",
"previousversions": [],
"version": "1.1",
"previousversions": [
"1.0"
],
"description": "# Character Sheet Utils\r\rThis script provides a collection of utility functions for reading and writing\rattributes from a character sheet, including support for attributes in\rrepeating sections and calculated attributes. This script does nothing on\rits own. It is a helper library meant to be used by other scripts.\r\rFull documentation for the functions provided by this script are given\rin the jsdoc annotations in the source code.\r\r## Help\r\rMy scripts are provided 'as-is', without warranty of any kind, expressed or implied.\r\rThat said, if you experience any issues while using this script,\rneed help using it, or if you have a neat suggestion for a new feature,\rplease shoot me a PM:\rhttps://app.roll20.net/users/46544/ada-l\r\rWhen messaging me about an issue, please be sure to include any error messages that\rappear in your API Console Log, any configurations you've got set up for the\rscript in the VTT, and any options you've got set up for the script on your\rgame's API Scripts page. The more information you provide me, the better the\rchances I'll be able to help.\r\r## Show Support\r\rIf you would like to show your appreciation and support for the work I do in writing,\rupdating, maintaining, and providing tech support my API scripts,\rplease consider buying one of my art packs from the Roll20 marketplace:\r\rhttps://marketplace.roll20.net/browse/publisher/165/ada-lindberg\r",
"authors": "Ada Lindberg",
"roll20userid": 46544,
"useroptions": [],
"dependencies": [],
"modifies": {}
}
}
82 changes: 40 additions & 42 deletions CharSheetUtils/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,58 +6,19 @@ var CharSheetUtils = (() => {
'use strict';

return class {
/**
* Attempts to force a calculated attribute to be corrected by
* setting it.
* @param {Character} character
* @param {string} attr
*/
static forceAttrCalculation(character, attr) {
// Attempt to force the calculation of the attribute by setting it.
createObj('attribute', {
_characterid: character.get('_id'),
name: attr,
current: -9999
});

// Then try again.
return CharSheetUtils.getSheetAttr(character, attr)
.then(result => {
if(_.isNumber(result))
return result;
else
log('Could not calculate attribute: ' + attr + ' - ' + result);
});
}

/**
* Asynchronously gets the value of a character sheet attribute.
* @param {Character} character
* @param {string} attr
* @return {Promise<number>}
* Contains the value of the attribute.
*/
static getSheetAttr(character, attr) {
static async getSheetAttr(character, attr) {
if(attr.includes('/'))
return CharSheetUtils.getSheetRepeatingAttr(character, attr);
else {
let rollExpr = '@{' + character.get('name') + '|' + attr + '}';
return CharSheetUtils.rollAsync(rollExpr)
.then((roll) => {
if(roll)
return roll.total;
else
throw new Error('Could not resolve roll expression: ' + rollExpr);
})
.then(value => {
if(_.isNumber(value))
return value;

// If the attribute is autocalculated, but could its current value
// could not be resolved, try to force it to calculate its value as a
// last-ditch effort.
else
return CharSheetUtils.forceAttrCalculation(character, attr);
return getSheetItem(character.id, attr).then((value) => {
return value;
});
}
}
Expand All @@ -74,6 +35,8 @@ var CharSheetUtils = (() => {
*/
static getSheetRepeatingAttr(character, attr) {
let parts = attr.split('/');
if(parts.length < 4) return;

let sectionName = parts[0];
let nameFieldName = parts[1];
let nameFieldValue = parts[2].toLowerCase();
Expand Down Expand Up @@ -161,5 +124,40 @@ var CharSheetUtils = (() => {
});
});
}

static send(text) {
sendChat('API', '' + `<div>${text}</div>`, null, {noarchive:true});
}

static async handleInput(msg) {
let args = msg.content.split(' ');
let command = args.shift().substring(1);
let extracommand = args.shift();

if (command === 'roll') {
let t = await CharSheetUtils.rollAsync(extracommand);
CharSheetUtils.send(t);
}

else if(command === 'getattr') {
if(!msg.selected) return;
for (const s of msg.selected) {
if (s._type !== 'graphic') continue;

let token = getObj('graphic', s._id);
if (!token) continue;
let character = getObj('character', token.get('represents'));

let t = await CharSheetUtils.getSheetAttr(character, extracommand);
CharSheetUtils.send(t);
}
}
}
};
})();

on('ready',function() {
'use strict';

on('chat:message', CharSheetUtils.handleInput);
});
Loading