Update Simple Worldbuilding for Foundry 0.8.x Compatibility

This commit is contained in:
Andrew 2021-05-18 17:36:07 -04:00
parent ee8814b76a
commit fb0add4cbc
11 changed files with 295 additions and 301 deletions

View file

@ -4,8 +4,13 @@
"SETTINGS.SimpleInitFormulaN": "Initiative Formula",
"SETTINGS.SimpleInitFormulaL": "Enter an initiative formula, such as d20+@dex",
"SIMPLE.NotifyInitFormulaUpdated": "Initiative formula was updated to:",
"SIMPLE.NotifyInitFormulaInvalid": "Initiative formula was invalid:",
"SIMPLE.ItemCreate": "Create Item",
"SIMPLE.ItemEdit": "Edit Item",
"SIMPLE.ItemDelete": "Delete Item",
"SIMPLE.ItemNew": "New Item",
"SIMPLE.NotifyInitFormulaUpdated": "Initiative formula was updated to",
"SIMPLE.NotifyInitFormulaInvalid": "Initiative formula was invalid",
"SIMPLE.NotifyGroupDuplicate": "Attribute group already exists.",
"SIMPLE.NotifyGroupAttrDuplicate": "Attribute group already exists as an attribute.",
"SIMPLE.NotifyGroupAlphanumeric": "Attribute group names may not contain spaces or periods.",

View file

