From bac1d023c3928cf9e53dbf3e05c9c2fbd0276a57 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Mon, 31 Aug 2020 02:09:21 +0000 Subject: [PATCH] 9: Add POC for actor templates - This commit adds a proof-of-concept for retrieving actor template types. These are actors using the `worldbuilding.isTemplate` flag set to true. If there are multiple actors with that flag set to true, clicking the create actor button will pull up a prompt to choose from one of those types, which will then create an actor by duplicating the template actor's data. The flag will be unset on the new actor. --- lang/en.json | 6 +- module/actor-sheet.js | 4 +- module/constants.js | 1 + module/item-sheet.js | 4 +- module/simple.js | 181 ++++++++++++++++++++++++++ module/templates.js | 16 +++ templates/actor-sheet.html | 43 +----- templates/item-sheet.html | 43 +----- templates/parts/sheet-attributes.html | 41 ++++++ templates/sidebar/entity-create.html | 16 +++ 10 files changed, 270 insertions(+), 85 deletions(-) create mode 100644 module/constants.js create mode 100644 module/templates.js create mode 100644 templates/parts/sheet-attributes.html create mode 100644 templates/sidebar/entity-create.html diff --git a/lang/en.json b/lang/en.json index 9ae578d..f3cf520 100644 --- a/lang/en.json +++ b/lang/en.json @@ -9,5 +9,9 @@ "SIMPLE.ResourceMin": "Min", "SIMPLE.ResourceValue": "Value", - "SIMPLE.ResourceMax": "Max" + "SIMPLE.ResourceMax": "Max", + + "SIMPLE.DefineTemplate": "Define as Template", + "SIMPLE.UnsetTemplate": "Unset Template", + "SIMPLE.NoTemplate": "No Template" } \ No newline at end of file diff --git a/module/actor-sheet.js b/module/actor-sheet.js index 86a97b1..2a42379 100644 --- a/module/actor-sheet.js +++ b/module/actor-sheet.js @@ -1,3 +1,5 @@ +import { ATTRIBUTE_TYPES } from "./constants.js"; + /** * Extend the basic ActorSheet with some very simple modifications * @extends {ActorSheet} @@ -21,7 +23,7 @@ export class SimpleActorSheet extends ActorSheet { /** @override */ getData() { const data = super.getData(); - data.dtypes = ["String", "Number", "Boolean", "Formula", "Resource"]; + data.dtypes = ATTRIBUTE_TYPES; for ( let attr of Object.values(data.data.attributes) ) { attr.isCheckbox = attr.dtype === "Boolean"; attr.isResource = attr.dtype === "Resource"; diff --git a/module/constants.js b/module/constants.js new file mode 100644 index 0000000..f2f79f6 --- /dev/null +++ b/module/constants.js @@ -0,0 +1 @@ +export const ATTRIBUTE_TYPES = ["String", "Number", "Boolean", "Formula", "Resource"]; \ No newline at end of file diff --git a/module/item-sheet.js b/module/item-sheet.js index 44ad27b..fed2f96 100644 --- a/module/item-sheet.js +++ b/module/item-sheet.js @@ -1,3 +1,5 @@ +import { ATTRIBUTE_TYPES } from "./constants.js"; + /** * Extend the basic ItemSheet with some very simple modifications * @extends {ItemSheet} @@ -20,7 +22,7 @@ export class SimpleItemSheet extends ItemSheet { /** @override */ getData() { const data = super.getData(); - data.dtypes = ["String", "Number", "Boolean", "Formula", "Resource"]; + data.dtypes = ATTRIBUTE_TYPES; for ( let attr of Object.values(data.data.attributes) ) { attr.isCheckbox = attr.dtype === "Boolean"; attr.isResource = attr.dtype === "Resource"; diff --git a/module/simple.js b/module/simple.js index d618abc..e2a04df 100644 --- a/module/simple.js +++ b/module/simple.js @@ -8,6 +8,7 @@ import { SimpleActor } from "./actor.js"; import { SimpleItemSheet } from "./item-sheet.js"; import { SimpleActorSheet } from "./actor-sheet.js"; +import { preloadHandlebarsTemplates } from "./templates.js"; /* -------------------------------------------- */ /* Foundry VTT Initialization */ @@ -89,4 +90,184 @@ Hooks.once("init", async function() { return value.slugify({strict: true}); }); + // Preload template partials. + preloadHandlebarsTemplates(); }); + +/** + * Adds the actor template context menu. + */ +Hooks.on("getActorDirectoryEntryContext", (html, options) => { + // Define an actor as a template. + options.push({ + name: game.i18n.localize("SIMPLE.DefineTemplate"), + icon: '', + condition: li => { + const actor = game.actors.get(li.data("entityId")); + return !actor.getFlag("worldbuilding", "isTemplate"); + }, + callback: li => { + const actor = game.actors.get(li.data("entityId")); + actor.setFlag("worldbuilding", "isTemplate", true); + } + }); + + // Undefine an actor as a template. + options.push({ + name: game.i18n.localize("SIMPLE.UnsetTemplate"), + icon: '', + condition: li => { + const actor = game.actors.get(li.data("entityId")); + return actor.getFlag("worldbuilding", "isTemplate"); + }, + callback: li => { + const actor = game.actors.get(li.data("entityId")); + actor.setFlag("worldbuilding", "isTemplate", false); + } + }); +}); + +/** + * Adds the item template context menu. + */ +Hooks.on("getItemDirectoryEntryContext", (html, options) => { + // Define an item as a template. + options.push({ + name: game.i18n.localize("SIMPLE.DefineTemplate"), + icon: '', + condition: li => { + const item = game.items.get(li.data("entityId")); + return !item.getFlag("worldbuilding", "isTemplate"); + }, + callback: li => { + const item = game.items.get(li.data("entityId")); + item.setFlag("worldbuilding", "isTemplate", true); + } + }); + + // Undefine an item as a template. + options.push({ + name: game.i18n.localize("SIMPLE.UnsetTemplate"), + icon: '', + condition: li => { + const item = game.items.get(li.data("entityId")); + return item.getFlag("worldbuilding", "isTemplate"); + }, + callback: li => { + const item = game.items.get(li.data("entityId")); + item.setFlag("worldbuilding", "isTemplate", false); + } + }); +}); + + +/** + * Adds the actor template selection dialog. + */ +ActorDirectory.prototype._onCreate = async (event) => { + // Do not allow the creation event to bubble to other listeners + event.preventDefault(); + event.stopPropagation(); + + _simpleDirectoryTemplates('actor'); +} + +/** + * Adds the item template selection dialog. + */ +ItemDirectory.prototype._onCreate = async (event) => { + // Do not allow the creation event to bubble to other listeners + event.preventDefault(); + event.stopPropagation(); + + _simpleDirectoryTemplates('item'); +} + +/** + * Display the entity template dialog. + * + * Helper function to display a dialog if there are multiple template types + * defined for the entity type. + * + * @param {string} entityType - 'actor' or 'item' + */ +async function _simpleDirectoryTemplates(entityType = 'actor') { + // Retrieve the collection and class. + const entityCollection = entityType == 'actor' ? game.actors : game.items; + const cls = entityType == 'actor' ? Actor : Item; + + // Query for all entities of this type using the "isTemplate" flag. + let entities = entityCollection.filter(a => a.data.flags?.worldbuilding?.isTemplate === true); + + // Initialize variables related to the entity class. + let ent = game.i18n.localize(cls.config.label); + + // Setup entity data. + let type = entityType == 'actor' ? 'character' : 'item'; + let createData = { + name: `New ${ent}`, + type: type, + folder: event.currentTarget.dataset.folder + }; + + // If there's more than one entity template type, create a form. + if (entities.length > 0) { + // Build an array of types for the form, including an empty default. + let types = [{ + value: null, + label: game.i18n.localize("SIMPLE.NoTemplate") + }]; + + // Append each of the user-defined actor/item types. + types = types.concat(entities.map(a => { + return { + value: a.data.name, + label: a.data.name + } + })); + + // Render the entity creation form + let templateData = {upper: ent, lower: ent.toLowerCase(), types: types}, + dlg = await renderTemplate(`systems/worldbuilding/templates/sidebar/entity-create.html`, templateData); + + // Render the confirmation dialog window + new Dialog({ + title: `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)); + + // 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}); + } + } + }, + default: "create" + }).render(true); + } + // Otherwise, just create a blank entity. + else { + cls.create(createData, {renderSheet: true}); + } +} \ No newline at end of file diff --git a/module/templates.js b/module/templates.js new file mode 100644 index 0000000..840f4f0 --- /dev/null +++ b/module/templates.js @@ -0,0 +1,16 @@ +/** + * Define a set of template paths to pre-load + * Pre-loaded templates are compiled and cached for fast access when rendering + * @return {Promise} + */ +export const preloadHandlebarsTemplates = async function() { + + // Define template paths to load + const templatePaths = [ + // Attribute list partial. + "systems/worldbuilding/templates/parts/sheet-attributes.html" + ]; + + // Load the template parts + return loadTemplates(templatePaths); +}; \ No newline at end of file diff --git a/templates/actor-sheet.html b/templates/actor-sheet.html index 555bb86..51656d8 100644 --- a/templates/actor-sheet.html +++ b/templates/actor-sheet.html @@ -73,47 +73,8 @@ -
    - {{#each data.attributes as |attr key|}} -
  1. - - {{!-- Handle booleans. --}} - {{#if attr.isCheckbox}} - - {{else}} - {{!-- Handle resources. --}} - {{#if attr.isResource}} -
    - - - - - - - - - - - - -
    - {{!-- Handle other input types. --}} - {{else}} - - {{/if}} - {{/if}} - - - -
  2. - {{/each}} -
+ {{!-- Render the attribute list partial. --}} + {{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.attributes dtypes=dtypes}} diff --git a/templates/item-sheet.html b/templates/item-sheet.html index 4e7712f..e02b092 100644 --- a/templates/item-sheet.html +++ b/templates/item-sheet.html @@ -38,47 +38,8 @@ -
    - {{#each data.attributes as |attr key|}} -
  1. - - {{!-- Handle booleans. --}} - {{#if attr.isCheckbox}} - - {{else}} - {{!-- Handle resources. --}} - {{#if attr.isResource}} -
    - - - - - - - - - - - - -
    - {{!-- Handle other input types. --}} - {{else}} - - {{/if}} - {{/if}} - - - -
  2. - {{/each}} -
+ {{!-- Render the attribute list partial. --}} + {{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.attributes dtypes=dtypes}} diff --git a/templates/parts/sheet-attributes.html b/templates/parts/sheet-attributes.html new file mode 100644 index 0000000..aa691d2 --- /dev/null +++ b/templates/parts/sheet-attributes.html @@ -0,0 +1,41 @@ +
    +{{#each attributes as |attr key|}} +
  1. + + {{!-- Handle booleans. --}} + {{#if attr.isCheckbox}} + + {{else}} + {{!-- Handle resources. --}} + {{#if attr.isResource}} +
    + + + + + + + + + + + + +
    + {{!-- Handle other input types. --}} + {{else}} + + {{/if}} + {{/if}} + + + +
  2. + {{/each}} +
\ No newline at end of file diff --git a/templates/sidebar/entity-create.html b/templates/sidebar/entity-create.html new file mode 100644 index 0000000..4f7ccb2 --- /dev/null +++ b/templates/sidebar/entity-create.html @@ -0,0 +1,16 @@ +
+
+ + +
+ +
+ + +

{{localize "ENTITY.TypeHint"}}

+
+