10: Add attribute groups base implementation

- Added support for attribute groups
- Added rollable buttons to formula attributes
- Added additional i18n translation strings
This commit is contained in:
Matt Smith 2020-09-28 15:54:14 +00:00 committed by Andrew
parent c515f8d5b7
commit f69e3841ff
13 changed files with 827 additions and 151 deletions

View file

@ -6,6 +6,8 @@
"SIMPLE.NotifyInitFormulaUpdated": "Initiative formula was updated to:", "SIMPLE.NotifyInitFormulaUpdated": "Initiative formula was updated to:",
"SIMPLE.NotifyInitFormulaInvalid": "Initiative formula was invalid:", "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.ResourceMin": "Min",
"SIMPLE.ResourceValue": "Value", "SIMPLE.ResourceValue": "Value",
@ -13,5 +15,16 @@
"SIMPLE.DefineTemplate": "Define as Template", "SIMPLE.DefineTemplate": "Define as Template",
"SIMPLE.UnsetTemplate": "Unset 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"
} }

View file

@ -14,6 +14,7 @@ export class SimpleActorSheet extends ActorSheet {
width: 600, width: 600,
height: 600, height: 600,
tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}], tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}],
scrollY: [".biography", ".items", ".attributes"],
dragDrop: [{dragSelector: ".item-list .item", dropSelector: null}] dragDrop: [{dragSelector: ".item-list .item", dropSelector: null}]
}); });
} }
@ -25,9 +26,46 @@ export class SimpleActorSheet extends ActorSheet {
const data = super.getData(); const data = super.getData();
data.dtypes = ATTRIBUTE_TYPES; data.dtypes = ATTRIBUTE_TYPES;
for ( let attr of Object.values(data.data.attributes) ) { for ( let attr of Object.values(data.data.attributes) ) {
if ( attr.dtype ) {
attr.isCheckbox = attr.dtype === "Boolean"; attr.isCheckbox = attr.dtype === "Boolean";
attr.isResource = attr.dtype === "Resource"; 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"); data.shorthand = !!game.settings.get("worldbuilding", "macroShorthand");
return data; return data;
} }
@ -38,18 +76,11 @@ export class SimpleActorSheet extends ActorSheet {
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
// Handle rollable items.
html.find(".items .rollable").on("click", this._onItemRoll.bind(this));
// Handle rollable attributes. // Handle rollable attributes.
html.find('.items .rollable').click(ev => { html.find(".attributes").on("click", "a.attribute-roll", this._onAttributeRoll.bind(this));
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: `<h2>${item.name}</h2><h3>${button.text()}</h3>`
});
});
// Everything below here is only needed if the sheet is editable // Everything below here is only needed if the sheet is editable
if ( !this.options.editable ) return; if ( !this.options.editable ) return;
@ -70,6 +101,51 @@ export class SimpleActorSheet extends ActorSheet {
// Add or Remove Attribute // Add or Remove Attribute
html.find(".attributes").on("click", ".attribute-control", this._onClickAttributeControl.bind(this)); 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: `<h2>${item.name}</h2><h3>${button.text()}</h3>`
});
}
/**
* 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 * 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 * @param {MouseEvent} event The originating left click event
@ -94,12 +208,16 @@ export class SimpleActorSheet extends ActorSheet {
event.preventDefault(); event.preventDefault();
const a = event.currentTarget; const a = event.currentTarget;
const action = a.dataset.action; const action = a.dataset.action;
const group = a.dataset.group;
let dtype = a.dataset.dtype;
const attrs = this.object.data.data.attributes; const attrs = this.object.data.data.attributes;
const groups = this.object.data.data.groups;
const form = this.form; const form = this.form;
// Add new attribute // Add new attribute
if ( action === "create" ) { 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 nk = Object.keys(attrs).length + 1;
let newValue = `attr${nk}`; let newValue = `attr${nk}`;
let newKey = document.createElement("div"); let newKey = document.createElement("div");
@ -107,7 +225,53 @@ export class SimpleActorSheet extends ActorSheet {
++nk; ++nk;
newValue = `attr${nk}`; newValue = `attr${nk}`;
}; };
newKey.innerHTML = `<input type="text" name="data.attributes.attr${nk}.key" value="${newValue}"/>`;
// 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]; newKey = newKey.children[0];
form.appendChild(newKey); form.appendChild(newKey);
await this._onSubmit(event); 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 = `<input type="text" name="data.groups.${newValue}.key" value="${newValue}"/>`;
// 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")} <strong>${group.val()}</strong>`,
buttons: {
confirm: {
icon: '<i class="fas fa-trash"></i>',
label: game.i18n.localize("Yes"),
callback: async () => {
groupHeader.parentElement.removeChild(groupHeader);
await this._onSubmit(event);
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
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 = '<div>';
// 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}"/>`;
}
// Close the HTML and return.
return result + '</div>';
}
/* -------------------------------------------- */
/**
* 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 */ /** @override */
_updateObject(event, formData) { _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 // Handle the free-form attributes list
const formAttrs = expandObject(formData).data.attributes || {}; const formAttrs = expandObject(formData).data.attributes || {};
const attributes = Object.values(formAttrs).reduce((obj, v) => { const attributes = Object.values(formAttrs).reduce((obj, 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(); let k = v["key"].trim();
if ( /[\s\.]/.test(k) ) return ui.notifications.error("Attribute keys may not contain spaces or periods"); if ( /[\s\.]/.test(k) ) return ui.notifications.error("Attribute keys may not contain spaces or periods");
delete v["key"]; delete v["key"];
// Add the new attribute only if it's ungrouped.
if ( !group ) {
obj[k] = v; obj[k] = v;
}
}
return obj; return obj;
}, {}); }, {});
@ -141,13 +444,57 @@ export class SimpleActorSheet extends ActorSheet {
if ( !attributes.hasOwnProperty(k) ) attributes[`-=${k}`] = null; 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 // Re-combine formData
formData = Object.entries(formData).filter(e => !e[0].startsWith("data.attributes")).reduce((obj, e) => { formData = Object.entries(formData).filter(e => !e[0].startsWith("data.attributes")).reduce((obj, e) => {
obj[e[0]] = e[1]; obj[e[0]] = e[1];
return obj; return obj;
}, {_id: this.object._id, "data.attributes": attributes}); }, {_id: this.object._id, "data.attributes": attributes});
// Update the Actor return formData;
return this.object.update(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;
} }
} }

