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.
This commit is contained in:
Matt Smith 2020-08-31 02:09:21 +00:00 committed by Andrew
parent d3f030b4f7
commit bac1d023c3
10 changed files with 270 additions and 85 deletions

View file

@ -9,5 +9,9 @@
"SIMPLE.ResourceMin": "Min", "SIMPLE.ResourceMin": "Min",
"SIMPLE.ResourceValue": "Value", "SIMPLE.ResourceValue": "Value",
"SIMPLE.ResourceMax": "Max" "SIMPLE.ResourceMax": "Max",
"SIMPLE.DefineTemplate": "Define as Template",
"SIMPLE.UnsetTemplate": "Unset Template",
"SIMPLE.NoTemplate": "No Template"
} }

View file

@ -1,3 +1,5 @@
import { ATTRIBUTE_TYPES } from "./constants.js";
/** /**
* Extend the basic ActorSheet with some very simple modifications * Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet} * @extends {ActorSheet}
@ -21,7 +23,7 @@ export class SimpleActorSheet extends ActorSheet {
/** @override */ /** @override */
getData() { getData() {
const data = super.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) ) { for ( let attr of Object.values(data.data.attributes) ) {
attr.isCheckbox = attr.dtype === "Boolean"; attr.isCheckbox = attr.dtype === "Boolean";
attr.isResource = attr.dtype === "Resource"; attr.isResource = attr.dtype === "Resource";

1
module/constants.js Normal file
View file

@ -0,0 +1 @@
export const ATTRIBUTE_TYPES = ["String", "Number", "Boolean", "Formula", "Resource"];

View file

@ -1,3 +1,5 @@
import { ATTRIBUTE_TYPES } from "./constants.js";
/** /**
* Extend the basic ItemSheet with some very simple modifications * Extend the basic ItemSheet with some very simple modifications
* @extends {ItemSheet} * @extends {ItemSheet}
@ -20,7 +22,7 @@ export class SimpleItemSheet extends ItemSheet {
/** @override */ /** @override */
getData() { getData() {
const data = super.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) ) { for ( let attr of Object.values(data.data.attributes) ) {
attr.isCheckbox = attr.dtype === "Boolean"; attr.isCheckbox = attr.dtype === "Boolean";
attr.isResource = attr.dtype === "Resource"; attr.isResource = attr.dtype === "Resource";

View file

@ -8,6 +8,7 @@
import { SimpleActor } from "./actor.js"; 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";
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Foundry VTT Initialization */ /* Foundry VTT Initialization */
@ -89,4 +90,184 @@ Hooks.once("init", async function() {
return value.slugify({strict: true}); 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: '<i class="fas fa-stamp"></i>',
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: '<i class="fas fa-times"></i>',
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: '<i class="fas fa-stamp"></i>',
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: '<i class="fas fa-times"></i>',
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: '<i class="fas fa-check"></i>',
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});
}
}

16
module/templates.js Normal file
View file

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

View file

@ -73,47 +73,8 @@
<a class="attribute-control" data-action="create"><i class="fas fa-plus"></i></a> <a class="attribute-control" data-action="create"><i class="fas fa-plus"></i></a>
</header> </header>
<ol class="attributes-list"> {{!-- Render the attribute list partial. --}}
{{#each data.attributes as |attr key|}} {{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.attributes dtypes=dtypes}}
<li class="attribute flexrow" data-attribute="{{key}}">
<input class="attribute-key" type="text" name="data.attributes.{{key}}.key" value="{{key}}"/>
{{!-- Handle booleans. --}}
{{#if attr.isCheckbox}}
<label class="attribute-value checkbox"><input type="checkbox" name="data.attributes.{{key}}.value" {{checked attr.value}}/></label>
{{else}}
{{!-- Handle resources. --}}
{{#if attr.isResource}}
<div class="attribute-group flexrow">
<span class="attribute-col flexcol">
<label for="data.attributes.{{key}}.min">{{localize "SIMPLE.ResourceMin"}}</label>
<input class="attribute-value" type="text" name="data.attributes.{{key}}.min" value="{{attr.min}}" data-dtype="{{attr.dtype}}"/>
</span>
<span class="attribute-col flexcol">
<label for="data.attributes.{{key}}.value">{{localize "SIMPLE.ResourceValue"}}</label>
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/>
</span>
<span class="attribute-col flexcol">
<label for="data.attributes.{{key}}.max">{{localize "SIMPLE.ResourceMax"}}</label>
<input class="attribute-value" type="text" name="data.attributes.{{key}}.max" value="{{attr.max}}" data-dtype="{{attr.dtype}}"/>
</span>
</div>
{{!-- Handle other input types. --}}
{{else}}
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/>
{{/if}}
{{/if}}
<input class="attribute-label" type="text" name="data.attributes.{{key}}.label" value="{{attr.label}}"/>
<select class="attribute-dtype" name="data.attributes.{{key}}.dtype">
{{#select attr.dtype}}
{{#each ../dtypes as |t|}}
<option value="{{t}}">{{t}}</option>
{{/each}}
{{/select}}
</select>
<a class="attribute-control" data-action="delete"><i class="fas fa-trash"></i></a>
</li>
{{/each}}
</ol>
</div> </div>
</section> </section>
</form> </form>

View file

@ -38,47 +38,8 @@
<a class="attribute-control" data-action="create"><i class="fas fa-plus"></i></a> <a class="attribute-control" data-action="create"><i class="fas fa-plus"></i></a>
</header> </header>
<ol class="attributes-list"> {{!-- Render the attribute list partial. --}}
{{#each data.attributes as |attr key|}} {{> "systems/worldbuilding/templates/parts/sheet-attributes.html" attributes=data.attributes dtypes=dtypes}}
<li class="attribute flexrow" data-attribute="{{key}}">
<input class="attribute-key" type="text" name="data.attributes.{{key}}.key" value="{{key}}"/>
{{!-- Handle booleans. --}}
{{#if attr.isCheckbox}}
<label class="attribute-value checkbox"><input type="checkbox" name="data.attributes.{{key}}.value" {{checked attr.value}}/></label>
{{else}}
{{!-- Handle resources. --}}
{{#if attr.isResource}}
<div class="attribute-group flexrow">
<span class="attribute-col flexcol">
<label for="data.attributes.{{key}}.min">Min</label>
<input class="attribute-value" type="text" name="data.attributes.{{key}}.min" value="{{attr.min}}" data-dtype="{{attr.dtype}}"/>
</span>
<span class="attribute-col flexcol">
<label for="data.attributes.{{key}}.value">Current</label>
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/>
</span>
<span class="attribute-col flexcol">
<label for="data.attributes.{{key}}.max">Max</label>
<input class="attribute-value" type="text" name="data.attributes.{{key}}.max" value="{{attr.max}}" data-dtype="{{attr.dtype}}"/>
</span>
</div>
{{!-- Handle other input types. --}}
{{else}}
<input class="attribute-value" type="text" name="data.attributes.{{key}}.value" value="{{attr.value}}" data-dtype="{{attr.dtype}}"/>
{{/if}}
{{/if}}
<input class="attribute-label" type="text" name="data.attributes.{{key}}.label" value="{{attr.label}}"/>
<select class="attribute-dtype" name="data.attributes.{{key}}.dtype">
{{#select attr.dtype}}
{{#each ../dtypes as |t|}}
<option value="{{t}}">{{t}}</option>
{{/each}}
{{/select}}
</select>
<a class="attribute-control" data-action="delete"><i class="fas fa-trash"></i></a>
</li>
{{/each}}
</ol>
</div> </div>
</section> </section>
</form> </form>

View file

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

View file

@ -0,0 +1,16 @@
<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>
<p class="notes">{{localize "ENTITY.TypeHint"}}</p>
</div>
</form>