@ -1,4 +1,5 @@
import { EntitySheetHelper } from "./helper.js";
import {ATTRIBUTE_TYPES} from "./constants.js";
/**
* Extend the basic ActorSheet with some very simple modifications
@ -6,9 +7,9 @@ import { EntitySheetHelper } from "./helper.js";
*/
export class SimpleActorSheet extends ActorSheet {
/** @override */
/** @inheritdoc */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["worldbuilding", "sheet", "actor"],
template: "systems/worldbuilding/templates/actor-sheet.html",
width: 600,
@ -21,42 +22,35 @@ export class SimpleActorSheet extends ActorSheet {
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
getData() {
const data = super.getData();
EntitySheetHelper.getAttributeData(data);
data.shorthand = !!game.settings.get("worldbuilding", "macroShorthand");
return data;
const context = super.getData();
EntitySheetHelper.getAttributeData(context.data);
context.shorthand = !!game.settings.get("worldbuilding", "macroShorthand");
context.systemData = context.data.data;
context.dtypes = ATTRIBUTE_TYPES;
return context;
}
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
activateListeners(html) {
super.activateListeners(html);
// Everything below here is only needed if the sheet is editable
if ( !this.options.editable ) return;
if ( !this.isEditable ) return;
// Handle rollable items and attributes
html.find(".items .rollable").on("click", this._onItemRoll.bind(this));
// Attribute Management
html.find(".attributes").on("click", ".attribute-control", EntitySheetHelper.onClickAttributeControl.bind(this));
html.find(".groups").on("click", ".group-control", EntitySheetHelper.onClickAttributeGroupControl.bind(this));
html.find(".attributes").on("click", "a.attribute-roll", EntitySheetHelper.onAttributeRoll.bind(this));
// Update Inventory Item
html.find('.item-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item");
const item = this.actor.getOwnedItem(li.data("itemId"));
item.sheet.render(true);
});
// Item Controls
html.find(".item-control").click(this._onItemControl.bind(this));
html.find(".items .rollable").on("click", this._onItemRoll.bind(this));
// Delete Inventory Item
html.find('.item-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item");
this.actor.deleteOwnedItem(li.data("itemId"));
li.slideUp(200, () => this.render(false));
});
// Add draggable for macros.
// Add draggable for Macro creation
html.find(".attributes a.attribute-roll").each((i, a) => {
a.setAttribute("draggable", true);
a.addEventListener("dragstart", ev => {
@ -64,12 +58,33 @@ export class SimpleActorSheet extends ActorSheet {
ev.dataTransfer.setData('text/plain', JSON.stringify(dragData));
}, false);
});
}
// Add or Remove Attribute
html.find(".attributes").on("click", ".attribute-control", EntitySheetHelper.onClickAttributeControl.bind(this));
/* -------------------------------------------- */
// Add attribute groups.
html.find(".groups").on("click", ".group-control", EntitySheetHelper.onClickAttributeGroupControl.bind(this));
/**
* Handle click events for Item control buttons within the Actor Sheet
* @param event
* @private
*/
_onItemControl(event) {
event.preventDefault();
// Obtain event data
const button = event.currentTarget;
const li = button.closest(".item");
const item = this.actor.items.get(li?.dataset.itemId);
// Handle different actions
switch ( button.dataset.action ) {
case "create":
const cls = getDocumentClass("Item");
return cls.create({name: game.i18n.localize("SIMPLE.ItemNew"), type: "item"}, {parent: this.actor});
case "edit":
return item.sheet.render(true);
case "delete":
return item.delete();
}
}
/* -------------------------------------------- */
@ -80,11 +95,11 @@ export class SimpleActorSheet extends ActorSheet {
*/
_onItemRoll(event) {
let button = $(event.currentTarget);
let r = new Roll(button.data('roll'), this.actor.getRollData());
const li = button.parents(".item");
const item = this.actor.getOwnedItem(li.data("itemId"));
r.roll().toMessage({
user: game.user._id,
const item = this.actor.items.get(li.data("itemId"));
let r = new Roll(button.data('roll'), this.actor.getRollData());
return r.toMessage({
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor: `<h2>${item.name}</h2><h3>${button.text()}</h3>`
});
@ -92,11 +107,11 @@ export class SimpleActorSheet extends ActorSheet {
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
formData = EntitySheetHelper.updateAttributes(formData, this);
formData = EntitySheetHelper.updateGroups(formData, this);
return this.object.update(formData);
/** @inheritdoc */
_getSubmitData(updateData) {
let formData = super._getSubmitData(updateData);
formData = EntitySheetHelper.updateAttributes(formData, this.object);
formData = EntitySheetHelper.updateGroups(formData, this.object);
return formData;
}
}

View file

@ -1,14 +1,14 @@
import { EntitySheetHelper } from "./helper.js";
/**
* Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system.
* Extend the base Actor document to support attributes and groups with a custom template creation dialog.
* @extends {Actor}
*/
export class SimpleActor extends Actor {
/** @override */
prepareData() {
super.prepareData();
/** @inheritdoc */
prepareDerivedData() {
super.prepareDerivedData();
this.data.data.groups = this.data.data.groups || {};
this.data.data.attributes = this.data.data.attributes || {};
}
@ -16,8 +16,19 @@ export class SimpleActor extends Actor {
/* -------------------------------------------- */
/** @override */
static async createDialog(data={}, options={}) {
return EntitySheetHelper.createDialog.call(this, data, options);
}
/* -------------------------------------------- */
/* Roll Data Preparation */
/* -------------------------------------------- */
/** @inheritdoc */
getRollData() {
const data = super.getRollData();
// Copy the actor's system data
const data = this.toObject(false).data;
const shorthand = game.settings.get("worldbuilding", "macroShorthand");
const formulaAttributes = [];
const itemAttributes = [];
@ -41,10 +52,11 @@ export class SimpleActor extends Actor {
delete data.abil;
delete data.groups;
}
return data;
}
/* -------------------------------------------- */
/**
* Apply shorthand syntax to actor roll data.
* @param {Object} data The actor's data object.
@ -53,9 +65,9 @@ export class SimpleActor extends Actor {
*/
_applyShorthand(data, formulaAttributes, shorthand) {
// Handle formula attributes when the short syntax is disabled.
for ( let [k, v] of Object.entries(data.attributes) ) {
for ( let [k, v] of Object.entries(data.attributes || {}) ) {
// Make an array of formula attributes for later reference.
if ( v.dtype == "Formula" ) formulaAttributes.push(k);
if ( v.dtype === "Formula" ) formulaAttributes.push(k);
// Add shortened version of the attributes.
if ( !!shorthand ) {
if ( !(k in data) ) {
@ -68,7 +80,7 @@ export class SimpleActor extends Actor {
data[k] = {};
for ( let [gk, gv] of Object.entries(v) ) {
data[k][gk] = gv.value;
if ( gv.dtype == "Formula" ) formulaAttributes.push(`${k}.${gk}`);
if ( gv.dtype === "Formula" ) formulaAttributes.push(`${k}.${gk}`);
}
}
}
@ -76,22 +88,25 @@ export class SimpleActor extends Actor {
}
}
/* -------------------------------------------- */
/**
* Add items to the actor roll data object. Handles regular and shorthand
* syntax, and calculates derived formula attributes on the items.
* @param {Object} data The actor's data object.
* @param {string[]} itemAttributes
* @param {Boolean} shorthand Whether or not the shorthand syntax is used.
*/
_applyItems(data, itemAttributes, shorthand) {
// Map all items data using their slugified names
data.items = this.data.items.reduce((obj, i) => {
let key = i.name.slugify({strict: true});
let itemData = duplicate(i.data);
data.items = this.items.reduce((obj, item) => {
const key = item.name.slugify({strict: true});
const itemData = item.toObject(false).data;
// Add items to shorthand and note which ones are formula attributes.
for ( let [k, v] of Object.entries(itemData.attributes) ) {
// When building the attribute list, prepend the item name for later use.
if ( v.dtype == "Formula" ) itemAttributes.push(`${key}..${k}`);
if ( v.dtype === "Formula" ) itemAttributes.push(`${key}..${k}`);
// Add shortened version of the attributes.
if ( !!shorthand ) {
if ( !(k in itemData) ) {
@ -104,7 +119,7 @@ export class SimpleActor extends Actor {
if ( !itemData[k] ) itemData[k] = {};
for ( let [gk, gv] of Object.entries(v) ) {
itemData[k][gk] = gv.value;
if ( gv.dtype == "Formula" ) itemAttributes.push(`${key}..${k}.${gk}`);
if ( gv.dtype === "Formula" ) itemAttributes.push(`${key}..${k}.${gk}`);
}
}
}
@ -115,7 +130,7 @@ export class SimpleActor extends Actor {
if ( !itemData[k] ) itemData[k] = {};
for ( let [gk, gv] of Object.entries(v) ) {
itemData[k][gk] = gv.value;
if ( gv.dtype == "Formula" ) itemAttributes.push(`${key}..${k}.${gk}`);
if ( gv.dtype === "Formula" ) itemAttributes.push(`${key}..${k}.${gk}`);
}
}
}
@ -125,12 +140,13 @@ export class SimpleActor extends Actor {
if ( !!shorthand ) {
delete itemData.attributes;
}
obj[key] = itemData;
return obj;
}, {});
}
/* -------------------------------------------- */
_applyItemsFormulaReplacements(data, itemAttributes, shorthand) {
for ( let k of itemAttributes ) {
// Get the item name and separate the key.
@ -151,30 +167,32 @@ export class SimpleActor extends Actor {
if ( !!shorthand ) {
// Handle grouped attributes first.
if ( data.items[item][k][gk] ) {
formula = data.items[item][k][gk];
data.items[item][k][gk] = EntitySheetHelper.replaceData(formula.replace('@item.', `@items.${item}.`), data, {missing: "0"});
formula = data.items[item][k][gk].replace('@item.', `@items.${item}.`);
data.items[item][k][gk] = Roll.replaceFormulaData(formula, data);
}
// Handle non-grouped attributes.
else if ( data.items[item][k] ) {
formula = data.items[item][k];
data.items[item][k] = EntitySheetHelper.replaceData(formula.replace('@item.', `@items.${item}.`), data, {missing: "0"});
formula = data.items[item][k].replace('@item.', `@items.${item}.`);
data.items[item][k] = Roll.replaceFormulaData(formula, data);
}
}
else {
// Handle grouped attributes first.
if ( data.items[item]['attributes'][k][gk] ) {
formula = data.items[item]['attributes'][k][gk]['value'];
data.items[item]['attributes'][k][gk]['value'] = EntitySheetHelper.replaceData(formula.replace('@item.', `@items.${item}.attributes.`), data, {missing: "0"});
formula = data.items[item]['attributes'][k][gk]['value'].replace('@item.', `@items.${item}.attributes.`);
data.items[item]['attributes'][k][gk]['value'] = Roll.replaceFormulaData(formula, data);
}
// Handle non-grouped attributes.
else if ( data.items[item]['attributes'][k]['value'] ) {
formula = data.items[item]['attributes'][k]['value'];
data.items[item]['attributes'][k]['value'] = EntitySheetHelper.replaceData(formula.replace('@item.', `@items.${item}.attributes.`), data, {missing: "0"});
formula = data.items[item]['attributes'][k]['value'].replace('@item.', `@items.${item}.attributes.`);
data.items[item]['attributes'][k]['value'] = Roll.replaceFormulaData(formula, data);
}
}
}
}
/* -------------------------------------------- */
/**
* Apply replacements for derived formula attributes.
* @param {Object} data The actor's data object.
@ -182,11 +200,9 @@ export class SimpleActor extends Actor {
* @param {Boolean} shorthand Whether or not the shorthand syntax is used.
*/
_applyFormulaReplacements(data, formulaAttributes, shorthand) {
// Evaluate formula attributes after all other attributes have been handled,
// including items.
// Evaluate formula attributes after all other attributes have been handled, including items.
for ( let k of formulaAttributes ) {
// Grouped attributes are included as `group.attr`, so we need to split
// them into new keys.
// Grouped attributes are included as `group.attr`, so we need to split them into new keys.
let attr = null;
if ( k.includes('.') ) {
let attrKey = k.split('.');
@ -195,15 +211,11 @@ export class SimpleActor extends Actor {
}
// Non-grouped attributes.
if ( data.attributes[k]?.value ) {
data.attributes[k].value = EntitySheetHelper.replaceData(data.attributes[k].value, data, {missing: "0"});
// TODO: Replace with:
// data.attributes[k].value = Roll.replaceFormulaData(data.attributes[k].value, data, {missing: "0"});
data.attributes[k].value = Roll.replaceFormulaData(data.attributes[k].value, data);
}
// Grouped attributes.
else {
if ( attr ) {
data.attributes[k][attr].value = EntitySheetHelper.replaceData(data.attributes[k][attr].value, data, {missing: "0"});
}
else if ( attr ) {
data.attributes[k][attr].value = Roll.replaceFormulaData(data.attributes[k][attr].value, data);
}
// Duplicate values to shorthand.

View file

@ -3,7 +3,6 @@ import { ATTRIBUTE_TYPES } from "./constants.js";
export class EntitySheetHelper {
static getAttributeData(data) {
data.dtypes = ATTRIBUTE_TYPES;
// Determine attribute type.
for ( let attr of Object.values(data.data.attributes) ) {
@ -61,7 +60,7 @@ export class EntitySheetHelper {
// Add label fallback.
if ( !gv.label ) gv.label = gk;
// Add formula bool.
if ( gv.dtype == "Formula" ) {
if ( gv.dtype === "Formula" ) {
gv.isFormula = true;
}
else {
@ -75,7 +74,7 @@ export class EntitySheetHelper {
// Add label fallback.
if ( !v.label ) v.label = k;
// Add formula bool.
if ( v.dtype == "Formula" ) {
if ( v.dtype === "Formula" ) {
v.isFormula = true;
}
else {
@ -94,7 +93,7 @@ export class EntitySheetHelper {
// Closing the form/sheet will also trigger a submit, so only evaluate if this is an event.
if ( event.currentTarget ) {
// Exit early if this isn't a named attribute.
if ( event.currentTarget.tagName.toLowerCase() == 'input' && !event.currentTarget.hasAttribute('name')) {
if ( (event.currentTarget.tagName.toLowerCase() === 'input') && !event.currentTarget.hasAttribute('name')) {
return false;
}
@ -108,7 +107,7 @@ export class EntitySheetHelper {
// Prevent attributes that already exist as groups.
let groups = document.querySelectorAll('.group-key');
for ( let i = 0; i < groups.length; i++ ) {
if (groups[i].value == val) {
if (groups[i].value === val) {
ui.notifications.error(game.i18n.localize("SIMPLE.NotifyAttrDuplicate") + ` (${val})`);
el.value = oldVal;
attrError = true;
@ -132,24 +131,21 @@ export class EntitySheetHelper {
/**
* Listen for click events on an attribute control to modify the composition of attributes in the sheet
* @param {MouseEvent} event The originating left click event
* @private
*/
static async onClickAttributeControl(event) {
event.preventDefault();
const a = event.currentTarget;
const action = a.dataset.action;
// Perform create and delete actions.
switch ( action ) {
case "create":
EntitySheetHelper.createAttribute(event, this);
break;
return EntitySheetHelper.createAttribute(event, this);
case "delete":
EntitySheetHelper.deleteAttribute(event, this);
break;
return EntitySheetHelper.deleteAttribute(event, this);
}
}
/* -------------------------------------------- */
/**
* Listen for click events and modify attribute groups.
* @param {MouseEvent} event The originating left click event
@ -158,14 +154,11 @@ export class EntitySheetHelper {
event.preventDefault();
const a = event.currentTarget;
const action = a.dataset.action;
switch ( action ) {
case "create-group":
EntitySheetHelper.createAttributeGroup(event, this);
break;
return EntitySheetHelper.createAttributeGroup(event, this);
case "delete-group":
EntitySheetHelper.deleteAttributeGroup(event, this);
break;
return EntitySheetHelper.deleteAttributeGroup(event, this);
}
}
@ -181,24 +174,24 @@ export class EntitySheetHelper {
const label = button.closest(".attribute").querySelector(".attribute-label")?.value;
const chatLabel = label ?? button.parentElement.querySelector(".attribute-key").value;
const shorthand = game.settings.get("worldbuilding", "macroShorthand");
// Use the actor for rollData so that formulas are always in reference to the parent actor.
const rollData = this.actor.getRollData();
let formula = button.closest(".attribute").querySelector(".attribute-value")?.value;
// If there's a formula, attempt to roll it.
if ( formula ) {
// Get the machine safe version of the item name.
let replacement = null;
if ( formula.includes('@item.') && this.item ) {
let itemName = this.item.name.slugify({strict: true});
let itemName = this.item.name.slugify({strict: true}); // Get the machine safe version of the item name.
replacement = !!shorthand ? `@items.${itemName}.` : `@items.${itemName}.attributes.`;
formula = formula.replace('@item.', replacement);
}
formula = EntitySheetHelper.replaceData(formula, rollData, {missing: null});
// Replace `@item` shorthand with the item name and make the roll.
// Create the roll and the corresponding message
let r = new Roll(formula, rollData);
r.roll().toMessage({
user: game.user._id,
return r.toMessage({
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor: `${chatLabel}`
});
@ -218,7 +211,7 @@ export class EntitySheetHelper {
*/
static getAttributeHtml(items, index, group = false) {
// Initialize the HTML.
let result = '<div>';
let result = '<div style="display: none;">';
// Iterate over the supplied keys and build their inputs (including whether or not they need a group key).
for (let [key, item] of Object.entries(items)) {
result = result + `<input type="${item.type}" name="data.attributes${group ? '.' + group : '' }.attr${index}.${key}" value="${item.value}"/>`;
@ -231,12 +224,13 @@ export class EntitySheetHelper {
/**
* Validate whether or not a group name can be used.
* @param {string} groupName Groupname to validate
* @param {string} groupName The candidate group name to validate
* @param {Document} document The Actor or Item instance within which the group is being defined
* @returns {boolean}
*/
static validateGroup(groupName, entity) {
let groups = Object.keys(entity.object.data.data.groups);
let attributes = Object.keys(entity.object.data.data.attributes).filter(a => !groups.includes(a));
static validateGroup(groupName, document) {
let groups = Object.keys(document.data.data.groups || {});
let attributes = Object.keys(document.data.data.attributes).filter(a => !groups.includes(a));
// Check for duplicate group keys.
if ( groups.includes(groupName) ) {
@ -255,7 +249,6 @@ export class EntitySheetHelper {
ui.notifications.error(game.i18n.localize("SIMPLE.NotifyGroupAlphanumeric"));
return false;
}
return true;
}
@ -283,7 +276,7 @@ export class EntitySheetHelper {
while ( objKeys.includes(newValue) ) {
++nk;
newValue = `attr${nk}`;
};
}
// Build options for construction HTML inputs.
let htmlItems = {
@ -364,7 +357,7 @@ export class EntitySheetHelper {
const form = app.form;
let newValue = $(a).siblings('.group-prefix').val();
// Verify the new group key is valid, and use it to create the group.
if ( newValue.length > 0 && EntitySheetHelper.validateGroup(newValue, app) ) {
if ( newValue.length > 0 && EntitySheetHelper.validateGroup(newValue, app.object) ) {
let newKey = document.createElement("div");
newKey.innerHTML = `<input type="text" name="data.groups.${newValue}.key" value="${newValue}"/>`;
// Append the form element and submit the form.
@ -374,6 +367,8 @@ export class EntitySheetHelper {
}
}
/* -------------------------------------------- */
/**
* Delete an attribute group.
* @param {MouseEvent} event The originating left click event
@ -410,15 +405,15 @@ export class EntitySheetHelper {
/**
* Update attributes when updating an actor object.
*
* @param {Object} formData Form data object to modify keys and values for.
* @returns {Object} updated formData object.
* @param {object} formData The form data object to modify keys and values for.
* @param {Document} document The Actor or Item document within which attributes are being updated
* @returns {object} The updated formData object.
*/
static updateAttributes(formData, entity) {
static updateAttributes(formData, document) {
let groupKeys = [];
// Handle the free-form attributes list
const formAttrs = expandObject(formData).data.attributes || {};
const formAttrs = foundry.utils.expandObject(formData)?.data?.attributes || {};
const attributes = Object.values(formAttrs).reduce((obj, v) => {
let attrs = [];
let group = null;
@ -453,14 +448,14 @@ export class EntitySheetHelper {
}, {});
// Remove attributes which are no longer used
for ( let k of Object.keys(entity.object.data.data.attributes) ) {
for ( let k of Object.keys(document.data.data.attributes) ) {
if ( !attributes.hasOwnProperty(k) ) attributes[`-=${k}`] = null;
}
// Remove grouped attributes which are no longer used.
for ( let group of groupKeys) {
if ( entity.object.data.data.attributes[group] ) {
for ( let k of Object.keys(entity.object.data.data.attributes[group]) ) {
if ( document.data.data.attributes[group] ) {
for ( let k of Object.keys(document.data.data.attributes[group]) ) {
if ( !attributes[group].hasOwnProperty(k) ) attributes[group][`-=${k}`] = null;
}
}
@ -470,18 +465,20 @@ export class EntitySheetHelper {
formData = Object.entries(formData).filter(e => !e[0].startsWith("data.attributes")).reduce((obj, e) => {
obj[e[0]] = e[1];
return obj;
}, {_id: entity.object._id, "data.attributes": attributes});
}, {_id: document.id, "data.attributes": attributes});
return formData;
}
/* -------------------------------------------- */
/**
* Update attribute groups when updating an actor object.
*
* @param {Object} formData Form data object to modify keys and values for.
* @returns {Object} updated formData object.
* @param {object} formData The form data object to modify keys and values for.
* @param {Document} document The Actor or Item document within which attributes are being updated
* @returns {object} The updated formData object.
*/
static updateGroups(formData, entity) {
static updateGroups(formData, document) {
// Handle the free-form groups list
const formGroups = expandObject(formData).data.groups || {};
const groups = Object.values(formGroups).reduce((obj, v) => {
@ -498,7 +495,7 @@ export class EntitySheetHelper {
}, {});
// Remove groups which are no longer used
for ( let k of Object.keys(entity.object.data.data.groups) ) {
for ( let k of Object.keys(document.data.data.groups) ) {
if ( !groups.hasOwnProperty(k) ) groups[`-=${k}`] = null;
}
@ -506,42 +503,71 @@ export class EntitySheetHelper {
formData = Object.entries(formData).filter(e => !e[0].startsWith("data.groups")).reduce((obj, e) => {
obj[e[0]] = e[1];
return obj;
}, {_id: entity.object._id, "data.groups": groups});
}, {_id: document.id, "data.groups": groups});
return formData;
}
/* -------------------------------------------- */
/**
* Replace referenced data attributes in the roll formula with the syntax `@attr` with the corresponding key from
* the provided `data` object. This is a temporary helper function that will be replaced with Roll.replaceFormulaData()
* in Foundry 0.7.1.
*
* @param {String} formula The original formula within which to replace.
* @param {Object} data Data object to use for value replacements.
* @param {Object} missing Value to use as missing replacements, such as {missing: "0"}.
* @return {String} The formula with attributes replaced with values.
* @see ClientDocumentMixin.createDialog
*/
static replaceData(formula, data, {missing=null,depth=1}={}) {
// Exit early if the formula is invalid.
if ( typeof formula != "string" || depth < 1) {
return 0;
static async createDialog(data={}, options={}) {
// Collect data
const documentName = this.metadata.name;
const folders = game.folders.filter(f => (f.data.type === documentName) && f.displayed);
const label = game.i18n.localize(this.metadata.label);
const title = game.i18n.format("ENTITY.Create", {entity: label});
// Identify the template Actor types
const collection = game.collections.get(this.documentName);
const templates = collection.filter(a => a.getFlag("worldbuilding", "isTemplate"));
const defaultType = this.metadata.types[0];
const types = {
[defaultType]: game.i18n.localize("SIMPLE.NoTemplate")
}
// Replace attributes with their numeric equivalents.
let dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi);
let rollFormula = formula.replace(dataRgx, (match, term) => {
let value = getProperty(data, term);
// If there was a value returned, trim and return it.
if ( value ) {
return String(value).trim();
}
// Otherwise, return either the missing replacement value, or the original @attr string for later replacement.
else {
return missing != null ? missing : `@${term}`;
}
});
return rollFormula;
for ( let a of templates ) {
types[a.id] = a.name;
}
// Render the entity creation form
const html = await renderTemplate(`templates/sidebar/entity-create.html`, {
name: data.name || game.i18n.format("ENTITY.New", {entity: label}),
folder: data.folder,
folders: folders,
hasFolders: folders.length > 1,
type: data.type || templates[0]?.id || "",
types: types,
hasTypes: true
});
// Render the confirmation dialog window
return Dialog.prompt({
title: title,
content: html,
label: title,
callback: html => {
// Get the form data
const form = html[0].querySelector("form");
const fd = new FormDataExtended(form);
let createData = fd.toObject();
// Merge with template data
const template = collection.get(form.type.value);
if ( template ) {
createData = foundry.utils.mergeObject(template.toObject(), createData);
createData.type = template.data.type;
delete createData.flags.worldbuilding.isTemplate;
}
// Merge provided override data
createData = foundry.utils.mergeObject(createData, data);
return this.create(createData, {renderSheet: true});
},
rejectClose: false,
options: options
});
}
}

View file

@ -1,4 +1,5 @@
import { EntitySheetHelper } from "./helper.js";
import {ATTRIBUTE_TYPES} from "./constants.js";
/**
* Extend the basic ItemSheet with some very simple modifications
@ -6,9 +7,9 @@ import { EntitySheetHelper } from "./helper.js";
*/
export class SimpleItemSheet extends ItemSheet {
/** @override */
/** @inheritdoc */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["worldbuilding", "sheet", "item"],
template: "systems/worldbuilding/templates/item-sheet.html",
width: 520,
@ -20,26 +21,30 @@ export class SimpleItemSheet extends ItemSheet {
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
getData() {
const data = super.getData();
EntitySheetHelper.getAttributeData(data);
return data;
const context = super.getData();
EntitySheetHelper.getAttributeData(context.data);
context.systemData = context.data.data;
context.dtypes = ATTRIBUTE_TYPES;
return context;
}
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
activateListeners(html) {
super.activateListeners(html);
// Everything below here is only needed if the sheet is editable
if (!this.options.editable) return;
if ( !this.isEditable ) return;
// Rollable attributes
// Attribute Management
html.find(".attributes").on("click", ".attribute-control", EntitySheetHelper.onClickAttributeControl.bind(this));
html.find(".groups").on("click", ".group-control", EntitySheetHelper.onClickAttributeGroupControl.bind(this));
html.find(".attributes").on("click", "a.attribute-roll", EntitySheetHelper.onAttributeRoll.bind(this));
// Add draggable for macros.
// Add draggable for Macro creation
html.find(".attributes a.attribute-roll").each((i, a) => {
a.setAttribute("draggable", true);
a.addEventListener("dragstart", ev => {
@ -47,24 +52,15 @@ export class SimpleItemSheet extends ItemSheet {
ev.dataTransfer.setData('text/plain', JSON.stringify(dragData));
}, false);
});
// Add or Remove Attribute
html.find(".attributes").on("click", ".attribute-control", EntitySheetHelper.onClickAttributeControl.bind(this));
// Add attribute groups.
html.find(".groups").on("click", ".group-control", EntitySheetHelper.onClickAttributeGroupControl.bind(this));
}
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
// Handle attribute and group updates.
formData = EntitySheetHelper.updateAttributes(formData, this);
formData = EntitySheetHelper.updateGroups(formData, this);
// Update the Actor with the new form values.
return this.object.update(formData);
_getSubmitData(updateData) {
let formData = super._getSubmitData(updateData);
formData = EntitySheetHelper.updateAttributes(formData, this.object);
formData = EntitySheetHelper.updateGroups(formData, this.object);
return formData;
}
}

22
module/item.js Normal file
View file

@ -0,0 +1,22 @@
import {EntitySheetHelper} from "./helper.js";
/**
* Extend the base Item document to support attributes and groups with a custom template creation dialog.
* @extends {Item}
*/
export class SimpleItem extends Item {
/** @inheritdoc */
prepareDerivedData() {
super.prepareDerivedData();
this.data.data.groups = this.data.data.groups || {};
this.data.data.attributes = this.data.data.attributes || {};
}
/* -------------------------------------------- */
/** @override */
static async createDialog(data={}, options={}) {
return EntitySheetHelper.createDialog.call(this, data, options);
}
}

View file

@ -5,6 +5,7 @@
// Import Modules
import { SimpleActor } from "./actor.js";
import { SimpleItem } from "./item.js";
import { SimpleItemSheet } from "./item-sheet.js";
import { SimpleActorSheet } from "./actor-sheet.js";
import { preloadHandlebarsTemplates } from "./templates.js";
@ -35,7 +36,8 @@ Hooks.once("init", async function() {
};
// Define custom Entity classes
CONFIG.Actor.entityClass = SimpleActor;
CONFIG.Actor.documentClass = SimpleActor;
CONFIG.Item.documentClass = SimpleItem;
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet);
@ -74,21 +76,12 @@ Hooks.once("init", async function() {
* @param {boolean} notify - Whether or not to post nofications.
*/
function _simpleUpdateInit(formula, notify = false) {
// If the formula is valid, use it.
try {
new Roll(formula).roll();
const isValid = Roll.validate(formula);
if ( !isValid ) {
if ( notify ) ui.notifications.error(`${game.i18n.localize("SIMPLE.NotifyInitFormulaInvalid")}: ${formula}`);
return;
}
CONFIG.Combat.initiative.formula = formula;
if (notify) {
ui.notifications.notify(game.i18n.localize("SIMPLE.NotifyInitFormulaUpdated") + ` ${formula}`);
}
}
// Otherwise, fall back to a d20.
catch (error) {
CONFIG.Combat.initiative.formula = "1d20";
if (notify) {
ui.notifications.error(game.i18n.localize("SIMPLE.NotifyInitFormulaInvalid") + ` ${formula}`);
}
}
}
/**
@ -98,8 +91,8 @@ Hooks.once("init", async function() {
return value.slugify({strict: true});
});
// Preload template partials.
preloadHandlebarsTemplates();
// Preload template partials
await preloadHandlebarsTemplates();
});
/**
@ -172,68 +165,3 @@ Hooks.on("getItemDirectoryEntryContext", (html, options) => {
}
});
});
async function _onCreateEntity(event) {
event.preventDefault();
event.stopPropagation();
return _simpleDirectoryTemplates(this, event);
}
ActorDirectory.prototype._onCreateEntity = _onCreateEntity; // For 0.7.x+
ItemDirectory.prototype._onCreateEntity = _onCreateEntity;
ActorDirectory.prototype._onCreate = _onCreateEntity; // TODO: for 0.6.6
ItemDirectory.prototype._onCreate = _onCreateEntity;
/**
* Display the entity template dialog.
*
* Helper function to display a dialog if there are multiple template types defined for the entity type.
* TODO: Refactor in 0.7.x to play more nicely with the Entity.createDialog method
*1
* @param {EntityCollection} entityType - The sidebar tab
* @param {MouseEvent} event - Triggering event
*/
async function _simpleDirectoryTemplates(collection, event) {
// Retrieve the collection and find any available templates
const entityCollection = collection.tabName === "actors" ? game.actors : game.items;
const cls = collection.tabName === "actors" ? Actor : Item;
let templates = entityCollection.filter(a => a.getFlag("worldbuilding", "isTemplate"));
let ent = game.i18n.localize(cls.config.label);
// Setup default creation data
let type = collection.tabName === "actors" ? 'character' : 'item';
let createData = {
name: `${game.i18n.localize("SIMPLE.New")} ${ent}`,
type: type,
folder: event.currentTarget.dataset.folder
};
if ( !templates.length ) return cls.create(createData, {renderSheet: true});
// Build an array of types for the form, including an empty default.
let types = [{
value: null,
label: game.i18n.localize("SIMPLE.NoTemplate")
}].concat(templates.map(a => { return { value: a.id, label: a.name } }));
// Render the confirmation dialog window
const templateData = {upper: ent, lower: ent.toLowerCase(), types: types};
const dlg = await renderTemplate(`systems/worldbuilding/templates/sidebar/entity-create.html`, templateData);
return Dialog.confirm({
title: `${game.i18n.localize("SIMPLE.Create")} ${createData.name}`,
content: dlg,
yes: html => {
const form = html[0].querySelector("form");
const template = entityCollection.get(form.type.value);
if ( template ) {
createData = mergeObject(template.data, createData, {inplace: false});
createData.type = template.data.type;
delete createData.flags.worldbuilding.isTemplate;
}
createData.name = form.name.value;
return cls.create(createData, {renderSheet: true});
},
no: () => {},
defaultYes: false
});
}

View file

@ -2,10 +2,9 @@
"name": "worldbuilding",
"title": "Simple World-Building",
"description": "A minimalist game system which provides configurable Actor and Item templates to support free-form system agnostic game-play.",
"version": 0.40,
"minimumCoreVersion": "0.6.6",
"compatibleCoreVersion": "0.7.3",
"templateVersion": 2,
"version": "0.5.0",
"minimumCoreVersion": "0.8.4",
"compatibleCoreVersion": "0.8.6",
"author": "Atropos",
"esmodules": ["module/simple.js"],
"styles": ["styles/simple.css"],
@ -22,7 +21,7 @@
"primaryTokenAttribute": "health",
"secondaryTokenAttribute": "power",
"url": "https://gitlab.com/foundrynet/worldbuilding/",
"manifest": "https://gitlab.com/foundrynet/worldbuilding/raw/master/system.json",
"download": "https://gitlab.com/foundrynet/worldbuilding/-/archive/release-040/worldbuilding-release-040.zip",
"manifest": "https://gitlab.com/foundrynet/worldbuilding/raw/0.5.x/system.json",
"download": "https://gitlab.com/foundrynet/worldbuilding/-/archive/release-050/worldbuilding-release-050.zip",
"license": "LICENSE.txt"
}

View file

@ -2,18 +2,20 @@
{{!-- Sheet Header --}}
<header class="sheet-header">
<img class="profile-img" src="{{actor.img}}" data-edit="img" title="{{actor.name}}" height="100" width="100" />
<img class="profile-img" src="{{data.img}}" data-edit="img" title="{{data.name}}" height="100" width="100" />
<div class="header-fields">
<h1 class="charname"><input name="name" type="text" value="{{actor.name}}" placeholder="Name" /></h1>
<h1 class="charname">
<input name="name" type="text" value="{{data.name}}" placeholder="Name" />
</h1>
<div class="resource">
<input type="text" name="data.health.value" value="{{data.health.value}}" data-dtype="Number" />
<input type="number" name="data.health.value" value="{{systemData.health.value}}"/>
<span> / </span>
<input type="text" name="data.health.max" value="{{data.health.max}}" data-dtype="Number" />
<input type="number" name="data.health.max" value="{{systemData.health.max}}"/>
</div>
<div class="resource">
<input type="text" name="data.power.value" value="{{data.power.value}}" data-dtype="Number" />
<input type="number" name="data.power.value" value="{{systemData.power.value}}"/>
<span> / </span>
<input type="text" name="data.power.max" value="{{data.power.max}}" data-dtype="Number" />
<input type="number" name="data.power.max" value="{{systemData.power.max}}"/>
</div>
</div>
</header>
@ -29,14 +31,14 @@
<section class="sheet-body">
{{!-- Biography Tab --}}
<div class="tab biography" data-group="primary" data-tab="description">
{{editor content=data.biography target="data.biography" button=true owner=owner editable=editable}}
<div class="tab description" data-group="primary" data-tab="description">
{{editor content=systemData.biography target="data.biography" button=true owner=owner editable=editable rollData=rollData}}
</div>
{{!-- Owned Items Tab --}}
<div class="tab items" data-group="primary" data-tab="items">
<ol class="item-list">
{{#each actor.items as |item id|}}
{{#each data.items as |item id|}}
<li class="item flexrow" data-item-id="{{item._id}}">
<img src="{{item.img}}" title="{{item.name}}" width="24" height="24" />
<h4 class="item-name">{{item.name}}</h4>
@ -75,12 +77,15 @@
{{/each}}
</div>
<div class="item-controls">
<a class="item-control item-edit" title="Edit Item"><i class="fas fa-edit"></i></a>
<a class="item-control item-delete" title="Delete Item"><i class="fas fa-trash"></i></a>
<a class="item-control" title="{{ localize "SIMPLE.ItemEdit" }}" data-action="edit"><i class="fas fa-edit"></i></a>
<a class="item-control" title="{{ localize "SIMPLE.ItemDelete" }}" data-action="delete"><i class="fas fa-trash"></i></a>
</div>
</li>
{{/each}}
</ol>
<p>
<a class="item-control" title="{{ localize "SIMPLE.ItemCreate" }}" data-action="create"><i class="fas fa-plus"></i> {{ localize "SIMPLE.ItemCreate" }}</a>
</p>
</div>
{{!-- Attributes Tab --}}
@ -94,14 +99,14 @@
</header>
{{!-- Render the attribute list partial. --}}
{{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.ungroupedAttributes dtypes=dtypes}}
{{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=systemData.ungroupedAttributes dtypes=dtypes}}
{{!-- Render the grouped attributes partial and control. --}}
<div class="groups">
{{> "systems/worldbuilding/templates/parts/sheet-groups.html" attributes=data.groupedAttributes groups=data.groups dtypes=dtypes}}
{{> "systems/worldbuilding/templates/parts/sheet-groups.html" attributes=systemData.groupedAttributes groups=systemData.groups dtypes=dtypes}}
<div class="group-controls flexrow">
<input class="group-prefix" type="text" val=""/>
<input class="group-prefix" type="text" value=""/>
<a class="button group-control" data-action="create-group"><i class="fas fa-plus"></i>Add Attribute Group</a>
</div>
</div>

View file

@ -1,15 +1,17 @@
<form class="flexcol {{cssClass}}" autocomplete="off">
<header class="sheet-header">
<img class="profile-img" src="{{item.img}}" data-edit="img" title="{{item.name}}" />
<img class="profile-img" src="{{data.img}}" data-edit="img" title="{{data.name}}" />
<div class="header-fields">
<h1 class="charname"><input name="name" type="text" value="{{item.name}}" placeholder="Name" /></h1>
<h1 class="charname">
<input name="name" type="text" value="{{data.name}}" placeholder="Name" />
</h1>
<div class="resource">
<label>Quantity</label>
<input type="text" name="data.quantity" value="{{data.quantity}}" data-dtype="Number" />
<input type="number" name="data.quantity" value="{{systemData.quantity}}"/>
</div>
<div class="resource">
<label>Weight</label>
<input type="text" name="data.weight" value="{{data.weight}}" data-dtype="Number" />
<input type="number" name="data.weight" value="{{systemData.weight}}"/>
</div>
</div>
</header>
@ -25,7 +27,7 @@
{{!-- Description Tab --}}
<div class="tab" data-group="primary" data-tab="description">
{{editor content=data.description target="data.description" button=true owner=owner editable=editable}}
{{editor content=systemData.description target="data.description" button=true owner=owner editable=editable rollData=rollData}}
</div>
{{!-- Attributes Tab --}}
@ -39,14 +41,13 @@
</header>
{{!-- Render the attribute list partial. --}}
{{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.ungroupedAttributes dtypes=dtypes}}
{{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=systemData.ungroupedAttributes dtypes=dtypes}}
{{!-- Render the grouped attributes partial and control. --}}
<div class="groups">
{{> "systems/worldbuilding/templates/parts/sheet-groups.html" attributes=data.groupedAttributes groups=data.groups dtypes=dtypes}}
{{> "systems/worldbuilding/templates/parts/sheet-groups.html" attributes=systemData.groupedAttributes groups=systemData.groups dtypes=dtypes}}
<div class="group-controls flexrow">
<input class="group-prefix" type="text" val=""/>
<input class="group-prefix" type="text" value=""/>
<a class="button group-control" data-action="create-group"><i class="fas fa-plus"></i>Add Attribute Group</a>
</div>
</div>

View file

@ -1,15 +0,0 @@
<form id="entity-create" autocomplete="off" onsubmit="event.preventDefault();">
<div class="form-group">
<label>{{localize "Name"}}</label>
<input type="text" name="name" placeholder="{{localize 'ENTITY.CreateNew'}} {{upper}}"/>
</div>
<div class="form-group">
<label>{{localize "Type"}}</label>
<select name="type">
{{#each types}}
<option value="{{this.value}}">{{this.label}}</option>
{{/each}}
</select>
</div>
</form>