diff --git a/lang/en.json b/lang/en.json
index f3cf520..e08325c 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -6,6 +6,8 @@
"SIMPLE.NotifyInitFormulaUpdated": "Initiative formula was updated to:",
"SIMPLE.NotifyInitFormulaInvalid": "Initiative formula was invalid:",
+ "SIMPLE.NotifyGroupDuplicate": "Attribute group already exists.",
+ "SIMPLE.NotifyGroupAlphanumeric": "Attribute group names may not contain spaces or periods.",
"SIMPLE.ResourceMin": "Min",
"SIMPLE.ResourceValue": "Value",
@@ -13,5 +15,16 @@
"SIMPLE.DefineTemplate": "Define as Template",
"SIMPLE.UnsetTemplate": "Unset Template",
- "SIMPLE.NoTemplate": "No Template"
+ "SIMPLE.NoTemplate": "No Template",
+
+ "SIMPLE.AttributeKey": "Attribute Key",
+ "SIMPLE.AttributeValue": "Value",
+ "SIMPLE.AttributeLabel": "Label",
+ "SIMPLE.AttributeDtype": "Data Type",
+
+ "SIMPLE.DeleteGroup": "Delete group?",
+ "SIMPLE.DeleteGroupContent": "Do you wish to delete this group? This will delete the following group and all attributes included in it: ",
+
+ "SIMPLE.Create": "Create",
+ "SIMPLE.New": "New"
}
\ No newline at end of file
diff --git a/module/actor-sheet.js b/module/actor-sheet.js
index 2a42379..831139f 100644
--- a/module/actor-sheet.js
+++ b/module/actor-sheet.js
@@ -7,13 +7,14 @@ import { ATTRIBUTE_TYPES } from "./constants.js";
export class SimpleActorSheet extends ActorSheet {
/** @override */
- static get defaultOptions() {
- return mergeObject(super.defaultOptions, {
- classes: ["worldbuilding", "sheet", "actor"],
- template: "systems/worldbuilding/templates/actor-sheet.html",
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ classes: ["worldbuilding", "sheet", "actor"],
+ template: "systems/worldbuilding/templates/actor-sheet.html",
width: 600,
height: 600,
tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}],
+ scrollY: [".biography", ".items", ".attributes"],
dragDrop: [{dragSelector: ".item-list .item", dropSelector: null}]
});
}
@@ -25,9 +26,46 @@ export class SimpleActorSheet extends ActorSheet {
const data = super.getData();
data.dtypes = ATTRIBUTE_TYPES;
for ( let attr of Object.values(data.data.attributes) ) {
- attr.isCheckbox = attr.dtype === "Boolean";
- attr.isResource = attr.dtype === "Resource";
+ if ( attr.dtype ) {
+ attr.isCheckbox = attr.dtype === "Boolean";
+ attr.isResource = attr.dtype === "Resource";
+ }
}
+
+ // Initialize ungrouped attributes for later.
+ data.data.ungroupedAttributes = {};
+
+ // Build an array of sorted group keys.
+ let groupKeys = Object.keys(data.data.groups).sort((a, b) => {
+ // Attempt to sort by the label, but fall back to the key.
+ let aSort = data.data.groups[a].label ? data.data.groups[a].label : a;
+ let bSort = data.data.groups[b].label ? data.data.groups[b].label : b;
+ return aSort.localeCompare(bSort);
+ });
+
+ // Iterate over the sorted groups to add their attributes..
+ groupKeys.forEach(key => {
+ // Retrieve the group.
+ let group = data.data.attributes[key];
+
+ // Initialize the attributes container for this group.
+ if ( !data.data.groups[key]['attributes'] ) data.data.groups[key]['attributes'] = {};
+
+ // Sort the attributes within the group, and then iterate over them.
+ Object.keys(group).sort((a, b) => a.localeCompare(b)).forEach(attr => {
+ // For each attribute, determine whether it's a checkbox or resource, and then add it to the group's attributes list.
+ group[attr]['isCheckbox'] = group[attr]['dtype'] === 'Boolean';
+ group[attr]['isResource'] = group[attr]['dtype'] === 'Resource';
+ data.data.groups[key]['attributes'][attr] = group[attr];
+ });
+ });
+
+ // Sort the remaining attributes attributes.
+ Object.keys(data.data.attributes).filter(a => !groupKeys.includes(a)).sort((a, b) => a.localeCompare(b)).forEach(key => {
+ data.data.ungroupedAttributes[key] = data.data.attributes[key];
+ });
+
+ // Add shorthand.
data.shorthand = !!game.settings.get("worldbuilding", "macroShorthand");
return data;
}
@@ -35,24 +73,17 @@ export class SimpleActorSheet extends ActorSheet {
/* -------------------------------------------- */
/** @override */
- activateListeners(html) {
+ activateListeners(html) {
super.activateListeners(html);
+ // Handle rollable items.
+ html.find(".items .rollable").on("click", this._onItemRoll.bind(this));
+
// Handle rollable attributes.
- html.find('.items .rollable').click(ev => {
- let button = $(ev.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,
- speaker: ChatMessage.getSpeaker({ actor: this.actor }),
- flavor: `
${item.name}
${button.text()}
`
- });
- });
+ html.find(".attributes").on("click", "a.attribute-roll", this._onAttributeRoll.bind(this));
// Everything below here is only needed if the sheet is editable
- if (!this.options.editable) return;
+ if ( !this.options.editable ) return;
// Update Inventory Item
html.find('.item-edit').click(ev => {
@@ -70,6 +101,51 @@ export class SimpleActorSheet extends ActorSheet {
// Add or Remove Attribute
html.find(".attributes").on("click", ".attribute-control", this._onClickAttributeControl.bind(this));
+
+ // Add attribute groups.
+ html.find(".groups").on("click", ".group-control", this._onClickAttributeGroupControl.bind(this));
+
+ // Add draggable for macros.
+ html.find(".attributes a.attribute-roll").each((i, a) => {
+ a.setAttribute("draggable", true);
+ a.addEventListener("dragstart", ev => {
+ let dragData = ev.currentTarget.dataset;
+ ev.dataTransfer.setData('text/plain', JSON.stringify(dragData));
+ }, false);
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _onSubmit(event, {updateData=null, preventClose=false, preventRender=false}={}) {
+ // Exit early if this isn't a named attribute.
+ if ( event.currentTarget ) {
+ if ( event.currentTarget.tagName.toLowerCase() == 'input' && !event.currentTarget.hasAttribute('name')) {
+ return;
+ }
+ }
+
+ let self = $(event.currentTarget);
+ let attr = null;
+
+ // If this is the attribute key, we need to make a note of it so that we can restore focus when its recreated.
+ if ( self.hasClass('attribute-key') ) {
+ let val = self.val();
+ let oldVal = self.parents('.attribute').data('attribute');
+ oldVal = oldVal.includes('.') ? oldVal.split('.')[1] : oldVal;
+ attr = self.attr('name').replace(oldVal, val);
+ }
+
+ // Submit the form.
+ await super._onSubmit(event, {updateData: updateData, preventClose: preventClose, preventRender: preventRender});
+
+ // If this was the attribute key, set a very short timeout and retrigger focus after the original element is deleted and the new one is inserted.
+ if ( attr ) {
+ setTimeout(() => {
+ $(`input[name="${attr}"]`).parents('.attribute').find('.attribute-value').focus();
+ }, 10);
+ }
}
/* -------------------------------------------- */
@@ -85,6 +161,44 @@ export class SimpleActorSheet extends ActorSheet {
/* -------------------------------------------- */
+ /**
+ * Listen for roll buttons on items.
+ * @param {MouseEvent} event The originating left click event
+ */
+ _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,
+ speaker: ChatMessage.getSpeaker({ actor: this.actor }),
+ flavor: `
${item.name}
${button.text()}
`
+ });
+ }
+
+ /**
+ * Listen for the roll button on attributes.
+ * @param {MouseEvent} event The originating left click event
+ */
+ _onAttributeRoll(event) {
+ event.preventDefault();
+ const button = event.currentTarget;
+ const formula = button.closest(".attribute").querySelector(".attribute-value")?.value;
+ const label = button.closest(".attribute").querySelector(".attribute-label")?.value;
+ const chatLabel = label ?? button.parentElement.querySelector(".attribute-key").value;
+
+ // If there's a formula, attempt to roll it.
+ if ( formula ) {
+ let r = new Roll(formula, this.actor.getRollData());
+ r.roll().toMessage({
+ user: game.user._id,
+ speaker: ChatMessage.getSpeaker({ actor: this.actor }),
+ flavor: `${chatLabel}`
+ });
+ }
+ }
+
/**
* 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
@@ -94,12 +208,16 @@ export class SimpleActorSheet extends ActorSheet {
event.preventDefault();
const a = event.currentTarget;
const action = a.dataset.action;
+ const group = a.dataset.group;
+ let dtype = a.dataset.dtype;
const attrs = this.object.data.data.attributes;
+ const groups = this.object.data.data.groups;
const form = this.form;
// Add new attribute
if ( action === "create" ) {
- const objKeys = Object.keys(attrs);
+ // Determine the new attribute key for ungrouped attributes.
+ let objKeys = Object.keys(attrs).filter(k => !Object.keys(groups).includes(k));
let nk = Object.keys(attrs).length + 1;
let newValue = `attr${nk}`;
let newKey = document.createElement("div");
@@ -107,7 +225,53 @@ export class SimpleActorSheet extends ActorSheet {
++nk;
newValue = `attr${nk}`;
};
- newKey.innerHTML = ``;
+
+ // Build options for construction HTML inputs.
+ let htmlItems = {
+ key: {
+ type: "text",
+ value: newValue
+ }
+ };
+
+ // Grouped attributes.
+ if ( group ) {
+ objKeys = attrs[group] ? Object.keys(attrs[group]) : [];
+ nk = objKeys.length + 1;
+ newValue = `attr${nk}`;
+ while ( objKeys.includes(newValue) ) {
+ ++nk;
+ newValue = `attr${nk}`;
+ }
+
+ // Update the HTML options used to build the new input.
+ htmlItems.key.value = newValue;
+ htmlItems.group = {
+ type: "hidden",
+ value: group
+ };
+ htmlItems.dtype = {
+ type: "hidden",
+ value: dtype
+ };
+ }
+ // Ungrouped attributes.
+ else {
+ // Choose a default dtype based on the last attribute, fall back to "String".
+ if (!dtype) {
+ let lastAttr = document.querySelector('.attributes > .attributes-group .attribute:last-child .attribute-dtype')?.value;
+ dtype = lastAttr ? lastAttr : "String";
+ htmlItems.dtype = {
+ type: "hidden",
+ value: dtype
+ };
+ }
+ }
+
+ // Build the form elements used to create the new grouped attribute.
+ newKey.innerHTML = this._getAttributeHtml(htmlItems, nk, group);
+
+ // Append the form element and submit the form.
newKey = newKey.children[0];
form.appendChild(newKey);
await this._onSubmit(event);
@@ -121,18 +285,157 @@ export class SimpleActorSheet extends ActorSheet {
}
}
+ /**
+ * Listen for click events and modify attribute groups.
+ * @param {MouseEvent} event The originating left click event
+ */
+ async _onClickAttributeGroupControl(event) {
+ event.preventDefault();
+ const a = event.currentTarget;
+ const action = a.dataset.action;
+ const form = this.form;
+
+ // Add new attribute group.
+ if ( action === "create-group" ) {
+ 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 && this._validateGroup(newValue) ) {
+ let newKey = document.createElement("div");
+ newKey.innerHTML = ``;
+ // Append the form element and submit the form.
+ newKey = newKey.children[0];
+ form.appendChild(newKey);
+ await this._onSubmit(event);
+ }
+
+ }
+
+ // Remove existing attribute
+ else if ( action === "delete-group" ) {
+ let groupHeader = a.closest(".group-header");
+ let group = $(groupHeader).find('.group-key');
+ // Create a dialog to confirm group deletion.
+ new Dialog({
+ title: game.i18n.localize("SIMPLE.DeleteGroup"),
+ content: `${game.i18n.localize("SIMPLE.DeleteGroupContent")} ${group.val()}`,
+ buttons: {
+ confirm: {
+ icon: '',
+ label: game.i18n.localize("Yes"),
+ callback: async () => {
+ groupHeader.parentElement.removeChild(groupHeader);
+ await this._onSubmit(event);
+ }
+ },
+ cancel: {
+ icon: '',
+ label: game.i18n.localize("No"),
+ }
+ }
+ }).render(true);
+ }
+ }
+
/* -------------------------------------------- */
+
+ /**
+ * Return HTML for a new attribute to be applied to the form for submission.
+ *
+ * @param {Object} items Keyed object where each item has a "type" and "value" property.
+ * @param {string} index Numeric index or key of the new attribute.
+ * @param {string|boolean} group String key of the group, or false.
+ *
+ * @returns {string} Html string.
+ */
+ _getAttributeHtml(items, index, group = false) {
+ // Initialize the HTML.
+ let result = '
';
+ // 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 + ``;
+ }
+ // Close the HTML and return.
+ return result + '
';
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Validate whether or not a group name can be used.
+ * @param {string} groupName Groupname to validate
+ * @returns {boolean}
+ */
+ _validateGroup(groupName) {
+ let groups = Object.keys(this.actor.data.data.groups);
+
+ // Check for duplicate group keys.
+ if ( groups.includes(groupName) ) {
+ ui.notifications.error(game.i18n.localize("SIMPLE.NotifyGroupDuplicate") + ` (${groupName})`);
+ return false;
+ }
+
+ // Check for whitespace or periods.
+ if ( groupName.match(/[\s|\.]/i) ) {
+ ui.notifications.error(game.i18n.localize("SIMPLE.NotifyGroupAlphanumeric"));
+ return false;
+ }
+
+ return true;
+ }
+
/** @override */
_updateObject(event, formData) {
+ // Handle attribute and group updates.
+ formData = this._updateAttributes(formData);
+ formData = this._updateGroups(formData);
+
+ // Update the Actor with the new form values.
+ return this.object.update(formData);
+ }
+
+ /**
+ * Update attributes when updating an actor object.
+ *
+ * @param {Object} formData Form data object to modify keys and values for.
+ * @returns {Object} updated formData object.
+ */
+ _updateAttributes(formData) {
+ let groupKeys = [];
+
// Handle the free-form attributes list
const formAttrs = expandObject(formData).data.attributes || {};
const attributes = Object.values(formAttrs).reduce((obj, v) => {
- let k = v["key"].trim();
- if ( /[\s\.]/.test(k) ) return ui.notifications.error("Attribute keys may not contain spaces or periods");
- delete v["key"];
- obj[k] = v;
+ let attrs = [];
+ let group = null;
+ // Handle attribute keys for grouped attributes.
+ if ( !v["key"] ) {
+ attrs = Object.keys(v);
+ attrs.forEach(attrKey => {
+ group = v[attrKey]['group'];
+ groupKeys.push(group);
+ let attr = v[attrKey];
+ let k = v[attrKey]["key"] ? v[attrKey]["key"].trim() : attrKey.trim();
+ if ( /[\s\.]/.test(k) ) return ui.notifications.error("Attribute keys may not contain spaces or periods");
+ delete attr["key"];
+ // Add the new attribute if it's grouped, but we need to build the nested structure first.
+ if ( !obj[group] ) {
+ obj[group] = {};
+ }
+ obj[group][k] = attr;
+ });
+ }
+ // Handle attribute keys for ungrouped attributes.
+ else {
+ let k = v["key"].trim();
+ if ( /[\s\.]/.test(k) ) return ui.notifications.error("Attribute keys may not contain spaces or periods");
+ delete v["key"];
+ // Add the new attribute only if it's ungrouped.
+ if ( !group ) {
+ obj[k] = v;
+ }
+ }
return obj;
}, {});
@@ -141,13 +444,57 @@ export class SimpleActorSheet extends ActorSheet {
if ( !attributes.hasOwnProperty(k) ) attributes[`-=${k}`] = null;
}
+ // Remove grouped attributes which are no longer used.
+ for ( let group of groupKeys) {
+ if ( this.object.data.data.attributes[group] ) {
+ for ( let k of Object.keys(this.object.data.data.attributes[group]) ) {
+ if ( !attributes[group].hasOwnProperty(k) ) attributes[group][`-=${k}`] = null;
+ }
+ }
+ }
+
// Re-combine formData
formData = Object.entries(formData).filter(e => !e[0].startsWith("data.attributes")).reduce((obj, e) => {
obj[e[0]] = e[1];
return obj;
}, {_id: this.object._id, "data.attributes": attributes});
- // Update the Actor
- return this.object.update(formData);
+ 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.
+ */
+ _updateGroups(formData) {
+ // Handle the free-form groups list
+ const formGroups = expandObject(formData).data.groups || {};
+ const groups = Object.values(formGroups).reduce((obj, v) => {
+ // If there are duplicate groups, collapse them.
+ if ( Array.isArray(v["key"]) ) {
+ v["key"] = v["key"][0];
+ }
+ // Trim and clean up.
+ let k = v["key"].trim();
+ if ( /[\s\.]/.test(k) ) return ui.notifications.error("Group keys may not contain spaces or periods");
+ delete v["key"];
+ obj[k] = v;
+ return obj;
+ }, {});
+
+ // Remove groups which are no longer used
+ for ( let k of Object.keys(this.object.data.data.groups) ) {
+ if ( !groups.hasOwnProperty(k) ) groups[`-=${k}`] = null;
+ }
+
+ // Re-combine formData
+ formData = Object.entries(formData).filter(e => !e[0].startsWith("data.groups")).reduce((obj, e) => {
+ obj[e[0]] = e[1];
+ return obj;
+ }, {_id: this.object._id, "data.groups": groups});
+
+ return formData;
}
}
diff --git a/module/actor.js b/module/actor.js
index 7621cca..abd86a4 100644
--- a/module/actor.js
+++ b/module/actor.js
@@ -25,6 +25,7 @@ export class SimpleActor extends Actor {
delete data.attributes;
delete data.attr;
delete data.abil;
+ delete data.groups;
}
return data;
@@ -44,7 +45,18 @@ export class SimpleActor extends Actor {
// Add shortened version of the attributes.
if ( !!shorthand ) {
if ( !(k in data) ) {
- data[k] = v.value;
+ // Non-grouped attributes.
+ if ( v.dtype ) {
+ data[k] = v.value;
+ }
+ // Grouped attributes.
+ else {
+ data[k] = {};
+ for ( let [attrKey, attrValue] of Object.entries(v) ) {
+ data[k][attrKey] = attrValue.value;
+ if ( attrValue.dtype == "Formula" ) formulaAttributes.push(`${k}.${attrKey}`);
+ }
+ }
}
}
}
@@ -110,15 +122,43 @@ export class SimpleActor extends Actor {
// 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.
+ let attr = null;
+ if ( k.includes('.') ) {
+ let attrKey = k.split('.');
+ k = attrKey[0];
+ attr = attrKey[1];
+ }
+ // Non-grouped attributes.
if ( data.attributes[k].value ) {
data.attributes[k].value = this._replaceData(data.attributes[k].value, data, {missing: "0"});
// TODO: Replace with:
// data.attributes[k].value = Roll.replaceFormulaData(data.attributes[k].value, data, {missing: "0"});
}
+ // Grouped attributes.
+ else {
+ if ( attr ) {
+ data.attributes[k][attr].value = this._replaceData(data.attributes[k][attr].value, data, {missing: "0"});
+ }
+ }
// Duplicate values to shorthand.
if ( !!shorthand ) {
- data[k] = data.attributes[k].value;
+ // Non-grouped attributes.
+ if ( data.attributes[k].value ) {
+ data[k] = data.attributes[k].value;
+ }
+ // Grouped attributes.
+ else {
+ if ( attr ) {
+ // Initialize a group key in case it doesn't exist.
+ if ( !data[k] ) {
+ data[k] = {};
+ }
+ data[k][attr] = data.attributes[k][attr].value;
+ }
+ }
}
}
}
diff --git a/module/macro.js b/module/macro.js
new file mode 100644
index 0000000..b194747
--- /dev/null
+++ b/module/macro.js
@@ -0,0 +1,47 @@
+/**
+ * Create a Macro from an attribute drop.
+ * Get an existing worldbuilding macro if one exists, otherwise create a new one.
+ * @param {Object} data The dropped data
+ * @param {number} slot The hotbar slot to use
+ * @returns {Promise}
+ */
+export async function createWorldbuildingMacro(data, slot) {
+ const item = data;
+
+ // Create the macro command
+ const command = `game.worldbuilding.rollAttrMacro("${item.label}", "${item.roll}");`;
+ let macro = game.macros.entities.find(m => (m.name === item.label) && (m.command === command));
+ if (!macro) {
+ macro = await Macro.create({
+ name: item.label,
+ type: "script",
+ command: command,
+ flags: { "worldbuilding.attrMacro": true }
+ });
+ }
+
+ game.user.assignHotbarMacro(macro, slot);
+ return false;
+}
+
+/**
+ * Create a Macro from an Item drop.
+ * Get an existing item macro if one exists, otherwise create a new one.
+ * @param {string} itemName
+ * @return {Promise}
+ */
+export function rollAttrMacro(attrName, attrFormula) {
+ let actor;
+ // Get the speaker and actor if not provided.
+ const speaker = ChatMessage.getSpeaker({ actor: this.actor });
+ if (speaker.token) actor = game.actors.tokens[speaker.token];
+ if (!actor) actor = game.actors.get(speaker.actor);
+
+ // Create the roll.
+ let r = new Roll(attrFormula, actor.getRollData());
+ r.roll().toMessage({
+ user: game.user._id,
+ speaker: speaker,
+ flavor: attrName
+ });
+}
\ No newline at end of file
diff --git a/module/simple.js b/module/simple.js
index e2a04df..4d047bb 100644
--- a/module/simple.js
+++ b/module/simple.js
@@ -9,11 +9,15 @@ import { SimpleActor } from "./actor.js";
import { SimpleItemSheet } from "./item-sheet.js";
import { SimpleActorSheet } from "./actor-sheet.js";
import { preloadHandlebarsTemplates } from "./templates.js";
+import { createWorldbuildingMacro, rollAttrMacro } from "./macro.js";
/* -------------------------------------------- */
/* Foundry VTT Initialization */
/* -------------------------------------------- */
+/**
+ * Init hook.
+ */
Hooks.once("init", async function() {
console.log(`Initializing Simple Worldbuilding System`);
@@ -26,6 +30,12 @@ Hooks.once("init", async function() {
decimals: 2
};
+ game.worldbuilding = {
+ SimpleActor,
+ createWorldbuildingMacro,
+ rollAttrMacro,
+ };
+
// Define custom Entity classes
CONFIG.Actor.entityClass = SimpleActor;
@@ -94,6 +104,11 @@ Hooks.once("init", async function() {
preloadHandlebarsTemplates();
});
+/**
+ * Macrobar hook.
+ */
+Hooks.on("hotbarDrop", (bar, data, slot) => createWorldbuildingMacro(data, slot));
+
/**
* Adds the actor template context menu.
*/
@@ -205,7 +220,7 @@ async function _simpleDirectoryTemplates(entityType = 'actor') {
// Setup entity data.
let type = entityType == 'actor' ? 'character' : 'item';
let createData = {
- name: `New ${ent}`,
+ name: `${game.i18n.localize("SIMPLE.New")} ${ent}`,
type: type,
folder: event.currentTarget.dataset.folder
};
@@ -231,40 +246,35 @@ async function _simpleDirectoryTemplates(entityType = 'actor') {
dlg = await renderTemplate(`systems/worldbuilding/templates/sidebar/entity-create.html`, templateData);
// Render the confirmation dialog window
- new Dialog({
- title: `Create ${createData.name}`,
+ Dialog.confirm({
+ title: `${game.i18n.localize("SIMPLE.Create")} ${createData.name}`,
content: dlg,
- buttons: {
- create: {
- icon: '',
- label: `Create ${ent}`,
- callback: html => {
- // Get the form data.
- const form = html[0].querySelector("form");
- mergeObject(createData, validateForm(form));
+ yes: html => {
+ // Get the form data.
+ const form = html[0].querySelector("form");
+ mergeObject(createData, validateForm(form));
- // Store the type and name values, and retrieve the template entity.
- let templateActor = entityCollection.getName(createData.type);
+ // Store the type and name values, and retrieve the template entity.
+ let templateActor = entityCollection.getName(createData.type);
- // If there's a template entity, handle the data.
- if (templateActor) {
- // Update the object with the existing template's values.
- createData = mergeObject(templateActor.data, createData, {inplace: false});
- createData.type = templateActor.data.type;
- // Clear the flag so that this doesn't become a new template.
- delete createData.flags.worldbuilding.isTemplate;
- }
- // Otherwise, restore to a valid entity type (character/item).
- else {
- createData.type = type;
- }
-
- cls.create(createData, {renderSheet: true});
- }
+ // If there's a template entity, handle the data.
+ if (templateActor) {
+ // Update the object with the existing template's values.
+ createData = mergeObject(templateActor.data, createData, {inplace: false});
+ createData.type = templateActor.data.type;
+ // Clear the flag so that this doesn't become a new template.
+ delete createData.flags.worldbuilding.isTemplate;
}
+ // Otherwise, restore to a valid entity type (character/item).
+ else {
+ createData.type = type;
+ }
+
+ cls.create(createData, {renderSheet: true});
},
- default: "create"
- }).render(true);
+ no: () => {},
+ defaultYes: false
+ });
}
// Otherwise, just create a blank entity.
else {
diff --git a/module/templates.js b/module/templates.js
index 840f4f0..0d84c42 100644
--- a/module/templates.js
+++ b/module/templates.js
@@ -8,7 +8,8 @@ export const preloadHandlebarsTemplates = async function() {
// Define template paths to load
const templatePaths = [
// Attribute list partial.
- "systems/worldbuilding/templates/parts/sheet-attributes.html"
+ "systems/worldbuilding/templates/parts/sheet-attributes.html",
+ "systems/worldbuilding/templates/parts/sheet-groups.html"
];
// Load the template parts
diff --git a/styles/simple.css b/styles/simple.css
index b6d6d9a..13bc5a0 100644
--- a/styles/simple.css
+++ b/styles/simple.css
@@ -109,10 +109,51 @@
flex: none;
width: auto;
}
+.worldbuilding .attributes {
+ position: relative;
+}
+.worldbuilding .attributes .attribute-control,
+.worldbuilding .attributes .group-control {
+ flex: 0 0 20px;
+ text-align: center;
+ line-height: 28px;
+ border: none;
+}
+.worldbuilding .attributes .attribute-roll {
+ flex: 0 0 20px;
+ text-align: center;
+ border-bottom: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.worldbuilding .attributes .group-prefix {
+ height: 31px;
+ margin-right: 10px;
+}
+.worldbuilding .attributes .button {
+ width: 100%;
+ flex: 1;
+ margin: 0;
+ background: rgba(0, 0, 0, 0.1);
+ border: 2px groove #f0f0e0;
+ border-radius: 3px;
+ font-size: 14px;
+ line-height: 28px;
+ display: block;
+}
+.worldbuilding .attributes .attribute-key {
+ background: transparent;
+ border: none;
+}
.worldbuilding .attributes-header {
+ position: sticky;
+ top: 0;
+ left: 0;
+ right: 0;
padding: 5px;
margin: 5px 0;
- background: rgba(0, 0, 0, 0.05);
+ background: #cfcdc2;
border: 1px solid #AAA;
border-radius: 2px;
text-align: center;
@@ -138,12 +179,6 @@
border-radius: 0;
border-bottom: 1px solid #AAA;
}
-.worldbuilding .attributes-list a.attribute-control {
- flex: 0 0 20px;
- text-align: center;
- line-height: 28px;
- border: none;
-}
.worldbuilding .attributes-list .attribute-col label {
font-size: 10px;
text-align: center;
@@ -155,6 +190,47 @@
line-height: 1;
height: 50%;
}
+.worldbuilding .attribute input[type="text"]::placeholder,
+.worldbuilding .group-header input[type="text"]::placeholder {
+ opacity: 0;
+ transition: opacity 0.25s ease-in-out;
+}
+.worldbuilding .attribute input[type="text"]:focus::placeholder,
+.worldbuilding .group-header input[type="text"]:focus::placeholder {
+ opacity: 1;
+}
+.worldbuilding .groups-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+.worldbuilding .group {
+ margin: 20px 0;
+ padding: 0;
+}
+.worldbuilding .group-header {
+ background: rgba(0, 0, 0, 0.05);
+ border: 1px solid #AAA;
+ border-radius: 2px;
+ padding: 5px;
+}
+.worldbuilding .group-key,
+.worldbuilding .group-label {
+ font-weight: bold;
+ border: none;
+ border-bottom: 1px solid #AAA;
+ background: transparent;
+ border-radius: 0;
+ margin-right: 6px;
+}
+.worldbuilding .group-key {
+ flex: 0 0 126px;
+ opacity: 0.75;
+}
+.worldbuilding .group-dtype {
+ margin: 0 5px;
+ flex: 0;
+}
.worldbuilding.sheet.actor {
min-width: 560px;
min-height: 420px;
diff --git a/styles/simple.less b/styles/simple.less
index 06402e1..592bb8d 100644
--- a/styles/simple.less
+++ b/styles/simple.less
@@ -125,10 +125,57 @@
}
/* Attributes */
+ .attributes {
+ position: relative;
+
+ .attribute-control,
+ .group-control {
+ flex: 0 0 20px;
+ text-align: center;
+ line-height: 28px;
+ border: none;
+ }
+
+ .attribute-roll {
+ flex: 0 0 20px;
+ text-align: center;
+ border-bottom: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .group-prefix {
+ height: 31px;
+ margin-right: 10px;
+ }
+
+ .button {
+ width: 100%;
+ flex: 1;
+ margin: 0;
+ background: rgba(0, 0, 0, 0.1);
+ border: 2px groove #f0f0e0;
+ border-radius: 3px;
+ font-size: 14px;
+ line-height: 28px;
+ display: block;
+ }
+
+ .attribute-key {
+ background: transparent;
+ border: none;
+ }
+ }
+
.attributes-header {
+ position: sticky;
+ top: 0;
+ left: 0;
+ right: 0;
padding: 5px;
margin: 5px 0;
- background: rgba(0, 0, 0, 0.05);
+ background: #cfcdc2;
border: 1px solid #AAA;
border-radius: 2px;
text-align: center;
@@ -158,13 +205,6 @@
border-bottom: 1px solid #AAA;
}
- a.attribute-control {
- flex: 0 0 20px;
- text-align: center;
- line-height: 28px;
- border: none;
- }
-
.attribute-col {
label {
font-size: 10px;
@@ -180,6 +220,58 @@
}
}
}
+
+ .attribute,
+ .group-header {
+ input[type="text"] {
+ &::placeholder {
+ opacity: 0;
+ transition: opacity 0.25s ease-in-out;
+ }
+
+ &:focus::placeholder {
+ opacity: 1;
+ }
+ }
+ }
+
+ .groups-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ .group {
+ margin: 20px 0;
+ padding: 0;
+ }
+
+ .group-header {
+ background: rgba(0, 0, 0, 0.05);
+ border: 1px solid #AAA;
+ border-radius: 2px;
+ padding: 5px;
+ }
+
+ .group-key,
+ .group-label {
+ font-weight: bold;
+ border: none;
+ border-bottom: 1px solid #AAA;
+ background: transparent;
+ border-radius: 0;
+ margin-right: 6px;
+ }
+
+ .group-key {
+ flex: 0 0 126px;
+ opacity: 0.75;
+ }
+
+ .group-dtype {
+ margin: 0 5px;
+ flex: 0;
+ }
}
.worldbuilding.sheet.actor {
diff --git a/template.json b/template.json
index 4e108a6..dec71a7 100644
--- a/template.json
+++ b/template.json
@@ -13,7 +13,8 @@
"min": 0,
"max": 5
},
- "attributes": {}
+ "attributes": {},
+ "groups": {}
}
},
"Item": {
@@ -22,7 +23,8 @@
"description": "",
"quantity": 1,
"weight": 0,
- "attributes": {}
+ "attributes": {},
+ "groups": {}
}
}
}
diff --git a/templates/actor-sheet.html b/templates/actor-sheet.html
index 51656d8..f953b5d 100644
--- a/templates/actor-sheet.html
+++ b/templates/actor-sheet.html
@@ -2,18 +2,18 @@
{{!-- Sheet Header --}}
-
+
-
+
-
+
/
-
+
-
+
/
-
+
@@ -36,46 +36,58 @@
{{!-- Owned Items Tab --}}
- {{#each actor.items as |item id|}}
+ {{#each actor.items as |item id|}}
-
+
{{item.name}}
{{!-- Iterate through all attributes on the item and output buttons for any that are formula. --}}
- {{#each item.data.attributes as |itemAttr key|}}
+ {{#each item.data.attributes as |itemAttr key|}}
{{#if (eq itemAttr.dtype "Formula")}}
- {{!-- Use the items.name.key format for shorthand. --}}
- {{#if ../../shorthand}}
-
- {{!-- Use the items.name.attributes.key.value format otherwise. --}}
- {{else}}
-
- {{/if}}
+ {{!-- Use the items.name.key format for shorthand. --}}
+ {{#if ../../shorthand}}
+
+ {{!-- Use the items.name.attributes.key.value format otherwise. --}}
+ {{else}}
+
{{/if}}
- {{/each}}
+ {{/if}}
+ {{/each}}
- {{/each}}
+ {{/each}}
{{!-- Attributes Tab --}}
- Attribute Key
- Value
- Label
- Data Type
-
+ {{localize "SIMPLE.AttributeKey"}}
+ {{localize "SIMPLE.AttributeValue"}}
+ {{localize "SIMPLE.AttributeLabel"}}
+ {{localize "SIMPLE.AttributeDtype"}}
+
{{!-- Render the attribute list partial. --}}
- {{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.attributes dtypes=dtypes}}
+ {{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.ungroupedAttributes dtypes=dtypes}}
+
+ {{!-- Render the grouped attributes partial and control. --}}
+
-
-
+
\ No newline at end of file
diff --git a/templates/item-sheet.html b/templates/item-sheet.html
index e02b092..9a27bf7 100644
--- a/templates/item-sheet.html
+++ b/templates/item-sheet.html
@@ -1,15 +1,15 @@
+
\ No newline at end of file
diff --git a/templates/parts/sheet-attributes.html b/templates/parts/sheet-attributes.html
index aa691d2..764cfdf 100644
--- a/templates/parts/sheet-attributes.html
+++ b/templates/parts/sheet-attributes.html
@@ -1,41 +1,55 @@
-
-{{#each attributes as |attr key|}}
-