View file

@ -25,6 +25,7 @@ export class SimpleActor extends Actor {
delete data.attributes; delete data.attributes;
delete data.attr; delete data.attr;
delete data.abil; delete data.abil;
delete data.groups;
} }
return data; return data;
@ -44,8 +45,19 @@ export class SimpleActor extends Actor {
// Add shortened version of the attributes. // Add shortened version of the attributes.
if ( !!shorthand ) { if ( !!shorthand ) {
if ( !(k in data) ) { if ( !(k in data) ) {
// Non-grouped attributes.
if ( v.dtype ) {
data[k] = v.value; 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,16 +122,44 @@ export class SimpleActor extends Actor {
// Evaluate formula attributes after all other attributes have been handled, // Evaluate formula attributes after all other attributes have been handled,
// including items. // including items.
for ( let k of formulaAttributes ) { 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 ) { if ( data.attributes[k].value ) {
data.attributes[k].value = this._replaceData(data.attributes[k].value, data, {missing: "0"}); data.attributes[k].value = this._replaceData(data.attributes[k].value, data, {missing: "0"});
// TODO: Replace with: // 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, {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. // Duplicate values to shorthand.
if ( !!shorthand ) { if ( !!shorthand ) {
// Non-grouped attributes.
if ( data.attributes[k].value ) {
data[k] = 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;
}
}
}
} }
} }

47
module/macro.js Normal file
View file

@ -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
});
}

View file

@ -9,11 +9,15 @@ import { SimpleActor } from "./actor.js";
import { SimpleItemSheet } from "./item-sheet.js"; import { SimpleItemSheet } from "./item-sheet.js";
import { SimpleActorSheet } from "./actor-sheet.js"; import { SimpleActorSheet } from "./actor-sheet.js";
import { preloadHandlebarsTemplates } from "./templates.js"; import { preloadHandlebarsTemplates } from "./templates.js";
import { createWorldbuildingMacro, rollAttrMacro } from "./macro.js";
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Foundry VTT Initialization */ /* Foundry VTT Initialization */
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Init hook.
*/
Hooks.once("init", async function() { Hooks.once("init", async function() {
console.log(`Initializing Simple Worldbuilding System`); console.log(`Initializing Simple Worldbuilding System`);
@ -26,6 +30,12 @@ Hooks.once("init", async function() {
decimals: 2 decimals: 2
}; };
game.worldbuilding = {
SimpleActor,
createWorldbuildingMacro,
rollAttrMacro,
};
// Define custom Entity classes // Define custom Entity classes
CONFIG.Actor.entityClass = SimpleActor; CONFIG.Actor.entityClass = SimpleActor;
@ -94,6 +104,11 @@ Hooks.once("init", async function() {
preloadHandlebarsTemplates(); preloadHandlebarsTemplates();
}); });
/**
* Macrobar hook.
*/
Hooks.on("hotbarDrop", (bar, data, slot) => createWorldbuildingMacro(data, slot));
/** /**
* Adds the actor template context menu. * Adds the actor template context menu.
*/ */
@ -205,7 +220,7 @@ async function _simpleDirectoryTemplates(entityType = 'actor') {
// Setup entity data. // Setup entity data.
let type = entityType == 'actor' ? 'character' : 'item'; let type = entityType == 'actor' ? 'character' : 'item';
let createData = { let createData = {
name: `New ${ent}`, name: `${game.i18n.localize("SIMPLE.New")} ${ent}`,
type: type, type: type,
folder: event.currentTarget.dataset.folder folder: event.currentTarget.dataset.folder
}; };
@ -231,14 +246,10 @@ async function _simpleDirectoryTemplates(entityType = 'actor') {
dlg = await renderTemplate(`systems/worldbuilding/templates/sidebar/entity-create.html`, templateData); dlg = await renderTemplate(`systems/worldbuilding/templates/sidebar/entity-create.html`, templateData);
// Render the confirmation dialog window // Render the confirmation dialog window
new Dialog({ Dialog.confirm({
title: `Create ${createData.name}`, title: `${game.i18n.localize("SIMPLE.Create")} ${createData.name}`,
content: dlg, content: dlg,
buttons: { yes: html => {
create: {
icon: '<i class="fas fa-check"></i>',
label: `Create ${ent}`,
callback: html => {
// Get the form data. // Get the form data.
const form = html[0].querySelector("form"); const form = html[0].querySelector("form");
mergeObject(createData, validateForm(form)); mergeObject(createData, validateForm(form));
@ -260,11 +271,10 @@ async function _simpleDirectoryTemplates(entityType = 'actor') {
} }
cls.create(createData, {renderSheet: true}); cls.create(createData, {renderSheet: true});
}
}
}, },
default: "create" no: () => {},
}).render(true); defaultYes: false
});
} }
// Otherwise, just create a blank entity. // Otherwise, just create a blank entity.
else { else {

View file

@ -8,7 +8,8 @@ export const preloadHandlebarsTemplates = async function() {
// Define template paths to load // Define template paths to load
const templatePaths = [ const templatePaths = [
// Attribute list partial. // 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 // Load the template parts

View file

@ -109,10 +109,51 @@
flex: none; flex: none;
width: auto; 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 { .worldbuilding .attributes-header {
position: sticky;
top: 0;
left: 0;
right: 0;
padding: 5px; padding: 5px;
margin: 5px 0; margin: 5px 0;
background: rgba(0, 0, 0, 0.05); background: #cfcdc2;
border: 1px solid #AAA; border: 1px solid #AAA;
border-radius: 2px; border-radius: 2px;
text-align: center; text-align: center;
@ -138,12 +179,6 @@
border-radius: 0; border-radius: 0;
border-bottom: 1px solid #AAA; 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 { .worldbuilding .attributes-list .attribute-col label {
font-size: 10px; font-size: 10px;
text-align: center; text-align: center;
@ -155,6 +190,47 @@
line-height: 1; line-height: 1;
height: 50%; 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 { .worldbuilding.sheet.actor {
min-width: 560px; min-width: 560px;
min-height: 420px; min-height: 420px;

View file

@ -125,10 +125,57 @@
} }
/* Attributes */ /* 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 { .attributes-header {
position: sticky;
top: 0;
left: 0;
right: 0;
padding: 5px; padding: 5px;
margin: 5px 0; margin: 5px 0;
background: rgba(0, 0, 0, 0.05); background: #cfcdc2;
border: 1px solid #AAA; border: 1px solid #AAA;
border-radius: 2px; border-radius: 2px;
text-align: center; text-align: center;
@ -158,13 +205,6 @@
border-bottom: 1px solid #AAA; border-bottom: 1px solid #AAA;
} }
a.attribute-control {
flex: 0 0 20px;
text-align: center;
line-height: 28px;
border: none;
}
.attribute-col { .attribute-col {
label { label {
font-size: 10px; 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 { .worldbuilding.sheet.actor {

View file

@ -13,7 +13,8 @@
"min": 0, "min": 0,
"max": 5 "max": 5
}, },
"attributes": {} "attributes": {},
"groups": {}
} }
}, },
"Item": { "Item": {
@ -22,7 +23,8 @@
"description": "", "description": "",
"quantity": 1, "quantity": 1,
"weight": 0, "weight": 0,
"attributes": {} "attributes": {},
"groups": {}
} }
} }
} }

View file

@ -46,10 +46,13 @@
{{#if (eq itemAttr.dtype "Formula")}} {{#if (eq itemAttr.dtype "Formula")}}
{{!-- Use the items.name.key format for shorthand. --}} {{!-- Use the items.name.key format for shorthand. --}}
{{#if ../../shorthand}} {{#if ../../shorthand}}
<button class="item-button rollable" data-roll="@items.{{slugify item.name}}.{{key}}" title="{{itemAttr.value}}">{{itemAttr.label}}</button> <button class="item-button rollable" data-roll="@items.{{slugify item.name}}.{{key}}" data-label="{{ itemAttr.label }}"
title="{{itemAttr.value}}">{{itemAttr.label}}</button>
{{!-- Use the items.name.attributes.key.value format otherwise. --}} {{!-- Use the items.name.attributes.key.value format otherwise. --}}
{{else}} {{else}}
<button class="item-button rollable" data-roll="@items.{{slugify item.name}}.attributes.{{key}}.value" title="{{itemAttr.value}}">{{itemAttr.label}}</button> <button class="item-button rollable"
data-roll="@items.{{slugify item.name}}.attributes.{{key}}.value" data-label="{{ itemAttr.label }}"
title="{{itemAttr.value}}">{{itemAttr.label}}</button>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/each}} {{/each}}
@ -66,16 +69,25 @@
{{!-- Attributes Tab --}} {{!-- Attributes Tab --}}
<div class="tab attributes" data-group="primary" data-tab="attributes"> <div class="tab attributes" data-group="primary" data-tab="attributes">
<header class="attributes-header flexrow"> <header class="attributes-header flexrow">
<span class="attribute-key">Attribute Key</span> <span class="attribute-key">{{localize "SIMPLE.AttributeKey"}}</span>
<span class="attribute-value">Value</span> <span class="attribute-value">{{localize "SIMPLE.AttributeValue"}}</span>
<span class="attribute-label">Label</span> <span class="attribute-label">{{localize "SIMPLE.AttributeLabel"}}</span>
<span class="attribute-dtype">Data Type</span> <span class="attribute-dtype">{{localize "SIMPLE.AttributeDtype"}}</span>
<a class="attribute-control" data-action="create"><i class="fas fa-plus"></i></a> <a class="attribute-control" data-action="create" data-group="{{group}}"><i class="fas fa-plus"></i></a>
</header> </header>
{{!-- Render the attribute list partial. --}} {{!-- 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. --}}
<div class="groups">
{{> "systems/worldbuilding/templates/parts/sheet-groups.html" attributes=data.groupedAttributes groups=data.groups dtypes=dtypes}}
<div class="group-controls flexrow">
<input class="group-prefix" type="text" val=""/>
<a class="button group-control" data-action="create-group"><i class="fas fa-plus"></i>Add Attribute Group</a>
</div>
</div>
</div> </div>
</section> </section>
</form> </form>

View file

@ -31,6 +31,7 @@
{{!-- Attributes Tab --}} {{!-- Attributes Tab --}}
<div class="tab attributes" data-group="primary" data-tab="attributes"> <div class="tab attributes" data-group="primary" data-tab="attributes">
<header class="attributes-header flexrow"> <header class="attributes-header flexrow">
<span class="attribute-roll"></span>
<span class="attribute-key">Attribute Key</span> <span class="attribute-key">Attribute Key</span>
<span class="attribute-value">Value</span> <span class="attribute-value">Value</span>
<span class="attribute-label">Label</span> <span class="attribute-label">Label</span>

View file

@ -1,41 +1,55 @@
<section class="attributes-group">
<ol class="attributes-list"> <ol class="attributes-list">
{{#each attributes as |attr key|}} {{#each attributes as |attr key|}}
<li class="attribute flexrow" data-attribute="{{key}}"> <li class="attribute flexrow" data-attribute="{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}">
<input class="attribute-key" type="text" name="data.attributes.{{key}}.key" value="{{key}}"/> <div class="attribute-key-wrapper flexrow">
{{#if (eq attr.dtype "Formula")}}
<a class="attribute-roll" data-label="{{attr.label}}" data-roll="{{attr.value}}"><i class="fas fa-dice-d20"></i></a>
{{/if}}
<input class="attribute-key" type="text" name="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.key" value="{{key}}" placeholder="{{localize "SIMPLE.AttributeKey"}}"/>
</div>
{{!-- Handle booleans. --}} {{!-- Handle booleans. --}}
{{#if attr.isCheckbox}} {{#if attr.isCheckbox}}
<label class="attribute-value checkbox"><input type="checkbox" name="data.attributes.{{key}}.value" {{checked attr.value}}/></label> <label class="attribute-value checkbox"><input type="checkbox" name="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.value"
{{checked attr.value}} /></label>
{{else}} {{else}}
{{!-- Handle resources. --}} {{!-- Handle resources. --}}
{{#if attr.isResource}} {{#if attr.isResource}}
<div class="attribute-group flexrow"> <div class="attribute-group flexrow">
<span class="attribute-col flexcol"> <span class="attribute-col flexcol">
<label for="data.attributes.{{key}}.min">{{localize "SIMPLE.ResourceMin"}}</label> <label for="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.min">{{localize "SIMPLE.ResourceMin"}}</label>
<input class="attribute-value" type="text" name="data.attributes.{{key}}.min" value="{{attr.min}}" data-dtype="{{attr.dtype}}"/> <input class="attribute-value" type="text" name="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.min" value="{{attr.min}}"
data-dtype="Number" />
</span> </span>
<span class="attribute-col flexcol"> <span class="attribute-col flexcol">
<label for="data.attributes.{{key}}.value">{{localize "SIMPLE.ResourceValue"}}</label> <label for="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.value">{{localize "SIMPLE.ResourceValue"}}</label>
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/> <input class="attribute-value" type="text" name="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.value"
value="{{attr.value}}" data-dtype="Number" />
</span> </span>
<span class="attribute-col flexcol"> <span class="attribute-col flexcol">
<label for="data.attributes.{{key}}.max">{{localize "SIMPLE.ResourceMax"}}</label> <label for="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.max">{{localize "SIMPLE.ResourceMax"}}</label>
<input class="attribute-value" type="text" name="data.attributes.{{key}}.max" value="{{attr.max}}" data-dtype="{{attr.dtype}}"/> <input class="attribute-value" type="text" name="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.max" value="{{attr.max}}"
data-dtype="Number" />
</span> </span>
</div> </div>
{{!-- Handle other input types. --}} {{!-- Handle other input types. --}}
{{else}} {{else}}
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/> <input class="attribute-value" type="text" name="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.value" value="{{attr.value}}"
data-dtype="{{attr.dtype}}" placeholder="{{localize "SIMPLE.AttributeValue"}}"/>
{{/if}} {{/if}}
{{/if}} {{/if}}
<input class="attribute-label" type="text" name="data.attributes.{{key}}.label" value="{{attr.label}}"/> <input class="attribute-label" type="text" name="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.label" value="{{attr.label}}"
<select class="attribute-dtype" name="data.attributes.{{key}}.dtype"> placeholder="{{localize "SIMPLE.AttributeLabel"}}"/>
<select class="attribute-dtype" name="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.dtype">
{{#select attr.dtype}} {{#select attr.dtype}}
{{#each ../dtypes as |t|}} {{#each ../dtypes as |t|}}
<option value="{{t}}">{{t}}</option> <option value="{{t}}">{{t}}</option>
{{/each}} {{/each}}
{{/select}} {{/select}}
</select> </select>
<input type="hidden" name="data.attributes.{{#if attr.group}}{{attr.group}}.{{/if}}{{key}}.group" value="{{attr.group}}" />
<a class="attribute-control" data-action="delete"><i class="fas fa-trash"></i></a> <a class="attribute-control" data-action="delete"><i class="fas fa-trash"></i></a>
</li> </li>
{{/each}} {{/each}}
</ol> </ol>
</section>

View file

@ -0,0 +1,21 @@
<ol class="groups-list">
{{#each groups as |group groupKey|}}
<li class="group" data-group="{{groupKey}}">
<div class="group-header flexrow">
<input class="group-key" type="text" readonly name="data.groups.{{groupKey}}.key" value="{{groupKey}}" />
<input class="group-label" type="text" name="data.groups.{{groupKey}}.label" value="{{group.label}}" placeholder="Group Label" />
<select class="group-dtype" name="data.groups.{{groupKey}}.dtype">
{{#select group.dtype}}
{{#each ../dtypes as |t|}}
<option value="{{t}}">{{t}}</option>
{{/each}}
{{/select}}
</select>
<a class="attribute-control" data-action="create" data-group="{{groupKey}}" data-dtype="{{group.dtype}}"><i class="fas fa-plus"></i></a>
<a class="group-control" data-action="delete-group"><i class="fas fa-trash"></i></a>
</div>
{{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=group.attributes group=groupKey dtypes=../dtypes}}
</li>
{{/each}}
</ol>