123 changes: 18 additions & 105 deletions module/applications/proficiency-selector.mjs
@@ -1,24 +1,17 @@
import TraitSelector from "./trait-selector.mjs";
import * as Trait from "../documents/actor/trait.mjs";

/**
* An application for selecting proficiencies with categories that can contain children.
* @deprecated since dnd5e 2.1, targeted for removal in 2.3
*/
export default class ProficiencySelector extends TraitSelector {

/**
* Cached version of the base items compendia indices with the needed subtype fields.
* @type {object}
*/
static _cachedIndices = {};

/* -------------------------------------------- */

/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
title: "Actor Proficiency Selection",
type: "",
sortCategories: false
type: ""
});
}

Expand All @@ -30,73 +23,26 @@ export default class ProficiencySelector extends TraitSelector {
const chosen = (this.options.valueKey) ? foundry.utils.getProperty(attr, this.options.valueKey) ?? [] : attr;

const data = super.getData();
data.choices = await this.constructor.getChoices(this.options.type, chosen, this.options.sortCategories);
data.choices = await Trait.choices(this.options.type, chosen);
return data;
}

/* -------------------------------------------- */

/**
* Structure representing proficiency choices split into categories.
*
* @typedef {object} ProficiencyChoice
* @property {string} label Localized label for the choice.
* @property {boolean} chosen Should this choice be selected by default?
* @property {ProficiencyChoice[]} [children] Array of children if this is a category.
*/

/**
* A static helper method to get a list of choices for a proficiency type.
*
* @param {string} type Proficiency type to select, either `armor`, `tool`, or `weapon`.
* @param {string[]} [chosen] Optional list of items to be marked as chosen.
* @returns {Object<string, ProficiencyChoice>} Object mapping proficiency ids to choice objects.
* @returns {Object<string, SelectChoices>} Object mapping proficiency ids to choice objects.
* @deprecated since dnd5e 2.1, targeted for removal in 2.3
*/
static async getChoices(type, chosen=[]) {
let data = Object.entries(CONFIG.DND5E[`${type}Proficiencies`]).reduce((obj, [key, label]) => {
obj[key] = { label: label, chosen: chosen.includes(key) };
return obj;
}, {});

const ids = CONFIG.DND5E[`${type}Ids`];
const map = CONFIG.DND5E[`${type}ProficienciesMap`];
if ( ids !== undefined ) {
const typeProperty = (type !== "armor") ? `${type}Type` : "armor.type";
for ( const [key, id] of Object.entries(ids) ) {
const item = await this.getBaseItem(id);
if ( !item ) continue;

let type = foundry.utils.getProperty(item.system, typeProperty);
if ( map && map[type] ) type = map[type];
const entry = {
label: item.name,
chosen: chosen.includes(key)
};
if ( data[type] === undefined ) {
data[key] = entry;
} else {
if ( data[type].children === undefined ) {
data[type].children = {};
}
data[type].children[key] = entry;
}
}
}

if ( type === "tool" ) {
data.vehicle.children = Object.entries(CONFIG.DND5E.vehicleTypes).reduce((obj, [key, label]) => {
obj[key] = { label: label, chosen: chosen.includes(key) };
return obj;
}, {});
data = dnd5e.utils.sortObjectEntries(data, "label");
}

for ( const category of Object.values(data) ) {
if ( !category.children ) continue;
category.children = dnd5e.utils.sortObjectEntries(category.children, "label");
}

return data;
foundry.utils.logCompatibilityWarning(
"ProficiencySelector#getChoices has been deprecated in favor of Trait#choices.",
{ since: "DnD5e 2.1", until: "DnD5e 2.3" }
);
return Trait.choices(type, chosen);
}

