Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
160 changes: 131 additions & 29 deletions js/5etools-backgrounds.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,20 +86,22 @@ function d20plusBackgrounds () {
const ptraitargs = {
countMin: 1,
countMax: 2,
random:true,
totallyRandom:true,
random: true,
totallyRandom: true,
skip: true
}
const args = {
countMin: 1,
countMax: 1,
random:true,
random: true,
skip: true
}

// Call the menu
// Call the menus, allowing for null sets
const pt = await d20plus.ui.chooseCheckboxList(ptrait, "Personality Trait", ptraitargs);
const id = await d20plus.ui.chooseRadioList(ideal, "Ideal", args);
const bd = await d20plus.ui.chooseRadioList(bond, "Bond", args);
const fl = await d20plus.ui.chooseRadioList(flaw, "Flaw", args);
const id = !ideal ? null : await d20plus.ui.chooseRadioList(ideal, "Ideal", args);
const bd = !bond ? null : await d20plus.ui.chooseRadioList(bond, "Bond", args);
const fl = !flaw ? null : await d20plus.ui.chooseRadioList(flaw, "Flaw", args);

// Return
return {
Expand All @@ -115,16 +117,49 @@ function d20plusBackgrounds () {

const renderer = new Renderer();
renderer.setBaseUrl(BASE_SITE_URL);
const renderStack = [];
let feature = {};
const featureSet = [
"Cultural Chameleon",
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not maintainable (and doesn't support homebrew, besides)--the data* already has .data.isFeature = true for features; this should be used instead of matching by name

* the 5etools source data, at any rate; if that doesn't make its way into here, then we should fix that first

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explicitly tagged the entries as features

"Dust Digger",
"Favored Event",
"Specialty",
"Contacts",
"How Do I Fit In?",
"Favorite Schemes",
"Faceless Persona",
"Why Are You Here?",
"Fey Mark",
"Feywild Visitor",
"Fishing Tale",
"Harrowing Event",
"Role",
"Path to Mystery",
"Hardship Endured",
"Variant Noble (Knight)",
"A Flair for the Dramatic",
"Life at Sea",
"Claim to Fame",
"Carnival Companion"
];
let features = [];
bg.entries.forEach(e => {
let feature = {};
if (e.name && e.name.includes("Feature:")) {
feature = JSON.parse(JSON.stringify(e));
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MiscUtil.copy( ... ) is available instead of JSON.parse(JSON.stringify( ... )), fwiw

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched code to use MiscUtil.copy()

feature.name = feature.name.replace("Feature:", "").trim();
}
} else if (e.name && (
e.name.includes("Guild Spells") || // covers all GGR backgrounds
e.name.includes("Origins") // Criminal, Folk Hero, Hermit, Outlander, etc
)) {
feature = JSON.parse(JSON.stringify(e));
} else if (e.name && featureSet.includes(e.name)) {
feature = JSON.parse(JSON.stringify(e));
} else return;

const renderStack = [];
renderer.recursiveRender({entries: feature.entries}, renderStack);
feature.text = renderStack.length ? d20plus.importer.getCleanText(renderStack.join("")) : "";
features.push(feature);
});
if (feature) renderer.recursiveRender({entries: feature.entries}, renderStack);
feature.text = renderStack.length ? d20plus.importer.getCleanText(renderStack.join("")) : "";

// Add skills

Expand Down Expand Up @@ -424,34 +459,96 @@ function d20plusBackgrounds () {
let ideal = null;
let bond = null;
let flaw = null;
const matchCharacteristics = [
"Suggested Characteristics", // Most backgrounds
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this I'm fine with, but it could maybe be a slightly-more-accepting regex /\bcharacteristics\b/i.test(ent.name) to catch any other minor variations. I doubt backgrounds should have other headers with "Characteristics" in them

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a little research, seems like includes() is a bit faster, so I used that instead of the regex.

"Horror Characteristics" // 'Haunted One' and 'Investigator'
];
// Get the JSON for all the tables
if (bg.entries) {
for (const ent of bg.entries) {
if (ent.name && ent.name === "Suggested Characteristics") {
if (ent.name && matchCharacteristics.includes(ent.name)) {
traits = ent;
} else if (ent.entries) {
for (const entItem of ent.entries) {
// look for embedded characteristics
if (entItem.name && entItem.name === "Suggested Characteristics") {
traits = entItem;
// look for embedded trinkets, and move to features
} else if (entItem.name && entItem.name.includes("Trinket")) {
const renderStack = [];
renderer.recursiveRender({entries: entItem.entries}, renderStack);
entItem.text = renderStack.length ? d20plus.importer.getCleanText(renderStack.join("")) : "";
features.push(entItem);
}
}
}
}
}

// Fill the rows
if (traits !== null && traits.entries?.length) {
for (let i = 0; i < traits.entries.length; i++) {
ent = traits.entries[i];
const ent = traits.entries[i];
// This seems to be the best way to parse the information with some room for errors
// It seems like the schema is based on on the website, which is why colLabels is where the identifier is
if (ent.colLabels && ent.colLabels.length === 2 && ent.rows) {

/**
* Clean up the formatting and reference syntaxes to get clean text.
*
* @param entry entry to clean up
* @return cleaned text
*/
const _cleanText = function (entry) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use Renderer.stripTags( ... ) instead

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Knew something like that had to exist, but couldn't find it. Thanks!

let cleanedText = "";
let closeIndex = 0;
while (true) {
const firstIndex = entry.indexOf("{@", closeIndex);
if (firstIndex === -1) {
// grab whatever is left in the entry
cleanedText += entry.substring(closeIndex);
break;
}
cleanedText += entry.substring(closeIndex, firstIndex);
const spaceIndex = entry.indexOf(" ", firstIndex);
if (spaceIndex === -1) {
// parse error, just grab the rest and bail
cleanedText += entry.substring(firstIndex);
break;
}
closeIndex = entry.indexOf("}", spaceIndex);
if (closeIndex === -1) {
// parse error, just grab the rest and bail
cleanedText += entry.substring(firstIndex);
break;
}
// look for any reference syntax between the space and the closure
const pipeIndex = entry.substring(spaceIndex + 1, closeIndex).indexOf("|");
if (pipeIndex === -1) {
// no reference, copy one past space up to closure
cleanedText += entry.substring(spaceIndex + 1, closeIndex);
} else {
// got a reference, copy one past space for number of characters up to pipe
// note that pipeIndex is zero-based
cleanedText += entry.substr(spaceIndex + 1, pipeIndex);
}
closeIndex++;
}
return cleanedText;
};

switch (ent.colLabels[1]) {
case "Personality Trait":
ptrait = ent.rows.map(r => r[1]);
ptrait = ent.rows.map(r => _cleanText(r[1]));
break;
case "Ideal":
ideal = ent.rows.map(r => r[1]);
ideal = ent.rows.map(r => _cleanText(r[1]));
break;
case "Bond":
bond = ent.rows.map(r => r[1]);
bond = ent.rows.map(r => _cleanText(r[1]));
break;
case "Flaw":
flaw = ent.rows.map(r => r[1]);
flaw = ent.rows.map(r => _cleanText(r[1]));
break;
}
}
Expand All @@ -464,18 +561,20 @@ function d20plusBackgrounds () {

// Update Sheet
const attrs = new d20plus.importer.CharacterAttributesProxy(character);
const fRowId = d20plus.ut.generateRowId();

if (d20plus.sheet === "ogl") {
attrs.addOrUpdate("background", bg.name);
attrs.addOrUpdate("gp", startingGold);

attrs.add(`repeating_traits_${fRowId}_name`, feature.name);
attrs.add(`repeating_traits_${fRowId}_source`, "Background");
attrs.add(`repeating_traits_${fRowId}_source_type`, bg.name);
attrs.add(`repeating_traits_${fRowId}_options-flag`, "0");
if (feature.text) {
attrs.add(`repeating_traits_${fRowId}_description`, feature.text);
for (const feature of features) {
const fRowId = d20plus.ut.generateRowId();
attrs.add(`repeating_traits_${fRowId}_name`, feature.name);
attrs.add(`repeating_traits_${fRowId}_source`, "Background");
attrs.add(`repeating_traits_${fRowId}_source_type`, bg.name);
attrs.add(`repeating_traits_${fRowId}_options-flag`, "0");
if (feature.text) {
attrs.add(`repeating_traits_${fRowId}_description`, feature.text);
}
}

skills.map(s => s.toLowerCase().replace(/ /g, "_")).forEach(s => {
Expand Down Expand Up @@ -512,10 +611,13 @@ function d20plusBackgrounds () {
if (flaws?.length === 1) attrs.addOrUpdate(`flaws`, flaws[0]);
} else if (d20plus.sheet === "shaped") {
attrs.addOrUpdate("background", bg.name);
attrs.add(`repeating_trait_${fRowId}_name`, `${feature.name} (${bg.name})`);
if (feature.text) {
attrs.add(`repeating_trait_${fRowId}_content`, feature.text);
attrs.add(`repeating_trait_${fRowId}_content_toggle`, "1");
for (const feature of features) {
const fRowId = d20plus.ut.generateRowId();
attrs.add(`repeating_trait_${fRowId}_name`, `${feature.name} (${bg.name})`);
if (feature.text) {
attrs.add(`repeating_trait_${fRowId}_content`, feature.text);
attrs.add(`repeating_trait_${fRowId}_content_toggle`, "1");
}
}

skills.map(s => s.toUpperCase().replace(/ /g, "")).forEach(s => {
Expand Down
22 changes: 20 additions & 2 deletions js/base-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,10 @@ function baseUi () {
* @param random show button for random choices
* @param randomMax Enforce max random choices
* @param totallyRandom select randomly number of items between countMin and countMax. Requires count to be null. This has higher priority than randomMax
* @param skip show skip button to allow no choices
* @return {Promise}
*/
d20plus.ui.chooseCheckboxList = async function (dataArray, dataTitle, {displayFormatter = null, count = null, countMin = null, countMax = null, additionalHTML = null, note = null, messageCountIncomplete = null , random = null, randomMax = null, totallyRandom = null} = {}) {
d20plus.ui.chooseCheckboxList = async function (dataArray, dataTitle, {displayFormatter = null, count = null, countMin = null, countMax = null, additionalHTML = null, note = null, messageCountIncomplete = null , random = null, randomMax = null, totallyRandom = null, skip = null} = {}) {
return new Promise((resolve, reject) => {
// Ensure count, countMin, and countMax don't mess up
// Note if(var) is false if the number is 0. countMin is the only count allowed to be 0
Expand Down Expand Up @@ -196,6 +197,14 @@ function baseUi () {
$dialog.dialog({
dialogClass: "no-close",
buttons: [
(skip ? {
text: "Skip",
click: function () {
$(this).dialog("close");
$dialog.remove();
resolve(null);
},
} : null),
{
text: "Cancel",
click: function () {
Expand Down Expand Up @@ -273,9 +282,10 @@ function baseUi () {
* @param additionalHTML additional html code, such as a button
* @param note add a note at the bottom of the window
* @param messageCountIncomplete message when user does not choose correct number of choices
* @param skip show skip button to allow no choices
* @return {Promise}
*/
d20plus.ui.chooseRadioList = async function (dataArray, dataTitle, {displayFormatter = null, random = null, additionalHTML = null, note = null, messageCountIncomplete = null} = {}) {
d20plus.ui.chooseRadioList = async function (dataArray, dataTitle, {displayFormatter = null, random = null, additionalHTML = null, note = null, messageCountIncomplete = null, skip = null} = {}) {
return new Promise((resolve, reject) => {


Expand All @@ -302,6 +312,14 @@ function baseUi () {
$dialog.dialog({
dialogClass: "no-close",
buttons: [
(skip ? {
text: "Skip",
click: function () {
$(this).dialog("close");
$dialog.remove();
resolve(null);
},
} : null),
{
text: "Cancel",
click: function () {
Expand Down