/* -------------------------------------------- */
Expand All @@ -114,47 +60,14 @@ export default class ProficiencySelector extends TraitSelector {
* false.
* @returns {Promise<Item5e>|object} Promise for a `Document` if `indexOnly` is false & `fullItem` is true,
* otherwise else a simple object containing the minimal index data.
* @deprecated since dnd5e 2.1, targeted for removal in 2.3
*/
static getBaseItem(identifier, { indexOnly=false, fullItem=false }={}) {
let pack = CONFIG.DND5E.sourcePacks.ITEMS;
let [scope, collection, id] = identifier.split(".");
if ( scope && collection ) pack = `${scope}.${collection}`;
if ( !id ) id = identifier;

const packObject = game.packs.get(pack);

// Full Item5e document required, always async.
if ( fullItem && !indexOnly ) {
return packObject?.getDocument(id);
}

const cache = this._cachedIndices[pack];
const loading = cache instanceof Promise;

// Return extended index if cached, otherwise normal index, guaranteed to never be async.
if ( indexOnly ) {
const index = packObject?.index.get(id);
return loading ? index : cache?.[id] ?? index;
}

// Returned cached version of extended index if available.
if ( loading ) return cache.then(() => this._cachedIndices[pack][id]);
else if ( cache ) return cache[id];
if ( !packObject ) return;

// Build the extended index and return a promise for the data
const promise = packObject.getIndex({
fields: ["system.armor.type", "system.toolType", "system.weaponType"]
}).then(index => {
const store = index.reduce((obj, entry) => {
obj[entry._id] = entry;
return obj;
}, {});
this._cachedIndices[pack] = store;
return store[id];
});
this._cachedIndices[pack] = promise;
return promise;
static getBaseItem(identifier, options) {
foundry.utils.logCompatibilityWarning(
"ProficiencySelector#getChoices has been deprecated in favor of Trait#getBaseItem.",
{ since: "DnD5e 2.1", until: "DnD5e 2.3" }
);
return Trait.getBaseItem(identifier, options);
}

/* -------------------------------------------- */
Expand Down
22 changes: 18 additions & 4 deletions module/applications/trait-selector.mjs
@@ -1,7 +1,20 @@
/**
* A specialized form used to select from a checklist of attributes, traits, or properties
* A specialized form used to select from a checklist of attributes, traits, or properties.
* @deprecated since dnd5e 2.1, targeted for removal in 2.3
*/
export default class TraitSelector extends DocumentSheet {
constructor(...args) {
super(...args);

if ( !this.options.suppressWarning ) foundry.utils.logCompatibilityWarning(
`${this.constructor.name} has been deprecated in favor of a more specialized TraitSelector `
+ "available at 'dnd5e.applications.actor.TraitSelector'. Support for the old application will "
+ "be removed in a future version.",
{ since: "DnD5e 2.1", until: "DnD5e 2.3" }
);
}

/* -------------------------------------------- */

/** @inheritDoc */
static get defaultOptions() {
Expand Down Expand Up @@ -58,9 +71,9 @@ export default class TraitSelector extends DocumentSheet {

// Return data
return {
allowCustom: o.allowCustom,
choices: choices,
custom: custom
custom: custom,
customPath: o.allowCustom ? "custom" : null
};
}

Expand All @@ -73,9 +86,10 @@ export default class TraitSelector extends DocumentSheet {
*/
_prepareUpdateData(formData) {
const o = this.options;
formData = foundry.utils.expandObject(formData);

// Obtain choices
const chosen = Object.entries(formData).filter(([k, v]) => (k !== "custom") && v).map(([k]) => k);
const chosen = Object.entries(formData.choices).filter(([, v]) => v).map(([k]) => k);

// Object including custom data
const updateData = {};
Expand Down
76 changes: 76 additions & 0 deletions module/config.mjs
Expand Up @@ -1491,6 +1491,82 @@ DND5E.CR_EXP_LEVELS = [
* @property {string[]} [skills]
*/

/* -------------------------------------------- */

/**
* Trait configuration information.
*
* @typedef {object} TraitConfiguration
* @property {string} label Localization key for the trait name.
* @property {string} [actorKeyPath] If the trait doesn't directly map to an entry as `traits.[key]`, where is
* this trait's data stored on the actor?
* @property {string} [configKey] If the list of trait options doesn't match the name of the trait, where can
* the options be found within `CONFIG.DND5E`?
* @property {string} [labelKey] If config is an enum of objects, where can the label be found?
* @property {object} [subtypes] Configuration for traits that take some sort of base item.
* @property {string} [subtypes.keyPath] Path to subtype value on base items, should match a category key.
* @property {string[]} [subtypes.ids] Key for base item ID objects within `CONFIG.DND5E`.
* @property {object} [children] Mapping of category key to an object defining its children.
* @property {boolean} [sortCategories] Whether top-level categories should be sorted.
*/

/**
* Configurable traits on actors.
* @enum {TraitConfiguration}
*/
DND5E.traits = {
saves: {
label: "DND5E.ClassSaves",
configKey: "abilities"
},
skills: {
label: "DND5E.TraitSkillProf",
labelKey: "label"
},
languages: {
label: "DND5E.Languages"
},
di: {
label: "DND5E.DamImm",
configKey: "damageTypes"
},
dr: {
label: "DND5E.DamRes",
configKey: "damageTypes"
},
dv: {
label: "DND5E.DamVuln",
configKey: "damageTypes"
},
ci: {
label: "DND5E.ConImm",
configKey: "conditionTypes"
},
weapon: {
label: "DND5E.TraitWeaponProf",
actorKeyPath: "traits.weaponProf",
configKey: "weaponProficiencies",
subtypes: { keyPath: "weaponType", ids: ["weaponIds"] }
},
armor: {
label: "DND5E.TraitArmorProf",
actorKeyPath: "traits.armorProf",
configKey: "armorProficiencies",
subtypes: { keyPath: "armor.type", ids: ["armorIds", "shieldIds"] }
},
tool: {
label: "DND5E.TraitToolProf",
actorKeyPath: "traits.toolProf",
configKey: "toolProficiencies",
subtypes: { keyPath: "toolType", ids: ["toolIds"] },
children: { vehicle: "vehicleTypes" },
sortCategories: true
}
};
preLocalize("traits", { key: "label" });

/* -------------------------------------------- */

/**
* Special character flags.
* @enum {CharacterFlagConfig}
Expand Down
1 change: 1 addition & 0 deletions module/documents/_module.mjs
Expand Up @@ -7,6 +7,7 @@ export {default as TokenDocument5e} from "./token.mjs";

// Helper Methods
export {default as Proficiency} from "./actor/proficiency.mjs";
export * as Trait from "./actor/trait.mjs";
export * as chat from "./chat-message.mjs";
export * as combat from "./combat.mjs";
export * as macro from "./macro.mjs";
34 changes: 0 additions & 34 deletions module/documents/actor/actor.mjs
Expand Up @@ -2556,40 +2556,6 @@ export default class Actor5e extends Actor {
return type;
}

/* -------------------------------------------- */

/**
* Populate a proficiency object with a `selected` field containing a combination of
* localizable group & individual proficiencies from `value` and the contents of `custom`.
*
* @param {object} data Object containing proficiency data.
* @param {string[]} data.value Array of standard proficiency keys.
* @param {string} data.custom Semicolon-separated string of custom proficiencies.
* @param {string} type "armor", "weapon", or "tool"
*/
static prepareProficiencies(data, type) {
const profs = CONFIG.DND5E[`${type}Proficiencies`];
const itemTypes = CONFIG.DND5E[`${type}Ids`];

let values = [];
if ( data.value ) values = data.value instanceof Array ? data.value : [data.value];

data.selected = {};
for ( const key of values ) {
if ( profs[key] ) {
data.selected[key] = profs[key];
} else if ( itemTypes && itemTypes[key] ) {
const item = ProficiencySelector.getBaseItem(itemTypes[key], { indexOnly: true });
if ( item ) data.selected[key] = item.name;
} else if ( type === "tool" && CONFIG.DND5E.vehicleTypes[key] ) {
data.selected[key] = CONFIG.DND5E.vehicleTypes[key];
}
}

// Add custom entries
if ( data.custom ) data.custom.split(";").forEach((c, i) => data.selected[`custom${i+1}`] = c.trim());
}

/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
Expand Down
254 changes: 254 additions & 0 deletions module/documents/actor/trait.mjs
@@ -0,0 +1,254 @@
/**
* Cached version of the base items compendia indices with the needed subtype fields.
* @type {object}
* @private
*/
const _cachedIndices = {};

/* -------------------------------------------- */
/* Trait Lists */
/* -------------------------------------------- */

/**
* Get the key path to the specified trait on an actor.
* @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
* @returns {string} Key path to this trait's object within an actor's system data.
*/
export function actorKeyPath(trait) {
const traitConfig = CONFIG.DND5E.traits[trait];
if ( traitConfig.actorKeyPath ) return traitConfig.actorKeyPath;
return `traits.${trait}`;
}

/* -------------------------------------------- */

/**
* Fetch the categories object for the specified trait.
* @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
* @returns {object} Trait categories defined within `CONFIG.DND5E`.
*/
export function categories(trait) {
const traitConfig = CONFIG.DND5E.traits[trait];
return CONFIG.DND5E[traitConfig.configKey ?? trait];
}

/* -------------------------------------------- */

/**
* Get a list of choices for a specific trait.
* @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
* @param {string[]} [chosen=[]] Optional list of keys to be marked as chosen.
* @returns {object} Object mapping proficiency ids to choice objects.
*/
export async function choices(trait, chosen=[]) {
const traitConfig = CONFIG.DND5E.traits[trait];

let data = Object.entries(categories(trait)).reduce((obj, [key, label]) => {
obj[key] = { label, chosen: chosen.includes(key) };
return obj;
}, {});

if ( traitConfig.children ) {
for ( const [categoryKey, childrenKey] of Object.entries(traitConfig.children) ) {
const children = CONFIG.DND5E[childrenKey];
if ( !children || !data[categoryKey] ) continue;
data[categoryKey].children = Object.entries(children).reduce((obj, [key, label]) => {
obj[key] = { label, chosen: chosen.includes(key) };
return obj;
}, {});
}
}

if ( traitConfig.subtypes ) {
const keyPath = `system.${traitConfig.subtypes.keyPath}`;
const map = CONFIG.DND5E[`${trait}ProficienciesMap`];

// Merge all IDs lists together
const ids = traitConfig.subtypes.ids.reduce((obj, key) => {
if ( CONFIG.DND5E[key] ) Object.assign(obj, CONFIG.DND5E[key]);
return obj;
}, {});

// Fetch base items for all IDs
const baseItems = await Promise.all(Object.entries(ids).map(async ([key, id]) => {
const index = await getBaseItem(id);
return [key, index];
}));

// Sort base items as children of categories based on subtypes
for ( const [key, index] of baseItems ) {
if ( !index ) continue;

// Get the proper subtype, using proficiency map if needed
let type = foundry.utils.getProperty(index, keyPath);
if ( map?.[type] ) type = map[type];

const entry = { label: index.name, chosen: chosen.includes(key) };

// No category for this type, add at top level
if ( !data[type] ) data[key] = entry;

// Add as child to appropriate category
else {
data[type].children ??= {};
data[type].children[key] = entry;
}
}
}

// Sort Categories
if ( traitConfig.sortCategories ) data = dnd5e.utils.sortObjectEntries(data, "label");

// Sort Children
for ( const category of Object.values(data) ) {
if ( !category.children ) continue;
category.children = dnd5e.utils.sortObjectEntries(category.children, "label");
}

return data;
}

/* -------------------------------------------- */

/**
* Fetch an item for the provided ID. If the provided ID contains a compendium pack name
* it will be fetched from that pack, otherwise it will be fetched from the compendium defined
* in `DND5E.sourcePacks.ITEMS`.
* @param {string} identifier Simple ID or compendium name and ID separated by a dot.
* @param {object} [options]
* @param {boolean} [options.indexOnly] If set to true, only the index data will be fetched (will never return
* Promise).
* @param {boolean} [options.fullItem] If set to true, the full item will be returned as long as `indexOnly` is
* false.
* @returns {Promise<Item5e>|object} Promise for a `Document` if `indexOnly` is false & `fullItem` is true,
* otherwise else a simple object containing the minimal index data.
*/
export function getBaseItem(identifier, { indexOnly=false, fullItem=false }={}) {
let pack = CONFIG.DND5E.sourcePacks.ITEMS;
let [scope, collection, id] = identifier.split(".");
if ( scope && collection ) pack = `${scope}.${collection}`;
if ( !id ) id = identifier;

const packObject = game.packs.get(pack);

// Full Item5e document required, always async.
if ( fullItem && !indexOnly ) return packObject?.getDocument(id);

const cache = _cachedIndices[pack];
const loading = cache instanceof Promise;

// Return extended index if cached, otherwise normal index, guaranteed to never be async.
if ( indexOnly ) {
const index = packObject?.index.get(id);
return loading ? index : cache?.[id] ?? index;
}

// Returned cached version of extended index if available.
if ( loading ) return cache.then(() => _cachedIndices[pack][id]);
else if ( cache ) return cache[id];
if ( !packObject ) return;

// Build the extended index and return a promise for the data
const promise = packObject.getIndex({ fields: traitIndexFields() }).then(index => {
const store = index.reduce((obj, entry) => {
obj[entry._id] = entry;
return obj;
}, {});
_cachedIndices[pack] = store;
return store[id];
});
_cachedIndices[pack] = promise;
return promise;
}

/* -------------------------------------------- */

/**
* List of fields on items that should be indexed for retrieving subtypes.
* @returns {string[]} Index list to pass to `Compendium#getIndex`.
* @protected
*/
export function traitIndexFields() {
const fields = [];
for ( const traitConfig of Object.values(CONFIG.DND5E.traits) ) {
if ( !traitConfig.subtypes ) continue;
fields.push(`system.${traitConfig.subtypes.keyPath}`);
}
return fields;
}

/* -------------------------------------------- */
/* Localized Formatting Methods */
/* -------------------------------------------- */

/**
* Get the localized label for a specific trait type.
* @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
* @param {number} [count] Count used to determine pluralization. If no count is provided, will default to
* the 'other' pluralization.
* @returns {string} Localized label.
*/
export function traitLabel(trait, count) {
let typeCap;
if ( trait.length === 2 ) typeCap = trait.toUpperCase();
else typeCap = trait.capitalize();

const pluralRule = ( count !== undefined ) ? new Intl.PluralRules(game.i18n.lang).select(count) : "other";
return game.i18n.localize(`DND5E.Trait${typeCap}Plural.${pluralRule}`);
}

/* -------------------------------------------- */

/**
* Retrieve the proper display label for the provided key.
* @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
* @param {string} key Key for which to generate the label.
* @returns {string} Retrieved label.
*/
export function keyLabel(trait, key) {
const traitConfig = CONFIG.DND5E.traits[trait];
if ( categories(trait)[key] ) {
const category = categories(trait)[key];
if ( !traitConfig.labelKey ) return category;
return foundry.utils.getProperty(category, traitConfig.labelKey);
}

for ( const childrenKey of Object.values(traitConfig.children ?? {}) ) {
if ( CONFIG.DND5E[childrenKey]?.[key] ) return CONFIG.DND5E[childrenKey]?.[key];
}

for ( const idsKey of traitConfig.subtypes?.ids ?? [] ) {
if ( !CONFIG.DND5E[idsKey]?.[key] ) continue;
const index = getBaseItem(CONFIG.DND5E[idsKey][key], { indexOnly: true });
if ( index ) return index.name;
else break;
}

return key;
}

/* -------------------------------------------- */

/**
* Create a human readable description of the provided choice.
* @param {string} trait Trait as defined in `CONFIG.DND5E.traits`.
* @param {TraitChoice} choice Data for a specific choice.
* @returns {string}
*/
export function choiceLabel(trait, choice) {
// Select from any trait values
if ( !choice.pool ) {
return game.i18n.format("DND5E.TraitConfigChooseAny", {
count: choice.count,
type: traitLabel(trait, choice.count).toLowerCase()
});
}

// Select from a list of options
const choices = choice.pool.map(key => keyLabel(trait, key));
const listFormatter = new Intl.ListFormat(game.i18n.lang, { type: "disjunction" });
return game.i18n.format("DND5E.TraitConfigChooseList", {
count: choice.count,
list: listFormatter.format(choices)
});
}
1 change: 1 addition & 0 deletions module/utils.mjs
Expand Up @@ -108,6 +108,7 @@ export async function preloadHandlebarsTemplates() {
const partials = [
// Shared Partials
"systems/dnd5e/templates/actors/parts/active-effects.hbs",
"systems/dnd5e/templates/apps/parts/trait-list.hbs",

// Actor Sheet Partials
"systems/dnd5e/templates/actors/parts/actor-traits.hbs",
Expand Down
16 changes: 8 additions & 8 deletions templates/actors/parts/actor-traits.hbs
Expand Up @@ -25,7 +25,7 @@

<div class="form-group {{system.traits.languages.cssClass}}">
<label>{{localize "DND5E.Languages"}}</label>
<a class="trait-selector" data-options="languages" data-target="system.traits.languages"
<a class="trait-selector" data-trait="languages"
title="{{localize 'DND5E.TraitConfig' trait=(localize 'DND5E.Languages')}}" tabindex="0">
<i class="fas fa-edit"></i>
</a>
Expand All @@ -39,7 +39,7 @@

<div class="form-group {{system.traits.di.cssClass}}">
<label>{{localize "DND5E.DamImm"}}</label>
<a class="trait-selector" data-options="damageTypes" data-target="system.traits.di"
<a class="trait-selector" data-trait="di"
title="{{localize 'DND5E.TraitConfig' trait=(localize 'DND5E.DamImm')}}" tabindex="0">
<i class="fas fa-edit"></i>
</a>
Expand All @@ -52,7 +52,7 @@

<div class="form-group {{system.traits.dr.cssClass}}">
<label>{{localize "DND5E.DamRes"}}</label>
<a class="trait-selector" data-options="damageTypes" data-target="system.traits.dr"
<a class="trait-selector" data-trait="dr"
title="{{localize 'DND5E.TraitConfig' trait=(localize 'DND5E.DamRes')}}" tabindex="0">
<i class="fas fa-edit"></i>
</a>
Expand All @@ -65,7 +65,7 @@

<div class="form-group {{system.traits.dv.cssClass}}">
<label>{{localize "DND5E.DamVuln"}}</label>
<a class="trait-selector" data-options="damageTypes" data-target="system.traits.dv"
<a class="trait-selector" data-trait="dv"
title="{{localize 'DND5E.TraitConfig' trait=(localize 'DND5E.DamVuln')}}">
<i class="fas fa-edit"></i>
</a>
Expand All @@ -78,7 +78,7 @@

<div class="form-group {{system.traits.ci.cssClass}}">
<label>{{localize "DND5E.ConImm"}}</label>
<a class="trait-selector" data-options="conditionTypes" data-target="system.traits.ci"
<a class="trait-selector" data-trait="ci"
title="{{localize 'DND5E.TraitConfig' trait=(localize 'DND5E.ConImm')}}">
<i class="fas fa-edit"></i>
</a>
Expand All @@ -92,7 +92,7 @@
{{#if isCharacter}}
<div class="form-group {{system.traits.weaponProf.cssClass}}">
<label>{{localize "DND5E.TraitWeaponProf"}}</label>
<a class="proficiency-selector" data-type="weapon" data-target="system.traits.weaponProf"
<a class="trait-selector" data-trait="weapon"
title="{{localize 'DND5E.TraitConfig' trait=(localize 'DND5E.TraitWeaponProf')}}">
<i class="fas fa-edit"></i>
</a>
Expand All @@ -105,7 +105,7 @@

<div class="form-group {{system.traits.armorProf.cssClass}}">
<label>{{localize "DND5E.TraitArmorProf"}}</label>
<a class="proficiency-selector" data-type="armor" data-target="system.traits.armorProf"
<a class="trait-selector" data-trait="armor"
title="{{localize 'DND5E.TraitConfig' trait=(localize 'DND5E.TraitArmorProf')}}">
<i class="fas fa-edit"></i>
</a>
Expand All @@ -118,7 +118,7 @@

<div class="form-group {{system.traits.toolProf.cssClass}}">
<label>{{localize "DND5E.TraitToolProf"}}</label>
<a class="proficiency-selector" data-type="tool" data-target="system.traits.toolProf"
<a class="trait-selector" data-trait="tool"
title="{{localize 'DND5E.TraitConfig' trait=(localize 'DND5E.TraitToolProf')}}">
<i class="fas fa-edit"></i>
</a>
Expand Down
2 changes: 1 addition & 1 deletion templates/advancement/advancement-config.hbs
@@ -1,3 +1,3 @@
<form autocomplete="off">
{{> "dnd5e.advancement-controls.hbs"}}
{{> "dnd5e.advancement-controls"}}
</form>
13 changes: 13 additions & 0 deletions templates/apps/parts/trait-list.hbs
@@ -0,0 +1,13 @@
<ol class="trait-list">
{{#each choices as |choice key|}}
<li>
<label class="checkbox">
<input type="checkbox" name="{{#if ../prefix}}{{../prefix}}.{{/if}}{{key}}" {{checked choice.chosen}}>
{{choice.label}}
</label>
{{#if choice.children}}
{{> "dnd5e.trait-list" choices=choice.children prefix=../prefix}}
{{/if}}
</li>
{{/each}}
</ol>
33 changes: 12 additions & 21 deletions templates/apps/trait-selector.hbs
@@ -1,27 +1,18 @@
<form autocomplete="off" onsubmit="event.preventDefault();">
{{> "dnd5e.trait-list" prefix="choices"}}

{{#*inline "traitList"}}
<ol class="trait-list">
{{#each choices as |choice key|}}
<li>
<label class="checkbox">
<input type="checkbox" name="{{key}}" data-dtype="Boolean" {{checked choice.chosen}}>
{{choice.label}}
</label>
{{#if choice.children}}
{{> traitList choices=choice.children}}
{{/if}}
</li>
{{/each}}
</ol>
{{/inline}}
{{#if customPath}}
<div class="form-group stacked">
<label>{{localize "DND5E.TraitSelectorSpecial"}}</label>
<input type="text" name="{{customPath}}" value="{{custom}}">
</div>
{{/if}}

{{> traitList}}
{{#if allowCustom}}
<div class="form-group stacked">
<label>{{ localize "DND5E.TraitSelectorSpecial" }}</label>
<input type="text" name="custom" value="{{custom}}" data-dtype="String"/>
</div>
{{#if bypassesPath}}
<h3>{{localize "DND5E.DamagePhysicalBypass"}}</h3>
<p class="note">{{localize "DND5E.DamagePhysicalBypassHint"}}</p>
{{> "dnd5e.trait-list" choices=bypasses prefix="bypasses"}}
{{/if}}

<button type="submit"><i class="far fa-save"></i> {{localize "DND5E.TraitSave"}}</button>
</form>